Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[finding] ComponentConfig is the same two-published-declarations shape as ComponentMeta, one type down — @object-ui/core vs @object-ui/types #6298

Closed

Description

@os-litant

Measured while converging ComponentMeta at the registry (#6067, PR #6297). Filed unassigned, out of that card's fence: its file surface was packages/core/src/registry/**, and closing this one requires a decision about shape rather than a mechanical follow-through.

Measured

Read off origin/main at merge-base c456d91f4.

A repo-wide search for a type / interface / class declaration of any name ending in ComponentConfig returns four, two of which are the same name:

declaration form
packages/types/src/base.ts:578 export interface ComponentConfig extends ComponentMeta { type: string; component: any; }
packages/core/src/registry/Registry.ts:141 export type ComponentConfig<T = any> = ComponentMeta & { type: string; component: ComponentRenderer<T>; }
packages/core/src/registry/Registry.ts:156 export type PublicComponentConfig<T = any> — a distinct name, not part of this
packages/types/src/mobile.ts:283 export interface MobileComponentConfig — a distinct name, not part of this

Control probe for the search, since a zero-hit is only a reading once a known-present neighbour has been probed: the same pattern run for ComponentMeta returns the two declarations #6067 is about, so the pattern matches both the export type and export interface spellings.

Both ComponentConfig declarations are published: packages/types/src/index.ts publishes its one, and @object-ui/core's src/index.ts carries export * from './registry/Registry.js' with package.json mapping "." to ./dist/index.d.ts.

What the delta actually is

They differ in two ways, and only one of them is closed by #6067:

  1. Which ComponentMeta they extend. Before [finding] @object-ui/core's Registry.ts is a THIRD ComponentMeta declaration, and it is missing tags/description too — the same delta #5893 just closed in packages/types #6067 this was the whole nine-versus-thirteen divergence. PR ComponentMeta at the registry: derive it from the one declaration, extend it under a named type #6297 reduces it: core's ComponentMeta is now CanonicalComponentMeta & RegistryComponentMetaExtras, so the shared eleven members are single-sourced on both sides. What remains here is the four registry-only keys, which is a declared extension rather than drift.
  2. Genericity and the component slot. @object-ui/types' is non-generic with component: any; core's is <T = any> with component: ComponentRenderer<T> (and ComponentRenderer<T> = T). This half is untouched by [finding] @object-ui/core's Registry.ts is a THIRD ComponentMeta declaration, and it is missing tags/description too — the same delta #5893 just closed in packages/types #6067 and is the part that makes the two same-named exports genuinely different types.

So after #6297 lands this is no longer a drifting copy, but it is still two same-named published declarations, which is the shape objectui#4580 ruled about: a structural copy would reproduce the defect the moment either side moved.

Why it is worth recording rather than folding in

The ComponentMeta serial (#4972 → #5671 → #5893 → #6067) has now converged three declarations of two type names, each time by making the downstream one derive from the canonical rather than restate it. ComponentConfig is the next name down the same file and the same import edge, and it was left untouched by every one of those cards for the same reason each time: it was outside the fence. Recording it means the census of same-named published pairs in this neighbourhood is complete rather than trailing off.

Not urgent, and nothing a user hits is broken today. Both spellings accept every registration that compiles against either, for the same all-optional reason recorded in #6067: excess-property checking fires only on object literals, never on the type relation. This is the same dormancy #4972's divergence had until it wasn't.

Possible directions

Duplicate search — what was and was not possible

Searched locally: every declaration of the name across packages/** and apps/** (above); every in-repo mention of ComponentConfig in .changeset/, docs/, content/ and package __tests__/ alongside an issue reference — zero hits, so no in-repo artifact points at an existing card for it. Read directly: #4631, the "wider three-surfaces-disagree" card, which is about a component type's three disagreeing surfaces (TS schema type / registry inputs array / renderer prop reads) and names ComponentConfig nowhere. #4972 is closed and scoped to ComponentInput's copies plus WidgetInput; #5893 is scoped to the two declarations inside packages/types; #6067 is scoped to ComponentMeta.

⚠️ The keyword search over open issues could not be completed. The dispatch's read-channel discipline puts issue reads on the zero-quota HTML channel, and GitHub's issue-search page does not server-render its results in this repository (disable_issues_react_ssr is on, so the embedded payload's preloadedQueries[0].result carries no rows) — while direct api.github.com returns 403 here. So the local evidence above is the whole dedup. If triage has search budget, one keyword pass over open issues for ComponentConfig would close the gap, and closing this as a duplicate is the cheap outcome if it finds one.

Related: #6067 (the ComponentMeta card this was measured under), PR #6297 (which reduces half of this delta), #4580 (the ruling and its reason), #4972, #5671, #5893, #4631.

Filed unassigned.

Activity

  1. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 6298,
      "status": "done",
      "branch": "claude/issue-6298-componentconfig-one-authority",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6937",
      "premise_still_valid": true,
      "summary": "The proviso was measured first and it SELECTS (b), but not for the reason triage expected. `ComponentRenderer` has exactly one declaration (core `Registry.ts:16`) and `@object-ui/types` cannot reach it - types depends on `@objectstack/spec` + `zod` only, core depends on types at `workspace:*`, so that edge is a cycle - so the proviso FAILS on its literal reading; but the alias is the IDENTITY, so `component: T` in types says exactly what `component: ComponentRenderer[T]` said in core with nothing to import, and (b) is reachable with no new edge. (a) was independently EXCLUDED: the gate counts a derived declaration as an authority, so it could not have delivered the mandated `KNOWN_COLLISIONS` deletion - proved on this tree, where `ComponentMeta` was converged the (a) way by PR #6297 and is STILL a baseline row one line below the row this PR removes. Landed: types' `ComponentConfig` gains the defaulted type parameter and owns the slot; core re-exports it (the #5671 `ComponentInput` form); the five registry-only keys move to a named extension `RegistryComponentConfig` that the registry getters return, type-identical to what they returned before, because `getNamespaceComponents` filters on `config.namespace`. Baseline row deleted (43 rows to 42), only that row; #6349's parallel-batch files untouched. PM note, NOT filed as a new card: `ComponentMeta` remains a KNOWN_COLLISIONS row for the same file pair and the same reason (core still declares it), and this PR demonstrates the route that would clear it - it is already tracked debt on the shrink-only baseline owned by the #6349 batch series, so filing would duplicate.",
      "tests": "ALL READINGS ON 6808939d9 (final commit; `git status` clean, so tree equals HEAD). Exit codes captured before any pipe.\nTYPE-CHECK (hyphenated script name echoed by pnpm): `pnpm --filter @object-ui/types type-check` gives exit 0 (`tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`); `pnpm --filter @object-ui/core type-check` gives exit 0 (`tsc --noEmit && tsc -p tsconfig.test.json`). NOT-MEASURED check discharged: `tsc -p packages/core/tsconfig.test.json --listFiles` shows the new pin file IS in the program (1 hit) and that it reads `packages/types/dist/base.d.ts` (line 610: the emitted declaration WITH the new type parameter), so the type-level assertions are enforced rather than erased. types was rebuilt before core's type-check.\nROOT-FORM vitest (no --filter): `pnpm exec vitest run packages/types/ packages/core/ scripts/` gives BEFORE (merge-base 40c479af2, run in a throwaway comparison worktree since the tree cannot be in two states at once) `Test Files 269 passed (269)` / `Tests 5462 passed (5462)`, exit 0; AFTER `Test Files 270 passed (270)` / `Tests 5472 passed (5472)`, exit 0. Delta plus-1 file / plus-10 tests, exactly the new pin file. Comparison worktree removed. Ratchet family re-run at final HEAD: 9 files / 132 tests, exit 0.\nABLATION, direction PREDICTED IN WRITING BEFORE THE RUN ('red in the FRESH direction - a NEW colliding name - not the stale one'). OBSERVED exactly that: `ComponentConfig - a NEW colliding name / packages/core/src/registry/Registry.ts:236 - type declaration / packages/types/src/base.ts:642 - interface declaration`, exit 1 (11 tests became 1 failed | 10 passed). NO REBUILD IS INVOLVED, and that is a measured property rather than an omission: the gate is a pure SOURCE scan - `populationFiles` reads `git ls-files` and the matcher runs over `readFileSync` of the tracked `.ts` text - so no `dist/` sits between the mutation and the assertion and a stale build cannot make this ablation lie. MUTATION CONFIRMED ON DISK before the run: anchored line count 1 became 0, and `git hash-object` 542a4459 became df1e7537; both were asserted, with the script exiting 9 if either failed to move. RESTORE PROVEN BOTH WAYS after: hash back to 542a4459, equal to `git rev-parse HEAD:...`; anchored count back to 1; `git diff HEAD` empty; `git status` clean. Ran under `trap restore EXIT INT TERM` with an absolute `REPO_ROOT` from `git rev-parse --show-toplevel` and `git checkout HEAD -- ABSOLUTE_PATH`, never bare. The FACT was mutated, never the assertion.\nPUBLISHED SURFACE, before/after, read by an out-of-package consumer compiled against the EMITTED .d.ts of both packages: types' declaration accepts a type argument - `TS2315 not generic` became compiles-exit-0; keys-on-core minus keys-on-types - 'the five registry-only keys' became `never`; keys-on-types minus keys-on-core - `never` stayed `never`; both `component` slots `any` stayed `any`; mutual assignability `true` stayed `true` (THE GHOST - green on the diverged pair, which is why the new pin compares key sets and source identity and keeps assignability only as a labelled control).\nNO-CONSUMER-CHANGES PROVED, with the control the card's own discipline requires: zero import sites of `ComponentConfig` from `@object-ui/core` across packages/apps/examples/e2e (the only textual hits are prose in the doc comment this PR adds), and the CONTROL PROBE on the same module and same form returns ComponentRegistry 388 sites / Registry 7 / ComponentMeta 6, so the zero is a reading. Out-of-package consumer simulation against the emitted .d.ts (all three getters, all five registry keys, canonical keys, assignment from BOTH packages, bare-spelling object literal) gives tsc exit 0; NEGATIVE CONTROL (a key on neither declaration) gives `TS2339 Property 'thisKeyDoesNotExist' does not exist on type 'RegistryComponentConfig[any]'`, so the probe is live rather than vacuous. Downstream `type-check` of @object-ui/sdui-parser, @object-ui/components, @object-ui/react, @object-ui/app-shell gives exit 0.\nMANDATED GATES: `node scripts/check-phantom-dependencies.mjs` exit 0, verdict line 'Every in-scope import is declared by the package that publishes it' (40 packages, 18109 specifiers). `pnpm exec eslint .` PLAIN FORM (no --no-inline-config) exit 0, '11516 problems (0 errors, 11516 warnings)'; the warnings are the pre-existing repo baseline and this diff is net ZERO on them (one defaulted-any removed in core and one added; one added in types and one `component: any` removed) and the new test file draws none. `node scripts/check-changeset-presence.mjs` exit 0: '2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'.\nDERIVED GATE FAMILY (objectui has no dispatch-gates.mjs; derived from the actual changed paths): check:control-bytes exit 0 (5784 tracked text files) plus a manual `grep -naP` control-byte scan of every touched file, no hits; check:doc-fences exit 0; check:doc-types exit 0; check:doc-snippets exit 0 after building its OWN declared `--build-filter` closure ('Semantic phase: 271 of 271 block(s) judged, 0 failed') - its first run exited 2 and SAID SO ('This is I-could-not-run, NOT I-ran-and-found-errors'), which was treated as PREREQUISITE NOT MET rather than as red; check:readme-exports exit 0 after the same build ('386 of 386 self-imports judged real, 0 fabricated') - likewise its first read was exit 1 with 364 of 364 findings in the single class 'run pnpm build first' and ZERO on either package I touched; check:self-import exit 0; check:spec-symbols exit 0; check:changeset-fixed / no-major / overwrite exit 0. Edited-gate rule discharged: the only other file naming `one-authority-per-exported-name-6273.test.ts` is `packages/app-shell/.../ActionParam.one-authority.test.ts`, which names it in PROSE and reads `packages/types/src/ui-action.ts` rather than the gate file; run anyway, 1 file / 9 tests, exit 0.\nDECLARED NARROWING: the vitest scope is packages/types/ plus packages/core/ plus scripts/ rather than the whole suite, and type-check covers the two edited packages plus four downstream consumers rather than the whole farm. CI runs both in full.",
      "mcp_calls": "0 - the whole run used REST (issue read, comments read, PR create, PR read-back) plus git; no MCP GitHub tool was called",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED, deliberately: `ComponentMeta` is still a KNOWN_COLLISIONS row for the same file pair (core/Registry.ts vs types/base.ts) because PR #6297 converged its SHAPE but left a declaration in core; this PR demonstrates the re-export route that would clear it. Not filed because it is already tracked debt on the shrink-only baseline the 2026-08-25 family ruling ordered cleaned, with the #6349 batch series as its owner mechanism, so a new card would duplicate an existing row. Raised here so PM can route it into a batch."
      ]
    }

    Generated by Claude Code

  2. claude commented on Aug 31, 2026

    @claude
    Contributor

    contract-review

    Verdict: ACCEPT-WITH-CONDITIONS — PR #6937, head 6808939d9, clause ② review of the ComponentConfig narrowing

    One condition, and it is a two-line frontmatter edit plus one sentence; everything else was re-derived independently and reproduced exactly. Every reading below is my own measurement on fresh builds of base 40c479af2 (the true merge-base; the stated base b03ba3ad is main's tip, one PR ahead) and head 6808939d9, probes compiled against the emitted .d.ts of both packages only, exit codes captured before any pipe.

    1. Routing — clause ② was correctly applied; the PM is right

    The narrowing is real and published: my cross-tree probe on the two emitted core surfaces reads the set difference of keys on base-core's ComponentConfig minus head-core's as exactly tier | namespace | skipFallback | labelling | deprecated, and nothing in the other direction. A consumer hits it the ordinary way: on head, c.tier off ComponentConfig imported from @object-ui/core gives — the compiler's own line — error TS2339: Property 'tier' does not exist on type 'ComponentConfig[any]', and the literal form gives error TS2353: Object literal may only specify known properties, and 'tier' does not exist in type 'ComponentConfig[any]'; both spellings compile green on base (hot control, same instrument, same pass). Out-of-repo consumers are unmeasurable, so review was owed. Routing correct; no lighter path.

    But the review's answer is that the narrowing should land, because it is constitutive of the fix, not collateral. The precise loser class: a consumer who (i) named ComponentConfig from @object-ui/core rather than from @object-ui/types, AND (ii) read or wrote one of the five registry-only keys against that annotation. Getter-fed consumers lose nothing — all three getters return RegistryComponentConfig, which I measured type-identical to the old core declaration (key set equal against the base tree's own emitted type, per-key member equality, parameterised slot equal). In-repo the loser class is empty: my own census, multi-line-import aware, finds 0 import clauses naming bare ComponentConfig from @object-ui/core across packages/apps/examples/e2e, with the control hot in the same pass (ComponentRegistry 388 import clauses, ComponentMeta 5); 0 module augmentations of @object-ui/types. The break is compile-loud with a mechanical one-line fix (import RegistryComponentConfig, or the same name from @object-ui/types), and the runtime surface is untouched.

    2. No deprecation shim exists inside the repo's own ruling set — so none is owed

    I checked every shape a shim could take. A derived local declaration in core (direction (a)) keeps a second authority — the gate's own doc: a declaration is an authority, and "⛔ A deliberate re-export is NOT an authority" — so the mandated KNOWN_COLLISIONS deletion is undeliverable that way. An aliasing re-export (RegistryComponentConfig as ComponentConfig) is also counted as an authority (gate doc and its own spelling-table test) and would additionally make the two published names genuinely different types again, which is the #4580 defect itself. A temporal window (keep the old declaration deprecated for one release) keeps the defect standing and the baseline row owed. The #5671 ComponentInput precedent — the same re-export disposition — shipped with no window. Landing as-is is right on all four axes; no maintainer ruling is needed, because the maintainer's standing policy already decides both the disposition (#4580 family ruling) and the grading (next section).

    3. The ghost is real, and the new pin bites — both verified by measurement

    Ghost, measured on base: on the diverged pair read from the emitted .d.ts, both directions of assignability typed true in my probe while the same program read the key-set difference as the five keys and error TS2315: Type 'ComponentConfig' is not generic (its own line) on the types side. Exit 0 on the pass-probe, so mutual assignability was green on two genuinely different types — an assignability pin would have been born green and stayed green. The dev's decision to build the pin on key sets + source identity and demote assignability to a labelled control is correct.

    Bite, by mutating the fact: in the head worktree I replaced the re-export line in Registry.ts with the old local declaration — prediction written before the run: pin red on both source-identity assertions, gate red in the FRESH direction. Mutation proven on disk (anchored count 1 to 0, git hash-object 944093f2b to 98547925d), no rebuild involved (both instruments are source scans — a measured property of the gate, confirmed). Observed exactly the prediction: Tests 3 failed | 18 passed (21) — × re-exports the canonical declaration and names the extension, × declares no ComponentConfig of its own, and the gate's ComponentConfig — a NEW colliding name. Restore proven both ways under trap EXIT INT TERM with absolute paths: hash back to the HEAD blob, anchored count back to 1, git diff HEAD 0 lines. Enforcement of the type-level pins is real, not vitest theater: tsc -p tsconfig.test.json --listFiles exit 0 shows the pin file in the program (1), reading packages/types/dist/base.d.ts (1) and types src (0).

    4. Direction (a)'s exclusion holds on the tree

    Registry.ts:260 on head still carries the (a)-shaped ComponentMeta declaration PR #6297 landed, and KNOWN_COLLISIONS line 374 still carries its row — one line below the comment block where the ComponentConfig row was removed. The dev's counterexample is live and load-bearing, and it is why triage's fallback was never actually available for this card's mandated outcome.

    5. Re-export shape — correct, and the only legal one

    export type on the re-export is required, not stylistic: tsconfig.base.json sets isolatedModules: true, under which a value-form re-export of a type-only name is a compile error, and the emitted Registry.js would otherwise carry a runtime re-export of a binding @object-ui/types never emits. What core publishes is proven empirically: my head probe imports ComponentConfig from core's emitted dist/index.d.ts (through the export * chain) and instantiates it — generic, identical to types' (mutual Exact true, key diffs never both ways, bare slot still any, ComponentRenderer still the identity). And the rehoming was forced, not decorative: namespace is in the extras-only key set, so a bare re-export as the entry type would have broken getNamespaceComponents' filter in core's own build.

    6. THE CONDITION — regrade the changeset from patch/patch to minor

    The grade contradicts the repo's written policy and every family precedent. AGENTS.md §版本号策略: "objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)", restated in check-changeset-no-major.mjs's own header: "objectui's own breaking changes ship as minor with the break spelled out in the changeset body." Precedents: PR #5671 — the identical re-export disposition — graded minor on both packages; PR #6297 graded minor on core; PR #6936 (the sibling this review is consistency-paired with) graded minor on types. This PR removes five keys from a published type name on core (breaking for the class in §1) and adds a type parameter plus a new exported name on both — additive capability. patch is wrong on both entries by the repo's own convention; the fixed-group mechanics make the released version identical either way, but the changeset is the release-notes input and a narrowing filed as patch mis-signals there.

    Condition: in .changeset/6298-componentconfig-one-authority.md, change both patch entries to minor, and add one sentence to the body naming the breaking class plainly, e.g.: "Breaking for consumers importing ComponentConfig from @object-ui/core and touching a registry-only key on it — switch that annotation to RegistryComponentConfig." The body's existing content is accurate and stays.

    Nothing else blocks. With the regrade in, this lands as-is: no shim, no deprecation window, no maintainer escalation.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions