Skip to content

Precompilation v2: compiled form, Heddle.Build, generated sites (3.0.0) - #60

Open
multiarc wants to merge 282 commits into
mainfrom
feature/v3_plus_bench
Open

multiarc wants to merge 282 commits into
mainfrom
feature/v3_plus_bench

Conversation

@multiarc

@multiarc multiarc commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

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 of CHANGELOG.md.

Engine and precompilation

  • Compiled form. heddle compile compiles 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 throws PrecompiledRegistrationException at registration.
  • Heddle.Build. MSBuild package that drives the out-of-process heddle compile host. Same <HeddleTemplate> items, per-item Key/Name/Precompile metadata now takes effect, Heddle.Generated.<Name>.Generate(...) typed entry points bind under PrecompiledTemplates.DefaultOptions / BindTyped. A template the build cannot precompile is left out with HED7031 and renders through the dynamic path.
  • Generated sites and strict load. Typed site table (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-aot publishes a native binary that renders its goldens under strict load.
  • Removed. 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 through HeddleTemplate.Register. PrecompiledFallbackEvent.Key is replaced by TemplateKey / AssemblyName; MemberBindingMismatch is a must-surface fallback reason.
  • Diagnostics. Build ids are re-keyed onto engine ids; retired ids stay in place in constants, catalog and registry; HED7037 names a retired MSBuild property; PrecompiledRefusalClass gives HED7031 a 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.sh and the Linux cross-check tooling. The corpus and every template are brandless.
  • Measurements are not stored in the repository; the harness contract lives in benchmarks/docs/.

Tests and gates

  • Suites run on xUnit v3 over Microsoft Testing Platform: Heddle.Tests, Heddle.LanguageServices.Tests, Heddle.Tool.Tests, Heddle.Build.Tests, each gating its membership from test-classes.txt, all on net48 / net8.0 / net10.0. The shared corpus (src/TestCorpus) declares every fixture's intent and tier.
  • Gated by tests: every documented member against the public API golden, every documentation link and anchor, every diagnostic id and catalog row, version consistency across projects, extension settings against the language server's option surface.

Docs, samples, editors

  • Site pages describe the current state only; links and heading anchors resolve on both the VitePress site and GitHub. Plan and spec records for the closed programs are retired; decisions live in docs/spec/common/.
  • Eleven samples under samples/ each run in CI with golden comparison, including precompiled-app and precompiled-aot.
  • VS Code extension under the multiarc publisher with README, LICENSE and packaging ignore file; per-target VSIXs bundle the language server.

CI

  • dotnet.yml builds and runs the four suites on Ubuntu and Windows; internal PRs publish beta NuGet packages and v*.*.* tags publish releases, both through NuGet trusted publishing.
  • npm.yml publishes the Ace bundle through npm trusted publishing; lsp.yml builds, tests, packs the LSP tool, packages the seven VSIXs and publishes them to the Visual Studio Marketplace on a release tag using the VSCE_PAT secret held by the marketplace environment.
  • samples.yml, docs.yml, dco.yml as 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.
  • The four suites through .github/scripts/dotnet-test-guarded.sh in Debug and Release on every target framework, each test-classes.txt gate green.
  • src/Heddle.Language/generated/ unchanged.
  • Every sample in samples.yml: capture and compare-golden.sh green.
  • Benchmark gates green in all six ecosystems: verify-corpus and gate for .NET, plus the Rust, JVM, JS, Python and Go gates.
  • cd docs && npm run docs:build green; repository-wide link and anchor check clean.
  • The extension packages locally with vsce package and ships only the manifest, README, LICENSE, grammar, language configuration and compiled entry point.

Checklist

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

AI-assistance disclosure

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

multiarc and others added 30 commits July 26, 2026 17:52
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread samples/precompiled-aot/Program.cs
Comment thread .github/workflows/samples.yml
Comment thread benchmarks/third-party/Heddle.Benchmarks.ThirdParty.csproj Outdated
Comment thread src/Heddle/Attributes/NotEncodeAttribute.cs
Comment thread src/Heddle/Attributes/OptionsAttribute.cs
Comment thread src/Heddle/Exceptions/TemplateParseException.cs
Comment thread src/Heddle/Language/CallParameter.cs
Comment thread src/Heddle/Runtime/IDataProcessor.cs Outdated
Comment thread src/Heddle/Runtime/IExtension.cs
Comment thread src/Heddle/Strings/Core/Replacement.cs
… 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multiarc and others added 6 commits September 19, 2026 18:50
…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
multiarc force-pushed the feature/v3_plus_bench branch from 2ddd0e6 to 9dd8827 Compare September 20, 2026 20:13
multiarc and others added 5 commits September 21, 2026 00:46
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>
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>
multiarc and others added 4 commits September 23, 2026 23:21
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

1 active deployment
marketplace — 9505f89f Deployed Sep 24, 2026 by multiarc via marketplace-publish #136
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants