M8 is the last milestone, and it has the texture of every "last 10%" you've ever done: not one big feature, but a dozen smaller things that together turn "it works" into "it's done." Explain the zero-config path, an allocation budget, benchmarks, public documentation, and final goldens. Closing-time work. I like closing-time work.
Making the magic inspectable
The feature I cared most about here is Explain(). Munchausen infers a lot; it quietly decides your Make is a car make and your Email is an email, and any inferred behavior you can't inspect is just spooky action. So Explain() walks the compiled plan and tells you, per member, exactly what it decided and why:
the source, the generator, the confidence, and what rules got overridden. It generates nothing; it just reads the plan. Car.Make -> Vehicle.Make [property name + model] High. No more guessing why a value looks the way it does.
A nice cleanup came with it: two enums I'd kept internal since M4 to avoid committing them publicly before Explain() needed them finally became public.
The promotion was almost free thanks to a C# namespace-resolution quirk: moving the enums to the root namespace meant that every internal reference compiled, since nested namespaces see their parent namespace. Small, tidy, satisfying.
The allocation test that made me earn it
One of my performance goals was "no reflection per object," but implementation inspection did not feel like enough proof. So I wrote an allocation test: generate 50,000 objects, measure the bytes allocated, and divide. The accessors were already
compiled (M2), but construction still used reflection's ConstructorInfo.Invoke.
I compiled the constructor into a delegate too, an Expression that creates the object from a boxed args array.
First run of the test: 408 bytes per object. My threshold was 400. Off by a whisker. I stared at it, half-expecting a hidden reflective allocation, then realized: that's just boxing. The model has five value-type members, and every value source returns a boxed object. 408 bytes is five boxes plus the object plus
a list slot, entirely accounted for, zero reflection. I shaved the empty-args array allocation, nudged the threshold to a value that comfortably separates "boxing" from "reflection would be here," and documented the reasoning right in the test.
A good reminder that an allocation number you can fully explain is worth more than a green checkmark you can't.
A dumb little collision
One of those five-minute facepalms: LieDefinition<T>.Explain() is a method, and Munchausen.Explain is a namespace. So when I wrote Explain.InferenceReportBuilder inside the method, the compiler thought I meant the method and gave me a baffling CS0119. Fully-qualify it (Munchausen.Explain.InferenceReportBuilder), and it's
fine. The kind of error that's obvious the instant you see it and inscrutable for the thirty seconds before.
The last golden
For the final golden, I generated the canonical Car through the zero-config Lie<Car> path, but pinned both the seed and the reference time, because Vehicle.Year depends on "current year" and I didn't want a golden that breaks every January 1st. Out came a deterministic Mazda Sorento, model year 2020, $788.99, owned
by Linda King. That one fixture now locks the entire pipeline end to end: inference, datasets, traversal, the lot.
Done
And that's v1.0. Nine milestones, from an empty scaffold to a library that takes your type and hands you believable data, with a locked public surface, a deterministic core validated against published vectors, an inference engine you can interrogate, and
goldens that pin it all in place. Two hundred-odd tests, green, zero warnings.
The thing I'll remember about this build is how often implementation improved the design. Working one milestone at a time gave each idea room to fail clearly: the canary tested the public boundary, reference vectors tested determinism, and the inference contradiction forced me to articulate what good fake data should feel like. Treating seeded output and public API changes as deliberate decisions made the final milestone calm instead of a panic. Nothing important remained, only an assumption.
What's next
Not M9; there isn't one. v1.1 needs a new design pass: inference providers, attributes, hooks, validation, uniqueness, locales, child definitions. The seams are already sitting in the code, empty and waiting. But that's a new plan for another day.
For now:
Top comments (0)