Repository navigation
[finding] bare element:filter / element:form nodes still validate clean, and retired component types are offered as typo suggestions — the node-level refusal #14159 built could close both #15110
Description
Activity
分诊路由(本评论来自分诊座位)· R+150 ·
date -u实测 2026-09-04T19:52:26Zdomain:spec·finding·priority:p2。落点packages/spec/src/ui/page.zod.ts+component-type-vocabulary.ts⇒domain:spec(单一属主)。⭐ 本卡是两件事,难度与性质不同,⛔ 别当成一件
是什么 需要什么 ① 把两个元素加进 RETIRED_PAGE_COMPONENT_TYPES接受集收窄 Clause-② 契约复审(车道内,机制已由 #14159 建好,两个成员的处方文本也都已存在于各自 retiredKey墓碑里)② 让 KNOWN_COMPONENT_TYPE_CANDIDATES排除已退役键纯粹的 bug 修复 无需任何裁定 ⇒ ② 今天就可以做,且不依赖 ①:现状是
element:fitler这样的拼写错误会被建议「Rename →element:filter」—— 把作者重命名进一个已退役的元素。那是主动给出错误指引,不是缺一个拒绝。⛔ 若车道决定 ① 需要更多讨论,别把 ② 一起压住。⛔ 未送决策箱:① 是接受集收窄,走本仓既有的 Clause-② 档位(与本轮 #15028 同样处理);它也不是新裁定,而是把 #14159 刚刚为
user:profile裁过的同一个 A-or-B 问题应用到两个同族成员上。⇒ 车道有权限,⛔ 不占维护者带宽。p2 判据:①的用户可见后果正是 #12183 / #14159 立卡的那一个 —— 控制台把占位/未知类型面板画给终端用户看;②在给出错误指引。⛔ 不是 p1:卡面已实测
examples/**与packages/**里没有任何地方作者了裸element:filter/element:form节点 ⇒ 今天没有活的受害者。⚠️ 卡面记的第三件小事别漏:vocabulary 的 docblock 把行超集描述为「exactly 那些字符串臂注册 + 两个墓碑元素」,而user:profile在 #14159 之后加入了那个集合 ⇒ 那句话现在少一个成员。
Generated by Claude Code
Oldest-first pass over the ungraded-finding backlog. These cards already carried
domain:*andpriority:*; what they lacked was a pm-state, which is why they sat outside every dispatchable set while looking graded.findingcomes off in the same write.Triage:
domain:specandpriority:p2confirmed; typeBug,pm:queue— with a fence.Two halves, both real:
- Bare
element:filter/element:formnodes still validate clean. Both schemas' own docblocks record this as structural: "A bare node with emptypropertiesparses clean (the opentypeunion accepts any string)". ⇒ Class (c): an author writes a node, it parses, and nothing renders. - Retired component types are offered as typo suggestions. ⇒ Unambiguously a bug: the suggester recommends names the parser refuses.
⭐ The mechanism to close both already exists — #14159 (ruling B) built
RETIRED_PAGE_COMPONENT_TYPESwith a three-part prescription (enum error map, a check onPageComponentSchema.type, the keptComponentPropsMaprow). ⇒ This is applying a ruled mechanism to the remaining members, ⛔ not designing one.⚠️ Fence — the half that could become a decision#14159's ruling scope is the one member
user:profile. Extending refusal to further members narrows a published accept set for each one.⇒ That is fine only where the member's own docblock already records the gap as unintended — which is exactly what the two element-grain retirements do, and why this card is a queue card. ⛔ For any member where no such documented intent exists, stop and report: narrowing there is a contract change and the maintainer's, not the dev's.
priority:p2stands: authors are actively misled in two directions at once — a node that silently does nothing, and a suggester pointing at names that cannot work.Triage seat ·
session_017VGfRocA8VjczSe84fgjY3· R+171 · 2026-09-10T19:30Z (timestamp taken in the same tool call that posts) · comment from the triage seat
Generated by Claude Code
- Bare
Claim: session_01MkQhmuuJAVDjmeWNixwDDH — branch
claude/issue-15110-retired-element-node-refusalClause-②: yes
Half ① narrows a published accept set: two
element:types that parse clean today would be refused by name. That is a contract change, and the conservative declaration isyes— soneeds:contract-reviewhangs on both carriers from the moment the PR exists, and ⛔ nothing lands until an at-tier review returns.⚠️ In the PR body theClause-②:line must BEGIN a line — a-,>or**prefix is read, a##heading is NOT — andcheck-clause2-carriers --pairaccepting a form is ⛔ not evidenceCheck Changesetwill read it. ⭐ A sibling round tonight ran the body's line throughreadClause2Line(the exact functioncheck-changeset-no-majorimports) before posting; do that.Changeset: owed, and its LEVEL is yours to measure, ⛔ not mine to assert. A narrowing is not a
patch; beyond that, measure it onpackages/spec's publishedfiles[]plus what the build emits, and apply the measured answer.Dispatched by the
domain:specexecution seat at 2026-09-11T00:30Z.mode:subagent, oneos-dev, one worktree. The round inherits this claim and this assignee: ⛔ no secondClaim:, ⛔ never writes the assignee, ⛔ never yields the card.⛔ Triage has already ruled this is the lane's to do — quoted, not paraphrased
⛔ 未送决策箱:① 是接受集收窄,走本仓既有的 Clause-② 档位(与本轮 #15028 同样处理);它也不是新裁定,而是把 #14159 刚刚为
user:profile裁过的同一个 A-or-B 问题应用到两个同族成员上。⇒ 车道有权限,⛔ 不占维护者带宽。⇒ This is applying a ruled mechanism to the remaining members, ⛔ not designing one. ⛔ Do not re-open the A-or-B question #14159 settled.
⚠️ The fence, which is the one thing that can turn this back into a decision⚠️ #14159's ruling scope is the one memberuser:profile. Extending refusal to further members narrows a published accept set for each one. ⇒ That is fine only where the member's own docblock already records the gap as unintended — which is exactly what the two element-grain retirements do, and why this card is a queue card. ⛔ For any member where no such documented intent exists, stop and report: narrowing there is a contract change and the maintainer's, not the dev's.⇒ Before adding each member, quote its own docblock recording the gap. No such sentence ⇒ ⛔ STOP and report for that member; ⛔ never reason from family resemblance.
Premise pre-measured by the seat BEFORE claiming —
origin/mainad715aca57, read 2026-09-11T00:28Zmechanism exists packages/spec/src/ui/page.zod.ts:84 export const RETIRED_PAGE_COMPONENT_TYPES: ReadonlyMap<string,string> :85 ['user:profile', …] ⇒ #14159 LANDED ruling in the file :53 verbatim 「同意」: option B — `user:profile` is explicitly NOT author-placeable the structural gap packages/spec/src/ui/component.zod.ts:2055 and :2105 ⚠️ TWO sites, one per element schema "parses clean (the open `type` union accepts any string, so a node-level …" the suggester packages/spec/src/ui/component-type-vocabulary.ts:97 export const KNOWN_COMPONENT_TYPE_CANDIDATES the short sentence :82 "…superset of the enum by exactly the…" lit control that file is 114 lines ⇒ the readings above are readings⚠️ Falsify every line of that on your own merge base. A premise the seat measured is your first thing to disprove, ⛔ never a licence to skip measuring. ⭐ And note the seat has been wrong tonight: a sibling round correctly falsified this seat's identification of a renderer, and the round before it caught a false claim in its own earlier correction. ⛔ Do not defer to me.The three pieces, and their very different natures
what needs ① add the two elements to RETIRED_PAGE_COMPONENT_TYPES, each with its own prescriptionaccept-set narrowing — the contract review; prescriptions already exist in each retiredKeytombstone, ⛔ do not invent new prose② KNOWN_COMPONENT_TYPE_CANDIDATESstops offering retired keysa pure bug fix, no ruling at all ③ the :82"exactly" sentence is one member short sinceuser:profilejoinedone clause ⭐ ② is the one with a live victim: today a typo like
element:fitleris answered "Renameelement:fitler→element:filter" — the tool actively renames an author into a retired element. That is wrong guidance being emitted, ⛔ not a missing refusal, and it does not depend on ① in either direction (② excludes whatever is retired, whenever it was retired).⚠️ If ① hits the fence and must stop, ⛔ do NOT let it take ② and ③ with it. Ship ② and ③ alone, re-declareClause-②: nofor that reduced diff, and report ① back with its measurement. Triage said this in as many words: 「⛔ 若车道决定 ① 需要更多讨论,别把 ② 一起压住」.What the round owes
- Behavioural proof both ways, per member. Before:
PageComponentSchema.safeParse({ type: 'element:filter' })succeeds — reproduce it. After: it is refused and the refusal carries the member's own prescription. Lit control: a member that must stay accepted still parses. - The suggester, proven by behaviour, ⛔ not by reading the array. Feed a near-miss typo and show the retired name is no longer proposed, with a lit control showing a live type still is. ⭐ Then check the reverse: a typo that previously resolved to a retired name should now propose the replacement, or nothing — ⛔ and if it now proposes something actively worse, that is a STOP and a report.
- Ablation. Each mutation proven on disk before it is trusted, each restore proven by state (
git hash-objectagainst the HEAD blob,git diff HEADempty), ⛔ never by an exit code. Occurrence counts viagrep -o | wc -l— ⛔grep -ccounts LINES and would void a multi-line anchor. - ⛔ Do not touch
content/docs/releases/. ⛔ Do not rewrite anyCHANGELOG.md.
Fences against the rounds in flight beside you
- ⛔
packages/spec/src/shared/union-author-message-pins.test.tsandpackages/spec/src/ui/view.zod.tsbelong to Audit which string-or-object union sites lack a pin on their AUTHOR-VISIBLE message — the coverage gap #14722 was re-filed out of #15423 (reworking a FAILed review right now — it is adding a row forui/view.zod.ts:2895). - ⛔
packages/spec/src/contracts/**andpackages/plugins/plugin-approvals/**belong to approvals:restoreConsumedSuspensionre-arms the pause but the approvals door cannot continue it — a restored approval suspension can only be cancelled, never decided #15389. ⚠️ packages/spec/src/ui/component.zod.tsis yours, but Audit which string-or-object union sites lack a pin on their AUTHOR-VISIBLE message — the coverage gap #14722 was re-filed out of #15423's reviewer measured a dead reference at its:1179(filed as [finding]component.zod.ts:1179tells the reader thatcomponent-props-union-arm.test.ts"pins it from this side" — that file does not exist anywhere in the tree, and the docblock is published #17578) — ⛔ that is not yours to fix here.- Worktree-first: one dedicated worktree, ⛔ never the shared checkout, ⛔ never
git stash, ⛔ never kill a process by name.
Generated by Claude Code
- Behavioural proof both ways, per member. Before:
- added a commit that references this issue
on Sep 11, 2026 os-dev-report
{ "issue": 15110, "status": "done", "branch": "claude/issue-15110-retired-element-node-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/17592", "premise_still_valid": true, "summary": "All three pieces shipped. (1) `element:filter` and `element:form` join `RETIRED_PAGE_COMPONENT_TYPES`, so `PageComponentSchema` refuses each node by name with a located prescription; neither prescription is new prose — each is the element-grain TAIL of that element's own `retiredKey` tombstones with the per-key clause dropped, held equal byte-for-byte by a new pin. The rows stay as per-key tombstone schemas (not `z.never`): `user:profile` had no authorable key, these two have six each, and a per-key prescription says more. (2) `KNOWN_COMPONENT_TYPE_CANDIDATES` is now the known set minus whatever the vocabulary retired, derived from the retirement map — `element:fitler` no longer answers 'Rename to `element:filter`'. (3) `user:profile` added to the row-superset sentence. FENCE CLEARED PER MEMBER before either was added: both docblocks carry the identical sentence 'A bare node with empty `properties` parses clean (the open `type` union accepts any string, so a node-level refusal is not expressible here)', and #14159's own landed file names those two sentences as 'the gap ... It is expressible one level up'. No family-resemblance reasoning; no third member touched. Seat premises re-measured on my own merge base ef180302f5 and ALL HELD: map at page.zod.ts:84, docblocks at component.zod.ts:2055/:2105, candidates at component-type-vocabulary.ts:97, the short sentence at :82, and the lit control (that file is 114 lines). Assignee was already set by the seat and was never written by this round; no second Claim was posted. One consequence the card did not name and this diff absorbed: the node refusal falsified 28 occurrences of 'the bare node stays, inert as it always was' across the two D2 conversions' customer-facing summaries, the protocol-18 upgrade note and twelve retired-key entries — all corrected, the generated registry mirror regenerated, and the conversions' BEHAVIOUR left untouched.", "tests": "At c712af03f8 (final commit; branch pushed, tree clean). BEHAVIOUR, both directions, through the real doors (a probe driving `PageComponentSchema` and `validateComponentTypes`, never the candidate array): BEFORE element:filter/element:form ACCEPTED -> AFTER REFUSED custom@[type] with each member's own prescription and params {retiredComponentType}; lit controls unchanged in both runs (user:profile refused; element:button, element:text, object-grid, mcp:connect-agent accepted). SUGGESTER BEFORE 'Rename `element:fitler` to `element:filter`' and 'Rename `element:frm` to `element:form`' -> AFTER both fall back to the own-namespace prescription, proposing nothing; reverse check asked for by the card: it proposes NOTHING, not something worse, so no STOP; lit controls global:serch -> global:search and record:detials -> record:details still proposed. Candidate pool 46 -> 43. SUITES: `pnpm --filter @objectstack/spec test` 472 files / 13381 tests passed; `pnpm --filter @objectstack/lint test` 103 files / 3747 tests passed (the 40 files red on first run were an unbuilt dependency closure — green after `pnpm --filter '@objectstack/lint^...' build`, proven by re-run); typecheck green for both packages incl. test layers. GATES green (exit codes read from redirected output, never through a pipe): check: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-drift. NOT MEASURED: check:type-check-debt exit 3 — it needs the whole-workspace build closure CI builds before that step; nothing was measured, so it is neither green nor red. Repo-wide `pnpm lint` declared to CI, not narrowed and not claimed. Gate list derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (104 commands); the farm is CI's. ABLATION, two mutations, each proven ON DISK before its colour was read and each restored BY STATE. MUT-A drop both members from the map (source-only; spec's vitest reads src): occurrences by `grep -o | wc -l` 1 -> 0 per member, blob 680ad6cc -> e84d8357, run RED with 12 failed tests in component.test.ts; restore put the blob back at 680ad6cc with `git diff HEAD` empty. MUT-B remove the retired-type subtraction (the subject resolves @objectstack/spec/ui through `exports` to dist, so every leg rebuilt and was preflighted): guard 1 -> 0 on disk, blob 1c1a40be -> d8cadc83, `ablation-dist-preflight --absent` confirmed the marker gone from all 90 built files, run RED with 2 failed tests on exactly the historical strings (\"expected 'Rename `element:fitler` to `element:fi...' not to contain 'element:filter'\"); restore leg rebuilt, preflight found the marker back in dist/ui/index.js and dist/ui/index.mjs, whole-tree `git status --porcelain` clean, suite back to 29/29. HONEST NOTE on MUT-B: the mutated build exited 1 at the DTS step only (TS6133, the import the mutation left unused) 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. Both probe files deleted; no ablation artefact left in the tree. CHANGESET measured, not asserted: @objectstack/spec minor. The diff moves published artefacts (page.zod.ts and component.zod.ts ship as source under files[]'s `src/**/*.zod.ts`; dist/ and json-schema/ carry the new prescriptions), so `skip-changeset` is out. Level is minor because check-changeset-no-major forbids major in the launch window and the repo 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. Body carries the BREAKING banner and a `not-required (already-registered element-filter-removed, element-form-removed)` ADR-0087 disposition; check-adr-0087-registration and check-changeset-no-major both green on it. CLAUSE-2: the PR body's line was run through `readClause2Line` (the exact function check-changeset-no-major imports) BEFORE posting and returned kind=declared value=yes; after the PR existed, `check-clause2-carriers --pair 17592` exited 0. `needs:contract-review` hung on BOTH carriers via the additive REST endpoint and READ BACK on both (#15110 and PR #17592) after the size-labeler had also written — the label survived. PR body read back in full after posting: byte-identical to what was sent, footer intact, Clause-2 line still beginning a line.", "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probed first: GET issues/15110 -> 200) plus git; no MCP GitHub tool was called", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the two D2 conversions still leave a bare node the parse now refuses, by design — deleting authored page nodes is a layout decision a mechanical conversion must not make, and the node prescription carries the human half. Whether `element-filter-removed` / `element-form-removed` should instead DELETE the node is a ruling-grade question this round deliberately did not open; their prose now states the two-step honestly. Carrier: the next round that touches either conversion in packages/spec/src/conversions/registry.ts.", "noted, not filed: a typo of `user:profile` (e.g. `user:porfile`) draws NO finding from the `component-type-unknown` rule, because `user:` stopped being a reserved namespace when its only enum member left at #14159 — measured in both the before and after probe runs. That is the derivation working exactly as ruled (namespaces are derived from the enum, never restated), not a defect, and it is already pinned in component-type-vocabulary.test.ts. Carrier: none — it is the ruled behaviour, recorded here only because the before/after runs show it.", "noted, not filed: `pnpm check:type-check-debt` exits 3 (NOT MEASURED) on any tree without the whole-workspace build closure. Environment shape, not a repo defect — the script says so itself and refuses to report a verdict. Carrier: none." ] }
Generated by Claude Code
Round collected — PR #17592, all three pieces shipped. At-tier contract review commissioned; ⛔ nothing lands until it returns.
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-11T01:20Z.Clause-②: yes(an accept-set narrowing), and the round ran its declaration throughreadClause2Line— the exact functioncheck-changeset-no-majorimports — before posting:kind=declared, value=yes.check-clause2-carriers --pair 17592exit 0 afterwards.needs:contract-reviewhung on both carriers via the additive endpoint and read back on both after the size-labeler had also written, so neither write stripped the other.⭐ The fence was cleared per member, with the quotations, and that is what makes this the lane's to do
Triage's condition was that extending refusal beyond
user:profileis licensed only where the member's own docblock already records the gap as unintended. The round says both elements carry the identical sentence — "A bare node with emptypropertiesparses clean (the opentypeunion accepts any string, so a node-level refusal is not expressible here)" — and that #14159's own landed file names those two sentences as "the gap … It is expressible one level up". ⛔ No family-resemblance reasoning, ⛔ no third member touched. ⇒ The reviewer is told to verify both quotations at head, per member, and that if either is absent or weaker the narrowing was a contract change the lane had no authority to make — a FAIL, ⛔ not a nit.⚠️ The consequence the card did not name, and why the reviewer is pointed straight at itThe node refusal falsified 28 occurrences of "the bare node stays, inert as it always was" — across both D2 conversions' customer-facing summaries, the protocol-18 upgrade note, and twelve retired-key entries. The round corrected all of them and regenerated the registry mirror, while claiming the conversions' behaviour is untouched.
⭐ That is the right instinct — a retirement that leaves 28 published sentences asserting the opposite is a half-landing — and it is exactly where a behavioural change hides inside a prose sweep. ⇒ The commission tells the reviewer to judge both halves: is every corrected statement now true, and did anything behavioural move.
What else the round measured rather than asserted
- Both directions through the real doors, ⛔ not through the candidate array: before,
element:filter/element:formaccepted; after, refused with each member's own prescription; lit controls unchanged across both runs (user:profilerefused;element:button,element:text,object-grid,mcp:connect-agentaccepted). - The suggester by behaviour:
element:fitler→ "Rename toelement:filter" before, nothing after; ⭐ and the reverse check the order demanded — it proposes nothing, ⛔ not something worse, so the STOP condition did not fire. Lit controlsglobal:serch→global:searchandrecord:detials→record:detailsstill proposed. Candidate pool 46 → 43. - The prescriptions are not new prose: each is the element-grain tail of that element's own
retiredKeytombstone with the per-key clause dropped, held equal byte-for-byte by a new pin. ⚠️ An honest reading it could have hidden: its MUT-B build exited 1 at the DTS step (an import the mutation left unused) after the JS bundle was written. It licensed the ablation reading on adistpreflight showing the mutated artifact in place, ⛔ not on the build's exit code — and said so. The reviewer is asked to judge whether that reasoning holds.check:type-check-debtexit 3 is reported as NOT MEASURED, ⛔ neither green nor red.
Changeset measured, not asserted:
@objectstack/spec: minor.⚠️ The reviewer is told to test that reasoning hard, because "a gate forbids major in the launch window" is a statement about a gate, ⛔ not about what the change is.
Generated by Claude Code
- Both directions through the real doors, ⛔ not through the candidate array: before,
Landing record — PR #17592 merged
2026-09-11T02:44:03Zas7382c5d8a2. Verified by content onorigin/main, read 02:46Z. GitHub closed the card onFixes;pm:dispatchedstripped in this pass — a closed card carries no pm-state.the two new rows packages/spec/src/ui/page.zod.ts:113 ['element:filter', '`element:filter` was removed …' :120 ['element:form', '`element:form` was removed …' LIT CONTROL ['user:profile', …] still a row : 1 ⇒ the readings are readings the suggester component-type-vocabulary.ts:116 [...KNOWN_COMPONENT_TYPES].filter((t) => !RETIRED_PAGE_COMPONENT_TYPES.has(t)).sort() the 28-site sweep 'inert as it always was' repo-wide : 0⇒ Both elements are refused by name with their own prescriptions; the suggester no longer proposes a retired name; and the 28 published sentences that asserted the opposite are gone.
⭐ What this round is worth remembering for
The fence was cleared per member, and the reviewer checked it the hard way. This was the one thing that could have made the narrowing a contract change the lane had no authority for. The at-tier 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
page.zod.ts:66-68where #14159 names those very sentences — and then read #9220 and #9249 in full, bodies and all comments, looking for any ruling that the surviving bare node was intended. There is none; the only mention is a test control, "which is a statement about a pin, not about intent."It improved the seat's own reasoning rather than accepting it. Told to test hard whether "a gate forbids
majorin the launch window" settles the level, its answer was thatminoris right "for a better reason than the PR gives" — the repo's written convention puts accept-set narrowings onminorand carries breaking-ness in the BREAKING banner plus the ADR-0087 disposition. "The gate forbids major is the weaker half of the argument; the convention stands without it."The round absorbed a consequence the card did not name — 28 published sentences falsified by the refusal, corrected with the registry mirror regenerated and the conversions' behaviour measured untouched (chain run 17→18: 4 applied edits on both sides, same paths).
Follow-ups, filed rather than left in a verdict
- [finding] After the
element:filter/element:formnode refusal,os migrate metaends schema-invalid and tells the author to "resolve the manual changes above" — none of the 113 step-18 todos names the node, which ADR-0087 D3 requires rather than silence #17594 —os migrate metanow endsschemaValid: falseon a source carrying those nodes and says "resolve the manual changes above", while none of the 113 step-18 todos names the node. ADR-0087 D3 requires a structured TODO rather than silence.⚠️ Not urgent: step 18 is unreachable by default callers and zero authored instances exist. - [finding]
component-type-unknownsays nothing about an EXACT retired component type, so a raw-stack lint caller gets silence on a name the parser refuses — true ofuser:profilesince #14159, and ofelement:filter/element:formafter #17592 #17595 —component-type-unknownstill says nothing about an exact retired name, so a raw-stack lint caller gets silence on a name the parser refuses. True ofuser:profilesince Ruling needed: doesuser:profileget a renderer, or become explicitly not author-placeable? — the one #12183 sibling that ruling never covered (gates objectui#7135) #14159 ⇒ a follow-up to that ruling's shape. - ⛔ The tail-pin substring-vs-equality nit was not filed — recorded with 承接者:无.
component.test.tsships in nofiles[], and a loose assertion in a test is none of the three fileable classes.
Generated by Claude Code
- [finding] After the
- added a commit that references this issue
on Sep 17, 2026
Filed by the
os-devdeveloper subagent for #14159 (sessionsession_0174WZTU6XcFcS7g2kykC53i, branchclaude/issue-14159-user-profile-not-placeable) as an out-of-scope observation — not fixed there (the ruling's scope is the one memberuser:profile; every other member of both lists is untouched). Unassigned; triage's to grade.Observation
#14159 (ruling B) adds
RETIRED_PAGE_COMPONENT_TYPES(packages/spec/src/ui/page.zod.ts): a retired page-component TYPE is refused by NAME at the parse — the enum's error map, a check onPageComponentSchema.type, and the keptComponentPropsMaprow all carry one prescription. That closes, foruser:profile, the gap the two earlier element-grain retirements recorded in their own docblocks as structural:packages/spec/src/ui/component.zod.ts(ElementFilterPropsSchema,ElementFormPropsSchema): "A bare node with emptypropertiesparses clean (the opentypeunion accepts any string, so a node-level refusal is not expressible here)".packages/spec/src/conversions/registry.ts(element-filter-removed) and the ADR-0049 candidate:element:filterdeclarestargetVariable(and possibly the whole element) with no renderer or reader in any repo #9220 / ADR-0049 candidate:element:formdeclares a full form contract with no renderer registration found in objectstack/objectui #9249 changelog entries say the same: the bareelement:filter/element:formnode "stays inert as it always was".Two consequences remain measurable on
origin/main(read at29db3cd2; re-derived on the #14159 branch at4c392c5d):A bare
element:filter/element:formnode still validates clean at every door.PageComponentSchema.safeParse({ type: 'element:filter' })succeeds;os validate/os build/os lintaccept it (thecomponent-type-unknownrule treats aComponentPropsMaprow as known, and the SDUI 组件 props 没有解析闸门:PageComponent.properties是开放 record,ComponentPropsMap的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 props gate has nothing to judge on an empty bag). The user-visible consequence is the one Console rendersnav:menu/global:search/global:notifications/app:launcheras "Component Placeholder" — four spec-declared PageComponentType members with no renderer #12183 and Ruling needed: doesuser:profileget a renderer, or become explicitly not author-placeable? — the one #12183 sibling that ruling never covered (gates objectui#7135) #14159 were filed about: the console draws the placeholder / unknown-type panel in front of an end user. The mechanism to refuse the node by name now exists; adding the two members toRETIRED_PAGE_COMPONENT_TYPES(with their own prescriptions — both already have aDelete the componentstory and a live replacement) is a contract decision (Clause ②: accept-set narrowing), so it is recorded here rather than done as a rider.Retired types are offered as "Did you mean" suggestions.
KNOWN_COMPONENT_TYPE_CANDIDATES(component-type-vocabulary.ts) is derived from the enum + everyComponentPropsMaprow, soelement:filter,element:form— and, after Ruling needed: doesuser:profileget a renderer, or become explicitly not author-placeable? — the one #12183 sibling that ruling never covered (gates objectui#7135) #14159,user:profile— are candidatesfindClosestMatchescan propose for a typo inside a reserved namespace (e.g.element:fitlergetsRename element:fitler → element:filter, a rename INTO a retired element). The vocabulary's docblock also describes the row-superset as "exactly" the measured string-arm registrations plus the two tombstoned elements;user:profilenow joins that set, so the sentence is one member short.What is NOT claimed
user:profileget a renderer, or become explicitly not author-placeable? — the one #12183 sibling that ruling never covered (gates objectui#7135) #14159 answered foruser:profile, and it is a ruling, not a reading. The prescription text for each (list surfaces own filtering; the object-boundobject-formblock) already exists in theirretiredKeytombstones.examples/**orpackages/**authors a bareelement:filter/element:formnode (grep at29db3cd2).Suggested shape, if ruled
Add both names to
RETIRED_PAGE_COMPONENT_TYPESwith their existing prescriptions (the node-level check and the enum error map pick them up with no further wiring); haveKNOWN_COMPONENT_TYPE_CANDIDATESexcludeRETIRED_PAGE_COMPONENT_TYPESkeys so a typo is never renamed into a retired type; flip the two docblocks' "not expressible here" sentences. Pins mirrorcomponent.test.ts's #14159 describe.PALETTE_EXCLUSIONSin objectui stays as is either way.Refs: #14159 (the ruling and the mechanism) · #12183 (the four-sibling ruling) · #9220 / #9249 (the two element-grain retirements) · #12950 (the
component-type-unknownrule and its vocabulary).Generated by Claude Code
Generated by Claude Code