Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,7 +51,7 @@
"children": [
{
"type": "icon",
"icon": "check-circle",
"icon": "circle-check-big",
"className": "h-4 w-4"
},
{
Expand All@@ -67,7 +67,7 @@
"children": [
{
"type": "icon",
"icon": "check-circle",
"icon": "circle-check-big",
"className": "h-4 w-4"
},
{
Expand All@@ -83,7 +83,7 @@
"children": [
{
"type": "icon",
"icon": "check-circle",
"icon": "circle-check-big",
"className": "h-4 w-4"
},
{
Expand Down
76 changes: 76 additions & 0 deletions scripts/__tests__/check-lucide-icon-record-names.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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', () => {
Expand DownExpand Up@@ -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
Expand Down
34 changes: 34 additions & 0 deletions scripts/check-lucide-icon-record-names.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -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' },
};

Expand Down
Loading