<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ernesto Herrera Salinas</title>
    <description>The latest articles on DEV Community by Ernesto Herrera Salinas (@ernestohs).</description>
    <link>https://dev.to/ernestohs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F784815%2Fd5daef24-3e99-433f-819f-5b37dfac6558.png</url>
      <title>DEV Community: Ernesto Herrera Salinas</title>
      <link>https://dev.to/ernestohs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ernestohs"/>
    <language>en</language>
    <item>
      <title>Dev Log: The big one: where the fake data starts looking real</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:14:24 +0000</pubDate>
      <link>https://dev.to/ernestohs/dev-log-the-big-one-where-the-fake-data-starts-looking-real-ch0</link>
      <guid>https://dev.to/ernestohs/dev-log-the-big-one-where-the-fake-data-starts-looking-real-ch0</guid>
      <description>&lt;p&gt;M7 is the milestone where Munchausen stops being a clever skeleton and starts producing data you'd actually believe. Eight dataset classes, a pile of English word tables, and the wiring that turns every "this should be a first name" decision from earlier into an actual "Anthony." It's the largest milestone by a wide margin, and it's the one where all those &lt;code&gt;throw new NotImplementedException("bound in M7")&lt;/code&gt; IOUs from M5 and M6 finally come due.&lt;/p&gt;

&lt;p&gt;There's a particular satisfaction in deleting placeholder code by replacing it with the real thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Curating fake reality
&lt;/h2&gt;

&lt;p&gt;A lot of this milestone is taste expressed as data: which first names, cities, and product adjectives to include. While designing the datasets, I decided their public methods should remain stable while the underlying tables could grow. The constraints I care most about are the safety ones: emails use RFC 2606 reserved domains, IPs come from TEST-NET documentation ranges, and phones stay in the&lt;br&gt;
fictional 555-01xx band. A mock-data library should never accidentally emit a real person's plausible-looking contact information.&lt;/p&gt;

&lt;p&gt;The VIN generator was the most fun to get right. A real VIN has a check digit at position 9 computed from a weighted transliteration of the other 16 characters, mod 11. I implemented it, then wrote the test to &lt;em&gt;independently&lt;/em&gt; recompute the check digit a different way and assert they agree. When two different implementations of a fiddly algorithm agree across 200 random VINs, you actually believe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sleight of hand I'm quietly proud of
&lt;/h2&gt;

&lt;p&gt;There was a real risk here. Back in M6, the type generators read from &lt;code&gt;context.Random&lt;/code&gt;, which was an internal handle on the PRNG. In M7, &lt;code&gt;Random&lt;/code&gt; has to become the &lt;em&gt;public&lt;/em&gt; &lt;code&gt;RandomData&lt;/code&gt; façade. Change that, and you risk shifting the byte stream and breaking the M6 golden, exactly the kind of seeded-output break the project treats as a serious event.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;RandomData.Int&lt;/code&gt; is a one-line forward to &lt;code&gt;DeterministicRandom.Int&lt;/code&gt;. Same underlying instance, same draws, same bytes. So I swapped the type from internal to public, held my breath, ran the M6 golden… and it passed, untouched. The façade is&lt;br&gt;
genuinely transparent. Threading a new public API over existing behavior without disturbing a single committed byte is the kind of small, invisible win that makes the careful-determinism discipline feel worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finally solving the tooling mystery
&lt;/h2&gt;

&lt;p&gt;And then there's the &lt;code&gt;dotnet format&lt;/code&gt; saga. Since M5 it had refused to auto-generate my public-API entries, warning about a "duplicate source file." I'd been hand-writing entries to get around it. This milestone added &lt;em&gt;75&lt;/em&gt; new public members, enough to make the workaround obviously unreasonable, so I finally sat down and read the warning properly.&lt;/p&gt;

&lt;p&gt;The culprit was sitting in my own csproj since M0: I'd explicitly added &lt;code&gt;&amp;lt;AdditionalFiles Include="PublicAPI.*.txt" /&amp;gt;&lt;/code&gt;, but the analyzer package already includes those files itself. Double-registered. Delete my redundant line, and &lt;code&gt;dotnet format&lt;/code&gt; instantly worked, spitting out all 75 entries perfectly formatted.&lt;br&gt;
A self-inflicted problem I'd been routing around for two milestones, fixed in thirty seconds once I investigated the message. The lesson was not about &lt;code&gt;dotnet format&lt;/code&gt;; it was about how quickly a workaround can become invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it leaves things
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Lie.Define&amp;lt;Car&amp;gt;().Build().Generate()&lt;/code&gt; now returns a Mazda Sorento, a believable price, and an owner with a real-looking name and an &lt;code&gt;@example.net&lt;/code&gt; email. The placeholders are gone. Every semantic name resolves to a real generator, every dataset draws from the one deterministic stream, and same-seed runs are identical.&lt;br&gt;
The library finally does, end-to-end, the thing it set out to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;M8, the finale. &lt;code&gt;Explain()&lt;/code&gt;, so the inference isn't a black box; the cached zero-config &lt;code&gt;Lie&amp;lt;T&amp;gt;&lt;/code&gt; automatic path; reflection-free construction, so generating a million objects allocates nothing but the objects themselves; a benchmark suite; and the final goldens.&lt;/p&gt;

&lt;p&gt;Then the release-hardening checklist is done, and v1.0 is done.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
    </item>
    <item>
      <title>Engineering Post: Executing the plan: lifecycle, cancellation, cycles, wrapped-once</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:19:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-executing-the-plan-lifecycle-cancellation-cycles-wrapped-once-mmp</link>
      <guid>https://dev.to/ernestohs/engineering-post-executing-the-plan-lifecycle-cancellation-cycles-wrapped-once-mmp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 7&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Engineering Post: the reasoning, trade-offs, API decisions, and technical choices behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M6 makes &lt;code&gt;Generate()&lt;/code&gt; real. A &lt;code&gt;GenerationOperation&lt;/code&gt; exists per call; it owns the PRNG and reference time (resolved once), traverses the frozen plan, and produces objects. This milestone tests whether the lifecycle I designed is precise enough to execute: ordering, cancellation checkpoints, exception wrapping, and depth/cycle handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  One operation, resolved once
&lt;/h2&gt;

&lt;p&gt;The operation constructor settles the things that must not drift mid-call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seed:&lt;/strong&gt; &lt;code&gt;options.Seed ?? defaults.Seed ?? entropy&lt;/code&gt;. The entropy path is the
&lt;em&gt;single&lt;/em&gt; place in &lt;code&gt;src/&lt;/code&gt; allowed to touch &lt;code&gt;System.Random&lt;/code&gt;, one draw from
&lt;code&gt;Random.Shared&lt;/code&gt; to seed an unseeded run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference time:&lt;/strong&gt; &lt;code&gt;options.ReferenceTime ?? defaults.ReferenceTime ??
options.TimeProvider?.GetUtcNow() ?? DateTimeOffset.UtcNow&lt;/code&gt;, captured once. No
method ever reads the clock again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The lifecycle, in order
&lt;/h2&gt;

&lt;p&gt;Per object: construct → populate members in &lt;code&gt;MetadataToken&lt;/code&gt; order → run derivations in registration order. That ordering is part of reproducibility, so the traversal is rigid by design. Each phase boundary is a cancellation checkpoint: before each root, nested object, collection element, and derivation pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdukvtj6al1ixqms5qlfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdukvtj6al1ixqms5qlfy.png" alt=" " width="800" height="737"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapped exactly once
&lt;/h2&gt;

&lt;p&gt;Every user-delegate site, a &lt;code&gt;ConstructWith&lt;/code&gt;, a &lt;code&gt;With&lt;/code&gt; generator, a &lt;code&gt;Derive&lt;/code&gt;, is wrapped in exactly one try/catch that throws &lt;code&gt;LieGenerationException&lt;/code&gt; carrying the model type, member path, index, and the &lt;code&gt;GenerationPhase&lt;/code&gt; of the site, with the original exception preserved as &lt;code&gt;InnerException&lt;/code&gt;. &lt;code&gt;OperationCanceledException&lt;/code&gt;&lt;br&gt;
passes through untouched (cancellation is never a "generation failure"). To keep&lt;br&gt;
the real user exception as the inner, not a &lt;code&gt;TargetInvocationException&lt;/code&gt;, typed&lt;br&gt;
user delegates are adapted to the uniform shape with a compiled &lt;code&gt;Expression.Invoke&lt;/code&gt;&lt;br&gt;
rather than &lt;code&gt;DynamicInvoke&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The exception hierarchy itself got corrected here: &lt;code&gt;LieDefinitionException&lt;/code&gt; and&lt;br&gt;
the new &lt;code&gt;LieGenerationException&lt;/code&gt; both derive from a public abstract&lt;br&gt;
&lt;code&gt;LieException&lt;/code&gt;. I had designed that common base but missed it in M5; implementing&lt;br&gt;
the runtime exposed the omission.&lt;/p&gt;
&lt;h2&gt;
  
  
  Path-based cycles, frame-based depth
&lt;/h2&gt;

&lt;p&gt;Cycle detection is &lt;strong&gt;path-based, not type-based&lt;/strong&gt;: a candidate nested type is a&lt;br&gt;
cycle only if it already appears in the active ancestor chain. So two &lt;code&gt;Address&lt;/code&gt;&lt;br&gt;
siblings (billing and shipping) both generate; they're on different branches, while &lt;code&gt;Employee.Manager&lt;/code&gt; terminates, because &lt;code&gt;Employee&lt;/code&gt; is its own ancestor. Depth&lt;br&gt;
is the object-frame count (the default max is 3); collections don't push a frame,&lt;br&gt;
but their element objects do. On depth or cycle, &lt;code&gt;Terminate&lt;/code&gt; nulls the reference /&lt;br&gt;
empties the collection, and &lt;code&gt;Throw&lt;/code&gt; raises a &lt;code&gt;LieGenerationException&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reflection-free collection materialization
&lt;/h2&gt;

&lt;p&gt;Collections materialize through delegates compiled at &lt;em&gt;build&lt;/em&gt; time, so generation&lt;br&gt;
stays reflection-free. A small generic helper is bound to the element type once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// bound via MakeGenericMethod(elementType).CreateDelegate at compile time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrays and the six sequence interfaces materialize fully; dictionaries land as the&lt;br&gt;
right empty type for now (their keys are usually strings, which arrive with the&lt;br&gt;
datasets).&lt;/p&gt;

&lt;h2&gt;
  
  
  The deferral question, answered
