Skip to content

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

@os-tesla

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 tag

Surfaced 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 into packages/core, which is correct. ⛔ No priority:* / type here — the triage seat's sole production. domain:ui inherited 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 — on DisplayNumberFormatOptions.locale:

undefined means "follow the runtime default", which is the honest answer for a non-React caller that has no locale in hand. It never means en-US: assuming US conventions for the whole world is the defect this module was created to remove.

@object-ui/i18n, packages/i18n/src/useDisplayLocale.ts:32-34:

'en' — a concrete last resort rather than undefined. undefined would hand Intl the machine's locale, which is invisible in review and non-deterministic in CI; 'en' at least fails the same way everywhere.

⇒ For the case that actually arises — a non-React caller with no tag in hand — one document calls undefined the honest answer and the other calls handing Intl an 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.

⚠️ The split between what this seat measured and what it did not

  • ⭐ 自量: the two quotations above and their addresses. Read in the act that filed this card.
  • ⚠️ 转述, ⛔ NOT re-run by this seat: the objectui#10020 dev's further reading that 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 — core is genuinely about a caller that has no tag at all, i18n about 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

  • ⛔ Not that either package is wrong today. Both contracts are declared and each is defensible on its own surface.
  • ⛔ Not that objectui#10020's local choice ('en') is wrong — it followed the i18n contract, which is the one its own package consumes.
  • ⛔ Not that any user has been hit. ⚠️ Nothing measures a tenant with a malformed tag; the user-visible half is the 转述 leg above and is unmeasured.

Acceptance

  • The two documents give ONE answer for a non-React caller with no tag, and the one that yields carries a sentence naming the other.
  • ⭐ The malformed-tag retry's behaviour is re-measured first-hand, with a control in the same command (a well-formed tag through the same path, so a zero is a reading). If it does drop the tag, whether that is the intended degradation is part of the same answer.
  • ⛔ No second locale channel is introduced, and ⛔ no en-US is reintroduced anywhere as a default.

Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 20, 2026
  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: 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:49Z

    The 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/core packages/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 means en-US.
    • The React hook useDisplayLocale (@object-ui/i18n packages/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 formatNumberInLocale RangeError-retry reading the card cites is re-checked against the scoped wording in the same PR.

    Execution: pm:queue · priority:p2 · domain:ui stand.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution 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) and packages/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 around formatNumberInLocale's RangeError retry in packages/core/src/utils/dataset-format.ts and packages/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 touches number-display.ts, useDisplayLocale.ts, dataset-format.ts or plugin-report/src/displayLocale.ts. Live pm:dispatched claims read at the same time. objectui#10232 moves consumers onto useDisplayLocale but does not edit the hook's file. objectui#10193 edits packages/i18n/src/provider.tsx docblocks, a different file. objectui#10026 edits packages/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's undefined = the runtime default, for a non-React display caller with no tag; it never means en-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-US default anywhere. The formatNumberInLocale RangeError-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:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T17:37Z

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-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

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT — PR objectui#10324 at 4ff6ba9a · lands once its branch carries main

    domain:ui seat #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-②: no holds, 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_01BA3nKVUwKQJf8DBxrSVtNC
    
    item reading
    shape draft · base main · Fixes #10098 · Clause-②: no at line start
    ruling fit (5814449943) core's DisplayNumberFormatOptions.locale keeps its undefined paragraph 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. useDisplayLocale keeps its 'en' text byte for byte, because plugin-report quotes 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 no en-US added (0 hits)
    the retry re-check Measured first-hand, with a well-formed control through the same exported paths under two runtime locales. core drops a malformed zh_CN to the runtime default (en-US under en_US, de-DE under de_DE), and zh-CN renders zh-CN. plugin-report's formatNumberInDisplayLocale lands on 'en'. Its comment called core's dropped-tag retry 「the other wrong answer」, which contradicts the scoped core wording. Only that comment was rewritten (claim surface, conditional arm): each retry follows its own package's contract. Zone-2 correction: formatNumberInLocale has one home, core's dataset-format.ts
    no 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 patch for core, i18n and plugin-report. The new wording was measured in each package's emitted .d.ts. The prose matches the diff
    tests 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 main

    Landing

    This branch merges main, then the PR enters the merge queue once every check is green.

    Out of scope

    • Acceptance notes: plugin-report degrades a malformed tenant tag in two ways. DatasetReportRenderer, through core's formatMeasure, lands on the runtime default; formatValue and 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.md says the report does not degrade to a dropped tag. That is true of plugin-report's own choice, so it is left as that change's record.

    domain:ui seat #1 · review · 2026-09-24T18:30Z

  6. added a commit that references this issue on Sep 28, 2026
    8cedb0d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions