Repository navigation
fix(spec)!: a fixed currency config needs its code — currencyConfig.defaultCurrency loses its CNY default - #27218
Conversation
…oses its CNY default
currencyConfig.defaultCurrency was .default('CNY'): a fixed config with no
code became a yuan field, and every dynamic config's parse output carried a
CNY no reader takes. The default goes; a superRefine refuses fixed without a
code with the prescription (give a code, or use dynamic), on a currency field
and on a formula returning currency alike.
ADR-0087: the D2 conversion currency-config-dynamic-default-currency-removed
strips the baked code from dynamic stored rows and keeps fixed ones; the D3
entry currency-config-fixed-default-currency-required carries the judgment.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh
Declares the defaultCurrency default change (CNY -> none) under major 18, the dropped-refinement sites the new superRefine adds, and the regenerated authorable defaults and reference docs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh
The fixed-without-code refusal names the release it ships in, joined in the retirement-release pin for the new D2 entry; the dropped-refinements header totals follow the 13 new sites. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh
…OGs excluded) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh
📓 Docs Drift CheckThis PR changes 3 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 149 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 142e1d1b1bbc587f8b51baccf2a4f743d5ab5da5 && git checkout 142e1d1b1bbc587f8b51baccf2a4f743d5ab5da5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cdb949a288e1bd3350f09527c5f2169eb4752c23 a7683e16f04b033a608a5bb8fdfe8cb03e3619ec && git checkout -B drift-repro cdb949a288e1bd3350f09527c5f2169eb4752c23 && git merge --no-ff a7683e16f04b033a608a5bb8fdfe8cb03e3619ec
node scripts/docs-audit/affected-docs.mjs --json cdb949a288e1bd3350f09527c5f2169eb4752c23
|
Contract reviewServed-tier: Inputs read (2026-10-28 11:01Z to 11:10Z): card #27157 body and both comments (claim 6465165193, os-dev-report 6467252308); ruling 6464202770 on #27119 (Q2 = X, G3); the ① Derived judgmentsThe refusal. The formula shape (#22727). ADR-0087 conversions. D2 A stored Readers of Survey. Declarations. Changeset Tests. No test deleted. Three re-pins move the bare JSON Schema projection. The rule is value-conditional ("required when ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
…#27157) Contract review round 2. A sys_metadata object row is kept as authored, so a row saved as { currencyMode: 'fixed' } has no code; the conversion writes none in. It loads and registers unchanged, is counted invalid with a [metadata_spec_invalid] line, and its next save is refused until a code is written; its display (the tenant default) does not change. The changeset, the D3 entry, the default-changes row, the conversion docblock, the field docblock, the upgrade-guide fragment and the console currency comment now say so, and the conversion test pins the row. The refusal drops its ADR-0049 citation (the narrowing is ruling-driven). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh
Contract reviewServed-tier: Round 2 on the round-1 FAIL (record 6467521422 on ① Derived judgmentsEvery sentence about stored rows is now true in code. The eight sites say one thing, and each clause has a reader on main:
The pin asserts what it claims. New test "leaves a stored fixed row with no code unconverted; the current schema refuses it with the prescription": a stored The acceptanceCriteria tells operators a correct remedy. "For stored sys_metadata object rows, look for the The refusal. The rest of the diff. ② Semver levelUnchanged from round 1: ③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读 (席位终稿)
席位意见:接受。契约复核第一轮 FAIL(6467521422),原因是「已存 fixed 行都带代码」这一前提不成立。第二轮 PASS(6468532881):八处文字改为实测行为,并在转换层钉住了无代码的 fixed 存量行。 席位核对 · ACCEPT ·
|
…ication notes PR #27218 (0cd578f) landed #27157 on 2026-10-28: defaultCurrency has no default and a fixed config without a code is refused. The readings stay dated at cdec747; a one-line note records the landing. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
Requested by Yibo Wang · project thread
Fixes #27157
Ruling-ref: 6464202770
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none —
skills/**teachesdefaultCurrencyin two places (skills/objectstack-data/rules/field-types.mdlines 55 and 174-187,skills/objectstack-ui/rules/dashboards.mdlines 303-304), and both already teach it only as an explicit code undercurrencyMode: 'fixed'; noskills/**text teaches the'CNY'default (git grep -n CNY -- skills→ 0 hits). Not Tier H.维护者速读
currencyConfig.defaultCurrency去掉'CNY'默认值;currencyMode: 'fixed'不写币种代码时在解析处拒收(货币字段和返回 currency 的公式字段同一规则),提示「写 ISO 4217 代码,或改用 dynamic(组织默认币种)」;dynamic配置解析后不再带defaultCurrency。fixed's CNY, the example's USD) #27119 Q2):币种未知的金额取组织默认币种,任何地方都不猜。之前不写代码的 fixed 字段静默变成人民币字段,dynamic 配置也每次被物化出一个没人读的 CNY。sys_metadata行按原样保存,所以以{ currencyMode: 'fixed' }存下的行没有代码,转换不补(补 CNY 正是裁决禁止的):它照常加载注册,启动计为 invalid 并打印[metadata_spec_invalid],下次保存 422 拒收直到写上代码;显示不变(之前就落到组织默认币种)。dynamic 行上的 CNY 由 ADR-0087 转换currency-config-dynamic-default-currency-removed在回读时剥掉,显示不变。回滚即 revert 本 PR。packages/spec/src/data/field.zod.ts(CurrencyConfigSchema与refuseFixedCurrencyWithoutCode)、packages/spec/src/conversions/registry.ts(新转换)、packages/formula/src/formula-return.ts的currencyOf。pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-fixed-code-required.test.ts src/conversions/currency-config-dynamic-default-currency-removed.test.ts,预期 2 个文件 12 个用例全绿:fixed 无代码在defaultCurrency处拒收,dynamic 输出无代码,已存 dynamic 行剥掉 CNY、带代码的 fixed 行保留,已存的无代码 fixed 行不被转换且仍被拒收。What changes
@objectstack/spec, minor, BREAKING).CurrencyConfigSchema.defaultCurrencyis.optional()instead of.default('CNY'), and the schema gains onesuperRefine:currencyMode: 'fixed'with nodefaultCurrencyanswers onecustomissue atdefaultCurrency(currencyConfig.defaultCurrencyon a field), first sentence "currencyConfig.currencyMode: 'fixed'needsdefaultCurrency, the field's one currency: give its ISO 4217 code (e.g.defaultCurrency: 'USD'), or usecurrencyMode: 'dynamic'to display amounts in the tenant default currency (thelocalization.currencysetting)."FieldSchema.currencyConfigis this one schema for a currency field and for a formula withreturnType: 'currency'(spec: a formula field cannot declare a currency result —returnTypeisnumber | text | boolean | date, so a formula over money renders as a bare number #22727), so both take the same refusal. Adynamic(or empty) config parses to{ currencyMode: 'dynamic' }; an authored code underdynamicstays legal and unread..describe(), the field form's help text, and the hand-written docs tables (content/docs/data-modeling/field-types.mdx,validation-rules.mdx) no longer say "Defaults to CNY".currency-config-dynamic-default-currency-removed(toMajor 18,retiredFromLoadPath,retiredAfter: '18.2.0', order 78): stripsdefaultCurrencyfrom a config whose mode isdynamicor absent, on objects and object extensions; afixedconfig is never touched, and no code is written into one. D3 semantic entrycurrency-config-fixed-default-currency-required(step 18,conversionIds= the D2 id) and aSTEP18_RATIONALEfragment.DEFAULT_CHANGES_BY_MAJOR[18]declaresdata/CurrencyConfig:defaultCurrency: "CNY" → (none). Thecurrency-config-precision-removedfixture's dynamic row drops its bakedCNYso the two fixtures stay disjoint.fixedconfigs carry the code the old parse filled in and keep it. Asys_metadataobject row is stored as authored (saveMetaItempersists the payload verbatim), so a row saved as{ currencyMode: 'fixed' }(Studio's Fixed mode with the code left blank, or a/metawrite) has no code, and the conversion writes none in. Measured on this head with a probe throughloadMetaFromDbandsaveMetaItem:{ loaded: 1, errors: 0, invalid: 1 }; the row registers unchanged and boot prints[metadata_spec_invalid] stored object/crm_invoice fails the current spec schema even after conversion (fields.amount.currencyConfig.defaultCurrency: …the prescription…). A dynamicCNYrow in the same probe:invalid: 0, registered as{ currencyMode: 'dynamic' }.INVALID_METADATA, onecustomissue atfields.amount.currencyConfig.defaultCurrency, until a code is written.resolveFieldCurrencyreads afixedcode only when one is present, so the field showed the tenant default before and still does (its test "fixed with no code: falls through to the tenant default" pins it).currencyOfrow below).ObjectSchema.safeParserefuses it atfields.amount.currencyConfig.defaultCurrencywith the prescription.dropped-refinements.baseline.jsongainsdata/CurrencyConfigplus thecurrencyConfigsite in the 12 schemas that embed it (totals 252 schemas / 900 sites);authorable-defaults/data.jsonloses theCNYrow;content/docs/references/data/field.mdxregenerated. Livenessfield.jsoncurrencyConfigrow: a dated note, verdict unchanged (live).ui/packages/i18n/src/currency.ts#resolveFieldCurrency(the display rule at :34-36 and :60): unchanged. It reads the code only underfixed, so a dynamic config withoutCNYdisplays the same. Its test "parsed-empty" now asserts the spec's empty-config output carries no code (toBeUndefined()) and still resolves to the tenantUSD; the docblock says the CNY now lives only in rows not yet stripped, and that a storedfixedrow saved without a code still loads with none and shows the tenant default.packages/formula/src/formula-return.ts#currencyOf: the one reader that changes. It parses throughCurrencyConfigSchema, so afixedconfig without a code now fails to parse and reads as an unproven currency (noreturnTypeinferred) instead of fixed CNY. New rowfixed_no_codepins it; thedefaultCurrency === undefinedguard only narrows the type.packages/services/service-analytics/src/plugin.ts(the field-currency relay, :1447): reads the raw config's code only underfixed; unchanged.packages/types/src/share-link-resolve-answer.ts(:120): copiesdefaultCurrencyonly when it is a string, so a dynamic field's projection loses theCNYkey; the console reads it only underfixed. No display changes.defaultCurrencyread underui/goes throughresolveFieldCurrencyor reads the legacy flat key.examples/**,packages/platform-objects/**,packages/qa/**, seeds and fixtures (CHANGELOGs excluded), 1,034 files: 11currencyMode: 'fixed'configs, 0 without a code. The same detector over the whole tree flags the CHANGELOG prose that namesfixedwithout a code, so it is not blind. A deployed database'ssys_metadatarows cannot be surveyed from here; any fixed row without a code there behaves as measured above.Tests
packages/spec/src/data/currency-config-fixed-code-required.test.ts(new): fixed without a code refused atdefaultCurrencywith the prescription; the same on a currency field and on a formula returning currency (currencyConfig.defaultCurrency); fixed with a code passes unchanged; dynamic output carries no code;parse(parse(x))equalsparse(x)for every accepted shape, on the config and onFieldSchema.packages/spec/src/conversions/currency-config-dynamic-default-currency-removed.test.ts(new): registered for 18, retired from the load path, linked from the D3 entry; a stored object row loses the code on dynamic configs (currency field and formula), keeps both fixed ones by reference, parses to itself, replays idempotently; the authoring load path does not run it; a stored{ currencyMode: 'fixed' }row with no code gets no notice and no change, and the current schema refuses it atfields.amount.currencyConfig.defaultCurrencywith the prescription.field.test.ts,currency-precision-iso4217.test.ts,formula-currency-result.test.ts(thefixed-without-code row moves to the refused list),retired-key-removal-version.pin.test.ts(JOIN row),packages/formula/src/formula-return.test.ts,ui/packages/i18n/src/__tests__/resolveFieldCurrency.test.ts.Acceptance notes
@objectstack/spec 18becauseretired-key-removal-version.pin.test.tsrequires every entry retired after the newest stable to name the release it derives.defaultCurrencyunderdynamicis not refused (the order asked for no such narrowing); the conversion strips it only from stored rows and artifacts.ui/packages/i18n/src/currency.tschanges a comment only, so no@object-ui/i18nchangeset line.Verification
All readings on base
main6866e2e7con a shared container; the lock's VERDICT lines are quoted. Round 1: code head5f876858f, headc172c878e(a changeset wording commit only: the survey count). Round 2 heada7683e16f(contract review 6467521422): wording at the stored-row sites, the ADR-0049 citation dropped from the refusal, one new conversion test; readings below marked (r2) were taken on it.Round 2 (a7683e1)
loadMetaFromDb/saveMetaItem(deleted after use), as quoted under Stored rows and built artifacts, measured.pnpm --filter @objectstack/spec gen:migration-registry && build, then the conversions, migrations, field, currency and retirement-pin tests → Test Files 31 passed (31), Tests 1274 passed. VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated→ current. Spec vitest--project local→ Test Files 742 passed (742), Tests 22244 passed | 1 todo (one more than round 1: the new pin).typecheck→ exit 0.uivitestpackages/i18n/→ Test Files 89 passed (89), Tests 1373 passed | 13 skipped. VERDICT command-exit 0.a7683e16f: the 34--commandsfamilies that read the changeset, the spec package, lint or i18n (includingcheck-adr-0087-registration --base origin/main, the changeset families,check:nul-bytes) → 34/34 exit 0, VERDICT command-exit 0. The--commandslist is identical (113 families). The other 79 read nothing this commit touches (formula, platform-objects and the dogfood battery included: no source there changed) and keep their round 1 readings.Measure first (origin/main 6866e2e)
CurrencyConfigSchema.safeParse({ currencyMode: 'fixed' })→{"currencyMode":"fixed","defaultCurrency":"CNY"}.{ currencyMode: 'dynamic' }→{"currencyMode":"dynamic","defaultCurrency":"CNY"};{}→ the same.returnType: 'currency'and{ currencyMode: 'fixed' }→{"currencyMode":"fixed","defaultCurrency":"CNY"}.Tests
pnpm --filter @objectstack/specvitest--project local(afterpnpm --filter @objectstack/spec build, VERDICT command-exit 0) → Test Files 742 passed (742), Tests 22243 passed | 1 todo.pnpm --filter @objectstack/spec typecheck→ exit 0 (tsc, scripts-typecheck, test-typecheck OK).pnpm --filter @objectstack/formula test→ Test Files 56 passed (56), Tests 1484 passed;typecheck→ exit 0.@objectstack/platform-objectsvitest (after its closure build) → Test Files 96 passed (96), Tests 1361 passed. VERDICT command-exit 0.uivitestpackages/i18n/(from the ui root) → Test Files 89 passed (89), Tests 1373 passed | 13 skipped. VERDICT command-exit 0.pnpm --filter @objectstack/spec check:generated --fix→ 14 artifacts current aftergen:docs(re-check green).pnpm --filter @objectstack/dogfood test, closure built, head5f876858f) → Test Files 348 passed | 3 skipped (351), Tests 2804 passed | 20 skipped (2824). VERDICT command-exit 0 · held the lock 1733s.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 113 derived families; the list is identical onc172c878e.pnpm --dir ui exec vitest run scripts/(first run: one 15 s timeout incheck-vi-mock-inherit.test.tswhile the dogfood held the box; that file alone then 83/83, and the whole gate exit 0 onc172c878e),spec check:skill-examplesandcheck:dual-build-cjs-loads(exit 3, built output missing; exit 0 after building client-react, studio and the other named packages).c172c878e, all exit 0:check-adr-0087-registration(→registered currency-config-fixed-default-currency-required, currency-config-dynamic-default-currency-removed (new here: both)),check-changeset-no-major,check-empty-changeset,check-closing-keyword-parity,check:changeset-gate-self-tests,check:pm-changeset-deadline-census.--ran→113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED.node scripts/check-nul-bytes.mjs→ exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzgHpEdH4Nc37mK73zh