Skip to content

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

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-mzbmzo
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-mzbmzo

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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/objectql export (backfillRecordCurrencyCodes and its report helpers), a new command os migrate record-currency-codes, and a new finding currency_mode_to_record registered in the existing 409 destructive_change face, lifted by its existing force
Metadata-protocol: none

维护者速读

  • 改了什么: 已有数据的 dynamic / fixed 金额字段切到 record 后,旧行有金额无币种码。新增显式回填 os migrate record-currency-codes --object --field --code:只给「有金额、码为空」的行写运维给的码,默认只报告,--apply 才写,可重复跑;元数据保存这种切换时返回现有 409 destructive_change(finding currency_mode_to_record),点名三个后果和命令,force 放行。
  • 为什么改: 不回填,这些行显示为纯数字、分组落进 null 组、币种筛选看不到,R2 合并会漏算。
  • 风险与回滚: 码从不推断;有码的行从不改;非 ISO 4217 码在读任何行前拒绝;写入走 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 放行)。
  • 需要您做的: 无,已裁 B;本 PR 不触及受治理路径,skills 一行与 ADR 开放项由 epic PM 另开 Tier H PR。

What changes

@objectstack/objectql — backfillRecordCurrencyCodes(engine, logger, { object, field, code, apply, maxRecords }) (record-currency-backfill.ts, modelled on summary-backfill.ts), exported with formatRecordCurrencyBackfillReport and recordCurrencyBackfillComplete.

  • Before any row is read: the object must resolve (OBJECT_NOT_FOUND / 404, objectNotFoundError), the field must be a record-mode currency field (recordCurrencyFieldOf, the write guard's own reading; else INVALID_FIELD / 400 naming the field), and the code must be an iso_4217_currency member (isValueDomainMember, the predicate the code field's valueDomain uses, exact spelling, no trim or case fold; else INVALID_FIELD / 400 naming the code field). Each refusal ends "Refused before the walk read or wrote any row."
  • The walk: a keyset walk over id reading id, the amount and the code, under the unscoped system context ({ isSystem: true }, as every os migrate data run). Per row, in this order: a code that is not blank → skippedCodePresent, never written; a blank amount → skippedAmountBlank; otherwise codeless, and on --apply one write. "Blank" is isRecordCurrencyBlank (absent, null, or white space only), now exported from validation/record-currency-code.ts so 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 of 0 is an amount.
  • ⛔ The code is the caller's, never derived: nothing reads defaultCurrency, localization.currency or any other source (ADR-0144 D8 rule 4).
  • Dry run unless apply. Idempotent: every write turns a blank code into a member code, so a second run counts zero codeless. One row's failure is recorded in failures and the walk continues; maxRecords (default 100 000) truncates and says so. No sys_migration flag.

Why engine.update, not a driver write (the card leaves this to the precedent picked): summary-backfill.ts itself writes through engine.update with the system context, as do comment-parent-pair, activity-changed-fields, audit-key-material, audit-mail-bodies and audit-metadata-bodies; no os migrate data 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's valueDomain judges 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 of os migrate summary-nulls, one for one: SQLite occupancy gate, --yes for a non-interactive apply, read-only boot for the dry run (deferSchemaDdl + readOnlyProbe), the "no app objects loaded" refusal, refuseUndeclaredObjects for --object, the journal-mode note after the dry-run line and after an INVALID_FIELD refusal on a SQLite file, --json envelope with errorCodeFields, exit 1 on any failure. --object, --field and --code are required. Everything else is the engine function's.

