Skip to content

chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired - #8941

Merged
hotlong merged 12 commits into
mainfrom
claude/pr-7058-lucide-react-1.41.0
Sep 27, 2026
Merged

hotlong merged 12 commits into
mainfrom
claude/pr-7058-lucide-react-1.41.0

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ GOVERNED — this PR stays a draft until an authorized approval is on it. node scripts/check-governed-queue-guard.mjs --test over this PR's file list answers GOVERNED — 1 of 42 path(s): skills/objectui/guides/plugin-development.md, the published skills catalog. One governed path governs the whole PR. ⛔ Not to be flipped ready, enqueued or armed with auto-merge until os-zhuang or hotlong leaves an APPROVED review; ⛔ no seat approves it. After that approval the claiming seat lands it.

Moves lucide-react from ^1.31.0 to ^1.43.0 across the workspace, repairs what the jump breaks, and executes the maintainer's ruling C on this PR (comment 5750512894, verbatim 「8941 C」): relax the two pins, land the bump. The re-measured census comes first, then ruling C, then the checks. The sections under Historical further down were measured on older trees and are ⛔ not re-derived.

objectui#7058 (the Dependabot branch this PR superseded) is already closed, unmerged, since 2026-09-13. This PR never named it with a closing keyword; closing it was never this PR's.

1. This round's census — re-measured on 2026-09-25 against the INSTALLED 1.43.0

Tree: this branch after merging main at 21d34d5e2 (merge commit 423a1a59a); the censuses read the committed tree through git ls-tree plus git cat-file, so enumeration and reading come from the same source. The installed lucide-react is 1.43.0, integrity-equal to the registry tarball (sha512-ubtnda1f…).

⚠️ Correction to the baseline. main does not install 1.31.0. Its manifests say ^1.31.0, but since the pnpm dedupe of #9316 (2026-09-13) its lockfile resolves 1.35.0, one copy. So this PR moves the declared range ^1.31.0 to ^1.43.0 and the installed version 1.35.0 to 1.43.0. Every comparison below is taken against both 1.31.0 and 1.35.0.

1a. What lucide itself changed (read off the three installed artifacts)

Surface 1.31.0 1.35.0 (main today) 1.43.0 Removed, 1.35.0 to 1.43.0
runtime exports of lucide-react 6068 6110 6224 0
public names in lucide-react.d.ts 6072 6114 6230 0
dynamicIconImports keys (= iconNames) 2025 2039 2077 0
keys of the runtime icons record 1767 1781 1818 1 — Trash2
icon modules exporting __iconNode 1767 1781 0 —
icon modules exporting __iconData 0 0 1818 —
icon modules declaring aliases 0 0 248 —

1.31.0 to 1.43.0 also removes 0 exports, 0 type names, 0 dynamic names and the same single record key.

1b. Census 1 — every name this tree imports from lucide-react resolves

TypeScript-AST scan of every tracked code file: import and export-from declarations (named, default, namespace, type), import() / vi.mock() / require.resolve() calls naming the package, import type nodes, and static member reads off a namespace import or off icons.

Tree importing files sites distinct value names type names fail on 1.43.0 fail on 1.35.0 fail on 1.31.0
this branch after the merge 413 1980 329 2 (LucideIcon, LucideProps) 0 0 0
6f20cfb06 (the 2026-09-20 head) 419 2018 331 2 0 0 0

Controls, in the same run: an injected Trash, type LucideIcon and namespace read L.Pencil resolve; an injected NotARealLucideIcon and L.AlsoNotReal are both reported as failures.

1c. Census 2 — every authored icon STRING resolves as a key of the runtime icons record

Every string in an icon-named position (icon, iconName, ...Icon properties, JSX attributes, resolveIcon(...) arguments) across code, JSON, YAML and Markdown, tokenised with the seam's own rule copied verbatim (split(/[-_\s]+/), capitalise, Home to House), judged against each version's record.

Reading Result
sites / distinct names 717 / 207
live on 1.43.0 641
live on 1.35.0 or 1.31.0 but dead on 1.43.0 (what the bump breaks) 0 outside this PR's own changeset prose, which names trash-2 as the spelling it retired
dead on all three versions 75 sites, 40 distinct — pre-existing and untouched by the bump: the deliberate retired-spelling negatives (edit, smile, filter, not-a-real-icon, definitely-not-a-lucide-icon…) and strings that are not icon names at all (colour classes, labels)
pnpm check:icon-record-names OK lucide icon names: 184 authored/declared names reaching 1 record-reading resolver are live icons keys (record 1818 keys ...)

Controls: an injected icon: 'trash' reads live, injected icon: 'trash-2' in JSON and in TS both read as regressions. Independently, a case-insensitive git grep for every spelling that tokenises to Trash2 finds only component imports of Trash2 (still a runtime export in 1.43.0), comments, and one vi.mock stub key: no authored string. The same census over main finds exactly 6 string sites that tokenise to Trash2 (5 trash-2, 1 Trash2, one per file); they are the ones the 2026-09-20 round repaired, and all 6 read trash here.

The glyph is unchanged by that repair, measured on icon nodes:

Comparison full node node without key
1.43.0 trash vs 1.31.0 trash-2 IDENTICAL IDENTICAL
1.43.0 trash vs 1.35.0 trash-2 IDENTICAL IDENTICAL
control: 1.43.0 pencil vs 1.31.0 / 1.35.0 pencil IDENTICAL IDENTICAL
control: 1.43.0 trash vs 1.35.0 trash DIFFERENT DIFFERENT

⚠️ The last control row is also a user-visible fact: lucide moved the two-line bin artwork under the trash name, so anything that already authored trash (10 string sites on main) now draws what trash-2 drew. Upstream also redrew other glyphs. Comparing the artwork each name draws on 1.35.0 against 1.43.0 (controls: Trash2 reads unchanged, Trash reads changed), the ones this tree uses are the components CheckCircle2 (29 importing files), Table2 (8), CircleDot (5), CircleCheck (2), Asterisk (1) and BadgeCheck (1), and the strings trash, shopping-cart, circle-check / CircleCheck and table-2. Redraws, not breakage.

1d. Census 3 — every reader of __iconNode / __iconData works

A git grep for __iconNode|__iconData|createLucideIcon|LucideIconData over the whole tree finds exactly one reader: the lazy draw in packages/components/src/renderers/action/resolve-icon.ts, which reads module.__iconData?.node ?? module.__iconNode (the 2026-09-20 repair). No other code reads an icon module's exports. Red, then green, on the lazy-draw row, predicted red before running, through scripts/ablation-replace.mjs from ../objectstack (anchor must hit exactly once, write verified on disk, restore proven by blob hash and an empty git diff HEAD):

mutation   const node = module.__iconData?.node ?? module.__iconNode;   ->   const node = module.__iconNode;
           ok mutation landed: anchor 1 -> 0, blob cc7d80e9edfc -> 271d15e3fa58
RED        x fills the path data in once the icon module arrives
           AssertionError: expected 0 to be greater than 0      Tests  1 failed | 5 skipped (6)
restore    ok restored: blob == HEAD (cc7d80e9edfc) and `git diff HEAD` is empty
GREEN      Tests  1 passed | 5 skipped (6)                       (at 423a1a59a)

Real-build probe, on the console built at 989330df0 (apps/console/dist), in Chromium: of 2345 built JS chunks, 1819 mention __iconData (the 1818 vendor-icon-* chunks plus the seam's ui-components chunk) and exactly 1 mentions __iconNode, the seam's fallback read; the built seam reads __iconData?.node??n.__iconNode. Four built icon chunks imported in the browser:

chunk __iconData.name aliases path elements first d __iconNode
vendor-icon-house-* house home 2 M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8 undefined
vendor-icon-trash-* trash trash-2 5 M10 11v6 undefined
vendor-icon-arrow-down-0-1-* arrow-down-0-1 arrow-down-01 5 m3 16 4 4 4-4 undefined
vendor-icon-loader-circle-* loader-circle loader-2 1 M21 12a9 9 0 1 1-6.219-8.56 undefined

So the shipped glyph data is non-empty and sits where the seam now reads it, and the pre-repair read would find nothing in the real build too. NOT MEASURED: a rendered page. With no backend the built console lands on /login, which draws no lucide svg at all (0 found), so there was no seam-drawn glyph on screen to count.

1e. Census 4 — every lucide-NAME class token in the tree, against the 1.43.0 alias index

Every lucide- token in every tracked text file (lockfile and binaries excluded), classified against an alias index built from the 1818 installed icon modules (canonical lucide- plus the icon name; one alias class per declared alias), against what the seam emits (the key-derived class and the canonical class, rule copied verbatim from lucideClassNames), and against what 1.35.0 emitted.

Class of token distinct disposition
canonical class of a 1.43.0 icon 29 safe: lucide's component and the seam both emit it
not an icon class (lucide-icon, script names such as lucide-record-icon-names) 5 out of scope
alias class: lucide-home (of house) 1 the button pin — relaxed under ruling C, see section 2
alias class: lucide-loader-2 (of loader-circle) 1 svg-host-dom-leak-5632.test.tsx, a spinner drawn by a component import, so lucide's own alias class IS on it; the exact assertion the 2026-09-20 round updated holds on 1.43.0
alias / retired: lucide-trash-2, lucide-trash2 2 prose only: the seam docblock and the pin comment (both rewritten in this round), and the pending objectui#9251 changeset (left alone; this PR's changeset dates its example)
seam-key-only class (emitted by the seam, not by lucide 1.43.0) 0 referenced —

Controls: an injected svg.lucide-trash-2 assertion is classified as an alias of trash; an injected svg.lucide-check reads canonical. Six dynamically built selectors (svg.lucide-${name} in the command and context-menu suites) iterate canonical names only.

1f. New since the 2026-09-20 round

main moved 294 commits past the old merge base 078f2e4f7. What the four censuses see that the 6f20cfb06 tree did not:

Census new at this head verdict on 1.43.0
imports 7 sites in 6 files; 4 files import lucide for the first time: packages/app-shell/src/layout/ReadRateBanner.tsx (TriangleAlert), packages/app-shell/src/views/ActionParamDialog.tsx (ChevronDown, Lock), packages/plugin-detail/src/__tests__/detailSectionAuthoredIcon-9529.test.tsx (MapPin), packages/app-shell/src/hooks/__tests__/userLocale-10059.test.tsx (a vi.mock of the package); plus Code2 in FlowKeyValueField.tsx and BookOpen in NavigationDesigner.tsx all resolve
authored strings 13 sites; new names share, map-pin all live, except 2 edit sites in menubar-item-icon.test.tsx, which are deliberate retired-spelling negatives, dead on all three versions
__iconNode / __iconData readers none —
class tokens 6 pairs, all in packages/components/src/__tests__/menubar-item-icon.test.tsx: lucide-chevron-right, lucide-download, lucide-file, lucide-mail, lucide-save, lucide-share all canonical

None of them needed a repair.

1g. The merge and the lockfile

origin/main merged at 21d34d5e2 (⛔ no rebase, no amend, no force-push): one conflict, packages/plugin-kanban/package.json, resolved as the union (main deleted @tanstack/react-virtual with the module that used it; this branch bumps lucide-react). pnpm-lock.yaml was regenerated by pnpm from main's own lockfile, ⛔ not hand-merged: main's file taken whole, then pnpm install --lockfile-only with the 22 manifests briefly at an exact 1.43.0 and again with ^1.43.0 restored, which pnpm keeps. A plain re-lock follows the caret to the newest 1.x (1.48.0 today). The delta against main is lucide alone: 22 importer specifiers, 22 resolutions and the fumadocs peer suffixes. All 22 manifests that declare lucide-react read ^1.43.0.

2. Ruling C — the two pins, relaxed and no more

⚠️ This weakens a gate that was telling the truth. The two rows below were red because the seam genuinely stopped reproducing lucide's classes; they were not flakes. lucide-react 1.43.0 emits one class per DECLARED ALIAS (house renders lucide-house lucide-home) and no longer the class it derived from the PascalCase key. The alias list lives only inside each icon module, which objectui#9251 made lazy, while the seam must put its classes on the first frame. The maintainer chose C over A (put every alias into the eager, build-generated name list, spending bytes under objectui#9251's ruling) and over B (hold at 1.31.0), with the weakening stated when it was presented. ⇒ After this PR, a seam-drawn glyph missing lucide's alias classes, or carrying the key-derived class lucide no longer emits, passes these rows. ⛔ It is not free.

Re-measured size of what is given up (1.43.0 facts, the seam's rule copied verbatim): of 1818 record keys, the seam's class set and lucide's agree on 1494 and disagree on 324 (80 seam-only, 223 lucide-only, 21 both ways) — the same figures the 2026-09-20 round read. Why the canonical form of C and not the subset form: the seam's classes are a subset of lucide's for only 1717 of 1818 keys (the key-derived class is not one lucide 1.43.0 emits), so "the seam's classes need only be a subset" would still be red on 101 keys. The canonical class is on the seam for 1818 of 1818.

Exactly two pins. They are the two rows Test (shard 3/4) and Test (shard 4/4) reported red on 6f20cfb06, read off those check runs' own annotations:

  • shard 3/4: button-shared-icon-resolver.test.tsx > resolves home through the RENAME alias to House, not to a dead home — expected null to be SVGSVGElement;
  • shard 4/4: resolve-icon-lazy-9251.test.tsx > reproduces lucide's per-icon class names over the WHOLE record vocabulary — AArrowDown: expected "lucide lucide-a-arrow-down", got "lucide lucide-a-arrow-down lucide-aarrow-down", and nine more.

Each now carries, at its site, what it used to refuse and why it no longer does. Verbatim below, with two deliberate substitutions: the generic in Record OF string, React.ElementType and the placeholder in lucide-ICONNAME are written in capitals instead of angle brackets, because GitHub deletes tag-shaped fragments from a body even inside a code fence. Byte-exact text: git show 178da2eea.

Pin 1 — packages/components/src/renderers/action/__tests__/resolve-icon-lazy-9251.test.tsx

Before (6f20cfb06):

  it('reproduces lucide\'s per-icon class names over the WHOLE record vocabulary', () => {
    // ⚠️ The class names are the part a conversion rule gets wrong silently: 95
    // of the record's keys pack digits that lucide splits in the module name and
    // not in the class derived from the key, so `Trash2` renders BOTH
    // `lucide-trash2` and `lucide-trash-2`. `createLucideIcon` is what builds
    // them and the seam no longer calls it, so every name is compared against
    // the record's own component here — not a sample, and not a copy of the
    // rule.
    const keys = Object.keys(icons);
    expect(keys.length).toBeGreaterThan(1000);
    const mismatches: string[] = [];
    let digitKeysChecked = 0;
    for (const key of keys) {
      if (/\d/.test(key)) digitKeysChecked += 1;
      const reference = renderIcon((icons as Record OF string, React.ElementType)[key], {});
      const seam = renderIcon(resolveIcon(key)!, {});
      const want = classesOf(reference).join(' ');
      const got = classesOf(seam).join(' ');
      if (want !== got) mismatches.push(`${key}: expected "${want}", got "${got}"`);
    }
    expect(mismatches.slice(0, 10)).toEqual([]);
    // Non-vacuity, both ways: the loop ran, and it ran over the class of name
    // this row exists for.
    expect(digitKeysChecked).toBeGreaterThan(50);
  });

After:

  it('carries lucide\'s CANONICAL per-icon class over the WHOLE record vocabulary', () => {
    // ⚠️ RELAXED under the maintainer's ruling C on objectui#8941 (comment
    // 5750512894, verbatim 「8941 C」). This row USED TO REFUSE any seam glyph
    // whose complete class set differed from lucide's own component's. It NO
    // LONGER DOES, because lucide-react 1.43.0 emits one class per DECLARED
    // ALIAS (`house` renders `lucide-house lucide-home`) and no longer emits the
    // class it used to derive from the key, and the alias list exists only
    // inside each icon module — the modules objectui#9251 made lazy — while the
    // seam has to put its classes on the FIRST frame. The ruling chose the bump
    // over carrying every alias in the eager static name list (option A), and
    // it was taken knowing that it weakens this row. So a seam glyph missing
    // lucide's alias classes, or carrying the key-derived class lucide no longer
    // emits, now passes here.
    //
    // What it still refuses: a seam glyph without `lucide` or without lucide's
    // CANONICAL class, `lucide-ICONNAME`, which lucide emits first after the
    // bare `lucide`. That is the class a conversion rule gets wrong silently,
    // because many keys pack digits that lucide splits in the module name
    // (`ArrowDown01` is `lucide-arrow-down-0-1`, never `lucide-arrow-down01`).
    // Every name is still compared against the record's own component — not a
    // sample, and not a copy of the rule.
    const keys = Object.keys(icons);
    expect(keys.length).toBeGreaterThan(1000);
    const mismatches: string[] = [];
    let digitKeysChecked = 0;
    for (const key of keys) {
      if (/\d/.test(key)) digitKeysChecked += 1;
      const reference = renderIcon((icons as Record OF string, React.ElementType)[key], {});
      const seam = renderIcon(resolveIcon(key)!, {});
      // Read in lucide's own order: `lucide`, then the canonical class, then
      // one class per alias. Only the first two are required of the seam.
      const [base, canonical] = (reference?.getAttribute('class') ?? '').split(/\s+/).filter(Boolean);
      const got = classesOf(seam);
      if (base !== 'lucide' || !canonical?.startsWith('lucide-') || !got.includes(base) || !got.includes(canonical)) {
        mismatches.push(`${key}: expected "${base} ${canonical}" among "${got.join(' ')}"`);
      }
    }
    expect(mismatches.slice(0, 10)).toEqual([]);
    // Non-vacuity, both ways: the loop ran, and it ran over the class of name
    // this row exists for.
    expect(digitKeysChecked).toBeGreaterThan(50);
  });

Pin 2 — packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsx

Before (6f20cfb06):

    it('resolves `home` through the RENAME alias to House, not to a dead `home`', () => {
      // The single entry both maps carried. It is the one input whose answer
      // would have changed had the dedupe dropped the alias on the floor.
      const button = renderButton({ icon: 'home' });
      const house = button.querySelector('svg.lucide-house');
      expect(house).not.toBeNull();
      // The negative half used to read `querySelector('svg.lucide-home')` is
      // null. That form RETIRED on lucide-react 1.43.0, which reshaped
      // `createLucideIcon` to take an icon-data object carrying `aliases` and
      // to emit one class PER ALIAS: `house` now renders
      // `class="lucide lucide-house lucide-home ..."` itself, so the class no
      // longer says which glyph was drawn (measured: 248 of 1818 icon modules
      // carry aliases; 1.31.0 icon modules have no `aliases` field at all).
      // The discriminating form is below and still fires for the same defect:
      // `Home` is not a key of the runtime record, so had the rename been
      // dropped this button would render NO glyph at all, and any SECOND glyph
      // would be a different element from `house`.
      expect(button.querySelectorAll('svg')).toHaveLength(1);
      expect(button.querySelector('svg.lucide-home')).toBe(house);
    });

After:

    it('resolves `home` through the RENAME alias to House, not to a dead `home`', () => {
      // The single entry both maps carried. It is the one input whose answer
      // would have changed had the dedupe dropped the alias on the floor.
      const button = renderButton({ icon: 'home' });
      expect(button.querySelector('svg.lucide-house')).not.toBeNull();
      // ⚠️ RELAXED under the maintainer's ruling C on objectui#8941 (comment
      // 5750512894, verbatim 「8941 C」). This row USED TO REFUSE a house glyph
      // that did not also carry lucide's `lucide-home` ALIAS class on the same
      // element: `querySelector('svg.lucide-home')` had to BE the house svg. It
      // NO LONGER DOES, because this button draws through the objectui#9251
      // seam, which emits lucide's canonical class and not lucide-react
      // 1.43.0's per-alias classes, and the ruling accepted that rather than
      // put every icon's aliases into the eager static name list.
      //
      // ⛔ Nor does it go back to the older form, `svg.lucide-home` is null:
      // lucide 1.43.0 itself puts `lucide-home` on the house glyph, so that
      // class no longer says which glyph was drawn, and pinning its absence
      // would pin the seam's divergence from lucide. `lucide-home` is neither
      // required nor forbidden here. What still discriminates is the count:
      // `Home` is not a key of the runtime record, so had the rename been
      // dropped this button would render NO glyph at all.
      expect(button.querySelectorAll('svg')).toHaveLength(1);
    });

(On main this row still reads expect(button.querySelector('svg.lucide-home')).toBeNull();, which is green under the seam on either version. ⛔ It is not restored: lucide 1.43.0 puts lucide-home on the house glyph itself, so pinning its absence would pin the seam's divergence from lucide, and the row's own comment now says so.)

Both relaxed rows can still fail, for what they still refuse. Same tool and restore proof as 1d, each prediction written before its run:

PIN 1   mutation  return [fromKey, `lucide-${kebab}`, className]  ->  return [fromKey, className]
        RED       x carries lucide's CANONICAL per-icon class over the WHOLE record vocabulary
                  "ArrowDown01: expected \"lucide lucide-arrow-down-0-1\" among \"lucide lucide-arrow-down01\"" (and 9 more)
        restore   ok restored: blob == HEAD (99c47bc10373) and `git diff HEAD` is empty
PIN 2   mutation  delete the `Home: 'House',` rename entry
        RED       x resolves `home` through the RENAME alias to House    AssertionError: expected null not to be null
        restore   ok restored: blob == HEAD (99c47bc10373) and `git diff HEAD` is empty

(The first attempt at the pin-2 mutation passed an empty replacement, which the tool refuses; no test ran on it. The run above used its delete mode.)

The seam's own docblock moved with the pins. lucideClassNames claimed its classes were derived the way lucide derives them and that a pin caught either derivation drifting, under a test file name that does not exist. Neither survives this bump and this ruling, so it now says what the seam emits, what 1.43.0 emits instead, and which pin holds how much. Comment only; no code in resolve-icon.ts changed this round.

⛔ No third pin. The full suite ran locally at 989330df0 in 16 shards (pnpm exec vitest run --shard=K/16, each under ../objectstack/scripts/pm/os-verify-lock.sh): 3618 test files (3612 passed, 6 skipped, 0 failed), 48571 tests (48527 passed, 44 skipped, 0 failed). CI on the same head: all eight Test (shard N/8) and Test (dist pins) green. No other pin went red, for this reason or for any other, so there was nothing to stop and report.

3. Checks

All at 989330df0, the head of this branch; heavy runs serialized through ../objectstack/scripts/pm/os-verify-lock.sh, every exit code captured before any pipe.

Check Command Reading
tests 16 local shards, above 3618 files, 48571 tests, 0 failed
type-check pnpm turbo run type-check --concurrency=2 Tasks: 81 successful, 81 total, 0 error TS
type-check, root projects pnpm type-check:scripts, type-check:vitest-setup, type-check:vitest-config exit 0, 0 error TS each
lint, whole repo pnpm turbo run lint --concurrency=2 Tasks: 47 successful, 47 total, 0 errors in every task (warnings only). The three files changed this round, linted alone: 0 errors, 0 warnings
build pnpm turbo run build --filter='./packages/*' --concurrency=2, then pnpm --filter @object-ui/console build 39 of 39; console exit 0
eager-closure budget node scripts/check-eager-closure-budget.mjs Console eager closure is 3054.8 KB gzipped across 329 of 2345 chunks (budget: 3104.5 KB, headroom: 49.7 KB); ui-components 266.5 KB / 282.2 KB. ⛔ No ceiling or baseline moved: the script is byte-identical to main's
locale catalogues, SDUI pins node scripts/check-eager-locale-catalogues.mjs, pnpm check:sdui-registration-pins exit 0 each
dist pnpm test:dist; check-node-esm-load, check-published-dist-tooling, check-dist-completeness --all 9 of 9 tasks (components: 2 files, 8 tests); the three gates exit 0
icon names pnpm check:icon-record-names; node scripts/regenerate-lucide-record-icon-names.mjs --check OK (184 names, record 1818 keys); the generated table matches the installed manifest
lockfile node scripts/check-lockfile-integrity.mjs --base main's lockfile --head pnpm-lock.yaml, and the default invocation; check-lockfile-dedupe VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy (1773 resolutions on both sides); VERDICT deduped
changesets check-changeset-presence, -no-major, -fixed, -overwrite, check-pending-changeset-literals exit 0 each; check-changeset-claims is report-only
touched-path gates check-control-bytes, check-new-cross-file-line-citations (0 new), check-skills-paths, check-skill-examples (after the build), check-skill-eval-tokens, check-test-path-roots, the three check-vi-mock-*, check-doc-fence-languages, check-doc-example-ids, check-shell-escape-residue, check-action-ref-convention, check-phantom-dependencies, check-unused-dependencies exit 0 each
governed node scripts/check-governed-queue-guard.mjs --test over the 42 paths GOVERNED — 1 of 42
CI the check runs on 989330df0, read when this was written 42 success, 3 skipped (dependabot and the two coverage lanes), 0 failed. The seat re-reads it

NOT MEASURED locally: Build & E2E and the live e2e lane (green on CI).

4. Changeset

.changeset/lucide-react-1-43-0-retired-trash2.md, restated against today's tree. It declares, all patch, the 20 released packages whose dependencies range moves, @object-ui/console (its devDependencies range moves and its bundle ships the new icons), and @object-ui/cli and @object-ui/types, whose source changed. It names the invisible-glyph repair as a user-visible fix, says which classes component-imported and string-resolved icons carry now, and dates the Trash2 class example in the pending objectui#9251 entry without editing that entry. Dropped from it: the claim that this bump collapses a double lucide install (main's #9316 dedupe already did).

5. Governed surface and skills readings

The one governed path is the plugin skeleton's lucide-react range in skills/objectui/guides/plugin-development.md, anchored to the in-repo range by doc-version-claims.test.ts. Readings: that file 481 lines before and after; the published skills/ tree 5365 lines before and after, its SKILL.md files 137 lines before and after. One line changed, zero added.

维护者速读(草稿)

改了什么 — 把全仓 22 个包声明的图标库 lucide-react 从 ^1.31.0 升到 ^1.43.0(实际安装版本从 1.35.0 升到 1.43.0),同时修掉升级带来的问题:懒加载图标的路径数据换了导出名(不修就是所有按名字解析的图标变成空框),以及一个被上游退役的图标名 trash-2 改成 trash(图形不变)。按您的裁决 C,放宽了两条测试钉子。

为什么改 — 上游 1.43.0 改了图标数据结构,并且给每个图标加了「别名 class」。我们的懒加载图标拿不到别名(别名只在懒加载的图标模块里),两条如实报告这一差异的测试因此变红。您选了 C:接受这个差异,落地升级,而不是把别名塞进首屏必载的名单(A)或停在旧版本(B)。

风险与代价(含回滚) — 代价是明确的:两条原本「说真话」的钉子被削弱了,之后按名字解析的图标少了 lucide 的别名 class,钉子不再报警;按别名 class 写的样式只对直接 import 的图标组件生效。另有一些图标上游重画了(例如 trash 现在画的是原来 trash-2 的样子)。回滚:整体 revert 本 PR 即回到 ^1.31.0 与旧钉子。

席位意见 — 建议批准(席位复核于 2026-09-25T14:03Z,head 989330df0)。放宽恰为两条钉子(178da2eea),各在原位写明原先拒什么、为何不再拒、仍拒什么,且各有一次预测后变红的证明;resolve-icon.ts 本轮只改注释;预算脚本与 main 字节一致,无上限或基线移动;22 个 manifest 均为 ^1.43.0,lockfile 中 lucide 只有 1.43.0 一份;该 head 上 CI 42 成功、3 跳过、0 失败。本分支合并的 main 基点之后 main 又前进 27 个提交:与本分支 merge-tree 无冲突,其新增 11221 行中 lucide 提及与图标字符串均为 0(同一扫描在本 PR 的 diff 上分别命中 99 与 7);合并队列会在最新 main 上重跑全量。开发代理提出的 objectui#9251 变更集措辞问题,席位取 A:不改那条,本 PR 的变更集已写明其示例的日期。

你要做的 — 本 PR 命中受管面(skills/objectui/guides/plugin-development.md),需要 os-zhuang 或 hotlong 留一条 APPROVED review;之后由认领席翻 ready 并入队。

Historical — measured on the 2026-09-10 and 2026-09-20 trees, ⛔ not re-derived here

Kept as the record of how this PR got here. Superseded where this round measured differently: main's installed version was 1.35.0, not 1.31.0; "main installs two physical copies" stopped being true at #9316; the governed count now reads 1 of 42; the Bundle Analysis NOT MEASURED and the eslint narrowing below are replaced by section 3; two path:line citations below are re-spelled by content (AGENTS.md #11), with nothing else in them changed; and the class note in the old section 3 describes component-imported icons only (string-resolved icons go through the seam, section 2).

Supersedes the stale Dependabot branch objectui#7058 (dependabot/npm_and_yarn/lucide-react-1.35.0). That branch is not pushed to here — Dependabot force-pushes over it on every rebase — so this is a fresh branch cut from current main. Once this lands, objectui#7058 can be closed as superseded. It is deliberately not named with a closing keyword above; closing it is a maintainer action, not this PR's.

1. Which of the seven red checks were staleness, and which were real

objectui#7058's base is 4e3a4f07e — weeks behind main. I brought its head up to date in a throwaway worktree, re-locked from current main, and re-measured. Four of the seven were artefacts of the stale base; three were real.

Check on objectui#7058 Verdict Evidence
Type Check REAL The job failed at pnpm check:icon-record-names, not at tsc. tsc never ran.
Test (shard 1/4) REAL packages/cli/src/__tests__/app-generator.test.ts — the generator's literal lucide-react range stayed at ^1.31.0 while the 22 sibling manifests moved.
Test (shard 3/4) MIXED check-lucide-icon-record-names.test.ts REAL (same four sites). ci-cd-pipeline-doc.test.ts STALE — the lockfile carried two @objectstack/spec versions.
Test (shard 4/4) MIXED doc-version-claims.test.ts REAL (skills/objectui/guides/plugin-development.md still taught ^1.31.0). check-lockfile-integrity.test.ts STALE — its fixture forks a zod@4.4.3 key the stale lockfile had replaced with 4.5.4.
Lockfile Integrity Check STALE VERDICT 12 finding(s): 7 backward move(s), 5 duplication(s) — Dependabot re-locked on an old base and dragged @objectstack/client, core, formula, lint, sdui-parser, types from 17.4.0 back to 17.3.0, plus a zod 4.4.3/4.5.4 split.
Bundle Analysis STALE vendor-objectstack 2874.0 KB / 1224.6 KB ceiling (OVER by 1649.3 KB) — that is the duplicated @objectstack tree above, which the lockfile gate itself names as the cause: "a bundle-size red downstream of this is caused by THIS, not by the dependency being bumped."
dependabot STALE Dependabot's own branch check.

On this branch, re-locked from current main, node scripts/check-lockfile-integrity.mjs reports VERDICT clean — no @objectstack/* identity moved backward and no package gained a copy.

2. Root cause of what survives — measured against the installed artifact

The hypothesis in the dispatch was a broad source-compatibility break. The measurement narrows it to a single retired name, and falsifies the type-checking half entirely.

I installed 1.31.0, 1.41.0 and 1.43.0 side by side and diffed three surfaces:

Surface 1.31.0 1.43.0 Removed
runtime exports of lucide-react 6068 6224 0
public names in lucide-react.d.ts 6072 6230 0
iconNames from lucide-react/dynamic.mjs 2025 2077 0
keys of the runtime icons record 1767 1818 1 — Trash2

Then I enumerated what this repository actually imports — 4700 source files, 333 distinct value names, one type name (LucideIcon), six namespace or default imports — and checked every one against both artefacts. Zero fail to resolve, in either version. Controls fired on the same run in both worlds: Trash resolves in both, a fabricated name resolves in neither.

So nothing breaks at an import and nothing breaks in the compiler. Trash2 is still exported by 1.43.0; it is simply no longer a key of the runtime icons record, and Trash2 === Trash there (in 1.31.0 they were distinct objects). That is exactly the class scripts/check-lucide-icon-record-names.mjs exists to catch: a retired spelling still imports, still type-checks, still renders as a COMPONENT, and resolves to nothing as a STRING.

Four authored sites resolve trash-2 through renderers/action/resolve-icon.ts, which reads record membership:

  • examples/schema-catalog/src/schemas/actions/action-button-variants.json
  • examples/schema-catalog/src/schemas/actions/confirmation-dialog.json
  • examples/schema-catalog/src/schemas/components-layout-card/profile-detail-card.json
  • packages/plugin-detail/src/DetailView.tsx — the record delete action

All four now write trash. The glyph does not change: 1.43.0's trash icon node is byte-identical to 1.31.0's trash-2 icon node (control: pencil is byte-identical across both versions, so the comparison is not returning equal for everything). Upstream replaced the old bin artwork under the trash name and kept trash-2 as a deprecated alias.

3. Why 1.43.0 and not the 1.41.0 the Dependabot PR advertises

The workspace declares a caret range, so a re-lock from current main resolves the newest 1.x — that is 1.43.0 today, under ^1.41.0 just as much as under ^1.43.0. Holding exactly 1.41.0 would need an exact pin or an override, which is not this repo's convention and not mine to invent. Measured, the extra two minors cost nothing in removals: 1.41.0 to 1.43.0 removes zero exports, zero record keys and zero dynamic names.

It does bring one behavioural change that 1.41.0 does not have, and it is worth a maintainer's eye:

1.43.0 reshaped createLucideIcon to take an icon-data object carrying aliases, and it now emits one CSS class per retired alias. A spinner renders class="lucide lucide-loader-circle lucide-loader-2 ...". Measured: 248 of 1818 icon modules carry an alias; 1.31.0 icon modules have no aliases field at all. This is additive, so a .lucide-loader-circle selector still matches — but a selector asserting the exact, complete class string does not, and neither does an assertion that a given lucide- class is ABSENT.

I censused the whole tree for both shapes, using the alias index built from the installed artefact as the oracle: 28 distinct lucide- class names are referenced anywhere in the repository (all file types, snapshots included) and exactly two are aliases in 1.43.0 — lucide-home (owned by house) and lucide-loader-2 (owned by loader-circle). Controls: injecting the known alias trash-2 into the census is found; check is correctly absent. Both affected pins are repaired here:

  • examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsx asserts the exact attribute set. The added class is named. The assertion stays exact on purpose — that list is what catches an authored prop reaching the DOM, which is the whole point of objectui#5632.
  • packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsx proved home resolved through the rename to House by asserting svg.lucide-home is null. house now emits lucide-home itself, so that class no longer says which glyph was drawn. Replaced with a form that still fires for the same defect: exactly one svg, and the alias class on that same element. Had the rename been dropped, Home is not a record key and the button would render no glyph at all.

4. Something the bump gives back

main installs two physical copies of the icon library: 1.31.0 for this workspace and 1.35.0 pulled by fumadocs-ui. On this branch both collapse onto a single 1.43.0. Lockfile resolutions go 1822 to 1821.

5. What was measured here

  • pnpm check:icon-record-names — OK ... 184 authored/declared names reaching 1 record-reading resolver are live icons keys (record 1818 keys).
  • Reverse verification, direction predicted before running: restoring trash-2 in DetailView.tsx's sys_delete action turns that gate red naming exactly that one site and nothing else; restoring the file returns it to green. Restoration proved by blob hash equality with HEAD and an empty git diff HEAD, not by an exit code.
  • pnpm type-check across the workspace — turbo 81 successful, 81 total over 45 packages, zero error TS.
  • pnpm exec vitest run packages/components/ examples/schema-catalog/ packages/plugin-view/ packages/plugin-list/ — Test Files 395 passed (395), Tests 5688 passed (5688).
  • pnpm exec vitest run packages/plugin-detail/ packages/types/ examples/schema-catalog/ — 350 files, 6769 tests, green after the pin repair.
  • pnpm exec vitest run packages/cli/src/__tests__/app-generator.test.ts — 43 passed.
  • pnpm exec vitest run scripts/__tests__/doc-version-claims.test.ts — 29 passed.
  • pnpm exec vitest run scripts/__tests__/check-lucide-icon-record-names.test.ts scripts/__tests__/ci-cd-pipeline-doc.test.ts scripts/__tests__/check-lockfile-integrity.test.ts — green.
  • node scripts/check-lockfile-integrity.mjs — VERDICT clean.
  • node scripts/check-changeset-presence.mjs and check-changeset-no-major.mjs — both green.

Declared narrowing: the full four-shard pnpm test and the repo-wide pnpm lint are left to CI. eslint over the changed source files reports one pre-existing error in action-bar.tsx (react-hooks/static-components) in code this PR does not touch (its only change to that file is a docblock comment); that attribution is stated, not yet controlled against main. The Bundle Analysis measurement was still building locally when this was opened and is reported as NOT MEASURED rather than assumed — the stale-base cause of its red is independently established above.

6. Governed surface — this PR stays a draft

node scripts/check-governed-queue-guard.mjs --test reports GOVERNED — 1 of 37 path(s): skills/objectui/guides/plugin-development.md, the published skills catalog. One governed path governs the whole PR. It is not to be flipped ready, enqueued, or armed with auto-merge; a human merge is the review record.

The changed line there is the plugin skeleton's lucide-react range, which doc-version-claims.test.ts anchors to the in-repo range. Its KNOWN_CLAIMS entry is keyed by the literal text, so the inventory key moved with it.


Generated by Claude Code

os-support-ai and others added 3 commits September 10, 2026 04:38
…tired spelling

Supersedes the stale Dependabot branch for this bump (objectui#7058), whose
base was weeks behind `main`.

Measured against the installed artifact, not the changelog: across the whole
1.31.0 -> 1.43.0 jump lucide removes ZERO public names from `lucide-react`
(6072 -> 6230 d.ts names, none dropped; 6068 -> 6224 runtime exports, none
dropped) and ZERO names from `lucide-react/dynamic.mjs`. Exactly one key
leaves the runtime `icons` record: `Trash2`, which upstream retired in favour
of `trash` (lucide-icons/lucide#3141). It survives as a DEPRECATED EXPORT of
the same object, so nothing breaks at a named import or in the type checker --
only STRING lookups through `resolveIcon`, which read record membership.

Repaired, all four sites the gate names:
  - examples/schema-catalog .../action-button-variants.json
  - examples/schema-catalog .../confirmation-dialog.json
  - examples/schema-catalog .../profile-detail-card.json
  - packages/plugin-detail/src/DetailView.tsx

`trash` is the visually lossless replacement: lucide 1.43.0's `trash` icon
node is byte-identical to 1.31.0's `trash-2` icon node (control: `pencil`
identical across both versions).

Also re-anchored the two ranges that quote the workspace:
  - packages/cli/src/utils/app-generator.ts (app-generator.test.ts anchor)
  - skills/objectui/guides/plugin-development.md (doc-version-claims anchor)

and the four doc/comment examples that taught the retired spelling.

The lockfile is regenerated from current `main`, so the @objectstack 17.4.0 ->
17.3.0 downgrade, the zod 4.4.3/4.5.4 split and the resulting duplicated
vendor chunk that reddened objectui#7058 are all absent here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
…alias classes retired

lucide-react 1.43.0 reshaped `createLucideIcon` to take an icon-data object
carrying `aliases`, and it now emits one CSS class per retired alias. Measured
on the installed artifact: 248 of 1818 icon modules carry an alias; 1.31.0 icon
modules have no `aliases` field at all.

Censused the whole tree for the two shapes this can break, using the alias
index as the oracle: 28 distinct `lucide-*` class names are referenced anywhere
in the repository (all file types, snapshots included) and exactly two of them
are aliases in 1.43.0 -- `lucide-home` (owned by `house`) and `lucide-loader-2`
(owned by `loader-circle`). Both are repaired here; the other 26 still name a
canonical icon and stay discriminating. Control: injecting the known alias
`trash-2` into the census is found, and `check` is correctly absent.

  - examples/schema-catalog svg-host-dom-leak-5632: asserts the EXACT attribute
    set, so the added class has to be named. Kept exact on purpose -- that list
    is what catches an authored prop reaching the DOM.
  - components button-shared-icon-resolver: its negative half read
    `querySelector('svg.lucide-home')` is null to prove `home` resolved through
    the rename to House. `house` now emits `lucide-home` itself, so that class
    no longer says which glyph was drawn. Replaced with a form that still fires
    for the same defect: one svg, and the alias class on that same element.

Also adds the changeset the four touched released packages owe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
Conflicts, both taken deliberately:
  - apps/site/package.json — kept our `lucide-react: ^1.43.0` AND main's
    `next: 16.3.3`. Neither side's other change is dropped.
  - pnpm-lock.yaml — took main's file and regenerated it with `pnpm install`
    rather than hand-merging YAML. Verified after: exactly one
    `lucide-react@1.43.0`, and every `@objectstack/*` package resolves to a
    single 17.4.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH
@github-actions

Copy link
Copy Markdown
Contributor

❌ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3504.9 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-B6n3sok8.js —
Status FAIL —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ❌ over its ceiling
Ceiling sensitivity (headroom) ✅ pass
Ceiling freshness (checkout vs. base branch) ✅ pass

📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 201.97KB 56.22KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…de 1.43.0

Measured on this branch's console build: `ui-components` is 410,904 bytes
gzipped against a 399,000 ceiling -- over by 11,904.

Ruled out as a tree-shaking regression BEFORE the number moved, because "the
bundler stopped shaking" and "the dependency got bigger" want opposite fixes
and only one of them is a ceiling:

  - ALL 1,818 keys of lucide 1.43.0's runtime `icons` record appear verbatim in
    the built chunk. Of 1.31.0's 1,767 keys, 1,766 appear in the same chunk --
    the one absent is `Trash2`, the key lucide retired, which is what shows the
    probe discriminates rather than matching everything. A fabricated key is
    not found.
  - Icons therefore do not tree-shake here and did not before. That is
    deliberate: `resolve-icon.ts` imports the whole record because string
    lookups read record MEMBERSHIP (the objectui#5935 seam). Moving it to
    `lucide-react/dynamic.mjs` to shed bytes would resolve the very names the
    record drops -- `trash-2`, `edit`, `smile` -- which the icon gate's own
    header calls worse than having no gate.
  - The library is bigger two ways. Same-artwork control: `pencil.mjs`, whose
    icon node is byte-identical across versions, goes 455 -> 522 bytes because
    1.43.0 reshaped every icon module into an `__iconData` object carrying
    `name`, `size` and `aliases`. And there are more icons: the icon directory,
    licence headers stripped and gzipped, goes 166,705 -> 176,968 bytes
    (+10,263, +6.2%) -- the same order as the chunk delta.

Ceiling and baseline move together, in this one commit. New headroom is 9,096
bytes = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES, the proportion
objectui#7399 re-pinned this key to -- chosen over "just enough to pass",
because a line left with almost no headroom is the defect objectui#8816
records. REGRESSION_THIS_GATE_MUST_CATCH_BYTES did not move. Nothing else
moved: the aggregate was re-read on the same build and passes on its own
(3,589,068 of 3,597,000, headroom 7,932), so it is left alone.

The raised gate still fails: padding `ui-components` by 15,000 bytes in a COPY
of the report gives `OVER by 5.8 KB` and exit 1, while the same invocation on
an unpadded copy exits 0. The real report was never mutated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01611D6ZaRaMmwTNQmSbk8MH

Copy link
Copy Markdown
Contributor Author

Bundle Analysis — measured, root-caused, and the ceiling raised deliberately

Re-measured on my own merge result (e85ffc530), not read off another seat's report. Only one line objects:

✅ Console eager closure is 3504.9 KB gzipped across 50 of 518 chunks (budget: 3512.7 KB, headroom: 7.7 KB).
❌ 1 eager chunk is over its per-chunk budget:
  ✅ vendor-objectstack      1207.3 KB / 1224.6 KB ceiling (headroom 17.3 KB)
  ✅ i18n-locales             443.9 KB / 444.3 KB ceiling (headroom 0.4 KB)
  ❌ ui-components            401.3 KB / 389.6 KB ceiling (OVER by 11.6 KB)
  ✅ framework                 71.8 KB / 97.7 KB ceiling (headroom 25.9 KB)

Exactly ui-components, 410,904 bytes against a 399,000 ceiling — over by 11,904. Worth noting on the way past: vendor-objectstack is 1207.3 KB here versus 2874.0 KB on objectui#7058's head, which is the duplicated @objectstack tree from its stale lockfile disappearing. That half of the red was staleness.

First: is this tree-shaking, or is the library bigger?

Asked before touching a ceiling, because the two want opposite fixes.

It is not a tree-shaking regression. All 1,818 keys of lucide 1.43.0's runtime icons record appear verbatim in the built chunk. Not most — all. Of 1.31.0's 1,767 keys, 1,766 appear in that same chunk; the single absent one is Trash2, the key lucide retired. That asymmetry is the control: the probe discriminates rather than matching everything, and a fabricated key is not found.

So icons do not tree-shake here and did not before. That is deliberate, not a defect: renderers/action/resolve-icon.ts imports the whole icons record because string lookups resolve against record MEMBERSHIP — the seam objectui#5935 consolidated onto and check-lucide-icon-record-names.mjs enforces. Moving that seam to lucide-react/dynamic.mjs to shed the bytes would resolve the very names the record deliberately drops (trash-2, edit, smile), which that gate's own header calls worse than having no gate. So there is no import-side fix available here that does not reverse a maintainer ruling.

The library is genuinely bigger, two independent ways:

  • Same-artwork control. pencil.mjs, whose icon node is byte-identical across the two versions, goes 455 to 522 bytes — because 1.43.0 reshaped every icon module into an __iconData object carrying name, size and aliases. The artwork is unchanged, so that delta is purely the reshape.
  • More icons. The whole icon directory, licence headers stripped and gzipped: 166,705 to 176,968 bytes (+10,263, +6.2%) from 1.31.0 to 1.43.0. Same order as the chunk delta.

So the ceiling moves, in the objectui#8888 / objectui#8901 shape

PER_CHUNK_GZIP_CEILINGS['ui-components'] 399,000 to 420,000, and PER_CHUNK_BASELINE['ui-components'] 391,095 to 410,904, together in one commit, with the reasoning written into the checker's own header as a Why ui-components moved UP section.

New headroom is 9,096 bytes = 0.10x REGRESSION_THIS_GATE_MUST_CATCH_BYTES — the proportion objectui#7399 re-pinned this key to, chosen over "just enough to pass", because a line left with almost no headroom is the defect objectui#8816 records and this key was the tightest of the four before the raise.

REGRESSION_THIS_GATE_MUST_CATCH_BYTES did not move. Nothing else moved: the aggregate was re-read on the same build and passes on its own — 3,589,068 of 3,597,000, headroom 7,932 bytes — so it is left alone.

Proof the raised gate can still fail

Both legs run the same --report invocation, so the red is attributable to the padding and not to the flag. The real report was never mutated — the padding went into a copy.

CONTROL (unpadded copy)   exit 0   ui-components 401.3 KB / 410.2 KB (headroom 8.9 KB)
PADDED  (+15,000 bytes)   exit 1   ui-components 415.9 KB / 410.2 KB (OVER by 5.8 KB)

pnpm exec vitest run scripts/__tests__/check-eager-closure-budget.test.ts — 104 passed.

What the bytes buy — and the part that makes this not optional

Roughly 16 KB gzipped on every console page load, for 51 new icons and a metadata reshape. On its own that is a thin trade.

But the published ranges are already ^1.31.0, which permits 1.43.0. Only this repository's lockfile is holding 1.31.0; anyone installing @object-ui/* fresh today already resolves 1.43.0, already pays these bytes, and already gets the blank DetailView delete icon that trash-2 now produces. This PR does not introduce that cost — it aligns the repository with what consumers already get, and repairs a glyph that is already broken for them.

If the trade still reads wrong, the lever is not this ceiling: it is pinning lucide-react to an exact version across the workspace, which is a policy change and a maintainer's call, not mine.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3504.9 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-B6n3sok8.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 201.97KB 56.22KB
fields (index.js) 246.97KB 62.30KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.54KB 46.34KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.96KB 40.93KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 57.58KB 16.47KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 2.71KB 1.35KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor

维护者速读(终稿)

改了什么:全仓 22 个包声明的图标库 lucide-react 从 ^1.31.0 升到 ^1.43.0(实装 1.35.0 → 1.43.0),并修掉升级带来的两处断裂:懒加载图标的路径数据换了导出名(不修则所有按名字解析的图标变成空框),上游退役的图标名 trash-2 改为 trash(图形相同)。按您 2026-09-20 的裁决 C,恰好放宽两条测试钉子,各在原位写明原先拒什么、为何不再拒、仍拒什么。受管面只碰 skills/objectui/guides/plugin-development.md 代码示例里的一个版本号。

为什么改:上游 1.43.0 改了图标数据结构并给图标加了别名 class,我们的懒加载图标拿不到别名。您选了 C:接受这个差异、落地升级,而不是把别名塞进首屏必载的名单(A)或停在旧版本(B)。

风险与代价(含回滚):两条「说真话」的钉子被削弱;按名字解析的图标不带 lucide 的别名 class,按别名写的样式只对直接 import 的图标组件生效;客户元数据里若写了 icon: 'trash-2',升级后该图标不再绘制(静默空位、无报错,与既有「退役拼写不绘制」的钉子一致),changeset 已写明。回滚 = revert 本 PR 整体。

席位意见:本席按达档复核 PASS(记录在本 PR 上一条评论)。head bb7d5e7187 是 989330df0f 与 main 的干净合并(树与 merge-tree 一致,无手改),对 main 704e05b0 无冲突,CI 42 成功、3 跳过。建议批准。

你要做的:一条 APPROVED review(os-zhuang 或 hotlong 账号)。作者账号是 os-zhuang,对它请审会被平台拒,故改为把 PR assign 给 os-zhuang、向 hotlong 请审。批准后席位走中继落地,不需要您合并。


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 27, 2026 15:10
@hotlong
hotlong enabled auto-merge September 27, 2026 15:10
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3076.4 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-Dum2NqST.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 555.90KB 132.95KB
core (index.js) 9.57KB 3.81KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 224.98KB 62.48KB
fields (index.js) 259.68KB 65.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 83.58KB 22.75KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 158.35KB 40.47KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6120a57b58199856eb25a2ea411c0e4603570eca

① Derived judgments

Scope of this record: the base-merge hop bb7d5e7187 → 6120a57b58 — merge commit 6120a57b58 of main bff63cc12e into the branch, made through the GitHub web flow (committer GitHub) at 2026-09-27T15:09Z, with no other commit. The increment was reviewed at bb7d5e7187 by record 5853185002 (PASS) and, before it, by the seat ACCEPT 5833734540 at 989330df0f; its derived judgments are adopted here unchanged, because the increment is unchanged, measured first-hand by the director seat on the fetched refs:

  1. The head's tree equals git merge-tree of its two parents, so the merge commit carries no hand edit.
  2. Three-dot against objectui origin/main dac022c4e4: 42 files, +286 / −138 at both heads, and the file set is identical (git diff --name-only at the old and the new head, sorted, differ by nothing).
  3. main moved between the two heads in three PR-owned files — apps/console/package.json, packages/types/src/objectql.ts (PR docs(types): ObjectChartSchema.series says its copy omits the per-series type and names chartType (objectui#10584) #10768's docblock, disjoint from the BulkActionDef.icon example this PR edits) and pnpm-lock.yaml — which is why the machine carry test refuses the hop and this record exists; git text-merged all three clean, and the PR's own delta in each (the lucide-react range, the one docblock line, the 1.43.0 resolution) is what it was.
  4. Everything the earlier records measured stands: no published type, schema or export moves; resolve-icon.ts reads __iconData.node with the __iconNode fallback; IconNode narrowed with NonNullable; class emission unchanged with the per-alias classes deliberately not reproduced (ruling C 5750512894); exactly two pins relaxed with their reasons at the site; trash-2 → trash rewritten in the repository's own uses and stated in the changeset; the one governed hunk is a dependency range inside a code sample in skills/objectui/guides/plugin-development.md.
  5. Mergeability and CI: git merge-tree --write-tree origin/main refs/pull/8941/head at dac022c4e4 exits 0 with no conflict. Check runs on 6120a57b58 at the time this record is rendered are read in ③.

② Semver level

Unchanged from 5853185002: patch on all 23 packages in .changeset/lucide-react-1-43-0-retired-trash2.md; the PR body declares no Clause-② line and this record reads it as no.

③ Boundary flags

  • Governed path (skills/**): Tier H. The landing waits for an authorized APPROVED review from os-zhuang or hotlong (GOVERNED_APPROVERS). This PR's author account is os-zhuang, and GitHub does not let a pull request's author approve it, so the click on this PR is hotlong's.
  • CI on 6120a57b58: rendered while the Test shards, Type Check and Spec Main Shape Gate were still running (32 success, 3 skipped, 10 in progress); the landing pre-check reads them complete and green before ready + auto-merge, and a red on this head returns the PR to its lane.
  • No other open question; the superseded Dependabot PR objectui#7058 is the maintainer's to close after this lands.

Implemented-by: claude/pr-7058-lucide-react-1.41.0
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

Rendered at tier by the director seat (summon #30 续) on the fetched head, adopting 5853185002 for the unchanged increment.

Landing note (folded into this record, one write)

Maintainer's word recorded, and why this PR still shows no APPROVED review. Director seat (summon #30 续) · session_01AsCNgFBs8HCjwhyHQsFbx3 · 2026-09-27T15:31Z.

  • The maintainer, verbatim in the seat chat: 「8941 已核准,我记得核准过多次,你帮我跟进到合并」. Recorded here per the charter.
  • Why none of those approvals is on record: this pull request's author account is os-zhuang, and GitHub does not let a pull request's author submit an APPROVED review on it (the Approve choice is unavailable to the author; only a comment goes through). The reviews list of this PR is empty, and the timeline carries no reviewed event. The governed queue guard (GOVERNED_APPROVERS = ['os-zhuang', 'hotlong']) reads APPROVED reviews only, so a comment or a chat word cannot stand in for the click.
  • What can: an APPROVED review submitted by hotlong, which is the account that merged main into this branch at 2026-09-27T15:09Z (6120a57b58). objectui's main rules set require_last_push_approval: false and dismiss_stale_reviews_on_push: false, so that account may approve after its own push, on any commit, and the approval survives later pushes. Review is re-requested from hotlong in the same batch as this comment.
  • The head moved by that base merge, so a fresh at-tier record is rendered above at 6120a57b58 (the increment is unchanged: same 42 files, +286 / −138, pure merge). Landing pre-checks then: the APPROVED review, CI complete and green on 6120a57b58 (Test shards, Type Check and Spec Main Shape Gate were still running at this stamp), no conflict (git merge-tree clean at dac022c4e4) — after which the director seat readies and arms auto-merge through the relay.

Generated by Claude Code

Merged via the queue into main with commit fb336df Sep 27, 2026
47 checks passed
@hotlong
hotlong deleted the claude/pr-7058-lucide-react-1.41.0 branch September 27, 2026 15:35
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…bjectui#9562) (objectstack-ai#9690)

Part of objectstack-ai#9562

The card reports that `scripts/check-lockfile-dedupe.mjs` returns
different verdicts on a byte-identical `pnpm-lock.yaml` while sitting in
the blocking `Test (shard 1/4)`. This lands the half that is dev work:
**the required lane stops carrying a reading that is not a function of
the repository's bytes.** It does not close the card — the residual is
named at the bottom and is a maintainer decision.

Clause-②: no

## What was actually wrong

`scripts/__tests__/check-lockfile-dedupe.test.ts` ran `node
scripts/check-lockfile-dedupe.mjs` **twice** — a `pnpm dedupe --check`
that resolves against `registry.npmjs.org`. That file is in the `unit`
vitest project, which `vitest.config.mts` includes as
`scripts/**/*.test.ts` and `ci.yml` shards four ways as `Test (shard
N/4)`. All four shards are in `REQUIRED_CONTEXTS` in
`scripts/dependabot-merge-gate.mjs`.

So a live network resolution decided a **required** verdict on every
pull request — including pull requests that touch nothing the reading is
about.

That is exactly what `lockfile-dedupe.yml` was built to prevent. That
workflow is path-filtered to `pnpm-lock.yaml` plus the gate's runtime
closure, and its own header says the filter is what keeps the live
reading out of the required set under objectstack-ai#3523's rule. The unit test walked
around the filter.

## The measurement

Lockfile blob held fixed as the control — `sha256` identical before and
after every leg, checked each time. Only terms **outside** the
repository were varied. pnpm keeps abbreviated and full packuments in
two separate caches, so "partial cache" is a real state and both halves
were tested.

| leg | what varied | exit | verdict |
|---|---|---|---|
| A | warm shared metadata cache | 0 | `VERDICT deduped` |
| B | cold private cache (both halves empty) | 0 | `VERDICT deduped` |
| C | abbreviated warm, full-metadata evicted | 0 | `VERDICT deduped` |
| D | full-metadata warm, abbreviated evicted | 0 | `VERDICT deduped` |
| E | same bytes, registry unreachable | **2** | `VERDICT could not take
a reading` |

Exit 2 is what the checker documents as "never a pass", and the old
assertion was a bare `expect(status).toBe(0)` — so leg E reds a required
context, and the failure the reader sees points at `pnpm dedupe` and
tells them to commit the lockfile. That advice is wrong there, and the
lockfile is shared by every open pull request.

**Honest limits on this.** Legs A–D did **not** reproduce the card's
red, and I did not identify the card's mechanism — the card's red
printed `VERDICT not deduped`, mine prints `VERDICT could not take a
reading`. What is reproduced is the **class**: byte-identical repository
input, two different verdicts, decided by a term the repository does not
contain. Also note the current lockfile blob is `b57d9644` and the
card's was `4e954308`, so this is the same property measured on a later
tree, not a replay of the card's run.

## The change

One file. The live reading stays where the repository already put it —
the path-filtered `Lockfile Dedupe Check` context, **still classified
BLOCKING** in `OPTIONAL_CONTEXTS`, untouched.
`scripts/dependabot-merge-gate.mjs`, `lockfile-dedupe.yml` and the
checker itself are all unmodified.

The test file now drives the **shipped** script through a stubbed `pnpm`
on `PATH`, following the convention `check-doc-snippet-types.test.ts`
already uses. The stub records its argv, and **every** run asserts that
recording exists — so a spawn that reached a live pnpm fails on the
control instead of quietly going to the network.

**Coverage goes up, not down.** The two live legs only ever exercised
the GREEN path through `main()`. Replacing them covers:

- the finding path — exit 1, package names, the `::error title=`
annotation, and that a finding never reaches stdout;
- the could-not-run path from a real captured registry failure — exit 2,
and explicitly **not** 1, so a crash can never be reported as a lockfile
finding;
- a non-zero exit with no output at all;
- the `dedupe --check` argv. This replaces the old `does not rewrite the
lockfile it judges` leg with its **cause**: the old leg could only catch
a dropped `--check` if the run happened to rewrite something, while this
catches it always.

## Ablation — both legs mutated on disk, both restored and proven

Run from the committed state; each mutation proved on disk by blob-hash
inequality against its `HEAD` blob before the suite ran, each restore
proved by blob-hash equality plus an empty `git diff HEAD`.

- **Remove the stub from `PATH`** so the real pnpm serves the run: `6
failed | 8 passed`, control message `the stub did not serve this run`
fired. Every hermetic test reds on the control rather than silently
going live.
- **Collapse `classify()` so a crash reads as a finding** (`return
'findings';`): `3 failed | 11 passed` — the checker's own `--self-test`,
the registry-unreachable leg and the signal-killed leg. The new
assertions discriminate the exact defect class.

A first attempt at the mutation was a `perl` no-op; the on-disk hash
check caught it and refused to run the suite, which is why it is there.

## Verification

- `vitest run --project unit
scripts/__tests__/check-lockfile-dedupe.test.ts` — `14 passed`,
**1.03s**. The two live legs alone cost 21–31s each warm here, and 71s
with pnpm's retry backoff when the registry was unreachable.
- `vitest run --project unit scripts/__tests__/` — `170 passed | 2
skipped (172)` files, `4891 passed | 2 skipped` tests.
- `tsc -p tsconfig.scripts.json --noEmit` — exit 0.
- Gates, each exit 0: `check:control-bytes`, `check:new-line-citations`,
`check:test-path-roots`, `check:shell-escape-residue`,
`check:entry-guard`, `check:pre-install-import-graph`,
`check:comment-mask-corpus`.
- `check-governed-queue-guard.mjs --test` on the changed path: `NOT
GOVERNED`.
- ESLint, narrowed and declared: `eslint --no-inline-config --format
json` on the one changed file, 0 errors / 0 warnings, run at
`94d803cb8`. Population read from `eslint.config.js` itself, which
configures **no** `project` / `projectService` — type-aware linting is
not enabled, so a rule's verdict on a file is a function of that file
alone and this one-file diff cannot move the verdict on any untouched
file. File count `1` read from the `--format json` output, not assumed.
The repo-wide `eslint .` run belongs to CI.

## Changeset

None. The gate's own verdict line on this branch:

```
Compared the working tree with 61b7553 (merge-base with origin/main): 1 file(s) changed,
0 of them published source of a package the release covers, 0 of them a manifest whose
published contract moved, 0 under a package changesets ignores, 0 changeset(s) added.
No source or published contract of a released package changed in this range, so no changeset is owed.
```

## Acceptance notes

**What is left of objectstack-ai#9562, and why it is not in this pull request.** The
live reading in `Lockfile Dedupe Check` is still registry-dependent: on
a pull request that moves `pnpm-lock.yaml`, a registry outage still reds
it via exit 2. That is deliberate and documented in the checker — "a
reading that could not be taken is NOT a deduped lockfile" — and the
only two ways out are `pnpm dedupe --check --offline`, which reds on a
cold runner and so trades one spurious red for another, or relaxing the
could-not-run verdict, which **is** a relaxation and per the dispatch is
a stop-and-report rather than a call made inside this card. Flagged, not
taken.

**A detection-latency change this makes, stated plainly.** Before, every
pull request re-asserted that `main`'s committed lockfile is still
deduped. Now that assertion runs on pull requests that touch the
lockfile or the gate's runtime closure. This is the path filter's own
documented argument — "a pull request that does not touch
`pnpm-lock.yaml` cannot change whether that lockfile is deduped, so the
filter costs no coverage" — and a pull request that could break the
property still meets the blocking gate. The direction it does change: if
the property ever drifted **without** the lockfile moving, it would now
be caught on the next lockfile-touching pull request instead of the next
pull request of any kind. Leg E shows the *verdict* can move without the
lockfile moving, so that premise is worth a maintainer's eye; it is
noted here rather than resolved.

**noted, not filed:** pnpm retries registry failures with backoff (10s,
then 1 minute) before giving up — leg E took 71s for that reason. Inside
a 20-minute job with an ~800s suite, a sustained registry problem spends
that budget before failing. Not filed: it is a property of pnpm's retry
policy, not a defect in this repository, and the carrier is the
`Lockfile Dedupe` workflow's own timeout derivation, which already flags
itself for re-derivation once the workflow has run history.

**noted, not filed:** `pnpm-lock.yaml` is currently held by objectstack-ai#8941 and
was deliberately not touched.

## Constraints honoured

- No deduped `pnpm-lock.yaml` committed; the lockfile is not in this
diff.
- No test skipped, disabled or quarantined; the file grew from 7 tests
to 14.
- No CI re-run performed to "confirm a flake".
- No new workflow and no new required check; `.github/workflows/ci.yml`,
`lint.yml` and `scripts/dependabot-merge-gate.mjs` are untouched and
stay with objectstack-ai#9584.
- No force-push, no history rewrite.

---
_Generated by [Claude
Code](https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ag half lost (objectui#7992) (objectstack-ai#9688)

Part of objectstack-ai#7992

## What changed — prose only

`packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts`
labelled a pair of
`SpecObjectListViewSchema` refusals with a single source line.

**Before:**

```js
    // schema-reference.md: an ObjectQL tuple filter and a `default: true` flag.
```

**After** — the label now covers only the half that still has a doc
side, and a second
comment says what the other half is:

```js
    // schema-reference.md: an ObjectQL tuple filter. Only `tuple` still has a
    // doc side, and that side is HELD — the "still what the docs teach" test
    // below pins this exact filter string off disk.
```

```js
    // `flagged` has NO doc side, and its removal was deliberate: objectui#7923
    // (PR objectui#7991) deleted the per-view `default: true` flag from that
    // same `object-view` example, because `NamedListView` never declared the
    // key and `ObjectView` never read it. What is left is a refusal pinned on
    // its OWN merits — the spec closes `default` too, which is why that doc fix
    // is not merely cosmetic — so this case is no longer a reading of any shape
    // the docs teach. ⚠️ Nothing re-derives that absence: read the sentence
    // above as history, not as a live reading of that file.
```

⛔ No assertion added, removed or weakened. Mechanically: every changed
line in the diff is a
`//` line — `git diff -U0 | grep '^[+-]' | grep -v '^[+-]\s*//'` (after
dropping the `+++`/`---`
headers) returns nothing. The `flagged` assertion is kept because it
still measures something
real, and the sibling `readRepo('content/docs/api/schema-reference.md')`
assertion — the one that
still HAS a live doc side — is untouched.

