Repository navigation
Commit e2bcd5c
feat(objectql, cli)!: run the record-currency code backfill one organization at a time (#30737)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D5TuSiSXrriwnF8WM65LyQr)_
domain:server#1 · os-elon-musk · session_016WV4cyT9hHRjH6qFYychv1
Fixes #30674
Clause-②: yes (narrowing) — `backfillRecordCurrencyCodes` and `os
migrate record-currency-codes`, given no organization over the rows of
more than one organization, are refused with `400 VALIDATION_ERROR`
(`field: 'organizationId'`) before any write, where they wrote the one
code into every organization's codeless rows; widened beside it: the
optional `organizationId` / `allOrganizations` options, the `--org` /
`--all-organizations` flags, and `recomputeHomeAmounts`'s optional
`organizationId` with `os migrate recompute-home-amounts --org`.
Metadata-protocol: none
Skills-docs: follow-up owed —
skills/objectstack-data/rules/field-types.md (its "Backfill: `os migrate
record-currency-codes`" hint names neither `--org` nor
`--all-organizations`, so on a multi-organization database it leads to
the refusal; `content/docs/deployment/cli.mdx` is updated here)
Claim: 6671428099 (amended 6671472723, 6671734394) · Branch:
`claude/project-thread-erl0zi` · Ruling-ref: 6670707546 (letter C, items
A and 3)
## 维护者速读
- 改了什么:币种码回填按组织走。`os migrate record-currency-codes --org ID`
只写这个组织的无码行;库里这个对象的行属于多个组织、又没给 `--org` 时,命令在任何写入前拒跑(退出
1),并列出每个组织的无码行数;`--all-organizations` 是显式「一码写全部组织」;两个一起给是用法错误。重算命令
`recompute-home-amounts` 加了同样的 `--org`,不给照旧跑全部组织、不拒跑。
- 为什么改:每个组织有自己的本位币,一个 `--code` 写进所有组织的无码行,对其他组织是静默写错(ADR-0144 Open item
13,裁决 6670707546 C 的 A 项和第 3 项)。
- 风险与回滚:小。多组织库上不带范围的跑法从「写」变「拒」,这是本单的目的(BREAKING,changeset
已写迁移说明);单组织库行为和报告文字不变,已钉。回滚即撤 PR。
-
先看:`packages/objectql/src/record-currency-backfill.ts`(`resolveScope`、`backfillRecordCurrencyCodes`
的先数后写、`walkCodelessRows`
的组织过滤),`packages/cli/src/commands/migrate/record-currency-codes.ts`
的拒跑输出,`packages/objectql/src/home-amount-recompute.ts` 的
`organizationId`。
- 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run
src/record-currency-backfill.test.ts -t "one organization at a time"`,预期
8 条全绿,其中「two organizations and no scope」断言零写入。
- 需要你做的:无,席位落地。
## What changes
**`@objectstack/objectql` — `backfillRecordCurrencyCodes`**
(`record-currency-backfill.ts`)
- New options `organizationId` and `allOrganizations`. The report gains
`organizationId`, `allOrganizations` and `organizations` (the same
counts per organization, ordered by id, null last).
- `organizationId`: the walk and every `engine.update` run under `{
isSystem: true, tenantId: organizationId }` (ruling item A.1), and the
keyset walk also carries `where: { organization_id: organizationId }`.
See the first acceptance note for why both.
- Neither option: the rows are walked once read-only and counted per
organization. More than one named organization ⇒ `VALIDATION_ERROR` /
400, `field: 'organizationId'`, with `organizations` on the error,
before any write, on a dry run as on an apply run. Otherwise the run
proceeds exactly as before (an apply run walks a second time to write).
- `allOrganizations`: the one code into every organization's codeless
rows, under the unscoped system context (today's behaviour, now
explicit).
- Refused before any row is read: both options together, a blank
`organizationId` (`VALIDATION_ERROR` / 400), and `organizationId` on an
object that stores no `organization_id` (`INVALID_FIELD` / 400, `field:
'organization_id'`).
- The report text gains a scope phrase and per-organization lines only
for a scoped or all-organizations run; an unscoped single-organization
run prints exactly what it printed before.
- ⛔ No organization's `localization.currency` is read (ADR-0144 D8 rule
4). The card allowed showing it as a reference in the refusal; this PR
does not, so the backfill keeps reading nothing about currencies but the
operator's `--code`.
**`@objectstack/objectql` — `recomputeHomeAmounts`**
(`home-amount-recompute.ts`, ruling item 3)
- New option `organizationId`: walk, the write's reads
(`homeAmountReadsFor`) and every update run under `tenantId`, the walk
filters on `organization_id`, and the report and the run's audit entry
carry `organizationId`. Without it, every organization, as before, never
refused.
- Refused before any row is read: a blank organization
(`VALIDATION_FAILED`, the run's existing code for a bad option) and an
organization on an object that stores no `organization_id`
(`INVALID_FIELD`).
- The `code_blank` remedy line and the header now name `--org` instead
of warning that the backfill writes every organization.
**`@objectstack/cli`**
- `os migrate record-currency-codes`: `--org ID`, `--all-organizations`
(oclif `exclusive` pair, so both together is a usage error, exit 2,
before the probe and the boot). The multi-organization refusal prints
each organization's codeless count and names both flags; under `--json`
the envelope carries `code`, `httpStatus` and `organizations`. The
`--apply` prompt names the scope.
- `os migrate recompute-home-amounts`: `--org ID`, handed to
`recomputeHomeAmounts` as `organizationId`.
- The command only parses and prints; scope, refusal and counts are the
engine function's, so the REST door (the B card) reuses them unchanged.
**Docs.** `content/docs/deployment/cli.mdx`: a "One organization at a
time" paragraph and an example for `record-currency-codes`, the `--org`
sentence and example for `recompute-home-amounts`, and the `code_blank`
row of its skip table.
## Tests
- `packages/objectql/src/record-currency-backfill.test.ts`, new describe
"one organization at a time" (memory driver): two organizations and no
scope ⇒ refused on dry and apply, both counts, `driver.update` never
called; `organizationId` writes only A and every walk page carries the
filter; under `isolated` the driver also receives `tenantId`;
`allOrganizations` writes both, counted apart; both options / blank ⇒
refused before any read; a tenancy-disabled object ⇒ `INVALID_FIELD`;
one organization plus rows naming none ⇒ today's behaviour and today's
report text.
- `packages/objectql/src/engine-home-amount-projection.test.ts`, new
describe "recompute: an organization filter": only B's row rewritten,
A's stored amount untouched, audit entry carries the organization;
without it both; refusals before any read.
- `packages/cli/src/commands/migrate/record-currency-codes.test.ts`: the
flags reach the engine; the pair is a usage error with nothing probed,
booted or walked; the real engine's multi-organization refusal reaches
the `--json` envelope with both counts and exits 1; the human face lists
both and names both flags. `recompute-home-amounts.test.ts`: `--org`
reaches the engine.
-
`packages/qa/dogfood/test/record-currency-backfill-organizations.test.ts`
(new) on better-sqlite3, sqlite-wasm and PostgreSQL, under the default
`single` posture: the refusal with zero writes, `--org` A then B with
B's own code, all organizations, one organization unchanged.
`home-amount-projection.test.ts`: the recompute's organization filter on
the same three cells.
- `packages/objectql/src/federated-injected-column-readers.test.ts`: the
backfill's new `organization_id` read is registered as a `skips` site
(its guard reds on an unregistered reader).
## Acceptance notes
- **The system context alone does not scope a read under `single`.**
`engine-driver-read-scope.test.ts` pins that `{ isSystem: true, tenantId
}` reaches the driver as an organization predicate only under a walled
posture (`isolated` / `group`); under `single` the verdict is `none`. A
CLI boot resolves the posture from its own shell (`OS_TENANCY_POSTURE`,
else `single`), which need not be the server's. So both runs also filter
on the row's own `organization_id`. Measured by ablation below: without
the filter the dogfood scoped pin goes red on all three drivers.
- **"More than one organization" is read from the object's own rows**,
the distinct non-null `organization_id` values the walk meets, not from
`sys_organization`. An organization with no row of the object has
nothing the code could be wrong for, and rows that name no organization
never make a run multi-organization (they ride along with an unscoped
run as before).
- The organization's `localization.currency` is not shown beside the
refusal's counts (the card made it optional); a door that wants it can
add it from `homeAmountReadsFor` without touching this function.
- **Commit trailers.** The four commits on this branch are WIP commits
without the `Claude-Session:` / `Co-authored-by:` trailer pair. The
branch is already pushed and is not force-pushed, so the pair belongs in
the squash message at landing.
- **Not re-exported.** The new types
`RecordCurrencyBackfillOrganization` and
`RecordCurrencyBackfillOrganizationsRefusal` are exported from the
module but not from the `@objectstack/objectql` barrel, which names its
exports one by one and lies outside this card's file surface. The CLI
recognises the refusal by its `organizations` array. The B card (the
REST door) can add both to the barrel when it needs them.
## Verification
<details><summary>Local tests (head f7b7814)</summary>
- `pnpm --filter @objectstack/objectql exec vitest run` (whole package):
556 files, 11079 tests passed.
- `pnpm --filter @objectstack/objectql exec vitest run
src/record-currency-backfill.test.ts
src/engine-home-amount-projection.test.ts
src/federated-injected-column-readers.test.ts`: green.
- `pnpm --filter @objectstack/cli exec vitest run --project integration
src/commands/migrate/record-currency-codes.test.ts
src/commands/migrate/recompute-home-amounts.test.ts`: 2 files, 26 tests
passed (the migrate command tests live in the `integration` project;
`--project unit` selects nothing).
- `pnpm --filter @objectstack/dogfood exec vitest run
test/record-currency-backfill-organizations.test.ts
test/home-amount-projection.test.ts` with `OS_TEST_POSTGRES_URL` set
(local PostgreSQL 16): 2 files, 57 tests passed, the PostgreSQL cells
included.
- Type-check of `@objectstack/objectql`, `@objectstack/cli` and
`@objectstack/dogfood`: exit 0.
- `pnpm lint`: exit 0.
</details>
<details><summary>Ablations</summary>
Each mutation was made on disk, measured red, restored, and measured
green again.
1. The refusal (`named.length > 1` changed to `> 999` in
`backfillRecordCurrencyCodes`): the objectql pin "two organizations and
no scope" goes red; restored green.
2. The organization filter (the `where` on the scoped walk removed): two
objectql pins go red. Then, because dogfood resolves
`@objectstack/objectql` through its `dist`, the same mutation was
rebuilt into `dist`, `scripts/ablation-dist-preflight.mjs` confirmed the
mutation was present in the built output, and the scoped dogfood pin
went red on all three cells (better-sqlite3, sqlite-wasm, PostgreSQL).
After the restore and rebuild, the preflight reported the marker absent,
the tree was clean, and all cells were green.
A first attempt at ablation 2 mutated only `src` and the dogfood leg
stayed green. That was a false green: the dogfood leg never read the
mutated source. It is not counted above. The rebuild and preflight above
replace it.
</details>
<details><summary>Derived gates (dispatch-gates --commands, reconciled
with --ran)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 94 families on this tree. Each was
run with its exit code captured before any pipe (`gates-final.txt`).
```
Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED (a DERIVED zero — all 94 recorded an exit code and none of them is 3).
```
On the first pass, three families exited 3 (PREREQUISITE NOT MET), which
are not findings:
- `check-plugin-teardown-shape --self-test` needed a deeper clone.
- `check:skill-examples` and `check:dual-build-cjs-loads` needed built
`dist/` for client-react, studio, embedder-openai, knowledge-memory,
knowledge-ragflow, service-cluster-redis and service-knowledge.
After `git fetch --unshallow origin main` and a build of those packages,
all three were rerun and exited 0. The CI families outside the
derivation are read from the PR's check-runs, not rerun here.
</details>
<details><summary>Full dogfood battery (final head)</summary>
`f7b7814e` · `bash scripts/pm/os-verify-lock.sh -c "pnpm --filter
@objectstack/dogfood test"` (no shard, no path), after the
`@objectstack/dogfood^...` closure build, with `OS_TEST_POSTGRES_URL`
set to a local PostgreSQL 16:
```
Test Files 422 passed | 3 skipped (425)
Tests 3547 passed | 17 skipped (3564)
os-verify-lock: VERDICT command-exit 0 · held the lock 1466s (24m26s)
```
No failures.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cyT9hHRjH6qFYychv1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e00bcc6 commit e2bcd5c
14 files changed
Lines changed: 904 additions & 39 deletions
File tree
- .changeset
- content/docs
- data-modeling
- deployment
- packages
- cli/src/commands/migrate
- objectql/src
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
144 | | - | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
145 | 150 | | |
146 | 151 | | |
147 | 152 | | |
| |||
362 | 367 | | |
363 | 368 | | |
364 | 369 | | |
365 | | - | |
366 | | - | |
| 370 | + | |
| 371 | + | |
367 | 372 | | |
368 | 373 | | |
369 | 374 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1623 | 1623 | | |
1624 | 1624 | | |
1625 | 1625 | | |
| 1626 | + | |
1626 | 1627 | | |
1627 | 1628 | | |
1628 | 1629 | | |
| |||
1634 | 1635 | | |
1635 | 1636 | | |
1636 | 1637 | | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
1637 | 1653 | | |
1638 | 1654 | | |
1639 | 1655 | | |
| |||
1658 | 1674 | | |
1659 | 1675 | | |
1660 | 1676 | | |
| 1677 | + | |
1661 | 1678 | | |
1662 | 1679 | | |
1663 | 1680 | | |
| |||
1683 | 1700 | | |
1684 | 1701 | | |
1685 | 1702 | | |
1686 | | - | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
1687 | 1707 | | |
1688 | 1708 | | |
1689 | 1709 | | |
| |||
1693 | 1713 | | |
1694 | 1714 | | |
1695 | 1715 | | |
1696 | | - | |
| 1716 | + | |
1697 | 1717 | | |
1698 | 1718 | | |
1699 | 1719 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
136 | 143 | | |
137 | 144 | | |
138 | 145 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
63 | 69 | | |
64 | 70 | | |
65 | 71 | | |
| |||
70 | 76 | | |
71 | 77 | | |
72 | 78 | | |
| 79 | + | |
73 | 80 | | |
74 | 81 | | |
75 | 82 | | |
| |||
89 | 96 | | |
90 | 97 | | |
91 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
92 | 103 | | |
93 | 104 | | |
94 | 105 | | |
| |||
168 | 179 | | |
169 | 180 | | |
170 | 181 | | |
171 | | - | |
172 | | - | |
| 182 | + | |
| 183 | + | |
173 | 184 | | |
174 | 185 | | |
175 | 186 | | |
| |||
228 | 239 | | |
229 | 240 | | |
230 | 241 | | |
| 242 | + | |
231 | 243 | | |
232 | 244 | | |
233 | 245 | | |
| |||
0 commit comments