Skip to content

Feature/benchmarks - #47

Draft
multiarc wants to merge 242 commits into
mainfrom
feature/benchmarks
Draft

Feature/benchmarks#47
multiarc wants to merge 242 commits into
mainfrom
feature/benchmarks

Conversation

@multiarc

@multiarc multiarc commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Related issue

How was this tested?

Checklist

  • I have read, understood, and tested this change myself (not unverified tool output).
  • Tests were added/updated and dotnet test src/Heddle.Tests passes locally.
  • My changes follow the project's .editorconfig / surrounding code style.
  • All commits are signed off for the DCO (git commit -s).
  • If AI/code-generation tools were used, I disclose it here and take responsibility for the result.

AI-assistance disclosure (if any)

multiarc and others added 30 commits July 21, 2026 22:26
Phase 1: five new workloads + 20 twins, parity contract v2 gates (N3b),
golden corpus with export-corpus/verify-corpus verbs, five new benchmark
suites. Phases 2-6: Rust (Askama/Tera + Criterion), JVM (JTE/Thymeleaf +
JMH), JS (Handlebars/Eta + mitata), Python (Jinja2/Mako + pyperf), Go
(text/html/template + templ) harnesses under benchmarks/<eco>/ - each
with all 32 controlled+idiomatic cells passing the byte gate/verifier.
Phase 8: linux-crosscheck tooling (validate.py, launchers, env capture);
full gate sweep green on WSL Ubuntu 24.04 at exact toolchain pins.

Measurement/publication runs deferred to a dedicated session; Phase 7
awaits published runs per its go/no-go gate. Errata E1-E3 recorded in
docs/spec/records.md.

Bug fixes related to type resolution

Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Two strands of work, both driven by gaps the 2026-07-25 run exposed.

## The report (complete)

Publish docs/benchmarks/2026-07-25 and withdraw 2026-07-22, whose JS render
figures were invalidated by the ConsString rope defect.

Present workloads in two tiers rather than protocol order. The boundary is the
CLR's 85,000-byte Large Object Heap threshold: .NET strings are UTF-16, so a
page crosses it at 42,500 characters, and past it copy throughput collapses 4.5x
(measured: 40.3 B/ns at 84,800 B, 8.9 B/ns at 108,680 B) with every render
driving a full Gen2 collection. Five workloads sit below it, three above. No
other ecosystem has this cliff. Tier order is DERIVED and owned by
consolidate.py -- no table in a published directory is ordered by hand.

Fix the implied-throughput numerator: it divided by the golden byteLength, which
is the NORMALIZED form, understating composed-page by 1.56x and hiding a cell
above the plausibility ceiling. consolidate.py now emits summary-tables.md as a
second generated artifact, which index.md embeds, so the narrative page
transcribes no measured figure by hand.

## The new .NET harness (W1-W7 of 12)

benchmarks/dotnet/, structured like the other five ecosystems, replacing
src/Heddle.Performance as the cross-stack .NET leg. The old project is still
intact and still builds; it is not retired until the remaining work items land.

- Corpus relocated to benchmarks/dotnet/GoldenCorpus and all six harnesses plus
  consolidate.py repointed. Bytes and SHA-256s unchanged; every gate re-verified.
- Razor now covers ALL EIGHT workloads, up from one. The "1.37x faster than
  Razor" claim previously rested on the single least favourable workload, and
  the gap had to be published as a caveat.
- Idiomatic track added for .NET (51 templates, 6 engines), discharging Phase 1
  D15 by its own recorded revisit trigger.
- All six Heddle render techniques implemented and gated: runtime and
  precompiled backends across string, TextWriter and UTF-8 IBufferWriter sinks.
  Only the runtime UTF-8 sink is wired into the cross-stack sweep -- one engine
  contributes one row -- and it is the fairest row, since the other five
  ecosystems all emit UTF-8/Latin-1 and never cross the LOH boundary.

Three things the harness makes explicit rather than assuming:

- Precompiled coverage is DISCOVERED from the manifest. The generator silently
  leaves uncovered templates un-precompiled and the engine serves them from the
  dynamic path, so an assuming harness would time the runtime backend under the
  precompiled name. Real coverage is 4/8 and the gate prints it.
- Sinks CHECKSUM rather than count. A counting writer proves a length was
  computed, not that characters were produced -- the defect class behind both
  the retired sink suite and the JS rope artifact. Verified by injecting a
  count-only body and watching the differential fail.
