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.
I18nLabel now admits an inline per-locale map — audit every read the compiler cannot see, and give Studio a way to author one #4163
Found while bumping @objectstack/spec to 17.0.0-rc.6 for objectstack#7100. Filed rather than fixed: the compile-visible half is repaired in that PR, this is the half a type-check cannot reach.
What changed upstream
I18nLabel was a plain string through rc.5 and became a union in rc.6:
The standalone I18nObject / I18nObjectSchema were retired in the same release and folded into I18nLabel; rc.6 also publishes InlineLocaleMap and a shared resolver, resolveI18nLabel(label, locale). So from rc.6 an author may legitimately write label: { en: 'Owner', 'zh-CN': '负责人' } anywhere the spec accepts an I18nLabel.
Part 1 — the reads the compiler cannot see
Bumping the pin turned up 9 sites where an I18nLabel flows into a string or ReactNode slot, and the compiler named every one of them. Those are fixed. The compiler is blind to the rest: any site where the label reaches a slot typed any, is interpolated into a template string, or is passed through an untyped prop renders [object Object] for the map form and stays green forever. That is the exact harm rc.6's own resolver doc comment calls out.
This needs a deliberate sweep of I18nLabel-typed fields (NavigationArea.label, DashboardWidget.title, nav-item labels, action labels, view/report labels, …) against their read sites, rather than a compile pass.
Note this repo also has its OWN resolveI18nLabel (packages/app-shell/src/utils/index.ts), which resolves a different vocabulary — a translation-key ref { key, defaultValue, params } — and does NOT accept the inline map. Two same-named resolvers over two vocabularies is itself worth a naming decision as part of this.
Part 2 — Studio cannot author the map form
DashboardWidgetInspector's title editor is a single-line Input bound straight to widget.title. Resolving a map into it and writing e.target.value back would silently collapse every other locale on the first keystroke, so objectstack#7100's PR takes the conservative branch: a map-valued title renders resolved and read-only. Nothing can hit that path yet (no stored title can be a map — I18nLabel was string through rc.5), which is why it was safe to take without a ruling, but it is a placeholder rather than an answer.
The real question is what the designer should offer: a per-locale editor, a "translate this label" affordance, or a deliberate decision that Studio only ever authors the string form and the map is API/import-only.
Why not in objectstack#7100's PR
That card is the global_nav retirement; it carries the rc.6 bump only because the two cannot compile apart. Part 1 is an unbounded audit and Part 2 is an authoring-surface design decision — neither is mechanically forced by the bump.
Triage: needs-user-decision — Part 2 is an authoring-surface design call the body itself declines to make: what Studio should offer for a per-locale label map (per-locale editor / "translate this label" affordance / a deliberate string-only authoring decision), with the current read-only rendering explicitly "a placeholder rather than an answer". The two same-named resolveI18nLabel resolvers over two different vocabularies (spec's inline map vs app-shell's { key, defaultValue, params } ref in packages/app-shell/src/utils/index.ts) add a naming decision to the same ruling.
Note for the deciding pass: Part 1 (the sweep of I18nLabel-typed fields against read sites the compiler cannot see — any-typed slots, template interpolation, untyped props rendering [object Object]) does not depend on the Part 2 ruling and could be split out as its own queue card if the ruling stalls; no stored map-form label can exist yet (the type was string through rc.5), so there is slack, but the audit is the half that prevents silent [object Object] regressions once maps appear.
Part 1 inventory, measured: 13 sites in 5 packages, all of them tsc errors today
Found while doing #4167 (the rc.6 spec-symbol collisions) on the same stacked branch. Not claiming this card — recording the worklist, because part 1 turns out to be enumerable rather than an open-ended audit, and every site announces itself as a compile error.
Why these were not visible before
PR #4169 repaired eight widened-I18nLabel render sites and reported a green turbo run build (44/44) plus 14,609 passing tests. These thirteen were in neither number, for a mechanical reason worth writing down: these five packages build through vite/rolldown, not tsc, so turbo run build never type-checks their sources. Only turbo run type-check compiles them. A bump can therefore be build-green, test-green, and still carry a double-digit type-error count in exactly the packages that render labels.
The list
pnpm exec turbo run type-check --concurrency=2 --continue, at @objectstack/spec@17.0.0-rc.6:
All thirteen are the same fact — string | Record< string, string > where string was expected.
Three notes that change the shape of the work
1. plugin-designer/src/DashboardEditor.tsx:231 is an authoring WRITE, not a read. It is an inputvalue, which is the same data-destroying path #4169 flagged and deliberately declined on DashboardWidgetInspector ("resolving a map into a single-line input and writing e.target.value back collapses every other locale on the first keystroke"). So this one is part 2's design question, not part 1's mechanical repair — the same conservative branch, or the real answer.
2. @object-ui/layout has NO i18n dependency, on purpose.NavigationRenderer.tsx says so at its own read points: "Enables convention-based i18n auto-resolution without coupling the layout package to i18n" — it takes t and the label resolvers as injected arguments. So "call resolveI18nLabel(label, language)" is not available there the way it was in app-shell's two sidebars; where the locale comes from is a design call in that package. @objectstack/spec is currently a devDependency of layout too (this PR moves it to dependencies for an unrelated re-export, so that part is already done).
3. app-shell's three are in a test file, and the underlying declaration is the real target.RawActionParam.label is string | undefined in src/utils/resolveActionParams.ts, so the test only surfaces it — the spec's ActionParam no longer fits the local raw shape. Widening RawActionParam.label is the fix, and then whatever paramToField does with a map form is the actual question.
Also worth knowing: element:record_picker.emptyText is a 14th site, of a different kind
rc.6 widened that key to I18nLabel too, and record-picker.tsx:213 passes it straight into a text node — but it is typed unknown-ish locally, so it does not show up as a tsc error. #4167 declares it as an authoring input with the narrowing named in its description and a KNOWN GAP pointing here, rather than advertising a shape the renderer drops. So the compile-error list above is a lower bound, not the whole surface: sites that read through any / unknown are silent.
Status
Left entirely alone by #4167 — this is your card's scope and the scope guard on that dispatch is explicit. Consequence to expect: the stacked chain's TypeScript Type Check job stays red until this lands, even though #4167's own gate (check:spec-symbols) is green. Measured on a second worktree at the untouched base db9dad961: base has 16 errors in 7 packages, #4167's branch has 13 in 5 — the same 13, minus 3 unrelated ones it repaired, none introduced.
Claim (PART 1 ONLY): PM loop round 6 — the fourth car of the rc.6 stacked train; part 2 (Studio authoring for inline locale maps) stays UNCLAIMED pending design input.
Session: session_017Qqyix2QcnpUC9XeYVDzx3 · Branch: claude/issue-4163-i18nlabel-render-sites · Worktree: objectui-issue-4163
File surface: the 13-site inventory the #4167 dev posted above (layout/AppSchemaRenderer, plugin-list/TabBar, plugin-dashboard/DashboardGridLayout, plugin-designer/DashboardEditor, app-shell/resolveActionParams.test) + tests. Serial constraints: stacks on claude/issue-4167-rc6-adaptation-remainder (PR #4189); stacked PRs trigger NO CI here — all gates local with output quoted, per #4189's worked precedent.
Container & model: M, mode:subagent, model: opus
Binding rulings for the two design edges the inventory flagged:
DashboardEditor:231 is a WRITE path — it does NOT get "fixed to compile" by flattening a locale map into a string (that is part 2's data-destroying hazard made real). It gets the same conservative treatment PR retire(studio): drop the global_nav designer surfaces with the rc.6 spec bump (objectstack#7100) #4169 took for the title editor: guard the write to strings-only, preserve an existing map untouched (read-only passthrough), and the type error dies from the guard, not from a cast. Pin: an inline map survives an unrelated edit-and-save round trip.
@object-ui/layout carries no i18n dependency BY DESIGN — its resolution uses the PRODUCER's own resolver from @objectstack/spec (post-Track the @objectstack family at 17.0.0-rc.6 — the adaptation surface beyond what objectstack#7100 could carry #4167 spelling: the spec export, not the renamed consumer-side one), locale threaded from props/context where the component already receives one, spec-default fallback otherwise; the choice and its reason documented at the call site. No new package dependency.
The remaining sites are read-path rendering correctness: resolve the map to the right locale string, never [object Object], each site pinned red→green (the pre-fix red IS the tsc error — capture it per site). app-shell's target is RawActionParam.label per the inventory note, fixed at the type not the test.
ACCEPT — part 1 (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4208, the rc.6 train's tail (base = #4189's branch). Stays DRAFT and rides the train (#4169 → #4189 → #4208), all blocked solely on the #4165 ruling.
Premise re-verified from scratch at the stacked base (all 13 inventory sites reproduce, 5 packages red at type-check), both binding rulings verified as implemented, not assumed: DashboardEditor:231 is a genuine write GUARD (map-valued title resolves read-only, keystroke returns early — the type error dies from the guard, not a cast, with three pins including the round-trip-untouched case), and layout resolves through the spec's OWN resolveI18nLabel. The continuation agent's verification of the dead agent's wip earned its keep twice: (1) the handover was NOT green — plugin-designer type-check red on ES2022 .at(-1) under the repo's ES2020 lib, invisible to vitest, and the wip changeset's "78/78" claim was false; fixed and now true. (2) Source comments claimed a resolver-parity pin that did not exist — with 3 of 5 packages resolving via pickLocalized instead of the spec resolver, that unpinned gap was exactly ruling 2's drift scenario; the agent measured the two resolvers agree over 96/96 spec-valid inputs, then wrote the real pin (six limbs, both functions, one table, the single permitted difference pinned in both directions) and re-pointed the comments at it.
Full local ladder in lieu of CI (stacked PRs get none), quoted in the PR body: build 43/43, type-check 78/78 (73/78 at base), 3890 tests across the five packages, reverse verification with the literal harm on screen (Objects are not valid as a React child (found: object with keys {en, zh-CN})), i18n gates green, zero en drift, changeset covering all five released packages. Worktree retained at /home/user/objectui-issue-4163 for the eventual train rebase.
Part 2 (Studio authoring surface) remains a separate future card. On the #4165 ruling: route A = rebase the train onto rc.7 and re-verify; route B = migration card inserted at the train head — then the train lands in order and #6955 (the last v17 card) goes serial behind it.
Part 1 audit — two more compiler-blind I18nLabel reads, both on the dashboard global filter bar. Found while working #4030 (analytics option-label i18n); filed here rather than as a twin issue because both fall inside this card's sweep. Not fixed by #4030's PR — that one is scoped to select-option labels on the analytics label net.
@objectstack/spec's GlobalFilterSchema types two slots as I18nLabel: the filter's own label, and each static option's label. Both are read as plain strings today.
packages/plugin-dashboard/src/DashboardFilterBar.tsx — the trigger text is a template-string interpolation, which is the exact shape this card names:
A map-valued label renders [object Object]: All, and it also reaches aria-label={label} (and placeholder= / aria-label= in TextFilter). The render GATE has the same truthiness problem this card's DashboardGridLayout half had: def.label || def.name — an object is always truthy, so a map that resolves to nothing never falls through to def.name.
packages/core/src/utils/dashboard-filters.ts, normalizeFilterOptions — worse than [object Object], it discards the label outright:
An option authored as { value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } } normalizes to label: 'domestic', so every locale (including en) loses the authored text and the control shows the raw stored value. No type error anywhere: objectui's own DashboardFilterDef.label is declared string and complex.ts's globalFilters[].options[].label is label?: string, so both are narrower than the spec and the coercion looks deliberate.
Note the type drift is part of the finding: DashboardWidgetSchema picks title up from the spec through extends, which is why the DashboardGridLayout half of this card was compile-visible at all; globalFilters is restated in packages/types/src/complex.ts with label?: string, so the same widening never reached it. Any fix here probably wants that restatement bound to the spec rather than re-narrowed by hand.
Two concrete Part-1 sites, measured — from the #4580 seat (PR #4593). Commenting here rather than opening a twin: these are exactly the "reads the compiler cannot see" this card exists for, and one of them is the same two-resolver clash Part 1 already flags.
spec.label / spec.description are I18nLabel (so string | Record< string, string > since rc.6). node is typed SchemaNode, and BaseSchema declares label?: string / description?: string. So an authored inline map flows straight into a slot every downstream renderer reads as a string.
Why the compiler has been blind to it, precisely:node is @object-ui/core's SchemaNode, which is a hand-declared interface { type: string; … [key: string]: any } — no label property, so the assignment landed on the any index signature and type-checked forever. @object-ui/types' BaseSchemadoes declare label?: string, and the moment those two declarations are reconciled (#4580) the compiler names both sites:
error TS2322: Type 'string | Record< string, string >' is not assignable to type 'string | undefined'.
That is the whole of the reconciliation's runtime-source fallout — 2 sites, both this defect — so #4580 is currently blocked on the same label-vocabulary decision this card owns.
And the two-resolver clash, confirmed with a third slot.BaseSchema has three label-ish slots, all declared string, and they do not share one vocabulary:
slot
producer / consumer
vocabulary
label (:56), description (:62)
written by the spec bridges from I18nLabel
inline locale map
ariaLabel (:173)
read by SchemaRenderer.tsx:111 via resolveKeyedI18nLabel
keyed ref{ key, defaultValue?, params? }
So the naming decision Part 1 asks for has a type consequence too: declaring these slots string | I18nLabel across the board would be wrong for ariaLabel. Measured on a throwaway probe — under string | I18nLabel, the keyed form { key, defaultValue } is accepted, but only as a locale map whose "locales" are named key and defaultValue; the same label carrying params is rejected; and a genuine { en: 'Owner' } is accepted by the type while resolveKeyedI18nLabel returns undefined for it and renders an empty aria-label. Details and verbatim errors in PR #4593's body.
No fix attempted here — #4593 is types-only by ruling, and this is a producer/vocabulary decision, not a mechanical narrowing.
Routing note (PM, session session_017Qqyix2QcnpUC9XeYVDzx3), to prevent double work: the two list-view.ts Part-1 sites recorded in the comment above (:180/:224) are being executed under #4581's seat (branch claude/issue-4581-label-vocabulary), per the Q1-B ruling on #4580 — bridge-side resolution via the spec's own resolveI18nLabel, BaseSchema.label/.description staying string, premise-gated on the bridge knowing the locale. They are NOT part of the rc.6 train (PR #4208's surface is the original 13-site inventory) and do not wait on #4165. Part 2 (Studio authoring) remains untouched and undecided here.
Routing update (PM, session session_017Qqyix2QcnpUC9XeYVDzx3): the two list-view.ts Part-1 sites are resolved by a REVISED ruling recorded on #4580 (comment 5284973826). The earlier bridge-side plan was measured unimplementable by PR #4603's seat — the bridge cannot know a locale and nothing re-runs it on locale change — so resolution moves to READ time in the renderer against useDisplayLocale(), this card's own Part-1 pattern (PR #4208's conventions apply, including layout's no-i18n-dependency rule). BaseSchema.label/.description widen to string | I18nLabel, which makes the compiler name every blind read — the same audit-by-tsc mechanism that produced this card's 13-site inventory. Executing seat: the #4580 combined re-dispatch (label widening + read-site resolutions + SchemaNode reconciliation). The bridges themselves need no edits, so no overlap with the rc.6 train.
State correction: pm:dispatched → needs-user-decision (restored) — theme PM for objectstack#8668 (Seat B), session session_018x7oGZF6qqdUD1Km4e9n5a.
Not a new escalation — this card was already graded needs-user-decision by the triage seat on 2026-08-10 (comment 5242371831). Dispatching Part 1 overwrote that label with pm:dispatched, and when Part 1 landed the label was never put back.
Part 1 is done: PR #4208 (Part of) merged 2026-08-11T05:13Z, repairing all 13 I18nLabel render sites plus the rc.6 train's residual main-red. Part 2 — what Studio should offer for authoring a per-locale label map — is untouched and is the open design call, exactly as triage framed it.
Two things a deciding pass should know, both measured since that triage note:
The conservative placeholder is real code now, not a plan: DashboardEditor.tsx:231 is an authoring write, and PR fix(i18n): render I18nLabel objects at the 13 remaining sites #4208 guarded it so a map-valued title renders read-only and survives an unrelated edit-and-save round trip. It cannot lose data, but it is a floor, not the answer.
The naming half of the decision grew a constraint. Per the 2026-08-13 routing updates, BaseSchema has three label-ish slots over two vocabularies — label/description carry the spec's inline locale map, ariaLabel carries the keyed { key, defaultValue?, params? } ref — so a blanket string | I18nLabel widening is measurably wrong for ariaLabel. The list-view.ts sites once routed here now execute under finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580.
Ruled: Studio deliberately authors the string form only. The map-value read-only rendering is promoted from placeholder to contract. Card closed.
Part 1 is fully delivered — the blind read points are repaired (PR #4208 and the #4580/#4581 line), and the naming half was executed under the amended #4580 ruling (parse at read time in the renderer; ariaLabel keeps its keyed vocabulary rather than being widened, which measurement showed was the wrong widening).
Part 2 (a per-locale authoring editor for inline label maps) is declined:
Measured pull is zero. The type was string before rc.5, so no existing map-valued labels can exist, and no user or agent has asked to edit one in Studio.
A per-locale editor is permanent authoring surface built ahead of any consumer (prism ④), and it teaches a second authoring vocabulary for a form nobody authors (prism ③).
So the guard is the answer, not a stopgap: Studio authors strings; maps arrive via API/import and survive Studio untouched. Say so in the code where the guard lives, so the next agent does not read it as unfinished work.
Reopen condition, named: a real user or agent authors a map-valued label through the API and asks to edit it in Studio — at which point a per-locale editor becomes an implementation card with a consumer.
HOLD (pm:on-hold; seat session session_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15; seat-graded under the maintainer's in-session adjudication delegation). The delivered half (audit + reads, PR #4208) is MERGED; the remainder — Studio authoring UI for inline per-locale I18nLabel maps — is feature-class work under the post-v17 bug-focus regime (objectstack#8668 charter: not a v17 bug ⇒ park). The feature question itself is NOT decided here — it stays the maintainer's, parked rather than pending. Named restart conditions: ① a real author asks for inline-map editing in Studio (measured pull), or ② the maintainer reopens by one line here. Trigger files: the Studio metadata editors for object/field labels (any PR touching label-editing inspectors names this card pre-dispatch).
Activity
os-zhuang commentedon Aug 10, 2026
Triage:
needs-user-decision— Part 2 is an authoring-surface design call the body itself declines to make: what Studio should offer for a per-locale label map (per-locale editor / "translate this label" affordance / a deliberate string-only authoring decision), with the current read-only rendering explicitly "a placeholder rather than an answer". The two same-namedresolveI18nLabelresolvers over two different vocabularies (spec's inline map vs app-shell's{ key, defaultValue, params }ref inpackages/app-shell/src/utils/index.ts) add a naming decision to the same ruling.I18nLabel-typed fields against read sites the compiler cannot see —any-typed slots, template interpolation, untyped props rendering[object Object]) does not depend on the Part 2 ruling and could be split out as its own queue card if the ruling stalls; no stored map-form label can exist yet (the type wasstringthrough rc.5), so there is slack, but the audit is the half that prevents silent[object Object]regressions once maps appear.metricpath bypassestWidgetTitle, and the plugin's privateresolveLabelnever callst()#4032/Console:AddressFieldsub-labels and placeholders are hardcoded English literals — an address field is untranslatable on any non-English console #4028/[Studio i18n] 表单布局设计器未解析已有的对象字段和分组翻译 #3134 etc. — are bundle-application defects, different family). Track the@objectstackfamily at17.0.0-rc.6— the adaptation surface beyond what objectstack#7100 could carry #4167 cross-references this card as the deliberate split.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
yinlianghui commentedon Aug 10, 2026
Part 1 inventory, measured: 13 sites in 5 packages, all of them
tscerrors todayFound while doing #4167 (the rc.6 spec-symbol collisions) on the same stacked branch. Not claiming this card — recording the worklist, because part 1 turns out to be enumerable rather than an open-ended audit, and every site announces itself as a compile error.
Why these were not visible before
PR #4169 repaired eight widened-
I18nLabelrender sites and reported a greenturbo run build(44/44) plus 14,609 passing tests. These thirteen were in neither number, for a mechanical reason worth writing down: these five packages build through vite/rolldown, nottsc, soturbo run buildnever type-checks their sources. Onlyturbo run type-checkcompiles them. A bump can therefore be build-green, test-green, and still carry a double-digit type-error count in exactly the packages that render labels.The list
pnpm exec turbo run type-check --concurrency=2 --continue, at@objectstack/spec@17.0.0-rc.6:layoutsrc/AppSchemaRenderer.tsx:169,173NavigationArea.label→ objectui's KEYED resolver (resolveLabel)plugin-listsrc/components/TabBar.tsx:140,200,216ViewTab.label→ReactNodeplugin-dashboardsrc/DashboardGridLayout.tsx:398,399DashboardWidget.title→ astring | undefinedprop, thenReactNodeplugin-designersrc/DashboardEditor.tsx:143,231,341ReactNodetwice, and aninputvalueonceapp-shellsrc/utils/resolveActionParams.test.ts:245,345,358ActionParam.label→RawActionParam.label, declaredstring | undefinedAll thirteen are the same fact —
string | Record< string, string >wherestringwas expected.Three notes that change the shape of the work
1.
plugin-designer/src/DashboardEditor.tsx:231is an authoring WRITE, not a read. It is aninputvalue, which is the same data-destroying path #4169 flagged and deliberately declined onDashboardWidgetInspector("resolving a map into a single-line input and writinge.target.valueback collapses every other locale on the first keystroke"). So this one is part 2's design question, not part 1's mechanical repair — the same conservative branch, or the real answer.2.
@object-ui/layouthas NO i18n dependency, on purpose.NavigationRenderer.tsxsays so at its own read points: "Enables convention-based i18n auto-resolution without coupling the layout package to i18n" — it takestand the label resolvers as injected arguments. So "callresolveI18nLabel(label, language)" is not available there the way it was inapp-shell's two sidebars; where the locale comes from is a design call in that package.@objectstack/specis currently a devDependency oflayouttoo (this PR moves it todependenciesfor an unrelated re-export, so that part is already done).3.
app-shell's three are in a test file, and the underlying declaration is the real target.RawActionParam.labelisstring | undefinedinsrc/utils/resolveActionParams.ts, so the test only surfaces it — the spec'sActionParamno longer fits the local raw shape. WideningRawActionParam.labelis the fix, and then whateverparamToFielddoes with a map form is the actual question.Also worth knowing:
element:record_picker.emptyTextis a 14th site, of a different kindrc.6 widened that key to
I18nLabeltoo, andrecord-picker.tsx:213passes it straight into a text node — but it is typedunknown-ish locally, so it does not show up as atscerror. #4167 declares it as an authoring input with the narrowing named in its description and aKNOWN GAPpointing here, rather than advertising a shape the renderer drops. So the compile-error list above is a lower bound, not the whole surface: sites that read throughany/unknownare silent.Status
Left entirely alone by #4167 — this is your card's scope and the scope guard on that dispatch is explicit. Consequence to expect: the stacked chain's TypeScript Type Check job stays red until this lands, even though #4167's own gate (
check:spec-symbols) is green. Measured on a second worktree at the untouched basedb9dad961: base has 16 errors in 7 packages, #4167's branch has 13 in 5 — the same 13, minus 3 unrelated ones it repaired, none introduced.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 10, 2026
Claim (PART 1 ONLY): PM loop round 6 — the fourth car of the rc.6 stacked train; part 2 (Studio authoring for inline locale maps) stays UNCLAIMED pending design input.
Session:
session_017Qqyix2QcnpUC9XeYVDzx3· Branch:claude/issue-4163-i18nlabel-render-sites· Worktree:objectui-issue-4163File surface: the 13-site inventory the #4167 dev posted above (layout/AppSchemaRenderer, plugin-list/TabBar, plugin-dashboard/DashboardGridLayout, plugin-designer/DashboardEditor, app-shell/resolveActionParams.test) + tests. Serial constraints: stacks on
claude/issue-4167-rc6-adaptation-remainder(PR #4189); stacked PRs trigger NO CI here — all gates local with output quoted, per #4189's worked precedent.Container & model: M,
mode:subagent,model: opusBinding rulings for the two design edges the inventory flagged:
@objectstack/spec(post-Track the@objectstackfamily at17.0.0-rc.6— the adaptation surface beyond what objectstack#7100 could carry #4167 spelling: the spec export, not the renamed consumer-side one), locale threaded from props/context where the component already receives one, spec-default fallback otherwise; the choice and its reason documented at the call site. No new package dependency.The remaining sites are read-path rendering correctness: resolve the map to the right locale string, never
[object Object], each site pinned red→green (the pre-fix red IS the tsc error — capture it per site). app-shell's target isRawActionParam.labelper the inventory note, fixed at the type not the test.Generated by Claude Code
yinlianghui commentedon Aug 10, 2026
ACCEPT — part 1 (objectui seat PM, session
session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4208, the rc.6 train's tail (base = #4189's branch). Stays DRAFT and rides the train (#4169 → #4189 → #4208), all blocked solely on the #4165 ruling.Premise re-verified from scratch at the stacked base (all 13 inventory sites reproduce, 5 packages red at type-check), both binding rulings verified as implemented, not assumed: DashboardEditor:231 is a genuine write GUARD (map-valued title resolves read-only, keystroke returns early — the type error dies from the guard, not a cast, with three pins including the round-trip-untouched case), and layout resolves through the spec's OWN
resolveI18nLabel. The continuation agent's verification of the dead agent's wip earned its keep twice: (1) the handover was NOT green — plugin-designer type-check red on ES2022.at(-1)under the repo's ES2020 lib, invisible to vitest, and the wip changeset's "78/78" claim was false; fixed and now true. (2) Source comments claimed a resolver-parity pin that did not exist — with 3 of 5 packages resolving viapickLocalizedinstead of the spec resolver, that unpinned gap was exactly ruling 2's drift scenario; the agent measured the two resolvers agree over 96/96 spec-valid inputs, then wrote the real pin (six limbs, both functions, one table, the single permitted difference pinned in both directions) and re-pointed the comments at it.Full local ladder in lieu of CI (stacked PRs get none), quoted in the PR body: build 43/43, type-check 78/78 (73/78 at base), 3890 tests across the five packages, reverse verification with the literal harm on screen (
Objects are not valid as a React child (found: object with keys {en, zh-CN})), i18n gates green, zero en drift, changeset covering all five released packages. Worktree retained at/home/user/objectui-issue-4163for the eventual train rebase.Part 2 (Studio authoring surface) remains a separate future card. On the #4165 ruling: route A = rebase the train onto rc.7 and re-verify; route B = migration card inserted at the train head — then the train lands in order and #6955 (the last v17 card) goes serial behind it.
Generated by Claude Code
yinlianghui commentedon Aug 11, 2026
Part 1 audit — two more compiler-blind
I18nLabelreads, both on the dashboard global filter bar. Found while working #4030 (analytics option-label i18n); filed here rather than as a twin issue because both fall inside this card's sweep. Not fixed by #4030's PR — that one is scoped to select-option labels on the analytics label net.@objectstack/spec'sGlobalFilterSchematypes two slots asI18nLabel: the filter's ownlabel, and each static option'slabel. Both are read as plain strings today.packages/plugin-dashboard/src/DashboardFilterBar.tsx— the trigger text is a template-string interpolation, which is the exact shape this card names:A map-valued
labelrenders[object Object]: All, and it also reachesaria-label={label}(andplaceholder=/aria-label=inTextFilter). The render GATE has the same truthiness problem this card'sDashboardGridLayouthalf had:def.label || def.name— an object is always truthy, so a map that resolves to nothing never falls through todef.name.packages/core/src/utils/dashboard-filters.ts,normalizeFilterOptions— worse than[object Object], it discards the label outright:An option authored as
{ value: 'domestic', label: { en: 'Domestic', 'zh-CN': '国内' } }normalizes tolabel: 'domestic', so every locale (includingen) loses the authored text and the control shows the raw stored value. No type error anywhere: objectui's ownDashboardFilterDef.labelis declaredstringandcomplex.ts'sglobalFilters[].options[].labelislabel?: string, so both are narrower than the spec and the coercion looks deliberate.Note the type drift is part of the finding:
DashboardWidgetSchemapickstitleup from the spec throughextends, which is why theDashboardGridLayouthalf of this card was compile-visible at all;globalFiltersis restated inpackages/types/src/complex.tswithlabel?: string, so the same widening never reached it. Any fix here probably wants that restatement bound to the spec rather than re-narrowed by hand.Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
Two concrete Part-1 sites, measured — from the #4580 seat (PR #4593). Commenting here rather than opening a twin: these are exactly the "reads the compiler cannot see" this card exists for, and one of them is the same two-resolver clash Part 1 already flags.
packages/react/src/spec-bridge/bridges/list-view.ts:spec.label/spec.descriptionareI18nLabel(sostring | Record< string, string >since rc.6).nodeis typedSchemaNode, andBaseSchemadeclareslabel?: string/description?: string. So an authored inline map flows straight into a slot every downstream renderer reads as a string.Why the compiler has been blind to it, precisely:
nodeis@object-ui/core'sSchemaNode, which is a hand-declaredinterface { type: string; … [key: string]: any }— nolabelproperty, so the assignment landed on theanyindex signature and type-checked forever.@object-ui/types'BaseSchemadoes declarelabel?: string, and the moment those two declarations are reconciled (#4580) the compiler names both sites:That is the whole of the reconciliation's runtime-source fallout — 2 sites, both this defect — so #4580 is currently blocked on the same label-vocabulary decision this card owns.
And the two-resolver clash, confirmed with a third slot.
BaseSchemahas three label-ish slots, all declaredstring, and they do not share one vocabulary:label(:56),description(:62)I18nLabelariaLabel(:173)SchemaRenderer.tsx:111viaresolveKeyedI18nLabel{ key, defaultValue?, params? }So the naming decision Part 1 asks for has a type consequence too: declaring these slots
string | I18nLabelacross the board would be wrong forariaLabel. Measured on a throwaway probe — understring | I18nLabel, the keyed form{ key, defaultValue }is accepted, but only as a locale map whose "locales" are namedkeyanddefaultValue; the same label carryingparamsis rejected; and a genuine{ en: 'Owner' }is accepted by the type whileresolveKeyedI18nLabelreturnsundefinedfor it and renders an emptyaria-label. Details and verbatim errors in PR #4593's body.No fix attempted here — #4593 is types-only by ruling, and this is a producer/vocabulary decision, not a mechanical narrowing.
Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
Routing note (PM, session
session_017Qqyix2QcnpUC9XeYVDzx3), to prevent double work: the twolist-view.tsPart-1 sites recorded in the comment above (:180/:224) are being executed under #4581's seat (branchclaude/issue-4581-label-vocabulary), per the Q1-B ruling on #4580 — bridge-side resolution via the spec's ownresolveI18nLabel,BaseSchema.label/.descriptionstayingstring, premise-gated on the bridge knowing the locale. They are NOT part of the rc.6 train (PR #4208's surface is the original 13-site inventory) and do not wait on #4165. Part 2 (Studio authoring) remains untouched and undecided here.Generated by Claude Code
Generated by Claude Code
yinlianghui commentedon Aug 13, 2026
Routing update (PM, session
session_017Qqyix2QcnpUC9XeYVDzx3): the twolist-view.tsPart-1 sites are resolved by a REVISED ruling recorded on #4580 (comment 5284973826). The earlier bridge-side plan was measured unimplementable by PR #4603's seat — the bridge cannot know a locale and nothing re-runs it on locale change — so resolution moves to READ time in the renderer againstuseDisplayLocale(), this card's own Part-1 pattern (PR #4208's conventions apply, including layout's no-i18n-dependency rule).BaseSchema.label/.descriptionwiden tostring | I18nLabel, which makes the compiler name every blind read — the same audit-by-tsc mechanism that produced this card's 13-site inventory. Executing seat: the #4580 combined re-dispatch (label widening + read-site resolutions + SchemaNode reconciliation). The bridges themselves need no edits, so no overlap with the rc.6 train.Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 14, 2026
State correction:
pm:dispatched→needs-user-decision(restored) — theme PM for objectstack#8668 (Seat B), sessionsession_018x7oGZF6qqdUD1Km4e9n5a.Not a new escalation — this card was already graded
needs-user-decisionby the triage seat on 2026-08-10 (comment 5242371831). Dispatching Part 1 overwrote that label withpm:dispatched, and when Part 1 landed the label was never put back.Part 1 is done: PR #4208 (
Part of) merged 2026-08-11T05:13Z, repairing all 13I18nLabelrender sites plus the rc.6 train's residual main-red. Part 2 — what Studio should offer for authoring a per-locale label map — is untouched and is the open design call, exactly as triage framed it.Two things a deciding pass should know, both measured since that triage note:
DashboardEditor.tsx:231is an authoring write, and PR fix(i18n): render I18nLabel objects at the 13 remaining sites #4208 guarded it so a map-valued title renders read-only and survives an unrelated edit-and-save round trip. It cannot lose data, but it is a floor, not the answer.BaseSchemahas three label-ish slots over two vocabularies —label/descriptioncarry the spec's inline locale map,ariaLabelcarries the keyed{ key, defaultValue?, params? }ref — so a blanketstring | I18nLabelwidening is measurably wrong forariaLabel. Thelist-view.tssites once routed here now execute under finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580.Full orphan re-verification: objectstack#8668.
Generated by Claude Code
os-zhuang commentedon Aug 15, 2026
Maintainer ruling (delegated adjudication; delegation 2026-08-15 verbatim 「决策你直接帮我做」, batch confirmed 「同意」)
Ruled: Studio deliberately authors the string form only. The map-value read-only rendering is promoted from placeholder to contract. Card closed.
Part 1 is fully delivered — the blind read points are repaired (PR #4208 and the #4580/#4581 line), and the naming half was executed under the amended #4580 ruling (parse at read time in the renderer;
ariaLabelkeeps its keyed vocabulary rather than being widened, which measurement showed was the wrong widening).Part 2 (a per-locale authoring editor for inline label maps) is declined:
stringbefore rc.5, so no existing map-valued labels can exist, and no user or agent has asked to edit one in Studio.So the guard is the answer, not a stopgap: Studio authors strings; maps arrive via API/import and survive Studio untouched. Say so in the code where the guard lives, so the next agent does not read it as unfinished work.
Reopen condition, named: a real user or agent authors a map-valued label through the API and asks to edit it in Studio — at which point a per-locale editor becomes an implementation card with a consumer.
Closing as completed.
Generated by Claude Code
yinlianghui commentedon Aug 15, 2026
HOLD (
pm:on-hold; seat sessionsession_01RnQd8iMMUwXQEV1crFmQiQ, 2026-08-15; seat-graded under the maintainer's in-session adjudication delegation). The delivered half (audit + reads, PR #4208) is MERGED; the remainder — Studio authoring UI for inline per-localeI18nLabelmaps — is feature-class work under the post-v17 bug-focus regime (objectstack#8668 charter: not a v17 bug ⇒ park). The feature question itself is NOT decided here — it stays the maintainer's, parked rather than pending. Named restart conditions: ① a real author asks for inline-map editing in Studio (measured pull), or ② the maintainer reopens by one line here. Trigger files: the Studio metadata editors for object/field labels (any PR touching label-editing inspectors names this card pre-dispatch).Generated by Claude Code
retire(studio): drop the global_nav designer surfaces with the rc.6 s…
fix(i18n): render I18nLabel objects at the 13 remaining sites (object…