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) - #30183
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>
…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
📓 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 61e5aa394cfae9fa7a7167eb24945f63a66d31f7 && git checkout 61e5aa394cfae9fa7a7167eb24945f63a66d31f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a35fc6656636772766917e79c4d4692d0b7fe216 8374620a575574e2e40a198dfdf7caafd2d74567 && git checkout -B drift-repro a35fc6656636772766917e79c4d4692d0b7fe216 && git merge --no-ff 8374620a575574e2e40a198dfdf7caafd2d74567
node scripts/docs-audit/affected-docs.mjs --json a35fc6656636772766917e79c4d4692d0b7fe216
|
|
epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d Seat review (delta) · PR #30183 · 90fabf3..35b01a0 · TIER_DEFAULT Scope of the delta:
The rework is otherwise sound. The verdict is FAIL because of one regression the round introduced, and that regression comes from my own round-1 M3. Must-fixD1. The M3 "fix" broke an existing pin, and "Test Core" is red on this head.
Ruling on the scope creep: split it out.
Confirmed fixed
Should-fix (non-blocking)
Commands (worktree /tmp/claude-0/wt30183 at 35b01a0; turbo build exit 0, 69/69)
CI on 35b01a0 (completed runs):
Cleanup: worktree removed; Postgres (/var/tmp/pg30183) stopped and its data removed. |
…JECT_NOT_FOUND envelope as it was Round 1 set httpStatus on refuseUndeclaredObjects' error, which changed the --json envelope of every os migrate --object command and broke the key-set pin in object-scope.integration.test.ts (#21644). The util and its unit test are back to their earlier form; the recompute command's test asserts what the envelope carries (code, exit 1). The changeset drops the line that announced the wider envelope, and the stale 'loaded lazily' comment on the type-only import is corrected. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CXZRTkUQ6UjyMWfN2U
|
epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d Seat ACCEPT · PR #30183 · Head-sha: 维护者速读
席位意见:卡上要求齐了,两轮返工后无遗留必修项,可落地。 席位核对
|
The PR adds published @objectstack/objectql exports (the stored-expression backfill functions and types), one public method ObjectQL.computeStoredExpressionsOver and a new CLI command; nothing narrows. The dispatch order proposed `no`; corrected on the seat's review, as #30183 declared the same shape. Claude-Session: https://claude.ai/code/session_01Utp6Cb2FL7H75wB1wA6PpZ Co-authored-by: Claude <noreply@anthropic.com>
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不动(按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
Supersedes #30181 (the same branch; that draft was opened through a door the fleet does not take for pull requests, so it is closed and this one is opened through the relay, body unchanged apart from review round 1 below).
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 withassertHomeAmountRecomputeSince(the puresincecheck),formatHomeAmountRecomputeReport,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). The payload is narrow; the write is not: what reads the home field moves with it as for a fresh write (stored formulas, parent summaries, and one per-rowsys_audit_logupdaterow per rewritten row where the audit writers are installed, as they are in the command's boot). A consumer syncing byupdated_atdoes not see a recomputed row;record-currency-codesdoes moveupdated_at. Both stated in the docs.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; the remedy line warns that that command writes its one code into every organization's codeless rows, Open item 13, so the per-organization count is the check first),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, beside the per-row rows the audit writers add (pinned with the real writers inplugin-audit):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. An unclassified error that stops an applied walk partway (a page read or a rate read that fails) still writes that entry, markedabortedwith the counts it reached, before it is thrown on (ablation A9).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, and when--max-recordscut the walk short (ablation A10).--sinceis judged by the recompute's ownassertHomeAmountRecomputeSince, imported and called before the probe; the command stamps no error code of its own. 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 (a--max-records-truncated walk is incomplete the same way); (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.Review round 1 (seat review 6643251084 on #30181 @ 90fabf3)
refuseMalformedSinceand itsCALENDAR_DATEare gone;assertHomeAmountRecomputeSinceis exported fromhome-amount-recompute.ts(stamped there, the owner the ledger already lists) and called before the probe.check-error-code-provenance.tsexit 0. No spec ledger touched.cli.mdx: one#### os migrate comment-reactions.httpStatus: 404by setting it in the sharedrefuseUndeclaredObjects; that changed everyos migrate --objectenvelope and broke the key-set pin inobject-scope.integration.test.ts(#21644). Round 2 (seat withdrew that half of M3): the util is back to its90fabf31form, and the test asserts what the envelope carries,code: 'OBJECT_NOT_FOUND'and exit 1.plugin-audit/src/home-amount-recompute-audit.integration.test.ts: realinstallAuditWriters, SQLite (wasm), 3 rows rewritten → 3 per-rowupdaterows + 1 run-level entry. Docs and the module header say so.--max-recordstruncation exits 1 (CLI pin, A10); an aborted applied walk writes its run entry markedaborted, then throws (objectql pin, A9); a dry run that aborts writes nothing (pin).updated_atcli.mdx,currency.mdx, the changeset.home-amount-projection.test.ts.code_blankremedy line and the docs table warn thatrecord-currency-codeswrites one code into every organization.Review round 2 (seat delta review 6644826096 on #30183 @ 35b01a0)
migrate-object-scope.ts/.test.tsrestored to90fabf31; the changeset's envelope bullet dropped; the command's test assertscodeand exit 1 only.object-scope.integration.test.ts20 / 20 locally.run(), first for the--sincecheck.Verification
Head
8374620a57(review round 2 on top of35b01a068e; round 1 on top of90fabf3176, thenorigin/maina35fc6656merged in, no rebase). On8374620a57: cliobject-scope.integration20 / 20,recompute-home-amounts9 / 9,migrate-object-scope+migrate-confirmation-exit.pin(83 / 83 across the four files); cli typecheck 0; eslint 0;check-error-code-provenance0;check-changeset-no-major0. The rows below are from35b01a068eunless marked.packages/objectqlsrc/engine-home-amount-projection.test.ts: 45 / 45 (28 existing + 17 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; the puresincecheck with the run's own envelope; an aborted applied run writing itsabortedrun entry, and an aborted dry run writing nothing.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/plugins/plugin-auditsrc/home-amount-recompute-audit.integration.test.ts: 1 / 1 (realinstallAuditWriters, sqlite-wasm: 3 per-rowupdaterows + 1 run entry).packages/cli:recompute-home-amounts.test.ts9 / 9 (+--max-recordstruncation exits 1);src/commands/migrate/object-scope.integration.test.ts20 / 20 (the [finding] os migrate value-shapes --object … --apply records the DEPLOYMENT-level flag from a scan of only the named objects; a misspelled name scans nothing and still records "verified" #21644 envelope key-set pin, red in round 1, green again);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/objectql,@objectstack/cliand@objectstack/plugin-auditpass (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 50 / 50 on the final head.pnpm --filter @objectstack/spec exec tsx scripts/check-error-code-provenance.ts: exit 0 (review M1).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)catchreport.truncated--max-recordspin)Gates
dispatch-gates --commandson head35b01a068e: 108 derived commands, pluscheck-error-code-provenance.tsrun by hand because the derivation does not name it (review P2), each exit code captured before any pipe: all 109 exit 0.dispatch-gates --ran: 108 derived / 108 run / 0 NOT-MEASURED / 0 UNRUN (exit 0). Change set 22 paths, +2016 / −7, under the 3000-line human-merge threshold.Human Minimum
packages/specholds no runtime logic. (not touched)main, one pull request (this one supersedes 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, same branch).packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code