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) - #30181

Closed
os-bill wants to merge 12 commits into
mainfrom
claude/project-thread-iobdbg
Closed

os-bill wants to merge 12 commits into
mainfrom
claude/project-thread-iobdbg

Conversation

@os-bill

@os-bill os-bill commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

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 记一条审计。
  • 为什么改: 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

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 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).
  • 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), 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 sys_audit_log entry per applied run: 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.
  • 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. 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; (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.

Verification

Head 90fabf3176 (the branch with origin/main 59e64635f4 merged in, no rebase).

  • packages/objectql src/engine-home-amount-projection.test.ts: 42 / 42 (28 existing + 14 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.
  • 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/cli: recompute-home-amounts.test.ts 8 / 8; 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 and @objectstack/cli 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 47 / 47.

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

Gates

dispatch-gates --commands on head 90fabf3176: 105 derived commands, each exit code captured before any pipe, all 105 exit 0; dispatch-gates --ran reconciles 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

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the touched files passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief is Chinese by the seat's convention).

Generated by Claude Code

claude added 10 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>
@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 68 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 fac58fcdf2db80ae23e2b98b6006d771eea6d948 — the merge of head 35b01a068ea3e350eac34f6e3b8446836550405d 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 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

⚠️ 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

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

Served-tier: TIER_DEFAULT (the seat's own review of record for the Clause-②: yes declaration; reviewer: opus subagent of the epic PM seat). Verdict at 90fabf3: FAIL (REWORK). The claim's Clause-②: line is revised to yes (widening) (comment 6640794926, edited). P1 is the seat's own miss: the dispatch order carried no Writes: line; this PR will be re-opened through the relay pr_create.

Seat review · PR #30181 @ 90fabf3 · TIER_DEFAULT
Verdict: FAIL

Scope: card #27193 (os migrate recompute-home-amounts, M2-3, epic #8345). Diff origin/main...90fabf31: 19 files, +1789/-5, no governed surface. Read: card body and comments (scope addition 6498215930, claim 6640794926, os-dev-report 6642646281), PR body, dispatch order /mnt/project-files/epic-8345/dispatch-27193.md, execution-duties 复核 + review-checklist.md, ADR-0144 on main (D5, D7 p1, D8 r4, Open items 2/3/6/8/13).

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

M1. packages/cli/src/commands/migrate/recompute-home-amounts.ts:35-47 (the stamp is at :43). The required CI job "Lint & Repo Gates" is red on head 90fabf3.

  • What fails: lane Lint · docs & code guards (job 126878559562) fails at gate step "Error-code provenance guard": @objectstack/cli stamps 'VALIDATION_FAILED' (assign) at packages/cli/src/commands/migrate/recompute-home-amounts.ts:43 — not listed under its own owner key.
  • Local check: pnpm --filter @objectstack/spec exec tsx scripts/check-error-code-provenance.ts gives exit 1 with the same line.
  • Suggested fix, in scope: drop the CLI's own copy of the since check (refuseMalformedSince and its second CALENDAR_DATE). Instead, export a pure validator from home-amount-recompute.ts (isCalendarDate + refusal(VALIDATION_FAILED_CODE, …), already stamped in objectql) and call it before the probe. This also removes a duplicate.
  • The other fix, a ledger row in packages/spec/src/api/error-code-ledger.zod.ts, is behind the dispatch's packages/spec/src/** fence.
  • Note: the report's "dispatch-gates --commands 105/105 exit 0" is true but did not derive this gate (see P2).

M2. content/docs/deployment/cli.mdx:1678-1679: the heading #### \os migrate comment-reactions`` now appears twice in a row.

  • The diff inserts one copy at the end of the new section, just above the existing one. The page gets an empty section with a duplicated anchor.
  • Fix: delete line 1678.

M3. packages/cli/src/commands/migrate/recompute-home-amounts.test.ts:157: the OBJECT_NOT_FOUND refusal asserts only code.

  • The envelope carries status 404 (migrate-object-scope.ts:70), and the checklist requires code + status on every refusal case.
  • Fix: toMatchObject({ code: 'OBJECT_NOT_FOUND', httpStatus: 404 }).
  • The other refusals already assert both: objectql ([code, status] × 5) and the CLI --since / INVALID_FIELD envelopes.

Process findings (the seat's call, not the dev's code)

P1. mcp_calls names a write tool: github create_pull_request.

  • .claude/settings.json deny-lists that tool. Checklist rule: "mcp_calls 点名写工具 ⇒ 拒收,⛔ 不带注放行" (reject; do not pass it with a note).
  • Root cause: the dispatch order dispatch-27193.md has no Writes: line, although execution-duties §派发 requires one ("PR 只经中继 pr_create"). On a project thread that line is the only defence.
  • The PR itself is draft, base main, with no assignee and no reviewer.
  • The seat has to rule on this; the dev cannot fix it in the code.

P2. dispatch-gates under-derived: its 105 commands did not include check:error-code-provenance, which CI runs in lint-docs-code. That gap is how M1 shipped with a "105/105" report.

P3. The declaration was corrected from the claim's Clause-②: no to yes (widening) in the PR body. Per the checklist ("dev 纠正 PM 要当众认", a dev's correction of the PM is acknowledged in public), the seat acknowledges it on #27193.

Should-fix

S1. More sys_audit_log rows than "one per run".

  • Under the CLI composition (buildDataMigrationPlugins({ settings, audit: true }) mounts AuditPlugin), an applied run writes N per-row sys_audit_log rows plus the 1 run entry.
  • Measured with a temporary probe (removed): engine + installAuditWriters + recomputeHomeAmounts(apply) over 3 rows → written 3, audit rows 4: [update a, update b, update c, update null].
  • The per-row history is arguably right (audit capture is always on). But the docs ("one per run") and the tests only see the run entry, because every test composition lacks the writers.
  • Fix: say so in cli.mdx / currency.mdx, and pin it once with the real writers.
  • This is also the first audit: true migrate command whose target is a business object, not an excluded ledger table.

S2. Two ways the run is incomplete but nothing signals it.

  • recompute-home-amounts.ts:256: the CLI exit code ignores report.truncated. A --max-records-capped walk exits 0 while homeAmountRecomputeComplete is false. Precedent record-currency-codes does the same, but this command's own completeness rule counts truncation.
  • home-amount-recompute.ts:463: an unclassified error (for example a rate read failing) is rethrown out of the walk after earlier rows were written, so the run gets no run-level audit entry. Consider try/finally writing a partial-run entry.

S3. updated_at is preserved.

S4. The CLI test docblock (recompute-home-amounts.test.ts:9) names packages/qa/dogfood/test/home-amount-recompute.test.ts, which does not exist. The file is home-amount-projection.test.ts.

S5. The code_blank remedy line points at record-currency-codes --code <ISO>. Under ADR-0144 Open item 13 / #28128 S1, that command writes one code into every organization's codeless rows. The report already has per-org code_blank counts, so a half-sentence warning would prevent a wrong deployment-wide code.

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)

  1. No second copy of the arithmetic: holds.
    • The preview calls projectHomeAmounts([entry], row, reads.serving, …) (home-amount-recompute.ts:442) over ObjectQL.homeAmountReadsFor, which returns the same private homeAmountServing and homeAmountOrganizationOf the write uses (engine-references-and-master-detail.ts:113-118).
    • The only local numeric code is sameStoredNumber, which compares and computes nothing.
    • Apply sends {id, <homeField>, updated_at}. homeAmountsTouched counts the home field as an input, so the by-id update re-projects over prior+patch (engine.ts ~10038-10046). The rate field comes from the write.
    • Fresh-write equality is pinned in memory and on 3 SQL cells. Ablations A2 and A8 in the PR body support the claim.
  2. Per organization: holds.
    • The organization is row.organization_id under the unscoped system read. rateOn runs {isSystem, tenantId: org}, so the driver scope is applied per organization.
    • Home currency and counts are per organization.
    • The in-memory two-org pin uses different currencies per org, so it does not exercise rate scoping. The dogfood isolated-posture pin does (EUR rows in both orgs, 0.128 vs 0.9→0.8), and it passes on PG.
    • Writes are by id only. No cross-org leakage found.
  3. Writes and isSystem:
  4. Skip classes: all five are listed, counted per org and reason, and never written.
    • code_blank is checked before projection, so it never folds into rate_missing (A4).
    • The remedy text names os migrate record-currency-codes --object ….
    • The Open item 8 proposal (not written; incomplete with exit 1 on dry run and apply; audit lists skipped rows, capped at 1000) is pinned (objectql + CLI) and is sensible. Rejecting a marker column is right under D8 r2.
  5. --since: filters on the document date (rateDate). A datetime is read in the org timezone via serving.timezone; a blank date is outside every window. Pinned at the Shanghai/UTC boundary in memory and on 3 SQL cells (A6).
  6. Clause-② yes (widening):
    • Purely additive: new objectql exports, the new ObjectQL.homeAmountReadsFor, and a new optional settings arm on the CLI util. Nothing narrows.
    • patch for objectql and cli is right under AGENTS.md §3 (Metadata-protocol: none ⇒ patch). Dogfood is private.
    • homeAmountReadsFor is necessary given the structural-engine design (homeAmountServing is private). It grants no privilege an in-process engine holder lacks. Optionally mark it @internal; seat's call.
  7. Ledger edits are legitimate.
    • federated-injected-column-readers rows: the function does ask isFederatedUnprovisionedInjectedColumn before selecting org/updated_at, and the file passes.
    • engine-double-contract.pinned.json: re-running --write reproduces it byte-identical (git status clean). The coverage ledger only grows.
    • The adr-anchors JSON passes check-adr-anchors.
  8. Tests: refusals assert code+status except M3; exit codes are asserted. The ablations are meaningful (A2 proves the write recomputes; A8 proves the single code path).
  9. Scope: no packages/spec/src, skills, docs/adr, .claude, ui or apps paths (grep exit 1 = none).

Commands (worktree /tmp/claude-0/wt30181 at 90fabf3; deps built with turbo, exit 0)

Command Exit Result
objectql vitest run src/engine-home-amount-projection.test.ts src/federated-injected-column-readers.test.ts 0 47/47
dogfood vitest run test/home-amount-projection.test.ts, OS_TEST_POSTGRES_URL = local PG 16.15 0 42/42, 3 cells incl. pg
cli vitest run src/commands/migrate/recompute-home-amounts.test.ts test/migrate-confirmation-exit.pin.test.ts 0 56/56
cli vitest run src/utils/schema-migrate.one-shot-family.integration.test.ts -t recompute-home-amounts 0 5 passed
cli OS_TEST_TIERS=nightly vitest run test/json-stdout-purity.e2e.test.ts -t recompute 0 3 passed
objectql pnpm run typecheck 0
cli NODE_OPTIONS=--max-old-space-size=8192 pnpm run typecheck 0
eslint on the 13 changed .ts files 0
node scripts/check-engine-double-contract.mjs 0 --write gives no diff
node scripts/check-adr-anchors.mjs 0
spec tsx scripts/check-error-code-provenance.ts 1 M1
audit-writers probe (temporary, deleted) 0 N+1 audit rows (S1)

CI check-runs on head 90fabf3: all success or skipped except Lint · docs & code guards = failure and Lint & Repo Gates = failure (M1). TypeScript Type Check = success.

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

2 participants