feat(spec): refuse bare element:filter / element:form nodes by name, and stop suggesting retired component types - #17592
Conversation
Source half of #15110: the two elements join RETIRED_PAGE_COMPONENT_TYPES with the element-grain tail of their own retiredKey tombstones, the typo suggester stops offering retired keys, and the prose the node refusal falsifies is corrected. Tests follow in the next commit. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…position Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 135 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 688df7b3ca547e80a0068567fa69f94b0ea9f9e2 && git checkout 688df7b3ca547e80a0068567fa69f94b0ea9f9e2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abc4b83ce8b01d4e5af3560a0547421f6f5ca98f c712af03f8803d717a3d30a4f6363d9c7f0cd565 && git checkout -B drift-repro abc4b83ce8b01d4e5af3560a0547421f6f5ca98f && git merge --no-ff c712af03f8803d717a3d30a4f6363d9c7f0cd565
node scripts/docs-audit/affected-docs.mjs --json abc4b83ce8b01d4e5af3560a0547421f6f5ca98f
|
|
Seat adoption record — adopted VERBATIM. Tier verified from the transcript, ⛔ not self-report: 136 harness-stamped ⭐ The fence held, and it was checked the hard way. This was the one thing that could have made the narrowing a contract change the lane had no authority for. The reviewer did not take the round's quotations on trust: it read both docblocks at the merge base and found the sentence byte-identical in each, read ⭐ It also improved the seat's own reasoning rather than just accepting it. The order told it to test hard whether "a gate forbids ⭐ And it found the consequence the PR did not name, which is the kind of thing an adversarial reader exists for. The node refusal flips Landing pre-checks — three read, ④ outstanding
⇒ Disposition of the three findings, applying the same test in both directions rather than filing everything the verdict names:
Contract review (
|
Fixes #15110
Clause-②: yes — half ① narrows a published accept set: two
element:types that parse clean today are refused by name.needs:contract-reviewhangs on both carriers; nothing lands until an at-tier review returns.The card is three pieces of very different natures. All three ship here.
element:filter/element:formjoinRETIRED_PAGE_COMPONENT_TYPESKNOWN_COMPONENT_TYPE_CANDIDATESstops offering retired keysThe fence, cleared per member before either was added
Triage's fence, quoted rather than paraphrased:
Each member's own docblock, quoted from
packages/spec/src/ui/component.zod.tsat merge baseef180302f5:ElementFilterPropsSchema(:2052–2055) — "A bare node with emptypropertiesparses clean (the opentypeunion accepts any string, so a node-level refusal is not expressible here); the migration strips the keys and leaves exactly that bare, inert node."ElementFormPropsSchema(:2102–2105) — the same sentence, verbatim, one element over."Not expressible here" is a capability statement, and it sits between two sentences that make its intent unambiguous: the same docblock says deleting the
ComponentPropsMaprow would turn "a loud retirement back into a silent no-op", which is the outcome it is written to avoid. The landed ruling names those two sentences as a gap in its own words —page.zod.ts(:66–68), shipped by #14159: "the gap theelement:filter/element:formretirements recorded as 'a node-level refusal is not expressible here'. It is expressible one level up." Fence cleared for both members. Nothing here reasons from family resemblance, and no member outside the two was touched.What changed
① Two entries in
RETIRED_PAGE_COMPONENT_TYPES. No new prose was authored: each node prescription is the element-grain tail of that element's ownretiredKeytombstones with its per-keyproperty KEYNAMEclause dropped, so the node door and the props door carry one text. A pin incomponent.test.tsholds them equal byte-for-byte, which is how the "one prescription, three doors, no drift" shape reaches a type whose row cannot bez.never.The rows stay as they are.
user:profileisretiredComponentPropsbecause it never had an authorable key; these two carry six tombstoned keys each, where a per-key prescription says more than one whole-bag refusal could. Both halves are pinned.② The candidate pool is now the known set minus whatever the vocabulary retired — derived from the retirement map, not restated beside it, so a type retired tomorrow leaves the pool the day it lands.
isKnownComponentTypeis unchanged: known and writable are different questions, and keeping the retired names known is what makes their refusal a located prescription instead of an unregistered-custom-string skip.③
user:profileadded to the row-superset sentence.Prose this change falsified, corrected in the same diff. The two D2 conversions' customer-facing
summarystrings and the protocol-18 upgrade note said the stripped node "stays, inert as it always was" — 28 occurrences acrossconversions/registry.ts,migrations/registry.tsand the twelve retired-key entries, now zero (the registry mirror regenerated withgen:migration-registry). The conversions' behaviour is untouched: they still strip the keys and still leave the node, because deleting authored page nodes is a layout decision a mechanical conversion must not make. What changed is that the node they leave is refused by name, so the prose says that instead.Behavioural proof, both directions, through the real doors
Measured with a probe driving
PageComponentSchemaandvalidateComponentTypes(thecomponent-type-unknownrule itself, never the candidate array).Node parse — before → after:
Suggester — before → after, and the reverse check:
The reverse check the card asked for: a typo that used to resolve to a retired name now proposes nothing and falls back to the own-namespace prescription. It does not propose something worse — no STOP. A pin asserts the stronger property directly: whatever the rule proposes must itself pass the rule. Candidate pool 46 → 43.
Ablation
Two mutations, each proven on disk before its colour was read, each restored by state.
MUT-A — drop both members from the map (source-only;
packages/spec's own vitest readssrc). Occurrence counts bygrep -o | wc -l: 1 → 0 per member; blob680ad6cc→e84d8357. Run: 12 tests failed incomponent.test.ts. Restore: blob back at680ad6cc,git diff HEADempty.MUT-B — remove the retired-type subtraction (the subject resolves
@objectstack/spec/uithroughexports→dist, so every leg rebuilt and was preflighted). Guard 1 → 0 on disk, blob1c1a40be→d8cadc83;ablation-dist-preflight --absentconfirmed the marker gone from all 90 built files. Run: 2 tests failed, on exactly the historical strings —expected 'Rename \element:fitler` → `element:fi…' not to contain 'element:filter'. Restore leg: rebuilt, preflight found the marker back indist/ui/index.jsanddist/ui/index.mjs, whole-treegit status --porcelain` clean, suite back to 29/29.One honest note on MUT-B: the mutated build exited 1 at the DTS step only (
TS6133, the import left unused by the mutation) — after the JS bundle was written. The preflight, not the build's exit code, is what licenses the reading, and it showed the mutated artifact in place.Verification
At
c712af03f8:pnpm --filter @objectstack/spec test— 472 files / 13381 tests passedpnpm --filter @objectstack/lint test— 103 files / 3747 tests passed (the 40 files that failed on first run were an unbuilt dependency closure; green after--filter '@objectstack/lint^...' build)typecheckfor both packages — green, test layers includedcheck:generated,check:migration-registry,check:spec-changes,check:upgrade-guide,check:docs,check:authorable-surface,check:api-surface,check:yaml-examples,check:liveness,check:strictness-ledger,check:adr-0087-registration,check:changeset-no-major,check:nul-bytes,check:doc-authoring,check:docs-spec-enumerations,check:quick-reference-counts,check:page-declaration-shape,check:published-files,check:type-check-coverage,check:pm-widening-tells,check:cross-package-test-inputs,check:test-source-alias,check:spec-docblock-symbol-anchors,check:comment-mask-adoption,check:comment-mask-corpus,check:doc-frontmatter,check:undeclared-dep-imports,check:closing-keyword-parity,docs-audit/check-affected-docs,check:docs-redirects,check:docs-audit-scope,check:changeset-gate-self-tests,check:docs-transcript-drift,check:corpus-claim-driftcheck:type-check-debt— NOT MEASURED, exit 3: it needs the whole-workspace build closure CI builds before that step. Nothing was measured, so this says nothing in either direction.pnpm lintis CI's run, not this round's.Changeset
@objectstack/spec: minor, measured rather than asserted. The change moves published artefacts —page.zod.tsandcomponent.zod.tsship as source underfiles[]'ssrc/**/*.zod.ts, and bothdist/andjson-schema/carry the new prescriptions. The level isminorbecausecheck-changeset-no-majorforbidsmajorduring the launch window and the repo's convention puts accept-set narrowings on minor releases (migrations/registry.ts: "the launch-window convention: accept-set narrowings ride minor releases"); #14159's own narrowing shipped the same way. The body carries the BREAKING banner and anot-required (already-registered …)ADR-0087 disposition naming the two pre-existing conversion ids.Scope
packages/spec/src/shared/union-author-message-pins.test.ts,packages/spec/src/ui/view.zod.ts,packages/spec/src/contracts/**andpackages/plugins/plugin-approvals/**are untouched. The dead reference atcomponent.zod.ts:1179(#17578) is untouched. Nothing undercontent/docs/releases/and noCHANGELOG.mdwas edited.Generated by Claude Code