Skip to content

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

Merged
objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/project-thread-iobdbg
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 13 commits into
mainfrom
claude/project-thread-iobdbg

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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 记一条运行级审计,审计插件另记逐行审计行。
  • 为什么改: ADR-0144 D5:改正过的汇率只能经「显式重算」才进到已存的行;给已有数据的对象新加本位币字段时,老行也靠它补齐。
  • 风险与回滚: 不复制任何换算:预览直接调写入时的同一投影函数,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 后与新写入的行相同,再跑一次无变化。
  • 需要您做的: 无。Open item 8(跳过行怎么保持可见)在下方提案并有测试钉住,由 epic PM 带进 ADR 修订。

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 with assertHomeAmountRecomputeSince (the pure since check), formatHomeAmountRecomputeReport, homeAmountRecomputeComplete and HOME_AMOUNT_SKIP_REASONS. Modelled on record-currency-backfill.ts / summary-backfill.ts: a keyset walk over id under the unscoped system context, one engine.update per row that differs.

  • No second copy of the arithmetic. The preview projects each stored row with projectHomeAmounts (the write's own function) over the engine's own reads, ObjectQL.homeAmountReadsFor(context): the same rate-row rule (latest effective_date on 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 a datetime document date read as its day in the organization's localization.timezone. The apply writes { id, <homeField> } through engine.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).
  • Per organization. The organization is the one a write converts the row under (the row's 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.
  • Writes only the two projected fields, under { isSystem, preserveAudit } with the row's stored updated_at (the comment-parent-pair / audit-key-material precedent), so updated_at does 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-row sys_audit_log update row per rewritten row where the audit writers are installed, as they are in the command's boot). A consumer syncing by updated_at does not see a recomputed row; record-currency-codes does move updated_at. Both stated in the docs.
  • Listed and skipped, never written (parameter 1), five reasons: rate_missing (with currency and date), code_blank (ruling 6492000994 item 5: an amount beside a blank code is its own class; remedy os 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).
  • since filters on the document date (homeAmount.rateDate, the date that picks the rate), read as the day the write reads it (a datetime in the organization's timezone); a row with no document date is outside every window. Pinned at the timezone boundary (ablation A6).
  • Audit: one run-level sys_audit_log entry per applied run, beside the per-row rows the audit writers add (pinned with the real writers in plugin-audit): action: 'update', object_name, no record_id, tenant_id empty (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 (the admin-import-users posture): with no ledger in the composition the report says audit: { 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, marked aborted with the counts it reached, before it is thrown on (ablation A9).
  • Refused before any row is read: unknown object (OBJECT_NOT_FOUND / 404), an object or field that declares no home amount (INVALID_FIELD / 400), a since that is not a real YYYY-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 is ReferencesAndMasterDetailFacet.homeAmountReadsFor.

@objectstack/cli — os migrate recompute-home-amounts --object <o> [--field <amount>] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url], the record-currency-codes shell one for one (occupancy gate, --yes, read-only dry-run boot, the "no app objects loaded" refusal, refuseUndeclaredObjects, journal-mode note, --json envelope). --since is judged before the probe, the prompt and the boot. The boot takes buildDataMigrationPlugins({ settings: true, audit: true }): a new settings arm 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-records cut the walk short (ablation A10). --since is judged by the recompute's own assertHomeAmountRecomputeSince, 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 asks isFederatedUnprovisionedInjectedColumn and leaves a federated object's unprovisioned organization_id / updated_at out of its read and its write (disposition skips). scripts/engine-double-contract.pinned.json gains the CLI test double, whose update routes through assertEngineUpdateDispatch (coverage grows, regenerated with --write).

Docs: content/docs/data-modeling/currency.mdx gains "Recomputing stored home amounts"; content/docs/deployment/cli.mdx gains the table row and the os migrate recompute-home-amounts section beside record-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:

  • Open item 8 (a row a backfill skips). Proposal, pinned: (1) a skipped row is never written: its home amount and rate stay as stored, so a never-converted row stays blank, never 0 or a 1:1 conversion; (2) the run is not complete while any row is skipped: homeAmountRecomputeComplete is 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's sys_audit_log entry 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.
  • Parameter 1 as built: report and skip a row with no rate in force on its document date, apply the rest; plus four sibling skip classes (above), each with its own remedy, a blank code apart from a missing rate (ruling 6492000994 item 5).
  • --since reading: the document date (rateDate), as the write reads it.

Declarations re-derived

The order proposed Clause-②: no · Metadata-protocol: none. Corrected to Clause-②: yes (widening): new @objectstack/objectql exports (recomputeHomeAmounts and its report helpers and types, the HomeAmountReads / HomeAmountServing / ExchangeRateRow types), a new public engine method (homeAmountReadsFor), a new CLI command. Nothing narrows. Metadata-protocol: none: no authorable key, value or type changes. One changeset, patch for both packages. packages/spec is not touched.

Review round 1 (seat review 6643251084 on #30181 @ 90fabf3)

Item Done
M1 error-code provenance The CLI's refuseMalformedSince and its CALENDAR_DATE are gone; assertHomeAmountRecomputeSince is exported from home-amount-recompute.ts (stamped there, the owner the ledger already lists) and called before the probe. check-error-code-provenance.ts exit 0. No spec ledger touched.
M2 duplicate heading cli.mdx: one #### os migrate comment-reactions.
M3 404 Round 1 asserted httpStatus: 404 by setting it in the shared refuseUndeclaredObjects; that changed every os migrate --object envelope and broke the key-set pin in object-scope.integration.test.ts (#21644). Round 2 (seat withdrew that half of M3): the util is back to its 90fabf31 form, and the test asserts what the envelope carries, code: 'OBJECT_NOT_FOUND' and exit 1.
S1 per-row audit rows plugin-audit/src/home-amount-recompute-audit.integration.test.ts: real installAuditWriters, SQLite (wasm), 3 rows rewritten → 3 per-row update rows + 1 run-level entry. Docs and the module header say so.
S2 incomplete runs --max-records truncation exits 1 (CLI pin, A10); an aborted applied walk writes its run entry marked aborted, then throws (objectql pin, A9); a dry run that aborts writes nothing (pin).
S3 updated_at One sentence each in cli.mdx, currency.mdx, the changeset.
S4 docblock The CLI test names home-amount-projection.test.ts.
S5 Open item 13 The code_blank remedy line and the docs table warn that record-currency-codes writes one code into every organization.
S6 "What is written" now says dependents of the home field move as for a fresh write.

Review round 2 (seat delta review 6644826096 on #30183 @ 35b01a0)

Item Done
D1 Test Core red migrate-object-scope.ts / .test.ts restored to 90fabf31; the changeset's envelope bullet dropped; the command's test asserts code and exit 1 only. object-scope.integration.test.ts 20 / 20 locally.
Stale comment The type-only import's comment now says the engine package is imported dynamically inside run(), first for the --since check.

Verification

Head 8374620a57 (review round 2 on top of 35b01a068e; round 1 on top of 90fabf3176, then origin/main a35fc6656 merged in, no rebase). On 8374620a57: cli object-scope.integration 20 / 20, recompute-home-amounts 9 / 9, migrate-object-scope + migrate-confirmation-exit.pin (83 / 83 across the four files); cli typecheck 0; eslint 0; check-error-code-provenance 0; check-changeset-no-major 0. The rows below are from 35b01a068e unless marked.

  • packages/objectql src/engine-home-amount-projection.test.ts: 45 / 45 (28 existing + 17 recompute pins, in-memory driver double over applyDriverQueryInMemory): a corrected rate (dry run lists 71994.24 @ 0.1389 → 80000 @ 0.125, writes nothing; apply moves only amount_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_mounted without 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 the record-currency-codes remedy; blank document date and unset home currency as their own classes; same currency at rate 1 with no rate read (1.005 → 1.01); a datetime document date across the Shanghai/UTC day boundary, for the rate and for since; since on the document date; two organizations, CNY and JPY homes, counted apart, a correction reaching only its own organization; four refusals before any read; the pure since check with the run's own envelope; an aborted applied run writing its aborted run entry, and an aborted dry run writing nothing.
  • packages/qa/dogfood test/home-amount-projection.test.ts: 42 / 42 = 3 cells (better-sqlite3, sqlite-wasm, PostgreSQL 16 via OS_TEST_POSTGRES_URL) × (8 existing + 6 recompute pins): corrected rate with report-then-apply idempotence, the fresh-write equality and updated_at kept; a missing rate beside a filled row; a blank code; the same currency; the timezone boundary through a datetime column; two organizations under the isolated posture, each reading its own rate rows.
  • packages/plugins/plugin-audit src/home-amount-recompute-audit.integration.test.ts: 1 / 1 (real installAuditWriters, sqlite-wasm: 3 per-row update rows + 1 run entry).
  • packages/cli: recompute-home-amounts.test.ts 9 / 9 (+ --max-records truncation exits 1); src/commands/migrate/object-scope.integration.test.ts 20 / 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.ts 48 / 48; schema-migrate.one-shot-family.integration.test.ts -t recompute-home-amounts 5 / 5; test/json-stdout-purity.e2e.test.ts (OS_TEST_TIERS=nightly) 95 / 95.
  • Typecheck: @objectstack/objectql, @objectstack/cli and @objectstack/plugin-audit pass (test-typecheck ledgers held; the cli one needs NODE_OPTIONS=--max-old-space-size=8192 on this container). eslint on every touched file: clean.
  • Full @objectstack/objectql suite: 548 / 549 files, 10 862 / 10 863 tests on the head before federated-injected-column-readers.test.ts learned the recompute's two column reads; the recompute now asks isFederatedUnprovisionedInjectedColumn before selecting organization_id / updated_at and 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).

# Mutation Result
A1 The report's value computed by a local float copy (amount ÷ rate, 2 decimals) instead of the projection's 2 / 42 red (1.005 same-currency; the JPY organization)
A2 The apply payload's home value replaced with 0 42 / 42 green: the stored value is the write's own projection, never the payload's
A3 Apply context without preserveAudit (objectql rebuilt under it; dogfood) 3 / 3 cells red on updated_at
A4 The blank-code class removed 1 red (the blank code is reported unconvertible)
A5 homeAmountRecomputeComplete ignores skipped rows 1 red
A6 since exclusive (<=) 1 red (the timezone-boundary pin)
A7 Organization forced to none 1 red (two organizations)
A8 The SHARED projection's half-away step removed (home-amount-projection.ts) the fixed-number pins red (2), while "a recomputed row equals a fresh write" stays green: write and recompute move together, one code path
A9 The aborted run's audit write removed from the walk's catch 1 / 45 red (the aborted-run pin)
A10 The CLI exit ignores report.truncated 1 / 9 red (the --max-records pin)

Gates

dispatch-gates --commands on head 35b01a068e: 108 derived commands, plus check-error-code-provenance.ts run 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


Generated by Claude Code

claude added 12 commits November 4, 2026 20:08
… 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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/objectql, touching 67 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

76 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a35fc6656636772766917e79c4d4692d0b7fe216.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: recordId (4 routes)
  • 2 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 80 pages), organization_id (literal, 38 pages)
  • 29 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 49 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a35fc6656636772766917e79c4d4692d0b7fe216 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 61e5aa394cfae9fa7a7167eb24945f63a66d31f7 — the merge of head 8374620a575574e2e40a198dfdf7caafd2d74567 into base a35fc6656636772766917e79c4d4692d0b7fe216, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a35fc6656636772766917e79c4d4692d0b7fe216 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d

Seat review (delta) · PR #30183 · 90fabf3..35b01a0 · TIER_DEFAULT
Verdict: FAIL

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-fix

D1. The M3 "fix" broke an existing pin, and "Test Core" is red on this head.

  • CI: run 41923989369, job 126904474671, "Test Core (1/8)", fails in @objectstack/cli src/commands/migrate/object-scope.integration.test.ts:390. The test is [#21644] an unknown --object is an error, never narrowed to nothing > '<cmd>': refused before anything was read or written. 4 tests fail (value-shapes, files-to-references, summary-nulls, duplicates), for example expected [ 'code', 'error', 'httpStatus' ] to deeply equal [ 'code', 'error' ].
  • Local reproduction: exit 1, 4 failed / 16 passed.
  • Ablation: restoring migrate-object-scope.ts to its 90fabf3 version makes it exit 0, 20/20. The cause is this PR's error.httpStatus = 404 in packages/cli/src/utils/migrate-object-scope.ts:83-86.
  • The dev's test list did not include that file.

Ruling on the scope creep: split it out.

Confirmed fixed

  • M1:
    • The CLI's own check (refuseMalformedSince, its CALENDAR_DATE and the VALIDATION_FAILED stamp) is removed.
    • assertHomeAmountRecomputeSince is exported from objectql (home-amount-recompute.ts, index.ts). resolvePlan calls it, and the CLI calls it first in run(), before the probe, the prompt and the boot.
    • check-error-code-provenance.ts exits 0 locally. CI "Lint · docs & code guards" and "Lint & Repo Gates" are success on 35b01a0.
    • No packages/spec path is in the diff (0 files).
    • The objectql pin checks that the pure check and the run produce an identical message and VALIDATION_FAILED/400/400.
  • M2: cli.mdx now has one #### os migrate comment-reactions heading (count 1).
  • M3: the assertion was added, but see D1.
  • S1:
    • plugin-audit/src/home-amount-recompute-audit.integration.test.ts uses the real installAuditWriters with the real SysAuditLog/SysActivity on sqlite-wasm. It shows 3 per-row update rows with record ids equal to the rewritten ids, plus 1 run-level entry (no record, event: home_amount.recompute, written: 3). Local run: 1/1.
    • This also proves the run-level insert works against the real ledger schema.
    • Docs, header and changeset all say "one run-level entry beside the per-row rows".
  • S2:
    • gap now includes report.truncated, with its own message. The CLI pin covers dry run and apply.
    • An applied walk that an unclassified error stops writes the run entry with aborted: <message> and the counts it reached, then rethrows (objectql pin: row 1 written, row 2 not, 1 audit row).
    • A dry run stopped the same way writes nothing, the ledger included.
    • Ablations reproduced locally: A10 (drop || report.truncated) gives 1/9 red; A9 (drop the abort audit write) gives 1/45 red. Both restored, with a clean git status.
  • S3: updated_at is kept, with the sync caveat and the contrast with record-currency-codes, in both docs pages, the header and the changeset.
  • S5: the remedy line, the docs table and the header warn that record-currency-codes writes its code into every organization (Open item 13).
  • S6: dependents moving with the home field (formulas, roll-ups, per-row audit) are now stated.
  • S4: the docblock now names home-amount-projection.test.ts.

Should-fix (non-blocking)

  • recompute-home-amounts.ts:24: the comment "Type-only, so the heavy engine package is still loaded lazily below" is now stale. run() dynamically imports @objectstack/objectql first thing, for the --since check. That is harmless; the one-shot-family and json-stdout-purity rows stay green. Reword the comment.

Commands (worktree /tmp/claude-0/wt30183 at 35b01a0; turbo build exit 0, 69/69)

Command Exit Result
cli object-scope.integration.test.ts 1 4 failed / 16 passed (D1)
same, with migrate-object-scope.ts restored to 90fabf3 0 20/20 (D1 causation)
cli recompute-home-amounts.test.ts + migrate-object-scope.test.ts + migrate-confirmation-exit.pin 0 63/63
cli one-shot-family -t recompute-home-amounts 0 5 passed
cli OS_TEST_TIERS=nightly json-stdout-purity -t recompute 0 3 passed
objectql engine-home-amount-projection + federated-injected-column-readers 0 50/50
plugin-audit home-amount-recompute-audit.integration 0 1/1
dogfood home-amount-projection.test.ts, local PG 16 0 42/42, 3 cells
spec check-error-code-provenance.ts 0
typecheck objectql / cli (8 GB heap) / plugin-audit 0 / 0 / 0
eslint on the 8 changed .ts files 0
ablation A10 1 1/9 red, restored
ablation A9 1 1/45 red, restored

CI on 35b01a0 (completed runs):

  • Lint & Repo Gates: success.
  • TypeScript Type Check: success.
  • Lint · docs & code guards: success.
  • Test Core: failure. Only shard 1/8 is red, and its only failure is the 4 tests in D1. Shards 2–8 are success.
  • A second set of runs was still in progress when I read them. The failure is deterministic, so it will recur.

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d

Seat ACCEPT · PR #30183 · Head-sha: 8374620a575574e2e40a198dfdf7caafd2d74567 · 2026-11-04T23:47Z

维护者速读

  • 改了什么: 新增 os migrate recompute-home-amounts --object <o>(及 objectql 的 recomputeHomeAmounts):对已存在的行,按写入时同一条投影(同一汇率行、同一舍入、各组织本位币与时区)重算本位币影子列;默认干跑只报告,--apply 只写有差异的行,并记一条运行级审计。换不了的行按原因列出(缺汇率、缺币种码、缺单据日期、组织未设本位币),一律不写。
  • 为什么改: 汇率补录、本位币或币种码修正之后,历史行的本位币金额不会自己更新,合并报表会错;此前没有回补手段。
  • 风险与回滚: 中低。写入走 engine.update,只动本位币与汇率字段、保留 updated_at;有跳过行或被 --max-records 截断时 exit 1;新增导出全为增量(Clause-② yes, widening);patch changeset。回滚即撤 PR,已执行的 --apply 可再跑一遍新汇率覆盖。
  • 先看: packages/objectql/src/home-amount-recompute.ts 的遍历与跳过分类,再看 CLI 外壳 recompute-home-amounts.ts。
  • 两分钟验证: npx vitest run packages/cli/src/commands/migrate/recompute-home-amounts.test.ts packages/cli/src/commands/migrate/object-scope.integration.test.ts(9 + 20 全绿)。
  • 需要您做的: 无(非受管面,席位落地)。

席位意见:卡上要求齐了,两轮返工后无遗留必修项,可落地。

席位核对

  1. 形态: 20 个文件(+2012/-5),13 个提交,无 rebase;patch changeset,Metadata-protocol: none。
  2. 前提实测: 复审者本地:dogfood home-amount-projection 在 PG16 上 42/42(三格);plugin-audit 真实审计账本集成 1/1;消融 A9/A10 各红 1 条后复原。第 2 轮增量(35b01a06..8374620,5 个文件 +8/-10)由席位直接核:migrate-object-scope.{ts,test.ts} 与 90fabf3 零差异,changeset 第 4 条已删,测试只断 code + exit 1;开发者本地 object-scope.integration 20/20、4 文件 83/83。
  3. 门禁 check-runs: head 8374620a success 40、skipped 14,无失败(上一 head 的 Test Core 1/8 红已消除)。check-governed-merges --pr 30183:NOT governed,2017 行 ≤ 3000。
  4. 接受的偏差: Clause-② 由认领时的 no 改为 yes(widening),认领评论 6640794926 已改。PR 经中继重开(取代 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,作者 objectstack-fleet[bot])。复审记录: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 第 1 轮 REWORK、本 PR 6644826096 增量 FAIL(D1 源于席位第 1 轮 M3 的错误前提,已撤回该半条),本笔前已核第 2 轮增量;Served-tier TIER_DEFAULT。
  5. 范围外去向: 给所有 os migrate --object 的 --json 信封加 httpStatus(需改 [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 键集钉)不在本卡,需要时另开卡;record-currency-codes 多组织写入问题(Open item 13)归 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 决策。Skills-docs: none。
  6. 落地条款: 非受管面:本笔之后 ready + auto-merge;合并后摘 cli: os migrate recompute-home-amounts --object X [--since DATE] [--apply] — report then apply; rows with a missing rate are listed and skipped (multi-currency M2-3, epic #8345) #27193 的 pm:dispatched,解锁 migrate: adding a homeAmount field to an existing object is the normal add-column path; backfill = recompute — one proving test (multi-currency M2-4, epic #8345) #27194。

os-bill pushed a commit that referenced this pull request Nov 5, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant