From 1443fcbf082f570d90ea58bc5850f35ed158bcdb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 08:10:20 +0000 Subject: [PATCH] fix(tooling): census `ui:icon` in the lucide gate, repair the three retired spellings it finds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ui:icon` has read `schema.icon` straight out of lucide's runtime `icons` record since #5631 — `import { icons } from 'lucide-react'` plus `(icons as any)[key]` — which makes it exactly the resolver class `check-lucide-icon-record-names.mjs` exists for. Its `type` was absent from part 2's `RECORD_READING_TYPES`, so its 96 authored glyph names were SEEN and DECLINED while the gate stayed green: the failure shape this gate was built to end, one level up in its own census. Part 1 needed no change and got none. `icon.tsx` imports the record DIRECTLY, so `DECLARED_RECORD_READERS` has carried it since #5633's own discovery run — the asymmetry against `dropdown-menu.tsx`, which routes through `resolveIcon` and correctly stays out (#5992). Part 1 knowing a module and part 2 knowing which `type` sends names to it are two independent facts; this supplies the second. `discovered.record` stays at 8, pinned. No `descendants`: the renderer resolves one name and returns a single element, never walking `children`, so there is no untyped-child population for a descent to reach — and a descent that reaches nothing is an ERROR under the non-vacuity rule. The census entry alone turns the gate RED on three nodes, which is the non-vacuity proof and the reason both halves land together. The repair is `circle-check-big`, DERIVED by object identity rather than guessed: lucide's `CheckCircle` export IS `CircleCheckBig`, so that spelling keeps the same glyph. `circle-check` is a different glyph and would have changed the artwork silently. --- .../src/schemas/marketing/call-to-action.json | 6 +- .../check-lucide-icon-record-names.test.ts | 76 +++++++++++++++++++ scripts/check-lucide-icon-record-names.mjs | 34 +++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/examples/schema-catalog/src/schemas/marketing/call-to-action.json b/examples/schema-catalog/src/schemas/marketing/call-to-action.json index 627425e333..78666b7b3b 100644 --- a/examples/schema-catalog/src/schemas/marketing/call-to-action.json +++ b/examples/schema-catalog/src/schemas/marketing/call-to-action.json @@ -51,7 +51,7 @@ "children": [ { "type": "icon", - "icon": "check-circle", + "icon": "circle-check-big", "className": "h-4 w-4" }, { @@ -67,7 +67,7 @@ "children": [ { "type": "icon", - "icon": "check-circle", + "icon": "circle-check-big", "className": "h-4 w-4" }, { @@ -83,7 +83,7 @@ "children": [ { "type": "icon", - "icon": "check-circle", + "icon": "circle-check-big", "className": "h-4 w-4" }, { diff --git a/scripts/__tests__/check-lucide-icon-record-names.test.ts b/scripts/__tests__/check-lucide-icon-record-names.test.ts index 57d5619284..d046927279 100644 --- a/scripts/__tests__/check-lucide-icon-record-names.test.ts +++ b/scripts/__tests__/check-lucide-icon-record-names.test.ts @@ -242,6 +242,63 @@ describe('an authored icon name reaching a record-reading resolver', () => { }); }); +/** + * `ui:icon` — the node type whose whole job is naming a glyph, and the one that + * could not be censused until objectui#5631 moved its key from `name` (the SDUI + * IDENTITY key) to `icon`. Until objectui#6009 added its census entry, its + * authored names were SEEN and DECLINED: 96 of them, three carrying a retired + * spelling that rendered the placeholder. + * + * Pinned separately from `button` above because the two answer part 1 + * differently, and that difference is the reasoning the entry rests on: + * `icon.tsx` imports the `icons` record DIRECTLY, so it is a part-1 resolver in + * its own right, where `dropdown-menu.tsx` routes through `resolveIcon` and + * correctly stays out (objectui#5992). + */ +describe('the `ui:icon` node type', () => { + const iconNode = (icon: string): string => JSON.stringify({ type: 'icon', icon }, null, 2); + + it('goes RED on a retired spelling, naming `icon.tsx` as the resolver', () => { + const result = judge('ui-icon-red', { + files: { 'examples/catalog/cta.json': iconNode('check-circle') }, + }); + + expect(result.errors).toEqual([]); + expect(result.violations).toHaveLength(1); + const [violation] = result.violations; + expect(violation.site).toBe('icon'); + expect(violation.resolver).toBe('packages/components/src/renderers/basic/icon.tsx'); + expect(violation.where).toBe('examples/catalog/cta.json $.icon'); + // Derived by object identity, not read off a list: lucide's `CheckCircle` + // export IS `CircleCheckBig`, so `circle-check-big` is the spelling that + // keeps the SAME glyph. `circle-check` is a different one. + expect(violation.detail).toContain('write `circle-check-big`'); + }); + + it('goes GREEN on the live spelling — and the name was really judged', () => { + const result = judge('ui-icon-green', { + files: { 'examples/catalog/cta.json': iconNode('circle-check-big') }, + }); + + expect(result.violations).toEqual([]); + expect(result.errors).toEqual([]); + expect(result.counters.authoredJudged).toBe(1); + }); + + it('declares NO descent — the renderer resolves one name and never walks children', () => { + // A `descendants: true` here would carry a `min` that ERRORS when it + // reaches nothing, and there is nothing for it to reach: `icon.tsx` + // returns a single element. + expect(RECORD_READING_TYPES.icon.paths).toEqual(['icon']); + // `'descendants' in …`, not `.descendants` — the same idiom the descent + // test below uses. `RECORD_READING_TYPES` is a plain object literal, so the + // key is absent from `icon`'s INFERRED TYPE, and reading it does not + // type-check. Asserting absence of the key is also the stronger fact. + expect('descendants' in RECORD_READING_TYPES.icon).toBe(false); + expect(RECORD_READING_TYPES.icon.resolver).toBe('packages/components/src/renderers/basic/icon.tsx'); + }); +}); + // ── 3. it is not a blanket string scan ─────────────────────────────────────── describe('a name whose resolver this gate cannot identify is declined, not flagged', () => { @@ -541,6 +598,25 @@ describe('this repository', () => { expect(RECORD_READING_TYPES['dropdown-menu'].resolver).toContain('renderers/action/resolve-icon.ts'); }); + it('really judges the `ui:icon` nodes objectui#6009 opened up', () => { + // The card's whole subject is a population that was SEEN and DECLINED while + // the gate stayed green, so "no violations" is not evidence on its own. + // Measured on this tree, the census entry moved 96 names from `declined` to + // `judged`; a floor well under that fails loudly if the walk stops reaching + // the corpus, without pinning a figure the catalog is free to move. + expect(Object.keys(RECORD_READING_TYPES)).toContain('icon'); + expect(repoResult.counters.authoredJudged).toBeGreaterThan(100); + }); + + it('did NOT grow part 1 for `ui:icon` either — it was ALREADY a declared resolver', () => { + // The asymmetry against dropdown-menu, pinned: `icon.tsx` imports the + // `icons` record directly and has been in part 1's census since + // objectui#5633's own discovery run. objectui#6009 supplies only the part-2 + // fact — which `type` sends names there — so this count must not move. + expect(repoResult.discovered.record).toContain('packages/components/src/renderers/basic/icon.tsx'); + expect(repoResult.discovered.record).toHaveLength(8); + }); + it('carries more record-reading resolvers than objectui#5633 catalogued by hand', () => { // The card's table listed four. Discovery found eight, which is the whole // argument for measuring the population instead of maintaining a list: the diff --git a/scripts/check-lucide-icon-record-names.mjs b/scripts/check-lucide-icon-record-names.mjs index 43b7afcedf..53a9bf451b 100644 --- a/scripts/check-lucide-icon-record-names.mjs +++ b/scripts/check-lucide-icon-record-names.mjs @@ -97,6 +97,25 @@ * `renderers/action/resolve-icon.ts`, which is already declared. Part 1 * stays at eight resolvers; this is a part-2 rule, not a census change. * + * ── Re-taken at objectui@e3784607f, and UNCHANGED ────────────────────── + * objectui#6009 censused the `icon` TYPE, which looks like it should move + * this table and does not: the table counts UNTYPED names, and a + * `type: 'icon'` node is typed, so the two populations are disjoint. The + * per-container figures above re-measure identically. Recorded rather than + * left implicit — "the numbers did not move" is a measurement, and the next + * reader should not have to re-derive that it was taken. + * + * ⚠️ SCOPE, which the count above does not carry on its face: it is + * measured over `examples/schema-catalog/`, while this gate SCANS + * `packages/`, `apps/` and `examples/`. Over the full scan roots the same + * walk finds 76 untyped names across NINE containers — the extra 15 all in + * `packages/types/examples/` (`tree-view` +6, `timeline` +3, plus `list` 3 + * and `sidebar` 3, two containers this table does not name at all). Both + * extra containers were read: `data-display/list.tsx` and + * `navigation/sidebar.tsx` never read `icon`, so those names reach no + * resolver and decline correctly. The table is right about what it judges; + * it is simply not the whole unjudged census. + * * 3. ANCHORED MAPS — the first-party const maps that feed a record-reading * resolver but are not authored nodes. This is the population the retired * local pins covered, generalised: each anchor carries a minimum entry @@ -197,6 +216,21 @@ export const RECORD_READING_TYPES = { min: 1, resolver: 'packages/components/src/renderers/action/resolve-icon.ts (via renderers/overlay/dropdown-menu.tsx)', }, + // `ui:icon` — the node type whose WHOLE job is naming a glyph. It could not + // have been censused before objectui#5631: that renderer named its glyph with + // `name`, the SDUI IDENTITY key, and every path in this table is an `icon` + // path. Post-#5631 it reads `schema.icon` straight out of lucide's runtime + // record (`import { icons } from 'lucide-react'` + `(icons as any)[key]`), so + // it is a DIRECT record reader, not a `resolveIcon` router — which is why + // part 1's `DECLARED_RECORD_READERS` already carries it and dropdown-menu's + // does not. Part 1 knowing a module and part 2 knowing its `type` are two + // independent facts; this entry supplies the second (objectui#6009). + // + // NO `descendants`: the renderer resolves exactly one name and returns a + // single element. It never walks `children`, so there is no untyped-child + // population for a descent to reach — and a descent that reaches nothing is + // an ERROR here, by the non-vacuity rule below. + 'icon': { paths: ['icon'], resolver: 'packages/components/src/renderers/basic/icon.tsx' }, 'view-switcher': { paths: ['views[].icon', 'viewActions[].icon'], resolver: 'packages/plugin-view/src/ViewSwitcher.tsx' }, };