Skip to content

Feature/benchmarks - #47

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

Feature/benchmarks#47
multiarc wants to merge 215 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 8, 2026 13:46
Extending the data-not-display separation to the floor workloads:
large-loop drops the pre-built row name (the template composes
row-@(Value)), mixed-page trades Sku and Blurb strings for SkuNumber
and Batch ints with the display text living in the template, and
conditional-heavy trades Note for Seq the same way. Zero-padded
identity names and the encoded suite's hostile payloads stay as model
data by design - one is row identity outside the Handlebars ceiling,
the other is the escaping subject itself. Golden bytes unchanged;
Heddle cells green; twin floor cells join the expected Stage-3 reds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last C# text is gone: the four inert areas and six asset/script
fragments move from AreaData and the assembly extensions into
chrome-fragments.heddle, a definition-only library layout.heddle
imports - every piece page-overridable, the file rendering nothing at
import. Components.cs, AreaData.cs and TwinContent.cs are deleted;
the six extensions had no purpose once they had no text to serve, so
composed-page is pure template composition and the model carries
exactly one thing: structured nav data. Golden bytes proven unchanged
twice over (scratch export byte-identical, verify-corpus green against
the committed corpus); selftest 145/145; all 66 Heddle cells green;
precompiled coverage holds at 6/8. Ledger E22 records the ruling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ecosystem model layers converge on the anchor's E20/E21/E22 shapes:
every blob fixture file is deleted (Rust data/, Go embeds, JVM
resources, the 1,169-line JS literal module, Python's 58KB of escaped
fragments) and composed-page's model everywhere becomes one structured
nav loaded once from the corpus nav.json fixture; fragment rows carry
four kinds, booleans, and an integer promo price; the floors carry
value, seq, sku_number and batch where display strings used to sit.
Calibration pins re-pinned to the slider and the mega-menu anchors.
Template trees still speak the old shapes on purpose - each engine's
own agent rewrites them next, per engine, in its own worktree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… agrees

The per-engine port wave lands: five .NET twins (Fluid, Scriban,
DotLiquid, Handlebars, Razor) and ten ecosystem engines (Askama, Tera,
JTE, Thymeleaf, eta, handlebars, Jinja2, Mako, Go stdlib, templ) each
rebuilt composed-page on their own native layout mechanism with a live
body slot, fragment-heavy on four dispatched kinds with nested badge
and price, and the E21 floors on template-composed display text. All
chrome is literal template text everywhere; the dead unsuffixed Liquid
sources are gone; consolidate.py re-pins the two changed sizes.

Notable landings: Handlebars partial blocks probe-verified on .NET
2.1.6 and JS 4.7.9; Thymeleaf passes the controlled byte gate with no
exclusion (two new authoring classes, B10 selector collision and B2b
attribute order, recorded in its feasibility ladder); templ gains real
children composition, with a documented templ.Raw accommodation for
void-element slashes the generator cannot emit.

Gates: .NET 146/146, Rust 32/32 (+32 lib tests), JVM 32/32, JS 32/32
(+82 selftest), Python 32/32 (+133 selftest), Go suites green.
selftest 145/145, corpus verified, goldens untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fourteen doc surfaces reconciled to the landed reality: the workload
mechanism and dispatch tables record executed probe results instead of
plans, the per-phase construct docs describe the templates that exist,
the JS helper surface is zero and says so, and the E23 ledger entry
records the fifteen-port landing - Handlebars partial blocks proven on
both platforms, Thymeleaf green with no exclusion and two new hazard
classes, templ's F7 void-slash accommodation, one forced rename. Also
fixes the pre-existing broken records.md link the doc gate had flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entry templates alone remain at the top of each track's heddle folder;
layout, chrome-fragments and the newly extracted fragments library live
under shared/, imported by root-relative path that spells identically at
runtime and in the precompile manifest. Output proven byte-identical by
before/after render hashing; all 146 gate cells, 145 selftest checks,
corpus freshness and 6/8 precompiled parity unchanged. workloads.md
follows the moved paths so the doc-link gate is green again, and the
two generator test projects gain Heddle.Tests' Windows-only net48
conditioning so bare dotnet test works on Linux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The entries-only layout rule lands in all fifteen engines: eight
runnable templates at the top of each track, everything else under
shared/ - or the native equivalent, Views/Shared for Razor, the shared
package for JTE, chrome/ and nav/ staying put beside shared/ in Rust.
Nearly every move is a pure rename; include names stay logical where
loaders map them, so most templates did not change a byte, and no
output did anywhere: all six ecosystem gates green, 146/146 .NET cells,
7,943 tests across three suites, four bench suites Dry-smoked clean.
The sweep also caught ColdCompile still loading the E23-deleted
unsuffixed Liquid entries - now dialect-suffixed. Docs follow the
moved paths; E24 records the rule and the landing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… manifest

Two halves close the last dynamic fallbacks behind gate-precompiled's strict 8/8
(ledger E25). The generator pins the step-back encoders (@string/@attr/@url/@js):
their one hook override re-types only a default body, so a bodiless call binds
exactly like a plain extension — proven byte-identical across the tiers by the
flipped ContextEncodingFallbackTests; the bodied form keeps its safe degrade.
The harness gains an Html-profile satellite assembly carrying just the two
encoded templates the compilation-wide Text profile could never hold; both
manifests register, and coverage is still discovered, never assumed.

gate-precompiled 8/8 with 3-sink parity, gate 152/152, selftest 151/151,
bench-techniques precompiled Dry 24/24; suites 4351 + 1228 + 2378 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ddle

Fifteen engines already name the composed-page entry for its workload; Heddle
was the one exception, so the one file renames instead of the fifteen (E26).
The "home" special case is dropped from the engine spec, the precompiled key,
the selftest's coverage probe and the cold suite; prose and links follow in the
specs, the corpus README and the port-side comment citations. Byte-neutral:
corpus verified without re-export, gate 152/152, gate-precompiled 8/8 with
parity, selftest 151/151, cold Dry 6/6, Go and doc-link suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every engine holds its tracks in controlled/ and idiomatic/ subfolders; Go was
the holdout (E27). templ's controlled ports move from the templeng package root
into templeng/controlled beside the existing idiomatic package, embedded source
paths re-rooted to what templ generate now emits. stdlib splits its one mixed
package into stdlibtpl/controlled and stdlibtpl/idiomatic, each with its own
render path, and RenderIdiomatic* drops the prefix the package path now carries.
The suites registry imports all four track packages under explicit aliases.
Moves only — no template source changed; build, vet, and the 32-cell suites
gate all green; phase-6 spec citations follow the new paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every suite's short profile must execute at least five measurement samples per
run (E28). The audit found one deficit: the .NET harness job ran ShortRun's
three iterations per launch, and the third-party head-to-head inherited the
same three from upstream's ShortRunJob. The harness job becomes L3/W3/I5 —
confirmed live in the emitted job line — and the third-party suite states the
same floor as an explicit SimpleJob, its one documented budget deviation from
the upstream methodology. JMH, criterion, pyperf, Go and JS shorts already met
the floor and are untouched. Baseline scales through the job rather than beside
it: --launchCount 10 over five iterations is 50 samples per cell, against
short's fifteen. Runner-script profile comments follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…istory

The plan, spec, and research trees of both completed programs are retired;
their ~190 decisions, the E1-E28 ledger, and the 2.0 release record collapse
into cross-cutting-decisions.md with origin ids as the decoder ring. The open
2.1 window record moves intact into breaking-windows.md. Review protocol and
findings register are promoted to common specs, joined by the new
shared-source-architecture.md. The benchmark contract docs move to
benchmarks/docs/ as operational documentation, and every code comment learns
coding-standards C4: state the rule, never cite the document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inst the specs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…its files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ngine's own spelling of chained and root

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, so chained, root and dynamic models stop costing templates their tier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gine evaluates it at compile time, and a rendered ref struct stringifies in place instead of costing the template its tier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e, and not a byte moves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…node picks a plan with refusal last, and HED7031 carries a machine-readable category beside its sentence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ngine's own accessor

A member-path value the engine resolves but generated C# cannot spell — a
referenced assembly's internal member without IVT, an error-obsolete getter, an
unnameable type mid-chain — used to de-precompile the whole template. Now the
plan ladder selects EngineAccessor for it: PrecompiledRuntime.MemberAccessor and
NativeAccessor wrap the engine's own member resolution and null-safe hop chain
(System.Linq.Expressions only, no Roslyn), built once at type-init into a static
delegate, one call and no allocation per render. Selection is proven, not hoped:
SymbolTypeResolver.EngineViewResolves walks the path under the engine's own
visibility rules, over the full-metadata view where the ordinary symbol model
hides the member, and declines anything the engine's expression trees would
refuse too. Escapes are for values only — bodies, branches, lists and definition
invocations still degrade whole-template.