@objectstack/metadata-protocol — detectDestructiveObjectChanges gains currency_mode_to_record (Phase 3a-destructive, saveMetaItem): the previous field is stored, its currencyConfig.currencyMode is not record (an absent config is the default dynamic), and the next field is a currency field with currencyMode: 'record'. The finding answers as the existing 409 DESTRUCTIVE_CHANGE with the existing force remedy; its message names the code field, the three consequences (plain number, the null group, outside every currency-paired filter) and the command with --object and --field filled in. It reads no row, like field_type_change and field_required_no_default. A new field declared in record mode, a field already in record mode, and a move out of record raise nothing. ⛔ No boot-time refusal: the gate runs only where a previous version exists (REST PUT /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 existing destructive_change refusal 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 without force now answers 409 until re-sent with force; it is a risk acknowledgement on an existing face, not a refusal of what an author can write, so Metadata-protocol: none (every body that saved still saves). One changeset, patch for 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 on 1ed33609 (all four R1 PRs on main) plus a merge of main at 549203d0 (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.ts 8 / 8 (dry run counts every kind of row and writes nothing; --apply fills only the codeless rows through the engine, 0 and 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 object OBJECT_NOT_FOUND; one failed write isolated, re-run finishes it; the dry-run report text). protocol-destructive-currency-mode.test.ts 7 / 7 (dynamic, fixed and unset → record: 409 with the finding, the message's three consequences and command, and no data read of the object; force lifts it; a new record-mode field, record → record, and record → dynamic / fixed raise nothing). protocol-destructive.test.ts 7 / 7 unchanged.
  • packages/cli: record-currency-codes.test.ts 9 / 9 (dry run default; the three flags required and --code missing refused; names handed through; --apply --yes; undeclared object refused before the backfill; a real INVALID_FIELD code refusal reaches the --json envelope with httpStatus; the dry-run line with the journal-mode note on SQLite and without it on PostgreSQL).
  • pnpm --filter typecheck: @objectstack/objectql, @objectstack/metadata-protocol, @objectstack/cli pass (test-typecheck ledgers held). eslint on every touched file: clean.
  • Ablations (scripts/ablation-replace.mjs, mutation landed and restored byte-identical each time):
    1. the backfill's code-present test if (!isRecordCurrencyBlank(row[codeField])) → if (false && …): 5 / 8 red (rows holding EUR and JPY overwritten).
    2. the same test → row[codeField] != null (a null-only blank): 4 / 8 red (the white-space code row skipped as "present").
    3. the finding's condition → false && … (metadata-protocol rebuilt under the mutation): the three 409 cases red, 3 / 7; the four non-firing cases stay green, as they must.
  • Full suites on 1636d8c3: @objectstack/objectql 496 files / 9615 tests passed; @objectstack/metadata-protocol 284 files / 21857 passed (26 skipped, pre-existing); packages/cli/src/commands/migrate/ 59 files / 429 passed (8 skipped, pre-existing).
  • Derived gates: dispatch-gates --commands on 1636d8c3 derives 65 families (9 paths vs merge base 549203d0, 1254 changed lines); each ran with its exit captured before any pipe, all 65 exited 0. dispatch-gates --ran reads: "65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED". On an earlier head, check:objectql-double-limit caught the backfill test's driver double counting reads inside find (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 lowercase error.code emission; 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, as field_type_change is.
  • Test Core: two CLI rosters that discover their members from the tree (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's INVALID_FIELD refusal, the same shape as the resume and plugin trust revoke rows. Full @objectstack/cli suite locally: 535 files / 6680 tests passed (9 skipped, pre-existing).

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CbJa6hKdimkAau1nbA


Generated by Claude Code

…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>
…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/metadata-protocol, @objectstack/objectql, touching 36 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.

62 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 27e0bd58489c8f5606674822a68f6250f4512871.

⛔ 10 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)
  • 15 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 51 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 27e0bd58489c8f5606674822a68f6250f4512871 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7460fc4e542de72034a9b29c4836ff731db5db11 — the merge of head 5d2f5efda8f9745e9f9e5141fca6fdb0e72dfdec into base 27e0bd58489c8f5606674822a68f6250f4512871, 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 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

⚠️ 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 27e0bd58489c8f5606674822a68f6250f4512871 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract-review-claim: session_01Utp6CbTZrFqRYNgjSaSbN5 · head 5d2f5ef · 2026-10-30T14:51Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 5d2f5efda8f9745e9f9e5141fca6fdb0e72dfdec
Local-runs: none

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 549203d0, and the 52 check-runs on this head. Gates: 39 success, 13 skipped (path-filtered), 0 failed, among them Test Core 1-8/8, Build Core, every Type Check and Lint lane, Check Changeset, PR-body guards and Governed Surface Queue Guard. No governed path is in the diff: the skills/** line, the ADR-0144 open item and the #27193 blank-code class stay with the epic PM, as the card says.

① Derived judgments

  • 409 finding currency_mode_to_record (packages/metadata-protocol/src/protocol.ts, detectDestructiveObjectChanges): right. It fires when the next field is a currency field with currencyConfig.currencyMode === 'record', the previous field exists, its mode is not record, and it is stored (isStored, the same storage predicate its siblings use). An absent previous config counts as dynamic, which is the spec default (field.zod.ts 667, .default('dynamic')), so unset to record fires correctly and record to record does not. A brand-new field never reaches it (if (!prevField) continue). It reads no row: the whole detector is a pure function of the two bodies. The finding lives in the existing issues array, so it answers as the existing DESTRUCTIVE_CHANGE / 409 with the per-face destructiveChangeRemedy. The only call site is saveMetaItem Phase 3a, inside if (!request.force && singularType === 'object'), and it runs only when readMetaItem returns a prev. So force lifts it, it fires only where a previous version exists, and it never fires at boot, because code-declared registration never calls saveMetaItem. No other tree file lists destructive finding codes (no enum or client roster to extend), and check:error-code-casing is green. The message names the code field, the three consequences and the command with --object and --field filled in, as the card requires.
  • Backfill backfillRecordCurrencyCodes (packages/objectql/src/record-currency-backfill.ts): right.
    • The code comes only from the operator: options.code is the only value written, and nothing reads defaultCurrency or localization.currency.
    • A bad code is refused before any read: resolvePair runs before keysetWalk is built, and refuses an unknown object (objectNotFoundError, 404), a non-record field (recordCurrencyFieldOf, the write guard's own reader; INVALID_FIELD, 400) and a non-member code (isValueDomainMember('iso_4217_currency', code), exact spelling; INVALID_FIELD, 400, naming the code field). The spec makes currencyField a field declaring valueDomain: 'iso_4217_currency' (object.zod.ts 1897), so this predicate is that field's own accept-set.
    • Only rows with a non-blank amount and a blank code are written: the code-present test runs first, then the amount-blank test, both through isRecordCurrencyBlank, which is now exported from the guard module and aliased back to isBlank there. The guard and the backfill therefore judge blankness by one predicate, and an amount of 0 counts as an amount.
    • It is a dry run unless apply === true, and a dry run calls no update.
    • It is idempotent: every write turns a blank code into a member code, and the keyset walk over id is not disturbed by writes that leave id alone.
    • Writes go through engine.update(object, { id, [codeField]: code }, { context: { isSystem: true } }), the same call shape as summary-backfill.ts (line 412). The R1 guard therefore judges the touched pair, and valueDomain judges the code. One row's failure is recorded and the walk continues.
  • Exported objectql surface (packages/objectql/src/index.ts): right. It adds backfillRecordCurrencyCodes, formatRecordCurrencyBackfillReport and recordCurrencyBackfillComplete, plus five types. The package has the single . export, so isRecordCurrencyBlank stays internal. The changeset names it only as the shared predicate and does not claim it as an export.
  • CLI command os migrate record-currency-codes (packages/cli/src/commands/migrate/record-currency-codes.ts): right.
    • --object, --field and --code are required.
    • It follows the summary-nulls shell one for one: the occupancy gate before boot, --yes or a TTY prompt for --apply, a read-only boot for the dry run (deferSchemaDdl + readOnlyProbe), the no-app-objects refusal, refuseUndeclaredObjects, the journal-mode note, the --json envelope with errorCodeFields, and exit 1 on failures. It exits 0 on truncation, the same as summary-nulls.
    • oclif discovers commands from dist/commands/**/*.js, so nothing needs registering. The two tree-discovered rosters gained their rows, and Test Core is green.

② Semver level

The changeset .changeset/27577-record-currency-code-backfill.md gives patch to @objectstack/objectql, @objectstack/cli and @objectstack/metadata-protocol, and those are exactly the three packages whose published code the diff changes. The rest of the diff is test files. Metadata-protocol: none is right: no spec key, value or metadata type is added, removed or re-meant, and every body that saved before still saves (with force for this one switch, which is the ruling's own reading). AGENTS.md's rule that none takes patch therefore holds. Clause-②: yes (widening) is right: there are new exports, a new command and a new member registered on the existing 409 refusal face. That is the reading of #27339 review 6490309706 that the ruling adopts word for word. The PR body and the changeset carry the same two lines, and Check Changeset is green.

③ Boundary flags

  • Dev flag (os-dev-report 6523900499): a non-forced save of exactly this switch now answers 409 until it is re-sent with force. Answered: accepted. The ruling requires this 409 by name (item 3), and it names "B′ (backfill without the 409)" as not chosen. It is an acknowledgement on an existing face with an existing remedy, the same kind as field_required_no_default. It does not narrow what an author can write, so the none / (widening) declarations stand.
  • open_questions: [], nothing to answer.
  • should-fix S1, escalated to the epic PM: the walk runs under the unscoped system context, so one --code is written across every organization's rows. A dynamic amount was displayed in each tenant's own localization.currency, so on a multi-organization deployment the codeless rows of different organizations may be in different currencies. The tool has no organization scope and no per-organization count to split them by. The ruling's text ("every row", operator-supplied, nothing derived) is what the diff implements. The module doc acknowledges that a mixed field must first be split by the operator's own writes. This is not a defect against the ruling, so it is not a must-fix, but an organization scope (or per-organization counts in the report) belongs with the open cloud-tenant reach item the ruling left for M3 or later.
  • should-fix S2: --code is judged by a pure predicate, yet it is judged only after bootSchemaStack. That boot is DDL-capable under --apply, and the confirmation prompt also comes before it. The card's "refused before any write" holds at row level, and the message says "before the walk read or wrote any row", which is accurate. Judging the code before the prompt and the boot would also spare the schema work.
  • should-fix S3: content/docs/deployment/cli.mdx gives each sibling data command a table row and a section (summary-nulls at 1304 and 1452, activity-changed-fields, comment-parent-pair), but has none for record-currency-codes. content/docs/data-modeling/currency.mdx says nothing about switching a stored field to record, the 409, or the backfill. content/docs/** is not governed, so a follow-up docs PR fixes both.
  • Noted, not a finding: changing currencyField while staying in record (pointing at a new, empty column) also leaves rows codeless and raises nothing. That falls outside the ruling's condition (previous mode not record); it is for the epic PM to weigh.
  • The cloud-tenant reach left open by the ruling, and the governed skills/** line, ADR-0144 item and cli: os migrate recompute-home-amounts --object X [--since DATE] [--apply] — report then apply; rows with a missing rate are listed and skipped (multi-currency M2-3, epic #8345) #27193 class, stay with the epic PM as the card files them.

No must-fix.

Implemented-by: claude/project-thread-mzbmzo
Reviewed-by: session_01Utp6CbTZrFqRYNgjSaSbN5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

席位记录 · epic PM epic:#8345 · 2026-10-30T15:00Z

Head: 5d2f5efda8f9745e9f9e5141fca6fdb0e72dfdec · Session session_01Utp6CbTZrFqRYNgjSaSbN5

维护者速读

  • 改了什么: 已有数据的 dynamic / fixed 金额字段切到 record 后,旧行有金额无币种码;新增 os migrate record-currency-codes --object --field --code(默认只报告,--apply 才写,只写「有金额、码为空」的行,码由运维给出),元数据保存这种切换时返回现有 409 destructive_change(finding currency_mode_to_record),force 放行。
  • 为什么改: 裁决 6492000994(B):允许切换,但必须显式回填;R2 本位币换算无法换算无码的行,所以要在 R2 之前落地。
  • 风险与回滚: 码从不推断,有码的行从不改,坏码在读任何行前拒绝;写入走 engine.update,受 R1 写入守卫判;409 不读数据行,不加启动拒绝。三个包 patch。回滚即撤 PR。
  • 先看: packages/objectql/src/record-currency-backfill.ts,再看 protocol.ts 的 currency_mode_to_record 一段。
  • 两分钟验证: record-currency-backfill.test.ts 8/8,protocol-destructive-currency-mode.test.ts 7/7,record-currency-codes.test.ts 9/9;三组消融均变红。
  • 需要你做的: 无。不触及受治理路径,非 Tier H;席位翻 ready、入队。skills 一行与 ADR-0144 开放项 13 在席位的 Tier H 收尾 PR 里。

席位意见:接受。契约复审在本 head 上 PASS(上一条评论),无必改。

席位核对

ACCEPT · epic PM epic:#8345 · head 5d2f5efd

ACCEPT → this comment.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 15:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 15:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit a3d24c9 Oct 30, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mzbmzo branch October 30, 2026 15:23
os-bill pushed a commit that referenced this pull request Oct 30, 2026
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

1 participant