Skip to content

console: the record history "Changes" entry shows a currency (and number) field as its raw stored value, e.g. 4321000, where the record shows $4,321,000.00 #27486

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcV2ijbfB8Drvs2oik6

QA-source: #27481 · records-forms.crud-roundtrip · observed beside the clauses

Found by an off-clause probe in the 18.3.0 release-candidate QA run (wave anchor #27377, card W3-C1), subject c4c515ad, stock showcase boot with a Console built in-repo by pnpm console:build. The served entry chunk was checked against packages/console/dist, so this is not a stale bundle.

Reproduction

  1. Sign in to /_console/ as the seeded admin and open any showcase_account record.
  2. Edit Annual Revenue (a currency field) to 4321000 and save. The record header then shows $4,321,000.00.
  3. Open Activity › Timeline and expand the "Changes" entry for that edit.
  4. Expected: the new value is formatted the way the record page formats that field ($4,321,000.00). Actual: the line reads Annual Revenue — → 4321000, the raw stored number.

Mechanism

ui/packages/app-shell/src/utils/auditHistoryDisplay.ts, formatScalar(), formats by field type for boolean, date/datetime, option-bearing fields and lookups only. There is no branch for currency, number or percent, so those values fall through to String(value). The file's own header lists "format values by field type" as step 3. The fix is to route numeric types through the same formatter the field renderer uses (currency code and scale from the field definition, and the viewer's locale).

Severity

Minor display inconsistency. No data is wrong.

