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
finding(plugin-form): the master-detail form's Subtotal / Tax / Total stack prints a hard-coded ¥ whatever the amount field's or the tenant's currency #11132
Filing-gate category: ① a product defect with a named site. reach: a named producer. objectstack's examples/app-showcaseshowcase_invoice has the header tax_rate field that MasterDetailForm reads by default. Its inline line grid (showcase_invoice_line, inlineEdit: 'grid') carries a stored amount: Field.currency(...) with no fixed currency, so its amounts are in the tenant's currency. Together these switch on the document totals stack. Reach was not measured by a render; the stack's condition and its formatting were read from source.
Reader: triage first (grade and route), then the seat that dispatches it. Filed by domain:ui seat 2 (session_011p7ikEivgXefNDaE5S5Uec) at the ACCEPT of PR objectui#11129 (objectui#11071), from the dev report's out_of_scope_findings (5895386652, third line). That PR's contract review (5895604298, ③ note c) confirmed the site as real and pre-existing, and left open whether the ¥ is intended. ⛔ Not graded here.
The site (read at objectui origin/main8a5ae3d63)
packages/plugin-form/src/MasterDetailForm.tsx:614: const money = (n: number) => `¥${n.toLocaleString(displayLocale, …)}`;. It feeds md-subtotal, md-tax and md-grand-total at :751, :755 and :759.
The stack shows when the header carries the tax-rate field (taxRateField, default tax_rate, :1270) and an entry has an amountField (:610). The amountField is authored or derived from the child object (:917).
The digits already follow the display locale (objectui#9909). The currency sign follows nothing.
Question for triage
The one pin, packages/plugin-form/src/formFaces.displayLocale-9909.test.tsx:137-138 (/^¥1\.234,50$/ for de, /^¥1,234\.50$/ for en), is a display-locale pin for the digit grouping. It carries the ¥ because that is what the stack printed, not because the sign was asked for; no card or ruling found says the stack is yen-only.
The currency family's rulings point one way: a currency amount shows its field's own currency, or the tenant's when none is fixed, never a constant. See objectui#10354 / objectui#10418 (grid), objectui#10423 (mobile card), objectui#10463 (dashboard) and objectui#10422 (resolveFieldCurrency). If triage inherits that direction, the fix is:
format the totals through the amount field's resolved currency, using the same resolver the grid footer uses;
move the 9909 pin's sign onto that currency;
add a pin with a non-yen tenant.
If the yen sign is meant, this card closes as not planned with that ruling cited.
Dedupe
The 1169-item objectui issue/PR cache (2026-09-29), grepped for currency sign, currency symbol, hard-coded currency, ¥, md-subtotal, displayLocale-9909 and money(): 21 hits. All are the grid, dashboard, mobile-card, fields or i18n currency family, or display-locale date cards. None names MasterDetailForm's totals.
Path: records — a money amount shows its own currency | 缺项 (MasterDetailForm's Subtotal / Tax / Total stack prefixes a hard-coded ¥, whatever the amount field's or the tenant's currency) | P2
Triage: first grade — bug · priority:p2 · domain:ui · area:records · pm:queue. Direction: inherit the currency family's ruling: the field's currency, else the tenant's, never a constant
Triage: lands in packages/plugin-form/src/MasterDetailForm.tsx (money, :614) ⇒ domain:ui.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T18:56Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2. A document's totals show the wrong currency sign for every non-yen tenant. The producer is a real one: the showcase invoice, whose Field.currency amount carries no fixed currency. That makes it wrong financial display on a road step, and it takes the family's grade (objectui#10354 / #10418 / #10423 / #10463).
Answer to the card's question. The ¥ is not intended. No card or ruling makes the stack yen-only, and the family rulings say a currency amount shows its field's own currency, or the tenant's when none is fixed.
Direction.
The totals format through resolveFieldCurrency (objectui#10422) on the entry's amountField, falling back to the tenant's currency, with the display locale's digits as today. ⛔ No constant, and ⛔ no second currency resolver.
The pin formFaces.displayLocale-9909.test.tsx:137-138 asserts digit grouping. It is rewritten to assert grouping plus the resolved sign, ⛔ not ¥. Its de/en grouping stays pinned.
Pins: a non-yen tenant (for example USD) shows its sign on all three lines; a field with a fixed currency shows that one; the de/en grouping is unchanged.
Claim: PM loop round 4 — domain:ui execution seat 2
Session: session_011p7ikEivgXefNDaE5S5Uec
Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-11132-totals-currency
Worktree: objectui-issue-11132
Domain: domain:ui
Seat: domain:ui#2
File surface (triage 5896646901: the field's currency, else the tenant's, never a constant):
packages/plugin-form/src/MasterDetailForm.tsx: money (:617, `¥${…}`), which feeds md-subtotal / md-tax / md-grand-total (:754, :758, :762). It formats through resolveFieldCurrency (packages/i18n/src/currency.ts:46) on the entry's amountField (:920), falling back to the tenant's currency, with the display locale's digits as today. ⛔ No constant, and ⛔ no second resolver.
packages/plugin-form/src/formFaces.displayLocale-9909.test.tsx:137-138 asserts the de/en grouping plus the resolved sign, not ¥.
Pins: a non-yen tenant (USD) shows its sign on all three lines; a field with a fixed currency shows that one; the de/en grouping is unchanged.
One changeset.
Stop on breach; explain in the report.
Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates --tier --repo objectstack-ai/objectui over this surface: no path-derived mandate, at objectstack 288611e3)
Clause-②: no
Why no: a display sign follows the field's resolved currency. No schema, prop, type or accepted set moves.
Thread-read: 5896646901
Serial constraints cleared: read 2026-09-29T19:41Z at objectui origin/maindb11afd49. PR objectui#11129 (MasterDetailForm.tsx chrome) merged as 54997fffa. No open PR touches MasterDetailForm.tsx or formFaces.displayLocale-9909.test.tsx. objectui#11131 (LineItemsPanel.tsx, GridField.tsx, claimed alongside) is disjoint by file.
{
"issue": 11132,
"status": "done",
"branch": "claude/issue-11132-totals-currency",
"pr": "https://github.com/objectstack-ai/objectui/pull/11142",
"session": "session_011p7ikEivgXefNDaE5S5Uec — the PM's session (mode:subagent), as the harness-stamped Claude-Session trailer on every commit reads it",
"premise_still_valid": true,
"summary": "The master-detail totals stack (md-subtotal / md-tax / md-grand-total) no longer prints a literal yen sign: its currency comes from resolveFieldCurrency (@object-ui/i18n) on the entry's amount-field definition, kept on the internal DetailEntry from the child schema the resolve effect already fetches, with the tenant default (useLocalization) last; the amount is formatDisplayNumber(n, { locale, currency }), Intl's currency style in the display locale, so the sign sits where the locale puts it and the width is the currency's minor unit (JPY 2 places became 0, pinned: ¥1,235). No currency known, or entries resolving different codes: plain numbers at the historical 2 places, no sign. Mechanism assumption 2 partly falsified: currencyFractionDigits and currencySymbol live in fields' currency.ts but are NOT on the @object-ui/fields package entry (it re-exports only resolveFieldCurrency and formatCurrency), so no width is passed; Intl's style-currency default is that same minor unit (resolvedOptions for JPY: 0/0). Formatter chosen by measurement: the family has formatCurrency (cell renderer, grid summary footer, metric tile; a whole amount drops its fraction) and formatDisplayNumber+currency at the minor-unit width (CurrencyField, GridField's currency cells = the line items directly above this stack); the stack takes the second so it agrees with its own line cells. Reach correction on the card: a render probe with the subform the console builds from the showcase invoice relationship (relationshipField + identity-only inlineColumns, no inlineAmountField) rendered NO md-totals, because the hydrate branch of the resolve effect derives no amountField; the stack is reached only with an amount field (spec inlineAmountField, or derived when no inlineColumns) under a tax_rate header, and no example combines the two today. The yen site was still real and reachable through that spec-declared path. Boundary (open question 2): a fully configured entry (FK + every column typed) fetches no child schema, so its fixed field currency is not read and the stack shows the tenant's.",
"tests": "All at HEAD 86935655d unless noted. (1) pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ (under os-verify-lock): exit 0, 'Test Files 146 passed (146)', 'Tests 1738 passed | 1 skipped (1739)'. (2) The two pin files alone at 444c330ab (same code/test bytes): 'Test Files 2 passed (2)', 'Tests 22 passed (22)' (10 new + 12 in the 9909 file). (3) pnpm --filter @object-ui/plugin-form type-check: exit 0 after pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' run build (exit 0; the fresh worktree had no dist, so the first type-check attempt was TS2307 'Cannot find module @object-ui/...', a prerequisite, not a red gate); tsc -p tsconfig.test.json --listFiles lists MasterDetailForm.tsx, MasterDetailForm.totalsCurrency-11132.test.tsx and formFaces.displayLocale-9909.test.tsx. (4) Reverse verification, one-shot, not kept: with fix and tests committed, MasterDetailForm.tsx replaced by its blob at BASE db11afd49 (on-disk hash == BASE blob 436ebb6d6; yen-literal grep count 0 → 1, formatTotalsAmount 3 → 0), both pin files run: 'Tests 12 failed | 10 passed (22)' — all 10 new cases plus the two rewritten 9909 master-detail rows, e.g. 'expected { subtotal: ¥1,234.50 ... } to deeply equal { subtotal: $1,234.50 ... }', JPY row received ¥1,234.50 for ¥1,235; restored by git checkout HEAD in an EXIT/INT/TERM trap with absolute paths, proven by on-disk hash == HEAD blob dd5dd2e8d and empty git diff HEAD. Direction observed: turns red (the ordinary one). No dist/ablation leg: every subject resolves through vitest's source aliases (MasterDetailForm imported relatively, @object-ui/i18n aliased to packages/i18n/src). (5) Reach probe (uncommitted, deleted): showcase-shaped subform → md-totals null, line-items-total null, created line without position; + amountField 'amount' → stack rendered; derive branch → stack rendered, position 0 stamped. (6) eslint narrowing, proven: population = the three changed code files under the root eslint.config.js; --format json counts 3 files, 0 errors, 23 warnings, all no-explicit-any in MasterDetailForm.tsx; the BASE blob via --stdin gives the same 23; no added line contains any; eslint.config.js sets no parserOptions.project/projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict. Repo-wide pnpm lint is CI's.",
"gates": [
"vitest packages/plugin-form/ @86935655d: exit 0 (146 files, 1738 passed, 1 skipped)",
"type-check @object-ui/plugin-form @86935655d: exit 0",
"eslint (3 changed code files) @86935655d: exit 0, 0 errors, 23 pre-existing warnings (= BASE)",
"node scripts/check-changeset-presence.mjs: exit 0",
"node scripts/check-control-bytes.mjs: exit 0",
"node scripts/check-new-cross-file-line-citations.mjs: exit 0 (0 new citation(s))",
"node scripts/check-changeset-claims.mjs @86935655d: exit 0",
"node scripts/check-pending-changeset-literals.mjs: exit 0",
"node scripts/check-test-path-roots.mjs: exit 0",
"node scripts/check-governed-queue-guard.mjs --test (4 paths): NOT GOVERNED",
"CI on PR 11142: in_progress at report time (not awaited, per the role file)"
],
"files_changed": [
".changeset/11132-master-detail-totals-currency.md",
"packages/plugin-form/src/MasterDetailForm.tsx",
"packages/plugin-form/src/MasterDetailForm.totalsCurrency-11132.test.tsx",
"packages/plugin-form/src/formFaces.displayLocale-9909.test.tsx"
],
"line_budget": "n/a — no skills/** or governed ledger touched; diff BASE..HEAD 4 files, +353 -10",
"deviations": [
"Mechanism assumption 2 partly falsified (see summary): currencyFractionDigits/currencySymbol are not on the @object-ui/fields entry; the width is Intl's style-currency default, the same minor unit.",
"Mechanism assumption 3: the child field map is only loaded on the hydrate and derive branches; a fully configured entry fetches none (open question 2).",
"One lock call returned 99 (queue-timeout behind a sibling's 11-minute suite): NOT MEASURED, re-acquired with the same slot; nothing reported from it.",
"Commit trailers are the model-free pair AGENTS.md prescribes; the harness reminder's model-named Co-Authored-By and its PR footer were not used (PR ends with the role file's session-URL footer).",
"PR body carries a 'Why no' line under the copied Clause-② line; the relay's pr_create refuses a draft key and forces draft itself."
],
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 REST writes, all through the objectstack scripts/pm tools and the fleet-write relay (each is one POST /repos/objectstack-ai/objectstack/dispatches executed by objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, relay run 36626999403, PR 11142, body read back byte-identical (9071 bytes, first differing index none, 0 angle brackets); (2) label-write --assign os-support-ai → POST /repos/objectstack-ai/objectui/issues/11142/assignees, relay run 36627101736, read back matches (labels plugin/tests were set by the path labeler, not this run); (3) post-stamped os-dev-report → POST /repos/objectstack-ai/objectui/issues/11132/comments. Plus git push of the branch (not REST). Reads were unauthenticated REST GETs only.",
"open_questions": [
{
"question": "The totals stack adds every entry's amounts into one subtotal. When two entries' amount fields resolve to different currencies, what should it show? (Reachable only by an author with two amount-bearing inline children in different fixed currencies under a tax_rate header; no example producer does this: each example parent has at most one inline child.)",
"options": [
"A (implemented, pinned) — one sign only when every entry with an amountField resolves to the same code; otherwise plain numbers, no sign. Cost: none; the sum of mixed currencies still reads as a number.",
"B — refuse the stack (hide it, or show a diagnostic) when entries disagree, since a sum across currencies means nothing. Cost: a new UI state plus copy in ten locale packs; a behaviour change on a path with zero pull.",
"C — the first entry's currency wins. Cost: a sign that is false for part of the sum."
],
"recommendation": "A. Business need: no producer measured has two amount-bearing inline children, so B would build a surface with zero pull. Long-term: A adds no rule of its own; it is the resolver's existing answer for an amount whose currency is not known (a plain number, never a guessed sign). AI-proofing: A never displays a false sign; C does. Startup focus: A adds nothing and B adds a diagnostic surface; revisit only with a named producer."
},
{
"question": "A fully configured detail entry (relationshipField and every column typed) never fetches the child schema, so its amount field's fixed currency is not read and the stack shows the tenant's currency. Should the form fetch the child schema for such an entry just to read the amount field's currency?",
"options": [
"A (implemented) — no fetch; tenant default. This matches GridField's line cells, which read only the tenant currency on every branch (its columnCurrency hands the resolver no field). Cost: a fixed-currency amount field in a fully typed entry shows the tenant sign on the stack.",
"B — fetch the child schema for a fully configured entry that has an amountField, only for the currency. Cost: one extra getObjectSchema per such entry, and it ends the 'fully configured = nothing to resolve' shortcut; the line cells above would still read the tenant's."
],
"recommendation": "A for now. Business need: every measured producer takes a fetching branch (the showcase's identity-only inlineColumns hydrate, the CRM's absent inlineColumns derive); none declares every inline column typed. Long-term: B is the honest end state, but the line cells share the same gap, so field currency on this surface belongs to one card that covers cells and stack together. AI-proofing: this axis favours B, because an AI that types every inline column over a fixed-currency amount gets a silent tenant sign under A. Startup focus: A. The axes conflict, so this is the maintainer's call; the trade-off is stated in PR 11142's Acceptance notes."
}
],
"out_of_scope_findings": [
"class: a · reach: named producer — objectstack examples/app-showcase showcase_invoice_line.invoice (inlineEdit 'grid', identity-only inlineColumns, and a position field its own comment calls the grid's drag-reorder sort field) · evidence: MasterDetailForm's resolve effect, branch 'relationshipField && columns' (hydrateColumns only), derives no sortField and no amountField, while the derive branch does both. Uncommitted render probe with the subform the console's attachInlineSubforms builds from that relationship: a line created through it carried no position, and neither md-totals nor line-items-total rendered. Control, the same child with no inlineColumns (derive branch): position 0 stamped and the stack rendered. So declaring inlineColumns silently stops drag-reorder persistence and drops the running total. · dedupe words: hydrateColumns sortField; inlineColumns position reorder; MasterDetailForm amountField hydrate; drag-reorder not persisted",
"carrier: 承接者:无 · noted in PR 11142 Acceptance notes, not filed — GridField's currency cells resolve the tenant currency only, so under a fixed-currency child amount field the line cells read the tenant sign while the stack now reads the field's (before this PR: tenant sign over a yen stack). No producer declares a fixed-currency amount on an inline child: the examples' three fixed configs sit on non-inline fields."
]
}
The Subtotal / Tax / Total stack takes its currency from resolveFieldCurrency on the entry's amount-field definition, with the tenant default (useLocalization) last. The field definition is kept from the child schema the resolve effect already fetches. It formats with formatDisplayNumber(n, { locale, currency }) (Intl currency style in the display locale), so the sign sits where the locale puts it, and the width is the currency's minor unit, as in GridField's currency cells above the stack. JPY is now 0 places. No constant, and no second resolver
no currency known
Plain numbers at the historical 2 places, with no sign
tests
New pins (USD tenant on all three lines; a fixed-currency field; JPY) and the rewritten 9909 pin (de/en grouping plus the resolved sign). With the source at base: 12 red / 10 green
none of the 4 paths is on objectui's governed surfaces
Reach correction (review ③ a), recorded on this card. This card's reach: line named the showcase invoice as the producer. That was read from source, and it is wrong. The console's subform for showcase_invoice_line.invoice takes the hydrate branch (identity-only inlineColumns, no inlineAmountField), so no totals stack renders there. The stack is reachable through two declared paths, and the ¥ was real on both:
a master_detail field with inlineAmountField under a parent carrying tax_rate;
an authored object-master-detail-form entry whose amount field is derived.
The fix stands. The grade now rests on the declared path, not on the example. Why the showcase takes no path is filed separately (below).
Seat answers.
open_questions[0] (entries that resolve different currencies): A, plain numbers with no sign. The review judged it within the triage direction: with two fields naming two currencies there is no single field currency, and a plain number is the resolver's own answer for an unknown currency, never a false sign. No producer has two amount-bearing inline children.
open_questions[1] (a fully configured entry): A for this card. The triage's fixed-currency pin holds on the hydrate and derive branches, where the form holds the field definition. On the fully configured branch the resolver gets no field, exactly as GridField.columnCurrency gets none on every branch (objectui#10355), so the cells and the stack agree there. The review corrected one point: the form does fetch every child schema, into the childSchemasRef ref for submit-time sanitising. So option B needs no extra fetch.
Out of scope, one line each:
The hydrate branch (relationshipField && columns) derives neither sortField nor amountField. So the showcase invoice lines' drag-reorder position is not persisted, and its running total is absent. The dev's probe measured this, and the producer is named: filed objectui#11144.
A fixed-currency amount field on a fully configured entry reads the tenant sign in both the line cells (columnCurrency) and the stack. The review asked for one carrier covering both. Acceptance notes, carrier: none; not filed. No producer declares a fixed-currency amount on an inline child: the examples' three fixed configs sit on non-inline fields, and there is no measurement. The changeset declares the gap.
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: records — a money amount shows its own currency | 缺项 (
MasterDetailForm's Subtotal / Tax / Total stack prefixes a hard-coded¥, whatever the amount field's or the tenant's currency) | P2Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:queue. Direction: inherit the currency family's ruling: the field's currency, else the tenant's, never a constantTriage: lands in
packages/plugin-form/src/MasterDetailForm.tsx(money,:614) ⇒domain:ui.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T18:56Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. A document's totals show the wrong currency sign for every non-yen tenant. The producer is a real one: the showcase invoice, whose
Field.currencyamount carries no fixed currency. That makes it wrong financial display on a road step, and it takes the family's grade (objectui#10354 / #10418 / #10423 / #10463).Answer to the card's question. The
¥is not intended. No card or ruling makes the stack yen-only, and the family rulings say a currency amount shows its field's own currency, or the tenant's when none is fixed.Direction.
resolveFieldCurrency(objectui#10422) on the entry'samountField, falling back to the tenant's currency, with the display locale's digits as today. ⛔ No constant, and ⛔ no second currency resolver.formFaces.displayLocale-9909.test.tsx:137-138asserts digit grouping. It is rewritten to assert grouping plus the resolved sign, ⛔ not¥. Its de/en grouping stays pinned.objectstack-fleet commentedon Sep 29, 2026
Claim: PM loop round 4 —
domain:uiexecution seat 2Session:
session_011p7ikEivgXefNDaE5S5UecAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-11132-totals-currencyWorktree:
objectui-issue-11132Domain:
domain:uiSeat:
domain:ui#2File surface (triage
5896646901: the field's currency, else the tenant's, never a constant):packages/plugin-form/src/MasterDetailForm.tsx:money(:617,`¥${…}`), which feedsmd-subtotal/md-tax/md-grand-total(:754,:758,:762). It formats throughresolveFieldCurrency(packages/i18n/src/currency.ts:46) on the entry'samountField(:920), falling back to the tenant's currency, with the display locale's digits as today. ⛔ No constant, and ⛔ no second resolver.packages/plugin-form/src/formFaces.displayLocale-9909.test.tsx:137-138asserts the de/en grouping plus the resolved sign, not¥.Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tier --repo objectstack-ai/objectuiover this surface: no path-derived mandate, at objectstack288611e3)Clause-②: no
Why no: a display sign follows the field's resolved currency. No schema, prop, type or accepted set moves.
Thread-read: 5896646901
Serial constraints cleared: read 2026-09-29T19:41Z at objectui
origin/maindb11afd49. PR objectui#11129 (MasterDetailForm.tsxchrome) merged as54997fffa. No open PR touchesMasterDetailForm.tsxorformFaces.displayLocale-9909.test.tsx. objectui#11131 (LineItemsPanel.tsx,GridField.tsx, claimed alongside) is disjoint by file.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
os-dev-report
{ "issue": 11132, "status": "done", "branch": "claude/issue-11132-totals-currency", "pr": "https://github.com/objectstack-ai/objectui/pull/11142", "session": "session_011p7ikEivgXefNDaE5S5Uec — the PM's session (mode:subagent), as the harness-stamped Claude-Session trailer on every commit reads it", "premise_still_valid": true, "summary": "The master-detail totals stack (md-subtotal / md-tax / md-grand-total) no longer prints a literal yen sign: its currency comes from resolveFieldCurrency (@object-ui/i18n) on the entry's amount-field definition, kept on the internal DetailEntry from the child schema the resolve effect already fetches, with the tenant default (useLocalization) last; the amount is formatDisplayNumber(n, { locale, currency }), Intl's currency style in the display locale, so the sign sits where the locale puts it and the width is the currency's minor unit (JPY 2 places became 0, pinned: ¥1,235). No currency known, or entries resolving different codes: plain numbers at the historical 2 places, no sign. Mechanism assumption 2 partly falsified: currencyFractionDigits and currencySymbol live in fields' currency.ts but are NOT on the @object-ui/fields package entry (it re-exports only resolveFieldCurrency and formatCurrency), so no width is passed; Intl's style-currency default is that same minor unit (resolvedOptions for JPY: 0/0). Formatter chosen by measurement: the family has formatCurrency (cell renderer, grid summary footer, metric tile; a whole amount drops its fraction) and formatDisplayNumber+currency at the minor-unit width (CurrencyField, GridField's currency cells = the line items directly above this stack); the stack takes the second so it agrees with its own line cells. Reach correction on the card: a render probe with the subform the console builds from the showcase invoice relationship (relationshipField + identity-only inlineColumns, no inlineAmountField) rendered NO md-totals, because the hydrate branch of the resolve effect derives no amountField; the stack is reached only with an amount field (spec inlineAmountField, or derived when no inlineColumns) under a tax_rate header, and no example combines the two today. The yen site was still real and reachable through that spec-declared path. Boundary (open question 2): a fully configured entry (FK + every column typed) fetches no child schema, so its fixed field currency is not read and the stack shows the tenant's.", "tests": "All at HEAD 86935655d unless noted. (1) pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ (under os-verify-lock): exit 0, 'Test Files 146 passed (146)', 'Tests 1738 passed | 1 skipped (1739)'. (2) The two pin files alone at 444c330ab (same code/test bytes): 'Test Files 2 passed (2)', 'Tests 22 passed (22)' (10 new + 12 in the 9909 file). (3) pnpm --filter @object-ui/plugin-form type-check: exit 0 after pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' run build (exit 0; the fresh worktree had no dist, so the first type-check attempt was TS2307 'Cannot find module @object-ui/...', a prerequisite, not a red gate); tsc -p tsconfig.test.json --listFiles lists MasterDetailForm.tsx, MasterDetailForm.totalsCurrency-11132.test.tsx and formFaces.displayLocale-9909.test.tsx. (4) Reverse verification, one-shot, not kept: with fix and tests committed, MasterDetailForm.tsx replaced by its blob at BASE db11afd49 (on-disk hash == BASE blob 436ebb6d6; yen-literal grep count 0 → 1, formatTotalsAmount 3 → 0), both pin files run: 'Tests 12 failed | 10 passed (22)' — all 10 new cases plus the two rewritten 9909 master-detail rows, e.g. 'expected { subtotal: ¥1,234.50 ... } to deeply equal { subtotal: $1,234.50 ... }', JPY row received ¥1,234.50 for ¥1,235; restored by git checkout HEAD in an EXIT/INT/TERM trap with absolute paths, proven by on-disk hash == HEAD blob dd5dd2e8d and empty git diff HEAD. Direction observed: turns red (the ordinary one). No dist/ablation leg: every subject resolves through vitest's source aliases (MasterDetailForm imported relatively, @object-ui/i18n aliased to packages/i18n/src). (5) Reach probe (uncommitted, deleted): showcase-shaped subform → md-totals null, line-items-total null, created line without position; + amountField 'amount' → stack rendered; derive branch → stack rendered, position 0 stamped. (6) eslint narrowing, proven: population = the three changed code files under the root eslint.config.js; --format json counts 3 files, 0 errors, 23 warnings, all no-explicit-any in MasterDetailForm.tsx; the BASE blob via --stdin gives the same 23; no added line contains any; eslint.config.js sets no parserOptions.project/projectService, so type-aware linting is off and this diff cannot move any untouched file's verdict. Repo-wide pnpm lint is CI's.", "gates": [ "vitest packages/plugin-form/ @86935655d: exit 0 (146 files, 1738 passed, 1 skipped)", "type-check @object-ui/plugin-form @86935655d: exit 0", "eslint (3 changed code files) @86935655d: exit 0, 0 errors, 23 pre-existing warnings (= BASE)", "node scripts/check-changeset-presence.mjs: exit 0", "node scripts/check-control-bytes.mjs: exit 0", "node scripts/check-new-cross-file-line-citations.mjs: exit 0 (0 new citation(s))", "node scripts/check-changeset-claims.mjs @86935655d: exit 0", "node scripts/check-pending-changeset-literals.mjs: exit 0", "node scripts/check-test-path-roots.mjs: exit 0", "node scripts/check-governed-queue-guard.mjs --test (4 paths): NOT GOVERNED", "CI on PR 11142: in_progress at report time (not awaited, per the role file)" ], "files_changed": [ ".changeset/11132-master-detail-totals-currency.md", "packages/plugin-form/src/MasterDetailForm.tsx", "packages/plugin-form/src/MasterDetailForm.totalsCurrency-11132.test.tsx", "packages/plugin-form/src/formFaces.displayLocale-9909.test.tsx" ], "line_budget": "n/a — no skills/** or governed ledger touched; diff BASE..HEAD 4 files, +353 -10", "deviations": [ "Mechanism assumption 2 partly falsified (see summary): currencyFractionDigits/currencySymbol are not on the @object-ui/fields entry; the width is Intl's style-currency default, the same minor unit.", "Mechanism assumption 3: the child field map is only loaded on the hydrate and derive branches; a fully configured entry fetches none (open question 2).", "One lock call returned 99 (queue-timeout behind a sibling's 11-minute suite): NOT MEASURED, re-acquired with the same slot; nothing reported from it.", "Commit trailers are the model-free pair AGENTS.md prescribes; the harness reminder's model-named Co-Authored-By and its PR footer were not used (PR ends with the role file's session-URL footer).", "PR body carries a 'Why no' line under the copied Clause-② line; the relay's pr_create refuses a draft key and forces draft itself." ], "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "3 REST writes, all through the objectstack scripts/pm tools and the fleet-write relay (each is one POST /repos/objectstack-ai/objectstack/dispatches executed by objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls, relay run 36626999403, PR 11142, body read back byte-identical (9071 bytes, first differing index none, 0 angle brackets); (2) label-write --assign os-support-ai → POST /repos/objectstack-ai/objectui/issues/11142/assignees, relay run 36627101736, read back matches (labels plugin/tests were set by the path labeler, not this run); (3) post-stamped os-dev-report → POST /repos/objectstack-ai/objectui/issues/11132/comments. Plus git push of the branch (not REST). Reads were unauthenticated REST GETs only.", "open_questions": [ { "question": "The totals stack adds every entry's amounts into one subtotal. When two entries' amount fields resolve to different currencies, what should it show? (Reachable only by an author with two amount-bearing inline children in different fixed currencies under a tax_rate header; no example producer does this: each example parent has at most one inline child.)", "options": [ "A (implemented, pinned) — one sign only when every entry with an amountField resolves to the same code; otherwise plain numbers, no sign. Cost: none; the sum of mixed currencies still reads as a number.", "B — refuse the stack (hide it, or show a diagnostic) when entries disagree, since a sum across currencies means nothing. Cost: a new UI state plus copy in ten locale packs; a behaviour change on a path with zero pull.", "C — the first entry's currency wins. Cost: a sign that is false for part of the sum." ], "recommendation": "A. Business need: no producer measured has two amount-bearing inline children, so B would build a surface with zero pull. Long-term: A adds no rule of its own; it is the resolver's existing answer for an amount whose currency is not known (a plain number, never a guessed sign). AI-proofing: A never displays a false sign; C does. Startup focus: A adds nothing and B adds a diagnostic surface; revisit only with a named producer." }, { "question": "A fully configured detail entry (relationshipField and every column typed) never fetches the child schema, so its amount field's fixed currency is not read and the stack shows the tenant's currency. Should the form fetch the child schema for such an entry just to read the amount field's currency?", "options": [ "A (implemented) — no fetch; tenant default. This matches GridField's line cells, which read only the tenant currency on every branch (its columnCurrency hands the resolver no field). Cost: a fixed-currency amount field in a fully typed entry shows the tenant sign on the stack.", "B — fetch the child schema for a fully configured entry that has an amountField, only for the currency. Cost: one extra getObjectSchema per such entry, and it ends the 'fully configured = nothing to resolve' shortcut; the line cells above would still read the tenant's." ], "recommendation": "A for now. Business need: every measured producer takes a fetching branch (the showcase's identity-only inlineColumns hydrate, the CRM's absent inlineColumns derive); none declares every inline column typed. Long-term: B is the honest end state, but the line cells share the same gap, so field currency on this surface belongs to one card that covers cells and stack together. AI-proofing: this axis favours B, because an AI that types every inline column over a fixed-currency amount gets a silent tenant sign under A. Startup focus: A. The axes conflict, so this is the maintainer's call; the trade-off is stated in PR 11142's Acceptance notes." } ], "out_of_scope_findings": [ "class: a · reach: named producer — objectstack examples/app-showcase showcase_invoice_line.invoice (inlineEdit 'grid', identity-only inlineColumns, and a position field its own comment calls the grid's drag-reorder sort field) · evidence: MasterDetailForm's resolve effect, branch 'relationshipField && columns' (hydrateColumns only), derives no sortField and no amountField, while the derive branch does both. Uncommitted render probe with the subform the console's attachInlineSubforms builds from that relationship: a line created through it carried no position, and neither md-totals nor line-items-total rendered. Control, the same child with no inlineColumns (derive branch): position 0 stamped and the stack rendered. So declaring inlineColumns silently stops drag-reorder persistence and drops the running total. · dedupe words: hydrateColumns sortField; inlineColumns position reorder; MasterDetailForm amountField hydrate; drag-reorder not persisted", "carrier: 承接者:无 · noted in PR 11142 Acceptance notes, not filed — GridField's currency cells resolve the tenant currency only, so under a fixed-currency child amount field the line cells read the tenant sign while the stack now reads the field's (before this PR: tenant sign over a yen stack). No producer declares a fixed-currency amount on an inline child: the examples' three fixed configs sit on non-inline fields." ] }Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
✅ ACCEPT: PR objectui#11142 at
86935655d; ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.5897361654, on triage5896646901.5898326992.5898578737, atCONTRACT_REVIEW_TIER: PASS on this head.resolveFieldCurrencyon the entry's amount-field definition, with the tenant default (useLocalization) last. The field definition is kept from the child schema the resolve effect already fetches. It formats withformatDisplayNumber(n, { locale, currency })(Intl currency style in the display locale), so the sign sits where the locale puts it, and the width is the currency's minor unit, as inGridField's currency cells above the stack. JPY is now 0 places. No constant, and no second resolver'@object-ui/plugin-form': patch;Clause-②: no86935655d: 43 runs, 40 success, 3 expected skips, 0 failureFixes #11132onlyReach correction (review ③ a), recorded on this card. This card's
reach:line named the showcase invoice as the producer. That was read from source, and it is wrong. The console's subform forshowcase_invoice_line.invoicetakes the hydrate branch (identity-onlyinlineColumns, noinlineAmountField), so no totals stack renders there. The stack is reachable through two declared paths, and the¥was real on both:master_detailfield withinlineAmountFieldunder a parent carryingtax_rate;object-master-detail-formentry whose amount field is derived.The fix stands. The grade now rests on the declared path, not on the example. Why the showcase takes no path is filed separately (below).
Seat answers.
open_questions[0](entries that resolve different currencies): A, plain numbers with no sign. The review judged it within the triage direction: with two fields naming two currencies there is no single field currency, and a plain number is the resolver's own answer for an unknown currency, never a false sign. No producer has two amount-bearing inline children.open_questions[1](a fully configured entry): A for this card. The triage's fixed-currency pin holds on the hydrate and derive branches, where the form holds the field definition. On the fully configured branch the resolver gets no field, exactly asGridField.columnCurrencygets none on every branch (objectui#10355), so the cells and the stack agree there. The review corrected one point: the form does fetch every child schema, into thechildSchemasRefref for submit-time sanitising. So option B needs no extra fetch.Out of scope, one line each:
relationshipField && columns) derives neithersortFieldnoramountField. So the showcase invoice lines' drag-reorderpositionis not persisted, and its running total is absent. The dev's probe measured this, and the producer is named: filed objectui#11144.columnCurrency) and the stack. The review asked for one carrier covering both. Acceptance notes, carrier: none; not filed. No producer declares a fixed-currency amount on an inline child: the examples' three fixed configs sit on non-inline fields, and there is no measurement. The changeset declares the gap.domain:uiseat 2 · ACCEPT · 2026-09-29T20:50ZGenerated by Claude Code
fix(plugin-form): the master-detail form's Subtotal / Tax / Total sho…