Contract question: should publish-time validation reject a declared currency precision that contradicts the currency's ISO 4217 digits? #7918
Description
Activity
Triage: lands in
packages/spec/src/data/field.zod.ts+ the publish-time validation path; rationale: adding a rejection rule changes the accept/reject behavior of the authorable face →domain:spec(acceptance-change red line), and a NEW publish-time rejection on a public contract is a protocol change → human floor, soneeds-user-decisionrather than auto-adjudication.Four-lens block (①–④ per #7498):
- ① Platform long-term coherence — today the
precision-vs-ISO-4217 contradiction is undefined rather than chosen; option A closes it where the currency is statically known (necessarily partial:dynamiccurrencyMode has no single currency to check). Option B fossilizes "the author may contradict ISO 4217" as contract-by-silence. A shrinks the undefined surface. - ② Measured business pull — the pull is authoring correctness, not capability: no business case was found for showing minor-unit digits a currency does not have (JPY
precision: 2), and the card came out of a real rendering defect (objectui#4361). Zero measured demand for B's override semantics. - ③ AI-agent error-resistance — strongest lens, favors A: an AI author writing
precision: 2next tocurrency: JPYgets told loudly once at publish time, versus silently wrong money in production (KWD silently drops a real fils digit). C (warn) mostly means unread warnings in an AI pipeline. - ④ Startup scope discipline — A is one narrow validation rule; B is a documented override capability nobody asked for, maintained forever.
All four lenses point at A (reject when statically known, message naming both digit counts) — matching the filer's recommendation. Withheld from the auto-adjudication lane only because it is a protocol/accept-set change (human floor class); this is a one-word confirmation for the maintainer. Nothing downstream is blocked: objectui#4361's renderer ruling stands under either answer.
Size/model suggestion once ruled: S–M,
mode:subagentacceptable, model: sonnet–opus.
Generated by Claude Code
- ① Platform long-term coherence — today the
Maintainer ruling — 2026-08-12
Ruled: Option A — publish-time validation rejects a declared
precisionthat contradicts the currency's ISO 4217 fraction digits, when the currency is statically known.Provenance: maintainer, triage PM session chat (
session_01NKGoRBFZELVKivDkiAswdz), 2026-08-12, verbatim: 「7918 A,7917 ②,7900 收敛两扇门,7929 来源标记」. Recorded by the triage seat;needs-user-decision→pm:queueswapped in the same write.Implementation boundaries, per the card as ruled:
- Rule fires only when the currency is statically known (field-level
currency, orcurrencyConfiginfixedmode);dynamiccurrencyMode is out of reach by design — the rule is deliberately partial. - Error message names both numbers ("currency JPY has 0 fraction digits;
precision: 2contradicts it"). - Mind the card's measured trap:
CurrencyConfigSchema.precisionmaterializes a default2at parse time — the check must distinguish authored-vs-defaulted there, or scope itself to where the distinction exists (the field-level key has no default). Thedecimals/scalealiases funnel into the same check. - Rejection tests carry
code+status(or the publish-validation equivalent) per the rejection-case minimum; docs paragraph states the rule. - objectui behavior is unaffected (its renderer ruling stands; the contradiction now simply cannot reach it).
Generated by Claude Code
- Rule fires only when the currency is statically known (field-level
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsClaim: PM loop round 2 (spec seat, seat post #6017; maintainer acceleration directive in force)
Session:session_0123k4cam2jEAkPmbJeoaY3r
Branch:claude/issue-7918-currency-precision-iso4217
Worktree:objectstack-issue-7918
Domain:domain:spec
File surface:packages/spec/src/data/field.zod.ts(currency field +CurrencyConfigSchemaregion), its tests, a docs paragraph stating the rule, regeneratedreferences/**/baselines ride. (stop on breach; explain in the report)
Container & model: S–M,mode:subagent,model: fable(mandatory clause — accept/reject change; triage's sonnet–opus suggestion not adopted for that reason)
Serial constraints cleared: in-flight claims holdcontracts/security-service.test.ts(#7831),data/external-lookup.zod.ts+system/message-queue.zod.ts(#8075 phase 2), the filter face + driver allowlists (#7872) — all disjoint fromfield.zod.ts. No open PR touches it.Dispatching against the maintainer's Option-A ruling on this thread (comment 5264125285, provenance 「7918 A…」): reject a declared
precisioncontradicting the currency's ISO 4217 fraction digits, only where the currency is statically known; message names both numbers; theCurrencyConfigSchemamaterialized-default trap and thedecimals/scalealias funnel are in the ruling's implementation boundaries.
Generated by Claude Code
- added a commit that references this issue
on Aug 12, 2026 huangyiirene commented
on Aug 12, 2026 CollaboratorMore actions{ "issue": 7918, "status": "done", "branch": "claude/issue-7918-currency-precision-iso4217", "pr": "https://github.com/objectstack-ai/objectstack/pull/8232", "premise_still_valid": true, "summary": "Option A implemented at both anchors with one shared verdict: FieldSchema-level `precision` (no default, checked in the existing superRefine chain) and CurrencyConfigSchema.precision, whose `.default(2)` was relocated into a post-check `.overwrite()` (per the #6926 precedent) so the refinement sees authored-vs-absent — authored-only firing, parse output byte-identical to before (pinned against origin/main 37b82ed5b literals). Deliberately partial per the ruling: fires only under currencyMode 'fixed'; dynamic mode and non-CLDR codes (BTC) fail open. Digit source is a checked-in CLDR 48.0 currencyData snapshot (162 codes, provenance + regen snippet in packages/spec/src/data/currency-fraction-digits.ts) — deterministic, no Intl in the validation path. One premise refinement: the ruling's parenthetical 'field-level `currency`' key does not exist in the spec's strict FieldSchema (it is an objectui-side read of looser configs; already an unrecognized-key rejection), so statically-known = fixed-mode currencyConfig — both precision keys are judged against it, including when defaultCurrency itself is defaulted (CNY). Docs paragraph added (data-modeling/field-types.mdx); changeset @objectstack/spec minor argued per #3746 in the PR body; in-repo census clean (3 currencyConfig declarations, all USD+2 agreeing, zero fixes needed). CI on the draft PR was in_progress at report time per the report-at-draft-PR contract.", "tests": "packages/spec full suite: 386 files / 10216 tests passed; typecheck green; check:generated 'All 13 generated artifacts are up to date' (re-verified after merging origin/main fa4897335, which moved spec contracts/*). 21 new tests in currency-precision-iso4217.test.ts: JPY+2 and KWD+2 refused at both anchors (code 'custom', paths ['precision'] / ['currencyConfig','precision'], message contains 'currency JPY has 0 fraction digits' + '`precision: 2` contradicts it'); USD+2 / JPY+0 / KWD+3 / absent-precision / dynamic / BTC+8 accepted, agreeing combos byte-identical to pre-change stringified outputs; decimals/scale alias funnel pinned as strict rejection naming 'precision'. Reverse verification A (predicted red): `const n: number = cfg.precision` vs rebuilt dist .d.ts -> TS2322, control clean. Reverse verification B (predicted 3 red, OBSERVED 4): restoring `.default(2)` flipped the noisy-shape guard, its byte-identity row, the field-level agreeing-acceptance test, PLUS the field-level JPY+2 rejection via issue-order displacement (config-level over-fire on the baked default lands first) — same mechanism, one more red than predicted; restored from committed state. Downstream after building closures: metadata-protocol 75 files / 1094 passed; platform-objects 16 files / 347 passed (first-lap exit-1 was the fresh-worktree unbuilt-closure false red). objectstack validate PASS in app-showcase, app-crm, app-todo. Gates all PASS: adr-anchors, changeset-gate-self-tests, cross-package-test-inputs, doc-formula-expressions, docs-audit-scope, i18n, i18n-coverage (12/12 configs), merge-driver, release-body, spec-parsed-alias, nul-bytes.", "open_questions": [], "out_of_scope_findings": ["filed as #8163: field-level `currency` is rejected with no pointer to `currencyConfig.defaultCurrency` — the natural spelling gets the history line but no prescription (guidance-entry fix shape, same class as #7887)"] }
Generated by Claude Code
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsACCEPT — PR #8232 (draft), reviewed per step 7 against GitHub.
- Scope verified on the diff: 6 files, +485/−3 —
field.zod.ts, the new checked-in CLDR 48 fraction-digit table (162 codes, provenance + regen snippet), one docs paragraph, tests, changeset. No generated faces moved ⇒ independent landing, no relay slot. - The ruling's hard part executed exactly: the materialized-default trap is dispatched by relocating
.default(2)into a post-check.overwrite()(finding:FormViewSchema.groupsis declared as "alias to sections" but nothing folds it — every consumer reads onlysections#6926 precedent) so the refinement sees authored-vs-absent — authored-only firing, with the parse output pinned byte-identical against origin/main literals (the pin that proves the relocation changed nothing else). Deliberately partial per the ruling:dynamicmode and non-CLDR codes (BTC) fail open; alias funnel covered. - Premise refinement honestly reported, substance preserved: the ruling's parenthetical "field-level
currency" names a key that does not exist on the strict FieldSchema — statically-known therefore = fixed-modecurrencyConfig, judged at both precision anchors. That is reading the tree over the ruling's letter while keeping its intent, and reporting the delta rather than absorbing it. - Verification: 21 new tests both directions with message substance (both digit counts named); reverse verification B predicted 3 reds, observed 4, with the fourth's mechanism explained (issue-order displacement) — the honest kind of miss; in-repo census clean (3 declarations, all agreeing); consumer suites + examples ×3 green; full gate list PASS.
- Out-of-scope finding verified filed: [finding] field-level
currencyis rejected with no pointer tocurrencyConfig.defaultCurrency— the natural spelling gets history, not a prescription #8163 (guidance-entry shape, already routeddomain:spec-surface).
Landing: flip + auto-merge PM-driven on verified gate-job conclusions at the PR head; no lap needed.
Generated by Claude Code
- Scope verified on the diff: 6 files, +485/−3 —
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Oct 7, 2026
The question
A currency field can declare a
precision. A currency also has a fraction-digit count of its own, fixed by ISO 4217 and carried in CLDR. Nothing today stops an author from declaring aprecisionthat contradicts it:Is that combination something publish-time validation should reject, or is it a legitimate authored display choice that overrides the currency's own convention?
This is a spec question, not a renderer question, which is why it is filed here rather than answered downstream. It came out of objectui#4361 (currency formatting overrode each currency's own fraction-digit convention); the PM ruling on that card is quoted at the bottom.
Measured — the two
precisionkeys are not the same keyMeasured against
@objectstack/spec@17.0.0-rc.6as consumed by objectui:precisionz.ZodOptional(z.ZodNumber)CurrencyConfigSchema.precision(packages/spec/src/data/field.zod.ts, around lines 112-114)z.number().int().min(0).max(10).default(2)2Worth stating plainly because it decides whether a consumer can even tell the two cases apart: a parsed FIELD carries no materialized
2, so a renderer can distinguish "the author declared 2" from "the author declared nothing". ThecurrencyConfigblock cannot be distinguished that way — its default is baked in at parse time, so by the time anyone reads it, an unsetprecisionand an authoredprecision: 2are the same value.Note also that
CurrencyConfigSchemaaliases bothdecimalsandscaleontoprecision, so three spellings reach one key.The digit counts in question
ICU, node 22, full ICU (icu 78.2), identical across every display locale tested (en-US, de-DE, ja-JP, ar-KW, zh-CN, fr-FR, pl-PL, es-ES) — the count comes from CLDR
currencyData, keyed by the currency and not by the reader:So
precision: 2on a JPY field asks for two digits of a minor unit that does not exist, andprecision: 2on a KWD field silently drops the third fils digit that does.Options
A. Reject at publish time — a declared
precisionthat disagrees with the currency's ISO 4217 digits is a validation error, when the currency is statically known (a field-levelcurrency, orcurrencyConfiginfixedmode).dynamiccurrencyMode field has no single currency to check against, so the rule is necessarily partial.precision: 2next tocurrency: JPYis told at publish time, once, instead of shipping money rendered with digits it does not have.B. Treat an authored
precisionas an intentional display override — document it as winning over the currency convention, and leave validation alone.C. Warn, do not reject. Splits the difference and, in an AI-authoring pipeline, mostly means the warning is not read.
Recommendation
A, scoped to the case where the currency is statically known, with the message naming both numbers ("currency JPY has 0 fraction digits;
precision: 2contradicts it"). B is defensible only if someone can name the business case that A would block — and that case should be captured in the spec's own docs either way, since today the behavior is undefined rather than chosen.Deciding this does not block anything: objectui already ships the renderer half.
What objectui does meanwhile (no coupling)
objectui#4361 fixes the rendering defect independently. Its PM ruling, quoted verbatim:
So an authored
precisioncurrently wins in the renderer, and an absent one derives from the currency. If this card is answered A, the contradicting combination stops reaching the renderer at all and the objectui behavior is unaffected. If it is answered B, objectui's behavior is already the documented one.Adjacent but different: objectstack#7501 asks whether a number field's declared
scaleis enforced on write. This card is about a declared display width contradicting a fixed property of the currency, at publish time.Generated by Claude Code