Uh oh!
There was an error while loading. Please reload this page.
feat(spec): reject a declared currency precision contradicting the currency's ISO 4217 fraction digits (#7918) - #8232
Conversation
…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
…rency-precision-iso4217
…ion optionality (#7918) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…rency-precision-iso4217
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
…tree (#7872 relay lap over #8230/#8232) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
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
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
precisionthat contradicts the currency's ISO 4217 / CLDR fraction digits, when the currency is statically known.What changed
Two anchors, one shared verdict (
currencyPrecisionContradictionin the newpackages/spec/src/data/currency-fraction-digits.ts):CurrencyConfigSchema.precision— checked in asuperRefinebefore 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 seesundefinedfor 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.groupsis declared as "alias to sections" but nothing folds it — every consumer reads onlysections#6926 precedent inview.zod.ts(a pipe has no.extendand breaks shape introspection); its one accepted cost, same as there:CurrencyConfigParsednow infersprecision?at the type level while the runtime value is always a number (ADR-0122 forbids hand-narrowing the alias).precision— checked inFieldSchema's existingsuperRefinechain (the same anchoring as the siblingstorage.notNull × requiredWhenrule, 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-levelcurrency, …)": the spec's strictFieldSchemadeclares no field-levelcurrencykey — 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-modecurrencyConfig, and both precision keys are judged against it. In authored-fixedmode the check also fires whendefaultCurrencyitself 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"):
dynamiccurrencyMode has no single currency to check against and is untouched; codes outside CLDRcurrencyData(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: 2contradicts it — …", plus the prescription (declare the currency's own count, or omitprecision).Alias funnel:
CurrencyConfigSchema'sdecimals/scalealiases are strict-rejection-with-suggestion (not renames), so an alias spelling cannot carry a contradicting width past the check — the author is pointed atprecision, 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 viaIntl.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
currencyConfigdeclarations repo-wide (examples/app-showcase: field-zoo, semantic-zoo, account), allfixed+USD+precision: 2— agreeing. Zero contradictions to fix; no field-levelprecisionon 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-majoralso refuses majors); the changeset argues the same and records the one type-level visible change.Verification
@objectstack/spec: fullpnpm test— 386 files / 10216 tests passed;pnpm typecheckgreen;check:generated— all 13 artifacts up to date after regenerating the one it proved stale (content/docs/references/**, a 1-line optionality rendering forcurrencyConfig.precision; rides in this PR). Re-verified after the finalorigin/mainmerge (fa48973 side: speccontracts/*moved — rebuilt, all 13 still green).custom, pathprecision/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).const n: number = cfg.precisionagainst the rebuilt dist.d.ts→ red (TS2322) exactly as predicted,number | undefinedcontrol 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 itscurrencyConfig.precisionissue 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 viagit checkout.@objectstack/metadata-protocol75 files / 1094 tests passed;@objectstack/platform-objects16 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 validategreen in all three example apps (showcase / crm / todo).Docs:
content/docs/data-modeling/field-types.mdxcurrency section states the rule, its partiality, and the authored-only firing.Generated by Claude Code