Repository navigation
feat(spec): currencyConfig.homeAmount — a record-mode amount declares its stored home-currency projection by pointer (multi-currency M2-1) - #28096
Conversation
…}` on a record-mode amount A `record`-mode amount declares, by three required pointers into the same object, where the engine stores it converted into the home currency, which document date picks the rate, and where the rate used is stored (ADR-0144 D5). - `CurrencyConfigSchema` gains `homeAmount`, a closed shape whose three pointers are required and non-empty; it is refused outside `record`. - `ObjectSchema`'s superRefine gains `refuseDishonestHomeAmount`: the amount is a stored `currency` field; `field` names another `dynamic`-mode `currency` field no other amount stores into; `rateDate` a `date` / `datetime` field; `rateField` a `number` field, shared only by amounts with the same code field and document date. The home and rate fields declare `readonly: true` and no `expression`. Each refusal is located at its pointer and carries a remedy and the object's candidates. - ADR-0144 anchors on field.zod.ts and object.zod.ts gain the D5 invariant. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…me-amount pointer The currency block of `objectstack-data/rules/field-types.md` gains the `homeAmount` example and one paragraph, paid for by deleting restatement in the same file (3207 -> 3181 tokens). The changeset declares `@objectstack/spec` and `@objectstack/skills` minor and marks the key as released with the write-time projection (ADR-0144 D9, R2). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…me-amount-pointer
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 76bd70cd0b5c52f45f1b96796bcfa3883ecf2330 && git checkout 76bd70cd0b5c52f45f1b96796bcfa3883ecf2330
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3fd4d6bafbe1c8198070fb3343322e3bff598439 cf13692214fde39ab2a9b113f1708485e382e6b5 && git checkout -B drift-repro 3fd4d6bafbe1c8198070fb3343322e3bff598439 && git merge --no-ff cf13692214fde39ab2a9b113f1708485e382e6b5
node scripts/docs-audit/affected-docs.mjs --json 3fd4d6bafbe1c8198070fb3343322e3bff598439
|
…and the strictness ledger for `homeAmount` Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…e engine writes it The form/Zod reconciliation gate refuses a key the Zod accepts and the form does not offer. `homeAmount` is offered in no form yet, deliberately: nothing writes the home amount until the write-time projection lands in the same release, so the gate's ledger carries a `planned` omit, the precedent of the other spec-first keys. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs read: card #27191 (body, both comments: the epic PM's queue order and the dev's claim), PR #28096 (body, file list, net diff of all 13 files), the 50 check-runs on the head (every non-skipped run ① Derived judgments
② Semver level
③ Boundary flags
No ④ Design judgment
Should-fix 5 (outside the hits, Tier H follow-up): amend ADR-0144 Open item 1 with the settled spelling, as the ADR says it will be. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 维护者速读(席位终稿 · head
|
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Seat hand-over to the approvers · 2026-10-30T16:43Z · head
|
…me-amount-pointer # Conflicts: # packages/spec/src/system/metadata-form-zod-reconciliation.test.ts
…erging main Main's side of the four regenerated files, re-derived from the merged tree so this branch's `homeAmount` rows sit beside main's new entries. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…main Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V New head · 2026-10-31T02:34Z · The prior PASS 6526217217 was on Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head cf13692 |
|
domain:spec#4 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#4 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: PR #28096 (body, all six comments, the file list of 13 files, the eight commits from the merge-base 3fd4d6b), the seat's hand-off 6537861938 naming this head and the one conflict, the prior record 6526217217 (triage, PASS on 361f8f2, five should-fix, each routed per the seat's hand-over 6528171740: 1 to #28205, 2 to the #27192 hand-off note, 3 to 5 to #28203), the 55 check-runs on this head (read 2026-10-31T02:59Z: all 55 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts), and the tree. This is a delta review of record 6526217217 on the head that merged ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The hand-written lines of this PR are byte-identical to the ones record 6526217217 passed; the one conflict keeps every row of both sides; the regenerations are the merged tree's, with the single changed count explained by Implemented-by: VERDICT: PASS |
Claude-Session: https://claude.ai/code/session_01Utp6CYjLJ7sxfUehRrLbT3 Co-authored-by: Claude <noreply@anthropic.com>
🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #27191
⛔ not released alone — R2: pairs with #27192 (the write-time projection that writes the home amount), #27185, #27190, #27198, #27193, #27196 (one Version Packages release; ADR-0144 D9: "the
homeAmountkey never ships ahead of the engine")Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here
Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.
维护者速读
currencyMode: 'record')的金额字段,现在可以声明homeAmount:指明换算成本位币后的金额存在哪个字段、按哪个日期字段取汇率、实际使用的汇率存在哪个字段。三个字段都必须是同一对象上作者自己声明的字段,平台不注入列、不猜默认(不默认created_at);本位币金额字段与汇率字段只由平台写入,作者须标readonly: true。homeAmount的应用零变化;本 PR 只有声明与拒收,真正写入本位币金额的引擎在 objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192。⛔ 不单独发版:必须与 objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 同一次 Version Packages 发布,否则是「声明了却不兑现」。回滚即撤 PR。packages/spec/src/data/object.zod.ts的refuseDishonestHomeAmount(指针所指字段的全部拒收);packages/spec/src/data/field.zod.ts的HomeAmountSchema与refuseRecordModeKeyMismatch新增的一臂;skills/objectstack-data/rules/field-types.md币种段。pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-home-amount.test.ts,预期全绿:非record模式写homeAmount、缺任一指针、本位币字段不是dynamic币种字段、日期字段不是日期、汇率字段不是数字、两字段未标readonly各自被拒并带处方。skills/**(受管面 Tier H),请在 PR 上审批技能文本;之后由席位在契约复审通过后落地,并与 objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 同批发版。What changes
CurrencyConfigSchema.homeAmount(packages/spec/src/data/field.zod.ts, ADR-0144 D5): a new optional key, a closed shape{ field, rateDate, rateField }. All three pointers are required wheneverhomeAmountis declared, and none may be empty. ADR-0144 D5 corrects the card's draftrateField?to required. A missing pointer and an empty one each get their own message with a remedy. Near-miss spellings (rate,date,homeField, …) are steered to the declared pointer by the shape'saliases.refuseRecordModeKeyMismatchgains one arm:homeAmountunderdynamicorfixed(including the defaulted mode) is refused athomeAmount, because only arecordamount is converted.refuseDishonestHomeAmount(packages/spec/src/data/object.zod.ts), inObjectSchema's superRefine beside thecurrencyFieldarm. It judges what the pointers name, against the authored field map. There is one located issue per offending pointer, atfields.AMOUNT.currencyConfig.homeAmount.POINTER. Each issue carries a remedy and the object's candidate fields.currencyfield. Aformulaamount is computed on read and never written, so it is refused athomeAmountitself.fieldmust name another declaredcurrencyfield indynamicmode. An absentcurrencyConfigcounts asdynamic. This follows ADR-0144 D8 rule 5: "the home field is adynamic-mode currency field", so metadata never names the home currency. A second amount may not claim a home field that another amount already writes.rateDatemust name a declareddateordatetimefield. Nothing defaults it tocreated_at.rateFieldmust name a declarednumberfield. Two amounts may share one rate field only when they name the samecurrencyFieldand the samerateDate, because otherwise they would store two different rates in one column.readonly: true, which drops every non-system write of the field from the payload. Neither field may also declare anexpression, which would be a second writer. No new key is invented.skills/objectstack-data/rules/field-types.md, governedskills/**): the currency block teacheshomeAmountin one example line and one paragraph; the example names are guidance, "never a contract" (ADR-0144 D8 rule 2). Paid for by deleting restatement in the same file: the intro line, the "Text with Max Length" example (a duplicate of the file's first code block), the "Master-Detail with Cascade" example (the Relational table's row says it) and a removed-key history comment. 3207 → 3181 tokens, 323 → 304 lines; bundle 219711 → 219685 tokens..changeset/27191-home-amount-pointer.md:@objectstack/specminor and@objectstack/skillsminor. The body carriesClause-②: yes (widening),Metadata-protocol: addedand the R2 pairing line.packages/spec/authorable-surface/data.json(gen:schema) andcontent/docs/references/**(gen:docs).field.zod.tsandobject.zod.tsgain the D5 invariant.Tests
packages/spec/src/data/currency-config-home-amount.test.ts:recordconfig withouthomeAmountparses byte-identical to before.homeAmountis refused underfixed,dynamicand the defaulted mode. Each missing pointer and each empty pointer is refused at its own path. An unknown key is steered torateField.datetimeand an explicitdynamichome field, andparse(parse(x))equalsparse(x). A consistently shared rate field is accepted. Each refusal is pinned withcode,pathand the message's first clause:field: points into nothing, at the amount itself, at a non-currencyfield (anumber, a currencyformula), at afixedorrecordhome field, or at a home field another amount already writes;rateDate: points into nothing, or at a non-date field;rateField: points into nothing, at a non-numberfield, or at a rate field shared inconsistently;readonly: true, or one declaring anexpression;formulaamount.currency-config-record-mode.test.tsstays green, unchanged.homeAmount: { field, rateDate, rateField? }on a record-mode amount — the stored home-currency projection declared by pointer (multi-currency M2-1, epic #8345; released with M2-2) #27191, at the final head.Acceptance notes
currencyMode: 'record'+currencyField— a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1) #27268) held its key with three things: a⛔ not released alone — pairs with …line in its PR body, a⛔ Not released alonesentence in its changeset, and the maintainer's hand on Version Packages (Prime Directive Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15; the epic [Epic] multi-currency (shape C′): record currency, dated exchange rates, write-time home-currency projection — 17 children, R1/R2/R3 release pairing (ruling 6465431966) #8345 body: "the maintainer merges Version Packages only when a pair is complete"). This PR does the same. No gate, ledger orrelease-pending-publish.mjsmode reads a pairing marker, and.changeset/config.jsonhas no pre-release mode. Once this lands,changeset-release/main(chore: version packages #26517) folds its changeset into the next Version Packages release with every other pending one. The epic's R2 sibling.changeset/27185-exchange-rate-table.mdis already onmainin the same state.readonly: trueand the engine (objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192):readonlystrips every non-system write of the field from the payload, on insert and on update. So the write-time projection has to store the home amount and the rate on a path that strip does not undo: a system-context write, or a write after the strip. That is objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192's to build and pin.formulaamount, a home field claimed twice, a rate field shared inconsistently, and anexpressionon a home or rate field. Each refuses a shape in which the engine would write a value that is wrong, or that another writer can overwrite. Each refusal is pinned.field.form.ts) offers nohomeAmountrow; the read-only display of the home field is ui: home field read-only with "converted at RATE on DATE"; live preview when a rate exists; Studio editor forhomeAmount(multi-currency M3-2, epic #8345; the read-only display is released with M2) #27196 (M3-2). The hand-written pagecontent/docs/data-modeling/currency.mdxdoes not mentionhomeAmount.Verification
Gate readings (
dispatch-gates.mjs --commandson the final diff, each exit code captured before any pipe,--ranreconciliation), the package test and typecheck runs, and the ablation are reported with the head sha in the os-dev-report comment on #27191.