Skip to content

docs(spec): the currency-chain text states the fixed-mode condition — spec sites 5–10 of the currency-mode closure - #20217

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20126-currency-chain-spec-sites
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20126-currency-chain-spec-sites

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20126
Clause-②: no

This PR covers remainder (b) of the currency-mode class-closure card: sites 5–10, appended by the seat in comment 5853407641 and kept in this lane by triage (5853734589). It does not close the card. What stays open is listed under Remainder at the end.

What was wrong

CurrencyConfigSchema declares two modes. Under currencyMode: 'fixed', the field has one currency, defaultCurrency. Under dynamic (the default), the field has no currency of its own: amounts display in the tenant default currency (localization.currency), or as a plain number when none is set, and defaultCurrency is not read, including its parse default CNY. The value is a bare number either way (ADR-0104).

The readers were re-measured for this PR, and each reads defaultCurrency only under 'fixed':

  • Analytics relay, at base 1c8b320a8. AnalyticsServicePlugin's sourceFieldMeta (packages/services/service-analytics/src/plugin.ts, the fixedCurrency ternary) answers currencyConfig.defaultCurrency only when currencyMode === 'fixed', and undefined otherwise. queryDataset then resolves mc.currency ?? meta?.defaultCurrency ?? context?.currency. context.currency is the tenant localization.currency (packages/core/src/security/assemble-execution-context.ts).
  • objectui's resolveFieldCurrency, at objectui main 25c7d584. packages/i18n/src/currency.ts:60 takes config.defaultCurrency only under currencyMode === 'fixed'.