## The doc-side measurement, with a control that FIRES

A bare "0 hits" would prove nothing here, and the dispatch's own first
probe is the demonstration:
`"default":` — with the colon, JSON-quoted — also scores **0** across
`content/docs`, so that
pattern matches nothing anywhere and a zero is a fact about the pattern,
not about the tree.
Readings below taken at `d83ea4160`, this branch's head; a historical
reading, ⛔ not re-derived by
anything and ⛔ not to be read as live.

| probe | reading |
|---|---|
| SUBJECT — `"default": true` across `content/docs` | **0** |
| ⚠️ blind-pattern check — `"default":` across `content/docs` | **0** —
why the subject alone proves nothing |
| CONTROL A — `"filter":` in `content/docs/api/schema-reference.md` |
**2** — the JSON-quoted key style DOES match in this file |
| CONTROL B — the word `default` in that same file | **34** — a zero for
the flag is not a file-wide blind spot |
| CONTROL C — `"filter": [["owner", "=", "${currentUser.id}"]],` in that
file | **present**, inside the `object-view` example — the tuple half's
doc side, and the string the sibling assertion pins |

⇒ The flag is genuinely gone, on controls that fire. What stands in its
place in that example is a
node-level `"defaultListView"` key — PR objectui#7991 spelled the
default list view once, at the
node, instead of as a per-view flag `NamedListView` never declared.

