Repository navigation
fix(bake-gate): a refusal can only stall a roll — never-built types cannot regress, a serving image never refuses itself (#5544) - #5725
Conversation
…annot regress, a serving image never refuses itself (#5544) memex.systemorph.com went fully down after the V58 migration: the nodetype_bake gate refused readiness on the new ci.9332 pod AND on the restarted pod of the serving ci.9218 image, on BinaryClickerV2/BinaryToggle — a type whose source has failed CS1929 on every image (#3883). Two defects: - A NeverBuilt entry counts as WasHealthy, and the baseline was built from that, so a type that never built read as a regression on any established instance. The Error stamp that would have corrected the record is a MeshPublicationGate publication and is DISCARDED on a refused pod, so the record stayed Ok forever. - Nothing distinguished a restart of the image the rollout falls back on from a roll candidate. Now, through the one shared stamp (BaselineStamp, used by the sweep and the GO): - per type: a failure regresses this image only when a working build is on record and was produced by a DIFFERENT, NOT-NEWER platform build (NodeTypeBakeEntry.IsRegressionBaselineFor, ProducedByPlatformBuild from CompiledPlatformVersion); - per report: an image whose build already stamped a working build here has served, and never refuses itself (NodeTypeBakeReport.ThisBuildHasServed). GateRelevant applies the same rules. WasHealthyBeforeBake keeps its #4496 meaning; the answer rides on HasRegressionBaseline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The serving-image exemption can fail after adopting and restarting a compatible prebuilt, potentially allowing the serving image to gate readiness.
Review effort: Lite
Findings: 1
What changed in this PR
This PR updates NodeType bake-gate regression detection so never-built types and serving images do not incorrectly block readiness.
Changes:
- Adds producer-aware regression baselines and serving-image handling.
- Expands deterministic rollout and restart test coverage.
- Documents the revised gate behavior and limitations.
| File | Summary |
|---|---|
test/MeshWeaver.Hosting.Test/TheBakeGateCannotTakeAnInstanceDownTest.cs |
Adds incident, restart, producer, and regression coverage. |
test/MeshWeaver.Hosting.Test/AFirstRolloutHasNoRegressionBaselineTest.cs |
Updates shared baseline assertions. |
test/MeshWeaver.Compiler.Pipeline.Test/NodeTypeBakeStatusTest.cs |
Verifies producer metadata propagation. |
src/MeshWeaver.Hosting/NodeTypeBakeGate.cs |
Updates gate diagnostics. |
src/MeshWeaver.Hosting/DynamicTypePreWarmer.cs |
Applies shared regression stamping. |
src/MeshWeaver.Documentation/Data/Architecture/TheBakeGateOnlyStallsARoll.md |
Documents the gate rules and limitations; unknown-producer exception needs clarification. |
src/MeshWeaver.Documentation/Data/Architecture/MeshAdmission.md |
Links the new architecture documentation. |
src/MeshWeaver.Documentation/Data/Architecture.md |
Adds the page to the topic map. |
src/MeshWeaver.Compiler.Pipeline/NodeTypeBakeStatus.cs |
Adds producer and serving-image tracking; the serving-image exemption is not durable for adopted prebuilt artifacts. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| public bool ThisBuildHasServed => | ||
| !string.IsNullOrWhiteSpace(LivePlatformVersion) | ||
| && Entries.Any(e => e.HadWorkingBuild | ||
| && string.Equals( | ||
| e.ProducedByPlatformBuild, LivePlatformVersion, StringComparison.Ordinal)); |
There was a problem hiding this comment.
Agreed, and it is worse than the adoption case alone. An ordinary roll compiles nothing, because the compatibility key is equal across every build of one epoch. So in the common case a serving image leaves no record naming itself, and provenance was the wrong witness.
Fixed in d5ecba3:
ServedBuildWitnesswrites a storage row atAdmin/ServedPlatformBuilds/<build>. The row does not depend on per-type provenance.- The armed sweep offers it through
MeshPublicationGate. The gate holds it while the bake measures, writes it when the pod is admitted, and discards it when the pod is refused. A bad image therefore cannot exempt itself on its next boot. - The row is read directly through
IStorageAdapter, the same way as the claim lock: no hub, and no point read of a missing node. A failed read counts as "not served", which keeps the strict reading.
- The armed sweep offers it through
NodeTypeBakeReport.ServedBeforecarries the witness into the shared stamp and intoGateRelevant, on both the sweep's report and the build protocol's.NodeTypeBakeGateState.ServedBeforealso relaxes aFaultedsweep on such an image. That was the one remaining way the gate could take a served instance down, since a sweep that errored has no per-type evidence to judge.
Tests:
TheBakeGateCannotTakeAnInstanceDownTest.AnAdmittedImageWhoseOnlyWorkingBuildsAnOlderImageProduced_DoesNotRefuseItselfis your scenario. It includes a control showing the same report without the marker still refuses.AServedBuildWitnessRecordsOnlyAnAdmittedBuildTestruns on real mesh storage and covers two cases: an admitted pod writes the marker and a restart reads it, and a refused pod never writes it. With the publication gate bypassed, both cases fail.
Your second location (line 665, GateRelevant) is covered by the same change.
…not per-type provenance Copilot's finding: ThisBuildHasServed read only record provenance, and a serving image can leave no record naming itself. An ordinary roll compiles nothing (the compatibility key is equal across builds of one epoch), and prebuilt adoption keeps the PRODUCER's platform version on purpose. So a restart of the serving image could still refuse itself on a BytesMissing type an older build had produced. - ServedBuildWitness: a storage row at Admin/ServedPlatformBuilds/<build>, offered through MeshPublicationGate by the armed sweep. It is held while measuring, written when the pod is admitted, and discarded when the pod is refused. It is read straight through IStorageAdapter (no hub, no point read of a missing node), and a failed read counts as not served. - NodeTypeBakeReport.ServedBefore carries it into the shared stamp and GateRelevant, on the sweep's report and on the build protocol's. - NodeTypeBakeGateState.ServedBefore also relaxes a Faulted sweep on such an image. That was the remaining path by which the gate could take a served instance down. Tests: AServedBuildWitnessRecordsOnlyAnAdmittedBuildTest runs on real mesh storage. With the publication gate bypassed, both of its cases fail. There are also unit cases for the reviewer's scenario and for Faulted, each with a negative control. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Refs #5544 · Refs #3883 · Refs #4684
What went wrong
After the V58 migration, memex.systemorph.com went fully down (nginx 503). The
nodetype_bakegate refused readiness on the newci.9332pod. When the last pod of the servingci.9218image restarted, the gate refused that pod too. Both refusals namedBinaryClickerV2/BinaryToggle. That type has failed CS1929 on every image since it was written (#3883).There were two defects:
NodeTypeBakeEntry.WasHealthycountsNeverBuiltas healthy. That part is correct: a type nobody has built is not known to be broken. But the regression baseline was built from that field. So on any established instance, a failing never-built type was treated as a regression. The first-bake rule (fix: the first rollout of a new instance has no regression baseline #4472) only covered reports where every type isNeverBuilt.Errorstamp is aMeshPublicationGatepublication, and the gate discards it on a refused pod. The failure refused the pod, so the refused pod could not record the failure. The next pod then readOkagain. That loop is why BinaryClickerV2/BinaryToggle NodeType cannot compile — layout source calls non-existent LayoutAreaHost.GetData<T>() #3883's record readOkwhile every compile failed.The fix
Everything goes through the one shared stamp,
DynamicTypePreWarmer.BaselineStamp. The sweep andBuildProtocolDriver.OutcomesOfboth use it, so the GO and the gate still agree (#4496).NodeTypeBakeEntry.IsRegressionBaselineFor): a failure counts as a regression of this image only if both hold:HadWorkingBuild: notNeverBuilt, notPreviouslyBroken);ProducedByPlatformBuild, taken fromCompiledPlatformVersion.NodeTypeBakeReport.ThisBuildHasServed): a pod whose build has already been admitted here never refuses itself. Two witnesses establish that, and both are admission-gated publications:ServedBuildWitness, a storage row atAdmin/ServedPlatformBuilds/<build>. It is offered throughMeshPublicationGate: held while the bake measures, written on admission, discarded on refusal. The report carries it asServedBefore.Faultedsweep on a served image no longer refuses.NodeTypeBakeGateState.ServedBeforerelaxes that one verdict. This was the remaining way the gate could take a served instance down.GateRelevant(the witness-unreadable path) applies both rules.WasHealthyBeforeBakekeeps its DynamicTypePreWarmer: a never-built type reports WasHealthyBeforeBake=false in the satellite lane — core green at the same sha, and it pins every satellite's platform ceiling #4496 meaning. The new answer is carried onHasRegressionBaseline.Net effect: only a pod strictly newer than the producer of the working build it failed to reproduce can refuse, and only while no replica of its image has been admitted. The older image's pods never meet that condition. Failures that no longer gate are still named in
WithoutBaseline. Once the pod is admitted, the heldErrorstamp is released, which ends the loop.Tests
TheBakeGateCannotTakeAnInstanceDownTesthas 8 deterministic cases, run from a bake report through the real stamp into an armedNodeTypeBakeGateState, withIsGatingFailureasserted alongside. It covers:Negative control: with
BaselineStampreverted to the old rule, 3 of the 8 tests fail (the never-built, restarted-old-pod and newer-producer cases).NodeTypeBakeStatusTestgained a probe case that checks the producer and the live build are carried.AFirstRolloutHasNoRegressionBaselineTest.TheGosOwnProjection_UsesTheSameStampchanged one assertion: aPreviouslyBrokentype on an established instance now readsHasRegressionBaseline == false. It was already non-gating throughWasHealthyBeforeBake.AServedBuildWitnessRecordsOnlyAnAdmittedBuildTestruns on real mesh storage. It checks that an admitted pod writes the witness, that a restart reads it, and that a refused pod never writes it. With the publication gate bypassed, both cases fail.Run locally with Release
-warnaserrorbuilds:Docs
The new page
Architecture/TheBakeGateOnlyStallsARollis listed in the topic map and linked from Mesh Admission.Not covered here
Faultedsweep on a NEW, never-admitted image still refuses readiness. This is by design.Addresses to recycle after deploy: none. This is boot-time sweep logic, so it takes effect on each pod's next start.
Pairs-with: none — no public type or member removed; additions only (init properties and one method on
NodeTypeBakeEntry, two properties onNodeTypeBakeReport).🤖 Generated with Claude Code