Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/8345-currency-mode-switch-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/skills': patch
---

docs(skills): the field-types guide names the backfill for an amount switched to record mode; the dashboards guide says a record-mode amount aggregate is served only grouped by its code field

Clause-②: no
Metadata-protocol: none

- **`objectstack-data/rules/field-types.md`**: an amount field with stored rows that is switched to `currencyMode: 'record'` is backfilled with `os migrate record-currency-codes`. The derived-measure sentence moves out of this guide; it already sits in the dashboards guide, which the line now points to.
- **`objectstack-ui/rules/dashboards.md`**: a record-mode amount refuses `sum` / `avg` / `min` / `max` unless grouped by its code field. The old "ungrouped" read as if any grouping would do.
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ Where a source leans one way, the lean is quoted here, not adopted. This record
`packages/platform-objects/src`. ADR-0029 D1, D3 and D6 give a first-party capability ownership of its `sys_*`
objects and decompose `platform-objects`, and ADR-0028 (Deferred) records the `sys` namespace design. #27185
settles the owner against them.
*Note (2026-10-30):* settled: `@objectstack/platform-objects` owns it (D3's one owner per object), landed in PR
#27246 (`194e66c0`). The table's one reader is the ObjectQL engine's write path, present in every composition that
can write a record, so it has no on/off capability boundary of its own (ADR-0029 D2) and no service or plugin
package would own it; it registers with the engine-facing infrastructure objects `sys_migration` and `sys_secret`,
and workload row M1-1 names the same package. Settlement:
[`6511509227`](https://github.com/objectstack-ai/objectstack/issues/27185#issuecomment-6511509227) on #27185.
12. **Whether today's cross-currency arithmetic between `fixed` fields of different codes is refused** (epic #8345).
Two shapes: a measure `currency` that contradicts a `fixed` field's code, and a derived measure over two different
`fixed` codes, which Context §1 counts among today's silently wrong numbers. The ruling on #27119 did not decide
Expand All @@ -362,6 +368,32 @@ Where a source leans one way, the lean is quoted here, not adopted. This record
the tree evidence. The general `fixed`-versus-`fixed` guard stays undecided. The authority is the maintainer's, and
epic #8345 carries the question
([`6485374827`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6485374827)).
*Note (2026-10-30):* settled: the maintainer ruled both shapes refused at dataset compile, letter A
([`6486427200`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6486427200) on #8345).
Card #27579 landed it in PR #27991 (`6dc06b2e`): a measure `currency` that differs from its `fixed` field's code,
and a derived measure, whatever its op, over two different `fixed` codes answer `DATASET_INVALID`; a `count`
operand is not compared. A `fixed` code is not judged against `localization.currency`, and `currency` over a
`dynamic` field still wins.
13. **An amount field switched to `currencyMode: 'record'` on an object that already holds rows** (#27577). A
`dynamic` or `fixed` field that turns `record` leaves its stored rows with an amount beside a blank code. No row is
refused for resting (ADR-0113 D1): the D8 rule 4 write guard refuses the next write that touches the pair
(`ERR_CURRENCY_CODE_REQUIRED`), and until the code is written the row shows a plain number, falls into the null
group of a grouped aggregate and sits outside every currency-paired filter. PR #27339's contract review
([`6490309706`](https://github.com/objectstack-ai/objectstack/pull/27339#issuecomment-6490309706), item ③)
escalated whether the switch is refused while such rows exist or a backfill is owed; this record decided neither.
*Note (2026-10-30):* settled: the maintainer ruled letter B
([`6492000994`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6492000994) on #8345): the
switch is allowed and owes an explicit backfill, landed in PR #28128 (`a3d24c99`). The operator runs
`os migrate record-currency-codes --object <object> --field <amount> --code <ISO 4217>`, which writes the given
code into every row that holds an amount beside a blank code (dry run by default, `--apply` writes; the code is
never derived, D8 rule 4), and the metadata save that makes the switch answers the existing 409
`destructive_change` with the finding `currency_mode_to_record`, lifted by the existing `force`. The finding reads
no row and fires only where a previous version exists, so a package registered at boot is not refused. The ruling
left open how a cloud-managed tenant without a CLI reaches the backfill; open beside it: the run walks every
organization under the unscoped system context and writes the one `--code` into each one's codeless rows (no
organization-scope flag, no per-organization count), so on a multi-organization deployment, where `dynamic`
amounts displayed in each organization's own `localization.currency`, that code can be wrong for some of them
(PR #28128's review should-fix S1, escalated to the epic PM).

## Verification notes

Expand Down
6 changes: 3 additions & 3 deletions skills/objectstack-data/rules/field-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ Stored as JSON on the parent row — no separate table / FK:
`$`); `'record'`, each record's code in the `text` field `currencyField`
names (by convention `currency_code`), with `valueDomain: 'iso_4217_currency'`. A
dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one
it may only restate the code, else `DATASET_INVALID` (delete it), as is a derived
measure over two different `'fixed'` codes (derive from one currency or drop an
operand). A `'fixed'` code is not judged against the tenant's.
it may only restate the code, else `DATASET_INVALID` (objectstack-ui →
Dashboards). A `'fixed'` code is not judged against the tenant's. Switched a
stored field to `'record'`? Backfill: `os migrate record-currency-codes`.

### Select with Default

Expand Down
12 changes: 6 additions & 6 deletions skills/objectstack-ui/rules/dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,12 @@ defineDataset({
dimensions: [{ name: 'signed_date', field: 'signed_date', type: 'date', dateGranularity: 'month' }],
measures: [{ name: 'signed_count', aggregate: 'count' }],
});
// A money measure may set `currency` (ISO 4217), e.g. `currency: 'USD'`.
// Chain: measure `currency` → the field's `defaultCurrency` (`fixed`
// only) → tenant `localization.currency`; omit it for counts. A
// `currencyMode: 'record'` amount refuses `currency` and ungrouped
// sum/avg/min/max; grouped by its code field, the column carries
// `currencyField` (per-row code column); a `count` never does. A `fixed`
// A money measure may set `currency` (ISO 4217, e.g. `'USD'`). Chain:
// measure `currency` → the field's `defaultCurrency` (`fixed` only) →
// tenant `localization.currency`; omit it for counts. A
// `currencyMode: 'record'` amount refuses `currency`, and sum/avg/min/max
// unless grouped by its code field; then the column carries
// `currencyField` (the per-row code column); a `count` never does. A `fixed`
// field refuses a different `currency` (delete it; restating is fine), and
// a derived measure refuses operands in two `fixed` codes (derive from one
// currency or drop one). Not judged: `currency` over a `dynamic` field, a
Expand Down
Loading