Skip to content

fix(#3461): publishing a generation is the lane's default, so all six prefixes flip - #4341

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/3461-generation-is-the-lane-default
Sep 14, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/3461-generation-is-the-lane-default

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Phase 4 of #3461, finished — by moving the default rather than opening five one-line PRs

main-cd.yml's plugins-bake flipped in #4269 and its pointer-ordering defect was fixed in #4273.
What was left on phase 4 was written down as "the five single-producer prefixes, each a one-line
change in its own repo"
(crm, education, reinsurance, socialmedia, manufacturing). That
framing leaves the larger half undone.

The thing that decided those prefixes' layout is the publication-layout INPUT DEFAULT, and
while that default is flat, a node repo that says nothing publishes in place — including every
node repo that will ever be created
. Five PRs would have flipped five prefixes and left the
default, and therefore the next repo, exactly where it is.

Why the default could not move before, and why it can now

The default was flat for a measured reason: a new writer and an old writer on one prefix leaves a
pointer-following reader serving its generation while the old writer refreshes the flat copy in
place — a stale serve with nothing red anywhere. #4249 removed that state rather than forbidding
it
: the layout belongs to the PREFIX, so a flat caller that resolves a live _current
publishes a generation too, loudly.

The precondition, measured today rather than inherited

#4249 lives in publish-bake-bundles.sh, so the guarantee is a claim about the publisher each
producer RUNS
. Measured 2026-09-14 over ci.yml on main of all six node repos:

uses: scripts-ref: passes publication-layout invokes the publisher directly
Plugins, Crm, Education, Reinsurance, SocialMedia, Manufacturing @main (every node-repo-*.yml) main none of the six none — publish-bake-bundles.sh appears in exactly one satellite file, in a comment

So no pinned publisher and no pinned reader of these prefixes exists in the fleet, nothing runs the
publisher outside the lane, and moving the default reaches all six producers at once.

Safe at any portal age — this is what separates phase 4 from phase 5

A generation publication still writes the flat compatibility copy. A reader that cannot resolve
_current — an image predating phase 1, or a torn pointer read on any image — is served exactly
what it was served before. Phase 5 is the change with a portal-age precondition; phase 4 has none.

The script's own default deliberately stays flat

${BAKE_PUBLICATION_LAYOUT:-flat} is the direct-invocation default (the overlap harness, a
manual run); the lane always passes the input through explicitly, so no lane run reaches it. The
comment above it read "IT DEFAULTS TO flat AND MUST STAY THAT WAY" — a sentence that, left
standing, is exactly how prose in this family has contradicted behaviour before. It now says which
default is which, as do the three sibling readers (bake-scope.sh, carry-forward-bundles.sh,
compose-sealed-modules.sh) whose comments each claimed the flat layout was "the only one anything
has written"
.

Watched, not assumed

harness result
test-publish-bake-overlap.py 98 passed / 0 failed — unchanged, because the script edits are comments and the three flat control cases still exercise the direct-invocation default
test-publication-pointer-readers.py 14 cases over 2 readers, all green
bake-scope.sh --self-test 12 fallbacks, 2 nothing-to-do verdicts, 4 pointer assertions, 4 narrowed assertions — green
check-workflow-yaml-keys.py 36 workflow files, 0 violations
check-workflow-timeouts.py 87 jobs, 0 violations
MeshWeaver.Documentation.Test (link integrity + topic map + What's New) 6/6

Phase 5 stays open, and its precondition is now stated with a denominator

Doc/Architecture/SealedPublicationGenerations gains what "once no deployed portal predates phase
1"
actually needs: memex and memex-cloud both answer /api/version with
3.0.0+c84c6c055…, which has phase 1 (a4109d422) as an ancestor; build exposes no commit
through Ops/Status; client-b does not resolve and reports replicas: []; and the four
Hosting/Deployment records are not provably the whole population — an install that has stopped
self-updating appears in none of them. The per-replica instrument that would answer it is blind
(#4218). The page also records, explicitly labelled as an inference and not a measurement, the
argument that the precondition may be self-satisfying (a pre-phase-1 image resolves a framework
identity nothing publishes under any more).

Closes nothing on its own. #3461 stays open on phase 5.

🤖 Generated with Claude Code

… prefixes flip

Phase 4 of the sealed-publication migration was described as "one line per
producer, in its own repository" — core CD's `plugins-bake` (landed, #4269) plus
five one-line PRs for `crm`, `education`, `reinsurance`, `socialmedia` and
`manufacturing`. That framing left the larger half undone: what decided those
prefixes' layout was the `publication-layout` INPUT DEFAULT, and while it was
`flat`, a node repo that said nothing published in place — including every node
repo that will ever be created.

The default was `flat` for a measured reason, and the reason is gone. #4249 made
the layout belong to the PREFIX rather than to the caller, so a `flat` caller that
resolves a live `_current` publishes a generation anyway; the half-migration the
default guarded against is unreachable rather than forbidden.

The precondition that remained is a claim about the PUBLISHER each producer RUNS,
because #4249 lives in `publish-bake-bundles.sh`. Measured 2026-09-14 over
`ci.yml` on `main` of all six node repos (Plugins, Crm, Education, Reinsurance,
SocialMedia, Manufacturing): every `uses:` of a `node-repo-*.yml` lane is `@main`,
every `scripts-ref:` is `main`, NONE passes `publication-layout`, and
`publish-bake-bundles.sh` is named in exactly one satellite file — in a comment.
No pinned publisher and no pinned reader of these prefixes exists in the fleet, and
nothing invokes the publisher outside the lane, so moving the default reaches all
six producers at once.

Safe at any portal age, which is what separates phase 4 from phase 5: a generation
publication still writes the flat compatibility copy, so a reader that cannot
resolve `_current` — an image predating phase 1, or a torn pointer read on any
image — is served exactly what it was served before.

The script's own `${BAKE_PUBLICATION_LAYOUT:-flat}` fallback deliberately stays
`flat`. It is the DIRECT-INVOCATION default (the overlap harness, a manual run) and
the lane always passes the input through explicitly, so no lane run reaches it. The
comment above it used to read "IT DEFAULTS TO flat AND MUST STAY THAT WAY" and now
says which default is which — that sentence, left standing, is how prose in this
family has contradicted behaviour before.

Harnesses, run locally against the branch: `test-publish-bake-overlap.py` 98
passed / 0 failed (unchanged — the script edits are comments, and the three flat
control cases are still the regression suite for the direct-invocation default);
`test-publication-pointer-readers.py` 14/14 over 2 readers; `bake-scope.sh
--self-test` all green; `check-workflow-yaml-keys.py` 36 files / 0 violations;
`check-workflow-timeouts.py` 87 jobs / 0 violations;
MeshWeaver.Documentation.Test link + topic-map tests 6/6.

Phase 5 (drop the flat copy) stays open. Its precondition is measured and stated in
Doc/Architecture/SealedPublicationGenerations with its denominator: 2 of the 4
`Hosting/Deployment` records are confirmed past phase 1 by `/api/version`, `build`
exposes no commit, `pearl` does not resolve, and the records are not provably the
whole population.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 14, 2026 18:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

An unresolved publication-transition race, rollout/test gaps, and contradictory migration documentation remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Changes the reusable node-repository publish lane default from flat to generation, while retaining the script’s direct-invocation fallback.

Changes:

  • Updates workflow, publisher, and reader guidance.
  • Refreshes pointer-reader test documentation.
  • Documents phase 4 rollout and adds a What's New entry.
File summaries
File Description
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-14-every-prepared-update-is-now-published-whole.md Adds the user-facing release note.
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md Documents rollout status and phase 5 preconditions.
.github/workflows/node-repo-publish-bake.yml Changes the publication default to generation.
.github/scripts/test-publication-pointer-readers.py Updates reader test documentation.
.github/scripts/publish-bake-bundles.sh Clarifies publication transition and default guidance.
.github/scripts/compose-sealed-modules.sh Clarifies pointer fallback behavior.
.github/scripts/carry-forward-bundles.sh Clarifies generation fallback behavior.
.github/scripts/bake-scope.sh Clarifies generation fallback behavior.
Review details

Suppressed comments (2)

.github/scripts/publish-bake-bundles.sh:172

  • The selector explanation is updated for the flip, but publish_publication still has a live comment at lines 1179–1183 saying retention is not implemented and is a precondition on flipping. This change now relies on retention being implemented to make the lane default safe, so that comment is false and will mislead maintainers about whether generation publishing is allowed. Update it with this rollout.
# 🚨 THE FLEET'S DEFAULT IS `generation`, AND IT IS SET BY THE LANE, NOT HERE. `node-repo-publish-
# bake.yml`'s `publication-layout` input defaults to `generation` (#3461 phase 4) and is always
# passed through to `BAKE_PUBLICATION_LAYOUT`, so no lane run ever reaches the fallback below.
# The fallback stays `flat` on purpose: it is the DIRECT-INVOCATION default — the overlap harness
# and a manual run — and the conservative value is the right one for an invocation that named no

.github/workflows/node-repo-publish-bake.yml:329

  • This fleet-wide claim relies on every caller resolving this revision, but the caller contract in this same file (lines 76–84) and src/MeshWeaver.Documentation/Data/Architecture/CiContentBake.md:1042-1079 require satellites to pin the reusable workflow to an immutable SHA. A caller pinned before this change keeps the old flat default, so neither that producer nor future repos following the contract are flipped by this default alone. Please reconcile the rollout by bumping all pinned callers or changing the pin contract/explicit caller settings.
          per-caller opt-in cost instead was a silent default: a node repo that says nothing — and
          every node repo that will ever be created — published in place. Measured 2026-09-14 over
          the `ci.yml` on `main` of all six node repos (Plugins, Crm, Education, Reinsurance,
          SocialMedia, Manufacturing): every `uses:` of a `node-repo-*.yml` lane is `@main`, every
          `scripts-ref:` is `main`, and NONE passes this input — so flipping the default here is
  • Files reviewed: 8/8 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +175 to +180
# The half-migration this used to guard against — a NEW writer and an OLD writer on one prefix, the
# new one moving the pointer while the old one replaces the flat copy in place and never touches it,
# so a pointer-following reader keeps serving its generation and never sees the old writer's NEWER
# publication — is now UNREACHABLE rather than forbidden: the layout belongs to the PREFIX (#4249,
# `pointer_moved_past_us` and the `$PUBLICATION_LAYOUT=generation` assignment below), so a flat
# caller that resolves a live `_current` publishes a generation anyway and says so. `plugins` is the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct that the residual exists, and the reasoning about where it now lives is what decides the action.

The flat arm has no reachable writer after this change, and I made that enforceable rather than observed. Three ways to reach it:

  1. A caller passing publication-layout: flat. None exists — measured 2026-09-14 across all six node repos' ci.yml on main: none passes the input at all. NodeRepoLaneHostGuard.NoCallerInThisRepository_PublishesFlat now refuses one in this repository, watched failing by flipping main-cd.yml's plugins-bake to flat.
  2. A run that resolves a live _current while asking for flat. That is not the flat arm: fix(#3461): the publication layout belongs to the PREFIX — phase 4 no longer has to be atomic across two repos #4249 promotes it to a generation and names the caller. Before this change a run resolving an empty prefix defaulted to flat; after it, the same run defaults to generation. So this change removes the population your scenario needs rather than leaving it.
  3. A run whose publish-bake-bundles.sh predates fix(#3461): the publication layout belongs to the PREFIX — phase 4 no longer has to be atomic across two repos #4249 — core's plugins-bake on a reconcile at an old platform-ref, the one producer that resolves scripts at the commit it publishes. 🚨 No check added to today's script can help this one, because such a run is not running today's script. And its direction is the safe one: it refreshes the flat copy without moving the pointer, so pointer-following readers stay on the newer generation.

A post-publish pointer re-check on the flat arm would therefore only ever fire for population 1, which the guard above keeps empty. That is a decision rather than an omission, and it is written down: Doc/Architecture/SealedPublicationGenerations gains "The residual phase 4 does NOT remove: a flat publication beside a generation one", with the three entry conditions as a table and the reason there is no check.

On the interleaving regression case: both publications you describe write the flat directory (a generation publication refreshes it last), so whenever they actually overlap there, the byte-level postcondition refuses one of them — which test-publish-bake-overlap.py already covers, same-lane shape included. The uncovered sliver is the one where they do not overlap (the flat run lands wholly after the generation run's seal), and a case asserting that would pin the residual's blast radius rather than prevent it. Phase 5 removes the flat copy and the residual with it; it is tracked on #3461 and its precondition is now measured, with its denominator, on the same page.

Addressed in 7c332547ea.

required: false
type: string
default: 'flat'
default: 'generation'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and this is the finding I should have reached myself — the question that catches this class is "if this default were flat right now, would anything else go red?", and the answer was no.

Added NodeRepoLaneHostGuard.ThePublishBakeLane_DefaultsToTheGenerationLayout, which asserts both halves of the property:

  • the publication-layout input block carries default: 'generation';
  • the publish step still exports BAKE_PUBLICATION_LAYOUT: ${{ inputs.publication-layout }}. That second assertion is not decoration: the script's own fallback is deliberately flat (the direct-invocation default), so a lane that stopped exporting the variable would publish flat with this default: still reading generation — a revert with no diff on the line you would look at.

Watched failing in both directions, since an assertion that has not been seen red is not yet an assertion:

mutation result
default: 'generation' → 'flat' red — Not found: "default: 'generation'", NodeRepoLaneHostGuard.cs:235
BAKE_PUBLICATION_LAYOUT: → BAKE_PUBLICATION_LAYOUT_DISABLED: red at NodeRepoLaneHostGuard.cs:237
both restored green, 14/14 in the file

A second guard, NoCallerInThisRepository_PublishesFlat, keeps the other half of the property: no caller in this repository may take the flat arm (watched failing by flipping main-cd.yml's plugins-bake to flat). Together they are the enforceable form of the measurement the PR body reports.

Landed in 7c332547ea.

Comment on lines +639 to +644
- **Phase 4 is landed** (2026-09-14) — first `main-cd.yml`'s `plugins-bake` passing
`publication-layout: generation`, then the lane input's own default moving to `generation`, which
is what flips the five single-producer prefixes (`crm`, `education`, `reinsurance`, `socialmedia`,
`manufacturing`) with no change in their repositories. Every reader that follows `_current` stops
reading a directory mutated in place, on every prefix. The satellite's own flip is explicitness,
not correctness (phase 4 above). **Phase 5 is open.**

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All of it fair, and each one is now either corrected or explicitly dated. Prose that contradicts behaviour is this family's own recurring defect — the sentence I removed from the script's header ("IT DEFAULTS TO flat AND MUST STAY THAT WAY") is the same shape.

what said what now
Phase 2's heading and its "what actually shipped" bullet — "defaulting to flat" the heading drops the claim; the section opens with a 🚨 line saying it is historical, as this phase shipped, and the bullet reads "defaulted to flat when this phase shipped and defaults to generation since phase 4"
"Its pin half, and phases 4–5, are open" "Its pin half and phase 5 are open … phase 4 is landed"
"until the writer flips, the window is shrunk, not closed" kept, and labelled as the half-truth it became: closed for readers that follow _current, still open for the flat compatibility copy, which is what the postcondition now carries alone
Verification: "90 assertions", one negative control 98 assertions, 98 passed / 0 failed, and every negative control is now labelled with the change that measured it — phase 2's 16-of-67, #4249's 87/3, #4273's 4 failures against 77ec6d4c74, and phase 4's own (the script is comment-only, so the harness is a regression control at 98/0 before and after, and the default is guarded in C# instead)
SealedPublicationReads.md — "defaults to flat, so nothing anywhere writes a generation" corrected, with the pointed remark that the flat compatibility copy is now the only thing on that page still describing what is live
SealedPublicationReads.md — "two Azure-direct readers still address the PREFIX" that was stale independently of this PR: both were fixed in phase 3 and are executed by test-publication-pointer-readers.py (14 cases over the two readers) — which is exactly what made this default move safe for them
BundleDeliveryStages.md — "open — phase 4/5 unlanded" and "phase 4 unreachable from every producer" stage ① now records phase 4 as landed on 2026-09-14, names both halves (#4269/#4273, then the default), and keeps the 2026-09-13 reading as the dated measurement it was

"A figure quoted without its change is how a verification section goes stale" is now written into that section, so the next reader adds a row rather than overwriting one.

Verified after the edits: DocumentationLinkIntegrityTest + topic map + What's New + NodeRepoLaneHostGuard, 14 passed / 0 failed.

Landed in 7c332547ea.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

262 tests  ±0   262 ✅ ±0   2m 10s ⏱️ -6s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 473 tests  ±0   1 473 ✅ ±0   2m 44s ⏱️ -11s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

915 tests  +2   915 ✅ +2   3m 35s ⏱️ +11s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 863 tests  ±0   1 671 ✅ ±0   4m 44s ⏱️ -5s
    4 suites ±0     192 💤 ±0 
    4 files   ±0       0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 103 tests  ±0   1 101 ✅ ±0   6m 15s ⏱️ -7s
    3 suites ±0       2 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

♻️ This comment has been updated with latest results.

…reconcile the migration prose

Three findings from the Copilot review of the phase-4 default move.

1. The fleet-wide behaviour change had NO test. Reverting
   `node-repo-publish-bake.yml`'s `publication-layout` default to `flat` would
   leave every existing test green while all six satellite lanes silently
   returned to in-place publication. `NodeRepoLaneHostGuard.
   ThePublishBakeLane_DefaultsToTheGenerationLayout` asserts the input's
   `default: 'generation'` AND that the step exports it as
   `BAKE_PUBLICATION_LAYOUT` (the script's own fallback is `flat`, so a lane that
   stopped exporting the variable would publish flat with the default still
   reading `generation`). Watched failing in both directions: default reverted
   to `flat` → red on the first assertion; the env export renamed → red on the
   second; both restored → green.

2. The transition residual the review named is real, is documented rather than
   patched, and is kept at zero by a second guard.
   `NoCallerInThisRepository_PublishesFlat` refuses any caller in this repository
   that would take the flat arm (watched failing by flipping `main-cd.yml`'s
   `plugins-bake` to `flat`). The remaining way to reach that arm is a run whose
   `publish-bake-bundles.sh` predates #4249 — which no code added to today's
   script can help, because such a run is not running it, and which leaves
   pointer-followers on the NEWER generation, the safe direction. A new table in
   Doc/Architecture/SealedPublicationGenerations states the residual, its three
   entry conditions and why there is no check on the flat arm.

3. The migration prose contradicted itself after the flip. Corrected:
   - phase 2's heading and its "what actually shipped" bullet said the selector
     defaults to `flat` — now labelled as this phase's own history, with the
     phase-4 value beside it;
   - "Its pin half, and phases 4-5, are open" — phase 4 is landed, and the
     "until the writer flips, the window is shrunk not closed" sentence is now
     stated as the half-truth it became (closed for pointer-followers, open for
     the flat copy);
   - the Verification section reported 90 assertions and only #4249's negative
     control; it now reports 98/0 and labels EVERY negative control with the
     change that measured it, which is what keeps such a section from going
     stale;
   - SealedPublicationReads said the selector "defaults to flat, so nothing
     anywhere writes a generation", and separately that the two Azure-direct
     readers still address the prefix — the latter was fixed in phase 3 and is
     executed by test-publication-pointer-readers.py;
   - BundleDeliveryStages carried "open — phase 4/5 unlanded" and the
     "phase 4 unreachable from every producer" reading of 2026-09-13.

Verified: MeshWeaver.Documentation.Test link integrity + topic map + What's New +
NodeRepoLaneHostGuard, 14 passed / 0 failed; build -c Release -warnaserror,
0 Warning(s) 0 Error(s). No workflow or script behaviour changed by this commit —
`.github/` is byte-identical to the previous one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±0      6 suites  ±0   4m 51s ⏱️ -20s
2 183 tests ±0  2 183 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 187 runs  ±0  2 187 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   18 files  ±0     18 suites  ±0   24m 21s ⏱️ -38s
7 799 tests +2  7 605 ✅ +2  194 💤 ±0  0 ❌ ±0 
7 803 runs  +2  7 609 ✅ +2  194 💤 ±0  0 ❌ ±0 

Results for commit 7c33254. ± Comparison against base commit b831c83.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit c86aea8 Sep 14, 2026
36 checks passed
@rbuergi
rbuergi deleted the fix/3461-generation-is-the-lane-default branch October 10, 2026 13:10
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