From af49342656f5fb8c8409d19214f2a18124a86001 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:33:58 +0000 Subject: [PATCH 1/3] test(components,scripts): retire the ui:icon pin's membership half, record the third zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectui#5936 asked for the consumer of a component registration's `icon` meta before extending the icon gate to it. Three first-party populations were measured and none has one (objectui, objectstack, cloud), so the gate is not extended — adjudicated 2026-09-04. Records that where the next reader hits it (the gate's own "Deliberate boundaries"), with the three readings, their controls, the adjudication, and the caveat that three zeros are not a proof of absence. Retires the membership half of the `ui:icon` local pin, which was kept at objectui#5633 on the premise that the palette lives outside this repo. It guarded 1 of the 45 registrations declaring an `icon` meta while the gate is adjudicated not to cover the other 44, and its hand-copied tokeniser had already gone stale against the seam objectui#5935 / PR #7491 created. The coupling half stays: it needs no external vocabulary and cannot drift. The published `ComponentMeta.icon` key is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .../icon-renderer-declared-default.test.ts | 139 ++++++++---------- scripts/check-lucide-icon-record-names.mjs | 42 ++++++ 2 files changed, 101 insertions(+), 80 deletions(-) diff --git a/packages/components/src/__tests__/icon-renderer-declared-default.test.ts b/packages/components/src/__tests__/icon-renderer-declared-default.test.ts index ef727907ea..30a264c55b 100644 --- a/packages/components/src/__tests__/icon-renderer-declared-default.test.ts +++ b/packages/components/src/__tests__/icon-renderer-declared-default.test.ts @@ -8,63 +8,74 @@ import { describe, it, expect } from 'vitest'; import { ComponentRegistry } from '@object-ui/core'; -import { icons } from 'lucide-react'; import '../renderers/basic/icon'; // --------------------------------------------------------------------------- -// objectui#5622 — the `icon` renderer's OWN declared default has to resolve. +// objectui#5622 — the `ui:icon` renderer's two declared spellings must agree. // // Two metadata spots feed the designer: the registration's `icon` meta (the // glyph on the palette entry) and the glyph INPUT's `defaultValue` (what an -// `icon` dropped from that palette renders before anyone types a name). Both -// are looked up in lucide's runtime `icons` record by `IconRenderer`, which -// `console.warn`s and draws a visible placeholder on a miss. +// `icon` dropped from that palette renders before anyone types a name). The +// defect objectui#5622 repaired was ONE name in TWO places, so the repair is +// only correct if they stay one name — split them and the palette advertises a +// glyph the dropped component does not render. // // ⚠️ That input is keyed `icon` since objectui#5631 — it was `name` until the -// glyph key migrated off the SDUI identity key. Only the SELECTOR below moved; -// this file's subject is still objectui#5622's retired-spelling question. -// -// lucide retires a spelling by dropping it from that record while keeping it as -// a deprecated named export, and `smile` was retired — so the palette entry's -// glyph was blank AND the component started at a default that rendered nothing. -// The two spots must hold together: repairing one alone leaves the other broken. -// -// ⚠️ MEMBERSHIP, not resolvability. `Smile === FaceSlightlySmiling` is TRUE on -// the installed lucide — the retired spelling names the very same glyph object, -// so an assertion that reaches for the export, or renders it and looks, passes -// on the broken name. Absence from the record is the only difference. +// glyph key migrated off the SDUI identity key. // // Read off the REGISTRY rather than out of source: the registry entry is the // artifact the designer palette actually consumes. // -// ⚠️ NOT retired by objectui#5633's repo-level gate -// (`scripts/check-lucide-icon-record-names.mjs`), deliberately. That gate judges -// names reaching a resolver it can SEE reading the record, and this repository -// contains no first-party consumer of a registration's `icon` meta at all — -// measured: `getMeta(...).icon` is read nowhere under `packages/**` or `apps/**`. -// The palette that renders it lives outside this repo, so the claim this pin -// makes is one the gate has no measured basis to generalise. It also asserts -// something no membership check can: that the palette glyph and the dropped -// default stay the SAME name. +// ── The MEMBERSHIP half was retired at objectui#5936 ──────────────────────── +// This file used to also assert that both spellings are live keys of lucide's +// runtime `icons` record — the objectui#5622 mechanism, where a retired name +// keeps its named export while dropping out of the record, so it imports, +// type-checks and renders as a COMPONENT while resolving to `null` as a STRING. +// That half is gone, and the reason is a measurement rather than a preference. +// +// It was KEPT at objectui#5633 (which built the repo-level gate, +// `scripts/check-lucide-icon-record-names.mjs`) on one explicit premise: that +// the palette reading this meta lives OUTSIDE this repo, so the gate had no +// measured basis to generalise the claim but a local pin could still hold it. +// objectui#5936 was filed to find that palette. It was not found — in objectui, +// in objectstack, or in cloud. The gate's own header carries the three readings, +// their controls, and the 2026-09-04 adjudication that the gate is NOT extended +// to registration `icon` meta. +// +// Given that, the membership half asserted the liveness of a string with no +// measured consumer, and it cost more than it looked: +// +// - It guarded ONE of the 45 registrations that declare an `icon` meta, while +// the gate is adjudicated NOT to cover the other 44. A membership check +// over 1/45 of its own population does not manage that risk; it SIGNALS +// that the risk is managed — and that signal is what sent objectui#5936 +// looking for a consumer this pin implied must exist. +// - It carried a hand-copied `toPascalCase` + `iconNameMap`, annotated +// "Copied EXACTLY — a pin that normalised names differently from the +// consumer would answer a question nobody asks" and "module-private there". +// objectui#5935 / PR #7491 collapsed the eight resolvers into ONE seam that +// EXPORTS `describeIconLookup`/`resolveIcon` and tokenises on +// `/[-_\s]+/`. This copy still said `split('-')`. Both halves of its own +// stated correctness condition had gone stale inside one round, and nothing +// went red — which is what a pin over a surface with no consumer does. +// - Measured at the retirement: all 45 declared registration `icon` metas are +// live on BOTH lucide surfaces, so no red was dropped by removing it. +// +// ⛔ The published `ComponentMeta.icon` key itself (`packages/types/src/base.ts`) +// is UNTOUCHED — removing a published capability is a maintainer decision and +// was fenced out of objectui#5936. What retired here is a TEST claim about it. +// If an actual reader of this meta is ever found, in any repo, the answer is to +// extend the gate to the whole population — ⛔ not to restore a pin over one +// registration. +// +// ── What this file still asserts, and why that half survives the same argument +// The coupling claim below is NOT a claim about lucide's vocabulary. It needs no +// external record, no copied tokeniser and no consumer to be well-formed: it +// says two declarations in ONE registration agree with each other. It therefore +// cannot drift the way the membership half did, costs nothing to keep, and +// becomes correct the instant any consumer appears. // --------------------------------------------------------------------------- -/** - * The transform `IconRenderer` applies before its record lookup, copied because - * it is module-private there. Copied EXACTLY — a pin that normalised names - * differently from the consumer would answer a question nobody asks. - */ -function toPascalCase(str: string): string { - return str - .split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(''); -} -const iconNameMap: Record = { Home: 'House' }; -const recordKeyFor = (name: string): string => { - const pascal = toPascalCase(name); - return iconNameMap[pascal] ?? pascal; -}; - const meta = ComponentRegistry.getMeta('icon', 'ui'); const glyphInput = meta?.inputs?.find(input => input.name === 'icon'); @@ -74,11 +85,12 @@ const DECLARED_DEFAULTS: Array<[string, string | undefined]> = [ ['`icon` input `defaultValue` (what a dropped `icon` renders)', glyphInput?.defaultValue as string | undefined], ]; -describe('the `ui:icon` renderer\'s declared default is a live `icons` key (objectui#5622)', () => { +describe('the `ui:icon` renderer\'s declared spellings agree (objectui#5622)', () => { it('both declared spellings were actually found — the precondition', () => { - // A registry read that came back `undefined` would leave the assertion - // below vacuously green on `undefined !== a retired name`, which is the - // failure mode this shape invites. + // Load-bearing, not ceremony: the assertion below compares two values that + // are BOTH `undefined` if the registry read comes back empty, and + // `undefined === undefined` passes. Without this the coupling check goes + // vacuously green on a registration that no longer exists. expect(meta, '`ui:icon` is not registered — the import above no longer registers it.').toBeDefined(); expect( glyphInput, @@ -89,43 +101,10 @@ describe('the `ui:icon` renderer\'s declared default is a live `icons` key (obje } }); - it('names only live `icons` keys, on BOTH surfaces', () => { - const retired = DECLARED_DEFAULTS.filter( - ([, spelling]) => - typeof spelling === 'string' - && !Object.prototype.hasOwnProperty.call(icons, recordKeyFor(spelling)), - ); - - expect( - retired, - 'The `ui:icon` registration declares a default that is NOT a key of lucide\'s runtime `icons`\n' - + 'record — i.e. a deprecated alias. `IconRenderer` returns `null` and warns on that lookup,\n' - + 'so the palette glyph is blank and a freshly dropped `icon` renders nothing. The name still\n' - + 'imports and type-checks as a component, so nothing else goes red. Replace it with the\n' - + 'spelling the record carries — in BOTH spots (objectui#5622).', - ).toEqual([]); - }); - it('keeps the palette glyph and the dropped default the same name', () => { // The defect was one name in two places; the repair is only correct if they // stay one name. Split them and the palette advertises a glyph the dropped // component does not render. expect(meta?.icon).toBe(glyphInput?.defaultValue); }); - - it('rejects a name the record does not carry — the control', () => { - // Same record, same membership predicate, same `recordKeyFor` transform as - // the assertion above, so it fails on exactly what that one passes on. - expect( - Object.prototype.hasOwnProperty.call(icons, recordKeyFor('no-such-lucide-icon')), - ).toBe(false); - }); - - it('rejects the spelling that shipped here — the control that matters', () => { - // The control above would also pass against a predicate that merely asked - // "is this importable from lucide-react". `Smile` is: it imports, it - // type-checks, and it IS `FaceSlightlySmiling`. Membership is the only - // thing that separates them, and this is the exact spelling that shipped. - expect(Object.prototype.hasOwnProperty.call(icons, recordKeyFor('smile'))).toBe(false); - }); }); diff --git a/scripts/check-lucide-icon-record-names.mjs b/scripts/check-lucide-icon-record-names.mjs index e1600563f1..830c2a8bb2 100644 --- a/scripts/check-lucide-icon-record-names.mjs +++ b/scripts/check-lucide-icon-record-names.mjs @@ -178,6 +178,48 @@ * beside a string map that resolves the same glyphs and a dead spelling gets * copied across — the exact path by which `bar-chart-3` and `gantt-chart` * reached a string map (objectui#5586). + * - A component REGISTRATION's `icon` meta is not judged, and this boundary is + * now CLOSED rather than open (objectui#5936). It is the palette-entry glyph + * on `ComponentRegistry.register(..., { icon })` — 45 registrations declare + * one. It was left unjudged at objectui#5633 because nothing could be found + * reading it, and objectui#5936 was filed to locate that reader before the + * gate was extended to the population. THE READER WAS NOT FOUND, in any of + * the three first-party populations anyone has: + * + * objectui zero — filing seat, 2026-08-24; RE-DERIVED at objectui@24e027e93 + * over all five meta-bearing registry read paths, not + * just `getMeta(...).icon`. `getMeta()` has four + * first-party callers and they read `.labelling`, + * `.namespace`, `.isContainer`, `.inputs` and + * `.deprecated`; `getPublicConfigs()` has one, reading + * `{ type, isContainer }`; `getConfig()`, + * `getAllConfigs()` and `getNamespaceComponents()` + * have none. Controls fired on the same tree: 51 + * `getMeta(` occurrences, 4898 `-i icon` lines. + * objectstack zero — PM seat, origin/main @ daacc1071, 2026-08-24. + * cloud zero — `repo:cloud` seat, cloud@9b6abe0f2fd5, 2026-09-03 + * (objectstack#12931). Structural, not just grep-deep: + * cloud has NO component-registration surface at all + * and no `@object-ui/*` dependency in any of its 19 + * package.json files. Controls fired: `getMeta` without + * the paren 276, `-i icon` 412. + * + * ⇒ ADJUDICATED 2026-09-04: the gate is NOT extended to registration `icon` + * meta. Both branches objectui#5936 offered — a record-reading consumer, or a + * dynamic-surface one — are NOT OBSERVED, so there is nothing to tell this + * gate which vocabulary such a name would reach. Judging it anyway would be + * this file's own cardinal error: guessing a vocabulary. ⛔ Do not re-open on + * the strength of the local `ui:icon` pin — that pin was the reason to go + * looking, not evidence of what was found, and objectui#5936 retired its + * membership half for exactly this reason. + * + * ⚠️ Three zeros close the three KNOWN populations; they are NOT a proof of + * absence. A customer-authored app or an unmeasured repo could read the meta, + * and `WidgetRegistry` copies `manifest.icon` INTO this meta from external + * widget manifests — a first-party producer path whose consumer is expected + * to be a designer palette outside this tree. So: measured dead here, not + * proven dead anywhere. NEW EVIDENCE — an actual reader, in any repo — is + * what re-opens this, and it re-opens it as a real extension. * * Run: node scripts/check-lucide-icon-record-names.mjs * node scripts/check-lucide-icon-record-names.mjs --report From af7a3f910a173bf180f869e7d29992ca844a5cf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:36:35 +0000 Subject: [PATCH 2/3] test(scripts): repoint the gate's own pin at the objectui#5936 adjudication The gate suite asserted the `ui:icon` pin "is kept, and says why", matching the literal string the pin carried. objectui#5936 retired that pin's membership half, so the assertion pinned a claim the adjudication removed. It now pins what must not go silently instead: the gate header carries the three readings, the adjudication, and the not-a-proof-of-absence caveat; and the pin file survives without re-importing the `icons` record. Matched against a whitespace-normalised header, because these are assertions about prose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- .../check-lucide-icon-record-names.test.ts | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/scripts/__tests__/check-lucide-icon-record-names.test.ts b/scripts/__tests__/check-lucide-icon-record-names.test.ts index dca50a9b0c..205bf82ce1 100644 --- a/scripts/__tests__/check-lucide-icon-record-names.test.ts +++ b/scripts/__tests__/check-lucide-icon-record-names.test.ts @@ -995,12 +995,49 @@ describe('the gate is wired and the local pins it subsumes are gone', () => { } }); - it('the pin the gate does NOT subsume is kept, and says why', () => { - // `ui:icon`'s registration meta: no first-party consumer of a - // registration's `icon` exists in this repository, so the gate has no - // measured basis to judge it. Retiring that pin would drop coverage. + it('records WHY a registration\'s `icon` meta is not judged, with the reading', () => { + // objectui#5936. This assertion used to read "the pin the gate does NOT + // subsume is kept, and says why", and its stated reason was that no + // first-party consumer of a registration's `icon` could be found in THIS + // repository, so "retiring that pin would drop coverage". + // + // That reason was a one-repo reading, and objectui#5936 finished the + // measurement: objectstack is also zero, and cloud is a THIRD zero with no + // component-registration surface at all. With no consumer found anywhere, + // the gate is adjudicated NOT to be extended to this population, and the + // pin's MEMBERSHIP half was retired rather than kept — it guarded 1 of the + // 45 registrations declaring an `icon` meta, over a surface with no + // measured reader. + // + // What this pins now is the part that must not go silently: the gate's + // header has to carry the reading, the adjudication, and the caveat that + // three zeros are not a proof of absence. A boundary a reader cannot see + // the reasoning for is re-litigated from scratch, which is what + // objectui#5936 cost. + // + // Matched against a WHITESPACE-NORMALISED header: these are assertions about + // prose, and prose re-wraps. Matching the raw text would make a reflow that + // changes nothing fail, which teaches the next reader to reflow the sentence + // rather than keep the meaning. + const header = fs.readFileSync(path.join(repoRoot, GATE), 'utf8') + .replace(/^\s*\*\s?/gm, ' ') + .replace(/\s+/g, ' '); + expect(header, 'the registration `icon` meta boundary is undocumented').toContain('objectui#5936'); + for (const population of ['objectui', 'objectstack', 'cloud']) { + expect(header, `the ${population} reading is missing from the boundary`).toContain(population); + } + expect(header, 'the boundary does not say the gate is NOT extended').toContain('NOT extended'); + expect(header, 'the boundary reads as a proof of absence').toContain('NOT a proof of absence'); + + // The pin file survives — its COUPLING half needs no external vocabulary + // and is not what was retired — but it must no longer assert record + // membership, or the retirement did not happen. const kept = 'packages/components/src/__tests__/icon-renderer-declared-default.test.ts'; expect(fs.existsSync(path.join(repoRoot, kept))).toBe(true); - expect(fs.readFileSync(path.join(repoRoot, kept), 'utf8')).toContain('NOT retired by objectui#5633'); + const pin = fs.readFileSync(path.join(repoRoot, kept), 'utf8'); + expect(pin, 'the pin still imports the `icons` record — the membership half is back').not.toMatch( + /^\s*import\s*\{[^}]*\bicons\b[^}]*\}\s*from\s*'lucide-react'/m, + ); + expect(pin, 'the pin no longer says which card retired its membership half').toContain('objectui#5936'); }); }); From c3372c227067a1b81af8d09e46a677fdfebd39b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:53:14 +0000 Subject: [PATCH 3/3] chore(changeset): declare objectui#5936 as releasing nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test and tooling only — the diff is confined to packages/components/src/__tests__/ and scripts/. The empty frontmatter rests on that, NOT on the "no consumer anywhere" premise the card's retirement half turns on. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 --- ...936-registration-icon-meta-dead-surface.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .changeset/5936-registration-icon-meta-dead-surface.md diff --git a/.changeset/5936-registration-icon-meta-dead-surface.md b/.changeset/5936-registration-icon-meta-dead-surface.md new file mode 100644 index 0000000000..40b9ba57f7 --- /dev/null +++ b/.changeset/5936-registration-icon-meta-dead-surface.md @@ -0,0 +1,23 @@ +--- +--- + +Test and tooling only; no published behaviour changes. + +objectui#5936 asked for the consumer of a component registration's `icon` meta +before the lucide icon gate was extended to that population. Three first-party +populations were measured (objectui, objectstack, cloud) and none has one, so +the gate is **not** extended — adjudicated 2026-09-04. This records that reading +in the gate's own header and retires the membership half of the `ui:icon` local +pin, which had been kept on the premise that the palette lives outside this repo. + +Why the EMPTY frontmatter, when PR #7590 an hour earlier declared a real `patch` +for the same card: that change edited two published packages' RENDERER source, +so consumers of the tarball could observe it. This one is confined to +`packages/components/src/__tests__/` and to `scripts/`, and it changes no +runtime code path in any package. + +⚠️ The empty declaration rests on "test and tooling only", NOT on the +"no consumer anywhere" premise. That premise is objectui#5936's ADR-0049 +retirement premise and this change deliberately does not lean on it: the +published `ComponentMeta.icon` key is untouched, and whether it should itself be +retired stays a maintainer decision.