Repository navigation
feat(objectql,cli): recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped (multi-currency M2-3) - #30181
Conversation
…projection (#27193) Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
…stgreSQL (#27193) Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
) Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
…ept on SQL (#27193) Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
… columns; pin the CLI double's update (#27193) Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 76 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 49 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fac58fcdf2db80ae23e2b98b6006d771eea6d948 && git checkout fac58fcdf2db80ae23e2b98b6006d771eea6d948
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a35fc6656636772766917e79c4d4692d0b7fe216 35b01a068ea3e350eac34f6e3b8446836550405d && git checkout -B drift-repro a35fc6656636772766917e79c4d4692d0b7fe216 && git merge --no-ff 35b01a068ea3e350eac34f6e3b8446836550405d
node scripts/docs-audit/affected-docs.mjs --json a35fc6656636772766917e79c4d4692d0b7fe216
|
|
epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d Served-tier: TIER_DEFAULT (the seat's own review of record for the Seat review · PR #30181 @ 90fabf3 · TIER_DEFAULT Scope: card #27193 (os migrate recompute-home-amounts, M2-3, epic #8345). Diff The design is sound. There is one copy of the arithmetic, the work is done per organization, skipped rows are never written, and the Open item 8 proposal is well pinned. The FAIL comes from a required CI job that is red on this head, a docs defect, and one refusal assertion that is missing. Must-fixM1.
M2.
M3.
Process findings (the seat's call, not the dev's code)P1.
P2. dispatch-gates under-derived: its 105 commands did not include P3. The declaration was corrected from the claim's Should-fixS1. More
S2. Two ways the run is incomplete but nothing signals it.
S3.
S4. The CLI test docblock ( S5. The S6. "Writes touch only the home and rate fields" is true of the payload. Engine-side dependents that read the home field still move with it: stored expressions, parent summaries and audit rows (S1). That is the correct consequence of "equals a fresh write", but the docs claim reads narrower. Checks (each item of the brief)
Commands (worktree /tmp/claude-0/wt30181 at 90fabf3; deps built with turbo, exit 0)
CI check-runs on head 90fabf3: all success or skipped except Cleanup: worktree removed; Postgres (/var/tmp/pg30181) stopped and its data removed. |
…k owned by the recompute, aborted runs audited, truncated runs exit 1 - The --since check is the recompute's own exported pure function (assertHomeAmountRecomputeSince); the CLI calls it before its probe and keeps no copy, so the VALIDATION_FAILED stamp has one owner. - An unclassified error that stops an applied walk still writes the run-level sys_audit_log entry, marked aborted, then is thrown on. - A walk --max-records cut short exits 1. - refuseUndeclaredObjects sets httpStatus 404, so the --json envelope carries it beside OBJECT_NOT_FOUND. - Pinned with the real audit writers: per-row update rows plus the run entry. Docs: the duplicated heading removed; per-row audit rows, updated_at kept, and record-currency-codes' every-organization code stated. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U
Requested by Bill · project thread
epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d
维护者速读
os migrate recompute-home-amounts --object X [--field] [--since DATE] [--apply]:按「现在生效的汇率」重算已存的本位币金额。默认只出报告(每个组织各自的本位币与条数、哪些行会变、从多少变成多少、哪些行转不了及原因),--apply才写,只写本位币字段与汇率字段,updated_at不动,每次 apply 记一条审计。engine.update、由写入自己再算一次,所以重算后的行与新写入一模一样(消融为证)。缺汇率、空币种码、无单据日期、未设本位币的行只列出、不写,命令保持退出码 1 直到补齐。回滚即撤 PR。packages/objectql/src/home-amount-recompute.ts(recomputeHomeAmounts),再看engine.ts新增的homeAmountReadsFor(委托到engine-references-and-master-detail.ts)。pnpm --filter @objectstack/objectql exec vitest run src/engine-home-amount-projection.test.ts -t recompute:改汇率后报告列出 71994.24 → 80000,apply 后与新写入的行相同,再跑一次无变化。Fixes #27193
R2.1 of epic #8345 (ADR-0144 D9 R2, M2-3). The R2 core (#28096, #27185, #27198, #28302) shipped in 18.3.0 (ruling 6539345871, letter A′); this rides a later Version Packages, as the epic PM pairs it.
Clause-②: yes (widening)
Metadata-protocol: none
What changes
@objectstack/objectql—recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })(home-amount-recompute.ts), exported withformatHomeAmountRecomputeReport,homeAmountRecomputeCompleteandHOME_AMOUNT_SKIP_REASONS. Modelled onrecord-currency-backfill.ts/summary-backfill.ts: a keyset walk overidunder the unscoped system context, oneengine.updateper row that differs.projectHomeAmounts(the write's own function) over the engine's own reads,ObjectQL.homeAmountReadsFor(context): the same rate-row rule (latesteffective_dateon or before the document date, in the row's organization), exact division, single half-away rounding to the home currency's CLDR fraction digits, rate 1 for the home currency, and adatetimedocument date read as its day in the organization'slocalization.timezone. The apply writes{ id, <homeField> }throughengine.update; a payload touching the home field makes the write project the row again over the stored row, so the value stored is the write's, never the preview's (ablation A2 below).organization_id); each converts into its own home currency at its own rates, and the report carries one entry per organization (homeCurrency,amounts,unchanged,changed,written, skipped per reason,outsideSince). No deployment-wide answer.{ isSystem, preserveAudit }with the row's storedupdated_at(thecomment-parent-pair/audit-key-materialprecedent), soupdated_atdoes not move (ablation A3, on SQL).rate_missing(with currency and date),code_blank(ruling 6492000994 item 5: an amount beside a blank code is its own class; remedyos migrate record-currency-codes, not a rate),document_date_blank,home_currency_unset,unconvertible(a code, amount or date the write's validator refuses).sincefilters on the document date (homeAmount.rateDate, the date that picks the rate), read as the day the write reads it (adatetimein the organization's timezone); a row with no document date is outside every window. Pinned at the timezone boundary (ablation A6).sys_audit_logentry per applied run:action: 'update',object_name, norecord_id,tenant_idempty (the run spans organizations: a deployment-level row),metadata={ event: 'home_amount.recompute', source, object, fields, since, counts, organizations, skippedByReason, skipped (≤ 1000), failures, truncated }. The registry is asked first (theadmin-import-usersposture): with no ledger in the composition the report saysaudit: { written: false, reason: 'ledger_not_mounted' }; a refused insert is warned and reported, and the run that already landed stands.OBJECT_NOT_FOUND/ 404), an object orfieldthat declares no home amount (INVALID_FIELD/ 400), asincethat is not a realYYYY-MM-DD(VALIDATION_FAILED/ 400). The function takes no CLI dependency, so a non-CLI door can call it (no HTTP or Setup door is built here; PR feat(objectql,cli,metadata-protocol): backfill the currency code of rows stored before an amount switched to record mode, and acknowledge the switch with a 409 at the save #28128 S1 is open).@objectstack/objectql—ObjectQL.homeAmountReadsFor(context?)returns{ serving, organizationOf(row) }, the reads the write-time projection makes. The facade stays delegation-only: the body isReferencesAndMasterDetailFacet.homeAmountReadsFor.@objectstack/cli—os migrate recompute-home-amounts --object <o> [--field <amount>] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url], therecord-currency-codesshell one for one (occupancy gate,--yes, read-only dry-run boot, the "no app objects loaded" refusal,refuseUndeclaredObjects, journal-mode note,--jsonenvelope).--sinceis judged before the probe, the prompt and the boot. The boot takesbuildDataMigrationPlugins({ settings: true, audit: true }): a newsettingsarm adds the one-shot settings composition (oneShotSettingsPlugin, never mints a key), because without the settings service every organization reads as having no home currency. Exit 1 while any row is listed and skipped, on a dry run as on an apply. Rows added to the three CLI rosters that discover commands from the tree (migrate-confirmation-exit.pin,schema-migrate.one-shot-family,json-stdout-purity).Ledgers this diff had to teach:
federated-injected-column-readers.test.ts(#21918) gains the recompute's rows: it asksisFederatedUnprovisionedInjectedColumnand leaves a federated object's unprovisionedorganization_id/updated_atout of its read and its write (dispositionskips).scripts/engine-double-contract.pinned.jsongains the CLI test double, whoseupdateroutes throughassertEngineUpdateDispatch(coverage grows, regenerated with--write).Docs:
content/docs/data-modeling/currency.mdxgains "Recomputing stored home amounts";content/docs/deployment/cli.mdxgains the table row and theos migrate recompute-home-amountssection besiderecord-currency-codes.ADR anchor:
scripts/adr-anchors/packages__objectql__src__home-amount-recompute.ts.json(ADR-0144: no second copy of the arithmetic; skip classes; Open item 8).ADR-0144 open items settled here
For the epic PM to carry into the ADR in its own PR:
homeAmountRecomputeCompleteis false and the command exits 1, on a dry run and on an apply, until a re-run skips nothing; (3) the applied run'ssys_audit_logentry records the skipped rows (counts per organization and reason, and each row with its reason, currency and date), so the gap outlives the terminal. A per-row marker column was not chosen: it would be a new author-visible field or an injected column (D8 rule 2), i.e. spec work.--sincereading: the document date (rateDate), as the write reads it.Declarations re-derived
The order proposed
Clause-②: no·Metadata-protocol: none. Corrected toClause-②: yes (widening): new@objectstack/objectqlexports (recomputeHomeAmountsand its report helpers and types, theHomeAmountReads/HomeAmountServing/ExchangeRateRowtypes), a new public engine method (homeAmountReadsFor), a new CLI command. Nothing narrows.Metadata-protocol: none: no authorable key, value or type changes. One changeset,patchfor both packages.packages/specis not touched.Verification
Head
90fabf3176(the branch withorigin/main59e64635f4merged in, no rebase).packages/objectqlsrc/engine-home-amount-projection.test.ts: 42 / 42 (28 existing + 14 recompute pins, in-memory driver double overapplyDriverQueryInMemory): a corrected rate (dry run lists 71994.24 @ 0.1389 → 80000 @ 0.125, writes nothing; apply moves onlyamount_home/amount_rate; a second run is clean); the recomputed row equals a fresh write; one audit entry per applied run, none for a dry run,ledger_not_mountedwithout the ledger; a missing rate listed with currency and date, skipped, the run incomplete, the audit entry naming it; a home field added over stored rows (rows without a rate stay blank, never 0); a blank code its own class with therecord-currency-codesremedy; blank document date and unset home currency as their own classes; same currency at rate 1 with no rate read (1.005 → 1.01); adatetimedocument date across the Shanghai/UTC day boundary, for the rate and forsince;sinceon the document date; two organizations, CNY and JPY homes, counted apart, a correction reaching only its own organization; four refusals before any read.packages/qa/dogfoodtest/home-amount-projection.test.ts: 42 / 42 = 3 cells (better-sqlite3, sqlite-wasm, PostgreSQL 16 viaOS_TEST_POSTGRES_URL) × (8 existing + 6 recompute pins): corrected rate with report-then-apply idempotence, the fresh-write equality andupdated_atkept; a missing rate beside a filled row; a blank code; the same currency; the timezone boundary through adatetimecolumn; two organizations under theisolatedposture, each reading its own rate rows.packages/cli:recompute-home-amounts.test.ts8 / 8;test/migrate-confirmation-exit.pin.test.ts48 / 48;schema-migrate.one-shot-family.integration.test.ts -t recompute-home-amounts5 / 5;test/json-stdout-purity.e2e.test.ts(OS_TEST_TIERS=nightly) 95 / 95.@objectstack/objectqland@objectstack/clipass (test-typecheck ledgers held; the cli one needsNODE_OPTIONS=--max-old-space-size=8192on this container). eslint on every touched file: clean.@objectstack/objectqlsuite: 548 / 549 files, 10 862 / 10 863 tests on the head beforefederated-injected-column-readers.test.tslearned the recompute's two column reads; the recompute now asksisFederatedUnprovisionedInjectedColumnbefore selectingorganization_id/updated_atand the table has its rows; that file and the recompute file re-run 47 / 47.Ablations
scripts/ablation-replace.mjs, each restored byte-identical (blob == HEAD).amount ÷ rate, 2 decimals) instead of the projection's0preserveAudit(objectql rebuilt under it; dogfood)updated_atunconvertible)homeAmountRecomputeCompleteignores skipped rowssinceexclusive (<=)home-amount-projection.ts)Gates
dispatch-gates --commandson head90fabf3176: 105 derived commands, each exit code captured before any pipe, all 105 exit 0;dispatch-gates --ranreconciles 105 derived / 105 run / 0 NOT-MEASURED / 0 UNRUN (exit 0). Change set 19 paths, +1789 / −5, under the 3000-line human-merge threshold.Human Minimum
packages/specholds no runtime logic. (not touched)main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code