Repository navigation
fix(service-analytics): a picklist-bound select dimension serves its category labels in the request locale - #22213
Conversation
…ry labels The analytics label bridge built its synthetic field from `options` alone, so `translateObject` never took its `picklists.<name>.options.<value>` branch and a picklist-bound select dimension rendered its authored labels in every locale. The synthetic field now carries the served field's `picklist`; a field with inline `options` is built exactly as before. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…hema Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
…rs in the request locale The sort-key pass read the authored option labels while the display pass rendered translated ones, so an `order` on a select dimension, and the `limit` window behind it, followed text the user never sees. Translating the picklist-bound dimension's display labels made that true for it too. The sort-key pass now reads the options `translateSelectOptions` answers for the request locale, exactly as the display pass does. Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ec6d07044e57c860245e12535efa5f61edbaaed3 && git checkout ec6d07044e57c860245e12535efa5f61edbaaed3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7d7943dd0dfba6c98afa2200a08983ec85f9849a 59ccbaf04dabe16ad6a54f9f77e7297f5024ed33 && git checkout -B drift-repro 7d7943dd0dfba6c98afa2200a08983ec85f9849a && git merge --no-ff 59ccbaf04dabe16ad6a54f9f77e7297f5024ed33
node scripts/docs-audit/affected-docs.mjs --json 7d7943dd0dfba6c98afa2200a08983ec85f9849a
|
Fixes #22178
Clause-②: no
A select dimension bound to a shared list (
picklist: 'NAME') now serves its chart and report category labels in the request's locale onPOST /api/v1/analytics/dataset/query. Before this change it served the authored (English) labels under everyAccept-Language.What changed
packages/services/service-analytics/src/plugin.ts, thetranslateSelectOptionsbridge. The synthetic field handed totranslateObjectnow carries the dimension field'spicklist. The value is read from the same served field the caller'soptionscame from (getObject(objectName).fields). The resolver looks uppicklists.NAME.options.VALUEonly for a field that carriespicklist, and the synthetic field used to carryoptionsalone. A field with inlineoptionshas nopicklist, so its synthetic field is built exactly as before.packages/services/service-analytics/src/dimension-labels.ts,createOrderLabelResolver.resolveLabels. For a select dimension, the sort-key pass now reads the optionstranslateSelectOptionsreturns for the request locale, the same options the display pass reads. TheDimensionLabelDeps.translateSelectOptionsdocblock now names the picklist address and both passes. The "Scope" section below explains why this file is in the PR.@objectstack/service-analyticspatchchangeset.Rulings executed (triage comment 6052688863, verbatim; its one angle-bracket placeholder respelled NAME)
Mechanism hypotheses, measured
6ed0c0f3.plugin.ts:1071-1073built a synthetic field holding onlynameandoptions.packages/spec/src/system/i18n-resolver.ts:3063-3068callslookupPicklistOptiononly whentypeof def.picklist === 'string'.getObjectserves the picklist-bound field withpicklistnext to its resolvedoptions(resolvePicklistFieldsOnto, the served shapePicklistServedFieldSchemadeclares). The bridge readspicklistthere, and the caller's signature is unchanged. The first test pins this precondition against a realObjectQLengine.picklistis set. The resolver's own precedence still applies: a field-level entry wins, then the list's entry. Azh-CNfield-level entry forlogisticspins it. A field with only inlineoptionsgets the same synthetic field as before.@objectstack/rest, which this package does not depend on, so the pins callAnalyticsService.queryDataset(the callPOST /api/v1/analytics/dataset/querymakes with the request's ExecutionContext). They run through the plugin's own composition, with a realObjectQLengine on SQLite asdataand an i18n service asi18n, on both strategies (native SQL and ObjectQL aggregate).Labels per locale
"Before" is
main's behaviour, measured by ablation A1 below. "After" is this branch. Fixture:industryis bound to picklistos22178_industry;typehas inline options.industry(picklist)industry(picklist)type(inline, control)type(inline, control)物流运输in zh-CN comes from the field-level entry: it applied before, and it still beats the list's物流after. For each locale, everyindustrylabel equals whattranslateObjectreturns for the served object (the object-metadata door's answer for the same field).Ascending
order(withlimit: 1keeping the first row):industrytypeindustrytypeScope: one step past the dispatch fence, and why
The dispatch fenced this card to the
translateSelectOptionsregion ofplugin.ts. Once the picklist-bound dimension's display labels were translated, the sort-key pass (createOrderLabelResolver) no longer matched the display for that dimension. Anordersorted, and alimitwindowed, by the English label while the row showed the translated one. Before the change both were English, so they agreed. Inline-options dimensions already had this gap, because the sort-key pass never read the locale.The dev agent definition requires fixing a shipped defect that the change makes false, and that rule outranks a dispatch fence. So the fix is here, with its own pins and ablation (A2). It is two lines that mirror the display pass. No open PR touches
packages/services/service-analytics(the file lists of all 11 open PRs were read).The claim's file surface should add
packages/services/service-analytics/src/dimension-labels.ts(createOrderLabelResolver.resolveLabels, theDimensionLabelDeps.translateSelectOptionsdocblock). Not touched:packages/spec, and theadmitObjectReadregion ofplugin.tsthat #22154 holds next.Tests (at
59ccbaf04d)src/__tests__/picklist-dimension-label-i18n.test.ts, 26 passed (26).pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2: Test Files 179 passed (179), Tests 4436 passed, 262 skipped (4698).pnpm --filter @objectstack/service-analytics typecheck(tsc --noEmit): exit 0.--listFilesincludes the new test file (176 test files in the program).pnpm --filter '@objectstack/service-analytics^...' build: exit 0.@objectstack/service-analyticsbuild: exit 0,check-dts-emitted2/2.eslint --no-inline-config --format jsonover the 3 changed.tsfiles. The JSON reports 3 files, 0 errors, 0 warnings, and none is ignored byeslint.config.mjs. The config enables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any file it does not touch. The fullpnpm lintrun is left to CI.Ablations
Each ablation used
scripts/ablation-replace.mjsin wrap mode under the verify lock: the anchor must hit, the on-disk blob change is verified, and the file is restored on exit, INT or TERM. The subject is imported relatively (../plugin.js,../dimension-labels.js) and resolves tosrc/, so neither leg goes throughdist/.picklistspread inplugin.ts. Anchor 1 to 0, blobb272fe903c6etofa6bf8178e43. Result: 12 failed, 14 passed. The 8 picklist display pins and the 4 picklist sort pins failed. The inline controls, the no-locale controls and the precondition stayed green. Restored: blob equals HEAD (b272fe903c6e), andgit diff HEADis empty.meta.optionsagain (dimension-labels.ts). Anchor 1 to 0, blob228491a908f1to7050404c0de1. Result: 8 failed, 18 passed. Exactly the 8 sort pins failed, each receiving the authored-label order (for example正式客户, 合作伙伴where合作伙伴, 正式客户was expected). Restored: blob equals HEAD (228491a908f1), andgit diff HEADis empty.localeCompare(the executor's comparator) produces, and that it differs from the authored-label order for every locale and dimension. So the sort pins cannot pass vacuously.Gates (at
59ccbaf04d)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 commands from the merge base6ed0c0f3e(4 paths). All 64 exited 0. Reconciliation with--ranreported: "64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED".The derivation names no i18n family. As extra evidence,
check:i18n,check:i18n-walk-parity,check:i18n-stale-fillandcheck:i18n-coveragewere also run, and each exited 0 with an OK line.The derivation itself lists what it leaves outside the 64 commands and does not measure: the CI jobs scheduled by these paths (Test Core, Temporal Conformance, Dogfood, Build Core), the CI type-check lanes, and the artifact-roster families. CI runs those.
Acceptance notes
queryDatasetunchanged, so the pins stand at that call.localeCompare()and passes no locale, so rendered labels collate in the process's root order, not the request locale's. For example, zh-CN pinyin order would be 教育 · 能源公用事业 · 物流运输, and root order is 教育 · 物流运输 · 能源公用事业. No contract names locale-aware collation. This is noted, not filed.Generated by Claude Code