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:
|
…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
… enforced once at the shared compile face for all five drivers (objectstack-ai#7872) (objectstack-ai#8234) * feat(spec): comparand-type door — accepted literal comparand set enforced at the shared compile face (objectstack-ai#7872) The measured superset string | number | bigint | boolean | null | Date is defined in packages/spec (filter-comparand-type.ts), enforced by parseFilterAST on everything it returns and by the engine's lowering seam on the object form; everything else is refused with the INVALID_FILTER / 400 envelope naming the accepted set. bigint is narrowed copy-on-write to its exact number (refused beyond 2^53), which kills the driver-memory mingo crash cell without touching the frozen driver; the mongo silent-edit cell ({qty: undefined} wiring to {} = match everything) becomes a refusal at the door. SQL-family allowlists source their set membership and wording from the door instead of local copies. FILTER_COMPARAND_TYPE_CASES conformance table added and consumed by all five driver suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * chore(spec): regenerate api-surface + export-origins for the comparand-type door exports (objectstack-ai#7872) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * test(objectql): re-judge the deep-equality operator-map pin under the objectstack-ai#7872 door Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * chore(spec): regenerate api-surface + export-origins from the merged tree (objectstack-ai#7872 relay lap over objectstack-ai#8230/objectstack-ai#8232) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * test(objectql): pay the two farm reds — registerObject arity, and un-erased engine option bags (objectstack-ai#7872) The door suite's registerObject(deal) call was missing the required packageId (the +1 over objectql's frozen TEST_DEBT 355); and its option bags used bare 'as any', which the query-options-erasure ratchet counts (240 -> 245). The bags now type-check uncast — FilterCondition's index signature admits the deliberately off-contract comparands, which is the point of the door — except the one FilterArray case, which carries the sanctioned 'as unknown as EngineQueryOptions' naming the contract it bypasses (not counted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --------- Co-authored-by: Claude <noreply@anthropic.com>
…ger — a boot refusal never reaches a wire envelope (objectstack-ai#8035) (objectstack-ai#8239) * fix(spec): MONGODB_MULTI_TENANT_UNSUPPORTED leaves the error-code ledger (objectstack-ai#8035) The code was registered under @objectstack/driver-mongodb (objectstack-ai#3724) but no response envelope can carry it: the CLI boot handler rethrows the tenancy refusal pre-HTTP and aborts, and the one request-reachable trigger (assertObjectsNotTenantScoped via syncObjectSchema inside the metadata protocol's ensureObjectStorage) is swallowed by a documented best-effort catch. Registered-but-unemittable is the class the ledger header names; precedent OVERLAY_PERSISTENCE_FAILED / objectstack-ai#5783 — this instance is the sister shape (never had a wire path, rather than lost it), now documented in the header. The boot refusal stays untouched: MULTI_TENANT_UNSUPPORTED_CODE, the error class, its message, and the CLI's duck-typed literal match. A new driver-mongodb test pins the code literal (with the ledger row gone it is the only cross-package guard on the literal serve.ts matches); the ledger test now asserts the wire vocabulary refuses the retired string. Reference docs regenerated (ErrorCode enum 264 -> 263). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * docs(spec): regenerate references from the merged tree (objectstack-ai#8035) 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: objectstack-ai#8230 retirement state (def names only in retirement prose + migration entry), objectstack-ai#8232 currency fraction-digit guard, objectstack-ai#8234's 13 comparand-door exports in api-surface/data.json, objectstack-ai#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 --------- Co-authored-by: Claude <noreply@anthropic.com>
…ered/virtualScroll (ADR-0049, objectstack-ai#7176) (objectstack-ai#8252) * refactor(spec)!: retire pass-through-only list-view keys striped/bordered/virtualScroll (ADR-0049, objectstack-ai#7176) Maintainer ruling 2026-08-10: every measured reader copies the key forward and ObjectGrid, where the chains end, never applies any of the three — copy-without-apply is dead in effect. retiredKey() tombstones on ListViewSchema (radiating into ObjectListViewSchema), ADR-0087 D2 conversion view-list-passthrough-keys-removed + D3 chain step + six RETIRED_KEYS_BY_MAJOR[17] entries, ledger rows flipped to dead with the retirement note, form inputs pruned, docs updated, changeset with FROM->TO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * test(spec): match the striped prescription's actual casing in the pin test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r * merge origin/main (os-regen artifacts taken from main; regeneration follows) * chore(spec): regenerate artifacts after merging origin/main (4dc1c7d relay: objectstack-ai#8230/objectstack-ai#8232/objectstack-ai#8234/objectstack-ai#8236/objectstack-ai#8239) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r --------- Co-authored-by: Claude <noreply@anthropic.com>
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