Repository navigation
fix(types): one SchemaNode and one label vocabulary — the union wins, labels resolve where the locale lives (#4580) - #4608
Merged
Conversation
… labels resolve where the locale lives (#4580) Piece B: core's hand-declared `interface SchemaNode` becomes a re-export of `@object-ui/types`' union (ruling 1). Piece A: `BaseSchema.label`/`.description` widen to `string | I18nLabel`, resolved at READ time against the display locale (revised Q1-A ruling, comment 5284973826) — the compiler inventory named four read sites, all one class. Piece C: the spec bridges declare `BaseSchema` returns instead of the union (ruling Q4-B), a type annotation only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…anode-round2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
1 similar comment
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
…4580) `typesNode` and `acceptsCoreNode` are `declare`d — type-level fictions with no runtime existence — so the top-level `const coreNodeHoldsTypesNode = typesNode` type-checked correctly but threw `ReferenceError: typesNode is not defined` the moment vitest imported the module, failing the suite before any case ran. Round 1 only ever type-checked this file, so the defect was latent in the preserved pin. Moved into a never-called function: `tsc` checks the body just as thoroughly and it never executes. Re-verified that the pin still goes red when core's re-export is reverted, naming both `dist` identities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…anode-round2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
1 similar comment
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 13, 2026 20:23
This was referenced Aug 14, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 17, 2026
…the docs site builds again (objectstack-ai#4617) (objectstack-ai#4621) `Build Docs` (`next build`, which type-checks apps/site) has failed on every push to `main` since PR objectstack-ai#4608 landed, with 5x TS2322 at the five call sites that hand a `SchemaNode` to `SchemaRenderer`'s `schema` prop. Route all five through `toRenderableSchema`, the adapter objectstack-ai#4548-Q2 kept for exactly this crossing: `SchemaNode` deliberately includes `number`/`boolean`, SchemaRenderer's component union deliberately excludes them. Not a prop widening, not a cast. Also close the gate gap that let this reach `main` green: apps/site's type-check script was named `types:check`, so `turbo run type-check` listed it as `<NONEXISTENT>` and never ran it. Renamed to `type-check`; its CHECKED_BY_OWN_BUILD exemption retires with the rename, as that gate's own ratchet requires. Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 Co-authored-by: Claude <noreply@anthropic.com>
os-sales
pushed a commit
that referenced
this pull request
Aug 21, 2026
…#4622) The closing paragraph of `toRenderableSchema`'s JSDoc said the two competing repo-wide `SchemaNode` spellings "have not been reconciled" and that "when it lands, the call sites using this can go back to forwarding directly". Both halves went false when PR #4608 merged, and the second half is the harmful one: it is an instruction whose trigger condition has now fired, sitting directly above the function a future author is about to call. The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of `@object-ui/types`' union — `packages/core/src/types/index.ts:41` is now `export type { SchemaNode } from '@object-ui/types';` — while SchemaRenderer's prop stays deliberately narrow per objectui#4548 ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So a `SchemaNode` became LESS assignable to that prop, not more, and the bridge is a permanent crossing between two intentionally different types. Following the old instruction has a measured cost, and the new paragraph names it: five `apps/site` call sites were forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly five hours until PR #4621 routed all five through this function (objectui#4617). Comment-only. The emitted JavaScript is byte-identical (both revisions of the file transpiled with `tsc --removeComments`, sha256 ce9ba5c4ce7590dce63e5ca853cfb7a79d55df338d1fd32403b215b39cefda06 on each side), and no changed line falls outside the JSDoc block. The paragraph does ship, which is why a patch changeset is owed: this package builds with plain `tsc`, so the comment is carried into `dist/schema-input.d.ts`. Fixes #4622 Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 21, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 23, 2026
…objectstack-ai#4622) (objectstack-ai#5602) * docs(react): toRenderableSchema's header says the bridge is permanent (objectstack-ai#4622) The closing paragraph of `toRenderableSchema`'s JSDoc said the two competing repo-wide `SchemaNode` spellings "have not been reconciled" and that "when it lands, the call sites using this can go back to forwarding directly". Both halves went false when PR objectstack-ai#4608 merged, and the second half is the harmful one: it is an instruction whose trigger condition has now fired, sitting directly above the function a future author is about to call. The reconciliation (objectui#4580 / PR objectstack-ai#4608) resolved the collision in favour of `@object-ui/types`' union — `packages/core/src/types/index.ts:41` is now `export type { SchemaNode } from '@object-ui/types';` — while SchemaRenderer's prop stays deliberately narrow per objectui#4548 ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So a `SchemaNode` became LESS assignable to that prop, not more, and the bridge is a permanent crossing between two intentionally different types. Following the old instruction has a measured cost, and the new paragraph names it: five `apps/site` call sites were forwarding directly when PR objectstack-ai#4608 landed, and `Build Docs` was red on `main` for roughly five hours until PR objectstack-ai#4621 routed all five through this function (objectui#4617). Comment-only. The emitted JavaScript is byte-identical (both revisions of the file transpiled with `tsc --removeComments`, sha256 ce9ba5c4ce7590dce63e5ca853cfb7a79d55df338d1fd32403b215b39cefda06 on each side), and no changed line falls outside the JSDoc block. The paragraph does ship, which is why a patch changeset is owed: this package builds with plain `tsc`, so the comment is carried into `dist/schema-input.d.ts`. Fixes objectstack-ai#4622 Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK Co-authored-by: Claude <noreply@anthropic.com> * docs(react): correct the emit claim on this branch — the JSDoc does ship as bytes The changeset on the previous commit said "the emitted JavaScript is byte-identical". That is FALSE, and the way it became false is worth recording, because it is the same failure mode this card exists to prevent — a confident claim that does not survive being checked. The measurement behind it transpiled both revisions with `tsc --removeComments` and compared sha256s. That is a correct measurement of a build this repo does not perform: `tsconfig.base.json:22` sets `"removeComments": false` deliberately, and this package builds with plain `tsc`, so comments are emitted into `dist/*.js`. The flag measured away the very thing being measured. Re-measured by building the package the way the repo builds it, at both revisions, with `dist/` cleared between runs: dist/schema-input.js 1,486 B -> 2,316 B (1.45 KB -> 2.26 KB) gzipped 850 B -> 1,246 B (0.83 KB -> 1.22 KB) +830 B raw, +396 B gzipped which reproduces CI's bundle-size rows for this PR (2.26 KB / 1.22 KB) and for two sibling PRs cut from the same merge-base that do not touch this file (1.45 KB / 0.83 KB). What IS true, and is what the changeset now says: all 18 differing lines in the emitted file are JSDoc continuations, and the three executable lines are byte-identical, so the growth is the paragraph and nothing else. The source change remains comment-only — the `git diff -U0` structural check is unaffected and still holds. The doc paragraph itself is unchanged: it makes no claim about emitted bytes, and the size is not being chased. Roughly 0.4 KB gzipped is the deliberate price of not repeating a five-hour `Build Docs` outage. Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK Co-authored-by: Claude <noreply@anthropic.com> * docs(react): scope the union spelling to objectstack-ai#4617's logs, so it cannot go stale PM ruling on the wording of the TS2322 clause: neither "against this function's return type" alone nor a bare `string | BaseSchema | null | undefined` alone, but both, with the literal spelling scoped as a historical quotation. The reasoning is the axis I had not weighed. Describing the type by its ROLE is self-updating — if `SchemaRendererProps['schema']` gains or loses a member tomorrow, the sentence stays true with no edit. Hardcoding today's spelling as a present-tense claim is NOT self-updating, and would have added a fresh way for this paragraph to go false, which is the exact defect the paragraph exists to fix. Attributing the spelling to what objectstack-ai#4617's logs said in August 2026 is a fixed fact that cannot rot however the union evolves, and it hands a reader with the log open the exact string to match in one hop. - `number` against this function's return type — until PR objectstack-ai#4621 routed all five - through here (objectui#4617). + `number` against this function's return type, spelled + `string | BaseSchema | null | undefined` in objectui#4617's logs — until + PR objectstack-ai#4621 routed all five through here. The paragraph grew, so both byte figures in the changeset are re-measured rather than left stale — the same build, at both revisions, with `dist/` cleared between runs: dist/schema-input.js 1,486 B -> 2,377 B (1.45 KB -> 2.32 KB) +891 B gzipped 850 B -> 1,266 B (0.83 KB -> 1.24 KB) +416 B previously 2,316 B / 1,246 B for the shorter paragraph. All 19 differing lines in the emitted file are JSDoc continuations (was 18) and the three executable lines remain byte-identical, so the growth is still the paragraph and nothing else. Noted for next time, per the PM: quoting exact byte counts in a changeset couples it to the comment's length, which is what made this edit cost a re-measure. Still the right call here — the numbers are the honest replacement for a claim that was wrong — but a cost to choose deliberately rather than by default. Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
os-sales
pushed a commit
that referenced
this pull request
Sep 17, 2026
…nce instead of letting it read as live
Three docblocks stated a retired producer's behaviour in the PRESENT tense
("assigns", "already writes"), one of them as the stated factual basis of
objectui#4580's revised Q1 ruling. The producer was removed by objectui#6366
(PR #6632) as a declared BREAKING CHANGE, so the sentences read as live while
being false, which is the shape AGENTS.md #9 forbids.
Established per site at the write-time ref, not per card: the sentences were
TRUE when PR #4608 wrote them on 2026-08-13 — at that commit
`packages/react/src/spec-bridge/bridges/list-view.ts` exported `bridgeListView`
and carried the label and description assignments at exactly the cited
addresses. They are therefore ROTTED, not born false, so each is KEPT, quoted,
dated, and followed by a dated note naming objectui#6366 as the falsifier.
Deleting them would erase that objectui#4580 ever had a stated rationale.
What the docblocks now assert points at instruments rather than at a retired
producer: the declared face is re-derived by
`inline-locale-declared-face-9092.test.ts`, the read side by the renderer pins.
No figure is written down.
Nothing executable moves — the diff is comment-only plus one changeset.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
This was referenced Sep 18, 2026
This was referenced Sep 27, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 28, 2026
…e instead of letting it read as live (objectstack-ai#9365) (objectstack-ai#9742) Fixes objectstack-ai#9365 Clause-②: no Three docblocks described `bridgeListView` — a producer objectui#6366 retired as a declared BREAKING CHANGE — in the PRESENT tense (`assigns`, `already writes`), one of them as the stated factual basis of objectui#4580's revised Q1 ruling. Nothing re-derives those sentences and nothing can, which is the shape AGENTS.md objectstack-ai#9 forbids. This is a paper-trail repair: nothing executable moves and objectui#4580 is **not** re-litigated. ## Per-site verdict — established at the WRITE-TIME ref, per SITE `git log -S` named one write-time ref for all three sentences: `c1d939f7f` — "fix(types): one SchemaNode and one label vocabulary" (objectstack-ai#4580, PR objectstack-ai#4608), 2026-08-13. Reading the tree AT that ref is what rules BORN FALSE out, and it rules it out byte-exactly. | site (addressed by symbol) | verdict | established at which ref, and on what reading | repair taken | |---|---|---|---| | `BaseSchema.label` docblock — `packages/types/src/base.ts` | **ROTTED** | `c1d939f7f`: that tree carries `export const bridgeListView` in `packages/react/src/spec-bridge/bridges/list-view.ts`, and the cited line is exactly `if (spec.label) node.label = spec.label;` | sentence KEPT, quoted and dated to its write-time ref, followed by a dated note naming the falsifier; the assertion now points at an instrument | | `BaseSchema.description` docblock — same file | **ROTTED** | `c1d939f7f`: the cited line in the same module is exactly `if (spec.description) node.description = spec.description;` | same | | header docblock of `packages/components/src/__tests__/inline-locale-label-read-sites.test.tsx` | **ROTTED** | `c1d939f7f` — the same commit wrote all three sentences | same | | the column-spelling comment in `packages/plugin-grid/src/ObjectGrid.tsx` | **NOT ROTTED — measured, not assumed** | read WHOLE on `20b5e361e` | none. "The one in-repo producer that spoke it, `bridgeListView` in `@object-ui/react`, was migrated in the same PR" is a PAST-tense account of objectui#5068's migration. It was true when written and it is true now. | | the retired-witness comment at the tail of `packages/plugin-grid/src/__tests__/gridNonAuthorKeys.test.tsx` | **CORRECT** | read WHOLE on `20b5e361e` | none. It already records the objectui#6366 retirement in the past tense. | ⇒ the card's "three rotted / two probably fine" split — which the card itself flagged as a one-line-of-grep-context reading — survives a whole-docblock re-read. **None of the five is BORN FALSE**, and all three repairs are the ROTTED route (keep + date + name the falsifier), not the correction route. **The falsifier, dated:** `bf283414f` — "refactor(react): retire the spec-bridge — SpecBridge, bridgeListView, bridgeFormView (objectstack-ai#6366) (objectstack-ai#6632)", 2026-08-29. `.changeset/retire-spec-bridge-6366.md` declares it BREAKING and removes the public exports outright, so the producer, its module and the addresses the sentences cite are all gone. **What the docblocks assert now.** The declared face is re-derived on every run by `inline-locale-declared-face-9092.test.ts` in `packages/types` (both `tsc -p packages/types/tsconfig.test.json` and vitest read it); the read side by the renderer pins in `@object-ui/components` and `@object-ui/plugin-dashboard`. No figure is written down — the instrument is named instead (AGENTS.md objectstack-ai#9). ⛔ No new gate, probe or citation was added: the only `path:line` addresses in the diff are the two the dead sentences already carried, kept verbatim inside the quotation. ## Measurement — the quotation trap, taken head on ⭐ A count is invariant under quotation, and the ROTTED route quotes what it retires. So the token and the rot PHRASE were each read in ASSERTION position and in QUOTED position, separately, with a whitespace-tolerant scanner over whole file contents (` * ` prefixes stripped, line wraps joined) rather than a line-anchored `git grep`. Corpus: every git-tracked file under `packages/`, `CHANGELOG.md` excluded. | reading (source files, CHANGELOG excluded) | BEFORE (assertion / quoted) | AFTER (assertion / quoted) | |---|---|---| | token `bridgeListView` | 5 / 0 | 5 / 3 | | phrase "bridgeListView assigns" (wrap-tolerant) | **3 / 0** | **0 / 3** | | phrase "producer already writes" (wrap-tolerant) | **2 / 0** | **0 / 2** | | **LIT CTRL** token `SchemaRenderer`, same instrument, same corpus | 3473 / 5 | 3473 / 5 | | **ABSENT CTRL** a minted token | 0 / 0 | 0 / 0 | Read the bare token row as the trap it is: it goes UP, because the repair's own dated notes name the three retired exports. The reading that answers the card is the PHRASE in ASSERTION position: 3 to 0 and 2 to 0, with every one of those occurrences still present in quoted position, which is the ROTTED route working as intended.⚠️ And the wrap-tolerance is load-bearing rather than ceremonial: the test-header sentence wraps between `bridgeListView` and `assigns`, so a line-anchored probe reads 2 where the truth is 3. **Whole-tree population.** 11 mentions across 5 files, not the card's 5 across 4. The extra six are all in `packages/react/CHANGELOG.md` — published release history, past tense, CORRECT, and ⛔ untouched. ## Gates and tests — every exit code captured to a file BEFORE any pipe | run | exit | reading | |---|---|---| | `check-changeset-presence` | 0 | "2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)" | | `check-changeset-overwrite` · `check:changeset-claims` · `check:pending-changeset-literals` · `check-changeset-no-major` · `check-changeset-fixed` | 0 each | — | | `check:control-bytes` | 0 | — | | `check:new-line-citations` | 0 | "VERDICT new-cross-file-line-citations: 0 new citation(s)" — the quoted addresses are byte-identical, so the multiset does not move | | `check-governed-queue-guard --self-test` | 0 | and `--test` on the three paths: "NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched" | | `check:doc-component-types` | 0 | it reads `packages/types/src/base.ts`; "Every documented component type is registered" | | `pnpm --filter @object-ui/types type-check` | 0 | `tsc --noEmit` + `tsconfig.examples.json` + `tsconfig.test.json` | | `pnpm --filter @object-ui/types test` | 0 | Test Files 203 passed (203) · Tests 4762 passed (4762) | | `pnpm --filter @object-ui/components type-check` | 0 | `tsc --noEmit` + `tsconfig.test.json`, after building that package's dependency closure | | `pnpm exec vitest run packages/components/src/__tests__/inline-locale-label-read-sites.test.tsx` | 0 | Test Files 1 passed (1) · Tests 9 passed (9) | | the three `scripts/__tests__/` suites that read either changed file | 0 | Test Files 3 passed (3) · Tests 57 passed (57) | Every heavy run went through the container's shared verify lock; each one printed `VERDICT command-exit 0`. **NOT MEASURED, with reasons.** `check:doc-example-types` exited **2** = PREREQUISITE NOT MET: it needs every package's `dist` on disk (a whole-monorepo turbo build). It is a repo-level scan and CI's run; a comment-only diff moves no type it reads. `@object-ui/types`'s dependency closure build is empty — `pnpm --filter "@object-ui/types^..." build` selects no workspace package with a build script — so step ① is a no-op for that package and was run for real for `@object-ui/components`. **Declared narrowings.** `eslint --no-inline-config` was run on the two changed files only, from the flat config that owns the population (`eslint.config.js`, whose lint population is `**/*.{ts,tsx}` minus its `ignores` block): 2 files linted (count read from `--format json`), 0 errors, 21 warnings, ALL of them pre-existing `@typescript-eslint/no-explicit-any` — `base.ts` reads 20 warnings both on `origin/main` and here. No type-aware linting is configured (no `project` / `projectService` in `eslint.config.js`), so a comment-only diff cannot move the verdict on any file it does not touch. Repo-wide `pnpm lint` is CI's run. The full `@object-ui/components` suite is likewise CI's; the changed file there is a header comment and the file itself was run. ##⚠️ One declared deviation from the dispatch word — the changeset is `patch`, not empty The dispatch asked for EMPTY frontmatter on the ground that no package moves. **Measured, it does.** `@object-ui/types` publishes `files: ["dist", ...]`, and after `pnpm --filter @object-ui/types build` the repaired prose is in `packages/types/dist/base.d.ts` (2 hits for the new dated note; POSITIVE CONTROL: 2 hits for neighbouring prose known to ship; ABSENT CONTROL: a minted token, 0 hits, exit 1). Published bytes therefore move, and `.changeset/9375-base-i18n-vocabulary-docblock.md` — the same file, the same class of comment-only repair — graded itself `patch` for exactly this reason. So this PR declares `'@object-ui/types': patch` and states the conflict rather than picking a side quietly. The components-side change is a test header, which reaches no tarball (`tsconfig.json` excludes `**/__tests__/**`), and is owed no declaration of its own. ## Out of scope, deliberately - ⛔ The `bridgeFormView` / `SpecBridge` sweep the card names as the obvious next question was **not** run and is not widened into here. - ⛔ `packages/react/CHANGELOG.md` is untouched. - ⛔ No label was added, removed or changed on this PR or on the card. --- _Generated by [Claude Code](https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L)_ Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4580.
Executes, as ONE seat with one repo-wide canary, the three pieces #4580's rulings left open: the revised Q1-A label-vocabulary ruling, the SchemaNode reconciliation (original ruling 1), and Q4-B (bridge return types).
The ruling chain
label/descriptionstaystring; bridges resolvelabel/descriptionwiden; resolve at READ timeBaseSchemaThe revision is the load-bearing one. Q1-B was ruled first and PR #4603's seat measured its premise false on every leg — the bridge is a plain class method that cannot call a hook,
BridgeContextdeclares no locale,updateContext()has zero callers. The PM revised rather than leaving the reconciliation permanently blocked:Piece A — the widening, and the inventory it produced
BaseSchema.labeland.descriptionare nowstring | I18nLabel, whereI18nLabelis the spec's INLINE locale map. Measured against the installed@objectstack/spec17.0.0-rc.6 rather than assumed:so
string | Record< string, string >— a real widening, not a no-op.The compiler inventory IS the audit. The widening turns every blind
schema.label-as-string read into a named TS2322. Repo-wide downstream canary,--continueso every package reports:components/src/renderers/complex/filter-builder.tsx:29{schema.label}into a< label >text node@object-ui/componentscomponents/src/renderers/navigation/sidebar.tsx:108{schema.label}into< SidebarGroupLabel >@object-ui/componentscomponents/src/renderers/overlay/dropdown-menu.tsx:61{schema.label}into< DropdownMenuLabel >@object-ui/componentsplugin-dashboard/src/DashboardGridLayout.tsx:328{schema.title || schema.label || 'Dashboard'}into< h2 >@object-ui/plugin-dashboardAll four report the identical error, e.g.:
Four sites, one class — well under the card's ~25-site STOP threshold, so piece A shipped.
descriptionproduced zero read-site errors. Both packages already depend on@object-ui/i18n, so both are i18n-reachable and nopackages/layout-style no-i18n-dependency workaround was needed.Red-first — the harm is a THROW, not
[object Object]Measured on the unfixed source before any edit, by invoking each registered renderer directly. All four sites, byte-identical message:
A text node is a position React refuses outright rather than stringifying, so the pre-fix harm is the whole subtree failing to render, not a cosmetic mis-render. This is worth stating because the card predicted
[object Object]or a throw; it is the throw.dropdown-menufirst measured as an EMPTY container with no throw — Radix mountsDropdownMenuContentlazily, so the label is unreachable with the menu closed. A case written that way would have shipped looking green while proving nothing.defaultOpen: trueis load-bearing in the shipped test for exactly that reason, and the test header says so.The fixes
Sites 1-3 resolve with the spec's own
resolveI18nLabelagainstuseDisplayLocale(), aliased on import following PR #4169's convention (andAppSchemaRenderer.tsx:44's use of it), because this repo has its ownresolveKeyedI18nLabelover a different vocabulary. Hooks are legal in these renderers: the registry renders its entries withReact.createElement(SchemaRenderer.tsx:621), andelements.tsx:380already callsuseDisplayLocale()in one. Two renderers gained a block body solely to host the hook.Site 4 deliberately uses
pickLocalizedagainst the active UI language instead, and this is a departure from the card's letter that I want reviewed.DashboardGridLayoutalready resolves the sibling slot —widget.title, the same inline-map vocabulary — withpickLocalized(widget.title, language)seventy lines below. Adding the spec resolver plususeDisplayLocale()would put two resolvers AND two locale channels in one component for one vocabulary, and those channels genuinely disagree:useDisplayLocale()prefers the tenant's regional default over the active UI language, so a heading and the widget titles beneath it could resolve to different languages in the same render. This is not a lenient fallback standing in for the real resolver —pickLocalizedis objectui's limb-for-limb twin ofresolveI18nLabel(objectstack#6765 aligned them deliberately), differing only in how each spells a miss (''vsundefined), pinned inplugin-list/src/__tests__/i18nLabel-resolver-parity.test.ts. The||chain is preserved exactly, and a test pins that the'Dashboard'backstop still fires.The resolver's documented fallback is pinned, not assumed
Cases cover exact match, base/region (
zh-CN↔zh), and the last-resort limb ({ 'ja-JP': … }viewed asfr), per the resolver's own doc. A resolver that only ever hits the exact limb is indistinguishable from a lookup that ignores the locale, so the other two limbs are pinned separately. String-passthrough-unchanged is pinned at every site.PIN MOVED
base-schema-label-vocabulary.test.ts'sassertionLabel/assertionDescriptionpinnedlabel/descriptionasstringunder Q1-B, and that file's header predicted in advance that "a future card that 'fixes' the bridge defect by wideningBaseSchema.labelturns them red on purpose." That is exactly what happened, on the very next card. Measured red before the move:They now pin the widened unions, annotated
PIN MOVED (objectui#4580 revised Q1), with the superseded history kept rather than rewritten. A newassertionVocabulariesAreDistinctpins thatlabelandariaLabeldo NOT collapse into each other — without it, an edit that "simplified" one union into the other would leave bothEqualpins green while silently swapping which resolver owns the slot. EveryEqualis invariant, for the reason #4593 established: a one-wayextendsis vacuous for a widening in both directions.The bridges are untouched by piece A — verified, not asserted
The reverse verification proves this directly. With piece C still applied and ONLY piece A reverted, the two bridge sites go red again at exactly the lines #4593 measured:
So it is piece A's widening — not piece C's annotation — that makes
node.label = spec.labelandnode.description = spec.descriptioncompile, and both assignment expressions are byte-unchanged.Piece B — one
SchemaNodepackages/core/src/types/index.ts's hand-declaredinterface SchemaNodebecomesexport type { SchemaNode } from '@object-ui/types';. No structural copy: a copy reproduces the defect the moment either side moves.Core's entry surface is unchanged — measured, both rounds:
packages/core/dist/index.d.tsis byte-identical across the change (sha2563d1bf0ff…21fb25on both trees).The preserved pin, restored and red-first
Round 1's withdrawn reconciliation pin was preserved byte-identical (sha256
1294d525…8e4d4) and is restored aspackages/react/src/__tests__/SchemaNode.reconciliation.test.ts. It lives in@object-ui/reactbecause the collision is only observable from a package resolving BOTH throughnode_modules; that package'stsconfig.test.jsonsets"paths": {}precisely so the twodistidentities are real.With piece B reverted, all three of its written-first predictions hold, and the error text names both identities verbatim — the #4548 class:
The pin as preserved could not run under vitest.
typesNodeandacceptsCoreNodearedeclared — type-level fictions with no runtime existence — so the top-levelexport const coreNodeHoldsTypesNode: CoreSchemaNode = typesNode;type-checked correctly but threw on import:failing the whole suite before a single case ran (
Test Files 1 failed | 44 passed,Tests 604 passed— zero test failures, a load failure). Round 1 only ever type-checked this file, so the defect was invisible there. It is moved into a never-called function, whichtscchecks just as thoroughly and which never executes. The pin was then re-verified red against a reverted core after the fix — the errors above are from that second run, so the fix is proven not to have made it vacuous.Piece C — the bridges return
BaseSchemaBridgeFn,SpecBridge.transform/transformListView/transformFormView, and both bridge functions now declareBaseSchema. Both bridges end in a singlereturn nodeon an object literal (list-view.ts:226,form-view.ts:195), so the union described nothing real while forcing a narrowing at every read.Reverse verification, piece C alone reverted — 272 errors, matching round 1's measurement exactly, across the same five suites:
P1SpecBridge.test.tsSpecBridge.test.tsFormViewSpecConformance.test.tsRowHeightDensityAgreement.test.tsListViewExportOptionsLift.test.tsby class: 134 × TS18049 (
'node' is possibly 'null' or 'undefined'), 131 × TS2339 (Property 'formType' does not exist on type 'string | number | boolean | BaseSchema'), 4 × TS2322, 2 × TS2533, 1 × TS7053. Piece C kills all 272 — no residue needed narrowing in test files.Canary — per class, before and after
Repo-wide, PREFIX filter = the downstream consumers (the direction that matters for a widening):
main; 6 errors when pinnedlist-view.tslabel/descriptionFinal state, post-merge, all three directions:
...@object-ui/types...@object-ui/core...@object-ui/reactThe collision class is worth one note: on
maintoday the canary is green, because PR #4578 sidestepped the collision rather than leaving it live. The 19-of-35 figure is #4548's measurement under its option A, and the honest way to show the defect still exists is the pin — which is why the pin, not the canary, carries piece B's red.Tests
Repo-root
vitest run --maxWorkers=2over the consumption radius, split into four runs (the single combined run exceeded the 600 s cap and was re-run split, not abandoned):Emit discipline
Both trees built with
dist/and every*.tsbuildinfocleared, through turbo so the dependency closure is built identically on both sides.@object-ui/typesb9a2769d…41d4cbboth sides@object-ui/core1c4ebaa7…839d042both sides — piece B proven types-only@object-ui/reacttransform; executable code byte-identical after comment strip@object-ui/components@object-ui/plugin-dashboardPiece A's runtime footprint is exactly the four inventory sites and nothing else. In the components bundle: one added import and exactly three resolver calls —
Bn(e.label, i),Bn(e.label, n),Bn(e.label, r)— against zero in the baseline. The remainder of that bundle's diff is a minifier alpha-rename cascade triggered by the new import shifting the identifier sequence (Bn→Vn→Hn→Un→…), not a semantic change. In plugin-dashboard the entire executable delta is one line:Per-package
.d.tsand grading — measured.d.tsdiffering@object-ui/typesbase.d.ts(the two widened unions + evidence comments);zod/index.zod.d.tsis order-only@object-ui/coretypes/index.d.ts— declaration moved to a re-export; entryindex.d.tsbyte-identical@object-ui/reactspec-bridge/types.d.ts,spec-bridge/SpecBridge.d.ts— return types@object-ui/componentsui/command.d.ts— order-only, unrelated to this change@object-ui/plugin-dashboardtypesis minor by position analysis: the diff ADDS members to unions on two authored-input-dominant properties, removes nothing, and nothing that type-checked before stops doing so — the #4593/#4603 line.coreis minor on #4569 (declaration moved, entry surface intact).reactis minor by measurement rather than by rule: the return type is NARROWED (SchemaNode→BaseSchema), which is free for callers reading the result but tightens the contract for anyone implementing aBridgeFn.components/plugin-dashboardare minor as a behavior move on the #4495/#4271/#4479 line. Never major.Two
.d.tsdiffs were checked rather than waved away, because an unexplained diff in a published declaration is exactly what a grading claim must not contain:types/zod/index.zod.d.ts— identical as a multiset of lines; pure key-order churn in tsc's zod inference, the same class PR fix(types): ariaLabel declares the keyed vocabulary, disabled accepts the predicate string (#4581) #4603 recorded.components/ui/command.d.ts— two lines,"onChange" | "value" | "type"versus"type" | "onChange" | "value"inside anOmit. Set-identical after sorting union members, and the file mentions none ofSchemaNode/BaseSchema/I18nLabel— tsc union-ordering nondeterminism, not reachable from this change.must-not-change
ComponentRendererPropsstays dual-declared (finding(types): ComponentRendererProps is the second dual declaration in the same two files as SchemaNode — core's is non-generic, types' is generic #4594). The two declarations still differ — types' is generic, core's is not — and that card measured zero consumers. Left alone deliberately, with a comment in core saying so. No side-effect widening was observed: core'sComponentRendererPropscompiles unchanged against the re-exportedSchemaNode, and core's entry.d.tsis byte-identical.toRenderableSchemainpackages/reactremains. SchemaRenderer's component-level union deliberately excludesnumber/boolean(finding(react): SchemaRenderer carries the #4422 prop erasure in a spelling every sweep and both guards are blind to —Record< string, any >instead of[key: string]: any#4548 ruling Q2), so the bridge still normalizes those after reconciliation.packages/types/src/views.ts,packages/i18nspec-formatter files, their CLAIM_DEBT lines. finding(types,i18n): eight doc comments claim alignment with @objectstack/spec symbols the spec does not export #4597 landed mid-task (PR fix(types,i18n): doc comments stop citing retired @objectstack/spec symbols (#4597) #4606) and was merged forward; finding(tooling): rule 2's tie test is symbol-agnostic, so a declaration can cite a retired spec symbol and pass because it references a different, live one #4607 (PR chore(tooling): a spec-alignment claim is judged by the symbols it cites, not by incidental ties (#4607) #4609) landed after it and was merged forward too. Neither overlaps a file this branch touches.content/docs/releases/**untouched.git checkout origin/main -- PATHSand patch/keep files throughout, nevergit stash(sharedrefs/stash). All three pieces restored and verified byte-identical by sha256;git diff HEADempty after each restore.Verification
core/src/types/index.ts, whose hand-declared interface (data?: anyplus an index signature) became a re-export. The 3 new files add 2 warnings, 0 errors.Filed, not fixed
Noted for #4605's zod-mirror drift census, per the card: this widening adds
labelanddescriptionto it.packages/types/src/zod/base.zod.tsstill declares both asz.string(), now narrower than the TypeScript declarations they mirror — the same class #4603 filed forvisible/disabled/ariaLabel. Not touched here: widening a published validator is a contract decision, and for these two slots it is the same INLINE-versus-KEYED question #4605 already holds.Generated by Claude Code