Dedupe: searched open and closed issues for history or audit currency formatting. The hits are the multi-currency epic cards (#27184, #27188), which concern record-mode currency resolution, not the history renderer.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 业务对象与记录 | records-forms.crud-roundtrip | P②

    Triage: p3 · domain:ui · area:records · 2026-10-29T12:07Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    Claim: PM loop round 1
    Session: session_01Utp6CbDAucNyQ4UtzCJKyh
    Account: os-bill
    Branch: claude/project-thread-q6ulq0 (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-27486
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/app-shell/src/utils/auditHistoryDisplay.ts and its tests, ui/packages/fields only to export an existing formatter (no behaviour change), root .changeset/; ⛔ RecordDetailView.tsx, ⛔ epic #8345 files, ⛔ packages/** outside ui/ (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier on 72c01528: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ui code (formatScalar) has no numeric arm | the field renderer's formatter already formats these types on the record page | every console user reading a record's history, today
    Thread-read: 6496229595
    Premise: holds
    Serial constraints cleared: no open PR edits auditHistoryDisplay.ts (open PR file scan at 14:31Z); no in-flight claim names it

    Dispatched 2026-10-29T14:35Z to a card thread of the seat's project; the order lives in the project. ⛔ The thread posts no claim and writes no assignee.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CXcn7RbCYknff87Ke1
    os-dev-report

    Posted 2026-10-29T15:46Z for draft PR #27638 at head fded0c5.

    {
      "issue": 27486,
      "status": "done",
      "branch": "claude/project-thread-q6ulq0",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27638",
      "head": "fded0c5f",
      "session": "session_01Utp6CXcn7RbCYknff87Ke1",
      "premise_still_valid": true,
      "summary": "auditHistoryDisplay formatScalar() had no arm for currency / number / percent (re-measured on origin/main 492703e1), so the History Changes entry printed String(value). It now formats a finite stored value through the record page cells' own calls in @object-ui/fields: formatCurrency + resolveFieldCurrency, formatNumberFieldValue, formatPercent + percentCellScale + resolveFieldScale, in the display locale. Currency modes: fixed reads its declared code; dynamic reads ctx.tenantCurrency (new optional context member) and, absent, the formatter's no-code plain number; record mode resolves to no code (the row's sibling code field is not handed to a per-value call) and reads as a plain number, never the tenant default. formatNumberFieldValue is newly exported from the @object-ui/fields entry and its locale parameter made optional; no rendering change in fields.",
      "tests": "head fded0c5f. pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/ : Test Files 28 passed | 1 skipped, Tests 433 passed | 8 skipped (the new file: 12/12). pnpm --filter @object-ui/app-shell run type-check (both tsconfigs) and @object-ui/fields type-check: exit 0. Ablation via scripts/ablation-replace.mjs: anchor 'if (num !== null) return formatNumericAmount(type, def, num, ctx);' replaced with an '&& false' guard, anchor 1 -> 0, blob 1f5e5663 -> f1635d11; new file 10 failed | 2 passed (the 2 are the scale-0 year and non-numeric cases, equal to String(value) by construction); restore blob == HEAD 1f5e5663, git diff HEAD empty. No dist build involved (the subject is imported by relative path).",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on fded0c5f: 34 commands",
        "ran": "34/34 exit 0 on fded0c5f, check-sdui-manifest and its self-test included; pnpm --dir ui exec vitest run scripts/ under the lock: Test Files 169 passed | 2 skipped, Tests 4902 passed | 2 skipped",
        "reconcile": "dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN",
        "first_pass": "the first pass (on c00377b3) went red on ui/scripts/__tests__/percent-storage-callers-11475.test.ts: the on-disk population of formatPercent callers grew by auditHistoryDisplay.ts; fixed by its table row in fded0c5f",
        "extra": "pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0",
        "ci": "in_progress at report time; the seat reads the PR check-runs",
        "ran_file": "/mnt/project-files/ui-seat4/27486-gates-ran.txt"
      },
      "line_budget": "6 files, +227 / -3 (numstat vs origin/main merge base); no budget declared in the order",
      "files_changed": [
        ".changeset/27486-history-numeric-amounts.md",
        "ui/packages/app-shell/src/utils/__tests__/auditHistoryDisplay.numericAmounts-27486.test.ts",
        "ui/packages/app-shell/src/utils/auditHistoryDisplay.ts",
        "ui/packages/fields/src/index.tsx",
        "ui/packages/fields/src/widgets/number-format.ts",
        "ui/scripts/__tests__/percent-storage-callers-11475.test.ts"
      ],
      "deviations": [
        "file surface widened by one repo-wide pin: ui/scripts/__tests__/percent-storage-callers-11475.test.ts gains the row for the new formatPercent caller (3 lines); the pin requires it of every new caller and went red without it",
        "ui/packages/fields/src/widgets/number-format.ts: formatNumberFieldValue's locale parameter widened from string to optional string (type only; every existing caller passes a string), so the History helper can hand over its optional display locale without a cast",
        "PR body attribution line 2 links this card thread (the session's own attribution block) rather than the seat thread link in the order text",
        "report header: the order's signature line is line 1 and the os-dev-report marker line 2"
      ],
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2: POST /repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create, run 40904760061, PR #27638 read back byte-identical, assignees none); this report comment via post-stamped. Harness after-PR assign/review step skipped per the order.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: seat (record-page chain owner). RecordDetailView's History effect builds its format context as { t, locale, lookupLabels } without tenantCurrency, so a dynamic-mode currency field reads as a plain number in history while the record page shows the tenant symbol; one-line wiring from useLocalization().currency in RecordDetailView.tsx, out of this order's bounds. Not filed. Dedupe words: history tenant currency, auditHistoryDisplay tenantCurrency, dynamic currency timeline",
        "carrier: none. Record-mode currency in history shows no symbol: a per-side code needs each side's audit payload handed to the formatter (caller change). Noted in Acceptance notes only."
      ]
    }
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    Claim amendment · 2026-10-29T15:47Z · to Claim 6499598670 (same session, branch claude/project-thread-q6ulq0 and assignee); supersedes that claim's Clause-②: and File surface: lines only.

    Clause-②: yes (widening)
    Clause-② basis: read by the seat on PR #27638 head fded0c5f — @object-ui/fields is published (private unset); its package entry now re-exports formatNumberFieldValue (module-internal before) and that function's locale parameter becomes optional. Additive; nothing narrows. The export exists so the History tab calls the record page's own formatter, per triage's 「No second currency formatter」. Lane precedent: #25908, #27480. The seat records a TIER_DEFAULT contract review on the final head before landing.
    File surface: adds ui/packages/fields/src/index.tsx (one re-export), ui/packages/fields/src/widgets/number-format.ts (the optional locale type), and one ledger row in ui/scripts/__tests__/percent-storage-callers-11475.test.ts (the repo-wide pin that lists every formatPercent caller).


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

    ACCEPT → PR #27638 comment 6501861574 · 2026-10-29T16:06Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uipm:dispatchedpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions