Repository navigation
Commit 57c51aa
feat(objectql): record-currency guards — blank code refuses the write; ungrouped sum/avg/min/max over a record-mode amount refused; summary roll-up over one refused at registration (#27339)
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_
Fixes #27186
⛔ not released alone — R1: pairs with #27184, #27186, #27187, #27188
(one Version Packages release)
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- **改了什么:** record 模式金额写入时币种码为空 ⇒
拒写(`ERR_CURRENCY_CODE_REQUIRED`,`validate()` 预览同样拒);`sum/avg/min/max` 对
record 金额不按币种码分组 ⇒ 拒收;summary 汇总 record 子字段 ⇒ 注册时拒收;公式引用 record 金额时声明为
record 币种。
- **为什么改:** 测量轮探针里 USD 100 + EUR 100 = 200、USD 100 + JPY 15000 汇总成
15100,都是静默错数;这三道门把它们变成响亮拒收(ADR-0144 D6、D8 规则 4)。
- **风险与回滚:** 只对声明了 `currencyMode: 'record'` 的字段生效,该模式由已合并的 #27184
引入,其他字段零影响;回滚即撤本 PR,但须与 R1 同批。
- **先看:**
`packages/objectql/src/validation/record-currency-code.ts`(更新路径用存量行叠加载荷判定);`packages/objectql/src/record-currency-aggregate-door.ts`;`packages/objectql/src/registry.ts`
的注册拒收位置。
- **两分钟验证:** `pnpm --filter @objectstack/objectql exec vitest run
--project local src/record-currency-guards.test.ts`,预期 13/13 通过(混币 sum
被拒、按币种码分组得到 USD 100 / EUR 100)。
- **需要你做的:** 无,席位落地(与 R1 同批发版)。
## What changes
- **Blank code refuses the write (ADR-0144 D8 rule 4, Open item 9).**
`validation/record-currency-code.ts` refuses a row image that holds a
value in a `record`-mode amount beside a blank code field (absent,
`null`, white space) with `ERR_CURRENCY_CODE_REQUIRED` / 400; `field`
names the code field, `amountField` the amount. Judged on insert (each
final row), by-id update (stored row with the final payload over it) and
predicate update (each matched row with the payload over it, read only
when the payload carries an amount or its code). An update that does not
touch the pair is never judged, so a pre-existing row is not refused for
an unrelated edit. The `validate()` preview (`engine-validate.ts`,
`validateData`) runs the same check at the write's step, on the same
image (the insert row; the stored row with the patch over it for a by-id
update), and answers it as the row's `ERR_CURRENCY_CODE_REQUIRED`
finding, so a dry run does not pass a row the write refuses (changeset
26684's parity). A predicate-dispatched update preview holds no stored
row and is not judged. The code is registered under
`@objectstack/objectql` in the ADR-0112 ledger
(`error-code-ledger.zod.ts`); the generated reference pages are
regenerated.
- **Ungrouped raw aggregate refused (ADR-0144 D6, Open item 10).**
`record-currency-aggregate-door.ts`, called in `ObjectQL.aggregate`
right after the aggregate × field-type door: `sum` / `avg` / `min` /
`max` over a `record`-mode amount answers `INVALID_FIELD` / 400 unless
`groupBy` names its code field (string or `{ field }` entry). The
message leads with the prescription (group by the code field, or
aggregate a stored home-currency amount). `count` and `count_distinct`
are not judged. No new wire code: ADR-0144 assigns ADR-0112 codes only
to the blank-code and missing-rate refusals.
- **Summary roll-up refused at registration (ADR-0144 D6).**
`SchemaRegistry.registerObject` refuses, before any materialization, a
body whose `summary` field rolls up `sum` / `avg` / `min` / `max` of a
`record`-mode child field — or a child body declaring a `record`-mode
field that a registered parent already rolls up that way.
`INVALID_METADATA` / 422 (`RecordCurrencySummaryRefusedError`), judged
at whichever registration completes the pair; an end not yet registered
is no verdict.
- **Rider, declared — from #27184's class-c finding.**
`packages/formula/src/formula-return.ts`: `currencyOf` mapped every
non-`fixed` config to `dynamic`, so `inferFormulaReturn` would stamp a
formula over a record-mode amount as a tenant-default amount. It now
carries `currencyConfig: { currencyMode: 'record', currencyField }` (the
shape #27184's object refinement already judges on a `formula`). Two
record-mode amounts are one currency only when they name the same code
field; record vs `fixed` / `dynamic` is not one currency.
- **File-surface addition, approved by the epic PM:**
`packages/qa/dogfood/test/record-currency-probes.test.ts` (test only,
private package, no changeset).
## Tests
- `packages/objectql/src/record-currency-guards.test.ts` (new, 13
cases): the blank-code refusal on insert (four blank spellings), by-id
update (clear code / amount onto a row without one / legacy row
untouched), predicate update (one matched row without a code refuses the
whole write, naming the record); the `validate()` preview, insert and
by-id update, answers the same finding, with the stored-code and
legacy-row controls valid; the aggregate door for all four functions,
grouped-by-other refused, grouped by the code answers USD 100 / EUR 100
(string and object `groupBy`), `count` and a fixed-currency sum not
judged; the summary refusal parent-second (all four functions) and
child-second, `count` and a non-record child field register. Every
thrown refusal asserts `code` + `status` + `httpStatus`; the preview's
finding asserts `field` + `code` + message.
- `packages/qa/dogfood/test/record-currency-probes.test.ts` (new):
PROBE-A, the write side and PROBE-R on `SqlDriver` over better-sqlite3
and on `SqliteWasmDriver`, 6/6. ⛔ No driver-memory arm: its test
consumers are a closed census (`check:driver-memory-census`) and the
project's test backends are sqlite `:memory:`; the in-memory aggregation
face is the objectql suite above.
- `packages/formula/src/formula-return.test.ts`: a new describe for the
record currency (stamped with its code field; ratio of two same-currency
amounts is not money; three cross-currency mixes are not money).
## Acceptance notes
- `Clause-②: yes (widening)`: the ledger gains a registered code (the
ledger header's #16404 ruling), and the formula rider widens the
exported `FormulaReturnDeclaration` by one union member. The level stays
`patch` and `Metadata-protocol: none` (precedent
`26506-toggle-flow-caller.md`).
- The update preview judges the stored row as the read door served it to
the caller. A code column withheld from that caller is absent from that
row, so the preview reads it as blank and refuses where the write would
admit: stricter than the write, never laxer, and it reveals nothing
about the withheld value.
- PROBE-A / PROBE-R come from the measurement round's design comment on
#8345 (6041378776); the probe files themselves were deleted then, so
these are re-authored fixtures with the record currency declared, not
the originals.
- A row that was stored with an amount and no code before this guard is
not refused until a write touches the amount or the code; ADR-0144 has
no backfill for that state, and `record` mode ships in the same release,
so no such row exists in a released build.
## Verification
Code readings on `2749517c`, the last commit that changes this PR's
code. Formula, spec and platform-objects readings are on `cf5be508`;
nothing after it touches those packages' code (the one spec change is a
ledger comment). On the head, `9d9f7eab` (main `f7188b56` merged in with
a merge commit; no regeneration was needed), the guards, the probes,
`check:generated` and the derived gates were re-read; see each section.
<details><summary>Tests</summary>
- `pnpm --filter @objectstack/objectql exec vitest run --project local
--maxWorkers=2 src/record-currency-guards.test.ts`: Test Files 1 passed,
Tests 13 passed (13), on `2749517c` and again on `9d9f7eab`.
- `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2
test/record-currency-probes.test.ts`: Test Files 1 passed, Tests 6
passed (6), on both SQLite drivers, on `2749517c` and again on
`9d9f7eab`.
- Full `@objectstack/objectql` local project: Test Files 479 passed
(479), Tests 8951 passed (8951).
- Full `@objectstack/formula`: Test Files 60 passed (60), Tests 1563
passed (1563).
- `@objectstack/spec` `src/data/currency-config-record-mode.test.ts`
(#27184's refinement, which the guards read): 15 passed. Full
`@objectstack/platform-objects`: 1369 passed (97 files).
- `pnpm --filter @objectstack/objectql typecheck` (tsc +
check:test-typecheck): exit 0. `@objectstack/formula` typecheck at
`cf5be508`: exit 0.
- The dogfood package typecheck was not measured, because its closure is
unbuilt in this container. No error line names the new file.
</details>
<details><summary>Ablation (one-shot; suite imports src, no build
leg)</summary>
`scripts/ablation-replace.mjs --delete` on the committed tree, run
against `record-currency-guards.test.ts`. Each time the restore was
verified (blob == HEAD) and `git diff HEAD` was empty.
- The `assertRecordCurrencyAggregatesGrouped` call in `engine.ts`: 2
failed / 10 passed (read at `cf5be508`, 12 cases).
- The insert `assertRecordCurrencyCodes` call in `engine.ts`: 1 failed /
11 passed (read at `cf5be508`, 12 cases).
- The `throw recordCurrencySummary` in `registry.ts`: 2 failed / 10
passed (read at `cf5be508`, 12 cases).
- The preview's guard in `engine-validate.ts` (the `if (mode ===
'insert' || storedRows[i])` arm made false): 1 failed / 12 passed, at
`2749517c`.
The by-id and predicate update call sites were not ablated separately.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `9d9f7eab`, change set = this PR's 14
paths against merge base `f7188b56`: 111 families derived, all 111 run.
The `--ran` reconciliation reads 109 run, 2 NOT-MEASURED, 0 UNRUN.
- NOT MEASURED (exit 3, prerequisite not met in this container):
`check-plugin-teardown-shape --self-test` (shallow clone) and spec
`check:skill-examples` (the client SDK dist predates the merged main).
CI runs both and is green.
- Red on the first sweep and fixed in this PR:
- `check:docs` / `check:generated`: the reference pages lacked the new
ledger code, so they were regenerated.
- `check:objectql-double-limit` and `check:query-options-erasure`: the
new test's driver double and option casts were fixed.
- `pnpm --filter @objectstack/spec check:generated` on `9d9f7eab`,
against a spec dist built by that run: all 14 artifacts current.
- CI on `9d9f7eab`: 50 check runs, 41 success, 9 skipped, 0 failure.
Check Changeset, Test Core (all 8 shards) and every Type Check and Lint
job pass. GitHub reports the PR `clean`.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3604371 commit 57c51aa
14 files changed
Lines changed: 1062 additions & 15 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- formula/src
- objectql/src
- validation
- qa/dogfood/test
- spec/src/api
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
350 | | - | |
| 350 | + | |
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
| |||
479 | 479 | | |
480 | 480 | | |
481 | 481 | | |
| 482 | + | |
482 | 483 | | |
483 | 484 | | |
484 | 485 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| 170 | + | |
170 | 171 | | |
171 | 172 | | |
172 | 173 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
| 337 | + | |
337 | 338 | | |
338 | 339 | | |
339 | 340 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
169 | 169 | | |
170 | 170 | | |
171 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
82 | | - | |
83 | | - | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
84 | 86 | | |
85 | 87 | | |
86 | 88 | | |
87 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
88 | 92 | | |
89 | 93 | | |
90 | | - | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
91 | 101 | | |
92 | 102 | | |
93 | 103 | | |
| |||
125 | 135 | | |
126 | 136 | | |
127 | 137 | | |
128 | | - | |
129 | | - | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
130 | 141 | | |
131 | 142 | | |
132 | 143 | | |
| |||
137 | 148 | | |
138 | 149 | | |
139 | 150 | | |
140 | | - | |
141 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
142 | 161 | | |
143 | 162 | | |
144 | 163 | | |
| |||
256 | 275 | | |
257 | 276 | | |
258 | 277 | | |
259 | | - | |
260 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
261 | 282 | | |
262 | 283 | | |
263 | 284 | | |
| |||
281 | 302 | | |
282 | 303 | | |
283 | 304 | | |
284 | | - | |
285 | | - | |
286 | | - | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
287 | 312 | | |
288 | 313 | | |
289 | 314 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
393 | 394 | | |
394 | 395 | | |
395 | 396 | | |
| 397 | + | |
| 398 | + | |
396 | 399 | | |
397 | 400 | | |
| 401 | + | |
398 | 402 | | |
399 | 403 | | |
400 | 404 | | |
| 405 | + | |
401 | 406 | | |
402 | 407 | | |
403 | 408 | | |
| |||
417 | 422 | | |
418 | 423 | | |
419 | 424 | | |
| 425 | + | |
420 | 426 | | |
421 | 427 | | |
422 | 428 | | |
| |||
543 | 549 | | |
544 | 550 | | |
545 | 551 | | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
546 | 567 | | |
547 | 568 | | |
548 | 569 | | |
| |||
571 | 592 | | |
572 | 593 | | |
573 | 594 | | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
574 | 599 | | |
575 | 600 | | |
576 | 601 | | |
| |||
0 commit comments