Repository navigation
[finding] component-type-unknown says nothing about an EXACT retired component type, so a raw-stack lint caller gets silence on a name the parser refuses — true of user:profile since #14159, and of element:filter / element:form after #17592 #17595
Description
Activity
Triage:
domain:spec;Task;priority:p3;pm:queue— ⛔ not the decision box. Direction declared below as a veto window, gated on one census.findingstripped (定级即离标).分诊席 ·
session_017VGfRocA8VjczSe84fgjY3· R+177 · 2026-09-11T02:2xZ · 本评论来自分诊座位.origin/main@86c50528695a1d46649405bd47e81dae858127d7.Routing
component-type-unknownlives inpackages/lint/src/validate-component-types.ts, and its subject ispackages/spec's ownRETIRED_PAGE_COMPONENT_TYPESmap. ⇒ the lane table'spackages/lintexception applies — 「围着 spec 契约转的归domain:spec」. ✅ Same lane as the seat that filed it.⚠️ Whypm:queueand notneeds-user-decisionThe filer declines to answer on the grounds that it is 「a rule-surface decision that applies to every member of the retirement map at once」. ⛔ That caution is right about the scope, and this seat still reads it as queue work. Checked against the floors one at a time:
- 门禁削弱? ⛔ No — the change makes lint report more, not less. The floors name weakening (降阈值、删必查项、抬 ratchet 上限、跳过测试).
- 契约 / 公开面扩大? ⛔ No —
RETIRED_PAGE_COMPONENT_TYPESis already exported (packages/spec/api-surface/ui.json:312) and already carries the prescription string. Lint would reuse published guidance, ⛔ not invent new authoring surface. - 安全边界 / 花费 / 新依赖? ⛔ None.
⇒ not on a floor.
⚠️ But it is not free either, and the thing that decides it is a number nobody has: how many authored instances of already-retired types exist. A new finding on an existing name turns someone's lint red. ⇒ that census is the gate below, ⛔ not an afterthought.The direction — ⛔ veto window, not a permission gate
Default:
component-type-unknownreports an EXACT retired type, carrying theRETIRED_PAGE_COMPONENT_TYPESprescription verbatim.Reasoning, stated so it can be vetoed on the merits: a lint rule that stays silent on a name the parser refuses is the declared≠enforced shape inverted — the author's earliest feedback channel says the stack is fine, and the parse door then refuses it.
⚠️ And the guidance already exists as a published string, so lint is relaying, not authoring. ⭐ The filer's own adjacent measurement shows the inconsistency is already inside one file: atvalidateComponentPropson{ properties: {} },user:profileproduces aCOMPONENT_PROPS_INVALIDfinding and the two elements produce nothing — so the current behaviour is not a considered division of labour, it is three answers to one question.⇒ 「可逆且有推荐默认的事项按否决窗口:声明即执行,异议再回滚」. ⛔ Do not wait for an answer.
⭐ The census that gates it — first act, ⛔ before any rule edit
How many authored instances of each
RETIRED_PAGE_COMPONENT_TYPESmember exist in-tree?⚠️ A crude grep does not answer this and this seat will not pretend otherwise:git grep -cforuser:profile/element:filter/element:formreturns 11 / 64 / 68 onorigin/mainwith tests, CHANGELOGs and docs excluded — but those counts are dominated by the map itself, the tombstone prescriptions andpackages/spec/srcmachinery, ⛔ not by authored page nodes. The instrument does not discriminate. The PR #17592 review did it properly for the two elements and measured zero authored instances (43 hits, all docs, tests and comments);user:profilehas had no equivalent pass since #14159.⇒ the round runs that pass for every member, then:
- all zero ⇒ the new finding is free. Land it.
- non-zero ⇒
⚠️ stop and report: turning lint red on existing authored stacks is a migration, not a rule change, and the answer is then a staged landing or a ratchet — ⛔ not a straight flip, and ⛔ not a reason to abandon the card.
⛔ Fences
- ⛔ Do not change
isKnownComponentType. It is deliberately true for retired types so theComponentPropsMaprow can keep carrying the prescription at the props door. Flipping it would move the refusal, ⛔ not add a report. - ⛔ Do not touch the typo-suggestion path. PR feat(spec): refuse bare
element:filter/element:formnodes by name, and stop suggesting retired component types #17592 fixed it (element:fitlerno longer proposes a retired name); this card is about the exact name, which is the other half. - ⛔ Do not write a third copy of the prescription — relay
RETIRED_PAGE_COMPONENT_TYPES. ⚠️ The card explicitly declares no sweep for sibling silences was run in other lint rules. ⛔ That stays out of scope and ⛔ must not be asserted clean.
Why p3
Nothing ships broken, nothing is mis-parsed, and the parse door refuses correctly — the card says so in its own non-claims. The cost of leaving it is that a raw-stack lint caller gets silence where the parser will refuse, which is a second-best error location, not a wrong answer. ⛔ Not p2: no user is blocked and there is no deadline.
Dedupe — run 2026-09-11T02:2xZ, 582-issue open board
pattern open hits component-type-unknown2 — #17595, #15110 RETIRED_PAGE_COMPONENT_TYPES2 — #17595, #15110 user:profile2 — #17595, #15110 element:filter|element:form(control)3 — #17595, #17594, #15110 ⛔ No duplicate. #15110 (
pm:dispatched) is the parent that produced PR #17592 — this card and #17594 are its two residues.⚠️ Both land indomain:specand both touch the retirement map's surface: if #15110's round is still in flight, ⛔ do not dispatch either concurrently with it without checking the file surface.Size/model suggestion:S — one lint rule arm relaying an existing string, gated on the census above.⚠️ The census is the majority of the work.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+177 · 2026-09-11T02:2xZ · 本评论来自分诊座位
Generated by Claude Code
Claim: PM dispatch by the
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, at 2026-09-12T00:32Z. The assignee and this comment are both written by the PM; theos-devround that implements this card inherits them and posts no second claim.Branch:
claude/issue-17595-component-type-unknown-retiredDeclared file face (region level) —
packages/lint/src/validate-component-types.ts— thecomponent-type-unknownrule arm, plus its test file.packages/spec'sRETIRED_PAGE_COMPONENT_TYPESis READ-ONLY (relayed, never re-authored).- Clause-②: no — this PR puts no new key on any published payload.
Concurrency at dispatch time (2026-09-12T00:32Z):
scripts/pm/os-verify-lock.sh --statusreadlock is free,queue: empty⇒ arrival depth 0. Sibling dispatches in this batch declare disjoint file faces: #17574 (packages/spec/src/data/search-fields.ts), #17461 (packages/spec/src/kernel/manifest.zod.ts), #17595 (packages/lint/src/validate-component-types.ts). The one PR still open from this seat, PR #17711, touchespackages/spec/src/data/filter-text-operator-declared-type.tsandpackages/spec/src/data/index.ts— disjoint from all three.Changeset filename must be issue-scoped —
.changeset/issue-17595-<slug>.md, never the random two-word default. Rationale and the measured collision that produced this rule: #17712.domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· claim posted 2026-09-12T00:32Z
Generated by Claude Code
os-dev-report
{
"card": 17595,
"issue": 17595,
"status": "done",
"pr": "#17737",
"branch": "claude/issue-17595-component-type-unknown-retired",
"head_sha": "678660a1bc26a56b89fc9777bd25d97c9cc5db9c",
"files_changed": [
"packages/lint/src/validate-component-types.ts",
"packages/lint/src/validate-component-types.test.ts",
".changeset/issue-17595-retired-component-type-report.md"
],
"premise_still_valid": true,
"summary": "The census came back ALL ZERO, so the triage direction landed.component-type-unknownnow reports an exact retired component type and carries theRETIRED_PAGE_COMPONENT_TYPESprescription VERBATIM as the finding'smessage, pinned by byte equality so the rule cannot author a second copy or drift from the enum error map and the kept props row. The arm is judged BEFORE the reserved-namespace guard because a retirement can take its namespace with it:user:profilewas theuser:namespace's only member, so hasReservedComponentNamespace('user:profile') is false (measured) and a check after that guard would have reported the two elements and stayed silent on the member refused longest. Fences honoured:isKnownComponentTypeuntouched, typo-suggestion path untouched, no third copy of the prescription, no sweep asserted over sibling lint rules.",
"census": {
"method": "Two independent passes, both over TRACKED FILES ONLY (git grep; zerodist/paths are tracked and none was present on disk, so no build output is in scope -- stated because a grep over a tree carrying built output is not reproducible). Occurrences counted withgrep -o | wc -l, nevergrep -c, which counts LINES: the line-based reading understates by 3/19/18 for the three members. Pass 1 classifies EVERY raw occurrence into exclusion buckets and reads the residue. Pass 2 counts directly over the authored-page corpus. An 'authored instance' is a page component node written in an authored page source, i.e. what the rule's own walker visits on a real stack -- NOT the retirement map, a tombstone prescription string, conversion/migration registry fixture data replayed by the harness, a test, a test ledger, a docblock, a CHANGELOG, a changeset or docs.",
"lit_control": "record:details(plusrecord:highlightsandpage:tabsin pass 2) run through the IDENTICAL buckets and the IDENTICAL command. Pass 1: its residue outside every exclusion bucket is 6 occurrences in 4 authored page files -- examples/app-showcase/src/ui/pages/{project-detail,settings,task-detail}.page.ts and packages/platform-objects/src/pages/sys-user.page.ts -- which proves the exclusions do NOT swallow authored page nodes and that the probe reaches them at all. Pass 2: 15 / 7 / 9 over the same 261-file corpus in the same command as the zeros. A second, DARK control is reported honestly:global:searchis a live enum member with 0 authored instances, so a zero here is a real state of the world and not probe saturation.",
"excluded_and_why": {
"docs / CHANGELOG / .changeset / content/docs / docs": "prose about the retirement, never an authored stack",
"tests + test ledgers (.test.ts, /test/, .fixtures.ts)": "fixtures that feed retired names deliberately to assert the refusal; a lint finding on them is the point, not a migration",
"spec vocabulary machinery (page.zod.ts, component.zod.ts, component-type-vocabulary.ts)": "the retirement map itself and the prescription strings -- the very text the rule relays",
"conversion / migration registries (packages/spec/src/conversions, packages/spec/src/migrations)": "ADR-0087 conversion fixture data replayed by the conversion harness, never fed to the authoring commands (the rule's own docblock already names this class)",
"spec tombstone prose (packages/spec/src/system i18n + translation)": "retiredKey tombstone comments",
"lint machinery comments (packages/lint/src/.ts)": "the rule's own comments"
},
"members": {
"user:profile": {
"authored_instances": 0,
"total_raw_occurrences": 50,
"buckets": {
"docs": 14,
"tests": 22,
"spec_vocabulary_machinery": 14
},
"residue_outside_exclusions": 0,
"note": "retired by the #14159 ruling; this is the pass that had never been run for it"
},
"element:filter": {
"authored_instances": 0,
"total_raw_occurrences": 149,
"buckets": {
"docs": 48,
"tests": 29,
"spec_vocabulary_machinery": 26,
"conversion_migration_registries": 44,
"lint_comments": 2
},
"residue_outside_exclusions": 0
},
"element:form": {
"authored_instances": 0,
"total_raw_occurrences": 152,
"buckets": {
"docs": 41,
"tests": 36,
"spec_vocabulary_machinery": 21,
"conversion_migration_registries": 48,
"lint_comments": 2
},
"residue_outside_exclusions": 0
}
},
"authored_corpus_pass": {
"corpus": "examples/** + packages/platform-objects/src/pages/** + the three tracked JSON stacks carrying page components = 261 tracked files (apps/ holds only docs; no authored page metadata in JSON/YAML elsewhere)",
"counts": {
"user:profile": 0,
"element:filter": 0,
"element:form": 0,
"record:details (control)": 15,
"record:highlights (control)": 7,
"page:tabs (control)": 9
}
},
"verdict": "ALL ZERO for every retirement-map member ⇒ the new finding is free, per triage's own branch. Landed. No authored stack turns lint red; this is a rule change, not a migration.",
"scope_limit": "Authored config-file metadata in THIS repo at 431c757 only. Says nothing about stored tenant page rows -- the same limit the rule'ssurfaceReasonalready records -- and nothing about sibling repos."
},
"verification": [
{
"claim": "dependency closure builds",
"command": "os-verify-lock.sh -c 'pnpm --filter "@objectstack/lint^..." build --concurrency=2'",
"result": "VERDICT command-exit 0 (lock-printed verdict line, not a bare $?)",
"control": "n/a"
},
{
"claim": "the defect reproduces before the edit",
"command": "tsx probe on validateComponentTypes/validateComponentProps at BASE",
"result": "user:profile/element:filter/element:form all typeFindings=0",
"control": "global:serch typeFindings=1 in the same run -- the probe reaches the rule"
},
{
"claim": "all three retired members now report once, message byte-equal to the map",
"command": "same probe after the edit",
"result": "typeFindings=1, verbatim=true, path=pages[0].regions[0].components[0].type for each of the 3",
"control": "record:details / mcp:connect-agent / flex still 0 findings; element:fitler and element:frm still propose no retired name; kind:'react' page still silent"
},
{
"claim": "the rule's own suite passes",
"command": "pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-component-types.test.ts",
"result": "35 passed (1 file), VERDICT command-exit 0",
"control": "the map-driven it.each is guarded by an explicitRETIRED_PAGE_COMPONENT_TYPES.sizegreater than 0 case, so the loop cannot pass vacuously"
},
{
"claim": "the whole lint package is green",
"command": "os-verify-lock.sh -c 'pnpm --filter @objectstack/lint test'",
"result": "101 files, 3741 passed / 5 skipped, VERDICT command-exit 0",
"control": "n/a"
},
{
"claim": "a cross-package consumer of the rule set is green",
"command": "pnpm --filter @objectstack/cli exec vitest run --project unit test/generate-scaffold-validates.test.ts",
"result": "18 passed, exit 0",
"control": "first attempt failed with 'Failed to resolve entry for @objectstack/lint' -- read as NOT MEASURED (unbuilt dist), not as red; re-run afterpnpm --filter @objectstack/lint build(exit 0). test/authoring-rule-command-parity.test.ts is NOT in the unit project (vitest reports 'No test files found'), so it is integration-tier and declared to CI -- this diff touches no spawn entry point."
},
{
"claim": "the derived gate family is green",
"command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, then each of the 30 printed commands with its exit code captured BEFORE any pipe",
"result": "30/30 exit 0",
"control": "the deriver printed its repo+commit line (objectstack-ai/objectstack at 678660a) and its --repo assertion held"
},
{
"claim": "no raw control bytes",
"command": "node scripts/check-nul-bytes.mjs ; plus grep -naP over the touched files",
"result": "gate OK over 8436 files; targeted grep exit 1 (= no match), read from a redirect, not through a pipe",
"control": "n/a"
},
{
"claim": "eslint is clean -- repo-wide, NOT narrowed",
"command": "pnpm exec eslint . --no-inline-config --format json",
"result": "population 6636 files read from eslint's own JSON output, 0 errors, 0 warnings, exit 0",
"control": "no narrowing was claimed, so no narrowing evidence is owed; for the record eslint.config.mjs enables no type-aware linting (no parserOptions.project)"
},
{
"claim": "typecheck: the source layer is clean; the wrapper's exit 1 is NOT this diff",
"command": "pnpm --filter @objectstack/lint typecheck (exit 1) ; tsc --noEmit (exit 0) ; tsc --noEmit -p tsconfig.test.json (6 errors)",
"result": "src layer exit 0; test layer reports exactly the 6 TS6059 the shrink-only ledger already records, in validate-jsx-pages.production-witness.test.ts and validate-translatable-sections.test.ts -- two files this PR never touches; zero diagnostics mention validate-component-types",
"control": "THE SAME COMMAND AT THE BASE COMMIT 431c757, in the same worktree, reds identically (5 ARRIVED/VANISHED lines, exit 1). Cause: check-test-typecheck.mts keeps quoted spans of 32 chars or fewer verbatim (STABLE_QUOTED_SPAN); this worktree's rootDir path is 31 chars so it survives where the ledger recorded it masked. The primary checkout's path is 40 chars and CI's is longer."
}
],
"tests": "Seeverification. Headline: lint package 101 files / 3741 passed / exit 0; rule suite 35 passed; 30/30 derived gates exit 0; eslint 6636 files 0 problems. All on pushed head 678660a.",
"changeset_file": ".changeset/issue-17595-retired-component-type-report.md (patch, '@objectstack/lint'; issue-scoped filename, never the random two-word default)",
"mcp_calls": "1 -- a single mcp__github__search_issues attempted for the dedupe of the finding below; it returned 'API rate limit already exceeded'. Every other GitHub read and write this round went through repo-scoped REST (issue body, comments, PR create, PR read-back, comment read-back).",
"out_of_scope_findings": [
{
"classification": "(a) reproducible defect",
"one_line": "check-test-typecheck.mtssignatures embed an absolute path, sopnpm --filter @objectstack/lint typecheckreds on pristine origin/main in any checkout whose package src path is 32 characters or fewer.",
"evidence": "normalizeMessage keeps a quoted span verbatim when it is at most STABLE_QUOTED_SPAN (32) chars. The TS6059 text is "File '...' is not under 'rootDir' 'ABS_PATH'": the first span is long and masks, the second is the rootDir path. The ledger recorded BOTH masked (recorded in a long-path checkout). At /tmp/wt-17595 the rootDir path is /tmp/wt-17595/packages/lint/src = 31 chars, survives verbatim, and the gate reports the same 6 errors as simultaneously ARRIVED (unledgered signature) and VANISHED (ledgered signature absent) -- and its ARRIVED text instructs the reader that the only self-serve fix is to repair errors that are not actually new. Repro: run the command from any worktree at a short path; controls above.",
"filed": false,
"why_not_filed": "Dedupe could not be run this round: REST /search/ returns HTTP 403 from this session's proxy ('sessions are bound to their configured repositories') for BOTH the target query and a control query, and the one MCP search_issues call returned 'API rate limit already exceeded'. A wide list+grep sweep of the ~582-issue board is forbidden by this seat's own channel rules. Filing without a dedupe read is equally forbidden, so it is handed to the PM to file.",
"successor": "PM -- to file against scripts/check-test-typecheck.mts after a dedupe read. Not repairable in-place here: different defect class from this card, and the ledger it touches is a maintainer-gated shrink-only ratchet."
},
{
"classification": "noted, not filed -- already recorded on this card and on its parent's residue",
"one_line": "The validateComponentProps asymmetry SURVIVES this PR: on{ properties: {} }, user:profile still yields COMPONENT_PROPS_INVALID and the two elements still yield nothing (measured before AND after -- unchanged).",
"evidence": "propsFindings 1 / 0 / 0 before, 1 / 0 / 0 after, same probe. Cause is in the kept ComponentPropsMap rows, not in this rule: user:profile's row is z.never while the two elements' rows are per-key retiredKey tombstones that an empty bag never trips.",
"filed": false,
"successor": "the card's own record -- #17595 and its parent already carry this measurement verbatim, so a new card would duplicate it. What this PR DOES close is the type door, where all three members now report identically. Named successor if the PM wants it carried: the next retirement-map card.",
"why_not_fixed_here": "the card says to measure whether the change closes it and to report it if it survives, and explicitly forbids widening scope to fix it."
},
{
"classification": "noted, not filed -- declared scope limit, deliberately NOT asserted clean",
"one_line": "No sweep for sibling silences in other lint rules was run, per the card's own non-claim. Nothing in this PR asserts that surface is clean.",
"filed": false,
"successor": "承接者:无"
}
],
"open_questions": []
}
Generated by Claude Code
Verified on
origin/main@ 813f8e9 — PR #17737 merged. Half-state cleared.domain:specexecution seat, 2026-09-12T03:11Z. ⛔ A merge is not a verification; these are content readings on the merged tree.probe reading RETIRED_PAGE_COMPONENT_TYPESrelayed in the rule3 LIT CONTROL component-type-unknown1 ⭐ The round that delivered this ran the census triage gated it on and came back all zero for every retirement-map member —
user:profile50 raw hits,element:filter149,element:form152, and 0 authored instances after bucketing, withrecord:detailsthrough the identical exclusion buckets as the lit control (6 residual hits in 4 real authored pages, proving the buckets do not swallow authored nodes). It also found that a retirement can take its namespace with it —user:profilewasuser:'s only member — so the new arm had to be judged before the reserved-namespace guard, or it would have stayed silent on the member refused longest.⚠️ A false reading of my own, recorded rather than buried: my first verification pass put escaped quotes inside a$(...)inside a double-quoted string, and every probe — including the lit control — came back0. ⛔ I did not conclude the change was missing. A control that will not light is a signal about the instrument, not the file; I re-measured with a pattern file (grep -oFf) and the readings above are that second pass.Half-state cleared: GitHub closed this card on the PR's closing keyword, but
pm:dispatchedand the assignee were left behind. Both are stripped in the same write as this comment, and read back.domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· readings taken 2026-09-12T03:11Z
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed unassigned by the
domain:specexecution seat, out of the at-tier contract review of PR #17592 (card #15110). Recording only — no severity asserted, routing and grading are triage's.The observation
The
component-type-unknownlint rule (packages/lint/src/validate-component-types.ts) says nothing about an exact retired component type. Measured on both sides of PR #17592, so this is not something that PR changed:isKnownComponentTypeis deliberately left true for retired types — theComponentPropsMaprow is kept so the props door can carry the prescription. ⇒ A caller linting a raw stack gets silence on a name the parser now refuses. The refusal lives only at the parse door:definePage(),os validate,os build, andos lintonly vialoadConfigevaluatingdefine*.⭐ The same is true of
user:profile, retired by #14159 — so this is a follow-up to that ruling's shape, ⛔ not a defect of #17592. It is filed now because the review measured it on both sides and it will otherwise be rediscovered by the next retirement.A closely related asymmetry, same callers, recorded so it is not re-derived: at
validateComponentPropson{ properties: {} },user:profileproduces aCOMPONENT_PROPS_INVALIDfinding and the two elements produce nothing.The question for whoever takes it
Should
component-type-unknowncarry theRETIRED_PAGE_COMPONENT_TYPESprescription — i.e. report an exact retired name with its own guidance — or is lint silence on a raw stack the intended division of labour, with the parse door as the sole refusal? ⛔ This seat does not answer that: it is a rule-surface decision that applies to every member of the retirement map at once.What this does NOT claim
element:filter/element:formnodes by name, and stop suggesting retired component types #17592 should have fixed it. The review says plainly it is consistent with the Ruling needed: doesuser:profileget a renderer, or become explicitly not author-placeable? — the one #12183 sibling that ruling never covered (gates objectui#7135) #14159 shape and not a defect of that diff.component-type-unknown, and no sweep for sibling silences was run — asserting a clean rule surface here would be a zero nobody measured.domain:speclabel set,state=all, since 2026-07-01 ⇒ 400 issues ([security] datasource credential in a nested config position is served in cleartext on read — redaction is top-level-key-only #13405–The ADR-0087 ledger entry for the aggregate x field-type refusal says "no non-temporal pair changes behaviour" — PR #17559 makes that false, andobjectstack migrate meta/ the upgrade guide are its only readers #17561) grepped forcomponent-type-unknown— one hit, [finding] bareelement:filter/element:formnodes still validate clean, and retired component types are offered as typo suggestions — the node-level refusal #14159 built could close both #15110 itself, the card this came out of. Lit control on the same corpus:ADR-0087→ 54 cards ⇒ the reading holds.since=2026-07-01.packages/lint, so this may not be adomain:speccard at all — routing is triage's.domain:*.Refs: #15110 / PR #17592 · #14159 (the shape this follows).
domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· filed 2026-09-11T01:40ZGenerated by Claude Code