Repository navigation
feat(spec,formula): a formula field declares a currency result — returnType 'currency' with its own currencyConfig; inferFormulaReturn stamps it when provably money - #22733
Conversation
Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…rmula-currency-result
… currency; forms and docs offer the currency result Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…tics walk holds a currency formula refused Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…rnType Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
… in all four locales; changeset Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…rmula-currency-result
📓 Docs Drift CheckThis PR changes 3 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 140 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 eea9af5be5a05c06869e4192aa3d40b4c0f2686d && git checkout eea9af5be5a05c06869e4192aa3d40b4c0f2686d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 1e9d5ba927d855691cfadd9704a2dea96756c9f3 && git checkout -B drift-repro b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20 && git merge --no-ff 1e9d5ba927d855691cfadd9704a2dea96756c9f3
node scripts/docs-audit/affected-docs.mjs --json b62260cb819a2fba6fb0c44ee9b4a0a4e53b3b20
|
…e card The '(no currency)' comment goes false when the currency result lands; it moves to a skills card blocked on this one, so this PR touches no governed surface. Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Check-runs on the head, read 2026-10-10T22:09Z (conclusions are the gate verdicts). Required seven: ① Derived judgmentsDiff: 28 files, +711/−59, merge-base
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…formula's Currency Config gate (62 -> 64) Claude-Session: https://claude.ai/code/session_016njDy8ozy9B9Ns5Y8kAWEK Co-authored-by: Claude <noreply@anthropic.com>
…rmula-currency-result
…rmula-currency-result
Contract reviewServed-tier: Check-runs on the head, read 2026-10-10T23:06Z, every one concluded (conclusions are the gate verdicts). Required seven, all success: ① Derived judgmentsDiff: 29 files, +715/−60, merge-base
② Semver levelUnchanged from the record of ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Dequeued by the merge-queue wall, not by this change.
Generated by Claude Code |
Fixes #22727
Clause-②: yes (widening)
Parked on #22744: the merge queue is walled for every lane.
render-projection-diff's base archive lacks the generatedregistry.tssince #22706, soType Check · source gatesfails on every merge-group run based at or aftered1de8c2db. This PR's own head1e9d5ba927is green, and it is re-queued once #22744 lands.What changed
@objectstack/spec: a formula declares a currency result.FieldSchema.returnTypeisnumber | text | boolean | date | currency. Acurrencyresult carries its currency the way a currency field does: in the formula's OWNcurrencyConfig, the sameCurrencyConfigSchema, the same defaults (absent means dynamic, the tenant default currency) and the same refusals (theprecisiontombstone, the three-character code, thecurrencyModeenum). One shape, not a second one.returnTypeanswerscurrencyby name, never by falling into a default arm (census below).field.form.tsandobject.form.tsoffer Currency (same list, same order, whichform-return-type-options.test.tsderives from the enum). The field designer's Currency Config row now shows for a formula whose return type is Currency (visibleWhenwidened withdata.returnType == 'currency'). Help text and the four locale bundles regenerated or re-translated (enregenerated;zh-CN/ja-JP/es-ESre-translated by hand for the two changed help texts).@objectstack/formula:inferFormulaReturn(expression, fields), the producer behind "authoring stamps it when the inferred type is provably money". It takes the host object's declared field map and returns the declaration to stamp:{ returnType: 'currency' }, pluscurrencyConfig: { currencyMode: 'fixed', defaultCurrency }when the source amounts share one fixed currency. Everything it cannot prove to be money answers exactly whatinferExpressionTypeanswers, which is unchanged.formulas.mdx,field-types.mdx,validation-rules.mdx) list the new member.Decision: "taken from the source field" is an authoring-time stamp, not a read-time derivation
Measured first:
currencyConfigis ALREADY admitted on aformulafield today (a fullFieldSchemaparse of a formula carryingcurrencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' }succeeds and keeps it; it is equally admitted, and unread, on atextfield). So "the field's own config" is an existing key gaining a reader, not a new admission. "From the source field" is realized byinferFormulaReturncopying the source currency into that declaration at authoring time. Nothing reads a referenced field's currency at display time.opportunity.expected_revenueand app-showcaseproject.budget_remaining— are both proven money by the new inference (pinned informula-return.test.ts).??(AGENTS.md Prime Directive 12): unchecked, and two consumers could pick two different sources.percentordatetimemember. No new gate, no new refusal (acurrencyConfigon a non-currency formula stays admitted-and-unread, exactly as on every other non-currency type). No MCP surface change.Where the stamp lives (mechanism assumption 3, measured)
returnTypelives only in objectui's designer:packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx:1103-1108, fed bycelAuthoring.ts:614-627, which calls@objectstack/formula'sinferExpressionTypeand coerces any value outside its localCEL_VALUE_TYPES(:600-602) tounknown. Not edited here (follow-up named in the report).inferFormulaReturn— lands here.@objectstack/mcp'svalidate_expressionreportsinferredTypefrominferExpressionType(unchanged, still nevercurrency); itsdescribeObjectprojection carries nocurrencyConfig, so it could not name the currency anyway. Noted below, not changed.Reader census
Instrument:
git grep -n "returnType"overpackages/**(non-test, non-dist), control: it hits the known readerfilter-boolean-comparand-declared-type.ts:318.currencyformulaFORMULA_RETURN_TYPE_AS_FIELD_TYPE(filter-text-operator-declared-type.ts), shared by the text, number and boolean doorscurrency→ judged as thecurrencyfield: text operator refused (INVALID_FILTER), number doorjudged, boolean doornot-judged— each pinnedNUMBER_COMPARAND_DOOR_FIXTURE_FIELDS,TEXT_OPERATOR_DOOR_FIXTURE_FIELDS)f_formula_currency; theirreturnTypetype is now derived fromField['returnType']instead of a four-literal uniondisplay-name.tsisTitleEligibletextis) — comment updatedaggregate-field-type-compatibility.tsformularefused for sum/avg/min/max whatever it returns — unchangedui/component.zod.tsinline form fieldreturnType(by reference)service-analyticsnative-sql-strategy.ts/measure-result-type.tsreturnTypeand is deferred; theformula-return-type-measurewalk now holdscurrencyto the sameundefinedTests
Package tests on
20d64a3102; the merge commit after it (de9f741f33) brought onlypackages/cloud-connectionand its changeset from main.@objectstack/spec:vitest run --project local— 643 files, 19205 passed, 1 todo. Newformula-currency-result.test.ts(a currency formula parses; itscurrencyConfigparses identically to a currency field's and is refused with identical code, path and message; CONTROLnumberunchanged; the enum is exactly the five).form-return-type-options.test.tsmoved with the enum:currencyoffered by both forms and parses;datetimestill refused and unoffered.@objectstack/formula: 47 files, 1326 passed. Newformula-return.test.ts, 11 cases — the card's two pins (a formula over a currency field declares the currency result; CONTROL: a numeric formula staysnumber), the two example-app formulas, every non-money case asserted equal to the plaininferExpressionTypeanswer, and every stamped declaration parsed by the rebuilt specFieldSchema.@objectstack/objectqldoor suites (3 files, 93 passed),@objectstack/service-analyticsformula-return-type-measure(10 passed),@objectstack/platform-objects(69 files, 1082 passed).Gates
de9f741f33.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 120 families (the same set it derived at20d64a3102). All 120 ran atde9f741f33and every one exited 0; the--ranreconciliation over the recorded exit codes reads "120 run, 0 NOT-MEASURED (a DERIVED zero — all 120 recorded an exit code and none of them is 3)". The set includescheck:generated,check:docs,check:api-surface,check:authorable-surface,check:adr-0087-registration(a widening carries no disposition marker; green),check:changeset-no-major,check:i18n,check:i18n-stale-fill,check:skills-token-ratchet,check:skill-examples,check:cross-package-test-inputs,check:type-check-coverage,check:type-check-debtandcheck:nul-bytes.20d64a3102two gates answered PREREQUISITE NOT MET (exit 3, nothing measured):check:skill-examples(client-react unbuilt) andcheck:dual-build-cjs-loads(nine package dists absent). Both re-ran green after the builds, and the second round above measured them green directly.check:generatednamed one stale artifact after the enum moved,content/docs/references/**, regenerated withgen:docs;authorable-surfaceandapi-surfacedid not move (an enum member is not a key, and no spec export was added).pnpm lint). Population: the 20 lintable files in this diff against the merge base, all undereslint.config.mjs's globalfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']object;--no-inline-config --format jsoncounted 20 files, 0 errors, 0 warnings. Invariance: the config enables no type-aware linting (noparserOptions.project, stated in its own header at lines 327-328), so this diff cannot move a verdict on a file it does not touch.Governed surface
The skill line
skills/objectstack-data/rules/field-types.md:253('(no 'currency')') goes false when this lands. The seat carries it to the skills lane as a card blocked on #22727.Acceptance notes
.objectui-shapin draws an undeclaredreturnTypethrough the text face (packages/fields/src/widgets/FormulaField.tsxfinal branch,packages/fields/src/index.tsx:2703-2704cell), so a formula hand-declaredcurrencyrenders as monospace text until objectui formats it. The card names that follow-up (filed beside it, on hold until a spec release carries the member). Nothing in this repo stampscurrencyautomatically, and the docs here state the declaration, not a display.ObjectFieldInspector.tsx:1103-1108,celAuthoring.ts:600-627) must callinferFormulaReturn(source, draft.fields)and write both members; until then it keeps stampingnumberfor a money formula, as today.validate_expression(MCP) keeps answeringinferredTypefrominferExpressionType; reporting a currency would needdescribeObject(packages/mcp/src/stdio-data-bridge.ts:662-680,packages/runtime/src/domains/mcp.ts:553) to projectcurrencyConfig. Not changed: no measured pull, and the answer it gives (number) is still honoured.currencyConfigon a formula whosereturnTypeis notcurrencyis admitted and unread, as on every non-currency type (FieldSchemaaccepts each per-type key on any type; the forms gate visibility). No refusal added: it would be a narrowing and a new gate.number; moving them tocurrencybelongs with the objectui rendering.object.form.tswas a stop boundary while docs(spec): fileAccessDelegate and the refused file marker name the record-read verdict beside the download (#22698) #22713 was in flight; docs(spec): fileAccessDelegate and the refused file marker name the record-read verdict beside the download (#22698) #22713 merged at 2026-10-10T20:27:42Z,origin/mainwas merged first, and only this file'sreturnTyperow moved.维护者速读(草稿)
改了什么
公式字段现在可以声明"货币"结果:
returnType新增currency,货币写在公式自己的currencyConfig里,与货币字段同一套结构和默认值(不写即 dynamic,用租户默认货币)。新增inferFormulaReturn:作者编辑公式时,能证明结果是钱的就给出currency和源字段的货币,证明不了就和以前一样。两个设计器表单都提供"货币"选项。为什么改
HotCRM 实测:引用金额字段的公式只能显示成裸数字。货币作为公式自身的声明,在保存时用货币字段同一套规则校验,不在渲染端回退去猜源字段。
风险与代价(含回滚)
纯加宽,以前合法的都仍合法。objectui 跟进之前,手工声明
currency的公式会以文本样式显示(比数字样式差),所以本 PR 不自动给任何现有公式盖章。回滚:还原本 PR 即可,没有数据迁移。席位意见
你要做的
本 PR 不改
skills/**,不触及受管面。那一行 skill 注释由席位另立 skills 卡(阻塞于 #22727),单独请你审。