Repository navigation
chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired - #8941
Conversation
…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
❌ Console Performance Budget
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:
📦 Bundle Size Report
Size Limits
|
…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
Bundle Analysis — measured, root-caused, and the ceiling raised deliberatelyRe-measured on my own merge result ( Exactly 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 So icons do not tree-shake here and did not before. That is deliberate, not a defect: The library is genuinely bigger, two independent ways:
So the ceiling moves, in the objectui#8888 / objectui#8901 shape
New headroom is 9,096 bytes = 0.10x
Proof the raised gate can still failBoth legs run the same
What the bytes buy — and the part that makes this not optionalRoughly 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 If the trade still reads wrong, the lever is not this ceiling: it is pinning Generated by Claude Code |
✅ Console Performance Budget
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
Size Limits
|
维护者速读(终稿)改了什么:全仓 22 个包声明的图标库 lucide-react 从 为什么改:上游 1.43.0 改了图标数据结构并给图标加了别名 class,我们的懒加载图标拿不到别名。您选了 C:接受这个差异、落地升级,而不是把别名塞进首屏必载的名单(A)或停在旧版本(B)。 风险与代价(含回滚):两条「说真话」的钉子被削弱;按名字解析的图标不带 lucide 的别名 class,按别名写的样式只对直接 import 的图标组件生效;客户元数据里若写了 席位意见:本席按达档复核 PASS(记录在本 PR 上一条评论)。head 你要做的:一条 APPROVED review(os-zhuang 或 hotlong 账号)。作者账号是 os-zhuang,对它请审会被平台拒,故改为把 PR assign 给 os-zhuang、向 hotlong 请审。批准后席位走中继落地,不需要您合并。 Generated by Claude Code |
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsScope of this record: the base-merge hop
② Semver levelUnchanged from 5853185002: ③ Boundary flags
Implemented-by: 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 续) ·
Generated by Claude Code |
…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>
…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>
… 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>
…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>
…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>
…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>
…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>
… 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>
…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>
Moves
lucide-reactfrom^1.31.0to^1.43.0across the workspace, repairs what the jump breaks, and executes the maintainer's ruling C on this PR (comment5750512894, 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
mainat21d34d5e2(merge commit423a1a59a); the censuses read the committed tree throughgit ls-treeplusgit cat-file, so enumeration and reading come from the same source. The installedlucide-reactis 1.43.0, integrity-equal to the registry tarball (sha512-ubtnda1f…).maindoes not install 1.31.0. Its manifests say^1.31.0, but since thepnpm dedupeof #9316 (2026-09-13) its lockfile resolves 1.35.0, one copy. So this PR moves the declared range^1.31.0to^1.43.0and 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)
lucide-reactlucide-react.d.tsdynamicIconImportskeys (=iconNames)iconsrecordTrash2__iconNode__iconDataaliases1.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-reactresolvesTypeScript-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 typenodes, and static member reads off a namespace import or officons.LucideIcon,LucideProps)6f20cfb06(the 2026-09-20 head)Controls, in the same run: an injected
Trash,type LucideIconand namespace readL.Pencilresolve; an injectedNotARealLucideIconandL.AlsoNotRealare both reported as failures.1c. Census 2 — every authored icon STRING resolves as a key of the runtime
iconsrecordEvery string in an icon-named position (
icon,iconName,...Iconproperties, JSX attributes,resolveIcon(...)arguments) across code, JSON, YAML and Markdown, tokenised with the seam's own rule copied verbatim (split(/[-_\s]+/), capitalise,HometoHouse), judged against each version's record.trash-2as the spelling it retirededit,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-namesOK 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, injectedicon: 'trash-2'in JSON and in TS both read as regressions. Independently, a case-insensitivegit grepfor every spelling that tokenises toTrash2finds only component imports ofTrash2(still a runtime export in 1.43.0), comments, and onevi.mockstub key: no authored string. The same census overmainfinds exactly 6 string sites that tokenise toTrash2(5trash-2, 1Trash2, one per file); they are the ones the 2026-09-20 round repaired, and all 6 readtrashhere.The glyph is unchanged by that repair, measured on icon nodes:
keytrashvs 1.31.0trash-2trashvs 1.35.0trash-2pencilvs 1.31.0 / 1.35.0penciltrashvs 1.35.0trashtrashname, so anything that already authoredtrash(10 string sites onmain) now draws whattrash-2drew. Upstream also redrew other glyphs. Comparing the artwork each name draws on 1.35.0 against 1.43.0 (controls:Trash2reads unchanged,Trashreads changed), the ones this tree uses are the componentsCheckCircle2(29 importing files),Table2(8),CircleDot(5),CircleCheck(2),Asterisk(1) andBadgeCheck(1), and the stringstrash,shopping-cart,circle-check/CircleCheckandtable-2. Redraws, not breakage.1d. Census 3 — every reader of
__iconNode/__iconDataworksA
git grepfor__iconNode|__iconData|createLucideIcon|LucideIconDataover the whole tree finds exactly one reader: the lazy draw inpackages/components/src/renderers/action/resolve-icon.ts, which readsmodule.__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, throughscripts/ablation-replace.mjsfrom../objectstack(anchor must hit exactly once, write verified on disk, restore proven by blob hash and an emptygit diff HEAD):Real-build probe, on the console built at
989330df0(apps/console/dist), in Chromium: of 2345 built JS chunks, 1819 mention__iconData(the 1818vendor-icon-*chunks plus the seam'sui-componentschunk) 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:__iconData.named__iconNodevendor-icon-house-*househomeM15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8vendor-icon-trash-*trashtrash-2M10 11v6vendor-icon-arrow-down-0-1-*arrow-down-0-1arrow-down-01m3 16 4 4 4-4vendor-icon-loader-circle-*loader-circleloader-2M21 12a9 9 0 1 1-6.219-8.56So 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-NAMEclass token in the tree, against the 1.43.0 alias indexEvery
lucide-token in every tracked text file (lockfile and binaries excluded), classified against an alias index built from the 1818 installed icon modules (canonicallucide-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 fromlucideClassNames), and against what 1.35.0 emitted.lucide-icon, script names such aslucide-record-icon-names)lucide-home(ofhouse)lucide-loader-2(ofloader-circle)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.0lucide-trash-2,lucide-trash2Controls: an injected
svg.lucide-trash-2assertion is classified as an alias oftrash; an injectedsvg.lucide-checkreads 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
mainmoved 294 commits past the old merge base078f2e4f7. What the four censuses see that the6f20cfb06tree did not: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(avi.mockof the package); plusCode2inFlowKeyValueField.tsxandBookOpeninNavigationDesigner.tsxshare,map-pineditsites inmenubar-item-icon.test.tsx, which are deliberate retired-spelling negatives, dead on all three versions__iconNode/__iconDatareaderspackages/components/src/__tests__/menubar-item-icon.test.tsx:lucide-chevron-right,lucide-download,lucide-file,lucide-mail,lucide-save,lucide-shareNone of them needed a repair.
1g. The merge and the lockfile
origin/mainmerged at21d34d5e2(⛔ no rebase, no amend, no force-push): one conflict,packages/plugin-kanban/package.json, resolved as the union (main deleted@tanstack/react-virtualwith the module that used it; this branch bumpslucide-react).pnpm-lock.yamlwas regenerated by pnpm from main's own lockfile, ⛔ not hand-merged: main's file taken whole, thenpnpm install --lockfile-onlywith the 22 manifests briefly at an exact1.43.0and again with^1.43.0restored, 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 declarelucide-reactread^1.43.0.2. Ruling C — the two pins, relaxed and no more
houserenderslucide-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)andTest (shard 4/4)reported red on6f20cfb06, read off those check runs' own annotations:button-shared-icon-resolver.test.tsx>resolves home through the RENAME alias to House, not to a dead home—expected null to be SVGSVGElement;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.ElementTypeand the placeholder inlucide-ICONNAMEare 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.tsxBefore (
6f20cfb06):After:
Pin 2 —
packages/components/src/renderers/form/__tests__/button-shared-icon-resolver.test.tsxBefore (
6f20cfb06):After:
(On
mainthis row still readsexpect(button.querySelector('svg.lucide-home')).toBeNull();, which is green under the seam on either version. ⛔ It is not restored: lucide 1.43.0 putslucide-homeon 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:
(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.
lucideClassNamesclaimed 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 inresolve-icon.tschanged this round.⛔ No third pin. The full suite ran locally at
989330df0in 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 eightTest (shard N/8)andTest (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.pnpm turbo run type-check --concurrency=2Tasks: 81 successful, 81 total, 0error TSpnpm type-check:scripts,type-check:vitest-setup,type-check:vitest-configerror TSeachpnpm turbo run lint --concurrency=2Tasks: 47 successful, 47 total, 0 errors in every task (warnings only). The three files changed this round, linted alone: 0 errors, 0 warningspnpm turbo run build --filter='./packages/*' --concurrency=2, thenpnpm --filter @object-ui/console buildnode scripts/check-eager-closure-budget.mjsConsole 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'snode scripts/check-eager-locale-catalogues.mjs,pnpm check:sdui-registration-pinspnpm test:dist;check-node-esm-load,check-published-dist-tooling,check-dist-completeness --allpnpm check:icon-record-names;node scripts/regenerate-lucide-record-icon-names.mjs --checknode scripts/check-lockfile-integrity.mjs --basemain's lockfile--head pnpm-lock.yaml, and the default invocation;check-lockfile-dedupeVERDICT clean — no @objectstack/* identity moved backward and no package gained a copy(1773 resolutions on both sides);VERDICT dedupedcheck-changeset-presence,-no-major,-fixed,-overwrite,check-pending-changeset-literalscheck-changeset-claimsis report-onlycheck-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 threecheck-vi-mock-*,check-doc-fence-languages,check-doc-example-ids,check-shell-escape-residue,check-action-ref-convention,check-phantom-dependencies,check-unused-dependenciesnode scripts/check-governed-queue-guard.mjs --testover the 42 pathsGOVERNED — 1 of 42989330df0, read when this was writtendependabotand the two coverage lanes), 0 failed. The seat re-reads itNOT MEASURED locally:
Build & E2Eand 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, allpatch, the 20 released packages whosedependenciesrange moves,@object-ui/console(itsdevDependenciesrange moves and its bundle ships the new icons), and@object-ui/cliand@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 theTrash2class 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-reactrange inskills/objectui/guides/plugin-development.md, anchored to the in-repo range bydoc-version-claims.test.ts. Readings: that file 481 lines before and after; the publishedskills/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 AnalysisNOT MEASURED and the eslint narrowing below are replaced by section 3; twopath:linecitations 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 currentmain. 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 behindmain. I brought its head up to date in a throwaway worktree, re-locked from currentmain, and re-measured. Four of the seven were artefacts of the stale base; three were real.Type Checkpnpm check:icon-record-names, not attsc.tscnever ran.Test (shard 1/4)packages/cli/src/__tests__/app-generator.test.ts— the generator's literallucide-reactrange stayed at^1.31.0while the 22 sibling manifests moved.Test (shard 3/4)check-lucide-icon-record-names.test.tsREAL (same four sites).ci-cd-pipeline-doc.test.tsSTALE — the lockfile carried two@objectstack/specversions.Test (shard 4/4)doc-version-claims.test.tsREAL (skills/objectui/guides/plugin-development.mdstill taught^1.31.0).check-lockfile-integrity.test.tsSTALE — its fixture forks azod@4.4.3key the stale lockfile had replaced with4.5.4.Lockfile Integrity CheckVERDICT 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,typesfrom 17.4.0 back to 17.3.0, plus azod4.4.3/4.5.4 split.Bundle Analysisvendor-objectstack 2874.0 KB / 1224.6 KB ceiling (OVER by 1649.3 KB)— that is the duplicated@objectstacktree 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."dependabotOn this branch, re-locked from current
main,node scripts/check-lockfile-integrity.mjsreportsVERDICT 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:
lucide-reactlucide-react.d.tsiconNamesfromlucide-react/dynamic.mjsiconsrecordTrash2Then 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:Trashresolves in both, a fabricated name resolves in neither.So nothing breaks at an import and nothing breaks in the compiler.
Trash2is still exported by 1.43.0; it is simply no longer a key of the runtimeiconsrecord, andTrash2 === Trashthere (in 1.31.0 they were distinct objects). That is exactly the classscripts/check-lucide-icon-record-names.mjsexists 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-2throughrenderers/action/resolve-icon.ts, which reads record membership:examples/schema-catalog/src/schemas/actions/action-button-variants.jsonexamples/schema-catalog/src/schemas/actions/confirmation-dialog.jsonexamples/schema-catalog/src/schemas/components-layout-card/profile-detail-card.jsonpackages/plugin-detail/src/DetailView.tsx— the record delete actionAll four now write
trash. The glyph does not change: 1.43.0'strashicon node is byte-identical to 1.31.0'strash-2icon node (control:pencilis byte-identical across both versions, so the comparison is not returning equal for everything). Upstream replaced the old bin artwork under thetrashname and kepttrash-2as 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
mainresolves the newest 1.x — that is 1.43.0 today, under^1.41.0just 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
createLucideIconto take an icon-data object carryingaliases, and it now emits one CSS class per retired alias. A spinner rendersclass="lucide lucide-loader-circle lucide-loader-2 ...". Measured: 248 of 1818 icon modules carry an alias; 1.31.0 icon modules have noaliasesfield at all. This is additive, so a.lucide-loader-circleselector still matches — but a selector asserting the exact, complete class string does not, and neither does an assertion that a givenlucide-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 byhouse) andlucide-loader-2(owned byloader-circle). Controls: injecting the known aliastrash-2into the census is found;checkis correctly absent. Both affected pins are repaired here:examples/schema-catalog/test/svg-host-dom-leak-5632.test.tsxasserts 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.tsxprovedhomeresolved through the rename toHouseby assertingsvg.lucide-homeis null.housenow emitslucide-homeitself, 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,Homeis not a record key and the button would render no glyph at all.4. Something the bump gives back
maininstalls two physical copies of the icon library: 1.31.0 for this workspace and 1.35.0 pulled byfumadocs-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).trash-2inDetailView.tsx'ssys_deleteaction 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 emptygit diff HEAD, not by an exit code.pnpm type-checkacross the workspace — turbo81 successful, 81 totalover 45 packages, zeroerror 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.mjsandcheck-changeset-no-major.mjs— both green.Declared narrowing: the full four-shard
pnpm testand the repo-widepnpm lintare left to CI.eslintover the changed source files reports one pre-existing error inaction-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 againstmain. TheBundle Analysismeasurement 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 --testreportsGOVERNED — 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-reactrange, whichdoc-version-claims.test.tsanchors to the in-repo range. ItsKNOWN_CLAIMSentry is keyed by the literal text, so the inventory key moved with it.Generated by Claude Code