Skip to content

objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192

Closed

Description

@objectstack-fleet

Blocked-by: #27191
Blocked-by: #27185

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.

Ruling-ref: 6465431966

维护者速读

  • 改了什么: 写入时:金额、币种或单据日期变更 ⇒ 查该币种 ≤ 单据日期的最新汇率,home = round(amount ÷ rate, 本位币小数位)(参数 3:1 本位币 = rate 外币;更正自 ×,见 ADR-0144 D7 与 PR docs(adr): ADR-0144 multi-currency — record currency, dated rates, write-time home projection; dated note on ADR-0104 #27239 契约复审 6468858562;rate 为 decimal(24,12),裁决 6469382727),写影子列与汇率快照;同币种汇率 1 不查表;缺汇率拒写并点名币种与日期;影子列与汇率列只读;批量写同路径。
  • 为什么改: 这是合并报表的核心。
  • 风险与回滚: L 级;⛔ 浮点算钱:用整数最小单位或 decimal,不用 number 算;SQLite 与 Postgres 各跑一遍舍入 pin;导入路径必须走同一写路径(在本卡实测)。
  • 先看: 先例 summary-aggregate.ts(写入时物化)、record-validator.ts L1173–1180(只读计算字段);CURRENCY_FRACTION_DIGITS 第一个读者。
  • 两分钟验证: USD 10,000 @0.1389 → ¥71,994.24;JPY 0 位、KWD 3 位;缺汇率拒写文案。
  • 需要您做的: 参数 3、4(方向、精度)已按默认裁定。

Task

  • Rate lookup (org, currency, latest effective_date ≤ rateDate), cached per write batch.
  • 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.

Activity

  1. added
    enhancementNew feature or request
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 28, 2026
  2. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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:

  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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.
  4. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    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-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 开发」).

  5. os-bill commented on Oct 31, 2026

    @os-bill
    Collaborator

    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

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, searchdomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:epicpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions