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: record-picker's label and placeholder drop the same inline-locale-map arm emptyText just stopped dropping — one throws, the other silently renders English or nothing #5637
Found while implementing #5590 (PR #5636), which fixed the third key of this same block. Not fixed there — outside that card's render-site fence, which the dispatching seat drew deliberately.
What
packages/components/src/renderers/basic/record-picker.tsx has three props whose contract is the I18nLabel union. #5590 fixed one (emptyText). The other two are still unresolved, and they fail in two different ways.
Measured on the installed pin (@objectstack/spec@17.0.0 GA), ElementRecordPickerPropsSchema's own shape — all three members resolve to optional -> union -> string | record:
top-level keys: object, labelField, valueField, label, filter, sort, limit,
targetVariable, placeholder, emptyText, displayField,
searchFields, multiple, aria
label union => string | record
placeholder union => string | record
1. placeholder — the map arm THROWS
placeholder is read raw (props.placeholder ?? 'Select a record…', :164 before #5590) and handed to SelectValue. Measured, rendering the block with placeholder: { en: 'Owner', 'zh-CN': '负责人' }:
placeholder=MAP lang=zh-CN :: THREW Objects are not valid as a React child
(found: object with keys {en, zh-CN}).
Identical harm to the one #5590 just removed from emptyText: not a mis-render, the whole picker subtree fails.
2. label — worse than a throw, because it looks like it works
label goes through this file's local toText() helper, whose object branch is
That reaches o.enunconditionally, so it is not locale resolution — it is an English pick wearing locale resolution's clothes. Two measured consequences, same fixture shapes, viewer language zh-CN:
label={ en: 'Owner', 'zh-CN': '负责人' } -> RENDERED text="Owner…" (English to a zh-CN viewer)
label={ 'zh-CN': '负责人', ja: '担当' } -> RENDERED text="Select a record…" (label element ABSENT)
The second is the quiet one. With no en entry toText returns '', and the render site is {label && ...} — so a correctly authored map that simply omits English makes the picker's label disappear, with nothing thrown and nothing logged.
3. The declarations say 'string' for both
inputs still declares { name: 'placeholder', type: 'string' } and { name: 'label', type: 'string' }. Under the ComponentInput.type rule (packages/types/src/base.ts) that narrowing is correct while the renderer drops the arm — the same reasoning emptyText carried until #5590 — so the declarations should move only in the change that makes the arms render, not before.
What a fix would decide
emptyText has just settled the shape for this file: resolve at the read site with pickLocalized, then widen the ComponentMeta arms in the same change. placeholder looks like a direct repeat of that.
label is not a repeat and is the reason this is filed rather than folded in:
toText is shared — it also renders row values (toText(row?.[labelField]), :206). Those are record field values, not I18nLabel, so changing toText itself changes a second, unrelated call site. The narrow move is to resolve label through pickLocalized at its own read site and leave toText to the row values.
Sibling read sites pair pickLocalized with translateLabel for label specifically (containers.tsx:569, :831) — whether this block's label should follow that pairing or use bare pickLocalized is a real choice, not a mechanical one.
Filed unassigned and unqueued: whether these two travel together, and whether label wants the translateLabel pairing, is a triage call.
Concentration-round first-touch (triage seat): promoted finding → pm:queue, Bug (label bug), size S. The two open calls the card left for triage, ruled here:
label takes the sibling pairing — pickLocalized + translateLabel, matching containers.tsx:569/:831; consistency across read sites for the same key is the point of this family. ⛔ toText itself stays untouched (it serves row values, a different contract) — the card's own analysis is adopted as the fence.
Claim: PM loop round 17
Session: session_01EuPCi56cnGyykygi3z9w4m
Branch: claude/issue-5637-record-picker-label-placeholder-i18n
Worktree: objectui-issue-5637
Domain: domain:ui
File surface: packages/components/src/renderers/basic/record-picker.tsx and its tests under packages/components/src/renderers/**/__tests__/ (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus
Clause-②: no
Serial constraints cleared: no in-flight card shares this file. Round-17 siblings fenced disjoint — #5710 (packages/types + packages/providers) and #5619 (packages/auth + app-shell + apps/console). Predecessor on this exact file, fa140b8 (#5590/PR #5636, the emptyText arm), is merged — this card is the remaining two keys of that same block.
Tier basis.node scripts/pm/dispatch-gates.mjs --tier packages/components/src/renderers/basic/record-picker.tsx — run at dispatch — returns no path-derived mandate (floor sonnet · default opus · ceiling fable). Content judgement: the change widens two ComponentMeta input arms from type: 'string' to the I18nLabel union, which on its face touches an authorable declaration — but it inherits the already-cleared mother ruling #5590 on the same file and the same block, whose reason applies verbatim here: the declaration moves only in the change that makes the arm render, so this is declared = enforced restoration, not a new capability. Mother's reason re-checked on this branch rather than assumed, per the same-family rule. Clause ② therefore does not trip; tier stays at the default judgement档 because the fix carries i18n render semantics rather than being purely gate-decided.
Premise re-verified before dispatch, on origin/main = 7138bc1. Triage's concentration-round first-touch (2026-08-22T13:12Z) settled both calls the card left open, and is carried into the brief as the ruling, not as a suggestion: (1) the two keys travel together in one PR on the settled emptyText shape, widening the two ComponentMeta arms in the same change and never before; (2) label takes the sibling pairing pickLocalized + translateLabel matching containers.tsx:569/:831; ⛔ toText itself stays untouched — it serves row values, a different contract, and the card's own analysis is adopted as the fence.
{
"issue": 5637,
"status": "rework",
"branch": "claude/issue-5637-record-picker-label-placeholder-i18n",
"pr": "https://github.com/objectstack-ai/objectui/pull/5714",
"premise_still_valid": true,
"issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.",
"summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.",
"deviations_from_dispatch": [
"PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed #5637 would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.",
"No new issue filed for the translateLabel gap — it stays on #5637, which `Part of` keeps open. Filing a second tracker would split it."
],
"tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — `npx vitest run <new test>`: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.",
"gates_run": [
"pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)",
"pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op",
"eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors",
"vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`",
"apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen",
"pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`",
"pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`",
"pnpm check:i18n-drift — `No en value changed in this range.`",
"pnpm check:i18n-dead-keys — report-only by its own header, exit 0",
"pnpm check:doc-types — `✅ Every documented component type is registered.`",
"pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`",
"pnpm changeset:check — `✅ No changeset declares a 'major' bump.`"
],
"declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.",
"files_changed": [
"packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)",
"packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)",
".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)"
],
"pm_assumptions_measured": [
"1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).",
"2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.",
"3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.",
"4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.",
"DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> [string | record]; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale."
],
"open_questions": [
{
"question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?",
"options": [
"A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.",
"B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.",
"C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting #5637 close on the defect it measured."
],
"recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement."
}
],
"out_of_scope_findings": [
"filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports `type-mismatch :: <element:text_input> prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.",
"NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, `<span>{schema.placeholder || 'Pick a date'}</span>`). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine."
],
"resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)."
}
Supersedes the previous comment — the GitHub body sanitizer ate its HTML-comment marker (read back: the body now begins with a blank line, no marker), which would make it invisible to a marker-based scan. Posted again with the marker as literal text, per the fallback rule. Three short angle-bracket fragments were eaten from the JSON in that copy too and are respelled here. No content changes otherwise.
{
"issue": 5637,
"status": "rework",
"branch": "claude/issue-5637-record-picker-label-placeholder-i18n",
"pr": "https://github.com/objectstack-ai/objectui/pull/5714",
"premise_still_valid": true,
"issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.",
"summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.",
"deviations_from_dispatch": [
"PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed this card would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.",
"No new issue filed for the translateLabel gap — it stays on this card, which `Part of` keeps open. Filing a second tracker would split it."
],
"tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — vitest run over the new test file: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.",
"gates_run": [
"pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)",
"pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op",
"eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors",
"vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`",
"apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen",
"pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`",
"pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`",
"pnpm check:i18n-drift — `No en value changed in this range.`",
"pnpm check:i18n-dead-keys — report-only by its own header, exit 0",
"pnpm check:doc-types — `✅ Every documented component type is registered.`",
"pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`",
"pnpm changeset:check — `✅ No changeset declares a 'major' bump.`"
],
"declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.",
"files_changed": [
"packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)",
"packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)",
".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)"
],
"pm_assumptions_measured": [
"1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).",
"2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.",
"3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.",
"4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.",
"DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> string | record; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale."
],
"open_questions": [
{
"question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?",
"options": [
"A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.",
"B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.",
"C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting this one close on the defect it measured."
],
"recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement."
}
],
"out_of_scope_findings": [
"filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports a `type-mismatch` diagnostic reading `prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.",
"NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, which puts `schema.placeholder || 'Pick a date'` inside a span element). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine."
],
"resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)."
}
ACCEPT on the measured defect — PR #5714 enqueued · reviewer of record, domain:ui seat, round 17
Verdict differs from the dev's self-report, deliberately. It filed status: rework; I am recording ACCEPT on the measured defect, with the ruling's second clause carved out to a decision — because nothing in the delivered work is defective. What is outstanding is a ruled clause that is unreachable from inside the fence, which is a scoping question, not rework.
Gates read by name at head 907ec3e64: 19 success + 3 skipped (dependabot, Test (coverage), the unexpanded shard placeholder) = 22. Green. Path surface is 3 files (record-picker.tsx, its new test, changeset) — no governed path. Flipped ready, auto-merge SQUASH enabled. ⚠️ The PR's first line is Part of #5637, so merging will not close this card — that is correct and deliberate, and the body is left untouched rather than round-tripped through this tooling.
Verified against the tree, not the report:toText's body is byte-unchanged (the only removed line is the read site; toText(row?.[labelField]) still serves row values); both ComponentMeta arms widen 'string' → ['string','object'] in the same commit as the renderer change, which is the order the ruling required. The test carries non-vacuity controls in both directions — not.toBeNull() asserted before textContent, paired with a control that the element stays absent when label is absent — and asserts the same fixture under two languages, which is what makes an .en pick (the actual bug) impossible to pass.
The carve-out: clause 2 is unreachable, and the measurement argues against it
Triage ruled label takes the sibling pairing pickLocalized+ translateLabel. ⛔ I am not overruling that, and this comment does not re-adjudicate it. I am reporting that implementing it requires a decision neither triage nor this seat may make, and routing it accordingly.
Why it is unreachable:translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which this card's fence marks hard out-of-scope. Reaching them needs either an export from a fenced file plus a new basic/ → layout/ static import between renderer families, or a hoist into a shared module across more out-of-fence files. The dev stopped at the fence and escalated instead of crossing it — the correct move.
Three measurements that were not available when clause 2 was written:
translateLabel is not a locale resolver. It is a hardcoded dictionary of well-known tab tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW only.
Both live call sites translate labels that buildDefaultTabsgenerated. A record_picker.label is author-supplied — so the pairing would mean the platform silently rewrites an author's literal string, and only for Chinese viewers.
The consistency argument behind clause 2 is real — same key family, same read-site treatment. What the measurement shows is that the two sibling sites and this one are not the same case: generated tokens versus authored text.
四维分析(业务角度)
一句话问题:作者在 record picker 上手写了英文 label「Owner」,中文用户应该看到「Owner」,还是平台自作主张把它改写成「负责人」?
选项
客户可感知的代价
A 从 containers.tsx 导出 translateLabel,在 record-picker 里用
State transition, in the same write as this comment:pm:dispatched → needs-user-decision, assignee cleared. The delivered work is done and enqueued; what remains on this card is the decision above. ⛔ The card does not close when #5714 merges.
Out-of-scope finding verified:#5717 filed — element:text_input is the exact inverse of this card (renderer already resolves all three keys through pickLocalized, but ComponentMeta still declares a single 'string' arm, so validateTree reports a type-mismatch on a legal write the renderer renders correctly). Dedupe-searched twice, and explicitly distinguished from the element:text_input.defaultValue control whose type-mismatch is correct and must stay.
⚠️One item for triage, not actioned here: the dev reports that #4163 — the original "audit every I18nLabel read the compiler cannot see" card — closed as completed on 2026-08-15 while this gap was open, and has no open successor. Two cards in two rounds have now come out of that supposedly-closed audit. Whether to re-open it is triage's call.
Drift worth fixing at the source: the installed pin is @objectstack/spec17.1.0, not the 17.0.0 GA that both this card and #5590 state. The union is unchanged on all three keys, so the premise held — but the version both documents cite is stale.
Maintainer ruling recorded (2026-08-23, live PM chat, verbatim: 「其他同意你的意见」— adopting recommendation C): the translateLabel pairing clause is ruled NOT APPLICABLE to author-supplied labels. An author's literal string is rendered as written; authors who want localization declare an inline locale map — the capability PR #5714 delivered (merged). The dictionary stays scoped to generated tab tokens; its language-completeness debt remains parked on #4645, whose reach is deliberately NOT widened.
Card closes completed (same stroke): the measured defect landed via PR #5714; clause 2 is resolved by this ruling with zero further code change. For the next reader: this closure supersedes the earlier Zone-1 clause-2 wording — do not re-open the pairing for record_picker.label without a new ruling. The #4163 audit-gap observation (two cards in two rounds out of a closed audit) stays with the triage seat: a third instance triggers a fresh audit card.
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat): promoted
finding→pm:queue, Bug (labelbug), size S. The two open calls the card left for triage, ruled here:emptyTextshape (components:record-picker'semptyTextdrops the inline-locale-map arm the contract admits, and the audit tracking it (#4163) is closed #5590): resolve both at their read sites withpickLocalized, widen the twoComponentMetaarms in the same change (never before), pin all three measured fixtures (map-throws, English-to-zh-viewer, no-en-map label vanishing).labeltakes the sibling pairing —pickLocalized+translateLabel, matchingcontainers.tsx:569/:831; consistency across read sites for the same key is the point of this family. ⛔toTextitself stays untouched (it serves row values, a different contract) — the card's own analysis is adopted as the fence.Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
Claim: PM loop round 17
Session:
session_01EuPCi56cnGyykygi3z9w4mBranch:
claude/issue-5637-record-picker-label-placeholder-i18nWorktree:
objectui-issue-5637Domain:
domain:uiFile surface:
packages/components/src/renderers/basic/record-picker.tsxand its tests underpackages/components/src/renderers/**/__tests__/(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opusClause-②: no
Serial constraints cleared: no in-flight card shares this file. Round-17 siblings fenced disjoint — #5710 (
packages/types+packages/providers) and #5619 (packages/auth+app-shell+apps/console). Predecessor on this exact file,fa140b8(#5590/PR #5636, theemptyTextarm), is merged — this card is the remaining two keys of that same block.Tier basis.
node scripts/pm/dispatch-gates.mjs --tier packages/components/src/renderers/basic/record-picker.tsx— run at dispatch — returns no path-derived mandate (floor sonnet · default opus · ceiling fable). Content judgement: the change widens twoComponentMetainput arms fromtype: 'string'to theI18nLabelunion, which on its face touches an authorable declaration — but it inherits the already-cleared mother ruling #5590 on the same file and the same block, whose reason applies verbatim here: the declaration moves only in the change that makes the arm render, so this isdeclared = enforcedrestoration, not a new capability. Mother's reason re-checked on this branch rather than assumed, per the same-family rule. Clause ② therefore does not trip; tier stays at the default judgement档 because the fix carries i18n render semantics rather than being purely gate-decided.Premise re-verified before dispatch, on
origin/main=7138bc1. Triage's concentration-round first-touch (2026-08-22T13:12Z) settled both calls the card left open, and is carried into the brief as the ruling, not as a suggestion: (1) the two keys travel together in one PR on the settledemptyTextshape, widening the twoComponentMetaarms in the same change and never before; (2)labeltakes the sibling pairingpickLocalized+translateLabelmatchingcontainers.tsx:569/:831; ⛔toTextitself stays untouched — it serves row values, a different contract, and the card's own analysis is adopted as the fence.Generated by Claude Code
fix(components): record_picker label/placeholder resolve the inline l…
os-zhuang commentedon Aug 22, 2026
{ "issue": 5637, "status": "rework", "branch": "claude/issue-5637-record-picker-label-placeholder-i18n", "pr": "https://github.com/objectstack-ai/objectui/pull/5714", "premise_still_valid": true, "issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.", "summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.", "deviations_from_dispatch": [ "PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed #5637 would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.", "No new issue filed for the translateLabel gap — it stays on #5637, which `Part of` keeps open. Filing a second tracker would split it." ], "tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — `npx vitest run <new test>`: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.", "gates_run": [ "pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)", "pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op", "eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors", "vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`", "apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen", "pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`", "pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`", "pnpm check:i18n-drift — `No en value changed in this range.`", "pnpm check:i18n-dead-keys — report-only by its own header, exit 0", "pnpm check:doc-types — `✅ Every documented component type is registered.`", "pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`", "pnpm changeset:check — `✅ No changeset declares a 'major' bump.`" ], "declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.", "files_changed": [ "packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)", "packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)", ".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)" ], "pm_assumptions_measured": [ "1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).", "2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.", "3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.", "4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.", "DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> [string | record]; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale." ], "open_questions": [ { "question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?", "options": [ "A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.", "B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.", "C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting #5637 close on the defect it measured." ], "recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement." } ], "out_of_scope_findings": [ "filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports `type-mismatch :: <element:text_input> prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.", "NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, `<span>{schema.placeholder || 'Pick a date'}</span>`). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine." ], "resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)." }Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
os-dev-report
Supersedes the previous comment — the GitHub body sanitizer ate its HTML-comment marker (read back: the body now begins with a blank line, no marker), which would make it invisible to a marker-based scan. Posted again with the marker as literal text, per the fallback rule. Three short angle-bracket fragments were eaten from the JSON in that copy too and are respelled here. No content changes otherwise.
{ "issue": 5637, "status": "rework", "branch": "claude/issue-5637-record-picker-label-placeholder-i18n", "pr": "https://github.com/objectstack-ai/objectui/pull/5714", "premise_still_valid": true, "issue_body_complete": "YES — read in full plus both comments (triage first-touch 2026-08-22T13:12Z, and the round-17 claim). The body ends with the `What a fix would decide` section closing on the translateLabel pairing question and the attribution footer. Not truncated.", "summary": "Both keys now resolve at their own read site through pickLocalized: `label = pickLocalized(props.label, language)` and `placeholder = pickLocalized(props.placeholder ?? 'Select a record…', language)`, default-before-resolution matching the settled emptyText shape. toText is byte-unchanged (verified: the only diff line matching its body text is a new comment) and still serves the row values. The two ComponentMeta inputs widen from `type: 'string'` to `['string','object']` in this same change, with both descriptions rewritten from renderer behaviour. STATUS IS rework, NOT done, for ONE reason: the translateLabel half of the Zone-1 ruling is NOT implemented. translateLabel and KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope ('every other renderer in packages/components'). Reaching them needs either an export from that fenced file plus a new basic/ -> layout/ static import between renderer families (a repo that budgets its eager import closure in bytes), or a hoist into a shared module across more out-of-fence files. Neither is mechanical, so I stopped at the fence and escalated rather than crossing it. All three MEASURED fixtures are fixed; only the pairing is outstanding.", "deviations_from_dispatch": [ "PR body first line is `Part of #5637`, NOT the `Fixes #5637` the dispatch specified. Standing clause: use Part of when merging should not close the card. A binding ruling clause is unimplemented, so a merge that closed this card would close it with that clause silently open. One-line edit if PM rules otherwise. Flagged in the PR body itself as the first thing a reader sees.", "No new issue filed for the translateLabel gap — it stays on this card, which `Part of` keeps open. Filing a second tracker would split it." ], "tests": "RED-FIRST (unfixed tree, new test file, viewer zh-CN) — vitest run over the new test file: `Test Files 1 failed (1)` / `Tests 6 failed | 5 passed (11)`. All three card-measured modes reproduced verbatim: (1) placeholder map -> `Error: Objects are not valid as a React child (found: object with keys {en, zh-CN}).`; (2) label map -> `AssertionError: expected 'Owner' to be '负责人'`; (3) label map without `en` -> `AssertionError: expected null not to be null` (label element gone). The 5 greens are the controls + reachability. GREEN, all at 907ec3e64 (the tree of the PR's only commit): new test + 3 neighbours -> `Test Files 4 passed (4)` / `Tests 38 passed (38)`; `vitest run packages/components` -> `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`. No ablation was run — a genuine red-first measurement against the unmutated pre-fix tree replaces it, so there is no mutation to confirm on disk and no rebuild leg to report. ASSERTION MECHANISM (dispatch Zone 3b): rendered output only. The registered renderer is invoked directly via ComponentRegistry.get inside an I18nProvider pinned to a language; the throw case is read as 'renders, and the trigger textContent contains the resolved string'; the language a viewer sees is `container.querySelector('label')?.textContent`, asserted for zh-CN AND en on the SAME map (an `.en` pick passes the English half alone); presence/absence is `expect(labelEl(container)).not.toBeNull()` asserted BEFORE its text, paired with a control that the element stays absent when `label` is absent.", "gates_run": [ "pnpm --filter '@object-ui/components^...' build — exit 0 (first command in the new worktree)", "pnpm --filter @object-ui/components type-check — exit 0; script echoed `tsc --noEmit && tsc -p tsconfig.test.json`, so not a zero-match no-op", "eslint . in packages/components — `✖ 909 problems (0 errors, 909 warnings)`, 396 files (count read from --format json). All 909 pre-existing; my two files contribute 0 errors", "vitest run packages/components — `Test Files 182 passed (182)` / `Tests 1638 passed (1638)`", "apps/console declaration-parity tests (component-input-union-specimens, registry-inputs-spec-parity, ga-honoured-inputs-author-reach, public-contract) — `Test Files 4 passed (4)` / `Tests 100 passed (100)`. RUN, never edited: the widening needed no out-of-fence change, unlike #5590 which had to flip a console specimen", "pnpm check:control-bytes — `✅ check-control-bytes: OK (scanned 4767 tracked text file(s); skipped 85 binary).`", "pnpm check:i18n-keys — `Every in-scope call-site key resolves against the en pack (2924 keys) …`", "pnpm check:i18n-drift — `No en value changed in this range.`", "pnpm check:i18n-dead-keys — report-only by its own header, exit 0", "pnpm check:doc-types — `✅ Every documented component type is registered.`", "pnpm check:spec-symbols — `✅ spec symbol derivation: 1294 files scanned against 4966 spec export names`", "pnpm changeset:check — `✅ No changeset declares a 'major' bump.`" ], "declared_narrowing": "pnpm lint is `turbo run lint` over 42 packages with a lint script; I ran @object-ui/components only. Three pieces of evidence, per the narrowing rule: (1) population read from eslint's own config — that package's whole `eslint .`, its entire file set, not a diff-derived subset; (2) file count 396, read from `--format json` output length, not estimated; (3) invariance — eslint.config.js declares no `project`/`projectService` (no type-aware linting), and no rule under eslint-rules/ reads any file other than the one it lints, so a change confined to packages/components cannot move a verdict in another package's run. CI runs the full farm regardless. NOT narrowed anywhere else: the full components suite and every dispatch-named gate ran unrestricted.", "files_changed": [ "packages/components/src/renderers/basic/record-picker.tsx (modified — read sites, prop types, two ComponentMeta inputs arms + descriptions; toText body untouched)", "packages/components/src/renderers/basic/__tests__/record-picker-label-placeholder-i18n.test.tsx (new, 11 cases — new directory, matching existing renderers/action|complex|form/__tests__/, which is the test surface the fence names; #5590 used packages/components/src/__tests__/ instead. Both are collected by the dom project's packages/**/*.test.tsx glob)", ".changeset/record-picker-label-placeholder-i18n-5637.md (new, patch on @object-ui/components)" ], "pm_assumptions_measured": [ "1. MOTHER LANDED — CONFIRMED. fa140b8 is in origin/main = 7138bc1; its diff was read before writing and this change is deliberately the same move (same helper, same default-before-resolution ordering, same declare-in-the-same-change rule, same comment register).", "2. LINE NUMBERS DRIFTED — CONFIRMED and re-derived. The card's `:164` and `:206` are both stale. Current tree before my change: label read at :172, placeholder at :173, row-value toText at :229. Not one card line number is cited in the PR. Two the card gives are still accurate: containers.tsx:569 and :831 still hold the sibling pairing.", "3. THE THREE FIXTURES — CONFIRMED, all three reproduced red before the fix (quoted above) and all three pinned. Assumption 3's framing is exactly right: it is three modes, not one, and the two silent ones are the reason a throw-only fix would be half a fix.", "4. toText IS SHARED — CONFIRMED, not routed around on faith. It has exactly two call sites in this file: the label read (which I moved off it) and `toText(row?.[labelField])` at what is now :229, which renders record FIELD values. Left untouched.", "DRIFT FOUND: the installed spec pin is @objectstack/spec 17.1.0, not the 17.0.0 GA the card and #5590 both state. The union is unchanged on all three keys (measured: optional -> union -> string | record; map safeParse OK on each; label:42 REJECTED), so the premise holds — but the version both documents cite is stale." ], "open_questions": [ { "question": "The Zone-1 ruling says `label` takes the sibling pairing pickLocalized + translateLabel. translateLabel and its KNOWN_LABEL_DICT are module-private to renderers/layout/containers.tsx, which the dispatch fences as hard out-of-scope. The ruling is not re-adjudicable but it is not reachable from inside the fence either. Which gives?", "options": [ "A — Widen the fence for a follow-up patch on this same claim: export translateLabel from containers.tsx and import it in record-picker.tsx. Smallest diff (one keyword + one import), but it adds a basic/ -> layout/ static import between renderer families in a repo that budgets the eager import closure in bytes (scripts/check-eager-closure-budget.mjs). Both are already pulled by the renderers barrel, so the closure likely does not move — but basic/ is separately importable, so the coupling is real for narrower entry points.", "B — Hoist translateLabel + KNOWN_LABEL_DICT into @object-ui/i18n as a shared export, then apply the pairing at all three sites. Architecturally the honest home if the pairing is meant to generalise to author-supplied labels; a larger change across packages/i18n and containers.tsx, and it deserves its own card.", "C — Ship the pickLocalized half (this PR) and re-scope the pairing clause to a separate card, letting this one close on the defect it measured." ], "recommendation": "C, with a measurement PM should weigh before choosing A or B for the follow-up. translateLabel is not a general locale resolver: it is a hardcoded dictionary of well-known TAB tokens (Details, Related, Notes, Files, …) with entries for zh-CN and zh-TW ONLY, and both live call sites translate labels that buildDefaultTabs GENERATED rather than ones an author wrote. Applying it to record_picker.label means the platform silently rewrites an author's literal English string — and only for Chinese viewers. Open card #4645 (pm:on-hold) already records that same dictionary leaving built-in tabs untranslated in ja-JP and es-ES while zh-CN is fine, so extending its reach would widen a surface that is already known-incomplete. None of that re-opens the ruling; it is the input the fence decision needs, and it is why I did not cross the fence on my own judgement." } ], "out_of_scope_findings": [ "filed as #5717: element:text_input's label / placeholder / description are the INVERSE of this card — text-input.tsx:96-98 already resolves all three through pickLocalized, the spec contract for all three is the I18nLabel union (measured on 17.1.0), but the ComponentMeta entries still declare a single 'string' arm, so validateTree reports a `type-mismatch` diagnostic reading `prop \"label\" expected a string` on a legal write the renderer renders correctly. Measured through the same manifestFromConfigs + validateTree path the console specimen test uses. Dedupe-searched twice before filing (no hit); explicitly distinguished from the component-input-union-specimens.test.ts:214 control on element:text_input.defaultValue, whose type-mismatch on a map is CORRECT and must stay. Filed unassigned, unlabelled, unqueued.", "NOT filed — scanned and dropped: other renderers reading a placeholder into a React child position (notably renderers/form/date-picker.tsx:53, which puts `schema.placeholder || 'Pick a date'` inside a span element). Those read a form-field node schema, and I could not establish within bounds that the key's contract admits the I18nLabel union — no form/field spec schema I probed carries a top-level placeholder. Reporting the candidate rather than filing an unmeasured card. Note #4163, the original 'audit every I18nLabel read the compiler cannot see' card, closed as completed on 2026-08-15 while this gap was open and has no open successor; whether to re-open that audit is a triage call, not mine." ], "resource_notes": "Every build/test went through scripts/pm/os-verify-lock.sh (objectstack's; objectui has no copy). Longest hold: 7m03s for the full components suite. Longest wait: 8m09s on the last probe — siblings were busy; waited in-turn with no-lock work in between, never ended a turn on a queue. Worktree removed without --force after the PR opened; node_modules deleted first. No repo-wide scan run locally (the 42-package turbo lint is CI's; see declared_narrowing)." }Generated by Claude Code
os-zhuang commentedon Aug 22, 2026
ACCEPT on the measured defect — PR #5714 enqueued · reviewer of record,
domain:uiseat, round 17Verdict differs from the dev's self-report, deliberately. It filed
status: rework; I am recording ACCEPT on the measured defect, with the ruling's second clause carved out to a decision — because nothing in the delivered work is defective. What is outstanding is a ruled clause that is unreachable from inside the fence, which is a scoping question, not rework.Gates read by name at head⚠️ The PR's first line is
907ec3e64: 19success+ 3skipped(dependabot,Test (coverage), the unexpanded shard placeholder) = 22. Green. Path surface is 3 files (record-picker.tsx, its new test, changeset) — no governed path. Flipped ready, auto-merge SQUASH enabled.Part of #5637, so merging will not close this card — that is correct and deliberate, and the body is left untouched rather than round-tripped through this tooling.Verified against the tree, not the report:
toText's body is byte-unchanged (the only removed line is the read site;toText(row?.[labelField])still serves row values); bothComponentMetaarms widen'string'→['string','object']in the same commit as the renderer change, which is the order the ruling required. The test carries non-vacuity controls in both directions —not.toBeNull()asserted beforetextContent, paired with a control that the element stays absent whenlabelis absent — and asserts the same fixture under two languages, which is what makes an.enpick (the actual bug) impossible to pass.The carve-out: clause 2 is unreachable, and the measurement argues against it
Triage ruled
labeltakes the sibling pairingpickLocalized+translateLabel. ⛔ I am not overruling that, and this comment does not re-adjudicate it. I am reporting that implementing it requires a decision neither triage nor this seat may make, and routing it accordingly.Why it is unreachable:
translateLabelandKNOWN_LABEL_DICTare module-private torenderers/layout/containers.tsx, which this card's fence marks hard out-of-scope. Reaching them needs either an export from a fenced file plus a newbasic/ → layout/static import between renderer families, or a hoist into a shared module across more out-of-fence files. The dev stopped at the fence and escalated instead of crossing it — the correct move.Three measurements that were not available when clause 2 was written:
translateLabelis not a locale resolver. It is a hardcoded dictionary of well-known tab tokens (Details,Related,Notes,Files, …) with entries forzh-CNandzh-TWonly.buildDefaultTabsgenerated. Arecord_picker.labelis author-supplied — so the pairing would mean the platform silently rewrites an author's literal string, and only for Chinese viewers.pm:on-hold) already records this same dictionary leaving built-in tabs untranslated inja-JPandes-ESwhilezh-CNis fine. Extending its reach widens a surface already recorded as incomplete and already parked.The consistency argument behind clause 2 is real — same key family, same read-site treatment. What the measurement shows is that the two sibling sites and this one are not the same case: generated tokens versus authored text.
四维分析(业务角度)
一句话问题:作者在 record picker 上手写了英文 label「Owner」,中文用户应该看到「Owner」,还是平台自作主张把它改写成「负责人」?
containers.tsx导出translateLabel,在 record-picker 里用basic/ → layout/静态导入,而 eager closure 预算目前只剩 1.9% 余量translateLabel+ 词表搬进@object-ui/i18n,三处统一用推荐 C,回退 B(若确实要泛化,先把词表搬进 i18n 并补全语言,再谈应用面)。
本分析看不见什么:真实租户里
record_picker.label的实际写法分布。如果绝大多数作者其实写的是英文单词、并且期待平台本地化,那 A/B 的价值比我估的高 —— 这一点我在仓内测不到。State transition, in the same write as this comment:
pm:dispatched→needs-user-decision, assignee cleared. The delivered work is done and enqueued; what remains on this card is the decision above. ⛔ The card does not close when #5714 merges.Out-of-scope finding verified: #5717 filed —
element:text_inputis the exact inverse of this card (renderer already resolves all three keys throughpickLocalized, butComponentMetastill declares a single'string'arm, sovalidateTreereports a type-mismatch on a legal write the renderer renders correctly). Dedupe-searched twice, and explicitly distinguished from theelement:text_input.defaultValuecontrol whose type-mismatch is correct and must stay.I18nLabelread the compiler cannot see" card — closed as completed on 2026-08-15 while this gap was open, and has no open successor. Two cards in two rounds have now come out of that supposedly-closed audit. Whether to re-open it is triage's call.Drift worth fixing at the source: the installed pin is
@objectstack/spec17.1.0, not the 17.0.0 GA that both this card and #5590 state. The union is unchanged on all three keys, so the premise held — but the version both documents cite is stale.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Maintainer ruling recorded (2026-08-23, live PM chat, verbatim: 「其他同意你的意见」— adopting recommendation C): the
translateLabelpairing clause is ruled NOT APPLICABLE to author-supplied labels. An author's literal string is rendered as written; authors who want localization declare an inline locale map — the capability PR #5714 delivered (merged). The dictionary stays scoped to generated tab tokens; its language-completeness debt remains parked on #4645, whose reach is deliberately NOT widened.Card closes completed (same stroke): the measured defect landed via PR #5714; clause 2 is resolved by this ruling with zero further code change. For the next reader: this closure supersedes the earlier Zone-1 clause-2 wording — do not re-open the pairing for
record_picker.labelwithout a new ruling. The #4163 audit-gap observation (two cards in two rounds out of a closed audit) stays with the triage seat: a third instance triggers a fresh audit card.Generated by Claude Code
fix(components): declare the I18nLabel arm on element:text_input's la…
fix(components): record_picker label/placeholder resolve the inline l…
fix(components): declare the I18nLabel arm on element:text_input's la…