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
Filing gate ③ (maintainer-directed). Ruling 6465431966 on #27119 (batch #421 item 2, Q1 = 乙-现在, maintainer 「同意立专题卡」 2026-10-28): the multi-currency epic resumes on #8345 in shape C′. Part of #8345. Filed by the director seat (session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). ⛔ Not a claim. Workload row and precedents: 6464936321; design: 6464438764.
Decimal arithmetic in minor units; rounding half-up documented in the ADR; CURRENCY_FRACTION_DIGITS reader.
Refusal ERR_EXCHANGE_RATE_MISSING naming currency and date; same-currency shortcut.
Door: home / rate fields refuse client writes (precedent summary).
Import path and REST writes verified to pass through the same engine path (reading recorded on the card).
Tests on memory, SQLite, Postgres: three currencies, three minor-unit classes, update paths (amount / code / date change).
Lane: server · Size: L · Review: default; contract review of the refusal envelope. Clause-②: no · Metadata-protocol: none. Early WIP commits (container death is a known failure mode).
Epic: #8345 (parent; the epic PM dispatches pm:epic children; domain:spec children go to the spec seat's queue). Dedupe: #8345 thread, #27119 (ruled), the workload list 6464936321; no open card carries this slice.
Note for whoever takes this card · 2026-10-30T15:31Z · ⛔ not a claim. From the #27191 dev (draft PR #28096, head 361f8f25, os-dev-report 6526415684), read in code:
Write path:homeAmount.field and homeAmount.rateField must declare readonly: true (the spec's existing platform-written mark), and readonly strips every non-system write on both insert and update. So the projection has to store the home amount and the rate through a system-context write, or after the strip; otherwise its own write is dropped.
Hand-off from the contract review of PR #28096 · 2026-10-30T16:42Z · triage review 6526217217 (PASS, should-fix 2 and ① item 2) leaves two things for this card's engine projection to pin, since the homeAmount door does not refuse them:
A home or rate field may carry a defaultValue. A stripped INSERT field still falls back to its defaultValue, so the projection must run after the default fill and overwrite it (or this card refuses defaultValue on the two fields). Pin whichever it does.
The UPDATE-only preserveAudit exemption admits author-declared business readonly fields, so a historical import can write the home field directly. Pin whether the projection recomputes over that write or keeps it.
Claim: PM loop round 1 · 2026-10-31T02:55Z
Session: session_01Utp6CbTZrFqRYNgjSaSbN5 (epic PM, epic:#8345)
Account: os-bill
Branch: claude/project-thread-4g7rek (card thread session_01Utp6CYjLJ7sxfUehRrLbT3; the platform assigns the thread's one push branch — the recorded platform exception), cut from PR #28096's head cf13692214 (#27191 not yet on main; Merge-after: #28096, main merged in once it lands, never rebased)
Worktree: objectstack-issue-27192
Domain: domain:server
Seat: epic:#8345
File surface: packages/objectql/src/** (the projection, rate lookup, rounding, refusal and their tests), the engine-side door for the home and rate fields, content/docs/data-modeling/currency.mdx (the homeAmount passage), .changeset/*; ⛔ no governed path, ⛔ no packages/spec/src/** (stop on breach; explain in the report)
Container & model: L, mode:thread, model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate), effort: default (the platform's; only the maintainer sets thread effort)
Clause-②: no — to confirm against the tree
Metadata-protocol: none — to confirm against the tree
Responsibility: n/a — not a defect card
Thread-read: 6528146352
Premise: holds
Serial constraints cleared: #27185 landed (PR #27246, 194e66c0); #27191 approved and in auto-merge (PR #28096), built on its head with Merge-after: #28096.
Release pairing: R2 — ⛔ not released alone; R2's release waits for the customer's terms (ruling 6522614678).
Maintainer authority: Bill, project thread, 2026-10-28 「epic的卡你统一派」, 「并发可以5」, 「你自主完成后续阶段」; ruling 6522614678 (letter A, 「A 现在启动 R2 开发」).
{
"issue": 27192,
"status": "done",
"branch": "claude/project-thread-4g7rek",
"pr": "https://github.com/objectstack-ai/objectstack/pull/28302",
"head": "c37e52a6687cec1d61aa9227abfd8d7a21b4ca9f",
"session": "session_01Utp6CYjLJ7sxfUehRrLbT3",
"premise_still_valid": true,
"summary": "A record-mode amount that declares currencyConfig.homeAmount now stores, on insert and on any update of its amount, code or document date, home = amount / rate (exact decimal, rounded once half away from zero to the home currency's ISO 4217 minor units) and the rate row's own value unrounded. The rate row is the organization's latest sys_exchange_rate on or before the document date; same currency is rate 1 with no lookup. A missing rate refuses ERR_EXCHANGE_RATE_MISSING/400 and an unset localization.currency refuses ERR_HOME_CURRENCY_UNSET/400, never a default; a divergent update by filter is refused. Two accepted deviations into packages/spec (two ledger rows; the CURRENCY_FRACTION_DIGITS export line), no Zod shape change. #28096 landed 03:21Z; main is merged in, no rebase, so no Merge-after line.",
"write_paths_reading": "REST: packages/rest/src/crud-routes.ts create/update call protocol createData/updateData, which call engine.insert/engine.update. Import: runImport (sync route and job worker) writes through insertManyData (engine.insertMany then engine.insert) or createData/updateData per row; treatAsHistorical sets preserveAudit. Every door lands in the same insert or update seam, so all convert and refuse alike; validate() and the import dry run preview the same projection. Pinned in packages/objectql/src/home-amount-write-paths.test.ts on a booted kernel with ObjectQLPlugin, a settings service, the real protocol and runImport.",
"handoff_points": {
"6526609699 system write or after the strip": "after the readonly strip, in every context; a client value is dropped (or refused under strictReadonlyWrites) and the engine writes both fields. Pinned.",
"6526609699 rateField scale below 12": "the engine never rounds the rate. A readonly value is validated for shape only and every numeric column is the same exact decimal, so scale 4 reaches display only: 0.138912345678 is stored in full, home 71987.84. Pinned. Spec refusal raised as an open question below.",
"6526609699 currency.mdx homeAmount passage": "written: the section 'Converting into the home currency' replaces the 'later release' paragraph.",
"6528146352 defaultValue on home or rate field": "overwritten by the projection. Pinned.",
"6528146352 preserveAudit import write to the home field": "recomputed over, not kept. Pinned on the engine and through runImport with treatAsHistorical."
},
"tests": "At head c37e52a668: packages/objectql full suite 504/504 files, 9803/9803 tests; objectql tsc --noEmit exit 0; dogfood test/home-amount-projection.test.ts 18/18 (better-sqlite3, sqlite-wasm, PostgreSQL 16 local via OS_TEST_POSTGRES_URL); spec error-code-ledger tests pass. Ablations, each applied on disk then restored with git checkout: engine face over objectql's three suites (48 tests): A0 baseline 48/48; A1 $lte to $lt 1 red; A2 latest to earliest row 2 red; A3 rate row ignores the date 9 red; A4 half-away to truncate 7 red; A5 minor units fixed at 2 1 red; A6 float division 2 red; A7 multiply instead of divide 20 red. Driver face (objectql rebuilt into dist per ablation, dogfood 18 tests over 3 drivers): A1 3 red, A3 9 red, A4 9 red, A6 3 red; restore build 18/18.",
"gates": "dispatch-gates --commands at the first head derived 117 commands; each run with its exit code captured before any pipe: 115 exit 0, 2 exit 1. check-skills-token-ratchet: the merge of main had duplicated #28096's homeAmount paragraph in skills/objectstack-data/rules/field-types.md (the squash landed beside the branch's original commits); restored to main's text, so this PR no longer touches skills/**. check:error-code-casing: the write-path test's key column was named code, read as an error code; renamed ref. Re-derived at final head c37e52a668: 110 commands, all 110 exit 0; --ran reconciliation: 110 derived families accounted for, 110 run, 0 NOT-MEASURED. CI check-runs on the PR: not yet read at report time.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "MCP GitHub: issue_read, pull_request_read (reads); create_pull_request (the draft PR, no assignee, no reviewers); add_issue_comment (this report). No GitHub CLI or REST token in this session, so the PR and this comment went through MCP rather than post-stamped.mjs.",
"api_writes": "git push of WIP commits to claude/project-thread-4g7rek (each through the commit-card-trailers pre-push check); PR create and this comment via MCP as above.",
"open_questions": [
{
"question": "Open item 4: when localization.currency is unset, what does a write that must convert do?",
"options": ["A refuse ERR_HOME_CURRENCY_UNSET/400, naming Localization, Default currency (built)", "B write the row with a blank home amount and rate", "C assume a default home currency"],
"recommendation": "A, because a blank home amount silently drops the row from every consolidated total and an assumed currency stores a wrong number; the sources already rule out a silent default."
},
{
"question": "A datetime document date: which calendar day picks the rate?",
"options": ["A its UTC calendar day (built)", "B the organization's timezone day", "C refuse datetime as rateDate"],
"recommendation": "A for R2, because the engine has no organization timezone at the write seam today; B is a later refinement if a customer needs it."
},
{
"question": "Should the spec refuse (or warn on) a homeAmount rateField declared with scale below 12?",
"options": ["A spec lane adds a refusal at parse", "B leave it: the engine stores the full rate and scale reaches display only (built)"],
"recommendation": "A, a spec-lane card, because a displayed rate rounded to 4 places no longer reproduces the stored home amount for a reader."
}
],
"out_of_scope_findings": [
"Noted, not filed: merging main into a branch cut from a squash-merged PR's head duplicates that PR's text edits where the squash and the original commits differ (seen here in skills/objectstack-data/rules/field-types.md; check-skills-token-ratchet caught it). carrier: this PR · fixed in place by restoring main's text."
]
}
Activity
objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
Note for whoever takes this card · 2026-10-30T15:31Z · ⛔ not a claim. From the #27191 dev (draft PR #28096, head
361f8f25, os-dev-report 6526415684), read in code:homeAmount.fieldandhomeAmount.rateFieldmust declarereadonly: true(the spec's existing platform-written mark), andreadonlystrips every non-system write on both insert and update. So the projection has to store the home amount and the rate through a system-context write, or after the strip; otherwise its own write is dropped.rateFielddeclaringscalebelow 12 makes every converting write refuse withmax_scale. The refusal is loud, so feat(spec):currencyConfig.homeAmount— a record-mode amount declares its stored home-currency projection by pointer (multi-currency M2-1) #28096 adds no spec refusal for it; decide here whether the engine rounds or the spec should refuse.content/docs/data-modeling/currency.mdxdoes not mentionhomeAmount; the page belongs with the change that makes the key live.homeAmount: { field, rateDate, rateField? }on a record-mode amount — the stored home-currency projection declared by pointer (multi-currency M2-1, epic #8345; released with M2-2) #27191 key out of a Version Packages release ahead of this engine. The hold is the maintainer's hand on Version Packages (asked on PR feat(spec):currencyConfig.homeAmount— a record-mode amount declares its stored home-currency projection by pointer (multi-currency M2-1) #28096).objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
Hand-off from the contract review of PR #28096 · 2026-10-30T16:42Z · triage review 6526217217 (PASS, should-fix 2 and ① item 2) leaves two things for this card's engine projection to pin, since the
homeAmountdoor does not refuse them:defaultValue. A stripped INSERT field still falls back to itsdefaultValue, so the projection must run after the default fill and overwrite it (or this card refusesdefaultValueon the two fields). Pin whichever it does.preserveAuditexemption admits author-declared businessreadonlyfields, so a historical import can write the home field directly. Pin whether the projection recomputes over that write or keeps it.objectstack-fleet commentedon Oct 31, 2026
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Claim: PM loop round 1 · 2026-10-31T02:55Z
Session:
session_01Utp6CbTZrFqRYNgjSaSbN5(epic PM,epic:#8345)Account:
os-billBranch:
claude/project-thread-4g7rek(card threadsession_01Utp6CYjLJ7sxfUehRrLbT3; the platform assigns the thread's one push branch — the recorded platform exception), cut from PR #28096's headcf13692214(#27191 not yet onmain;Merge-after: #28096, main merged in once it lands, never rebased)Worktree:
objectstack-issue-27192Domain:
domain:serverSeat:
epic:#8345File surface:
packages/objectql/src/**(the projection, rate lookup, rounding, refusal and their tests), the engine-side door for the home and rate fields,content/docs/data-modeling/currency.mdx(thehomeAmountpassage),.changeset/*; ⛔ no governed path, ⛔ nopackages/spec/src/**(stop on breach; explain in the report)Container & model:
L,mode:thread,model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate),effort: default (the platform's; only the maintainer sets thread effort)Clause-②: no — to confirm against the tree
Metadata-protocol: none — to confirm against the tree
Responsibility: n/a — not a defect card
Thread-read: 6528146352
Premise: holds
Serial constraints cleared: #27185 landed (PR #27246,
194e66c0); #27191 approved and in auto-merge (PR #28096), built on its head withMerge-after: #28096.Release pairing: R2 — ⛔ not released alone; R2's release waits for the customer's terms (ruling 6522614678).
Maintainer authority: Bill, project thread, 2026-10-28 「epic的卡你统一派」, 「并发可以5」, 「你自主完成后续阶段」; ruling 6522614678 (letter A, 「A 现在启动 R2 开发」).
wip(objectql): home-amount projection module; register ERR_EXCHANGE_R…
wip(objectql): wire the home projection into insert, by-id and predic…
wip(objectql): home projection tests (#27192)
wip(objectql): home projection engine tests green on the in-memory fa…
wip(dogfood): home projection rounding pins on better-sqlite3, sqlite…
wip: write-path (import/REST) test; float-rounding pins (#27192)
wip: currency.mdx homeAmount passage (#27192)
wip: changesets; regenerated spec api-surface and reference pages (#2…
fix: drop the merge's duplicate skills paragraph; the write-path test…
os-bill commentedon Oct 31, 2026
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
os-dev-report
{ "issue": 27192, "status": "done", "branch": "claude/project-thread-4g7rek", "pr": "https://github.com/objectstack-ai/objectstack/pull/28302", "head": "c37e52a6687cec1d61aa9227abfd8d7a21b4ca9f", "session": "session_01Utp6CYjLJ7sxfUehRrLbT3", "premise_still_valid": true, "summary": "A record-mode amount that declares currencyConfig.homeAmount now stores, on insert and on any update of its amount, code or document date, home = amount / rate (exact decimal, rounded once half away from zero to the home currency's ISO 4217 minor units) and the rate row's own value unrounded. The rate row is the organization's latest sys_exchange_rate on or before the document date; same currency is rate 1 with no lookup. A missing rate refuses ERR_EXCHANGE_RATE_MISSING/400 and an unset localization.currency refuses ERR_HOME_CURRENCY_UNSET/400, never a default; a divergent update by filter is refused. Two accepted deviations into packages/spec (two ledger rows; the CURRENCY_FRACTION_DIGITS export line), no Zod shape change. #28096 landed 03:21Z; main is merged in, no rebase, so no Merge-after line.", "write_paths_reading": "REST: packages/rest/src/crud-routes.ts create/update call protocol createData/updateData, which call engine.insert/engine.update. Import: runImport (sync route and job worker) writes through insertManyData (engine.insertMany then engine.insert) or createData/updateData per row; treatAsHistorical sets preserveAudit. Every door lands in the same insert or update seam, so all convert and refuse alike; validate() and the import dry run preview the same projection. Pinned in packages/objectql/src/home-amount-write-paths.test.ts on a booted kernel with ObjectQLPlugin, a settings service, the real protocol and runImport.", "handoff_points": { "6526609699 system write or after the strip": "after the readonly strip, in every context; a client value is dropped (or refused under strictReadonlyWrites) and the engine writes both fields. Pinned.", "6526609699 rateField scale below 12": "the engine never rounds the rate. A readonly value is validated for shape only and every numeric column is the same exact decimal, so scale 4 reaches display only: 0.138912345678 is stored in full, home 71987.84. Pinned. Spec refusal raised as an open question below.", "6526609699 currency.mdx homeAmount passage": "written: the section 'Converting into the home currency' replaces the 'later release' paragraph.", "6528146352 defaultValue on home or rate field": "overwritten by the projection. Pinned.", "6528146352 preserveAudit import write to the home field": "recomputed over, not kept. Pinned on the engine and through runImport with treatAsHistorical." }, "tests": "At head c37e52a668: packages/objectql full suite 504/504 files, 9803/9803 tests; objectql tsc --noEmit exit 0; dogfood test/home-amount-projection.test.ts 18/18 (better-sqlite3, sqlite-wasm, PostgreSQL 16 local via OS_TEST_POSTGRES_URL); spec error-code-ledger tests pass. Ablations, each applied on disk then restored with git checkout: engine face over objectql's three suites (48 tests): A0 baseline 48/48; A1 $lte to $lt 1 red; A2 latest to earliest row 2 red; A3 rate row ignores the date 9 red; A4 half-away to truncate 7 red; A5 minor units fixed at 2 1 red; A6 float division 2 red; A7 multiply instead of divide 20 red. Driver face (objectql rebuilt into dist per ablation, dogfood 18 tests over 3 drivers): A1 3 red, A3 9 red, A4 9 red, A6 3 red; restore build 18/18.", "gates": "dispatch-gates --commands at the first head derived 117 commands; each run with its exit code captured before any pipe: 115 exit 0, 2 exit 1. check-skills-token-ratchet: the merge of main had duplicated #28096's homeAmount paragraph in skills/objectstack-data/rules/field-types.md (the squash landed beside the branch's original commits); restored to main's text, so this PR no longer touches skills/**. check:error-code-casing: the write-path test's key column was named code, read as an error code; renamed ref. Re-derived at final head c37e52a668: 110 commands, all 110 exit 0; --ran reconciliation: 110 derived families accounted for, 110 run, 0 NOT-MEASURED. CI check-runs on the PR: not yet read at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "MCP GitHub: issue_read, pull_request_read (reads); create_pull_request (the draft PR, no assignee, no reviewers); add_issue_comment (this report). No GitHub CLI or REST token in this session, so the PR and this comment went through MCP rather than post-stamped.mjs.", "api_writes": "git push of WIP commits to claude/project-thread-4g7rek (each through the commit-card-trailers pre-push check); PR create and this comment via MCP as above.", "open_questions": [ { "question": "Open item 4: when localization.currency is unset, what does a write that must convert do?", "options": ["A refuse ERR_HOME_CURRENCY_UNSET/400, naming Localization, Default currency (built)", "B write the row with a blank home amount and rate", "C assume a default home currency"], "recommendation": "A, because a blank home amount silently drops the row from every consolidated total and an assumed currency stores a wrong number; the sources already rule out a silent default." }, { "question": "A datetime document date: which calendar day picks the rate?", "options": ["A its UTC calendar day (built)", "B the organization's timezone day", "C refuse datetime as rateDate"], "recommendation": "A for R2, because the engine has no organization timezone at the write seam today; B is a later refinement if a customer needs it." }, { "question": "Should the spec refuse (or warn on) a homeAmount rateField declared with scale below 12?", "options": ["A spec lane adds a refusal at parse", "B leave it: the engine stores the full rate and scale reaches display only (built)"], "recommendation": "A, a spec-lane card, because a displayed rate rounded to 4 places no longer reproduces the stored home amount for a reader." } ], "out_of_scope_findings": [ "Noted, not filed: merging main into a branch cut from a squash-merged PR's head duplicates that PR's text edits where the squash and the original commits differ (seen here in skills/objectstack-data/rules/field-types.md; check-skills-token-ratchet caught it). carrier: this PR · fixed in place by restoring main's text." ] }Generated by Claude Code
test: rates are per organization on the real drivers; CLDR fraction d…
feat(objectql): a datetime document date is dated in the organization…