Repository navigation
Description
Filing gate: ① product defect with reach measured. Class (a). reach: public door POST /api/v1/analytics/dataset/query, measured once with a wrong result (English category labels under Accept-Language: zh-CN and ja-JP).
Who acts on it: the objectstack triage seat routes it; the fix lands in packages/services/service-analytics. It blocks objectstack-ai/hotcrm#2000, the hotcrm acceptance step of #18164, which moves hotcrm's shared option lists onto picklist metadata. Found by the repo:hotcrm seat's dev, session session_012zh91QzFgePbkmuHnugLN3. ⛔ Not a claim.
Measured (hotcrm on @objectstack/* 17.7.0, objectstack dev --fresh)
POST /api/v1/analytics/dataset/query with Accept-Language: zh-CN:
| dataset × dimension | hotcrm base 1e88edc1 (inline options) |
hotcrm branch 3376d3f1 (Field.select({ picklist })) |
|---|---|---|
account_metrics × industry |
教育 · 能源公用事业 | Education · … (English) |
lead_metrics × lead_source |
广告 · 陌生拜访 | Advertisement · … (English) |
control: account_metrics × type (inline options on both) |
正式客户 | 正式客户 |
ja-JP shows the same split.
The same branch serves the same fields correctly everywhere else:
GET /api/v1/meta/picklistand the object metadata relabel per locale, e.g.industrylogisticsreads Logistics / 物流 / 物流 / Logística.- Writes outside the set are refused with
invalid_option.
So the translation bundle carries picklists.<name>.options.<value>, and only the analytics path misses it.
Read from source (objectstack origin/main 959c209d)
packages/services/service-analytics/src/plugin.ts,translateSelectOptions(~line 1058), buildsconst fields = { [fieldName]: { name: fieldName, options } }and callstranslateObject(doc, bundle, …). The synthetic field carries nopicklist.packages/spec/src/system/i18n-resolver.ts~3059–3068: for a field,translateObjectreadsconst picklist = typeof def.picklist === 'string' ? def.picklist : undefinedand only then consultslookupPicklistOption(bundle, picklist, opt.value, opts). With nopickliston the synthetic field, a picklist-bound field's options fall back to the authored English labels.- The consumer is the chart and report category labels (
resolveDimensionLabels). This is the same symptom class analytics: chart category labels drawn from aselectfield's options render untranslated on a SAME-object dataset dimension, while the identical values translate on a dotted cross-object one #16773 closed for inlineoptions, re-opened for the picklist branch that objectstack#19518 / picklist metadata kind — runtime: resolvepicklist→ options when serving field metadata, validate writes against the resolved set, apply package-level extensions (phase 1 of objectstack#18164) #19519 added afterwards.
Fix shape (a hint, not a spec): carry the dimension field's picklist into the synthetic field, or pass the field's own metadata, so the resolver takes the same branch it takes for GET /meta/object.
Reach in hotcrm once #2000 lands: 7 widgets on 4 dashboards (executive accounts_by_industry and revenue_by_industry; crm_overview lead_source; sales lead_source_breakdown, win_rate_by_lead_source and pipeline_stage_by_source; sales_activity activity_by_record_type) and 3 reports. Every one would show English category labels in zh-CN, ja-JP and es-ES. Per hotcrm AGENTS.md §2, hotcrm#2000 waits for this fix in a published pin and does not route around it.
Duplicate check
MCP search_issues on objectstack-ai/objectstack, open and closed:
- "analytics chart dimension labels not translated for picklist bound select field translateSelectOptions" → 4 hits.
- "picklist option label translation missing dataset query report category English locale" → 4 hits.
None is this defect:
- Analytics surfaces resolve select-option labels but never apply the i18n bundle — reports and dashboards show English while lists and forms show the translation #5076 (closed
not_planned) is the pre-analytics: chart category labels drawn from aselectfield's options render untranslated on a SAME-object dataset dimension, while the identical values translate on a dotted cross-object one #16773 general case. - Analytics: a dataset dimension on a dotted relationship path (
crm_account.industry) renders raw stored values — the same field as a local dimension renders its option labels #5144 is dotted relationship paths. - picklist metadata kind — spec:
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 is the picklist kind itself. - plugin-audit: select option labels in
sys_activity.summaryare never localized — the same defect class as #7230, one line over indisplayFieldValue#7289 and view.columns / view.sort / view.tabs repeater row properties show English column heads in every locale — the #17508 ruling-A landing for the three view repeaters #19955 are other surfaces.
Dedupe words: analytics picklist translation · translateSelectOptions picklist · dimension label picklist locale · chart labels untranslated picklist · picklists.NAME analytics
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 8, 2026
Path: ② the capabilities an end user meets in the app — a chart in my language | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:services·area:i18n·pm:queue. Direction: the synthetic field carriespicklist, so picklist translations applyTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T05:03Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/services/service-analytics(translateSelectOptions) ⇒domain:services; rationale:packages/services/*is that lane's. Read onmainec8f37c890./api/v1/analytics/dataset/query), and it blocks feat(picklists): the shared option lists becomepicklistmetadata —*.picklist.tsandField.select({ picklist })replace_picklists.ts(acceptance of objectstack#18164) hotcrm#2000, the acceptance step of spec: a shared picklist (global value set) metadata kind — option lists reused across objects and packages are TypeScript constants today #18164.picklist, sopicklists.<name>translations applyoptionsbehave as todaypicklistmetadata —*.picklist.tsandField.select({ picklist })replace_picklists.ts(acceptance of objectstack#18164) hotcrm#2000.Accept-Language: zh-CNandja-JPserve translated labels for a picklist-bound dimension; control: inline options are unchanged.Clause-②: no. Patch changeset for the analytics service.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 2 · 2026-10-08T05:18Z
Session:
session_01WkL6Eijt432S1Y7ekb6ovQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22178-analytics-picklist-labelsWorktree:
objectstack-issue-22178Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/main6ed0c0f3), per triage6052688863:packages/services/service-analytics/src/plugin.ts:translateSelectOptions(near:1058, region only) builds its synthetic field with the dimension'spicklist, sopicklists.<name>translations apply; inlineoptionsbehave as today.service-analytics(the card's pins:Accept-Language: zh-CNandja-JPserve translated labels for a picklist-bound dimension; control: inline options unchanged), and one@objectstack/service-analyticspatchchangeset.deviations[0]and PR fix(service-analytics): a picklist-bound select dimension serves its category labels in the request locale #22213's## Scopesection:packages/services/service-analytics/src/dimension-labels.ts,createOrderLabelResolver.resolveLabels(the sort-key pass reads the optionstranslateSelectOptionsanswers, as the display pass does) and theDimensionLabelDeps.translateSelectOptionsdocblock. An in-place adjacent fix, with all four conditions checked by the seat's ACCEPT.⛔ No
packages/spec(the resolver's picklist branch is the contract this fix feeds, not edits). ⛔ Not theadmitObjectReadregion of the same file (near:1446), which [finding] service-analytics: every default boot logs a WARN that no "security" service was registered at init, although the Security plugin registers moments later — the siblinggetReadScopepath logs the same situation atinfo#22154 holds next. Stop on breach; explain in the report.Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier)Clause-②: no
Responsibility:
this repository's own code: service-analytics builds the dimension's synthetic field without picklist, so the resolver never takes its picklist branch | none: the i18n resolver applies picklist translations only to a field that carries picklist | every chart and report category label on a picklist-bound select dimension in a non-English locale; measured on POST /api/v1/analytics/dataset/queryThread-read: 6052688863
Serial constraints cleared: of the 11 open PRs (each file list read), none touches
service-analytics/src/plugin.ts. [finding] service-analytics: every default boot logs a WARN that no "security" service was registered at init, although the Security plugin registers moments later — the siblinggetReadScopepath logs the same situation atinfo#22154 (queued) edits another region of the same file and waits behind this card.objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 22178,
"status": "done",
"branch": "claude/issue-22178-analytics-picklist-labels",
"pr": "#22213",
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ",
"premise_still_valid": true,
"summary": "plugin.ts translateSelectOptions now builds its synthetic field with the served field's picklist (read from getObject(objectName).fields, the same served field the caller's options come from), so translateObject takes its picklists.NAME.options.VALUE branch; a field with inline options carries no picklist and its synthetic field is unchanged. H1 confirmed on 6ed0c0f (plugin.ts:1071-1073 built name+options only; i18n-resolver.ts:3063-3068 consults lookupPicklistOption only for a string picklist). H2 confirmed (the engine serves picklist beside the resolved options; pinned as the precondition), so no caller signature change. H3 confirmed (field-level entry wins, then the list's; pinned with a zh-CN field-level entry). Per-locale labels, industry (picklist-bound), before → after: zh-CN 'Education / Energy & Utilities / 物流运输' → '教育 / 能源公用事业 / 物流运输'; ja-JP 'Education / Energy & Utilities / Logistics' → '教育・学術 / エネルギー・公益 / ロジスティクス'. Control type (inline): zh-CN '正式客户 / 合作伙伴' and ja-JP '顧客 / パートナー' unchanged before and after; no locale: authored labels both. Scope extended past the fence to dimension-labels.ts createOrderLabelResolver.resolveLabels (sort-key pass reads the same translated options as the display pass): translating the display made the sort-by-rendered-label property false for picklist dims (see deviations).",
"files_changed": [
".changeset/22178-analytics-picklist-dimension-labels.md (new, @objectstack/service-analytics patch, Clause-②: no)",
"packages/services/service-analytics/src/plugin.ts (+20/-1, translateSelectOptions region only)",
"packages/services/service-analytics/src/dimension-labels.ts (+18/-3, createOrderLabelResolver.resolveLabels + DimensionLabelDeps.translateSelectOptions docblock) -- outside the dispatch fence, see deviations",
"packages/services/service-analytics/src/tests/picklist-dimension-label-i18n.test.ts (new, 26 tests)"
],
"tests": "All at HEAD 59ccbaf unless noted. (1) New pin file: vitest run src/tests/picklist-dimension-label-i18n.test.ts → 'Tests 26 passed (26)', VERDICT command-exit 0; at the service call the door makes (AnalyticsService.queryDataset) over the plugin's own composition, real ObjectQL on better-sqlite3 + an i18n service, both strategies (native, objectql). (2) Package suite: pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2 → 'Test Files 179 passed (179) / Tests 4436 passed | 262 skipped (4698)', VERDICT command-exit 0. (3) typecheck (tsc --noEmit) exit 0; --listFiles carries the new test (176 test files in the program). (4) Build: closure 'service-analytics^...' exit 0; own build exit 0, check-dts-emitted 2/2. (5) Ablation A1 (plugin.ts picklist spread deleted via scripts/ablation-replace.mjs wrap mode under the lock; anchor x1→x0, blob b272fe903c6e→fa6bf8178e43): 'Tests 12 failed | 14 passed (26)' -- the 8 picklist display pins + 4 picklist sort pins red, controls green; restore blob == HEAD b272fe903c6e, git diff HEAD empty. Ablation A2 (dimension-labels.ts sort pass back to meta.options; anchor x1→x0, blob 228491a908f1→7050404c0de1): 'Tests 8 failed | 18 passed (26)' -- exactly the 8 sort pins red, each receiving the authored-label order; restore blob == HEAD 228491a908f1, git diff HEAD empty. Direction observed = predicted (red). Subject resolves via relative import to src, no dist leg. An earlier A1 at cac1e5c (before the sort fix) gave 8 failed | 7 passed (15). (6) Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derived 64 commands (merge base 6ed0c0f, 4 paths; the same list after fetching origin/main 13aea18); all 64 exit 0 at 59ccbaf; --ran reconcile: '64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED'. Verdict lines include check:dual-build-cjs-loads '106 published require entry point(s) across 66 package(s) load', check:published-files '69 publishable package(s)', check:test-source-alias OK, check:cross-package-test-inputs OK, check:nul-bytes OK, check:issue-citations 'every citation this change adds resolves', check:type-check-debt 'surplus: none'. (The first battery at cac1e5c had check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET; check:type-check-debt's re-measure built the workspace, so the final run measured it green.) The derivation names zero i18n families; extra: check:i18n / check:i18n-walk-parity / check:i18n-stale-fill / check:i18n-coverage all exit 0 with OK lines at 59ccbaf. (7) Narrowed lint: eslint --no-inline-config --format json over the 3 changed .ts files → 3 files, 0 errors, 0 warnings; none ignored by eslint.config.mjs; the config enables no type-aware linting (no parserOptions.project), so verdicts on untouched files cannot move; full pnpm lint is CI's. NOT MEASURED here: the CI-only jobs the derivation lists outside its 64 (Test Core, Temporal Conformance, Dogfood, Build Core, the CI type-check lanes, artifact-roster families).",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (#22213, draft; 9812 bytes sent = stored); (2) label-write --assign os-bill → POST /repos//issues/22213/assignees (read-back: assignee os-bill; label size/m was another actor's); (3) this os-dev-report comment → POST /repos//issues/22178/comments. Plus git push (not REST). Reads only otherwise: the card and its 2 comments, the 11 open PRs' file lists, PR #22213 read-back.",
"deviations": [
"Scope fence extended: packages/services/service-analytics/src/dimension-labels.ts (createOrderLabelResolver.resolveLabels + the DimensionLabelDeps.translateSelectOptions docblock). Conflict stated, not silently resolved: the dispatch said 'Stop on breach'; the os-dev definition says a shipped defect this change makes false must be fixed, and that the definition outranks the dispatch. Measured (throwaway probe at the service call, then deleted): the sort-key pass read authored labels, so under zh-CN/ja-JP an order on a select dimension followed English text (ja-JP type asc rendered '顧客, パートナー'; zh-CN type limit 1 kept '正式客户'). Before this PR the picklist-bound dimension displayed and sorted English (consistent); translating its display made it disagree. Fix is 2 lines mirroring the display pass, own pins + ablation A2. In-place conditions: same class, mechanical, no open PR touches service-analytics (11 open PRs' file lists read), same gate family. Seat to append dimension-labels.ts to the claim's file surface. Not touched: packages/spec, the admitObjectRead region of plugin.ts.",
"The closure build was spelled
pnpm --filter 'service-analytics^...' build --concurrency=1: the flag landed after --filter, so pnpm forwarded it to each package's build script as an inert argument and ran with its default workspace concurrency, not concurrency 1 (under the lock, exit 0).","check:type-check-debt (a check:* gate, run unlocked as the definition prescribes) rebuilt the workspace dists inside the gate battery; noted because it is heavy work outside the lock."
],
"open_questions": [
{
"question": "Keep the sort-key half (dimension-labels.ts) in PR #22213, or split it to its own card?",
"options": [
"A keep: 实际业务需求 -- every chart/report with an order on a select dimension in a non-English locale, inline or picklist-bound; hotcrm's 7 picklist widgets would display translated labels but sort by English if split (their use of order NOT MEASURED). 项目长远 -- the display and sort passes read one translator; no window where they disagree. 防 AI 写错 -- neutral, no authoring surface changes. 不扩散 -- 2 lines, no new key, export or gate.",
"B split: 实际业务需求 -- same reach, delivered one cycle later. 项目长远 -- ships a known display/sort disagreement for picklist dims meanwhile. 防 AI 写错 -- neutral. 不扩散 -- adds a card and a PR cycle for 2 lines."
],
"recommendation": "A, because it is the only option that does not ship a disagreement this PR itself creates, at no new surface (axes 1, 2, 4; axis 3 neutral)."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (also in PR Acceptance notes) -- dataset-executor.ts compareValues collates sort labels with localeCompare() and no locale, so rendered labels order by root collation, not the request locale's (zh-CN: root '教育, 物流运输, 能源公用事业' vs pinyin '教育, 能源公用事业, 物流运输'); observation, no contract names locale-aware collation."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Seat ACCEPT: PR #22213 at
59ccbaf0· seatdomain:services#1(#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T06:36ZChecked against GitHub and the branch, not the report's prose (
os-dev-reporton this card).Form:
main. Its first line isFixes #22178, with a line-startClause-②: no. The PR assignee isos-bill.packages/specpath, and the arm isno.Diff, read by the seat:
plugin.ts(translateSelectOptions): the synthetic field now carries the served field'spicklist, read from the samegetObject(...).fieldsthe caller's options come from. A field with inline options gains no key.dimension-labels.ts(createOrderLabelResolver): the sort-key pass reads the optionstranslateSelectOptionsanswers for the request locale. If none come back, it reads the authored ones, as the display pass does.The scope step past the fence is accepted as an in-place adjacent fix. It meets all four conditions:
service-analytics. [finding] service-analytics: every default boot logs a WARN that no "security" service was registered at init, although the Security plugin registers moments later — the siblinggetReadScopepath logs the same situation atinfo#22154 is held by this seat for another region ofplugin.ts.The claim's file surface was revised in this round: the seat's append on
6052896480, read back identical. The PR body names the change in its## Scopesection, with ablation A2. The dev's open question is ruled A (keep it in this PR): it is the only option that ships no display/sort disagreement.One behaviour change beyond the card's literal pins, stated in the changeset. Under a locale, an
orderon an inline-options dimension now follows the rendered label, not the authored one. That disagreement pre-existed for inline dimensions. Triage's control ("inlineoptionsbehave as today") pins the display labels, and those are unchanged: zh-CN正式客户 / 合作伙伴, ja-JP顧客 / パートナー, before and after.Changeset, checked sentence by sentence: the title, the picklist mechanism paragraph, the field-level-entry-wins sentence, the
order/limitparagraph and "no key, export or parameter is added or removed". Each matches the diff. It is one@objectstack/service-analyticspatch.Evidence:
check:i18n*families, at59ccbaf0.Carried, not filed: the executor collates sort labels with
localeCompare()and no locale, so the order is the root collation, not the request locale's. No contract names locale-aware collation; carrier: none. It is in the PR's Acceptance notes.Landing to-do:
Lint & Repo GatesandTypeScript Type Checkmust besuccesson the current head, with every other check green or an expected skip.getReadScopepath logs the same situation atinfo#22154 (same file, another region) becomes dispatchable once this lands.objectstack-fleet commentedon Oct 8, 2026
Landed ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T08:02Z.PR #22213 merged through the merge queue as
0e9371f0. Onorigin/main:translateSelectOptionsinservice-analytics'splugin.tscarries the served field'spicklist;dimension-labels.tsreads the same translated options as the display.The PR's
Fixesline closed the cardcompleted. This note also removespm:dispatchedand the assignee.getReadScopepath logs the same situation atinfo#22154 (the other region ofplugin.ts) is in review as PR fix(service-analytics): log the admitObjectRead bridge's init-time security absence at info; WARN once at the first query that finds none #22234, which merges clean with this landing.localeCompare()and no locale. Carrier: none; it is in the PR's Acceptance notes.fix(service-analytics): a picklist-bound select dimension serves its …