fix(spec): the 17 → 18 chain names the bare element:filter / element:form node it leaves standing - #17845
Conversation
…tanding The two ADR-0087 D2 conversions element-filter-removed and element-form-removed strip every authorable key and deliberately leave the bare component node, because deleting an authored page node is a layout decision a mechanical conversion must not make. Once both names joined RETIRED_PAGE_COMPONENT_TYPES the parse began refusing them, so deleting the node stopped being optional and became a required upgrade step with no structured TODO naming it: measured on a stack carrying both nodes, the replay ended schemaValid false and none of the 115 step-18 todos named either element. Adds the ADR-0087 D3 semantic entry element-filter-and-form-node-refused (one file under migrations/entries/semantic, regenerated into the marked region by gen:migration-registry), pins it against the replay, and moves the node-refusal changeset's ADR-0087 disposition onto it. Neither conversion's behaviour changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…ep18-element-node-todo
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 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 7acf81f23bd221590c3719ac021404de9910218c && git checkout 7acf81f23bd221590c3719ac021404de9910218c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 758ac40973fd106b7c61fe0ef14bdf9db641c67a 95f4fb4b9d40c640d778fd4d0ad219c751cd2504 && git checkout -B drift-repro 758ac40973fd106b7c61fe0ef14bdf9db641c67a && git merge --no-ff 95f4fb4b9d40c640d778fd4d0ad219c751cd2504
node scripts/docs-audit/affected-docs.mjs --json 758ac40973fd106b7c61fe0ef14bdf9db641c67a |
retired-key-migrate-sentence.test.ts scans every string literal under packages/spec/src for a backticked `os migrate meta --from N` and requires the house tombstone sentence at that marker. The new entry's acceptance criterion named the command that way to say the upgrade is done, so the scanner judged it as a tombstone prescription and refused it; the sentence was also split across a concatenation seam, which the scanner reconstructs, so both halves were read as one. No entry file in migrations/entries spells that marker: nine name the bare command or a flagged variant, none names --from N. The acceptance criterion now says the same checkable thing without the marker -- replay the same chain over the edited source and the run reports the stack schema-valid rather than the manual-changes warning. Regenerated; the pin cases, the entry id and the ADR-0087 disposition are unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Fixes #17594
Clause-②: no — an ADR-0087 D3 semantic TODO plus a disposition move. No key is added to any published payload, no accept set moves, and the refusal this TODO describes already exists at HEAD.
What was wrong
element:filterandelement:formwere retired whole at element grain (ADR-0049). The two ADR-0087 D2 conversions that carry the retirement —element-filter-removedandelement-form-removed— strip every authorable key and deliberately leave the bare component node: deleting an authored page node is a layout decision a mechanical conversion must not make.That residue was inert until both names joined
RETIRED_PAGE_COMPONENT_TYPESandPageComponentSchema.typebegan refusing them by name. At that point deleting the node stopped being optional and became a required step of the 17 → 18 chain — and nothing in the chain said so.The defect, reproduced before the change
A stack carrying one node of each, driven through the real CLI (
packages/cli/bin/run-dev.js migrate meta --from 17 --to 18):⇒ the zero is a reading, not a probe that could not reach. The nine mechanical rewrites the same run attributed are all to
element-filter-removed/element-form-removed, so the run really did visit both nodes and really did leave them standing.After
Same input, same command:
It names the thing to delete rather than describing it. And the prescription is checkable: with both nodes deleted by hand and nothing put in their place, the same command reports
schemaValid: trueand the human path prints✓ Migrated stack is schema-valid.schemaValidstaysfalsein the after-run by design — the node is still there until a human deletes it. What changed is that the author is now told which node, and what to put in its place.How the entry was added
Per
packages/spec/src/migrations/entries/README.md: one new file, named for its id, then the generator. ⛔ Nothing was typed between theos-generatedmarkers.spec-changes.jsonanddocs/protocol-upgrade-guide.mdare unchanged, correctly: both project only up toPROTOCOL_MAJOR(17). Lit control — the pre-existing step-18 entryelement-number-filter-rule-arrayhas 0 hits in either file too, so the empty diff is a property of the projection range and not of this entry.The ADR-0087 disposition move
.changeset/15110-retired-element-node-refusal.md— the changeset of the PR that made the node refusable — moves fromto
The old disposition was gate-true and semantically thin: the two ids it named registered the key strips, not the node deletion that the refusal turned into a required step. The
registeredform the gate parses carries ids only, so the old marker's rationale sentence does not survive the move — it is superseded by the entry's ownreasonfield, which states the same thing at the place a consumer reads it.node scripts/check-adr-0087-registration.mjs --base origin/main→ exit 0 (and--self-test→ exit 0).Ablation of the new pin
The pin is six cases in
packages/spec/src/migrations/migrations.test.ts. Mutation = delete the entry file and re-run the generator; the fix was committed first, so the restore point is a commit that exists.The sixth case — the replay really does leave the bare nodes — stays green through the ablation on purpose: it pins the conversions' behaviour, which this PR does not touch, so the block is not one undifferentiated assertion.
trap RESTORE_FN on EXIT INT TERMwith absolute paths throughout; the restore is proven bygit hash-object, never by an exit code.No dist preflight: the pinned subject is
./registry.jsimported relatively from inside the same package, so vitest resolves it tosrc/. Noexportshop, nodiston the resolution path.Changeset — measured, not assumed
Build first (both passes confirmed:
dist/.build-input-hashanddist/.build-input-hash-dtsstamped at the same input hash,check-dts-emitted: 34/34), thennpm pack --dry-run --jsonoverpackages/spec(2012 files):dist/index.js,dist/index.mjs,dist/browser/index.js,dist/browser/index.mjs)surfaceproseDelete the component node.element-number-filter-rule-arrayreasonprose//comment⇒ this round's text reaches a published
dist. Changeset written:.changeset/17594-step18-element-node-todo.md,@objectstack/spec: patch.The entry file's internal comment (the measurement provenance) lands in no published byte: it sits above the
import, so the generator reads it as file scaffolding and it never entersregistry.ts. Thepackages/spectsup-does-not-strip-comments trap therefore does not apply here — measured, not reasoned.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackdispatch-gates.mjs --ranreconciliationpnpm --filter @objectstack/spec exec vitest run --project local src/migrations/migrations.test.tspnpm --filter @objectstack/spec test(the local vitest project)VERDICT command-exit 0pnpm --filter @objectstack/spec test:repo(the repo project — the corpus scanners)VERDICT command-exit 0pnpm --filter @objectstack/spec typecheckcheck:test-typecheck, which compiles the test layer undertsconfig.test.json, so the new pin is type-checked rather than merely executedpnpm --filter @objectstack/spec check:migration-registry/check:generatedpnpm exec eslint . --no-inline-configbca74a4601Two of the 83 first came back NOT MEASURED, never green:
pnpm check:dual-build-cjs-loads→ exit 3,PREREQUISITE NOT MET(twelve packages had nodist). Built the closure (turbo run build --filter='./packages/*' --filter='./packages/*/*') and re-ran → exit 0.pnpm check:type-check-debt→ exit 143 (SIGTERM) on the first pass, then exit 3 for the same missing-closure reason. Re-ran after the build → exit 0, 5 ledger entries re-measured, none above its recorded number.Every exit code above was captured before any pipe (
cmd > file 2>&1; EXIT=$?), and every locked run was read from the wrapper's ownVERDICT command-exitline, never a bare$?.The turbo build is a shared cache across worktrees, so
packages/spec/distwas re-checked for this entry by occurrence count after it (1 hit indist/index.js, 1 indist/index.mjs) rather than assumed intact.Correction after the first CI run
The first head (
bca74a46) went red onTest Core, and the failure was this round's:src/shared/retired-key-migrate-sentence.test.tsrefused the new entry's acceptance criterion.That gate scans every string literal under
packages/spec/src(andpackages/lint/src) for abackticked
os migrate meta --from Nmarker and requires the house tombstone sentence at it —Run
os migrate meta --from Nto list the mechanical edits for existing sources; apply them byhand. The acceptance criterion named the command that way to say the upgrade is finished, so the
scanner read it as a tombstone prescription. The sentence was also split across a string-concatenation
seam, which
reconstruct()merges, so both halves were judged as one.Derived from the gate, not from other entries by eye, and then checked against them: no file under
migrations/entries/spells the--from Nmarker. Nine name the bare command or a flagged variant(
--stored); zero name--from N. The house form belongs toretiredKey()guidance an author meets ina parse error; a D3
acceptanceCriteriais the consumer's verify loop, and it stays checkable — themeasurement above showed that deleting both nodes makes the same command report
schemaValid: true.⛔ The gate was not weakened, skipped, or given a baseline entry. The sentence moved.
Re-measured on head
95f4fb4b:Why the first round missed it:
packages/specdeclares two vitest projects —testruns--project local,test:reporuns--project repo— and the repo-wide corpus scanners live in thesecond. A green from
testis not a reading abouttest:repo.testrun on the freshly re-created worktree reported470 passed | 1 skipped.packages/spec/distdid not exist yet, so a dist-dependent file skippeditself. Building spec and re-running returned the exact 471 / 13405 above — a property of the checkout,
not of the edit.
Unchanged by this correction: the six pin cases, the entry id, and the ADR-0087 disposition move.
验收备注
.changeset/15110-…mdnow readsregistered element-filter-and-form-node-refusedwhile the entry is added by this PR rather than that one. The gate never judges that row (its changeset is breaking at base as well as at head, so it is inherited stock and skipped), and both changesets ship in the same unreleased window, so the ledger is truthful at release time. Noted rather than filed: it is a property of moving a disposition across PRs, not a defect.Generated by Claude Code
Generated by Claude Code