## Verification

| check | reading |
|---|---|
| `pnpm vitest run` over the changed test file | exit 0 — 1 file, 164
tests passed |
| `pnpm --filter @object-ui/types test` (via the shared verify lock) |
exit 0 — 198 files, 4656 tests passed |
| `pnpm --filter @object-ui/types type-check` | exit 0 |
| `eslint .` in `packages/types` | exit 0 — 269 files linted, 0 errors |
| `node scripts/check-changeset-presence.mjs` | exit 0 — the
empty-frontmatter declaration is this gate's own explicit exemption,
quoted in its output |
| `pnpm changeset:check` | exit 0 |
| `pnpm check:new-line-citations` | exit 0 — 0 new citations |
| `pnpm check:control-bytes` | exit 0 |
| `pnpm check:changeset-claims` · `pnpm
check:pending-changeset-literals` | exit 0 |
| `node scripts/check-governed-queue-guard.mjs --test` over both changed
paths | NOT GOVERNED — ordinary review and merge-queue route |

Every exit code above was captured before any pipe (`cmd > log 2>&1;
EXIT=$?`), not through one.
The lint reading is the whole affected package rather than a narrowing;
type-aware linting is not
enabled in this repo's eslint config either way (no `project` /
`projectService` key), so this diff
cannot move the verdict on a file it does not touch. The repo-level
`turbo run lint` belongs to CI.

## Changeset

`.changeset/issue-7992-unmirrored-keys-doc-side.md`, EMPTY frontmatter —
declared as releasing
nothing. Followed the gate rather than guessed:
`check-changeset-presence.mjs` scored the test file
as published source of a released package and demanded a declaration,
and names the empty
frontmatter as "a pass, not a workaround".

## Scope

- ⛔ The docs are NOT touched: objectui#7923 removed that flag
deliberately, and restoring it to
  make the old comment true again is the wrong direction.
- ⛔ objectui#7721's class — prose inside a pin stating a measurement
that has gone stale, with
nothing holding prose to measurement — is NOT closed here. This card is
one instance of it.
- ⚠️ The card's own address for the comment has drifted (the card cites
an older line number). It
is located by content — the quoted line above — and per AGENTS.md objectstack-ai#11
the repair is not minted
into a new line address here. Reported, not fixed: this is a
scope-pinned card, so objectstack-ai#11's
  opportunistic citation repair does not apply.
- ⚠️ PR objectstack-ai#8941 is open on `content/docs/api/schema-reference.md` (a
lucide icon rename in the
detail-view example). Re-measured: it touches neither the `object-view`
example nor `default`.
  No overlap with this branch, which does not touch that file at all.

