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.

finding(tooling): the lucide icon-record gate cannot see icons on UNTYPED child nodes, so dropdown-menu item icons stay unjudged even now that they reach a resolver #5992

Closed

Description

@yinlianghui

Found while implementing #5930 (routing ui:dropdown-menu item icons through resolveIcon). Filed unassigned; not repaired there, because changing what the gate judges is a scope of its own.

What was measured

scripts/check-lucide-icon-record-names.mjs part 2 judges icon names on nodes whose own type is a censused record-reading renderer. Menu items in ui:dropdown-menu are child objects that carry no type key at all, so they are never judged — the gate's own header states this boundary explicitly ("an icon on an UNTYPED node is not judged at all").

Before #5930 that was harmless: the site reached no resolver. After #5930 it reaches one, and the gate still does not cover it.

Measured directly, on the #5930 branch with the fix in place — the retired spelling edit was restored to examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json and the gate re-run:

proof on disk — retired 'edit' present (expect 1): 1
gate exit with retired 'edit' in fixture = 0
OK  lucide icon names: 64 authored/declared names reaching 8 record-reading resolvers are live `icons` keys

Exit 0, same 64 names. A dead spelling that now renders no glyph in a published fixture passes the gate silently. That fixture is a declared AI few-shot retrieval source, so the failure mode is teaching a dead name, not just a missing icon.

Why the census part did not catch it either

Part 1 rediscovers modules that read lucide's icons record directly. #5930 routes through resolveIcon rather than importing icons, so renderers/overlay/dropdown-menu.tsx correctly does not enter DECLARED_RECORD_READERS — the census stayed at 8, as designed. The blind spot is entirely in part 2's node-typing rule, not in the census.

Also stale after #5930

The gate's header carries a measured parenthetical that PR for #5930 falsifies:

the eight such names in the schema catalog are child items of button-group, breadcrumb, command and dropdown-menu — three of which never read icon, and the fourth renders it as raw text

