You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 26, 2026. It is now read-only.
ui:icon reads the SDUI identity key name as its lucide icon name, so any icon node that authors name renders nothing at all #5631
But name is not this renderer's private prop — it is the SDUI identity key every authored node carries, alongside id. So a perfectly ordinary node like
resolves SaveIcon, misses, and returns null. Nothing renders. There is no error boundary, no placeholder, no visible failure — just an absent icon and one console.warn, which Vitest 4 discards for a passing test (silent: 'passed-only') and which nobody reads in a browser either.
How it was measured
The sweep renders every registry-reachable widget through the real SchemaRenderer path with a canary node that sets name: 'canary_node' — an ordinary authored identity, nothing exotic. ui:icon was one of 12 targets that rendered zero elements and therefore read as clean, because an empty scan reports no leaks. It only started rendering once the target was given schemaExtras: { name: 'check' } to override the identity key, and once it did it turned out to leak 14 attributes (it is in PR #5629's ledger under BARE_SPREAD_ON_SVG).
So the collision was invisible in two independent ways at once: silent at runtime, and clean-looking to a gate.
Why this is a contract question, not a one-line fix
The tempting fix — read schema.icon ?? schema.name in the consumer — is the tolerant-consumer shape #5574's family is being migrated away from, and it would make name mean two different things depending on whether the lookup happens to hit. The real question is which key names an icon on an icon node, answered once in the spec rather than per renderer:
icon is the icon key; name is identity, always. Consistent with every other renderer and with action:*, which already reads schema.icon. Breaks any authored content relying on name, which is why it needs a look at real metadata rather than a guess.
ui:icon keeps name, and the identity key is documented as unavailable on this one node type — cheap, but it is a carve-out in the authoring model that an author cannot discover before it silently renders nothing.
Either way the failure mode should stop being silent: an unresolvable icon should render a visible placeholder or be refused at publish time, not return null.
Related but distinct: #5622 and #5586 both concern retired lucide spellings reaching this same icons-record resolver. Those are about which names resolve; this is about which schema key is consulted in the first place. A fix for either does not close the other.
Labelled finding + domain:ui by the domain:ui execution seat (#5560). Filed by this lane's dev during round 8's leak-sweep widening. Not self-promoted — grading is triage's.
One connection worth having before this is graded, because it decides whether it batches with others or stands alone.
This is the third distinct defect on the same resolver, and they are three different bugs, not one. The dev's own framing is right and I am amplifying it rather than adding to it:
This card — ui:icon consults the SDUI identity key name as its lucide icon name. Wrong schema key.
A fix for any one of them leaves the other two intact. Batching them under "the icon thing" would produce a PR that touches three unrelated mechanisms; treating this as a duplicate of either would lose it.
Why it is worse than its size suggests, and why the dev refused the obvious patch. It fails silently — the renderer returns null, so there is no console warning, no error boundary, and nothing user-visible except a missing glyph. It also reads clean to a gate: a renderer that returns null spreads no attributes, which is exactly how it earned a phantom-clean reading in the first pass of the sweep this card came out of. So the defect actively evades the instrument that found it.
The tempting one-line fix is schema.icon ?? schema.name. The dev declined it, correctly: that is precisely the tolerant-consumer shape the element:/ui: family is migrating away from, and adding one more would be a contract decision made silently inside a bug fix. Which key ui:icon should consult, and what should happen to nodes already authoring name, is a real question with a compatibility surface — which is why this is filed rather than fixed.
Concentration-round first-touch (triage seat): finding → decision inbox (needs-user-decision) — which schema key names an icon on an icon node is an authoring-contract question on a published surface, and option 1 breaks any authored content relying on name: the manual floor, not a renderer patch.
Maintainer ruling recorded — A: icon is the icon key; name is identity, always — and the silent failure ends
Provenance: maintainer, 2026-08-22, live session with the triage seat (「同意所有」, item 15 = A).
State: needs-user-decision → pm:queue (ui seat). Scope: ui:icon reads schema.icon (⛔ no tolerant icon ?? name fallback — ruled out in the adjudication); a sweep of real authored metadata for name-as-icon reliance runs first and its reading goes in the PR body (hits get a conversion/warning path, not silent breakage); an unresolvable icon renders a visible placeholder instead of null regardless; the BARE_SPREAD_ON_SVG ledger row for this renderer is re-measured in the same change. Sequencing note for the #5632 burn-down: land this before (or exclude ui:icon's row from) the SVG batch. Related-not-same: #5622/#5586 lucide spellings, #5633's resolver consolidation follows this ruling's key semantics.
Claim: PM loop round 34
Session: session_019ZyKZejBWZoCSj1NP35wcp
Branch: claude/issue-5631-ui-icon-icon-key
Worktree: objectui-issue-5631
Domain: domain:ui
File surface: packages/components/src/renderers/basic/icon.tsx (+ its tests, + the BARE_SPREAD_ON_SVG ledger row for this renderer, + a changeset) (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus
Clause-②: yes — the authoring key ui:icon consults changes, so a node that authors name stops resolving. That is a change in accept/reject behaviour on an authoring surface, declared here rather than left as a silent gap.
Serial constraints cleared: packages/components/src/renderers/basic/icon.tsx — no in-flight claim touches it; zero claude/issue-* branches on the remote; last change d40d295 (2026-08-23, citation re-anchor only). ⚠️#5632 (the BARE_SPREAD_MINUS_NAME / SVG burn-down) is NOT dispatched this round precisely because of this card — see the fence note below.
Clause-② tier, stated openly
The contract-review tier is Fable, and Fable's quota is exhausted for this seat (maintainer, 2026-08-23: 「Fable 5 配额已耗尽,可以用 Opus 继续,并发还是保持 5」). This card therefore goes out at Opus under the recorded quota exemption, and needs:contract-review — the normal compensating label — does not exist in this repository (ensure-pm-labels.sh creates it for objectstack only). Recording the exemption explicitly is the compensation available here; ⛔ it is not a licence to widen the change beyond the ruling.
The ruling — carried verbatim, ⛔ not re-opened
Maintainer, 2026-08-22 (「同意所有」, item 15 = A), recorded in comment 5380754137:
A: icon is the icon key; name is identity, always — and the silent failure ends
Scope, as ruled:
ui:icon reads schema.icon. ⛔ No tolerant icon ?? name fallback — explicitly ruled out in the adjudication. That shape is the tolerant-consumer pattern this family is migrating away from; ⛔ do not reintroduce it as a "migration convenience".
A sweep of real authored metadata for name-as-icon reliance runs FIRST, and its reading goes in the PR body. Hits get a conversion/warning path, ⛔ not silent breakage. If the sweep finds a population you cannot migrate cleanly, stop and report — do not ship a break you cannot account for.
An unresolvable icon renders a visible placeholder instead of null, regardless of the key question. Today's return null is what made this defect invisible to both a human and a gate.
The BARE_SPREAD_ON_SVG ledger row for this renderer is re-measured in the same change (the renderer previously read as clean only because it rendered nothing).
Fence, from the ruling's own sequencing note: land this before#5632's SVG batch, or that batch must exclude ui:icon's row. #5632 is deliberately held out of this round for that reason.
Related but NOT this card — ⛔ do not fold in: #5622 / #5586 are retired lucide spellings on the same resolver (wrong name); #5935 is the eight-resolver consolidation (wrong resolver table). This card is the wrong schema key. A fix for any one leaves the others intact.
ACCEPT (partial) — PR #5959 lands ruling items 3 + 4. Item 1 is escalated back, not re-ruled.
Accepted by direct PR review: the dev died on a terminal API error (529) before delivering its report, so under the report-loss fallback the PR itself is the record — draft PR present, CI green (22/22: 19 success + 3 skipped), death confirmed, report undelivered. Review criteria undiminished.
⚠️This seat is NOT re-ruling the card. The measurement below changes what option A costs, and a published-contract change is the manual floor. Recording the fork rather than resolving it.
What landed
ruling item
state
3 — unresolvable icon renders a visible placeholder, regardless of the key question
✅ landed
4 — BARE_SPREAD_ON_SVG ledger row re-measured
✅ landed, unchanged (14 attributes, same bare spread onto the same SVG host)
2 — sweep of real authored metadata, reading in the PR body
✅ ran first, as the ruling requires
1 — ui:icon reads schema.icon
⛔ not landed — the sweep stopped it
Part of #5631, deliberately not a closing keyword, verified on GitHub: closed_by_pull_requests is 0. The card stays open. That is the #5701 shape handled correctly without being told.
The sweep is why item 1 stopped, and its reading is decisive
The ruling made "a population you cannot migrate cleanly" a stop-and-report. It fired:
98 authored icon nodes across the corpus; 100% name the glyph with name; zero already use icon. Not a legacy tail — the whole population. No code path synthesizes icon nodes either, so there is no producer to fix at the producer.
Honestly bounded: deployed tenant metadata is unmeasured, not zero, and the prior is high reliance given 98/98 in-repo and that name is what the docs and the type advertise. cloud was not checked out.
And the contract blocks item 1 independently of the corpus. Measured against the built zod mirror:
REJECT { type:'icon', icon:'check' } -> invalid_type at [name]: expected string, received undefined
ACCEPT { type:'icon', name:'check' }
IconSchema declares name: z.string()required. So under option A as written, an author writing the ruled shape is refused by the published contract while the renderer reads a key the contract does not declare — contract-first exactly backwards.
Item 1 therefore cannot land at the renderer alone. It needs, in one change: packages/types/src/layout.ts + its zod mirror (declare icon, demote name to identity), the 98-node corpus conversion, content/docs/components/basic/icon.mdx, and the renderer's registry inputs entry — ~29 files across four workspace entries including the protocol package.
A third silent failure was found and fixed: a node with no name at all reached toPascalCase(undefined), threw on undefined.split, and had the throw swallowed by SchemaErrorBoundary.
The reverse verification proves the workaround removal is load-bearing, not cosmetic: restoring return null turns the leak sweep red on exactly one target — ui:icon — because without the placeholder it cannot be swept on an ordinary authored node at all.
The registry inputs list still advertises name, and is deliberately left alone with a comment: renaming it while the resolver still reads name would be this same defect pointing the other way.
Maintainer ruling recorded — A′: full contract migration, properly chartered
Provenance: maintainer, 2026-08-24, live PM chat, verbatim: 「5631 A′,按一次正经的契约迁移立项。」 This supersedes the interim question left by the 2026-08-24 escalation; the original 2026-08-22 direction (A: icon is the icon key; name is identity, always) is confirmed at its true, measured price.
Ruled scope — one chartered migration, not a renderer patch:
Contract: packages/types/src/layout.ts + its zod mirror — declare icon as the icon key on ui:icon; name demotes to pure identity (optional, per the SDUI identity contract). The current mirror's name: z.string()required is part of what moves.
Corpus: convert all 98 measured name-as-icon nodes to icon; the conversion carries a warning path for anything the sweep cannot convert mechanically. Deployed tenant metadata is unmeasured — the migration ships a conversion/warning story for stored nodes (loud, never silent breakage), and the changeset names the break prominently (minor per AGENTS.md §版本号策略).
Docs + registry: content/docs/components/basic/icon.mdx and the registry inputs entry move in the same stroke — the deliberately-left registry advertisement of name retires here.
Clause-②: yes (authoring accept/reject behaviour changes). Sequencing: the #5632 SVG burn-down keeps excluding ui:icon's ledger row until this lands. Size: M/L — the dispatching seat prices it as the ~29-file, four-workspace change the escalation measured, not as the original one-file estimate.
Claim: PM loop round 37
Session: session_01CSoz9uGhaaSgiq3hshtN7L
Branch: claude/issue-5631-ui-icon-contract-migration
Worktree: objectui-issue-5631
Domain: domain:ui
File surface: packages/types/src/layout.ts + its zod mirror, packages/components/src/renderers/basic/icon.tsx + its registry inputs entry, the 98-node authored corpus, content/docs/components/basic/icon.mdx, + a changeset. The escalation measured this at ~29 files across four workspace entries including the protocol package — that is the expected size, not a breach (stop on anything outside it; explain in the report)
Container & model: L, mode:subagent, model: opus
Clause-②: yes — authoring accept/reject behaviour changes: icon becomes the icon key and name demotes to identity. Contract-review tier is Fable and Fable is exhausted for this seat; needs:contract-review does not exist in this repo. Goes out at Opus under the recorded quota exemption, declared here. ⛔ The exemption covers the tier, not the scope.
Serial constraints cleared: packages/components/src/renderers/basic/icon.tsx — released when PR #5959 merged (R34). ⚠️ Concurrent sibling #5853 writes packages/types/src/data-display.ts + zod/data-display.zod.ts + renderers/complex/data-table.tsx; yours is layout.ts + renderers/basic/. Disjoint by full path.⚠️PR #6003 (#4600) is in the merge queue and touches packages/types/src/index.ts + zod/index.zod.ts — if your migration edits either barrel, merge base before opening and expect a possible queue conflict (cheap: re-merge and re-enter, no re-review). ⛔ #5935 (the eight-resolver consolidation) is deliberately held out of this round — it is the wrong resolver table, yours is the wrong schema key; they would collide.
⛔ RULED 2026-08-24 — A′, and this is the second ruling on this card
Maintainer, verbatim: 「5631 A′,按一次正经的契约迁移立项。」 This confirms the original 2026-08-22 direction (A: icon is the icon key; name is identity, always) at its true, measured price — the escalation established that A cannot land at the renderer alone, and A′ is the properly chartered version. ⛔ Do not re-open the fork; ⛔ do not re-price it downward.
Ruled scope, four parts, one stroke:
Contract — packages/types/src/layout.ts + its zod mirror: declare icon as the icon key on ui:icon; name demotes to pure identity (optional, per the SDUI identity contract). ⚠️ The current mirror's name: z.string() required is part of what moves — that requirement is why the ruled shape is refused by the published contract today.
Corpus — convert all 98 measured name-as-icon nodes to icon. The conversion carries a warning path for anything the sweep cannot convert mechanically. Deployed tenant metadata is unmeasured: ship a conversion/warning story for stored nodes — loud, never silent breakage — and name the break prominently in the changeset (minor per AGENTS.md §版本号策略).
Docs + registry — content/docs/components/basic/icon.mdx and the registry inputs entry move in the same stroke. The registry's advertisement of name was deliberately left in place by fix(components): render a visible placeholder when a ui:icon glyph does not resolve #5959 with a comment saying renaming it while the resolver still read name would be the same defect pointing the other way — that comment retires here.
What is already true, so you do not re-measure it
From the #5959 sweep, recorded on this card: 98 authored icon nodes, 100% name the glyph with name, zero already use icon — the whole population, not a legacy tail. No code path synthesizes icon nodes, so there is no producer to fix at the producer. And the contract measurement:
REJECT { type:'icon', icon:'check' } -> invalid_type at [name]: expected string, received undefined
ACCEPT { type:'icon', name:'check' }
Re-verify both before building on them (the sweep is ~4 hours old and #5959 has since merged), but do not redo them from scratch.
Sequencing — load-bearing
⛔ #5632's BARE_SPREAD_ON_SVG burn-down keeps excluding ui:icon's ledger row until this lands. PR #5959 re-measured that row as unchanged (still 14 attributes). Do not touch #5632's batch, and do not "fix" that row here.
⛔ Not this card, do not fold in:#5622 / #5586 are retired lucide spellings on the same resolver (wrong name); #5935 is the eight-resolver consolidation (wrong resolver table); #5993 is ui:button's inlined copy of resolveIcon. This card is the wrong schema key. A fix for any one leaves the others intact.
⚠️ If the corpus conversion turns up nodes you cannot convert mechanically and the warning path does not cover them honestly — stop and report. Shipping a break you cannot account for is the one outcome the ruling's own conversion clause exists to prevent.
{
"issue": 5631,
"status": "done",
"branch": "claude/issue-5631-ui-icon-contract-migration",
"pr": "https://github.com/objectstack-ai/objectui/pull/6012",
"premise_still_valid": true,
"summary": "Landed A' as the chartered four-part migration in one stroke. Contract: IconSchema (packages/types/src/layout.ts + zod/layout.zod.ts) declares `icon: string` REQUIRED — exactly as `name` was required, a key rename at constant strictness — and `name` reverts to the optional identity inherited from BaseSchema; keeping requiredness constant is what leaves the zod-mirror-parity ledger untouched. Corpus: all 98 measured nodes converted by codemod across 22 files, plus one doc example. Renderer: icon.tsx reads schema.icon with NO tolerant fallback — schema.name is read at exactly one place, to make the migration diagnostic specific, and never as a glyph source. Docs + registry moved in the same change and #5959's deliberate `name` advertisement comment is retired. The break is loud in three places: the mirror refuses a legacy node with a message naming the rename and the converter (not zod's default `expected string, received undefined`), the renderer draws #5959's unchanged placeholder plus a warning carrying the exact rename and a new data-objectui-icon-legacy-name-key marker, and a new `migrateIconNodeKeys` export converts stored tenant metadata in bulk — a one-shot conversion, never wired into any read path, that reports rather than guesses on nodes carrying both keys or no glyph key. The ruling's stop-and-report clause did not fire: every node was mechanically convertible as a key rename. #5632's BARE_SPREAD_ON_SVG row is untouched and still 14 attributes; the new marker cannot perturb it because `data-` is an OPEN_PREFIXES entry in the leak judge, and the sweep confirms it green with no ledger edit.",
"tests": "All final runs on cc4d700a (branch head after merging origin/main, which brought in PR #6003; packages/types/src/index.ts auto-merged cleanly). ZONE-2 RE-VERIFICATION, with control probes: contract probe before the change reproduced the recorded reading exactly — REJECT {type:'icon',icon:'check'} -> invalid_type at [name]; ACCEPT {type:'icon',name:'check'} — and the control {type:'icon',icon:42} was ALSO refused at [name], proving `icon` was not a declared key at all. After the change: ACCEPT the ruled shape; REJECT the legacy one at [icon] with the migration message; control {type:'icon',icon:42} -> 'invalid_type at [icon]: expected string, received number', i.e. the migration message is correctly scoped to the absent case only. Corpus sweep re-run with a brace-matching scanner over all tracked ts/tsx/json/mdx/md: 98 nodes naming the glyph with `name`, 0 already using `icon` — 98/0 confirmed exactly. Of the 98, 95 resolve to a live lucide glyph; the 3 that do not are all icon:'check-circle' in examples/schema-catalog/src/schemas/marketing/call-to-action.json, a RETIRED LUCIDE SPELLING belonging to #5622/#5586 — they rendered nothing before and render the placeholder after, behaviour unchanged, deliberately not fixed here. Post-migration sweep: zero authored name-as-glyph nodes remain; every residual hit is prose or a deliberate legacy fixture. VITEST (repo root, path filters, per AGENTS.md — never `pnpm --filter PKG test`): packages/types/ + basic-renderers + icon-renderer-declared-default + icon-unresolvable-placeholder + widget-dom-leak-sweep + examples/schema-catalog -> exit 0, 'Test Files 71 passed (71) | Tests 2502 passed (2502)'. Earlier pre-merge full runs: `vitest run packages/components/` -> 183 files / 1681 tests, whose single failure was icon-renderer-declared-default.test.ts (it selects the registry input by key, so the rename left it selecting undefined) fixed in f4bd018; `vitest run examples/schema-catalog/ packages/react/... packages/layout/` -> 31 files / 1859 tests green. TYPE-CHECK/LINT (script names echoed in output, so no zero-match silent pass): types+components `run type-check` EXIT=0; types+components `run lint` EXIT=0 (0 errors, 908 pre-existing warnings in src/ui/** no-touch files); @object-ui/site `run lint` EXIT=0 (0 errors). REPO GATES, derived by hand from package.json + .github/workflows/ because this repo has no dispatch-gates.mjs, each quoting the gate's OWN verdict line rather than $? after a pipe: check-changeset-presence EXIT=0 ('9 source file(s) of 2 released package(s) changed... 1 changeset(s)'), check-changeset-no-major EXIT=0, check-control-bytes EXIT=0 ('scanned 4975 tracked text file(s)'), check-lucide-icon-record-names EXIT=0, check-doc-component-types EXIT=0, check-doc-snippet-types EXIT=0 ('Semantic phase: 148 of 148 block(s) judged, 0 failed' — it caught an uncompilable docs snippet first, fixed in 40d733e), check-doc-links EXIT=0, check-type-check-coverage EXIT=0 ('41/41 packages'), check-lint-coverage EXIT=0 ('46/46 packages'), check-package-self-import EXIT=0. REVERSE VERIFICATION, direction predicted before running (RED, because the new declaration makes `icon` required): pasting the retired spelling into packages/components against the REBUILT .d.ts gave EXIT=1 with 'error TS2741: Property icon is missing in type { type: \"icon\"; name: string; } but required in type IconSchema'; removing it gave EXIT=0. Run from a committed state under a `trap ... EXIT INT TERM` that restores the probe, and the tree was afterwards verified byte-identical to HEAD via `git diff HEAD --exit-code`. The out-of-scope measurement for #6009 used the same discipline — the corpus reverted in place under a restoring trap, the gate's own --report counter read before and after, then restored and re-verified byte-identical to HEAD. NOTE ON THAT TRAP: its first spelling (`git checkout -- FILES`) restored from the INDEX, which the preceding `git checkout BASE -- FILES` had also moved, so it silently restored the wrong content; caught by checking `git status` afterwards and repaired with `git checkout HEAD -- ...`, which is why the byte-identity proof is quoted rather than assumed. DECLARED NARROWING: repo-wide `turbo run lint` and the full `vitest run` were left to CI; local runs were filtered to the affected packages plus every suite the post-migration sweep proved could contain an icon node — after conversion, zero icon nodes exist outside packages/types, packages/components, examples/schema-catalog, apps/site and the docs, all of which were run.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #6009: `ui:icon` becomes a record-reading lucide resolver in this change, but its type is absent from check-lucide-icon-record-names.mjs's RECORD_READING_TYPES census, so 96 authored glyph names go unjudged — measured from the gate's own --report counter on the same tree before/after conversion (judged 30 -> 30, declined 254 -> 350, i.e. +96 = exactly the non-mdx converted nodes), and the gate stays GREEN either way. Adding the census entry turns it red on the three check-circle nodes, which would drag #5622's retired-spelling repair into this PR — hence filed unassigned with `finding` + `domain:ui`, no pm:queue."
]
}
PR #6012, head cc4d700a. Gates not yet read (run started 12:44Z; this seat reads by name ~11 min after that PR's own run started_at, never earlier). Enqueue follows the read.
This is the strongest card this seat has landed. Below is what I re-derived myself on the branch rather than taking from the report — the report was right on every leg I checked, and the checks are recorded so a later reader does not have to re-run them.
The contract change, verified
packages/types/src/layout.ts — IconSchema declares icon: string, required. The old name: string is gone from the interface, so name falls back to BaseSchema.name — and I confirmed that is name?: string, optional at packages/types/src/base.ts:86.
packages/types/src/zod/layout.zod.ts — the mirror declares iconrequired, and no longer declares name, which therefore inherits BaseSchemaCore's name: z.string().optional() (packages/types/src/zod/base.zod.ts:82).
So the "constant strictness" claim holds on both sides, independently checked: required→required on the glyph key, optional→optional on identity. That is precisely why the zod-mirror-parity ledger stays silent and needs no KnownDrift entry — and I want that reasoning on the record, because it is the non-obvious part. A migration that had made iconoptional to soften the break would have landed a new drift row into the very ledger #5927 is out burning down this round.
The three things I checked hardest
1. The custom error is a message, not an accept.error: (issue) => issue.input === undefined ? "…" : undefined — returning undefined for the non-absent case falls back to zod's own diagnosis. Your control probe proves it end-to-end: { icon: 42 } still gets expected string, received number, not a wrong story about a rename. ✅ And the note that it lives on the field rather than an object-level .check() — because zod 4 skips object-level checks once a field issue exists, so an object-level diagnostic for a missing key would never run — is the kind of measured mechanism note that stops the next person from "simplifying" it into silence.
2. There is genuinely no tolerant read.const requested = typeof schema.icon === 'string' ? schema.icon : '' is the only lookup input. legacyGlyphName is read once and gated behind !requested in all three of its uses (warn text, aria-label, the data marker) — it can never become a glyph source. ✅ And the legacy-shape pin deliberately uses name: 'check' — a real glyph — so a tolerant fallback would resolve it and turn the case green. A nonsense name could not tell a fallback from a miss. That is the right way to build that pin.
3. The residual sweep. I ran my own scan over the PR head for icon nodes still naming the glyph with name. Exactly one hit:content/docs/components/basic/icon.mdx:38. I read its context before judging it — it sits under the heading "name is identity, not the glyph", immediately followed by "That node is refused by IconSchema". ✅ It is the deliberate counter-example, not a leak. Everything else is prose or a legacy fixture, as reported.
I also confirmed data-is an OPEN_PREFIXES entry (packages/test-support/src/dom-leak-judge.ts:63), so the new data-objectui-icon-legacy-name-key marker is allowlisted by construction and structurally cannot move #5632's BARE_SPREAD_ON_SVG row. The fence held.
Ratified explicitly, because it is more than a rename
migrateIconNodeKeys is a NEW PUBLIC EXPORT of @object-ui/types — a surface widening on top of the ruled rename, and the one thing in this PR a reviewer could reasonably ask whether the charter covered. This seat ratifies it, on these grounds and no others:
The ruling chartered 「一次正经的契约迁移」 — a proper contract migration. Deployed tenant metadata is unmeasured and the prior on reliance is high; a migration that breaks stored documents and ships no converter is not the proper version of itself.
It is not a read path. Nothing calls it during rendering or parsing; it is zod-free, lives on the main entry beside the existing dashboard-filter-alias.ts precedent, and is run once by a deployer. The ruling's ⛔ on icon ?? name is about the read path and is untouched by it.
It is declared in the changeset, in the PR body, and in the docs page — a host cannot meet it by surprise.
It reports rather than guesses on both ambiguous cases: a node with both keys (icon wins; overwriting would replace a working glyph with an identity string) and a node with neither. ✅ Guessing on the both-keys case is the failure mode that would have made it dangerous, and it declines.
Credited
✅ The fixture triage is four dispositions, not one sweep — and the "given teeth" one is the valuable one. basic-renderers.test.tsx asserted only "some svg rendered", which the placeholder also satisfies: an assertion that survives this migration by being unable to fail. Finding and fixing that is worth more than the rename it accompanied. ✅ And icon-renderer-declared-default.test.ts was found by running, not by grepping — it selected the registry input by key, so the rename left it selecting undefined. A grep-driven migration ships that one broken.
✅ The icon: 42 control is the sharpest single reading in the report. Refused at [name] before the change means icon was not a declared key at all — the published contract required the broken shape and did not know the ruled one. That is the fact that made the escalation correct and the renderer-only fix impossible, and it is now measured rather than argued.
✅ The trap bug you caught on yourself and reported.git checkout -- FILES restoring from the index — which the preceding git checkout BASE -- FILES had also moved — silently restored the wrong content. You caught it with git status and repaired it with git checkout HEAD -- …, which is exactly why the byte-identity proof is quoted rather than assumed. ⚠️ This goes into the seat's standing notes: a restoring trap that reads the index is not a restoring trap. Reporting a near-miss in your own verification harness is worth more to this lane than a clean-looking run.
✅ The scope narrowing is bounded by the sweep, and says so. "Zero icon nodes exist outside these five roots after conversion, all of which were run" is a measured bound on what CI must catch, not a hope.
Correctly left alone. They are a retired lucide spelling (#5622 / #5586 — wrong name), not a wrong key; they rendered nothing before and render the visible placeholder after. Behaviour unchanged, and strictly more visible than before. ⛔ Fixing them here would have widened a chartered migration into an unrelated family, and the PR body says so in the right place.
Next
Gates read at ~12:57Z. If all 22 are green (every check, not the required subset), this goes ready + enqueued. Fixes #5631 closes the card on merge; #6009 stays open for triage.
Activity
os-sales commentedon Aug 21, 2026
Labelled
finding+domain:uiby thedomain:uiexecution seat (#5560). Filed by this lane's dev during round 8's leak-sweep widening. Not self-promoted — grading is triage's.One connection worth having before this is graded, because it decides whether it batches with others or stands alone.
This is the third distinct defect on the same resolver, and they are three different bugs, not one. The dev's own framing is right and I am amplifying it rather than adding to it:
ViewSwitcherrenders NO icon forchartandganttviews: both icon names were dropped from lucide'siconsrecord #5586 (landed this round, PR fix(plugin-view): givechartandganttviews an icon that resolves, and pin every name both icon maps supply #5626) — two icon maps named lucide spellings that were retired from the runtimeiconsrecord. Wrong name.icons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622 (already gradedbug/pm:queue) — four hand-copied resolvers read theiconsrecord while app-shell'sgetIconand layout'sresolveIconreadlucide/dynamic'siconNames, which does carry retired aliases. So the same name renders in one surface and renders nothing in another. Wrong resolver table.ui:iconconsults the SDUI identity keynameas its lucide icon name. Wrong schema key.A fix for any one of them leaves the other two intact. Batching them under "the icon thing" would produce a PR that touches three unrelated mechanisms; treating this as a duplicate of either would lose it.
Why it is worse than its size suggests, and why the dev refused the obvious patch. It fails silently — the renderer returns
null, so there is no console warning, no error boundary, and nothing user-visible except a missing glyph. It also reads clean to a gate: a renderer that returnsnullspreads no attributes, which is exactly how it earned a phantom-clean reading in the first pass of the sweep this card came out of. So the defect actively evades the instrument that found it.The tempting one-line fix is
schema.icon ?? schema.name. The dev declined it, correctly: that is precisely the tolerant-consumer shape theelement:/ui:family is migrating away from, and adding one more would be a contract decision made silently inside a bug fix. Which keyui:iconshould consult, and what should happen to nodes already authoringname, is a real question with a compatibility surface — which is why this is filed rather than fixed.Not adding
bugor a priority.Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat):
finding→ decision inbox (needs-user-decision) — which schema key names an icon on an icon node is an authoring-contract question on a published surface, and option 1 breaks any authored content relying onname: the manual floor, not a renderer patch.[facets-block]
ui:icon节点静默渲染空白——作者可达、双重不可见(运行时静默 + gate 读作 clean);修是必须的,分歧只在「哪个键是图标名」。name在整个 SDUI 词汇里是身份键,action:*已用icon键——A(icon 是图标键,name 恒为身份)是唯一不给词汇表留特例的答案;B 的 carve-out 是作者无法发现的暗礁。name必然带上(它是身份键)——现状等于给 AI 铺了一条静默失败路;schema.icon ?? schema.name的宽容读法是明令要退役的消费端容错形状,排除。一句话问题:
ui:icon的图标名改从icon键读(name回归纯身份键),还是把「此节点上身份键不可用」立为文档化特例?icon是图标键;name恒为身份name当图标名的存量授权内容断(需扫描实测存量;迁移走 conversion/告警)ui:icon保留name,文档化特例推荐 A(置信度中高),且无论 A/B,静默失败必须终结:解析不到的图标渲染可见占位符或在 publish 期响亮拒绝——这一半不待裁决即可随实现落地。本分析看不见什么:真实部署里依赖
name-as-icon 的存量元数据量(零读数,决定 A 的迁移成本);cloud 侧内容不可测。裁决格式:回「A」/「B」。裁后执行:A ⇒ 晋级pm:queue派 ui 席(实现 + 存量扫描 + 响亮 fallback +BARE_SPREAD_ON_SVG账本行同步);B ⇒ 文档化 + 响亮 fallback 卡。相邻不混:#5622/#5586(退役 lucide 拼写)是另一族。Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded — A:
iconis the icon key;nameis identity, always — and the silent failure endsProvenance: maintainer, 2026-08-22, live session with the triage seat (「同意所有」, item 15 = A).
State:
needs-user-decision→pm:queue(ui seat). Scope:ui:iconreadsschema.icon(⛔ no toleranticon ?? namefallback — ruled out in the adjudication); a sweep of real authored metadata forname-as-icon reliance runs first and its reading goes in the PR body (hits get a conversion/warning path, not silent breakage); an unresolvable icon renders a visible placeholder instead ofnullregardless; theBARE_SPREAD_ON_SVGledger row for this renderer is re-measured in the same change. Sequencing note for the #5632 burn-down: land this before (or excludeui:icon's row from) the SVG batch. Related-not-same: #5622/#5586 lucide spellings, #5633's resolver consolidation follows this ruling's key semantics.Generated by Claude Code
os-warren commentedon Aug 24, 2026
Claim: PM loop round 34⚠️ #5632 (the
Session:
session_019ZyKZejBWZoCSj1NP35wcpBranch:
claude/issue-5631-ui-icon-icon-keyWorktree:
objectui-issue-5631Domain:
domain:uiFile surface:
packages/components/src/renderers/basic/icon.tsx(+ its tests, + theBARE_SPREAD_ON_SVGledger row for this renderer, + a changeset) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusClause-②: yes — the authoring key
ui:iconconsults changes, so a node that authorsnamestops resolving. That is a change in accept/reject behaviour on an authoring surface, declared here rather than left as a silent gap.Serial constraints cleared:
packages/components/src/renderers/basic/icon.tsx— no in-flight claim touches it; zeroclaude/issue-*branches on the remote; last changed40d295(2026-08-23, citation re-anchor only).BARE_SPREAD_MINUS_NAME/ SVG burn-down) is NOT dispatched this round precisely because of this card — see the fence note below.Clause-② tier, stated openly
The contract-review tier is Fable, and Fable's quota is exhausted for this seat (maintainer, 2026-08-23: 「Fable 5 配额已耗尽,可以用 Opus 继续,并发还是保持 5」). This card therefore goes out at Opus under the recorded quota exemption, and
needs:contract-review— the normal compensating label — does not exist in this repository (ensure-pm-labels.shcreates it for objectstack only). Recording the exemption explicitly is the compensation available here; ⛔ it is not a licence to widen the change beyond the ruling.The ruling — carried verbatim, ⛔ not re-opened
Maintainer, 2026-08-22 (「同意所有」, item 15 = A), recorded in comment 5380754137:
Scope, as ruled:
ui:iconreadsschema.icon. ⛔ No toleranticon ?? namefallback — explicitly ruled out in the adjudication. That shape is the tolerant-consumer pattern this family is migrating away from; ⛔ do not reintroduce it as a "migration convenience".name-as-icon reliance runs FIRST, and its reading goes in the PR body. Hits get a conversion/warning path, ⛔ not silent breakage. If the sweep finds a population you cannot migrate cleanly, stop and report — do not ship a break you cannot account for.null, regardless of the key question. Today'sreturn nullis what made this defect invisible to both a human and a gate.BARE_SPREAD_ON_SVGledger row for this renderer is re-measured in the same change (the renderer previously read as clean only because it rendered nothing).Fence, from the ruling's own sequencing note: land this before #5632's SVG batch, or that batch must exclude
ui:icon's row. #5632 is deliberately held out of this round for that reason.Related but NOT this card — ⛔ do not fold in: #5622 / #5586 are retired lucide spellings on the same resolver (wrong name); #5935 is the eight-resolver consolidation (wrong resolver table). This card is the wrong schema key. A fix for any one leaves the others intact.
Generated by Claude Code
os-warren commentedon Aug 24, 2026
ACCEPT (partial) — PR #5959 lands ruling items 3 + 4. Item 1 is escalated back, not re-ruled.
Accepted by direct PR review: the dev died on a terminal API error (529) before delivering its report, so under the report-loss fallback the PR itself is the record — draft PR present, CI green (22/22: 19
success+ 3skipped), death confirmed, report undelivered. Review criteria undiminished.What landed
BARE_SPREAD_ON_SVGledger row re-measuredui:iconreadsschema.iconPart of #5631, deliberately not a closing keyword, verified on GitHub:closed_by_pull_requestsis 0. The card stays open. That is the #5701 shape handled correctly without being told.The sweep is why item 1 stopped, and its reading is decisive
The ruling made "a population you cannot migrate cleanly" a stop-and-report. It fired:
name; zero already useicon. Not a legacy tail — the whole population. No code path synthesizes icon nodes either, so there is no producer to fix at the producer.nameis what the docs and the type advertise.cloudwas not checked out.And the contract blocks item 1 independently of the corpus. Measured against the built zod mirror:
IconSchemadeclaresname: z.string()required. So under option A as written, an author writing the ruled shape is refused by the published contract while the renderer reads a key the contract does not declare — contract-first exactly backwards.Item 1 therefore cannot land at the renderer alone. It needs, in one change:
packages/types/src/layout.ts+ its zod mirror (declareicon, demotenameto identity), the 98-node corpus conversion,content/docs/components/basic/icon.mdx, and the renderer's registryinputsentry — ~29 files across four workspace entries including the protocol package.Three things in this PR worth keeping
icons-record lookup — becauseCircleHelpandHelpCircleare both absent from lucide's runtime record while still resolving as named exports (the Two more retired lucide spellings reach theicons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622 mechanism). Looked up the usual way, the placeholder would itself have silently rendered nothing: the original bug one level up. A named import fails the build instead. Pinned.nameat all reachedtoPascalCase(undefined), threw onundefined.split, and had the throw swallowed bySchemaErrorBoundary.return nullturns the leak sweep red on exactly one target —ui:icon— because without the placeholder it cannot be swept on an ordinary authored node at all.inputslist still advertisesname, and is deliberately left alone with a comment: renaming it while the resolver still readsnamewould be this same defect pointing the other way.升级:选项 A 的成本与当初裁定时不同,请重新定价
一句话问题: 2026-08-22 裁定的 A 案(
icon是图标键、name恒为身份)在渲染器一层无法落地——已发布的契约本身拒绝这个写法,而全仓 98 个图标节点 100% 依赖name。要按原方向做一次跨包契约迁移,还是改走别的路?[facets-block]
name: 'save_icon'静默渲染空白),但最痛的一半已经修好了——占位符落地后失败不再隐形。剩下的只是「哪个键命名图标」,今天没有用户因为它拿不到图标。action:*已读icon)。但实测显示它不是渲染器补丁,而是协议包 + 98 节点语料 + 文档 + 注册表的一次性迁移;当初按「一个渲染器 + 一次扫描」定价,现在是 ~29 个文件、跨四个工作区条目。name会看到占位符而不是空白,错误已经可见——这条轴的紧迫性被 PR fix(components): render a visible placeholder when aui:iconglyph does not resolve #5959 大幅降低了。但契约与渲染器长期各说各话(类型要求name,裁决要渲染器读icon)本身就是新的分歧面。icon、name降为身份、98 节点转换、文档与注册表同步ui:icon以name为图标键,写进文档与类型描述作为明示特例推荐 B(置信度中),A′ 次之。 理由:A 的决定性优势原本是「防 AI 静默犯错」,而 #5959 已经把静默失败终结了——这条轴不再压倒性支持 A;剩下的收益是词表整洁,代价却涨到一次跨包契约迁移加全量语料转换。B 用一行文档把特例变成可发现的,成本近零。⛔ 但这是已发布契约的方向问题,属人工地板,本席不代裁。
本分析看不见什么: 已部署租户里
ui:icon的真实写法(未测量,且先验是高度依赖name);cloud仓未检出未扫描;以及维护者当初裁 A 时是否已知道类型要求name——若已知,B 可能早被排除过。裁决格式: 回「A′」/「B」/「C」即可。
Generated by Claude Code
os-zhuang commentedon Aug 24, 2026
Maintainer ruling recorded — A′: full contract migration, properly chartered
Provenance: maintainer, 2026-08-24, live PM chat, verbatim: 「5631 A′,按一次正经的契约迁移立项。」 This supersedes the interim question left by the 2026-08-24 escalation; the original 2026-08-22 direction (A:
iconis the icon key;nameis identity, always) is confirmed at its true, measured price.Ruled scope — one chartered migration, not a renderer patch:
packages/types/src/layout.ts+ its zod mirror — declareiconas the icon key onui:icon;namedemotes to pure identity (optional, per the SDUI identity contract). The current mirror'sname: z.string()required is part of what moves.name-as-icon nodes toicon; the conversion carries a warning path for anything the sweep cannot convert mechanically. Deployed tenant metadata is unmeasured — the migration ships a conversion/warning story for stored nodes (loud, never silent breakage), and the changeset names the break prominently (minorper AGENTS.md §版本号策略).icon.tsxreadsschema.icon; ⛔ no toleranticon ?? namefallback (stands as ruled 2026-08-22). The landed placeholder behaviour (PR fix(components): render a visible placeholder when aui:iconglyph does not resolve #5959) is unchanged.content/docs/components/basic/icon.mdxand the registryinputsentry move in the same stroke — the deliberately-left registry advertisement ofnameretires here.Clause-②: yes (authoring accept/reject behaviour changes). Sequencing: the #5632 SVG burn-down keeps excluding
ui:icon's ledger row until this lands. Size: M/L — the dispatching seat prices it as the ~29-file, four-workspace change the escalation measured, not as the original one-file estimate.State:
needs-user-decision→pm:queue(ui lane).Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
Claim: PM loop round 37⚠️ Concurrent sibling #5853 writes ⚠️ PR #6003 (#4600) is in the merge queue and touches
Session:
session_01CSoz9uGhaaSgiq3hshtN7LBranch:
claude/issue-5631-ui-icon-contract-migrationWorktree:
objectui-issue-5631Domain:
domain:uiFile surface:
packages/types/src/layout.ts+ its zod mirror,packages/components/src/renderers/basic/icon.tsx+ its registryinputsentry, the 98-node authored corpus,content/docs/components/basic/icon.mdx, + a changeset. The escalation measured this at ~29 files across four workspace entries including the protocol package — that is the expected size, not a breach (stop on anything outside it; explain in the report)Container & model:
L,mode:subagent,model: opusClause-②: yes — authoring accept/reject behaviour changes:
iconbecomes the icon key andnamedemotes to identity. Contract-review tier is Fable and Fable is exhausted for this seat;needs:contract-reviewdoes not exist in this repo. Goes out at Opus under the recorded quota exemption, declared here. ⛔ The exemption covers the tier, not the scope.Serial constraints cleared:
packages/components/src/renderers/basic/icon.tsx— released when PR #5959 merged (R34).packages/types/src/data-display.ts+zod/data-display.zod.ts+renderers/complex/data-table.tsx; yours islayout.ts+renderers/basic/. Disjoint by full path.packages/types/src/index.ts+zod/index.zod.ts— if your migration edits either barrel, merge base before opening and expect a possible queue conflict (cheap: re-merge and re-enter, no re-review). ⛔ #5935 (the eight-resolver consolidation) is deliberately held out of this round — it is the wrong resolver table, yours is the wrong schema key; they would collide.⛔ RULED 2026-08-24 — A′, and this is the second ruling on this card
Maintainer, verbatim: 「5631 A′,按一次正经的契约迁移立项。」 This confirms the original 2026-08-22 direction (A:
iconis the icon key;nameis identity, always) at its true, measured price — the escalation established that A cannot land at the renderer alone, and A′ is the properly chartered version. ⛔ Do not re-open the fork; ⛔ do not re-price it downward.Ruled scope, four parts, one stroke:
packages/types/src/layout.ts+ its zod mirror: declareiconas the icon key onui:icon;namedemotes to pure identity (optional, per the SDUI identity contract).name: z.string()required is part of what moves — that requirement is why the ruled shape is refused by the published contract today.name-as-icon nodes toicon. The conversion carries a warning path for anything the sweep cannot convert mechanically. Deployed tenant metadata is unmeasured: ship a conversion/warning story for stored nodes — loud, never silent breakage — and name the break prominently in the changeset (minorper AGENTS.md §版本号策略).icon.tsxreadsschema.icon. ⛔ No toleranticon ?? namefallback — stands as ruled 2026-08-22. The landed placeholder behaviour from PR fix(components): render a visible placeholder when aui:iconglyph does not resolve #5959 is unchanged; do not revisit it.content/docs/components/basic/icon.mdxand the registryinputsentry move in the same stroke. The registry's advertisement ofnamewas deliberately left in place by fix(components): render a visible placeholder when aui:iconglyph does not resolve #5959 with a comment saying renaming it while the resolver still readnamewould be the same defect pointing the other way — that comment retires here.What is already true, so you do not re-measure it
From the #5959 sweep, recorded on this card: 98 authored icon nodes, 100% name the glyph with
name, zero already useicon— the whole population, not a legacy tail. No code path synthesizes icon nodes, so there is no producer to fix at the producer. And the contract measurement:Re-verify both before building on them (the sweep is ~4 hours old and #5959 has since merged), but do not redo them from scratch.
Sequencing — load-bearing
⛔ #5632's
BARE_SPREAD_ON_SVGburn-down keeps excludingui:icon's ledger row until this lands. PR #5959 re-measured that row as unchanged (still 14 attributes). Do not touch #5632's batch, and do not "fix" that row here.⛔ Not this card, do not fold in: #5622 / #5586 are retired lucide spellings on the same resolver (wrong name); #5935 is the eight-resolver consolidation (wrong resolver table); #5993 is
ui:button's inlined copy ofresolveIcon. This card is the wrong schema key. A fix for any one leaves the others intact.Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
os-dev-report
{ "issue": 5631, "status": "done", "branch": "claude/issue-5631-ui-icon-contract-migration", "pr": "https://github.com/objectstack-ai/objectui/pull/6012", "premise_still_valid": true, "summary": "Landed A' as the chartered four-part migration in one stroke. Contract: IconSchema (packages/types/src/layout.ts + zod/layout.zod.ts) declares `icon: string` REQUIRED — exactly as `name` was required, a key rename at constant strictness — and `name` reverts to the optional identity inherited from BaseSchema; keeping requiredness constant is what leaves the zod-mirror-parity ledger untouched. Corpus: all 98 measured nodes converted by codemod across 22 files, plus one doc example. Renderer: icon.tsx reads schema.icon with NO tolerant fallback — schema.name is read at exactly one place, to make the migration diagnostic specific, and never as a glyph source. Docs + registry moved in the same change and #5959's deliberate `name` advertisement comment is retired. The break is loud in three places: the mirror refuses a legacy node with a message naming the rename and the converter (not zod's default `expected string, received undefined`), the renderer draws #5959's unchanged placeholder plus a warning carrying the exact rename and a new data-objectui-icon-legacy-name-key marker, and a new `migrateIconNodeKeys` export converts stored tenant metadata in bulk — a one-shot conversion, never wired into any read path, that reports rather than guesses on nodes carrying both keys or no glyph key. The ruling's stop-and-report clause did not fire: every node was mechanically convertible as a key rename. #5632's BARE_SPREAD_ON_SVG row is untouched and still 14 attributes; the new marker cannot perturb it because `data-` is an OPEN_PREFIXES entry in the leak judge, and the sweep confirms it green with no ledger edit.", "tests": "All final runs on cc4d700a (branch head after merging origin/main, which brought in PR #6003; packages/types/src/index.ts auto-merged cleanly). ZONE-2 RE-VERIFICATION, with control probes: contract probe before the change reproduced the recorded reading exactly — REJECT {type:'icon',icon:'check'} -> invalid_type at [name]; ACCEPT {type:'icon',name:'check'} — and the control {type:'icon',icon:42} was ALSO refused at [name], proving `icon` was not a declared key at all. After the change: ACCEPT the ruled shape; REJECT the legacy one at [icon] with the migration message; control {type:'icon',icon:42} -> 'invalid_type at [icon]: expected string, received number', i.e. the migration message is correctly scoped to the absent case only. Corpus sweep re-run with a brace-matching scanner over all tracked ts/tsx/json/mdx/md: 98 nodes naming the glyph with `name`, 0 already using `icon` — 98/0 confirmed exactly. Of the 98, 95 resolve to a live lucide glyph; the 3 that do not are all icon:'check-circle' in examples/schema-catalog/src/schemas/marketing/call-to-action.json, a RETIRED LUCIDE SPELLING belonging to #5622/#5586 — they rendered nothing before and render the placeholder after, behaviour unchanged, deliberately not fixed here. Post-migration sweep: zero authored name-as-glyph nodes remain; every residual hit is prose or a deliberate legacy fixture. VITEST (repo root, path filters, per AGENTS.md — never `pnpm --filter PKG test`): packages/types/ + basic-renderers + icon-renderer-declared-default + icon-unresolvable-placeholder + widget-dom-leak-sweep + examples/schema-catalog -> exit 0, 'Test Files 71 passed (71) | Tests 2502 passed (2502)'. Earlier pre-merge full runs: `vitest run packages/components/` -> 183 files / 1681 tests, whose single failure was icon-renderer-declared-default.test.ts (it selects the registry input by key, so the rename left it selecting undefined) fixed in f4bd018; `vitest run examples/schema-catalog/ packages/react/... packages/layout/` -> 31 files / 1859 tests green. TYPE-CHECK/LINT (script names echoed in output, so no zero-match silent pass): types+components `run type-check` EXIT=0; types+components `run lint` EXIT=0 (0 errors, 908 pre-existing warnings in src/ui/** no-touch files); @object-ui/site `run lint` EXIT=0 (0 errors). REPO GATES, derived by hand from package.json + .github/workflows/ because this repo has no dispatch-gates.mjs, each quoting the gate's OWN verdict line rather than $? after a pipe: check-changeset-presence EXIT=0 ('9 source file(s) of 2 released package(s) changed... 1 changeset(s)'), check-changeset-no-major EXIT=0, check-control-bytes EXIT=0 ('scanned 4975 tracked text file(s)'), check-lucide-icon-record-names EXIT=0, check-doc-component-types EXIT=0, check-doc-snippet-types EXIT=0 ('Semantic phase: 148 of 148 block(s) judged, 0 failed' — it caught an uncompilable docs snippet first, fixed in 40d733e), check-doc-links EXIT=0, check-type-check-coverage EXIT=0 ('41/41 packages'), check-lint-coverage EXIT=0 ('46/46 packages'), check-package-self-import EXIT=0. REVERSE VERIFICATION, direction predicted before running (RED, because the new declaration makes `icon` required): pasting the retired spelling into packages/components against the REBUILT .d.ts gave EXIT=1 with 'error TS2741: Property icon is missing in type { type: \"icon\"; name: string; } but required in type IconSchema'; removing it gave EXIT=0. Run from a committed state under a `trap ... EXIT INT TERM` that restores the probe, and the tree was afterwards verified byte-identical to HEAD via `git diff HEAD --exit-code`. The out-of-scope measurement for #6009 used the same discipline — the corpus reverted in place under a restoring trap, the gate's own --report counter read before and after, then restored and re-verified byte-identical to HEAD. NOTE ON THAT TRAP: its first spelling (`git checkout -- FILES`) restored from the INDEX, which the preceding `git checkout BASE -- FILES` had also moved, so it silently restored the wrong content; caught by checking `git status` afterwards and repaired with `git checkout HEAD -- ...`, which is why the byte-identity proof is quoted rather than assumed. DECLARED NARROWING: repo-wide `turbo run lint` and the full `vitest run` were left to CI; local runs were filtered to the affected packages plus every suite the post-migration sweep proved could contain an icon node — after conversion, zero icon nodes exist outside packages/types, packages/components, examples/schema-catalog, apps/site and the docs, all of which were run.", "open_questions": [], "out_of_scope_findings": [ "filed as #6009: `ui:icon` becomes a record-reading lucide resolver in this change, but its type is absent from check-lucide-icon-record-names.mjs's RECORD_READING_TYPES census, so 96 authored glyph names go unjudged — measured from the gate's own --report counter on the same tree before/after conversion (judged 30 -> 30, declined 254 -> 350, i.e. +96 = exactly the non-mdx converted nodes), and the gate stays GREEN either way. Adding the census entry turns it red on the three check-circle nodes, which would drag #5622's retired-spelling repair into this PR — hence filed unassigned with `finding` + `domain:ui`, no pm:queue." ] }Generated by Claude Code
yinlianghui commentedon Aug 24, 2026
PM review — ACCEPT
PR #6012, head
cc4d700a. Gates not yet read (run started 12:44Z; this seat reads by name ~11 min after that PR's own runstarted_at, never earlier). Enqueue follows the read.This is the strongest card this seat has landed. Below is what I re-derived myself on the branch rather than taking from the report — the report was right on every leg I checked, and the checks are recorded so a later reader does not have to re-run them.
The contract change, verified
packages/types/src/layout.ts—IconSchemadeclaresicon: string, required. The oldname: stringis gone from the interface, sonamefalls back toBaseSchema.name— and I confirmed that isname?: string, optional atpackages/types/src/base.ts:86.packages/types/src/zod/layout.zod.ts— the mirror declaresiconrequired, and no longer declaresname, which therefore inheritsBaseSchemaCore'sname: z.string().optional()(packages/types/src/zod/base.zod.ts:82).So the "constant strictness" claim holds on both sides, independently checked: required→required on the glyph key, optional→optional on identity. That is precisely why the
zod-mirror-parityledger stays silent and needs noKnownDriftentry — and I want that reasoning on the record, because it is the non-obvious part. A migration that had madeiconoptional to soften the break would have landed a new drift row into the very ledger #5927 is out burning down this round.The three things I checked hardest
1. The custom error is a message, not an accept.
error: (issue) => issue.input === undefined ? "…" : undefined— returningundefinedfor the non-absent case falls back to zod's own diagnosis. Your control probe proves it end-to-end:{ icon: 42 }still getsexpected string, received number, not a wrong story about a rename. ✅ And the note that it lives on the field rather than an object-level.check()— because zod 4 skips object-level checks once a field issue exists, so an object-level diagnostic for a missing key would never run — is the kind of measured mechanism note that stops the next person from "simplifying" it into silence.2. There is genuinely no tolerant read.
const requested = typeof schema.icon === 'string' ? schema.icon : ''is the only lookup input.legacyGlyphNameis read once and gated behind!requestedin all three of its uses (warn text,aria-label, the data marker) — it can never become a glyph source. ✅ And the legacy-shape pin deliberately usesname: 'check'— a real glyph — so a tolerant fallback would resolve it and turn the case green. A nonsense name could not tell a fallback from a miss. That is the right way to build that pin.3. The residual sweep. I ran my own scan over the PR head for icon nodes still naming the glyph with
name. Exactly one hit:content/docs/components/basic/icon.mdx:38. I read its context before judging it — it sits under the heading "nameis identity, not the glyph", immediately followed by "That node is refused byIconSchema". ✅ It is the deliberate counter-example, not a leak. Everything else is prose or a legacy fixture, as reported.I also confirmed
data-is anOPEN_PREFIXESentry (packages/test-support/src/dom-leak-judge.ts:63), so the newdata-objectui-icon-legacy-name-keymarker is allowlisted by construction and structurally cannot move #5632'sBARE_SPREAD_ON_SVGrow. The fence held.Ratified explicitly, because it is more than a rename
migrateIconNodeKeysis a NEW PUBLIC EXPORT of@object-ui/types— a surface widening on top of the ruled rename, and the one thing in this PR a reviewer could reasonably ask whether the charter covered. This seat ratifies it, on these grounds and no others:dashboard-filter-alias.tsprecedent, and is run once by a deployer. The ruling's ⛔ onicon ?? nameis about the read path and is untouched by it.iconwins; overwriting would replace a working glyph with an identity string) and a node with neither. ✅ Guessing on the both-keys case is the failure mode that would have made it dangerous, and it declines.Credited
basic-renderers.test.tsxasserted only "somesvgrendered", which the placeholder also satisfies: an assertion that survives this migration by being unable to fail. Finding and fixing that is worth more than the rename it accompanied. ✅ Andicon-renderer-declared-default.test.tswas found by running, not by grepping — it selected the registry input by key, so the rename left it selectingundefined. A grep-driven migration ships that one broken.icon: 42control is the sharpest single reading in the report. Refused at[name]before the change meansiconwas not a declared key at all — the published contract required the broken shape and did not know the ruled one. That is the fact that made the escalation correct and the renderer-only fix impossible, and it is now measured rather than argued.git checkout -- FILESrestoring from the index — which the precedinggit checkout BASE -- FILEShad also moved — silently restored the wrong content. You caught it withgit statusand repaired it withgit checkout HEAD -- …, which is exactly why the byte-identity proof is quoted rather than assumed.ui:iconbecomes a record-reading resolver in #5631, but its type is absent from the lucide gate's census — 96 authored glyph names go unjudged, three of them already retired spellings #6009 filed, not folded. The+96 declined, judged unmovedreading — taken from the gate's own--reportcounter, before and after, on the same tree under a restoring trap — is a real measurement of a real blind spot, and the reason for not fixing it here is correct: the census entry would turn the gate red on the threecheck-circlenodes and drag Two more retired lucide spellings reach theicons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622's retired-spelling repair into this PR.The three
check-circlenodesCorrectly left alone. They are a retired lucide spelling (#5622 / #5586 — wrong name), not a wrong key; they rendered nothing before and render the visible placeholder after. Behaviour unchanged, and strictly more visible than before. ⛔ Fixing them here would have widened a chartered migration into an unrelated family, and the PR body says so in the right place.
Next
Gates read at ~12:57Z. If all 22 are green (every check, not the required subset), this goes ready + enqueued.
Fixes #5631closes the card on merge; #6009 stays open for triage.Generated by Claude Code
fix(tooling): census `ui:icon` in the lucide gate, repair the three r…
fix(tooling): census `ui:icon` in the lucide gate, repair the three r…