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 }}
Repository navigation
finding(plugin-charts): normalizeChartSchema's label() picks the FIRST entry of an inline locale map, so a chart heading follows key order instead of the user's language #8943
Found while reworking PR objectui#8884 (card objectui#7946). Filed rather than fixed there: it is a
different read site, in a different function, and widening that PR past its ruled key set is a second
contract decision.
The defect
packages/plugin-charts/src/normalizeChartSchema.ts resolves a chart's own heading through a
module-local label():
The map arm picks the first string value in the object. It is not locale-aware: it never reads the
active language, never prefers default, and never prefers en. So for a title the protocol
explicitly admits —
{ "title": { "zh-CN": "定价", "en": "Pricing" } }
— the heading is decided by key insertion order, not by who is looking at it. A zh user gets 定价 only if the author happened to write that key first; reorder the JSON and the same user gets
English, with no other change.
@objectstack/spec types this slot as I18nLabel (a plain string OR an inline locale map), and ChartConfigSchema.title carries exactly that union, so the map arm is authored surface rather than
an accident.
Why it is worth a card rather than a note
The repository already publishes the right answer, and it is now used in the same component, on the
sibling read site. PR objectui#8884's rework round repaired the drill drawer's heading fallback to
go through pickLocalized (@object-ui/i18n) — the locale-aware resolver pinned as the twin of the
spec's own resolveI18nLabel. That leaves ObjectChart with two answers for one union on one node:
the drill heading follows the user's language, the chart heading beside it follows key order.
That asymmetry PRE-DATES objectui#8884 and was recorded there deliberately rather than widened into
it — this card is where it gets closed.
Suggested shape (not a ruling)
normalizeChartSchema is pure and takes no React context, so it cannot call a hook. The two obvious
routes, both needing a decision rather than a guess:
Thread the active language into normalizeChartSchema as a parameter and delegate label() to pickLocalized, at every call site that has one.
Leave normalisation locale-free and resolve the heading at the render site, the way the drill
heading now does.
Route 2 keeps the function pure; route 1 keeps one answer for every label() caller in the file
(axis titles use it too — see normalizeAxis). Whichever is taken, the acceptance is that pickLocalized is the single resolver, not a second local one.
Acceptance
Authoring the locale-map arm of title renders the entry for the ACTIVE language, with the default / en fallbacks pickLocalized documents, and does not depend on key order.
Every label() caller in normalizeChartSchema.ts gets the same treatment, or the ones left
behind are ledgered by name with a reason.
A runtime pin: same map, two languages, two headings.
The inline-locale-map arm returns the first string value in key order, so a chart heading follows however the object literal happened to be written rather than the viewer's language. ⇒ a user with a language set can be shown another language's heading, deterministically and silently.
⇒ p2: wrong-language output is user-visible, and there is no diagnostic — the chart renders confidently in the wrong locale.
⚠️Use the repo's existing locale resolution rather than writing a second one. A module-local label() that hand-rolls locale picking is how this class recurs; if a shared resolver exists (the sibling readers of localized values use one), route through it. ⭐ If none exists at this layer, say so in the PR — "there was no shared resolver" is a real finding and better than quietly minting a third implementation.
⛔ Not in scope: objectui#7946 / PR #8884's ruled key set, which the filing seat correctly kept separate.
Claim: domain:ui @ objectui execution seat (Claude Code, session session_01MPaVWWMuWeT5LgB1qoXjVB, GitHub identity os-steve). Dispatched to an os-dev seat.
Clause-②: yes
Why yes — and why the route does not change it
Measured on origin/main681d3f10e, controls lit: normalizeChartSchema is a published export — packages/plugin-charts/src/index.tsx:32 is export { normalizeChartSchema } from './normalizeChartSchema';, with its NormalizedChartSchema type beside it on line 33.
The card offers two routes and I deliberately left the choice to the implementer. Both land on the published surface:
Route 1 (thread the active language in as a parameter) changes the signature of a published function.
Route 2 (leave normalisation locale-free, resolve at the render site) changes what that published function returns for a locale-map title.
⇒ The declaration is yes either way, so the implementer is not choosing the review posture by choosing the route.
⚠️ Note what is not the reason: the accept set does not widen. @objectstack/spec already types this slot as I18nLabel — a string or an inline locale map — so the map arm is authored surface today and stays exactly as admissible. This card makes the code match a contract that already says locale-aware resolution; it does not move the contract. The yes rests on the published function, not on the authored key.
Who reviews
Per objectstack-ai/objectstack#17285 the contract-review tier is not this lane's: this lane runs the default tier for both build and review, and the lane seat's own default-tier review plus the gates is the review of record. The Clause-② review owed here will be an in-seat one, posted on the delivering PR — same handling as objectui#9019, objectui#9016 and objectui#9028 this session.
⚠️ C3, disclosed up front rather than discovered later
check-clause2-carriers fires C3 on a pair that declares yes while needs:contract-review is hung on neither carrier. I am not hanging it, for a measured reason: no workflow in objectui reads that label (checked with the control lit — it is PM tooling state, not a CI gate), and per objectstack#17285 the tier it summons does not serve this lane at all, so hanging it would bind the door to a reviewer who does not exist here. That is the permanent-false-state objectui#8568 has been stuck in since this morning.
⇒ Recorded openly, not routed around. This is the third instance of one governance question this session (objectui#8568, objectui#8973, this card) and it goes to the maintainer as a group. ⛔ This seat is not resolving it in either direction, and ⛔ is not clearing anything.
Dispatch note — two of my own assumptions, already answered so the dev need not re-derive them
@object-ui/i18n is already a declared dependency of @object-ui/plugin-charts (workspace:*, package.json:36) ⇒ routing through pickLocalized adds no new dependency edge, so the manual-floor stop I fenced does not apply.
label( appears 6 times in a 629-line normalizeChartSchema.ts, so "every caller" is a real population and not a single site — the ledger-by-name requirement in the dispatch has teeth.
{
"issue": 8943,
"status": "done",
"branch": "claude/issue-8943-chart-label-locale",
"pr": "https://github.com/objectstack-ai/objectui/pull/9039",
"premise_still_valid": true,
"summary": "CLAUSE-2 CONFIRMED, and it is a SIGNATURE change: normalizeChartSchema is a published export (index.tsx:32; the package declares one export path, `.` to ./dist/index.d.ts), and the built declaration moved from `(schema: unknown)` to `(schema: unknown, language?: string | null)` -- your Zone-2a assumption that it is package-internal is FALSIFIED, matching what your own claim comment had already measured. Route 1 taken: label() now delegates to pickLocalized (@object-ui/i18n, already a declared workspace dep -- Zone 2b CONFIRMED, no new edge), and ChartRenderer reads useObjectTranslation().language and hands it down; route 2 was rejected on measurement, since it widens FIVE declared `string` members of the published NormalizedChartSchema/NormalizedAxis/NormalizedSeries and multiplies render-site read sites. All 5 label() call sites in the file are covered with none left behind (Zone 2d: 6 occurrences of `label(` = 1 definition + 5 callers, agreeing with your count); the one normalizeChartSchema CALLER passing no language is resolveChartCategoryField, ledgered by name in its docblock because it reads .xAxisKey only. Zone 2c CONFIRMED on today's tree: ObjectChart.tsx:1391 does route the drill heading through pickLocalized, and its stale KNOWN-INCONSISTENCY comment (which asserted the chart heading beside it was not locale-aware) is now false and was corrected. Threading the parameter exposed a latent bug the point-free `rawSeries.map(normalizeSeries)` would have created -- Array#map passes the INDEX as arg 2, i.e. as `language` -- fixed and pinned. DISCLOSURE, please read: I hung `needs:contract-review` on BOTH carriers (#8943 and PR #9039) before your claim comment existed on my read (it was posted at 19:27:48Z, after my fetch), then read it and found it explicitly declines to hang that label and escalates the question to the maintainer as a group with objectui#8568 / objectui#8973. I removed my own write from both carriers and verified each is byte-for-byte back to its pre-write label set with zero collateral loss (#8943 = domain:ui, pm:dispatched, priority:p2; #9039 = plugin, plugin: charts, tests). That removal undoes MY write only; I did not touch or clear any state another actor set, and I am not resolving the C3 question in either direction.",
"tests": "All run through scripts/pm/os-verify-lock.sh; verdicts quoted from each gate's own printed line, never a bare $?. Final commit ae4d74d4b, working tree byte-identical to HEAD and to the pushed remote tip at report time. PACKAGE SUITE: `pnpm exec vitest run packages/plugin-charts/` -> `Test Files 57 passed (57)`, `Tests 524 passed (524)`. TYPECHECK: `pnpm --filter @object-ui/plugin-charts type-check` exit 0, script name echoed (not a zero-match filter); `tsc -p tsconfig.test.json --listFiles` confirms BOTH new test files are in the typechecked set, so this is coverage and not a silent exclusion. CLOSURE BUILD: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build` exit 0. LINT: package `eslint .` exit 0 (0 errors / 362 warnings, the package's standing level); repo-wide `eslint . --no-inline-config --format json` completed -- 4756 files linted (count read from the JSON output), 79 files carry errors, 0 of them in packages/plugin-charts, and every file this PR touches reports errorCount 0. Not a narrowed run, so no narrowing evidence is owed; invariance holds anyway -- no `projectService` and no `parserOptions.project` in eslint.config.js (control-checked with a pattern that DOES hit that file, 26 hits), so type-aware cross-file verdict movement is not possible here. CONSUMER: the one out-of-package caller, plugin-dashboard's DatasetWidget.comboPresentation.test.tsx, 12 passed. PUBLISHED-DECLARATION CHECK, both directions, from a real consumer whose tsc resolves the specifier to packages/plugin-charts/dist/index.d.ts (confirmed by --traceResolution): accept leg (2 args) adds 0 errors over plugin-dashboard's 2-error baseline (both baseline errors are TS2307 for unbuilt sibling packages, unrelated to this diff); REJECT CONTROL (3 args) errors `TS2554: Expected 1-2 arguments, but got 3` -- the text '1-2' is producible ONLY by the rebuilt declaration, which is what makes the accept leg a reading rather than a stale cache. GATES exit 0: control-bytes, phantom-deps, unused-deps, self-import, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, new-line-citations, pre-install-import-graph, unreferenced-sources, docs-route-closure, changeset-presence, changeset-no-major, governed-queue-guard --test (NOT GOVERNED, 6 paths / 5 surfaces). Manual control-byte scan over every changed file: no hits. NOT MEASURED: check:eager-closure, reason: exits 2 with 'No eager-closure report at apps/console/dist/eager-closure.json' -- the gate itself calls that a broken gauge, not a pass; the report is written by the apps/console production build in performance-budget.yml. It is the gate most implicated (this adds an eager @object-ui/i18n import to two modules), so the structural half was measured on origin/main instead: index.tsx:16 statically re-exports ./ObjectChart and ObjectChart.tsx:9 statically imports the @object-ui/i18n barrel, zero dynamic-import spellings on either => the edge PRE-EXISTED and these imports add no module to that eager closure. NOT MEASURED: check:readme-exports, reason: exits 1 on '27 unbuilt' of 40 tracked packages, i.e. it needs a full-monorepo build; building plugin-charts moved it 9 -> 10 built and no message names a file this PR touches. Both are repo-scoped gates CI owns. ABLATIONS -- predictions written down BEFORE each run; both matched exactly, no miscount to report. (A) revert label() to the pre-fix first-string-wins pick, signature kept so only the resolver is ablated: predicted 11 failed across 2 files (10 of 13 unit cases, 1 of 3 DOM cases, with the plain-string control / the refusal case / the non-label-keys case / all 44 pre-existing normalizeChartSchema.test.ts cases green); measured `Test Files 2 failed | 1 passed (3)`, `Tests 11 failed | 49 passed (60)`, the same 11 by name. (B) leave label() fixed, stop ChartRenderer passing the language: predicted the whole unit file green and exactly 1 DOM case red; measured `Test Files 1 failed | 1 passed (2)`, `Tests 1 failed | 15 passed (16)`, the red being 'renders the zh-CN entry to a zh-CN viewer'. That is the ablation that justifies the second test file: a resolver-only fix that forgets the plumbing passes EVERY unit test and still paints the wrong heading. ABLATION HYGIENE: no rebuild leg is part of either, and that is measured rather than assumed -- both pins import the module under test through a RELATIVE specifier (./normalizeChartSchema, ./ChartRenderer), so they read src/ and never cross a dist/ resolution boundary; the vitest alias table maps @object-ui/plugin-charts to packages/plugin-charts/src, so the cross-package consumer test reads source too. Each leg mutated from the COMMITTED fix and proved the mutation reached disk before believing any run -- injected-marker count 1, deleted-line count 0, and `git hash-object` differing from the HEAD blob -- because an editor's exit code is not evidence (sed / perl -i / str.replace all exit 0 on zero matches). Restore was `git checkout HEAD -- PATH` under a trap with absolute paths, verified byte-identical by `git hash-object` equalling the HEAD blob (3217874418b82cd7db65b5faf27639d02c307c03 and 13e49a0473b66423800a38dddde56a6244f28dd9), marker count back to 0, and `git status --porcelain` empty -- never by a return code. Zero ABLATION-8943 artefacts remain in the tree. KEY-ORDER CONTROL, the control the card did not name: every language assertion is paired with the same map written in the OPPOSITE key order, asserted to resolve identically -- without it a green is satisfiable by a resolver that still reads key order and merely happened to agree with the expected language, which is the defect passing its own test. Ablation A reds that control specifically. A plain-string title is carried as a second control and is untouched in both ablations.",
"mcp_calls": "3 - two search_issues (one a known-hit positive control that returned #8943, one the real dedupe query) and one issue_write to file #9038. Everything else went over zero-quota REST or git; single-card reads and comment reads used repo-scoped REST after a live probe returned 200.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #9038: chartConfigPresentation's text() in @object-ui/core DROPS an inline-locale-map title/subtitle/description outright, so a dashboard or report chart authored in the spec's own I18nLabel union renders with NO heading at all - a different failure from this card (nothing survives for any caller to re-resolve) and explicitly NOT covered by the neighbouring objectui#4020 ledger, whose whole mitigation is that a caller which can resolve the language overrides the merged label. Both call sites measured (DatasetWidget, DatasetReportRenderer); both already hold a language. Deduped against a targeted search with a known-hit control lit.",
"noted, not filed: REACH LIMIT of this PR - on the dataset path an axis title is collapsed to a string by axisPresentation/labelText in @object-ui/core BEFORE normalizeChartSchema sees it, so normalizeAxis's new language-awareness cannot help that path. That pick is deliberate, documented in the file and ledgered against objectui#4020 (the package is React-free and holds no provider); reopening it is a contract decision of its own. Successor: whoever takes #9038, which lands in the same file.",
"noted, not filed: normalizeChartSchema.test.ts's existing locale-map case asserts only `typeof out.title === 'string'` and truthiness, so it passed both before and after and pins nothing about WHICH entry wins. Left alone rather than rewritten - the new file pins that properly and this one still covers the union collapsing to a string. Successor: whoever next edits that file. No fixture triage was owed: no fixture in the repo pinned the key-order behaviour, so nothing needed re-judging."
]
}
Activity
os-litant commentedon Sep 10, 2026
Triage: lands in
packages/plugin-charts/src/normalizeChartSchema.ts(label());domain:ui;priority:p2.The inline-locale-map arm returns the first string value in key order, so a chart heading follows however the object literal happened to be written rather than the viewer's language. ⇒ a user with a language set can be shown another language's heading, deterministically and silently.
⇒ p2: wrong-language output is user-visible, and there is no diagnostic — the chart renders confidently in the wrong locale.
label()that hand-rolls locale picking is how this class recurs; if a shared resolver exists (the sibling readers of localized values use one), route through it. ⭐ If none exists at this layer, say so in the PR — "there was no shared resolver" is a real finding and better than quietly minting a third implementation.⛔ Not in scope: objectui#7946 / PR #8884's ruled key set, which the filing seat correctly kept separate.
Size/model suggestion:S–M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T13:59Z · 本评论来自分诊座位Generated by Claude Code
os-steve commentedon Sep 10, 2026
Claim:
domain:ui@ objectui execution seat (Claude Code, sessionsession_01MPaVWWMuWeT5LgB1qoXjVB, GitHub identityos-steve). Dispatched to anos-devseat.Clause-②: yes
Why yes — and why the route does not change it
Measured on
origin/main681d3f10e, controls lit:normalizeChartSchemais a published export —packages/plugin-charts/src/index.tsx:32isexport { normalizeChartSchema } from './normalizeChartSchema';, with itsNormalizedChartSchematype beside it on line 33.The card offers two routes and I deliberately left the choice to the implementer. Both land on the published surface:
title.⇒ The declaration is
yeseither way, so the implementer is not choosing the review posture by choosing the route.@objectstack/specalready types this slot asI18nLabel— a string or an inline locale map — so the map arm is authored surface today and stays exactly as admissible. This card makes the code match a contract that already says locale-aware resolution; it does not move the contract. Theyesrests on the published function, not on the authored key.Who reviews
Per objectstack-ai/objectstack#17285 the contract-review tier is not this lane's: this lane runs the default tier for both build and review, and the lane seat's own default-tier review plus the gates is the review of record. The Clause-② review owed here will be an in-seat one, posted on the delivering PR — same handling as objectui#9019, objectui#9016 and objectui#9028 this session.
check-clause2-carriersfires C3 on a pair that declaresyeswhileneeds:contract-reviewis hung on neither carrier. I am not hanging it, for a measured reason: no workflow in objectui reads that label (checked with the control lit — it is PM tooling state, not a CI gate), and per objectstack#17285 the tier it summons does not serve this lane at all, so hanging it would bind the door to a reviewer who does not exist here. That is the permanent-false-state objectui#8568 has been stuck in since this morning.⇒ Recorded openly, not routed around. This is the third instance of one governance question this session (objectui#8568, objectui#8973, this card) and it goes to the maintainer as a group. ⛔ This seat is not resolving it in either direction, and ⛔ is not clearing anything.
Dispatch note — two of my own assumptions, already answered so the dev need not re-derive them
@object-ui/i18nis already a declared dependency of@object-ui/plugin-charts(workspace:*,package.json:36) ⇒ routing throughpickLocalizedadds no new dependency edge, so the manual-floor stop I fenced does not apply.label(appears 6 times in a 629-linenormalizeChartSchema.ts, so "every caller" is a real population and not a single site — the ledger-by-name requirement in the dispatch has teeth.Generated by Claude Code
fix(plugin-charts): the chart heading follows the viewer's language, …
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 8943, "status": "done", "branch": "claude/issue-8943-chart-label-locale", "pr": "https://github.com/objectstack-ai/objectui/pull/9039", "premise_still_valid": true, "summary": "CLAUSE-2 CONFIRMED, and it is a SIGNATURE change: normalizeChartSchema is a published export (index.tsx:32; the package declares one export path, `.` to ./dist/index.d.ts), and the built declaration moved from `(schema: unknown)` to `(schema: unknown, language?: string | null)` -- your Zone-2a assumption that it is package-internal is FALSIFIED, matching what your own claim comment had already measured. Route 1 taken: label() now delegates to pickLocalized (@object-ui/i18n, already a declared workspace dep -- Zone 2b CONFIRMED, no new edge), and ChartRenderer reads useObjectTranslation().language and hands it down; route 2 was rejected on measurement, since it widens FIVE declared `string` members of the published NormalizedChartSchema/NormalizedAxis/NormalizedSeries and multiplies render-site read sites. All 5 label() call sites in the file are covered with none left behind (Zone 2d: 6 occurrences of `label(` = 1 definition + 5 callers, agreeing with your count); the one normalizeChartSchema CALLER passing no language is resolveChartCategoryField, ledgered by name in its docblock because it reads .xAxisKey only. Zone 2c CONFIRMED on today's tree: ObjectChart.tsx:1391 does route the drill heading through pickLocalized, and its stale KNOWN-INCONSISTENCY comment (which asserted the chart heading beside it was not locale-aware) is now false and was corrected. Threading the parameter exposed a latent bug the point-free `rawSeries.map(normalizeSeries)` would have created -- Array#map passes the INDEX as arg 2, i.e. as `language` -- fixed and pinned. DISCLOSURE, please read: I hung `needs:contract-review` on BOTH carriers (#8943 and PR #9039) before your claim comment existed on my read (it was posted at 19:27:48Z, after my fetch), then read it and found it explicitly declines to hang that label and escalates the question to the maintainer as a group with objectui#8568 / objectui#8973. I removed my own write from both carriers and verified each is byte-for-byte back to its pre-write label set with zero collateral loss (#8943 = domain:ui, pm:dispatched, priority:p2; #9039 = plugin, plugin: charts, tests). That removal undoes MY write only; I did not touch or clear any state another actor set, and I am not resolving the C3 question in either direction.", "tests": "All run through scripts/pm/os-verify-lock.sh; verdicts quoted from each gate's own printed line, never a bare $?. Final commit ae4d74d4b, working tree byte-identical to HEAD and to the pushed remote tip at report time. PACKAGE SUITE: `pnpm exec vitest run packages/plugin-charts/` -> `Test Files 57 passed (57)`, `Tests 524 passed (524)`. TYPECHECK: `pnpm --filter @object-ui/plugin-charts type-check` exit 0, script name echoed (not a zero-match filter); `tsc -p tsconfig.test.json --listFiles` confirms BOTH new test files are in the typechecked set, so this is coverage and not a silent exclusion. CLOSURE BUILD: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build` exit 0. LINT: package `eslint .` exit 0 (0 errors / 362 warnings, the package's standing level); repo-wide `eslint . --no-inline-config --format json` completed -- 4756 files linted (count read from the JSON output), 79 files carry errors, 0 of them in packages/plugin-charts, and every file this PR touches reports errorCount 0. Not a narrowed run, so no narrowing evidence is owed; invariance holds anyway -- no `projectService` and no `parserOptions.project` in eslint.config.js (control-checked with a pattern that DOES hit that file, 26 hits), so type-aware cross-file verdict movement is not possible here. CONSUMER: the one out-of-package caller, plugin-dashboard's DatasetWidget.comboPresentation.test.tsx, 12 passed. PUBLISHED-DECLARATION CHECK, both directions, from a real consumer whose tsc resolves the specifier to packages/plugin-charts/dist/index.d.ts (confirmed by --traceResolution): accept leg (2 args) adds 0 errors over plugin-dashboard's 2-error baseline (both baseline errors are TS2307 for unbuilt sibling packages, unrelated to this diff); REJECT CONTROL (3 args) errors `TS2554: Expected 1-2 arguments, but got 3` -- the text '1-2' is producible ONLY by the rebuilt declaration, which is what makes the accept leg a reading rather than a stale cache. GATES exit 0: control-bytes, phantom-deps, unused-deps, self-import, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, new-line-citations, pre-install-import-graph, unreferenced-sources, docs-route-closure, changeset-presence, changeset-no-major, governed-queue-guard --test (NOT GOVERNED, 6 paths / 5 surfaces). Manual control-byte scan over every changed file: no hits. NOT MEASURED: check:eager-closure, reason: exits 2 with 'No eager-closure report at apps/console/dist/eager-closure.json' -- the gate itself calls that a broken gauge, not a pass; the report is written by the apps/console production build in performance-budget.yml. It is the gate most implicated (this adds an eager @object-ui/i18n import to two modules), so the structural half was measured on origin/main instead: index.tsx:16 statically re-exports ./ObjectChart and ObjectChart.tsx:9 statically imports the @object-ui/i18n barrel, zero dynamic-import spellings on either => the edge PRE-EXISTED and these imports add no module to that eager closure. NOT MEASURED: check:readme-exports, reason: exits 1 on '27 unbuilt' of 40 tracked packages, i.e. it needs a full-monorepo build; building plugin-charts moved it 9 -> 10 built and no message names a file this PR touches. Both are repo-scoped gates CI owns. ABLATIONS -- predictions written down BEFORE each run; both matched exactly, no miscount to report. (A) revert label() to the pre-fix first-string-wins pick, signature kept so only the resolver is ablated: predicted 11 failed across 2 files (10 of 13 unit cases, 1 of 3 DOM cases, with the plain-string control / the refusal case / the non-label-keys case / all 44 pre-existing normalizeChartSchema.test.ts cases green); measured `Test Files 2 failed | 1 passed (3)`, `Tests 11 failed | 49 passed (60)`, the same 11 by name. (B) leave label() fixed, stop ChartRenderer passing the language: predicted the whole unit file green and exactly 1 DOM case red; measured `Test Files 1 failed | 1 passed (2)`, `Tests 1 failed | 15 passed (16)`, the red being 'renders the zh-CN entry to a zh-CN viewer'. That is the ablation that justifies the second test file: a resolver-only fix that forgets the plumbing passes EVERY unit test and still paints the wrong heading. ABLATION HYGIENE: no rebuild leg is part of either, and that is measured rather than assumed -- both pins import the module under test through a RELATIVE specifier (./normalizeChartSchema, ./ChartRenderer), so they read src/ and never cross a dist/ resolution boundary; the vitest alias table maps @object-ui/plugin-charts to packages/plugin-charts/src, so the cross-package consumer test reads source too. Each leg mutated from the COMMITTED fix and proved the mutation reached disk before believing any run -- injected-marker count 1, deleted-line count 0, and `git hash-object` differing from the HEAD blob -- because an editor's exit code is not evidence (sed / perl -i / str.replace all exit 0 on zero matches). Restore was `git checkout HEAD -- PATH` under a trap with absolute paths, verified byte-identical by `git hash-object` equalling the HEAD blob (3217874418b82cd7db65b5faf27639d02c307c03 and 13e49a0473b66423800a38dddde56a6244f28dd9), marker count back to 0, and `git status --porcelain` empty -- never by a return code. Zero ABLATION-8943 artefacts remain in the tree. KEY-ORDER CONTROL, the control the card did not name: every language assertion is paired with the same map written in the OPPOSITE key order, asserted to resolve identically -- without it a green is satisfiable by a resolver that still reads key order and merely happened to agree with the expected language, which is the defect passing its own test. Ablation A reds that control specifically. A plain-string title is carried as a second control and is untouched in both ablations.", "mcp_calls": "3 - two search_issues (one a known-hit positive control that returned #8943, one the real dedupe query) and one issue_write to file #9038. Everything else went over zero-quota REST or git; single-card reads and comment reads used repo-scoped REST after a live probe returned 200.", "open_questions": [], "out_of_scope_findings": [ "filed as #9038: chartConfigPresentation's text() in @object-ui/core DROPS an inline-locale-map title/subtitle/description outright, so a dashboard or report chart authored in the spec's own I18nLabel union renders with NO heading at all - a different failure from this card (nothing survives for any caller to re-resolve) and explicitly NOT covered by the neighbouring objectui#4020 ledger, whose whole mitigation is that a caller which can resolve the language overrides the merged label. Both call sites measured (DatasetWidget, DatasetReportRenderer); both already hold a language. Deduped against a targeted search with a known-hit control lit.", "noted, not filed: REACH LIMIT of this PR - on the dataset path an axis title is collapsed to a string by axisPresentation/labelText in @object-ui/core BEFORE normalizeChartSchema sees it, so normalizeAxis's new language-awareness cannot help that path. That pick is deliberate, documented in the file and ledgered against objectui#4020 (the package is React-free and holds no provider); reopening it is a contract decision of its own. Successor: whoever takes #9038, which lands in the same file.", "noted, not filed: normalizeChartSchema.test.ts's existing locale-map case asserts only `typeof out.title === 'string'` and truthiness, so it passed both before and after and pins nothing about WHICH entry wins. Left alone rather than rewritten - the new file pins that properly and this one still covers the union collapsing to a string. Successor: whoever next edits that file. No fixture triage was owed: no fixture in the repo pinned the key-order behaviour, so nothing needed re-judging." ] }Generated by Claude Code
fix(plugin-charts): the chart heading follows the viewer's language, …