dropdown-menu no longer renders it as raw text. Left untouched deliberately (out of that card's fence); it should be corrected with whatever change addresses the above.

Options, not a recommendation

  • Judge an icon on an untyped child node against its nearest typed ancestor whose renderer is a censused record reader.
  • Or declare, per container type, which child-item keys are icon names.

Related: #5931 is the fixture-side half of the neighbouring class; #5633 built this gate.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 24, 2026
  2. claude commented on Aug 25, 2026

    @claude
    Contributor

    Concentrated triage round: finding → pm:queue, Task; relabelled domain:ui → domain:devx (three-stream split: the fix lands in scripts/check-lucide-icon-record-names.mjs, engineering face — symptom location does not set the lane). Upstream #5930 is closed completed, so the gap is live: extend part 2 to judge icon keys on the censused UNTYPED child paths (dropdown items measured as the concrete case; the retired-edit ablation in the body is the acceptance probe). Coordinate with #6009 (same gate file) — fold-or-serial is a required answer at claim.


    Generated by Claude Code

  3. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 25, 2026
  4. added theissue type on Aug 25, 2026
  5. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    Claiming this card.

    • session: session_019b5UBNMtTzKbVtZZGvFuxe
    • branch: claude/issue-5992-lucide-gate-untyped-child-icons
    • repo: objectstack-ai/objectui

    Fold-or-serial answer for #6009 (required at claim): SERIAL, not folded. Same file, disjoint regions and disjoint families:

    Folding them would put a census-table change and a spelling repair inside a card whose acceptance evidence is specifically "census UNCHANGED at 8", making both halves unverifiable. They also conflict only textually, not semantically; whichever lands second rebases over a few lines.

    Files I expect to touch: scripts/check-lucide-icon-record-names.mjs, its test file under scripts/__tests__/ (or equivalent), and possibly examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json only transiently for the ablation (reverted, verified with git diff --exit-code).

    First action after this: re-verify the card's premise on current origin/main — restore the retired edit spelling and confirm the gate still exits 0. If the gate already catches it, the premise is dead and I stop and report rather than "fixing" a closed gap.

    Generated by Claude Code


    Generated by Claude Code

  6. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor
    {
      "issue": 5992,
      "status": "done",
      "branch": "claude/issue-5992-lucide-gate-untyped-child-icons",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6277",
      "premise_still_valid": true,
      "summary": "Re-verified the premise on current origin/main (ef2a3bd8d) BEFORE changing anything: restored the retired spelling `edit` into examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json under a restoring trap, confirmed on disk (grep -c 'edit' = 1, 'square-pen' = 0, diff --stat 1 insertion/1 deletion), and the gate still exited 0 with the same 64 names. The gap is live. Part 2 of check-lucide-icon-record-names.mjs now judges an `icon` on an UNTYPED node against its NEAREST TYPED ANCESTOR, but only when that ancestor's census entry declares `descendants: true` — OPTION 1, chosen in the code. Measured reason: a pure nearest-typed-ancestor rule with no other change judges ZERO additional names, because `dropdown-menu` is not in RECORD_READING_TYPES at all; both options require declaring the container, so they differ only in what is judged under it. Option 2's `items[].icon` is measurably insufficient — renderMenuItems recurses into item.children and resolves the submenu trigger through the SAME resolveIcon call, while the paths grammar ^(\\w+)\\[\\]\\.icon$ is single-level by construction, so a key list closes the leaf and leaves the submenu open (the narrower version of the same bug #5930 refused to ship). Descent is opt-in per container rather than blanket, so a judgement measured against one shape is not extended to descendant shapes nobody read off a renderer. A typed child ENDS descent (a type:'separator' item draws no icon and stays declined), and a descent reaching nothing is an ERROR via `min`, mirroring ANCHORED_MAPS. Part 1's census is untouched at 8. The stale header parenthetical was re-measured rather than reworded: 61 untyped icon names across SEVEN containers, not eight across four — the old count was low by 53 and did not know context-menu, timeline or tree-view existed.",
      "tests": "ALL RUNS ON BRANCH HEAD e78f7c811; exit codes captured BY REDIRECT before any pipe; verdicts quoted from each gate's own printed line. (1) PREMISE RE-VERIFICATION on origin/main ef2a3bd8d, before any edit — fixture mutated on disk under a `trap ... EXIT INT TERM`, mutation confirmed on disk by grep -c on BOTH the injected and the removed text ('edit'=1, 'square-pen'=0) plus git diff --stat: 'gate exit with retired edit in fixture = 0' / 'OK  lucide icon names: 64 authored/declared names reaching 8 record-reading resolvers are live `icons` keys'. Fixture restored, `git diff --exit-code` => CLEAN (byte-identical to HEAD). No build/dist is involved — this gate reads source and the installed lucide directly, so there is no rebuild leg to state. (2) RED AFTER THE FIX, three ablations, each with on-disk confirmation and trap restore. A, depth 1: 'gate exit with retired edit in fixture = 1' / 'FAIL  lucide icon names' / '- examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json $.items[0].icon  [dropdown-menu (untyped child item)]' / '\"edit\" -> `Edit` is not a key of the runtime `icons` record. ... write `square-pen`' / 'Resolved through: packages/components/src/renderers/action/resolve-icon.ts (via renderers/overlay/dropdown-menu.tsx)'. B, NESTED submenu depth 2 (the case option 2 cannot express): 'gate exit with nested retired name = 1' / '- ...with-icons.json $.items[0].children[0].icon  [dropdown-menu (untyped child item)]' / '\"more-horizontal\" -> `MoreHorizontal` ... write `ellipsis`'. C, control — retired 'edit' on a type:'separator' item: exit 0, descent correctly stops at a typed child. All three restored; final 'git diff --exit-code' => CLEAN. (3) GREEN with the fixture back to a live name: 'OK  lucide icon names: 67 authored/declared names reaching 8 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).' (4) CENSUS UNCHANGED: --report before => 'record-reading resolvers discovered (8)' + 'authored icon names judged: 30 | ... declines to judge: 350' + 'anchored map entries judged: 34'; after => identical 8-entry resolver list + 'authored icon names judged: 33 (3 of them on UNTYPED child items of a declared container) | ... declines to judge: 347' + 'anchored map entries judged: 34'. +3 judged, -3 declined, census 8 -> 8. (5) `pnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts --reporter=verbose` => 'Test Files  1 passed (1)' / 'Tests  34 passed (34)', with all 9 NEW tests collected BY NAME (6 descent tests + 'really judges the untyped child items objectui#5992 opened up' + 'did NOT grow part 1 in the process' + 'no longer carries the parenthetical objectui#5930 falsified'). (6) `pnpm run type-check:scripts` => EXIT=0, pnpm echoed 'tsc -p tsconfig.scripts.json' as the positive control (it went red first on a real Record<string,any> mismatch; fixed by a JSDoc typedef on analyze, not by a cast). (7) `pnpm run lint:root` UNNARROWED => EXIT=0, '188 problems' no — 'x 28 problems (0 errors, 28 warnings)', and ZERO warnings in either changed file; the first run had 34 (6 mine) and I cleared all 6, so 28 is the pre-change baseline. (8) `pnpm run check:control-bytes` => EXIT=0, 'check-control-bytes: OK (scanned 5171 tracked text file(s); skipped 85 binary)'; plus a manual grep -naP over both changed files: no match. (9) `pnpm run check:icon-record-names` (the wired script name) => EXIT=0. (10) `pnpm exec vitest run scripts/__tests__ --maxWorkers=2` => 'Test Files  77 passed (77)' / 'Tests  2216 passed (2216)'. ONE DECLARED NARROWING: the unnarrowed root `vitest run` was attempted first under the shared verify lock and held it 663s producing ZERO test-file results — output was 'RUN v4.1.10' then nothing but repeated 'Error: connect ECONNREFUSED 127.0.0.1:3000', an environment precondition of that suite (a dev server nobody started) untouched by this diff — while two other agents queued behind it; I stopped only my own PIDs (4432/4420/4408/4407/4372, each verified as mine by ps before signalling) and released the lock. Replacement is a three-part measurement, not a guess: (a) population read from vitest's OWN config, `pnpm exec vitest list --filesOnly` => 1995 test files; (b) every one of those 1995 grepped for an import of the changed module => exactly 1, scripts/__tests__/check-lucide-icon-record-names.test.ts, run green above — the four other repo references are prose comments, not imports; (c) invariance: the diff anchored to the branch point ef2a3bd8d (NOT two-dot origin/main..HEAD, which by then attributed four siblings' landed PRs to this branch — caught and corrected) changes exactly 2 files, both under scripts/, which is not a workspace package and is in no package's tsconfig or src, so nothing in packages/apps/examples changed and no untouched test's verdict can move. On top of the single observer I ran all 77 files of scripts/__tests__, where the pins governing this file live. CI runs the full farm regardless. CHANGESET: none, deliberately — scripts/ is not published source; check-changeset-presence.mjs exits 0 either way and did not decide it; the previous change to this very file (94bc6e5a4) carried none, nor does the commit this branch is cut from. objectui has no skip-changeset label and none was created or applied.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6278 (sub-issue of the already-queued #5931, which owns this class): `ui:context-menu` never reads an item's authored `icon` — line-for-line the shape dropdown-menu.tsx had before #5930, same submenu recursion, 4 authored names in basic-context-menu.json rendering nothing. Unlike #5931's other three containers the fix shape is not an open question: #5930 already decided it (RECORD surface via resolveIcon, LazyIcon ruled out, both arms not just the leaf). Searched open issues first; #5931 already existed and this is attached to it rather than filed loose. Note recorded there: the gate change in this PR correctly stays SILENT on context-menu by construction until its renderer reads the key, and the census entry should land in the same PR as that repair.",
        "NOT filed, recorded here as measured context for whoever picks up #5931: button-group (8 names), breadcrumb (3) and command (9) never read `icon` — already #5931's body. timeline (4) renders `icon` as raw text and the authored names are emoji, so it is arguably correct as-is. tree-view (30) reads `icon` only as a two-valued literal switch (node.icon === 'folder'), never a name lookup. None is a new defect; all are in the re-measured header table in the gate and in the PR body."
      ]
    }

    Draft PR: #6277 (draft, auto-merge NOT armed). CI status at report time: in_progress — per the dispatch contract the report lands at draft-PR time and CI convergence is the PM's read, not mine.

    Fold-or-serial answer for #6009 stands as declared at claim: SERIAL. This PR changes part 2's traversal rule and adds no census entry; #6009 adds a RECORD_READING_TYPES entry for the 'icon' type and necessarily lands a retired-spelling fixture repair with it. Whichever lands second rebases over a few lines in the same file — they conflict textually, not semantically. Worth noting for that card: the descendants/min mechanism this PR adds is available to it, but ui:icon is a typed node, so #6009 needs only the plain paths: ['icon'] entry it already proposes.

    Generated by Claude Code


    Generated by Claude Code

  7. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Contributor

    os-dev-report

    Supersedes the previous comment — read-back showed GitHub's sanitizer had eaten its leading HTML-comment marker (the body now begins with a blank line), which makes it invisible to a marker scan. Same report, marker as literal text. Two sanitizer casualties in that copy are also repaired here: a Record generic lost its angle-bracketed parameters, and one lint line carried a garbled fragment.

    {
      "issue": 5992,
      "status": "done",
      "branch": "claude/issue-5992-lucide-gate-untyped-child-icons",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6277",
      "premise_still_valid": true,
      "summary": "Re-verified the premise on current origin/main (ef2a3bd8d) BEFORE changing anything: restored the retired spelling `edit` into examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json under a restoring trap, confirmed on disk (grep -c of 'edit' = 1, of 'square-pen' = 0, diff --stat 1 insertion / 1 deletion), and the gate still exited 0 with the same 64 names. The gap is live. Part 2 of check-lucide-icon-record-names.mjs now judges an `icon` on an UNTYPED node against its NEAREST TYPED ANCESTOR, but only when that ancestor's census entry declares `descendants: true` — OPTION 1, decided in the code. Measured reason: a pure nearest-typed-ancestor rule with no other change judges ZERO additional names, because `dropdown-menu` is not in RECORD_READING_TYPES at all; both options require declaring the container, so they differ only in what is judged under it. Option 2's `items[].icon` is measurably insufficient — renderMenuItems recurses into item.children and resolves the submenu trigger through the SAME resolveIcon call, while the paths grammar is single-level by construction, so a key list closes the leaf and leaves the submenu open (the narrower version of the same bug #5930 refused to ship). Descent is opt-in per container rather than blanket, so a judgement measured against one shape is not silently extended to descendant shapes nobody read off a renderer. A typed child ENDS descent (a type 'separator' item draws no icon and stays declined), and a descent reaching nothing is an ERROR via `min`, mirroring the ANCHORED_MAPS precondition. Part 1's census is untouched at 8. The stale header parenthetical was re-measured rather than reworded: 61 untyped icon names across SEVEN containers, not eight across four — the old count was low by 53 and did not know context-menu, timeline or tree-view existed.",
      "tests": "ALL RUNS ON BRANCH HEAD e78f7c811; exit codes captured BY REDIRECT before any pipe; every verdict quoted from the gate's own printed line, never a bare status. (1) PREMISE RE-VERIFICATION on origin/main ef2a3bd8d, before any edit — fixture mutated on disk under a trap on EXIT INT TERM, mutation confirmed on disk by grep -c on BOTH the injected and the removed text ('edit' = 1, 'square-pen' = 0) plus git diff --stat: 'gate exit with retired edit in fixture = 0' / 'OK  lucide icon names: 64 authored/declared names reaching 8 record-reading resolvers are live `icons` keys'. Fixture restored; git diff --exit-code => CLEAN (byte-identical to HEAD). No build/dist leg exists to state: this gate reads source and the installed lucide directly, so there is nothing for a stale dist to falsify. (2) RED AFTER THE FIX — three ablations, each with on-disk confirmation and trap restore. A, depth 1: 'gate exit with retired edit in fixture = 1' / 'FAIL  lucide icon names' / '- examples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.json $.items[0].icon  [dropdown-menu (untyped child item)]' / 'edit -> Edit is not a key of the runtime icons record ... write square-pen' / 'Resolved through: packages/components/src/renderers/action/resolve-icon.ts (via renderers/overlay/dropdown-menu.tsx)'. B, NESTED submenu at depth 2, the case option 2 cannot express: 'gate exit with nested retired name = 1' / '- ...with-icons.json $.items[0].children[0].icon  [dropdown-menu (untyped child item)]' / 'more-horizontal -> MoreHorizontal ... write ellipsis'. C, control — the same retired name on a type 'separator' item: exit 0, descent correctly stops at a typed child. All three restored; final git diff --exit-code => CLEAN. (3) GREEN with the fixture back to a live name: 'OK  lucide icon names: 67 authored/declared names reaching 8 record-reading resolvers are live `icons` keys (record 1767 keys; dynamic surface 4 sites, 2025 names, not judged here).' (4) CENSUS UNCHANGED: --report before => 'record-reading resolvers discovered (8)' + 'authored icon names judged: 30 | icon names on nodes this gate declines to judge: 350' + 'anchored map entries judged: 34'; after => an identical 8-entry resolver list + 'authored icon names judged: 33 (3 of them on UNTYPED child items of a declared container) | icon names on nodes this gate declines to judge: 347' + 'anchored map entries judged: 34'. +3 judged, -3 declined, census 8 to 8. (5) pnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts --reporter=verbose => 'Test Files  1 passed (1)' / 'Tests  34 passed (34)', all 9 NEW tests collected BY NAME (6 descent tests + 'really judges the untyped child items objectui#5992 opened up' + 'did NOT grow part 1 in the process' + 'no longer carries the parenthetical objectui#5930 falsified'). (6) pnpm run type-check:scripts => EXIT=0, with pnpm echoing 'tsc -p tsconfig.scripts.json' as the positive control. It went RED first, on a real TS2740 where the fixture census table did not satisfy the literal type inferred from the default — repaired with a JSDoc typedef on analyze that states the seam's real contract, not with a cast. (7) pnpm run lint:root UNNARROWED => EXIT=0, '28 problems (0 errors, 28 warnings)', ZERO of them in either changed file. The first run reported 34, six of which were mine; all six were cleared, so 28 is the pre-change baseline. (8) pnpm run check:control-bytes => EXIT=0, 'check-control-bytes: OK (scanned 5171 tracked text file(s); skipped 85 binary)'; plus a manual grep -naP for control bytes over both changed files: no match. (9) pnpm run check:icon-record-names, the wired script name => EXIT=0. (10) pnpm exec vitest run scripts/__tests__ --maxWorkers=2 => 'Test Files  77 passed (77)' / 'Tests  2216 passed (2216)'. ONE DECLARED NARROWING: the unnarrowed root vitest run was attempted FIRST, under the shared verify lock, and held it 663s producing ZERO test-file results — its output was 'RUN v4.1.10' followed by nothing but repeated 'Error: connect ECONNREFUSED 127.0.0.1:3000', an environment precondition of that suite (a dev server nobody started) that no part of this diff touches — while two other agents queued behind it. I stopped only my own PIDs (4432, 4420, 4408, 4407, 4372, each verified as mine with ps before signalling; never by process name) and released the lock. The replacement is a three-part measurement, not a guess: (a) population read from vitest's OWN configuration, pnpm exec vitest list --filesOnly => 1995 test files; (b) each of those 1995 grepped for an import of the changed module => exactly 1, scripts/__tests__/check-lucide-icon-record-names.test.ts, run green above — the four other repo references to the gate are prose comments, not imports; (c) invariance: the diff, anchored to the recorded branch point ef2a3bd8d and NOT to two-dot origin/main..HEAD (which by then attributed four siblings' landed PRs to this branch — caught and corrected mid-run), changes exactly 2 files, both under scripts/, which is not a workspace package and appears in no package's tsconfig or src, so nothing under packages/, apps/ or examples/ changed and no untouched test's verdict can move. On top of that single observer I ran all 77 files of scripts/__tests__, where the pins governing this file live (check-entry-guard, scripts-type-check, the type-check and lint coverage gates). CI runs the full farm regardless. CHANGESET: none, deliberately — scripts/ is not published source; check-changeset-presence.mjs exits 0 either way and did not decide it; what decided it is precedent, the previous change to this very file (94bc6e5a4, the lazy lookup maps) carried none, nor does the commit this branch is cut from. objectui has no skip-changeset label; none was created or applied.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6278, as a sub-issue of the already-queued #5931 which owns this class: `ui:context-menu` never reads an item's authored `icon` — line-for-line the shape dropdown-menu.tsx had before #5930, same submenu recursion, 4 authored names in basic-context-menu.json rendering nothing. Unlike #5931's other three containers the fix shape is NOT an open question: #5930 already decided it (RECORD surface via resolveIcon; LazyIcon ruled out because it degrades an unknown name to the Database glyph; both arms, not just the leaf). Searched open issues before filing; #5931 already existed, so this is attached to it rather than filed loose. Recorded there: the gate change in this PR correctly stays SILENT on context-menu by construction until its renderer reads the key, and the census entry should land in the same PR as that repair.",
        "NOT filed, recorded as measured context for whoever picks up #5931: button-group (8 names), breadcrumb (3) and command (9) never read `icon` — already that issue's body. timeline (4) renders `icon` as raw text and its authored names are emoji, so it is arguably correct as-is. tree-view (30) reads `icon` only as a two-valued literal switch, never a name lookup. None is a new defect; all appear in the re-measured header table in the gate and in the PR body."
      ]
    }

    Draft PR: #6277 — draft, auto-merge NOT armed. CI at report time: in_progress; per the dispatch contract the report lands at draft-PR time and CI convergence is the PM's read.

    Fold-or-serial for #6009 stands as declared at claim: SERIAL. This PR changes part 2's traversal rule and adds no census entry; #6009 adds a RECORD_READING_TYPES entry for the icon type and necessarily lands a retired-spelling fixture repair with it. Whichever lands second rebases over a few lines of the same file — they conflict textually, not semantically. Useful for that card: the descendants / min mechanism added here is available to it, but ui:icon is a typed node, so #6009 still needs only the plain paths: ['icon'] entry it already proposes.

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:queue

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions