Dev Log: The milestone where Build() stops lying A developer revived Munchausen, a C# NuGet package, after 9 years. In milestone M5, the Build() method was implemented to compile a frozen plan from captured builder data and inference results, collecting all errors (LIE001-LIE005) before throwing. The implementation handles recursive types like Employee with a visited set and worklist, and emits placeholder delegates for generators that will be bound in M7. I’m reviving Munchausen, a C NuGet package I started 9 years ago. This is part 6of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project. For three milestones, Build has been a method that throws NotImplementedException . M5 is where it finally earns its name. This is the compiler: take everything the builder captured, plus everything inference decided, and assemble it into one frozen plan, or fail with a tidy list of every reason it couldn't. While designing Build , I kept returning to one question: what would make a failed definition easy to fix? My answer was to avoid bare exceptions. Every failure gets a stable code, LIE001 through LIE005 for the v1.0 cases, and Build collects all of them before throwing. If your definition has a bad expression, a rule conflict, and an empty name, you get one exception listing all three instead of discovering them one at a time. I built each diagnostic with a deliberately triggerable test: LIE001 : With c = c.Owner.FirstName, ... nested path, illegal target LIE002 : With ... and Ignore ... on the same member contradiction LIE003 : a required member of an interface type can't be resolved LIE004 : two equally-resolvable constructors, no tiebreaker LIE005 : WithName " " empty/whitespace There's something satisfying about writing tests whose job is to provoke failure and then checking that the failure is well labeled. Good error messages are a feature. Keeping the code registry and expected diagnostic table in sync through a conformance test makes that feature difficult to erode accidentally. The scary part, on paper, was the recursive-type graph. Employee has a Manager that's an Employee ; compile that naively, and you recurse forever. My original architecture used a two-phase "allocate empty shells, then fill them" dance. While implementing it, I found a simpler route: keep a visited set of types and a worklist, and have nested members reference their child by type rather than a direct plan pointer. The runtime looks the plan up in a dictionary later. Compiling Employee now completes in a single pass and records a friendly LIE009 Info note. The implementation improved the architecture. Here's the uncomfortable bit. The compiler wants to bind a generator to each inferred member, but the actual generators the thing that produces "Anthony" for a FirstName are datasets, and datasets are two milestones away in M7. Classic ordering tension. The pragmatic answer was to emit a placeholder delegate that throws "bound in M7." Since generation itself is still stubbed, those placeholders are never called, while everything I can verify, the plan structure, constructor choices, diagnostics, and recursive termination, is real. It nagged at me to add deliberate throw s, but this let me preserve the milestone boundary without pretending the leaves were finished. I left the IOUs explicit for future-me in M7. This is also the milestone when my beloved dotnet format trick for generating the public API file suddenly stopped working; it complained about a "duplicate source file" and refused to populate anything. I hand-wrote the new exception-type entries instead and moved on, but it bugged me. Spoiler: I finally diagnosed it in M7, and the fix was embarrassingly simple. Foreshadowing. Lie.Define