Skip to content

fix(everything-dev): release-scoped extends inheritance + deploy-time composition gate - #656

Merged
elliotBraem merged 2 commits into
mainfrom
fix/release-scoped-extends-inheritance
Oct 29, 2026
Merged

elliotBraem merged 2 commits into
mainfrom
fix/release-scoped-extends-inheritance

Conversation

@elliotBraem

@elliotBraem elliotBraem commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

The incident

A child app pinned to framework rc.24 pushes a commit. CI runs, the deploy train pulls the extends chain — and the auth slot (children never author app.auth; bos init deletes it) resolves from the parent's live published config, which the root moved to rc.25 with its latest deploy. The child's own ui is still built against its lockfile (rc.24). At the next host boot:

Shared singleton "everything-dev/ui/auth" skew across UI sources: "auth" was built against 2.0.0-rc.25,
"ui" against 2.0.0-rc.24 — the composition refuses to load a split module graph.

SSR composition fails, health stays degraded / the prod host exits. Any host restart after the root's publish hits this — the child's git push is only the common trigger. This should never happen.

Why the plan 057 semver fix doesn't cover it: checkPlatformCompat only ran in bos upgrade reports / bos mf check — never in the deploy train — and a child resolving via its own rc.24 slot pin passes compat; the inherited rc.25 auth pin was invisible to every gate. The host boot guard was the only detector — i.e. after the broken composition was already live.

The fix (ADR 0034 amendment — release-scoped extends inheritance)

Layer 2 — the root cause: extends inheritance is pointer-live, not release-scoped

  • The release manifest records its own slot-pin map (slots — additive, optional; keyed by dotted config path, base + immutable version-manifest pin), composed from the release payload on the release leg (sorted keys — byte-stable; the burned-version guard already enforces immutability).
  • The deploy train stamps every inherited slot (a slot in the release's slots map that the authored config does not declare) with the authored platform pin's release-recorded production URL + pin. Child-wins deep merge then shadows the parent's live pointer at every consumer boot — a pinned child's runtime becomes a deterministic function of (child code, pinned release). Authored slots are untouched; ui: false opt-outs own their slot; manifests without slots (pre-amendment releases) stamp nothing. Config-only publishes carry the stamps forward as pipeline state (mergeAuthoredOverPublished keeps published-only keys); rollback payloads skip stamping (canonical).
  • Stamped pins get SRI/byte verification for free via the existing verifyRollbackSnapshot leg.

Layer 1 — the deploy-time composition gate (PublishTrainError kind composition)

  • Before publish: the extends-merged shape is cross-compared on embedded shared singletons — the host boot guard's mirror (boot-check.ts:resolveBootShape shares one walk with verifyBootPayload).
  • For authored platform pins, the ADR 0034 compat check now runs in the deploy train, scoped to the child's own resolved release (slot-first) — never the parent's latest. Catches the reverse skew too (lockfile bumped without the platform pin, e.g. a Renovate auto-merge).
  • A skewed deploy is now a red CI run with the bos upgrade remediation named — never a degraded production boot.

Explicitly rejected: relaxing the everything-dev/ui/auth strict singleton to sonner-style tolerance — it exists because two divergent session-read copies disagreed into redirect loops (d57b8f4); tolerance reintroduces that bug class.

Rollout notes

  • Already-published release slots are immutable (burned-version guard), so the very first slots-carrying release can't hand child pins retroactive rescue — children pinned to pre-slots releases are protected by the composition gate (deploy blocked, remediation named) until one bos upgrade + deploy; after that they're permanently immune.
  • Unpinned configs (no platform field — hand-rolled; bos init always writes one) resolve latest and stamp the latest release's slots; the gate covers their drift.
  • Child deploy smoke stays opt-in.

Tests

  • tests/unit/composition.test.ts (16 tests): stamping (inherited/authored/ui:false/stale-override/no-slots), authoredSlotPaths, skew violation + skip-unresolvable + agree + non-UI, compat leg (aligned / lockfile drift / unpinned), release-slot composition + byte stability.
  • platform-upgrade-e2e.test.ts (2 new legs on the in-memory registry + a local HTTP server for pinned bytes): the incident replay — root publishes rc.1 (slots recorded) then rc.2 (live config moves to react 19.3.0); the pinned rc.1 child's deploy stamps auth slots from rc.1 and the gate is green; without stamps the gate blocks with the named skew and remediation.
  • Full framework suite: pnpm --filter everything-dev test — 1689 passed. Typecheck + biome + effect lint clean.

Addresses the recurring "auth skew on redeploy" class; #546 supplies the error taxonomy.

… composition gate

A root publish moved the inherited `app.auth` slot (children never author it;
`bos init` deletes it) to the newest release while a platform-pinned child's
own ui kept building against its lockfile — the next child deploy/restart
composed auth@rc.N+1 with ui@rc.N and the #546 boot guard degraded health.
The platform pin governed image, sync, and compat reporting, but nothing
mapped it onto inherited slot pins: extends inheritance was pointer-live,
not release-scoped.

Two layers (ADR 0034 amendment):

- The release manifest records its own slot-pin map (`slots`, additive,
  composed from the release payload on the release leg — sorted keys so
  identical payloads compose identical bytes).
- The deploy train stamps every inherited slot (absent from the authored
  config) with the authored platform pin's release-recorded production URL +
  pin — child-wins over the parent's live pointer at every consumer boot;
  a child's runtime becomes a deterministic function of (code, pinned
  release). Config-only publishes carry the stamps forward via
  mergeAuthoredOverPublished; rollback payloads skip stamping.
- The deploy train gains the composition gate (`PublishTrainError` kind
  "composition"): extends-merged shared-singleton cross-check pre-publish —
  the host boot guard's mirror — plus the platform compat check scoped to
  the child's own resolved release. A skewed composition now dies as a red
  CI run with the `bos upgrade` remediation named.
- boot-check gains `resolveBootShape` (the walk shared with
  `verifyBootPayload`) and `boot-check`'s e2e coverage extends to the
  incident replay.
- runPublishTrain skips the composition gate for exactPayload rollbacks —
  the recovery path must never be blockable by the parent's moved-on pins
  (boot-check + pin verification already gate rollback bytes); a missing
  boot shape fails loud instead of silently gating a stale fallback.
- boot-check splits verifyBootPayload into resolveBootShape (walk) +
  verifyResolvedBootShape (pure verify); the train shares one walk with the
  gate, the untyped preResolved param and its unreachable payload fallback
  are gone, and the pre-rewrite non-object payload message is restored.
- new services/config-slots.ts — the one app/plugins slot walker (canonical
  dotted paths; ui:false and bare-ref visits included) plus
  ensureObjectContainers; collectPinnedSlots, collectReleaseSlotPins,
  authoredSlotPaths, and slotPins/pointerFingerprint all derive from it,
  ending the four silently-divergent traversal copies (rollback's hardcoded
  app keys vs iterate-all). Rollback slot names canonicalize to the shared
  dotted paths ("auth" → "plugins.auth").
- the composition gate reports unresolvable merged-shape UI pins outside
  the pin-verification-verified set as violations instead of skipping
  silently — inherited pins were not covered by that leg.
- resolveReleaseManifestForDeploy returns the manifest directly (the
  single-field wrapper is gone); shared tests/helpers/version-manifests.ts
  replaces the duplicated manifest-fixture fetchers; unmarked comments in
  the touched hunks are deleted or KEEP-marked.
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.

1 participant