Skip to content

fix(spec): never materialize the currency precision default the schema itself refuses — bare fixed-JPY parse is idempotent (#11423) - #11766

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-11423-currency-precision-idempotent
Aug 24, 2026
Merged

fix(spec): never materialize the currency precision default the schema itself refuses — bare fixed-JPY parse is idempotent (#11423)#11766
os-warren merged 2 commits into
mainfrom
claude/issue-11423-currency-precision-idempotent

Conversation

@os-warren

@os-warrenos-warren commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11423

What

CurrencyConfigSchema's .overwrite() no longer materializes the relocated precision default (2) onto the one combination whose authored spelling its own superRefine refuses: currencyMode: 'fixed' with a defaultCurrency whose ISO 4217 / CLDR fraction digits contradict 2 (the JPY/KRW/KWD class). A bare fixed-JPY config now parses to output that OMITS precision, so parse(parse(x)) holds on the mainline ObjectSchema.create()defineStack re-parse chain. One conditional, mirroring the #11406 master_detail guard one screen below in the same file; the superRefine rejection of an AUTHORED contradictory precision is untouched, message and path included.

Governing ruling

Routed by the spec seat under the maintainer's ruling on #9689 (comment 5393503175), verbatim:

「The same principle prescribes the fix for the #7918 currency twin (#11423) — the spec seat should route it under this ruling.」

The principle, as landed by PR #11406 (commit 9086761ed): never materialize a default the schema itself would refuse as authored.

Reverse verification (both legs, measured)

Unfixed source (branch base 2a6122bd9):

Fixed source (this branch, head c3613e137):

  • parse({ currencyMode: 'fixed', defaultCurrency: 'JPY' }){"currencyMode":"fixed","defaultCurrency":"JPY"}; re-parse → success, no issues
  • full pin file + field tests: 2 files / 192 tests passed

Behavior matrix (pinned)

inputbeforeafter
bare fixed-JPY/KRW/KWDmaterialized precision: 2; re-parse REJECTEDomits precision; re-parse green (idempotent)
authored precision: 2 + fixed JPYrejectedrejected — same message, same path
bare fixed-USD (2-digit class)precision: 2precision: 2, byte-identical
bare dynamic (any code) / fixed unknown code (BTC)precision: 2precision: 2 — the superRefine cannot refuse these

Contract note

Clause-②: yes — parse output changes for previously-accepted bare fixed-currency inputs; the PR stays draft, the contract-review chain runs before enqueue.

Stored rows / built artifacts that carry the OLD baked precision: 2 on a fixed zero/three-digit currency re-parsed RED before this change (that is the defect) and still read as authored contradictions after it; same population shape #11406 handled with a no-automatic-conversion migration entry on its twin. Whether this card owes its own registry entry is left to the contract review — the dispatch's declared file surface deliberately did not include the migration registry, and no consumer outside packages/spec reads currencyConfig.precision (measured: analytics/dogfood read only defaultCurrency).

Verification — real readings at head c3613e137

Gate derivation line (quoted per dispatch): dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit c3613e137 (/home/user/objectstack-11423). — change set 3 path(s) vs merge base 2a6122bd9 (field.zod.ts, currency-precision-iso4217.test.ts, the changeset).

Heavy runs all through scripts/pm/os-verify-lock.sh; every exit captured before any pipe; each reading quotes the gate's own verdict line:

runreading
pnpm --filter @objectstack/spec test (full)Test Files 420 passed (420) · Tests 11224 passed (11224) · VERDICT command-exit 0
pnpm --filter @objectstack/spec typecheck (tsc + scripts + test-typecheck)VERDICT command-exit 0
pnpm --filter @objectstack/spec check:generatedVERDICT command-exit 0 — no stale artifact, tree clean after
examples/app-showcasetypecheck && vitest runTest Files 26 passed (26) · Tests 362 passed (362) · VERDICT command-exit 0
pnpm --filter @objectstack/lint testTest Files 81 passed (81) · Tests 2280 passed (2280) · VERDICT command-exit 0
CLI test/migrate-meta.e2e.test.tsTest Files 1 passed (1) · Tests 14 passed (14) · VERDICT command-exit 0
qa/dogfoodtest/expression-conformance.test.tsTest Files 1 passed (1) · Tests 3 passed (3) · VERDICT command-exit 0
turbo run build --filter=./packages/* --filter=./packages/*/*70 successful, 70 total · VERDICT command-exit 0

Derived gate families, each exit 0 (captured pre-pipe): nul-bytes · changeset-no-major · adr-0087-registration · empty-changeset · cross-package-test-inputs (both spellings) · test-source-alias · merge-driver · objectui-changeset · spec-parsed-alias · slot-lookup · published-files · type-source-resolution · where-matcher · engine-double-contract · query-options-erasure · changeset-gate-self-tests · dev-prereqs (post-build) · plugin-teardown-shape · docs-audit affected-docs · release-rehearsal-clone --self-test · lint-pkg doc-formula-expressions · spec check:liveness / check:empty-state / check:strictness-ledger / check:variant-docs · check:type-check-coverage.

One run predates the head commit by content-irrelevant bytes: the full spec suite started at f96b1c3ec (the fix commit); the only later commit c3613e137 adds .changeset/thin-yen-keeps-no-cents.md, which no test reads. Every other reading above was taken at c3613e137.

Declared narrowing (CI runs the full farm regardless): repo-wide check:type-check-debt --re-measure was not run locally — the diff touches only @objectstack/spec, which has no entry in the root debt ledger (its test-typecheck debt lives in packages/spec/test-typecheck-debt.json, gated by spec's own check:test-typecheck, executed inside the spec typecheck reading above), and spec's emitted .d.ts surface is unchanged (check:generated green includes check:api-surface), so no other package's ledger count can move from this diff. Repo-wide pnpm lint (eslint sweep) is CI-owned and was not run.


Generated by Claude Code

…xed currency whose fraction digits refuse it (#11423)
The CurrencyConfigSchema .overwrite() baked precision 2 onto a bare
fixed-JPY/KRW/KWD-class config; the superRefine refuses that spelling as
authored, and the two are indistinguishable by design - so parse output
rejected itself on the mainline ObjectSchema.create() -> defineStack
re-parse. One conditional in the .overwrite() (the #9689 master_detail
precedent one screen away): the refused combination parses to output that
OMITS precision; every other combination keeps byte-identity. parse(parse(x))
pinned idempotent at CurrencyConfigSchema, FieldSchema and the
create() -> defineStack chain; authored contradictions stay rejected with the
same message; bare fixed-USD still materializes 2 byte-identically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 126 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 6d0cccc7271f295cac1d46423285ee0e8c4cb78dpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 005d5f65b7d4bdd958b83b95ee28c51972df64aa — the merge of head c3613e13715d3a71d27000481e24196bd30915ab into base 6d0cccc7271f295cac1d46423285ee0e8c4cb78d, 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 005d5f65b7d4bdd958b83b95ee28c51972df64aa && git checkout 005d5f65b7d4bdd958b83b95ee28c51972df64aa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6d0cccc7271f295cac1d46423285ee0e8c4cb78d c3613e13715d3a71d27000481e24196bd30915ab && git checkout -B drift-repro 6d0cccc7271f295cac1d46423285ee0e8c4cb78d && git merge --no-ff c3613e13715d3a71d27000481e24196bd30915ab
node scripts/docs-audit/affected-docs.mjs --json 6d0cccc7271f295cac1d46423285ee0e8c4cb78d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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

3 participants

@os-warren@os-zhuang@claude