Repository navigation
plugin-dashboard: ObjectMetricWidgetProps still declares the RETIRED { key, defaultValue } label form — a TS consumer writing the declared shape renders the raw dotted i18n key as the visible label #5264
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorAuthorMore actionsTriage:
pm:queue· Bug — a public exported props interface still invites the rc.6-retired{ key, defaultValue }label form (objectstack#5055), and the runtime paints the raw dotted key with property-order-dependent behavior; the siblingMetricWidgetPropswas already migrated for exactly this reason (#4358). Fix: migrate the four members tostring | I18nLabel, plus the same-pass fixture sweep the card names (#4032 fixture-triage discipline: a fixture pinning the retired branch keeps passing precisely because nothing produces it any more).
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 13
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5264-object-metric-label-i18n
Worktree:objectui-issue-5264
Domain:repo:objectui(plugin-dashboard)
File surface:packages/plugin-dashboard/src/**—ObjectMetricWidgetProps+ the fixtures the same-pass sweep names (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Clause-②: yes — content limb. Migrating the four members from the retired{ key, defaultValue }form tostring | I18nLabelchanges a published props interface's accept set. It narrows toward an already-retired contract (objectstack#5055 retired that form at rc.6) rather than widening, and the siblingMetricWidgetPropswas migrated for exactly this reason in #4358 — but it is declared here rather than argued away.
Serial constraints cleared:packages/plugin-dashboardwas released minutes ago. It was held all round by the parked PR #5207 (#5064) — that PR merged at 07:10Z after its changeset was correctedmajor→minorper the maintainer's 2026-08-19 ruling. No open PR and no in-flight claim now touches this package. Sibling #5280 is dispatched in the same batch and writes onlysrc/__tests__/DatasetWidget.test.tsx— disjoint from this card's surface, recorded here per the serialization discipline.⚠️ PR #5207 just re-keyeddashboardComponentsinsrc/index.tsx(11 PascalCase class names → 8 schematypekeys). Mergemainbefore opening the PR so you are working against that, and ⛔ do not carry any line number from this card — the package moved minutes ago.Triage's fix shape is binding and carried into the order: migrate the four members to
string | I18nLabel, plus the same-pass fixture sweep — per #4032's fixture-triage discipline, a fixture pinning the retired branch keeps passing precisely because nothing produces it any more, so a green fixture is not evidence the branch is live.
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 os-support-ai commented
on Aug 19, 2026 CollaboratorAuthorMore actions{ "issue": 5264, "status": "done", "branch": "claude/issue-5264-object-metric-label-i18n", "pr": "https://github.com/objectstack-ai/objectui/pull/5303", "premise_still_valid": true, "summary": "Migrated the four ObjectMetricWidgetProps members (label, trend.label, description, title) from the rc.6-retired `{ key, defaultValue }` form to `string | I18nLabel`, matching the sibling MetricWidgetProps migration (#4358) verbatim. Also fixed the members' only reader in the same file: `drawerTitle` destructured `title?.defaultValue` / `label?.defaultValue`, a limb an inline locale map does not have, so an authored drill-down title resolved to '' and the panel silently fell back to the literal word 'Details' — both reads now go through `pickLocalized` against the same UI language the tile uses. CORRECTION TO THE CARD'S CLAUSE-② FRAMING, measured with tsc: this is NOT a narrowing. `I18nLabel`'s object half erases to `Record<string,string>` in the emitted .d.ts (the BCP-47 key regex is a Zod runtime refinement that does not survive into the type), so `{ key, defaultValue }` REMAINS structurally assignable — nothing stops type-checking. What starts type-checking is the inline per-locale map, which the old declaration rejected with TS2353; a consumer writing the only vocabulary the spec admits previously could not compile. The retired form is refused where refusal is expressible: I18nLabelSchema rejects it at authoring time in both property orders, pinned in the tests. #5207's re-key touched a different map and none of these four members; main is merged.", "tests": "All at 2c64c75f2 (branch head + merge commit). `pnpm exec vitest run packages/plugin-dashboard` from the repo root (per #3378): 'Test Files 65 passed (65) / Tests 590 passed (590)', including 12 new in src/__tests__/ObjectMetricWidget.i18nLabel.test.tsx. `pnpm --filter @object-ui/plugin-dashboard run type-check`: clean, and the output echoes `tsc --noEmit && tsc -p tsconfig.test.json` so the hyphenated script really ran rather than matching zero scripts. lint: 0 errors, 317 warnings, all pre-existing no-explicit-any (my files add none — a new unused-import warning I introduced was removed). Dependency closure built first via --filter '@object-ui/plugin-dashboard^...' build; emitted dist/ObjectMetricWidget.d.ts carries `string | I18nLabel`. Gates re-derived from the changed paths (objectui has no scripts/pm/dispatch-gates.mjs): check:control-bytes, check:phantom-deps, check:self-import, check:spec-symbols, check:doc-types, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, changeset:check, check-changeset-presence, type-check:coverage, lint:coverage — all exit 0. REVERSE VERIFICATION, directions predicted before running and written into the test file header: predicted 3 RED / 9 GREEN in vitest, observed exactly 'Tests 3 failed | 9 passed (12)' — the three reds are the two drawer-title cases (both got 'Details') and the key-first card/drawer coherence case, whose failure message reproduces the issue's own measurement: \"expected 'crm.dashboards.sales.widgets.revenue.…' to contain 'Total Revenue'\". type-check predicted RED, observed RED with 7 errors; one prediction missed — I wrote TS2353 and observed TS2322, same meaning, different code because the failures land in JSX attribute position rather than a fresh literal binding. Restore leg: source restored byte-identically (git diff --stat HEAD empty), 12/12 and type-check green again. ABLATION REBUILD: no rebuild is in the vitest leg's resolution path and none can hide a stale result — vitest.config.mts aliases every @object-ui/* specifier to that package's src/, and the ablated file is reached by a relative import (../index -> ../ObjectMetricWidget), always source. For the type-check leg tsconfig.test.json sets \"paths\": {} so @object-ui/types resolves through its built .d.ts; that artifact IS in the path, so the dependency closure was built before any measurement was read, and it is unchanged by the ablation either way.", "open_questions": [ { "question": "TypeScript cannot express the rejection of the retired `{ key, defaultValue }` form on ANY `I18nLabel` surface in either repo, because `InlineLocaleMapSchema` is `z.record(z.string().regex(BCP47), z.string())` and the key regex is erased from the emitted type, leaving `Record<string,string>`. Every string key therefore type-checks. Should the spec narrow the emitted key type so `declared = enforced` holds at compile time, not only at publish time? This is a cross-repo spec change (@objectstack/spec), so it is raised rather than filed here.", "options": [ "A — leave as-is: the spec's runtime schema rejects it at authoring time in both orders (verified), publish-time refusal is the contract boundary, and the emitted type stays simple.", "B — narrow InlineLocaleMap's key to a template-literal/branded locale-tag type in @objectstack/spec, so `{ key, defaultValue }` becomes a compile error on every I18nLabel surface at once.", "C — keep Record<string,string> but add a repo gate that greps for `{ key, defaultValue }` object literals in label position." ], "recommendation": "B, on the third axis — making AI-written metadata hard to get wrong. This defect existed for a full release precisely because the wrong shape compiled; a publish-time-only refusal catches it after the author has already written and shipped it, and the harm here (raw dotted key painted as a KPI label, property-order-dependent) is exactly the class that survives review. B fixes it once for every I18nLabel surface in both repos rather than per-interface. On real business need it is justified by a measured, shipped defect rather than a speculative surface; on long-term soundness it is contract-first (the constraint already exists in the schema — B stops throwing it away at the type boundary) instead of C's grep, which is a second de-facto contract. Cost: a template-literal key type is more complex to read and may need an escape hatch for computed keys, which is why this is the maintainer's call and not mine." } ], "out_of_scope_findings": [ "filed as #5301: plugin-dashboard WidgetConfigPanel's private `resolveLabel` is the fourth copy of the resolver #4032 swept and is inverted — it returns \"\" for the spec's inline locale map and resolves only the retired form; because its result seeds the editable draft via normalizedConfig, a map-titled widget opens the config panel EMPTY and a save writes '' over the author's map. Filed unassigned and unlabelled (concrete defect, PM triages). Not fixed in this PR: different member surface, and the fix is not mechanical — resolveLabel has no locale in scope, so it needs a decision on which locale an authoring panel displays and what a save writes back." ] }
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorAuthorMore actionsCorrection: my Clause-② framing was backwards, and tsc says so
I would have classified this migration as a narrowing — retiring
{ key, defaultValue }in favour ofstring | I18nLabelreads like removing an accepted form. The dev measured it instead of accepting the framing, and it is not a narrowing:I18nLabel's object half erases toRecord<string, string>in the emitted.d.ts. The BCP-47 key regex is a Zod runtime refinement that does not survive into the type. So{ key, defaultValue }remains structurally assignable — nothing about this change stops it type-checking.What starts type-checking is the inline per-locale map, which the old declaration rejected with TS2353. Stated plainly:
A consumer writing the only vocabulary the spec admits previously could not compile.
So the accept set on the type axis widens, toward the contract — the opposite of what "retire the old form" suggests. The retired form is refused where refusal is actually expressible:
I18nLabelSchemarejects it at authoring time in both property orders, now pinned in tests.Recording it because the wrong framing was mine and it was published in the dispatch order.
The reader fix is the part users would have noticed
drawerTitledestructuredtitle?.defaultValue/label?.defaultValue— a limb an inline locale map does not have. An authored drill-down title therefore resolved to''and the panel silently fell back to the literal word "Details". Both reads now go throughpickLocalizedagainst the same UI language the tile uses. A declaration migration that left this reader alone would have type-checked, passed, and still shown the wrong title.Escalated, not decided here
The dev's open question is a genuine fork and it is not this seat's to settle: TypeScript cannot express the rejection of
{ key, defaultValue }on anyI18nLabelsurface in either repo, becauseInlineLocaleMapSchemaisz.record(z.string().regex(BCP47), z.string())and the key regex is erased — every string key type-checks.That is a
@objectstack/speccontract change, so it goes to the spec seat's queue and the maintainer's inbox rather than being answered in an objectui PR. Filed as objectstack-ai/objectstack#9925. Its recommendation (narrow the key to a template-literal/branded locale-tag type) is carried across with the dev's own reasoning, including the cost it names — computed keys may need an escape hatch, which is exactly why it is a maintainer call.Two more things worth keeping
Reverse verification named a missed prediction rather than rounding it off. Predicted 3 RED / 9 GREEN in vitest, observed exactly that — the failure text reproduces the issue's own measurement verbatim (
expected 'crm.dashboards.sales.widgets.revenue.…' to contain 'Total Revenue'). For type-check it predicted RED and observed RED, but predicted error code TS2353 and observed TS2322 — same meaning, different code, because the failures land in JSX attribute position rather than a fresh literal binding. It reported the miss.The ablation's rebuild analysis is the standard to copy. The vitest leg has no artifact in the path (
vitest.config.mtsaliases every@object-ui/*to that package'ssrc/, and the ablated file is reached by a relative import). But the type-check leg'stsconfig.test.jsonsets"paths": {}, so@object-ui/typesresolves through its built.d.ts— that artifact is in the path. It built the dependency closure before reading any measurement, and noted the artifact is unchanged by the ablation either way. Most reports assert "no rebuild needed" globally; this one checked each leg separately and found they differed.PR #5303 is not being flipped to ready:
Build & E2Eis broken repo-wide (#5304, p0) and the maintainer ruled 2026-08-19 「先修 CI,PR 一律等着」.
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Found while implementing #4032 item 4 (PR #5262). Not fixed there — different file, different defect class, filed rather than ridden along.
What
packages/plugin-dashboard/src/ObjectMetricWidget.tsxdeclares the key-reference label form on four members:That vocabulary was RETIRED by the spec in rc.6 (objectstack#5055). PR #4358 migrated the sibling
MetricWidgetPropstostring | I18nLabelfor exactly this reason, and its own doc comment says so — "It is not accepted here any more because the spec rejects it at authoring time".ObjectMetricWidgetPropswas not migrated in the same pass. These four are the only remaining declarations of the shape inpackages/*/src(grepped, excluding tests).Why it is not merely inert
ObjectMetricWidgetforwardslabel/description/trendstraight toMetricWidget, which resolves them withpickLocalized. Handed the retired shape,pickLocalizedmisses every locale limb and falls through to its last resort —Object.values(o).find(v =typeof string) — which returns the FIRST string property in insertion order. Measured againstpackages/i18n/src/pickLocalized.tson this tree:So a TypeScript consumer who writes the shape the interface still invites — in the natural
{ key, defaultValue }order, which is how the retired form was always spelled — gets the raw dotted translation key painted into the KPI card as its visible label. The outcome depends on property order, which is why it would not read as a systematic bug in review.The type is what makes this reachable: this is a public exported props interface, so the declaration is an invitation, and the runtime neither resolves the key nor rejects the object.
Suggested shape
Migrate the four members to
string | I18nLabel, matchingMetricWidgetProps. Worth checking in the same pass whether any call site or fixture in the repo still constructs the retired form (the #4032 fixture triage discipline: a fixture pinning the retired branch keeps passing precisely because nothing is produced any more).Scope note
Deliberately NOT bundled into PR #5262. That PR is #4032 item 4 — the sub-caption's convention key — and it neither reads nor writes these declarations; folding an unrelated public-type migration into it would have widened its verification surface without evidence in its own tests.