Repository navigation
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
Conversation
…ows stored before an amount switched to record mode - objectql: backfillRecordCurrencyCodes (record-currency-backfill.ts), modelled on summary-backfill.ts. Operator-supplied ISO 4217 code, never derived; dry run by default; idempotent; per-row counts; writes through engine.update. isRecordCurrencyBlank exported from the write guard so both judge one predicate. - cli: os migrate record-currency-codes --object --field --code [--apply]. - metadata-protocol: detectDestructiveObjectChanges gains currency_mode_to_record (409 destructive_change, force lifts it, reads no row, no boot-time refusal). Maintainer ruling 6492000994 (letter B). Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
…e_to_record finding Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
… smoke test Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
…er double, not inside it The double's own counter was unreadable to check:objectql-double-limit's probe. Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
…-mzbmzo Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 62 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 51 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7460fc4e542de72034a9b29c4836ff731db5db11 && git checkout 7460fc4e542de72034a9b29c4836ff731db5db11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 27e0bd58489c8f5606674822a68f6250f4512871 5d2f5efda8f9745e9f9e5141fca6fdb0e72dfdec && git checkout -B drift-repro 27e0bd58489c8f5606674822a68f6250f4512871 && git merge --no-ff 5d2f5efda8f9745e9f9e5141fca6fdb0e72dfdec
node scripts/docs-audit/affected-docs.mjs --json 27e0bd58489c8f5606674822a68f6250f4512871
|
…ercase code literal
check:error-code-casing reads `{ code: '<lowercase>' }` as an error.code emission;
a destructive-change finding code is the issue's own field-addressed vocabulary.
Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA
Co-authored-by: Claude <noreply@anthropic.com>
…nd-family rosters The confirmation-exit pin and the bootSchemaStack one-shot family both discover their members from the tree; the new command joins each. Claude-Session: https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: PR #28128, card #27577, governing ruling 6492000994 on #8345 (letter B). Read: the card body and both comments (claim 6498318163, os-dev-report 6523900499), the ruling, the PR body and its 11 files, the net diff against merge base ① Derived judgments
② Semver levelThe changeset ③ Boundary flags
No must-fix. Implemented-by: VERDICT: PASS |
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 席位记录 · epic PM
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Requested by Bill · project thread
Fixes #27577
Ruling-ref: 6492000994
⛔ lands before R2 (#27191 / #27192 / #27193). Not release-paired: ships in the next Version Packages.
Clause-②: yes (widening) — a new
@objectstack/objectqlexport (backfillRecordCurrencyCodesand its report helpers), a new commandos migrate record-currency-codes, and a new findingcurrency_mode_to_recordregistered in the existing 409destructive_changeface, lifted by its existingforceMetadata-protocol: none
维护者速读
dynamic/fixed金额字段切到record后,旧行有金额无币种码。新增显式回填os migrate record-currency-codes --object --field --code:只给「有金额、码为空」的行写运维给的码,默认只报告,--apply才写,可重复跑;元数据保存这种切换时返回现有 409destructive_change(findingcurrency_mode_to_record),点名三个后果和命令,force放行。engine.update,受 R1 写入守卫与valueDomain判;409 不读数据行、不加启动拒绝。回滚即撤 PR。packages/objectql/src/record-currency-backfill.ts,再看protocol.ts里detectDestructiveObjectChanges新增的一段。record-currency-backfill.test.ts(干跑、写入、幂等、有码不动、空金额跳过、坏码拒绝)与protocol-destructive-currency-mode.test.ts(dynamic/fixed→record 返回 409、新字段不触发、force放行)。What changes
@objectstack/objectql—backfillRecordCurrencyCodes(engine, logger, { object, field, code, apply, maxRecords })(record-currency-backfill.ts, modelled onsummary-backfill.ts), exported withformatRecordCurrencyBackfillReportandrecordCurrencyBackfillComplete.OBJECT_NOT_FOUND/ 404,objectNotFoundError), the field must be arecord-modecurrencyfield (recordCurrencyFieldOf, the write guard's own reading; elseINVALID_FIELD/ 400 naming the field), and the code must be aniso_4217_currencymember (isValueDomainMember, the predicate the code field'svalueDomainuses, exact spelling, no trim or case fold; elseINVALID_FIELD/ 400 naming the code field). Each refusal ends "Refused before the walk read or wrote any row."idreadingid, the amount and the code, under the unscoped system context ({ isSystem: true }, as everyos migratedata run). Per row, in this order: a code that is not blank →skippedCodePresent, never written; a blank amount →skippedAmountBlank; otherwisecodeless, and on--applyone write. "Blank" isisRecordCurrencyBlank(absent,null, or white space only), now exported fromvalidation/record-currency-code.tsso the backfill and the write guard judge one predicate: the rows filled are exactly the rows the guard would refuse on their next write of the pair. An amount of0is an amount.defaultCurrency,localization.currencyor any other source (ADR-0144 D8 rule 4).apply. Idempotent: every write turns a blank code into a member code, so a second run counts zerocodeless. One row's failure is recorded infailuresand the walk continues;maxRecords(default 100 000) truncates and says so. Nosys_migrationflag.Why
engine.update, not a driver write (the card leaves this to the precedent picked):summary-backfill.tsitself writes throughengine.updatewith the system context, as docomment-parent-pair,activity-changed-fields,audit-key-material,audit-mail-bodiesandaudit-metadata-bodies; noos migratedata run writes at the driver. The engine path is also the one that already judges this exact write: the R1 guard sees the payload touch the code beside the stored amount, and the code field'svalueDomainjudges the code. A write the backfill makes is therefore one the platform would accept from any caller, audited and hooked like any other; a driver write would step around both and record nothing.@objectstack/cli—os migrate record-currency-codes --object <o> --field <amount> --code <ISO> [--apply] [--yes] [--json] [--force] [--max-records] [--database-url](commands/migrate/record-currency-codes.ts). The shell ofos migrate summary-nulls, one for one: SQLite occupancy gate,--yesfor a non-interactive apply, read-only boot for the dry run (deferSchemaDdl+readOnlyProbe), the "no app objects loaded" refusal,refuseUndeclaredObjectsfor--object, the journal-mode note after the dry-run line and after anINVALID_FIELDrefusal on a SQLite file,--jsonenvelope witherrorCodeFields, exit 1 on any failure.--object,--fieldand--codeare required. Everything else is the engine function's.@objectstack/metadata-protocol—detectDestructiveObjectChangesgainscurrency_mode_to_record(Phase 3a-destructive,saveMetaItem): the previous field is stored, itscurrencyConfig.currencyModeis notrecord(an absent config is the defaultdynamic), and the next field is acurrencyfield withcurrencyMode: 'record'. The finding answers as the existing 409DESTRUCTIVE_CHANGEwith the existingforceremedy; its message names the code field, the three consequences (plain number, thenullgroup, outside every currency-paired filter) and the command with--objectand--fieldfilled in. It reads no row, likefield_type_changeandfield_required_no_default. A new field declared inrecordmode, a field already inrecordmode, and a move out ofrecordraise nothing. ⛔ No boot-time refusal: the gate runs only where a previous version exists (RESTPUT /meta, Studio, package publish).Not in this diff (governed paths, kept with the epic PM's Tier H PR per the card): the
skills/**line and the ADR-0144 open item with its dated note. The epic PM also adds the blank-code class to #27193. #27193's code is not touched.Declarations, confirmed against the tree.
Clause-②: yes (widening): new exports and a new command widen the public face; the finding is a new member of the existingdestructive_changerefusal face, which is the reading the card and ruling take from #27339's review 6490309706. One thing for the contract review to weigh: a save of exactly this switch that succeeded withoutforcenow answers 409 until re-sent withforce; it is a risk acknowledgement on an existing face, not a refusal of what an author can write, soMetadata-protocol: none(every body that saved still saves). One changeset,patchfor all three packages.Why
Ruling 6492000994 on #8345 (letter B): the switch is allowed and owes an explicit, audited backfill, and the R2 home projection cannot convert a codeless row, so this lands before R2.
Verification
Head
1636d8c3: the branch's four commits on1ed33609(all four R1 PRs onmain) plus a merge ofmainat549203d0(including #27991), no conflicts. After the merge:pnpm install --frozen-lockfile && pnpm build(whole workspace), then everything below on that head.packages/objectql:record-currency-backfill.test.ts8 / 8 (dry run counts every kind of row and writes nothing;--applyfills only the codeless rows through the engine,0and white-space codes included, codes and amounts otherwise untouched; idempotent; five non-member codes refused with no read; three non-record fields refused with no read; unknown objectOBJECT_NOT_FOUND; one failed write isolated, re-run finishes it; the dry-run report text).protocol-destructive-currency-mode.test.ts7 / 7 (dynamic, fixed and unset → record: 409 with the finding, the message's three consequences and command, and no data read of the object;forcelifts it; a new record-mode field, record → record, and record → dynamic / fixed raise nothing).protocol-destructive.test.ts7 / 7 unchanged.packages/cli:record-currency-codes.test.ts9 / 9 (dry run default; the three flags required and--codemissing refused; names handed through;--apply --yes; undeclared object refused before the backfill; a realINVALID_FIELDcode refusal reaches the--jsonenvelope withhttpStatus; the dry-run line with the journal-mode note on SQLite and without it on PostgreSQL).pnpm --filtertypecheck:@objectstack/objectql,@objectstack/metadata-protocol,@objectstack/clipass (test-typecheck ledgers held). eslint on every touched file: clean.scripts/ablation-replace.mjs, mutation landed and restored byte-identical each time):if (!isRecordCurrencyBlank(row[codeField]))→if (false && …): 5 / 8 red (rows holding EUR and JPY overwritten).row[codeField] != null(a null-only blank): 4 / 8 red (the white-space code row skipped as "present").false && …(metadata-protocol rebuilt under the mutation): the three 409 cases red, 3 / 7; the four non-firing cases stay green, as they must.1636d8c3:@objectstack/objectql496 files / 9615 tests passed;@objectstack/metadata-protocol284 files / 21857 passed (26 skipped, pre-existing);packages/cli/src/commands/migrate/59 files / 429 passed (8 skipped, pre-existing).dispatch-gates --commandson1636d8c3derives 65 families (9 paths vs merge base549203d0, 1254 changed lines); each ran with its exit captured before any pipe, all 65 exited 0.dispatch-gates --ranreads: "65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED". On an earlier head,check:objectql-double-limitcaught the backfill test's driver double counting reads insidefind(its probe could not drive it); the count moved to spies beside the double (3c340f77).CI. Head
5d2f5efd: 35 check runs success, 13 skipped (path-filtered), none failed;mergeable_state: clean. Two reds on the way there, both this PR's own and fixed:check:error-code-casing(not in the path-derived list) read the test's{ code: 'currency_mode_to_record' }as a lowercaseerror.codeemission; the test now reads the raised finding codes as a list (0f5c7f1f). A destructive-change finding code is the issue's own field-addressed vocabulary, asfield_type_changeis.test/migrate-confirmation-exit.pin.test.ts,src/utils/schema-migrate.one-shot-family.integration.test.ts) did not yet name the new command; both rows added (5d2f5efd). The one-shot fixture declares no record-mode amount, so its two cases reach the boot and then the backfill'sINVALID_FIELDrefusal, the same shape as theresumeandplugin trust revokerows. Full@objectstack/clisuite locally: 535 files / 6680 tests passed (9 skipped, pre-existing).Human Minimum
packages/specholds no runtime logic. (not touched)main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA
Generated by Claude Code