Feature/benchmarks - #47
Draft
multiarc wants to merge 215 commits into
Draft
Conversation
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>
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&b<c>. 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Related issue
How was this tested?
Checklist
dotnet test src/Heddle.Testspasses locally..editorconfig/ surrounding code style.git commit -s).AI-assistance disclosure (if any)