Repository navigation
objectql + cli: backfill the currency code when a stored amount switches to record mode, and a 409 acknowledgement at the metadata door (multi-currency, epic #8345) #27577
Description
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Filed by the epic PM under maintainer ruling 6492000994 on #8345 (letter B, 2026-10-29T08:39Z, 「B 放行,欠显式回填」). Part of #8345. ⛔ Not a claim. Lands after R1 (#27268, #27339, #27338, #27340 merged 2026-10-29) and ⛔ before R2 (#27191 / #27192 / #27193): the R2 home projection has no code to convert a codeless row, so a consolidated sum would silently omit it.
Ruling-ref: 6492000994
维护者速读
- 改了什么: 已有数据的
dynamic/fixed金额字段改成record模式后,旧行没有币种码。新增一个显式回填:运维给定币种码(--code USD),把「有金额、无币种码」的行补上;默认只报告,--apply才写,可重复跑。元数据保存时,这种切换会返回现有的 409destructive_change,说明三个后果和回填命令,force可放行。 - 为什么改: 不回填的话,这些行显示为纯数字、分组落进 null 组、币种筛选里看不到,R2 合并报表也会漏掉它们。
- 风险与回滚: 回填只写币种码为空的行,从不推断币种码;409 不读数据行,不加启动时拒绝。回滚即撤 PR。
- 先看: 先例
packages/objectql/src/summary-backfill.ts与os migrate summary-nulls;packages/metadata-protocol/src/protocol.ts的detectDestructiveObjectChanges(Phase 3a)。 - 两分钟验证: 对一个有旧行的对象把金额切到
record:保存返回 409 并点名命令;dry run 报出待补行数,--apply后再跑一次报 0。 - 需要您做的: 无,已裁 B。
Task
- Backfill function in
@objectstack/objectql, modelled onsummary-backfill.ts. For one object and onerecord-mode amount field, write an operator-supplied ISO 4217 code into every row whose amount is not blank and whose code field (currencyConfig.currencyField) is blank. ⛔ The code is never derived: not from a retireddefaultCurrency, not fromlocalization.currency, not from anything else (ADR-0144 D8 rule 4; [Decision] Currency, all of it: resume multi-currency (#8345) and in what shape, and one rule for an amount whose currency is not known (Studio's USD, the template's USD,fixed's CNY, the example's USD) #27119 Q2 X). A row that already holds a code is never touched; a code that failsiso_4217_currencyis refused before any write. Dry run by default,--applyto write, idempotent (a second run finds nothing), per-row counts: written / skipped (code present) / skipped (amount blank). Nosys_migrationflag: nothing gates on the run. Whether it writes throughengine.updateor at the driver assummary-backfill.tsdoes is the card's call: follow the precedent you pick and say why in the PR. - CLI face
os migrate record-currency-codes --object <o> --field <amount> --code <ISO> [--apply]in@objectstack/cli, the same split assummary-backfill.ts/os migrate summary-nulls. - Metadata-door acknowledgement:
detectDestructiveObjectChanges(packages/metadata-protocol/src/protocol.ts, thesaveMetaItemPhase 3a-destructive gate) gains one finding,currency_mode_to_record: the previouscurrencyConfig.currencyModeis notrecord, the next isrecord, and the field is stored. It answers as the existing HTTP 409destructive_changewith the existingforceremedy. Its message names the three consequences (codeless rows display as a plain number, fall into thenullgroup, sit outside every currency-paired filter) and the command above. It reads no rows, like its siblingsfield_type_changeandfield_required_no_default. ⛔ No boot-time refusal: a code-declared package registered at boot has no previous version to diff. - Tests: the backfill (dry run, apply, idempotence, a row with a code untouched, a blank amount skipped, a bad code refused); the 409 finding (fires on dynamic→record and fixed→record over a stored field, not on a new field, lifted by
force); a CLI smoke.
Out of this card's diff, kept with the epic PM (governed paths, Tier H, not mixed into a code PR): the one-line skills/** note (switching a stored field to record leaves rows codeless; run the backfill) and the ADR-0144 open item with its dated note. The epic PM also adds to #27193 that blank-code rows are reported as their own class, apart from missing-rate rows.
Lane: server (objectql, metadata-protocol), with the packages/cli command as the cross-lane half, dispatched by the epic PM per 「epic的卡你统一派」 · Size: M · Review: default. Declarations, for the card to confirm against the tree: Clause-②: yes (widening) (a new member of an existing refusal face, the reading of #27339's review 6490309706) · Metadata-protocol: none (what an author can write is unchanged; force lifts the acknowledgement). Left open on purpose: how a cloud tenant without a CLI reaches the backfill (an admin route or Studio action, M3 or later).
Epic: #8345. Dedupe: #8345 thread, ruling 6492000994, #27193 (R2 recompute: a different operation on the home columns); no open card carries this slice.
Activity
objectstack-fleet commentedon Oct 29, 2026
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Claim: PM loop round 1 · 2026-10-29T13:42Z
Session:
session_01Utp6CbTZrFqRYNgjSaSbN5(epic PM,epic:#8345)Account:
os-billBranch:
claude/project-thread-mzbmzo(card threadsession_01Utp6CbJa6hKdimkAau1nbA; the platform assigns the thread's one push branch — the recorded platform exception)Worktree:
objectstack-issue-27577Domain:
domain:serverSeat:
epic:#8345File surface:
packages/objectql/src/**(the backfill and its tests),packages/cli/src/commands/migrate/**(the thin command, cross-lane half),packages/metadata-protocol/src/protocol.tsand its tests,.changeset/*; ⛔ no governed path (stop on breach; explain in the report)Container & model:
M,mode:thread,model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate),effort: highClause-②: yes (widening) — to confirm against the tree
Metadata-protocol: none — to confirm against the tree
Responsibility: n/a — not a defect card
Thread-read: none
Premise: holds
Serial constraints cleared: yes — R1 (#27268, #27339, #27338, #27340) landed 2026-10-29; ⛔ lands before R2 (#27191–#27193).
Release pairing: none — ships in the next Version Packages.
Maintainer authority: Bill, project thread, 2026-10-28 「epic的卡你统一派」, 「并发可以5」, 「你自主完成后续阶段」; ruling 6492000994 (letter B).
objectstack-fleet commentedon Oct 30, 2026
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
os-dev-report — #27577
{ "issue": 27577, "status": "done", "branch": "claude/project-thread-mzbmzo", "pr": "https://github.com/objectstack-ai/objectstack/pull/28128", "head": "1636d8c36b729631cc3a7ce056b9dfc6d3a2e4fc", "session": "session_01Utp6CbJa6hKdimkAau1nbA", "premise_still_valid": true, "summary": "Ruling 6492000994 letter B, all three code halves. objectql: backfillRecordCurrencyCodes (record-currency-backfill.ts, modelled on summary-backfill.ts): operator code only, never derived; object/field/code refused before any row is read (OBJECT_NOT_FOUND 404; INVALID_FIELD 400 for a non-record field and for a non-ISO-4217 code via isValueDomainMember); keyset walk under the system context; counts codeless / written / skippedCodePresent / skippedAmountBlank; dry run by default, idempotent, no sys_migration flag. Writes through engine.update by precedent (summary-backfill.ts and every os migrate data run write there; none writes at the driver), so the R1 write guard and the code field's valueDomain judge each write. isRecordCurrencyBlank is exported from the write guard so both judge one predicate. cli: os migrate record-currency-codes --object --field --code [--apply], the summary-nulls shell one for one. metadata-protocol: detectDestructiveObjectChanges gains currency_mode_to_record (stored prev field not in record mode, next a record-mode currency field) as the existing 409 DESTRUCTIVE_CHANGE, force lifts it, message names the code field, the three consequences and the command; reads no row; no boot refusal. One changeset, patch x3. Declarations confirmed: Clause-②: yes (widening), Metadata-protocol: none. For the contract review: a non-forced save of exactly this switch now answers 409 until re-sent with force (an acknowledgement on the existing face, per the 6490309706 reading the card names).", "tests": "head 1636d8c3 (merge of main 549203d0, whole workspace built): objectql 496 files / 9615 tests passed; metadata-protocol 284 files / 21857 passed (26 skipped, pre-existing); cli src/commands/migrate 59 files / 429 passed (8 skipped, pre-existing). New: record-currency-backfill.test.ts 8/8, protocol-destructive-currency-mode.test.ts 7/7, record-currency-codes.test.ts 9/9. Typecheck objectql, metadata-protocol, cli exit 0; eslint on touched files clean. Ablations via ablation-replace, each landed and restored blob == HEAD: (1) code-present test -> false: 5/8 red; (2) code-present test -> null-only blank: 4/8 red; (3) the finding's condition -> false (metadata-protocol rebuilt): the three 409 cases red, 3/7, the four non-firing cases green.", "gates": "dispatch-gates --commands on 1636d8c3: 65 families (9 paths vs merge base 549203d0, 1254 changed lines). All 65 ran with exits captured, all exit 0. dispatch-gates --ran: 65 accounted, 65 run, 0 NOT-MEASURED. Records: /mnt/project-files/epic-8345/gates-27577-ran.txt (project files). An earlier head's run caught check:objectql-double-limit on the new test double (read counter inside find); fixed in 3c340f77. CI on the PR: not yet read at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "1 — mcp__github__issue_read (get #27577), no write tools", "api_writes": "2 — fleet-write relay pr_create (objectstack#28128, no assignee, no review request per Bill's rule), post-stamped os-dev-report comment on #27577", "open_questions": [], "out_of_scope_findings": [ "none — the skills/** line, the ADR-0144 open item and the #27193 blank-code class stay with the epic PM per the card" ] }Report taken 2026-10-30T13:24Z.
Generated by Claude Code