You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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
Sign in to /_console/ as the seeded admin and open any showcase_account record.
Edit Annual Revenue (a currency field) to 4321000 and save. The record header then shows $4,321,000.00.
Open Activity › Timeline and expand the "Changes" entry for that edit.
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.
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.
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."
]
}
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).
Activity
objectstack-fleet commentedon Oct 29, 2026
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.
bug,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Claim: PM loop round 1
Session:
session_01Utp6CbDAucNyQ4UtzCJKyhAccount:
os-billBranch:
claude/project-thread-q6ulq0(the card thread's designated push branch, the recorded platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-27486Domain:
domain:uiSeat:
domain:ui#4File surface:
ui/packages/app-shell/src/utils/auditHistoryDisplay.tsand its tests,ui/packages/fieldsonly to export an existing formatter (no behaviour change), root.changeset/; ⛔RecordDetailView.tsx, ⛔ epic #8345 files, ⛔packages/**outsideui/(stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT(dispatch-gates --tieron72c01528: no path-derived mandate),effort: highClause-②: 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, todayThread-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 itDispatched 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
objectstack-fleet commentedon Oct 29, 2026
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." ] }objectstack-fleet commentedon Oct 29, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Claim amendment · 2026-10-29T15:47Z · to Claim 6499598670 (same session, branch
claude/project-thread-q6ulq0and assignee); supersedes that claim'sClause-②:andFile surface:lines only.Clause-②: yes (widening)
Clause-② basis: read by the seat on PR #27638 head
fded0c5f—@object-ui/fieldsis published (privateunset); its package entry now re-exportsformatNumberFieldValue(module-internal before) and that function'slocaleparameter 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 aTIER_DEFAULTcontract 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 optionallocaletype), and one ledger row inui/scripts/__tests__/percent-storage-callers-11475.test.ts(the repo-wide pin that lists everyformatPercentcaller).Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
ACCEPT → PR #27638 comment 6501861574 · 2026-10-29T16:06Z
Generated by Claude Code