Conversation
Q8.33 -- PrecompiledFallbackEvent.Key is REMOVED, not narrowed, replaced by TemplateKey + AssemblyName with exactly one populated and ForTemplate/ForAssembly factories in place of the public ctor. Removal follows the ruling's own reasoning: narrowing leaves a 2.0 host silently reading null on the channel whose entire purpose is that failures are not silent, while removal is a compile error at the one line that has to change. Declared a 2.1 binary break with a disposition in breaking-windows.md on the same footing as Q8.2's floor rise. The reason -> populated-carrier mapping is pinned from both sides: each factory refuses the other carrier's reasons and refuses a blank carrier, and the classifier behind them is an exhaustive switch that THROWS on default, so a reason added later is unraisable until it is classified. PrecompiledFallbackCarrier Tests checks that classification against Enum.GetValues in both directions and pins the absence of a Key member by reflection. Q8.32(b) -- an unnormalizable RegisteredName now reports HED7104 instead of being continued past in silence, naming both the refused spelling and the requesting key. No new id: the registry row and the enum member's doc were widened, because the two causes are one situation with one remedy from the host's side. Sub-question (a) is deliberately absent -- the per-request gauntlet arm was ruled out as over-engineering, and PrecompiledGauntlet.cs changes only by the Q8.33 call-site rename. Each register entry carries an explicit "deliberately not done" list so the scope reads as a decision. One ruling premise was half wrong and is corrected: the Key overload was stated in the type's XML doc, NOT in docs/precompilation.md, which never mentioned it. That file now carries a carrier table. Mutation testing: 10 run, 9 killed, 1 survivor proved extensionally equal (indexing a spelling outside TryNormalize's range is unreachable, since TryGet normalizes before consulting the name index) with the argument written into the fixture. One automated revert mis-anchored on an empty-string replacement; caught, restored by hand, and the affected mutants re-run clean. Suite per leg: Heddle.Tests 1797/1797 on net8.0 and net10.0; Generator.Integration Tests 402/402 x2; Generator.Tests 434/434 x2; LanguageServices.Tests 188/188; Tool.Tests 6/6. net6.0 compiles but has no runtime on this box; net48 unrunnable on Linux. Release build clean, docs site builds, no generated-source diff. The public API golden moves by exactly 4 lines, reviewed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Q8.37 -- the static-constructor assembly walk and the scan-all extension discovery are REMOVED in the 2.1 window, not gated behind a switch and not narrowed. Explicit host registration replaces them, on the shape PrecompiledTemplates.Register already has; where removing the scan leaves a real need with no API, that is a missing extension point to add rather than a reason to keep the walk. Two obligations attached: a breaking-windows disposition naming what stops working for a host that declared [ExportExtensions] and registered nothing, and reconciliation with README finding 3 in the same change, since the generator scanning all referenced assemblies and the runtime scanning only [ExportExtensions]-carrying ones are two halves of one seam. Q8.38 -- closed as not a question, and the fault is the entry's: it describes a constraint at length and then offers three shapes without asking anything a ruling could answer. The constraint stays recorded (a per-request fingerprint cannot be hoisted into a per-configuration pass) as an implementation fact to be resolved in code and pinned by a test. Docs only; no code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The rule lands as breaking-windows.md policy item 7, with the other six rather than in the not-window-gated section, because it governs how every future window is composed instead of dispositioning one change. Both halves stated: the schema number bumps only when an already-emitted manifest can no longer be read or bound, and an additive change is read through a per-feature <Feature>SchemaVersion gate and does not bump it. Package version and schema version are independent, so a release with no schema change is normal -- conflating them is what produced three unreleased schema numbers for internal churn this cycle. The proof obligation is the operative part: a fixture holding a PRE-CHANGE manifest that still reads correctly afterwards. "The reviewer judged it additive" is explicitly rejected as evidence, because the failure mode is a constructor signature that no longer binds -- invisible in source, visible only in emitted IL, exactly what the Q8.2 fixture had to construct. No fixture means breaking by default. No code change: Min = Max = Current = 3 stays, and the rule does not retroactively relax Min for the schema-3 break. No test either -- the obligation attaches to a future change, which has no pre-change manifest to hold today. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Q8.9 -- cross-cutting-decisions.md gains D10, in two parts because the ruling asked for two things that are only useful together. (a) The MAPPING: nine rows, one normative home per claim block, with an explicit tie-break for claims spanning two homes -- the home whose diagnostics the claim can produce wins, since a diagnostic has a registry owner and prose does not. (b) The CONDITION (phase 8's D3, moved here to outlive the program): a home outranks the implementations only for a claim carrying a verification marker or covered by a gate; an unmarked, ungated claim is evidence of intent, not an authority. Two things the question left implicit are now stated. A claim block absent from the table has NO normative document -- implementations are the authority and the runtime engine breaks the tie -- so the table is closed, not illustrative. And a document acquires a block by being added to the table, not by asserting authority in its own prose, which is how the convention became diffuse. Non-retroactive as ruled, with the reason recorded. Q8.10 -- phase 8's D9 is rewritten as rejected, superseded design kept in a collapsed block rather than deleted so the plan records what was decided against. Consequences propagated rather than left in the D-item: the phase-7 dependency is gone from the header, stage 5 is no longer "blocked", both D9 risk rows are void, and the VitePress @include reach question is retired unanswered. WI14 is repurposed, not dropped: from an include spike to deleting ScopeChannelDocExampleTests' "verbatim from docs" coupling. What that fixture asserts about Scope channel behaviour stays where it is genuinely a behaviour test; what goes is the claim to BE the document's bytes, which nothing enforced and which the ruling makes deliberately false. Done-when requires the behavioural coverage to be reasserted or recorded as dropped, so the deletion cannot lose a test quietly. One cost accepted knowingly and recorded in D9: the old rationale's strongest point survives rejection -- this program shipped a byte-changing formatter change and a profile default flip, either of which can invalidate a documented output, and review is a weaker guard than a gate. D11's currency rule carries it. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…ict)
Rows 8 and 9 of the 2.1 as-shipped record now carry the fallback-carrier split
and the unnormalizable-name report, so the decisions live in the spec rather than
only in the Q&A register.
Q8.39 is a genuine conflict found by consolidating: records.md states "2.1 opens
no window" and disposes every item as defect repair to keep policy rule 1 intact,
while the Q8.37 ruling calls 2.1 "the current breaking window" and schedules the
auto-load removal into it. That removal is not defect repair by the policy's own
test -- a host that declares [ExportExtensions] and registers nothing works today
and would stop, which is behaviour a user can correctly depend on.
The two readings license different things, which is why it is raised rather than
picked: window-less means the removal needs its own not-window-gated disposition
and every other break argues separately; a window means policy rule 1 ("one per
major") needs amending for a minor, and the release owes a contents table and a
migration note that do not exist. Three exits recorded, including moving the
removal to 3.0 and shipping explicit registration additively in 2.1.
Docs only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Agent worktrees are created under .claude/worktrees/, which made a nested git checkout show up as untracked content in the parent. Ignored rather than removed: the worktrees hold in-flight work and their branches are the only handle on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Q8.17 -- the type-index cache gets a retention contract. New SymbolTypeIndexCache holds what was a bare static Dictionary<Compilation, SymbolTypeIndex>: Get / Contains / Clear, observable Count / Capacity / MaxIdleGenerations / Generation. Eviction is generation-counted with NO clock: a generation is one compilation admitted (an edit epoch), hits do not advance it, so a busy project cannot age out its neighbour and no sweep runs on the hit path. The register's premise was wrong and is corrected: an edit-then-revert does not restore an identical-yet-old entry, because Compilation is immutable and the reverted state is yet another new instance. Old entries are unreachable and merely pinned, holding their whole symbol universe alive -- a worse leak than described, and exactly what an age bound collects. Determinism is pinned, not asserted: EvictionCannotChangeWhatTheIndexAnswers resolves the same spellings through a capacity-1 zero-tolerance cache and a never-evicting one and requires identical answers. 9 mutants, 9 killed; three were real gaps found by mutation, including a test that asserted presence where evict-then-rebuild also satisfies it. Q8.19 -- collected refusals, for the half that was contained. PopulateBody records the first reason and keeps walking; refusal still propagates, so a template with an unwritable construct still emits no .g.cs and no manifest row (pinned separately -- partial emit would be far worse than one-at-a-time diagnostics). Covers HED7025, HED7008, HED7006, HED7015, HED7017, HED7014 and HED7022 for SIBLING elements. The expression walk is deliberately untouched: NativeExpressionWriter is string-or-null composition, so continuing means fabricating placeholders for the parent to compose and discard -- the restructuring the ruling says to stop at. So @(min(1,2u)) @(max(1,2u)) now reports twice and @(min(1,2u) + max(1,2u)) still reports once. Stated, not hidden. A pre-existing tripwire fired exactly as designed: a phase-4 test written so that "a later change that makes the emitter continue past an unwritable construct reddens here and gets to decide deliberately". It was flipped consciously. Q8.14 -- CLOSED, premise falsified. [NotEncode] is AttributeTargets.Property and [EncodeOutput] is AttributeTargets.Class, so declaring both on one extension type is CS0592 -- a compiler error in the author's own project, which is the surface the ruled declaration-side analyzer was to occupy, at a stronger severity, delivered by the language. The pair is observable only in forged metadata, where both tiers answer RenderType.Raw -- indistinguishable from carrying neither attribute, so there is no divergence for a use-site error to close. HED7026 and HED7027 are not claimed and stay free. The finding lands as tests on both tiers instead, written to redden if NotEncodeAttribute's targets ever widen, with the reopening condition in their doc comments rather than only in the register. Also: the version-consistency gate skipped bin/obj but walked dot-directories, so agent worktrees under .claude/ made it scan a second copy of the tree and fail on files no release ships. It now skips dot-directories too. RuntimeDocument.cs carries the user's own comment on SingleStrategy.Execute documenting why the value path drops a non-string rather than stringifying it. Suite (net8.0): Heddle.Tests 1798/0, Generator.Tests 448/0, Generator.IntegrationTests 406/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
PrecompiledTemplates.ValidateAll(TemplateOptions) -> PrecompiledValidationReport runs the existing gauntlet over every registered entry and COLLECTS the failures instead of stopping at the first. No generated-code change, no manifest row, no schema bump, and per-request behaviour is untouched -- this adds a pre-render check, it does not move one. Both motivating findings were re-verified at HEAD. The typed entry point runs no gauntlet at all: Templates_X.Generate(model) reaches PrecompiledRuntime. GenerateString with no Validate and no TryResolve, and the gauntlet's only production callers are TemplateResolver.Search and ConsultPrecompiled. That is NOT the silent-fallback shape this program was built around -- there is no fallback on that path, so a drifted binding renders precompiled against a stale target. For a host on the documented recommended API this pass is the only check that exists. Second: a precompiled hit never enters TemplatesCache, so the per-request Validate re-runs every request. Q8.38's constraint is resolved in code rather than escalated, as ruled. Four gauntlet inputs are per-request, so a verdict is only ever about one options shape: ValidateAll takes a required TemplateOptions with NO parameterless form, the report snapshots the four inputs immutably so mutating the caller's options afterwards cannot make it describe a shape it never checked, and the green property is PassedForValidatedOptions -- a test asserts the names IsValid, Success and Passed are all absent, because any of them would read as a blanket all-clear. ToString ends "This verdict is about these options only." Pinned by a test validating one registry under Text and Html with complementary failure sets. It is a report, not a gate: no OnFallback, no throw under Strict. Mutation: 13 run, 13 killed, 2 survivors found and closed as real gaps -- no test supplied a host BindingResolver, and the request-registry branch of ToString was unexercised. Every restore touched the source before rebuild, the trap that has now silently verified a mutant DLL twice in this program. Also: the claimed-id registry gains "deliberately unclaimed" rows for HED7026/ HED7027 (Q8.14, withdrawn on assessment) and HED7029 (Q8.28, never needed), and DiagnosticIdTests now understands them -- such a row no longer demands a constant, and the INVERSE is asserted, so taking one of those ids without first rewriting its row reddens at the moment the decision is reversed. That turns the documentation into a gate rather than a comment, and replaces what would have been a silently growing whitelist. Public API is additive only: ValidateAll plus the report type, no removals and no signature changes. Suite (net8.0): Heddle.Tests 1810/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
1513 -> 1065 lines, 64 -> 65 entries, 51 of them thinned to a fixed shape: question, Resolution (the final answer only), and where it now lives. Entries are in numeric order for the first time. Superseded intermediate rulings are deleted rather than preserved, except where the reversal is itself a decision a future reader must not re-litigate -- Name is NOT an override, schemas 4 and 5 never shipped -- which state the final rule plus one sentence on what it replaced. Nothing was deleted without first confirming it exists elsewhere. Six pieces of content lived only in the register and were MOVED before their entries were cut: Q8.34's rule that the engine must not decide which assemblies are loaded became cross-cutting decision D11; Q8.14's and Q8.28's declined ids became deliberately-unclaimed registry rows carrying their reopening conditions; Q8.18's static-binding scope went to precompilation.md's functions section; Q8.19's split outcome and its match-principle note went to precompilation.md's diagnostics section. Seven conflicts collapsed to the later ruling, each recording what it replaced: the schema floor 4 -> 3; Q5.1's "Name removed" against the three later Name rulings; Name additive rather than override; Name's scope across all three states; Q8.27's #line comment superseded by Q8.31's manifest field; Q8.14's two diagnostics withdrawn by its assessment; and Q7.2's change-nothing ruling against Q8.11's deletion of a dead <Version>. One new conflict filed rather than resolved -- Q8.40: testing-standards E9 forbids reaching a sibling project's bin by walking up out of one's own, naming the exact failure mode (a test that finds nothing and passes), while Q8.20's landing knowingly left CorpusResolverSweepTests doing it. Both records stand on their own terms, so it needs a ruling. The header status block was wrong in five ways and is rebuilt as a table of the nine unfinished questions. It capped the range at Q8.35 though Q8.37-Q8.39 exist, listed three closed or landed items as unimplemented, called a made assessment unmade, and never recorded that Q8.36 was skipped -- now stated so nobody hunts for it. Two spec defects found by consolidating. precompilation.md contradicted itself on HED7018: the Name metadata row said Name does not suppress it (correct per Q8.25, with a three-arm test) while the Setup prose and diagnostics table said Key/Name both do -- corrected to Key only. And phase 1 still said the schema is "bumped 4->5"; corrected, with the collapse recorded rather than silently overwritten. Suite unaffected and green: Heddle.Tests 1810/0 on net8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Q8.39 -- 2.1 IS a ratified breaking window, scoped to binary changes and minor API changes or additions; a substantial API change is refused entry and held for a major rather than argued in. records.md's "2.1 opens no window" is superseded and rewritten, and policy rule 1 loses its "per major" wording: a window is opened by an explicit maintainer decision, not by the version component that carries it. The per-item not-window-gated dispositions stay accurate about why each change is safe -- they simply no longer carry the weight of keeping the release window-less. Recorded with the process note that this had already been ruled at Q8.37 and re-asking cost a round trip. The conflict was real, but the fix was to correct the stale record against the standing ruling rather than re-open the decision. Q8.40 -- no conflict. E9 governs test INPUTS, which are the git-stored things: templates, goldens, fixtures. A compiled sibling assembly is an output, so the rule never covered that read. What does cover it is the obligation both cases share -- conflict-free and loud on a miss -- and both already hold: CorpusResolverSweepTests asserts the directory was found instead of returning early, and Q8.20 ordered the build with ReferenceOutputAssembly="false". No code change. E9's wording is amended to say inputs and to state the outputs case, since as written it read as covering both. Suite green: Heddle.Tests 1810/0 on net8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Both were findings with no work item, which is how a docs defect quietly becomes permanent. WI16 -- shipped documents state that 1.x precompiled assemblies fall back because the engine-version gate rejects 1.x manifests. Precompilation shipped IN 2.0.0, so no 1.x manifest has ever existed and no gate has fired for one. Q8.24 closed the behavioural half as invalid and named the rest a docs defect; this is that item. records.md is append-only, so the 2.0 record gets an appended correction rather than a rewrite, and the correction has to say why the claim was wrong instead of deleting it silently. WI17 -- D11 permits documentation to SUGGEST a registration-ordering pattern and nothing more. That third consequence had no home. It belongs beside extension registration in custom-extensions.md, and is done only when the pattern is unmistakably a host-side recommendation rather than a rule the engine enforces -- the distinction D11 exists to protect, and the one the Razor/ASP.NET precedent shows is easy to lose. Sequenced after Q8.37, which lands the API it describes. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Undoes the 112 renames that moved src/Heddle.Tests/TestTemplate/ to src/TestCorpus/. They landed by accident: the phase-7 agent had STAGED them, and my next commit (d7ddd00) committed the whole index rather than only the docs paths I passed to git add -- the same failure as git add -A, wearing a different shape. The relocation was also wrong on the merits, by ruling: copying an input into the build output for a test to read is a BUILD COPY, not a logical conflict with where the input is stored. Test inputs are the git-tracked things and they live in tracked folders; nothing about reading them from bin/<cfg>/<tfm> at run time required moving them out of the project that owns them. E9 was read as forbidding something it never addressed. Preserved rather than discarded: the full move is at branch wip/phase7-corpus-move, and the agent's TestCorpus.props / TestCorpusIndex.cs scaffolding is in the session scratchpad. Nothing is recoverable only from memory. Suite green on net8.0 after the restore: Heddle.Tests 1810/0, Generator.IntegrationTests 406/0, Generator.Tests 448/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
D1 relocated the corpus to a neutral src/TestCorpus/ because a shared input "should not live inside one consumer". Rejected: a build copy into a consumer's output directory is not a second home for the input and raises no ownership question to solve by moving files. Inputs are the git-tracked artifacts, they live in tracked folders, and which project directory holds them is a filing detail. D1 now says the corpus stays in src/Heddle.Tests/TestTemplate/ and only the SHARING MECHANISM gets built -- one props file, Content links, a per-consumer copy in each consumer's own output. That is what lets the generator suites consume the same files the engine suites already use, which is what makes the ~130 inline-string feature templates cross the precompiled gauntlet. The folder layout was never the point; that gap is. The superseded text is kept collapsed rather than deleted, and the reversal's cost is stated: the move was executed once and reverted, preserved at branch wip/phase7-corpus-move. No test changed meaning, because renaming an input the build copies anyway is invisible to every reader. testing-standards gains a sentence saying a build copy is not a second home -- the rule kept being read as a storage-location rule, twice now, which makes the wording the defect rather than the readings. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
A comment explains the code and never cites a document. No inline comment, XML doc or test doc may reference a spec, plan, phase, decision id, open-question number, ledger entry, commit or ticket. If the reason needs a name, name the BEHAVIOUR, not the document that ratified it. The reference direction is one-way and the rule says so: specs point at code -- that makes a rule findable from the document -- while code pointing back duplicates the decision into a place nothing keeps in sync, and turns every renumbering into a small lie in a dozen files. Brevity is part of the rule; a comment restating the line below it is noise, and none beats ceremonial. Three exceptions earn their length: a non-obvious why, a test doc naming the scenario it pins (the scenario, not its provenance), and an XML summary describing a contract to a caller who cannot see the spec. Applies to existing code on touch: strip the citations you find, do not preserve them for symmetry. Also strips the citations from the two fixtures I wrote this session, which is where I kept breaking this rule while agreeing to it. Repo-wide there are ~1074 citations across 432 files; that sweep runs as its own pass once the corpus work releases the tree, because it touches nearly every file and must not collide. Verified: DiagnosticIdTests and VersionConsistencyTests 22/22 on net8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The corpus stays in src/Heddle.Tests/TestTemplate/. src/TestCorpus/ holds WIRING ONLY -- a props file two test projects import, globbing the templates as Content with Link and CopyToOutputDirectory so each consumer gets its own build copy. That replaces 112 hand-listed <None Update> rows with one glob; audited at the swap, list and directory agreed 112-to-112, so nothing had drifted yet and nothing prevented it either. The bin traversal is deleted rather than hardened: no test walks out of its own bin into a sibling project any more, and the five silent dir == null returns are gone with it. Heddle.Tests.dll is copied into the integration suite's own output, so that lookup is AppContext.BaseDirectory like everything else. Four count gates became set equality, one more than the plan's survey found -- ParticipantScanLockstepTests' Assert.Equal(62, files.Count) was missed. A count goes green by editing one digit and the commit looks the same either way; set equality can only go green by naming the drifting file and writing why in its row. One literal survives, DeclaredRowCount. All four validation scenarios rehearsed, including mutating the TABLE rather than the code -- still red, still names the file. Six written artifacts relocated to a TestOutput/ folder, hash-verified identical, and all 25 write sites across 14 files repointed at the writer's own output directory -- the plan expected 6. Runtime writes no longer land in the source tree, which is what let the output corpus dir accumulate 148 files against a tracked 106. Three findings recorded rather than folded in: DegradesToMarker had zero members and was never asserted (all 17 non-precompiling entries are Absent, and the suite computed a markers set it never checked); the sweep byte-compared 10 entries where 32 render standalone identically, because a blanket render:false cost all 40 their byte assertion for the sake of 8; and the corpus's 8 BOM-bearing templates now exercise the staging path that a synthetic fixture already covered in shape only. Stages 1-5 are STOPPED, not skipped -- see the following commit's register entries. Suite green: Heddle.Tests 1817/0, Generator.IntegrationTests 407/0, Generator.Tests 448/0 on net8.0. Corpus suites run ~4 s/TFM against a 15 s budget, down from ~5.8 s despite 22 more byte-compared renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Left over from the corpus work: TestOutput/ needed the same eol=lf pin its predecessor had, or a Windows checkout would rewrite the six relocated artifacts and break byte comparison. The accompanying note is written to the new comment rule -- it explains that a BOM is independent of the line-ending pin and points at the gate, without citing a phase or a decision id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Extends the comment rule with the part that decides whether a comment should be written at all. Default to none: before writing one, try to make it unnecessary -- a clearer name, a named local, an extracted method whose name is the sentence you were about to write, an early return that removes the case you were about to explain. The comment is the last resort, not the polite thing to add on the way past. Three kinds earn their place: a why no naming can express, a constraint a future edit would silently violate, and a test's scenario where the name cannot carry it. Everything else -- restating the line below, narrating what is visible, section markers, documentation-shaped filler -- loses nothing on deletion and costs every future reader the time to find that out. Public API documentation is exempt from "only if necessary", because a public member is documented, but not from being useful. Its job is what a caller cannot see: contract, units, valid input, failure behaviour, ownership. Length is calibrated to that -- too long and the contract drowns, too short and it restates the signature while answering nothing. The test for all of it: read as someone who has never seen the change. If the comment tells them what the code cannot, keep it; if it tells them what they would have known anyway, delete it -- and where the code is why they would not have known, fix the code instead. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The requirements had accumulated across two edits as prose. They are now eight numbered rules an implementer can apply mechanically and a reviewer can cite: C1 default to none, C2 the three kinds that earn their place, C3 delete what carries no meaning, C4 never cite a document, C5 keep what survives short, C6 public API docs exempt from C1-C3 but not from being useful, C7 never weaken a test to satisfy C1-C5, C8 what is not a citation (crefs, diagnostic ids, member names). No requirement changed; the content is the same decisions, restructured so a sweep has a rule set rather than a narrative to interpret. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
444 files. 935 document citations removed, 278 comments deleted outright, 551 rewritten -- run as 53 Haiku agents over disjoint areas, each returning a structured count rather than prose, so an agent that did nothing could not report completion. What went: phase and decision references (D-, F-, OQ-, WI-, Q8.x), ledger entries, spec and plan filenames, section markers, and comments that restated the line below them. What stayed: the reason attached to each citation, constraints a future edit would violate, test scenarios and the reasoning behind an assertion's shape, <see cref> links, and diagnostic ids -- those name code, not documents. One real breakage, found by building rather than by reading: an agent applied "delete the section marker" by stripping // from two divider lines and leaving the dashes as bare code, which is a compile error. Fixed here. It is the argument for the constraint the swarm ran under -- comment-only edits, no builds -- since 53 concurrent MSBuild runs would have corrupted each other and hidden this behind noise instead of surfacing it in one clean build. Verified after the fix: solution builds with 0 errors (the 8 remaining warnings are pre-existing NU1510 package-pruning notes). Suites on net8.0 -- Heddle.Tests 1817/0, Generator.Tests 448/0, Generator.IntegrationTests 407/0, LanguageServices.Tests 188/0, Tool.Tests 6/0. Not finished: ~155 citations survive, concentrated in the generator and the two largest test projects. A second pass closes those. The swarm was also denied C1's "fix the code instead of commenting" option -- renames and extractions can change behaviour, and that judgement does not belong in agents running without a build -- so those cases were collected as notes and remain to be done deliberately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
51 files, 164 more citations removed, 27 comments deleted, 134 rewritten. Areas were cut to seven files each: the survivors clustered in the largest, most heavily commented files, where a twenty-five-file agent had been skimming. Also swept the corpus intent table's Why column. Those citations live in string literals, so the sweep agents correctly refused to touch them under their comment-text-only constraint and flagged them instead -- but that column IS the rationale, and a citation rots there exactly as it does in a comment. Rewritten to state the behaviour each fixture exercises. The commented-out code in ParseContext was removed rather than rewritten: a commented-out property and a commented-out initializer are comments carrying no meaning, which is what C3 is for. One area of ten returned no structured result. Its files are re-checked in the next pass rather than assumed done. Verified: solution builds with 0 errors; Heddle.Tests 1817/0, Generator.Tests 448/0, Generator.IntegrationTests 407/0 on net8.0. The corpus intent gate passes, which is what proves the Why rewrites did not break the set-equality declaration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Q8.42 -- option 2 with relocation permitted for genuinely cross-project artifacts. An artifact more than one project needs may live in a common folder and be linked; a test whose two tiers feed different inputs and assert different outputs is not two copies of one test and is not touched. The fixture-model unification is DECLINED: reconciling the region models and then choosing short-name vs AQN spelling would change what each suite tests in order to make the text identical. The plan's "18 character-for-character duplicates" is corrected to 18 shared substrings. This closes phase 0's D4 residue rather than leaving it open. The residue asked that feature templates cross the gauntlet; the answer is that they do where the generator-matches-runtime requirement applies, and are left alone where it does not -- a per-fixture judgement recorded in the intent table, not a backlog. Q8.43 -- fix it, by populating the tier: a delegate-only function really does yield a manifest row with a null entry point, so the marker tier describes real behaviour and should contain something. The defect worth not repeating is that the suite computed a markers set and asserted nothing about it, so an emptied bucket looked healthy. Q8.44 -- same rule: two consumers justify the shared props file; a third is added when it needs the corpus, never for symmetry. No change today. Docs only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Eight files, at one to three files per agent so the largest generator sources got undivided attention. TemplateEmitter and HeddleTemplateGenerator each had an agent to themselves: they carry the emitted-code comments, where an edit changes generated output and breaks snapshot goldens rather than just reading badly. Verified: solution builds with 0 errors; Heddle.Tests 1817/0, Generator.Tests 448/0, Generator.IntegrationTests 407/0 on net8.0 -- including the snapshot suites that would have caught an emitted-comment edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Zero document citations remain in source. What is left is deliberate: two references to C# language-standard section 10.2.3, which is an external standard rather than one of our documents, and three doc-comment strings the generator EMITS -- those were left because editing them moves the snapshot goldens, and that belongs in a change that reviews the golden diff rather than riding along here. These files needed a stronger reader than the swarm. A scripted attempt was made and reverted: most of the remaining citations sat inside multi-line comment blocks, so line-at-a-time editing capitalized continuation lines and cut clauses mid-sentence. The unit of work is the block, not the line -- TemplateEmitter alone held 68 of them. Worth stating plainly, because it sets up the next pass: NOTHING was deleted here, only rewritten. Three swarm passes and this one have removed ~1100 citations and deleted barely 300 comments, against 10,925 comment lines over 52,748 lines of code. C4 is satisfied; C1, C2, C3 and C5 are essentially untouched, and several files still carry more comment than code -- PrecompiledSchema 90 comment lines to 35 of code, HeddleDiagnosticIds 163 to 91. Build: 0 errors. Suites on net8.0: Heddle.Tests 1817/0, Generator.Tests 448/0, Generator.IntegrationTests 407/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
7470 comment lines, down from 10,925 -- a third of them gone. 312 files, 665 comments deleted outright, 973 shortened. This is the pass the earlier sweeps skipped: four passes had removed ~1100 document citations and deleted barely 300 comments, so C4 was satisfied while C1, C2, C3 and C5 went untouched. What changed in the brief is why it worked. The earlier sweeps preserved everything because every comment was accurate, and accuracy reads like justification. This one states outright that accuracy is not the test, necessity is: if deleting a comment loses nothing a reader of the code needs, it goes. It also named the deletions to make without hesitation -- restatements of the line below, section banners, XML docs paraphrasing the method name, restatements above an assertion, multi-paragraph design essays, sequence narration -- and required before/after comment-line counts so the result could be measured instead of described. The twelve heaviest files each got their own agent: TemplateEmitter at 532 comment lines, HeddleCompiler at 221, HeddleTemplateGenerator at 172. Those are where the volume was and exactly where a six-file agent skims. Individual reductions ran deep -- a 14-line element-walk explanation to 2, a 15-line #line rationale to 4, a 19-line DynamicMember summary to 8 keeping the binder-context security reasoning and dropping the history. What was kept, deliberately: guards against real failures, orderings that matter, constraints a future edit would silently violate, the reason an assertion is shaped as it is, thread-safety and ownership contracts, and public API docs reduced to the contract a caller cannot see. Design arguments moved out; their consequences stayed. Verified: 0 build errors. Suites -- Heddle.Tests 1817/0, Generator.Tests 448/0, Generator.IntegrationTests 407/0, LanguageServices.Tests 188/0, Tool.Tests 6/0. The snapshot suites passing is what proves no emitted comment moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The engine loaded the entry assembly's whole transitive reference closure plus every DependencyContext default assembly name from a static constructor, with loader failures swallowed, then scanned that set for [ExportExtensions]. Because the scanned set decides extension name ownership, an assembly the integration layer never chose to load could take a name or throw TemplateOverrideException out of a type initializer. The engine now loads nothing. It observes assemblies the host already loaded from disk into the default context, and takes anything else — an in-memory assembly, a collectible context, an extension provider — only by explicit registration. [ExportExtensions] is read at registration time, per assembly, so a loaded-but-unregistered assembly offers nothing. HeddleTemplate.Register(Assembly) is the seam; Configure is kept as its alias, and its one-shot latch is gone, so repeated calls now take effect rather than being silently dropped. Also removes the Microsoft.Extensions.DependencyModel package reference (the walk was its only consumer), three dead PlatformAbstractions usings it made compile, and AssemblyHelper's unreachable type-scan surface (GetAllTypes, GetTypesByCustomAttribute and friends had no callers). Heddle.Generator.IntegrationTests registers itself for its [ExportExtensions] fixtures — the pattern a host now follows. All ten samples already called Configure and needed no change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…scovery The 2.1 window record gains row 10 with the disposition the ruling required: what stops working (a host exporting extensions from an assembly it never registers, and a template naming a type in an assembly it has never touched), and what to call instead. The disposition lands here rather than in breaking-windows.md, which is instruction-only and holds no landed items — noted in the row so the trail is explicit. D11's known-violation paragraph is replaced by how the engine now obtains its assembly set: observation of the host's default-context, on-disk assemblies, plus registration, and no third source. Generator-plan finding 3 is closed in two parts. Its generator half was already false when written — Q8.4 had aligned ExtensionBinder to [ExportExtensions] — and the residue that remains is irreducible rather than drift: the build binds what the compilation references because that is the only fact it has, and the gauntlet reconciles per request with a reported ExtensionBindingMismatch. Phase 8 WI17 lands here too, as precompilation.md's startup-order section: it describes the API this change added, so shipping it in the docs phase would have described a seam that did not exist yet. getting-started, csharp-api, custom-extensions, docs/README and editor-support all told hosts that Configure walks an assembly's references. It never will again, so they now say registration is per assembly and not transitive, with a migration note. Also corrects FunctionRegistry.RegisterFrom, an instance method the editor-support page spelled as static. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…ion entry DegradesToMarker described a manifest row with a null strategy and had zero members, so the whole-corpus set equality was pinning an empty set — and before stage 0 the suite computed the markers set and asserted nothing at all about it, which is why a bucket that had emptied out looked healthy. fn-unresolvable-marker.heddle calls a function resolvable from neither the default table nor any referenced export, the only construct that produces a marker row rather than no row. Declared ResolveOnly: it renders only against a host that registers the delegate, so no shared harness renders it, and UnresolvableFunctionTests owns its classification and its HED7014 directly. Checked by mis-declaring the row as FallsBackSafely, which reddens the set equality — the assertion is load-bearing, not decorative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
… place The "18 distinct template literals appear character-for-character in both suites" claim is corrected wherever it was asserted — the phase 7 thesis, its external grounding, phase 0's D4 residue note, and the E9 ledger entry. They are shared substrings: re-measured, 31 literals over 14 characters, ~21 template-ish, and none a whole template on either side. The overstatement mattered because it made stage 1 look mechanical when single-sourcing it would have meant unifying two projects' fixture models and changing what each suite tests. Stages 1-5 are now recorded as ruled not to run rather than stopped with cause, with the disposition spelled out: the fixture-model unification is declined, tests fed different inputs and asserted against different outputs are not touched, and shared output vectors keep the linked-file pattern that already solves them. Phase 0's D4 coverage residue closes on that ruling. The ~130-feature- tests figure is retired as a debt measure — an inline template is a per-fixture judgement, not a backlog item, and the judgement is recorded in the intent table for everything that is genuinely shared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The comment sweep was scoped to .cs, so every csproj, props, targets, .gitattributes and .gitignore comment still cited plans, phases, decisions and question numbers — the same C4 violation, in the same repo, in files the sweep never opened. Reasons kept, citations gone, and the long ones cut: TestCorpus.props loses 20 lines of argument it no longer needs to win, Generator.props' metadata block keeps every constraint a future edit would violate and drops the history of how it got there. src/Heddle.Performance is untouched, per its change-nothing ruling. Also removes "(phase 7 D11)" and "(phase 8)" from the three doc comments the emitter writes into generated code. Those shipped into every consumer's build output, which makes them the worst place for an internal citation to sit. Five snapshot goldens are re-ratified in the same change; all suites and all ten sample goldens are green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
The first sweep read .cs only, which is how every csproj and props file kept its citations. Naming the scope in the rule set is what stops the next sweep from having the same blind spot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (10)
Remove unsupported dynamic compilation from the NativeAOT path · New Add a dedicated NativeAOT publish and golden comparison CI path · New Correct the Fluid.Core version in the benchmark comment · New Restore documentation for the public encoding-control attribute · New Document the public attribute and its template-name constructor contract · New Restore documentation for the public parsing exception · New Restore documentation for the public properties · New Document ProcessData's computed-value contract · New Restore XML documentation for the public extension interface · New Document the public field's null-as-empty behavior · New
Resolved since last review (13)
Bound expression nesting depth during decoding Reject overflowing variable-length integers Use request-specific strategy options Track assembly identity for registration idempotence Scope member validation to each template Preserve default C# namespace imports Exclude opted-out templates from artifact entries Invalidate incremental builds when compiler options change Separate non-precompiled items in design-time builds Document property exclusion attribute behavior Document the extension main data type Restore extension data type documentation Remove stale test package attribution
… native binary Every public declaration that had lost its XML summary carries an accurate one again: the extension attributes, TemplateParseException, CallParameter and its shapes, the expression nodes, DocumentElement, FunctionRegistry.Contains, NumericPromotion.TryPromote, IExtension's lifecycle, IDataProcessor's computed-value contract and Replacement. The precompiled-aot sample pins the host, the engine and its parser runtime out of the loaded assemblies and nothing else: the JIT's dynamic-methods assembly and lazily loaded helpers described the runtime, not the host, so the published native binary could not capture green on that file. samples.yml gains an aot job that publishes the linux-x64 native binary and captures with it against the same goldens. The third-party benchmark comment names the Fluid.Core package actually pinned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
One or more issues must be addressed before approval.
Review effort: Balanced
Findings: None
Resolved since last review (10)
Remove unsupported dynamic compilation from the NativeAOT path Add a dedicated NativeAOT publish and golden comparison CI path Document the public field's null-as-empty behavior Restore XML documentation for the public extension interface Document ProcessData's computed-value contract Restore documentation for the public properties Restore documentation for the public parsing exception Document the public attribute and its template-name constructor contract Restore documentation for the public encoding-control attribute Correct the Fluid.Core version in the benchmark comment
…ets, loader bounds Sandbox. The most-derived declaration of a name decides whether a template may read it: a [Hidden] override, a non-public or static `new` property, or a field or method of that name ends the member walk with HED0001 instead of binding the base property. [Hidden] is matched by metadata name, so a second loaded copy of the engine still hides. Indexers follow the same rule for the type's real indexer only; an explicit interface indexer withholds nothing. Printed native expressions evaluate as the engine does. Grouping comes from node kinds, null-safe hops fold into the expression so guarded operands stay lazy and side effects keep their order, site bodies are unchecked, reference comparisons print as reference comparisons and bound operators are pinned, constant subtrees print the value the engine computed (a throwing or culture-formatted one is not folded), and literals go through the shared escaper, which now escapes U+0085, U+2028 and U+2029. Generated source is global::-qualified, wrapper members cannot collide with a template name, and a key that sanitizes to Generate is HED7010. Type names spell closed and nested generics and mixed-rank arrays correctly; an internal model is named only when the consumer assembly can see it, otherwise the wrapper takes object. Engine. EmptyExtension.ProcessData returns text again and a standalone function call in a chain is typed as its carrier, so processed bodies keep non-string values and no chain throws. A bodiless @list on the process path contributes nothing instead of throwing. A bodied standalone function call records its body in the compiled form. One unloadable type no longer hides its assembly's other types from type resolution. The model type of a precompiled row resolves lazily and retries; replayed preparse diagnostics keep their severity. Compiled form. The header template count is a sized scalar, so an artifact with more than 32 templates loads. Heddle.Build. The intermediate model compile uses the project's output type, signing, features, analyzer config and additional files, excludes only Heddle's own generated sources, runs only when its content digest moves, and rebuilds a damaged image. A model edit recompiles the templates; stale model directories are pruned and clean removes every output. Item paths resolve against the project, not the template root. Templates are up-to-date-check and watch inputs. The host learns the consumer's assembly name. BuildSurfaceContractTests still forbids any compiler-visible declaration and anything reaching the real compile; it now allows exactly the one forwarding of the project's own AdditionalFiles to the intermediate pass. The codegen-t4-successor golden is regenerated through the sample's capture: every changed line is the global:: spelling, the reference null test or a renamed private member. NativeOperatorRules moves to the test project, where its lockstep sweep lives; nothing in the product consulted it. Comments across the product name behaviour instead of citing plans, phases or decisions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…nce, generated C# names
Parsing a page over a definitions library no longer costs the calls
times the square of the definitions. Each output chain's isolated view
of the definitions visible where it was written shares its source's
history up to the point it was taken, and copies a definition's body,
the chains and the raw items only when they are first read. What is
read late reads as it stood when the view was taken: the table, each
definition's position, body and whole base chain, and each chain's
position. Diagnostics, rendered output and the stored compiled form are
unchanged across the corpus, and reading a compiled template's parse
tree from several threads stays safe. A member path read many times in
one compile is compiled to a delegate once per compile.
The editor's parse tries the fast prediction mode first, as a compile
always has, and parses again in the exact mode only when the first
attempt reports an error, so its diagnostics and their order are those
of the exact parse. That second parse used to begin by clearing the
error list, which an import shares with its importer, so a syntax error
in an imported file erased everything reported before it, at run time
too; only the first attempt's own errors are dropped now.
The language server no longer loads a second engine beside the
workspace's models, whether found next to them or listed in
`assemblies`, so the workspace's `[Hidden]` and export attributes are
the engine's own. It survives a workspace built against a newer engine:
an export or a member it cannot read is skipped and named in the log
rather than thrown out of `initialize` or completion, a member whose
attributes cannot be listed stays hidden unless its metadata proves
otherwise, and every request answers or logs instead of faulting.
Exports are read from the loaded model assemblies on every workspace
load, so a workspace configured later offers its own. An expression
outside every body is now a scope like any body, and completion keeps
the document's members after a literal `{{` and inside a body still
being typed.
Generated C# spells nested, generic and keyword-named model types and
namespaces, escapes every identifier it takes from a consumer's
metadata, and the embedded-C# tier carries the model, chained and root
types. A model declared by an opted-out library that a compiled
template imports still starts the intermediate model compile; when that
compile fails the build says so with HED7038 instead of leaving bare
C# errors from a hidden target; and the not-fully-precompiled notice is
visible in an ordinary build. Non-public types in printed sites, and
the strict-load and AOT hosts that refuse them, are documented as a
limitation. The build host resolves a consumer image's satellite
resources and native libraries again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…ody is published safely A published NativeAOT app serves reflection from a form that has no metadata tokens and throws when asked for one, so keying a member path by its hops' tokens made every member read of a dynamic-tier compile there a failed compile, and the AOT sample aborted on its dynamic twin. A hop is hashed by its own hash code instead, which every reflection implementation answers and which agrees with the equality the key already used: hops are still compared as the reflection objects they are, never by name. A test keys a path whose member refuses its token, and another holds the compile path to that one reader of a token. A definition's deferred body context was read without a barrier outside the lock that publishes it, so a reader that found no context and then found the promise already kept could be handed the null it had loaded first: the net48 JIT reuses that load where the modern one repeats it. The copy is published, and the promise cleared, in the order the getter reads them, and every read of the two takes a barrier. The copy is still made outside the lock, because building one loads types and nothing may hold a lock across that. A view of a context takes the first n entries of a list as it stands when it is first read, so a host that had removed entries after taking the view left it reading past the end. Each of those reads now takes what is left of its prefix, and the changelog says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…code The precompiled tier paid a full reflective validation on every template resolution and never cached its result, so a precompiled view cost about 1140x a dynamic cache hit. The verdict is now memoized per template, request shape and assembly generation; resolution allocates 336 B where it allocated 136 KB. A recorded option value that named no enum member silently selected the zero member, turning HTML encoding off. It is refused instead. A refused call inside a body was sliced from a document's local text with an absolute position, so it recorded text that did not parse and the template fell back to the dynamic tier. Slicing is offset-correct, the load side anchors fragment errors at their true source, and refusals inside an @<< import record from the imported file's own text. The compiled form's serialization types are internal: 42 types leave the public surface, which drops from 189 to 147 exported types. TemplateOptions.ValidateModelType, which no longer gated anything, is removed. Six dead types, a dead method and three single-implementer generic seams go; the remaining seams collapse to Type. The artifact is decoded once per assembly rather than once per template, and its digest is verified in place rather than over a copy of the image. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
… import The branch had spent two schema increments, 3 and 4, on a form that never shipped. Both collapse into 3 — one step from the last released number. Schema 1 and 2 manifests are still rejected at registration. The stored fixture is regenerated from a real compile of the documented template and renamed to match. A recorded body was correlated by position and parsed template alone. Two @<< imports can put a bodied call at one offset of their own files, so one key met two calls and the first served both. A compiled item now carries its import anchor, and both the body map and the refusal map key on it, so a site is qualified by the import it came from. The recorded refusal position goes back to being the reported position rather than doubling as a discriminator. A definition body still compiles once per call site, so one key legitimately meets several recorded bodies of the same call; that is settled by the frame the loader serves under, not by the key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Microsoft.Build.Utilities.Core 17.8.3 carries CVE-2025-55247, a Linux denial of service through predictable MSBuild temporary directory paths. 17.8.43 is the first patched release on that line; staying on it keeps the minimum MSBuild a consumer needs where it was. The package is compile-only here. HeddleCompile derives from the MSBuild Task base class, and the pack layout ships only the task assembly and System.Reflection.Metadata, so the hosting MSBuild has always supplied its own copy and no vulnerable code shipped. What this removes is a standing build warning, in a log where a standing warning hides the next real one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
multiarc
force-pushed
the
feature/v3_plus_bench
branch
from
September 20, 2026 20:13
2ddd0e6 to
9dd8827
Compare
The build read files it never declared. Deleting a template, renaming one, or editing an @<< import that is not itself an item all left the compile target up to date, so a green build at exit 0 embedded and shipped the previous artifact; only Rebuild recovered. The host now records the paths it reads through the import disk fallback, folds their content into its stamp, and the targets read that list back as inputs and as editor up-to-date and watch inputs. The item list itself is written into the options file, so removing or renaming an item invalidates. The stamp records the hash of the text the parse was handed rather than re-reading the file afterwards: a save landing during a compile used to certify an artifact built from the bytes before it. A compile that cannot describe what it read now removes its stamp instead of writing one, so the next build recompiles rather than trusting it. The template root joins the digest, since it decides every key the artifact records and every file the disk fallback reaches while the item rows name absolute paths. Unreadable and absent inputs are classified in one place: absent is stable, unreadable salts. The task parameter that shadowed ToolTask.ToolPath is now ToolAssembly. The two named different things, a file against a directory, and which one MSBuild bound was never contractual. Test assertions that reported worse failures than they needed to say what they mean, and the two pack warnings that are structural for a tasks package carry their reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Every push to a pull request published something: a -beta.<run> set of
packages to nuget.org and a staged -beta.<run> Ace bundle to npmjs.org.
None of the staged npm versions was ever approved, about 126 of them
accumulated, and npm now refuses new ones with a bare 409 Conflict, which
is what turned this branch's CI red.
Pull requests and main now build, test and pack, and publish nothing
anywhere. Publishing is driven by the tag alone, resolved in one place
(release-tag.yml, over .github/scripts/resolve-release-tag.sh) that every
publishing job depends on:
vX.Y.Z a release; the tagged commit must be on main
vX.Y.Z-{alpha,beta,rc}.N a pre-release, from any branch
The number is dotted because SemVer compares "beta10" and "beta9" as text.
Any other v* tag, or a bare tag off main, fails before anything publishes.
The trigger pattern v*.*.* already matched suffixed tags, so until now a
pre-release tag would have published npm as latest and pushed a
Marketplace release.
A pre-release goes to nuget.org as a pre-release version, to npm under its
alpha, beta or rc dist-tag (never latest), to GitHub as a pre-release, and
to the Marketplace as a pre-release. The Marketplace takes only a plain
X.Y.Z and never reuses one, so a pre-release extension is numbered
X.Y.(Z*10000 + rank*1000 + N): 3.0.0-beta.2 ships as 3.0.2002. On a tag the
VSIX also stamps the tagged version into its bundled server and its
install hint.
The dotnet pack step still runs on every pull request, so a broken pack
shows up there rather than at release time. The DCO workflow stays the
required check; it loses the workflow_call role it had only to gate the
beta publishes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Two defects in the release pipeline, both invisible to CI because every publishing job is skipped on a pull request: a green run says nothing about them. The Marketplace publish named its packages relative to the step's working directory. The artifacts download to $GITHUB_WORKSPACE/vsix and the step runs in editors/vscode, so ../vsix was editors/vsix, a directory nothing creates. The tag-driven extension release could not have published a single package, with a valid token and seven freshly built VSIXs on disk. Packages are now named from the workspace root, and a discovery step fails loudly unless one package per target arrived, rather than handing vsce a glob that matched nothing. Publication also waited on the Debug suites alone. The Release leg ran in lsp.yml and the samples never ran for a tag at all, so a tag could publish over a red Release suite or a broken sample; the branch ruleset required neither. Jobs cannot depend on another workflow's jobs, so the Release leg moves into its own callable workflow: lsp.yml calls it instead of declaring it, and each publishing job waits on it. The NuGet packages also wait on the whole sample gallery, which is the engine's end-to-end test. The gates run only for a tag, so pull requests do not pay for them twice. Both fixes are pinned by WorkflowContractTests: a published package path is rooted at the workspace and names a directory an artifact download wrote, and every publishing job reaches the Release-suites job through its needs chain. Both were rehearsed red against the unfixed workflows, which is also how the first draft of the parser was caught reading a comment and a defaults block as jobs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…d not Five defects found by a multi-role review of the release pipeline. Every one of them is on a path CI never executes: publishing jobs are skipped on pull requests, and the tag resolver runs only when someone pushes a tag, so a green run said nothing about any of this. Each fix carries an instrument, and every instrument was rehearsed red. The release job calls gh without checking out. gh resolves the repository from --repo, then GH_REPO, then the git remotes of the working directory; it does not read GITHUB_REPOSITORY. Run outside a checkout it fails with "no git remotes found" - and it fails at the last step, after six packages are already public on nuget.org. With no --skip-duplicate, a re-run dies on the duplicate before reaching it, so that version would have been unrecoverable. GH_REPO now names the repository. Symbol packages were built and then filtered out. Three libraries opt into IncludeSymbols and the release artifact collected *.nupkg, which does not match *.snupkg; dotnet nuget push uploads a symbol package only from beside its package, so none has ever reached the publishing runner. Both globs are uploaded now. The docs workflow granted pages: write and id-token: write at workflow level, so the build job held them too - on pull requests, while running a downloaded ANTLR jar, npm ci and a browser install. The scopes moved to the job that deploys, which is the only one that needs them. The generated-grammar drift guard used git diff, which compares tracked files only: a regeneration that emitted a NEW file left it untracked and the guard reported no drift, while the stale checked-in tree went on being bundled into the npm package. It stages the intent first. The Marketplace version map stranded pre-release users. A release was numbered at its bare patch while a pre-release was Z*10000 + rank*1000 + N, so every pre-release in a minor line outranked every later release in it and the gallery would never offer a pre-release user a stable patch fix. Both sides scale now: v3.0.1 is 3.0.10000, above v3.0.0-rc.1 at 3.0.3001 and below v3.0.1-rc.1 at 3.0.13001. The displayed extension version no longer resembles the tag, which is the price of three integers and a channel that has to roll forward. The instruments: four facts in WorkflowContractTests, and a self-test that drives the resolver over a table of refs - which shapes publish, which are refused, which map to what, and that the numbers strictly increase - so its first execution is not a release. It runs in the build job. A git shim answers the two questions the resolver asks about main, so the table tests the decision rather than this clone's history. TheBetaVersionSuffixCarriesNoLeadingDash could not fail: it scanned one file for a flag this branch had already removed from every workflow, so it iterated nothing. It now scans every workflow in both spellings, and asserts the premise the rule rests on - that the version is still composed from a prefix - on every run. building.md also claimed any other v* tag fails before anything is published. A tag with fewer than two dots matches no trigger and starts no run at all; the sentence now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
A multi-role review of a5ed30b found that two of its fixes were wrong and one did not reach far enough. All three sit on paths a pull request cannot exercise, which is how they got past the gate that was green when they landed. Moving the Pages scopes onto the deploy job left `actions/configure-pages` running in `build` with `pages: none`. It reads GET /repos/{owner}/{repo}/pages, so it would have failed on the next push to main and taken the documentation deploy with it - and never on a pull request, because the step is skipped there. The step earns nothing in return: no `id:`, so nothing reads its base_url/base_path, it ran after the site was already built, and the site's base is fixed in the VitePress config. It is gone, and a new test requires any job using a Pages action to declare the scope itself, so the negative gate added last commit now has a positive counterpart. The drift guard traded one blind spot for another. Measured in a scratch repository: `git add --intent-to-add -A` makes a new file visible to `git diff`, and stages a deletion, which then hides it. `git status --porcelain` reports additions, modifications and deletions alike and is empty on a clean tree. All four copies use it now - the one in docs.yml and the three in benchmarks/ that the same class of defect had been sitting in, untouched, in two languages. The Marketplace map still stranded pre-release users, worst exactly where it mattered most. Scaling a release by patch*10000 is zero at patch 0, so v3.0.0 - the next release this repository will cut - mapped to 3.0.0, below every pre-release of the whole 3.0 line. A release now maps to patch*10000 + 9999, which sits above its own pre-releases (at most +3999) and below the next patch's, so in release order the numbers are 1001, 2002, 3001, 9999, 13001, 19999, 29999 - strictly increasing, and a pre-release user is offered the release they were testing. v3.0.1 publishes as extension 3.0.19999. Three instruments were weaker than they read. The self-test's ordering table was sorted by the resolver's own output, which made the assertion a tautology; it is in release order now, and rehearsing it against the previous mapping prints the defect directly (1001 2002 3001 0 13001 10000 20000). Its loop dropped a ref whose value came back empty, because `[ "" -le N ]` exits 2 and an empty word vanishes under splitting; both are checked. And the self-test only gated the NuGet release: jobs cannot depend on another workflow's jobs, so it moves into release-tag.yml, which all three publishing workflows call, while staying in the build matrix that gates merges. A test asserts every *-selftest.sh is invoked by some workflow, since the one line keeping an instrument alive was the one line nothing pinned. Smaller corrections in the same pass: the version-suffix test's "premise" assertion re-checked a pattern the accessor it called had already asserted, so it could not fail; release-tag.yml still carried the sentence about malformed tags that docs/building.md corrected; lsp.yml still described only pre-releases as renumbered; and the benchmark docs still quoted the old drift command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
multiarc
had a problem deploying
to
marketplace
September 23, 2026 20:11 — with
GitHub Actions
Failure
The Marketplace publish was already gated by the `marketplace` environment; nuget.org and npm were not. So "which refs may publish" was a rule implied separately by three workflows, and only one of them was enforced by a setting. The first real release tag found the gap from the other side: the environment's policy admitted protected branches only, which no tag can satisfy, so marketplace-publish was rejected before its first step while nuget.org and npm published normally. All three publishing jobs now run in the same environment. Its deployment policy admits the `main` branch and tags matching `v*.*.*` - the same glob the three workflows trigger on - so the ref rule lives in one place and covers the three registries together. This changes the OIDC subject claim for the two Trusted Publishing paths from repo:<owner>/<repo>:ref:refs/tags/<tag> to repo:<owner>/<repo>:environment:marketplace, so the nuget.org and npm policies have to name the environment as well; one configured without it refuses the token. Both were updated before this landed. The failure would be at the token exchange, before any bytes ship, but it would take all three registries at once rather than one. EveryPublishingJobRunsInTheSameEnvironment pins both halves: a publishing job that declares no environment, and publishing jobs that declare different ones. Rehearsed red both ways. building.md also said pushes to main "never publish anything" four lines before describing the docs site deploying from main. It says "never publish a package". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
v3.0.0-beta.2's Marketplace leg failed with actions/checkout unable to verify GitHub's certificate on the linux-arm64 runner - a trust-store fault in the runner image, retried three times. Because the vsix matrix took the default fail-fast, that one leg cancelled the other six and marketplace-publish was skipped, so a transient infrastructure blip on one platform failed the release for all seven. The re-run, with nothing else changed, was green on every target including linux-arm64. The vsix matrix was the only one in the repository without fail-fast: false. dotnet.yml has it on the build matrix and samples.yml has it with the reason written down - "one broken sample must not mask the others" - which is the same reason here: the seven targets share nothing but a workflow. MatrixLegsDoNotCancelTheirSiblings asserts it for every matrix job, so the next one cannot omit it. Rehearsed red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
vsce warns on every one of the seven per-target VSIXs that the extension ships 356 files, 182 of them JavaScript. One runtime dependency, vscode-languageclient, resolves to nine packages, main points at raw tsc output, and .vscodeignore never excluded node_modules - so the whole tree ships loose, seven times over. 181 of the 182 JavaScript files are dependencies; one is ours. The plan is written down rather than executed because the non-obvious part is worth agreeing first: tsc and a bundler both write dist/extension.js, whichever runs last wins, and npm run compile would silently replace a bundle with unbundled output - visible only at F5 or in a shipped VSIX. The resolution is noEmit in tsconfig.json so dist/ belongs to the bundler alone and no invocation of tsc can clobber it, with type checking kept as its own step because esbuild does not type-check. Also recorded: why platform: 'node' is mandatory rather than stylistic (the language client exports ./node under the node condition with no default), why the output stays unminified (no .map ships, so an unminified stack trace is all a bug report carries), the denylist .vscodeignore as the low-risk primary with the allowlist as optional hardening to validate by packaging, and the risks checked against the installed dependency tree rather than assumed. CI needs no change: vsce package triggers vscode:prepublish, and the existing step order already stamps PINNED_VERSION into the source before packaging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
Every per-target VSIX shipped vscode-languageclient's whole dependency tree as loose files, because main pointed at raw tsc output and .vscodeignore never excluded node_modules. esbuild now collapses it into one dist/extension.js. The linux-x64 VSIX goes from 263 entries and 182 JavaScript files to 8 entries and one, and vsce's bundling warning is gone. (The plan's 356 predated the existing dist/test and map exclusions; the JavaScript count matched.) tsconfig.json is now noEmit, so dist/ belongs to the bundler alone and no tsc invocation in the directory can overwrite the bundle with unbundled output. Type checking survives as check-types, and vscode:prepublish runs it before bundle because esbuild does not type-check and vsce runs only that hook. watch is the bundler now; under noEmit a tsc watch would leave F5 loading a stale or missing file. esbuild is pinned exactly, like vsce, since it produces the shipped bytes, and the lockfile carries the Linux x64 and arm64 binaries that npm ci on ubuntu-latest needs. The bundle was loaded under plain node with a stubbed vscode module and exported exactly activate and deactivate. That evaluates the languageclient -> jsonrpc -> RAL.install chain, where a bundling defect would throw. The packaged VSIX installed with dist/ holding only extension.js and no node_modules. The GUI leg - coloring, the install hint, a live server, Restart Language Server - has not been run yet. VsCodeExtensionPackagingTests pins the chain whose broken link would install cleanly and throw at activation: node_modules excluded, main naming the bundle with prepublish running check-types and bundle, the bundler's options, and tsconfig emitting nothing. Rehearsed red by dropping bundle from prepublish. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
…ore upload Bundling took vscode-languageclient's nine packages out of the VSIX as loose files, and their License/LICENSE files went with them; esbuild keeps only /*! and @license comments, which none of them carries. Every per-target VSIX would have redistributed MIT, ISC and BlueOak code with no notice at all. bundle.js now writes dist/ThirdPartyNotices.txt from the packages the metafile says went into the bundle, and fails the build when one has no license text. The bundle's bytes are unchanged. Nothing could see a bundle that packages cleanly and throws at activation: the pins read source text, and vsce refuses only a missing entry point. `packages: 'external'` in bundle.js kept all four pins green and shipped a VSIX that fails with "Cannot find module 'vscode-languageclient/node'". build/verify-vsix.js now runs on every packaged VSIX in the vsix job: it loads the bundle against a stubbed vscode module from outside any project, requires exactly activate,deactivate, no node_modules, one JavaScript file, notices for every bundled package, and no source map it does not ship. The pins now read whole lines and exact script bodies, and a new one requires the check to be the last line of an unskippable step between the last vsce package and the upload. The Marketplace job's npm ci ran every dependency's install script with VSCE_PAT in the environment - esbuild's now among them, in a job that never bundles. Scoping the token to steps is not enough, since an install script can plant code the token steps then run, so the job sets npm_config_ignore_scripts (which covers npx too) and the token reaches only verify-pat and publish; vsce verify-pat and publish both reach the Marketplace without the scripts. NoJobHoldingACredentialRunsInstallScripts holds every job with a secret, the job token or a write permission to it. BUNDLING-PLAN.md records what the review changed and corrects its figures: 356 files included server/, the bundle is ~950 KB, and nothing gates activation, so the RAL install is not proven by any check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Aliaksandr Kukrash <multiarc@gmail.com>
This branch was successfully deployed
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
Heddle 3.0.0. The precompilation build tier runs on a stored compiled form produced by the real engine, the Roslyn source generator is gone, and the repository carries a cross-stack benchmark harness, a shared test corpus, and tag-driven publishing for every artifact. No rendered byte changes on either tier. 3.0 is a ratified breaking window; the full item list with migration guidance is the
[3.0.0]section ofCHANGELOG.md.Engine and precompilation
heddle compilecompiles each template through the engine at build time and stores the compiled graph (Heddle.CompiledForm, schema 4). The loader re-materializes the engine's own objects; only schema 4 is accepted, and a lower schema throwsPrecompiledRegistrationExceptionat registration.Heddle.Build. MSBuild package that drives the out-of-processheddle compilehost. Same<HeddleTemplate>items, per-itemKey/Name/Precompilemetadata now takes effect,Heddle.Generated.<Name>.Generate(...)typed entry points bind underPrecompiledTemplates.DefaultOptions/BindTyped. A template the build cannot precompile is left out withHED7031and renders through the dynamic path.IPrecompiledSiteTable) printed from the engine's bound trees;TemplateOptions.PrecompiledStrictLoad/PrecompiledStrictLoadException. The engine is trim- and AOT-clean (IsTrimmable,IsAotCompatible, IL analyzer warnings are errors);samples/precompiled-aotpublishes a native binary that renders its goldens under strict load.Heddle.Generator*projects and packages,Heddle.Performance, every member whose only caller was generated 2.x code, and assembly auto-loading: the engine reads what the host has loaded plus what it registers throughHeddleTemplate.Register.PrecompiledFallbackEvent.Keyis replaced byTemplateKey/AssemblyName;MemberBindingMismatchis a must-surface fallback reason.HED7037names a retired MSBuild property;PrecompiledRefusalClassgivesHED7031a machine-readable category.Benchmarks
benchmarks/is a six-ecosystem harness (.NET, Rust, JVM, JS, Python, Go): eight workloads, controlled and idiomatic tracks per engine, one golden oracle corpus exported from Heddle with a hash manifest, a byte gate and functional verifiers per ecosystem,run-all.ps1/run-all.shand the Linux cross-check tooling. The corpus and every template are brandless.benchmarks/docs/.Tests and gates
Heddle.Tests,Heddle.LanguageServices.Tests,Heddle.Tool.Tests,Heddle.Build.Tests, each gating its membership fromtest-classes.txt, all on net48 / net8.0 / net10.0. The shared corpus (src/TestCorpus) declares every fixture's intent and tier.Docs, samples, editors
docs/spec/common/.samples/each run in CI with golden comparison, includingprecompiled-appandprecompiled-aot.multiarcpublisher with README, LICENSE and packaging ignore file; per-target VSIXs bundle the language server.CI
dotnet.ymlbuilds and runs the four suites on Ubuntu and Windows; internal PRs publish beta NuGet packages andv*.*.*tags publish releases, both through NuGet trusted publishing.npm.ymlpublishes the Ace bundle through npm trusted publishing;lsp.ymlbuilds, tests, packs the LSP tool, packages the seven VSIXs and publishes them to the Visual Studio Marketplace on a release tag using theVSCE_PATsecret held by themarketplaceenvironment.samples.yml,docs.yml,dco.ymlas before; every contributed PR is DCO-gated before anything is published.How was this tested?
The merge gate as CI runs it, on Windows 11 with the SDK pinned in
global.json:dotnet build -c Release: zero errors and zero IL analyzer warnings..github/scripts/dotnet-test-guarded.shin Debug and Release on every target framework, eachtest-classes.txtgate green.src/Heddle.Language/generated/unchanged.samples.yml: capture andcompare-golden.shgreen.verify-corpusandgatefor .NET, plus the Rust, JVM, JS, Python and Go gates.cd docs && npm run docs:buildgreen; repository-wide link and anchor check clean.vsce packageand ships only the manifest, README, LICENSE, grammar, language configuration and compiled entry point.Checklist
dotnet test src/Heddle.Testspasses locally..editorconfig/ surrounding code style.git commit -s).AI-assistance disclosure
Parts of this branch were produced with Claude Code under a criteria-bound review against the specs; the gate above was run locally on the final tree.
🤖 Generated with Claude Code