HeddleNodeFallback (default on) threads through props → ConfigReader →
GlobalConfig with the existing lockstep gates extended; false restores the
whole-template degrade, pinned by RefusalCategory.MemberAccess in the tests that
used to pin it as the default. The recovered shapes are pinned byte-identical
with the tier declared, including encoded output, and each escape carries a
zero-allocation render-loop proof against its direct-plan twin. The corpus moves
no row: props-card's first refusal is DefinitionProps, not MemberAccess, so it
stays declared FallsBackSafely. Benchmark gate 152/152.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t render

A call whose target only a run-time registration supplies used to cost the whole
file its tier: the emitter recorded an HED7014 marker and the template went to
the dynamic path. But the build never lacked the call — it lacked the target.
The name, the argument count and each argument's static type are all known at
build time, and they are exactly the input the engine's own overload selection
takes.

So the call is emitted as a PrecompiledFunctionSite: one static field per call
site that, at first use, freezes the render's effective registry, ranks the
overloads through the engine's own OverloadRank (reached by an internal
BindOverload the expression compiler now shares), builds the engine's own
invocation tree, and caches the delegate. The same overload wins on both tiers
by construction rather than by agreement. Argument types ride the site's
generic parameters, so the consumer's compiler infers what the engine's
compiled arguments carry; the null literal, which inference cannot type, is
spelled object and flagged in a mask so the ranker still sees a null literal.

Failure is the engine's failure, not a new one. An unregistered name, an
ambiguous call and a call no overload accepts each raise HED1001, HED1013 and
HED1012 with the engine's own sentence and position, captured once and
re-raised the way a computed @partial name's fault already is — the sentences
moved into a shared FunctionCallMessages so the two writers cannot drift. On
the registry path the gauntlet gets there first: a null-target manifest row now
means "late-bound", and a request whose live registry does not hold the name,
or holds it as an extension, falls back to the tier whose compile owns the
answer. The resolver's adapter carries the request's options into the render so
the registry the gauntlet judged is the registry the site binds.

Steady state costs nothing: a volatile read, a reference comparison and the
delegate call, with the bind's type vector a static field of a generic holder
rather than an array built per call. The zero-allocation twin measures the
late-bound document against the built-in one over the same loop and requires
the delta not to grow.

HED7014 keeps its id and narrows its meaning, deliberately. What still reaches
it is the call late binding genuinely cannot serve: an argument whose static
type has no build-time answer, so there is nothing to select an overload
against. The corpus records both halves — the old marker fixture moves to
fn-late-bound.heddle and precompiles, and fn-unresolvable-marker.heddle is
rewritten to the shape that still degrades, so DegradesToMarker keeps a tested
member instead of emptying out. That also makes a template precompile and still
refuse at render for the first time, which the ResolveOnly gate had been
asserting could not happen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rving the build's guess to a host that typed its own

`TemplateResolver.ConsultPrecompiled` threw away `context.RootScopeType`, the gauntlet never
looked at a model type, and `PrecompiledTemplateInfo.ModelType` was written by the generator and
read by nobody. A template with no `@model` was therefore served from the precompiled tier —
typed against `object` at build time — to a host that had typed its context, which is the one
configuration where the two tiers resolve members differently. Benign for a public property;
not for an `internal` member, a native expression's start type, or `@list` element derivation.

The request's model type now reaches the gauntlet, through `ConsultPrecompiled` and the hosted
`Search` ladder alike, and a new model-type step consumes `ModelType` at last. The step applies
to one entry shape only: the manifest gained `ModelTypeIsAmbient`, set when the template declares
no `@model` and the recorded type is the build's own answer (`ModelType` item metadata, else
`object`). A declared `@model` — `dynamic` included — pins both tiers whatever the host asked
for, so its request model type is not evidence and is not consulted; for an ambient entry the
answer is type identity rather than assignability, because what decides the bytes is the typing
done before the generated cast, not the cast.

A mismatch reports the new `PrecompiledFallbackReason.ModelTypeMismatch` through the existing
`HED7101` per-request carrier and takes the byte-identical dynamic path. Manifests written before
this value existed read it back false and behave exactly as they do today; the wider constructor
is a real one, so the narrower shapes stay in metadata, and it is emitted only where it says
something.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssemblies to look in

The editor has a workspace `assemblies` key and the runtime has `Register`; the
build tier had nothing, so a model type the host configured perfectly well could
still be invisible to the generator — and the reverse, a build that precompiled
from a reference the running process had never loaded.

`[assembly: HeddleModelAssembly(typeof(T))]` closes both directions at once. The
`typeof` is the whole design: a type cannot be spelled without a reference, so
the build's requirement becomes CS0246 in the host's own source rather than a
rule prose asks you to remember, and `AssemblyHelper.Register` reads the same
declaration at startup. No new HED id — the C# compiler already owns that
diagnostic.

