Repository navigation
fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) - #4593
Merged
Merged
Conversation
…rer evaluates (#4581) `visible` was declared `boolean`, but the renderer never read it as one — it evaluates the key. `SchemaRenderer.tsx:382` calls `evaluator.evaluateCondition(schema.visible)`, and `evaluateCondition` is declared `(condition: string | boolean | undefined, context?) => boolean`. The sibling keys `visibleWhen` and the deprecated `visibleOn` are `string` for that same reason; `visible` under-reported a capability it already had, and fixtures exercising it had to cast past the declaration. Widened to `boolean | string` — exactly what the evaluator accepts, no wider — and the two `as unknown as BaseSchema` casts in the expressions suite that existed only for this gap are dropped. Type-only: all 54 emitted `.js` files in @object-ui/types are byte-identical to origin/main. Part of #4581. #4580 and the ariaLabel half are escalated rather than implemented — see the PR body. 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. |
This was referenced Aug 13, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 13, 2026 17:14
This was referenced Aug 13, 2026
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 17, 2026
… the predicate string (objectstack-ai#4581) (objectstack-ai#4603) * fix(types): ariaLabel declares the keyed vocabulary, disabled accepts the predicate string (objectstack-ai#4581) Executes objectstack-ai#4580's label-vocabulary rulings Q2-B and Q3-A. PR objectstack-ai#4593 landed the `visible` half and escalated the rest. Q2-B: `BaseSchema.ariaLabel` widens from `string` to `string | KeyedI18nLabel`, where `KeyedI18nLabel` is a new exported type in `@object-ui/types` naming the keyed form `{ key, defaultValue?, params? }` that `SchemaRenderer.tsx:111` actually resolves via `resolveKeyedI18nLabel`. Deliberately NOT the spec's `I18nLabel` (the inline locale map) — PR objectstack-ai#4593 measured that spelling accepting the keyed fixture only vacuously, rejecting `params`, and type-checking a genuine locale map that renders an empty aria-label. The three inline copies of the keyed shape were verified identical before the name was minted; react/utils/i18n.ts and layout/NavigationRenderer.tsx now import it. app-shell/utils/index.ts keeps its inline spelling because PR objectstack-ai#4208 has a pending change to that file — comment added naming the type and the wait. Q3-A: `BaseSchema.disabled` widens from `boolean` to `boolean | string`, same evidence as `visible` (SchemaRenderer.tsx:466 evaluates it through the same `evaluateCondition`; `disabledOn?: string` sibling). Three `as unknown as BaseSchema` casts drop (aria :59, expressions :132/:141), closing the class the objectstack-ai#4581 census named. Q1-B is NOT implemented: its premise measured false. The spec bridges have zero production callers, `BridgeContext` has no locale field, the display-locale channel is a React hook unusable from a plain class method, and nothing re-runs the bridge on a locale change. `BaseSchema.label`/`.description` stay `string` and are pinned that way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 * fix(components): the toggle renderer resolves its keyed ariaLabel instead of forwarding it raw (objectstack-ai#4581) Found by the downstream type-check sweep over consumers of @object-ui/types, not by a test: with `BaseSchema.ariaLabel` honestly declared as `string | KeyedI18nLabel`, `aria-label={schema.ariaLabel}` in packages/components/src/renderers/form/toggle.tsx stops type-checking against Toggle's `aria-label: string | undefined`. The runtime harm is narrower than it first looks, and the test says so. Through SchemaRenderer the bug is INVISIBLE: SchemaRenderer injects its own already resolved `aria-label` via `...ariaProps` (SchemaRenderer.tsx:599/:625) and this renderer spreads `{...props}` after its own attribute, so the resolved value wins. Invoked directly the raw forward emits `aria-label="[object Object]"`. My first prediction — that the harm would show through SchemaRenderer — was wrong, and the test header records that rather than being rewritten to match: a SchemaRenderer-driven case is green in both directions and would have shipped looking like proof. The discriminating case invokes the registered renderer directly via ComponentRegistry.get('toggle'). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 28, 2026
…red (objectui#9375) (objectstack-ai#9719) Fixes objectstack-ai#9375 Clause-②: no The vocabulary prose on `BaseSchema` said the two i18n label shapes "each accept the other's shape vacuously". Measured against the tree, that is false in both directions — and the `label` docblock one property over was saying the same thing in its own words, so both are corrected here. Comment-only: no declaration, accept set or requiredness moves, per objectui#9092's read-only fence on `BaseSchema`. ## 1. The instrument first — does it discriminate? The card rests on `packages/types/src/__tests__/inline-locale-declared-face-9092.test.ts` (added by objectui#9364). Taken on its own word, a green run proves nothing: the file's `tsc` half is read only by `tsc -p packages/types/tsconfig.test.json`, and an `@ts-expect-error` on a line that the compiler would never flag is itself an error, so the baseline zero had to be lit before it could be read. Three ablations, each proved to have reached the disk (marker counts before/after + `git hash-object` against the HEAD blob), each restored with `git checkout HEAD --` and `git diff HEAD` proved empty, all under a trapped script: | direction | ablation | exit | what tsc printed | |---|---|---|---| | refusal is real (keyed slot) | drop the directive over `ariaLabel: LOCALE_MAP` | 2 | `(172,49): error TS2322: Type 'Record...&{key?: undefined; defaultValue?: undefined;}' is not assignable to type 'string | KeyedI18nLabel | undefined'` | | refusal is real (inline slot) | drop the directive over `ObjectGridSchema.label: KEYED_REF` | 2 | `(159,83): error TS2322: Type '{ key: string; defaultValue: string; }' is not assignable to ...` | | it can also tell ACCEPT apart | add a directive over the line that must compile (`ariaLabel: KEYED_REF`) | 2 | `(178,5): error TS2578: Unused '@ts-expect-error' directive.` | Baseline with all three restored: `tsc -p tsconfig.test.json` exit 0, `vitest` 10/10. The third row is the one that matters most: the instrument is not a slot that refuses every object — it distinguishes the two vocabularies. ## 2. Then the same two crossings, measured on `BaseSchema` itself The instrument asserts the inline crossings on inheritors (`ObjectGridSchema`, `AppComponentSchema`, `PageNodeSchema.aria`) and the keyed crossing on the flat slot. Since the docblock being corrected is `BaseSchema`'s own, both crossings were re-measured directly on `BaseSchema`, in a throwaway probe that was deleted afterwards (worktree proved clean): - `tsc`: `label: KEYED`, `description: KEYED` and `ariaLabel: MAP` each fire an `@ts-expect-error`; `label/description: MAP` and `ariaLabel: KEYED` compile as controls. Exit 0 with all five present. - parse (`zod/base.zod.ts#BaseSchema`): `safeParse` is `false` for the same three crossings and `true` for the same two controls. Mechanism, read off the installed pin rather than assumed: `InlineLocaleMapSchema` is typed `Record<string, string> & { key?: never; defaultValue?: never }`, and the refusal message names the pattern `^(?!(?:key|defaultValue)$)(default|[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*)$`. ## 3. What a wrong slot actually costs — also measured, not quoted Against the installed pin, with controls: - `resolveI18nLabel({ key: 'a.b', defaultValue: 'D' }, 'en')` returns `"a.b"` — the key renders. Control: `resolveI18nLabel({ en: 'Accounts', 'fr-FR': 'Comptes' }, 'en')` returns `"Accounts"`; `resolveI18nLabel(42, 'en')` returns `undefined`. - `resolveKeyedI18nLabel({ en: 'Owner' })` returns `undefined` — the aria-label renders empty. Control: `resolveKeyedI18nLabel({ key: 'a.b', defaultValue: 'D' })` returns `"D"`. So the residual hazard is a WRONG ANSWER, paid by metadata that reaches a resolver without passing either face — the normal case for server-driven JSON — and not the vacuous acceptance the prose claimed. ## 4. Sentences changed **A. `BaseSchema.ariaLabel`, the sentence the card names.** - before: "Two properties apart, both spelled `string | {object}`, and each accepts the other's shape vacuously. Check which resolver owns a slot before writing an object into it; the ruling accepted this cost with exactly this naming + cross-referencing as the mitigation." - after: "Two properties apart, both spelled `string | {object}` — but NEITHER SLOT ADMITS THE OTHER'S SHAPE. The installed pin types the inline arm with `key?: never; defaultValue?: never` and its `INLINE_LOCALE_KEY` pattern excludes both names, so a keyed ref written into `label` / `description`, and an inline map written into this slot, are each REFUSED — at `tsc` on the declaration and at `safeParse` on the zod mirror. The cross-vocabulary block of `__tests__/inline-locale-declared-face-9092.test.ts` re-derives that on every run, in both directions and with the accepting control beside each refusal; read it rather than this sentence." Followed by a new paragraph carrying the wrong-answer cost measured in section 3, and keeping the original advice ("check which resolver owns a slot") and the ruling's mitigation clause verbatim. **B. `BaseSchema.label` — the other half of the same claim, which the card did not name.** This is the correction that would have been missed by fixing only the named sentence. - before: "they are structurally confusable: a keyed ref typed into this slot is accepted only *vacuously*, as a locale map whose "locales" are named `key` and `defaultValue`." - after: "they stay confusable to a READER — but NOT to the compiler or the parser: a keyed ref written into this slot is REFUSED, because the installed pin types the inline arm with `key?: never; defaultValue?: never` and its `INLINE_LOCALE_KEY` pattern excludes both names." Plus a pointer to `ariaLabel` for the full statement. **C. The `NOT I18nLabel` paragraph on `ariaLabel` — marked as history rather than rewritten.** PR objectstack-ai#4593's three measurements were correct about the spelling the ruling withdrew, so they stay; what changed is that they no longer read as the current reading. - before: "...withdrew it (objectstack-ai#4580 Q2-B): `I18nLabel` is the spec's INLINE LOCALE MAP (`string | Record<string, string>`), so the shipped keyed fixture was accepted only *vacuously*..." - after: "...withdrew it (objectstack-ai#4580 Q2-B). Read those three as HISTORY, against the `I18nLabel` of that release — `string | Record<string, string>`, a map with no name excluded from it: the shipped keyed fixture was accepted *vacuously*, ... The pin has since closed the vacuous half — see below. What survives is that the two vocabularies are structurally confusable to a READER — objectui#4167's exact hazard." **D. Two spellings of the inline arm, which were the premise the refuted claim rested on.** The file-level `KeyedI18nLabel` vocabulary block and the `label` docblock both spelled the spec's inline form `string | Record<string, string>` flat — a spelling under which `{ key, defaultValue }` IS a member, which is exactly what made "accepted vacuously" readable as current. Both now carry the pin's exclusion (`key?: never; defaultValue?: never`). No other wording in those paragraphs moves. **E. One line address removed.** The `ariaLabel` docblock cited `packages/react/src/SchemaRenderer.tsx:111`; the call is at line 126 on `origin/main`. The quoted code is unchanged and still correct, so the citation now names the file and the symbol and drops the number — a number in published prose is a claim with a shelf life, and `check:new-line-citations` reports on exactly this shape. ## 5. Gates Every exit code captured to disk before any pipe. | gate | command | exit | printed verdict | |---|---|---|---| | package type-check | `pnpm --filter @object-ui/types type-check` | 0 | `tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`, silent | | package tests | `pnpm --filter @object-ui/types test` | 0 | `Test Files 202 passed (202) · Tests 4698 passed (4698)` | | build (published bytes) | `pnpm --filter @object-ui/types build` | 0 | `dist completeness: 1 package(s) complete (130 emitted files verified)` | | changeset presence | `node scripts/check-changeset-presence.mjs` | 0 | `1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)` | | changeset claims | `node scripts/check-changeset-claims.mjs` | 0 | OK | | changeset fixed / no-major / pending literals | the three scripts | 0 / 0 / 0 | all OK | | line citations | `node scripts/check-new-cross-file-line-citations.mjs` | 0 | `0 new citation(s)` | | control bytes | `node scripts/check-control-bytes.mjs` | 0 | `OK (scanned 7818 tracked text file(s))` | | installed pin claims | `node scripts/check-installed-spec-pin-claims.mjs` | 0 | `OK` | | governed surface | `node scripts/check-governed-queue-guard.mjs --test ...` | 0 | `NOT GOVERNED — 2 path(s) checked` | | eslint | `npx eslint packages/types/src/base.ts .changeset/9375-*.md` | 0 | 0 errors, 21 pre-existing `any` warnings on untouched lines | Whole-repo `pnpm lint` and the rest of the gate farm are left to CI. Published-bytes check, on the final commit: after building, `dist/base.d.ts` carries the corrected sentence and no longer carries the old one — which is why the changeset is a `patch` rather than an empty declaration. ## 6. Pending-changeset re-read (the `changeset-claim-re-read` gate) The gate listed 11 pending changesets whose bodies name `packages/types/src/base.ts`. It is report-only and judges name resolution, not meaning; the meaning question is answered here. **Verdict: none of the 11 goes false, and one of them is independently corroborated by this PR's measurements. No pending body is edited.** **The decisive property, re-derived here on two instruments against merge-base `78a9c6744`.** - Line level: of the changed lines in `base.ts`, 0 are non-comment. Every `+`/`-` line matches `^[+-] +\*`. - Whole file: with comments stripped, both sides hash identically — `sha256[:16] = 1d8d2a4a9d1ed76f`, 3202 bytes each. **Lit control:** renaming one declared key (`ariaLabel` to `ariaLabelX`) in the head copy and re-stripping gives `122f6229754cf290`, so the hash is capable of telling a declaration change apart from a comment change. - Block level: of the 77 JSDoc blocks in the file, exactly 3 changed bytes, and they document `KeyedI18nLabel` (L22), `BaseSchema.label` (L91, `label?: string | I18nLabel`) and `BaseSchema.ariaLabel` (L465). Every other block is byte-identical, including `ComponentInput`, `ComponentMeta`, `StyleProps`, `children`, `inputType`, `defaultValue`, `description`, and the `label?: never` member of `ComponentInput` at L733 — a different `label` from the one this PR edits. ⇒ A pending claim can only go false here if it asserts something about the text of those three docblocks. Claims about members, retirement kits (`?: never` + `retirementTombstone()`), mirrors, re-exports or parity are untouched by construction, and the block-level reading is what makes that a measurement rather than an argument. **The one the gate's reader is right to single out: `.changeset/7088-hidden-jsdoc-single-hide-path.md`.** It is a claim about comment text in this very file, so the comment-only argument does not dispose of it. Read in full, its subject is `BaseSchema.hidden`'s JSDoc and the sibling `visible` comment (plus the schema-reference `hidden` row and `SchemaRenderer.hiddenVisibleSynonymy.test.tsx`, neither in this diff). Measured: the `hidden`, `visible`, `visibleWhen`, `visibleOn` and `hiddenOn` JSDoc blocks are byte-identical between merge-base and this branch, and no sentence of 7088's body names `label`, `ariaLabel`, `KeyedI18nLabel`, `I18nLabel` or i18n at all (0 hits for that token set; `base.ts` resolves as the lit control). Its general sentence — "the JSDoc is the authority a later docs correction is measured against" — is not disputed by this change either: 7088 itself backs its JSDoc with a pin test, and this PR moves a JSDoc toward what an instrument re-derives, which is the same direction. **Still true.** **`.changeset/zod-base-schema-mirror-parity-4605.md` — the one this PR corroborates.** It says `label`/`description` take the spec's inline map and `ariaLabel` the keyed reference, that "each slot admits only its own vocabulary and both cross pairings are pinned as rejections", and that `resolveKeyedI18nLabel` "returns `undefined` for a locale map and would render an EMPTY aria-label". Both sentences were re-measured for this PR (section 2 and section 3) and both hold. **Still true — and it is the pending body that already said what the docblock is only now catching up to.** **The other nine.** Verified rather than inherited: each was scanned for any token of the changed docblocks' subject, with `base.ts` as the lit control for the scanner. Two produced a hit and were read: `5928-classname-style-props-rename` uses "vocabulary" of `StyleProps`' Tailwind scale (that block is byte-identical), and `componentinput-reexport-4972` uses it of `ComponentInputControlType` (likewise). The remaining seven — `5905-componentinput-inputtype-tombstone`, `5905-componentinput-retire-constraint-keys`, `6067-component-meta-derive-from-canonical`, `7493-componentinput-retire-label-defaultvalue-advanced`, `component-meta-converge-and-deprecate-alias-5893`, `default-children-retired-5051`, `plugin-component-input-deprecate-5674` — are about declarations, retirement kits and re-exports, and name none of the three changed blocks. **All still true.** ## Acceptance notes Found and deliberately NOT done: - `packages/types/src/objectql.ts` says "neither resolver **accepts** the other's shape" — true of the two resolvers' signatures, misleading about runtime for the reason measured in section 3. The card names this sibling explicitly as a different claim; that file is also the surface of open PR objectui#9709, so it is left untouched. Noted, not filed — the card already records it. - `packages/{components,core,plugin-dashboard,react,types}/CHANGELOG.md` each carry the old sentence in a released entry. Release records are history and are never rewritten. A whitespace-tolerant scan of the whole tree (the prose wraps near eighty columns, so a line-anchored matcher is blind to it) found no other LIVE copy: `packages/types/src/app.ts` and the instrument's own header were already corrected by objectui#9364. - `packages/types/src/base.ts` has two other cross-file line citations of the same rotting shape (`.../bridges/list-view.ts:180` and `:224`, in the `label` and `description` docblocks). Not verified and not touched — outside the vocabulary claim this card is about. Noted, not filed; the handler is whoever next edits those two docblocks, and `check:new-line-citations` already reports the class. --- 🤖 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>
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.
Part of #4581.
SchemaNode(#4580) and to ride #4581 along with it. Measurement blocked both of those on one unruled contract question, so this PR ships only the half that measured clean, and the rest is escalated rather than guessed. #4580 is not touched here and stays open; #4581 is half done, hencePart of, notFixes.What landed
BaseSchema.visibleis declaredboolean | string.It was
boolean, but the renderer never read it as a boolean — it evaluates the key. Both call sites the ruling asked me to verify, cited:visible—packages/react/src/SchemaRenderer.tsx:382:and
evaluateConditionis declared(condition: string | boolean | undefined, context?) => boolean(packages/core/dist/evaluator/ExpressionEvaluator.d.ts:143). The sibling keysvisibleWhenand the deprecatedvisibleOnarestringfor exactly this reason. The widening isboolean | string— what the evaluator accepts, no wider.ariaLabel—packages/react/src/SchemaRenderer.tsx:111callsresolveKeyedI18nLabel(schema.ariaLabel). This call site is what disproved the ruling's spelling for the ariaLabel half. See "Why ariaLabel did not land" below.Two
as unknown as BaseSchemacasts inSchemaRenderer.expressions.test.tsxexisted only for this gap and are dropped.The census — the recorded "five" is not what is on disk
The ruling said five casts, "each carrying a comment naming this gap". Grepping
as BaseSchema/as unknown as BaseSchemaacrosspackagesandappsfinds six, in three different classes, and only one carries such a comment:react/__tests__/SchemaRenderer.expressions.test.tsx:52visible: '${data.role === "admin"}'react/__tests__/SchemaRenderer.expressions.test.tsx:61visible: '${data.role === "admin"}'react/__tests__/SchemaRenderer.expressions.test.tsx:132disabled: '${data.status === "locked"}'disabledgap — not ruled, leftreact/__tests__/SchemaRenderer.expressions.test.tsx:141disabled: '${data.status === "locked"}'disabledgap — not ruled, leftreact/__tests__/SchemaRenderer.aria.test.tsx:59ariaLabel: { key: …, defaultValue: … }components/__tests__/html-anchor-links.test.tsx:33{ type: 'a', ...schema }Record< string, unknown >spread, no commentSo of the six, this card's ruled widenings close two. Three further notes:
disabledis the same defect asvisible, with the same evidence —SchemaRenderer.tsx:466evaluates it through the sameevaluateCondition, and adisabledOn?: stringsibling exists for the same reason. finding(types): BaseSchema under-declares visible (predicate string) and ariaLabel (I18nLabel) — the renderer supports both, five test fixtures now cast past the gap #4581 named onlyvisibleandariaLabel, so I did not widen it unruled. Two casts and one one-line widening are waiting on a word.html-anchor-links.test.tsxis the file finding(types): BaseSchema under-declares visible (predicate string) and ariaLabel (I18nLabel) — the renderer supports both, five test fixtures now cast past the gap #4581 named, but the cast in it is not this gap — it is aRecord< string, unknown >spread being cast toBaseSchema.plugin-dashboard's twoas BaseSchemaare 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's deliberate narrowing casts, untouched.Red-first — predictions written into the test header before the run
packages/types/src/__tests__/base-schema-visible-predicate.test.tspins the widening. Againstorigin/main(92250d648),tsc -p packages/types/tsconfig.test.jsonreported, verbatim:Post-fix: clean, exit 0.
The equality assertion is invariant (
Equal, notextends) on purpose, and the header says why: asatisfies-style or one-wayextendscheck would be vacuous in both directions here — the narrowbooleanis assignable to the wideboolean | string, so a widening that never happened and a widening that overshot toanywould both stay green.BaseSchema's[key: string]: anyindex signature makes the overshoot a live risk, not a hypothetical: deleting the declared property altogether leavesvisibletypedanyand every fixture still compiling. Pinning the exact union is the only assertion that can go red for the right reason.must-not-change
.jsfiles in@object-ui/types'disthave equal sha256 against anorigin/maincompare worktree, built the same way. This is the finding(plugin-dashboard, plugin-list): the two packages #4422 left unswept still erase every declared prop — and the #4438 guard cannot see them #4528 bundle-sha256 bar, and a type-only card meets it exactly — ruling 4's "zero runtime behavior change" is satisfied at the strongest available standard rather than argued.packages/types+packages/coresuites: 119 files, 2200 tests, all passing.--filter='...@object-ui/types'= the downstream consumers, 27 packages): zero new errors attributable to this widening. Measured in isolation, with theSchemaNodechange reverted, precisely so the two changes could not launder each other.origin/maincompare worktree: 21 warnings / 0 errors both sides — net zero; the new test file contributes 0.Published
.d.tsdiff, and the grading analysisBuilt both ways with
dist/and*.tsbuildinfocleared between builds. The entire diff:/** * Controls whether the component is visible. * When false, component is not rendered (display: none). + * … (evidence comment) * @default true */ - visible?: boolean; + visible?: boolean | string;Graded
minorby position analysis, not by assumption: the diff adds a member to a union on an authored-input-dominant property, removes nothing, and touches no other declaration — the #4586/#4591 shape. Authors gain a spelling; nothing that type-checked before stops doing so. Readers ofschema.visiblewere already coping withanythrough the index signature. Never major (major tracks@objectstack).@object-ui/coregets no changeset entry: its declaration is untouched in this PR, so it has no source or declaration diff.toRenderableSchemainpackages/reactSTAYSStated explicitly so no future card "cleans it up":
SchemaRenderer's component-level union deliberately excludesnumber/boolean(#4548 ruling, Q2), so the bridge still normalizes those onto their text form. It is a total function, not a cast. Nothing in this PR makes it an identity function, and nothing here is a reason to remove it.Why
SchemaNode(#4580) did not land — the measured blockerThe reconciliation itself works. Implemented as ruled (core's
interface SchemaNodebecomesexport type { SchemaNode } from '@object-ui/types';), built clean, and the red-first collision pin went red pre-fix exactly as predicted and clean post-fix — the #4548 error class, verbatim, naming bothdistidentities:core/dist/index.d.tswas unchanged by it — the entry surface is preserved, as the ruling required.What stopped it is the repo-wide canary. The reconciliation surfaces 274 errors in
@object-ui/react:spec-bridgesuites) —TS18049×134 andTS2339×131, all of the formProperty 'sections' does not exist on type 'string | number | boolean | BaseSchema'. Mechanical narrowing, in scope, mine — but 272 of them is a cost worth a decision on its own, and it points at the bridges' declared return type as the real lever rather than 272 local narrowings.packages/react/src/spec-bridge/bridges/list-view.ts:180and:224:from
if (spec.label) node.label = spec.label;and thedescriptiontwin.spec.labelis the spec'sI18nLabel;BaseSchema.labelisstring. Core's[key: string]: anyhad been absorbing that mismatch — remove the duplicate declaration and a real latent defect surfaces. Fixing it needs a cast (the lenient-consumer fallback the contract-first rule forbids) or a producer-side widening that nobody has ruled. Per ruling 4, runtime-source reshaping is a STOP, so I stopped.Why
ariaLabeldid not land — the ruling's spelling is the wrong vocabularyThe ruling says widen
ariaLabeltostring | I18nLabel. Measured, that spelling declares a shape the renderer cannot resolve, and still rejects the shape it can.@object-ui/typesre-exportsI18nLabelfrom@objectstack/spec/ui, where it is the inline locale mapstring | Record< string, string >. ButSchemaRenderer.tsx:111resolvesariaLabelwithresolveKeyedI18nLabel, whose declared input is the keyed formstring | { key: string; defaultValue?: string; params?: Record< string, any > }.packages/react/src/utils/i18n.tsdocuments these as two vocabularies that "answer wrongly for the other's input, silently" (objectui#4167, PR #4169).A throwaway probe measured all four consequences, and all four predictions held:
I18nLabelisstring | Record< string, string >{ key, defaultValue }understring | I18nLabelkeyanddefaultValueparams: { name: 'Ada' }Type '{ name: string; }' is not assignable to type 'string'{ en: 'Owner' }resolveKeyedI18nLabelreturnsundefinedfor it at runtime, rendering an emptyaria-labelSo the ruled widening would have turned the aria test green vacuously, invited a shape that renders empty, and still rejected keyed labels with
params. That is the opposite of the ruling's own stated intent ("matching what the renderer actually supports"), so I did not write it.The two escalations are one question
BaseSchemadeclares three sibling label slots —label?: string(:56),description?: string(:62),ariaLabel?: string(:173) — and all three under-declare, in two different i18n vocabularies:label/descriptionreceive the spec's inlineI18nLabelfrom the bridges, whileariaLabelis resolved with the keyed resolver. Core's index signature was hiding all of it.That is why #4580 is blocked on #4581 rather than the other way round: the ruling treated the
ariaLabelwidening as a rider on the reconciliation, but the reconciliation is what forces the label-vocabulary decision. One ruling on "which vocabulary does eachBaseSchemalabel slot declare" unblocks the reconciliation, theariaLabelwidening, and the twolist-view.tsdefects together. Options and a recommendation are in the seat's report.Generated by Claude Code