Skip to content

docs(adr,skills): ADR-0144 Open items 11 and 12 settled and Open item 13 for the record-mode switch; skills name the backfill and the grouped-only aggregate (multi-currency, epic #8345) - #28181

Merged
os-zhuang merged 3 commits into
mainfrom
claude/project-thread-0jat8q
Oct 31, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/project-thread-0jat8q

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

Part of #8345 (no card of its own: the epic PM carries these governed lines, as cards #27577, #27579 and #27185 and the PR #27475 review record route them here).
PR #28128 (the command the field-types line names; Open item 13 cites it) merged as a3d24c99; this branch carries that merge.
Ruling-ref: 6522614678

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

docs/adr/0144-…, Open items (this record is amended with each answer, as its own Open items preamble says):

skills/objectstack-data/rules/field-types.md adds one line: an amount field with stored rows that is switched to 'record' is backfilled with os migrate record-currency-codes. It is paid for by deleting the derived-measure sentence, which restates objectstack-ui/rules/dashboards.md; a pointer to that guide replaces it. The ratchet reads 3207 / 3207.

skills/objectstack-ui/rules/dashboards.md: "refuses currency and ungrouped sum/avg/min/max" becomes "refuses currency, and sum/avg/min/max unless grouped by its code field". This is the should-fix from PR #27475's review (6499106478), which its seat record (6499122847) carried here. It is paid for by dropping a restated key (e.g. currency: 'USD' → e.g. 'USD'). The ratchet reads 6261 / 6262.

.changeset/8345-currency-mode-switch-skills.md: @objectstack/skills patch, the same shape as #27475's skills changeset.

Why

The ADR says it is "amended with each answer". Open items 11 and 12 have been answered and have landed, and the mode switch was ruled after the ADR merged. The skills catalog ships verbatim to third-party agents, so it has to name the backfill and describe the grouped-only aggregate rule that the engine enforces. Ruling 6522614678 (letter A) puts this PR on the R2 development path; it is not release-paired.

Verification

Head: the docs commit, a merge of main after #28128 landed, and the item 13 citation of a3d24c99. The text was drafted by the seat, and the final pass was made at the derived mandated tier, which was checked against git show 194e66c0, git show 6dc06b2e and PR #28128's diff.

  • node scripts/check-skills-token-ratchet.mjs: exit 0 (field-types 3207 / 3207, dashboards 6261 / 6262).
  • pnpm check:adr-anchors, pnpm check:adr-index, pnpm check:adr-symbol-anchors and node scripts/check-adr-links.mjs: exit 0.
  • dispatch-gates --commands on 23b3eb6c (4 paths against origin/main) derives 49 families. Each was run with its exit code captured before any pipe, and all 49 exited 0. The dist-reading families ran after pnpm install --frozen-lockfile && pnpm build. That build had 114 of 115 tasks green. The one red was @object-ui/console, which read a @object-ui/react dist built before main was merged in mid-build. It is unrelated to this markdown-only diff.
  • The derived model tier for docs/adr/** and skills/** is the mandated tier at xhigh. The final text pass ran there.

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. (no names)
  • 3. No formatter ran; markdown only.
  • 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_01Utp6CbTZrFqRYNgjSaSbN5

… for the record-mode switch; skills name the backfill and the grouped-only aggregate

ADR-0144: Open item 11 gets a dated note: platform-objects owns sys_exchange_rate (#27185, PR #27246). Open item 12 gets one too: ruling 6486427200, letter A, refuses both fixed-vs-fixed shapes (PR #27991). New Open item 13 covers switching a stored amount to record mode: ruling 6492000994, letter B, implemented by PR #28128. It records that the multi-organization single-code reach stays open.

Skills: field-types.md names `os migrate record-currency-codes`. dashboards.md now says sum/avg/min/max are refused "unless grouped by its code field" (the should-fix from PR #27475's review). Both files stay inside the token ratchet.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract-review-claim: session_01Utp6CbTZrFqRYNgjSaSbN5 · head 23b3eb6 · 2026-10-30T15:57Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 23b3eb6cef51f90f8498baf47fba5e5b25049451
Local-runs: none
Scope: docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md, skills/objectstack-data/rules/field-types.md, skills/objectstack-ui/rules/dashboards.md + read on origin/main c48cd2c1: packages/platform-objects/src/plugin.ts (the first-manifest roster and its sys_exchange_rate bullet), docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md (D2, D3), docs/adr/0113-required-write-contract-vs-column-constraint.md (D1), packages/services/service-analytics/src/measure-currency-coherence.ts, packages/services/service-analytics/src/cube-record-currency-door.ts, packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns, 3186–3226), packages/services/service-analytics/src/tests/record-currency-analytics.test.ts (the count controls), packages/spec/src/ui/dataset.zod.ts (DatasetMeasureSchema.field), packages/metadata-protocol/src/protocol.ts (detectDestructiveObjectChanges 5535–5639, the Phase 3a call site 20780–20860), packages/objectql/src/record-currency-backfill.ts, packages/objectql/src/validation/record-currency-code.ts, packages/cli/src/commands/migrate/record-currency-codes.ts, packages/services/service-settings/src/manifests/localization.manifest.ts (currency), packages/spec/src/api/error-code-ledger.zod.ts (ERR_CURRENCY_CODE_REQUIRED), scripts/check-skills-token-ratchet.mjs (the two ceilings), packages/skills/package.json, .changeset/8345-currency-mode-switch-skills.md; the three landing commits the notes cite (194e66c0, 6dc06b2e, a3d24c99) read with git show.

PR #28181 has no card of its own. Read, as the brief lists them: cards #27577, #27579 and #27185 (body and every comment); rulings 6486427200, 6492000994 and 6522614678 on #8345, with 6485374827 and 6511509227 that the ADR notes link; PR #27475's review 6499106478 and seat record 6499122847; PR #28128's squash diff (3cc45cc4..a3d24c99, 11 files), its review 6525881524 and seat record 6525901387; PR #27339's review 6490309706; this PR's body, its four files and the net diff against main at 23b3eb6c; the 42 check-runs on that head. Nothing was built, tested or re-run; every reading below is a git show or git grep of a committed tree, named by commit.

① Derived judgments

  • Face: rules, so CONTRACT_REVIEW_TIER — right. Two governed register rows in the file list, docs/adr/** and skills/**, both Tier H. hits are the three paths on the Scope: line; the changeset is the remainder. No packages/spec/src/**, no .claude/**, no (narrowing) arm.
  • Published surface moved: @objectstack/skills alone — right. packages/skills/package.json is the published package whose build syncs the repository's skills/** into its dist, so a byte change in two rule files releases it. Prose only: no schema, export, authorable key or accepted value moves. docs/adr/** publishes nothing. Clause-②: no and Metadata-protocol: none follow.
  • Base — right. The merge-base a3d24c99 is 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's own squash merge, so the head carries the command the field-types line names and the 409 finding Open item 13 cites; the branch merged main after 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 landed (ab1b65b7) and the last commit (23b3eb6c) swaps the item 13 citation to that merge sha. origin/main has since moved two commits (412908cf, c48cd2c1), neither touching a hit or any cited file; mergeable_state is clean.
  • Token ratchet — right, read off the committed trees with the gate's own convention (ceil of utf8 bytes over 4). Ceilings unchanged at 3207 (field-types.md) and 6262 (dashboards.md). field-types.md: 12827 bytes on main (3207) to 12826 at the head (3207), at its ceiling before and after; dashboards.md: 25047 (6262) to 25043 (6261). The deletions in the same file pay for the additions, exactly as the ratchet's discipline requires; no row moves, so no ruling quote is owed. Lint & Repo Gates success is the gate's verdict.
  • Check-runs on 23b3eb6c: 42, of which 29 success, 13 skipped, 0 failed, none in progress. The skipped runs are the ui/** path-filtered contexts, the opt-in harnesses and the schedule gate. Of the ten required contexts, every one that ran is success: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI; Lint UI, Type Check UI and Build & E2E UI skipped on the path filter, as on every diff without ui/**.
  • ADR form — right. Three dated notes and one new numbered item under Open items; D1–D9 untouched; no decision reversed. The Open items preamble ("This record is amended with each answer") is what authorises the shape, and item 5's precedent is followed: items 11 and 12 keep their text and gain a note, so the numbering below them does not move.
  • Shape of the PR: 3 commits, 4 files, +52/−9, size/s, draft, head repo equals base repo, branch claude/project-thread-0jat8q (the platform-thread branch the body names), author objectstack-fleet[bot].

② Semver level

③ Boundary flags

④ Design judgment

docs/adr/0144 — Open items 11, 12 and 13. An ADR is judged against its first consumer and its execution plan: each note names a landing commit and a ruling, so each sentence was read against the code at that commit as it stands on origin/main c48cd2c1, and against ruling 6522614678's order.

Item 11 note — holds.

Item 12 note — holds; one should-fix, routed to the code it cites.

  • 6dc06b2e is fix(service-analytics): a dataset refuses a measure currency that contradicts its field's fixed code, and a derived measure across two different fixed codes #27991's squash merge (measure-currency-coherence.ts and three test files). Letter A is 6486427200. "a measure currency that differs from its fixed field's code ... answer DATASET_INVALID": guard 1, field.defaultCurrency !== m.currency; a restated code is equal and passes. "a derived measure, whatever its op, over two different fixed codes": guard 2 iterates every derived measure regardless of op, and pairRefused holds a.fixed && b.fixed && a.code !== b.code. "A fixed code is not judged against localization.currency": a tenant-kind operand never pairs with a fixed code in pairRefused, and guard 1 has no tenant branch. "currency over a dynamic field still wins": a dynamic field relays no defaultCurrency, so the explicit code resolves with fixed: false and nothing refuses it (the service-analytics: refuse fixed-versus-fixed cross-currency measures at dataset compile (multi-currency, epic #8345) #27579 dev report's pinned CONTROL).
  • "a count operand is not compared" — true for a bare count, which is the only count the guard tests pin (invoices), and it is the ruling's own sentence and the module header's. Not true at one edge, which the code owns, not this note. DatasetMeasureSchema.field is optional for count but allowed (a count of non-null values; the test dataset's own priced_invoices: { aggregate: 'count', field: 'amount' }), and measureCurrencyResolver.resolvedOf resolves a non-derived measure from fieldOf(m) without reading m.aggregate, so a count over a record-mode or fixed amount resolves to that amount's currency and guard 2 compares it: a ratio of usd_total (fixed USD) over priced_invoices (count over the record amount) is refused as "amounts in different currencies" although one operand counts rows. enrichResultColumns and the cube door both gate on AMOUNT_AGGREGATES and get the same count right. should-fix: a defect against @objectstack/service-analytics to file under Prime Directive chore: version packages #10 (the fix is one AMOUNT_AGGREGATES test in resolvedOf's non-derived branch and a pin); the ADR sentence states the ruled behaviour and stays as written. Not a must-fix: the shape is rare, loud (DATASET_INVALID at compile, never a wrong number), and nothing in this PR makes it worse.

Item 13 — holds.

  • The question is quoted as 6490309706 ③ escalated it, and "this record decided neither" is right: nothing in D1–D9 covers the switch. "No row is refused for resting (ADR-0113 D1)": D1 is the non-regression invariant, and the write guard's header names it. "the D8 rule 4 write guard refuses the next write that touches the pair (ERR_CURRENCY_CODE_REQUIRED)": validation/record-currency-code.ts judges only pairs the payload touches and throws that code, registered at error-code-ledger.zod.ts 801. The three consequences are the 409 message's and the ruling's.
  • Letter B is 6492000994; a3d24c99 is 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's squash merge. The command line matches the landed flags: --object, --field and --code are required: true, --apply is a boolean defaulting to false, so "dry run by default, --apply writes" is right. "writes the given code into every row that holds an amount beside a blank code": the walk skips a present code first, then a blank amount, both by isRecordCurrencyBlank, and writes only options.code; "never derived" holds, since nothing reads defaultCurrency or localization.currency.
  • "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": detectDestructiveObjectChanges pushes that finding when the next field is currency in record mode, the previous mode is not record (an absent config counts, the spec default being dynamic) and the previous field is stored; the Phase 3a gate runs under if (!request.force && singularType === 'object'), reads prev through readMetaItem, and throws DESTRUCTIVE_CHANGE / 409 only if (prev). So "reads no row" (a pure function of the two bodies), "fires only where a previous version exists" and "a package registered at boot is not refused" (code-declared registration never calls saveMetaItem) all hold. Spelling nit, no fix owed: the wire code is DESTRUCTIVE_CHANGE; destructive_change is the spelling the gate's own docblock (protocol.ts 5486), the card and the ruling use for the 409.
  • "the run walks every organization under the unscoped system context": SYSTEM_CTX = { isSystem: true } with the comment saying exactly that. "(no organization-scope flag, no per-organization count)": the CLI's flags are database-url, object, field, code, apply, yes, force, max-records, json; the report has no per-organization field. "where dynamic amounts displayed in each organization's own localization.currency": the setting is tenant-scoped (ADR Context §1; the manifest). "(PR 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's review should-fix S1, escalated to the epic PM)" is 6525881524's S1 verbatim in substance. "The ruling left open how a cloud-managed tenant without a CLI reaches the backfill" is 6492000994's "Not judged, left open".
  • Tried to break it. (i) A formula currency re-declared record: isStored(prevField) is false, no 409, and no stored row owes a code — the note's "stored rows" and "an object that already holds rows" are the right qualifiers. (ii) A brand-new record field: if (!prevField) continue, no finding — consistent with "fires only where a previous version exists". (iii) force is read on the one REST face that has it; destructiveChangeRemedy words the remedy per face — a property of the existing 409, not of this finding; the note says "the existing force", which is right. (iv) The re-pointed currencyField case (③) is outside the ruled condition and the note does not claim it. No finding.

skills/objectstack-ui/rules/dashboards.md (the measure-currency comment, lines 298–307 at the head) — holds; the carried should-fix is delivered.

  • "A money measure may set currency (ISO 4217, e.g. 'USD')": DatasetMeasureSchema.currency, unchanged meaning, one restated key fewer.
  • "refuses currency": guard 1's record branch throws whatever the grouping.
  • "and sum/avg/min/max unless grouped by its code field": cube-record-currency-door.ts judges exactly AMOUNT_AGGREGATES (sum, avg, min, max) and admits only when a dimensions entry resolves to the code field on the same object; a time dimension is no group and a where is not judged. The ObjectQL half judges the same four on groupBy.
  • "then the column carries currencyField (the per-row code column)": enrichResultColumns sets fc.currencyField to the name of the selected dimension over the code field, under AMOUNT_AGGREGATES.has(m.aggregate). "a count never does": gated out by the same set, and pinned for both the bare count and the count that names the amount (priced_invoices).
  • The remainder of the comment (a fixed field refuses a different currency, a derived measure refuses two fixed codes, the not-judged pair) is unchanged text and holds against guards 1 and 2 as read above.
  • Tried: could "unless grouped by its code field" be read as reaching back to currency? The comma before "and" separates the two objects, and the next clause ("then the column carries currencyField") only makes sense with currency still refused. Acceptable at one token of headroom, and the earlier ambiguity ("ungrouped" as "no dimension at all") is gone. The comment sits inside the os:check TypeScript block, so the compiled example is byte-identical to the base's. No finding.
  • Scenario: no card and no eval — skills/objectstack-ui/evals/ carries no currency case on main. Judged against the spec it teaches: ADR-0144 D6 and the landed code above. Ratchet 6261/6262.

skills/objectstack-data/rules/field-types.md (lines 180–188 at the head) — holds.

  • "over a 'fixed' one it may only restate the code, else DATASET_INVALID (objectstack-ui → Dashboards)": guard 1; the pointer is the form this file already uses at line 124 ("objectstack-ui → Dashboards → Dataset-Bound Widgets"), and the derived-measure rule it replaces is stated in dashboards.md lines 304–305. A file-level pointer is right here: the rule lives in a comment under the "Server-side date bucketing" heading, which a section-level pointer would misname.
  • "A 'fixed' code is not judged against the tenant's." — unchanged, holds (item 12 above).
  • "Switched a stored field to 'record'? Backfill: os migrate record-currency-codes.": the command is on main at the merge base (oclif discovers dist/commands/migrate/record-currency-codes.js). The three required flags are not named; the command's --help and the 409 at the switch carry them with --object and --field filled in, so an author who follows the sentence lands on the prescription. Acceptable at zero headroom (3207/3207); "stored" is the right qualifier (a formula switched to record owes nothing).
  • Tried: an author switching a fixed field to record — defaultCurrency is refused under record by the object door and the backfill never reads it; the sentence implies neither. An author on a new object with no rows runs a dry run that reports zero; harmless. No finding.
  • Scenario: no card and no eval — skills/objectstack-data/evals/ carries no currency case on main. Judged against the currency chain the file teaches and the landed code. The file was already at its ceiling on main and stays there; the next word added here is paid by a cut in the same file.

Findings: 0 must-fix · 1 should-fix (@objectstack/service-analytics: the compile guard's resolver compares a count that names a field; a card, not an edit to this PR) · observations: destructive_change versus the wire DESTRUCTIVE_CHANGE; the currencyField re-point hazard is not in item 13 by the seat's choice; field-types.md keeps zero headroom.

Implemented-by: claude/project-thread-0jat8q
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-30T16:15Z

Head: 23b3eb6cef51f90f8498baf47fba5e5b25049451 · Session session_01Utp6CbTZrFqRYNgjSaSbN5

维护者速读

席位意见:建议批准。复审无必改;一条建议指向已落地代码而非本 PR,已立卡 #28194。

席位核对

ACCEPT · epic PM epic:#8345 · head 23b3eb6c

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 size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants