Repository navigation
fix(compile): the adopted-source fingerprint now covers @@ includes - #3000
Conversation
…2948) `NodeTypeSourceFingerprint.Compute` hashed the compile input as `NodeCompileShaping.CollectCompileSources` yields it. An `@@` include pulls a Code node that NO source query matches: it reaches the emitted assembly (`NodeSetCompiler.ResolveInputs` substitutes it AFTER the fold) and was absent from the hash. So editing an included-only snippet moved neither `AdoptedSourceFingerprint` nor `CurrentSourceFingerprint`, and a prebuilt assembly baked before that edit still adopted as `AdoptedVerified`. That is the worst of the three rows to be wrong on. `AdoptedUnverified` says "nobody established where these bytes came from" and reads as the warning it is; `AdoptedVerified` is an ASSERTION that the shipped bytes match the source this mesh holds. Standing over source that was never hashed it is a FALSE VERIFICATION — the #2813 incident one layer in. Both halves now resolve includes before hashing: - PRODUCER: `ResolveCodeIncludes` takes an optional collector and records `resolved path -> code text` at the point it consumes each include, so the closure is a by-product of the ONE walk that already substitutes them. `CompileInputs.ResolvedIncludes` carries it; TreeBake/CascadeBuild fold it straight in — no second walk, no second read, no new blocking bridge. - CONSUMER: the sources watcher resolves it through the mesh. Reads cannot happen inside a pure `Update` lambda, so the fingerprint moved into an observable step ahead of it, composed with `Switch()` so a newer source set supersedes an in-flight resolution. Deterministic and cycle-safe: the closure is keyed by resolved path and returned SORTED (ordinal), roots are walked in `CollectCompileSources` order and SERIALLY (one SelectMany chain, never a Merge), and the per-root visited set both brakes cycles (A->B->A, and self-includes) and is result-preserving — skipping a re-read of the same anchored path can only re-derive an entry that is already there. An order-dependent fingerprint would manufacture phantom staleness, which is worse than the gap being closed. And an unreadable include is INCONCLUSIVE, never absent. `SourceFingerprintIncludeReader` reads via `GetMeshNodeOutcome` and keeps the three states apart: Present contributes, Absent contributes nothing (it contributes nothing to the bytes either), Unavailable/DeleteInProgress/timeout raise `SourceIncludeUnavailableException`. The watcher catches exactly that and leaves the previous fingerprint standing, so the judgement degrades to `AdoptedUnverified`. Degrading a stalled read to "absent" would shorten the closure, hash differently from every honest producer and REFUSE a good adoption — terminal on a `Modules:RequirePrebuilt` mesh. Same #890 rule: a probe must not answer its scariest branch on its own inability to run. `AdoptedUnverified` is still never a refusal, and the include closure is deliberately NOT added to `CompiledSources`/`CurrentSourceVersions`: that is the recompile-TRIGGER half (a change feed, not a hash) and a producer/consumer contract `BakeEquivalenceTest` pins. That test is reconciled, not loosened — the include stays asserted ABSENT from `sourceVersions`, and a new assertion pins it PRESENT in the bake's resolved include set, so assertion 3b's cross-producer hash equality can no longer be satisfied by both producers ignoring it. The include-less `Compute` overload REFUSES a set that has an include rather than silently under-covering — a convenience overload that can under-cover is exactly how #2813 sat inert (the 7-arg `Seed` hard-coded `sourceFingerprint: null` and both production callers took it). Closes #2948 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Falsification output — every new test proven able to failA test that passes after a change proves nothing unless it would have failed before it. Four Probe 1 —
|
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness gap in build provenance verification: NodeTypeSourceFingerprint now incorporates @@-included Code nodes (which are compiled into the output but were previously invisible to source-query-based hashing). This prevents stale prebuilt assemblies from being incorrectly marked AdoptedVerified when only included snippets changed.
Changes:
- Producer side: capture the resolved
@@include closure during include substitution (CompileInputs.ResolvedIncludes) and fold it into the source fingerprint during baking. - Consumer side: resolve
@@include closure through the mesh when computingCurrentSourceFingerprint, with deterministic, cycle-safe traversal and an “inconclusive” failure mode for unreadable includes. - Add/adjust tests and documentation to pin the contract (include closure affects fingerprint but not
sourceVersions) and validate refusal/adoption outcomes.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tools/MeshWeaver.PluginTester/TreeBake.cs | Folds resolved include closure into baked fingerprint; exposes resolved include paths for reporting/tests. |
| tools/MeshWeaver.PluginTester/CascadeBuild.cs | Folds resolved include closure into baked fingerprint for cascade builds. |
| tools/MeshWeaver.PluginTester/BakeOutput.cs | Computes fingerprint via include-reader path so mesh-driven bake matches compiler-driven bake. |
| test/MeshWeaver.PluginTester.Test/BakeEquivalenceTest.cs | Pins include targets absent from sourceVersions but present in resolved include set (and thus fingerprint input). |
| test/MeshWeaver.Compiler.Pipeline.Test/SourceFingerprintIncludeClosureTest.cs | New unit tests covering include closure hashing, determinism, cycles, inconclusive reads, and adoption verdicts. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-02-a-shared-snippet-no-longer-hides-behind-a-verified-build.md | Adds user-facing “What’s New” entry describing the verification fix. |
| src/MeshWeaver.Documentation/Data/Architecture/NodeTypeCompilation.md | Documents the new fingerprint behavior, determinism arguments, and what remains deliberately unchanged. |
| src/MeshWeaver.Compiler/NodeTypeSourceFingerprint.cs | Extends fingerprinting to include the resolved @@ closure; adds overloads and refusal behavior for include-bearing sets without closure. |
| src/MeshWeaver.Compiler/NodeSetCompiler.cs | Records resolved includes during ResolveInputs and surfaces them via CompileInputs.ResolvedIncludes. |
| src/MeshWeaver.Compiler/NodeCompileShaping.cs | Adds CollectIncludeClosure and optional closure collection to ResolveCodeIncludes. |
| src/MeshWeaver.Compiler.Pipeline/SourceFingerprintIncludeReader.cs | New mesh reader for include closure resolution with “inconclusive != absent” semantics. |
| src/MeshWeaver.Compiler.Pipeline/NodeTypeCompilationHelpers.cs | Moves fingerprint computation upstream of the Update lambda and uses Switch() to supersede in-flight resolutions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 0)216 tests 216 ✅ 2m 25s ⏱️ Results for commit 81e2e25. |
Test Results (shard 3)586 tests 586 ✅ 50s ⏱️ Results for commit 81e2e25. |
Test Results (shard 1)419 tests 419 ✅ 1m 13s ⏱️ Results for commit 81e2e25. |
Test Results (shard 5) 3 files 3 suites 1m 11s ⏱️ Results for commit 81e2e25. |
Test Results (shard 2)529 tests 527 ✅ 1m 31s ⏱️ Results for commit 81e2e25. |
Test Results (shard 4)1 685 tests 1 685 ✅ 1m 44s ⏱️ Results for commit 81e2e25. |
Test Results 14 files 14 suites 8m 57s ⏱️ Results for commit 81e2e25. |
Closes #2948
The gap
NodeTypeSourceFingerprint.Computehashed the compile input asNodeCompileShaping.CollectCompileSourcesyields it. An@@include pulls a Code node that nosource query matches: it reaches the emitted assembly (
NodeSetCompiler.ResolveInputssubstitutesit after the fold) and was absent from the hash. So editing an included-only snippet moved neither
AdoptedSourceFingerprintnorCurrentSourceFingerprint, and a prebuilt assembly baked beforethat edit still adopted as
AdoptedVerified.That is the worst of the three rows to be wrong on.
AdoptedUnverifiedsays "nobody establishedwhere these bytes came from" and an operator reads it as the warning it is.
AdoptedVerifiedis anassertion that the shipped bytes match the source this mesh holds — standing over source that
was never hashed, it is a false verification: the #2813 incident one layer in.
The fix — both sides resolve includes before hashing
NodeCompileShaping.ResolveCodeIncludestakes an optional collector and recordsresolved path → code textat the point it consumes each include, so the closure is a by-productof the one walk that already substitutes them — there is no second traversal to disagree with
the first.
CompileInputs.ResolvedIncludescarries it;TreeBake/CascadeBuildfold itstraight in. No second walk, no second read, and no new blocking bridge at a synchronous build
step (the
ProductionBlockingBridgeSitesratchet does not move).Updatelambda, so the fingerprint moved into an observable step ahead of it, composed withSwitch()so a newer source set supersedes an in-flight resolution rather than racing it.CollectIncludeClosurescans each source's text for@@and issues no read at all when thereis none — almost every type — so only a type that actually has includes pays, and it pays the same
reads its own compile would.
Deterministic and cycle-safe
An order-dependent fingerprint manufactures phantom staleness (endless recompiles, or a false
refusal), which is worse than the gap being closed. Three properties:
(ordinal);
PartitionSourceFingerprintsorts again. Roots are walked inCollectCompileSourcesorder — itself ordinal by node path — and serially (one
SelectManychain, never aMerge),so there is no read interleaving to observe either.
walked; skipping a re-read of the same anchored path can only re-derive an entry that is
already present, so which parent reached a shared snippet first cannot change the closure.
A → B → Aadds each path once; the second visit takes the already-addedbranch, reads nothing, recurses no further. A self-include is the same case with one hop.
An unreadable include is INCONCLUSIVE, never absent
The producer's lookup is in-memory and never stalls. A consumer that quietly treated a stalled read
as "the include is gone" would hash a shorter closure — indistinguishable from a stale bundle — so
a perfectly good adoption is refused, and on a
Modules:RequirePrebuiltmesh that is terminal.SourceFingerprintIncludeReaderreads viaGetMeshNodeOutcomeand keeps the three states apart:Presentcontributes,Absentcontributes nothing (it contributes nothing to the bytes either —the directive stays verbatim, so both sides agree), and
Unavailable/DeleteInProgress/ atimeout raise
SourceIncludeUnavailableException. The watcher catches exactly that and leaves thepreviously published fingerprint standing, so the judgement takes the "nothing has been compared"
branch. Same #890 rule: a probe must not answer its scariest branch on its own inability to run.
AdoptedUnverifiedis still never a refusal. Pinned by a test in this PR.What was NOT changed, deliberately
The include closure is not added to
CompiledSources/CurrentSourceVersions. That is therecompile-trigger half — a change feed, not a hash — and
sourceVersionsis a producer/consumercontract mirroring the RAW query match on both bakes.
#2948closes the verified claim, whichis decided by the two fingerprints and nothing else. Stated in the doc rather than assumed.
BakeEquivalenceTest— reconciled, not loosenedIt pinned
Widget/Snippets/Greetingabsent fromsourceVersions. That assertion stays andis now explained:
sourceVersionsmirrorsCompiledSources(the raw query match), which is adifferent field answering a different question. A new assertion pins the include present in
the bake's resolved include set — because without it, assertion 3b's cross-producer hash equality
would be satisfiable by both producers ignoring the include, which is exactly the state #2948
found them in.
MeshDrivenAndCompilerDrivenBakes_ProduceTheSameArtifactspassing is now the proofthat the mesh bake (real workspace, mesh reads) and the tree bake (
NodeSet) resolve the sameclosure.
Binary compatibility
CompileInputs.ResolvedIncludesis arequired initmember, not a positional parameter:positional would have changed the primary constructor's arity, which
scripts/check-record-signatures.pycorrectly rejects as a binary break between a module and theplatform it loads into.
requiredbecause forgetting it is silent — an empty closure for a typewith includes is a refusal, not "the fingerprint minus the includes". For the same reason the
include-less
Computeoverload now refuses a set that has an include rather than under-coveringit: a convenience overload that can under-cover is exactly how #2813 sat inert (the 7-arg
Seedhard-coded
sourceFingerprint: nulland both production callers took it).Types with no includes fold to the identical value as before, so there is no fleet-wide
fingerprint churn; and this file is inside
FrameworkBuildIdentity.FullMvidAssemblies, so a producerand a consumer that can adopt across each other necessarily run the same implementation.
Verification
Every new test was proven falsifiable — inverted, seen red, restored (output in the PR thread).
MeshWeaver.Compiler.Pipeline.Test— 583 passed (whole project, no--filter)MeshWeaver.PluginTester.Test— 312 passed (whole project; includesBakeEquivalenceTest)MeshWeaver.Documentation.Test— 166 passed (whole project), plus a broken-link probe provingDocumentationLinkIntegrityTestnames both edited pagesdotnet build -c Release -warnaserror— 61 projects,0 Warning(s) 0 Error(s)scripts/check-record-signatures.py— cleanDocs:
Doc/Architecture/NodeTypeCompilationgains the mechanism (determinism argument, cycle brake,the inconclusive rule) and its "deliberately does NOT do" list is rewritten to state what is still
open. What's New entry,
Category: Fix.