- Razor's default HtmlEncoder numeric-escapes non-ASCII, which N5 does not
  canonicalize; it inflated encoded-loop by 125,000 characters. Fixed by
  configuring the encoder (contract v2 D2's preferred remedy), not by widening
  the gate.

Gate: 140 passed, 0 failed. Selftest: 138 passed, 0 failed, including N3b byte
lengths matching the JS reference exactly on all eight workloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolves the duplication documented in docs/research/generator-code-sharing/:
every rule the source generator and the runtime engine maintained as two
hand-kept copies moves into one shared artifact, and the fifteen verified live
drifts are fixed.

Phase 0 (gate) — the suite can no longer pass through an unintended
precompiled→dynamic fallback: FallbackGuard sentinel + Strict policy,
RenderViaResolver in registry-only and file-backed modes, registry isolation
with a leakage canary, a gauntlet-crossing corpus sweep, and a seeded-mismatch
meta-suite whose negative control pins the pre-phase-0 world. Known drifts
landed as quarantined red fixtures handed to their owning phases.

Phase 5 — content hash, key↔path derivation, .heddle extension rule,
schema/engine versioning, option names/defaults. Fixes the hash-input mismatch
(BOM'd/UTF-16 templates were permanently StaleContent), the silent key
fallback, the fabricated engine version, and dead item metadata. Claims
HED7018/HED7019/HED7020; replaces the generator's blanket catch with a
per-template error under the Q2.2 fallback-legitimacy ruling.

Phase 2 — one implementation of every byte-affecting shaping machine. Fixes the
WidenToWholeLine clamp drift (the generator was wrong on all three sub-cases);
DocumentShaper.cs 398→118 lines.

Phase 4 — shared Roslyn-free rule tables for numeric kinds, operator legality,
member visibility, hop form, literal formatting and overload rank. Fixes the
ToString("R") round-trip (a build-machine-dependent value difference) and
unguarded binary emission.

Phase 6 — one diagnostic identity across build tier, run tier and editor:
shared catalog, projection, one line-index rule, one alias table. Fixes
forwarded-warning ID/Fix loss and the line-index \r mismatch; LSP gains full
options parity and its default profile aligns Text→Html.

Phase 3 — one rule-core each for extension identity/discovery, function
exports, prop layouts, assignability, member paths and model type names. Fixes
false HED7006, the nested/generic AQN mismatch (discovery *and* formatting) and
the BranchRole mirror. Claims HED7021/HED7023; fixes the runtime's
order-dependent short-name tie in lockstep per Q3.5.

Phase 1 — emitter↔runtime drift. Fixes the needsLocals scan and per-carrier
flags, unknown-@Profile silent acceptance, and DefaultConvertible gaps. Claims
HED7022/HED7024.

Quarantine register emptied: every red fixture earned green by its owning
phase, none deleted or weakened (phase 1's F11 reshaped, documented in place).
Suite 2630 → 4808 passing, 0 failing, 0 skipped on the legs that run.

Known outstanding items are recorded in docs/generator_plan/README.md
(post-implementation review findings + the compile-channel drain gap).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A review subagent reverted a mutation experiment with `git checkout` while the
whole program was still uncommitted, resetting PrecompiledGauntlet.cs to its
pre-program state. Three pieces were lost; all three are restored here from
their specs and red tests:

- Phase 3 OQ4 — the prop-layout fingerprint check in the extension-binding arm.
  The AQN check proves both sides name the same type, not that it still lays
  its [Prop] slots out the same way; without this a package that re-orders a
  slot renders into the wrong slots. Vacuous when the fingerprint is absent, per
  the additive-schema contract. Specified by PropLayoutFingerprintTests.
- Phase 5 D2 — both staleness path reconstitutions go back through the shared
  TemplateKey.ToPath instead of hand-rolled Path.Combine (behaviourally
  identical; the regression was to the shared-artifact claim, not behaviour).
- Phase 6 WI5 — the gauntlet's HED7101 const points at the shared
  HeddleDiagnosticIds row again, as its sibling in PrecompiledTemplates does.

HashFile's decode-then-hash and the AqnFormatter routing had already been
restored by the reviewer from its own context; both verified correct here.

Suite green: 4808 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WI1-WI10 verified intact against source and the plan's Implementation record.
The hand-restored TemplateKey.ToPath call sites and HashFile body confirmed
faithful by their original author.

One residue found: 'using System.Globalization;' in PrecompiledGauntlet.cs,
whose only consumer was the CultureInfo.InvariantCulture inside the ToHex that
WI1 deleted. The revert restored the using without the code. Removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tring

Tranche A + B verified intact. The hand-restored prop-layout fingerprint check
was behaviourally faithful but wrong in its detail string:

- it broke the file's uniform "<Thing> 'name': manifest=X live=Y" shape; and
- it dropped the <none> sentinel. PropLayout.Fingerprint returns null when the
  live extension declares no [Prop] at all -- the package-removed-them case,
  the most diagnostic one -- and interpolating null rendered 'live=)', which
  reads as a formatting bug rather than a finding.

Root cause was in the original work, not the reconstruction: the test asserted
only Contains("prop layout"), so any message shape passed. The detail is now
pinned in full, and a new test covers the null-live-fingerprint path, which had
no coverage before.

Also reworded phase 3's Implementation record, whose '2 skipped' parenthetical
had gone stale once phase 1 un-skipped the last fixture, and added a
restoration note recording what was destroyed and why the drift was invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The audit ran under two transient API failures; its in-flight work is complete
and verified here.

D12.5 was the adversary reviewer's finding: the plan asserted that 'a host that
drops a channel turns the suite red', but no test compared the LSP drain
against the generator drain -- what shipped was unit tests of the drain rule
plus one single-template check. Now closed. DiagnosticCorpusVectors is one
shared table asserted by all three hosts (runtime, build tier, editor), so they
are compared to each other instead of to three sets of hand-written
expectations that can drift apart in exactly the way phase 6 exists to stop.

Also: catalog row count corrected 80 -> 82 in the README (the plan record had
already been corrected).

BOM investigation closed as a non-finding. Two goldens did lose a UTF-8 BOM
(Example5_DefinitionWithProps, Example7_FunctionShimCall), but the pre-program
baseline was itself inconsistent -- those two carried a BOM and the other six
did not. The program normalised all eight to no-BOM, which is homogeneous and
matches what Verify emits. Recorded rather than reverted.

Suite: 4930 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lse rationale

Done inline: the phase-0 subagent died to four consecutive API 529s without
starting, and the queue should not stall on a server-side outage.

Coverage gates tightened -- both were reported by the verifying reviewer:
- The precompiled-corpus gate asserted '>= 25' against an actual 40, so fifteen
  templates could stop precompiling with nothing going red. Now an exact count,
  with a comment saying a change to it is a deliberate act.
- Five corpus tests carried 'if (dir == null) return;', which turns the whole
  gate into a silent no-op if the build layout ever changes -- zero signal,
  reported as a pass. All five now fail loudly with a message saying how to
  build the corpus.

D7's third seed added. The meta-suite had triples for content hash and
extension AQN but none for the options fingerprint -- gauntlet step 1, the
earliest check, previously unseeded. D7 calls this seed 'wrong fingerprint
arity'; a literal arity change would not compile the manifest, so what is
seeded is a wrong fingerprint *value*, the observable form of the same
condition. Strict throws OptionsMismatch, the sentinel fires, and the unguarded
negative control renders identical bytes.

D4's rationale corrected. It claimed 'the corpus is the union of the feature
templates', and that claim was load-bearing for the whole coverage argument.
It is false: feature suites build templates as inline strings, so ~130 feature
tests never cross the gauntlet. Criterion 2 is likewise restated as what the
suite actually asserts -- exact-count gauntlet coverage over the precompiled
corpus, plus byte parity on the model-less subset in both sub-modes -- rather
than the corpus-wide byte parity it claimed and never had. The residue is
recorded as open, not quietly closed.

Suite: 4936 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…delivered

No restoration damage: DocumentShaping.cs verified byte-faithful to the
pre-program runtime method-by-method, with only the two licensed differences
(the safe ApplyRemove/Replace pair, and comments promoted to doc comments). The
clamp fix is intact across all three sub-cases, and the observer re-hosting
preserves the pre-extraction HED300x ordering exactly.

Pin 7 could not have caught the divergence it was written for. Inside the strip
machine, BranchKind.Participant and Other are extensionally equal -- both do
stripPrev = null -- so no assertion over the working document or the rebased
positions can separate them. Deleting the Participant arm reddens nothing. The
plan's risk row said as much, but the pin was recorded as though it discharged
the risk. The constraint now sits where it actually exists: enum arity pinned so
a backslide to four kinds is a red build, plus a recording observer asserting
that a [ScopeChannel] non-role chain is reported Participant rather than Other,
that no gap crosses it, and that events arrive classified -> gap -> completed
(the ordering every re-hosted diagnostic depends on, previously unpinned at
machine granularity). The residual unpinnable case is stated in the test's own
doc comment.

Nine surviving mutants closed. The landed pins covered the three classification
*classes* but never an *equality*: six boundary comparisons in
ShiftBySkippedTokens, two in ShiftListsAfter's chain-enclosing arm, and the
TrimHiddenRemnantLines already-removed-span guard. Pin 2's 'two comments on one
line' row did not constrain the guard it named. Two other survivors are
provably extensionally equal and are recorded as such rather than 'fixed'.

Q2.1's pin was half-missing: WI8's criterion demands red if *either* side
reintroduces the empty-default-chain skip, but the only test drove the generator
half. The runtime half is now pinned in the pass-order lockstep suite.

WI5's benchmark recorded as undelivered rather than argued. Its done-when
demanded allocation-neutrality 'proven, not argued'; it was never measured and
the omission was disclosed nowhere. A credible paired run needs a mutation held
across a multi-minute BDN run -- the posture that caused the contamination these
audits exist to clean up. The standing argument is now labelled as an argument,
and it corrects WI9's claim: render allocation is unchanged by construction, but
the compile path did gain three allocations per body (one display class, two
capturing delegates) that WI9 glossed as zero.

Suite: 4986 passed, 0 failed, 0 skipped on the legs that run (was 4936).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…le floor

Docs-only for the plan: phase 7 is authored as proposed, not started, because
its territory (src/Heddle.Tests/TestTemplate/) was under concurrent audit.

The principle lands as a standing rule, not just a plan's motivation:
testing-standards.md gains a Test-input single-sourcing section headed "a
duplicate test input is a duplicate rule one level up", with ledger entry E9
recording it as additive and as the mechanism E8's corpus-contribution bullet
had asked for without specifying.

Load-bearing design decisions: a shared MSBuild props file rather than a new
csproj or embedded resources (the contract under test is file-based -- RootPath,
the .heddle rule, HashFile's FileStream+BOM decode); a compile-checked intent
table on two orthogonal axes (Tier x Render) with mandatory justification and
bidirectional completeness gates; and set-equality gating rather than a count,
because a count is still rubber-stampable by editing one digit whereas set
equality can only be made green by naming the file and writing why.

Also tightened here, found by the phase-7 survey and missed by the phase-0
audit: CorpusDifferentialTests asserted a ">= 40" floor against an actual 62, so
a third of the corpus could disappear silently. Same anti-pattern as the
">= 25"-against-40 gate fixed in 45154fd, which is itself the argument for
phase 7's D5 replacing both with set equality.

Suite: 4986 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ants

Note on provenance: part of this audit's work (the TemplateEmitter fold,
GeneratorNumericTableAdoptionTests, and an intermediate LiteralRoundTripTests)
was swept into commit 8f9c1fe by an orchestrator "git add -A" while the audit
was still running. No mutation residue reached any commit -- verified across all
eight. This commit carries the remainder.

The duplicate numeric table is folded. TemplateEmitter.IsImplicitNumericWidening
was a live second copy of the C# 10.2.3 widening table with five call sites and
ZERO test references -- the record's claim that "the lockstep test covers both
existing copies" was false, because that test transcribes the runtime's body
only and phase 1 never adopted the shared table. It now delegates to
NumericTable.IsImplicit via SymbolFacts.ToNumericKind, with the deleted body
kept verbatim as a characterization pin swept exhaustively over SpecialType
squared in three directions. Two record claims corrected in place.

Drift #9's guard strengthened, and its limit stated. Reverting G17/G9 to "R"
reddened exactly one test -- a literal string assertion -- while all four
round-trip legs (60,000 values plus corners) passed under the bug, because "R"
IS shortest-round-trippable on CoreCLR. The guard is now a format-identity
assertion, and the same mutation reddens 23 cases across 5 methods. What stays
unverifiable here is the defect itself: "R" only mis-round-trips under a .NET
Framework host, so G17's sufficiency can only be observed on net48. A revert is
caught everywhere; the Windows check remains the only thing that closes #9.

Four surviving mutants, each a real hole, each now killed and re-mutated dead:
- Cast-pinning deleted entirely reddened nothing. D10's whole mechanism was
  unobserved -- only the ranker's refusals were covered. Harmless for built-ins
  today, but phase 3 routed arbitrary host export signatures through the same
  path, where WI10's 0-of-480 measurement explicitly does not carry.
- Unary and ternary guards deleted together reddened nothing, despite the
  success criteria naming all three arities. Real divergences behind it,
  including a ternary that is CS0173 in the consumer's build.
- The shift row's degrade arm deleted reddened nothing. C# has no <<(int, long),
  so "Count << Big" was CS0019 in the consumer's build precompiled and rendered
  at run time -- precisely the class this phase exists to close, on a row no
  fixture reached.
- The string-+ row promoted to Supported reddened nothing, because every
  fixture partner's ToString happened to match C#'s overload choice.

Also: the OQ3 internal-property criterion rested on a fixture no test used
(Order.Secret was added for it and never referenced), so the criterion was
carried by a unit pin over a Heddle.Tests-internal type -- a different
accessibility situation from generated code in a third assembly. Now covered on
both tiers. And bool & bool? is confirmed contained, unchanged, and pinned for
the first time, with the contrast that bool == bool? does get positioned
HED1008.

Suite: 5108 passed, 0 failed, 0 skipped on the legs that run (was 4986).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Q7.* questions were written into the phase-7 plan and never reached
docs/generator_plan/open-questions.md, which is the authoritative register. That
bookkeeping failure is the reason this section exists.

Sweeps every unresolved question the two post-implementation reviews and the six
phase audits raised, not just phase 7's: Q7.1-Q7.4 (corpus scope, benchmark
convergence, written artifacts, stage-5 boundary) and Q8.1-Q8.8 (the overload-tie
silent degrade against the match principle, the P1 gate mechanism, the two
generator-only shared cores, [ExportExtensions], StripGlobal's hard-coded
assembly name, the compile-channel drain, native-expressions deviation 1, and
net48/net6.0 verification).

Each carries a stated default so work can proceed under a named assumption if a
ruling does not arrive -- the convention the resolved sections already use. The
file header no longer claims everything is resolved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q7.4: all migration stages including 5 land inside phase 7 -- the D4 coverage
residue is closed completely rather than left as a tail; each stage keeps its own
byte-neutral gate so the open-ended scope is bounded by per-stage acceptance.

Q8.1: the overload-tie silent degrade becomes a build error, HED7025. The
generator must not stay silent about an illegality it has already proved.
Accepted consequence: a project with an ambiguous overload call that builds green
today starts failing -- the same posture as phase 5's emitter fault and phase 3's
HED7021. The side condition is mandatory (report only when no argument estimate
is Unknown), and phase 4's fixture must stop pinning the silence.

Q8.2: raise MinSupportedSchemaVersion to 4. The break ships as declared at
version 2.1 with no compatibility shim, but the gate must reject cleanly rather
than advertise a support window the metadata cannot honour -- and the rejection
must be demonstrated by a manifest fixture built at the old schema, not asserted.

Q8.3: fold the runtime onto ExtensionRegistrationRules and TypeSpelling.
Acceptance is that mutating each shared rule reddens at least one RUNTIME test --
the property whose absence made them transcriptions rather than sources of truth.

Q8.4: model [ExportExtensions] in generator discovery. Acceptance requires a
fixture using an extension in an assembly without the attribute, since no test
uses such an assembly today.

Q8.5: fix StripGlobal's hard-coded assembly name, TDD -- the reproducing fixture
lands red first, because the defect's whole character is that no fixture reaches
it.

Unruled and standing at their defaults: Q7.1 (editor templates stay separate),
Q7.2 (props serves Heddle.Performance; template sets do not converge), Q7.3
(delete the written artifacts), Q8.6 (compile-channel drain stays unscheduled),
Q8.7 (native-expressions deviation 1 fixed in a docs pass), Q8.8 (drift #9 stays
unclosed until the Windows net48 run).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q7.1: keep the editor-tier templates separate. No EditorOnly tier; the intent
table's three-value Tier axis stands.

Q7.2: leave Heddle.Performance alone entirely -- a benchmark effort is mid-flight
there. This overrides the plan's default, which had the shared props file serving
it so the path-traversal helper would die everywhere. It now serves the four test
projects only, and the benchmark project keeps its traversal helper. Recorded as
ACCEPTED RESIDUE in both the register and the plan's non-goals, explicitly so a
later reader does not mistake it for an oversight and 'helpfully' fix it: the
failure class D2 eliminates is gone from the test suites and survives in the
benchmark project until the benchmark work settles.

Q7.3: relocate the six written artifacts rather than delete them. Also overrides
the recorded default. WI4's done-when now requires the artifacts still to exist at
their new location, not merely to be absent from the glob.

With Q7.4 already ruled, phase 7's open-questions section is fully resolved and
now points at the register as authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q8.7's ruling, widened from one wrong sentence to a planned sweep. Docs-only;
proposed, not started.

The survey found the anchor defect was not alone. native-expressions.md carries
FOUR false normative claims, not one -- and because the authority convention
points at that document first, each is a latent bug rather than a typo:
- user-defined operators are documented as honored; they are honored for
  arithmetic/relational/equality/coalesce and NOT AT ALL for bitwise, shifts or
  any unary, where FailBinary/FailUnary fire before the factory;
- the shift row documents an int right operand, but any integral is accepted and
  converted, so a long-shift expression compiles here and is CS0019 in C# -- the
  doc UNDERSTATES, so matching code to it would break working templates;
- lifted operands are documented as covering relational AND equality exactly as
  C#, but lifting is numeric-path only.

The sharpest inversion: the tree's most accurate description of
native-expression semantics is NativeOperatorRules.cs, whose comments document
every divergence correctly beside the verdicts that encode them -- while the
document the authority convention designates is the least accurate. Hence D3
narrows the convention: a normative doc outranks code only for claims that are
gated or dated-verified; an ungated claim is evidence of intent, not authority.

Diagnostics are the clearest split. All 24 HED70xx are triple-gated and
drift-free across three phases, while 20 of 82 shipped IDs appear in no
published document and HED1xxx -- whose registry-designated owner IS
native-expressions.md -- is 2 of 17. HED1008, the error the anchor defect is
about, is documented nowhere.

Stale citations are not where anyone expected: all 830 file-and-line references
sit in generator_plan and research (86 past EOF), and the published docs and
specs carry zero. So class S is scoped as a gate only, never a hand-fix.

Three code defects escalated rather than fixed, per D2 (the sweep corrects
documents, never code): the id-less nullable-bool bitwise error, floor of an int
reaching HED1013, and a shipped sample still passing the removed Name item
metadata.

Version sweep assigned to Q8.2, not here: 2.1 IS the declaration of the binary
break and must ship atomically with MinSupportedSchemaVersion = 4 (verified
still 1) and the old-schema fixture. The surface is 13 must-change files plus 5
coupled, not nine, and the riskiest is the VS Code extension's PINNED_VERSION,
outside any check today.

Four new open questions recorded with defaults: Q8.9 (convention placement and
retroactivity), Q8.10 (phase 7 slip), Q8.11 (centralise the version elements),
Q8.12 (the sample's stale metadata).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sions]

Both ruled by the user after the post-implementation reviews. Phase 1's audit is
editing this tree concurrently; its files are deliberately excluded here and land
separately.

Q8.3 -- the two "shared" cores had no runtime caller, so they were transcriptions
rather than sources of truth. Baseline reproduced before fixing: mutating
ExtensionRegistrationRules.Resolve's Conflict verdict killed only the direct table
assertion and no behavioural runtime test, exactly as the reviewer reported.

Now TemplateFactory.AddExtensions resolves every name collision through Resolve
and switches on the verdict, LoadExtensions sorts by OrderingKey, and
ReflectionHelper.ResolveType drives TypeSpelling.TryResolve through a new
reflection ITypeLookup adapter -- deleting its duplicate ExtractGenericArguments,
TryFindMatchingAngleBracket, SplitTopLevelArguments, ResolveGenericType,
ResolveArrayType and the tuple regex (-185 lines). ResolveSimpleType stays
deliberately: the assembly-scan index, the dot-to-plus retry ladder and the
ambiguity rule are the reflection tier's own universe, not grammar.

Acceptance was that mutating each shared rule must redden a RUNTIME test. Nine
mutations, nine runtime deaths, all reverted -- table in the plan record. The new
ExtensionRegistrationPrecedenceTests assert the three verdicts through the public
AddExtensions seam; their absence is why the transcription was invisible.
ExportBookkeeping had no test at all and now has nine, placed in Heddle.Tests on
purpose so mutating it reddens a runtime leg.

The fold surfaced two real divergences, both fixed in the shared file so the tiers
move together: a one-element tuple is legal and reflection has always resolved it,
so the parser's two-element floor was a build-tier-only refusal the extraction
itself introduced; and a whitespace-padded top-level spelling now resolves on the
run tier where its own dispatch threw.

Q8.4 -- the generator scanned all referenced assemblies while the runtime only
scans those carrying [ExportExtensions], so it precompiled extensions the live
registry can never resolve. ExtensionBinder.CollectExported now reproduces
TemplateFactory.ObtainExtensions: the engine assembly whole and unconditional, any
other assembly only what the attribute names or the whole assembly for the
parameterless All form (short-circuiting the rest exactly as the runtime's break
does), and nothing without the attribute. Nested-container discovery survives on
both paths -- asserted, because Q8.4 must not undo F1.

TDD: the fixture went red first and for the right reason -- exactly the three
narrowing assertions failed while the eight must-still-work assertions already
passed.

No rendered byte changes and nothing that renders today stops rendering: a
bound-but-unexported extension produced a manifest row the gauntlet already
rejected on every request, so the dynamic tier was already serving it. The change
removes dead precompiled output and a permanent per-request fallback. Two verdict
changes: a bodied call to an unexported name is now HED7006 at Error, converting a
first-render failure into a build failure; a bodiless call degrades quietly with
identical bytes.

Two fixture debts fell out and were paid, both making an implicit declaration
explicit: probe compilations declare their exports through a single-sourced
harness helper rather than copying the attribute into ten probe strings (testing
standards, test-input single-sourcing), and the integration suite declared nine
extension types its own tests exercise and never exported.

Neither fix is window-gated; both judgements are recorded in breaking-windows.md.
Q8.4 is defect repair under the match principle -- no correct build regresses.
Q8.3's two items are widenings: one repairs build-tier drift, the other widens
accepted input with no resolved type changing, and depending on ResolveType
throwing for padded input is not a dependency the contract offers.

The phase-3 record's B4 row is corrected: TypeSpelling was a re-implementation at
landing, not the split it claimed.

Suite: 5206 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…closed

24 mutants, 24 killed, 0 survivors. The record's factual claims all hold; what did
not hold was the coverage two of them implied.

BodyModelRules had no Release enforcement -- the emitter's only link to it was a
Debug.Assert, and the table's Chained column had NO consumer anywhere. The emitter
now DERIVES each nested body's build context from the table row, so it is
load-bearing in Release, and the tautological adoption theory (which asserted the
table against its own InlineData and never ran the emitter) is replaced by tests
that run the generator and read the consequence off emitted source. Byte-neutral.

Two tests pinned method NAMES, so a re-implementation under another name passed.
Replaced with pins on the INPUTS a re-implementation must read -- proven by adding
a renamed leftmost-only probe and watching it go red.

The participant-scan lockstep was 6 hand-picked rows where the matrix promised a
whole-corpus sweep; it now sweeps all 62 corpus templates, pins by name the six
that provision a frame as an anti-vacuity check, and carries an empty named
over-provision allow-list. Exact agreement on all 62 today.

The test matrix's six "corpus guardrail entries" had zero fixture files, and the
claim was wrong twice over: CorpusDifferentialTests has no fixture set of its own,
and none of the six shapes was ever added to the corpus it sweeps. Corrected rather
than backfilled -- each guardrail now names the differential or lockstep asset that
actually pins it (rendered bytes on both tiers, a stronger gate than the corpus's
classification pin), and three of the five are stated as unable to be corpus files
at all. Eleven further rows naming never-created files now name what exists. Root
cause was a naming class of error, now stated at the top of the matrix: the
integration suite holds no .heddle files, so a "fixture" there is an inline string
keyed by a views/<stem>.heddle path.

Q8.5 is closed as phase 1's own D14 artifact: StripGlobal is deleted and the branch
arm's manifest row takes Info.BareTypeName/Info.AssemblyName. Byte-neutral, and
honestly labelled -- the defect is unreachable by test because the arm gates on
IsEngineAssembly, so the guard is a source-shape pin rather than behavioural
coverage.

WI7's actual fix had no test at all; the new CallTargetAdoptionTests exports a
function named raw from a synthetic compilation and requires the emitter to bind
the extension and emit nothing into the container. Swapping the classifier's arms
reddens it.

The F11 fixture is narrowed honestly: clause 3 was a verbatim duplicate of a
lockstep test and is dropped; clause 2 was decorative (leftmost participants the
old buggy probe already handled) and is replaced by the observable half of the same
drift, the per-carrier flag asymmetry. Reverting the flag OR now reddens this
fixture, not only its sibling suite.

Five mutants were killed only by rule-level tests with every differential and
golden suite green. Recorded in the plan because it locates blind spots rather than
closing them: no fixture inherits out:: from a base layer, no fixture has a
foreign-origin region-fill candidate, and NO extension anywhere carries
[EncodeOutput] and [NotEncode] together.

Q1.2 confirmed: no present coercion-rail mismatch. Both additive APIs are in the
public-API golden and now genuinely exercised -- BindDefinitionOverloadTests is the
first test of the "existing overloads unchanged, both flags equal implies identical
binding" claim, which the golden only covered at signature level.

Suite: 5208 passed, 0 failed, 0 skipped on the legs that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q8.13 the value-path coercion rail has no byte-level fixture (shape plus
render-path only); Q8.14 RenderTypeRules' fourth truth-table row is unreachable
from any real extension, so [EncodeOutput]+[NotEncode] is either meaningful and
needs a fixture or incoherent and should be an error on both tiers; Q8.15 two
generator tests fail intermittently under concurrent multi-TFM runs, which matters
because every mutation result in these audits rested on a test going red for the
right reason; Q8.16 RegionTests.LocationOffsetOf returns a hard-coded 0, so it
reads as a position assertion while asserting nothing; Q8.17 SymbolTypeIndex.Cache
pins every Compilation for the process lifetime.

Each carries a stated default. Q8.15's default is to stabilise before further
mutation work, on the grounds that a flaky gate is a broken gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty of twenty-one post-implementation questions are now ruled. Only Q8.6 (the
compile-channel drain) stands at its default, deliberately.

The substantive correction is Q8.12. The register recorded Q5.1 as "Name removed
per the recommendation" and phase 5 implemented that removal. The user's
instruction is that removal was never the ask -- only wiring Precompile was. So
Name is being restored as real optional custom key mapping. Name was dead code
(CompilerVisibleItemMetadata, never read), so removing it changed no behaviour and
the sample's Name="BuildReport" was always ignored; the defect was that the
feature was never wired, not that the metadata existed. Restoring it is the
smaller fix and closes Q8.12 as a side effect.

Q8.10 rejects phase 8's D9 as designed: documentation has a different job from a
test fixture, and byte-identity with a corpus entry is not a property worth buying.
No @include: from corpus templates, no corpus intent rows for doc examples, and
phase 8 no longer blocks on phase 7. The false doc/test coupling that already
exists gets deleted rather than formalised.

Q8.14 opens a genuinely new surface: a declaration-side analyzer, so an extension
author sees an incoherent [EncodeOutput]+[NotEncode] pairing in their OWN build
(warning, HED7027) rather than only their consumers seeing it in theirs (error,
HED7026). That is Q6.1's early-surfacing principle applied to authorship.

Q8.15 forbids the easy answer explicitly: a retry attribute is not an acceptable
resolution for the two flaky generator tests, because it preserves exactly the
property that makes a surviving mutant invisible -- and every mutation result in
these audits rested on a test going red for the right reason.

Q8.17 asks for an operational contract rather than a size cap: a real operation
API, observable capacity, and eviction in which AGE participates, motivated by the
edit-and-edit-back case where a cached entry is identical yet old.

Q8.11 adds strong-naming for our own unsigned assemblies alongside centralised
versioning, so the CS8002 warnings stop; third-party unsigned references are
handled explicitly rather than blanket-suppressed. Q8.9 puts the documentation
mapping into cross-cutting-decisions. Q8.8 and Q8.13/Q8.16 take their defaults or
straightforward yes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four ruled items plus one build-order fix, landed by three concurrent agents on
disjoint file sets. Committed together because they share the diagnostics
registry and docs surfaces, and splitting those by hunk would risk a broken
intermediate commit.

THE MOST IMPORTANT FINDING IS NOT ANY OF THE RULED ITEMS.

Wiring Name revealed that NONE of the three HeddleTemplate item metadata worked
from a real project. Heddle.Generator.targets restated each one as
<Key>%(HeddleTemplate.Key)</Key> inside an Include="@(HeddleTemplate)" transform.
The transform already copies every metadatum, and outside a target a cross-item
%() reference evaluates to empty -- so each element OVERWROTE the copied value
with "". Key was inert. Precompile="false" was inert. Nothing noticed because
every test suite injects build_metadata.* directly and never crosses that file.

That means phase 5's Q5.1 deliverable -- "wire Precompile properly" -- did not
work end to end, and its acceptance tests could not have shown it. The user's
instinct that Name should never have been removed was right for a deeper reason
than the one recorded: the feature was not broken because the metadata existed,
it was broken because the targets file nulled it. Fixed by deleting the
restatements, gated structurally by a set-equality test that also refuses any
future restatement, and behaviourally by the sample.

Q8.12 -- Name restored as optional custom key mapping, implemented as a second
spelling of Key so it shares every downstream rule rather than acquiring parallel
ones. Full participation in HED7002 duplicate and HED7003 case-only-twin checks;
HED7018 out-of-root deliberately SUPPRESSED by an explicit Key or Name, because
that warning's premise is that the flattened key was not asked for. HED7028 was
NOT claimed: both new faults are instances of HED7004's existing fault class at
the same severity, position and remediation, so its message was generalised
instead. A second fix was needed to keep the sample's golden to one line: emitted
#line directives named the registration key, so an explicit Name pointed every
mapped span at a path that exists nowhere. #line now names the root-relative
path -- byte-identical where no explicit key is set, which is why no Verify
snapshot moved.

Q8.2 -- MinSupportedSchemaVersion = 4, and the rejection is DEMONSTRATED. The
fixture compiles a manifest against a reference facade declaring the
pre-schema-4 surface under the real assembly's identity, public-signed, with the
real Heddle excluded from both compilations' reference sets -- so the IL genuinely
carries a MemberRef to .ctor(string, string) that cannot bind to the current
3-arg form. That is exactly what the old test could not be: writing
new PrecompiledExtensionBinding("a","b") binds to the 3-arg ctor and passes null,
producing a new-schema call wearing an old-schema shape. Three arms, including a
control that admits the SAME BYTES at schema Min and observes the
MissingMethodException -- so "the gate prevents a startup crash" is evidence
rather than narration, and 4 is shown to be the exact boundary.

Q8.11 -- one VersionPrefix (2.1.0) replacing nine per-project elements, plus
fourteen kinds of version statement now gated by 17 cases. Two things the survey
had missed: LspServer.InformationalVersion was a live drift ("1.0.0" reported by
heddle-lsp --version for the whole 2.0 line, guarded only by a test comparing it
against itself), and the beta job's --version-suffix had to lose its leading dash
or compose 2.1.0--beta.N. All CS8002 warnings gone: Demo.Models, Demo.Wasm and --
found by the new gate rather than the warning -- LanguageServices.Tests.Corpus,
whose csproj already carried a comment claiming it was signed. Scriban is handled
by a declared accepted-unsigned list, with the honest bound recorded: Roslyn has
no per-reference CS8002 suppression, measured, so the mechanism is keyed on the
reference rather than scoped to it.

Q8.1 -- HED7025, the overload-ambiguity build error. BindOutcome is propagated
out of both binders instead of collapsing to null, and the side condition is
implemented as an early return placed BEFORE OverloadRank.Bind runs, so no
verdict is ever computed for untypeable arguments and then suppressed. Three
tests pin the silent-degrade half. One residue found and pinned: the name-keyed
model answers IsReferenceAssignable false by construction, which was harmless
while it could only degrade and is sound now only because no shipped built-in
parameter type is a reference type other than String/Object -- so a test pins
that parameter-type set. The quarantine fixture is reshaped a second time, and
this reshape REVERSES an assertion the first added: phase 4 pinned the build
tier's silence as though silence were the fix.

Q8.16 -- RegionTests.LocationOffsetOf no longer returns a hard-coded 0. It
recomputes the anchor from what the diagnostic reports, asserting the right file
and that line/column agrees with the raw span. Mutation-proven: span shift, line
drift and wrong-file all redden it. A second decorative assertion in the same
fixture (Assert.NotNull after an Assert.Single that already guaranteed it) was
replaced with a real both-tier offset twin.

Q8.20 -- the missing build-order edge. CorpusResolverSweepTests reads
src/Heddle.Tests/bin/** with no ProjectReference ordering it, which was
reproduced as a real failure. Added with ReferenceOutputAssembly="false" --
build order only, deliberately not a compile-time reference, because that suite
hands hand-filtered reference sets to compilations it creates and loads the DLL
by path at run time.

Mutation testing across all three items: 27 mutants, 26 killed, 1 survivor
classified as provably extensionally equal (a null-coalesce whose two orderings
are reachable only when the operands are Ordinal-equal). The load-bearing mutant
is the one that turns the old-schema fixture INTO the optional-parameter
substitution -- it reddens the suite, which is what proves the fixture is not
that substitution.

Breaking-window dispositions recorded for Q8.2, Q8.11 and Q8.12, plus a 2.1
as-shipped record. Q8.2's is the interesting one: the break ALREADY SHIPPED in
2.0.0, because schema 4's optional parameter removed the 2-arg ctor then. What
2.1 changes is only whether the engine says so or crashes.

Build: 0 errors, 0 CS8002 (was 8). Suite: 5312 passed, 0 failed, 0 skipped on
the legs that run. All 10 samples run and compare-golden clean, with only
codegen-t4-successor's golden changed and reviewed line by line. The eight Verify
snapshots differ by exactly the engineVersion line. net6.0 and net48 remain
unrun on this box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Promoted from the phase-5 plan's implementation record to the README findings,
because it invalidates a DELIVERED acceptance claim rather than an unverified one.

Heddle.Generator.targets restated each HeddleTemplate metadatum inside an
Include="@(HeddleTemplate)" transform, and outside a target a cross-item %()
reference evaluates to empty -- so each element overwrote the copied value with
"". Key, Precompile and Name were all inert. Phase 5's Q5.1 deliverable did not
work end to end, and its acceptance tests could not have shown it: every suite
injects build_metadata.* directly and no test crosses the targets file.

Also reframes Q8.12 -- the record said Name was dead code and removal was
harmless, which was true as far as it went, but all three were dead because of
the targets file, not because the metadata existed.

Standing lesson recorded: a build-surface contract verified only through injected
analyzer-config values is unverified. Where a claim depends on MSBuild
evaluation, something must actually evaluate MSBuild -- which is why the fix is
gated both structurally and through the sample gallery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q8.18 dissolves: verified in source that precompiled function calls are
statically bound (NativeExpressionWriter emits a direct shim call from a
build-time table; PrecompiledRuntime never consults options.Functions at
render). So the build-time inventory is the only scope that CAN be correct for
what precompiles, and a host adding overloads is handled by the gauntlet
degrading to the dynamic tier. The escape hatch was verified real:
Precompile="false" continues before key derivation and emit.

Q8.24 closed as invalid -- precompilation shipped in 2.0.0, so no 1.x manifest
has ever existed and the reason-code change has no population to affect. Also
recorded plainly: no option was added for manifest rejection.
PrecompiledMismatchPolicy is pre-existing 2.0 API and phase 0 depends on Strict;
the entry read as though proposing a switch, which it was not.

Q8.25 is a CORRECTION to work landed in daf7f91. Name was implemented as a
second spelling of Key -- an override -- so a named template stopped resolving
by its path. It must be ADDITIVE: the path-derived key is kept and the
registered name is added, so imports by either spelling resolve, with a warning
(HED7028) where a named template is imported by path. This also invalidates the
HED7018 suppression recorded under Q8.12, which must be re-derived rather than
assumed to carry over.

Q8.19 collect all errors if contained; stop and report the cost if it needs the
body walk restructured. Q8.26 leave as is, no TreatWarningsAsErrors. Q8.27 prefer
absolute paths and mark relativity where relative is genuinely right, rather than
converting everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Corrects the Name implementation landed in daf7f91, which made Name "a second
spelling of Key" -- an override, so a named template stopped resolving by its
path and drew HED7011. The ruling is that Name is an additional name register for
imports, not an override: nothing that resolved before may stop resolving.

Additivity is now structural rather than conditional. The import map is built in
two passes and the ORDER is the mechanism: pass 1 registers every template's key,
pass 2 adds each Name alias on top, skipping any spelling already present. A
registered name therefore cannot displace a key. Name was removed from DeriveKey
entirely and a new DeriveName owns it.

TDD: ANamedTemplateIsStillImportableByItsPath was written first and reddened with
exactly the HED7011 the defect produces. BothSpellingsResolveInOneCompilation then
pins that both resolve in one run, so an implementation that merely swapped which
spelling works cannot pass. Resolution is asserted by the imported definition body
turning up inlined in the importer's pieces -- @<< pulls in definitions, not text,
so "no HED7011" alone would also be satisfied by silently dropping the import.

HED7028 (Warning) advises where an import resolves through the key of a template
that has a registered name. A new id was right here where landing 1 was right to
decline one: every other HED70xx key diagnostic reports something UNUSABLE, and
this reports something that works, at a different severity and position.

Four of six Q8.12 decisions changed once re-derived off the additive premise
rather than adjusted:
- HED7018 out-of-root: Key only. An additive Name leaves the path-derived key in
  place and still out of root, so the warning is still about something real.
- HED7002 duplicate and HED7003 case-only twin: keys only. A name registers no
  manifest row and is never a runtime registry lookup.
- Key+Name is no longer a conflict at all -- two names for one template. That
  HED7004 arm is deleted; the name arms that remain cover the two ways a name is
  genuinely unusable.
The #line key/file separation stands, and the sample changed (below).

The breaking-window disposition was corrected IN PLACE rather than appended to, so
the normative document does not carry two dispositions: the rename clause now
applies to Key alone. Name moves nothing -- not the key, the manifest row, the
class name or #line -- so it cannot break a call site or stop an import resolving.

Q8.27: absolute where it costs nothing, relative-and-labelled where it does.
Outside HeddleTemplateRoot there is no anchor and the old fallback was the bare
filename -- unopenable and colliding across directories -- so that now emits the
template's own absolute path, at zero snapshot cost (the affected snapshots use
synthetic relative paths and gained the views/ prefix they should always have
had). Under the root the form stays root-relative, because absolute would bake
this machine's layout into the sample golden and any rooted snapshot. Every
generated file now states which form its #line directives use, directly under the
auto-generated marker.

The sample now demonstrates Name the way it actually works. Name="BuildReport" on
report.heddle became decorative once the key survived, so the entry class reverts
to Templates_Report and a new import-only partial carries Precompile="false"
Name="Banner", imported as @<<{{Banner}}. Rendered output is byte-identical. This
also repairs a gate that had silently died: finding 10's behavioural half was
"Name renames the class Program.cs calls", which stopped gating anything the
moment Name went additive. Proven by deleting Name="Banner" and watching the real
MSBuild build fail with HED7011.

Mutation testing: 16 mutants, 15 killed, 1 survivor classified extensionally
equal with proof (an early-out whose absence routes through the collision guard to
the same observable state). Restoring the override kills 12 tests. One real gap
was found by mutation and closed: dropping the HED7028 per-import dedup survived,
because no test imported one spelling twice.

Harness note recorded for whoever mutation-tests next: rsync -a preserves mtimes,
so a reverted source can look older than the mutant's build output and MSBuild
skips the rebuild. That produced one false failure, traced and fixed by touching
after revert.

Suite: 5348 passed, 0 failed, 0 skipped on the legs that run. All 10 samples run
and compare-golden clean. README intro refreshed to the real count and to the
HED7018-HED7028 range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Q8.28/Q8.29 are one defect from either end: an opted-out template is still a
participant in the import graph, so its key and name must be derived, validated
and advised even though it contributes no entry point or manifest entry. The
population whose faults are currently unreportable is exactly the population the
Name feature is for.

Q8.30 REVERSES the import-only scope the Q8.25 landing assumed. The reasoning
recorded: if Name is a useful key for imports it is a useful key full stop, so a
named template should resolve by that name at run time too, and the asymmetry was
an artifact of the wiring rather than a designed boundary. The manifest carries
the name, the runtime registry consults it, and the assertion asked for is of
that behaviour -- not, as the question originally proposed, that a runtime name
lookup misses.

Q8.31 moves the #line path-form choice from prose in generated code into the
manifest as machine-readable data, and deletes the comment.

Q8.30 and Q8.31 both add manifest fields, so they share one schema bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lidation

Completes the work checkpointed in d1fa89f. Suite green again: 5414 passed,
0 failed, 0 skipped -- the one red test that WIP commit recorded is reconciled.

Q8.30 -- PrecompiledTemplateInfo.RegisteredName, and the runtime resolves by it.
The generator emits only a name that actually registered at build time, so both
tiers hold the same spellings. PrecompiledTemplates gains a second index and
TryGet consults keys then names, so TryResolve, TemplateResolver and
PrecompiledRuntime all get name search without their own changes.

KEYS WIN, always, independent of registration order. Three grounds: additivity
(an addition that displaced an existing spelling IS the override Q8.25 corrected);
the match principle (the build-time import map is already keys-first, so the tiers
cannot disagree about what a spelling means); and determinism (one merged
dictionary would make the winner depend on which assembly loaded first, which is
the host's business, not ours). Mechanically that is two indexes plus a
disjointness invariant enforced from BOTH directions -- a name whose spelling a key
owns is refused at insert, and a key arriving later evicts the name shadowing it.
Both directions are needed because either can happen first across assemblies.

The cross-assembly collision gets a RUNTIME id, HED7104
(PrecompiledFallbackReason.RegisteredNameUnavailable), because the build tier
structurally cannot see it: a referenced manifest's rows live in a GetTemplates
method body -- IL, not symbol metadata. It deliberately does not join the
duplicate-key throw, since two templates claiming one key is unresolvable whereas a
name/key collision is already resolved by the ordering rule.

Q8.31 -- LinePathForm on the manifest as an enum rather than a bool, because a
fallback-marker row has no generated source and must be able to claim nothing
(Unspecified). The enum file is linked into the generator so a rename breaks its
compile instead of emitting a manifest that will not compile downstream. The prose
comment is gone from generated code, asserted together with the field's presence so
an implementation leaving two carriers cannot pass.

Q8.28/Q8.29 -- key and name derivation moved ahead of the Precompile gate, so a
malformed Key, a malformed Name or a taken Name now reports HED7004 on the item
that is actually wrong rather than surfacing as HED7011 at some importer. Opted-out
items parse in advisory-only mode so their own imports draw HED7028. HED7029 was
not needed: every fault had a home. A "name is build-time-only" warning was
declined because it would fire on the feature's intended shape. Opted-out items
still contribute no entry point and no manifest entry -- pinned, and verified end to
end from the real sample csproj.

Schema stays at Min = Max = Current = 3. The 11-arg PrecompiledTemplateInfo ctor is
kept as a REAL overload rather than defaulted parameters, deliberately not
repeating the mistake that caused the whole schema episode; the public-API diff
confirms it survives.

Mutation testing: 17 run, 16 killed. M6 was a real gap -- checking names against
pre-registration keys survived, because the name landed in the index violating
disjointness and the lookup still returned the key owner (two redundant guards
masking each other). Fixed by asserting the HED7104 report, not just the
resolution. The surviving mutant is proved extensionally equal against the code:
one insertion site each, byKey never removes, and pass 2 only inserts spellings
absent from the complete staged key set, so at most one dictionary holds any
spelling and lookup order cannot matter.

Q8.2's breaking-window disposition is REPLACED, not softened -- its ground (a),
that the break already shipped in 2.0.0, is withdrawn outright. Q8.30 is recorded
honestly as a widening taken anyway: scoped to opt-in data no 2.0 project can have
(the metadata was inert until 2.1), nothing that resolved before stops resolving
structurally, and the shadowing hazard is reported rather than silent.

Five new questions recorded, the sharpest being that RegisteredName has no gauntlet
coverage (every other manifest field is validated at request time) and that
Min == Max makes the support window a point, so every future manifest change
becomes a whole-assembly rejection for the previous version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
multiarc and others added 30 commits August 9, 2026 14:21
… not one precompiled byte moves

The emitter now consults the probe for a body-hosting call's roles, with BodyModelRules behind it. The
probe wins where it has an answer, because it is the extension speaking rather than a prediction about
it — a distinction the table itself earned, having carried a wrong @list chained column for as long as
it existed. The table is not deleted: it is the answer for a build that is not probing, which is every
build by default, and for an extension whose assembly no immutable root holds. Both orders of
preference agree on every name the table names, and that is asserted rather than assumed.

The gate, run before any coverage was claimed from it. Every row BodyModelRules pins is reproduced by
the reflection driver across the assembly boundary, so the reading survives the one thing the build tier
adds over the engine-side driver. And the whole 65-template corpus is generated twice, with the property
on and off, and compared byte for byte: nothing moved, and the manifest — which carries every binding
row, so a changed render type or bound type shows there even where an entry class did not — is
identical. The declared-movement list is empty and the test names it, so the first template the probe
recovers has to be declared rather than absorbed.

The gate also asserts it is not vacuous: this compilation's engine reference really does yield a live
probe, and that probe really does answer for @list. A byte comparison against a build where probing
never engaged would pass for the wrong reason.

Riding along, the fix that must not arrive after the arms collapse: AllocateBodyExtension hard-coded
RenderType.Raw. Invisible for @if/@for/@list, which carry neither encoding attribute — and a silent XSS
the moment an [EncodeOutput] extension hosts a body through that allocator, which is exactly what the
collapse makes possible. The render type is now derived from the bound extension's own symbols, as every
other bind on this side already did, and the type name comes from the binder rather than from a literal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tarts reading what their hooks do

The engine branch trio, @list, @for and the generic custom path were four dispatch arms that differed
in two things — the body's model role, and @list's element-type ambiguity check — and agreed in
everything else, down to calling the same allocator. The role is an answer now, so they are one arm.
214 lines out, 128 in, and the emitter no longer compares a name against "list", "for", or a branch
role it had to be told about.

The step-back list goes with them. ExtensionBinder carried four type names asserting "this hook is the
step-back shape and nothing more"; nine built-ins share that hook body verbatim, so five of them —
@Guid, @time, @Date, @int, @money — were refused for no reason but the list's length. All nine are now
rows in the shared table, and the table is not a prediction any more: the engine-side lockstep suite
holds every row equal to what the extension's own InitStart does, which is what makes it safe to
consult when nothing can be probed. Two corpus rows move on that alone, with probing off:
ctx-encoding-bodied as planned, and partial.heddle, whose Why blamed a @partial call site it does not
contain — its blocker was @money(Cost){{@(Locale)}} all along.

The security fix earns its test here. @attr and @string carry [EncodeOutput], and with the allocator's
hard-coded RenderType.Raw restored the new test fails exactly as predicted: the precompiled tier
renders a&b<c> where the engine renders a&amp;b&lt;c&gt;. Nothing could have caught that before,
because no encoding extension could host a body.

And the third-party case the whole design exists for, proven end to end: @Bellow(){{loud}}, a bodied
call to an extension in a REFERENCED assembly with no attribute, no declaration and no name the build
knows, precompiles under probing and renders the same bytes as the engine. It is the shadow gate's one
declared byte movement; everything else in the corpus is still identical with the property on and off,
manifest included. Beside it, the boundary: an extension whose hook the probe reads perfectly well and
whose answer is a role the emitter cannot emit degrades either way. Observing a hook is not a licence
to emit one.

The gate that keeps this from growing back reads the generator's own sources: no Heddle.Extensions type
literal, no compare against the engine's assembly name, no == against any name the engine registers —
except an allowlist of six name compares and three type literals, each carrying what it is and why.
@out and @partial emit their own shapes rather than binding an instance; the unnamed carrier is chosen
by the output profile, not by a name in the template; and the four directive names survive only as the
zero-output fallback for an engine reference older than [ZeroOutput], which v2.0.0 is. Set equality
with counts, so the list can only grow by someone writing down what they added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…en from what the code now does

Stage 14, documentation and spec reconciliation only. No behaviour changes; the code
edits are stale doc comments and one stale diagnostic message.

docs/precompilation.md gains the page the whole effort was for: what precompiles now
(embedded C# as a compiled fragment, computed @partial names, rendered ref structs,
hidden members through the engine's accessor, late-bound functions, bodied step-back
encoders, bodied third-party extensions, model-type routing), where the boundary
genuinely still runs, and the per-category HED7031 table generated from the
RefusalCategory enum. HED7015's row stops saying "unevaluable" — not-read is the
condition, and probing is what reads it. The diagnostic table loses a blank line that
had split it in two and puts HED7030/7031/7032 back in order.

The same correction reaches the catalogue message, which now names
HeddleProbeExtensionHooks as the remedy, and the emitter's definition-override comment,
which pointed at a rationale that was not in the file.

Records: the registry gains a HED7014 row for its narrowed obstacle and says HED7031
carries its class as a diagnostic property; the 2.1 window records the widening as
additive with the fingerprint constructor named as the trap it did not spring; the
findings register gains the probe's one blind spot — a hook that mutates compile state
is unobserved, and what makes that safe is the population, not a check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nothing can take it away with the probe

The fix that made AllocateBodyExtension derive its RenderType from the extension's own
[EncodeOutput]/[NotEncode] instead of hard-coding Raw landed inside the probe-wiring commit,
tangled with a signature change. The probe is going away; the fix must not go with it.

So the property gets its own guard, and the guard touches nothing the probe owns: @string is an
[EncodeOutput] AbstractHtmlExtension whose body typing the shared table already carries, so the
call site precompiles in a default build. Two assertions, because either alone can pass by
accident — the rendered bytes on both tiers with the tier pinned, and the emitted Bind call read
back as source.

Rehearsed red: restoring the hard-coded Raw makes the render assertion report the divergence it
exists to catch (dynamic "a&amp;b&lt;c&gt;d", precompiled "a&b<c>d") and the source assertion
report the missing RenderType.Encode. Restoring the derived literal makes both green again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ow gated by naming the class

The floors were meant to notice a suite going quiet and could not. They drifted: 17aef41 added six
tests, raised dotnet.yml and left lsp.yml, so the Release-only leg tolerated a six-test regression in
silence for four commits. They name nothing when they redden, they are satisfied by editing a digit,
and they fail a leg for the wrong reason the moment a test is legitimately quarantined.

So dotnet-test-guarded.sh loses its <minimum> parameter, --minimum-expected-tests and the exit-9 arm,
across all nine call sites. Exit 8 stays — a filter matching nothing is a different concept and the
real discovery guard. In their place the script passes --fail-skips on, once, where it belongs: the
option exists under this MTP version (xunit.v3 3.2.2) and the repo carries no Skip= attribute today,
so it costs nothing on landing.

What replaces the floors is a membership gate, not a size gate: src/<Suite>/test-classes.txt, one
line per fact-declaring class, ordinal-sorted, asserted by set equality against Assembly.GetTypes()
inside the suite it describes. A red gate reads "In the assembly but NOT checked in (1):
Heddle.Tool.Tests.HeddleToolTests" — the review artifact a count was reaching for and structurally
could not produce. It is honest about its limit: the unit is the class, so a single [Fact] deleted
from a class that still exists goes unseen.

The confirmed-red protocol moves from Skip to [Fact(Explicit = true)], because the platform reports
an explicit test as not run rather than skipped and so it survives --fail-skips while an unplanned
skip does not. And CorpusIntent.DeclaredRowCount goes, in the one file whose header is a three-
paragraph argument against counts: membership is set-equality gated against the on-disk list and
duplicates already throw by name out of ByName, so the count was pinning nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssembly, and its ledger triples

Three independent things were wrong with the agnosticism gate, all of them silent.

It walked the wrong files. `Heddle.Generator.Common.props` compiles the whole of
`src/Heddle/Language/**` plus twenty-odd more links INTO `Heddle.Generator.dll`, so a
directory walk over `src/Heddle.Generator/` saw 37 of the assembly's 120 source files —
and the sixteen-name table in `BodyModelRules`, the largest name table in the build, sat
in the 83 it could not see.

It used the wrong matcher. `TheGeneratorComparesNothingAgainstTheEngineAssemblyName`
asserted `DoesNotMatch(@"[!=]=\s*""Heddle""")` while `ExtensionBinder.BuildInfo` writes
`string.Equals(assemblyName, "Heddle", Ordinal)` — no `==`, so the test was green while
the code did exactly what it claims to ban, and its doc comment described a
`SymbolEqualityComparer` implementation that never existed. That comment is deleted, not
paraphrased.

It knew the wrong vocabulary. It compared against the names the ENGINE registers, so
hardcoding a third-party extension name passed every assertion the class made — the
failure its own doc comment says it exists to prevent. Confirmed by running the old gate
with `name == "acmewidget"` and `case "list":` added to the emitter: eight tests, zero
failures.

So the props file now writes `@(Compile)` — repo-root-relative, ordinal-sorted, through
an inline task, hooked ahead of `AssignTargetPaths` because a resource added after
`_GenerateCompileInputs` never reaches csc — and embeds it in the assembly. Not the PDB
document table: documents are emitted per sequence point, so a file of constants with no
method body can be absent, and a hole in the enumerator is worse than no enumerator. The
manifest is asserted on its own terms: large, every path on disk, and a superset of the
directory walk it replaces, so a target that stops running reddens instead of making
everything below it vacuous.

Over that file set the gate collects, by Roslyn syntax and with no vocabulary at all,
every identifier-shaped literal in a name-ish position — `==`/`!=`, `case`, a constant
pattern, an `Equals`/`Contains`/`TryGetValue`/`StartsWith` argument, an initializer
element, an element-access key, a `const string`, a default parameter value — and compares
the set to a declared ledger of `(file, value, form, count, why)`. The form is part of the
key, so rewriting `string.Equals(x, "Heddle", Ordinal)` as `x == "Heddle"` is a different
row rather than the same one. Two more gates join it: engine-extension types are searched
for under the namespaces the engine actually declares `[ExtensionName]` types in, computed
rather than spelled; and every static string-keyed table in the built assembly is read BY
VALUE and checked against the built-in names, which covers the linked shared types and
survives any rename or move. A table that cannot be read fails with its name rather than
being skipped.

The ledger lands at 111 rows against the old six, because it inspects three times the
files and every form a name can be written in. Each row says what becomes of it:
`permanent`, or the stage that deletes it — Stage 3 for the three `"Heddle"` assembly-name
compares, Stage 4 for the probe's `param` bootstrap, Stage 5 for `IsDirectiveName` and the
`out`/`partial` dispatch, Stage 6 for `BodyModelRules`. The ninety-four `HED####` constants
are held by a declared shape rather than a row each; anything in that file which is not an
ID still needs one.

Red-gate proofs, each reverted after: `name == "acmewidget"` names the literal and the
file; `assemblyName == "Heddle"` reddens as a form change with both rows printed;
`case "list":` reddens as a `case` row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he one that declares AbstractExtension

Three sites asked "is this assembly called Heddle?" when the question they
meant was "is this the assembly that declares AbstractExtension?". The two
answers differ under ILMerge, under an extern alias and under a rename, and
only the second one is true in all three.

The binder's IsEngineAssembly now compares the containing assembly symbol
against the one it already resolved AbstractExtension out of, rather than
comparing that assembly's name against a literal. ResolveEngineVersion, which
picks the reference whose version becomes the manifest's engineVersion and
therefore what HED7019 reports, asks the same question through the same new
ExtensionBinder.EngineAssemblyOf. And RecordExtensionBinding loses the
assembly = "Heddle" default parameter that was stamping a spelled name into
every manifest row written by the two callers that omitted it; both now pass
the binder's computed engine assembly name.

The property itself stays: TemplateEmitter still reads IsEngineAssembly to
decide whether HED7015 fires and whether the body-typing table may be
consulted. What changes here is only how it is computed.

engineVersion and every binding row's assembly column come out byte-identical
— no Verify snapshot moved. Three rows leave the agnosticism ledger, which
now stands at 108.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… as a rendering test

The probe compiled three fabricated documents against four sentinel types to
ask a referenced extension what its InitStart does with a body. It was off by
default, it discarded two of the three answers it computed, it hardcoded
@PARAM's semantics in a template string, and its flagship end-to-end proof
only ever passed through a test-host loader exemption that a real dotnet build
cannot take. Running the extension's own hook in the consumer's assembly, at
type-init, answers the same question without any of that, so the probe is
deleted rather than repaired.

Out: src/Heddle.Generator/Probe/, HookProbeProtocol, the four test files, the
HeddleProbeExtensionHooks property in every place it was stated (props, the
shared options table, ConfigReader, GlobalConfig, the contract assertions), and
the NuGetPackageFolders plumbing that existed only to bound where the loader
could load from — 1,941 lines of deleted files and 170 more from the sites that
fed them. HeddleProbeExtensionHooks and the three HeddleBuildOptions members it
needed were added and removed inside the still-open 2.1 window and never
appeared in a shipped assembly, verified against the v2.0.0 tag; the three
public-API lines come out of the golden deliberately.

TryHookRoles keeps its second arm alone: an engine extension is typed by
BodyModelRules, everything else degrades under HED7015 as before. The table
itself stays until the build can observe a real compile.

The probe's one genuine finding survives it. @list's chained channel is
Int32Index, not None — ListExtension.InitStart hands the body compile an int
and scope.Model(item, index) puts the index there — and that is pinned by
BodyModelRuleTableTests, which RENDERS the difference. The fact is a one-off
with a test and earns no register row; the class behind it does, and joins
"tests that cannot fail": a table column no production arm reads is not a
contract, because a wrong value in it costs nothing until something reads it.

Docs lose what the deletion falsifies — the whole "Hook probing (opt-in)"
section, its options-table row, the two capability rows that promised it, four
boundary rows collapsed into the one that is now true, and the paragraphs in
custom-extensions.md and editor-support.md. The wider coverage story is Stage
7's.

Six more rows leave the agnosticism ledger: 102.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l hook without compiling its body

AbstractExtension.InitSubTemplate is the only door to a body compile, so a one-shot thread-static
supply in front of it lets a generated call site run the extension's own InitStart for real and
hand it a prebuilt strategy where the engine would have compiled one. The supply reproduces all
three of InitSubTemplate's post-states field for field, records the typing the hook handed the body
compile, and refuses a second consumption.

PrecompiledRuntime.Init and InitDefinition are the entry points generated code will call in the
next stage. Init reproduces InitializeTemplate against a synthesized InitContext/CompileScope/
ParseContext built from PrecompiledInitSite, then drains the delayed-template queue the way the
engine does; InitDefinition runs the two hooks CreateExtension runs, positioning both carriers at
the definition's declaration as CompileFromDefenition does. Neither throws for anything but a null
argument, because the manifest touches strategy: and all of this runs at registration: a hook that
throws or an extension that will not construct costs the call site and gets a substitute that
compiles the call's own source text at first render, while reported compile errors or a typing
answer that contradicts the build cost the template.

Nothing emits against this yet. No emitter change, no generated output change, no golden moved.

InitSynthesisFidelityTests compares the synthesized post-state against the engine's over every
built-in the engine registers, plus both definition carriers, plus the surface ledger over
InitContext/CompileScope/CompileContext/ParseContext and a source gate on the SourceItem read
closure the witness call item answers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d calls precompile in a default build

A call site to a bound extension no longer carries what the build guessed the extension's
compile-time hook would do. It carries a PrecompiledInitSite — position, source text, the three
ExType arguments, model/root/slot types, profile, expression mode, namespaces, call shape — and
PrecompiledRuntime.Init constructs the extension inside the consumer's assembly and runs its real
InitStart against it, supplying the already-generated body instead of compiling one. Every such
field is typed AbstractExtension, so a call the seam could not serve answers with its substitute and
costs no per-render branch.

Two rows of the corpus move to Precompiles as a result, and they are the ones the whole program was
about: a bodied call to a third-party extension whose hook re-types its body, and a call to one
whose hook the build had never read. Neither needs a property, a probe or a name list.

Where the hook chooses a model type the build cannot resolve, the body is emitted with NO model cast
and everything model-dependent in it registers on the site instead: a member read becomes a
PrecompiledLateAccessor bound to the engine's own member walk once the hook has answered, and a
nested call becomes a dependent site whose dataType that same answer fills in — so nested typing
cascades rather than standing beside its parent. One field read and one delegate call per render;
nothing here allocates. Three shapes still cannot be written without that type and take a
per-call-site substitute rather than the template's tier: a computed native expression, whose result
type depends on an operand type that does not exist yet and whose DLR promotion is not the engine's;
an embedded C# expression, whose model parameter has to be spelled; and a nested call needing the
same answer. A body reaching for an enclosing definition, an ambient fill scope or an active prop
layout is refused whole instead — a fragment compiled as its own document would see none of the
three, and the substitute says so in its own doc comment.

[PrecompileUnsupported] is the extension author's way to decline the seam, read off the symbol at
build time and off the LIVE type at registration, so a package that adds it after a consumer has
built still falls back. HED7033 carries the declared reason verbatim and costs one call site.
HED7015 is retired in place: no build raises it any more, the id stays claimed, and its registry and
published rows say why.

Retired with it: the four-name directive list behind zero-output classification, which made a custom
zero-output extension diverge silently and now reads [ZeroOutput] and nothing else; the emitter's
own model/using/profile spellings, moved to an engine-owned table as the parse-level language
keywords they are; the four EmptyExtension/EmptyHtmlExtension type literals, replaced by the binder's
answer for the registry name the shared profile rule computes; and DerivedRenderTypeLiteral, because
the live hook derives it now and there is no literal left to be wrong. The agnosticism ledger loses
eleven rows and gains three.

Init's answer reaches the manifest: a template-scope fault — compile errors the hook reported, or a
typing answer contradicting what the build assumed — travels on the entry's InitSites and the
gauntlet turns it into a per-request fallback under its recorded reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rice is measured rather than asserted

The three shapes a body typed by an unread hook cannot carry each get a case that pins where the cost
lands. A computed native expression costs its own call site and nothing else — the template keeps its
entry, the neighbouring call keeps its bind, and both tiers render the same bytes. The substitute's
own stated bound gets the other case: a body that needs the substitute AND names an enclosing
definition is refused whole, under HookBehavior rather than under the inner construct's category,
because what costs the template there is the bound and not the expression that reached it. Both
halves are needed to get there, which is the test's point: a definition call a type-agnostic body can
emit outright is no problem at all.

The per-render price is measured in the differential style the accessor escapes already use: the same
bodied call to the same extension, one body reading through the root — which the build knows, so it
stays typed — and one reading the model the hook chooses, through PrecompiledLateAccessor. Same bytes
out, and the delta over two thousand renders admits no per-render byte.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and the engine-type ledger empties

@out was the one call the build constructed by name and configured by hand: PrecompiledRuntime.BindOut
bypassed InitStart entirely and the emitter re-derived slot mode, the has-value test, the render type
and the locals flag beside it, with nothing in CI diffing that copy against OutExtension. Adding
[EncodeOutput] to OutExtension would have rendered unencoded on this tier and no test would have said
so.

It now goes through the same Init every other bound extension goes through. The extension reads the
active slot parameter type off the compile context the site rebuilds and decides slot mode itself,
raises its own five diagnostics, and derives its own render type from its own attributes. The
build-time shape checks stay where they are — they are the engine's compile-time refusals reproduced
early, and they still keep a template the engine will not compile off this tier — but nothing about
what @out *is* is written down here twice any more.

The type it binds comes from the binder, under the name the call uses. With the unnamed carrier
already bound the same way, the engine-extension-type ledger is now empty and asserted empty: there is
no built-in whose type the generator spells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e position the engine gives them

A definition call built two carriers by hand and told them what to be: slot mode from the build's own
reading of the header, the recursion limit from a constant baked out of HeddleMaxRecursionCount, a
hard-coded Raw render type. It now runs the engine's own CreateExtension sequence — two InitStart
runs, the outer over the caller content under the definition's slot type and the inner over the
definition body under its own — so all three come from DefinitionBaseExtension itself. SetMaxRecursion
is no longer emitted by anything.

With it comes a fix that had to move a coordinate. CompileFromDefenition positions both carriers at the
definition's DECLARATION, and that is what a fault or a slot-less @out is reported against on the
dynamic tier; the build wrote the CALL's position instead, so the two tiers gave different coordinates
for the same fault and the precompiled one was a coordinate the engine never produces. The site now
carries both and InitDefinition uses the declaration's, as the engine does. No golden moved, because
nothing pinned the coordinate — so a test pins it now, on the two values being different for one call.

Two snapshots move for the emission shape alone: the definition site and its InitDefinition call
replace the BindDefinition line, and every site gains the definition slot-type row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the times it cannot

HeddleObserveEngine is tri-state — Off, Auto (the default), Strict — and threads the way every
other option does: declared in the props, read by ConfigReader through the shared name constant,
carried on GlobalConfig, and gated by PipelineContractTests' set equality in both directions. It
decides which TYPING a body is emitted with and never a rendered byte, so it is not a fingerprint
input and sits beside HeddleNodeFallback rather than beside the profile.

HeddleObserveIntermediatePath comes with it and is assigned in the targets rather than the props,
because $(IntermediateOutputPath) does not exist at props-evaluation time. The same targets file
creates the directory and empties it on Clean: RS1035 bans System.IO.Directory in an analyzer, so
the generator can open a FileStream in that directory but can never make one. The files are
deliberately kept out of @(FileWrites) — a locked file in the Clean list fails Clean on Windows,
and an accumulating cache under obj is the cheaper of the two problems.

HED7034 is claimed for the case the switch exists to govern: the build could not observe, so a body
whose model type only a hook can supply is emitted type-agnostically. It is a note under Auto and
an error under Strict, one id with two shapes, the way a forwarded diagnostic already has two. Not
a warning in either mode, because the template still precompiles and renders identical bytes —
HED7031 reports a template leaving the tier and this reports nothing of the sort.

That third severity is the one registry change here. HeddleDiagnosticSeverity was deliberately
two-valued because a Heddle compile diagnostic is an error or a warning; Info exists for the single
row that describes the BUILD rather than a template, and its own doc comment said adding a level
would be a registry change rather than a table change. It is.

Nothing consumes the option yet. The public surface grows by ObserveMode and five members on
HeddleBuildOptions, and the golden is regenerated deliberately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ep the first run's output

These suites take minutes each, and the single most expensive habit available on them is piping a
run through tail or grep as its only sink and then re-running it to recover a detail the first run
already contained. The rule now says to tee every run to a file and read that file, to redirect per
suite so the runs stay separable, and to prefer one combined pass over four invocations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… file could not put it

$(IntermediateOutputPath) is defined by the SDK's own targets, so a project that imports this file
by hand rather than through a package — which is every sample in this repository — evaluated it as
empty and put the cache directory in the SOURCE tree, where nothing cleans it and the default glob
would eventually find it. The path now falls back to $(BaseIntermediateOutputPath) and then to
obj/, so it lands under obj either way.

It stays an evaluation-time property rather than moving into a target, and that is the constraint
that rules out the obvious fix: a CompilerVisibleProperty reaches a generator only through the
editor-config file the compile writes, and that file is written from the evaluated value. A path
resolved inside a target is a path the generator is never told about, which was measured directly —
the property arrived at the generator empty and observation reported that it had nowhere to write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ne compile of the real template

The compilation being built is emitted to a real .dll under the intermediate output path, at a
filename that is a digest of its own content, and loaded as a bundle with the consumer's own engine,
every extension assembly and whatever the referenced closure needs. Then the real template is
compiled through the real engine — ParserSettings, DocumentParser.Parse, TemplateOptions with
ProvideLanguageFeatures, CompileContext, CompileScope, HeddleCompiler.Compile, the way
DocumentAnalyzer already drives it — and the model type the engine chose for a body is read off its
own scope map and emitted as a direct cast. An extension declared in the compilation being built now
types its own body, which was structurally impossible before an intermediate assembly existed.

Content addressing is the whole safety argument. LoadFrom holds a file open for the compiler
server's life, which is why build outputs were refused outright before; a changed input is a
different filename, so the permanent lock is always on a file nothing will ever rewrite, and a test
reads every path this process loaded back and asserts each one is named after its own bytes. Nothing
is loaded from where the build wrote it either — every bundle member is first copied to such a path
and the copy is what opens.

It is an optimisation and only that. It is consulted where the build has no typing of its own, so it
can only ever ADD a typed body: the corpus is generated with observation off and on and rendered
through both, and the bytes are identical; the refusal categories with it on are a subset of the
ones with it off; and no template changes tier. Where it cannot answer, the body is still
precompiled through the engine's own zero-allocation accessors.

Nothing about it enters the incremental pipeline — no Assembly, no Type and no harvest is ever a
provider payload — and it is not built until an emitter meets a body it cannot type. Building it is
a whole C# compile and the generator runs on every change to the compilation, so an unconditional
one would put that compile on an editor's keystroke path for every consumer, including the many with
nothing to gain from it. A build that never asks emits nothing, loads nothing, and cannot fail.

Two things it learned from being run rather than reasoned about. The resolve handler is
process-wide, so it answers only for names the bundle itself loaded and never with a version behind
the one requested — the first draft preferred any loaded assembly of the same simple name and
thereby made an assembly-qualified @model spelling bind to a version the runtime refuses, which a
suite caught. And a span's answer is used only when every scope-map entry for it agrees, because a
definition body compiles once per call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he body-typing table is a floor

The sixteen keys of the engine's body-typing table, the two name-keyed tables that hold them, and
the @out and @partial name dispatch were all marked as retiring in this stage. They are marked
permanent instead, because the thing that was to replace them cannot reach every build.

Observation needs the IMPLEMENTATION of the engine and of every extension assembly. A package
reference names one; a project-to-project reference hands the compiler a reference assembly, which
carries no method bodies and cannot be executed, so there is no InitStart to run. That case is now
detected off the symbol and named in HED7034 rather than surfacing as a bare load failure. It means
observation is structurally unavailable to a whole class of consumer, and for those builds the table
is what types an @if, an @list or a step-back encoder's body.

Removing it was measured rather than argued about: with the table's lookup disabled, thirty-five
test methods went red across a hundred and twenty cases — corpus templates leaving the precompiled
tier outright, embedded C# inside a branch body degrading, region and slot element typing losing
their engine-parity build checks, and the monotonicity gate itself. A floor that only sometimes has
something above it is still a floor.

@out and @partial have no structural classifier to move to. @out is the slot-channel keyword a def
header already reserves, and this ledger already calls it a grammar keyword twice elsewhere.
@partial evaluates its own name body at compile time to resolve a template key, which no attribute
declares and no third party could be recognised by. Both say so, and neither names a stage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… resolved the layer each call site sees

The emitter refused every `<name:name>` on a belief about itself — that it
resolved definitions flatly, always to the most-derived layer, so an override
calling its own name would recurse. It never resolved them. It asks
`resolutionCtx.GetDefenition(name)`, the same `ParseContext` lookup
`HeddleCompiler.CompileItem` makes, and the parser has already put the layer
each call site sees in that context: `EnterSubtemplate` isolates a full
override's body at its declaration, deep-copying the layer underneath, so a
call to the name inside an override reaches the BASE — measured, not assumed —
while every other context holds the most-derived one. Definitions are
materialized where they are used and precompilation starts from the top-level
template, so the layer arrives resolved and emits as an ordinary definition
call, with its own body, its own props layout and its own declaration position.

Deleting the guard is the whole change. `GetOrBuildDefinitionBody` already keys
emitted bodies by parse-context identity and pre-marks the entry before
populating it, so a self-call terminates at emit; `PropLayout` and `SlotRules`
already walk `BaseDefinition`; the recursion counter is the engine's own carrier
on both tiers. A genuinely circular pair — a base layer whose body calls its
name, reached through an override whose body calls its name — stops at
`MaxRecursionCount` with the same exception and the same message on both tiers,
which `DefinitionLayeringTests` renders rather than reasons about.

`DefinitionLayeringTests` pins ten shapes byte-for-byte with the tier declared:
the plain override, a call reaching the layer below, three layers each reaching
one step down, props declared on two layers, a narrowed `:: T` over an `object`
base, caller content spliced per layer, an override declared inside another
definition's body, two call sites sharing one layer, a slot definition layered
over, and a default output chain under a layer. Name-differing inheritance is
the control: it resolves the other way, through the live document table, and
that asymmetry is now a test rather than a surprise. The eleventh case is the
only layering shape neither tier serves and the front end owns it — a default
output chain declared ON a full override is a parse error both tiers report.

Three corpus rows move to `Precompiles` (`empty-override`, `vc-test`,
`regr-import-multiline-override` — an imported base layers like any other), and
`def-layering.heddle` joins them as the model-less document that carries the
shapes through the sweep every run. Two `RegionTests` cases that declared the
sibling-override idiom un-precompilable now assert parity instead, and the
import-only fragment's importer precompiles.

`RefusalCategory.DefinitionLayering` is retired in place — declared, reached by
nothing, the way `ClrWall` is — because the degrade-expectation seam pins these
members by name. The one site that borrowed it for an unrelated last-resort (a
definition body the shared piece walk abandoned without a reason) says so now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d a row that earns it

A template that gives up one call site and a template that gives up nothing were the same
row in the manifest and therefore the same row in the intent table, which meant the corpus
reported this program's headline capability as indistinguishable from full precompilation.
CorpusTier.PrecompilesWithSiteFallback names it: a bound entry class AND at least one
recorded per-call-site fallback, read off the generated source, because the manifest cannot
see the difference. Gated by set equality like the other four.

No existing row qualified — verified, not assumed: the gate ran green against an empty
declared set before the fixture existed — so ext-site-fallback.heddle is added rather than
moved. It calls @scanner, whose [PrecompileUnsupported] declaration is its author's own
statement that a hook walking the enclosing document cannot be reproduced from a static
initializer, so the build writes a SiteFallback for that call, reports HED7033 quoting the
declared sentence, and emits the rest of the document normally. Model-less and standalone,
so every run byte-compares what the substitute renders against the dynamic tier.

Both precompiling tiers carry a bound entry class, so the render, sweep and observation
gates read CorpusIntentRow.Bound rather than one enum member — splitting the population
must not quietly narrow the gates to the rows that happen to give nothing up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and the ones that lied stop

RefusalCategory.HookBehavior claimed to name InitStart/CompleteInit overrides, bodied custom
extensions and missing body model-typing rows. None of the three is a refusal any more. Its
three real sites are the per-call-site substitute's own bound — a type-agnostic body that
reaches for an enclosing definition, an ambient fill scope or an active prop layout, none of
which a fragment compiled as its own document would see — plus a call shape such a body
cannot carry, so that is what the member now says.

Enumerated from source rather than sampled, and the count is what the ledger records: fifteen
of the seventeen have live sites, and exactly two have none. DefinitionLayering was retired in
place by the layering work; ClrWall has always been declared and unreached, and now says so on
itself rather than only in DefinitionLayering's doc. EmbeddedCSharp and NativeExpression gained
the type-agnostic-body arm they acquired without saying so — in both cases the arm costs a call
site, not the template, which is the distinction the enum's own summary now draws.

Comments that described a build which no longer exists go with them: the emitter's arm still
said an unread hook degrades under HED7015, the generator's still called HED7015 the emitter's
warning channel, BodyModelRules.TryGet still explained that a missing row is "exactly why the
emitter refuses to precompile a bodied custom call", and the two fixtures still documented
themselves as degrade cases while their own tests assert they precompile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… extensions instead of guessing

The single most user-visible outcome of this program had no home in the docs: custom and
third-party extensions precompile in a default build — bodied, hook-overriding, [Prop]-declaring,
branch-role — with no opt-in and no name list. precompilation.md now opens its coverage section
on that, and replaces "a template either precompiles whole or renders through the dynamic path
whole", which stopped being true the moment a call site could fall back on its own. There are two
sizes of fallback now and the page says which is which. The type-agnostic body and its
zero-allocation accessor path, the substitute's stated bound, and HeddleObserveEngine get their
own treatment: three modes, and two honest limits — a project-to-project reference hands the
compiler a reference assembly with no method bodies, so there is no hook to run (HED7034), and
each distinct source state leaves one LoadFrom-locked intermediate assembly on disk until Clean.
custom-extensions.md loses the leaked role taxonomy — "two roles are emittable today" was
generator vocabulary and is now false besides — and says plainly what an extension author owes.

HED7015 is retired in place. Its registry row now records what it reported, that it shipped as an
Error and was relaxed to a Warning inside this same window, that it is unreachable because the
build no longer reasons about a hook at all, and that NOTHING user-visible replaced it —
[PrecompileUnsupported] is an author's choice, not a fault, so HED7033 is a neighbour and not a
successor. The constant, the catalog row, the registry row and the published mention all stay,
because DiagnosticIdTests reads all four and because an id once shipped is never renumbered; that
rule is now stated in CLAUDE.md and .claude/rules/errors-diagnostics.md rather than only known.

HeddleNodeFallback was audited against the code rather than the plan's prediction of it. The
property is read at exactly two sites, both on the typed path, so =false still restores the
whole-template degrade for an unnameable member and does NOT take a type-agnostic body off the
tier — it is a naming switch, and inside such a body nothing could have been named. Recorded as a
dated amendment note with the test that pins it, and stated in the option's own row.

Also: breaking-windows gains the program's row, verified against the public-API golden — additive
throughout, one real constructor on PrecompiledTemplateInfo rather than an optional parameter, no
schema increment, three removals that row 11 already records as added and removed inside this same
window. shared-source-architecture describes the binding seam as what it is, a contract in emitted
text between two sides that cannot reference each other. The findings register loses one row: the
reason under "@out(::X) never type-checked" tested false — BuildParamExpr emits root-reference
call parameters now, and CallSiteValueType has an arm that types them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…declares, not names the compiler knows

The build tier reached two of its routes by asking whether the call was
spelled "out" or "partial". A third-party extension that projects the
enclosing definition's slot, or that resolves and hosts a template its own
body names, was served by neither — for no reason but what it was called,
and both compares sat in the agnosticism ledger with a rationale claiming
they were permanent.

They are roles, and roles are declared. [SlotProjection] and
[ChildTemplateHost] join [BranchRole]/[ScopeChannel] in the same shape:
compile-time only, Inherited = true, read off the bound type by the binder
and surfaced on Info beside HasScopeChannel. OutExtension and
PartialExtension carry them; the emitter dispatches on them; the two ledger
rows are gone and the walk finds no name left in the emitter to allow.

Both roles on one type is two incompatible call shapes — a projection takes
no body, a host's body is its child's name — so the emitter refuses rather
than picking whichever it tested first. A chain, a fill and a definition
already return before the roles are read, so a role only ever answers for a
call the extension tier owns. Against an engine that predates the
attributes both reads answer false and the call takes the ordinary
bound-extension route.

The private-slot-walk gate counted "HasSlot" as a substring, so
Info.HasSlotProjection read to it as three new walks; it counts whole
identifiers now, which is what it was always claiming to count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… template becomes a supply, not a copy

The precompiled tier never constructed PartialExtension. It emitted a normalized TemplateKey, a
PrecompiledPartialName field for a computed name, and a LazyInitializer-memoised ResolvePartial per
call site — and that copy was not byte-neutral: its Render called Generate on the child and pushed the
resulting string through the caller's renderer, materialising output the real RenderData streams.

The call now takes the ordinary bound-extension route. PrecompiledRuntime.Init constructs the
extension and runs its real InitStart/CompleteInit at static init, so the name is evaluated by the
hook itself against Scope.Null, the child is queued on the engine's own delayed-template queue, and
the ImportOrigin marking is the extension's. A new one-shot seam, PrecompiledChildSupply, sits in
front of HeddleTemplate.Compile(CompileContext) — the engine's only door to a named compile, as
InitSubTemplate is to a body — armed only for the drain of a site the build marked
HostsChildTemplate. It supplies a child that binds once on first use: the registry entry when the
named template is precompiled, the extension's own compile under the request's options when it is
not. Two structural facts keep that binding off static init, and both are stated where the seam is:
Register materialises every entry's strategy before it publishes the snapshot, so a parent asking for
its own assembly's sibling at type-init always misses; and the dynamic arm needs the request's
RootPath, which no build can carry.

Computed names fall out of it. The role declares its own body typing — the attribute already obliges
the extension to compile both its body and its child against the call value — so the name body is
emitted typed and verified against the hook's answer at registration, and BodyModelSource.Data gains
the emission TryPartialNameBodyContext already was.

The site's position was the chain's span, which indexes the shaped working document; it is now the
engine's own item position, which is what the engine puts on the extension, on the witness source
item and on a fault raised while compiling the item. Two snapshots move by that line. A fragment the
per-call-site substitute compiles now reports its errors at the call rather than at an offset into a
document that exists nowhere.

Measured, 64 KiB child, bytes per render: a registry child into a writer sink 393,472 -> 32; into a
string 818,976 -> 425,536; a disk-compiled child into a string 1,094,360 -> 425,536. What is left is
the caller's own result.

ResolvePartial's three overloads, EvaluatePartialName and PrecompiledPartialName are retired in
place, not deleted: generated code from earlier generator versions calls them, and removing a member
an already-built assembly's IL names faults that assembly at registration. Removal is a next-window
candidate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s them, and the sixth stays because a byte says so

`BuildOutCall` was the last role-dispatched arm that predicted what an extension's own
`InitStart` would do. It read `SlotRules.HasOutValue`, refused a bodied projection, a value
outside a slot-declaring definition, a valueless one inside it and prop arguments on a valued
one, and kept a hand-written twin of `PropConversion.CanConvert(…, allowBoxToObject: false)`
with the HED5014 family behind it — then allocated the carrier through a path of its own.

All of that is deleted. A `[SlotProjection]` call now takes the one bound-extension route, and
`OutExtension.InitStart` decides its own state: `PrecompiledInitSite.SlotType` already carried
the active `CompileContext.SlotParameterType` for every call site and `PrecompiledRuntime.Init`
already installed it on the synthesized scope, so the hook reaches the same `_slotMode` with no
site flag of its own — checked before one was added, and the public-API golden is unchanged.
A hook that reports the engine's HED5012/HED5013/HED5014 there is a template-scope fault, which
is the gauntlet's instruction to render the request on the dynamic tier, where the engine refuses
it with the same id, sentence and position. The reader's diagnostic does not move; the build-time
warning that anticipated it does.

The bodied projection stays a build refusal, and not as a preference. Outside a slot-declaring
definition the hook accepts the body, and the engine compiles a body with no dynamic content in
it to no processors at all — so the projection emits the chained value alone where an emitted
body is a real strategy. Deleting the refusal rendered `[@out(){{BODY}}]` as `[BODY]` against the
engine's `[]`. Its detail string is parameterised off the call name, byte-identical for the
built-in.

`PrecompiledRuntime.BindOut` is retired in place — the generator emits no call to it, but
already-built consumer assemblies name it — and `SetPrecompiledSlotMode` stays as its one
callee. `BodyTypingRules.TrySlotValue` and `RefStructUse.Boxed` go with the twin; a ref-struct
slot value now degrades naming the model sink it actually lands in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…top being the engine's private business

`[SlotProjection]` obliged an extension to read the enclosing definition's slot type in its own
`InitStart` and to render through the scope's slot carrier — and then left both behind an
`internal` wall, so the role was declarable from outside the engine and not implementable. The
suite's own third-party fixture had to say so in its doc comment.

`CompileContext.SlotParameterType`, `InitContext.CallCarriesValue` and
`InitContext.IsChainedConsumer` become public reads; `Scope.SlotCarrier` becomes a public
`ISlotContent` instead of an `object` the caller had to pattern-match back to a type it could not
name. `ISlotContent` is the face of the carrier — `InvocationScope`, `RenderCallerContent`,
`RenderCallerContentInto` — implemented explicitly by `SlotContent` so the engine's own funnel
keeps reaching `Outer` directly. Additive throughout: four rows enter the public-API golden and
none leaves.

`OutExtension` now reads its own public surface, which is the proof the surface is sufficient: it
lost the `is SlotContent` pattern and the `carrier.Outer` hop, and nothing else moved.

The two gates that pin the synthesis catch up. `InitSynthesisFidelityTests` declares the three new
public context members as reconstructed — the witness already answered all three — and the
`SourceItem` read-closure ledger gains the two lines in `InitContext` that are those reads' public
faces. `custom-extensions.md` gains the worked projection the attribute table has been describing
since it landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… emits the engine's own TemplateChain

A chain is N direct calls threading one value, and the refusal that cost every
multi-item chain its tier said only that the build had never written the loop.
It writes it now, statement for statement as TemplateChain.RenderData runs it:
the chained channel seeds the running value, each item is handed
scope.Chain(running) — as its own static helper, so the item's call parameter is
read off that scope rather than the ambient one — and the leftmost item renders
where the rest process. A chain in call-parameter position takes the same shape
through ChainedParameter, which is TemplateChain.ProcessData.

The one thing about a chain a build cannot name is the chained type: the engine
threads returnTypeChainedPrevious right to left, so an item's chainedType is
whatever the item to its right returned, and naming it would mean predicting a
hook. The build names the producer instead. PrecompiledInitSite.ChainProducer
points at the producer's site, Init publishes what that site's real InitStart
returned, and the consumer reads it — the producer's field initializer precedes
the consumer's, because items are emitted right to left. A carrier the build
Binds is the exception and not a prediction: Bind already stands in for the base
InitStart, whose answer is string.

IsChainedConsumer and HasProducerToRight were declared, read by PrecompiledRuntime
and written by nothing, which was sound only while no chain reached this tier.
They are written now, and cleared across every body walk — a call inside a chained
item's body is not itself a chain item, and left carried it would arm @out's
composed-projection guard on an @out that composes nothing.

Three of the five ChainCarrier sites are retired. The bodied unnamed carrier stays
for a rendered byte: the engine compiles a body with no dynamic content to no
processors, so the carrier renders its model where an emitted body would render
itself. The bodied chain-parameter item and the producer-less call parameter stay
as floors the grammar does not reach.

ChainedDefinitionFallbackTests becomes ChainedDefinitionTests with the same bytes
and the opposite tier assertion, and the composed-projection guard is now pinned
from both tiers rather than from the dynamic one alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the engine's third post-state

AbstractExtension.InitSubTemplate has three post-states and the emitter produced two of them.
Every bodied call was handed a real BodyN strategy, so a body the engine compiles to no
processors — RuntimeDocument.Empty, which the engine builds and then discards — left InnerExist
true on this tier where the engine leaves it false. @raw(){{mid}} over "Hi" rendered <x>mid</x>
precompiled against <x>Hi</x> dynamic, and byte identity between the tiers is the only hard
contract there is.

The predicate is the engine's own, not a guess about @ signs. CompileBody adds a document element
for a chain whose composed returnTypeChainedPrevious is non-null and for no other, so a body is
empty exactly when no output chain survives shaping — which makes a body of nothing but directives
empty, and a chain that composes to nothing empty, as much as one of static text. The shared
DocumentShaper already computes that list on this side, so the answer is read off it rather than
off the built segments: a definition body is registered before it is populated so a self-call can
find it, and a half-built segment list told that call the body was empty and emitted a null
strategy for a body that ends up holding processors.

Every site that installs a body takes it: the bound-extension call, the parameter-declaring one,
the definition's own body and its invocation's caller content. The child-template host's name body
and every chain item reach it through those. The document root does not — it is not a call body.
A body handed to nobody is written for nobody, so its class is skipped and the numbering carries
gaps; the one test that read Body1 by name now finds the first nested class by scanning.

With the byte gone, the refusal it justified goes too. A bodied unnamed carrier — refused since
it was written, for this and nothing else — takes the ordinary bound-call route through the shared
carrier rule, and @( ){{text}} renders the model on both tiers. Retiring it routed
wierd-whitespace.heddle into a hole the substitute always had: it compiles the call's own text
under the request's own options, so a refusal the build configuration makes is one it meets too,
at first render instead of at build. The expression-mode gate is asked before the type-agnostic
arm now, and a HostSetup refusal costs the template, which is what the engine does with that
document.

The bodied projection stays refused and the reason is recorded rather than assumed: the same four
shapes were measured byte-identical with the refusal removed, and what keeps it is that
BuildSlotProjectionCall would become a passthrough — a role-dispatch decision, not a byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eads it instead of refusing the template

A body compiled against a dynamic scope reports no model type, and the late accessor read that as
the absence of an answer rather than as the answer it is. The engine resolves such a body's reads at
render and refuses nothing at compile time; the accessor refused, which faulted the enclosing call
site into the site substitute — and a substitute compiles that call as its own document, so it cannot
carry a protocol the enclosing scope owns. A nested branch set under a model-less document published
its state where the sibling terminal could not read it, and the render threw '@else is a branch
terminal with no matching opener' where the engine renders the else body.

The accessor now binds the dynamic tier's own per-hop read, in the same binder context, with the same
null-propagating hop rule, over the model channel or the root channel as the path asks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… body's typing is read off a real engine compile or off nothing at all

The body-model table listed sixteen built-in extension names and what each one compiles its body
against. It survived one cycle past its own retirement condition for a single reason: a real engine
compile cannot run against a reference assembly, which is exactly what a project-to-project reference
hands the compiler, so a whole class of consumer had nothing but the table. That reason is gone.
MSBuild knows both halves of such a reference — @(ReferencePath) is the implementation and
%(ReferenceAssembly) is what CoreCompile passes — and _HeddleResolveObserveImplementationPath now
declares the first for exactly the references that carry the second. The compilation still compiles
against the reference assemblies; observation loads the implementations, matched by AssemblyIdentity
so a near-miss is never substituted, and anything unmatched keeps the reference the compiler gave it.

The list travels as a '|'-separated value and not a ';'-separated one, because it arrives through the
generated .editorconfig, where a value ends at the first ';' and the rest is a comment: a ';'-joined
list would have reached the generator truncated to its first entry, silently.

With the table gone, BodyModelSource keeps the two roles the build can read rather than predict — the
model the observed compile reports for a body's span, and the call's own data value that a
[ChildTemplateHost] author declares — and ElementOfData, which no longer described either, is now
named Observed. A built-in's body is typed by exactly what types a third party's. Twenty rows leave
the extension-agnosticism ledger with the table that needed them.

Dynamic becomes an answer rather than silence on the observed side too: a body the engine compiled
against a dynamic scope is reproduced as a dynamic body, which still reads members and hosts branch
participants, instead of going through the type-agnostic substitute.

The observe memo stops outliving the file it names. It recorded that this process had produced a
content, never that the file was still there — and _HeddleCleanObserveCache empties that directory on
every clean, so the second build in a compiler-server session was handed a deleted path and every
build after it observed nothing for the life of the server. It now agrees with the store.

The three Off-versus-Strict gates are restated around what is still invariant. Comparing a blind
build against an observed one and demanding equal tiers asserted something the architecture no longer
claims: Off is a genuinely reduced mode now, not an equivalent one. What has not moved is that the
precompiled tier renders what the engine renders, so each mode's precompiled output is compared
against the dynamic engine's own bytes — strictly stronger than the comparison it replaces, since two
modes wrong the same way used to pass, and it is what caught the branch set fixed in the commit
before this one. Monotonicity survives per template rather than per refusal category: observation may
never take a template off the precompiled tier, but removing an early refusal reveals the later one
behind it, which is why recursion.heddle reads HookBehavior blind and HostSetup observed while being
the same refused template either way. A fifth gate pins that the two runs are genuinely different
runs, without which every comparison here would measure nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant