Repository navigation
Description
Filing gate: ① a defect, class (c): an author, AI or human, sets currencyConfig.precision expecting it to decide a currency's decimal places, and nothing honours it. Filed by domain:spec execution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr) because the maintainer's ruling orders it. ⛔ Not graded or routed here; that is triage's.
Provenance. Ruling 乙 on #19910 (record 5805782503, batch #218 item 2, maintainer 「其他同意」), item 3, verbatim: 「Two findings follow through the filing gate, filed by the domain:spec seat, ⛔ not riders: currencyConfig.precision is declared and read by nothing (ADR-0049 enforce-or-remove); objectui's CurrencyField reads the field-level precision (a digit count) as decimal places.」 This is the first of the two. The second goes to objectstack-ai/objectui.
Readings, re-taken at filing on origin/main c8399867b8
- Declared:
packages/spec/src/data/field.zod.ts:1663declarescurrencyConfig: CurrencyConfigSchema.optional(), andCurrencyConfigSchemacarriesprecision. - Its only reader is its own validation:
field.zod.ts:457-458checks an authoredprecisionagainst the currency's ISO 4217 digits, viacurrencyPrecisionContradiction. - Nothing reads it for behaviour. A
git grep -n -E "currencyConfig"overpackages/*/src, non-test, finds these, and none of them readsprecision:driver-sql/src/builtin-column-collision.ts:89(storage mode);service-analytics/src/analytics-service.ts:1794andplugin.ts:1070-1084(defaultCurrencyonly);- spec prose.
- Firing control: the same instrument finds
currencyConfig?.defaultCurrencyread atservice-analytics/src/plugin.ts:1084. - Renderer side, at
.objectui-sha62597c588072:packages/fields/src/widgets/CurrencyField.tsxresolves decimals from the field-levelprecisionor the ISO 4217 minor unit. The file's own comment says thecurrencyConfigkey is one 「this widget never reads」 ([Decision] Ruling 5791803339 points currency authors atcurrencyConfig.precision— but nothing reads that key, and removingscaleleaves two console faces rendering currency with zero decimals until objectui#10221 lands #19910 triage5800441682).
⇒ A declared, validated key whose value changes nothing a user sees.
The decision this card carries (ADR-0049)
Enforce it (a named consumer reads currencyConfig.precision), or remove it (a retirement with its ADR-0087 entry). Ruling 乙 on #19910 already set the direction for currency: 「a currency's decimal places are the currency's, not a setting」. That reads toward removal, but ⛔ this card does not decide it.
Related, ⛔ not folded
- PR fix(spec,objectql)!: retire
scalefrom the currency field type — refused at parse, no longer enforced on writes #19909 (card [finding]FieldSchema.scaleon acurrencyfield is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629) retiresscaleon currency; under ruling 乙 its remedy no longer points at this key. - currency fields: hide the
scaledesigner control; the summary footer and the metric tile take a currency's decimals from its ISO 4217 minor unit (objectstack#19910 ruling 乙) objectui#10221 widens the summary footer and metric tile to the ISO minor unit.
Dedupe
mcp__github__search_issues, repo-scoped: 「currencyConfig.precision declared but nothing reads it enforce-or-remove」 → 2 hits, #19910 (closed, the ruling's own card) and #7918 (closed, a publish-time contradiction question). No open card carries this.
Activity
objectstack-fleet commentedon Sep 24, 2026
Blocked-by: #19909
分诊首次定级:
priority:p3·bug·domain:spec·pm:blocked——currencyConfig.precision声明了、也校验了,但没有任何渲染或运行时读取它Path:
packages/spec/src/data/field.zod.ts(currencyConfig声明:1663,校验:458)Triage: lands in the spec field contract ⇒
domain:spec,bug,priority:p3,pm:blockedBlocked-by #19909 (the in-flight currencyscaleretirement, which edits the samefield.zod.ts); rationale: an ADR-0049 declared-but-unenforced key filed on ruling 乙 of #19910 — an author who sets it expects decimal places and gets none — and ruling 乙's own principle 「a currency's decimal places are the currency's, not a setting」 points the enforce-or-remove choice at removal.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T15:30Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在main(a7581b326c)上核对了代码,读了 PR #19909 的改动文件。本席核对
field.zod.ts:1663声明currencyConfig,:458用currencyPrecisionContradiction按 ISO 4217 校验precision。与卡面一致。scalefrom the currency field type — refused at parse, no longer enforced on writes #19909([finding]FieldSchema.scaleon acurrencyfield is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629,退役货币字段的scale)改动了packages/spec/src/data/field.zod.ts和同一组货币测试,与本卡同一文件。定级说明
currencyConfig.precision— but nothing reads that key, and removingscaleleaves two console faces rendering currency with zero decimals until objectui#10221 lands #19910 的裁定乙已经写明原则"货币的小数位由货币本身决定,不是一个设置项",⇒ 本席按移除方向定级(ADR-0049 的 remove 一侧,走 ADR-0087 退役登记,先弃用再删除)。如果实施时发现确有需要读取它的使用方,停下来报给分诊。scalefrom the currency field type — refused at parse, no longer enforced on writes #19909 改同一文件。⇒ Blocked-by fix(spec,objectql)!: retirescalefrom the currency field type — refused at parse, no longer enforced on writes #19909,合并后即可派发。执行要点
CurrencyField把字段级precision当小数位读)按裁定另开在 objectui,⛔ 不并进本卡。Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Evidence from the
domain:specexecution seat 1 (session_013RDBh5DqXd2xnLwvHLgLFr), 2026-09-24T18:31Z. These are two neighbours measured by the round-3 dev of PR #19909 on its headd7740c324e. ⛔ This is not a claim, ⛔ no label is changed, and ⛔ no new card is filed. The card's disposition carries them.packages/spec/src/data/field.zod.ts:420:CurrencyConfigSchemarenamesscale→precisionanddecimals→precision. An author who writescurrencyConfig.scalehas it moved intocurrencyConfig.precision, the key this card records as read by nothing. Whatever enforce-or-remove disposition this card takes has to take the aliases with it.precisionrow inpackages/spec/src/data/field.form.ts:78is offered oncurrencyandnumberwithhelpText: 'Decimal places (e.g., 2 for $10.50)'.FieldSchema.precision's describe reads "Total digits (non-negative integer)" (field.zod.ts:1211), andobjectForm's ownprecisionrow says "Total digits" (object.form.ts:189).CurrencyFieldreads the field-levelprecisionas fraction digits.field.zod.tsalso records ruling Contract question: should publish-time validation reject a declared currencyprecisionthat contradicts the currency's ISO 4217 digits? #7918 (Option A): the field-levelprecisiondoubles as the currency display width.objectstack-fleet commentedon Sep 27, 2026
Unlock scan: the same-file holder merged ·
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T06:01Z. ⛔ Not a claim, ⛔ not a dispatch. This is part of the blocked-card cleanup the maintainer asked for in this session's chat (「继续清理」, 2026-09-27).Blocked-by: #19909was PR fix(spec,objectql)!: retirescalefrom the currency field type — refused at parse, no longer enforced on writes #19909 ([finding]FieldSchema.scaleon acurrencyfield is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629, retiring the currencyscale), which heldpackages/spec/src/data/field.zod.ts. It merged at 2026-09-25T01:00Z. That PR was the file holder, not this card's own work, so this card is free to take.origin/main:CurrencyConfigSchema.precisionis still declared (field.zod.ts:435), with the aliasesdecimals/scale→precision(:420);:458);field.form.ts:78still offers the field-levelprecisionwith the help text 「Decimal places」, against the describe 「Total digits」 (the neighbour evidence5819897278).precisionas its display width, refusingprecision: 18on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #20011, released in this same act, edits the same currency-precision checks. Take the two serially, or in one PR.priority:p3·domain:specstay.objectstack-fleet commentedon Sep 27, 2026
Family site appended: the FIELD-level
precisionalso has no reader · 2026-09-27T09:22Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), from the #20011 dev report (PR #20209,out_of_scope_findings[0]). ⛔ Not a claim. It is recorded here because this card owns the family 「a declaredprecisionkey that nothing reads」, and the closure rule appends sites to the family card rather than filing them singly.packages/spec/src/data/field.zod.tsFieldSchema.precision,.describe('Total digits (non-negative integer)')(:1219at PR fix(spec): a currency field's precision is total digits — stop refusing precision: 18 on a fixed-USD field (#20011) #20209's head), on every numeric field type.f8a9d0fbpackages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx:917-919writes a top-levelprecision(patchDef({ precision: v })) on every numeric type.(65, 30)column (numeric-column-representation.ts:227for currency), anddriver-sqlbuiltin-column-collision.ts:77says 「this driver does not read it yet」;numberwithprecision: 1,scale: 2parses and is refused nowhere);packages/spec/liveness/field.jsonprops.precisionstill sayslive, citing objectui@11c1e71ereads (CurrencyField.tsx:45,PercentField.tsx:13,index.tsx:662,useColumnSummary.ts:261) that are all retired at the pin.precision: 10and expecting aDECIMAL(10, s)bound is silently ignored.precisionas its display width, refusingprecision: 18on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #20011) removed the field-level ISO 4217 comparison, because the key means total digits, not decimals. It kept thecurrencyConfig.precisionanchor, which is this card's original site.Dedupe words:
field precision no reader·Total digits precision unenforced·liveness field.json precision stale evidenceobjectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 1 · 2026-09-27T10:51Z
Session:
session_01Rjy9MeetSfq34PKn81CRiNAccount:
os-zhuangBranch:
claude/issue-19992-currency-config-precision-retireWorktree:
objectstack-issue-19992Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/data/field.zod.tsCurrencyConfigSchemaonly: theprecisionkey (:443on3875ae67), its aliasesdecimals/scale(:428; the other aliases stay), its ISO 4217 check (.superRefine/.overwrite);packages/spec/src/data/currency-fraction-digits.tsif the helper loses every reader;packages/spec/src/data/field.form.ts:78(the field-levelprecisionhelp text only); one ADR-0087 entry +packages/spec/src/migrations/registry.ts;packages/spec/liveness/field.json; the data-modeling doc sentences that state this rule; generated artefacts; tests;.changeset/. ⛔ Not thedefaultCurrencydescribe (open PR #20217). ⛔ NotFieldSchema.precisionitself. (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgment tier(the default slot is taken; a retirement with a premise to measure first). The diff hits the enqueue gate's path limb, so it is reviewed atCONTRACT_REVIEW_TIERbefore enqueue.Clause-②: no
Thread-read: 5854612946
Serial constraints cleared:
#20011 (the same-file sibling triage named) has landed as 84156c7d. Open PR #20217 (#20126, this seat) edits the defaultCurrency describe a few lines below; the dev does not touch it and merges main after it lands. No other open PR touches field.zod.ts, field.form.ts or currency-fraction-digits.ts. The folded field-level precision site (5854612946) is ⛔ not in scope: this PR is Part of #19992 and names it as the remainder. Direction: triage 5853208433 (remove, with the aliases; stop if a real reader is measured).objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{
"issue": 19992,
"status": "done",
"branch": "claude/issue-19992-currency-config-precision-retire",
"pr": "#20251",
"session": "session_01Rjy9MeetSfq34PKn81CRiN — the PM's (mode:subagent); identity = the branch",
"premise_still_valid": true,
"summary": "Retired currencyConfig.precision (ADR-0049 enforce-or-remove, triage direction REMOVE under ruling 乙): strict deletion from CurrencyConfigSchema plus a guidance entry carrying the prescription; the decimals/scale aliases became guidance entries with the same reason; the #7918 .superRefine and the #11423 .overwrite (which baked precision: 2 into parse output) are removed; currencyPrecisionContradiction and currencyFractionDigits (no reader left) removed, CURRENCY_FRACTION_DIGITS kept for the iso_4217_currency value domain. ADR-0087: D2 conversion currency-config-precision-removed (toMajor 18, retiredFromLoadPath, strips the key at rest on objects/objectExtensions), step-18 wiring, RETIRED_KEYS_BY_MAJOR[18] data/CurrencyConfig:precision. Field-form precision help text now 'Total digits' (+ en/zh-CN/ja-JP/es-ES bundles); docs, published skill, 3 showcase objects, generated references and ledgers updated; changeset spec minor + platform-objects patch, BREAKING, adr-0087 registered. PR body: first line 'Part of #19992'; #19992 remains open for the folded FIELD-level precision family site (5854612946). PR is Tier H because skills/** is on the diff.",
"tests": "All on head 3681939 (batch at 74cf413 + the en bundle commit, tree identical). spec build exit 0; spec check:generated exit 0 (15/15); spec typecheck exit 0 (incl. check:test-typecheck); spec vitest --project local: 546 files passed, 16062 tests passed, 2 todo. Consumers: example-showcase typecheck exit 0; platform-objects typecheck exit 0, tests 55 files / 911 passed; service-analytics currency-mode-relay + query-dataset 51 passed; pnpm check:i18n exit 0 (9 packages in sync). Reverse verification Leg A (ablation-replace, anchor 1→0, blob 58a9af7e→700705a8): showcase tsc exit 1, account.object.ts(71,25) TS2353 'precision' does not exist in type {currencyMode?, defaultCurrency?} against the rebuilt spec .d.ts; restored blob==HEAD, git diff HEAD empty. Ablation Leg B (precision re-declared on the schema, blob 7d36200c→86fdbf2d): 9 failed / 279 passed in the two pin files (every retirement pin red; alias, parse-output and conversion pins stay green by design); restored blob==HEAD, git diff HEAD empty; tree clean after both legs (0 bytes). Direction observed: red. No permanent ablation files left.",
"gates": {
"head": "36819398",
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 120 commands (repo objectstack-ai/objectstack at 3681939; 3 commits behind origin/main, none touching derivation inputs)",
"reconciliation": "dispatch-gates --ran: 120 derived famil(ies) accounted for — 119 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3), 0 UNRUN; exit 0",
"nonzero_first_pass": [
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 (client-react dist missing) → after pnpm --filter '@objectstack/client-react...' build: exit 0, '259 prose examples type-check across 3 surface(s)'",
"pnpm check:dual-build-cjs-loads :: exit 3"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads — reason: prerequisite is every package's dist/ (full pnpm build); this diff changes no package entry or exports"
],
"early": [
"node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (1 declared-breaking changeset, registered currency-config-precision-removed)",
"node scripts/check-changeset-no-major.mjs --base origin/main exit 0",
"pnpm check:nul-bytes exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs exit 0"
],
"ci": "0 check runs registered on 3681939 at report time — not waited on (PM owns CI convergence)",
"not_run_locally": "repo-wide pnpm lint (CI's run); cli integration tier (diff touches no packages/cli file); qa/dogfood (boots showcase; CI)"
},
"line_budget": {
"pr_changed_lines": "30 files, +659 / -511 = 1170 vs human-merge threshold 5000: under",
"skills": "skills/objectstack-data/rules/field-types.md 328 → 327 lines; skills/objectstack-data markdown total 3717 → 3716; all skills/*/SKILL.md 4402 → 4402 (pure removal; an added 3-line note was dropped to keep net ≤ 0)"
},
"deviations": [
"liveness/field.json NOT edited (order listed its currencyConfig row): measured — the row covers the container, asserts nothing about precision and stays live (defaultCurrency read); field/currencyConfig is in scripts/liveness/undrilled-containers.baseline.json so no per-key row existed; #20217 had just re-cited that row.",
"File surface beyond the claim, each because this change would otherwise leave it false: shared/value-domain.zod.ts docblock (named the removed function), one comment in conversions/registry.ts, 3 examples/app-showcase objects, content/docs fields.mdx + getting-started/common-patterns.mdx + protocol/objectql/types.mdx, skills/objectstack-data/rules/field-types.md (Tier H), platform-objects translation bundles, and generated/ledger rows (authorable-surface/data.json hand-removed per gate (a); authorable-defaults/data.json regenerated; dropped-refinements.baseline.json 13 sites hand-removed; references regenerated). Also comment-only edits elsewhere in field.zod.ts that named the removed twin.",
"PR is Tier H (skills/** on the diff): landing needs the maintainer's APPROVED review, not only the contract-tier record. The body carries the 维护者速读(草稿) section with 席位意见 left blank.",
"No tree-scoped absence text pin, deliberately: precision stays authorable as the FIELD-level key, so the retired thing is a key in a position; followed the dashboard-chart-structure-refusal.test.ts precedent (rationale block in the pin file); tsc (Leg A) and the closed parse door (Leg B) stand in.",
"decimals/scale: guidance entries (strict route, FieldSchema 'currency' guidance precedent) rather than bare unknown-key reports; not converted (never legal, no stored row carries them).",
"Clause-② copied verbatim from the claim as 'Clause-②: no' (no arm); the change is a narrowing — the changeset's BREAKING banner and adr-0087 marker carry that (check-adr-0087-registration green). The '(narrowing)' arm was not added because the claim did not carry it.",
"Merged origin/main (3cb84d0, #20217 landed) with scripts/pm/os-regen-merge.sh before opening the PR; 4 conflicts resolved by stacking both sides; sibling entries measured present (same counts as origin/main). The regeneration commit first carried the merge message and was reworded locally before any push.",
"docs/qa/platform-checklist/areas/records-forms.json not edited (item broadly stale on currency scale from an earlier card) — Acceptance notes.",
"Verify-lock queue: ~2h of cumulative queue time across batches (99s retried with the same slot; queue progressed throughout)."
],
"files_changed": [
".changeset/19992-currency-config-precision-retired.md",
"content/docs/data-modeling/field-types.mdx",
"content/docs/data-modeling/fields.mdx",
"content/docs/data-modeling/validation-rules.mdx",
"content/docs/getting-started/common-patterns.mdx",
"content/docs/protocol/objectql/types.mdx",
"content/docs/references/data/field.mdx",
"content/docs/references/data/object.mdx",
"content/docs/references/shared/value-domain.mdx",
"content/docs/references/system/migration.mdx",
"examples/app-showcase/src/data/objects/account.object.ts",
"examples/app-showcase/src/data/objects/field-zoo.object.ts",
"examples/app-showcase/src/data/objects/semantic-zoo.object.ts",
"packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts",
"packages/spec/authorable-defaults/data.json",
"packages/spec/authorable-surface/data.json",
"packages/spec/dropped-refinements.baseline.json",
"packages/spec/src/conversions/registry.ts",
"packages/spec/src/data/currency-fraction-digits.ts",
"packages/spec/src/data/currency-precision-iso4217.test.ts",
"packages/spec/src/data/field.form.ts",
"packages/spec/src/data/field.test.ts",
"packages/spec/src/data/field.zod.ts",
"packages/spec/src/migrations/entries/retired-keys/18.data__CurrencyConfig__precision.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/shared/value-domain.zod.ts",
"skills/objectstack-data/rules/field-types.md"
],
"mcp_calls": "0",
"api_writes": "3 relay writes as objectstack-fleet[bot] via repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches each): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (run 36327385351, PR #20251 draft); (2) label-write assign → POST /repos//issues/20251/assignees os-zhuang (run 36327433373, read back); (3) post-stamped → POST /repos//issues/19992/comments (this os-dev-report). Plus 7 git pushes on the paced route (write-pace --kind 'git push'), not REST.",
"open_questions": [
{
"question": "Landing path for the Tier H surface: keep skills/objectstack-data/rules/field-types.md in this PR, or split it into its own PR so the code half lands on the contract-tier record alone?",
"options": [
"A — keep one PR (Tier H: maintainer APPROVED review, then the owning seat lands it)",
"B — split: code/docs PR lands at contract tier; a one-file skills PR waits for the maintainer"
],
"recommendation": "A. 实际业务需求: the skill is what AI authors read, and after the code lands it would teach a refused key — the trap this card removes. 长远合理性: one landing keeps the contract and its teaching text consistent at every commit of main. 防 AI 犯错: under B there is a window where the published skill contradicts the schema. 不扩散: B costs a second PR and review for a 2-line removal. The cost of A is waiting for the maintainer's word."
}
],
"out_of_scope_findings": [
"carrier: next checklist-author sweep; 承接者:无 · docs/qa/platform-checklist/areas/records-forms.json item records-forms.field-type-constraints describes f_currency as 'scale 2 currencyConfig{precision 2}' and probes currency scale — stale on both halves; internal QA prose, not published · noted, not filed",
"carrier: 承接者:无 · standalone 'field' metadata rows are outside every field conversion (applyConversionsToStoredItem has no 'fields' stack collection), this one included, same as its precedents; no producer of such rows measured · noted, not filed"
]
}
Generated by Claude Code
11 remaining items
objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 1
Session:
session_01B3TqpoQbTAfG7G74GMDWNWAccount:
hotlong(the seat's linked user asGET /useranswers it; the card's assignee; seated on the maintainer's ruling 「放行,以 hotlong 坐席」, seat post #6017 marker5864561291)Branch:
claude/issue-19992-field-precision-write-seamWorktree:
objectstack-issue-19992Domain:
domain:spec(a verticalSeam:dispatch per triage5864304093; both ends declared below)Seat:
domain:spec#1(seat post #6017)File surface (the remainder only:
FieldSchema.precision, "Total digits", ENFORCE at the write seam):packages/objectql/src/validation/record-validator.ts(a digit-count refusal beside the existingscalerule, on the same stored-value basis) and its tests inpackages/objectql/src/validation/.precisiondescribeinpackages/spec/src/data/field.zod.ts(FieldSchema, not the retiredcurrencyConfig.precisiontombstone), and theprops.precisionrow inpackages/spec/liveness/field.json, re-evidenced with the new write-seam pin..changeset/.numeric-column-representation.tsstays). ⛔ Not the major-18 migration entries ([finding] Two major-18 D3 entries printCurrencyConfigSchema.precision … unchangedthroughos migrate meta, which PR #20251 makes false in the same major #20379's).(stop on breach; explain in the report)
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectstack packages/objectql/src/validation/record-validator.ts packages/spec/src/data/field.zod.ts packages/spec/liveness/field.json: no path-derived mandate). A narrowing, declaredyesconservatively because a new validation code may join a published vocabulary; the at-tier contract review is owed before enqueue.Clause-②: yes
Thread-read: 5864304093
Serial constraints cleared: read at 2026-09-28T06:57Z.
record-validator.ts,field.zod.tsorliveness/field.json(open-PR file lists read at this claim).domain:engine,pm:blocked) will edit the number arm ofrecord-validator.tsfor its string half after driver-sql on PostgreSQL answers 500 for a non-numeric string against a number field —where { amount: { $gt: "abc" } }isDATABASE_ERROR/ 500 over REST, while InMemoryDriver and SQLite answer 200 with no rows #20336 lands. It is not in flight now; the later lander resolves.CurrencyConfigSchema.precision … unchangedthroughos migrate meta, which PR #20251 makes false in the same major #20379 (pm:queue) edits the two major-18 D3 entries' text only; disjoint.fix(spec,objectql)!: retire `scale` from the currency field type — re…
fix(spec): a currency field's precision is total digits — stop refusi…
feat(spec)!: retire currencyConfig.precision — a currency's decimal p…
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 19992,
"status": "done",
"branch": "claude/issue-19992-field-precision-write-seam",
"pr": "#20423",
"head": "bb33240d",
"session": "session_01B3TqpoQbTAfG7G74GMDWNW — the PM's (mode:subagent); identity = the branch named by claim 5865023687",
"premise_still_valid": true,
"summary": "FieldSchema.precision ('Total digits') is now enforced at the objectql write seam. record-validator.ts refuses a number/currency/percent/rating/slider write whose digit count exceeds the declared count, with 400 VALIDATION_FAILED and field code max_precision, and never rounds. The count is the SQL DECIMAL(p, s) reading: digits are counted at the decimal places the max_scale rule applies (scaleAllowance), or at the value's own when it applies none (undeclared scale, and currency, where scale is refused); a fraction-stored percent is counted two places right (scale+2, or 2 with no scale). Spec end: FieldErrorCode gains max_precision (the ADR-0114 D2 closed catalog, not ERROR_CODE_LEDGER/StandardErrorCode), with four-locale templates max_precision/max_precision_scaled; the precision describe states the rule; the liveness row is re-evidenced at the write seam. No storage or DDL change. Draft PR #20423, Fixes #19992, Clause-②: yes (copied from the claim), assigned hotlong.",
"premises": {
"A1": "HELD. git grep -n precision over record-validator.ts at df3ba16: 0 hits (scale: 29). Non-spec packages//src readers: only the fixed NUMERIC_COLUMN_REPRESENTATION numeric.precision (cli generate, driver-sql DDL) and datetime precision.",
"A2": "DECIMAL(p, s) reading implemented and pinned: p5 s2 refuses 1234.5 (1234.50 = 6) and accepts 123.45/999.99. Undeclared scale: own decimal places, leading zeros never count, integer trailing zeros do (p4: 0.001 ok, 10000 refused). Currency: own decimals count toward the total, decimals themselves unconstrained. Fraction percent: counted at scale+2, or 2 with no scale, i.e. the percentage-point digits as displayed (p4 s2: 0.9999 ok, 1 refused; p3 no scale: 10 = 1000% refused). p below s: |v| below 10^(p-s) (p1 s2: 0.05 ok, 0.1 refused).",
"A3": "The vocabulary is FieldErrorCode in packages/spec/src/api/errors.zod.ts (ADR-0114 D2 closed field-level catalog); NOT a new ERROR_CODE_LEDGER / StandardErrorCode member (the top-level code is the already-registered VALIDATION_FAILED). Registration also needed four-locale templates (validation-message.ts), a foreign-vocabulary row in packages/runtime/src/dispatcher-error-vocabulary.ts (check:dispatcher-error-vocabulary reds without it) and the error-catalog.mdx row.",
"A4": "Census at df3ba16, regex \bprecision\s:\s*[0-9]+ minus currencyConfig/datetime/generated: 0 field-level precision in examples, templates, platform-objects, seeds or JSON fixtures. Positive control: the same form for scale hits examples/app-todo (2). Two test fixtures (precision 5 scale 0, values 1-12) all fit. No seed value is refused.",
"A5": "Matches. objectui pin f8a9d0f ObjectFieldInspector.tsx:914-921 writes top-level precision labelled 'Precision' / 精度 beside 'Scale' / 小数位; its offersScale docblock calls it the TOTAL digit count. No finding.",
"A6": "validateRecord call sites (engine.ts): validate() dry run, insert() one row and an array (whole batch refused), update() by id and by predicate (multi). insertMany routes through insert with per-row outcomes. REST create/createMany/batch/import reach these; the import create leg is createManyData → engine.insert(rows[]). update.options.upsert is a retired tombstone, so there is no separate upsert door. Pinned: insert one, insert[], insertMany, update by id, update multi, validate, REST create (400 envelope), REST import (dry run + commit)."
},
"tests": "Heavy runs through os-verify-lock (slot issue-19992-precision). AT bb33240 (final head): objectql validator suites (precision, record-validator, number-value) 3 files 247/247 plus rest import-integration.test.ts 44/44 (42 on main + 2 new), one VERDICT command-exit 0; objectql typecheck + rest typecheck + rest zod-union-fields.test.ts 18/18, VERDICT 0; spec build (after the merge) exit 0; spec check:generated 'All 15 generated artifacts are up to date'; spec typecheck exit 0; spec --project local over src/api src/system src/data scripts/liveness 202 files, 6840 passed, 1 todo (a declared narrowing; the rest of spec is declared to CI). Earlier heads: objectql full --project local 326 files / 6058 passed (d6056d9; the merge brought no objectql change); spec touched suites (validation-message, errors, field, currency-precision-iso4217) 356/356 (563e11d). ABLATION at bb33240, with the fix committed first: scripts/ablation-replace.mjs anchor = the if that compares actual with def.precision, replaced by if (false) with an ABLATION-19992 marker, anchor 1→0, blob 84ef8a2e→c928a426; the pin file went 18 failed / 4 passed (the 4 green are the controls: undeclared precision, ordering, malformed declaration, fitting-value doors). Restored: blob 84ef8a2e == HEAD, git diff HEAD empty, marker count 0. Direction observed: red. The subject resolves via a relative import to src, so no dist rebuild was involved.",
"gates": {
"head": "bb33240d",
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at bb33240: 114 commands (the dispatch list's 77 + 37 added by the diff); identical list at d6056d9",
"reconciliation": "dispatch-gates --ran: 114 derived famil(ies) accounted for — 111 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3), 0 UNRUN; exit 0",
"nonzero_first_pass": [
"pnpm check:error-code-casing :: exit 1 at 563e11d's parent (four code: assertions in the new pin file not field-addressed) → field: added → exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 1 (max_precision stamp site unclassified) → foreign-vocabulary row added → exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 2 (shallow clone refusal) → git fetch --shallow-since=2026-06-23 origin main, as it prescribes → exit 0",
"pnpm --filter @objectstack/spec check:generated :: exit 1 at d6056d9's parent (check:docs stale) → gen:docs → 15/15"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads — reason: exit 3, needs every package's dist (whole-repo build); this diff changes no package entry or exports",
"pnpm check:type-check-debt — reason: exit 3, needs the full packages build (lint.yml builds it first)",
"pnpm --filter @objectstack/spec run check:skill-examples — reason: exit 3, needs the client-react closure built; this diff touches no skill and no client"
],
"ci": "32 check runs registered on bb33240 at report time: 12 success, 2 skipped, 18 in_progress, 0 failing. Not waited on (PM owns convergence).",
"not_run_locally": "repo-wide pnpm lint (CI's); runtime typecheck/tests (closure unbuilt here — rest, plugin-auth, service-cluster/job/messaging dist missing; the one runtime edit is a declaration-table row read by the green check:dispatcher-error-vocabulary and matching the conformance test's rules by shape); the rest of the spec/rest/objectql suites beyond the files named in tests"
},
"line_budget": {
"pr_changed_lines": "16 files, +638 / -16 = 654 vs human-merge threshold 5000: under",
"skills": "no skills/** file touched; no governed surface on the diff (not Tier S/H)"
},
"deviations": [
"File surface beyond the claim, each because the order or a gate required it: packages/rest/src/import-integration.test.ts (the order's REST-envelope pin); packages/runtime/src/dispatcher-error-vocabulary.ts (the gate-required classification of the new stamp site — not published: imported only by a runtime test, runtime's tsup entry is src/index.ts, so no runtime changeset); packages/spec/src/system/validation-message.ts + its test (the code's templates, part of registration); content/docs/api/error-catalog.mdx (the catalog's bounded-ranges row); content/docs/protocol/objectql/types.mdx (the currency line taught 'precision (0-10, default 2) for decimal places', which enforcement turns into refused writes); generated references (gen:docs).",
"Engine-door pins live in packages/objectql/src/validation/record-validator.precision.test.ts (inside the claimed validation/ directory) rather than a new engine-*.test.ts beside engine.ts.",
"The fraction-stored percent is counted at a two-place floor even with no scale declared (the scale+2 shift applied at scale 0). Reason: without it a percent's precision would count stored-fraction digits when scale is undeclared and displayed digits when it is declared. Pinned; stated in the describe and the PR body.",
"Clause-② copied verbatim as 'Clause-②: yes' (no arm), in the PR body and the changeset. The fuller spelling for this diff is 'yes (narrowing)'. The BREAKING banner and the adr-0087 marker carry the narrowing (check-adr-0087-registration green: not-required (no-migration-prescription)).",
"Merged origin/main once with scripts/pm/os-regen-merge.sh (merge 1c82935): the os-regen driver kept one side of references/data/object.mdx, and pre-commit refused the hand-off until it was regenerated. gen:schema + gen:docs ran on the COMMITTED merge (MERGE_HEAD absent, so not in MERGE state), unlocked as generators, then bb33240. Sibling counts origin/main/HEAD: action-aria-removed 20/20, filter-cross-field-comparison-class 2/2.",
"Two commands went past the 600 s foreground ceiling while still running (a gate batch; a spec run that acquired the lock late). Each was waited on in the foreground with tail --pid until it exited, and its log was read. Nothing was left running.",
"Verify-lock: five queue-timeouts (exit 99, NOT MEASURED, slot kept), then re-acquired. The holders were other sessions' spec test:repo / build runs; the queue moved throughout."
],
"files_changed": [
".changeset/19992-field-precision-write-seam.md",
"content/docs/api/error-catalog.mdx",
"content/docs/protocol/objectql/types.mdx",
"content/docs/references/api/errors.mdx",
"content/docs/references/data/field.mdx",
"content/docs/references/data/object.mdx",
"content/docs/references/system/migration.mdx",
"packages/objectql/src/validation/record-validator.precision.test.ts",
"packages/objectql/src/validation/record-validator.ts",
"packages/rest/src/import-integration.test.ts",
"packages/runtime/src/dispatcher-error-vocabulary.ts",
"packages/spec/liveness/field.json",
"packages/spec/src/api/errors.zod.ts",
"packages/spec/src/data/field.zod.ts",
"packages/spec/src/system/validation-message.test.ts",
"packages/spec/src/system/validation-message.ts"
],
"mcp_calls": "0",
"api_writes": "3 relay writes as objectstack-fleet[bot], each a repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (run 36409109961, PR #20423 draft; body read back byte-identical); (2) label-write --assign hotlong → POST /repos//issues/20423/assignees (run 36409198983, read back: assignees hotlong); (3) post-stamped os-dev-report → POST /repos//issues/19992/comments (this comment). Plus 6 git pushes on the paced route (write-pace --kind 'git push': the empty-branch probe, dc6e8a6, 897593a, 563e11d, d6056d9, bb33240), not REST. Zero label adds: the order named none, and skip-changeset does not apply (a changeset ships).",
"open_questions": [
{
"question": "Currency's applied scale: this seam resolves no currency, so a currency amount is counted at its own decimals (a 17-digit integer USD amount fits precision 18). The spec's DECIMAL(18,2) reading of a fixed-USD field would bound the integer part at 16. Keep, or derive the minor unit?",
"options": [
"A: keep. Count at the amount's own decimals, one rule for fixed and dynamic currency, no currency resolution at the validator.",
"B: for currencyMode fixed, count at max(ISO 4217 minor unit, own decimals); dynamic stays as A. This needs CURRENCY_FRACTION_DIGITS at the validator."
],
"recommendation": "A. 实际业务需求: the gap lies above 2^53 (about 16 significant digits), where a double holds no exact amount, and no in-repo field declares precision on a currency. 长远合理性: B makes one key count two ways depending on currencyMode. 防 AI 犯错: A is one rule an author can predict from the describe. 不扩散: B adds currency resolution to the write seam, a capability this card was not given."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · boundary, not class (c): content/blog/protocol-first-development.mdx:652,680-681 and metadata-driven-architecture.mdx:263 teach precision: 1/2 as decimal places; enforcement makes following them refuse ordinary values (budget of 100 or more, hours of 10 or more). The posts use shapes already refused at parse (ObjectProtocol.define, default:, enable), so there is no live producer · noted, not filed · dedupe words: blog precision decimal places, stale blog field examples",
"carrier: 承接者:无 · observation: FieldSchema accepts precision on every field type and it is read only on the five numeric types. No producer writes it elsewhere (object.form number/currency/percent, field.form number/currency, designer isNumeric) · noted in Acceptance notes, not filed · dedupe words: precision non-numeric inert, precision applicability refinement",
"carrier: 承接者:无 · observation: precision: 0 parses (min 0) and refuses every non-zero write. Loud, not silent; a producer-side min(1) would be a separate narrowing · noted, not filed",
"carrier: next checklist-author sweep; 承接者:无 · docs/qa/platform-checklist/areas/records-forms.json item records-forms.field-type-constraints still says precision/scale are 'DECLARED but NOT enforced on the write path' (already stale for scale); internal QA prose, not published · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Review: ACCEPT · PR #20423 at head
bb33240df524· 2026-09-28T10:33Zdomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW), reviewer of record under claim5865023687. Dev report on this card (os-dev-report). Readings below are the seat's, taken on GitHub or onorigin/main.main, first body lineFixes #19992, which is the only closing keyword.mergeable_stateblocked(draft), notdirty.record-validator.tsplus its new pin file, theFieldSchema.precisiondescribe, and theliveness/field.jsonrow. The extras each follow from the order or a gate: the REST import pin (the order asked for an envelope pin); theFieldErrorCodemember inerrors.zod.tsand its four-locale templates invalidation-message.ts(A3's registration); thedispatcher-error-vocabulary.tsrow (check:dispatcher-error-vocabularyrequires it);error-catalog.mdx, and thetypes.mdxcurrency line, which taughtprecisionas decimal places and is now false; and the generated references.@objectstack/objectqlminor+@objectstack/specminor, with a BREAKING banner andClause-②: yes. There is no runtime changeset, which is correct:packages/runtime/src/dispatcher-error-vocabulary.tsis not reachable from runtime's tsup entry (src/index.ts;http-dispatcher.tsnames it only in a comment), so it does not ship.check-governed-merges.mjs --pr 20423: not governed; 654 changed lines.A3: the new code joinsFieldErrorCode(ADR-0114 D2's field-level catalog), notERROR_CODE_LEDGER/StandardErrorCode; the top-level code staysVALIDATION_FAILED.bb33240dwith the fix committed first, turned the pin file 18 red / 4 green, and the 4 green are the controls.Flags carried to the at-tier review (③):
percentis counted at a two-place floor even with noscale. This is the dev's own reading, pinned and stated in the describe.open_questions[0](currency counted at its own decimals). The dev recommends A (no currency resolution at the validator). The seat agrees: one rule an author can predict. It is the reviewer's to confirm or escalate.types.mdxis a published-doc correction that rides this PR because enforcement made the old line false.Out of scope, all Acceptance notes (no
reach:, nothing filed): the two blog posts that teachprecisionas decimal places (their shapes are already refused at parse);precisionaccepted on non-numeric types and inert there;precision: 0refusing every non-zero write; and the stale QA checklist prose onrecords-forms.field-type-constraints.Next:
needs:contract-reviewgoes on PR #20423 in this act. The isolated at-tier review runs when a slot frees; on PASS and a green head, the seat lands it through the queue.objectstack-fleet commentedon Sep 28, 2026
Close-out by the
domain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW) at 2026-09-28T15:11Z.Delivered. PR #20423 merged as
b98fbc21athrough the merge queue, and this card closedcompletedon itsFixesline. A field's declaredprecision(total digits, thepofDECIMAL(p, s)) is now enforced at the objectql write seam: an oversize write is refused400 VALIDATION_FAILEDwith the field codemax_precision. There is no storage or DDL change. At-tier record5868385995PASS atbb33240d.pm:dispatchedremoved. Onmainafter the merge, the validator arm andmax_precisionare present, and the regenerated field reference carries the total-digit describe.What remains, and who carries it.
CurrencyConfigSchema.precision … unchangedthroughos migrate meta, which PR #20251 makes false in the same major #20379 (pm:queue), its own card, untouched here.precision: 0;precisionas decimal places;Generated by Claude Code
feat(objectql,spec)!: enforce a field's declared `precision` (total d…
fix(spec): the protocol 16 → 17 conversion summaries and three descri…
fix(spec): the protocol 17 → 18 conversion summaries state each decis…
test(spec): the first data/ file group's test titles state each cited…
test(spec): the second data/ file group's test titles state each cite…