---
_Generated by [Claude
Code](https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… pronoun blindness (objectui#9727) (objectstack-ai#9752)

Fixes objectstack-ai#9727

Clause-②: no

The first step on this card is not repairing 53 changeset entries. It is
rebuilding the instrument, because both tables the card carries are
transcribed and the filing seat marked them NOT MEASURED: the producing
script was cleaned up with its author's worktree, so no number on that
card is re-derivable. **This pull request edits no changeset body.** It
adds a measurement instrument, its pins, and one adjudication.

## The rebuilt instrument

`pnpm census:changeset-polarity`
(`scripts/changeset-polarity-census.mjs`), report-only, wired into no
workflow — `census:*` is this tree's spelling for runnable, reported,
not blocking.

**P, the population.** Every sentence of every pending `.changeset/*.md`
except `README.md`, matched when the sentence names a schema symbol and
carries a present-tense declaration verb. Bare past `declared` is
excluded on purpose: a historical sentence cannot rot, which is the
distinction objectui#9713 turns on. Present-tense passive (`is
declared`) is included, because it is a live claim and rots like the
active voice.

**V, the verdict.** For each (schema, key) pair a matched assertion
predicates, membership is resolved against that schema's member set and
compared with the sentence's polarity. A positive claim is contradicted
when the key is absent; a negative claim when it is present.

Four properties the rebuild has that the transcribed instrument did not:

1. **Whole-file, whitespace-tolerant read.** This corpus wraps near
eighty columns, so a line-anchored probe is structurally blind to any
claim that wraps — and a claim naming a schema *and* a key is long
enough that most of them do. Lines are joined per paragraph,
continuation prefixes (blockquote markers, list bullets, table pipes)
stripped first, all whitespace runs collapsed before a sentence is cut.
Backticked spans are masked during the cut so `foo.md` never splits a
sentence.
2. **Assertion position distinguished from quoted position.** A count
over prose is *not* invariant under quotation, and treating it as
invariant is how a repaired site gets re-flagged forever.
objectui#9713's repair quotes, in guillemets, the very sentence it
retires. A sentence reached through a fence, a blockquote or quotation
marks is counted, reported, and never flagged. Measured on this tree:
that repaired 8802 sentence is the corpus's **one** quoted match, and it
draws **zero** flags.
3. **A name is not a key — a key is (interface, name).** Membership is
built with the TypeScript parser over `packages/types/src`, across both
published faces (the `interface` and its zod mirror), with `extends`
resolved transitively. The zod walk is **structural**: only the literal
passed to `z.object()` / `.extend()`, the arms of a union, and the base
of a `.merge()` / `.and()` count. A schema named in a member's *value*
(`columns: z.array(ObjectKanbanLaneSchema)`) is a different object —
walking into it read a lane's `cards` as a member of the *board*, which
is the over-approximation this rule forbids, and fixing it is what
removed those flags. An inherited index signature (`[key: string]: any`)
is deliberately **not** membership: a key riding it is admitted and
never examined, which is the condition the whole family of cards is
about.
4. **The cross-sentence pronoun** — below.

## The blindness closed, and its pin

The transcribed instrument missed **the site that motivated the card**.
The claim in
`.changeset/8802-8257-8008-kanban-gantt-family-retirement.md` reads,
across a sentence boundary:

> ... the only one that refused `allowCollapse` / `cardTemplates` /
`columnWidths` / `titleField` / `draggable` / `onColumnAdd` /
`onCardAdd` by name, and — through `columns: KanbanColumn[]` — the only
one that judged a lane's `cards`. **The surviving `ObjectKanbanSchema`
face declares none of them.**

The object is `them`. A matcher that reads only the sentence carrying
the verb finds a schema, finds no key of its own, and reports nothing.
An instrument blind to the shape that produced it reports a **floor**,
not a census.

The rebuild resolves a pronoun object (`them`, `these`, `those`, `none
of them`, `either`, `both`) from the nearest preceding sentence in the
same paragraph that names any key. The pin lives in
`scripts/__tests__/changeset-polarity-census.test.ts` and carries the
**pre-repair** text of that site as a fixture, so it survives a shallow
clone where `adf581278` is unreachable.

⭐ **The pin is stronger than "found".** Run against that pre-repair
text, the rebuilt instrument returns exactly `columns`, `cardTitle`,
`titleField`, `allowCollapse` — the four keys objectui#9713 adjudicated,
and no others. The other keys that sentence names are genuinely absent
from the surviving face, so a negative claim about them is *true* and is
correctly not flagged. Reproducing a known-good human adjudication from
a fixture is the strongest available evidence that the rebuild reads
correctly. Both halves are asserted: the flags are found, and every one
of them is reached `viaPronoun`.

## The re-derived count, against the card's 154

Read these from a run, not from this paragraph (commandment objectstack-ai#9). At
`1cfdff814` plus this branch, `pnpm census:changeset-polarity` prints:

| | card (transcribed, `dea17b469`) | this rebuild |
|---|---|---|
| entries scanned | 1642 | 1654 |
| matched, assertion position | 199 across 169 | 218 across 176 |
| matched, quoted position | not distinguished | 1 (reported, never
flagged) |
| object resolved across a sentence boundary | 0 — the blindness | 9 |
| **candidate contradictions** | **154 across 53** | **134 across 58** |
| claims naming a schema this tree does not declare | not reported | 61
|

Controls, same corpus and same instrument, on every run — the run exits
2 and voids its own numbers if either fails:

| control | probe | reading | expected |
|---|---|---|---|
| corpus lit | present-tense declaration verb | 931 | > 0 |
| corpus absent | `zzqqNoSuchMemberZZ` | 0 | 0 |
| member lit | `BaseSchema.type` | 1 | 1 |
| member absent | `BaseSchema.zzqqNoSuchMemberZZ` | 0 | 0 |

**It differs from 154, and it differs in both directions.** Flags are
**down** (154 → 134) while entries are **up** (53 → 58), which is not a
contradiction — they move for different reasons:

- **Down**, from precision. A backticked PascalCase token is a *type*
name (`ObjectGridComponentProps`, `GanttConfig`), never a key, and
reading one as a key inflated the count. The structural zod walk stopped
a referenced schema's keys counting as the referrer's. And a sentence in
quoted position is no longer flagged at all.
- **Up**, from sensitivity. The whole-file whitespace-tolerant read and
the pronoun resolution both find sites a line-anchored, sentence-local
matcher could not see — nine flags on this tree are reached across a
sentence boundary alone. The corpus also grew by 12 entries between the
two readings.

⛔ 154 was never reproduced by construction, and the rebuild was not
tuned toward it. The two numbers are readings of different instruments
over different trees, and the card's own statement stands: its number
was a floor.

## The ten adjudications

Sample rule, fixed before looking: **the first ten distinct flagged
entries in sorted filename order**. No cherry-picking. Seventeen flags
across those ten entries.

For each, the first question is whether the flag is a **true positive**
at all. Only a true positive can be ROTTED or BORN FALSE — a sentence
that is *true today* is neither, and saying otherwise would be a false
record of its own. Write-time refs are given for all ten so any reader
can re-derive; all ten lie inside this clone's shallow window (boundary
`c35fed098`, 2026-08-07), and every reading here is an affirmative one,
so no negative leg needs a control.

| # | entry | flags | verdict | established at |
|---|---|---|---|---|
| 1 | `5632-svg-host-dom-passthrough.md` | `SpinnerSchema.icon`,
`SpinnerSchema.color` | **TRUE POSITIVE → BORN FALSE** | write-time ref
`e304a4ef7` |
| 2 | `6011-tosortitems-order-spelling.md` | `SortUISchema.toSortItems`,
`.direction` | false positive ×2 | `c5fbe0b99`; true at `1cfdff814` |
| 3 | `6051-gantt-flat-config-declared-keys.md` |
`ObjectGanttSchema.startDateField`, `.endDateField`, `.titleField` |
false positive ×3 | `75bd83d6d`; true at `1cfdff814` |
| 4 | `6067-component-meta-derive-from-canonical.md` |
`ComponentMetaSchema.tier`, `.namespace`, `.skipFallback`, `.labelling`
| false positive ×4 | `44d075ba0`; true at `1cfdff814` |
| 5 | `6121-retire-report-data-source.md` | `ReportComponentSchema.data`
| false positive | `6414dfd45`; true at `1cfdff814` |
| 6 | `6132-undeclared-action-props.md` |
`DropdownMenuSchema.onOpenChange` | false positive | `e28fbf92f`; true
at `1cfdff814` |
| 7 | `6150-undeclared-but-consumed-keys.md` | `TreeViewSchema.value` |
false positive | `2c45966ff`; true at `1cfdff814` |
| 8 | `6169-chatbot-authoring-face-type.md` | `ChatbotSchema.chatbot` |
false positive | `52a43ded8`; true at `1cfdff814` |
| 9 | `6235-mergedsort-wrap.md` | `ObjectGridSchema.sort` | false
positive | `9caa7d474`; true at `1cfdff814` |
| 10 | `6247-option-visiblewhen-metadata-admin.md` |
`SelectOptionSchema.unrecognized_keys` | false positive | `0ea559e7c`;
true at `1cfdff814` |

**Entry 1, the one true positive, in full.** The sentence is:

> `IconSchema` and `SpinnerSchema` declare only `icon` / `size` /
`color`, and both renderers already consume all three by name, so the
SDUI pass-through list withholds nothing they need.

On `main` at `1cfdff814`, `SpinnerSchema` declares `type` and `size`,
plus `body` / `children` as retirement tombstones. It declares neither
`icon` nor `color` — those are `IconSchema`'s. The sentence is false of
`SpinnerSchema`.

**The verdict is BORN FALSE, and it is established at the write-time
ref, not today.** At `e304a4ef7` (2026-09-03), the commit that added
this entry, `SpinnerSchema` was exactly `{ type, size }`. Nothing later
falsified the sentence; it was false the day it was written. Re-derived
mechanically by running the rebuilt census with `--corpus` and `--types`
both taken from that ref — two flags, `SpinnerSchema.icon` and
`SpinnerSchema.color`, with all four controls passing at that ref.

⚠️ That historical run is also what found a defect **in this
instrument**, and it is recorded because it is this card's own class
wearing the instrument's clothes. The member lit control was first
spelled `ObjectKanbanSchema.groupBy` — a member objectui#7322 added. At
any ref older than that card the control read 0, the run exited 2, and
every historical reading voided itself — and a historical reading is
*exactly* what establishing BORN FALSE requires. **A control pinned to a
fact a later card moves is the defect this card is about.** It is now
`BaseSchema.type`, the protocol's own recursion point, and the choice is
pinned with its reason.

**The nine false positives, and the three limits they name.** All are
recorded in the script header so the next reader does not re-derive
them:

- **Window pairing** (entries 3, 4, 7, 8, 9, 10). The sentence names
schema S and key K and predicates K of something else: a registry-local
type (`ComponentMeta` in `Registry.ts`, objectstack-ai#4), another node's schema
(`TextSchema.value`, objectstack-ai#7; the `list-view` node's `sort`, objectstack-ai#9), a spec
schema (`GanttConfigSchema`, objectstack-ai#3), a registration name rather than a key
(`chatbot`, objectstack-ai#8), or a zod issue code rather than a key at all
(`unrecognized_keys`, objectstack-ai#10). This is the limit the card's own author
stated first and it is still the largest source.
- **Polarity by keyword** (entries 3, 5, 6, 9). Polarity is read from
negation words anywhere in the sentence, so an unrelated clause inverts
the verdict. Entry 6 is the clearest: *"None of the three menu schemas
declares any event slot (`DropdownMenuSchema` declares `onOpenChange`
and nothing else)"* — the parenthetical asserts exactly what the member
set says, and `None` flipped it.
- **Top-level membership only** (entry 2). `SortUISchema` declares
`sort?: Array<{ field; direction }>`, so `direction` is a member of the
inline sort *item*, not of the schema — and *"`direction` is the key
`SortUISchema` legitimately declares"* is true in context. Descending
into inline member objects would re-create the "a name is not a key"
failure one level down, so the narrow reading is deliberate and the cost
is named rather than paid silently.

⛔ None of the three is a reason to stop reporting a flag. They are the
reason a flag is a **candidate**: each is resolved by a human reading
the sentence, and none is resolvable by reading the count.

## Recommendation on the grade condition

Triage's checkable condition was: true-positive rate materially non-zero
⇒ defect list, p2 confirmed, splittable; ≈ 0 ⇒ re-shape the card into
"fix the instrument".

**Measured: 1 of 10 entries, 2 of 17 flags.** That is not zero — there
is a real, previously unrecorded BORN FALSE claim in the sample, and it
is a *second* instance of a class objectui#9713 had already established
twice. So the existence question stays answered and `priority:p2` is
right.

But it is nowhere near a defect list, and **⛔ the number must stop being
carried as one**. At the measured yield, splitting the 58 flagged
entries into dispatchable items would dispatch roughly fifty no-ops,
each costing a history read — which is the same measurement error in a
new coat.

⇒ **Re-shape the card to "fix the instrument, then triage its output".**
Concretely: the instrument now exists, is re-derivable from a clean
checkout, and its three false-positive sources are named and
individually attackable — polarity-by-keyword is the cheapest and would
clear four of the nine. The 154 (now 134) should be carried as
*"whatever `pnpm census:changeset-polarity` prints"*, never as a figure
in prose, which is commandment objectstack-ai#9 applied to this card. The one true
positive found here is a candidate for its own small card; ⛔ it is
**not** repaired in this pull request, because the fence on this
dispatch is absolute.

## Boundaries honoured

- ⛔ **No changeset body was edited** — not one of the 53, not any other.
The only `.changeset/` file this branch touches is the one it adds.
- ⚠️ `.changeset/6051-gantt-flat-config-declared-keys.md` is in the
candidate set and is adjudicated above as a **reading only**. It is
being repaired right now by objectui#9743 on another branch, and nothing
here touches it. (`.changeset/5903-objectgantt-declared-keys.md` left
the candidate set entirely once PascalCase tokens stopped reading as
keys.)
- ⛔ No published face, mirror, declaration or accept set moved. No
issues created. No labels written.
- Open-PR intersection re-taken on this branch rather than inherited:
nine open pull requests, of which four touch `scripts/` (objectui#9748,
objectstack-ai#9584, objectstack-ai#9488, objectstack-ai#8941) and four touch `.changeset/` (objectstack-ai#9540, objectstack-ai#9391, objectstack-ai#8941,
and the release PR objectstack-ai#5400). None touches any path this branch adds or
modifies. The one shared file is `scripts/markdown-test-inputs.mjs`,
which this branch adds one ledger row to and no open pull request edits.

## The changeset grade — measured, not assumed

`.changeset/9727-changeset-polarity-instrument-rebuild.md` carries
**empty frontmatter**, which is the explicit claim that nothing
published moved. That claim is a gate reading:

- **absent control / the real diff** — `node
scripts/check-changeset-presence.mjs` on this branch: *"10 file(s)
changed, 0 of them published source of a package the release covers, 0
of them a manifest whose published contract moved"*, exit **0**.
- **lit control, same tree, same gate** — appending one comment line to
a file under `packages/types/src` flips the same gate to exit **1** and
demands an entry. Reverted in the same run and proven byte-identical by
`git hash-object` before and after (`bcebba9b9` both sides), with `git
diff HEAD` empty.

Two zeros on one instrument would mean a broken instrument. This is one
zero against a firing control.

## Gates

Exit codes captured to disk before any pipe.

| gate | exit |
|---|---|
| `node scripts/check-changeset-presence.mjs` | 0 |
| `node scripts/check-changeset-overwrite.mjs` | 0 |
| `pnpm changeset:check` | 0 |
| `pnpm check:changeset-claims` | 0 |
| `pnpm check:pending-changeset-literals` | 0 |
| `pnpm check:control-bytes` | 0 |
| `pnpm check:governed-queue-guard` | 0 |
| `pnpm check:new-line-citations` | 0 |
| `pnpm check:test-path-roots` | 0 |
| `pnpm check:required-check-set` | 0 |
| `pnpm lint:coverage` | 0 |
| `pnpm check:lint-rule-coverage` | 0 |
| `pnpm check:esm-specifiers` | 0 |
| `pnpm check:node-esm-load` | 0 |
| `pnpm type-check:scripts` | 0 |
| `pnpm exec eslint` on the three added source files | 0, three files
linted |
| `pnpm exec vitest run` — the new suite plus `markdown-test-inputs`,
`check-changeset-presence`, `check-changeset-claims` | 0 |

The governed-surface guard was run at
`scripts/check-governed-queue-guard.mjs`, not under `scripts/pm/`.

## The declared file surface, and the two increments beyond it

The dispatch declared "a script under `scripts/` plus its test, and a
new `.changeset/9727-*.md`". Two files land outside that, both required,
both named here rather than left unexplained:

- **`package.json` (+1 line)** — the `census:changeset-polarity` entry.
Without it the instrument has no invocation the tree knows about, and
`census:*` is the spelling that carries "runnable, reported, not
blocking". It is a `scripts` key: not one of the eight publish-contract
fields, and the root manifest is private, which is part of why the
changeset grade measures as nothing-published.
- **`scripts/markdown-test-inputs.mjs` (+10 lines)** — one ledger row.
That ledger asserts every tracked markdown document is an input to some
test, and the six fixture documents this change adds would otherwise be
uncovered and red it. The row declares what the new test reads and that
it walks a markdown tree; the scanner re-derives the same answer from
the source, which is why the test spells its fixture paths as literal
segment runs.

Both are additive and neither is touched by any open pull request.

## The fixture names ARE the trap list

They encode the failure modes this lane actually hit, not a happy path:

| fixture | the trap it holds open |
|---|---|
| `01-pronoun-pre-repair.md` | the proven false negative — a claim whose
object is a cross-sentence pronoun, carried at its pre-repair text |
| `02-quotation-position.md` | a count is **not** invariant under
quotation; a repaired sentence quoted in its own retirement note must
never be re-flagged |
| `03-wrapped-assertion.md` | prose wraps near eighty columns, and it
carries its own **dark leg**: the pin asserts a line-anchored probe
reads nothing on that exact file, so the lit reading proves the
tolerance rather than assuming it |
| `04-past-tense-only.md` | a historical sentence cannot rot and stays
out of the population |
| `05-referenced-schema-scoping.md` | a name is not a key — a referenced
schema's members are its own |
| `06-index-signature-is-not-membership.md` | a key riding `[key:
string]: any` is admitted and never examined; counting it as declared
erases the finding |

## ⭐ A hand-typed entry guard, caught by CI and fixed — the same defect,
in this instrument

The first push carried a hand-typed `process.argv[1]` entry guard, and
`check:entry-guard` failed the Lint job on it. **It is this script's own
defect class.** Node resolves symlinks for the module graph but leaves
`process.argv[1]` as the caller typed it, so a census reached through a
symlink compares two different paths, answers false, does nothing, and
exits **0 with no output** — a clean-looking zero from an instrument
that never ran, inside an instrument whose entire purpose is to stop
false zeros being reported as measurements. A wrapper holding only
`result.status` cannot tell that apart from a pass.

It now routes through the one predicate, `isEntrypoint` from
`scripts/invoked-as.mjs`, with the reason recorded at the guard. ⛔
`KNOWN_HAND_TYPED_GUARDS` was not touched — it is shrink-only, and
adding a baseline line would have been the false record rather than the
fix.

| | exit |
|---|---|
| `node scripts/check-entry-guard.mjs` **before** | **1**, naming
`scripts/changeset-polarity-census.mjs` |
| `node scripts/check-entry-guard.mjs` **after** | **0** — 103 scripts,
0 hand-typed guards |
| `node scripts/check-entry-guard.mjs --self-test` | 0 — 63 cases |
| `node scripts/invoked-as.mjs --self-test` | 0 — 12 cases, real
symlinks |
| census re-run after the fix | 0, controls PASS, reading unchanged |
| the instrument's own suite re-run | 0 — 47 pins |

The remaining Lint-job guard steps were run locally rather than eslint
alone: `check-lint-coverage`, `check-entry-guard`,
`check-upstream-port-parity`, `check-bash32-floor`,
`check-vi-mock-override-shape`, `check-test-path-roots`,
`check-cross-repo-closer-outcome` — all exit 0.

⚠️ One gate is **NOT MEASURED** here and is not this change's: `pnpm
check:node-esm-load` (its own workflow, not Lint) exits 1 in this
container because turbo shares one cache across every worktree of a
checkout and two entries — `@object-ui/auth`, `@object-ui/react-runtime`
— were replayed from a sibling agent's worktree. The gate refuses to
grade artifacts this tree did not produce, which is correct behaviour;
the refusal is about the shared cache, not about this diff, which
touches no package source.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…nitialValues`, `initialData` and `submitBehavior` (objectstack-ai#9759)

Refs objectstack-ai#8071

Clause-②: no

objectui#8071 slice 10. Three `object-form` keys leave
`MEMBER_PIN_EXEMPTIONS` and gain real per-block member pins.
`MEMBER_PIN_EXEMPTION_CEILING` follows **31 -> 28** in the same commit,
as every slice of that card owes.

## ⚠️ First: the dispatch's own premise was FALSIFIED, and the card body
is the reason

This batch was dispatched to convert the three "near-miss" keys the card
body flags — `object-form.fields`, `object-grid.exportOptions`,
`object-grid.bulkActions` / `bulkActionDefs` — plus
`page:accordion.items`, `page:tabs.items`, `record:path.stages`,
`record:alert.action`, `element:button.action` as follow-on candidates.

**All nine have been pinned since slices 1-7.** Measured on
`origin/main` `f73521f93` by TypeScript AST walk of the constant, not by
reading the card:

```
MEMBER_PIN_EXEMPTIONS on origin/main = 31, in five blocks
  object-grid.                15
  object-form.                 7
  object-master-detail-form.   6
  object-kanban.               2   (held: `dataSource` is not a block member at all)
  record:related_list.actions  1   (NO_READ_SITE_TO_PIN — a different debt)
```

The card body's enumerated 58-key list has been stale since PR objectstack-ai#8737
merged on 2026-09-09, and issuecomment-5599611864 already recorded that
the authoritative list is the constant, never the card's prose. It has
now mis-aimed a dispatch a second time. That note is repeated inside the
constant's own docblock in this PR so the next dispatcher reads it where
the numbers live.

⇒ the batch was re-selected from the live table. `object-kanban` (held
on a stated question) and `record:related_list.actions` (a different
debt) were left alone; `object-form` was taken because it is the
smallest of the three open multi-key blocks.

## What was converted, and the renderer read pinned for each

### `object-form.initialValues` and `object-form.initialData` — NEW file


`packages/plugin-form/src/__tests__/objectFormInitialMembers-8071.test.tsx`

Read site: `ObjectForm.tsx`, the two sites that seed `initialData` state
(the inline-fields effect and the create branch of `fetchInitialData`),
both spelled `setInitialData(schema.initialData || schema.initialValues
|| {})`.

1. a MEMBER is a FIELD NAME and its value is that control's opening
value;
2. ⭐ the two keys are chosen between **as whole objects** — `||`, never
a merge. With both authored, **every** `initialValues` member is
dropped, including the ones `initialData` says nothing about. Nothing
declared distinguishes that from `{ ...initialValues, ...initialData }`,
which is what "alternate spelling … read FIRST" reads like and what a
per-member precedence would mean;
3. the sharp edge: `||` tests the OBJECT's truthiness, so an **empty**
`initialData` shadows a populated `initialValues` completely;
4. non-vacuity control: with neither key authored the same controls
render empty.

Both keys are registered `type: 'object'` and typed `Record<string,
any>`, so every object parses on both declared sides — the read site is
the whole member contract.

### `object-form.submitBehavior` — EXISTING file, GROWN before it was
registered

`packages/plugin-form/src/ObjectForm.submitBehavior.test.tsx`

This is a genuine near miss, unlike two of the three the card named at
slice 1: its whole subject is the key and it drives the real renderer.
But it left two members of the read unasserted, so it was strengthened
rather than credited as found.

Read site: `ObjectForm.tsx`'s `switch (behavior.kind)`. Every member is
touched once there — `url` and `delayMs ?? 0` on `redirect`, `title` and
`message` on `thank-you` (both guarded on the discriminant, so no other
arm reaches them), and nothing at all on `continue` or `next-record`.

- **`delayMs` is a MEMBER, not a constant.** Collapsing
`behavior.delayMs ?? 0` to a bare `0` removes the readable pause without
changing any destination, so every other row in this file and in
`ObjectForm.submitRedirect.test.tsx` stays green. Pinned on the number
the wait is armed with, with the **same declaration minus the member as
the lit control** — without it, an empty `armed` would be
indistinguishable from a spy that never sees this renderer's timers.
- **`next-record` is a declared arm that reads NO members.** It shares
the confirmation-panel body with `thank-you` by falling through, so an
edit giving it its own arm or dropping it into `continue`'s no-op would
leave a spec-legal authored value doing something else with nothing red.

## Ablations — every pin shown FAILING, restore proven by blob

Each from the committed state, `trap … EXIT INT TERM`, restore by `git
checkout HEAD -- path` (never a bare checkout), on-disk proof read
**before** any result.

| leg | mutation | anchor before/after | result |
|---|---|---|---|
| 1 | `schema.initialData \|\| schema.initialValues \|\| {}` → `{
...(schema.initialValues ?? {}), ...(schema.initialData ?? {}) }` (the
plausible "improvement") | 2 → 0, injected 2 | **exit 1** — rows **3 and
4** red, rows 1/2/5 green **by construction** |
| 2a | `delayMs: behavior.delayMs ?? 0` → `delayMs: 0` | 1 → 0, injected
1 | **exit 1** — the `delayMs` row red, 6 passed |
| 2b | `case 'next-record'` moved into `continue`'s no-op arm | verified
at `:1105-1106` | **exit 1** — the `next-record` row red, 6 passed |

Blobs: HEAD `42b7a9b0855c5a53276af4f1d9b3842ad6a0899d`; mutated
`30e2c464…` / `e50a99be…` / `ce8521db…`; restored `42b7a9b0…` = the HEAD
blob in all three legs, `git diff HEAD` empty each time. Restored runs:
pin A **5 passed**, pin B **7 passed**.

⚠️ Rows 1, 2 and 5 of pin A survive leg 1 **by construction** and the
file says so — a merge and a `||` agree when only one key is authored.
Not every row is presented as if it reddened.

## Exemption ledger — measured by AST, not by arithmetic

TypeScript AST walk (`ts.createSourceFile` → `VariableDeclaration` →
initializer unwrapped through `as` / `satisfies` / parens → **direct**
`PropertyAssignment` children):

```
                                origin/main (f73521f)   this branch
MEMBER_PIN_EXEMPTIONS                    31                   28
MEMBER_PIN_EXEMPTION_CEILING             31                   28
MEMBER_PINS                              60                   63
NEWLY_JUDGED_UNPINNED_MEMBERS             2                    2
```

Exemptions **−3**, pins **+3** — they move together, so the three keys
were **converted**, not dropped. Instrument control:
`nonPropertyAssignmentMembers` is `[]` on both trees, so no spread,
getter or shorthand hides inside a span the counter treats as flat.

`object-form` is **not** closed: `customFields`, `dataSource`, `mobile`
and `sections` remain — the two-bite shape slice 9 said every remaining
block now has. `NEWLY_JUDGED_UNPINNED_MEMBERS` and
`record:related_list.actions` are untouched by this slice.

## Gates — exit codes captured to disk before any pipe

| gate | exit |
|---|---|
| `apps/console` `registry-inputs-spec-parity.test.ts` (the gate this
card serves) | **0** — 199 passed |
| `pnpm --filter @object-ui/console test` | **0** — 101 files, 1158
tests |
| `pnpm --filter @object-ui/plugin-form test` | **0** — 93 files, 902
passed / 1 skipped |
| `pnpm --filter @object-ui/plugin-form type-check` | **0** |
| `pnpm --filter @object-ui/console type-check` | **0** |
| `pnpm --filter @object-ui/plugin-form lint` · `@object-ui/console
lint` | **0** · **0** (0 errors) |
| the repo's 61 `check:*` scripts | **60 × 0**, one caveat below |
| `check-governed-queue-guard.mjs --test` over the four changed paths |
**0** — NOT GOVERNED, so no maintainer-summary section is owed |

⚠️ **`check:node-esm-load` exited 1 on the first run, and it is not this
diff.** Its own provenance leg refused 2 of 37 entries because turbo's
shared cache replayed artifacts built by a *different worktree*
(`/home/user/objectui-issue-9659` — `@object-ui/auth`,
`@object-ui/react-runtime`), neither of which this diff touches. The
script names that condition and prescribes the remedy itself; re-run
with its own `--force-build`: **exit 0, 37 of 37 gradable entries built
by this tree.**

**Typecheck coverage proven, not assumed:** `tsc -p tsconfig.test.json
--listFiles` lists both `ObjectForm.submitBehavior.test.tsx` and
`objectFormInitialMembers-8071.test.tsx`, so the greens are not the
excluded-tests trap.

**Control bytes:** `grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'` over
all four changed files — no match (and `check:control-bytes` exit 0).

## Changeset grade — MEASURED from a build, with both controls

Empty-frontmatter changeset. Both packages were built and their
published `files[]` output grepped:

| marker | corpus | hits |
|---|---|---|
| `objectFormInitialMembers` | `packages/plugin-form/dist` | **0** |
| `Saved — next one`, `from initialValues` | `packages/plugin-form/dist`
| **0** |
| `MEMBER_PIN_EXEMPTION_CEILING`, `AWAITING_A_PIN` | `apps/console/dist`
| **0** |
| `submitBehavior` (positive control) | `packages/plugin-form/dist` |
**7 files** |
| `object-form` (positive control) | `apps/console/dist` | **12 files**
|

Two zeros with a lit control on the same instrument and corpus. One
near-miss chased down: `registry-inputs-spec-parity` matches **1** file
in `apps/console/dist`, and it is `vendor-objectstack-*.js` —
`@objectstack/spec`'s own prose naming this gate file, not a byte of
this diff.

## Serial constraint — re-taken immediately before the first write

Enumerated over the **11** open PRs (the dispatch's table listed 3; this
lane's tables have been short before). Intersection at FILE level:
**empty**. Nothing open touches
`apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` or any
`packages/plugin-form/src/**`. PR objectstack-ai#8941 touches
`packages/plugin-form/package.json` only. Root `package.json` is held by
PR objectstack-ai#9752 in the queue — this batch does not need it.

## Out of scope — handed back, not filed and not fixed

- The **233** array/object inputs on blocks with no spec contract entry
at all are explicitly not this card. Nothing here widens into them.
- ⚠️ An **empty `initialData` silently discarding a populated
`initialValues`** is pinned as the renderer's behaviour and deliberately
not endorsed. Changing it is a renderer change, and Clause-② for this
batch is `no` — so it is reported to the dispatching seat rather than
filed or repaired here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L

---
_Generated by [Claude
Code](https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ction's `resultDialog` (objectui#9542) (objectstack-ai#9826)

Fixes objectstack-ai#9542

Clause-②: no

`@object-ui/core`'s `ResultDialogSpec` claimed in its own docblock to
mirror `Action.resultDialog` in `@objectstack/spec` and did not. Four of
its members — `title`, `description`, `acknowledge` and `fields[].label`
— were hand-written `string` where the contract declares every one of
them `I18nLabel`: a plain string **or** an inline per-locale map, both
authorized by that type's own docblock and neither deprecated. So this
repo refused what the platform accepts, which is `check:spec-symbols`'
rule-2 failure class — an alignment CLAIM with a hand copy behind it.

The three steps: derive the members from the contract, do the resolution
at the dialog, then delete the workaround objectui#8648 left behind.

## The runtime severity: MEASURED, not restated

The card filed this reading as unverified and asked for confirmation.
**Confirmed — and it is a throw, not a mis-render.** A rendering test
with a locale-map `title` on the unfixed tree:

```
Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).
 ❯ throwOnInvalidObjectTypeImpl …/react-dom-client.development.js
 ❯ reconcileChildFibersImpl …
 ❯ renderRootSync …
```

Nothing upstream coerces or guards the value first. React 19.2.8 throws
at the first such member reached, so the dialog never mounts — on an
action that has **already succeeded**, and this dialog is the only place
a one-shot reveal is ever shown. Three rendering legs failed this way
before the change; the three plain-string legs in the same file passed
unchanged, which is the control that makes this a reading about the map
arm and nothing else.

⚠️ The card's 2FA / OAuth `client_secret` / backup-code examples are the
contract's own illustrations of what `resultDialog` is for. They are
**not** a measured population and are not restated as one here.

## What changed

**Derive, do not hand-copy.** `@object-ui/core` already depends on
`@objectstack/spec` and already imports `Action` from
`@objectstack/spec/ui`, so the contract type is reachable — checked
rather than assumed, and no hand-written fallback was needed. The four
label members are now indexed-access derivations off the contract's own
result-dialog block through two module-local aliases. ⛔ No new exported
symbol: the two interfaces stay the only names this module publishes for
the block, so the clause-② `no` stands on its terms.

**Resolve at the dialog.** Through `@objectstack/spec/ui`'s
`resolveI18nLabel`, imported under this package's established
`resolveInlineI18nLabel` alias. ⛔ Not `resolveKeyedI18nLabel` — that
takes the KEYED vocabulary and these members are the INLINE form. A
plain-string label is unaffected: the resolver returns it unchanged and
the existing fallback chain still answers for an absent label or a map
with no usable entry (the `||` chain is kept over `??` for exactly that
reason).

**The workaround is out.** Both action renderers drop `as ActionDef`
OPEN-BRACKET `'resultDialog'` CLOSE-BRACKET, so the whole forward is
compiler-checked with nothing between it and `ActionDef` again. (Spelled
in words: GitHub's body sanitizer eats tag-shaped fragments, and an
index into a type is what this sentence is about.)

## Zone-2 mechanism assumptions, each tested

**The locale source — one source, no fork.** All six in-package
`resolveInlineI18nLabel` sites trace to the same value,
`useObjectTranslation().language` from `@object-ui/i18n`: `AppSidebar`
and `UnifiedSidebar` destructure `language` off that hook and pass it
directly; `resolveActionParams` takes it threaded in and says so in its
own docblock; the three metadata-admin sites take a `locale` prop fed by
`useMetadataLocale`, which reads that same `language` and clamps it to
that designer's two bundled catalogue locales. The clamp is about the
designer's own copy catalogue, not a second source — so the un-clamped
`language` is what a runtime surface like this dialog wants, matching
the three runtime sites. The dialog already called that hook for `t`.

**The sibling type carries the same drift, and repairing it is FORCED by
ruling 5 — measured.** `ResultDialogFieldSpec.label` is the contract's
fourth `I18nLabel` member on this block. It is not a scope judgement:
with `label` reverted to `string` and everything else in place,
`@object-ui/components`' type-check goes red at BOTH forward sites with
`TS2322 … Types of property 'label' are incompatible`, because the
narrowing assertion that used to absorb it is gone. Deleting the
assertions and leaving the sibling drifted do not compose. The ablation
was run on disk and restored (`git diff HEAD` empty afterwards).

## Tests

A case that is red before and green after, plus a converted pin that is
red under ablation.

- `packages/app-shell/src/views/__tests__/ActionResultDialog.test.tsx` —
three new legs. **Before:** `Tests 3 failed | 3 passed (6)`, all three
failing with the React throw above. **After:** green. One leg varies
only the display language and asserts the other locale's text is
resolved AWAY, so the legs cannot pass by rendering both.
-
`packages/components/src/renderers/action/__tests__/action-undeclared-keys-8648.test.ts`
— the objectui#8648 ledger leg is converted, not merely deleted. Its `as
any` negative was never the workaround: `castBefore` only matches a cast
between `schema` and the key, so a write-side `as any` walks straight
past it and this is the only leg that refuses it. **Ablation:**
re-injecting the narrowing assertion at both sites turns the converted
legs red (`Tests 2 failed | 14 passed`); restored, `git diff HEAD`
empty.

Commands and exit codes, each captured before any pipe, on `7321c6e3`:

> ⚠️ **Seat note (`domain:ui` seat 3, added after the fact — the dev
writes this body once and does not PATCH it).** The head has since moved
to `23592bab`, two commits on: one re-attaching the spec-alignment
docblock to the interface it describes (the derived aliases had landed
between them, detaching the claim and reddening `check:spec-symbols`),
and one appending the supersession note to
`.changeset/8648-ui-action-four-undeclared-keys.md`. The dev reports
every ratchet family re-run green on that head. Independently measured
by this seat on `23592bab` at the time of this edit: **29 success · 3
expected skips · 0 non-success**, with four `Test` shards still running.
⛔ The table below is left exactly as authored — it is the reading on
`7321c6e3` and is not restated as a reading on the current head.

| command | exit |
|---|---|
| `pnpm exec vitest run packages/core/ packages/components/` | 0 — `436
passed (436)` files, `6022 passed (6022)` tests |
| `pnpm exec turbo run build --filter=@object-ui/app-shell
--filter=@object-ui/components --filter=@object-ui/core` | 0 — 29/29 |
| `pnpm exec turbo run type-check` (same three filters) | 0 — 32/32 |
| `pnpm check:spec-symbols` | 0 |
| `pnpm check:action-forward-parity` | 0 |
| `pnpm check:control-bytes` | 0 |
| `pnpm check:new-line-citations` | 0 |
| `pnpm check:phantom-deps` · `check:unused-deps` · `check:self-import`
· `check:esm-specifiers` | 0 |
| `pnpm check:published-dist` · `check:published-tsconfig-exclude` ·
`check:readme-exports` · `check:unreferenced-sources` | 0 |
| `pnpm check:comment-mask-corpus` · `check:i18n-keys` | 0 |
| `node scripts/check-changeset-presence.mjs` | 0 |
| `pnpm exec turbo run lint` (same three filters) | 0 — 4/4, zero
error-level findings |
| `pnpm check:eager-closure` | 0 — ⚠️ see below |

⚠️ **`check:eager-closure` is a cached-artifact reading.** The gauge
needs `apps/console/dist/eager-closure.json`; the console build it comes
from was a 100% turbo cache hit (35 cached / 35, 1s), and this checkout
shares one turbo cache with every sibling worktree. So the gate's exit 0
does not prove anything about THIS tree's closure. It is reported as it
is rather than greened over. The diff adds no new module edge into the
console's eager graph — `@objectstack/spec/ui` is already imported
eagerly by `AppSidebar` in this same package — so the expected movement
is zero, but that is reasoning, not the measurement.

The build had to run before the type-checks: a package type-check ahead
of its dependency closure produces `TS6305` false reds. Both readings
were taken in that order and both were green.

## Deviations from the declared file surface — two, both declared

1.
**`packages/app-shell/src/views/__tests__/ActionResultDialog.test.tsx`**
is not on the declared surface, and the dispatch also requires a case
that fails before the change and passes after. The card's own example is
a locale-map `title` reaching this dialog, and its existing sibling test
file is where that lives. Extended rather than stopped, and named here.
2. **A second commit re-attaches the spec-alignment docblock** to the
interface it describes. The derived aliases had landed BETWEEN that
docblock and the interface, which detached the claim — `attachedDoc` in
`scripts/check-spec-symbol-derivation.mjs` reads only the LAST leading
comment block — and `pnpm check:spec-symbols` went red asking for the
ledger entry to be deleted. Moving the aliases above the docblock
restores the attachment. ⛔ No gate script was edited.

## Acceptance notes

- **`check:spec-symbols`' rule-2 ledger has no state for "claim now
BACKED by derivation".** Measured: with the claim re-attached and the
members derived, deleting `ResultDialogFieldSpec` from `CLAIM_DEBT`
makes the gate red — ``a spec-alignment claim has nothing behind it`` —
so the entry must stay although the debt it records is paid.
`CLAIM_DEBT` means unbacked and `CLAIM_ALLOW` means deliberate
divergence; a repaired symbol fits neither, so it can never leave the
ledger, and the ledger's own burn-down instruction cannot be followed
for this class. Left as a finding; a gate change is a maintainer-floor
item and ⛔ not this card's.
- **The card's "one package over from where that gate matches by name"
is true of rule 1 only.** Rule 2 already covers `@object-ui/core` and
had this very symbol ledgered as `CLAIM_DEBT` the whole time. Extending
rule 1 is therefore a smaller idea than the card implies — it was the
CLAIM half, not the coverage half, that was missing.
- Not filed, noted only: the dialog's `fields` fallback entry spells
`label: undefined` explicitly where omission would read the same;
nothing carries it.

## What this PR does not touch

Out of scope: `objectstack-ai#9572` is not addressed here, and `objectstack-ai#8941` remains open —
both are neighbours in the same directories on different files, and
neither is touched. objectui#8648 is referenced only as the card that
left the workaround behind.

This PR was drafted by an agent seat; the session reference is
`session_01Xm4WFhEe5mwcgyqHjxR2hn`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Xm4WFhEe5mwcgyqHjxR2hn)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ormat in every record-title reader (ADR-0079 order) (objectstack-ai#10358)

Fixes objectstack-ai#9436
Clause-②: yes

This PR executes ruling C1 on this card: the ADR-0079 declared name
pointer outranks `titleFormat` in every record-title reader. The
authority is class-1 ruling comment 5657441402, ratified in comment
5814246926. The scope is the re-scoped claim, comment 5820165439: one
ruled order, three readers, one PR. LookupField is left out (see
Acceptance notes).

## The new public export (why `Clause-②: yes`)

`@object-ui/core` now exports **`declaredNameField(objectDef)`**. The
function already existed privately in `record-title.ts`, and its
behaviour is unchanged. It returns the object's declared record-title
pointer exactly as `getRecordDisplayName` reads it at steps 1+2:
`nameField`, then the deprecated `displayNameField` and `NAME_FIELD_KEY`
aliases. It returns `undefined` when none is declared, and it **never
derives**. Never deriving is the difference from `resolveNameField`, and
it is what lets a caller put the type-aware derivation on a lower rung
than its own `titleFormat` rendering. All three readers value it the
same way, `recordDisplayValueAt(record, declaredNameField(objectDef))`.
None of them re-types the `??` chain. The claim marks this export
`Clause-②: yes`, so a review-tier contract review is owed before
enqueue.

## Authority, as read

- `@objectstack/spec` 17.4.0 (installed), `titleFormat` describe:
"[DEPRECATED → nameField (ADR-0079)] Render-only title template; the
server cannot return or query it, and an explicit nameField now takes
precedence."
- `@object-ui/core` `getRecordDisplayName` docblock, step 3:
"`objectDef.titleFormat` (rendered template) — LEGACY, render-only, kept
for back-compat only; an explicitly-declared field (1/2) now wins over
it."

## `PageHeaderRenderer` title ladder, before and after

The new rung is inserted directly above the template. Nothing else
moves, and the template keeps the header's own interpolation (i18n
option labels, separator cleanup).

| # | Before (`main`) | After (this PR) |
|---|---|---|
| 1 | explicit `schema.title` | explicit `schema.title` |
| 2 | `titleFormat`, via the header's interpolation | **declared
pointer**: `nameField`, then `displayNameField`, when it holds a value
on the record. This is a new rung, filtered by the same floor test as
rung 4. |
| 3 | unified resolver: `nameField`, `displayNameField`, core
`formatTitleTemplate`, type-aware derivation | `titleFormat`, via the
header's interpolation (unchanged) |
| 4 | record-key rung, only for an object naming no title field |
unified resolver (unchanged) |
| 5 | `${objectLabel} ${id}` placeholder | record-key rung (unchanged) |
| 6 | | `${objectLabel} ${id}` placeholder (unchanged) |

On an object without a `titleFormat`, rung 2 answers exactly what rung 3
answered before. A pointer that is blank on the record falls through to
the template, just as `getRecordDisplayName` walks from a blank steps
1+2 to step 3.

## The other two readers of the same order

- **`DetailView.resolveDisplayTitle`**: `primaryField`, then **declared
pointer (new step 1b)**, then `titleFormat` (still step 2), then the
unified resolver, then `schema.title`, then the record-key probe, then
the floor. The rung is numbered 1b so that existing citations of "step
2" as the template step stay true.
- **`record:details` H1 dedupe**: when the declared pointer holds a
value, that field IS the H1. Its row is hidden and the template is not
consulted. Otherwise the objectstack-ai#8351 template scan and the value walk run
unchanged.

Why all three move together was measured on the stop report (comment
5820114435). With only the header moved, the synthesized page
(`page:header` + `record:details`) showed a duplicate row under an
identical H1 for a composite template. For a single-field template it
also hid a row the H1 no longer showed. `DetailView` rendered
`HT-2026-001 - Acme Corporation` over `nameField: 'contract_no'`.

## Upgrade effect: whose H1 moves

The H1 moves on any object that declares BOTH a `nameField` (or
`displayNameField`) and a `titleFormat` whose rendering differs from
that field's value.

- **objectui `examples/**` and `apps/**`**: zero objects declare
`titleFormat` at all. In-repo declarations are test fixtures only.
- **ObjectStack platform objects**, measured once at objectstack
`61609edf` and not re-derived by any gate here. The instrument was `git
grep -l` for a `titleFormat` key over non-test, non-md sources, then a
per-file co-occurrence scan for `nameField` / `displayNameField`.
  - 42 platform objects declare both.
- 26 have a single-field template equal to the pointer, so nothing
visible changes.
- **16 have an H1 that moves.** 11 of them move to another field's
value: `sys_import_job`, `sys_job_queue`, `sys_job_run`, `sys_session`,
`sys_setting`, `sys_migration_journal`, `sys_activity`, `sys_audit_log`,
`sys_comment`, `sys_sharing_rule` and `sys_webhook`.
- **The other 5 declare `nameField: 'id'`, so their H1 becomes the raw
record id**: `sys_approval_action`, `sys_approval_approver`,
`sys_approval_request`, `sys_automation_run` and `sys_http_delivery`.

This is a known consequence of the ruled order, and nothing in the
renderer works around it. The fix belongs to those objects' metadata in
ObjectStack: designate a formula field as `nameField`, as the
`titleFormat` describe itself advises for a composite title. The seat is
carding that in objectstack. The changesets for `@object-ui/components`
and `@object-ui/plugin-detail` state the same effect. The
`@object-ui/core` changeset names the export.

## Pins

- `packages/components/src/__tests__/page-header-title.test.tsx`: the
pin "titleFormat still outranks nameField (legacy header behaviour)" is
**rewritten, not deleted**, as "the declared nameField outranks
titleFormat (ADR-0079 protocol order, objectstack-ai#9436)". The H1 must equal the
pointer's value, and the template must render nowhere.
  - A second pin covers the `displayNameField` alias.
- Four controls: a blank pointer falls through to the template; the
template still outranks the type-aware derivation; the template keeps
the header's option-label interpolation (a select value renders as `In
Progress`, not `in_progress`, which also guards against consulting the
whole resolver above the template); an explicit `schema.title` still
wins.
- `record-details.titleFormatNoDedupe-8351.test.tsx`: the two old-order
cases are **rewritten, not deleted**. "HALF 1" now asserts the pointer's
row DROPS. "hides the row the DECLARED POINTER names, not the one the
template names" is the other.
- The objectstack-ai#8351 template outcomes (composite hides nothing, empty walks on,
collapse hides that row) now run on an object with no declared pointer,
where the template really is the H1.
  - A scan-not-peek case is added.
- `record-details.headerDedupeAgreement-9436.test.tsx`: the stop
report's probe, committed. It renders the real `page:header` H1 beside
the real `record:details` body, for a composite and a single-field
template, plus a no-pointer control.
- `DetailView.declaredPointerOutranksTitleFormat-9436.test.tsx`: two
pins (`nameField`, `displayNameField`) and three controls: no pointer, a
blank pointer, and the view-level `primaryField` still winning.
- `record-title.declaredNameField-9436.test.ts`: the export's contract.
It covers the alias order, never deriving (with a control showing that
`resolveNameField` would derive), and agreement with
`getRecordDisplayName` at steps 1+2.

## Ablations (one-shot, run at `96e1b800`, the commit holding
implementation and pins)

Each ablation used `node ../objectstack/scripts/ablation-replace.mjs`.
That tool requires the anchor to hit exactly once, verifies the mutation
on disk (anchor count 1 to 0, blob hash changed), then restores and
proves the restore (blob equals HEAD, `git diff HEAD` empty). Each run
covered the same five files, 38 tests. Vitest aliases these packages to
`src`, so no `dist` was involved.

| Ablation | Mutation | Red (predicted = observed) | Green |
|---|---|---|---|
| A1: header back to template-first | `declaredTitle \|\|` becomes `''
\|\|` | both header order pins, and both agreement cases (the H1
assertion) | 34, including every control |
| A2: dedupe back to template-first | the declared-pointer branch
becomes `if (false)` | both flipped objectstack-ai#8351 cases, and both agreement
cases (the body assertion) | 34 |
| A3: `DetailView` back to template-first | its declared rung becomes
`if (false)` | both `DetailView` pins | 36 |

Later commits added the changesets, removed one unused eslint directive
from the agreement test (a comment line), and merged `main`. None of
them touches the mutated source lines.

## Verification at `d02362e7` (final head)

- `turbo run build --filter='@object-ui/plugin-detail^...'`: 11/11
successful. Then `type-check` of `@object-ui/core`,
`@object-ui/components` and `@object-ui/plugin-detail`: exit 0. All five
test files were confirmed inside the `tsconfig.test.json` programs by
`--listFiles`.
- From the repo root: `pnpm exec vitest run packages/core/
packages/plugin-detail/` gave 367 files passed and 1 skipped (a
pre-existing timezone `skipIf`), 5460 tests passed. `pnpm exec vitest
run packages/components/` gave 289 passed and 1 skipped, 2809 tests
passed.
- `pnpm check:control-bytes` OK. `pnpm check:new-line-citations`: 0 new
citations. `node scripts/check-changeset-presence.mjs`: 9 source files
of 3 released packages, 3 changesets. `check-changeset-no-major`,
`-fixed`, `-overwrite`: OK. `pnpm check:pending-changeset-literals` OK.
`pnpm check:test-path-roots` OK.
- `pnpm check:changeset-claims`: report-only. It flagged 10 pending
changesets naming a touched file. Each paragraph was read, and none is
falsified: they concern tab walkers, action ids, name-space
`resolveNameField`, `NAME_FIELD_KEY` being read only inside
`record-title.ts` (still true: the export lives there), and
`interpolate()` rendering `titleFormat` (still true).
- ESLint, narrowed to the 9 changed `.ts`/`.tsx` files: 0 errors
(warnings only, from rule classes already present in these files).
- The population is eslint's own config: `ESLint.isPathIgnored` is false
for 9 of 9.
  - The file count is read from `--format json`: 9.
- Invariance: `eslint.config.js` sets no `parserOptions` /
`projectService` (no type-aware linting), and no rule under
`eslint-rules/` reads other files. So this diff cannot move a verdict on
an untouched file.
- NOT MEASURED: `pnpm check:readme-exports`. Its prerequisite is every
package's `dist`: its only findings were missing `dist` entries and the
vacuity floors those trip. This diff edits no README, and the `core` /
`components` READMEs had zero findings. CI runs it on a full build.

## Hunk fences

- `record-details.tsx`: this PR's hunks are the import block and the
dedupe region, far from the `@@ -139` / `@@ -197` hunks of objectstack-ai#10279.
- `DetailView.tsx`: this PR's hunks are the import line and
`resolveDisplayTitle`, far from the `@@ -934` hunk of objectstack-ai#8941.
- `packages/fields/` is untouched.

## Acceptance notes

- **LookupField `recordToOption`** has the same defect class: the
referenced object's `titleFormat` outranks its declared pointer in
lookup option labels. Per the re-scope it is ⛔ not folded here.
`packages/fields/` is held by objectstack-ai#10223, and the seat files it as its own
card.
- **A pre-existing gap in the objectstack-ai#8351 dedupe scan**, not moved by this PR.
The scan only matches values against its candidate list: the name
pointer, the derived field and six literal names. Take an object with NO
declared pointer whose template collapses onto a field outside that
list, for example `{contract_no} - {name}` with `name` blank. Its H1
reads the `contract_no` value while the `contract_no` row still prints
under it. This PR reproduced it while retargeting "LIT CONTROL B"; the
template-scan code that decides it is unchanged from `main`. That
control now collapses onto `name`, a candidate. The gap is reported for
the seat to card.

The session for this change is
`https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…unwrap `rows` (objectui#7028) (objectstack-ai#10363)

Fixes objectstack-ai#7028
Clause-②: yes

## What

`aggregate()` now reads the `client.analytics.query` answer in one
spelling: `rows` on the post-unwrap `AnalyticsResult`. This is condition
3 of the objectstack#13079 ruling's landing requirements (director batch
objectstack-ai#19, option A, maintainer 2026-08-31), quoted verbatim: 「objectui
容错链同波收紧:已立 objectui#7028,带时序门(严格在收敛合并、objectui 采版之后 —— 该链今天是承重的)」. The
card's sequencing gate is met: `@objectstack/client` 17.3.0 carries the
convergence, and the workspace lockfile resolves 17.4.0.

- **The ladder had five spellings, not the card's two.** It read a bare
array, `rows`, `data` as an array, `data.data.rows` and `results`, and
answered anything else with `[]`. Only `rows` survives. Any other value
throws the new exported `AnalyticsResultShapeError` (`code:
'ANALYTICS_RESULT_SHAPE_INVALID'`, `envelope: true` for the pre-17.3.0
envelope).
- **Loud means a throw, and the throw escapes the fallback.** The row
read sits inside `aggregate()`'s `try`. Its `catch` would have
classified the error as `unknown` and answered it with
`aggregateViaFind`'s client-side numbers, so the catch now rethrows
`AnalyticsResultShapeError` before it classifies anything. I chose a
throw over a diagnostic because the file already treats contract
violations that way: `AnalyticsQueryRejectedError` refuses the fallback
for the same reason (framework#3878). A `[]` would repeat the confident
zero that objectui#5954 removed on the failure side.
- **The widened `AnalyticsResult` alias is gone**, along with the
branches it existed for.
- **The `@objectstack/client` floor moves from `^17.0.0` to `^17.3.0`**
in `packages/data-objectstack` and `apps/console`. Only the lockfile
specifier changes; the resolution stays at 17.4.0.
`QUICK_REFERENCE.md`'s Client row was rewritten by `pnpm
quick-reference:sync`.

## The premise, measured

- **This was never a server question.** The comment above the old ladder
(and the objectui#7122 changeset) described deleting the branches as "a
runtime compatibility decision about servers older than #13079". The
convergence commit `db16b94` changes only `packages/client` and the spec
migration registry, and no server code. `POST /analytics/query` answers
`deps.success(result)` in objectstack's `domains/analytics.ts` from its
first extraction (`8f124a7b7`, 2026-07-27, before 17.0.0 shipped on
2026-08-14) to `main`. Every 17.x server therefore sends the same `{
success, data }` envelope. Only the client decides whether it is
unwrapped: `unwrapResponse` strips it at 17.3.0+, while 17.2.0 ended
`return res.json()` (read in the installed 17.4.0 `dist/index.mjs` and a
packed 17.2.0 tarball). No server is dropped. The compatibility axis is
the **client** range, and that is why the floor moves.
- **Which producer each branch served** (`git log -S` on the unshallowed
history):
- `rows` and `data.data.rows` came from `70cb62b85` (2026-04-01): the
post-unwrap and envelope forms of the one route.
- A bare array, `data` as an array and `results` came from `f25e6c288`
(2026-02-25). That commit targeted a raw `GET
/api/v1/analytics/{resource}` fetch, which `d91f2e2a0` replaced with
`client.analytics.query` ten minutes later, and the three branches were
carried over without shape evidence.
- Even the retired in-kernel shim (removed by objectstack `77fadbfca`
before 17.0.0) answered `{ success, data: { rows, fields } }`. No
producer of this call site ever returned the other three shapes.
- **`analytics.meta` and `analytics.explain` have no call sites in
objectui.** `git grep` finds only `analytics.query` here, with a
positive control on the same pattern. In the installed 17.4.0 client,
`meta` and `explain` both end `return this.unwrapResponse(res)`,
declared `AnalyticsMetadataResponse["data"]` and
`AnalyticsSqlResponse["data"]`. `analytics.query` is declared as a
Promise of `AnalyticsResult` (the generic is spelled out in words
because GitHub strips angle-bracket spans).

## Surface beyond the claim, stated

The claim named only `index.ts` (the ladder), the tests beside it and
one changeset. Two extensions were needed:

1. **The rethrow line in `aggregate()`'s `catch` plus the new error
class.** Without them the throw is silently answered by the fallback,
which the pins require it must not be.
2. **The client floor, in `packages/data-objectstack/package.json`,
`apps/console/package.json`, the two `pnpm-lock.yaml` specifier lines
and `QUICK_REFERENCE.md`.** Leaving `^17.0.0` would publish a range this
change makes false.
`scripts/__tests__/quick-reference-current-release-4143.test.ts` went
red on the adapter-only bump, because that row anchors both manifests.
No open pull request holds these lines: objectstack-ai#10341 and objectstack-ai#8941 touch the
lockfile and the console manifest in disjoint hunks, and objectstack-ai#5400 is the
release pull request.

`isLegacyOverlayRow` and the rest of `index.ts` are untouched.

**Seat amendment (2026-09-24T20:14Z).** The seat accepted both
extensions and widened the claim's File surface to match (claim comment
`5820950909`, edited). Line 2 now reads `Clause-②: yes`, because the
diff adds a public export (`AnalyticsResultShapeError`); a contract
review is owed before enqueue. The changeset level moved from `patch` to
`minor` at head `e6e26e35d` (objectui precedent objectstack-ai#9061 / objectstack-ai#9175).

## Pins (site-scoped, new file)


`packages/data-objectstack/src/aggregate-rows-post-unwrap-7028.test.ts`,
7 tests:
- The unwrapped shape yields its rows. The server's `{ success, data: {
rows } }` wire envelope goes through the real client.
- An envelope at the boundary throws, and `/api/v1/data` is never
requested. This is tested twice: through the real client (a success-less
`{ data: { rows } }` body that `unwrapResponse` leaves alone), and as
the value a pre-17.3.0 client handed back.
- The other three retired spellings throw instead of degrading to `[]`.
- CONTROL: rows missing the measure still fall back to client-side
aggregation.

## Verification (final head `52f5a6c21`)

- `pnpm --filter @object-ui/data-objectstack type-check`: exit 0.
`--listFiles` confirms the new test file is in that program.
- `pnpm exec vitest run packages/data-objectstack/` from the repo root:
`Test Files 68 passed (68)`, `Tests 933 passed (933)`.
- These root suites read the touched files off disk, outside the package
graph: every `scripts/__tests__` suite naming `data-objectstack`,
`QUICK_REFERENCE`, `apps/console/package.json` or `pnpm-lock.yaml`.
Result: `Test Files 27 passed (27)`, `Tests 1137 passed (1137)`.
- `eslint .` in the package: 77 files linted (from `--format json`), 0
errors. Type-aware linting is off, so this diff cannot move any verdict
in untouched files.
- Gates, all exit 0: `check:control-bytes`, `check:new-line-citations`
(0 new), `check-changeset-presence`, `check:changeset-claims`,
`check:lockfile-integrity`, `check:lockfile-dedupe`,
`check:installed-pin-claims`, `check:pending-changeset-literals`,
`quick-reference:check`.
- `check:changeset-claims` lists `5793`, `6361` and `7122` for naming
`pnpm-lock.yaml`. I read each paragraph, and none is falsified by this
diff.
- `check:spec-floors`: NOT MEASURED. It refuses on an unbuilt workspace
(`no-artifact`), and it reads `@objectstack/spec` floors only, which
this diff does not move.
- **Ablation 1:** I restored the five-spelling ladder with
`ablation-replace.mjs` (anchor 1 to 0, blob `0b7bc68a9ed1` to
`3b936fc60bb8`). Result: `Tests 5 failed | 2 passed (7)`, with every
failure being `aggregate() resolved; it was expected to throw
AnalyticsResultShapeError`. The two greens are the positive pin and the
control. The restore is proven: blob equals HEAD `0b7bc68a9ed1` and `git
diff HEAD` is empty.
- **Ablation 2:** I deleted the `catch` rethrow. Result: the same 5 fail
and 2 pass, because the error is answered by the fallback. The restore
is proven the same way.
- Both ablations run from source through a relative `./index` import, so
no `dist` leg applies.

## Acceptance notes

- `analytics.meta` and `analytics.explain` are not called anywhere in
this repo, so they have nothing to tighten.
- The pending `.changeset/7122-objectstack-family-17-3-0.md` still says
the branches were kept "rather than deleted" and frames the question as
one about servers. It is true of its own change and will publish in the
same release as this one, so the new changeset names it and supersedes
it rather than editing it.
- The existing `aggregate-capability.test.ts` case "a `{ success, data:
{ rows } }` envelope is still a result" stays green. It is the server's
wire envelope, which the installed client unwraps. After this change it
is a positive control, not tolerance.
- Dependents' type-check: `plugin-charts`, `plugin-dashboard` and
`components` read `aggregate()` results through the `DataSource`
interface and do not depend on this package. The only export-surface
change is one added class: `export` lines in the diff are `+1 -0`, and
the `aggregate` signature is untouched. No export-star re-export exists,
and no other declaration of the name exists, so no importer's type-check
input changes. The full workspace Type Check runs in CI.

Session: `https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC` (dev
run dispatched by the `domain:ui` seat 1).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
… storage key (objectui#8522) (objectstack-ai#10549)

Fixes objectstack-ai#8522
Clause-②: yes — a published export (`useColumnWidths` and its two types)
is removed.

Executes ruling `5810224713` (remove: reading 2 of the card, superseded
and left behind). Dispatched by the `domain:ui` seat 2 PM; implemented
by its `os-dev` in session
`https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`.

## What changed

- `packages/plugin-kanban/src/useColumnWidths.ts` deleted: the hook,
`UseColumnWidthsOptions`, `UseColumnWidthsReturn`, and its storage key
`objectui:kanban-column-widths`.
- `packages/plugin-kanban/src/index.tsx`: the three entry exports
removed (the `useColumnWidths` value export and the two-type `export
type` line).
- `@object-ui/types` docblocks, comment-only hunks:
- `complex.ts`, the bare-`kanban` retirement docblock: the "NOT retired"
sentence no longer names the hook as a consumer. `KanbanCard` /
`KanbanColumn` / `CardTemplate` keep the renderer and `CardTemplates` as
consumers, and `ColumnWidthConfig` is recorded as having lost its one
`plugin-kanban` reader to this card, which did not rule on the type.
- `objectql.ts`, the `ObjectKanbanSchema` lane "member set is the READ
set" paragraph: the hook is dropped from the list of read sites. The
hook destructured `columns` as `_columns` and read no lane member, so
the counted member set does not move.
- `kanban-plugin-dialect-authoritative-7664.test.ts`: the pin-1 comment
stops naming the hook. No assertion changed.
- New pin
`packages/plugin-kanban/src/__tests__/useColumnWidthsRetired-8522.test.ts`,
following the `plugin-form` `entrySurface-6059` barrel-pin pattern
(`import * as entry` + `Object.keys(entry)`) and the `spec-symbol-4650`
compile-time `import('..').X` + `@ts-expect-error` pattern.
- `.changeset/8522-retire-use-column-widths.md`:
`'@object-ui/plugin-kanban': minor`, with a BREAKING body in migration
terms. There is no `@object-ui/types` entry: `check-changeset-presence`
asks for presence only, and the types hunks are comment-only. Its last
migration bullet names objectui#7742.
- `.changeset/7742-kanban-arm-batch70.md`, a pending changeset that this
change falsifies, is reconciled here in prose only:
- The sentences naming the `useColumnWidths` widths channel, and saying
the prop and the hook consume the types, are scoped to "at this change".
- A dated note (2026-09-25) records that the hook is retired
(objectui#8522), that `ColumnWidthConfig` has no `plugin-kanban` reader,
and that `CardTemplate` is still consumed by the `CardTemplates`
component's `templates` prop.
- The frontmatter is byte-identical: md5 of the frontmatter block is
`b62313df8fb2af929fbe2c21c5ae3873` before and after.

No tests were deleted. No test existed only to test the hook: the
tracked tree had zero test importers of it.

## Premise, measured on `origin/main` `d461b0de8`

- Readers. `git grep` over `packages apps examples content skills` found
`useColumnWidths` 7 hits in 5 files, `UseColumnWidths` 4 hits in 2
files, and `kanban-column-widths` 1 hit in 1 file. Every hit was the
hook's own file, the entry, the two docblocks, or the 7664 comment. On
the same instrument, the control `useCrossSwimlaneMove` fired with 5
hits in 3 files. Zero readers holds.
- Published surface. H3 answers yes. `@object-ui/plugin-kanban@17.6.0`
from the npm registry exports the hook in its `dist/index.js` export
list as `Ue as useColumnWidths`, and `dist/index.d.ts` re-exports it and
both types. The storage-key literal is present in `dist/index.js` and
`dist/index.umd.cjs`. That is why the changeset is written as a
migration.
- A tree-wide grep found one more carrier outside the file surface: the
pending changeset `.changeset/7742-kanban-arm-batch70.md`. It names "a
`useColumnWidths` hook option for widths" and says "the prop and the
hook still consume the types". Both go false when this merges, and both
would publish into the same release, so this PR reconciles it (see
above).
- A re-grep of every pending `.changeset/*.md` for `useColumnWidths`,
`UseColumnWidths` and `kanban-column-widths` hits only 7742 and this
PR's own changeset.
  - The control `CardTemplate` hits 3 files on the same instrument.
- The two `7664-*` changesets that name `ColumnWidthConfig` claim only
that it is declared and re-exported, which remains true.

## Verification

The code legs below ran on `e62e390f4`. The only later commit,
`8be30d7fc`, edits changeset prose only; the changeset gates were re-run
on it.

- Build, under the verify lock: `pnpm --workspace-concurrency=2 --filter
'@object-ui/plugin-kanban...' build` printed `VERDICT command-exit 0`.
The rebuilt `plugin-kanban/dist` has `useColumnWidths` 0 and
`kanban-column-widths` 0 in `index.js`, `index.d.ts` and
`index.umd.cjs`, and no `useColumnWidths.d.ts`. The control
`useCrossSwimlaneMove` is 2 / 2 / 1.
- `pnpm --filter @object-ui/plugin-kanban type-check` (`tsc --noEmit &&
tsc -p tsconfig.test.json`) exits 0. `tsc -p tsconfig.test.json
--listFilesOnly` lists the new pin, so the compile-time half is
compiled.
- `pnpm --filter @object-ui/types type-check` (including
`tsconfig.examples.json` and `tsconfig.test.json`) exits 0.
- `pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/` reports
`Test Files 61 passed (61)` and `Tests 412 passed (412)`. That is 61
tracked test files: 60 on base plus the new pin.
- `pnpm exec vitest run --maxWorkers=2 packages/types/` reports `Test
Files 233 passed (233)` and `Tests 5184 passed (5184)`.
- Red on base. The base `index.tsx` and `useColumnWidths.ts` were
written back onto disk. The landing was checked on disk: the entry had
`useColumnWidths` twice and the hook file was present with its key.
Under a `trap` restore:
- vitest: `Tests 1 failed | 1 passed (2)`, with `AssertionError:
expected [ 'KANBAN_UNCOLUMNED_ID', …(10) ] to not include
'useColumnWidths'`. The failure is the runtime pin; the type-only test
is erased at runtime.
- `tsc -p tsconfig.test.json` exits 2 with `error TS2578: Unused
'@ts-expect-error' directive.` twice, once on each retired type query.
- After restore, the `index.tsx` blob matches HEAD `7e09f2d92`, the hook
file is absent, and `git diff HEAD` and `git status --porcelain` are
both empty.
- Changeset and prose gates, on `8be30d7fc`:
- `node scripts/check-changeset-presence.mjs` printed `✅ 6 source
file(s) of 2 released package(s) changed, and this change declares 1
changeset(s)`.
- `node scripts/check-changeset-overwrite.mjs` exited 0 (report-only).
It printed `1 changeset(s) added, 1 modified, 0 deleted`. For 7742 it
printed `declared at base: @object-ui/types: minor,
@object-ui/plugin-kanban: minor` and `declares now: @object-ui/types:
minor, @object-ui/plugin-kanban: minor`.
- `node scripts/check-changeset-claims.mjs` exited 0 (report-only),
reading `Corpus: 2 changeset(s) this change adds or modifies` with no
self-contradiction. It listed 27 pending changesets that name
`complex.ts` or `objectql.ts`; none of them names the hook.
- `node scripts/check-pending-changeset-literals.mjs` printed `✅ No test
source names a pending changeset`.
- `node scripts/check-changeset-no-major.mjs` printed `✅ No changeset
declares a major bump`.
- `node scripts/check-new-cross-file-line-citations.mjs` printed
`VERDICT new-cross-file-line-citations: 0 new citation(s)`.
  - `node scripts/check-control-bytes.mjs` passed.
- Source gates, on `e62e390f4`. These exited 0:
`check:unreferenced-sources`, `check:self-import`,
`check:side-effects-array`, `check:handler-key-reads`,
`check:entry-guard`, `check:phantom-deps`, `check:unused-deps`,
`check:test-path-roots`, `check:vi-mock-specifiers`,
`check:spec-symbols` and `check:esm-specifiers`.
- `check:readme-exports` passed after the build. No README names any of
the three removed exports.
- ESLint on the 5 touched text files (`--no-inline-config --format
json`): 0 errors. The 66 warnings are pre-existing `no-explicit-any` /
`react-refresh` warnings in untouched lines, and there are none in the
new pin.
- NOT MEASURED:
- `check:sdui-registration-pins`: it needs an `apps/console` build and
exits 2 without one. No registration changed here.
- `check:published-dist`: its turbo build of the whole workspace,
`apps/console` included, was killed by my 300s timeout (exit 124). It
was run outside the verify lock by mistake. That is declared to CI.

## Overlap

The one `types/src/objectql.ts` hunk here is the one-line docblock edit
in the `ObjectKanbanSchema` lane paragraph. objectui#10533 and objectstack-ai#10522,
which also edited that file while this PR was being written, have since
merged, and this hunk still applies on `main`. At landing, the open PRs
that edit `objectql.ts` are objectui#10550, objectstack-ai#10546 and objectstack-ai#8941, and all
three are hunk-disjoint from this edit.

## Acceptance notes

- After this change, `ColumnWidthConfig` (in `@object-ui/types`,
re-exported by `plugin-kanban`, and mirrored by
`ColumnWidthConfigSchema`) has no reader in `plugin-kanban`. The ruling
named the hook, its exports and its key, not the type, so the type
stays. Recorded here only: there is no reproducible defect, and nobody
is known to own it next.
- The 7664 pin-1 comment still says `KanbanColumn` / `KanbanCard` are
consumed by `CardTemplates`. The `CardTemplates` component consumes
`CardTemplate`, not those two. That wording predates this change and is
kept, because the dispatch limits this file to removing the hook's name.

---

_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…r barrel reached (objectui#8932) (objectstack-ai#10588)

Fixes objectstack-ai#8932

Clause-②: no

`@object-ui/plugin-kanban`'s `exports` map has no `./KanbanEnhanced`
entry. The published 17.6.0 map is `.` only; this release's in-repo map
is `.` and `./style.css`. The entry point does not export the component
by name, so the declared surface does not move (the ruling's ③). The
changeset still names the file for anyone who deep-imported it.

Executes ruling `5634037484`, ratified `5810258711` (maintainer: 「同意」):
**A — delete `packages/plugin-kanban/src/KanbanEnhanced.tsx`** and its
test references. PM claim `5830002817`. Rework round after contract
review `5831016914`. Implemented by the `os-dev` run in session
`https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`.

## What changes

- `packages/plugin-kanban/src/KanbanEnhanced.tsx` is deleted.
- **Where the component was.** I read the published 17.6.0 tarball with
`npm pack`:
- `dist/index.js` and `dist/index.umd.cjs` carry the component inlined
(a `KanbanEnhanced` export of the chunk, bound to the minified function
`ct`). It was reachable through the `kanban-enhanced` registry key and
the entry's `kanbanComponents` map.
  - `dist/index.d.ts` references `./KanbanEnhanced`.
  - The `exports` map is `.` only. The tarball contains no CSS file.
- The component leaves the bundle in this release with the objectui#8257
/ objectui#8802 key retirement (the pending `8802-8257-8008` entry), not
with this PR. Building the base (`d22b37bd8`, after objectstack-ai#8865) and this
branch, `dist/index.js` and `dist/index.umd.cjs` contain
`KanbanEnhanced` 0 times on both sides (control: `KanbanImpl` is found
in `dist/index.js`).
- **What this PR removes from the published package**, measured by
building `@object-ui/plugin-kanban` on the base and on the branch:
- `dist/KanbanEnhanced.d.ts` and `dist/KanbanEnhanced.d.ts.map`. Nothing
else leaves the `dist/` listing.
  - `dist/index.d.ts` is byte-identical (`cmp` exit 0).
- `@tanstack/react-virtual` leaves the plugin's `dependencies`. The
deleted module was the only file in the package that imported it, and
`check-unused-dependencies` fails without this change. `pnpm-lock.yaml`
loses just this importer's three lines. `@object-ui/plugin-grid` still
declares the package, so no resolution moves.
- **`style.css`.** The stylesheet first ships in this release (pending
`4929-plugin-published-stylesheets.md`). Built after this deletion, it
does not carry three utility rules that only the deleted module used:
`border-border/50`, `text-yellow-500` and `shadow-xl`. Building with the
module keeps 65 rules; building without it keeps 62.
- **`@object-ui/types` JSDoc** (comment only). Four present-tense
passages that named `KanbanEnhanced` as a live reader now name
`KanbanImpl` as the one board. The two past counts are dated and name
objectui#8932 rather than being deleted:
  - `KanbanCard`: "what `KanbanImpl` reads back".
- `KanbanColumn.cards`: "measured at objectui#6939: 12 lines in
`KanbanImpl`, and 8 in `KanbanEnhanced` until objectui#8932 deleted it".
- `ObjectKanbanSchema.columns`, twice: "what `KanbanImpl` consumes", and
"counted when there were two board implementations … `KanbanImpl` alone
still reads all six members". I re-measured that last claim:
`KanbanImpl.tsx` reads `id`, `title`, `cards`, `limit`, `className` and
`collapsed` off the lane.
- The past-tense passages on `KanbanColumn.collapsed`, on the retired
`color` member, and on the retired lane-collapse key stay as they were.
  - `git diff -w` over the two files shows only lines starting with `*`.
- Changesets:
- `.changeset/8932-retire-kanban-enhanced.md`
(`'@object-ui/plugin-kanban': patch`) states how the component left the
bundle, what this change removes, the deep-import and dependency notes,
and that `style.css` ships without the three rules.
- `.changeset/8932-kanban-enhanced-types-jsdoc.md` (`'@object-ui/types':
patch`) is one sentence on the JSDoc.

## Premise re-checked on the current `main` (`d22b37bd8`)

**H1: confirmed for the source tree, with lit controls.**

- The in-repo `exports` keys are `.` and `./style.css`.
- The entry has no `import` or `export` line that names `KanbanEnhanced`
(grep exit 1). Control: the same grep finds the `React.lazy` import of
`./KanbanImpl`.
- A module-specifier grep over the whole tree (`from` / `import(` /
`require(` / `vi.mock(`) finds `KanbanEnhanced` twice, both times in
tests. Non-test importers: 0. Control: the same pattern for `KanbanImpl`
finds the production import in `packages/plugin-kanban/src/index.tsx`.
- ⚠️ Corrected in this rework: the first round built only the base and
the branch, both after objectstack-ai#8865, and wrongly concluded the component was
never bundled. The published 17.6.0 package does bundle it (see "Where
the component was" above).
- PR objectstack-ai#8865's registry-key retirement did not change the file's
situation. `kanban-enhanced` is unregistered and pinned by
`kanban-family-registry-keys-retired-8257.test.ts`. The file was still
on disk at the base.

**H2: falsified.** The module had two test references, not one:

- `src/__tests__/kanban-handler-slots-7664.test.tsx`: a `vi.mock` target
whose prop recorder no leg read.
- `src/cardPredicateScope.test.tsx`: a direct import. This is the
reference the `index.tsx` tombstone itself named.

I handled both as the ruling's intent directs: each test is deleted, or
rewritten to what it still pins.

## Test files: every assertion kept, removed or moved

`kanban-handler-slots-7664.test.tsx`: the `KanbanEnhanced` prop recorder
and its `enhanced` log are removed. All legs are kept verbatim, because
every probe records the `KanbanImpl` chunk. The `log` parameter type
narrows to `keyof typeof recorded`. A docblock paragraph records the
removal.

`cardPredicateScope.test.tsx`:

- **Removed:** "KanbanEnhanced binds the ambient predicate scope
alongside the card". Its `KanbanBoard` twin stays and pins the same
verdict on the one registered board.
- **Moved, subject `KanbanEnhanced` → `KanbanBoard`:**
- "a scope-gated condition fails SOFT (no style) outside the provider".
It is now the negative control for the live positive leg, which had none
of its own.
- "a bare-field condition no longer binds the card (objectui#5741)". Its
canonical-spelling control moved with it.
- **Kept unchanged:** "KanbanBoard (impl) binds the ambient predicate
scope alongside the card" and the four relation-field legs
(objectui#3501).

## Other files the deletion forced

- `packages/plugin-kanban/src/index.tsx` (comment only). The
`kanban-enhanced` tombstone said the file "stays". It now records the
deletion, names the new pin, and says that through 17.6.0 the component
was also bundled behind the `kanban-enhanced` key.
- `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`: in
`KNOWN_COLLISIONS`, `ConditionalFormattingRule` drops its third site,
the deleted file. This baseline is shrink-only and fails on a stale
site.
- Ablation on the committed tree: I put the three-site entry back with
`ablation-replace` (anchor 1 → 0, blob `0a8b7933d98f` → `d610bed3eea6`).
- Result: "declares no name twice outside the named, shrinking baseline"
goes red with "KNOWN_COLLISIONS lists collisions that no longer exist",
naming the deleted path.
  - Restore: blob equals HEAD, and `git diff HEAD` is empty.
- `packages/plugin-kanban/package.json` and `pnpm-lock.yaml`: the
dependency removal described above.

## Pending changesets (H4)

Three pending changesets are reconciled in the objectui#10533 / objectstack-ai#10540
form: the sentence is scoped to "at this change" and a dated note is
added. The frontmatter stays byte-identical: the md5 of each file's
frontmatter block is equal on base and head, and
`check-changeset-overwrite` prints the same "declared at base" and
"declares now" lines.

- `8802-8257-8008-kanban-gantt-family-retirement.md`:
"`KanbanEnhanced.tsx` has zero non-test importers. ⛔ The file is
deliberately left in place".
- `6172-markdown-kanban-one-authority.md`: "both files now re-point at
it".
- `8913-object-kanban-columns-declared.md`: "exactly the members the two
board implementations read" and "before either board implementation sees
one". `check-changeset-claims` surfaced this entry once the branch
edited `objectql.ts`.

Read and left as they are:

- `6939-kanban-column-cards.md`: a measurement anchored at `78a3cc238`.
- `9628-kanban-column-collapsed-honoured.md`: describes the reader as it
was before objectui#9628.
- `6172-kanban-trio-rename.md`: it names the `kanban-enhanced` registry
key among "four registered kanban renderers". That went stale with the
objectui#8257 / objectstack-ai#8802 key retirement, not with this file deletion.
- `6349-types-internal-name-collisions-batch-1.md`: flagged for naming
the ratchet file. Its "43 entries → 40" is that change's own count, and
this PR does not change the entry count.
- The other pending entries flagged because they name `complex.ts` or
`objectql.ts` describe members this PR does not touch. The edits in
those files are comment-only on the four kanban passages above.

## `CardTemplate` / `ColumnWidthConfig`: report only, not deleted

Neither type is in `KanbanEnhanced`'s condition. Both are exported
through `.` (the entry's `export type` line from `./types`), and the
deleted module named neither of them (0 hits).

- **`CardTemplate` is consumed.** `CardTemplates.tsx` imports it for its
`templates` prop and its `onSelect` signature, and `CardTemplates` is
itself exported from the entry. Importers of the `CardTemplates`
component in `apps/`, `packages/`, `examples/`, `content/` and
`skills/`, other than its own file and the entry: 0.
- **`ColumnWidthConfig` has no reader.** 5 non-test source files name
it: its declaration in `packages/types/src/complex.ts`, its Zod mirror,
and three re-exports. That matches the reading objectui#8522 recorded.
- Control on the same query: `KanbanCard` is named by the consumers
`KanbanImpl.tsx` and `useQuickAddReorder.ts`.

## Pin (H5): red on the base, green on the head


`packages/plugin-kanban/src/__tests__/kanbanEnhancedRetired-8932.test.ts`
follows the `chart-primitives-removed-7397` file-removal shape
(`existsSync` with a lit control) and this package's
`useColumnWidthsRetired-8522` naming. It pins two things:

- The file is absent from `src/`. Control: `KanbanImpl.tsx` in the same
directory reads present.
- No file under `src/` names the module as a specifier (static or
dynamic import, re-export, `require`, `vi.mock` / `vi.doMock`). Firing
control: the same walk and pattern find the live board's lazy import in
`index.tsx`.

Red on `76265dfdc` (the base plus the pin alone): 2 failed. The first
reported `{ retired: true, live: true }`. The second listed
`__tests__/kanban-handler-slots-7664.test.tsx` and
`cardPredicateScope.test.tsx`, after its control assertion passed. Green
on the head.

## Verification (final head `313ca6321`)

Test runs, all from the repo root under the shared verify lock:

- `pnpm exec vitest run --maxWorkers=2 packages/types/
packages/plugin-kanban/`: 298 files and 5636 tests passed.
- `pnpm exec vitest run --maxWorkers=2 scripts/` (the root-level suite,
which is outside the package graph): 177 files passed, 2 skipped, 5303
tests passed.
- At `2165ee472` (the deletion without the dependency removal) the same
suite had 1 failure: `check-unused-dependencies.test.ts` › "is green:
every gated declaration has a consumer", with `@tanstack/react-virtual`
/ `declared-without-consumer`.

Type-check, lint and build:

- `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && tsc -p
tsconfig.examples.json && tsc -p tsconfig.test.json`): exit 0 (at
`abda7fc09`; the types source is unchanged since).
- `pnpm --filter @object-ui/plugin-kanban type-check`: exit 0.
`--listFiles` on the test program lists the pin and both rewritten
tests.
- `pnpm type-check:scripts`: exit 0.
- `pnpm --filter @object-ui/plugin-kanban lint`: 0 errors.
- `pnpm --filter @object-ui/plugin-kanban build`: exit 0.
- These four ran in the first round. The plugin-kanban source changed
since only in one `index.tsx` comment.

Gates, all exit 0 at `313ca6321`:

- Changesets: `check-changeset-presence` ("7 source file(s) of 2
released package(s) changed, and this change declares 2 changeset(s)"),
`check-changeset-no-major`, `check-changeset-fixed`,
`check:pending-changeset-literals`.
- Report-only changeset gates: `check-changeset-claims` and
`check-changeset-overwrite`. Their findings are addressed above.
- Line citations and bytes: `check:new-line-citations` ("0 new
citation(s)") and `check:control-bytes`.

First-round gates, green at `a843148be` and not reached by the later
edits:

- Mocks and test paths: `check-vi-mock-specifiers`,
`check-vi-mock-inherit`, `check-vi-mock-override-shape`,
`check-test-path-roots`.
- Source and dependency checks: `check:handler-key-reads`,
`check:unreferenced-sources`, `check:unused-deps`, `check:phantom-deps`.
- Lockfile: `check:lockfile-integrity` and `check:lockfile-dedupe`.
- Governed surface: `check-governed-queue-guard --test` answers NOT
GOVERNED.

Merge check: `git merge-tree --write-tree origin/main HEAD` against
`origin/main` `50e41f738` exits 0. `main` has not touched any file this
branch changes since `d22b37bd8`, so no merge was needed.

NOT MEASURED:

- `check:sdui-registration-pins`: it exits 2 without a console build (a
prerequisite, not a verdict), and this diff changes no registration.
- `check:readme-exports`: it needs every package built. The entry's
`dist/index.d.ts` is byte-identical.

## Surface

The claim's file surface named the module, one test, a pin, any pending
changesets, and one new changeset. Beyond that, this PR touches:

- `cardPredicateScope.test.tsx`: it imported the module.
- `index.tsx` (comment only).
- `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`: the
ratchet.
- `packages/plugin-kanban/package.json` and `pnpm-lock.yaml`: the
unused-dependency gate.
- The two `@object-ui/types` files, `packages/types/src/complex.ts` and
`packages/types/src/objectql.ts` (JSDoc only), plus their changeset.
This was asked for by the contract review.

Serial overlaps:

- objectui#10524 edits `ChatbotSchema.body` JSDoc in `complex.ts`, and
objectui#10518 edits `ObjectChartSchema` in `objectql.ts`. Both are far
from the four passages edited here.
- ⚠️ Draft PR objectstack-ai#8941 (the lucide-react bump) edits the adjacent
`lucide-react` line in `packages/plugin-kanban/package.json` and the
same importer block in `pnpm-lock.yaml`. Whichever PR lands second meets
a textual conflict there, cleared by merging `main` and re-running `pnpm
install`.

## Acceptance notes

- Test docblocks still mention `KanbanEnhanced` as it was when they were
written: `columnCollapsedHonoured-9628.test.tsx`,
`examples/schema-catalog/test/kanban-column-cards-6939.test.tsx`,
`object-kanban-columns-declared-8913.test.ts` and
`one-authority-per-exported-name-6273.test.ts`. They are not shipped and
are left alone.
- Observation, not filed: the pending `8802-8257-8008` entry does not
say that the entry's published `kanbanComponents` map narrows from four
keys to `object-kanban` alone. In 17.6.0's typings the map included
`'kanban-enhanced'`. That narrowing is objectui#8257 / objectstack-ai#8802's, not this
PR's.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants