Skip to content

feat(spec): currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1) - #27268

Merged
os-zhuang merged 14 commits into
mainfrom
claude/project-thread-0wmk7j
Oct 29, 2026
Merged

os-zhuang merged 14 commits into
mainfrom
claude/project-thread-0wmk7j

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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

Requested by Bill · project thread
Fixes #27184

⛔ not released alone — R1: pairs with #27186, #27187, #27188 (one Version Packages release)
Merge-after: #27218, #27233, #27239 (all three landed; main merged in at 73725c20)
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here

Head: b6f152bd — CI fully green (65 check runs: 58 success, 7 skipped, 0 failed). Contract review PASS at 90114641 (comment 6487611079); its should-fixes are applied in f3d286ce and b6f152bd (see "Contract-review follow-ups" below).

维护者速读

What changes

  • CurrencyConfigSchema.currencyMode gains record (ADR-0144 D2). currencyField (new, optional key) names the sibling field holding each record's ISO 4217 code. currencyField is never empty (.min(1)). A config-level refinement, refuseRecordModeKeyMismatch, makes it required under record, refuses it under dynamic / fixed, and refuses defaultCurrency under record, each at its own key with a prescription; it is chained after spec(field): fixed-mode currencyConfig with no defaultCurrency materialises 'CNY' — require the code, drop the default (narrowing; #27119 Q2 X, G3) #27157's refuseFixedCurrencyWithoutCode on the same schema. dynamic keeps tolerating an inert defaultCurrency for the rows spec(field): fixed-mode currencyConfig with no defaultCurrency materialises 'CNY' — require the code, drop the default (narrowing; #27119 Q2 X, G3) #27157 left behind; record is new, so it needs no such tolerance (ADR-0078). The amount stays a bare number (ADR-0104 D1, kept by ADR-0144 D2).
  • ObjectSchema's superRefine gains refuseDishonestRecordCurrencyField, beside refuseNonPictureImageField: for every field whose config is record, the pointer must name another declared field whose type may carry a value domain (VALUE_DOMAIN_FIELD_TYPES, text today; the door reads the set rather than repeating the literal) and which declares valueDomain: 'iso_4217_currency'. Three refusals, each located at fields.FIELD.currencyConfig.currencyField, each carrying the prescription and the object's candidate code fields (or, when there are none, how to declare one). A formula with returnType: 'currency' meets both doors in the same shape, since it carries the same currencyConfig.
  • field.form.ts: the record option and a currencyField row in the currency composite; the currencyMode help text describes all three modes, currencyConfig says the value is a bare number "in every mode", and defaultCurrency says it is refused in record mode. The metadata-form translation bundles are regenerated (check-i18n-bundles.mjs --write), and the zh-CN / ja-JP / es-ES text for the new row and the three changed help texts is written by hand.
  • @object-ui/i18n resolveFieldCurrency: the parameter type accepts record, and a record-mode field resolves to undefined outright, with no fallback to the tenant default, config.defaultCurrency or a legacy defaultCurrency. The field has no currency of its own; a record whose code is blank shows no symbol rather than a currency it never named (aligned with feat(ui): record-mode currency — each record shows its own currency; ISO 4217 code select pre-filled with the organization currency; no cross-currency totals; Studio currency section #27340, the per-row display).
  • Generated: content/docs/references/** (gen:docs) and packages/spec/authorable-surface/data.json (gen:schema, + data/CurrencyConfig:currencyField). The dropped-refinements.baseline.json sites this PR first added (the data/CurrencyConfig root plus currencyConfig under 12 schemas, header 252 / 900) are now identical to what fix(spec)!: a fixed currency config needs its code — currencyConfig.defaultCurrency loses its CNY default #27218 landed, so the ledger is no longer in this diff.
  • skills/objectstack-data/rules/field-types.md: the currency block now covers all three modes, with a one-line record-mode example ({ currencyMode: 'record', currencyField: 'currency_code' }, the conventional name per ADR-0144 D8 rule 2). "A measure's own currency wins" is scoped to fixed / dynamic (feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338 refuses a measure currency over a record amount). The home-projection names AMOUNT_home / AMOUNT_rate are dropped until that projection exists. Paid for inside the same file: 322 → 320 lines, 3151 / 3158 tokens (ratchet green); no SKILL.md touched.
  • Changeset .changeset/27184-currency-record-mode.md: @objectstack/spec minor, @object-ui/i18n minor, @objectstack/platform-objects minor (it ships the regenerated metadata-form bundles).
  • ADR anchors: ADR-0144 added to scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json and …object.zod.ts.json, each with its D2 invariant.

Tests

  • New packages/spec/src/data/currency-config-record-mode.test.ts (15 tests): record ⇔ currencyField at the config door (required / refused under dynamic, fixed and the defaulted mode); an empty currencyField refused in every mode; defaultCurrency refused under record (and still tolerated under dynamic); the three object-level refusals plus the self-pointer; the formula shape; parse(parse(x)) equals parse(x) for configs and an object; dynamic and fixed outputs pinned byte-identical (JSON.stringify) to the post-spec(field): fixed-mode currencyConfig with no defaultCurrency materialises 'CNY' — require the code, drop the default (narrowing; #27119 Q2 X, G3) #27157 output. Refusals assert code, path and the message's first sentence.
  • ui/packages/i18n/src/__tests__/resolveFieldCurrency.test.ts: one new case — a parsed record config resolves to undefined with a tenant default, with config.defaultCurrency, with a legacy defaultCurrency, and with no tenant default.
  • packages/platform-objects/src/apps/translations/: two hand-pinned catalog counts move for the new row — field-panel-echo-decisions UNWALKED_CHILDREN 10 → 11 (names currencyConfig.currencyField), and the object-lifecycle-panel-echo-decisions per-locale positive control 694 → 695 (main's Studio still offers field externalId as an upsert key after it was graded dead #26674 took it 695 → 694).
  • Ablations, one per new refusal, through scripts/ablation-replace.mjs on the committed head (anchor 1 → 0 on disk, restored to the HEAD blob): .min(1) → .min(0) turns exactly "refuses an empty currencyField…" red (1 failed / 14 passed); disabling the record + defaultCurrency arm turns exactly "refuses defaultCurrency under record…" red (1 failed / 14 passed).
  • At b6f152bd: @objectstack/spec 835 files / 23779 tests pass, typecheck exit 0; @objectstack/platform-objects 97 files / 1369 tests pass. At 90114641: ui packages/i18n/ 89 files / 1374 tests pass, @object-ui/i18n type-check exit 0 (no ui file changed since). Earlier, at bf7f2b74, ui packages/i18n/ plugin-detail/ plugin-gantt/ types/ 15806 tests pass.

Acceptance notes

Contract-review follow-ups (review 6487611079 at 90114641, PASS)

  • ② @objectstack/platform-objects added to the changeset.
  • ④.1a currencyField .min(1); ④.1b defaultCurrency refused under record; ④.1c / ④.2b ADR-0144 anchors on field.zod.ts and object.zod.ts.
  • ④.2a the object door reads VALUE_DOMAIN_FIELD_TYPES.
  • ④.3 / ③ zh-CN / ja-JP / es-ES currencyMode help text now describes three modes (plus the currencyConfig and defaultCurrency texts that the new rule changed).
  • ④.4a–c the skill: measure-currency sentence scoped, AMOUNT_home / AMOUNT_rate dropped, record-mode example added, within the ratchet.
  • Not this PR's and left as the review placed them: the share-link projection gap (escalated to the epic PM), the os-zhuang approval on 5232c8b1 (escalated to the maintainer), and the inherited AddFieldOperation ledger drop (fix(spec)!: a fixed currency config needs its code — currencyConfig.defaultCurrency loses its CNY default #27218).

Verification

Dispatch gates — 123 derived, 123 run, all exit 0 at b6f152b (reconciled with --ran)

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at b6f152bd (20 paths vs merge base 73725c20); every family run with the closure built, each exit code recorded; reconciliation: "123 derived famil(ies) accounted for — 123 run, 0 NOT-MEASURED (a DERIVED zero — all 123 recorded an exit code and none of them is 3)". All 123 exit 0. check:generated (spec): all artifacts current.

Earlier rounds, kept for the record: at 90114641, 115 / 115 exit 0; at 5232c8b1, 112 derived / 112 run, with check-skills-token-ratchet (fixed by deletion in the same file) and check:i18n (bundles regenerated and translated) red on the first pass and green after; at a9f5abfd, 112 / 112 exit 0.

CI at b6f152b — 65 check runs, 58 success, 7 skipped, 0 failed

The same set as below, all success again: Lint & Repo Gates, TypeScript Type Check, Test Core (1–8/8), Test UI (1–16/16), Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Build Core / Docs / UI Deps, Check Changeset, PR-body guards, Governed Surface Queue Guard and the rest; the same 7 opt-in or schedule-gated jobs skipped.

CI at 9011464 (the reviewed head) — 65 check runs, 58 success, 7 skipped, 0 failed

Success: Lint & Repo Gates (and its Lint · … jobs), TypeScript Type Check (and its Type Check · … jobs), Test Core (1–8/8), Test Core, Test UI (1–16/16), Test UI, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Build Core, Build Docs, Build UI Deps, Plan Test Core shards, Check Changeset, Check PR Size, Check Documentation Links, Spec property liveness, Governed Surface Queue Guard, Flag docs affected by code changes, PR-body guards, Auto Label, filter. Skipped (opt-in or schedule-gated): Dogfood Verify CLI, Dogfood Regression Gate matrix, Temporal Conformance, UI Published Artifacts, 18.x upgrade harness, Packed-tarball smoke, Schedule gate.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f

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

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 5232c8b14bd74f8ab8f496ef06cdf0e3ee189a78 · 2026-10-28T14:57Z

Triage seat, under gap rule (b) (#27062): this head is over 60 minutes old with no record and no claim. The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Re 6472421622: please withdraw. This head is not the one to review. #27268 was developed ahead of its blockers on purpose (claim 6469449494). It still has to take in main for #27157 (landed, PR #27218), then for #27182 (PR #27233) and the ADR (PR #27239), and its byte-identical pins move with each merge. The epic PM hangs needs:contract-review once, on the head after the last of those merges. The head will move, so a review of 5232c8b1 would have to be redone. The PR stays draft until then.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as draft October 28, 2026 20:20
auto-merge was automatically disabled October 28, 2026 20:20

Pull request was converted to draft

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), currencyMode (literal, a string literal in sections), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections))
  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/data-modeling/field-types.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), currencyMode (literal, a string literal in sections), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections), iso_4217_currency (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/data-modeling/fields.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), currencyMode (literal, a string literal in sections), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections), iso_4217_currency (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/data-modeling/formulas.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/data-modeling/validation-rules.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections), iso_4217_currency (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/getting-started/common-patterns.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), currencyMode (literal, a string literal in sections), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections))
  • content/docs/protocol/objectql/schema.mdx (via iso_4217_currency (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/protocol/objectql/types.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField), currencyMode (literal, a string literal in sections), defaultCurrency (literal, a string literal in refuseRecordModeKeyMismatch; a string literal in sections))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))
  • content/docs/releases/v17/17-5.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestRecordCurrencyField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json) — pages documenting those are invisible to this run
  • 1 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 — 166 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 ad2c2869bb67fe24750021aa5b94c99c1ac7c957 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6cdf5e82136715fff92e3eb51bd0d87d8d1577fb — the merge of head b6f152bd17b1d7e70bed8c6b59098b3353f38691 into base ad2c2869bb67fe24750021aa5b94c99c1ac7c957, 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 6cdf5e82136715fff92e3eb51bd0d87d8d1577fb && git checkout 6cdf5e82136715fff92e3eb51bd0d87d8d1577fb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ad2c2869bb67fe24750021aa5b94c99c1ac7c957 b6f152bd17b1d7e70bed8c6b59098b3353f38691 && git checkout -B drift-repro ad2c2869bb67fe24750021aa5b94c99c1ac7c957 && git merge --no-ff b6f152bd17b1d7e70bed8c6b59098b3353f38691

node scripts/docs-audit/affected-docs.mjs --json ad2c2869bb67fe24750021aa5b94c99c1ac7c957

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

CI red on a9f5abfd — Lint · repo & tooling gates, step "ADR anchors + number uniqueness" (run 40767250949). It is this PR's, it is expected, and it is not fixed here on purpose.

node scripts/check-adr-anchors.mjs reproduces it locally: ADR-0144 is cited by 5 file(s) but names no record under docs/adr/ (the changeset, field.zod.ts, object.zod.ts, field.form.ts and the new test). ADR-0144 is the multi-currency record, and it lands in #27239. This PR is Merge-after: #27218, #27233, #27239, and Prime Directive #13 asks implementing code to carry its ADR's id. So the citation is correct. The gate turns green when #27239 merges and main is brought into this branch, which is already the planned next step. Rewording five citations now, only to restore them later, would churn the branch that #27186 / #27187 / #27188 build on.

Every other gate in that lane passed (60 of 61). Locally at a9f5abfd, the 112 derived gate families all exit 0. check-adr-anchors is not one of them, because the derivation scores it whole-tree.

Docs drift (bot comment 6480459047): the hand-written docs pages that name currencyConfig / currencyMode are left to #27189, the M0-5 currency docs page in the same release. This PR changes only generated references and the skills guidance.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract-review-claim: session_01Utp6CbTZrFqRYNgjSaSbN5 · head 9011464 · 2026-10-29T03:44Z

The epic PM seat runs this CONTRACT_REVIEW_TIER review in-seat with an isolated reviewer agent that did not draft this PR.

os-bill pushed a commit that referenced this pull request Oct 29, 2026
os-bill pushed a commit that referenced this pull request Oct 29, 2026
…-currency display branch

Bring-in order. Two conflicts:
- ui/packages/i18n/src/currency.ts: #27268 made record mode resolve to
  undefined with no tenant fallback; this branch reads the record's code
  when the caller passes the row. Kept the record read (a call with no
  record still resolves to undefined, which #27268's tests pin) and
  #27268's doc text.
- ObjectFieldInspector.tsx: both import blocks kept.

Claude-Session: https://claude.ai/code/session_01Utp6CXtadYa5QXb2PbXPev
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 90114641aeaeaa9a71a153be5051bba23326e8ad
Local-runs: none
Scope: packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts, skills/objectstack-data/rules/field-types.md + packages/spec/src/data/currency-config-record-mode.test.ts, packages/spec/authorable-surface/data.json, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, ui/packages/i18n/src/currency.ts (all at the head); on main 28872e68: packages/formula/src/formula-return.ts, packages/spec/dropped-refinements.baseline.json, scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json and …object.zod.ts.json, packages/types/src/share-link-resolve-answer.ts, docs/adr/0144; consumer heads #27339 4201bb53 (objectql record-currency-code.ts, record-currency-aggregate-door.ts, formula formula-return.ts), #27338 507ed8f9 (service-analytics plugin.ts, measure-currency-coherence.ts), #27340 7520d3e4 (ui/i18n currency.ts, app-shell CurrencyConfigField.tsx). The rest of the 18-file diff (changeset, generated reference pages, translation count pins, the ui/i18n test) is the lane seat's ACCEPT at TIER_DEFAULT.

Gates on the head: the ten required contexts all success; the Check Changeset run cancelled at 03:42Z is superseded by the 03:44Z success — not a finding.

① Derived judgments

② Semver level

Changeset 27184-currency-record-mode.md: @objectstack/spec minor, @object-ui/i18n minor; Clause-②: yes (widening) and Metadata-protocol: added, line-initial in the changeset and the PR body, and both true of the diff: one new enum value and one new optional key, and nothing an author could write before is refused now (currencyField was an unknown key under strictObject). added grades spec minor — right. @object-ui/i18n minor — right (its accepted input widens and a record config changes its answer).

③ Boundary flags

④ Design judgment

  1. field.zod.ts — holds against D2 and its first consumers. record ⇔ currencyField is the right split: the config door judges what one config can see, the object door what the pointer names; refuseFixedCurrencyWithoutCode changes only in its type. Should-fix: (a) currencyField: z.string() admits '': it passes the config door under record, the object door refuses it with a message naming an empty field, a standalone FieldSchema parse accepts it, and 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's recordCurrencyFieldOf treats '' as "no pointer" and stands down — .min(1) (or the field-name regex) closes the one shape a record amount can carry unjudged. (b) Refuse defaultCurrency under record at the same door, or write down why the inert key is kept (ADR-0078 symmetry with the arm this hunk adds). (c) Add ADR-0144 to scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json with the D2 invariant (PD [WIP] Add Chinese version of the documentation #13 corollary; the anchor file exists and names ADR-0120 only). Inherited, not this PR's: system/AddFieldOperation and the eleven other dropped-refinements.baseline.json sites drop CurrencyConfigSchema's refinements wholesale (fix(spec)!: a fixed currency config needs its code — currencyConfig.defaultCurrency loses its CNY default #27218's ledger), so a migration addField with record and no pointer is caught only at ObjectSchema.parse on rehydration (registry.ts:5675).
  2. object.zod.ts — holds: three refusals at fields.F.currencyConfig.currencyField, prescription plus candidates, self-pointer refused, formula covered, ObjectSchema.create() reaches it through ObjectSchemaBase.parse; the precedent's shape is kept to the letter. Tried and not broken: a pointer at the amount itself (refused), at a formula returning text (refused, non-text), two amounts sharing one code field (accepted — two currencies per record stays legal), fields absent (returns), an unnamed object (placeholder name). Should-fix: (a) the 'text' literal duplicates VALUE_DOMAIN_FIELD_TYPES (field.zod.ts:185, the set the ADR's "text field" rests on); read the set or state the coupling, per the file's own two-copies rule. (b) Add ADR-0144 to scripts/adr-anchors/packages__spec__src__data__object.zod.ts.json.
  3. field.form.ts — holds: the option and the row mirror the composite's defaultCurrency row (no sub-row visibleWhen, as the composite precedent). Should-fix: hand-translate the currencyConfig.currencyMode helpText in zh-CN / ja-JP / es-ES to the three-mode text, as this PR did for the currencyField row; today only the English bundle describes the option the select offers.
  4. skills/objectstack-data/rules/field-types.md — holds against the schema it teaches (bare number in every mode, text + valueDomain: 'iso_4217_currency', three modes, convention-not-contract per D8 rule 2); the skill's evals carry no currency prompt, so it is judged against the spec. Should-fix: (a) "A measure's own currency wins" is false under record once R1 lands — feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338 refuses a measure currency over a record amount (DATASET_INVALID); scope the sentence to fixed / dynamic. (b) AMOUNT_home / AMOUNT_rate are named with nothing to attach them to until R2's homeAmount; one clause saying they are the home-projection fields of a later release, or defer them, so an author does not declare inert fields now. (c) A one-line record-mode example (currencyConfig: { currencyMode: 'record', currencyField: 'currency_code' }) would teach the shape the paragraph only implies; the ratchet (3157 / 3158) means paying by deletion in the same file.

No must-fix. Each should-fix is a follow-up the owning seat can take on this branch or as an R1 rider; none changes the contract this head publishes.

Implemented-by: claude/project-thread-0wmk7j
Reviewed-by: session_01Utp6CbTZrFqRYNgjSaSbN5

VERDICT: PASS

os-bill pushed a commit that referenced this pull request Oct 29, 2026
Brings in the record currency mode at its final head (main merged
through 73725c2). No conflicts.

Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb
Co-authored-by: Claude <noreply@anthropic.com>
os-bill pushed a commit that referenced this pull request Oct 29, 2026
…rency

R1 rider from #27268's contract review: the share-link resolve answer
projected only currencyMode/defaultCurrency, so a shared record-mode
amount rendered as a plain number. Project the currencyField pointer when
the record carries the code field (never naming a redacted one), and hand
the shared record to the currency face on the shared-record page.

Claude-Session: https://claude.ai/code/session_01Utp6CXtadYa5QXb2PbXPev
Co-authored-by: Claude <noreply@anthropic.com>
… defaultCurrency

Contract-review should-fixes on the record currency mode (ADR-0144 D2):

- `currencyField` is never empty (`.min(1)`), so no record amount
  carries a pointer the object door names as nothing.
- `defaultCurrency` is refused under `record`, at the key, with a
  prescription. `record` is new, so no stored row needs the tolerance
  `dynamic` keeps; the inert key is ADR-0078's shape.
- The object door reads `VALUE_DOMAIN_FIELD_TYPES` instead of repeating
  the `'text'` literal.
- ADR-0144 anchors on field.zod.ts and object.zod.ts.
- The changeset lists `@objectstack/platform-objects`, which ships the
  regenerated metadata-form bundles.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
…urrency block scoped to record mode

Contract-review should-fixes:

- The `currencyConfig.currencyMode` help text in zh-CN, ja-JP and es-ES
  now describes `record` as the English does. `currencyConfig` says the
  value is a bare number in every mode, and `defaultCurrency` says it is
  refused in record mode, in all four locales.
- field-types.md: a one-line record-mode example; "a measure's own
  `currency` wins" scoped to fixed/dynamic; `AMOUNT_home`/`AMOUNT_rate`
  dropped until the home projection exists. Paid within the same file
  (3151 / 3158 tokens).
- The field reference page regenerated from the new descriptions.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b6f152bd17b1d7e70bed8c6b59098b3353f38691
Local-runs: none
Scope: packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts, skills/objectstack-data/rules/field-types.md + .changeset/27184-currency-record-mode.md, packages/spec/src/data/currency-config-record-mode.test.ts, scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json and …object.zod.ts.json, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, content/docs/references/data/field.mdx (the 13 files of 90114641..b6f152bd, +105/−57); main 28872e68 for the narrowing question. Delta review of the PASS record on 90114641 (comment 6487611079): everything that record judged and this diff does not touch stands as judged there.

Gates on b6f152bd: 60 success, 9 skipped, 0 failed, none pending; the ten required contexts all success; Check Changeset ran twice (05:23Z, 05:46Z), both success.

① Derived judgments

② Semver level

Changeset: @objectstack/spec minor, @object-ui/i18n minor, @objectstack/platform-objects minor (added — ② of the prior record resolved); body text names the two new refusals. Clause-②: yes (widening) / Metadata-protocol: added are unchanged and remain true: one new enum value, one new optional key, the refusals confined to that value. Right.

③ Boundary flags

④ Design judgment

  1. field.zod.ts — holds. The config door now refuses every declared-but-inert key the new mode can carry (ADR-0078 applied symmetrically), and the docblock states why dynamic is the one exception. Tried and not broken: record + defaultCurrency + no pointer (two located issues), fixed + currencyField: '' (min plus out-of-mode, no fixed-without-code issue), parse(parse(x)) unchanged. No finding.
  2. object.zod.ts — holds; the set-driven type test changes no verdict today and removes the second copy. No finding.
  3. field.form.ts — holds; the three help texts now say what the schema does, and the four bundles say the same. No finding.
  4. skills/objectstack-data/rules/field-types.md — holds against the schema (three modes, text + valueDomain: 'iso_4217_currency', bare number, measure sentence scoped). Should-fix, small: the rewrite dropped the D8 rule 2 wording — currency_code now reads as the name rather than a conventional one ("convention, not contract" is gone with the deferred names); two words such as "by convention" restore it inside the ratchet's 7-token headroom (3151 / 3158). The dropped ADR-0104 D1 citation in prose is acceptable: the schema's own describe carries it.

No must-fix. The one should-fix is wording in a governed skill file and does not change the contract this head publishes.

Implemented-by: claude/project-thread-0wmk7j
Reviewed-by: session_01Utp6CbTZrFqRYNgjSaSbN5

VERDICT: PASS

Seat note: the one remaining should-fix (skill wording "by convention", D8 rule 2) is carried by the R1 docs card #27189 rather than moving this head again.

os-bill pushed a commit that referenced this pull request Oct 29, 2026
…27188)

#27268 now refuses `defaultCurrency` under `currencyMode: 'record'`. The
Studio currency section already drops it on a mode switch; this pins the
whole path through the inspector (fixed EUR, switch to record, pick the
pointer) and parses the saved object against the spec.

Claude-Session: https://claude.ai/code/session_01Utp6CXtadYa5QXb2PbXPev
Co-authored-by: Claude <noreply@anthropic.com>
os-bill pushed a commit that referenced this pull request Oct 29, 2026
Brings in #27268's contract-review should-fixes. No conflicts.

Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

席位记录 · epic PM epic:#8345 · 2026-10-29T09:03Z

Head: b6f152bd17b1d7e70bed8c6b59098b3353f38691 · Session session_01Utp6CbTZrFqRYNgjSaSbN5

维护者速读

席位意见:建议批准。契约复审在本 head 上 PASS,无必改;仅剩的一条建议(skill 里字段名写明是约定 "by convention",D8 rule 2)由 R1 文档卡 #27189 / PR #27475 在本 PR 落地后补上,不再移动本 head。

席位核对

ACCEPT · epic PM epic:#8345 · head b6f152bd

ACCEPT → this comment.

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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(field): currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345)

3 participants