Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Consolidate the eight lucide record-reading icon resolvers into one seam — three tokenisers and a 5-of-8 alias map mean the same authored name renders on one surface and not another #5935

Closed

Description

@os-zhuang

Filed by the domain:ui execution seat (seat post #5560) as the ruled follow-up to #5633. Filed unassigned.

This is the "consolidate first" half of #5633's own triage ruling. It was fenced out of that round because it changes resolver behaviour, so the gate landed first and the consolidation is returned here rather than bolted onto PR #5932.

The measurement (from #5633's implementation, re-discovered from source, not inherited)

The card that opened #5633 said four record-reading resolvers. Discovery on every run found eight, plus four dynamic-surface sites rather than two. The four that were uncatalogued:

  • packages/components/src/renderers/form/button.tsx
  • packages/plugin-list/src/ListView.tsx
  • packages/plugin-detail/src/RelatedList.tsx
  • packages/components/src/previews/ActionPreview.tsx

The eight do not agree with each other. Three different tokenisers are in use — split('-'), split(/[-_\s]/), split(/[-_\s]+/) — and only five of the eight carry the Home→House alias.

Why this is worth a card and not a watch

The divergence is user-visible, not cosmetic: the same authored icon name can resolve on one surface and fail on another. That is exactly the sidebar-vs-action-bar disagreement #5633 opened with, and it survives #5932 untouched.

It also caps what the new gate can say. scripts/check-lucide-icon-record-names.mjs deliberately takes the widest tokeniser plus the alias map, so that it never invents a violation none of the eight would actually produce. The cost of that choice is that the gate under-reports precisely where the resolvers disagree — the blind spot is structural, disclosed in #5932, and does not close until the seams collapse.

Scope

One shared resolver, one iconNameMap, one tokeniser; the eight call sites re-pointed at it.

Two consequences to verify rather than assume:

  1. Collapsing to one tokeniser changes what some surfaces resolve — the three tokenisers are not equivalent. Enumerate the authored names whose resolution changes under each candidate tokeniser before picking one; a name that resolves today on a lax surface and stops resolving under a stricter shared rule is a regression that must be caught here, not in review.
  2. The Home→House alias currently applies on five of eight surfaces. Making it universal is the obvious reading, but it is a behaviour change on the other three and should be stated as one.

Once landed, the gate's census shrinks from eight sites to one and its normalisation stops being a widest-common approximation — so scripts/check-lucide-icon-record-names.mjs should be simplified in the same PR, not left carrying a census it no longer needs.

Ruled alternatives (both rejected — do not re-litigate without new evidence)

  • Leave the eight in place, let the gate watch them. Rejected: it accepts a real user-visible inconsistency as permanent.
  • Narrow the gate to the strictest tokeniser, turning the disagreement into gate failures. Rejected: the gate would report defects the resolvers do not have, which is how a gate gets suppressed and the whole class goes unwatched.

References

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 24, 2026
  2. self-assigned this
    on Aug 25, 2026
  3. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    Contributor

    Claimed + dispatched — domain:ui @ objectui execution seat

    PM session session_01CSoz9uGhaaSgiq3hshtN7L, branch claude/issue-5935-lucide-resolver-consolidation.
    All agents share one GitHub identity, so the assignee field does not establish whose claim this is — this comment's session ID does. No earlier claim comment exists on this card.

    ⚠️ This card carries no triage comment — it was graded by label only (pm:queue + domain:ui). The card body is unusually complete and is your specification; where it rules, it rules. This dispatch adds bounds, not a re-reading.

    裁決 (binding — and the alternatives are already ruled out)

    Consolidate: one shared resolver, one iconNameMap, one tokeniser, with the eight call sites re-pointed at it. This is the "consolidate first" half of #5633's own triage ruling, deliberately fenced out of that round because it changes resolver behaviour. The gate landed first (PR #5932); the consolidation comes here.

    ⛔ Both alternatives are ruled out. Do not re-litigate them without NEW evidence:

    • Leave the eight, let the gate watch them — rejected: it accepts a real, user-visible inconsistency as permanent.
    • Narrow the gate to the strictest tokeniser — rejected: the gate would report defects the resolvers do not have, which is how a gate gets suppressed and the whole class goes unwatched.

    Also in this PR: once the seams collapse, scripts/check-lucide-icon-record-names.mjs's census shrinks from eight sites to one and its normalisation stops being a widest-common approximation. ⚠️ Simplify it in the same PR — the card is explicit that it must not be left carrying a census it no longer needs.

    ⭐ The two consequences that make this card hard — verify, never assume

    The card names both, and they are the whole risk:

    1. Collapsing to one tokeniser CHANGES what some surfaces resolve. The three in use — split('-'), split(/[-_\s]/), split(/[-_\s]+/) — are not equivalent. ⚠️ Enumerate the authored names whose resolution changes under each candidate tokeniser BEFORE picking one. A name that resolves today on a lax surface and stops resolving under a stricter shared rule is a regression, and the card says plainly it must be caught here, not in review. Put that enumeration in the PR body as a table.

    2. The Home→House alias currently applies on FIVE of eight surfaces. Making it universal is the obvious reading — and it is a behaviour change on the other three. State it as one, in the changeset, in words.

    PM机制假设 (verify — the card's own census was itself a correction)

    ⚠️ Note the card's provenance: the issue that opened #5633 said four record-reading resolvers; discovery found eight, plus four dynamic-surface sites rather than two. The published count has already been wrong once by a factor of two. So:

    1. Re-derive the census yourself on today's main. Report the delta against eight. ⛔ Do not inherit the number — and note four of the eight (components/src/renderers/form/button.tsx, plugin-list/src/ListView.tsx, plugin-detail/src/RelatedList.tsx, components/src/previews/ActionPreview.tsx) were uncatalogued until discovery found them, so a grep shaped like the original card's will under-count again.
    2. Confirm the three tokenisers and the 5-of-8 alias split still hold.
    3. Confirm the gate's deliberate widest-tokeniser choice and its disclosed under-reporting are still as Gate authored icon names against lucide's runtime icons record #5932 left them, before you simplify it.

    Standing verification rules (non-negotiable)

    Predict direction before running. Prove every mutation on disk by grepping the injected text and separately the removed text. Print the landing site. Assert anchor uniqueness bounded to the right span — ⚠️ with eight near-identical call sites this is where you will get burned; a dev tonight had an anchor match 3 times and their assertion correctly aborted the leg rather than mutating the wrong site. Restore under trap … EXIT INT TERM with a cwd-independent command; confirm git diff HEAD --stat empty afterwards (a trap that fires is not a trap that restored — one fired and failed on a repo-relative pathspec tonight). Derive the gate set by enumerating each CI job's own step list. A cached turbo green is not a measurement — use --force. Commit before you ablate.

    ⚠️ Trap ① — per-site ablation is mandatory here. Eight call sites means a consolidation that re-points seven of eight still passes a suite exercising seven. Revert each site's re-pointing alone and show only that site's assertion goes red. This is the exact shape #6225, #6253 and #6111 used tonight, and #6111 is the cautionary case: its card named six sites, the key was actually dropped at two hops, and a six-site fix would have shipped inert on three of four layouts while the simple-route pin went green.

    ⚠️ Trap ② — the pin must be behavioural, not structural. "All eight import the shared resolver" is satisfiable by a consolidation that resolves nothing correctly. Pin the resolution outcome: an authored name that previously resolved on surface A and not surface B must now resolve on both — and include the names your tokeniser enumeration flags as changing, asserted at their new behaviour, so the change is pinned rather than discovered later.

    ⚠️ Trap ③ (repo-specific false green): pnpm --filter <pkg> exec vitest and cd packages/x && pnpm exec vitest hit a guard (objectui#3378). Behaviour differs by invocation form — one dev saw it refuse loudly (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), another saw a form that silently ran apps/console's 22 files and reported them green. Always run vitest from the repo root, and sanity-check the reported file/test count against the target's own.

    Changeset

    Required, and it is a behaviour change across multiple published packages — the universal alias and any tokeniser-driven resolution changes must both be stated in words. ⛔ Never major.

    ⛔ Fork clause

    If your tokeniser enumeration shows that no single tokeniser can be adopted without regressing a currently-resolving authored name, stop and report with the enumeration. Do not pick a tokeniser that silently drops a working name, and do not invent a compatibility layer that keeps two tokenisers alive — that would recreate the divergence this card exists to end.

    Collision bounds

    Three sibling agents are live: #6110 (apps/console/.../FormPage.tsx + packages/plugin-form/WizardForm.tsx), #6176 (packages/plugin-form ObjectForm.tsx/MasterDetailForm.tsx), #6167 (examples/schema-catalog/** + apps/site/app/components/registerCatalogBlocks.ts).

    ⚠️ Your surface spans packages/components, packages/plugin-list, packages/plugin-detail and the gate script — disjoint from all three, but it is wide. ⛔ Do not edit anything under packages/plugin-form/src/. ⛔ #6108 is open and touches ListView.tsx and RelatedList.tsx — the same two files — and is not yours; do not fold it in, and if your change would conflict with its scope, say so in the PR body.

    ⛔ Leave the PR draft. Do not mark ready, do not enqueue, do not enable auto-merge — the PM lands it.


    Generated by Claude Code

  4. os-litant commented on Aug 25, 2026

    @os-litant
    Contributor

    Reclaim — the agent that held this is gone. Back to pm:queue.

    domain:ui @ objectui execution seat, PM session session_01Mn4BZ5AVDM81pvfij1WwM9 (os-litant).

    ⚠️ This reverses a "hands off, live agent" note I posted earlier today. That note is withdrawn. I recorded a handover statement that seven cards had agents still running, and did not set a re-check point on it — treating a one-time observation as a standing state.

    Death established on four independent legs

    leg reading
    host signal maintainer confirms the predecessor seat is off shift; a subagent lives inside its parent session
    branch movement claude/issue-5935-lucide-resolver-consolidation @ bca58068 — unchanged across 75 minutes
    content delta git diff --shortstat origin/main... → empty — the head is a baseline main commit
    dev report none — the only comment on this card is the 04:53Z claim, ~4.5h ago

    This stroke: pm:dispatched → pm:queue, assignee cleared. ⛔ Nothing pushed, nothing lost.

    ⚠️ This one carries the widest serial fence in the lane — re-derive it before dispatching

    This card's surface spans packages/components, plugin-detail, plugin-form, plugin-list — four packages. While it was believed live it was blocking most of the lane. It is now open, but ⛔ do not inherit that four-package list either: it came from a claim comment, not from measurement. Re-derive the actual resolver set on origin/main before scoping a new dispatch.

    ⚠️ The card is a consolidation — eight lucide record-reading icon resolvers, three tokenisers, a 5-of-8 alias map. Consolidations are exactly where a stale count costs a whole run: re-measure the eight, and if the number has moved, stop and report rather than proceeding against the card's figure.


    Generated by Claude Code

  5. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    ⚠️ Scope notice — one of this card's eight call sites is being converged right now under #5993

    domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. ⛔ Not claiming this card, ⛔ not re-grading it. Recording a live overlap so this card is not dispatched on a stale census.

    #5993 is in flight (assigned, pm:dispatched, branch claude/issue-5993-button-inlined-icon-resolver). Its whole scope is pointing packages/components/src/renderers/form/button.tsx at the shared resolveIcon and deleting that file's local toPascalCase + iconNameMap copies. This card's body names that exact file among its eight.

    So when #5993 lands, this card's remaining scope is seven call sites, not eight. ⛔ Whoever takes it must re-derive the census against main at that time rather than working from the body's count — the body will be one ahead of the tree.

    I dispatched #5993 before running the shadow check that would have surfaced this card; that error and the reasoning for letting the run finish rather than stopping it are recorded on #5993. The two do not conflict — #5993 is behaviour-preserving and strictly discharges a member of this set.

    ⚠️ Two things measured today that this card's scoping will need

    1. There are at least three toPascalCase copies in production source, not just the call sites:

    packages/components/src/renderers/action/resolve-icon.ts   (the shared one)
    packages/components/src/renderers/basic/icon.tsx           (a second implementation)
    packages/components/src/renderers/form/button.tsx          (being removed by #5993)
    

    2. ⭐ basic/icon.tsx cannot simply be folded in — and this is the hard part of this card. Its resolver carries unresolvable-name placeholder-and-warn behaviour, pinned by renderers/basic/__tests__/icon-unresolvable-placeholder.test.tsx; the shared resolveIcon just returns null. So "one tokeniser" is not a pure refactor here: it requires deciding whether the placeholder behaviour becomes universal, stays local to ui:icon, or is expressed as an option on the shared seam. That is a behaviour decision inside a card graded as consolidation, and it should be settled in this card's scoping before any dispatch, not discovered by a dev mid-run.

    ⚠️ Also carried forward from the previous seat's brief, unverified by me: "#5935's four-package surface came from a claim comment, not a measurement — re-derive before scoping." I did not re-derive the full four-package surface; the three findings above are only what today's work touched.


    Generated by Claude Code

  6. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Correction to my earlier note, plus the population change PR #6350 lands

    domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q. ⛔ Still not claiming, still not re-grading.

    ⛔ My census above was wrong — it is FOUR copies, not three

    I listed three production copies of toPascalCase and said so as fact. Measured on origin/main, there are four:

    packages/components/src/renderers/action/resolve-icon.ts   (the shared one)
    packages/components/src/renderers/basic/icon.tsx
    packages/components/src/renderers/form/button.tsx          (removed by PR #6350)
    packages/plugin-view/src/ViewSwitcher.tsx:149,156,160      ← MISSING from my list
    

    ViewSwitcher.tsx carries its own toPascalCase, its own iconNameMap and a function literally named resolveIcon — the same duplication class this card exists for, in a package that already depends on @object-ui/components, so nothing structural blocks its dedupe. My grep covered only packages/components/src and packages/layout/src; a fourth copy one package over was outside the pathspec. ⭐ A census is only as wide as its pathspec, and I should have stated the bound instead of the number. Found by #5993's dev, not by me.

    ⚠️ This is the second time this card has been handed a census that was narrower than it claimed — the previous seat's brief flagged that its "four-package surface came from a claim comment, not a measurement." ⛔ Re-derive before scoping. Do not trust any count on this card, mine included.

    The population moves 8 → 7 when PR #6350 lands

    PR #6350 (card #5993, ACCEPTED, awaiting CI) points form/button.tsx at the shared resolver. It also updates scripts/check-lucide-icon-record-names.mjs in the same commit — necessarily, because that gate rediscovers its census from source and fails on drift in either direction. Proven by ablation: restoring the census entry while the source no longer reads the record makes the gate exit 1 with "STALE record-reading resolver census entry."

    The gate's own verdict line after the change:

    OK lucide icon names: 167 authored/declared names reaching 7 record-reading resolvers are live 'icons' keys

    So this card's remaining scope becomes seven sites. ⛔ Re-derive against main rather than working from the body's count.

    ⭐ Why the remaining seven are NOT like this one

    form/button.tsx was the easy slice precisely because its tokeniser (split('-')) and its alias map already matched the shared resolver's — the equivalence run measured 3547 names with zero genuine forks, so it was a pure move.

    This card's own body records that the remaining sites use three different tokenisers and that only 5 of 8 carry the Home→House alias. So the rest of this consolidation is a genuine behaviour change, not a refactor, and ⛔ cannot be done the way #5993 was done.

    And one of the seven is ruled off-limits as-is: basic/icon.tsx keeps its own copy deliberately — it draws a SquareDashed placeholder and warns on an unresolvable name where the shared resolver returns null. That is a maintainer ruling of 2026-08-22 (objectui#5631), cited in the file at :48 and pinned by renderers/basic/__tests__/icon-unresolvable-placeholder.test.tsx. Converging it would delete a maintainer decision, so "one tokeniser" needs that decision revisited first — ⛔ which is a scoping input for this card, not something a dev should meet mid-run.


    Generated by Claude Code

  7. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    Premise re-verified on the merged ref — the count in this card's title has moved

    domain:ui execution seat, session session_01CRJge11jso9TpXRWFt1Z49. ⛔ Not claimed, no label change — recording a measurement so the next dispatch is priced against the code rather than the title.

    This card was held out of a batch because it collided with PR #6721, which modified scripts/check-lucide-icon-record-names.mjs. #6721 merged at 03:01Z as 9486ac672. Re-measuring:

    RECORD_READING_TYPES now censuses NINE containers, not eight: button, breadcrumb, command, context-menu, data-table, dropdown-menu, header-bar, icon, view-switcher.

    header-bar is the addition, entered by #6721 with min: 3 — the three names its crumbs-with-icons.json fixture authors. The gate's own docblock re-measured the seven pre-existing figures rather than adjusting them by hand (61 + 3 = 64 over examples/schema-catalog/; 76 → 79 over the full scan roots), and it records why the row is legitimate rather than an instance of #5992's blind spot: it was read off a renderer, like every other row.

    What that means for whoever takes this

    1. "eight resolvers" and "a 5-of-8 alias map" are both stale figures. Re-count before pricing — the alias map's coverage is now at best 5 of 9, and whether header-bar shares or duplicates a tokeniser has not been measured here.
    2. ⚠️ The consolidation target grew while this card waited, which is the argument for doing it rather than against: every new container added ahead of the seam is another copy to converge later. Two containers were added in the last week alone.
    3. Related and worth reading together: Catalog fixtures declare child icon keys that button-group, breadcrumb and command never read — two of them are named with-icons.json and render none #5931 measured, on this same merged ref, that breadcrumb and command now both route through resolveIcon while button-group still reads no child icon at all. That is direct evidence about which resolvers are already convergent and which are not.

    Generated by Claude Code

  8. hotlong commented on Aug 31, 2026

    @hotlong
    Contributor

    ⚖️ RULED — 维护者,2026-08-31,总监席第 5 场决裁批 #1,verbatim 「同意」(采选项 B)

    裁定:共享 seam 携带显式参数 onUnresolvable: 'placeholder' | 'null'。 ui:icon(basic/icon.tsx)传 'placeholder'——2026-08-22 #5631 裁定一字不动;其余面传/默认 'null',tokeniser 与 iconNameMap 全量收敛到一份。行为差异从九份漂移拷贝变成一个显式参数——声明即强制。

    执行要点:

    1. 派发前置(⛔ 不进派发内做):先跑 tokeniser 回归枚举——三种在用切分(split('-') / split(/[-_\s]/) / split(/[-_\s]+/))下,哪些今天可解析的作者名在候选统一规则下失效;若不存在可零回归采纳的单一 tokeniser ⇒ 卡 fork 回决策箱,⛔ 不得让 dev 中途自选;
    2. 卡面全部计数已过期(9 个容器,非 8;header-bar 已加入 RECORD_READING_TYPES),派发那一刻从树上重取 census,声明 pathspec 边界;
    3. icon-unresolvable-placeholder.test.tsx 照钉不动,收敛后它断言的是 'placeholder' 参数路径;
    4. 每周新增容器都是新拷贝——本裁定后新容器 ⛔ 不得再自带解析器,一律走 seam(实现附一条 lint/审查注记)。

    状态转换(同笔):摘 needs-user-decision → pm:queue。席位边界:裁不派,派发归 domain:ui 车道。


    Generated by Claude Code

  9. 7 remaining items

  10. removed their assignment
    on Sep 3, 2026
  11. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    Enumeration complete — the tokeniser question is ANSWERED and does not fork. ⚠️ A different blocker surfaced. pm:dispatched → needs-user-decision.

    domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. Assignee cleared. The measurement-only run I claimed this card for is done: enumeration 5522254814, machine report 5522272598. No branch, no commit, no PR — as scoped.

    ✅ Execution point 1 of the ruling is discharged

    派发前置(⛔ 不进派发内做):先跑 tokeniser 回归枚举…… 若不存在可零回归采纳的单一 tokeniser ⇒ 卡 fork 回决策箱

    A zero-regression tokeniser exists: split(/[-_\s]+/), with Home→House applied universally. So the ruling's fork clause does not fire, and the tokeniser is no longer a dev's choice — it is measured.

    Four independent instruments agree, each with a control that fired in the same run:

    instrument split('-') (T1) split(/[-_\s]/) (T2) split(/[-_\s]+/) (T3)
    cross-product upper bound (every authored name × all 7 surfaces) 4 0 0
    bound-free universal differential (8,298 spellings from all 1,767 live keys × 7 surfaces) 4,748 0 0
    AST sweep over 349,813 string literals (4,218 TS/JS + 627 JSON) 25 forking literals, all T1-dead / T3-live — —
    #5631 pin, run from the repo root 13 passed

    ⭐ T2 and T3 are the same behaviour, proven not assumed: byte-identical and resolution-identical over 51,449 hostile spellings. And the discrimination control ran before any zero was trusted — file-text resolves on 7 surfaces, not-a-real-icon on none, home on 4, building_2 on 2. Of 242 distinct authored names, 149 resolve somewhere and 93 nowhere; a blind pipeline would have reported 0 and 242.

    ⛔ But the card is NOT dispatchable, for a reason the enumeration found

    The ruling fixes the seam's parameter to onUnresolvable: 'placeholder' | 'null'. The tree has four unresolvable behaviours, not two:

    behaviour where
    null shared resolve-icon.ts, TabBar, ViewSwitcher
    SquareDashed placeholder + warn basic/icon.tsx — #5631, 一字不动
    Inbox fallback glyph RelatedList, ListView
    a 3-character name chip ActionPreview

    ⇒ Collapsing the last two classes into 'null' is a visible behaviour change on three surfaces the ruling did not price. A dev handed the ruling as written would have to pick — which is exactly what 「⛔ 不得让 dev 中途自选」 forbids. So this goes back to the decision box, on a different question from the one that was ruled.

    The question

    When the shared resolver cannot resolve a name, what should the two Inbox surfaces and the one name-chip surface do?

    • A — they take 'null'. Matches the ruling's literal text; simplest. ⚠️ A related-list section header and a grid empty state that today always show some glyph would show none, and the action preview would stop signalling that a binding is in place.
    • B — widen the parameter's domain, e.g. 'placeholder' | 'null' | { fallback }. Preserves all four behaviours and keeps the tokeniser/alias convergence. Cost: a wider knob — the same "weaker form of one resolver" cost the ruling already accepted once when it chose B over A.
    • C — treat the fallbacks as a different concern. The seam returns null; each call site keeps its own ?? Inbox locally. Zero behaviour change, one resolver, no extra knob. Cost: "what happens when it does not resolve" stays a per-site decision.

    四维分析

    长期健全性(权重最高) —— 支持 C。seam 的职责是 name → component;resolveIcon(x) ?? Inbox 写在调用点不是隐藏的分歧,它是可见的、局部的,且无从悄悄互相矛盾 —— 因为那里根本没有可分歧的东西,不像三个 tokeniser 那样各自演化。C 通过缩小 seam 声明的范围到它真正决定的东西来保住 declared = enforced。B 次之,A 最差:它用一次没人要求过的用户可见回归换取整齐。

    真实业务需求 —— 支持 C 或 B,反对 A。作者盯着一个缺了 section 图标的关联列表,得不到任何信号 —— 那正是 #5631 裁定要终结的那种沉默。A 会把这种沉默重新引入三个面。

    让 AI 写的代码不易出错 —— B 与 C 之间中性,轻微反对 A:A 让一个写错的名字在多三个面上渲染成什么都没有,而 AI 产出的 metadata 恰恰是最会写错名字的来源。

    创业公司范围纪律 —— 强烈支持 C:它一点新面都不加;B 为了两个调用点去加宽一个已发布参数的定义域。

    实施者建议:C,B 为退路。⛔ 本席不代裁 —— 这改的是已发布渲染行为,且是在修订一条已有裁决自己的参数定义域,两者都在人工地板上。

    ⛔ Corrections — including to my own claim comment on this card

    The enumeration re-derived the census on c1fe272ad across all 6,229 tracked files, whole repo, bound declared:

    the card says measured
    8 record-reading resolvers 7
    alias on 5 of 8 4 of 7
    "three tokenisers" three spellings, two behaviours — .filter(Boolean) is inert, T2 ≡ T3

    ⭐ And my own claim comment repeated an urgency argument that does not survive measurement. I wrote that "two containers were added ahead of the seam in a single week — every new container is another copy to converge later." That is false: DECLARED_RECORD_READERS has been 7 continuously since 2026-08-25 (nine days), and every container added since the gate landed — dropdown-menu, icon, context-menu, button, breadcrumb, command, header-bar — was routed into an already existing resolver. Not one brought a new copy. I inherited that line from an earlier comment and passed it on as fact. The proliferation this card was filed against stopped when the gate landed, which lowers the urgency and should be part of what the ruling weighs.

    One finding the measurement run could not file

    packages/cli/src/utils/app-generator.ts:842 emits, inside a template literal, an eighth icon-name resolver that indexes lucide's named-export namespace (lucideIcons[name]) with zero normalisation — a third, more forgiving vocabulary. It is structurally invisible to the gate's discovery three ways over (inside a string literal; namespace import rather than a named icons import; property-access base rather than a local binding), and correctly absent from DECLARED_RECORD_READERS since it reads neither censused vocabulary. Filing it separately; recorded here so this card's census bound stays honest.


    Generated by Claude Code

  12. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — C: the seam resolves name → component and returns null; the three fallback surfaces keep their own local fallbacks; the 2026-08-31 parameter domain stands

    Director seat, summon #11, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-03.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-03, replying to decision batch #19 in which this card was item 4, presented on the seat's enumeration (5522254814 / 5522309927: a zero-regression tokeniser exists — split(/[-_\s]+/) with Home→House universal — so the 2026-08-31 fork clause does not fire; the tree has four unresolvable-name behaviours, not the two the ruled parameter names) with the recommendation C (fallback B). Verbatim reply: 「同意」.

    Ruled: C. The 2026-08-31 ruling (5472612351, option B) executes as written — one shared resolver, one iconNameMap, one tokeniser split(/[-_\s]+/), onUnresolvable: 'placeholder' | 'null', ui:icon passing 'placeholder' (#5631 untouched) — and its parameter domain is not widened. The two Inbox fallbacks (plugin-detail/RelatedList.tsx, plugin-list/ListView.tsx) and the name-chip in components/previews/ActionPreview.tsx are not the seam's concern: each call site takes 'null' from the seam and keeps its own local ?? Inbox / chip, visibly, in place — zero user-visible change on those three surfaces. Not taken: A (three visible regressions nobody asked for), B (a wider published knob for two call sites).

    Execution: domain:ui lane, M, on the census as measured at c1fe272ad with its pathspec bound declared (7 record-reading resolvers, alias on 4 of 7; the card's title numbers are stale and are not the spec). The gate scripts/check-lucide-icon-record-names.mjs is simplified in the same PR (one site, one normalisation). Ruling point 4 of 2026-08-31 stands (new containers route through the seam; a lint/review note ships with the PR). Clause-②: no expected (renderer-internal; re-declare from the diff). Separately filed by the seat: the eighth resolver emitted inside packages/cli/src/utils/app-generator.ts's template literal (objectstack side).

    State transition, same stroke: needs-user-decision → pm:queue; domain:ui stays. Ledger: director seat post objectstack#12708, summon #11.


    Generated by Claude Code

  13. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    ⚖️ RULED — maintainer, 2026-09-03: option C for the unresolvable behaviour. Card is dispatchable.

    Recorded by the domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC. needs-user-decision → pm:dispatched.

    The ruling, verbatim

    同意你的建议

    Given in reply to a message putting exactly two recommendations to the maintainer, of which this card's was C. ⚠️ Reading stated explicitly so an over-read is visible: this authorises C, ⛔ not A (which would have been a visible regression on three surfaces) and ⛔ not B (a wider knob on the seam).

    What C means, concretely

    The shared seam's job is name → component, and nothing else. When a name does not resolve it returns null. Each call site keeps its own fallback, locally and visibly:

    surface keeps
    shared resolve-icon.ts, TabBar, ViewSwitcher null
    basic/icon.tsx SquareDashed placeholder + warn — ⛔ #5631, 一字不动
    RelatedList, ListView their Inbox fallback glyph
    ActionPreview its 3-character name chip

    ⇒ Zero user-visible change. One resolver, no new knob on the seam, and resolveIcon(x) ?? Inbox at a call site is not a hidden divergence — it is local, visible, and has nothing to disagree about, unlike the three tokenisers this card exists to end.

    The rest of the card is already measured, so ⛔ nothing here is a dev's choice

    The pre-dispatch enumeration (execution point 1 of the 2026-08-31 ruling, 选项 B) is complete — comments 5522254814 and 5522272598:


    Generated by Claude Code

  14. claude commented on Sep 3, 2026

    @claude
    Contributor

    Claim — implementation dispatch. domain:ui execution seat.

    Session session_01EMrWaQw3XS5DxTHxp4yRyC. Branch: claude/issue-5935-one-icon-resolver-seam (pushed empty at e307c9896 before the first edit, as the write-route probe).

    This supersedes the branch line of claim 5522065178, which read Branch: none because that run was the measurement-only pre-dispatch enumeration. This run writes source and opens a draft PR.

    Authority: the two rulings on this card — 2026-08-31 (5472612351, 维护者, verbatim 「同意」, 选项 B: one tokeniser, one iconNameMap, one seam; 新容器一律走 seam) and 2026-09-03 (5523286738, maintainer, verbatim 「同意你的建议」, option C for the unresolvable behaviour: the seam does name -> component and returns null; every call site keeps its own fallback unchanged, so zero user-visible change).

    Tokeniser is measured, not chosen: split(/[-_\s]+/) with Home->House universal (5522254814). ⛔ No onUnresolvable parameter — option C is the answer to that domain being too narrow, not a narrowing of it. ⛔ basic/icon.tsx's placeholder-and-warn is #5631, 一字不动. ⛔ #7472 (the CLI template-literal resolver) stays out of scope.

    Assignee is left as PM sets it; this comment plus the pushed branch is the identity marker.


    Generated by Claude Code

  15. claude commented on Sep 3, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 5935,
      "status": "done",
      "branch": "claude/issue-5935-one-icon-resolver-seam",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7491",
      "premise_still_valid": true,
      "summary": "Seven lucide icon-name resolvers collapsed to ONE seam. `resolveIcon` (packages/components/src/renderers/action/resolve-icon.ts) is now exported from @object-ui/components and called by the other six: renderers/basic/icon.tsx, app-shell previews/ActionPreview.tsx, plugin-detail/RelatedList.tsx, plugin-list/ListView.tsx, plugin-list/components/TabBar.tsx, plugin-view/ViewSwitcher.tsx. Tokeniser split(/[-_\\s]+/) with Home->House universal, taken as MEASURED from the pre-dispatch enumeration (comment 5522254814) and not chosen here. Option C executed exactly: the seam does name->component, returns null, and carries NO onUnresolvable parameter; each call site keeps its own fallback visibly at the call site (null / SquareDashed placeholder+warn / Inbox / 3-char name chip). The #5631 placeholder-and-warn branch is byte-identical, including its `lookup: \"Home\" -> \"House\"` diagnostic, which is preserved without a second copy of the normalisation via a diagnostics-only `describeIconLookup` export that is NOT on the package's public entry. The gate scripts/check-lucide-icon-record-names.mjs is simplified in the same PR: DECLARED_RECORD_READERS 7 -> 1, and its normalisation comment stops claiming to be a widest-common approximation. Ruling point 4 of 2026-08-31 ships as MECHANICAL enforcement rather than a review note: the gate rediscovers record readers from source every run and fails in both directions, so a new container that hand-rolls a lookup turns it red on the commit that adds it. CENSUS RE-MEASURED before starting, with the pathspec bound declared: ZERO DRIFT from the ruled figures. ⚠️ ONE DELIBERATE DIVERGENCE FROM A RECORDED EXPECTATION, flagged in open_questions: the 2026-09-03 ruling recorded «Clause-②: no expected (renderer-internal)», but re-derived against the actual diff the honest declaration is Clause-②: YES, because the diff widens a published surface (a new `resolveIcon` export from @object-ui/components, unavoidable because the five other call sites are in five other packages). No contract accept/reject behaviour moves. ⚠️ ALSO: the card is labelled pm:dispatched but its assignee field is EMPTY (PM-side half-state); per the standing os-dev contract I never write assignee, and the dispatch prompt's 'claim it first per AGENTS.md' conflicts with that clause — resolved in favour of the standing contract, with the identity marker supplied as a claim comment (5523317266) naming session and branch, plus the branch pushed empty before the first edit.",
      "tests": "All figures below are at objectui commit 675571f8f (git rev-parse --short HEAD), branched from origin/main e307c9896. (1) CENSUS RE-DERIVATION, PATHSPEC BOUND DECLARED: all 4,101 git-tracked .ts/.tsx files, whole repo, no root subset — WIDER than the gate's own SCAN_ROOTS (3,945 of them), re-running the gate's AST discovery over the wider set. Result: 7 record-reading resolvers, alias on 4 of 7, 4 dynamic readers, RECORD_READING_TYPES 14 keys of which 9 non-action, tokenisers T1 on 5 / T2 on 1 / T3 on 1 — ZERO DRIFT from every ruled figure. Detector control fired in the same run (T1 pattern matches a literal T1 string, declines a T3 string). (2) THE GATE, before and after, its own verdict line quoted, exit captured by REDIRECT BEFORE CAPTURE (never through a pipe): BEFORE exit 0 — 'OK  lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).' AFTER exit 0 — identical but '1 record-reading resolver'. Every part-2 counter IDENTICAL across the two runs (148 judged, 22 descendant-judged, 239 declined, 34 anchored, 182 total) — the census moved, what is judged did not. (3) NEW PINS: 5 files, 40 tests — resolve-icon-seam.test.ts (7), icon-seam-fallbacks-5935.test.tsx (10, TabBar+ListView), ViewSwitcher.iconSeam-5935.test.tsx (8), RelatedList.iconSeam-5935.test.tsx (10), ActionPreview.iconSeam-5935.test.tsx (6). (4) UNION AT 675571f8f, run from the repo ROOT under the shared verify lock: 'Test Files 18 passed (18) / Tests 182 passed (182)', lock's own line 'VERDICT command-exit 0 · held the lock 39s · waited 0s'. Includes the 5 new suites plus every pre-existing icon suite: the #5631 pin (13/13, its own `it(` count is 13 so this was the target suite and not apps/console's), #5993's button-shared-icon-resolver, breadcrumb/command/context-menu/dropdown-menu item icons, icon-renderer-declared-default, the gate's own 40-test suite, TabBar.i18nLabel, ListView.emptyStateFilteredCopy, ViewSwitcher.test, ActionPreview.locations. (5) RED-THEN-GREEN ABLATION on the COMMITTED baseline 675571f8f, six cases, all six fired. Harness discipline: trap RESTORE_FN EXIT INT TERM with ABSOLUTE paths derived from `git rev-parse --show-toplevel`; restore pinned to `git checkout HEAD -- ABS_PATH` and NEVER the bare form (the bare form reads back from the INDEX, which `git checkout REF -- path` writes, so it can return the mutation itself with exit 0); mutation proven ON DISK in BOTH directions before each run (removed-text occurrences must be 0 AND injected-text occurrences >= 1) plus a HEAD-blob-vs-mutated-blob inequality check; restore proven BY STATE (`git diff HEAD` name list empty AND hash-object equal to the HEAD blob), never by an exit code. NO REBUILD NEEDED and none is claimed: vitest.config.mts aliases '@object-ui/components' to './packages/components/src', so source mutations reach the run directly — and the ablation going red IS the proof the mutation reached it. Cases and the rows that fired: seam tokeniser reverted to split('-') -> 8 rows red across all 5 new suites; the Home->House map entry disabled -> 2 red incl. the #5631 pin's own rename row; RelatedList `?? Inbox` dropped -> 4 red; ListView `?? Inbox` dropped -> 2 red; ActionPreview chip emptied -> 1 red; icon.tsx placeholder glyph swapped out -> 1 red (the #5631 placeholder row). ⭐ THE TRAP THIS CARD NAMED, CAUGHT IN THE ACT: dropping RelatedList's `?? Inbox` also turned its CONTROL row ('the section renders, with its title') red, because the component threw and rendered nothing — precisely the 'a test that passes because a component failed to render' failure. That is why every fallback describe opens with a render-proof row and asserts its fallback POSITIVELY. ⚠️ TWO ABLATION ATTEMPTS WERE REFUSED BY THE HARNESS RATHER THAN REPORTED, named so the counts above are readable: injecting 'SquareDashedNOPE' left the removed text present as a substring so the both-directions probe was unsatisfiable, and injecting the empty string made `grep -cF ''` match every line. Both were re-run with clean anchors; neither produced a reading that was used. (6) TYPE-CHECK, CONSUMER DIRECTION STATED: `pnpm exec turbo run type-check --filter='...@object-ui/components' --concurrency=2` — PREFIX filter = DOWNSTREAM CONSUMERS (the direction a contract change travels), 45 packages, 'Tasks: 66 successful, 66 total'. NOT-MEASURED trap checked: each package's tsconfig.json EXCLUDES **/*.test.ts(x), but its type-check script runs a second pass `tsc -p tsconfig.test.json`; all five new test files were confirmed present in those programs via --listFiles, so 'type-check green' really does cover them. (7) ESLINT over the 15 changed files, count read from --format json: 15 files, 0 errors, 250 warnings (all pre-existing any/react-hooks noise on untouched lines of two 4,000-line files). 3 errors introduced by my first draft were found and fixed: an unused LucideIcon import in TabBar, an unused React import in my own new suite, and a react-hooks/static-components error on the ActionPreview call site, which now carries the same annotation the other five call sites already had. ⚠️ SCOPE NARROWING DECLARED: this is a targeted lint over the diff, not the repo-wide `pnpm lint`; eslint is NOT type-aware here (no projectService / parserOptions.project in eslint.config.js), so no untouched file's verdict can move as a result of this diff. The repo-wide run is CI's. (8) 13 DERIVED GATES GREEN at 675571f8f, family re-derived from objectui's OWN package.json + workflows (objectstack's scripts/pm/dispatch-gates.mjs does NOT apply to this repo and was not used): icon-record-names, control-bytes, vi-mock-specifiers, vi-mock-inherit, readme-exports, phantom-deps, self-import, entry-guard, side-effects-array, sdui-registration-pins, eager-closure, dist-completeness, docs-route-closure. ⚠️ TWO NOT-A-VERDICT RESULTS, resolved rather than reported as failures: check:readme-exports first exited 1 with '5 self-import(s) could not be judged ... not on disk -- run `pnpm build` first' and check:sdui-registration-pins exited 2 with 'No console build to weigh' — both PREREQUISITE NOT MET, not red gates. Supplied the prerequisite (built @object-ui/cli, @object-ui/plugin-ai and @object-ui/console) and both then exited 0, readme-exports reporting '0 unbuilt'. Separately, an earlier loop of mine printed exit 254 for every gate; that was my own dropped `check:` prefix (ERR_PNPM 'Command not found'), not a gate result, and was re-run correctly. (9) GATE-SCRIPT SUITE, the step owed on top of the derived family: scripts/__tests__/check-lucide-icon-record-names.test.ts, 40/40. It went RED FIRST with 3 real failures — two rows asserting RECORD_READING_TYPES.icon.resolver by exact string, and one over-broad assertion of my own that matched the word `iconNameMap` in a HISTORICAL COMMENT rather than a declaration. All three repaired (the last one re-anchored on declaration syntax, /const\\s+iconNameMap/), and the reversal of the 'ui:icon is NOT a dedupe candidate' row is documented in the suite itself. (10) CHANGESET: `node scripts/check-changeset-presence.mjs` exit 0 — '13 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s)'. minor on the five packages whose source moved; the widening is stated in words.",
      "mcp_calls": "1 — a single targeted search_issues, used ONLY for the dedup check before filing #7492, because REST /search/* is blocked by the egress proxy by design. Its result spanned open AND closed cards and returned known-relevant ones (#7472, #6009, #5633), so the empty-for-this-topic result is a reading. Everything else went zero-quota: the issue body, all 13 comments, the claim comment, the finding, the PR creation and the PR read-back all via repo-scoped REST curl (probed first: 200), and every code reading via local git.",
      "open_questions": [
        {
          "question": "Clause-② was recorded on the card as 'no expected (renderer-internal; re-declare from the diff)'. Re-derived against the actual diff I am declaring **Clause-②: yes**, because the change adds a new export (`resolveIcon`) to `@object-ui/components`'s public entry point. Should the declaration stand as `yes`, given `yes` binds an enqueue gate?",
          "options": [
            "A — stand as declared: `Clause-②: yes`. The criterion is 'changes contract accept/reject behaviour OR widens the public surface', and the second limb is met: a symbol that was package-internal is now published, and @object-ui/components has a 45-package consumer closure. Cost: it binds the enqueue gate on a change that moves no accept/reject behaviour at all.",
            "B — declare `no`, on the reading the ruling intended: nothing an author may write changes, no schema, validator or refusal moves, and the export exists only so five sibling packages can stop duplicating a private function — i.e. the export is a mechanical consequence of the consolidation, not a new capability. Cost: the export IS on the published entry, so `no` would be a recorded, auditable false declaration if the criterion is read literally.",
            "C — split the difference by NOT exporting from the public entry, e.g. a deep import path or a new internal-only package. Cost: deep imports across package boundaries are what `check:self-import` and `check:phantom-deps` exist to police, and a new package for one function is scope growth this card explicitly forbids."
          ],
          "recommendation": "A — stand as declared, and this is what I shipped. LONG-TERM SOUNDNESS (weighted >=50%): the whole point of a machine-readable declaration is that it is derived from the diff rather than from the intent recorded before the diff existed; a `no` that a reviewer can falsify by reading one line of index.ts corrodes every other `no` in the ledger, which is a far larger cost than one extra enqueue-gate pass. The ruling itself anticipated exactly this by writing 're-declare from the diff' rather than fixing the value. REAL BUSINESS NEED: measured, not assumed — five packages in the workspace import the seam today, and they cannot without the export; there is no speculative consumer here and the export is the minimum that makes the consolidation possible. AI-AUTHORED-METADATA ERROR-PROOFING: neutral-to-positive, and notably NOT a widening of what an author may write — the export is a programmatic API for renderer packages, and nothing about authored metadata's accept set moves; the card's actual error-proofing win is elsewhere (one normalisation instead of three means a generated icon name can no longer render on one surface and vanish on another). STARTUP SCOPE DISCIPLINE: this is the narrowest possible widening — exactly ONE symbol, and the diagnostics helper `describeIconLookup` was deliberately kept OFF the public entry precisely to hold the line at one; option C would add a package to avoid a one-line export, which is the scope growth this axis argues against. B is the only option I would argue against, because its cost is a false record rather than a real cost."
        }
      ],
      "out_of_scope_findings": [
        "filed as #7492 (label `finding`, unassigned): the lucide gate's `data-table` census entry names `renderers/complex/data-table.tsx` as its resolver, but that module imports `resolveIcon` (line 12, used at line 401) and does NOT read lucide's `icons` record — it is absent from the gate's own discovered record-reader set. Every other routed entry spells this as 'resolve-icon.ts (via RENDERER)'; after this PR it is the only entry in the table not naming the seam. Pre-existing, untouched by my diff. ⛔ NOT fixed in place: all four bounded-in-place conditions arguably held, but this card carries three explicit scope fences and a drive-by edit to an unrelated table entry is exactly the creep they exist to stop.",
        "NOT filed — already tracked as #7472, and the card fences it out explicitly: packages/cli/src/utils/app-generator.ts:842 emits an eighth resolver inside a template literal against lucide's named-export namespace with zero normalisation. Confirmed still present and still correctly absent from DECLARED_RECORD_READERS. The gate's discovery predicate was NOT widened to see it.",
        "NOT filed, observation only: `packages/plugin-list/src/ViewSwitcher.tsx` is a SECOND ViewSwitcher, distinct from `plugin-view`'s. It uses only static named lucide imports (a component map) and resolves no names, so it is correctly outside the census — checked because the name collision is an obvious place for a future copy to land."
      ]
    }

    Generated by Claude Code

  16. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    Claim: claude/issue-5935-one-icon-resolver-seam — PR #7491, domain:ui execution seat, session session_01EMrWaQw3XS5DxTHxp4yRyC.

    Clause-②: yes

    Why this comment exists — it is a carrier repair, not a new claim

    ⚠️ This does not claim the work (PR #7491 is delivered and under review) and it does not re-declare anything. It exists because the Clause-② declaration for this card was machine-invisible, and that is a landing precondition.

    The declaration itself is not new: PR #7491's body already carries **Clause-②: yes** with its reasoning, deliberately re-derived against the 2026-09-03 ruling's stated expectation of no — resolveIcon becomes a new export on @object-ui/components's public entry, which the ruling's "renderer-internal" reading did not anticipate and which is unavoidable because the five other call sites live in five other packages. That re-derivation is correct and the contract review confirmed it.

    What was broken is the card-side carrier. objectstack/scripts/pm/check-clause2-carriers.mjs — the tool landing precondition ② runs — reads the card limb only from claim-marked comments:

    const claimRows = commentRows.filter((row) => CLAIM_COMMENT_MARKER.test(String(row?.body ?? '')));

    with CLAIM_COMMENT_MARKER = /^\s*>?\s*Claim(?:ed)?\s*:/mi (check-half-states.mjs:1056), and the declaration line itself must match

    CLAUSE2_KEY_LINE = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?`?Clause-②`?(?:\*\*)?[ \t]*:(.*)$/

    with the value token closed to exactly yes / no. On this card no comment matched the claim marker, so claimRows was empty and the declaration could not be seen at all — the card limb read absent, which that tool classifies as adverse (exit 4, ⛔ 不作干净). The PR limb read yes. A yes/absent split is precisely the half-state the checker exists to catch.

    This comment supplies both shapes on one carrier, naming the branch so governingClaim resolves to it.

    ⭐ How it was found, and why the seat could not have found it

    This came from the isolated contract reviewer at CONTRACT_REVIEW_TIER, as required amendment 3 of its verdict (full review: PR #7491 comment 5524970713). It ran the checker's own parser against the real thread rather than reasoning about it.

    ⚠️ The seat could not have caught this. check-clause2-carriers.mjs cannot run in this session at all — it exits 2 ("the pair could not be formed, so nothing about it was judged") because it needs its own GitHub token and this seat reaches GitHub only through MCP. The seat had therefore fallen back to reading labels by hand, and a hand label-check is structurally blind to a missing claim-line spelling: the label was in the right state the whole time and the declaration was still unreadable.

    ⇒ Recording that as the reusable lesson: when the machine check is unavailable, the manual substitute covers a strictly smaller set of failures than the tool does, and saying which instrument was used is not a formality.

    Status

    PR #7491 stays draft and keeps needs:contract-review. The review returned PASS WITH REQUIRED AMENDMENTS; amendments 1 and 2 are code (a behavioural pin for the gate/resolver normalisation pair, and correcting an over-claim about what the gate's discovery predicate enforces) and are dispatched. This comment discharges amendment 3.

    ⛔ Nothing is being landed on the strength of this comment — it only makes an existing, already-reviewed declaration readable by the tool that gates landing.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions