…ating it
core hand-declared a non-generic `ComponentRendererProps` typing `schema` as
`SchemaNode`, against `@object-ui/types`' generic
`ComponentRendererProps< TSchema extends BaseSchema = BaseSchema >` — the second
such pair in the same file, two lines below `SchemaNode`. Core's becomes a
re-export, per objectui#4580's ruling for the identical shape and objectui#4972
for `ComponentInput`, and the deliberate "not reconciled" marker comes out with
it.
Zero consumers re-verified repo-wide on the merged ref, not inherited from the
card. The re-export is NOT surface-neutral: resolved through the TypeScript
checker from core/dist/index.d.ts over a clean rebuild of both legs, `schema`
narrows from the `SchemaNode` union back to `TSchema` (default `BaseSchema`) and
the type gains a parameter. `core/dist/index.d.ts` byte-identity is not cited as
evidence — it is an `export *` barrel and cannot fail for this change class
(objectui#5673).
A compile-time pin in @object-ui/react, the only position resolving both
packages through node_modules, holds the reconciliation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
Fixes #4594
Converges the second dual declaration living in the same two files as
SchemaNodeonto the one declaration in@object-ui/types, per triage's ruled scope and the #4580 / PR #4608 precedent sitting two lines above it in the same file. The ⛔ "Deliberately NOT reconciled … stays dual-declared until #4594 is dispatched on its own evidence" marker comes out in the same diff —mainwas waiting on this card.packages/core/src/types/index.ts— the local non-genericinterface ComponentRendererProps { schema: SchemaNode; [key: string]: any }becomesexport type { ComponentRendererProps } from '@object-ui/types'. Theimport type { SchemaNode }line above it goes with it: the deleted interface was its only reader.packages/react/src/__tests__/ComponentRendererProps.reconciliation.test.ts— new compile-time pin, beside theSchemaNodeone, in the only position that resolves both packages throughnode_modules.Third convergence across this fault line, after
SchemaNode(#4580) andComponentInput(#4972 / PR #5671).Obligation 1 — zero consumers, re-verified on the merged ref
Re-run rather than inherited from the card, repo-wide on
d7573b3f4(grep -rn ComponentRendererProps, excludingnode_modules/.git/dist). Five hits, zero of them a consumer:packages/types/src/base.ts:341packages/types/src/index.ts:93packages/core/src/types/index.ts:46,53packages/core/src/index.ts:9packages/components/CHANGELOG.md:1725ComponentRendererProps"No import on either side, so the mechanical disposition stands and nothing downstream can observe the shape move measured below.
Obligation 2 — the surface measurement, and the gauge this PR refuses to use
core/dist/index.d.tsbyte-identity is not cited as evidence here.packages/core/src/index.tsis a 95-lineexport *barrel naming neither symbol, so the emittedcore/dist/index.d.tsis byte-identical under any change to a re-exported module and is incapable of failing for this change class — #5673, filed off #5671's control leg. It came out byte-identical again this round (f6494f80…on both legs, the same hash #5671 recorded), and that is reported as a control, not as a result.Both legs built from a fully cleaned state:
dist/removed and everytsconfig.tsbuildinfoswept repo-wide. The sweep is load-bearing — those files live atpackages/core/tsconfig.tsbuildinfoandpackages/types/tsconfig.tsbuildinfo, i.e. outsidedist/, so wipingdist/alone leaves compositetscskipping emit and compares two stale trees for free. Measured: the CHANGE leg swept exactly those two. Compared by sha256, never by byte count.Whole-tree
.d.tssha256 manifests overpackages/core/dist+packages/types/dist— 145 files on each leg, exactly one differs:core/dist/types/index.d.ts893cb23b…0e64c8c6…core/dist/index.d.tsf6494f80…f6494f80…What the published surface actually does
Reachability and shape resolved through the TypeScript checker (
checker.getExportsOfModuleon each package's entry.d.ts, then aliases followed to the declaration), never grepped —export *propagates a symbol without naming it, so a grep in a barrel proves nothing.ComponentRendererPropsas reached through@object-ui/core:schemaresolves toBaseSchema | string | number | boolean | null | undefinedpackages/core/dist/types/index.d.tsTSchema extends BaseSchema = BaseSchemaTSchema(defaultBaseSchema)packages/types/dist/base.d.tsSo this change does move the resolved shape, in two ways, and the declaration home collapses to the single site shared with
@object-ui/types:schemanarrows — from theSchemaNodeunion back to the object form. Core's copy had silently widened when PR fix(types): one SchemaNode and one label vocabulary — the union wins, labels resolve where the locale lives (#4580) #4608 made core'sSchemaNodea re-export of types' union; the card predicted that side effect and it happened. This closes the interim state rather than creating one.schemato its ownBaseSchemasub-interface through core's name.Can anything downstream see it? No — Obligation 1: nothing imports the type, on either side. The narrowing is declared in the changeset because it is a change to a published type, not because a consumer is affected.
Ablation — direction predicted before running
Predicted RED: restore core's hand-declared interface, rebuild, and the new pin must fail with (1) TS2344 where
Equal<>resolvesfalse, (2) TS2315Type 'ComponentRendererProps' is not genericwherever a type argument is applied to core's name, (3) TS2322 atconst node: TextSchema = props.schema. Thetsc --noEmithalf oversrcwas predicted to stay green (no source consumer).Observed —
pnpm --filter @object-ui/react type-checkexit 2, five errors:(1) and (2) landed as predicted, 2× and 3×. The
srchalf stayed green —&&short-circuits, so the test project could only have run because it passed.(3) did not appear, and the prediction was wrong rather than the leg void. TS2315 resolves the offending type reference to the error type, so
propsis poisoned toanyand the assignment underneath it is never checked: the third error is absorbed by the second, not additive. Reported as observed.Both legs rebuilt (
tsconfig.tsbuildinfoswept,typesthencore), and the mutation confirmed on disk and indist/on each leg by anchored greps rather than by an editor's exit code — mutation leg: local interface present 1 / re-export absent 0 in bothsrcanddist; restore leg: the inverse, 1 / 0. The script carriedtrap restore EXIT INT TERMso a killed run could not leave the tree mutated. Restore is provably byte-identical to the commit:git status --porcelainempty, and the rebuiltcore/dist/types/index.d.tsre-hashes to0e64c8c6…, the CHANGE-leg value.Tests and gates
All against the final commit,
641dcea68.pnpm exec vitest run packages/react/src/__tests__/ComponentRendererProps.reconciliation.test.ts—Test Files 1 passed (1)/Tests 1 passed (1), exit 0.pnpm exec vitest run --maxWorkers=2 packages/core/ packages/types/ packages/react/—Test Files 187 passed (187)/Tests 3110 passed (3110), exit 0.pnpm --filter @object-ui/react type-check(tsc --noEmit && tsc -p tsconfig.test.json) — exit 0, after building react's dependency closure (--filter '@object-ui/react^...' build, exit 0).check-changeset-presence✅ ·check-changeset-fixed✅ ·check-changeset-no-major✅ ·check-control-bytes✅ (4724 tracked text files) ·check-type-check-coverage✅ ·check-lint-coverage✅ ·check-package-self-import✅ ·check-phantom-dependencies✅ ·check-doc-component-types✅ ·check-skills-paths✅.check:eager-closure,check:doc-snippets,check:published-distnot run locally — known-broken/over-cap in this container; CI owns all three.Lint was narrowed deliberately, and the narrowing is measured rather than assumed. ① Population read from eslint's own config, not guessed:
ESLint#isPathIgnoredovergit ls-filessays 3526 of 3528 tracked source files are in scope. ② Files actually linted, read from--format json: 2 — exactly the two this diff touches, 0 errors, 0 warnings, exit 0. ③ Invariance for the untouched 3524:eslint.config.jsdeclares noprojectServiceand noparserOptions.project(no type-aware rules), no custom rule ineslint-rules/reads any file other than the one being linted (readFileSync/readdirSync/globSync: zero hits), and this diff does not touch the config — so each other file's verdict is a function of its own unchanged text and the unchanged config. CI runs the full farm regardless.Scope notes
packages/types/src/zod/base.zod.ts, app-shell'sPageHeader,AiChatPage/ console-mount, andpackages/components/**'s action dialog are all untouched.packages/typesis untouched entirely — the winning declaration needed no edit.content/docs/**orapps/site/**file is in this diff (Build Docsis red onmain:fsreaches the browser bundle via@objectstack/spec→pg-connection-string#5668).SchemaNodedocstring's vacuous justification is that card's job, not a rider on this one. One data point falls out of this PR for free, though: the emitted file that can move for a re-export incore/src/types/index.tsiscore/dist/types/index.d.ts, and it did (893cb23b…→0e64c8c6…), while the barrel did not — a third independent confirmation that the cited gauge cannot discriminate. The new docstring onComponentRendererPropsstates the checker-resolved measurement instead of repeating the barrel claim.Generated by Claude Code
Generated by Claude Code