You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
components: element:text_input's label / placeholder / description are the INVERSE of #5637 — the renderer already resolves the locale map, the declaration still says 'string', so the gate warns on a legal write #5717
Found while implementing #5637 (element:record_picker's label / placeholder). Out of that card's fence — filed, not fixed there.
This is the same family, opposite direction. #5590 and #5637 are "the contract admits the map, the renderer drops it, so the declaration correctly stays narrow until the render site catches up". element:text_input is the case where the render site already caught up and the declaration never moved.
Measured
packages/components/src/renderers/basic/text-input.tsx resolves all three keys at their read sites:
The contract for all three is the I18nLabel union — measured on the installed @objectstack/spec17.1.0 pin, ElementTextInputPropsSchema:
label union [string | record] | map safeParse: OK
placeholder union [string | record] | map safeParse: OK
description union [string | record] | map safeParse: OK
But the ComponentMeta entries still declare one arm each (text-input.tsx:146, :147, :183):
Why this is the defect and not a harmless narrowing
ComponentInput.type in packages/types/src/base.ts states the rule in both directions: declare an arm the renderer drops and you advertise a shape that never reaches the screen; withhold an arm the renderer resolves and the manifest gate reports type-mismatch on a legal write. Its own words for the second half — "one platform authority contradicting itself on the write it just recommended, at warning severity — which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss the unknown-prop and type-mismatch reports that ARE real."
Here the author writes a map, the renderer renders it correctly in the viewer's language, and the gate warns anyway. That is precisely the direction #5590's changeset called "the false declaration in the other direction".
Not a duplicate of the defaultValue specimen
apps/console/src/__tests__/component-input-union-specimens.test.ts:214 asserts that an I18N_MAP on element:text_input.defaultValuedoes report type-mismatch, and that assertion is correct and must stay: defaultValue's spec type is string | number, a key whose contract has no object arm. It is the control that keeps the widening per-key. The three keys above are a different set with a different contract, and none of them is pinned anywhere today.
What a fix would decide
Mechanically it looks like the emptyText move run backwards: widen the three inputs arms to ['string', 'object'] and rewrite the descriptions from what the renderer does. Two things worth a triage look rather than assuming:
Whether all three travel together, or description is separable — it has a different destination than the two label-ish keys.
Whether the console specimen file should gain a pin for this block, since the specimens are where "declared arms == contract arms" is currently guarded, and a fix with no pin can silently regress the same way.
Triage: pm:queue, domain:ui, bug. Declaration lags the renderer (renderer resolves the locale map, ComponentMeta still declares 'string'), so the gate warns on a legal write — the exact inverse of #5637 and the direction ComponentInput.type's own rule already condemns. Restores declared = enforced; no contract change (spec 17.1.0 already admits the map on all three keys, measured in the card). Scope: widen the three arms to ['string','object'], rewrite descriptions from what the renderer does, and answer the card's two look-items in the PR: whether description travels with the two label-ish keys, and add the console specimen pin for this block so it cannot silently regress. Note: #5637 (the inverse direction, record_picker) sits in the decision box — do not touch its keys here.
Claim: PM loop round 19
Session: session_01EuPCi56cnGyykygi3z9w4m
Branch: claude/issue-5717-text-input-declared-arms
Worktree: objectui-issue-5717
Domain: domain:ui
File surface: packages/components/src/renderers/basic/text-input.tsx and its tests, plus apps/console/src/__tests__/component-input-union-specimens.test.ts for the pin triage requires (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus
Clause-②: no
Serial constraints cleared: packages/components is free — #5714 merged 20:43Z. Round-19 siblings fenced disjoint: #5703 (packages/auth/src/createAuthenticatedFetch.ts) and #5660 (apps/console/index.html + src/main.tsx). ⚠️#5660 also lives in apps/console but touches different files; the specimen test is yours, index.html/main.tsx are its.
Tier basis. No path-derived mandate. Content judgement: clause ② does not trip — triage measured that spec 17.1.0 already admits the map on all three keys, so widening the declaration restores declared = enforced rather than widening any accept set. This is the mirror of #5637, which cleared for the same reason.
Premise re-verified on origin/maind8afbe519, every line reproduced exactly as the card cites it:
text-input.tsx:96 / :97 / :98 — pickLocalized(props.label|placeholder|description, language). The renderer already resolves all three.
:146 / :147 / :183 — { name: 'label'|'placeholder'|'description', type: 'string', … }. The declaration still says one arm.
This is the inverse of #5637 and the direction ComponentInput.type's own rule condemns: the author writes a map, the renderer renders it correctly in the viewer's language, and the manifest gate warns type-mismatch anyway. Its own words for why that matters: "one platform authority contradicting itself on the write it just recommended, at warning severity — which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss the unknown-prop and type-mismatch reports that ARE real."
Triage's ruling, binding, with two items it requires answered in the PR: widen the three arms to ['string','object'], rewrite the descriptions from what the renderer does, and (1) state whether description travels with the two label-ish keys or is separable, (2) add the console specimen pin for this block so it cannot silently regress.
⛔ Do not touch element:record_picker's keys — #5637 (the inverse direction) sits in the decision box. ⛔ Do not weaken component-input-union-specimens.test.ts:214, which asserts an I18N_MAP on element:text_input.defaultValuedoes report type-mismatch. That assertion is correct and must stay: defaultValue's spec type is string | number, a contract with no object arm. It is the control that keeps this widening per-key rather than blanket — if your change makes it go green, you have widened too far.
{
"issue": 5717,
"status": "done",
"branch": "claude/issue-5717-text-input-declared-arms",
"pr": "https://github.com/objectstack-ai/objectui/pull/5736",
"premise_still_valid": true,
"card_body_complete": "Yes — read in full, ends with 'Filed unassigned and unqueued.' Not truncated. Both comments read: triage first-touch (binding) and the round-19 claim.",
"summary": "Widened the three declared arms on element:text_input to ['string','object'] and wrote descriptions for all three from what the renderer does. Every PM measurement re-measured and reproduced exactly on origin/main d8afbe519: renderer resolves all three at :96/:97/:98, declaration says 'string' at :146/:147/:183, and spec 17.1.0 admits string|record on all three (defaultValue: string|number, no object arm). Added the console specimen pin triage required, spec-derived trio assertions in the components parity test, and a new renderer read-site pin. No renderer behaviour changed.",
"gates_run": [
"pnpm --filter @object-ui/components --filter @object-ui/console type-check -> GREEN ('packages/components type-check: Done', 'apps/console type-check: Done'; script names echoed, so not a zero-match silent pass)",
"pnpm --filter @object-ui/components --filter @object-ui/console lint -> GREEN ('0 errors, 910 warnings' / '0 errors, 202 warnings'; lint.yml sets no --max-warnings, this gate is about errors)",
"vitest run packages/components/ -> GREEN ('Test Files 183 passed (183)', 'Tests 1654 passed (1654)')",
"vitest run apps/console/ -> GREEN ('Test Files 72 passed (72)', 'Tests 790 passed (790)')",
"node scripts/check-changeset-presence.mjs -> GREEN ('4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)')",
"node scripts/check-changeset-fixed.mjs -> GREEN (exit 0)",
"node scripts/check-changeset-no-major.mjs -> GREEN (exit 0)",
"node scripts/check-control-bytes.mjs -> GREEN ('OK (scanned 4784 tracked text file(s); skipped 85 binary)')",
"node scripts/check-doc-component-types.mjs -> GREEN ('Every documented component type is registered.')",
"pnpm check:i18n-keys -> GREEN ('Every in-scope call-site key resolves against the en pack (2924 keys)')",
"pnpm check:spec-symbols -> GREEN ('1295 files scanned against 4966 spec export names')"
],
"gate_derivation_note": "dispatch-gates.mjs deliberately NOT used (objectstack-only; objectui has no scripts/pm). Set derived from objectui's own .github/workflows/ (ci.yml, lint.yml, control-bytes.yml, doc-component-types.yml, changeset-presence.yml) plus root package.json.",
"union_head_sha": "9711f5ca8",
"tests": "Gate measured through manifestFromConfigs + validateTree, the same path the card and the specimen test use — a type-level assertion cannot see this defect. BEFORE (d8afbe519): label/placeholder/description each declaredArms=\"string\" with one type-mismatch diagnostic apiece on { en: 'Owner', 'zh-CN': '负责人' }. AFTER: all three declaredArms=[\"string\",\"object\"] with diags=[] individually and on one whole node. ABLATION A (revert the three arms to 'string'): mutation confirmed on disk by anchored counts 3->0 widened / 0->3 narrow before the run; predicted red on the declaration assertions and green on the renderer pin; observed exactly that — 7 failed / 34 passed, the seven being the four new specimen cases and the three parity it.each arms, with text-input-i18n-label-arms.test.tsx entirely green. ABLATION B (drop pickLocalized at the three read sites): mutation confirmed on disk, 3 read sites replaced and markers counted; predicted red on the renderer pin only, failure mode a throw for the two React-child positions; observed 6 failed / 35 passed, all six in the renderer pin, mode 'Error: Objects are not valid as a React child (found: object with keys {en, zh-CN})', both declaration files green. NO REBUILD NEEDED and this was checked rather than assumed: the root vitest config aliases @object-ui/* to sibling src/, so tests run against source and no dist participates. Both legs restored via trap ... EXIT INT TERM; working tree verified byte-identical to HEAD afterwards.",
"look_item_1_does_description_travel_with_label_and_placeholder": "YES — all three travel together, and the card's premise about the destination is correct but not load-bearing. The three destinations genuinely differ: label is a label element ABOVE the field (tied by htmlFor), placeholder is the native attribute INSIDE it, description is a p element BELOW it. Destination is not what decides an arm. ComponentInput.type names exactly two conditions — the contract accepts the shape AND the renderer resolves it — and description satisfies both identically to label: same pickLocalized call shape one line apart, same contract, each measured per key rather than inherited. Splitting on destination would have left description emitting type-mismatch on a value that renders correctly, i.e. this same defect kept alive on one third of it. The destinations are pinned anyway: one renderer case writes a DISTINCT map per key, because three identical maps cannot tell 'each key reached its own destination' from 'one value rendered three times'. One real behavioural difference does separate them and the arms cannot carry it — label is announced to assistive tech, description is not — so it went into the published description and became finding #5735.",
"look_item_2_specimen_pin": "ADDED, in apps/console/src/__tests__/component-input-union-specimens.test.ts as its own describe, shaped like the neighbouring #4970 block: arms DERIVED from ElementTextInputPropsSchema's own verdicts rather than restated, so either side moving turns it red. Includes the derivation guard (a schema refusing every probe returns [] and would make the comparisons vacuous), a reachability case, per-arm controls on every specimen, a whole-node case reproducing the card's write, and a block-level assertion that the widening did not reach defaultValue. File grew 349 -> 550 lines.",
"defaultValue_specimen_verdict_after_change": "UNCHANGED AND GREEN AS A FAILURE ASSERTION — an I18N_MAP on element:text_input.defaultValue still reports type-mismatch. Measured directly in the after-probe (declaredArms=[\"string\",\"number\"], diags non-empty) and green in every run of the file. Its arms did not move. Re-measured the contract independently: specAcceptedArms(ElementTextInputPropsSchema,'defaultValue') === ['string','number'], no object arm — so the separation is now asserted from the CONTRACT in both files rather than left as convention. I did not widen too far.",
"controls_that_must_not_move": "Both honoured. defaultValue specimen untouched and green (run first, before any edit, and last). element:record_picker keys untouched — verified by diff: the only file changed under renderers/ is basic/text-input.tsx.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #5735: element:text_input renders `description` as an unassociated paragraph — no aria-describedby, so assistive tech never announces it with the field, while packages/components/src/renderers/form/form.tsx wires the identical key correctly. Dedupe-searched first (semantic search plus an a11y-label listing, no open match). Filed unassigned, no labels, for PM triage.",
"NOT filed, reported here instead — family sweep for the same defect class elsewhere came back EMPTY. Swept packages/** for a renderer resolving pickLocalized(props.X, ...) while declaring a single 'string' arm for X: no hits after this change. The instrument was controlled against the pre-fix file, where it returns exactly label, placeholder, description — so the empty result is a measurement, not a broken regex. The fence predicted hits were likely; they are not there."
],
"files_changed": [
"packages/components/src/renderers/basic/text-input.tsx (three arms widened, three descriptions written from renderer behaviour, block comment recording the inverse direction)",
"apps/console/src/__tests__/component-input-union-specimens.test.ts (the pin triage required; +201 lines, no existing assertion weakened)",
"packages/components/src/__tests__/text-input-inputs-spec-parity.test.ts (spec-derived trio arms + description content assertions)",
"packages/components/src/__tests__/text-input-i18n-label-arms.test.tsx (NEW — renderer read-site pin; the declaration now rests on 'the renderer resolves it' and nothing pinned that for these three keys)",
".changeset/text-input-i18n-label-arms-5717.md (patch, @object-ui/components)"
],
"notes_for_pm": [
"Branch cut from origin/main d8afbe519, not local main — local main was 9 commits behind, and d8afbe519 is the sha the PM measurements cite.",
"skip-changeset NOT touched: that label does not exist in objectui and I did not create one. A real changeset is present.",
"Sanitizer datapoint confirming the dispatch warning: the PR body's single angle-bracket span was eaten INSIDE BACKTICKS on create (`string | Record<string, string>` landed as `string | Record`). Reworded to remove angle brackets and re-verified. This report body contains no angle-bracket spans.",
"Foreground cap hit once: a combined components+console vitest run was SIGTERMed at 10m (exit 143) after a 3m21s lock wait. Split into two acquisitions; both then completed and are reported above. No result was inferred from the killed run."
]
}
ACCEPT (on merits) — PR #5736 · reviewer of record, domain:ui seat, round 19
⚠️On merits; landing held. Gates by name at 02:41Z (PR started 02:38Z): 16 success + 3 skipped + 6 in_progress. ⛔ Not flipped ready until 19 + 3 is verified.
Fence clean: 5 files — three in packages/components, the one granted apps/console specimen, one changeset. No governed path. The record_picker control holds: zero record-picker files in the diff, verified against the tree.
Look-item 1 was answered with an argument, not a preference
Yes, all three travel together — and the card's premise about destinations is correct but not load-bearing. The destinations really do differ (label is a label element above the field, tied by htmlFor; placeholder is the native attribute inside it; description is a pbelow it). But ComponentInput.type names exactly two conditions — the contract accepts the shape AND the renderer resolves it — and destination is not one of them. description satisfies both identically to label: same pickLocalized call shape one line apart, same contract, each measured per key rather than inherited.
Splitting on destination would have left description emitting type-mismatch on a value that renders correctly — this same defect kept alive on one third of it.
And the destinations are pinned anyway, with a control I want to name: one renderer case writes a distinct map per key, because three identical maps cannot tell "each key reached its own destination" from "one value rendered three times."
One real behavioural difference does separate them, and the arms cannot carry it — label is announced to assistive tech, description is not. That went into the published description and was filed as #5735. A difference that exists but is not an arm difference, disposed of as documentation plus a card rather than smuggled into the type. Correct on both counts.
The defaultValue control did better than I asked for
I asked that it not break, and flagged it as the single most informative signal available. It is unchanged and green as a failure assertion — an I18N_MAP on element:text_input.defaultValue still reports type-mismatch, measured directly (declaredArms=["string","number"], diags non-empty), and run first before any edit and last.
But it went further: it re-measured the contract independently — specAcceptedArms(ElementTextInputPropsSchema,'defaultValue') === ['string','number'], no object arm — so the separation is now asserted from the CONTRACT in both files rather than left as convention. I asked for a control to survive; what landed makes the control self-verifying. That is the difference between "we did not widen too far" and "widening too far now turns something red."
The specimen pin checks its own instrument
Arms are derived from ElementTextInputPropsSchema's own verdicts rather than restated, so either side moving turns it red. And it carries a derivation guard: a schema refusing every probe returns [], which would make the comparisons vacuous — so the vacuity check is applied to the instrument, not only to the assertions. Plus a reachability case, per-arm controls on every specimen, a whole-node case reproducing the card's write, and a block-level assertion that the widening did not reach defaultValue. 349 → 550 lines, no existing assertion weakened or re-pointed.
Two ablations in orthogonal directions
A — revert the three arms to 'string': predicted red on the declaration assertions, green on the renderer pin. Observed exactly that: 7 failed / 34 passed, the seven being the four new specimen cases and the three parity arms, with the renderer pin entirely green.
B — drop pickLocalized at the three read sites: predicted red on the renderer pin only, failure mode a throw. Observed 6 failed / 35 passed, all six in the renderer pin, mode Objects are not valid as a React child (found: object with keys {en, zh-CN}), both declaration files green.
Two ablations pointing in opposite directions separate "the declaration moved" from "the renderer resolves" as independent facts. A single ablation could not have.
A new pin file exists because the fix creates a new dependency.text-input-i18n-label-arms.test.tsx was added on the reasoning that "the declaration now rests on 'the renderer resolves it' and nothing pinned that for these three keys." Noticing that a fix makes a previously-incidental fact load-bearing, and pinning it in the same change, is the thing that stops this defect class from recurring in the other direction.
The family sweep came back empty, and that is a measurement. A sweep of packages/** for a renderer resolving pickLocalized(props.X, …) while declaring a single 'string' arm for X returns no hits after this change — and the instrument was controlled against the pre-fix file, where it returns exactly label, placeholder, description. So the empty result is a reading, not a broken regex.
Sanitizer, confirmed again: one angle-bracket span inside backticks was eaten on PR-body create — a generic parameter list vanished, leaving a bare Record. Reworded to avoid angle brackets entirely and re-verified by read-back. Third independent confirmation that "backticks protect" is false.
One foreground-cap kill (exit 143 at 10m, after a 3m21s lock wait) on a combined components+console run, split into two lock acquisitions and both completed. ⛔ No result was inferred from the killed run — stated explicitly, which is the part that matters.
The landing hold stated in the ACCEPT is lifted. Gates read by name at 02:49Z, one call, no polling:
19 success + 3 skipped = 22. Zero in_progress, zero failure.
success (19): Type Check · Lint · Test (shard 1/4) · Test (shard 2/4) · Test (shard 3/4) · Test (shard 4/4) · Build & E2E · Live E2E (informational) · Bundle Analysis · Build Docs · Doc Component Type Check · Doc Snippet Type Check · Internal Docs Link Check · Control Byte Scan · Skill Guide Path Check · Changeset Declaration · Changeset Bump Policy · Changeset Fixed Group Check · label
skipped (3, the standing no-ops): dependabot · Test (coverage) · the unexpanded Test (coverage shard ${{ matrix.shard }}/4) placeholder
Shard window measured on this PR: started 02:38:06Z, last shard completed 02:48:42Z — 10m36s, the upper edge of this lane's three-round baseline of 8m54s–10m36s. That is why the read point is 11 minutes after the PR's own started_at, and why the 02:41Z read (16 success + 3 skipped + 6 in_progress) was correctly treated as not a gate reading.
Fence re-confirmed against the tree before release — 5 files, nothing outside the granted surface, no governed path (AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**), and the record_picker control holds at zero:
Activity
os-zhuang commentedon Aug 23, 2026
Triage:
pm:queue,domain:ui, bug. Declaration lags the renderer (renderer resolves the locale map,ComponentMetastill declares'string'), so the gate warns on a legal write — the exact inverse of #5637 and the directionComponentInput.type's own rule already condemns. Restores declared = enforced; no contract change (spec 17.1.0 already admits the map on all three keys, measured in the card). Scope: widen the three arms to['string','object'], rewrite descriptions from what the renderer does, and answer the card's two look-items in the PR: whetherdescriptiontravels with the two label-ish keys, and add the console specimen pin for this block so it cannot silently regress. Note: #5637 (the inverse direction,record_picker) sits in the decision box — do not touch its keys here.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim: PM loop round 19⚠️ #5660 also lives in
Session:
session_01EuPCi56cnGyykygi3z9w4mBranch:
claude/issue-5717-text-input-declared-armsWorktree:
objectui-issue-5717Domain:
domain:uiFile surface:
packages/components/src/renderers/basic/text-input.tsxand its tests, plusapps/console/src/__tests__/component-input-union-specimens.test.tsfor the pin triage requires (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opusClause-②: no
Serial constraints cleared:
packages/componentsis free — #5714 merged 20:43Z. Round-19 siblings fenced disjoint: #5703 (packages/auth/src/createAuthenticatedFetch.ts) and #5660 (apps/console/index.html+src/main.tsx).apps/consolebut touches different files; the specimen test is yours,index.html/main.tsxare its.Tier basis. No path-derived mandate. Content judgement: clause ② does not trip — triage measured that spec 17.1.0 already admits the map on all three keys, so widening the declaration restores declared = enforced rather than widening any accept set. This is the mirror of #5637, which cleared for the same reason.
Premise re-verified on
origin/maind8afbe519, every line reproduced exactly as the card cites it:text-input.tsx:96/:97/:98—pickLocalized(props.label|placeholder|description, language). The renderer already resolves all three.:146/:147/:183—{ name: 'label'|'placeholder'|'description', type: 'string', … }. The declaration still says one arm.This is the inverse of #5637 and the direction
ComponentInput.type's own rule condemns: the author writes a map, the renderer renders it correctly in the viewer's language, and the manifest gate warnstype-mismatchanyway. Its own words for why that matters: "one platform authority contradicting itself on the write it just recommended, at warning severity — which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss theunknown-propandtype-mismatchreports that ARE real."Triage's ruling, binding, with two items it requires answered in the PR: widen the three arms to
['string','object'], rewrite the descriptions from what the renderer does, and (1) state whetherdescriptiontravels with the two label-ish keys or is separable, (2) add the console specimen pin for this block so it cannot silently regress.⛔ Do not touch
element:record_picker's keys — #5637 (the inverse direction) sits in the decision box. ⛔ Do not weakencomponent-input-union-specimens.test.ts:214, which asserts anI18N_MAPonelement:text_input.defaultValuedoes reporttype-mismatch. That assertion is correct and must stay:defaultValue's spec type isstring | number, a contract with no object arm. It is the control that keeps this widening per-key rather than blanket — if your change makes it go green, you have widened too far.Generated by Claude Code
fix(components): declare the I18nLabel arm on element:text_input's la…
os-zhuang commentedon Aug 23, 2026
os-dev-report
{ "issue": 5717, "status": "done", "branch": "claude/issue-5717-text-input-declared-arms", "pr": "https://github.com/objectstack-ai/objectui/pull/5736", "premise_still_valid": true, "card_body_complete": "Yes — read in full, ends with 'Filed unassigned and unqueued.' Not truncated. Both comments read: triage first-touch (binding) and the round-19 claim.", "summary": "Widened the three declared arms on element:text_input to ['string','object'] and wrote descriptions for all three from what the renderer does. Every PM measurement re-measured and reproduced exactly on origin/main d8afbe519: renderer resolves all three at :96/:97/:98, declaration says 'string' at :146/:147/:183, and spec 17.1.0 admits string|record on all three (defaultValue: string|number, no object arm). Added the console specimen pin triage required, spec-derived trio assertions in the components parity test, and a new renderer read-site pin. No renderer behaviour changed.", "gates_run": [ "pnpm --filter @object-ui/components --filter @object-ui/console type-check -> GREEN ('packages/components type-check: Done', 'apps/console type-check: Done'; script names echoed, so not a zero-match silent pass)", "pnpm --filter @object-ui/components --filter @object-ui/console lint -> GREEN ('0 errors, 910 warnings' / '0 errors, 202 warnings'; lint.yml sets no --max-warnings, this gate is about errors)", "vitest run packages/components/ -> GREEN ('Test Files 183 passed (183)', 'Tests 1654 passed (1654)')", "vitest run apps/console/ -> GREEN ('Test Files 72 passed (72)', 'Tests 790 passed (790)')", "node scripts/check-changeset-presence.mjs -> GREEN ('4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)')", "node scripts/check-changeset-fixed.mjs -> GREEN (exit 0)", "node scripts/check-changeset-no-major.mjs -> GREEN (exit 0)", "node scripts/check-control-bytes.mjs -> GREEN ('OK (scanned 4784 tracked text file(s); skipped 85 binary)')", "node scripts/check-doc-component-types.mjs -> GREEN ('Every documented component type is registered.')", "pnpm check:i18n-keys -> GREEN ('Every in-scope call-site key resolves against the en pack (2924 keys)')", "pnpm check:spec-symbols -> GREEN ('1295 files scanned against 4966 spec export names')" ], "gate_derivation_note": "dispatch-gates.mjs deliberately NOT used (objectstack-only; objectui has no scripts/pm). Set derived from objectui's own .github/workflows/ (ci.yml, lint.yml, control-bytes.yml, doc-component-types.yml, changeset-presence.yml) plus root package.json.", "union_head_sha": "9711f5ca8", "tests": "Gate measured through manifestFromConfigs + validateTree, the same path the card and the specimen test use — a type-level assertion cannot see this defect. BEFORE (d8afbe519): label/placeholder/description each declaredArms=\"string\" with one type-mismatch diagnostic apiece on { en: 'Owner', 'zh-CN': '负责人' }. AFTER: all three declaredArms=[\"string\",\"object\"] with diags=[] individually and on one whole node. ABLATION A (revert the three arms to 'string'): mutation confirmed on disk by anchored counts 3->0 widened / 0->3 narrow before the run; predicted red on the declaration assertions and green on the renderer pin; observed exactly that — 7 failed / 34 passed, the seven being the four new specimen cases and the three parity it.each arms, with text-input-i18n-label-arms.test.tsx entirely green. ABLATION B (drop pickLocalized at the three read sites): mutation confirmed on disk, 3 read sites replaced and markers counted; predicted red on the renderer pin only, failure mode a throw for the two React-child positions; observed 6 failed / 35 passed, all six in the renderer pin, mode 'Error: Objects are not valid as a React child (found: object with keys {en, zh-CN})', both declaration files green. NO REBUILD NEEDED and this was checked rather than assumed: the root vitest config aliases @object-ui/* to sibling src/, so tests run against source and no dist participates. Both legs restored via trap ... EXIT INT TERM; working tree verified byte-identical to HEAD afterwards.", "look_item_1_does_description_travel_with_label_and_placeholder": "YES — all three travel together, and the card's premise about the destination is correct but not load-bearing. The three destinations genuinely differ: label is a label element ABOVE the field (tied by htmlFor), placeholder is the native attribute INSIDE it, description is a p element BELOW it. Destination is not what decides an arm. ComponentInput.type names exactly two conditions — the contract accepts the shape AND the renderer resolves it — and description satisfies both identically to label: same pickLocalized call shape one line apart, same contract, each measured per key rather than inherited. Splitting on destination would have left description emitting type-mismatch on a value that renders correctly, i.e. this same defect kept alive on one third of it. The destinations are pinned anyway: one renderer case writes a DISTINCT map per key, because three identical maps cannot tell 'each key reached its own destination' from 'one value rendered three times'. One real behavioural difference does separate them and the arms cannot carry it — label is announced to assistive tech, description is not — so it went into the published description and became finding #5735.", "look_item_2_specimen_pin": "ADDED, in apps/console/src/__tests__/component-input-union-specimens.test.ts as its own describe, shaped like the neighbouring #4970 block: arms DERIVED from ElementTextInputPropsSchema's own verdicts rather than restated, so either side moving turns it red. Includes the derivation guard (a schema refusing every probe returns [] and would make the comparisons vacuous), a reachability case, per-arm controls on every specimen, a whole-node case reproducing the card's write, and a block-level assertion that the widening did not reach defaultValue. File grew 349 -> 550 lines.", "defaultValue_specimen_verdict_after_change": "UNCHANGED AND GREEN AS A FAILURE ASSERTION — an I18N_MAP on element:text_input.defaultValue still reports type-mismatch. Measured directly in the after-probe (declaredArms=[\"string\",\"number\"], diags non-empty) and green in every run of the file. Its arms did not move. Re-measured the contract independently: specAcceptedArms(ElementTextInputPropsSchema,'defaultValue') === ['string','number'], no object arm — so the separation is now asserted from the CONTRACT in both files rather than left as convention. I did not widen too far.", "controls_that_must_not_move": "Both honoured. defaultValue specimen untouched and green (run first, before any edit, and last). element:record_picker keys untouched — verified by diff: the only file changed under renderers/ is basic/text-input.tsx.", "open_questions": [], "out_of_scope_findings": [ "filed as #5735: element:text_input renders `description` as an unassociated paragraph — no aria-describedby, so assistive tech never announces it with the field, while packages/components/src/renderers/form/form.tsx wires the identical key correctly. Dedupe-searched first (semantic search plus an a11y-label listing, no open match). Filed unassigned, no labels, for PM triage.", "NOT filed, reported here instead — family sweep for the same defect class elsewhere came back EMPTY. Swept packages/** for a renderer resolving pickLocalized(props.X, ...) while declaring a single 'string' arm for X: no hits after this change. The instrument was controlled against the pre-fix file, where it returns exactly label, placeholder, description — so the empty result is a measurement, not a broken regex. The fence predicted hits were likely; they are not there." ], "files_changed": [ "packages/components/src/renderers/basic/text-input.tsx (three arms widened, three descriptions written from renderer behaviour, block comment recording the inverse direction)", "apps/console/src/__tests__/component-input-union-specimens.test.ts (the pin triage required; +201 lines, no existing assertion weakened)", "packages/components/src/__tests__/text-input-inputs-spec-parity.test.ts (spec-derived trio arms + description content assertions)", "packages/components/src/__tests__/text-input-i18n-label-arms.test.tsx (NEW — renderer read-site pin; the declaration now rests on 'the renderer resolves it' and nothing pinned that for these three keys)", ".changeset/text-input-i18n-label-arms-5717.md (patch, @object-ui/components)" ], "notes_for_pm": [ "Branch cut from origin/main d8afbe519, not local main — local main was 9 commits behind, and d8afbe519 is the sha the PM measurements cite.", "skip-changeset NOT touched: that label does not exist in objectui and I did not create one. A real changeset is present.", "Sanitizer datapoint confirming the dispatch warning: the PR body's single angle-bracket span was eaten INSIDE BACKTICKS on create (`string | Record<string, string>` landed as `string | Record`). Reworded to remove angle brackets and re-verified. This report body contains no angle-bracket spans.", "Foreground cap hit once: a combined components+console vitest run was SIGTERMed at 10m (exit 143) after a 3m21s lock wait. Split into two acquisitions; both then completed and are reported above. No result was inferred from the killed run." ] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
ACCEPT (on merits) — PR #5736 · reviewer of record,
domain:uiseat, round 19success+ 3skipped+ 6in_progress. ⛔ Not flipped ready until 19 + 3 is verified.Fence clean: 5 files — three in
packages/components, the one grantedapps/consolespecimen, one changeset. No governed path. Therecord_pickercontrol holds: zerorecord-pickerfiles in the diff, verified against the tree.Look-item 1 was answered with an argument, not a preference
Yes, all three travel together — and the card's premise about destinations is correct but not load-bearing. The destinations really do differ (
labelis a label element above the field, tied byhtmlFor;placeholderis the native attribute inside it;descriptionis apbelow it). ButComponentInput.typenames exactly two conditions — the contract accepts the shape AND the renderer resolves it — and destination is not one of them.descriptionsatisfies both identically tolabel: samepickLocalizedcall shape one line apart, same contract, each measured per key rather than inherited.And the destinations are pinned anyway, with a control I want to name: one renderer case writes a distinct map per key, because three identical maps cannot tell "each key reached its own destination" from "one value rendered three times."
One real behavioural difference does separate them, and the arms cannot carry it —
labelis announced to assistive tech,descriptionis not. That went into the published description and was filed as #5735. A difference that exists but is not an arm difference, disposed of as documentation plus a card rather than smuggled into the type. Correct on both counts.The
defaultValuecontrol did better than I asked forI asked that it not break, and flagged it as the single most informative signal available. It is unchanged and green as a failure assertion — an
I18N_MAPonelement:text_input.defaultValuestill reportstype-mismatch, measured directly (declaredArms=["string","number"], diags non-empty), and run first before any edit and last.But it went further: it re-measured the contract independently —
specAcceptedArms(ElementTextInputPropsSchema,'defaultValue') === ['string','number'], no object arm — so the separation is now asserted from the CONTRACT in both files rather than left as convention. I asked for a control to survive; what landed makes the control self-verifying. That is the difference between "we did not widen too far" and "widening too far now turns something red."The specimen pin checks its own instrument
Arms are derived from
ElementTextInputPropsSchema's own verdicts rather than restated, so either side moving turns it red. And it carries a derivation guard: a schema refusing every probe returns[], which would make the comparisons vacuous — so the vacuity check is applied to the instrument, not only to the assertions. Plus a reachability case, per-arm controls on every specimen, a whole-node case reproducing the card's write, and a block-level assertion that the widening did not reachdefaultValue. 349 → 550 lines, no existing assertion weakened or re-pointed.Two ablations in orthogonal directions
'string': predicted red on the declaration assertions, green on the renderer pin. Observed exactly that: 7 failed / 34 passed, the seven being the four new specimen cases and the three parity arms, with the renderer pin entirely green.pickLocalizedat the three read sites: predicted red on the renderer pin only, failure mode a throw. Observed 6 failed / 35 passed, all six in the renderer pin, modeObjects are not valid as a React child (found: object with keys {en, zh-CN}), both declaration files green.Two ablations pointing in opposite directions separate "the declaration moved" from "the renderer resolves" as independent facts. A single ablation could not have.
A new pin file exists because the fix creates a new dependency.
text-input-i18n-label-arms.test.tsxwas added on the reasoning that "the declaration now rests on 'the renderer resolves it' and nothing pinned that for these three keys." Noticing that a fix makes a previously-incidental fact load-bearing, and pinning it in the same change, is the thing that stops this defect class from recurring in the other direction.The family sweep came back empty, and that is a measurement. A sweep of
packages/**for a renderer resolvingpickLocalized(props.X, …)while declaring a single'string'arm for X returns no hits after this change — and the instrument was controlled against the pre-fix file, where it returns exactlylabel,placeholder,description. So the empty result is a reading, not a broken regex.Sanitizer, confirmed again: one angle-bracket span inside backticks was eaten on PR-body create — a generic parameter list vanished, leaving a bare
Record. Reworded to avoid angle brackets entirely and re-verified by read-back. Third independent confirmation that "backticks protect" is false.One foreground-cap kill (exit 143 at 10m, after a 3m21s lock wait) on a combined components+console run, split into two lock acquisitions and both completed. ⛔ No result was inferred from the killed run — stated explicitly, which is the part that matters.
Landing blocked only on the by-name gate read.
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Gate release — PR #5736 enqueued.
The landing hold stated in the ACCEPT is lifted. Gates read by name at 02:49Z, one call, no polling:
19
success+ 3skipped= 22. Zeroin_progress, zerofailure.success(19): Type Check · Lint · Test (shard 1/4) · Test (shard 2/4) · Test (shard 3/4) · Test (shard 4/4) · Build & E2E · Live E2E (informational) · Bundle Analysis · Build Docs · Doc Component Type Check · Doc Snippet Type Check · Internal Docs Link Check · Control Byte Scan · Skill Guide Path Check · Changeset Declaration · Changeset Bump Policy · Changeset Fixed Group Check · labelskipped(3, the standing no-ops):dependabot·Test (coverage)· the unexpandedTest (coverage shard ${{ matrix.shard }}/4)placeholderShard window measured on this PR: started 02:38:06Z, last shard completed 02:48:42Z — 10m36s, the upper edge of this lane's three-round baseline of 8m54s–10m36s. That is why the read point is 11 minutes after the PR's own
started_at, and why the 02:41Z read (16 success + 3 skipped + 6 in_progress) was correctly treated as not a gate reading.Fence re-confirmed against the tree before release — 5 files, nothing outside the granted surface, no governed path (
AGENTS.md,CLAUDE.md,.claude/**,docs/adr/**), and therecord_pickercontrol holds at zero:Flipped ready → auto-merge (SQUASH). Landing verification against
origin/mainfollows the merge.Generated by Claude Code
fix(components): declare the I18nLabel arm on element:text_input's la…