Repository navigation
finding(core,i18n): two DECLARED locale contracts give opposite guidance for a non-React caller with no tag — undefined is "the honest answer" in one and the wrong answer in the other #10098
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 20, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRuling: UI closure batch 14 item 4 · letter scope (both declared locale contracts stay, each states the caller it governs) · maintainer 「同意」 (chat, director seat summon #29,
session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T12:49ZThe two docblocks are each right for a different caller, and wrong only where they read as universal.
- Non-React display callers with no tag (
@object-ui/corepackages/core/src/utils/number-display.ts,DisplayNumberFormatOptions.locale):undefined= the runtime default. The viewer's own environment is the honest locale for a user-facing display, and it never meansen-US. - The React hook
useDisplayLocale(@object-ui/i18npackages/i18n/src/useDisplayLocale.ts): its last resort stays the concrete'en', for determinism, when the provider chain yields no tag at all.
Deliverable: both docblocks are rewritten to name the caller they govern, and each links to the other. ⛔ No behaviour change in either module. ⛔ No third rule. The
formatNumberInLocaleRangeError-retry reading the card cites is re-checked against the scoped wording in the same PR.Execution:
pm:queue·priority:p2·domain:uistand.
Generated by Claude Code
- Non-React display callers with no tag (
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 1 —
domain:uiexecution seat
Session:session_01BA3nKVUwKQJf8DBxrSVtNC
Branch:claude/issue-10098-locale-contracts-name-caller
Worktree:objectui-issue-10098
Domain:domain:ui
Seat:domain:ui#1
File surface: docblocks only.packages/core/src/utils/number-display.ts(DisplayNumberFormatOptions.locale) andpackages/i18n/src/useDisplayLocale.ts(the last-resort'en'paragraph). Also, only if the re-check under the ruling finds their wording contradicts the scoped contracts: the comments aroundformatNumberInLocale'sRangeErrorretry inpackages/core/src/utils/dataset-format.tsandpackages/plugin-report/src/displayLocale.ts. Plus one.changeset/10098-…md(stop on breach; explain in the report)
Container & model:S,mode:subagent,model: opus(default judgement tier) —priority:p2, docblock-only under a ruling.
Clause-②: no
Thread-read: 5814449943
Serial constraints cleared: open-PR file lists read 2026-09-24T17:36Z ⇒ none touchesnumber-display.ts,useDisplayLocale.ts,dataset-format.tsorplugin-report/src/displayLocale.ts. Livepm:dispatchedclaims read at the same time. objectui#10232 moves consumers ontouseDisplayLocalebut does not edit the hook's file. objectui#10193 editspackages/i18n/src/provider.tsxdocblocks, a different file. objectui#10026 editspackages/core/src/utils/date-display.ts, also a different file. ⇒ all three are file-disjoint.Scope
The ruling (
5814449943, letter scope, maintainer 「同意」) is the deliverable. Each of the two declared locale contracts is rewritten to name the caller it governs, and each links to the other:core'sundefined= the runtime default, for a non-React display caller with no tag; it never meansen-US;useDisplayLocale's concrete'en'last resort, for determinism, when the provider chain yields no tag.
⛔ No behaviour change in either module. ⛔ No third rule. ⛔ No
en-USdefault anywhere. TheformatNumberInLocaleRangeError-retry reading is re-measured first-hand, with a well-formed-tag control through the same path, and checked against the scoped wording. ⇒Fixes #10098.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-24T17:37Zobjectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-dev-report
{ "issue": 10098, "status": "done", "branch": "claude/issue-10098-locale-contracts-name-caller", "pr": "https://github.com/objectstack-ai/objectui/pull/10324", "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run, parent's harness-stamped id (Claude-Session line)", "premise_still_valid": true, "summary": "Following ruling 5814449943, each of the two display-locale contracts now names the caller it governs, and each points at the other in plain text (package + symbol + path). {@link} cannot resolve across the core/i18n boundary. (1) core DisplayNumberFormatOptions.locale: the existing undefined paragraph is kept byte for byte. A new paragraph scopes it to a non-React display caller with no tag, and says the runtime default is then the viewer's own environment, the honest locale for a user-facing display. It adds that a malformed tag lands in the same place via the retry in formatDisplayNumber, and that a React renderer follows useDisplayLocale ('en' last resort) instead. (2) i18n useDisplayLocale: the step-3 'en' text is kept byte for byte, because plugin-report quotes it verbatim. A new paragraph scopes it to a React renderer whose provider chain yields no tag, and names core's DisplayNumberFormatOptions.locale as the other caller's rule. (3) The retry re-check found that plugin-report's formatNumberInDisplayLocale note contradicted the scoped core wording: it called core's dropped-tag retry 'the other wrong answer'. I rewrote that comment only; it now says each retry follows its own package's declared contract. No executable byte moved: the non-comment diff filter prints nothing, and the comment-stripped transpile of every touched module is byte-identical to BASE. The changeset is patch for core, i18n and plugin-report, because the new text is measured in each package's emitted dist .d.ts. Zone-2 readings: assumption 1 holds; the passages sit where the dispatch said. Assumption 2 is partly falsified: formatNumberInLocale has ONE home, core dataset-format.ts, a module-private adapter over formatDisplayNumber. plugin-report neither defines nor wraps it; it has a separate sibling, formatNumberInDisplayLocale, which falls back to 'en'. Assumption 3 holds: patch, chosen over empty frontmatter because the wording ships in the .d.ts files. Commit trailers are model-free (Claude-Session plus Co-authored-by: Claude), per the dispatch; the harness reminder's model-named trailer was not used. The assignee (os-bill) was not touched. No fenced file was touched.", "tests": "Retry re-measure: a tsx probe in the scratchpad, run from the worktree root on this branch's source, not committed. Malformed zh_CN and control zh-CN went through the same exported paths (formatMeasure with CNY, formatDisplayNumber, formatDimensionValue) under LC_ALL en_US and de_DE. A bare Intl.NumberFormat('zh_CN') throws RangeError; zh-CN resolves to zh-CN. Core with zh_CN: 'CN¥1,234.50' (en-US, the runtime default) under en_US and '1.234,50 CN¥' (de-DE, the runtime default) under de_DE. formatDimensionValue gives '1.234.567,89' under de_DE, so the malformed tag drops to the runtime default. Core with zh-CN: '¥1,234.50' (zh-CN) under both. plugin-report formatNumberInDisplayLocale with zh_CN: 'CN¥1,234.50' (en) under both runtimes; with zh-CN: '¥1,234.50' (zh-CN). Plain decimals cannot separate zh-CN from en; CNY can. No-executable-bytes proof: the non-comment changed-line filter over git diff -U0 BASE -- packages printed nothing (grep exit 1). TS transpileModule with removeComments: true, BASE vs HEAD, gave identical emit for all 3 modules (sha256 prefixes 959e7c0be2fc6f9a, 838246c545c47ff0, 11c182aa22edd48f). en-US in added lines: 0. At HEAD 4ff6ba9a: the dependency closure was built via os-verify-lock (VERDICT command-exit 0). The first type-check before that build hit TS6305 (missing dist), a prerequisite miss, not a reading. pnpm --filter core/i18n/plugin-report run type-check: all three Done, lock VERDICT command-exit 0. plugin-report build: exit 0. Marker objectui#10098 is present once in core dist/utils/number-display.d.ts, i18n dist/useDisplayLocale.d.ts and plugin-report dist/displayLocale.d.ts, each with a pre-existing-phrase positive control (1 hit). pnpm exec vitest run --maxWorkers=2 packages/core/src/utils/__tests__/ packages/i18n/ packages/plugin-report/ from the repo root: Test Files 161 passed (161), Tests 2832 passed (2832), lock VERDICT command-exit 0. 161 = 72+70+19, the full file population of those paths. Light gates, all exit 0: check:control-bytes OK (8302 files); check:new-line-citations 0 new; check-changeset-presence pass (3 source files, 1 changeset); check:changeset-claims no findings; check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check:unreferenced-sources, check:spec-symbols. Lint narrowed to the 3 touched files: eslint --no-inline-config --format json gave 3 files, 0 errors, 0 warnings. Population per --print-config: each file in scope, 94 active rules. Invariance: no parserOptions.project/projectService, so linting is not type-aware and a comment-only diff cannot move an untouched file's verdict. Governed guard --test: NOT GOVERNED. NOT MEASURED: full pnpm test shards, repo-wide pnpm lint and workflow-only gates; reason: CI owns them. CI at first read on 4ff6ba9a: 42 check-runs, 24 in_progress, 15 success, 3 skipped, 0 failed, i.e. in_progress. The known Spec Main Shape Gate drift (objectui#10287, pending objectui#10315) is expected red and not this diff. No ablation: no behaviour and no pin (prose only).", "mcp_calls": "0 — no MCP GitHub tools called", "api_writes": "2 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches (event fleet-write) executed as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectui/pulls, draft, which opened #10324 (run 36037755901, success); (2) os-dev-report comment, i.e. POST /repos/objectstack-ai/objectui/issues/10098/comments. Two earlier pr_create attempts were REFUSED by the local write-pace hourly budget (exit 10) before any request left, so zero bytes were written. Plus 2 git pushes (empty-branch probe, then 4ff6ba9a), which are not REST. Zero label writes, zero PATCH.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed — @object-ui/plugin-report degrades a malformed tenant tag two ways: DatasetReportRenderer via core formatMeasure lands on the runtime default, while formatValue and the exporters via formatNumberInDisplayLocale land on 'en'. Under the ruling each follows the contract of the formatter it runs through, so this is not a class a/b/c finding; it is recorded in the PR's Acceptance notes so it is not re-derived.", "carrier: 承接者:无 · noted, not filed — pending .changeset/10020-report-display-locale.md says the report does NOT degrade to a dropped tag. That is true of plugin-report's own choice and names no core behaviour as wrong; left as that change's record (Acceptance notes)." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions✅ ACCEPT — PR objectui#10324 at
4ff6ba9a· lands once its branch carriesmaindomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. I read the report in full, and the seat read the diff itself: docblocks, one comment and a changeset. No review-tier record is owed:Clause-②: noholds, because nothing is accepted or exported differently. The seat checked the changeset prose against the diff and the dev's measurements.Implemented-by: claude/issue-10098-locale-contracts-name-caller Reviewed-by: session_01BA3nKVUwKQJf8DBxrSVtNCitem reading shape draft · base main·Fixes #10098·Clause-②: noat line startruling fit ( 5814449943)core'sDisplayNumberFormatOptions.localekeeps itsundefinedparagraph byte for byte, and gains the caller it governs: a non-React display caller with no tag, whose runtime default is the viewer's own environment.useDisplayLocalekeeps its'en'text byte for byte, becauseplugin-reportquotes it, and gains its caller: a React renderer whose provider chain yields no tag. Each names the other in plain text (package, symbol, path), since{@link}cannot resolve across the package boundary. ⛔ No third rule, and noen-USadded (0 hits)the retry re-check Measured first-hand, with a well-formed control through the same exported paths under two runtime locales. coredrops a malformedzh_CNto the runtime default (en-US underen_US, de-DE underde_DE), andzh-CNrenderszh-CN.plugin-report'sformatNumberInDisplayLocalelands on'en'. Its comment calledcore's dropped-tag retry 「the other wrong answer」, which contradicts the scopedcorewording. Only that comment was rewritten (claim surface, conditional arm): each retry follows its own package's contract. Zone-2 correction:formatNumberInLocalehas one home,core'sdataset-format.tsno behaviour change The non-comment changed-line filter prints nothing, and the comment-stripped transpile of all three modules is byte-identical to the base changeset patchforcore,i18nandplugin-report. The new wording was measured in each package's emitted.d.ts. The prose matches the difftests 161 files / 2832 tests pass across the three packages from the repo root. The type-checks and light gates exit 0 CI the head was cut before objectui#10315, so its gate reads the old drift. The branch is being brought up to mainLanding
This branch merges
main, then the PR enters the merge queue once every check is green.Out of scope
- Acceptance notes:
plugin-reportdegrades a malformed tenant tag in two ways.DatasetReportRenderer, throughcore'sformatMeasure, lands on the runtime default;formatValueand the exporters land on'en'. Under the ruling, each follows the contract of the formatter it runs through. This is not an a/b/c class. - Acceptance notes: the pending
.changeset/10020-report-display-locale.mdsays the report does not degrade to a dropped tag. That is true ofplugin-report's own choice, so it is left as that change's record.
domain:uiseat #1 · review · 2026-09-24T18:30Z- Acceptance notes:
- added a commit that references this issue
on Sep 28, 2026
Ruled: 5814449943 · letter scope (each locale contract names its caller) · 2026-09-24T12:52Z
Path: P2 | 那条路第 2 步「本地跑起来、看到」 | 两份已声明的 locale 契约,对「无标签的非 React 调用方」给出相反指引
分诊重测与定级:2026-09-20T15:58Z
Dedupe words:
two locale contracts disagree non-React caller·useDisplayLocale contract undefined wrong answer·number-display undefined means machine locale·formatNumberInLocale RangeError retry·malformed locale retry drops tagSurfaced by the objectui#10020 dev while threading a display locale through
@object-ui/plugin-report; it declined to act on it and declined to widen intopackages/core, which is correct. ⛔ Nopriority:*/typehere — the triage seat's sole production.domain:uiinherited from the in-flight parent objectui#10020.⭐ Two DECLARED contracts, read first-hand by this seat on
origin/main, giving opposite guidance for the same case@object-ui/core,packages/core/src/utils/number-display.ts:58— onDisplayNumberFormatOptions.locale:@object-ui/i18n,packages/i18n/src/useDisplayLocale.ts:32-34:⇒ For the case that actually arises — a non-React caller with no tag in hand — one document calls
undefinedthe honest answer and the other calls handingIntlan absent tag the wrong answer, for a reason (non-determinism in CI, invisibility in review) that applies to exactly that caller. ⭐ Both are prose a reader is meant to follow, and both are in force.formatNumberInLocale's malformed-tag retry drops the tag, so a tenant with a typo'd locale (an underscore instead of a hyphen — the file's own comment calls that the likeliest tenant-config typo) silently renders every dataset measure in the runner's locale. ⇒ ⭐ the taking dev re-takes this leg first; it is the part that turns a documentation conflict into a user-visible one, and it deserves its own measurement rather than this card's word.Why it is worth a card rather than a comment
An author or a later dev reading either document gets a rule, follows it, and is following the other one's ⛔ refusal.⚠️ ⛔ This is NOT a request to change behaviour in either package: the two may well be reconcilable —
coreis genuinely about a caller that has no tag at all,i18nabout one that is resolving a display locale and could have one. ⭐ Deciding whether they reconcile is the deliverable, and whichever way it goes, the losing document gets a sentence saying so, so the next reader does not re-derive the conflict.⛔ The objectui#10020 dev deliberately did NOT resolve it in that PR, and ⛔ deliberately did not widen into
packages/core. Its change degrades to'en'and says so in the code — which is one of the two answers, taken locally, ⛔ not a ruling for the repo.⛔ What this card does NOT claim
'en') is wrong — it followed thei18ncontract, which is the one its own package consumes.Acceptance
en-USis reintroduced anywhere as a default.Generated by Claude Code