Six texts under packages/spec/** and examples/** still described an unconditioned defaultCurrency step, or called the chain "ADR-0053". ADR-0053 is the date / datetime semantics record and never names currency.

What changed: text only, site by site

Site 5, packages/spec/src/contracts/analytics-service.ts, the AnalyticsResult.fields[].currency TSDoc. It ships in the published .d.ts. The ADR-0053 label is gone. The middle step is now "the source field's FIXED currency (currencyConfig.defaultCurrency, read only under currencyMode: 'fixed' — a dynamic field has no currency of its own, so its defaultCurrency is not read)". The text cites CurrencyConfigSchema and ADR-0104, and it says the column is also absent when no step answers. The date-bucketing ADR-0053 Phase 2 citation in the same file (now :300) really is a date use, and is untouched.

Site 6, packages/spec/src/data/percent-scale.ts, module header. "This mirrors the ADR-0053 currency chain" now reads "This mirrors the currency chain", with the three steps and the fixed condition spelled out.

Site 7, packages/spec/src/api/analytics.zod.ts, AnalyticsResultResponseSchema docblock. "(the ADR-0053 / percent-scale renderer chains)" now reads "(the currency / percent-scale renderer chains)".

Site 7, in-class extra in the same file: the data.fields[].currency describe. This is the .describe() mirror of site 5, and it said "source-field default" with no mode:

  • FROM: Resolved ISO 4217 code for a MONETARY measure (explicit measure `currency`, then source-field default, then tenant default). Absent on non-monetary columns, which must never render a symbol.
  • TO: Resolved ISO 4217 code for a MONETARY measure (explicit measure `currency`, then the source field's fixed currency — its `currencyConfig.defaultCurrency`, read only under `currencyMode: 'fixed'` — then the tenant default). Absent on non-monetary columns, which must never render a symbol.

Site 8, packages/spec/liveness/dataset.json (currency) and packages/spec/liveness/field.json (currencyConfig). Evidence text is hand-maintained. check:liveness resolves its paths and #symbol anchors, and gen:liveness-counts tallies statuses only, so the owning route is a hand edit gated by check:liveness.

  • dataset.json: the evidence drops the ADR-0053 label and names the middle step "the source field's FIXED currency". It gains a gate-checked anchor at the relay, packages/services/service-analytics/src/plugin.ts#AnalyticsServicePlugin.
  • field.json: re-measured at objectui 25c7d584, and the pointers moved with the code: currency.ts :32 → :60, and CurrencyField.tsx :45/:51 → :55/:91. At the old pin 11c1e71e the read was unconditioned, so rewording the text without re-measuring would have pinned a false sentence to that sha.
  • Both rows get verifiedAt re-dated to 2026-09-27 and a dated note. Both verdicts stay live; no measurement falsified either one. state-counts.md is unchanged.

Site 9, packages/spec/src/data/field.zod.ts:447, the defaultCurrency describe string only. The .default('CNY') and every other line of the region-split file are untouched. The diff is one line.

  • FROM: Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR)
  • TO: The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency. Not read under `dynamic` (the default), where amounts display in the tenant default currency.

gen:docs regenerated the two matching rows of content/docs/references/data/field.mdx. check:generated proved that one artifact stale, and no other.

Site 10, examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts. The comment stated the chain with no mode, cited ADR-0053, and spelled the key as a default- / Currency wrap, so a line grep for defaultCurrency missed it. It also told authors to "set a field currencyConfig" to pin a symbol, which pins nothing without currencyMode: 'fixed'. project.budget / spent declare no currencyConfig at all, since Field.currency() injects none. The comment now states the conditioned chain and the fixed form an author would actually write.

In-class extras found by the census, fixed in place

All four conditions of the bounded in-place rule hold for these:

The extras:

  • packages/spec/src/api/analytics.zod.ts, the fields[].currency describe (above).
  • examples/app-showcase/src/data/objects/account.object.ts:63 and semantic-zoo.object.ts:53: "spec channel: currencyConfig.defaultCurrency" gains "read only under currencyMode: 'fixed'". Both fields already declare 'fixed'.
  • examples/app-showcase/e2e/detail-shapes.spec.ts:125: "currencyConfig.defaultCurrency drives a real $ symbol" now reads "the fixed-mode currencyConfig.defaultCurrency (USD)". The field is semantic-zoo.budget, fixed / USD.
  • packages/spec/src/data/field.test.ts:176: this test-block comment names currencyConfig.defaultCurrency as the declarable nested form. It now adds "under currencyMode: 'fixed' — the only mode that reads it". It is a comment only, with no assertion change.

Census: the seed for remainder (c)

Instrument: a scratchpad script (not committed) that reads each file at a git ref. The scopes are packages/spec/src/**, packages/spec/liveness/** and examples/**: 1881 files at both refs. Base is 1c8b320a8. Head is 6ff1a15c, the final head after merging main e7f69dbb; it reads identically to the pre-merge head 452bd5d4.

Reading Base Head
ADR-0053 within five lines of currenc 6 (the six site lines) 0
defaultCurrency lines 79 86 (the corrected text adds lines)
default- / Currency wrapped tokens, which a line grep misses 1 (site 10) 0

Controls.

  • The base reading of 6 is the lit leg of the same instrument.
  • 38 ADR-0053 lines remain in these scopes at head. All are date uses outside the window: analytics-service.ts:300 (date bucketing, the :294 the order excluded, shifted by this diff) and execution-context.zod.ts:91 (timezone, ADR-0053 Phase 2). No genuine date-bucketing use falls inside the window.

Sentence-level judgment at head. Every prose sentence naming defaultCurrency in these scopes carries the fixed condition or says it is not read under dynamic:

Sentence Where
Corrected by this PR field.zod.ts:446, :447, :460–:463, :1060, :1066–:1068; analytics.zod.ts:111–:116; analytics-service.ts:79–:90; percent-scale.ts:14–:18; both ledger rows; chart-gallery.dataset.ts:44–:51; account.object.ts:63; semantic-zoo.object.ts:53; detail-shapes.spec.ts:125; field.test.ts:176
Code, not sentences the aliases map (field.zod.ts:427), the precision-check code (:465, :505, :512, :2327), and every test fixture
Judged pass, flagged for the pin's author field.test.ts:202, the it title "names the declarable nested form, currencyConfig / defaultCurrency". It labels exactly what its assertions check (the refusal message contains both tokens), and its body parses the fixed form. Retitling it to claim fixed would need a new message assertion, which is outside a text-only PR. A pin that scans test titles should decide this line deliberately.

Acceptance notes (observed, not filed)

  • examples/app-showcase/src/data/objects/semantic-zoo.object.ts:54–:55 says a bare field-level currency key "gets stripped at parse". FieldSchema refuses it: field.test.ts:198 pins success: false, and the guidance message is at field.zod.ts:1066. This is a different defect class, left untouched.
  • Released CHANGELOG entries still say "ADR-0053 currency chain": packages/spec/CHANGELOG.md :28383 and :66160, and packages/services/service-analytics/CHANGELOG.md :1976 and :8504, all under 17.0.0. These are release-owned. AGENTS.md routes an amendment to a dedicated docs-only PR, never a rider here.
  • objectui 25c7d584, packages/i18n/src/currency.ts:22 and packages/fields/src/widgets/CurrencyField.tsx:52, still cite ADR-0053 for the tenant step. :30 still quotes the spec's pre-docs(spec): currencyMode says what each mode does; the currency-chain text states the fixed condition #20179 "user selectable" wording. The code is right; the comments are in objectui's lane.

Verification

All readings are at head 6ff1a15c: this branch plus a merge of main e7f69dbb. Main brought no file this PR touches. The branch delta against main is byte-identical before and after the merge: git patch-id --stable gives 224b6215 on both.

Build and tests (one os-verify-lock.sh invocation, VERDICT command-exit 0):

  • turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2: "Tasks: 71 successful, 71 total".
  • pnpm --filter @objectstack/spec run check:generated: "All 15 generated artifacts are up to date". This is the second leg: before gen:docs, the same command read "1 of 15 artifact(s) stale: content/docs/references/**", and exactly the 2 defaultCurrency rows moved.
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: "Test Files 540 passed (540) / Tests 15878 passed | 2 todo (15880)".
  • pnpm --filter @objectstack/spec run typecheck: exit 0 (tsc --noEmit, check:scripts-typecheck, and "check:test-typecheck: OK").
  • pnpm --filter @objectstack/example-showcase run typecheck: exit 0.
  • pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2: "Test Files 29 passed (29) / Tests 384 passed (384)".

What ships. The measurement was taken on the built dist, with a positive control:

  • The new TSDoc is in dist/contracts/index.d.ts and dist/percent-scale-*.d.ts / .d.mts.
  • Both new describes are in dist/ui and dist/api.
  • "ADR-0053 currency chain" has zero hits in dist.
  • The field.test.ts comment is not shipped.

Consumer fixture triage. No test or snapshot pins any changed string. git grep for the old describe texts and the old example comments outside their own files gives 0 hits, and the same search over examples/app-showcase lights on the edited files as a control. The one generated consumer, field.mdx, is regenerated.

Gates.

  • dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6ff1a15c derives 109 commands (13 paths vs merge base e7f69dbb). All 109 were run, each exit code recorded before any pipe, and all 109 exit 0. --ran answers "Run reconciliation — 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero: every line carries its exit code).
  • The roster families whose roster sits under a touched directory, 6 of them, all exit 0: check-changeset-fixed, spec check:meta-url-spelling, spec check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity.
  • check:partof-closing-keyword on this body, offline via PR_BODY: exit 0.

Lint. The run is narrowed, since CI owns pnpm lint. eslint --no-inline-config --format json on the 9 touched .ts files: 9 files, 0 errors, 0 warnings, none ignored. --print-config puts each file in eslint's population, with 4 or 5 rules each. eslint.config.mjs sets no parserOptions.project, so no type-aware linting runs and this diff cannot move a verdict on an untouched file.

Bytes. A control-byte scan over all 13 files gives grep exit 1 (none); the control fixture is lit. check:nul-bytes is in the union.

Ablation. None. The diff is text only: describes, TSDoc, ledger text and comments. No assertion or runtime line changed, so there is no negative pin to ablate. The check:generated red-then-green above is the regenerated artifact's own two-leg reading.

Remainder after this PR


Generated by Claude Code

…nder fixed mode

The AnalyticsResult.fields[].currency TSDoc, its AnalyticsResultResponseSchema
describe mirror, the response docblock and the percent-scale header called the
measure-currency chain "ADR-0053" (the date/datetime record) and stated its
middle step as an unconditioned "source-field default". The chain the service
runs reads the field's currencyConfig.defaultCurrency only under
currencyMode 'fixed'; a dynamic field has no currency of its own and falls
through to the tenant localization.currency. Text only.

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

dataset.measures.currency and field.currencyConfig cited the currency chain
as "ADR-0053" (the date/datetime record) with an unconditioned
defaultCurrency step. Re-measured: the analytics relay
(AnalyticsServicePlugin sourceFieldMeta) and objectui's resolveFieldCurrency
(objectui @25c7d584, currency.ts:60) both read defaultCurrency only under
currencyMode 'fixed'. Evidence re-worded and re-anchored, verifiedAt
re-dated; both verdicts stay LIVE.

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

The chart-gallery dataset comment stated the measure-currency chain with an
unconditioned currencyConfig.defaultCurrency step, cited ADR-0053 (the
date/datetime record), and advised "set a field currencyConfig" to pin a
symbol, which pins nothing without currencyMode 'fixed'. The account and
semantic-zoo field comments and the grouped-detail e2e comment named
currencyConfig.defaultCurrency as the ISO channel without the mode; the
fields those three describe already declare currencyMode 'fixed'. Comments
only.

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

CurrencyConfigSchema.defaultCurrency read "Default or fixed currency code",
which teaches a dynamic-mode default that no reader honours: under dynamic
the key is not read and amounts display in the tenant default currency. The
describe now states that, matching the currencyMode describe. The #8163
test-block comment names the same nested form with its mode. Describe string
and comment only; the .default('CNY') and every other line are untouched.

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

gen:docs output for the two CurrencyConfigSchema defaultCurrency rows.

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

The two re-dated notes named the retired citation to say the chain is not
it; a count-based closure pin cannot tell that negation from a citation.
They now say the chain no longer cites the date/datetime-semantics ADR.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/dataset.json, packages/spec/liveness/field.json, packages/spec/src/data/percent-scale.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/api/data-api.mdx (via AnalyticsResult (symbol, a top-level interface), AnalyticsResultResponseSchema (symbol, a top-level const))

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

  • content/docs/releases/v17/17-3.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/releases/v9.mdx (via AnalyticsResult (symbol, a top-level interface))

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
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/dataset.json, packages/spec/liveness/field.json, packages/spec/src/data/percent-scale.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 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; 98 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 — 136 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 585c9af587a165c066c7815185a6652c2b9c545e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 585c9af587a165c066c7815185a6652c2b9c545e

⚠️ 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 585c9af587a165c066c7815185a6652c2b9c545e → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6ff1a15cf4998a432bada27954b4b9bf359ca3c4

① Derived judgments

Inputs read: card #20126 body and comments 5853407641 / 5853734589 / 5855121790 / 5855122068; docs/adr/0053-date-and-datetime-semantics.md and docs/adr/0104-field-runtime-value-shape-contract.md at origin/main; the PR body, diff.patch, pr.json, and the head's check-runs; the repo at origin/main (3875ae67) and at the head; objectui at /home/user/objectui (pin f8a9d0fb, origin/main 25c7d584). The supplied diff.patch is byte-equal (modulo index lines) to git diff origin/main...6ff1a15c: 13 files, +71/−34, merge base e7f69dbb.

  • The readers, quoted at the head (all three files byte-identical on origin/main). The relay reads defaultCurrency ONLY under fixed: packages/services/service-analytics/src/plugin.ts:1101-1104 — const fixedCurrency = f.currencyConfig?.currencyMode === 'fixed' ? f.currencyConfig.defaultCurrency : undefined; return { type: f.type, max: f.max, defaultCurrency: fixedCurrency };. The resolver, analytics-service.ts:1809-1812 — const monetary = !!mc.currency || meta?.type === 'currency'; if (monetary) { const resolved = mc.currency ?? meta?.defaultCurrency ?? context?.currency; if (resolved) fc.currency = resolved; }. No other production sourceFieldMeta provider exists (git grep at head: only the plugin's). The spec's own reader, field.zod.ts:464-465 — if (config.precision === undefined || config.currencyMode !== 'fixed') return; const contradiction = currencyPrecisionContradiction(config.defaultCurrency, config.precision);. RIGHT: every in-repo reader is mode-conditioned.
  • 「the tenant default currency (the localization.currency setting)」 is accurate. packages/core/src/security/resolve-authz-context.ts:1570/:1584/:1610 read settings namespace localization, key currency, scope: 'tenant'; assemble-execution-context.ts:368 sets currency: anonymous ? undefined : localization?.currency; the resolver consumes it as context?.currency (:1811). RIGHT.
  • objectui resolveFieldCurrency, at the pin AND at main. At the Console pin .objectui-sha = f8a9d0fb (same at head and origin/main), packages/i18n/src/currency.ts:32 reads field?.currencyConfig?.defaultCurrency || UNCONDITIONALLY: the fix ea9d17fb6 (objectui#10461) is NOT an ancestor of the pin. At objectui main 25c7d584, currency.ts:60 reads (config?.currencyMode === 'fixed' ? config.defaultCurrency : undefined) ||. So the new defaultCurrency describe's 「Not read under dynamic」 holds against every reader in this repository and against objectui main, and does NOT hold against the field faces of the Console vendored here. This is the same state the seat recorded as non-blocking for the release lane at the docs(spec): currencyMode says what each mode does; the currency-chain text states the fixed condition #20179 ACCEPT (5853407641), and the identical claim is already published on main in the currencyMode describe (field.zod.ts:446, 「defaultCurrency is not read」). Not new to this PR; see ③.
  • Site 9, field.zod.ts:447 defaultCurrency describe (→ field.mdx:32 / :240). One line; .length(3) and .default('CNY') untouched. Text is TRUE per the readers above and CONSISTENT with the currencyMode describe at :446 (both: fixed = the field's one currency; dynamic = tenant default, defaultCurrency not read). The two regenerated field.mdx rows are byte-equal to the describe; the old string 「Default or fixed currency code」 survives nowhere at head but the changeset's FROM line, and the new string appears only in field.zod.ts, field.mdx and the changeset (no other generated artifact carries describe text). Build Docs check-run success. RIGHT.
  • Site 7 extra, analytics.zod.ts:111-116 fields[].currency describe. Node evaluation of the new concatenation equals the changeset's TO byte-for-byte, and of the old equals FROM. The stated order (measure currency → the field's fixed currencyConfig.defaultCurrency, read only under fixed → tenant default) is the :1811 chain plus the :1101 relay. Ships via the ./api export. RIGHT.
  • Site 5, contracts/analytics-service.ts:79-90 TSDoc. The chain matches :1809-1812; 「absent … when no step answers」 is if (resolved) fc.currency = resolved;; 「renders as a plain number」 is objectui's formatMeasure (packages/core/src/utils/dataset-format.ts:352, no-currency fall-through to plain numeral formatting) and formatAmount; 「CurrencyConfigSchema in spec/data」 — packages/spec/src/data/index.ts:132 re-exports field.zod, and ./data is in the exports map; 「bare number (ADR-0104)」 — ADR-0104 :60 / :144. The :300 ADR-0053 Phase 2 citation in the same file is date bucketing and is untouched. RIGHT.
  • Site 6 percent-scale.ts:14-18 and site 7 analytics.zod.ts:80 docblock. Comment-only; the spelled-out chain and its cross-reference (AnalyticsResult.fields[].currency) are consistent with the above. ADR-0053 (docs/adr/0053-date-and-datetime-semantics.md) contains no occurrence of currenc, so dropping the label is correct. RIGHT.
  • Site 8, liveness/dataset.json currency. Both #symbol anchors resolve at head (analytics-service.ts:841 export class AnalyticsService, plugin.ts:261 export class AnalyticsServicePlugin); Spec property liveness check-run success. The monetary rule and the chain are quoted correctly; 「Re-closed by hand against 1c8b320」 — that commit is an ancestor of the head and already carries the fixedCurrency ternary (:1101). Verdict live unchanged. Nit, not a falsehood: the chain sits in the private enrichResultColumns (:1735), which queryDataset calls at :1434 and :1691; 「(queryDataset: the currency chain …)」 is pre-existing wording and loosely right. RIGHT.
  • Site 8, liveness/field.json currencyConfig. Re-measured at objectui 25c7d584 (= objectui origin/main): currency.ts:60 (conditioned read), CurrencyField.tsx:55 (resolveFieldCurrency call) and :91 (formatAmount, → formatDisplayNumber → Intl.NumberFormat, packages/i18n/src/utils/formatting.ts:122) all exist as cited; 「since objectui#10461」 is ea9d17fb6; the old pin 11c1e71e really read unconditionally at :32, which is why re-wording without re-measuring would have been false. The README's house style is satisfied: re-attestation is 「re-reading at its current head, re-pin the commit, stamp the date」 (liveness/README.md:311-313), the sha is pinned in the prose, evidenceScope: cross-repo kept; no ledger row cites the .objectui-sha pin (rows cite 11c1e71e, 87af769e, 08ca73f8, 53ded82bf7, 9ce096fb0), so citing main is convention-consistent. Verdict live unchanged. RIGHT.
  • verifiedAt re-dates (2026-08-28 → 2026-09-27; 2026-08-10 → 2026-09-27). Justified: the evidence was materially re-measured (new anchor, moved line pointers verified above), which is the README's re-verification event, not a re-stamp. state-counts.md is untouched.
  • Site 10, chart-gallery.dataset.ts:44-51. project.object.ts:63-64 — budget: Field.currency({ label: 'Budget', min: 0 }), spent: Field.currency({ …, defaultValue: 0 }): no currencyConfig; the factory (field.zod.ts:2551, ({ type: 'currency', ...config })) injects none. The chain and the prescribed fixed form are correct; the old wrapped default-/Currency token (the one a line grep missed) is gone. RIGHT.
  • The four comment-only extras. account.object.ts:63 — the field declares currencyMode: 'fixed', defaultCurrency: 'USD' (:71); semantic-zoo.object.ts:53 — :60 fixed/USD; detail-shapes.spec.ts:125 — the asserted $100,000 is semantic-zoo.budget, fixed/USD, and renders the symbol at both objectui shas since a fixed read is unconditional either way; field.test.ts:176 「the only mode that reads it」 — two modes, only fixed reads; no assertion changed (the it at :202 and its success:false pin at :198 are untouched). No new false statement.
  • Behaviour: text only. Every hunk is a describe string, TSDoc, comment, JSON evidence/note/verifiedAt string, the changeset or the generated mdx. The only source-level change of form is the re-split string concatenation in analytics.zod.ts, evaluated identical above. field.zod.ts differs from the merge base by that one line. Build Core, Type Check · source gates, Lint & Repo Gates, Check Changeset, Spec property liveness, Build Docs are success at the head; at my last poll 29 success / 5 skipped / 3 in progress (Test Core 1/6, 6/6, Type Check · workspace) / 0 failing.
  • Main drift since the merge base. origin/main moved to 3875ae67; of the 13 files only field.zod.ts changed on main (fix(spec): a currency field's precision is total digits — stop refusing precision: 18 on a fixed-USD field (#20011) #20209, hunks at :29-:33 and :2306-:2330 — the FieldSchema-level precision check removed), not :447; GitHub reports mergeable: true. The changeset's 「the spec's own precision refinement in CurrencyConfigSchema」 stays true after fix(spec): a currency field's precision is total digits — stop refusing precision: 18 on a fixed-USD field (#20011) #20209 (the CurrencyConfigSchema twin at :464 is kept).
  • Own census (scopes packages/spec/src/**, packages/spec/liveness/**, examples/**; base e7f69dbb → head). ADR-0053 within five lines of currenc: 6 → 0, the six lines being exactly sites 5–10; wrapped default-/Currency: 1 → 0; ADR-0053 lines overall 44 → 38, every remaining one a date use; defaultCurrency lines 79 → 86. Sentence by sentence at head: every prose sentence naming defaultCurrency in scope carries the fixed condition or the not-read-under-dynamic negation (field.zod.ts:446/:447/:460-:462/:1060/:1067, analytics.zod.ts:114, contracts/analytics-service.ts:82-:84, percent-scale.ts:17, both ledger rows, the four example/test comments); every other line is code or a fixture; the it title at field.test.ts:202 is a label, correctly flagged for (c). A grep for chain sentences that avoid the key token (「source field」/「tenant default」 near currenc) finds only percent-scale.ts:18, conditioned. Nothing in scope was missed.

② Semver level

'@objectstack/spec': patch is the right level. @objectstack/spec publishes dist, src/**/*.zod.ts and liveness (package.json files), so the describes, the emitted TSDoc, the two .zod.ts sources and both ledger files ship — a changeset is owed and skip-changeset would be wrong (AGENTS.md step 3). Clause-②: no matches the diff (no key, default, accept set, type or export moves), and only yes takes at least minor, so patch is both floor and ceiling. Precedent: #20179's .changeset/20126-currency-mode-describe.md, the same class, patch, merged. The FROM → TO pairs are accurate (both evaluated byte-equal to the old and new source strings). No ADR-0087 entry is owed: a disposition marker is required only on a BREAKING changeset (AGENTS.md:1085), nothing authorable is removed or renamed, and spec-changes.json is untouched. Check Changeset check-run success.

③ Boundary flags

  • Console pin vs. published text (non-blocking, recorded). At .objectui-sha f8a9d0fb the Console's field faces still read defaultCurrency unconditionally (currency.ts:32); the describe, the changeset sentence naming 「objectui's resolveFieldCurrency」 and the field.json row are true of objectui main 25c7d584. Same state as the docs(spec): currencyMode says what each mode does; the currency-chain text states the fixed condition #20179 ACCEPT recorded for the release lane, and the :446 describe already on main makes the identical claim. The PR body cites objectui main only and does not restate the pin fact; the card carries it (5853407641). Not a defect of this PR; it closes when the pin bumps past ea9d17fb6.
  • Family append 5855122068. The released CHANGELOG entries (packages/spec/CHANGELOG.md:28383 / :66160; service-analytics/CHANGELOG.md:1976 (17.0.0) / :8504 (17.0.0-rc.2)) are correctly left out: AGENTS.md:699 makes them release-owned, amended only in a dedicated docs-only PR. One more line of the same kind that the append does not list: packages/services/service-analytics/CHANGELOG.md:10728 (under ## 10.0.0), 「source-field currencyConfig.defaultCurrency → tenant default (ctx.currency)」 — same disposition, pointer for the family. The objectui comment pointers (currency.ts:22/:30, CurrencyField.tsx:52 at 25c7d584) are objectui-lane and correctly not filed here.
  • Part of with skills(objectstack-ui): dashboards.md teaches the measure-currency chain with no currencyMode: 'fixed' condition and cites ADR-0053 — remainder (a) of #20126 #20196 open. REST read this act: skills(objectstack-ui): dashboards.md teaches the measure-currency chain with no currencyMode: 'fixed' condition and cites ADR-0053 — remainder (a) of #20126 #20196 is open, pm:queue, domain:skills. So Part of #20126, no closing keyword (check-run Part-of PR must not also close its card success), and the enumerating pin deferred to remainder (c) per triage 5853734589 is the right shape — (c) covers skills/** and can only go green after (a).
  • For (c)'s pin design, two further pointers from the whole-repo census (minus CHANGELOGs) at head: the remaining ADR-0053-near-currenc lines are skills/objectstack-ui/rules/dashboards.md:308 (= skills(objectstack-ui): dashboards.md teaches the measure-currency chain with no currencyMode: 'fixed' condition and cites ADR-0053 — remainder (a) of #20126 #20196), this PR's own .changeset negation sentences (a count-based rule over .changeset/** would red on them), current-user-endpoints.ts:908 (a 「used to … citing ADR-0053」 negation), localization.manifest.ts:6 (ADR-0053 Phase 2 as the lineage of the localization settings family, which includes currency — lineage, not the chain), and two plugin-email date uses. The scope triage set for (c) excludes all of these except the skills site.
  • Concurrency. All 12 open PRs' file lists were scanned: none touches any of the 13 files. Check-runs No other open PR may claim the same single-writer path / …the same issue success.
  • PR is a draft; mergeable_state: blocked reflected required checks still in progress at read time, not a conflict.

Implemented-by: claude/issue-20126-currency-chain-spec-sites
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 10:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 03d6cb0 Sep 27, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20126-currency-chain-spec-sites branch September 27, 2026 11:17
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…etire

Conflicts resolved by stacking both sides: #20217's defaultCurrency describe kept
beside the removed precision; #20085's view-item conversion and rationale kept
beside currency-config-precision-removed; dropped-refinements totals recomputed.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…(step 4 of the os-regen merge)

Both sides edited the generated CurrencyConfig table: main rewrote the
defaultCurrency row (#20217), this branch removed the precision row.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…laces are its currency's (ADR-0049) (objectstack-ai#20251)

Part of objectstack-ai#19992
Clause-②: no

Retires `currencyConfig.precision` under ADR-0049 enforce-or-remove, in
the direction triage set (`5853208433`: 「**Direction unchanged:**
remove, per ADR-0049 with the ADR-0087 retirement entry, under ruling
乙's principle that a currency's decimal places are the currency's. The
aliases go with the key.」) and ruling 乙 on objectstack-ai#19910 (`5805782503`: 「a
currency's decimal places are the currency's, not a setting」).

objectstack-ai#19992 remains open for its folded family site (`5854612946`): the
FIELD-level `precision` ("Total digits") that nothing reads. That key is
untouched here apart from its form help text; objectui's Studio
inspector writes it, so retire-versus-enforce is still an open choice
for that card.

## What changes for an author

| before (17.4) | after |
| --- | --- |
| `currencyConfig: { precision: 2, currencyMode: 'fixed',
defaultCurrency: 'USD' }` parses | refused at `currencyConfig` as
`unrecognized_keys`, with the prescription below |
| `currencyConfig.decimals` / `currencyConfig.scale` refused with *Did
you mean → `precision`* | refused with the reason, and no rename
suggested |
| `CurrencyConfigSchema.parse({})` →
`{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}` | →
`{"currencyMode":"dynamic","defaultCurrency":"CNY"}` |
| authored `precision` contradicting a fixed currency's ISO 4217 digits
→ `custom` issue at `currencyConfig.precision` | the check is gone with
the key |
| field designer help text for the field-level `precision`: "Decimal
places (e.g., 2 for $10.50)" | "Total digits" (the key's describe; the
object designer's row already said so) |

### Refusal texts, verbatim (rendered from `src` at this head)

`precision`:

```text
Unrecognized key(s) on this currency configuration: `precision`.
  • `currencyConfig.precision` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever read it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute.
```

`decimals` (and `scale`, identical but for the key name):

```text
Unrecognized key(s) on this currency configuration: `decimals`.
  • `currencyConfig.decimals` is not a currency configuration key, and nothing replaces it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute.
```

The trailing sentence is the schema's existing `history`
(`FIELD_HISTORY`), appended by `strictObject`; it is not new text.

## Premises measured (Zone 2)

1. **No reader, anywhere** — held. Instrument: `git grep currencyConfig`
(non-test) plus indirect spellings (a config held in a variable, then
`.precision`), with a lit control.
- objectstack `packages/**` + `examples/**` + `apps/**` at `4df101c3`: 0
readers of `precision`; the control `currencyConfig?.currencyMode` IS
read (`service-analytics/src/plugin.ts:1101`).
- objectui at the pin `f8a9d0fb` and at `main` `25c7d584e`: 0 readers.
`CurrencyField.tsx:82` derives width as
`currencyFractionDigits(currency)` on both; its `:74` comment says it
never read the key. Control: `currencyMode` / `defaultCurrency` reads 7
(pin) / 10 (main).
- cloud `main` `48d7066`: 0 `currencyConfig` mentions; the instrument
fires on that ref (`defaultCurrency` in `connector-stripe`).
2. **Producers.** Authored: three `examples/app-showcase` objects
(`account`, `field-zoo`, `semantic-zoo`), now edited. Studio inspector:
none (objectui's own `ObjectFieldInspector.currencyScale-10221.test.tsx`
pins that it writes the field-level key, never
`currencyConfig.precision`). **Stored rows and built artifacts: nearly
all of them** — the removed `.overwrite()` baked `precision: 2` into
parse output, so every persisted currency config carries it unwritten.
That population decides the route: a D2 conversion replayed at rest, not
a bare refusal.
- What a stored field experiences after upgrade: the stored-row and
artifact seams replay `currency-config-precision-removed`
(`includeRetired`), strip the key, and serve the row canonical; it then
parses. Pinned end to end through
`applyConversionsToStoredItem('object', …)` in
`currency-precision-iso4217.test.ts` — the row as stored is refused by
today's door, the converted row parses, and the field-level `precision:
10` survives.
3. **Aliases** — the schema is a `strictObject`, and its `aliases` are
rejection-with-suggestion, not renames. With the target gone they would
point at a refusal (and `alias-integrity` audits that). Precedent for a
natural spelling with no landing key is `FieldSchema`'s `currency`
guidance, so `decimals` / `scale` get `guidance` entries with the same
reason and no command (no conversion strips them: the closed shape
always refused them, so no stored row carries them). Pinned: code, path,
key, text, and the absence of "Did you mean".
4. **`currencyPrecisionContradiction`** lost its only reader, and
`currencyFractionDigits` was read only by it: both are removed (neither
was exported from a public entry — no barrel row, no `api-surface/`
row). `CURRENCY_FRACTION_DIGITS` stays: `shared/value-domain.zod.ts:160`
reads its key set for the `iso_4217_currency` value domain. The module
docblock now says so and keeps the CLDR provenance.
5. **Docs** — every published sentence that stated the rule is
corrected: `data-modeling/field-types.mdx`, `validation-rules.mdx`,
`fields.mdx`, `getting-started/common-patterns.mdx` (two `os:check`
blocks), `protocol/objectql/types.mdx`, the published skill
`skills/objectstack-data/rules/field-types.md`, and the generated
references (`references/data/field.mdx`, `object.mdx`,
`system/migration.mdx`, `shared/value-domain.mdx`).

## The retirement kit

- Schema: `precision` deleted from `CurrencyConfigSchema`;
`CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCE` carries the three
prescriptions; the `.superRefine` and `.overwrite` are removed. Stale
comments that named the twin (`FieldSchema.precision`, the objectstack-ai#20011 note,
the `.overwrite` precedent notes) now say it is gone.
- ADR-0087: D2 conversion `currency-config-precision-removed` (toMajor
18, `retiredFromLoadPath`, strips the key under every field's
`currencyConfig` on `objects` and `objectExtensions`, 2-notice fixture),
wired into step 18's `conversionIds` with the rationale extended;
`RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision` (one
entry file, regenerated).
- Generated / ledgers: `authorable-surface/data.json` row removed
deliberately (gate (a) tripwire; gate (c) proof 4 — guidance route —
then passes); `authorable-defaults/data.json` loses
`CurrencyConfig:precision = 2` (regenerated);
`dropped-refinements.baseline.json` loses the 13 sites the removed
`.superRefine` produced (`data/CurrencyConfig` itself plus 12
embeddings) — the refinement was removed with its key, the projection
did not learn anything.
- Form + i18n: `field.form.ts` help text; the `en` bundle regenerated,
`zh-CN` / `ja-JP` / `es-ES` set to the object designer row's existing
translations (总位数 / 総桁数 / Total de dígitos).
- `spec-changes.json` / the upgrade guide do not move: neither carries
any protocol-18 entry yet (the precedent `metric-filters-removed` is
absent too), and `check:spec-changes` / `check:upgrade-guide` read up to
date.
- Changeset: `@objectstack/spec` minor + `@objectstack/platform-objects`
patch, `**BREAKING**`, FROM → TO, and `adr-0087: registered
currency-config-precision-removed` (`check:adr-0087-registration`
green).

## Absence half — no tree-scoped text pin, on purpose

`precision` does not leave the tree: it stays the field-level
total-digit count on every numeric field. What is retired is a key in a
position (`fields.NAME.currencyConfig.precision`), which a grep either
matches everywhere or, scoped down, only where its author already knew
to look. The `dashboard-chart-structure-refusal.test.ts` precedent
covers this shape; the rationale block is in the test file. Standing in
its place: `tsc` (the key is off `CurrencyConfig`'s input type — Leg A
below) and the closed parse door (Leg B below).

## Proofs (one-off, nothing left behind)

- **Leg A — reverse verification against the rebuilt `.d.ts`.** `node
scripts/ablation-replace.mjs` put `precision: 2` back into
`account.object.ts` (anchor 1 → 0, blob `58a9af7e` → `700705a8`), then
`tsc --noEmit` on the showcase: exit 1, `account.object.ts(71,25): error
TS2353: Object literal may only specify known properties, and
'precision' does not exist in type '{ currencyMode?: …`. Restored: blob
== HEAD, `git diff HEAD` empty. Control (committed tree): showcase
`typecheck` exit 0.
- **Leg B — ablation of the refusal.** Re-declared `precision` on the
schema (blob `7d36200c` → `86fdbf2d`), then the two pin files: **9
failed | 279 passed** — every retirement pin went red; the alias pins
stayed green (they pin the guidance, which the ablation leaves in place)
and so did the parse-output and conversion pins (independent of the
key's declaration). Restored: blob == HEAD, `git diff HEAD` empty; tree
clean after both legs (0 bytes of `git diff HEAD --stat` + `git status
--porcelain`). Direction observed: red, as expected.

## Verification (head `36819398`; patch round on `71ea994d` below)

- `pnpm --filter @objectstack/spec build` exit 0; `check:generated` exit
0 (15 of 15 up to date, after the one stale `check:docs` was regenerated
with `--fix`).
- `pnpm --filter @objectstack/spec typecheck` exit 0 (includes
`check:test-typecheck`, so the `@ts-expect-error` in the pin file is a
used directive).
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **546 files passed, 16062 tests passed, 2 todo**.
- Consumer suites (contract-face fixture triage):
`@objectstack/example-showcase` typecheck exit 0 (after its dependency
closure built); `@objectstack/platform-objects` typecheck exit 0 and
tests **55 files / 911 passed**; `@objectstack/service-analytics`
`currency-mode-relay.test.ts` + `query-dataset.test.ts` **51 passed**;
`pnpm check:i18n` exit 0 (9 packages in sync, after the gate's own
prerequisite closure).
- Gate union: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **120** commands on this head; all
run, exit codes on disk; `--ran` reconciles **119 run, 1 NOT-MEASURED, 0
UNRUN**.
- `check:skill-examples` (covers the two edited `os:check` blocks):
first refused on its prerequisite (exit 3, `client-react` unbuilt);
after `pnpm --filter '@objectstack/client-react...' build` it exit 0 —
"259 prose examples type-check across 3 surface(s)", 109 files.
- NOT MEASURED: `check:dual-build-cjs-loads` — reason: its prerequisite
is every package's `dist/` (a full `pnpm build`), and this diff changes
no package entry or `exports`.
  - Not run locally: repo-wide `pnpm lint` (CI's run).
- Console Pin Gate premise: objectui at the pin `f8a9d0fb` and at `main`
imports neither removed helper nor `CurrencyConfigParsed`, and writes no
`currencyConfig` literal carrying `precision` (multi-line scan over
every file mentioning `currencyConfig`: 0 hits; control `currencyConfig:
{ … defaultCurrency … }` literals: 5 files at the pin, 13 at `main`).
- The showcase `^...` closure build, the i18n prerequisite closure and
every heavy run went through `scripts/pm/os-verify-lock.sh`.
- Merged `origin/main` (`3cb84d08`) with `scripts/pm/os-regen-merge.sh`:
conflicts stacked both sides (objectstack-ai#20217's `defaultCurrency` describe kept
beside this removal; objectstack-ai#20085's `view-item-owner-hidden-removed` kept
beside the new conversion, in both registries); every sibling entry
measured present after the merge, same counts as `origin/main`.
- **Patch round (merge conflict with objectstack-ai#20223), final head `71ea994d`.**
- Merged `origin/main` twice with `scripts/pm/os-regen-merge.sh`:
`0d3ec471` (merge `1e6f29b2`, regeneration `0b2e3964`), then `e4621867`
(merge `71ea994d`). No rebase, no force-push.
- Both conflicts were in
`packages/spec/dropped-refinements.baseline.json`, which is
hand-maintained by rule (no `gen:` script; `build-schemas.ts` gates it).
Both sides are stacked: objectstack-ai#20223's 12 `inlineColumns.element` sites and
`data/InlineGridColumn` root are kept, this PR's 12 `currencyConfig`
sites and `data/CurrencyConfig` root stay removed, and objectstack-ai#20205's three
automation entries are kept. The header totals were recounted to 210
schemas / 591 sites, and `gen:schema` accepts the ledger (exit 0).
- Interaction with objectstack-ai#20223: no contradiction. Its field/column
currency-scale refusals name neither `currencyConfig` nor `precision`,
and both sides prescribe 「delete the key; decimal places are the
currency's ISO 4217 minor unit」.
  - Readings on `71ea994d` (locked):
- spec build exit 0; `check:generated` exit 0 (15/15); spec typecheck
exit 0;
- touched suites 6 files / 350 passed; full spec `--project local` **549
files / 16138 passed, 2 todo**;
- `check-adr-0087-registration`, `check-changeset-no-major` and
`check:migration-registry` (265 semantic / 215 retired-key / 199
retired-def) all exit 0.
- The derived gate list is byte-identical to the one reconciled at
`36819398`.
    - CI on `71ea994d`: 35 check runs, 33 success, 2 skipped, 0 failing.

## Scope notes for the reviewer

- **Tier H.** `skills/objectstack-data/rules/field-types.md` is on this
diff because it taught the retired key in its field-type table and code
sample; leaving it would ship a published skill that teaches a refused
key. The edit is a pure removal: file 328 → 327 lines;
`objectstack-data` markdown 3717 → 3716; all `SKILL.md` files 4402 →
4402. It makes this PR Tier H.
- Outside the claim's listed surface, each because this change would
otherwise leave it false: the `value-domain.zod.ts` docblock (cited the
removed function), one comment in `conversions/registry.ts` (named the
removed key's bounds), the three showcase objects, `fields.mdx` /
`common-patterns.mdx` / `types.mdx`, the platform-objects translation
bundles, and the generated rows above.
- `packages/spec/liveness/field.json` is **not** edited. The
`currencyConfig` row covers the container, asserts nothing about
`precision`, and stays `live` (`defaultCurrency` is read). The container
is in `undrilled-containers.baseline.json`, so no per-key row existed to
remove. objectstack-ai#20217 had just re-cited that row.

## Acceptance notes

- `docs/qa/platform-checklist/areas/records-forms.json` item
`records-forms.field-type-constraints` still describes `f_currency` as
`scale 2 currencyConfig{precision 2}`. Both halves are stale now: the
`scale` half since an earlier card retired it from currency, the
`precision` half since this one. Its SCALE gap probe targets a key the
currency type refuses. Internal QA prose, not published. carrier: the
next checklist-author sweep; 承接者:无.
- Standalone `field` metadata rows are outside every field conversion's
reach (the stored-row seam has no `fields` stack collection), this one
included, the same as its precedents. Observation only; no producer of
such rows was measured.
- Two semantic ledger entries of earlier protocol-18 migrations
(`18.field-scale-precision-integer-refused`,
`18.ui-form-field-precision-scale-integer-refused`) say
`CurrencyConfigSchema.precision` "is a different surface". That is a
scope remark about those migrations, historically accurate, and left as
recorded.

## 维护者速读(草稿)

**改了什么**:货币字段配置里的
`currencyConfig.precision`(「小数位」)被删除。现在写这个键会在保存/发布时被拒绝,并提示直接删掉;它的两个近义写法
`decimals`、`scale`
也给出同样的说明。已经存进数据库或打进构建产物里的旧配置,读取时会自动去掉这个键,不会报错。字段设计器里「精度」一栏的提示文字从「小数位数」改成「总位数」。

**为什么改**:这个键从来没有任何界面或运行时读取过——金额显示几位小数一直由币种本身决定(美元 2 位、日元 0 位、科威特第纳尔 3
位)。作者(包括 AI)以为设了小数位,实际什么都没发生。裁定乙已定原则「币种的小数位属于币种,不是一个设置项」,分诊定了删除方向。

**风险与代价(含回滚)**:这是破坏性收窄:仍在源码里写这个键的应用,升级后会在发布时报错,需要删掉这个键(`os migrate meta
--from 17` 会列出要改的地方)。已存数据不受影响(读取时自动转换)。回滚:revert 本 PR 即可,已存数据没有被改写。本 PR
改到了对外发布的技能包 `skills/`,因此属于 Tier H,需要您批准才能合并。

**席位意见**:

**你要做的**:审阅后在本 PR 上给出批准(APPROVED review),或告诉席位把 `skills/` 的改动拆成单独的 PR。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…tstack-ai#20126) (objectstack-ai#20415)

Fixes objectstack-ai#20126
Clause-②: no

Remainder (c) of the currency-mode class-closure card: the family's one
enumerating pin. Remainder (b) landed in PR objectstack-ai#20217, the skills half
(objectstack-ai#20196) in PR objectstack-ai#20256, and the runtime member (objectstack-ai#20091) in PR objectstack-ai#20119, so
this test-only PR closes the card.

## What the pin is

One new test in spec's `repo` vitest project:
`packages/spec/src/data/currency-mode-family-closure.pin.test.ts`. The
maintainer's ruling on objectstack-ai#20088 asked for one enumerating pin in the
owning package and ⛔ no new `check-*` gate. `packages/spec` owns
`CurrencyConfigSchema`, and its repo project already walks `skills/**`
and `content/**`. The pin enumerates the corpus, not the sites. A new
site anywhere in the radius reddens it, as well as a regression of an
old one.

- **Rule A.** Every `defaultCurrency` in `skills/**/*.md` and
`content/docs/**/*.mdx` sits in a unit that carries `fixed`, or that
says it is not the displayed currency (or not read) under `dynamic`.
- **Rule B.** The date record's number within five physical lines of
`currenc` (any case) counts 0. The matcher reads `ADR-0053`, `ADR 0053`
and the `adr/0053-` file link. The roots are `skills/**`,
`content/docs/**` and `packages/services/service-analytics/src/**`. Per
the family's appended sites 5-10 (ACCEPT `5853407641`, triage
`5853734589`), they also include `packages/spec/src/**`,
`packages/spec/liveness/**` and `examples/**`: non-code files anywhere,
plus `.ts` under each app's `src/`.
- **Runtime member.** It is objectstack-ai#20091's own pin,
`packages/services/service-analytics/src/__tests__/currency-mode-relay.test.ts`,
on `main` (`git merge-base --is-ancestor 6780e34 HEAD` exit 0). The
closure pin only checks that this file still stands, and ⛔ does not
re-assert the resolver.
- **Site table.** Each family site (1-10, the three pages objectstack-ai#20090 landed,
and 11-14) is mapped to the leg that guards it. The test asserts each
file's membership in each corpus, so if a site moves out of the radius,
the pin goes red.

## Outside the corpus, and why

- **Released entries.** This covers every `CHANGELOG.md` and
`content/docs/releases/**`. The seat ruled sites 11-14 (the released
17.0.0 entries) accepted history, since a released entry records what
shipped (claim `5866047669`). The two package CHANGELOGs sit outside
every root by construction. `examples/*/CHANGELOG.md` is inside a root
and is skipped by name. The test asserts both exclusions, with a live
control proving the by-name skip has something to skip.
- **The pin's own file.** It quotes both rules and their fabricated
controls.

## The three design facts (`5855122068`), each answered in the pin's
header

1. **Test titles.** Rule A reads published prose only (`.md` / `.mdx`),
so no test title is judged by it. The `field.test.ts` title stays
outside by corpus, not by exemption. Rule B does read test files inside
its roots.
2. **Negation versus citation.** Rule B does not try to tell them apart:
a negation within the window counts too. Currency text has no reason to
name the date record; the pin's message says to cite ADR-0104.
3. **Wrapped keys.** Both rules read logical text. A key hyphen-wrapped
at a line end (`default-` / `Currency`, `ADR-` / `0053`) rejoins after
the next line's comment prefix is stripped.

**Unit for rule A: not the line.** A strict line reading reds on 5
correct, accepted lines in
`skills/objectstack-data/rules/field-types.md` (ACCEPT `5853407641`'s
pin note). Instead, the unit is:
- in prose, the sentence across wrapped lines;
- in a table, the row;
- in a fenced comment, the sentence of its comment run;
- in fenced code, the innermost object literal holding the key, or the
sibling mapping in YAML.

So one `fixed` field in a fence never vouches for a neighbour's bare
`defaultCurrency`. There is one shape exemption: a parenthesised key
list that also names `currencyMode`, `(currencyMode, defaultCurrency)`.
It names keys and teaches no reading; this is `field-types.md:55`.

## Measured on this branch's base `dcd3bceaa` (assumption 1 holds)

- **Rule A:** 445 files in the corpus. 34 `defaultCurrency` occurrences
in 11 files; 0 failing, 1 of them the key-list exemption.
- **Rule B:** 2550 files in the corpus, 0 hits. 65 date-record lines
remain, all outside the window (date uses).
- **No live text site:** no text site was edited in this PR.

## Declared radius (AGENTS.md: a test that reads outside its package is
spelled so the gate can see it)

Rule B's example-code leg is new radius for
`@objectstack/spec#test:repo`. It is declared in two places:
- `scripts/cross-package-test-inputs.mjs` gets `examples/*/src/**/*.ts`;
- `turbo.json` gets the matching `$TURBO_ROOT$/...` input.

The glob is deliberately ⛔ not `examples/**/*.ts`. That glob covers the
CRM example's smoke test, which is the dispatch-gates self-test's
outside-packages specimen. The declaration comment states that the glob
reaches the two translation tests inside the todo app's `src/`. The
pin's site table quotes the showcase dataset's path, which holds the
glob on the roster. `check:cross-package-test-inputs` and
`check:ci-filter-parity` are both green.

## Verification

All readings are at head `7fc9f86b`, the branch's only commit; every
build and test ran through `scripts/pm/os-verify-lock.sh` (`VERDICT
command-exit 0` each).

- **The pin alone:** `pnpm --filter @objectstack/spec exec vitest run
--project repo --maxWorkers=2
src/data/currency-mode-family-closure.pin.test.ts` → `Test Files 1
passed (1)` / `Tests 6 passed (6)`.
- **Ablation, one leg per rule** (from the committed state,
`scripts/ablation-replace.mjs` in wrap mode, under the lock). The
expected direction was red, and both legs went red:
- Leg A drops ` (only under currencyMode: 'fixed')` from
`skills/objectstack-ui/rules/dashboards.md`, site 2's regression. Anchor
1 → 0, blob `570341f3` → `8e0c44f3`. Rule A goes red with exactly
`skills/objectstack-ui/rules/dashboards.md:307`: `Tests 1 failed / 5
passed`. Restored: blob equals HEAD `570341f3`, and `git diff HEAD` is
empty.
- Leg B inserts ` (ADR-0053)` after the key in
`examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts`, site
10's regression and the newly declared example-code radius. Anchor 1 →
0, blob `31348e15` → `1ca43894`. Rule B goes red with exactly
`examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts:47`:
`Tests 1 failed / 5 passed`. Restored: blob equals HEAD `31348e15`.
- The first leg-B attempt was a no-op and is not a reading: its
replacement embedded its own anchor, and the tool refused (anchor 1 → 1)
and restored before any test ran. It was re-run with an anchor the
replacement does not contain.
- Restored leg: `Tests 6 passed (6)`. `git diff HEAD` is 0 bytes and
`git status --porcelain` is empty.
- **spec, both projects:**
- `vitest run --project repo`: `Test Files 36 passed (36)` / `Tests 640
passed (640)`.
- `vitest run --project local`: `Test Files 561 passed (561)` / `Tests
16517 passed | 1 todo (16518)`.
- `pnpm --filter @objectstack/spec run typecheck` exits 0, and
`check:test-typecheck` is OK. `tsc -p tsconfig.test.json
--listFilesOnly` lists the new file, so the typecheck really reads it.
- **Builds:** `pnpm --filter @objectstack/spec build` exits 0. `turbo
run build --filter='./packages/*' --filter='./packages/*/*'
--concurrency=2` gives `Tasks: 71 successful, 71 total`.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 96 commands over the 4 changed
paths, and every one was run. `--ran` gives `96 derived, 96 run, 0
NOT-MEASURED, 0 UNRUN`. On the first pass, four gates exited 3 because
their prerequisite packages were not built. They exited 0 after the full
build: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt`.
- **Readers of the edited declaration module:** each exits 0.
- `node scripts/pm/dispatch-gates.mjs --self-test`: 1976 cases,
including "no hint of this gate reaches a test file outside
packages/**".
  - `node scripts/check-ci-filter-parity.mjs --self-test`.
  - `node scripts/partition-test-shards.mjs --self-test`.
  - `node scripts/check-published-list-mirrors.mjs`.
  - `node scripts/check-examples-live-imports.mjs`.
- **Lint, narrowed** (CI owns `pnpm lint`):
- `eslint --no-inline-config --format json` on the two lintable changed
files gives 2 files / 0 errors / 0 warnings; `--print-config` gives them
4 and 2 active rules.
- `turbo.json` and `vitest.repo-tests.json` answer "File ignored because
no matching configuration", so they are outside eslint's population.
- `eslint.config.mjs` sets no `parserOptions.project`, so there is no
type-aware linting, and no untouched file's verdict can move.
- **Ships nothing:** spec's `files[]` is `dist`, `json-schema`,
`liveness`, `src/**/*.zod.ts` and so on. A grep of the published paths
for the pin finds 0 files; the control grep for `CurrencyConfigSchema`
in `dist` finds 30. So the diff takes `skip-changeset`.
- **Base:** the branch is one commit behind `origin/main` (`2c310705`).
That commit adds two `packages/spec/src/data` files with no date-record
token, so rule B cannot move on the merge. Not merged here.

## Acceptance notes (not filed)

- `.changeset/*.md` is outside the corpus. It is where released
CHANGELOG text originates, so a pending changeset citing the date record
for currency would not redden this pin before release. Carrier: none;
noted only.
- The `user selectable` wording that site 1 corrected is not a rule of
the card's pin definition, so it is not guarded here.
- Example tests (`examples/*/test/**`, `examples/*/e2e/**`) are outside
rule B's example-code leg, which follows the declared radius above.
- `node scripts/pm/measurement-claim-triage.mjs --self-test` (run as a
reader of the edited module) is red on the base, naming only
`scripts/check-dts-references.mjs:74` (UNTRIAGED), a file this PR does
not touch. No workflow or package script invokes it. Carrier: none.
- The objectui pointer in `5855122068` (objectui
`packages/i18n/src/currency.ts`, `CurrencyField.tsx`) is another
repository and is not read here.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants