Uh oh!
There was an error while loading. Please reload this page.
fix(tooling): judge lucide icon names on untyped child items of a declared container - #6277
Conversation
…lared container `check-lucide-icon-record-names.mjs` part 2 judged an `icon` only on a node whose OWN `type` was a censused record-reading renderer. `ui:dropdown-menu` menu items are child objects with no `type` key, so they were never judged. That was harmless until the dropdown-menu icon routing landed: since then a retired spelling in the published `with-icons.json` fixture renders no glyph and the gate stays green. That fixture is a declared AI few-shot retrieval source, so the failure mode is teaching a dead name. An `icon` on an untyped node is now judged against its NEAREST TYPED ANCESTOR, and only when that ancestor's census entry declares `descendants: true` — read off the renderer the way every `paths` entry was. A typed node ends any descent it sits inside, so a `type: 'separator'` item (which the renderer returns early for, drawing no icon) stays declined. `min` makes a descent that reaches nothing an ERROR rather than a silent green, the precondition ANCHORED_MAPS already states. Descent rather than a per-container key list because `renderMenuItems` recurses into `item.children` and resolves the submenu trigger through the same call: the `paths` grammar is single-level by construction, so a key list closes the leaf and leaves the submenu open. Part 1's census is untouched — `dropdown-menu.tsx` imports `resolveIcon`, not `icons`, so it correctly stays out of DECLARED_RECORD_READERS at eight. Also re-measures the header parenthetical that the icon routing falsified: 61 untyped `icon` names across seven containers, not eight across four, with each container's renderer read to say which of them reaches a record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
yinlianghui-tw
commented
Aug 25, 2026
PM review — ACCEPT. ⭐ The nested-submenu ablation is the proof that decided the design, and the header re-measurement moved the card's own numbers by 7×.Reviewed by the The option-1-vs-2 decision — resolved by measurement, not preferenceThe dispatch leaned option 1 on generality grounds. What you found is sharper: a pure nearest-typed-ancestor rule judges zero additional names —
⭐ The premise re-verified before any edit — on current main, not the card's branchThe card's proof was measured on the #5930 branch; you reran it on The header parenthetical — re-measured, and the old sentence was off by 53
This is why the dispatch said re-measure rather than reword. The stale sentence was not slightly off; it described a different corpus. The census stayed 8 → 8 with the movement fully accounted: +3 judged, −3 declined, anchored maps untouched at 34. Evidence quality — three habits worth naming
#6278 — filed correctly as a sub-issue of #5931
Changeset — none, correct
⛔ Not armed yet
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5992
The gap
scripts/check-lucide-icon-record-names.mjspart 2 judged aniconname only on a node whose owntypewas a censused record-reading renderer.ui:dropdown-menumenu items are child objects carrying notypekey at all, so they were never judged — the gate's own header stated that boundary explicitly.That was harmless until #5930 routed those icons through
resolveIcon. Since then a retired lucide spelling inexamples/schema-catalog/src/schemas/components-overlay-dropdown-menu/with-icons.jsonrenders no glyph and the gate stays green. That fixture is a declared AI few-shot retrieval source, so the failure mode is teaching a dead name, not merely a missing icon.Premise re-verified on current
origin/main(ef2a3bd8d) before changing anythingThe card's proof was measured on the #5930 branch. Re-run here on
origin/main, mutating the fixture on disk under a restoring trap:Exit 0. The gap is live; the premise holds. Fixture restored,
git diff --exit-codeclean.The option chosen, and why
Both listed options require the same first move, which the card's framing does not make obvious. I measured it before writing anything: a pure nearest-typed-ancestor rule, applied with no other change, judges zero additional names —
dropdown-menuis not inRECORD_READING_TYPESat all, so an untyped item has no censused ancestor to answer to. The container type must be declared either way. The options therefore differ only in what gets judged under the declared container.Option 1 (nearest typed ancestor), and the reason is mechanical, not stylistic.
renderMenuItemsrecurses:Both the leaf arm and the submenu-trigger arm read the same
resolveIcon(item.icon), so a name at any depth reaches the record. Thepathsgrammar is^(\w+)\[\]\.icon$— one level, by construction. Option 2'sitems[].iconwould have closed the leaf and left the submenu open, which is the narrower version of the same bug #5930 explicitly refused to ship. Proven with a nested ablation, below.But descent is opt-in per container, not blanket. Applying "nearest censused ancestor" to every entry would extend a judgement measured against one shape (
action:bar'sactions[]) to descendant shapes nobody read off a renderer —data-table'scolumns[], say — which is exactly the guessing that table's header exists to refuse. So a censused entry declaresdescendants: true, a fact read off the renderer the same way everypathsentry was. That is one boolean per container, not a key registry that drifts as schema shapes change.Two properties fall out and are pinned:
typestill answers for it. Atype: 'separator'item is returned early by the renderer and draws no icon; descent running through it would invent a violation no resolver would ever produce.min), not zero violations — the preconditionANCHORED_MAPSalready states, applied to the population this card added.Acceptance evidence — the RED is the deliverable
All three runs mutate the fixture on disk under a restoring
trap, with the mutation confirmed bygrep -con both the injected and the removed text; the tree is verified byte-identical afterwards.A — the card's probe, retired
editatitems[0](depth 1):B — nested submenu, retired
more-horizontalatitems[0].children[0](depth 2) — the case a single-levelitems[].iconpath cannot express:C — control, retired
editon atype: 'separator'item (descent must stop at a typed child):Green once the fixture is back to a live name, and the fixture is byte-identical to
HEAD:Part 1's census is UNCHANGED at 8
--report, before → after:+3 judged, −3 declined, census untouched. #5930 routes through
resolveIconrather than importingicons, sorenderers/overlay/dropdown-menu.tsxcorrectly stays out ofDECLARED_RECORD_READERS; the entry'sresolverfield namesrenderers/action/resolve-icon.ts, which is already declared. Pinned by a new test that assertsdiscovered.recordhas length 8 and does not containdropdown-menu.tsx.The stale header sentence — re-measured, not reworded
The header carried, as a measurement:
Both halves are wrong. Re-measured over the schema catalog at
ef2a3bd8d, by reading each renderer rather than adjusting the wording: 61 untypediconnames across seven containers, and exactly one reaches a record-reading resolver.icondropdown-menuresolveIcon(item.icon)(#5930). Judged here, recursively.button-groupbasic/button-group.tsxnever readsbutton.iconat all — the names render nothingbreadcrumbiconcommandiconcontext-menuicon— dropdown-menu's twin, not routed by #5930timeline<span>{item.icon}</span>; the four authored names are emojitree-viewnode.icon === 'folder') — never a record lookupThe old count was low by 53 and did not know
context-menu,timelineortree-viewexisted. The replacement is anchored toobjectui@ef2a3bd8din the file, and a test pins that the falsified sentence is gone and that all seven containers are named.Unrelated but worth flagging for a reviewer:
scripts/check-entry-guard.mjssays this suite "fails 5 of 25". That is a recorded measurement explicitly anchored to #6092's branch, not a live count — it is deliberately left alone rather than updated to 34, since rewriting a recorded measurement falsifies it.Verification
Exit codes captured by redirect before any pipe; each verdict is the gate's own printed line.
node scripts/check-lucide-icon-record-names.mjs(clean tree)EXIT=0·OK lucide icon names: 67 authored/declared names …1,1,0— quoted abovepnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts --reporter=verboseTest Files 1 passed (1)·Tests 34 passed (34); all 9 new tests collected by namepnpm run type-check:scripts(tsc -p tsconfig.scripts.json— script name echoed as a positive control)EXIT=0pnpm run lint:root— unnarrowed, the full root scriptEXIT=0·✖ 28 problems (0 errors, 28 warnings); zero warnings in the two changed files, and 28 is the pre-change baselinepnpm run check:control-bytesEXIT=0·✅ check-control-bytes: OK (scanned 5171 tracked text file(s); skipped 85 binary).grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over both changed filespnpm run check:icon-record-names(the wired script name, as a positive control)EXIT=0· sameOKlinepnpm exec vitest run scripts/__tests__ --maxWorkers=2Test Files 77 passed (77)·Tests 2216 passed (2216)pnpm exec vitest run --maxWorkers=2All of the above ran on
e78f7c811, the branch head.The one narrowing, declared with its measurement
The unnarrowed root suite was attempted first, under the shared verify lock. It held the lock for 663s and produced zero test-file results — its output was
RUN v4.1.10followed by nothing butError: connect ECONNREFUSED 127.0.0.1:3000, an environment precondition of that suite (a dev server nobody started) which no part of this diff touches — while two other agents queued behind it on the same lock. I stopped my own process tree by PID and released it.The replacement is a measurement, not a guess, and it is three parts:
pnpm exec vitest list --filesOnly→ 1995 test files in the root suite.scripts/__tests__/check-lucide-icon-record-names.test.ts, which is run green above (34/34, verbose, each new test named). The four other repo references to the gate are prose comments, not imports.ef2a3bd8d(not two-dotorigin/main..HEAD, which by now attributes four siblings' landed PRs to this branch), changes two files, both underscripts/.scripts/is not a workspace package and is in no package'stsconfigorsrc, so no package test can import it; nothing inpackages/,apps/orexamples/changed, and no untouched test's verdict can move.On top of that single observer I ran the whole of
scripts/__tests__— 77 files, 2216 tests — which is where the pins that govern this file live (check-entry-guard,scripts-type-check, the type-check- and lint-coverage gates). CI runs the full farm regardless.New tests, all collected by name under
--reporter=verbose:No changeset, deliberately
The diff touches
scripts/only —scripts/check-lucide-icon-record-names.mjsand its pin test.scripts/is not a workspace package and publishes nothing, socheck-changeset-presence.mjs(which guards published package source) exits 0 either way and is not what decided this. What decided it is the precedent: the previous change to this very file,94bc6e5a4 refactor(tooling): build the lucide gate's two lookup maps lazily, carried no changeset, as does the commit this branch is cut from. The fixture underexamples/was mutated only inside the ablations and is byte-identical toHEADin the diff.Generated by Claude Code