&lt;/h2&gt;

&lt;p&gt;M6 hit the ordering tension in my plan head-on: the real scalar generators arrive&lt;br&gt;
in M7, so what can the runtime honestly generate today? I chose to bind only the&lt;br&gt;
&lt;strong&gt;pure-PRNG type defaults&lt;/strong&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;Guid&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, &lt;code&gt;decimal&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;byte[]&lt;/code&gt;, and friends, and defer strings, dates, semantic generators, and rich&lt;br&gt;
test-model goldens to M7. Every runtime &lt;em&gt;mechanic&lt;/em&gt; is testable now with primitive&lt;br&gt;
fixtures and user delegates; only dataset content waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M7, the Datasets
&lt;/h2&gt;

&lt;p&gt;M7 binds the leaves. Eight public datasets, the English data tables, the internal&lt;br&gt;
generators (phone, guid-string, short-code), and the wiring that connects every&lt;br&gt;
semantic catalog name to a real implementation, so &lt;code&gt;FirstName&lt;/code&gt; finally produces&lt;br&gt;
"Anthony" and &lt;code&gt;Email&lt;/code&gt; produce something &lt;code&gt;@example.com&lt;/code&gt;. After M7, the placeholders from M5 are gone, and the canonical &lt;code&gt;Car&lt;/code&gt; generates a full, believable graph.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Dev Log: It generates! (Mostly. The strings are IOUs.)</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sun, 21 Jun 2026 20:50:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/dev-log-it-generates-mostly-the-strings-are-ious-23nj</link>
      <guid>https://dev.to/ernestohs/dev-log-it-generates-mostly-the-strings-are-ious-23nj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 7&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M6 is the milestone where Munchausen finally &lt;em&gt;does the thing&lt;/em&gt;. &lt;code&gt;Generate()&lt;/code&gt; runs.&lt;br&gt;
There's a traversal that constructs an object, fills its members in order, runs&lt;br&gt;
derivations, handles cancellation, wraps errors, and refuses to recurse forever.&lt;br&gt;
After five milestones of plumbing, watching a definition actually produce an object&lt;br&gt;
is a genuine hit of dopamine.&lt;/p&gt;

&lt;p&gt;But first I had to answer a question I'd been dodging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chicken-and-egg
&lt;/h2&gt;

&lt;p&gt;The runtime's whole job is to execute the plan, which means running generators. But&lt;br&gt;
the &lt;em&gt;real&lt;/em&gt; generators, the things that turn a &lt;code&gt;FirstName&lt;/code&gt; into "Anthony", are&lt;br&gt;
datasets, and datasets are the &lt;em&gt;next&lt;/em&gt; milestone. The string type-default literally&lt;br&gt;
is "two lorem words," and lorem lives in M7. So if I run the runtime against the&lt;br&gt;
canonical &lt;code&gt;Car&lt;/code&gt;, the moment it hits &lt;code&gt;Make&lt;/code&gt; (a string), it throws "bound in M7."&lt;/p&gt;

&lt;p&gt;This exposed a genuine fork in my milestone plan. I could build the runtime now&lt;br&gt;
and defer dataset-backed generation to M7, or drag datasets forward and blur the&lt;br&gt;
boundary. I chose the clean split: bind only the &lt;em&gt;pure-PRNG&lt;/em&gt; type defaults in M6,&lt;br&gt;
ints, bools, guids, doubles, anything made from the random stream alone, and let&lt;br&gt;
strings, dates, and semantic generators arrive with the datasets they need.&lt;/p&gt;

&lt;p&gt;The realization that made this comfortable was that almost every runtime&lt;br&gt;
&lt;em&gt;behavior&lt;/em&gt;, lifecycle order, cancellation, exception wrapping, depth limits,&lt;br&gt;
cycle detection, and reproducibility, can be tested with primitive-only fixtures&lt;br&gt;
and user delegates. M6 could be rigorous without pretending to be complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprise while capturing a golden
&lt;/h2&gt;

&lt;p&gt;Here's my favorite moment of the milestone. I needed a golden for the runtime, so&lt;br&gt;
I built a tiny model with neutral field names, &lt;code&gt;Alpha&lt;/code&gt;, &lt;code&gt;Beta&lt;/code&gt;, &lt;code&gt;Gamma&lt;/code&gt;, &lt;code&gt;Delta&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Epsilon&lt;/code&gt;, all primitive types, specifically to avoid the unbound semantic&lt;br&gt;
generators. Ran it. &lt;strong&gt;Boom, "bound in M7."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait, what? They're all primitives. Which one tripped a semantic generator?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Epsilon&lt;/code&gt;. Because &lt;code&gt;epsilon&lt;/code&gt; &lt;em&gt;ends with&lt;/em&gt; &lt;code&gt;lon&lt;/code&gt;, and &lt;code&gt;lon&lt;/code&gt; is a catalog alias for&lt;br&gt;
&lt;strong&gt;longitude&lt;/strong&gt;, which is a &lt;code&gt;double&lt;/code&gt;, and &lt;code&gt;Epsilon&lt;/code&gt; is a &lt;code&gt;double&lt;/code&gt;. The suffix matcher&lt;br&gt;
did exactly what it was built to do and matched &lt;code&gt;Epsi-LON&lt;/code&gt; to longitude. A&lt;br&gt;
perfectly correct false positive, and a perfect little demonstration of &lt;em&gt;why&lt;/em&gt; the&lt;br&gt;
catalog uses confidence levels and modes in the first place. I renamed it &lt;code&gt;Score&lt;/code&gt;&lt;br&gt;
and moved on, grinning. The inference engine caught my own test off guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting construction right (and fast)
&lt;/h2&gt;

&lt;p&gt;One detail I cared about: no reflection during generation. The accessors were already compiled back in M2. Construction was the last reflective holdout, and I'll fully fix that in M8, where the allocation test forces the issue, but M6 lays the groundwork: collection materializers are compiled to delegates at build time so the per-element loop never reflects.&lt;/p&gt;

&lt;p&gt;And the exception story took real care. When your &lt;code&gt;With&lt;/code&gt; delegate throws, you should see &lt;em&gt;your&lt;/em&gt; exception as the &lt;code&gt;InnerException&lt;/code&gt;, not some &lt;code&gt;TargetInvocationException&lt;/code&gt; wrapper from reflection's &lt;code&gt;DynamicInvoke&lt;/code&gt;. So user delegates are adapted with compiled &lt;code&gt;Expression.Invoke&lt;/code&gt;, which throws cleanly. Wrap exactly once, tag it with the right phase (Construction / MemberPopulation /&lt;br&gt;
Derivation), let cancellation pass straight through. Details, but they're the difference between an exception you can debug and one you curse at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it leaves things
&lt;/h2&gt;

&lt;p&gt;You can now generate primitive-and-nested graphs, in batches, reproducibly, with proper cancellation and depth/cycle safety. The lifecycle is exactly ordered, the goldens reproduce across runs, and a self-referential &lt;code&gt;Node&lt;/code&gt; terminates cleanly at depth 1 instead of blowing the stack. The canonical &lt;code&gt;Car&lt;/code&gt; still can't fully generate, its strings are IOUs, but every &lt;em&gt;mechanism&lt;/em&gt; is real and tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;M7: the datasets. The biggest milestone by far. Eight public dataset classes, the English data tables, VIN check digits, RFC-safe email domains, and the wiring that finally connects every semantic name to a real generator. It will test whether the abstractions from M4 through M6 really accept their missing leaves as cleanly as I hoped.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Engineering Post: Cashing the IOUs: eight datasets and a check-digit</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sun, 21 Jun 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-cashing-the-ious-eight-datasets-and-a-check-digit-2ihn</link>
      <guid>https://dev.to/ernestohs/engineering-post-cashing-the-ious-eight-datasets-and-a-check-digit-2ihn</guid>
      <description>&lt;p&gt;M7 is the biggest milestone. It binds every leaf the previous two milestones left&lt;br&gt;
as placeholders. Eight public dataset classes, the English data tables, the&lt;br&gt;
internal-only generators, &lt;code&gt;Dataset&amp;lt;T&amp;gt;()&lt;/code&gt; resolution, and the wiring that connects&lt;br&gt;
every semantic catalog name to a real implementation. This is where I find out&lt;br&gt;
whether the abstractions from M4 through M6 can accept their missing leaves&lt;br&gt;
without being redesigned.&lt;/p&gt;
&lt;h2&gt;
  
  
  The datasets
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RandomData&lt;/code&gt; (the primitive façade over &lt;code&gt;DeterministicRandom&lt;/code&gt;), plus &lt;code&gt;NameData&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;InternetData&lt;/code&gt;, &lt;code&gt;AddressData&lt;/code&gt;, &lt;code&gt;DateData&lt;/code&gt;, &lt;code&gt;LoremData&lt;/code&gt;, &lt;code&gt;VehicleData&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;CommerceData&lt;/code&gt;. I designed their public surfaces before implementation, then used&lt;br&gt;
the &lt;code&gt;PublicApiAnalyzer&lt;/code&gt; to check that the code introduced nothing extra.&lt;br&gt;
Constructors are internal; callers reach datasets through the context, never&lt;br&gt;
&lt;code&gt;new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most important dataset choice is that realism must remain fictionally safe:&lt;br&gt;
email domains come only from the RFC 2606 reserved set&lt;br&gt;
(&lt;code&gt;example.com/org/net&lt;/code&gt;), IPs from TEST-NET documentation blocks, phone numbers&lt;br&gt;
from the 555-01xx range, and VINs are structurally valid but not&lt;br&gt;
manufacturer-registered. That last one is the fun bit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 17 chars, no I/O/Q, with a real check digit at position 9&lt;/span&gt;
&lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ComputeCheckDigit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// weighted transliteration mod 11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract test recomputes the check digit with an independent implementation and&lt;br&gt;
asserts it matches. Because the two use different code paths, a &lt;code&gt;switch&lt;/code&gt; and a&lt;br&gt;
lookup string, this is a genuine cross-check rather than a tautology.&lt;/p&gt;
&lt;h2&gt;
  
  
  Binding the catalog
&lt;/h2&gt;

&lt;p&gt;In M4 the semantic catalog stored generator &lt;em&gt;names&lt;/em&gt; (&lt;code&gt;"Name.First"&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;"Internet.Email"&lt;/code&gt;). M7 maps each name to a real delegate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Name.First"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Internet.Email"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Internet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Vehicle.Vin"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VehicleData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Vin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"internal phone"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InternalGenerators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dates need a twist: the catalog's date generators yield &lt;code&gt;DateTimeOffset&lt;/code&gt;, but the&lt;br&gt;
member might be a &lt;code&gt;DateTime&lt;/code&gt; or &lt;code&gt;DateOnly&lt;/code&gt;. So the compiler adapts the generator to&lt;br&gt;
the member's declared type. The type-default generators extended too, &lt;code&gt;string&lt;/code&gt;&lt;br&gt;
becomes &lt;code&gt;Lorem.Words(2)&lt;/code&gt;, &lt;code&gt;Uri&lt;/code&gt; becomes an &lt;code&gt;Internet.Url&lt;/code&gt;, and the date/time family&lt;br&gt;
binds to &lt;code&gt;DateData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3mmwb1j52zw8wgafm57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3mmwb1j52zw8wgafm57.png" alt=" " width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A determinism-preserving sleight of hand
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GenerationContext.Random&lt;/code&gt; became the public &lt;code&gt;RandomData&lt;/code&gt; façade, but it wraps the&lt;br&gt;
&lt;em&gt;same&lt;/em&gt; &lt;code&gt;DeterministicRandom&lt;/code&gt; the type generators were already using. Since&lt;br&gt;
&lt;code&gt;RandomData.Int&lt;/code&gt; just forwards to &lt;code&gt;DeterministicRandom.Int&lt;/code&gt;, the byte stream is&lt;br&gt;
byte-for-byte identical to M6. The M6 golden still passes untouched. Datasets are&lt;br&gt;
instantiated once per operation and cached, all drawing from that single stream, so&lt;br&gt;
"every dataset method draws only from the operation stream" is structurally true, and a determinism test proves two same-seed operations produce identical output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tooling mystery, solved
&lt;/h2&gt;

&lt;p&gt;Remember M5's "duplicate source file" error that broke &lt;code&gt;dotnet format&lt;/code&gt;? Here's the&lt;br&gt;
diagnosis: the analyzer package &lt;em&gt;already&lt;/em&gt; auto-includes &lt;code&gt;PublicAPI.*.txt&lt;/code&gt;, and my&lt;br&gt;
M0 csproj &lt;em&gt;also&lt;/em&gt; added them explicitly via &lt;code&gt;&amp;lt;AdditionalFiles&amp;gt;&lt;/code&gt;. Double-registered →&lt;br&gt;
&lt;code&gt;dotnet format&lt;/code&gt; choked. Deleting that one ItemGroup fixed it, and &lt;code&gt;dotnet format&lt;/code&gt;&lt;br&gt;
happily generated all &lt;strong&gt;75&lt;/strong&gt; new dataset surface entries, correctly, down to&lt;br&gt;
&lt;code&gt;int.MinValue&lt;/code&gt; → &lt;code&gt;-2147483648&lt;/code&gt;. A whole class of future pain, gone, because I&lt;br&gt;
finally read the warning properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M8, the finale
&lt;/h2&gt;

&lt;p&gt;The last milestone tests the complete design: &lt;code&gt;Explain()&lt;/code&gt; makes inference&lt;br&gt;
inspectable, the cached &lt;code&gt;Lie&amp;lt;T&amp;gt;&lt;/code&gt; path removes configuration, reflection-free&lt;br&gt;
construction faces an allocation test, and final goldens cover the complete&lt;br&gt;
pipeline. After M8, I should know whether v1.0 works as one coherent library.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
    </item>
    <item>
      <title>Engineering Post: Build() works! one pipeline, every error, a stable code each</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sat, 20 Jun 2026 20:45:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-build-works-one-pipeline-every-error-a-stable-code-each-4e05</link>
      <guid>https://dev.to/ernestohs/engineering-post-build-works-one-pipeline-every-error-a-stable-code-each-4e05</guid>
      <description>&lt;p&gt;M5 is where the pieces become a product. The &lt;code&gt;DefinitionCompiler&lt;/code&gt; takes a snapshot&lt;br&gt;
of the builder state and runs it through a pipeline that ends in a frozen,&lt;br&gt;
immutable &lt;code&gt;GenerationPlan&lt;/code&gt;. It tests a choice I made while designing &lt;code&gt;Build()&lt;/code&gt;:&lt;br&gt;
&lt;strong&gt;every failure carries a LIE code from a registry, never a bare exception, and&lt;br&gt;
the compiler reports &lt;em&gt;every&lt;/em&gt; detectable error, not just the first.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Validate options (empty name → &lt;strong&gt;LIE005&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Resolve member expressions via the M3 resolver (bad shape → &lt;strong&gt;LIE001&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Group rules per member and detect conflicts (&lt;strong&gt;LIE002&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Plan construction (&lt;strong&gt;LIE004&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Infer every unruled member (M4).&lt;/li&gt;
&lt;li&gt;Validate results, a required member that resolves to "unsupported" → &lt;strong&gt;LIE003&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Compile the reachable child graph, cycle-safe.&lt;/li&gt;
&lt;li&gt;If any error-severity diagnostic exists, throw; otherwise freeze the plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything appends to one &lt;code&gt;DiagnosticBag&lt;/code&gt;, and the pipeline runs to completion so&lt;br&gt;
the exception carries the full list. &lt;code&gt;LieDefinitionException.Message&lt;/code&gt; is composed&lt;br&gt;
to a contract, the first error, prefixed with the model and suffixed with the&lt;br&gt;
remaining count, so naive logging is already useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Definition for Car is invalid: Conflicting member rules for Car.Year (LIE002).
(2 more errors; see Diagnostics.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Constructor selection
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ConstructorPlanner&lt;/code&gt; implements the order I chose during design: a single&lt;br&gt;
&lt;code&gt;[LieConstructor]&lt;/code&gt;-marked ctor wins; otherwise the public ctor with the most&lt;br&gt;
&lt;em&gt;resolvable&lt;/em&gt; parameters; a parameterless ctor is preferred only on a tie;&lt;br&gt;
otherwise it's ambiguous (&lt;strong&gt;LIE004&lt;/strong&gt;). "Resolvable" is decided by running the&lt;br&gt;
inference pipeline against a synthetic member built from each parameter, so&lt;br&gt;
constructor parameters and properties share one inference implementation by&lt;br&gt;
construction, which is exactly the promise the API makes. The selection matrix&lt;br&gt;
(attribute, most-resolvable, parameterless tie, ambiguous failure, positional&lt;br&gt;
record) is a five-case test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-phase child compilation, the easy way
&lt;/h2&gt;

&lt;p&gt;The reachable type graph, nested members, collection element types, is compiled&lt;br&gt;
in the same &lt;code&gt;Build()&lt;/code&gt;. Recursive graphs (&lt;code&gt;Employee.Manager&lt;/code&gt; is an &lt;code&gt;Employee&lt;/code&gt;) have&lt;br&gt;
to terminate. The architecture describes "shells filled in two phases"; I got the&lt;br&gt;
same guarantee more simply with a &lt;strong&gt;visited-set worklist over types&lt;/strong&gt;. A&lt;br&gt;
&lt;code&gt;NestedSource&lt;/code&gt; holds the child &lt;em&gt;type&lt;/em&gt;, not a direct plan reference, and the runtime&lt;br&gt;
resolves it through a &lt;code&gt;ReachablePlans&lt;/code&gt; dictionary. So compilation is just: pop a&lt;br&gt;
type, build its plan (which enqueues its children), mark it visited, repeat.&lt;br&gt;
&lt;code&gt;Employee&lt;/code&gt; enqueues &lt;code&gt;Employee&lt;/code&gt;, sees it's already building, and stops. One pass, no&lt;br&gt;
infinite recursion, no mutable shells. A self-type reference emits &lt;strong&gt;LIE009&lt;/strong&gt; (Info,&lt;br&gt;
not an error), the build still succeeds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxyjiunbe4jhtmjo4wkg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxyjiunbe4jhtmjo4wkg.png" alt=" " width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry is executable, too
&lt;/h2&gt;

&lt;p&gt;The diagnostic codes live in an internal registry, code, default severity, and&lt;br&gt;
title. A conformance test asserts the table matches the diagnostic set I designed.&lt;br&gt;
It is the same move as the inference catalog: turn an intended shape into&lt;br&gt;
something the build verifies.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honest deferral
&lt;/h2&gt;

&lt;p&gt;There's a wrinkle in the milestone order I chose: the compiler needs &lt;em&gt;generators&lt;/em&gt;&lt;br&gt;
for inferred scalars, but the real generators (names, emails, lorem) arrive in&lt;br&gt;
M7. M5 therefore emits placeholder delegates that throw "bound in M7."&lt;br&gt;
Generation is still stubbed, so they are never invoked, while the plan&lt;br&gt;
&lt;em&gt;structure&lt;/em&gt; remains fully correct and testable. The &lt;code&gt;MemberReportData&lt;/code&gt; captures&lt;br&gt;
the real inference result; only the executable function waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M6, the Generation Runtime
&lt;/h2&gt;

&lt;p&gt;The plan exists; nothing runs it. M6 builds &lt;code&gt;GenerationOperation&lt;/code&gt;, the per-call&lt;br&gt;
engine that traverses the plan in lifecycle order (construct, populate in member&lt;br&gt;
order, derive in registration order), checks cancellation at the documented&lt;br&gt;
checkpoints, wraps user-delegate failures exactly once with the right&lt;br&gt;
&lt;code&gt;GenerationPhase&lt;/code&gt;, and handles depth and cycles. And it surfaces the question I'd&lt;br&gt;
been quietly dreading: if generators are M7, what exactly can M6 generate?&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Dev Log: The milestone where Build() stops lying</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sat, 20 Jun 2026 20:35:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/dev-log-the-milestone-where-build-stops-lying-1b2o</link>
      <guid>https://dev.to/ernestohs/dev-log-the-milestone-where-build-stops-lying-1b2o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 6&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For three milestones, &lt;code&gt;Build()&lt;/code&gt; has been a method that throws&lt;br&gt;
&lt;code&gt;NotImplementedException&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors are first-class citizens
&lt;/h2&gt;

&lt;p&gt;While designing &lt;code&gt;Build()&lt;/code&gt;, 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 &lt;code&gt;Build()&lt;/code&gt; collects &lt;em&gt;all&lt;/em&gt; of them before throwing. If your definition has a bad expression, a rule conflict, and an empty name, you get one exception listing all&lt;br&gt;
three instead of discovering them one at a time.&lt;/p&gt;

&lt;p&gt;I built each diagnostic with a deliberately triggerable test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LIE001&lt;/code&gt;: &lt;code&gt;With(c =&amp;gt; c.Owner.FirstName, ...)&lt;/code&gt; (nested path, illegal target)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIE002&lt;/code&gt;: &lt;code&gt;With(...)&lt;/code&gt; and &lt;code&gt;Ignore(...)&lt;/code&gt; on the same member (contradiction)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIE003&lt;/code&gt;: a &lt;code&gt;required&lt;/code&gt; member of an interface type (can't be resolved)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIE004&lt;/code&gt;: two equally-resolvable constructors, no tiebreaker&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIE005&lt;/code&gt;: &lt;code&gt;WithName("   ")&lt;/code&gt; (empty/whitespace)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's something satisfying about writing tests whose job is to &lt;em&gt;provoke&lt;/em&gt;&lt;br&gt;
failure and then checking that the failure is well labeled. Good error messages&lt;br&gt;
are a feature. Keeping the code registry and expected diagnostic table in sync&lt;br&gt;
through a conformance test makes that feature difficult to erode accidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recursion, without the headache
&lt;/h2&gt;

&lt;p&gt;The scary part, on paper, was the recursive-type graph. &lt;code&gt;Employee&lt;/code&gt; has a &lt;code&gt;Manager&lt;/code&gt; that's an &lt;code&gt;Employee&lt;/code&gt;; compile that naively, and you recurse forever. My original architecture used a two-phase "allocate empty shells, then fill them" dance.&lt;br&gt;
While implementing it, I found a simpler route: keep a &lt;em&gt;visited set&lt;/em&gt; of types and a worklist, and have nested members reference their child by &lt;em&gt;type&lt;/em&gt; rather than a direct plan pointer. The runtime looks the plan up in a dictionary later.&lt;br&gt;
Compiling &lt;code&gt;Employee&lt;/code&gt; now completes in a single pass and records a friendly LIE009 Info note. The implementation improved the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deferral I had to make peace with
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable bit. The compiler wants to bind a &lt;em&gt;generator&lt;/em&gt; to each inferred member, but the actual generators (the thing that produces "Anthony" for a &lt;code&gt;FirstName&lt;/code&gt;) are datasets, and datasets are two milestones away in M7. Classic ordering tension.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;can&lt;/em&gt; verify, the plan structure, constructor choices, diagnostics, and recursive termination, is real. It nagged at me to add deliberate &lt;code&gt;throw&lt;/code&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tooling mystery
&lt;/h2&gt;

&lt;p&gt;This is also the milestone when my beloved &lt;code&gt;dotnet format&lt;/code&gt; 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.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it leaves things
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Lie.Define&amp;lt;Car&amp;gt;().Build()&lt;/code&gt; now works end to end. It returns an immutable&lt;br&gt;
&lt;code&gt;LieDefinition&amp;lt;Car&amp;gt;&lt;/code&gt; holding a complete, frozen plan: a constructor choice, every&lt;br&gt;
member's value source, derivations, the reachable child plans, retained&lt;br&gt;
Info/Warning diagnostics. &lt;code&gt;Generate()&lt;/code&gt; still throws, there's no runtime, but the&lt;br&gt;
&lt;em&gt;compile&lt;/em&gt; is real, validated, and recursion-safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;M6: actually running the thing. The generation runtime, lifecycle, cancellation, exception wrapping, depth, and cycle handling. It also forces me to revisit the milestone plan: with the real generators still in M7, what can the runtime honestly produce?&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Engineering Post: The part with taste: classifying types and matching names</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sat, 20 Jun 2026 05:23:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-the-part-with-taste-classifying-types-and-matching-names-6pf</link>
      <guid>https://dev.to/ernestohs/engineering-post-the-part-with-taste-classifying-types-and-matching-names-6pf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 5&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Engineering Post: the reasoning, trade-offs, API decisions, and technical choices behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M4 is the brain. Given a member the user didn't configure, the inference engine decides what to generate. It runs in two parts: a structural classifier that asks &lt;em&gt;what shape is this?&lt;/em&gt;, and a stage pipeline that asks &lt;em&gt;what value belongs here?&lt;/em&gt;, both producing entries for an immutable plan model that the compiler will later freeze.&lt;br&gt;
This milestone tests the part of my design that is most subjective.&lt;/p&gt;
&lt;h2&gt;
  
  
  Structural classification
&lt;/h2&gt;

&lt;p&gt;Before any name matching, every member type is bucketed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalar&lt;/strong&gt;: primitives, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;Guid&lt;/code&gt;, enums, the date/time family,
&lt;code&gt;decimal&lt;/code&gt;, &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt; of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested&lt;/strong&gt;: a class/struct with discoverable construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection&lt;/strong&gt;: &lt;code&gt;T[]&lt;/code&gt;, &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;, the read-only and interface variants,
&lt;code&gt;Dictionary&amp;lt;K,V&amp;gt;&lt;/code&gt; and friends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported&lt;/strong&gt;: interfaces/abstracts without registration, exotic value types
(&lt;code&gt;nint&lt;/code&gt;, &lt;code&gt;Half&lt;/code&gt;, &lt;code&gt;Int128&lt;/code&gt;), pointers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's one deliberate special case: &lt;strong&gt;&lt;code&gt;byte[]&lt;/code&gt; is a scalar&lt;/strong&gt;, not a collection.&lt;br&gt;
It infers as "16 random bytes," because nobody wants a &lt;code&gt;List&amp;lt;byte&amp;gt;&lt;/code&gt;-style element&lt;br&gt;
walk over a blob. The classifier checks for it explicitly before the array branch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The semantic catalog
&lt;/h2&gt;

&lt;p&gt;The heart of the milestone is a 44-row candidate table, the project's curated&lt;br&gt;
"taste," captured in the design document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"firstname"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"givenname"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"forename"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Name.First"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"emailaddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mail"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;      &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Internet.Email"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"make"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"manufacturer"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;VehicleHints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Vehicle.Make"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NoHintConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Member names are normalized (split on case/underscore/hyphen, lowercase, join) so&lt;br&gt;
&lt;code&gt;FirstName&lt;/code&gt;, &lt;code&gt;first_name&lt;/code&gt;, and &lt;code&gt;FIRST-NAME&lt;/code&gt; all become &lt;code&gt;firstname&lt;/code&gt;. The matcher&lt;br&gt;
then applies the documented rules: the member's value type must equal the&lt;br&gt;
candidate's; an exact name match with a matching &lt;em&gt;model hint&lt;/em&gt; is High confidence; an exact match with no hints uses the candidate's base; a hint-gated miss drops a level; a suffix match (&lt;code&gt;CustomerEmail&lt;/code&gt; ends with &lt;code&gt;email&lt;/code&gt;) drops one below the exact&lt;br&gt;
result. The selected mode (Conservative/Balanced/Aggressive) then filters by confidence, and rejected candidates are recorded for &lt;code&gt;Explain()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c75zqn4kbypav52oms4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5c75zqn4kbypav52oms4.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A real design fork
&lt;/h2&gt;

&lt;p&gt;The catalog had an internal tension I had not noticed while designing it. The&lt;br&gt;
general rule says a hint-gated miss drops &lt;em&gt;one&lt;/em&gt; level (High → Medium). But the&lt;br&gt;
Vehicle rows annotate &lt;code&gt;make&lt;/code&gt;/&lt;code&gt;model&lt;/code&gt; as "no hint: &lt;strong&gt;Low&lt;/strong&gt;" (two levels) and&lt;br&gt;
&lt;code&gt;year&lt;/code&gt; as "no hint: a &lt;em&gt;different&lt;/em&gt; generator at Medium." My rule and exceptions&lt;br&gt;
disagreed, and the choice affects seeded output, so I stopped implementing and&lt;br&gt;
revisited the intended user experience.&lt;/p&gt;

&lt;p&gt;The resolution (the per-row notes win over the general rule) came down to a&lt;br&gt;
user-experience argument worth repeating: the words &lt;em&gt;make&lt;/em&gt; and &lt;em&gt;model&lt;/em&gt; are&lt;br&gt;
extremely vehicle-specific. If a &lt;code&gt;Printer.Make&lt;/code&gt; confidently resolves to "Toyota"&lt;br&gt;
under the default mode, that's the worst failure for a mock-data tool, output&lt;br&gt;
that &lt;em&gt;looks&lt;/em&gt; plausible but is nonsense and slips into fixtures unnoticed. Dropping&lt;br&gt;
to Low means it falls back to generic lorem text under the default, which the user&lt;br&gt;
&lt;em&gt;sees&lt;/em&gt; and corrects. A visible false-negative beats an invisible false-positive.&lt;br&gt;
So the candidate model grew optional &lt;code&gt;NoHintConfidence&lt;/code&gt;/&lt;code&gt;NoHintGenerator&lt;/code&gt; fields&lt;br&gt;
to encode that decision explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalog is executable
&lt;/h2&gt;

&lt;p&gt;A conformance test records all 44 expected rows &lt;em&gt;independently&lt;/em&gt; from the catalog&lt;br&gt;
code and compares them one-to-one against the implementation: names, hints, value&lt;br&gt;
types, and base confidence. Drift in either direction fails the build. The&lt;br&gt;
catalog is not just data; its intended shape is executable, the same approach I&lt;br&gt;
will use for the diagnostic registry in M5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal enums, on purpose
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;InferenceConfidence&lt;/code&gt; and &lt;code&gt;InferenceSource&lt;/code&gt; are public types in the final API, but they belong to the report family that ships in M8. To keep M4 free of public surface, they live as &lt;em&gt;internal&lt;/em&gt; mirrors here and get promoted later (a trick that works cleanly because a &lt;code&gt;Munchausen.Inference&lt;/code&gt; type resolves an unqualified &lt;code&gt;InferenceConfidence&lt;/code&gt; against the enclosing &lt;code&gt;Munchausen&lt;/code&gt; namespace).&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M5, the Definition Compiler
&lt;/h2&gt;

&lt;p&gt;M4 produces inference &lt;em&gt;decisions&lt;/em&gt;; M5 assembles them into a real, frozen plan.&lt;br&gt;
The &lt;code&gt;DefinitionCompiler&lt;/code&gt; pipeline, resolve expressions, detect rule conflicts, plan construction, infer, validate, compile the reachable child graph (including recursive types), wired to a diagnostic registry where every &lt;code&gt;Build()&lt;/code&gt; failure carries a stable LIE code. It's where &lt;code&gt;Build()&lt;/code&gt; finally stops throwing &lt;code&gt;NotImplementedException&lt;/code&gt; and starts doing its job.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Dev Log: When the design contradicts itself, stop typing</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Sat, 20 Jun 2026 05:10:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/dev-log-when-the-design-contradicts-itself-stop-typing-9dj</link>
      <guid>https://dev.to/ernestohs/dev-log-when-the-design-contradicts-itself-stop-typing-9dj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 5&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M4 is the milestone with opinions. It's the catalog that decides &lt;code&gt;FirstName&lt;/code&gt;&lt;br&gt;
should be a name, &lt;code&gt;Email&lt;/code&gt; an email, &lt;code&gt;Price&lt;/code&gt; some money, the part that makes Munchausen feel smart instead of just random. And it's the milestone where, for the first time, I stopped building and revisited an assumption because my design genuinely disagreed with itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The matching rules are mostly mechanical: normalize the member name (so &lt;code&gt;FirstName&lt;/code&gt;, &lt;code&gt;first_name&lt;/code&gt;, and &lt;code&gt;FIRST-NAME&lt;/code&gt; all collapse to &lt;code&gt;firstname&lt;/code&gt;), check the value type matches, score the candidate's confidence, and let the selected mode filter it. Forty-four rows of curated candidates, captured in the design document. Straightforward.&lt;/p&gt;

&lt;p&gt;Except that the catalog had a knot in it. The general rule says: if a candidate is gated by a &lt;em&gt;model hint&lt;/em&gt; (like "this only applies to car-ish models") and the hint doesn't match, drop the confidence one level. But the Vehicle rows had hand-written notes saying &lt;code&gt;make&lt;/code&gt;/&lt;code&gt;model&lt;/code&gt; should drop to &lt;strong&gt;Low&lt;/strong&gt; (two levels), and &lt;code&gt;year&lt;/code&gt; should switch to an entirely &lt;em&gt;different&lt;/em&gt; generator. I had written a general rule and exceptions that disagreed. Since the choice affects seeded output, every future golden would preserve whichever interpretation I picked.&lt;/p&gt;

&lt;p&gt;Instead of choosing the easier implementation, I returned to the user experience I wanted the catalog to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  The argument that settled it
&lt;/h2&gt;

&lt;p&gt;Think about a non-vehicle model with a &lt;code&gt;Make&lt;/code&gt; property, a &lt;code&gt;Printer&lt;/code&gt;, a &lt;code&gt;Shirt&lt;/code&gt;.&lt;br&gt;
Under the lenient reading, &lt;code&gt;Make&lt;/code&gt; confidently generates "Toyota." Under the strict&lt;br&gt;
reading (the row notes), it drops to Low, gets rejected by the default mode, and&lt;br&gt;
falls back to obvious lorem filler. Which behavior would I rather debug?&lt;/p&gt;

&lt;p&gt;The lorem is better. A car brand sitting in a printer's &lt;code&gt;Make&lt;/code&gt; field is the&lt;br&gt;
&lt;em&gt;worst&lt;/em&gt; kind of bug for a fake-data library: it looks completely plausible, so it&lt;br&gt;
sails through review and into your test fixtures, quietly wrong. Generic filler,&lt;br&gt;
on the other hand, screams "I didn't recognize this", you see it, you add a rule,&lt;br&gt;
done. A false negative you can spot beats a false positive you can't. So the row&lt;br&gt;
notes won, and the candidate model grew a couple of optional override fields to&lt;br&gt;
encode that decision.&lt;/p&gt;

&lt;p&gt;I love that this was a &lt;em&gt;taste&lt;/em&gt; decision hiding inside a matching rule. Resolving it taught me something important about the library: protecting users from confidently wrong data matters more than maximizing how often inference succeeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the catalog defend itself
&lt;/h2&gt;

&lt;p&gt;The other thing I'm proud of here is the conformance test. The catalog is 44 rows of data, and the data rots. A future edit could quietly shift a confidence or alias.&lt;br&gt;
So I wrote the intended catalog a &lt;em&gt;second&lt;/em&gt; time, independently, in the test and asserted the two match row-for-row. Now changing the design requires an explicit change to both the implementation and its expectation. I'll reuse the same idea for diagnostic codes next milestone.&lt;/p&gt;

&lt;p&gt;It passed on the first run, which means my two independent transcriptions agreed, a small but real confidence boost that I didn't fat-finger the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sneaky edge case
&lt;/h2&gt;

&lt;p&gt;There's one candidate (&lt;code&gt;category&lt;/code&gt;) that's hinted but has a &lt;em&gt;Medium&lt;/em&gt; base, where a hint match produces a slightly surprising promotion to High. I kept the general behavior and left a comment because &lt;code&gt;category&lt;/code&gt; has no explicit exception. The surprise is now visible and easy to reconsider later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it leaves things
&lt;/h2&gt;

&lt;p&gt;The engine can now look at any member and decide: scalar or nested or collection or&lt;br&gt;
unsupported; if scalar, which semantic generator (by name) and at what confidence,&lt;br&gt;
or else a type default. It doesn't &lt;em&gt;run&lt;/em&gt; any generators yet; it just produces&lt;br&gt;
decisions and the plan-model types to hold them. The taste is in; the execution is&lt;br&gt;
next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;M5: the compiler. Time to take all these inference decisions plus the user's rules and assemble them into one frozen, immutable plan, detecting conflicts, choosing constructors, walking the recursive type graph, with every possible failure carrying a stable diagnostic code. It's the milestone where &lt;code&gt;Build()&lt;/code&gt; finally&lt;br&gt;
works.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Engineering Post: A builder that captures everything and validates nothing (yet)</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Fri, 19 Jun 2026 05:56:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-a-builder-that-captures-everything-and-validates-nothing-yet-p3b</link>
      <guid>https://dev.to/ernestohs/engineering-post-a-builder-that-captures-everything-and-validates-nothing-yet-p3b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 4&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Engineering Post: the reasoning, trade-offs, API decisions, and technical choices behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M3 is where Munchausen grows a public face: &lt;code&gt;Lie.Define&amp;lt;T&amp;gt;()&lt;/code&gt;, the fluent&lt;br&gt;
builder, the options records, and the little expression resolver that polices how&lt;br&gt;
you target members. It tests an API choice I made during design: &lt;strong&gt;the builder&lt;br&gt;
captures state and validates nothing; every failure is reported at &lt;code&gt;Build()&lt;/code&gt;, in&lt;br&gt;
one place, with a diagnostic code.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Capture now, fail later
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LieDefinitionBuilder&amp;lt;T&amp;gt;&lt;/code&gt; wraps an internal &lt;code&gt;BuilderState&lt;/code&gt;. Each fluent method&lt;br&gt;
appends a record and returns &lt;code&gt;this&lt;/code&gt;; nothing is parsed or checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;MemberRuleRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;LambdaExpression&lt;/span&gt; &lt;span class="n"&gt;MemberExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MemberRuleKind&lt;/span&gt; &lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// WithValue, WithGenerator, Derive, Ignore, Preserve&lt;/span&gt;
    &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;RegistrationIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the builder dumb has three payoffs: it's cheap, it preserves registration&lt;br&gt;
order (which &lt;code&gt;Derive&lt;/code&gt; and last-write-wins resolution depend on), and it funnels&lt;br&gt;
&lt;em&gt;all&lt;/em&gt; error reporting into &lt;code&gt;Build()&lt;/code&gt; so users get every problem at once instead of&lt;br&gt;
one-at-a-time exceptions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithDefaults&lt;/code&gt; is the one method that merges immediately, non-null properties&lt;br&gt;
overwrite, implementing per-property last-write-wins. &lt;code&gt;WithSeed(42)&lt;/code&gt; is literally&lt;br&gt;
&lt;code&gt;WithDefaults(new GenerationDefaults { Seed = 42 })&lt;/code&gt;, no special case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdtef99o5ie22mfywp6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdtef99o5ie22mfywp6o.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The single targeting rule
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ExpressionMemberResolver&lt;/code&gt; enforces exactly one shape: &lt;code&gt;x =&amp;gt; x.Property&lt;/code&gt;. It&lt;br&gt;
unwraps the &lt;code&gt;Convert&lt;/code&gt;/&lt;code&gt;ConvertChecked&lt;/code&gt; node the compiler inserts when a value-type&lt;br&gt;
member is read through a generic lambda, then checks the body is a member access&lt;br&gt;
rooted directly at the parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;UnaryExpression&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;NodeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ConvertChecked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Operand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;MemberExpression&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Member&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PropertyInfo&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
    &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ParameterExpression&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;
    &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;ReferenceEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ExpressionResolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resolved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ExpressionResolution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LIE001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* expression text */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything else, &lt;code&gt;x =&amp;gt; x.Owner.Name&lt;/code&gt; (nested), &lt;code&gt;x =&amp;gt; x.Make.ToUpper()&lt;/code&gt; (method),&lt;br&gt;
&lt;code&gt;x =&amp;gt; x.Items[0]&lt;/code&gt; (indexer), a captured variable, a constant, produces &lt;strong&gt;LIE001&lt;/strong&gt;&lt;br&gt;
with the offending expression text. Single-level targeting is a deliberate v1.0&lt;br&gt;
rule; nested behavior is the job of child definitions later. Putting that decision&lt;br&gt;
in &lt;em&gt;one&lt;/em&gt; class means there's exactly one place it's enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The locked surface, for real this time
&lt;/h2&gt;

&lt;p&gt;M3 is the first milestone that adds public API, so the &lt;code&gt;PublicApiAnalyzer&lt;/code&gt; from M0&lt;br&gt;
finally bites. Adding the builder forced a realization: its method signatures&lt;br&gt;
reference types that don't fully exist yet. &lt;code&gt;With(expr, Func&amp;lt;GenerationContext,&lt;br&gt;
TProperty&amp;gt;)&lt;/code&gt; needs &lt;code&gt;GenerationContext&lt;/code&gt;; &lt;code&gt;Build()&lt;/code&gt; returns &lt;code&gt;LieDefinition&amp;lt;T&amp;gt;&lt;/code&gt;, whose&lt;br&gt;
&lt;code&gt;Explain()&lt;/code&gt; returns &lt;code&gt;InferenceReport&lt;/code&gt;. So those types get introduced now as&lt;br&gt;
&lt;strong&gt;public stubs&lt;/strong&gt;, declared, documented, with &lt;code&gt;NotImplementedException&lt;/code&gt; internals&lt;br&gt;
that later milestones fill in. When I split the work into stages, I allowed those&lt;br&gt;
placeholders until M6 so I could test the complete public type graph before all&lt;br&gt;
of its behavior existed.&lt;/p&gt;

&lt;p&gt;Two surface mechanics worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The builder's constructor is &lt;code&gt;internal&lt;/code&gt;, you create builders through
&lt;code&gt;Lie.Define&amp;lt;T&amp;gt;()&lt;/code&gt;, never &lt;code&gt;new&lt;/code&gt;, so the public listing has no builder ctor.&lt;/li&gt;
&lt;li&gt;The two &lt;code&gt;Generate&lt;/code&gt; overloads use optional parameters, which trips the analyzer's
&lt;code&gt;RS0026&lt;/code&gt; ("don't overload with optional params"). I revisited the overloads,
still preferred their call-site ergonomics, and suppressed &lt;code&gt;RS0026&lt;/code&gt; with the
rationale recorded. The warning informed the decision without making it for me.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Generating the surface file
&lt;/h2&gt;

&lt;p&gt;Hand-writing &lt;code&gt;PublicAPI.Shipped.txt&lt;/code&gt; is error-prone (every default value,&lt;br&gt;
nullability annotation, and generic arity has to be exact). The trick is&lt;br&gt;
&lt;code&gt;dotnet format analyzers --diagnostics RS0016&lt;/code&gt;, which applies the analyzer's&lt;br&gt;
"add to public API" fix and writes the entries in the precise format, then I move&lt;br&gt;
them from &lt;code&gt;Unshipped&lt;/code&gt; to &lt;code&gt;Shipped&lt;/code&gt; after reviewing them against the API design.&lt;br&gt;
(This worked here, broke mysteriously in M5, and got properly diagnosed in M7.&lt;br&gt;
More on that saga later.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The resolver accept/reject corpus produces LIE001 with expression text on every&lt;br&gt;
bad shape; &lt;code&gt;WithDefaults&lt;/code&gt; merge tests cover per-property last-write, null&lt;br&gt;
no-opinion, and &lt;code&gt;WithSeed&lt;/code&gt; equivalence; an acceptance test fluently chains the&lt;br&gt;
whole builder from an external assembly to prove the surface is usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M4, the Inference Engine
&lt;/h2&gt;

&lt;p&gt;M3 captures &lt;em&gt;what you said&lt;/em&gt;. M4 figures out &lt;em&gt;what to do with everything you didn't&lt;/em&gt;, the inference engine. Structural classification (scalar / nested / collection /&lt;br&gt;
unsupported), the semantic catalog that maps &lt;code&gt;FirstName&lt;/code&gt; → a name and &lt;code&gt;Email&lt;/code&gt; → an&lt;br&gt;
email, the confidence model, and the plan types the compiler will later freeze.&lt;br&gt;
It's the milestone with the most "taste" baked in, and the one where implementation exposes a contradiction in my design.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Dev Log: The first public API, and the type graph that wouldn't stay small</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Fri, 19 Jun 2026 05:34:00 +0000</pubDate>
      <link>https://dev.to/ernestohs/dev-log-the-first-public-api-and-the-type-graph-that-wouldnt-stay-small-4h74</link>
      <guid>https://dev.to/ernestohs/dev-log-the-first-public-api-and-the-type-graph-that-wouldnt-stay-small-4h74</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 4&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Dev Log: the practical work, cleanup, implementation steps, and day-to-day progress behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;M3 is a milestone with a flag on it: the first public surface. &lt;code&gt;Lie.Define&amp;lt;T&amp;gt;()&lt;/code&gt;,&lt;br&gt;
the fluent builder, the options records. Up to now everything was internal&lt;br&gt;
plumbing nobody could call. Now I'm testing the API I designed by committing to&lt;br&gt;
shapes that users will eventually depend on. Once a public member ships, changing&lt;br&gt;
it becomes expensive.&lt;/p&gt;
&lt;h2&gt;
  
  
  The builder is deliberately lazy
&lt;/h2&gt;

&lt;p&gt;The fun design decision here is that the builder does almost nothing. You call&lt;br&gt;
&lt;code&gt;.With(...)&lt;/code&gt;, &lt;code&gt;.Ignore(...)&lt;/code&gt;, &lt;code&gt;.Derive(...)&lt;/code&gt; and it just appends a little record&lt;br&gt;
and hands you back &lt;code&gt;this&lt;/code&gt;. No validation, no parsing. All of that is deferred to&lt;br&gt;
&lt;code&gt;Build()&lt;/code&gt;. It feels almost too lazy until you see why: you want &lt;em&gt;all&lt;/em&gt; your&lt;br&gt;
definition errors reported together, in one exception, not dribbled out one&lt;br&gt;
&lt;code&gt;throw&lt;/code&gt; at a time as you chain methods. So the builder's job is purely to remember&lt;br&gt;
what you said, in order, and &lt;code&gt;Build()&lt;/code&gt; is where judgment happens. Cheap builder,&lt;br&gt;
smart compile.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Wait, I have to declare half the library"
&lt;/h2&gt;

&lt;p&gt;Here's the thing that caught me. To write the builder's method signatures, I need&lt;br&gt;
the types they mention. &lt;code&gt;With(...)&lt;/code&gt; takes a &lt;code&gt;Func&amp;lt;GenerationContext, T&amp;gt;&lt;/code&gt;. &lt;code&gt;Build()&lt;/code&gt;&lt;br&gt;
returns a &lt;code&gt;LieDefinition&amp;lt;T&amp;gt;&lt;/code&gt;. &lt;code&gt;Explain()&lt;/code&gt; returns an &lt;code&gt;InferenceReport&lt;/code&gt;. None of&lt;br&gt;
those are built yet, they're milestones away. But you can't have a public method&lt;br&gt;
whose parameter type doesn't exist.&lt;/p&gt;

&lt;p&gt;So M3 quietly drags a chunk of the type graph into existence as &lt;em&gt;stubs&lt;/em&gt;: public&lt;br&gt;
types, fully documented, whose bodies just &lt;code&gt;throw new NotImplementedException()&lt;/code&gt;.&lt;br&gt;
When I divided the work into stages, I allowed those placeholders until M6. It&lt;br&gt;
still feels strange to add a &lt;code&gt;GenerationContext&lt;/code&gt; that does nothing and a&lt;br&gt;
&lt;code&gt;Generate()&lt;/code&gt; that refuses to run, but it lets me test the complete API shape&lt;br&gt;
before every behavior exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  Falling in love with &lt;code&gt;dotnet format&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Maintaining the locked-surface text file by hand is miserable, you have to write&lt;br&gt;
each entry in the analyzer's exact dialect, down to how &lt;code&gt;int.MinValue&lt;/code&gt; is spelled&lt;br&gt;
and where the &lt;code&gt;!&lt;/code&gt; nullability markers go. I discovered that&lt;br&gt;
&lt;code&gt;dotnet format analyzers --diagnostics RS0016&lt;/code&gt; just &lt;em&gt;generates&lt;/em&gt; all of it&lt;br&gt;
correctly. Run it, review the generated surface against the API I designed, move&lt;br&gt;
the lines into the shipped file. This will become a recurring character in the&lt;br&gt;
story, including the episode two milestones from now where it inexplicably stops&lt;br&gt;
working and I waste real time before figuring out why.&lt;/p&gt;
&lt;h2&gt;
  
  
  A small principled stand
&lt;/h2&gt;

&lt;p&gt;The analyzer also complained (&lt;code&gt;RS0026&lt;/code&gt;) about the two &lt;code&gt;Generate&lt;/code&gt; overloads both&lt;br&gt;
having optional parameters, a legitimate general warning, but it conflicted with&lt;br&gt;
the API shape I had chosen. This forced me to revisit that choice rather than&lt;br&gt;
blindly obey either side. I still preferred the overloads, so I suppressed the&lt;br&gt;
rule with a comment explaining why. A linter is useful evidence, not the designer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it leaves things
&lt;/h2&gt;

&lt;p&gt;You can now write the fluent thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Lie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Define&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cars"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;With&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Make&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Saab"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;With&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"900"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1989&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and it compiles, captures everything correctly, and rejects &lt;code&gt;x =&amp;gt; x.Owner.Name&lt;/code&gt;&lt;br&gt;
with a clear LIE001. &lt;code&gt;Build()&lt;/code&gt; still throws, there's no compiler yet, but the&lt;br&gt;
&lt;em&gt;ergonomics&lt;/em&gt; are real, and the public surface is locked and reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;M4: inference. This is the milestone with opinions, the catalog that decides&lt;br&gt;
&lt;code&gt;FirstName&lt;/code&gt; should be a first name and &lt;code&gt;Price&lt;/code&gt; should be money. It's also where I&lt;br&gt;
find the first genuine contradiction in my design and have to decide which&lt;br&gt;
behavior better serves the user.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Stop Training Your AI to Agree With You</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Thu, 18 Jun 2026 00:32:19 +0000</pubDate>
      <link>https://dev.to/ernestohs/stop-training-your-ai-to-agree-with-you-5gh7</link>
      <guid>https://dev.to/ernestohs/stop-training-your-ai-to-agree-with-you-5gh7</guid>
      <description>&lt;p&gt;The easiest way to make an AI assistant useless is to reward it for being pleasant.&lt;/p&gt;

&lt;p&gt;You ask whether your product launch plan is strong. It says the plan is exciting.&lt;br&gt;
You ask whether your pricing page is clear. It says the structure is solid.&lt;br&gt;
You ask whether your idea is worth pursuing. It says you are thinking in the right direction.&lt;/p&gt;

&lt;p&gt;None of those responses are necessarily lies. That is what makes the problem hard to notice. The AI is not always wrong. It is often just too eager to be helpful in the shallowest possible sense: it gives you momentum when what you needed was judgment.&lt;/p&gt;

&lt;p&gt;Most people do not actually want an AI that agrees with them. They want an assistant that catches mistakes earlier, challenges weak assumptions, and helps them produce better work. They want something closer to a capable coworker: direct, useful, respectful, and willing to say, "I would not do it this way."&lt;/p&gt;

&lt;p&gt;You can configure an AI system to move in that direction. Not perfectly. Not permanently. But meaningfully enough that it changes the quality of the work you get back.&lt;/p&gt;

&lt;p&gt;The trick is not to tell the model, "Be more critical." That instruction is too vague, and it often creates a new problem: an assistant that performs skepticism on everything, including simple tasks that do not need debate. The better goal is calibrated pushback. Challenge the user when it improves the outcome. Get out of the way when it does not.&lt;/p&gt;

&lt;p&gt;This article shows how to do that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Yes-Man Problem
&lt;/h2&gt;

&lt;p&gt;Imagine you type this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to launch this product next week. Write the plan."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A compliant assistant will probably give you a launch plan. It may include milestones, marketing copy, onboarding steps, and a post-launch review. The response may look professional. It may even be useful.&lt;/p&gt;

&lt;p&gt;But a better assistant starts one step earlier:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I can help with the plan, but launching next week may be premature if onboarding, support, and payment flows have not been tested. Before committing to that date, I would check these risks..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second response is not less helpful. It is more helpful because it protects the decision, not just the document.&lt;/p&gt;

&lt;p&gt;This is the central problem with many AI workflows: the assistant treats the user's immediate instruction as the highest priority, even when the instruction is built on a shaky assumption.&lt;/p&gt;

&lt;p&gt;That behavior is partly a product-design problem, partly a training problem, and partly a user-expectation problem. Modern assistants are shaped by feedback systems that tend to reward responses that feel helpful, polite, and satisfying. Agreement is often safer than friction. Validation is often easier than correction. The model learns that making the user feel served is a reliable path to a good interaction.&lt;/p&gt;

&lt;p&gt;But good coworkers do not optimize for approval. They optimize for the outcome.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Useful AI Should Sometimes Slow You Down
&lt;/h2&gt;

&lt;p&gt;The point is not to make your AI argumentative. An assistant that challenges everything is just another kind of bad assistant.&lt;/p&gt;

&lt;p&gt;If you ask it to add a 404 page using the standard template, you do not need a lecture about whether 404 pages are philosophically optimal. You need the 404 page.&lt;/p&gt;

&lt;p&gt;If you ask it to brainstorm rough names for a prototype, you probably do not need legal-risk analysis on every phrase. You need options.&lt;/p&gt;

&lt;p&gt;But if you ask it to replace your entire customer support team with an AI agent, a good assistant should not open with "Great idea." It should notice the operational risk, the customer-experience risk, the edge cases, the fallback problem, and the need for staged rollout.&lt;/p&gt;

&lt;p&gt;The difference is stakes.&lt;/p&gt;

&lt;p&gt;A useful AI collaborator should be configured to do five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Challenge weak assumptions before executing on them.&lt;/li&gt;
&lt;li&gt;Ask clarifying questions when guessing would be risky.&lt;/li&gt;
&lt;li&gt;Point out risks, tradeoffs, missing evidence, and hidden dependencies.&lt;/li&gt;
&lt;li&gt;Recommend a concrete path forward instead of merely criticizing.&lt;/li&gt;
&lt;li&gt;Calibrate pushback to the reversibility and impact of the decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point matters most. The assistant should not be equally skeptical about renaming a local variable, changing a pricing model, deleting production data, and publishing legal advice. Those are different classes of decision. Your configuration should make that distinction explicit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Start by Changing the Job Description
&lt;/h2&gt;

&lt;p&gt;Many people give their AI a role like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a helpful assistant.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds harmless, but it leaves too much room for the model's default behavior. "Helpful" often collapses into "agreeable." The assistant tries to satisfy the request instead of improving the result.&lt;/p&gt;

&lt;p&gt;Use a role that names the real job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a pragmatic coworker and effective mentor.

Your job is to help me reach better outcomes, not merely agree with me.
Be respectful and concise, but challenge unclear goals, weak reasoning,
risky assumptions, and low-quality plans. When you disagree, explain why
and propose a better path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not just cosmetic. It changes the target. You are telling the model that success is not compliance; success is better judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define Good Pushback and Bad Pushback
&lt;/h2&gt;

&lt;p&gt;If you simply say "challenge me," the assistant may overcorrect. It will start objecting to everything because objection looks like rigor.&lt;/p&gt;

&lt;p&gt;That is how you get responses like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Before implementing the 404 page, I want to challenge whether a 404 page is the right solution. Perhaps we should audit the entire URL structure first..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not critical thinking. That is performative friction.&lt;/p&gt;

&lt;p&gt;Good configuration draws a boundary around when disagreement is useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push back when:
- My request contains an unsupported assumption that affects the outcome.
- I appear to be optimizing for speed while ignoring quality, safety,
  cost, ethics, maintainability, or user impact.
- There is a simpler, safer, or more effective approach I have not considered.
- The plan has hidden dependencies or likely failure points.
- Important context is missing and guessing wrong would be costly.

Do not push back when:
- The task is straightforward, low-risk, and well specified.
- I explicitly ask for a quick draft or brainstorm and precision
  is not yet required.
- The disagreement is about preference rather than correctness.
- You would be restating a risk I have already acknowledged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second half is as important as the first. Without the "do not push back" rules, you do not get a wise assistant. You get a speed bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teach It to Weigh the Decision
&lt;/h2&gt;

&lt;p&gt;Most prompt advice tells the model to "consider risks and tradeoffs." That is fine, but generic. The model already knows how to list risks. What it needs is guidance on which risks matter enough to interrupt the user.&lt;/p&gt;

&lt;p&gt;Give it a weighting system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before responding, evaluate the request against these criteria:

1. Reversibility: Can this decision be easily undone? If yes, bias toward
   action with a brief risk note. If no, slow down and flag concerns.

2. Blast radius: Does this affect one person, a team, customers, finances,
   production systems, or public reputation? Scale your scrutiny accordingly.

3. Information completeness: Am I working with enough context to give
   a useful answer? If critical information is missing and guessing wrong
   would be costly, ask before proceeding.

4. Stated vs. actual goal: Does the immediate request align with what
   the user is trying to achieve? If there is a mismatch, address the
   real goal.

Show your reasoning only when it helps the user make a better decision.
For routine tasks, just do the work.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents two common failures at once. It reduces blind agreement on high-stakes work, and it reduces tedious over-analysis on low-stakes work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Uncertainty Verifiable
&lt;/h2&gt;

&lt;p&gt;One popular instruction is to ask the AI to label its confidence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Tell me whether you are 70%, 80%, or 90% confident."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That feels rigorous, but it is often theater. Large language models are not reliably calibrated about their own uncertainty. When a model says it is "90% confident," it is not usually reporting a measured probability. It is generating language that resembles how confident people sound.&lt;/p&gt;

&lt;p&gt;A better instruction is not "rate your confidence." It is "show me what your claim rests on."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When making claims:
- Cite specific sources, data points, examples, or reasoning steps
  that support the claim.
- If you cannot point to a concrete basis for a claim, say:
  "I am inferring this from [X], but I have not verified it."
- Never present a guess with the same confidence as a verified fact.
- When confidence is low, state what information would resolve it.
- Do not use numeric confidence scores unless I explicitly ask for them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user becomes the calibration mechanism. You can inspect the evidence, challenge the inference, or decide that the claim is too weak to act on.&lt;/p&gt;

&lt;p&gt;That is far more useful than a fake percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Modes, But Do Not Let Modes Become Escape Hatches
&lt;/h2&gt;

&lt;p&gt;Modes can be useful. Sometimes you want a critic. Sometimes you want a mentor. Sometimes you want execution with minimal commentary.&lt;/p&gt;

&lt;p&gt;But modes create their own failure mode: the user can switch into "execution mode" to avoid valid criticism.&lt;/p&gt;

&lt;p&gt;So define modes with an override:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Modes of operation:

Default mode:
Help directly, but flag meaningful risks or weak assumptions before proceeding.

Critic mode:
Stress-test my idea. Look for flaws, missing evidence, bad incentives,
and hidden costs. Be rigorous but constructive. End with a recommendation.

Mentor mode:
Help me improve my thinking. Ask questions before giving answers when
appropriate. Explain principles and patterns, not just solutions.

Execution mode:
Once we agree on the plan, focus on implementation speed and clarity.

Exception:
If you encounter a risk involving significant harm, data loss, security
exposure, legal risk, or irreversible damage, flag it regardless of mode.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exception is the important part. Otherwise "execution mode" becomes a way to tell the assistant, "Stop protecting me from bad decisions."&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure the Tone, Not Just the Logic
&lt;/h2&gt;

&lt;p&gt;Good pushback fails if it is wrapped in bad tone.&lt;/p&gt;

&lt;p&gt;You do not want an assistant that flatters you. You also do not want one that performs harshness as a substitute for clarity. The goal is direct, calm, practical correction.&lt;/p&gt;

&lt;p&gt;Give the model specific language patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use a direct, calm, and professional tone. Do not flatter me. Do not
over-apologize. If an idea is weak, say so plainly and explain the
practical consequence. Focus on the work, not on me personally.

Avoid:
- "That's a great question!"
- "You're absolutely right..."
- "I'd be happy to help with that!"
- "While there are some concerns..."

Prefer:
- "The main risk is [X]. Here is why it matters: [Y]."
- "This works for [scenario], but breaks under [condition]."
- "I would not do it this way. The better path is [Z] because [reason]."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concrete anti-patterns work better than abstract tone advice. "Be direct" is easy for a model to interpret loosely. "Do not open with empty validation" is harder to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt Is Not the Whole System
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable part: you are configuring the AI to challenge your assumptions using your own assumptions about what good challenge looks like.&lt;/p&gt;

&lt;p&gt;That is circular.&lt;/p&gt;

&lt;p&gt;If your model of useful feedback is flawed, your AI may reinforce that flaw while sounding rigorous. It may push back in ways that feel intelligent but miss the actual issue. It may learn your preferred style of criticism and give you more of that, whether or not it improves the work.&lt;/p&gt;

&lt;p&gt;You need maintenance habits, not just a prompt.&lt;/p&gt;

&lt;p&gt;Test the assistant with known-bad inputs. Give it a plan you know is flawed and see whether it catches the problem. If it does not, your configuration is too loose. If it flags the wrong thing, your configuration is miscalibrated.&lt;/p&gt;

&lt;p&gt;Rotate the configuration periodically. Every few months, look at the mistakes you have actually made. Did the assistant catch them? If not, add conditions that would have helped.&lt;/p&gt;

&lt;p&gt;Compare across models when the decision matters. Different models have different sycophancy profiles. One may fold quickly under user pressure. Another may overcorrect into contrarian noise. Running the same question through multiple systems can reveal what your primary setup is missing.&lt;/p&gt;

&lt;p&gt;Treat the prompt like code. It needs testing against real cases, not admiration in the abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reusable System Prompt
&lt;/h2&gt;

&lt;p&gt;Here is a compact version you can adapt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are my pragmatic coworker and effective mentor.

Your job is to help me reach better outcomes, not to simply agree with me.

Core behavior:
- Be direct, respectful, and concise.
- Challenge weak assumptions, vague goals, and risky plans.
- Do not flatter me or validate ideas automatically.
- If I am wrong or missing something important, say so clearly.
- Explain the practical consequence of the issue.
- Offer a better alternative or next step.

Push back when:
- The request is based on an unsupported assumption that affects the outcome.
- The plan ignores quality, security, cost, ethics, maintainability,
  or user impact.
- There is a simpler, safer, or more effective approach.
- Important context is missing and guessing would be risky.

Do not push back when:
- The task is simple, low-risk, and well specified.
- I explicitly ask for a quick draft or brainstorm and precision is not required.
- The disagreement is about preference, not correctness.
- You would be restating a risk I have already acknowledged.

Calibration:
- Consider reversibility and blast radius before deciding how much to push back.
- For irreversible or high-impact decisions, slow down and flag concerns.
- For low-risk, reversible tasks, help directly with a brief risk note if needed.

Uncertainty:
- Cite evidence or reasoning for claims. If you cannot, say so.
- Never present inference with the same confidence as verified fact.
- When confidence is low, state what information would resolve it.
- Do not self-rate confidence numerically unless I ask.

Modes:
- Default: Help directly, but flag meaningful risks before proceeding.
- Critic: Stress-test the idea. Find flaws, missing evidence, and hidden costs.
  End with a recommendation.
- Mentor: Help me improve my thinking. Explain principles and patterns,
  not just answers.
- Execution: Focus on implementation speed and clarity.
  Exception: Flag any risk involving significant harm, data loss, security
  exposure, legal risk, or irreversible damage regardless of mode.

Output style:
- Lead with the most important point.
- Be specific. Use examples, checklists, or criteria when useful.
- Avoid generic encouragement, filler enthusiasm, and sycophantic preambles.
- Show reasoning only when it helps me make a better decision.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prompt will not make the model perfectly honest, perfectly calibrated, or immune to drift. If you push hard enough, many assistants will still eventually agree with you. In long conversations, persona instructions can weaken. And no prompt can make a model reliably know what it does not know.&lt;/p&gt;

&lt;p&gt;But perfection is not the standard. The standard is whether your assistant becomes less likely to help you walk into avoidable mistakes.&lt;/p&gt;

&lt;p&gt;An AI becomes a yes-man when its highest priority is satisfying your immediate instruction.&lt;/p&gt;

&lt;p&gt;It becomes a coworker when its highest priority is helping you reach the right outcome.&lt;/p&gt;

&lt;p&gt;Ready to transform your AI into a truly pragmatic coworker? I’ve organized these principles into a modular, ready-to-use framework. &lt;/p&gt;

&lt;p&gt;I invite you to explore and use the &lt;a href="https://github.com/ernestohs/ai-calibrated-pushback-skill" rel="noopener noreferrer"&gt;AI Calibrated Pushback Skill repository&lt;/a&gt;, where you’ll find the complete system prompt and implementation guides for ChatGPT, Gemini, and Codex. &lt;/p&gt;

&lt;p&gt;Try it out, fork it to suit your needs, and let’s work together to build AI partners that care more about the right outcome than the easy answer.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>chatgpt</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Engineering Post: Reflecting once, at build time, behind a seam</title>
      <dc:creator>Ernesto Herrera Salinas</dc:creator>
      <pubDate>Wed, 17 Jun 2026 12:04:17 +0000</pubDate>
      <link>https://dev.to/ernestohs/engineering-post-reflecting-once-at-build-time-behind-a-seam-51n</link>
      <guid>https://dev.to/ernestohs/engineering-post-reflecting-once-at-build-time-behind-a-seam-51n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I’m reviving Munchausen, a C# NuGet package I started 9 years ago. This is &lt;strong&gt;part 3&lt;/strong&gt; of an 8-part series documenting both the development process and the engineering decisions behind bringing the project back to life.&lt;/p&gt;

&lt;p&gt;This is the Engineering Post: the reasoning, trade-offs, API decisions, and technical choices behind this part of the project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Munchausen's performance story is "eager Build, cheap Generate." All the expensive discovery, reflection, expression compilation, and inference happen once when you call &lt;code&gt;Build()&lt;/code&gt;. Generating a million objects after that touches no reflection at all. M2 is the first half of that bargain: the metadata layer that inspects a type, exactly once, and caches the result. Implementation is where I discovered how much complexity lay hidden within that simple promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two seams
&lt;/h2&gt;

&lt;p&gt;Everything is built behind two internal interfaces, so a source-generated, AOT-safe implementation can replace the reflection one later without touching anything downstream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IModelMetadataProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ModelMetadata&lt;/span&gt; &lt;span class="nf"&gt;GetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IMemberAccessorFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MemberAccessor&lt;/span&gt; &lt;span class="nf"&gt;CreateAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemberMetadata&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqwo0j2ek6rr9dv5oepx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqwo0j2ek6rr9dv5oepx.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;v1.0 ships one implementation of each: &lt;code&gt;ReflectionModelMetadataProvider&lt;/code&gt; and &lt;code&gt;CompiledExpressionAccessorFactory&lt;/code&gt;. I kept them as interfaces, despite having a single implementation today, because they preserve the option to replace reflection with generated, AOT-safe metadata later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Order is a feature
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ModelMetadata.Members&lt;/code&gt; comes back sorted by &lt;code&gt;MemberInfo.MetadataToken&lt;/code&gt;.&lt;br&gt;
Reflection doesn't guarantee member order, and order is part of the determinism contract; members are populated in &lt;code&gt;MetadataToken&lt;/code&gt; order, which matches source declaration order in practice. The consequence, documented and tested:&lt;br&gt;
&lt;strong&gt;Reordering properties in your model changes seeded output.&lt;/strong&gt; That's by design;&lt;br&gt;
Determinism has to be anchored to &lt;em&gt;something&lt;/em&gt; stable.&lt;/p&gt;
&lt;h2&gt;
  
  
  The nullability matrix
&lt;/h2&gt;

&lt;p&gt;Reading nullable-reference-type annotations is fiddlier than it looks.&lt;br&gt;
&lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt; is easy (&lt;code&gt;Nullable.GetUnderlyingType&lt;/code&gt;), but reference-type&lt;br&gt;
nullability lives in attributes that the compiler emits, surfaced through&lt;br&gt;
&lt;code&gt;System.Reflection.NullabilityInfoContext&lt;/code&gt;. The layer classifies each member as&lt;br&gt;
&lt;code&gt;NonNullable&lt;/code&gt;, &lt;code&gt;Nullable&lt;/code&gt;, or &lt;code&gt;Oblivious&lt;/code&gt; (pre-NRT / &lt;code&gt;#nullable disable&lt;/code&gt;), and the&lt;br&gt;
test matrix covers all of them plus &lt;code&gt;required&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;non-nullable ref / nullable ref / oblivious ref&lt;/li&gt;
&lt;li&gt;value type / &lt;code&gt;Nullable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the C# &lt;code&gt;required&lt;/code&gt; modifier (via &lt;code&gt;RequiredMemberAttribute&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;DataAnnotations &lt;code&gt;[Required]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;IsRequired&lt;/code&gt; is the union of the modifier and the attribute, matching the API's&lt;br&gt;
definition of "required."&lt;/p&gt;
&lt;h2&gt;
  
  
  Init-only is just a modreq
&lt;/h2&gt;

&lt;p&gt;Writability is &lt;code&gt;Writable&lt;/code&gt;, &lt;code&gt;InitOnly&lt;/code&gt;, or &lt;code&gt;ReadOnly&lt;/code&gt;. The interesting case is&lt;br&gt;
init-only: a property with a setter whose return parameter carries the&lt;br&gt;
&lt;code&gt;IsExternalInit&lt;/code&gt; required-custom-modifier. That &lt;code&gt;init&lt;/code&gt; restriction is a&lt;br&gt;
&lt;em&gt;compile-time&lt;/em&gt; C# rule, not a runtime one, which means the accessor can still&lt;br&gt;
invoke the setter after construction. That fact is exactly why the accessor&lt;br&gt;
round-trip tests cover init-only members: the runtime needs to populate them, and&lt;br&gt;
it can.&lt;/p&gt;
&lt;h2&gt;
  
  
  Accessors: compiled, with a fallback
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CompiledExpressionAccessorFactory&lt;/code&gt; builds get/set delegates with&lt;br&gt;
&lt;code&gt;Expression.Compile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// getter: instance =&amp;gt; (object)((TModel)instance).Property&lt;/span&gt;
&lt;span class="c1"&gt;// setter: (instance, value) =&amp;gt; ((TModel)instance).Property = (TProperty)value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Value-type members box/unbox through &lt;code&gt;Expression.Convert&lt;/code&gt;. When&lt;br&gt;
&lt;code&gt;RuntimeFeature.IsDynamicCodeSupported&lt;/code&gt; is false (NativeAOT), it degrades to plain &lt;code&gt;PropertyInfo.GetValue&lt;/code&gt;/&lt;code&gt;SetValue&lt;/code&gt; instead of crashing. The tests exercise both strategies over the same members, including forcing the reflection fallback, so the degraded path is proven, not assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cached, immutable, shared
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ModelMetadata&lt;/code&gt; instances are immutable and cached process-wide in a &lt;code&gt;ConcurrentDictionary&amp;lt;Type, ModelMetadata&amp;gt;&lt;/code&gt;, shared by every definition and the automatic path. I normally avoid process-global mutable state, but this cache stores only immutable facts derived from types. That made it a useful exception rather than a hidden source of behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next: M3, the builder and expression resolver
&lt;/h2&gt;

&lt;p&gt;M2 is internal plumbing; M3 is where Munchausen finally grows a public face.&lt;br&gt;
&lt;code&gt;Lie.Define&amp;lt;T&amp;gt;()&lt;/code&gt;, the fluent builder, the options records, and the&lt;br&gt;
&lt;code&gt;ExpressionMemberResolver&lt;/code&gt; that turns &lt;code&gt;x =&amp;gt; x.Property&lt;/code&gt; into a member reference&lt;br&gt;
while rejecting anything fancier (&lt;code&gt;x =&amp;gt; x.Owner.Name&lt;/code&gt;, method calls, indexers)&lt;br&gt;
with a diagnostic. It's also the first milestone that adds to the locked public&lt;br&gt;
surface, which means the &lt;code&gt;PublicApiAnalyzer&lt;/code&gt; from M0 finally has something to&lt;br&gt;
track.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>nuget</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