`RegisterModelAssemblies` and `Register` now share a private
`AddAssemblies(list, trackForUnregister)`; both public signatures are untouched.
A workspace registration stays tracked so a reload can drop it and let its
collectible context collect. A declaration is not: it names a compile-time
reference of an assembly the host loaded statically, and taking it away on
somebody else's reload would un-resolve models nobody asked to withdraw.

`@(HeddleModelAssembly)` / `@(HeddleExtensionAssembly)` are the escape hatch for
projects that cannot carry a typeof. The targets append them to
`@(ReferencePath)`, so the item reaches the compiler and not the generator —
which is what keeps build output a function of the compilation's declared
inputs, and why there is no CompilerVisibleItem. Items, not properties with
defaults, so the props-defaults count literal is untouched and absent items are
byte-identical output. Assembly configuration is not a fingerprint input and now
says so where the fingerprint is defined: it decides whether a template
precompiles, never a rendered byte.

The test that matters is the negative control. Agreement in success was already
pinned; agreement in refusal was not, and it is the half that means something,
since two tiers that both do nothing also agree byte for byte. Configure
neither, and neither may claim the template. Its sibling pins the ambiguity
configuration creates, which is parity rather than regression — a runtime with
both assemblies registered raises the same error the build does.

`samples/precompiled-app` gains a model assembly it takes no compile reference
on, so a real project build proves the targets; the suites that inject build
metadata directly are exactly the blind spot that once left Key, Name and
Precompile inert for a release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bout int

`@model Foo` was resolved twice: `ReflectionHelper` walked global:: → alias →
index → using static over its reflection name maps, and `SymbolTypeIndex`
walked the same arms in the same order over the compilation's reference
closure. Two copies of one algorithm can only agree by inspection, and they
had already stopped agreeing.

The ladder now lives once, in `src/Heddle/Language/Binding/TypeNameIndex.cs`,
generic over the type representation behind `ITypeNameMaps<TType>` — the
`ITypeFacts<TType>` shape, with the three genuinely tier-shaped questions on
the seam: what a predefined-type alias means, what an assembly-qualified
spelling means, and what makes two types the same one. `UsingDirectives` is
the input on both sides, as it already was. The runtime supplies the maps it
had; `SymbolTypeIndex` becomes the build tier's `ITypeNameMaps` and sheds the
mirror.

The disagreement the extraction found: the build tier consulted its keyword
table in front of the whole ladder, so `@using(){{int = Acme.Money}}` bound
`int` to `System.Int32` where the engine binds `Acme.Money`, and
`@using(){{string = Nope.Missing}}` precompiled a template the engine refuses.
An alias claims the head before any predefined name is read — that is C#, and
it is what the engine has always done — so the keyword arm moved to where the
engine keeps it and the build tier follows.

Pinned by a new suite that runs the one ladder through both seams in a single
process, over a symbol universe built from exactly the assemblies the engine
has loaded, so a red row can only ever mean the maps disagree. The named
lockstep and spelling suites are untouched: a refactor that needs its own test
edited was not a refactor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…template, so the refusal keeps its ground and says why

Step 10 of the pre-compilation plan set out to emit a generic entry class plus a
non-generic strategy for the registry, on the reading that the build refused open
generic model types for a reason it did not have — it writes the entry signature,
so it can declare type parameters. The reading is right about the build and wrong
about the outcome, and the source says so twice.

HeddleTemplate.Generate gates every render on ScopeType.Type.IsInstanceOfType, and
no value is ever an instance of a generic type DEFINITION: a template declaring
@model List`1 refuses List<int> along with everything else. A template that reads
one member never reaches that gate, because ModelParameter builds an
Expression.Convert to the scope type and System.Linq.Expressions rejects an open
generic outright, so the engine's own compile fails with HED0005. A generic entry
class would have rendered both, which is the one divergence worse than a missed
optimisation.

So the refusal stays and stops describing a wall it does not sit behind. The model
position re-categorises it from UnnameableType — the csc story, which is what made
it look recoverable — to EngineParity, with a sentence naming the tier that
actually declines, and both walls are pinned by tests that assert the engine's
failure beside the build's degrade. Constructed generics are pinned from the other
side: List<int>, Dictionary<string, int> and Outer<int>.Inner precompile, render
the engine's bytes, and survive the registry round trip.

Chasing the registry half turned up the gap that was really there. The
precompiled adapter skipped the model-type check entirely, so a wrong model
reached the generated cast and came back as a raw InvalidCastException where the
dynamic tier raises a Heddle fault — and a body reading no member rendered a page
the dynamic tier refuses. The manifest has always carried the type the generated
code was compiled against; the adapter now gates on it, with the same fault and
the same sentence, so the two tiers agree in the failure as well as in the bytes.
Untyped entries record object and are held pre-reduced to null, so the render path
pays one null compare.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… said it wrong

Hook probing, staged so the idea is tested before any loader exists. Three pieces, in order.

HED7015 stops failing the build. A third-party extension whose InitStart the generator cannot
evaluate is not an authoring error — it is a property of someone else's package — so it costs its
call site the precompiled tier and reports a warning at the call, the same answer HED7014 and
HED7030 already give for their causes. The id is kept rather than folded into HED7031: naming the
extension and the hook is the one fact the author can act on.

BodyModelRules' `list` row said the chained channel carries nothing. ListExtension.InitStart hands
`new ExType(typeof(int))` to the body compile and scope.Model(item, index) at render, so `@out()`
in a `@list` body splices the index exactly as it does in `@for`. The row was wrong from the day it
was written and stayed latent because nothing read the chained column — the emitter consults the
body column alone. Fixed, and now read: revert the row and the new test reddens with "012".

Then the protocol itself. Four sentinel types and three probe documents ask each extension what its
hook does, and the answers are read out of state the engine already keeps — the ScopeMap entry for
the body compile is the hook's dataType/chainedType verbatim, and the item's return is null for a
zero-output directive. Nothing calls InitStart directly: InitContext.SourceItem is internal and
@out reads it, so a hand-built context would make @out answer as a different extension.

The gate says GO. Across all 26 built-ins the probe agrees with every pinned row, classifies the
19 the table has no row for, and reaches Unclassified for none of them. It also rediscovers the
`list` defect on its own, which is the strongest evidence the observation is worth more than the
prediction. Two roles had no name in the vocabulary — @out and @swap put the chained value on the
model channel, and the model or the data value back on the chained one — so the enums gained them.

Corpus gains the bodied step-back encoder, the row that could not exist before: its refusal is the
emitter's, not the language's, and 11h flips it in one line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…somewhere nothing rewrites

The loader first, because it is the rule the rest of hook probing stands on. Assembly.LoadFrom holds a
file open for the life of the process and the compiler is a persistent server, so loading a consumer's
bin/ or obj/ output once breaks the NEXT build of that project. The predicate is therefore explicit and
tested: a rooted path, no bin or obj segment anywhere in it, and under one of this restore's package
folders. No roots means no probing rather than "anything goes", and a project-to-project reference —
which surfaces as a CompilationReference with no file at all, and is where the IDE and the CLI would
otherwise disagree — is simply never loadable.

$(NuGetPackageFolders) is read rather than invented. RS1035 bans System.Environment in an analyzer, and
an environment variable is not a compilation input, so the roots arrive as a compiler-visible property
NuGet itself writes. Assembly.LoadFrom is not banned; System.IO.File is, which is why nothing here asks
whether a file exists and a missing one is a caught exception.

An assembly the process already holds is handed back rather than loaded again. Loading is what locks a
file; reuse locks nothing, and it is also the natural cache for the second compilation in a server's
lifetime. In a real build the only assemblies the loader can find already loaded are ones it loaded
itself from an immutable root, so reuse is a subset of load — it is wider only in a test host, which is
what lets the driver be exercised end to end against a project-referenced engine.

Then the driver. It compiles the protocol's three documents through the loaded engine and reads the
answers out of the ScopeMap entry and the compiled item's return, exactly as the engine-side driver
does, so both tiers meet at one Decode. The sentinels stay this side's types — a role is a property of
the extension, not of the type system the sentinel lives in — and that turns out to work against a real
engine across the assembly boundary, which is the fact the new suite pins. Two guards make an answer
this build's own: the loaded engine must register the name AND bind it to the same type this
compilation resolved, and every extension is probed twice on fresh instances and refused if the two
runs disagree.

There is no sandbox and the code says so rather than implying one. netstandard2.0 across a .NET
Framework host and a .NET host leaves neither AssemblyLoadContext nor AppDomain, so what is bounded is
the WAIT: a background thread, a timeout, and the first timeout ends probing for the whole compilation.

HeddleProbeExtensionHooks threads props to targets to ConfigReader to GlobalConfig, default false, and
the compiler-visible property set gains a gate of its own: declared in XML and read by ConfigReader,
asserted as set equality both ways through the shared name constants. Nothing consumes an answer yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 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>
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