Skip to content

feat(spec): reject a declared currency precision contradicting the currency's ISO 4217 fraction digits (#7918) - #8232

Merged
huangyiirene merged 4 commits into
mainfrom
claude/issue-7918-currency-precision-iso4217
Aug 12, 2026
Merged

feat(spec): reject a declared currency precision contradicting the currency's ISO 4217 fraction digits (#7918)#8232
huangyiirene merged 4 commits into
mainfrom
claude/issue-7918-currency-precision-iso4217

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#7918

Implements the maintainer's Option-A ruling (issue comment, 2026-08-12, provenance 「7918 A,7917 ②,7900 收敛两扇门,7929 来源标记」): publish-time validation rejects a declared currency precision that contradicts the currency's ISO 4217 / CLDR fraction digits, when the currency is statically known.

What changed

Two anchors, one shared verdict (currencyPrecisionContradiction in the new packages/spec/src/data/currency-fraction-digits.ts):

  1. CurrencyConfigSchema.precision — checked in a superRefinebefore the default materializes. The ruling's design trap governs the shape: the property carried .default(2), which bakes in at parse, so authored-vs-defaulted is indistinguishable post-parse. Measured anchor choice: the .default(2) moved off the property into a post-check .overwrite(), so the refinement sees undefined for an unauthored precision (authored-only firing), and the parse OUTPUT stays byte-identical to the .default(2) era — measured on origin/main (37b82ed) before the change and pinned in tests: {"precision":2,"currencyMode":"fixed","defaultCurrency":"JPY"} for an untouched fixed-JPY config, same shape order, same materialized default. .overwrite() rather than .transform() per the measured finding: FormViewSchema.groups is declared as "alias to sections" but nothing folds it — every consumer reads only sections #6926 precedent in view.zod.ts (a pipe has no .extend and breaks shape introspection); its one accepted cost, same as there: CurrencyConfigParsed now infers precision? at the type level while the runtime value is always a number (ADR-0122 forbids hand-narrowing the alias).
  2. Field-level precision — checked in FieldSchema's existing superRefine chain (the same anchoring as the sibling storage.notNull × requiredWhen rule, so every publish door inherits it). This key has no schema default, so authored-vs-absent needed no relocation.

Statically known means currencyConfig.currencyMode: 'fixed'. One premise refinement against the ruling's parenthetical "(field-level currency, …)": the spec's strict FieldSchema declares no field-level currency key — that spelling is an objectui-side read of looser column configs, and writing it on a spec field is already an unrecognized-key rejection (filed the missing-guidance gap as #8163). The only statically-known-currency form the spec can express is fixed-mode currencyConfig, and both precision keys are judged against it. In authored-fixed mode the check also fires when defaultCurrency itself was defaulted (CNY): the fixed currency is statically known either way, and the precision was authored — pinned in tests.

Deliberately partial, by design (do not "improve"):dynamic currencyMode has no single currency to check against and is untouched; codes outside CLDR currencyData (crypto/custom, e.g. the existing BTC-precision-8 test) fail open — refusing unknown codes is a different rule nobody ruled.

Error message names both numbers, per the ruling: "currency JPY has 0 fraction digits; precision: 2 contradicts it — …", plus the prescription (declare the currency's own count, or omit precision).

Alias funnel:CurrencyConfigSchema's decimals/scale aliases are strict-rejection-with-suggestion (not renames), so an alias spelling cannot carry a contradicting width past the check — the author is pointed at precision, where the check waits. Pinned.

The digit table

Static, checked-in snapshot of CLDR 48.0 currencyData (ICU 78.2, node v22.22.2 full-icu), 162 codes, generated locale-free via Intl.NumberFormat(...).resolvedOptions().maximumFractionDigits — provenance and regeneration snippet in the module header. Checked-in rather than probed at validation time so the verdict is deterministic (a small-icu host answers 2 for everything) and the validation path takes no Intl dependency. Renderers keep deriving from live Intl (objectui#4361); both read CLDR, so they agree. The card's measured anchors (0: JPY/KRW/CLP/ISK/VND; 2: USD/EUR/CNY/GBP; 3: KWD/BHD/OMR/TND) are pinned against the table.

In-repo census (PM assumption #3)

Three currencyConfig declarations repo-wide (examples/app-showcase: field-zoo, semantic-zoo, account), all fixed + USD + precision: 2 — agreeing. Zero contradictions to fix; no field-level precision on any in-repo currency field. Census clean, as expected.

Bump: minor, per the #3746 precedent

Acceptance narrowing in the #3746 shape (strict-closing family, shipped as Minor Changes in the 17.0.0-rc line): every newly-rejected input is a contradiction no consumer could honor — renderers already derive the width from the currency when precision is absent, and the rejected combinations rendered money with digits the currency does not have. Not major (check:changeset-no-major also refuses majors); the changeset argues the same and records the one type-level visible change.

Verification

  • @objectstack/spec: full pnpm test — 386 files / 10216 tests passed; pnpm typecheck green; check:generatedall 13 artifacts up to date after regenerating the one it proved stale (content/docs/references/**, a 1-line optionality rendering for currencyConfig.precision; rides in this PR). Re-verified after the final origin/main merge (fa48973 side: spec contracts/* moved — rebuilt, all 13 still green).
  • Both directions pinned (21 new tests): JPY+2 and KWD+2 refused at both anchors (issue code custom, path precision / currencyConfig.precision, message naming both digit counts); USD+2, JPY+0, KWD+3, absent-precision, dynamic-mode (authored and defaulted), unknown-code (BTC+8) accepted — the agreeing combos byte-identically (stringified parse outputs measured on origin/main 37b82ed before the change, pinned as literals).
  • Reverse verification, directions declared before running. A (type-level): const n: number = cfg.precision against the rebuilt dist .d.ts → red (TS2322) exactly as predicted, number | undefined control clean — proves the declaration change is real and rebuilt, not cached. B (anchor choice): temporarily restoring .default(2) on the property predicted 3 reds; observed 4 — the three predicted (noisy-shape guard, its byte-identity row, the field-level agreeing-acceptance test) plus the field-level JPY+2 rejection test, which failed on issue ORDER: the config-level check over-fires on the baked default and its currencyConfig.precision issue displaces the field-level issue as first. One more red than predicted, same mechanism — the baked-default over-firing shape the relocation exists to prevent, proven live from the committed state, then restored via git checkout.
  • Downstream (consumer direction): @objectstack/metadata-protocol 75 files / 1094 tests passed; @objectstack/platform-objects 16 files / 347 tests passed (both after building their dependency closures — the first lap's exit-1 was the fresh-worktree unbuilt-closure false red, not a regression); objectstack validate green in all three example apps (showcase / crm / todo).
  • Spec gate farm locally, all green: adr-anchors, changeset-gate-self-tests, cross-package-test-inputs, doc-formula-expressions, docs-audit-scope, i18n + i18n-coverage (12/12 configs measured), merge-driver, release-body, spec-parsed-alias, nul-bytes.

Docs: content/docs/data-modeling/field-types.mdx currency section states the rule, its partiality, and the authored-only firing.


Generated by Claude Code

…rrency's ISO 4217 fraction digits (#7918)
Maintainer ruling 2026-08-12, Option A. Two anchors, one shared verdict:
the field-level precision key (no default — checked in FieldSchema's
superRefine) and CurrencyConfigSchema.precision (default(2) relocated into
a post-check .overwrite() so the superRefine sees authored-vs-absent;
parse output stays byte-identical). Fires only when the currency is
statically known (currencyConfig fixed mode); dynamic mode and non-CLDR
codes fail open by design. Digit table is a checked-in CLDR 48.0 snapshot
with provenance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…ion optionality (#7918)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 9:31pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 12, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review August 12, 2026 22:18
@huangyiirene
huangyiirene added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit e5bd2f6Aug 12, 2026
27 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-7918-currency-precision-iso4217 branch August 12, 2026 22:35
huangyiirene pushed a commit that referenced this pull request Aug 12, 2026
…tree (#7872 relay lap over #8230/#8232)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
huangyiirene pushed a commit that referenced this pull request Aug 13, 2026
os-regen merge lap for landing: merge origin/main (committed first, per
scripts/pm/os-regen-merge.sh), then wholesale regen of the os-regen'd
references/api pages from the merged tree. Sibling assertions on the
regenerated tree: #8230 retirement state (def names only in retirement
prose + migration entry), #8232 currency fraction-digit guard, #8234's
13 comparand-door exports in api-surface/data.json, #8236's groupBy
union row all survive; MONGODB_MULTI_TENANT_UNSUPPORTED stays absent
from the ledger and the references ErrorCode union reads main-minus-one
(+265 -> +264; the union grew by 2 on main since the previous lap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
huangyiirene pushed a commit that referenced this pull request Aug 13, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Contract question: should publish-time validation reject a declared currency precision that contradicts the currency's ISO 4217 digits?

2 participants

@huangyiirene@claude