Repository navigation
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
Conversation
…unt points at a sibling ISO 4217 code field Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…ent in the dropped-refinements ledger Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…mode sites Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…field naming convention, paid by tightening the currency block Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…cord-mode currencyField Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…, ungrouped raw aggregate refused, summary roll-up refused at registration ADR-0144 D6 and D8 rule 4. A record-mode amount beside a blank currency code is refused ERR_CURRENCY_CODE_REQUIRED (registered in the ADR-0112 ledger); sum/avg/min/max over a record-mode amount refuses INVALID_FIELD unless grouped by its code field; a summary roll-up reading one refuses INVALID_METADATA at registration. Rider: inferFormulaReturn carries record mode and its currencyField for a formula over a record-mode amount. Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…UIRED Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
…ns and hold the find double's limit Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
…ore copying rows Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
…e field-level answer never reads a code from a record-mode config Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…urrencyField row The field form's new `currencyConfig.currencyField` row is one more unwalked composite child (10 -> 11) and one more translated row label in each locale's positive control (695 -> 696). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
… default Under `currencyMode: 'record'` each record names its own code in the sibling field, which only a per-row renderer can read. The field-level answer is now `undefined` outright, so a record with a blank code shows no symbol instead of a currency it never named. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
The authorable-surface baseline is taken from main (#27233 retired `CurrencyValue`) and rebuilt, keeping `currencyConfig.currencyField`; the field reference page is regenerated from the merged tree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
…roject-thread-1v9hrc # Conflicts: # packages/formula/src/formula-return.ts
Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: Gates on ① Derived judgments
② Semver levelChangeset
③ Boundary flags
④ Design judgment —
|
… code; Clause-② declared as a widening The insert and by-id update previews now judge the write's blank-code refusal on the write's own image (the insert row, or the stored row with the patch over it) and answer it as the row's ERR_CURRENCY_CODE_REQUIRED finding, so a dry run no longer passes a row the write refuses (changeset 26684's parity). The changeset declares Clause-②: yes (widening): the ledger gains a registered code and the formula rider widens the exported FormulaReturnDeclaration. The ledger comment says the write is never asked, not the driver. Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
…roject-thread-1v9hrc
The authorable-surface baseline is taken from main and rebuilt, keeping `currencyConfig.currencyField`; the API reference pages are regenerated from the merged tree. Claude-Session: https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: Gates on ① Derived judgments
② Semver levelChangeset and live body: ③ Boundary flags
④ Design judgment —
|
…roject-thread-1v9hrc
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: Gates on ① Derived judgments
② Semver levelLive body (v4) lines 8–9: ③ Boundary flags
④ Design judgment —
|
and #27339 Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Requested by Bill · project thread
Fixes #27186
⛔ not released alone — R1: pairs with #27184, #27186, #27187, #27188 (one Version Packages release)
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
ERR_CURRENCY_CODE_REQUIRED,validate()预览同样拒);sum/avg/min/max对 record 金额不按币种码分组 ⇒ 拒收;summary 汇总 record 子字段 ⇒ 注册时拒收;公式引用 record 金额时声明为 record 币种。currencyMode: 'record'的字段生效,该模式由已合并的 spec(field):currencyMode: 'record'+currencyField— a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345) #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)。What changes
validation/record-currency-code.tsrefuses a row image that holds a value in arecord-mode amount beside a blank code field (absent,null, white space) withERR_CURRENCY_CODE_REQUIRED/ 400;fieldnames the code field,amountFieldthe 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. Thevalidate()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'sERR_CURRENCY_CODE_REQUIREDfinding, 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/objectqlin the ADR-0112 ledger (error-code-ledger.zod.ts); the generated reference pages are regenerated.record-currency-aggregate-door.ts, called inObjectQL.aggregateright after the aggregate × field-type door:sum/avg/min/maxover arecord-mode amount answersINVALID_FIELD/ 400 unlessgroupBynames 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).countandcount_distinctare not judged. No new wire code: ADR-0144 assigns ADR-0112 codes only to the blank-code and missing-rate refusals.SchemaRegistry.registerObjectrefuses, before any materialization, a body whosesummaryfield rolls upsum/avg/min/maxof arecord-mode child field — or a child body declaring arecord-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.currencyMode: 'record'+currencyField— a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345) #27184's class-c finding.packages/formula/src/formula-return.ts:currencyOfmapped every non-fixedconfig todynamic, soinferFormulaReturnwould stamp a formula over a record-mode amount as a tenant-default amount. It now carriescurrencyConfig: { currencyMode: 'record', currencyField }(the shape spec(field):currencyMode: 'record'+currencyField— a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345) #27184's object refinement already judges on aformula). Two record-mode amounts are one currency only when they name the same code field; record vsfixed/dynamicis not one currency.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); thevalidate()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 objectgroupBy),countand a fixed-currency sum not judged; the summary refusal parent-second (all four functions) and child-second,countand a non-record child field register. Every thrown refusal assertscode+status+httpStatus; the preview's finding assertsfield+code+ message.packages/qa/dogfood/test/record-currency-probes.test.ts(new): PROBE-A, the write side and PROBE-R onSqlDriverover better-sqlite3 and onSqliteWasmDriver, 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 [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landedno, #15963 landsyes, and they are the same class #16404 ruling), and the formula rider widens the exportedFormulaReturnDeclarationby one union member. The level stayspatchandMetadata-protocol: none(precedent26506-toggle-flow-caller.md).recordmode 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 oncf5be508; nothing after it touches those packages' code (the one spec change is a ledger comment). On the head,9d9f7eab(mainf7188b56merged in with a merge commit; no regeneration was needed), the guards, the probes,check:generatedand the derived gates were re-read; see each section.Tests
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), on2749517cand again on9d9f7eab.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, on2749517cand again on9d9f7eab.@objectstack/objectqllocal project: Test Files 479 passed (479), Tests 8951 passed (8951).@objectstack/formula: Test Files 60 passed (60), Tests 1563 passed (1563).@objectstack/specsrc/data/currency-config-record-mode.test.ts(spec(field):currencyMode: 'record'+currencyField— a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345) #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/formulatypecheck atcf5be508: exit 0.Ablation (one-shot; suite imports src, no build leg)
scripts/ablation-replace.mjs --deleteon the committed tree, run againstrecord-currency-guards.test.ts. Each time the restore was verified (blob == HEAD) andgit diff HEADwas empty.assertRecordCurrencyAggregatesGroupedcall inengine.ts: 2 failed / 10 passed (read atcf5be508, 12 cases).assertRecordCurrencyCodescall inengine.ts: 1 failed / 11 passed (read atcf5be508, 12 cases).throw recordCurrencySummaryinregistry.ts: 2 failed / 10 passed (read atcf5be508, 12 cases).engine-validate.ts(theif (mode === 'insert' || storedRows[i])arm made false): 1 failed / 12 passed, at2749517c.The by-id and predicate update call sites were not ablated separately.
Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon9d9f7eab, change set = this PR's 14 paths against merge basef7188b56: 111 families derived, all 111 run. The--ranreconciliation reads 109 run, 2 NOT-MEASURED, 0 UNRUN.check-plugin-teardown-shape --self-test(shallow clone) and speccheck:skill-examples(the client SDK dist predates the merged main). CI runs both and is green.check:docs/check:generated: the reference pages lacked the new ledger code, so they were regenerated.check:objectql-double-limitandcheck:query-options-erasure: the new test's driver double and option casts were fixed.pnpm --filter @objectstack/spec check:generatedon9d9f7eab, against a spec dist built by that run: all 14 artifacts current.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 PRclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CboxRwQPbQZnhxyuzt