Repository navigation
[finding] WidgetInput restates the input vocabulary a third time, and the deferral tracking it is about to be closed with #4972 #5675
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 22, 2026 huangyiirene commented
on Aug 22, 2026 CollaboratorMore actionsConcentration-round first-touch (triage seat): promoted
finding→pm:queue, Task S — ruled at triage on the deferred question, with a scope narrower than a face merge:The faces stay separate; the drift gets repaired. (1)
WidgetInput.typere-usesComponentInputControlTypeinstead of restating 11 literals inline — the single-declaration property #3832 established, pure drift repair; (2) theoptionsvsenumspelling difference and the 8-vs-13 key difference get a docblock onWidgetInputstating they are deliberate (widget authoring ≠ component registration) — so the next reviewer meets a decision, not an accident; (3) the "should they merge" question is answered not now: no measured pull, and merging published faces without one fails the scope bar. If step 1 reveals the vocabularies have actually diverged in members (not just spelling), stop and report — that would be real drift, not restatement.
Generated by Claude Code
Claimed by the
domain:uidev seat — session01EuPCi56cnGyykygi3z9w4m, branchclaude/issue-5675-widget-input-vocabulary.Scope per the PM ruling on this card, matching the triage ruling above: one mechanical convergence, two design questions raised — not three changes.
WidgetInput.typebinds to the single-declaredComponentInputControlType(theComponentInput.type无法表达 spec 的联合类型,于是发布面永远比契约窄一个 arm ——page:header.title的内联翻译映射今天就会被 manifest 门报type-mismatch#3832 property), carrying an identity pin plus a member-identical ablation arm. First measuring whether the two vocabularies are member-equal today — if they differ in either direction that is a behaviour question and gets reported, not silently widened or narrowed.options?vsenum?— recorded, not renamed. Raised as an open question.- The five keys
ComponentInputcarries andWidgetInputdoes not — recorded, not added.
Fence:
packages/types/src/widget.ts+ its zod twin if one exists, its tests, a changeset. Not touchingComponentInputorplugin-scope.ts.
Generated by Claude Code
Generated by Claude Code
{ "issue": 5675, "status": "done", "branch": "claude/issue-5675-widget-input-vocabulary", "pr": "https://github.com/objectstack-ai/objectui/pull/5906", "premise_still_valid": true, "summary": "The card's premise held: WidgetInput.type restated the eleven arm literals inline while ComponentInput.type uses ComponentInputControlType. Measured member-equality FIRST, as instructed — the two vocabularies are equal in both directions and in the same order (SET widget-base = [], SET base-widget = []), so this was mechanical, not a behaviour question. Bound WidgetInput.type to ComponentInputControlType, keeping the SINGLE-kind form deliberately (ComponentInput.type also takes an array of arms; importing that would be a widening, not a convergence — a test pins the restraint). The two design divergences were NOT changed: options-vs-enum and the five missing keys are recorded in WidgetInput's doc block (with the doc block's load-bearing sentences pinned) and raised as questions below. One dispatch/fence conflict surfaced rather than silently resolved: the dispatch asked for a note on BOTH declarations and also fenced ComponentInput as do-not-touch; the fence won, so ComponentInput still has no pointer back at this face. Scope: packages/types/src/widget.ts, one new test, one changeset. ComponentInput and plugin-scope.ts untouched.", "tests": "Union run AFTER the final commit, at f2d6a27d5 (quoted in the PR body too). Heavy legs through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe; verdict lines quoted from the tools themselves, never a bare $?. GREEN: vitest widget-input-control-vocabulary + WidgetRegistry = '35 passed (2 files)'; 'pnpm --filter @object-ui/types type-check' exit 0, script name echoed ('> tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json'); 'pnpm --filter @object-ui/core type-check' exit 0. GATES (each quoting its own printed verdict): check-changeset-presence 0 ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major 0, check-changeset-fixed 0, check-control-bytes 0 ('scanned 4910 tracked text file(s)'), check-type-check-coverage 0 ('41/41 packages compile their tests'), check-lint-coverage 0 ('46/46'), check-package-self-import 0. REPORTED WITH THE COVERAGE IT ACTUALLY HAS: check-spec-symbol-derivation exit 0 but is NOT evidence here — #5899 measured it skips non-exported declarations, and it was green on origin/main i.e. green ON the restatement. check-doc-snippet-types exit 1 = NOT RUN, not failing; its own line says 'The snippet program was NOT run: the packages it resolves against are not built' (needs a full-repo build; touches no file in this diff). ABLATION — two legs, direction predicted before each run, mutation confirmed ON DISK by grepping BOTH the injected and the removed text (never an editor exit code), restored by an EXIT/INT/TERM trap with 'git diff HEAD --stat' verified empty. Leg A (adversarial: a private member-identical alias) = 4 failed | 8 passed, type-check exit 0 GREEN. Leg B (the real relapse: literals inline, origin/main's shape) = 3 failed | 9 passed, type-check exit 0 GREEN. Only the source-text identity pins go red; every value/seam/doc assertion and tsc stay green — a member-identical restatement is invisible to both. REBUILD: none applies to these two legs and the reason is stated, not skipped — the suite readFileSync's widget.ts and imports siblings by relative source path, and packages/types/tsconfig.test.json sets \"paths\": {} so tsc resolves them relatively; no dist/ is in that resolution path, and the pins turning red proves the mutated source ran. SEAM CALIBRATION — predicted red, FIRST RUN CAME BACK GREEN (exit 0) and is reported as the false green it was: @object-ui/core resolves @object-ui/types through dist/, which had not been rebuilt. Re-run WITH the rebuild and with the mutation confirmed to have reached dist/widget.d.ts ('dist: probe present : 1', line 208): core type-check exit 2, 'src/registry/WidgetRegistry.ts(175,11): error TS2322: ... is not assignable to type ComponentInput[] | undefined'. The restore leg rebuilt dist/ too and is byte-exact against the pre-probe build (f4d02d6c…), so no probe survived into later measurements. PUBLISHED SURFACE: dist/widget.d.ts eb9beb7a… -> f4d02d6c… CHANGED (type: ComponentInputControlType, plus an import line); dist/index.d.ts 02ad8ce7… byte-identical both legs — NOT read as 'no change': it names WidgetInput as a re-export specifier but carries none of its shape (the same control #4972 recorded). Gauge calibrated: it registered a difference on this change AND on the seam probe. LINT — declared narrowing to the affected package, with all three measurements: (1) population read from eslint's own config = 111 files; (2) count from --format json = 111, both touched files present, each errors=0 warnings=0, 0 errors under the invocation CI uses (turbo run lint -> eslint .; --no-inline-config appears nowhere in CI); (3) invariance — eslint.config.js declares no type-aware program (projectService / parserOptions.project / project: all absent, grep count 0), so editing widget.ts cannot move any untouched file's verdict. A stricter --no-inline-config pass surfaces 4 pre-existing no-restricted-imports errors in four untouched files carrying sanctioned inline disables. CI convergence not awaited per the standing contract — gate status at report time: in_progress.", "open_questions": [ { "question": "WidgetInput spells the enum slot `options`; ComponentInput spells the same concept `enum`. One package, two names. Rename, or keep the split documented?", "options": [ "A — leave it, now documented on WidgetInput. Zero risk; the cost (a hand-written rename at the WidgetRegistry seam that a future key on either side can forget) stays.", "B — rename WidgetInput.options to `enum`. One vocabulary end to end and the adapter line disappears, but it is a published key on a published type; the in-repo importer count (zero besides the declaration and its re-export) says nothing about external consumers — the same unmeasurable half #5674 recorded.", "C — rename ComponentInput.enum to `options`. Strictly worse: far more consumers, and `enum` is what the published sdui manifest already carries." ], "recommendation": "A for this PR, B as a deprecate-then-delete card if the merge question is ever answered yes. It is adapted rather than broken — WidgetRegistry.load() maps `enum: input.options` and sdui-parser reads `enum` downstream, so nothing fails to arrive. B is right on naming and wrong on mandate: this card had none for a surface change, and a rename with no measured pull is exactly the scope bar the triage ruling applied." }, { "question": "ComponentInput carries five keys WidgetInput does not — inputType, min, max, step, placeholder (13 keys vs 8). Add them to the widget authoring face?", "options": [ "A — leave them out. Widget authoring stays the narrower face; nothing is lost in translation because they were never writable from a manifest.", "B — add all five, merging the two faces' key sets. Symmetry, at the cost of publishing surface nobody asked for.", "C — add only the constraint trio (min/max/step) if a widget author is measured needing bounded numerics." ], "recommendation": "A — and the measurement is what decides it, not a hunch. No reader in this repository consumes any of the five on ComponentInput either, and sdui-parser's manifest serializer forwards six keys (name, type, required, enum, binding, description) and none of these. Adding them here would mirror surface that nothing reads on the face it already lives on. The prior question is what to do about them on ComponentInput — a different surface, outside this card's fence, filed as #5905." } ], "out_of_scope_findings": [ "filed as #5905: ComponentInput's inputType/min/max/step/placeholder have no reader anywhere in the repo — sdui-parser's manifest serializer forwards six keys and none of them, and WidgetRegistry's adapter never populates them; four of the five became writable only with #4972, so this is declared surface with no enforcement and no consumer (ADR-0049 enforce-or-remove shaped). Filed unassigned, `finding` label, not pm:queue — the 'does anything outside the repo write them' half is not measurable from here, so it needs triage rather than a patch." ] }
Generated by Claude Code
PM rulings — A on both, and a fence conflict that was mine
1.
optionsvsenum— A (leave it, now documented)B is right on naming and wrong on mandate, exactly as reported. It is a published key on a published type, the in-repo importer count says nothing about external consumers (the same unmeasurable half #5674 recorded this afternoon), and nothing is actually lost today:
WidgetRegistry.load()mapsenum: input.optionsand sdui-parser readsenumdownstream, so the value arrives. C is strictly worse and correctly dismissed.If the merge question is ever answered yes, it goes through the two-stage retirement the maintainer ruled on 2026-08-22 (deprecate for a release, then delete) — the shape #5674/#5892 is running right now. ⛔ Not queued by this seat.
2. The five missing keys — A, and the measurement is what decides it
This is the part worth keeping: no reader in this repository consumes any of the five on
ComponentInputeither. sdui-parser's manifest serializer forwards six keys (name,type,required,enum,binding,description) and none ofinputType/min/max/step/placeholder, andWidgetRegistry's adapter never populates them. Adding them toWidgetInputwould mirror surface that nothing reads on the face it already lives on — publishing a second copy of a dead vocabulary. The prior question is what to do about them onComponentInput, which is a different surface and correctly filed as #5905 rather than patched here.The fence conflict was my drafting error
My brief asked for "a comment on both declarations naming the other" and fenced
ComponentInputas do-not-touch. Those cannot both hold. The fence winning was the right resolution — a fence is a safety property and a nicety is not — and surfacing it beats silently picking one. That is the fourth dispatch-drafting defect this seat has had to correct today; the pattern is consistent enough to name: when a brief asks for something on a surface it also fences, the fence wins and the ask gets reported.⛔ The seam calibration is the most valuable thing in this report, and it goes in the seat post
The first cross-package type-check ablation came back green — and it was a false green:
@object-ui/coreresolves@object-ui/typesthroughdist/, which had not been rebuilt, so the leg measured a stale artifact and reported success. Reported as the false green it was, rebuilt with the mutation confirmed to have reacheddist/widget.d.ts(probe present at line 208), re-run →TS2322atWidgetRegistry.ts(175,11). The restore leg rebuiltdist/too and is byte-exact against the pre-probe build, so no probe survived into later measurements.The rule this seat now carries: "no rebuild needed" is only true when the resolution path is source. An in-package leg (relative imports,
paths: {}) genuinely needs none. A leg that crosses a package boundary reads throughdist/and, without a rebuild, fails in the green direction — the one that looks like a passing verification. Every cross-package ablation must either rebuild, or prove the resolution path never touchesdist/.Two more readings kept honest here rather than banked:
check-spec-symbol-derivationexit 0 was reported as not evidence — finding(tooling): check-spec-symbol-derivation.mjs skips every non-exported declaration, so the #5652 mirrors it was cited to catch were invisible to both rules #5899 measured it skips non-exported declarations, and it was green onorigin/maintoo, i.e. green ON the restatement.check-doc-snippet-typesexit 1 was reported as NOT RUN (its own line: the snippet program was not run because the packages are unbuilt), not as a failure and not as a pass.
On the convergence itself
- Member-equality was measured first, and that is what made this mechanical. Equal in both directions and in the same order; had it differed either way, binding would have silently widened or narrowed what a widget author may declare, and the right answer would have been to report instead of implement.
- Keeping the SINGLE-kind form is the restraint I did not think to ask for:
ComponentInput.typealso accepts an array of arms, so importing that type wholesale would have been a widening dressed as a convergence. Pinning the restraint with a test is exactly right. - Both ablation legs confirm the finding(plugin-dashboard): two more private copies of the reference-bearing field family — #5312's "LAST private copy" claim is false #5692 lesson at its sharpest: a private member-identical alias (leg A) and the literal relapse (leg B) each leave every value, seam and doc assertion green, and
tscgreen — only the source-text identity pins go red. A convergence without an identity pin is not verified, it is hoped. - The
dist/index.d.tsbyte-identical reading was not misread as "no change" — it namesWidgetInputas a re-export specifier and carries none of its shape, and the gauge was shown to register a difference on both this change and the seam probe before being trusted.
PR #5906 lands at its gate window.
Generated by Claude Code
Generated by Claude Code
Filed by the
domain:uiexecution seat (sessionsession_012u2pRjcqAYtoEjgr3wwhnK), surfaced while reviewing #5671 (#4972). Filed now, deliberately, because the deferral it records currently lives only in #4972's body and stops existing the moment that card closes.Measured on
origin/mainpackages/types/src/widget.ts,WidgetInput:Two divergences from the
ComponentInputface that #4972 just converged:ComponentInput.typeusesComponentInputControlType, single-declared sinceComponentInput.type无法表达 spec 的联合类型,于是发布面永远比契约窄一个 arm ——page:header.title的内联翻译映射今天就会被 manifest 门报type-mismatch#3832.options?, whereComponentInputspells the same conceptenum?. Same idea, two names, one package.WidgetInputalso carries 8 keys againstComponentInput's 13 (post-#4972): noinputType,min,max,step,placeholder.Why it needs its own card
#4972's body defers this explicitly — 「建议单独判」 — and the
domain:uiseat fenced it out of round 13 on that basis. #5671 is landing with its closer flipped toFixes #4972, so that deferral is about to stop being tracked anywhere.Not pre-judged
I am not asserting the two faces should merge. Widget authoring and component registration are not obviously the same surface, and
optionsvsenummay be a deliberate distinction rather than drift. That is exactly the judgment #4972 said should be made on its own, and this card is the place to make it — including the answer "they are legitimately separate, close this."Filed unassigned.