Uh oh!
There was an error while loading. Please reload this page.
fix(spec,core): HotReloadConfig.stateStrategy refuses the two values it never implemented; distributedConfig retired (#12340) - #12425
Conversation
…nted; distributedConfig retired (#12340) ADR-0049 enforce-or-remove, applied one level inside the host-driven library the 2026-08-25 #11825 ruling kept. The 'disk' and 'distributed' arms of PluginStateManager.saveState both wrote to the same in-memory Map as 'memory' and said so at debug level only; distributedConfig had zero readers anywhere. - spec: stateStrategy narrowed to ['memory','none'] with the prescription on the enum's own error map (invisible to the four ratchets by construction); distributedConfig and DistributedStateConfigSchema removed (route 3 — no authored document, nothing parses this schema). - core: registerPlugin refuses an unhonoured stateStrategy and a leftover distributedConfig with an ADR-0112 envelope, before the enabled check. - ADR-0087: RETIRED_DEFS_BY_MAJOR[18] + D3 semantic entry. - the #11825 survivor pin for DistributedStateConfigSchema moves here deliberately, with the reasoning recorded beside it. Part of #12340 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
…emoved') The refusal fired correctly; the assertion named the singular. Caught by the run, not by reading. Part of #12340 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
…dStateConfig removal kernel/ 277 -> 274, fully accounted for: the removed DistributedStateConfigSchema carried exactly three z.object nodes (the def plus its inline auth and replication sub-objects). The prose ledger has no per-file verdict for plugin-lifecycle-advanced.zod.ts, so no Class verdict needed re-examining. Part of #12340 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 132 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2001bbe4e6a73549193e5ded1afb552cde65d5ac && git checkout 2001bbe4e6a73549193e5ded1afb552cde65d5ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b6c96bcea5d4352c5a63b4aa9a8d6809b9ce15cf d76e5e5a61f7748c01061aed52d6306d7d2335cb && git checkout -B drift-repro b6c96bcea5d4352c5a63b4aa9a8d6809b9ce15cf && git merge --no-ff d76e5e5a61f7748c01061aed52d6306d7d2335cb
node scripts/docs-audit/affected-docs.mjs --json b6c96bcea5d4352c5a63b4aa9a8d6809b9ce15cf
|
…tree The merge of `main` into this branch left the `kernel/` count as OURS (274) — the `merge=os-regen` driver defers rather than text-merging, because a generator run mid-merge reads a half-merged tree. Regenerated from the fully merged sources, as that driver's pending marker requires. 277 at the merge base; this branch's DistributedStateConfig removal takes 3 (the def plus its inline `auth` and `replication` objects) and #12417's `contributes.routes` retirement on main takes 1, so 273 is the composition of both sides, not a choice between them. Predicted before the run; observed exactly. `check:strictness-ledger` green.
os-warren
commented
Aug 26, 2026
Conflict with |
| file | resolution |
|---|---|
packages/spec/src/migrations/registry.ts | both sides' rows kept — verified by regenerating, not by reading the merge |
docs/audits/2026-07-unknown-key-strictness-ledger.counts.md | regenerated — the text merge was wrong here |
The registry needed no correction, and that is a measurement
Both entries survived: this branch's hot-reload-inert-state-strategies-retired / kernel/DistributedStateConfig, and #12417's plugin-manifest-contributes-routes-retired / kernel/Manifest:contributes.routes. check:migration-registry (--self-test --check) is green — 123 semantic, 59 retired-key, 68 retired-def — so the merged text is byte-identical to what the generator produces from the merged entry files. That is the check that matters; agreeing rows in a diff would not have been.
The counts ledger was silently wrong after a clean merge
This file is merge=os-regen. That driver deliberately does not text-merge — it resolves to OURS as a placeholder and defers, because a generator run mid-merge reads a half-merged tree. So git reported no conflict and left the branch's own number standing:
merge base cdbd9204b6 : kernel/ 277
this branch 2d0b7a6f88 : kernel/ 274 (-3: DistributedStateConfig + inline auth/replication)
main b6c96bcea5 : kernel/ 276 (-1: #12417 contributes.routes)
after merge, before regen : kernel/ 274 <- OURS placeholder, main's -1 dropped
273 is the composition of both sides, not a choice between them. Predicted in writing before running the generator; gen:strictness-ledger produced exactly 273 and no other row moved. The pre-commit hook then confirmed the deferred artifact was current and cleared the pending marker.
Re-verified on the merged tree
Green, each exit code captured before any pipe: check:strictness-ledger (436 sites, counts file current), check:migration-registry, check:spec-changes, check:upgrade-guide, check:adr-0087-registration (1 declared-breaking changeset, disposition registered), check-changeset-no-major (no major bump). Tests: spec plugin-lifecycle-advanced + -retirement19/19, core hot-reload11/11.
No source file was touched by this resolution — the diff over 2d0b7a6f88 is one line in the counts ledger. Auto-merge re-armed; the rest is CI.
Generated by Claude Code
Fixes#12340
Clause-②: yes — this narrows an accept set. Configurations that boot today start failing: any host passing
stateStrategy: 'disk'/'distributed', or adistributedConfig, is now refused. In-tree blast radius is zero (measured, below); the cost is borne by out-of-tree hosts of a published library.What was wrong
PluginStateManager.saveStatehad fourstateStrategyarms and implemented two.'disk'and'distributed'both wrote to the same in-memory Map as'memory'— the in-source comments said "memory fallback" — and announced the substitution at debug level only. A host that asked for durable or cluster-replicated state got process-local memory and no error: state that does not survive the restart it was configured to survive.distributedConfighad zero readers, so an author could name a Redis endpoint, a TTL and a replication factor and nothing ever opened a connection.Reproduced before repairing (worktree, base cdbd920):
Zero-reader measurement, with a firing positive control
A zero-hit is not a reading until a control fires.
stateStrategyresolves to real readers —packages/core/src/hot-reload.ts(3) andpackages/core/examples/phase2-integration.ts(2). The scan sees readers.distributedConfigreference is insidepackages/specitself (declaration, its own unit test, generated artifacts) plus the generated reference page. Nothing else.incomplete_results: true), so I am declaring it uncovered rather than counting its zero as evidence.'disk'or'distributed'. Every real caller passes'memory'or'none'. The two values appear only in spec's own unit test — which pinned exactly the arms being deleted.Route, and why
ADR-0049 enforce-or-remove. The triage charter chose remove/narrow-loudly; the retirement playbook's fork turns on one fact I measured: nothing in the tree parses
HotReloadConfigSchemaoutside its own unit test. That is playbook route 3 — no tombstone, no D2 conversion, because a parse-time prescription reaches nobody andHotReloadConfigis not an authorable surface (no metadata-type binding, stack collection or manifest embed ever carried it). Same route the sibling retirement in this module used yesterday.Two shapes in one card, and the playbook predicts opposite ratchet readings for them — both confirmed:
stateStrategyenum-value narrowingkernel/HotReloadConfig:stateStrategyunchangedDistributedStateConfigwhole-def removalapi-surface-3,authorable-surface-8,json-schema.manifest-1The def-removal route printed its own evidence — the build refused first and named the remedy:
Because route 3 leaves no parse-time prescription, the doors moved to where the host actually meets the library:
HotReloadConfigParsedfieldstateStrategyis now'memory' | 'none', a compile error at the call site.HotReloadManager.registerPlugin, with an ADR-0112 envelope (code: VALIDATION_ERROR,status: 400) carrying the prescription. Thrown before theenabledcheck, so a disabled config cannot smuggle the false declaration through.distributedConfigis refused too, rather than silently stripped — this is the answer to the authorable-surface gate's stated worry, for the one audience that exists.errormap carries the prescription for anyone who does parse, dispatched byissue.input, so a typo is not told it "was removed".The one thing to look at
DistributedStateConfigSchemawas named in the 2026-08-25 survivor list of the #11825 ruling. This PR reverses that line. The new evidence: that ruling measured the container's six groups and never this key's own readers. Its only referencing key left with the enum value its doc comment called it "required" for, so the schema had nothing left to be the vocabulary of.The keep itself is intact and still pinned:
HotReloadConfigSchema,PluginStateSnapshotSchemaand the health vocabularies still export;HotReloadManager/PluginHealthMonitoruntouched. The pin moves in the same commit with the reasoning recorded beside it, and now asserts the surrounding keep is intact — never a quiet edit to make a red pin green. This is the call worth a maintainer's eye.Verification (all at 2d0b7a6)
Gate union derived, not recalled —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackover the real changeset: 45 families. Each exit code captured before any pipe; ratchet families re-run at the final head.sdui-parser,formula, specdist), and 2 were real:check:strictness-ledgerandcheck:generated.check:liveness,check:empty-state,check:variant-docs,check:adr-0087-registration,check:changeset-no-major: 11/11 green.check:strictness-ledger:kernel/277 to 274. Read, as the gate demands: the removed def carried exactly threez.objectnodes (itself plus inlineauthandreplication). Fully accounted for; the prose ledger has no per-file verdict for this file to re-examine.check:skill-examples(needsclient-reactbuilt; the foreground build hit the container's 10-minute cap). It had already cleared the 227 spec+docs blocks this change touches and stopped only on the unrelated client-SDK surface.check-dev-prereqslikewise wants a full monorepo build. Both left to CI.Ablations — direction predicted in writing first, mutation proved on disk by anchored
grep -cFcounts before any result was read, restore trap on both:registerPluginrefusal call'disk'back in the spec enum'disk'refusal'disk'refusalNeither rebuilt, and neither needed to: both subjects resolve through relative source imports, not through a package
exportsfield todist. The only@objectstack/spec/kernelimport in the core test isimport type, erased at runtime. Both restore legs verified byte-identical to HEAD.Changeset
minorfor both packages, graded deliberately: this module's sibling retirement established the lockstep launch-window convention — v17.0.0 is already cut, so breaking export removals rideminoron the 17.x line with the prescription registered under protocol major 18.check:changeset-no-majoris green, which is the mechanical confirmation.Generated by Claude Code
Generated by Claude Code