Repository navigation
Gate discovery must equal the baker's, and the PR lane bakes with the compiler (#2063, #2064 part 1) - #2250
Merged
Conversation
… compiler #2063 — the gate never checked a NodeType whose .json carries a BOM. PluginGateRunner.DiscoverNodeTypes parsed package bytes with a raw JsonDocument.Parse while the installer strips the UTF-8 BOM (FileFormatParserRegistry.WithoutBom, #1767). Package content arrives as BYTES and is decoded with Encoding.UTF8.GetString, which PRESERVES U+FEFF — so a BOM'd file threw here and the `catch (JsonException) { continue; }` dropped it under a comment ("malformed json is surfaced by the install itself") that had stopped being true. samples/Graph/Data/PensionFund ships 5 BOM'd NodeTypes and CI printed `[PASS] PensionFund (72 node(s), 0 type(s))` — a green verdict over nothing. It now reads `(72 node(s), 5 type(s))`. A SECOND divergence, same shape, found while fixing the first: discovery mapped file->path with NodeFileMapper.FromRelativePath, only half of PackageInstaller.NodePathForFile, which also excludes README.md, manifest.lock and content/** assets. A NodeType-shaped .json under content/** was therefore discovered by the gate and installed by nobody, so the gate would wait out its whole compile budget and die with a TimeoutException reading as harness noise. Both file-shaped decisions now come from the installer itself, and GateDiscoveryEqualsBakeDiscoveryTest pins the gate's discovered type-path set EQUAL to the compiler-driven bake's over one fixture tree carrying every shape the two halves could disagree about. The drift, not either symptom, is the bug; verified to fail independently against each defect (gate 1 vs bake 3; gate 4 vs bake 3) and to reject two empty sets. Two pre-existing PensionFund failures this makes visible are ratcheted into .github/samples-gate.allow, each diagnosed in place, neither bulk-added. #2064 (part 1) — the PR lane still baked through a mesh. dotnet-test.yml's doc-gate and plugin-gate ran the FUSED shape: one `mw-plugin-test <stage> --bake-output <dir>` that stood up an in-process mesh and let the MESH compile every NodeType — the very thing #1763 removed from CD, still running in the platform's own PR gate. They now run the same two steps main-cd's publish-bake does, held in .github/scripts/bake-then-gate.sh so the two lanes cannot drift: `compile <stage> --output <dir>`, then the gate with `--seed <dir>`. No `--bake-output` invocation survives in dotnet-test.yml. The split is also a stronger gate: what renders and what runs the `Tests` areas is now the assembly the bake produced, not a private recompile nothing ships. assert-bake-consumption.sh makes that claim checkable, because adoption is invisible in a gate verdict by construction — an adopted type renders exactly like a compiled one, so the consuming half could stop working with every run green. It greps both logs for the compile-fallback family and reads the gate's own `seed: adopted N of M` line, requiring M > 0 as well as N >= M. The M > 0 is load-bearing: BakeSeed.Shortfall() returns a PASS over an EMPTY bake, since "adopted everything declared" is vacuously true of nothing. N >= M rather than N == M is the tester's own contract, not a loosened one: N counts adoption events and the gate installs every package twice for the idempotence pin, so `adopted 32 of 28` is healthy — measured on samples/Graph/Data. An equality test would be red on every green run. All six negative cases were run and watched go red: vacuous 0-of-0, shortfall, no seed line, a fallback phrase, no bundles, and an empty identity file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hub/scripts entry Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens MeshWeaver’s content-gating pipeline by ensuring NodeType discovery in the gate matches the installer/baker’s discovery (fixing BOM/path-rule drift) and by updating the PR CI lane to follow the same compiler-driven bake + seeded gate split used in CD.
Changes:
- Align
PluginGateRunner.DiscoverNodeTypeswith installer rules (BOM handling + canonical node path mapping) and expose a seam for testing. - Add
GateDiscoveryEqualsBakeDiscoveryTestto pin gate discovery == bake discovery over a fixture that exercises the known drift cases. - Update the PR workflow to bake via
compile --outputand gate via--seed, with postconditions asserting that the bake was actually consumed (no silent fallback to local compilation), plus documentation/ratchet updates.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tools/MeshWeaver.PluginTester/PluginGateRunner.cs | Gate-side NodeType discovery updated to use installer path mapping and BOM stripping; discovery made internal for test pinning. |
| tools/MeshWeaver.PluginTester/MeshWeaver.PluginTester.csproj | Grants test assembly access to internals to enable drift-proof discovery tests. |
| test/MeshWeaver.PluginTester.Test/GateDiscoveryEqualsBakeDiscoveryTest.cs | New fixture-based test asserting gate discovery equals bake discovery (and pins the expected discovered set). |
| .github/workflows/dotnet-test.yml | PR lane updated to run bake+gate as two steps via shared scripts; summaries updated; bake identity cross-check added. |
| .github/scripts/bake-then-gate.sh | New helper to run compiler-driven bake then seeded gate, preserving the existing “verdict line” behavior. |
| .github/scripts/assert-bake-consumption.sh | New postcondition checks ensuring the gate consumed baked bundles and did not fall back to local compilation. |
| .github/samples-gate.allow | Ratchets newly revealed PensionFund failures as known debt (explicitly diagnosed). |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-25-content-checks-stop-skipping-what-they-claim-to-check.md | WhatsNew entry documenting the behavioral fix and its impact. |
| src/MeshWeaver.Documentation/Data/Architecture/CiContentBake.md | Documentation updated to reflect the two-step bake+gate model and its postconditions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… the BOM Copilot review on #2063, and it is the same defect one layer further in. DiscoverNodeTypes parsed with JsonDocument.Parse's DEFAULT JsonDocumentOptions, which REJECT comments and trailing commas. The installer (JsonFileParser) and the bake (TreeNodeLoader.ContentJsonDocument) both opt into JsonCommentHandling.Skip + AllowTrailingCommas — copied off the hub's own options — so a node file carrying either parses for both of them and threw here, landing in the same `catch (JsonException)` that swallowed the BOM. That is #2063 exactly: discovered by one half, installed and shipped by the other, green either way. JsonFileParser already carries the warning verbatim ("JsonDocumentOptions defaults REJECT comments and trailing commas… taking the default here would have made a node file that parsed yesterday start failing today"). Discovery now shares those tolerances. The drift test did NOT catch this — its fixture had no comment and no trailing comma, which is why the review found it and the test did not. The fixture now carries a node file with both, and the assertion was verified to fail without the fix: bake discovered 4 types, gate discovered 3 (Widget/Tolerant dropped). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
The gate reports it STALE ("now passing — remove it") and the ratchet may
only shrink, so leaving it fails the gate. Confirmed still stale on
82f3c16, i.e. after the parser-tolerance change as well.
It passes for the right reason, verified from the run rather than assumed:
[DEBT] PensionFund (72 node(s), 5 type(s)) <- was 0 type(s)
STALE allow entry (now passing — remove it): PensionFund idempotence
Discovery now finds PensionFund's 5 NodeTypes where it previously found 0,
so the idempotence check executes against compiled content types for the
first time rather than being carried as debt for a check that never ran.
(Re-applied: an earlier commit of this change was dropped by a force-push.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I removed this earlier on the gate's own report that it was STALE ("now
passing — remove it"). The very next run reported it as a NEW failure.
Both readings are correct: the check is nondeterministic. Whether it fires
depends on whether the Currency NodeType has compiled yet, which is exactly
what the BOM discovery fix in this PR changed. So the entry is not stale —
it is intermittently visible debt, and a single green run is not evidence
that it is gone.
The underlying defect is real and unfixed (default-suppression drops a
property whose value equals the type's default, so the stored node differs
from the authored file and the unchanged-skip misses). Documented at the
entry and tracked separately rather than papered over here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2063. Advances #2064 (part 1 — the PR lane; the satellite lane stays open, see below).
#2063 — the gate never checked a NodeType whose
.jsoncarries a BOMPluginGateRunner.DiscoverNodeTypesparsed package bytes with a rawJsonDocument.Parsewhile theinstaller strips the UTF-8 BOM (
FileFormatParserRegistry.WithoutBom, #1767). Package contentarrives as bytes and is decoded with
Encoding.UTF8.GetString, which preserves U+FEFF — so aBOM'd file threw here and the
catch (JsonException) { continue; }dropped it, under a comment("malformed json is surfaced by the install itself") that had quietly stopped being true.
samples/Graph/Data/PensionFundships 5 BOM'd NodeTypes. Measured before and after, locally:A second divergence of the same shape
Found while fixing the first: discovery mapped file→path with
NodeFileMapper.FromRelativePath,which is only half of
PackageInstaller.NodePathForFile— the other half excludesREADME.md,manifest.lockandcontent/**assets. A NodeType-shaped.jsonundercontent/**was thereforediscovered by the gate and installed by nobody, so the gate would wait out its full compile
budget and die with a
TimeoutExceptionthat reads as harness noise.Both file-shaped decisions now come from the installer itself.
The real deliverable: the drift-proof test
GateDiscoveryEqualsBakeDiscoveryTestrunsPluginGateRunner.DiscoverNodeTypesand thecompiler-driven bake's discovery (
TreeNodeLoader.Load(...).Where(Content is NodeTypeDefinition))over ONE fixture tree carrying every shape the two halves could disagree about — a plain NodeType, a
BOM'd one, a nested BOM'd one, a NodeType-shaped
.jsonundercontent/**, a non-NodeType node,README.md, and malformed JSON — and asserts the type-path sets are equal. It also pins whatthat set is, so the equality cannot be satisfied by both halves regressing to empty together.
I watched it fail on each defect independently (reverting one fix at a time):
Bommed, Nested/Deep, ThingThingAssert.Single() Failure: The collection was empty, the0 type(s)pass in miniatureBommed, Nested/Deep, Thing… , content/AssetDiscovery only — no mesh, no Roslyn. 3 ms, so it can be exhaustive about file shapes in a way
BakeEquivalenceTest(which compiles both sides) is not.Newly-revealed failures, ratcheted individually
Both are pre-existing debt made visible, not caused by this change. Diagnoses are inline in
.github/samples-gate.allow.1.
PensionFund/BalanceSheet compile—NuGet.Protocol.FatalProtocolException: The local source '<repo>/dist/packages' doesn't exist.BalanceSheetScopes.cs:20declares#r "nuget:MeshWeaver.BusinessRules.Generator".GeneratorPipeline.StripBuiltInScopeGeneratorRefremoves that from the NuGet resolve set only whenBuiltInGeneratorPathsis non-empty, i.e. whenMeshWeaver.BusinessRules.Generator.dllsits besidethe app. BusinessRules left the platform (f7a8c08 — it ships in the
MeshWeaver.Plugins/BusinessRulesplugin), so that list is EMPTY on the gate host and the strip is a no-op. The directive then reaches
NuGet, where
NuGet.config'spackageSourceMappingpinsMeshWeaver.*exclusively to themesh-localsourcedist/packages— a feed #395 stopped producing, whichdotnet-test.yml:372-377documents CI deliberately no longer packs. Deterministic, not a flake: verified no such dll in the
tester's output and no
dist/packageson any runner. (No CS code — it faults before Roslyn.) The realfix is content-side and needs the plugin's generator reachable from the gate host — a separate change.
2.
PensionFund idempotence— the second install rewritesCurrency/{CHF,EUR,USD}(3 nodes,expected 0). Not the BOM, and not all instances:
Year/2024and everyPosition/*round-trip clean.The three that churn are exactly the three whose content is default-valued —
Currency.DecimalPlacescarries= 2and all three files author"decimalPlaces": 2. Once theCurrencyNodeType compiles its type is registered, so the installer materialises the instance as atyped
Currencyinstead of an untypedJsonElement, re-serialises it, and default-suppression dropsthe property. The stored node then differs from the authored file and the unchanged-skip misses. This
is the deploy-flicker the idempotence pin exists to catch.
#2064 part 1 — the PR lane bakes with the compiler
doc-gateandplugin-gateran the fused shape: onemw-plugin-test <stage> --bake-output <dir>that stood up an in-process mesh and let the mesh compile every NodeType — the very thing #1763
removed from CD, still running in the platform's own PR gate. They now run the same two steps
main-cd'spublish-bakedoes, held in.github/scripts/bake-then-gate.shso the two lanes cannotdrift:
compile <stage> --output <dir>, then the gate with--seed <dir>.No
--bake-outputinvocation survives indotnet-test.yml(only historical prose in comments).One bake directory per tree (
bake/doc,bake/samples) — a shared one would let each gate's--seedsee the other's bundles and satisfy the postcondition with bundles it never installed.The assertions, and why they are not decoration
Adoption is invisible in a gate verdict by construction: an adopted type renders and tests exactly
like a compiled one, so the consuming half could stop working with every run green.
assert-bake-consumption.shgreps both logs for the compile-fallback family (no prebuilt bundle,carried no assemblies,could resolve NO artifact,no prebuilt assemblies adopted) and reads thegate's own
seed: adopted N of Mline.M > 0is load-bearing:BakeSeed.Shortfall()returns a PASS over an empty bake, because"adopted everything declared" is vacuously true of nothing.
N >= M, notN == M— the tester's own contract (if (Adopted >= expected.Count) return null),not a loosened one.
Ncounts adoption events and the gate installs every package twice forthe idempotence pin. My first draft used
-neand it went red on a healthy run atadopted 32 of 28;a gate that cries wolf is one that gets switched off.
Every negative case was run and watched go red (
assert-bake-consumption.shagainst synthetic logs):And end-to-end on the real samples tree:
compile: 28/31 NodeType(s) compiled, 8 bundle(s)→gate adopted 32/28 baked assembly(ies)→ exit 0.🚧 What #2064 still needs — and a correction to the plan
Stage 2 (
node-repo-publish-bake.yml: the capability floor probe, the two-step, and the"published nothing must go RED" job) is not in this PR, and part of it cannot work as
specified:
Also worth recording: MeshWeaver.Education does not call the reusable workflow at all. Its
publish-bake(ci.yml:785) is an inline job that vendoredpublish-bake-bundles.sh(ci.yml:939), soEducation#211's live symptom cannot be fixed from this repo's
node-repo-publish-bake.ymlunder anyshape. Plugins and Reinsurance/SocialMedia do call it.
Verification
dotnet build -c Release -warnaserrorclean (0 Error(s),0 Warning(s)):MeshWeaver.PluginTester,MeshWeaver.PluginTester.Test,MeshWeaver.Documentation.Test.MeshWeaver.PluginTester.Test: 79/79 pass, incl.BakeEquivalenceTestandBakeGateSplitTest.MeshWeaver.Documentation.Testlink-integrity +PlatformBakeLaneGuard+UpstreamBuildGateGuardGREEN — 4 known-debt failure(s) allowed.dotnet-test.ymlre-parsed as YAML; both new scriptsbash -nclean.No satellite repo is touched;
node-repo-publish-bake.ymlis unchanged, so the pinned callers(Plugins
@731620dc, Reinsurance/SocialMedia@d193dd20) are unaffected.🤖 Generated with Claude Code