Skip to content

feat(spec)!: refuse a homeAmount rateField declaring scale below 12; document CURRENCY_FRACTION_DIGITS as CLDR fraction digits - #30644

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-30103-rate-field-scale-floor
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-30103-rate-field-scale-floor

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Fixes #30103

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-data/rules/field-types.md (L175-178) teaches rateField as a readonly number with no scale, which this diff still accepts; the floor ships in the refusal text, the changeset's FROM → TO and the upgrade-guide entry home-amount-rate-field-scale-floor.

维护者速读

  • 改了什么: 对象校验新增一条:homeAmount.rateField 指向的数字字段若声明 scale 小于 12(汇率的小数位,裁决 6469382727),建模时拒收,错误点名字段、下限 12 和改法;未声明 scale 照常通过。同时把 CURRENCY_FRACTION_DIGITS 与币种文档改写为 CLDR 小数位(不是 ISO 4217 最小单位),并列出实测不同的 16 个币种。
  • 为什么改: 引擎按汇率行原值(12 位小数)存汇率,字段声明的 scale 只影响显示;声明 scale 4 时页面显示 0.1389,与实际折算用的 0.138912345678 不一致,看到的汇率复算不出存下的本币金额。
  • 风险与回滚: 收窄了 18.3.0 已发布的接受集:已声明 scale 0–11 汇率字段的应用会在 os validate/保存时报错,改法一行(改成 scale: 12 或删掉 scale);已存的记录数据不受影响。已构建的旧产物若含此类字段,加载时整份被拒,需修源后重建;sys_metadata 里已存的对象行照常加载注册,启动日志打 [metadata_spec_invalid],下次保存被 422 拒收,直到把 scale 提到 12 以上或删掉。回滚即撤销本 PR。
  • 先看: ① packages/spec/src/data/object.zod.ts 的 rateField 分支(新增 scale 判断);② packages/spec/src/data/currency-config-home-amount.test.ts 末尾新 describe;③ .changeset/30103-home-amount-rate-field-scale-floor.md。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-home-amount.test.ts,预期 38 条全过,其中 scale 4/0/11 被拒、12/14/未声明通过。
  • 需要你做的: 无,席位落地(收窄需席位做一次契约级复审)。

What changes

  • refuseDishonestHomeAmount, the rateField arm (packages/spec/src/data/object.zod.ts). After the existing target and writer checks, a rate field declaring scale below 12 is refused at fields.AMOUNT.currencyConfig.homeAmount.rateField. The message names the field, its scale, the floor 12 and the fix (scale: 12 or more, or delete scale). The floor is a module-private HOME_AMOUNT_RATE_SCALE = 12 citing ruling 6469382727. It restates platform-objects' EXCHANGE_RATE_SCALE because spec cannot import that package. A rate column shared by consistent amounts is judged once, like the readonly mark.

  • Undeclared scale is accepted, and here is the reading behind it. At origin/main 1c5cee67:

    • The engine stores the rate row's own value, never rounded (packages/objectql/src/home-amount-projection.ts L38-44).
    • Every number column is one exact decimal, NUMERIC_COLUMN_PRECISION 65 / NUMERIC_COLUMN_SCALE 30 (packages/spec/src/data/numeric-column-representation.ts L174/L181; driver-sql table.decimal(name, 65, 30)), whatever scale the field declares.
    • The rate row itself carries scale 12 / precision 24 (sys_exchange_rate.rate).
    • The field schema says an omitted scale on a non-percent numeric type declares no fixed width. resolveFieldScale answers undefined for it, and the console's number formatter then uses maximumFractionDigits: 20. So an undeclared rate field shows all twelve stored digits.

    This follows from the existing field contract. It needs no choice beyond the ruling.

  • CURRENCY_FRACTION_DIGITS TSDoc (packages/spec/src/data/currency-fraction-digits.ts). It now opens "CLDR currency fraction digits — NOT ISO 4217 minor units" and carries a measured table of the 16 differing codes.

  • content/docs/data-modeling/currency.mdx. The fraction-digits sentence now lists the same 16 codes.

  • Changeset (@objectstack/spec minor, **BREAKING**, FROM → TO). It has a semantic upgrade-guide entry 18.home-amount-rate-field-scale-floor.ts (disposition registered).

The 16 codes, measured. The table's 162 entries were compared with SIX's ISO 4217 List One, published 2024-06-25: the iso-4217-list-one.xml that [email protected] ships, sha256 2dea9812…71b8b. The cross-read used the datasets/currency-codes codes-all.csv active rows (sha256 c4b6829a…7407f). Both give the same set:

ISO 4217 minor unit CLDR digits Codes
3 0 IQD
2 0 AFN, ALL, COP, HUF, IDR, IRR, KPW, LAK, LBP, MGA, MMK, PKR, SOS, SYP, YER

Not comparable: XDR and XSU (ISO N.A., table 2); HRK, SLL and ZWL are withdrawn and absent from List One. XCG is absent from the 2024-06-25 XML and equals 2 in the CSV. ANG, BGN and CUC are absent from the CSV's active rows and equal in the XML. SIX's own host (www.six-group.com) was refused by this container's egress proxy, so the list was read from those two copies. The triage's "about seventeen" included SLL. SLL is withdrawn, so no current ISO minor unit exists to compare it against.

Verification

Census: in-repo homeAmount.rateField targets declaring scale < 12 — 1 found, not changed

I ran git grep -n "rateField:" over the whole tree and read every rate field definition: content/docs, skills/, objectql, cli, plugin-audit, service-analytics, service-settings, dogfood and the spec tests. examples/ declares no homeAmount.

  • 1 hit: packages/objectql/src/engine-home-amount-projection.test.ts L331, invoice({ rate: { scale: 4 } }). It is an engine pin that a scale-4 rate field still stores the full rate (from PR feat(objectql): store a record-mode amount converted into the home currency on every write (multi-currency M2-2) #28302). It registers the object through engine.registry.registerObject, which does not run ObjectSchema parse, and it stays green (below). It pins what the engine does with an object stored before this release (stored rows are kept as authored). It is outside this PR's file surface (objectql) and is left as is.
  • Every other carrier is undeclared (Field.number({ readonly: true })) and is accepted.
Pins (ObjectStackDefinitionSchema.safeParse, the parse os validate and os compile call)

There is one new top-level describe in the existing harness file packages/spec/src/data/currency-config-home-amount.test.ts:

  • scale: 4 is refused. The pin asserts code: 'custom', path objects.0.fields.amount.currencyConfig.homeAmount.rateField, the message's first sentence (field, scale: 4, floor 12), the prescription, and that the message has no tracker number.
  • scale 0 and 11 are refused at the same path.
  • scale 12 and 14 are accepted, and an undeclared scale is accepted.
  • A shared rate column at scale: 4 gets exactly one issue.

File: 38/38. Full @objectstack/spec suite: 816/816 files, 25671 passed, 1 todo.

Ablation

node scripts/ablation-replace.mjs replaced the arm's condition with if (false && …). The anchor went x1 → x0 and the blob 7f70cb81 → c41206f1. Result: 3 red (the scale: 4, the 0/11, and the shared-column pins) and 35 green. The preservation pins stayed green. The tool restored the file: blob == HEAD 7f70cb81, git diff HEAD empty.

Dogfood battery and consumer tests
  • pnpm --filter @objectstack/dogfood test on 997cb02d30, after a build of the @objectstack/dogfood^... closure (exit 0): VERDICT command-exit 0, 420 passed / 3 skipped of 423 files, 3500 passed / 36 skipped of 3536 tests. No failure.
  • Consumer test files that parse homeAmount objects: each exit captured separately, all 0: objectql engine-home-amount-projection + home-amount-projection + home-amount-write-paths 3/3 files, 73/73 (the scale-4 engine pin among them); plugin-audit home-amount-recompute-audit.integration 1/1; service-analytics home-amount-consolidated-measure 18 passed / 18 skipped (PostgreSQL legs, no OS_TEST_POSTGRES_URL); service-settings localization-currency-lock 24/24; cli recompute-home-amounts 9/9.
Gates on 997cb02d30 (exit code captured before any pipe)
Family Exit
pnpm --filter @objectstack/spec build 0
pnpm --filter @objectstack/spec check:generated (14/14 up to date, --fix not needed) 0
dispatch-gates --commands derived set: 103 commands 103 × 0
dispatch-gates --ran reconciliation 0 (103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN)
pnpm check:adr-0087-registration 0 (registered home-amount-rate-field-scale-floor, new here)
pnpm check:doc-authoring 0
node scripts/check-nul-bytes.mjs 0
eslint --no-inline-config on the 4 changed .ts files 0

Six derived commands first exited 3 (PREREQUISITE NOT MET: unbuilt dists). Each was re-run after the closure build, and that re-run is the exit recorded for --ran: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:lean-entry-closure, check:dual-build-cjs-loads, all 0. check:generated was run at --max-old-space-size=6144 (CI's figure). Under the default heap, check:test-typecheck's tsc ran out of memory: no verdict, not a finding.

Acceptance notes

  • File surface, one addition. The ADR-0087 disposition has to be registered, because a FROM → TO body refuses no-migration-prescription, and every spec no (narrowing) changeset in stock is registered. So the diff adds packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts, which the generated registry picks up. Nothing else outside the claimed surface is touched.
  • Metadata-protocol: narrowed, read from the diff: the change refuses a value an author could write before (scale 0–11 on the named rate field). The card body says none, and the claim already says narrowed.
  • Prose still reading "ISO 4217 minor unit" outside this surface. git grep -n -i "ISO 4217 minor unit" -- packages/spec/src ':!**/*.test.ts' gives 20 lines. They are in field.zod.ts describes and refusals, five ADR-0087 migration entries, and registry.ts.template; field-types.mdx L237/L248/L596 and validation-rules.mdx L160 say the same. They describe how a currency amount's decimals display, and the console reads those from Intl (CLDR). Several quote ruling 5805782503's own remedy words. Noted, not filed. carrier: domain:spec seat.
  • HomeAmountSchema.rateField .describe() (field.zod.ts L657) does not mention the floor. It is outside this surface. The refusal carries the prescription. carrier: domain:spec seat.
  • The engine pin at scale 4 (census) stays as an engine-level pin for stored objects that predate this refusal.
  • Patch round (contract review ④ 3). The upgrade entry and the changeset now name the two metadata legs: an older built artifact carrying such a rate field fails its ObjectStackDefinitionSchema parse at load (packages/metadata/src/plugin.ts _parseAndRegisterArtifact, a throwing .parse before any registration, reached from start), so it is refused whole and must be rebuilt; a stored sys_metadata object row registers anyway with a [metadata_spec_invalid] line (packages/objectql/src/registry.ts registerItem → validate → ObjectSchema.parse, caught and logged) and its next save is refused 422 INVALID_METADATA (packages/metadata-protocol/src/protocol-save.ts, the object schema is ObjectSchema). Stored data rows are unaffected.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/currency-fraction-digits.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/currency.mdx (via rateField (literal, a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/field-types.mdx (via rateField (literal, a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/validation-rules.mdx (via rateField (literal, a string literal in refuseDishonestHomeAmount))
  • content/docs/protocol/objectql/schema.mdx (via rateField (literal, a string literal in refuseDishonestHomeAmount))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-3.mdx (via rateField (literal, a string literal in refuseDishonestHomeAmount))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/currency-fraction-digits.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 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 af8aab1b1c4f479b9b102da9227279534b4d27cd → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3cbe1a1fb6f87c73cb6711a28417be8dc1a8ac27 — the merge of head fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e into base af8aab1b1c4f479b9b102da9227279534b4d27cd, 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 3cbe1a1fb6f87c73cb6711a28417be8dc1a8ac27 && git checkout 3cbe1a1fb6f87c73cb6711a28417be8dc1a8ac27
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin af8aab1b1c4f479b9b102da9227279534b4d27cd fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e && git checkout -B drift-repro af8aab1b1c4f479b9b102da9227279534b4d27cd && git merge --no-ff fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e

node scripts/docs-audit/affected-docs.mjs --json af8aab1b1c4f479b9b102da9227279534b4d27cd

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs af8aab1b1c4f479b9b102da9227279534b4d27cd → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 997cb02d308ced5b5bccb081b0bec4f13fae256e
Local-runs: none
Scope: packages/spec/src/data/currency-fraction-digits.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts + packages/spec/src/data/field.zod.ts (FieldSchema.scale L1487, HomeAmountSchema.rateField L657), packages/spec/src/data/field-scale.ts (resolveFieldScale, the ruled meaning of an absent number scale), packages/spec/src/data/numeric-column-representation.ts (NUMERIC_COLUMN_SCALE 30), packages/spec/src/stack.zod.ts + packages/spec/src/system/environment-artifact.zod.ts (metadata: ObjectStackDefinitionSchema), packages/spec/src/migrations/types.ts (SemanticMigration), packages/spec/src/migrations/entries/semantic/18.currency-config-fixed-default-currency-required.ts and 18.field-currency-scale-refused.ts (siblings), packages/platform-objects/src/system/sys-exchange-rate.object.ts (EXCHANGE_RATE_SCALE 12 / EXCHANGE_RATE_PRECISION 24), packages/objectql/src/home-amount-projection.ts, packages/objectql/src/validation/record-validator.ts (ReadonlyValueScope), packages/objectql/src/registry.ts (registerObject, register → validate diagnostic), packages/objectql/src/engine-home-amount-projection.test.ts L331, packages/drivers/driver-sql/src/sql-driver-column-creation.ts L971-981, packages/cli/src/commands/validate.ts L375 and compile.ts L442, packages/metadata/src/plugin.ts L1383 (artifact ingest), docs/adr/0087-metadata-protocol-upgrade-contract.md D3, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, AGENTS.md Post-Task Checklist step 3, skills/objectstack-data/rules/field-types.md L175-178.

Inputs read: card #30103 body (Ruling-ref 6469382727, read on #27185: "scale 12 — twelve digits after the decimal point, total precision 24"), triage 6632541832, claim 6665552839, os-dev-report 6668122247, amendment 6668151276; PR #30644 body, its 6-file list, the net diff b6ae87c2..997cb02d (merge-base against main; the diff against current main is the same 6 files); the check-runs on the head (below). Reading time 2026-11-05T20:29Z.

Check-runs on 997cb02d30 at reading time: 49 success, 8 skipped, 0 failure. Green among them: Check Changeset, Lint · PM dispatch-gates, Lint & Repo Gates, Governed Surface Queue Guard, PR-body guards, Build Core, Build Docs, Dogfood Regression Gate, Spec property liveness, Type Check · workspace / consumer gates / source gates / debt ledger, Type Check UI, Test Core 2/5/6/7/8 of 8, Test UI 1/2/4/6/7/9/10/11/12/13/14/15/16 of 16. Still in progress, so no conclusion rendered here: Test Core 1/3/4 of 8, Test UI 3/5/8 of 16. None of the derived gate families has a red; the queue reads those shards itself when they land.

① Derived judgments

  1. Newly refused — right. An object whose record-mode currency amount declares currencyConfig.homeAmount.rateField naming a number field with a DECLARED scale of 0 to 11. One custom issue at fields.AMOUNT.currencyConfig.homeAmount.rateField naming the field, its scale, the floor 12 and the two fixes (object.zod.ts L2171-2184). It runs inside ObjectSchema's superRefine (L3950), so it reaches every door that parses an object: ObjectStackDefinitionSchema (os validate validate.ts L375, os compile compile.ts L442, the lint scorer, scaffold-validate, os migrate meta L1232), EnvironmentArtifactSchema.metadata (artifact ingest, plugin.ts L1383, a throwing .parse), the metadata save door (422), and the registry's registration diagnostic for a stored row (register → validate → ObjectSchema.parse, logged [metadata_spec_invalid], registered anyway; registry.ts L7248). Nothing else is refused: scale is read only off the parsed number target (typeof scale === 'number'), so a non-number scale is already FieldSchema's refusal, a non-number target is the existing arm above it, and scale on the amount or the home field is not read.
  2. Still accepted — right. scale 12 or more; an UNDECLARED scale (pinned: 'scale' in BASE_FIELDS.amount_rate is false and the parse passes); a rate column shared by amounts with the same currencyField and rateDate, judged once on the first holder (rateOwner), so the shared-column pin gets exactly one issue. Accepting an undeclared scale needs no new choice: an absent scale on a number field is already ruled "no fixed width — the value's natural precision" (maintainer ruling 5791809146, field-scale.ts L59-62; resolveFieldScale answers undefined), so an undeclared rate field shows every digit the column holds. The dev's reading is the platform's own, not a decision taken for the maintainer.
  3. The floor's premise holds against the cited code. The rate row is sys_exchange_rate.rate, Field.number({ scale: EXCHANGE_RATE_SCALE /* 12 */, precision: EXCHANGE_RATE_PRECISION /* 24 */ }), the ruling's numbers. The projection stores "the rate row's own value, never rounded" (home-amount-projection.ts L38-44). A rate field is readonly, and a readonly value is reached for its SHAPE only — ReadonlyValueScope lists min / max / scale / precision as the constraints it does NOT reach (record-validator.ts L707-709) — so the declared scale constrains no write; the column is numericColumnFor('number') = DECIMAL(65,30) on every SQL driver (sql-driver-column-creation.ts L971-981) whatever the field declares. So a scale below 12 reaches the display alone and shows a rate that does not reproduce the stored home amount — the exact dishonesty the card names. Refusing it at parse is the right seam; the floor 12 is the ruling's number, not a new one.
  4. Engine and stored data untouched — right. The diff moves nothing in objectql, platform-objects or driver code; every stored row keeps its digits. (Stored METADATA is another matter: see ④ 3.)
  5. CURRENCY_FRACTION_DIGITS TSDoc — right. The table's values are a CLDR currencyData probe (Intl.NumberFormat(...).resolvedOptions().maximumFractionDigits, provenance block L44-50), so "CLDR fraction digits, not ISO 4217 minor units" is what the code does. The sixteen codes re-derived here from the table: of its 33 zero-digit entries, 16 carry a non-zero ISO 4217 minor unit (IQD 3; AFN, ALL, COP, HUF, IDR, IRR, KPW, LAK, LBP, MGA, MMK, PKR, SOS, SYP, YER 2) plus SLL, whose ISO entry is withdrawn; every 3-digit entry (BHD, JOD, KWD, LYD, OMR, TND) matches ISO; no 2-digit entry is an ISO 0-, 3- or 4-minor-unit code; XDR and XSU are ISO N.A.. The triage's "about seventeen" was SLL. No export, key or value of the table moves.
  6. Public surface. No new export (HOME_AMOUNT_RATE_SCALE is module-private). The new semantic entry enters the generated registry (gen:migration-registry runs on build/test; no checked-in registry file) and so spec-changes.json and the upgrade guide, both built artifacts — check:generated and Lint & Repo Gates are green on the head. content/docs/data-modeling/currency.mdx says the same sixteen codes; skills/objectstack-data/rules/field-types.md L175-178 teaches rateField as a readonly number with no scale, which stays accepted — Skills-docs: none is right.

② Semver level

.changeset/30103-home-amount-rate-field-scale-floor.md: '@objectstack/spec': minor, feat(spec)!:, **BREAKING**, Clause-②: no (narrowing), Metadata-protocol: narrowed, a FROM → TO with the one-line fix, and one marker adr-0087: registered home-amount-rate-field-scale-floor resolving to the entry this diff adds. That is AGENTS.md Post-Task Checklist step 3 to the letter: a value refused that an author could write is narrowed; a kind takes minor on the package the diff moves (check-changeset-no-major.mjs LEVEL axis: narrowed beside a (narrowing) arm, grading minor); a breaking changeset carries its migration and exactly one ADR-0087 disposition, and a FROM → TO body cannot ride no-migration-prescription (check-adr-0087-registration.mjs R2), so registered is the only honest disposition — amendment 6668151276 widened the file surface for exactly that entry. The PR body carries the same two declaration lines. The card body's Metadata-protocol: none was wrong and the claim corrected it from the diff; the changeset and PR say narrowed. Check Changeset, Lint · PM dispatch-gates and Lint & Repo Gates are green on the head. Level: right.

③ Boundary flags

  • Undeclared scale accepted without a maintainer choice — answered, right (① 2): the meaning of an absent number scale is already ruled, and the card asked the dev to "decide and state" it, which the arm's TSDoc (L1965-1968), the refusal text, the changeset and the semantic entry all do.
  • 16 codes, not "about 17" — answered, right (① 5). SLL is withdrawn from the current list, so there is no current ISO minor unit to compare; the TSDoc says so by name.
  • The objectql engine pin at scale: 4 (engine-home-amount-projection.test.ts L331) — accepted as is. boot() registers through engine.registry.registerObject, whose body calls neither register nor validate (read at the head), so no ObjectSchema parse and no [metadata_spec_invalid] line; the pin keeps proving the engine-level fact the refusal rests on (a declared scale is no write constraint) for a row stored before this release. Out of this PR's surface; nothing to change.
  • 20 remaining "ISO 4217 minor unit" lines (field.zod.ts L492 / L1463 / L1488 / L3162 describes and refusal text, five step-18 entries, registry.ts.template L1630; field-types.mdx, validation-rules.mdx) — escalated as a should-fix follow-up card for the domain:spec seat, not this PR. For the sixteen codes the sentence "a currency amount's decimal places are its ISO 4217 minor unit" is now stated false by this very TSDoc (an IQD amount rounds to whole dinars, not fils), and two of the lines are PUBLISHED descriptions (precision L1463, scale L1488) that an author reads before any refusal. The lines quoting ruling 5805782503's remedy stay as quoted (AGENTS.md: a quoted ruling is not rewritten); the describes and refusal text are the seat's to correct.
  • HomeAmountSchema.rateField .describe() does not name the floor (field.zod.ts L657) — should-fix, carrier domain:spec seat. The describe is the published JSON-schema sentence an AI author reads before writing the field; today it says "a number field declaring readonly: true" and nothing about scale. One clause ("declaring no scale, or scale: 12 or more") closes it. Outside this PR's hits; acceptable as a follow-up, and trivially foldable into this PR.
  • The 12 exists twice, no pin (HOME_AMOUNT_RATE_SCALE object.zod.ts L1969, EXCHANGE_RATE_SCALE sys-exchange-rate.object.ts L20) — should-fix follow-up. Spec cannot import platform-objects, but platform-objects imports spec: export the floor from @objectstack/spec/data and let sys-exchange-rate.object.test.ts (which already pins EXCHANGE_RATE_SCALE to 12 at L124) pin the two equal, or declare scale on the rate row from the spec constant. Both cite ruling 6469382727, so drift is unlikely but unpinned.
  • File surface widened by one entry — right, recorded in amendment 6668151276 before the report.
  • open_questions: [], security_findings: none — nothing to escalate.

④ Design judgment

  1. packages/spec/src/data/object.zod.ts — holds. Judged against ruling 6469382727 and the first consumers (os validate / os compile through ObjectStackDefinitionSchema, artifact ingest, the save door). The arm sits after the target and writer checks and inside the holder === undefined branch, so it is judged once per column like the readonly mark; the path is the pointer's; the message names field, declared scale, floor, consequence and both fixes, and carries no tracker number (pinned by the test). Tried to break it: a scale on a formula or summary rate target — refused earlier as non-number; a first holder refused as non-number with a second sharer — the second gets its own existing refusal, unchanged; a rate field declaring precision below 24 — not refused, and rightly: on a readonly field precision is no write constraint either and reaches no display, so there is nothing dishonest to refuse; a scale above NUMERIC_COLUMN_SCALE (30) — the column caps it, display only, out of this card. One wording nit, not a finding: "stores the rate it used with all 12 decimals" reads as if every rate carries twelve; it is the rate row's own value, up to twelve. The changeset uses the same phrase.
  2. packages/spec/src/data/currency-fraction-digits.ts — holds. TSDoc only; the table and its key set are untouched, the new text describes what the probe measures (① 5), and the sixteen codes re-derive from the table. The claim "every other code here equals its ISO 4217 minor unit" rests on the CSV cross-read for XCG (absent from the 2024 List One); the PR body says so. No finding.
  3. packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts — holds, with one should-fix. Against SemanticMigration: kebab id, dotted surface, replacement, reason, acceptanceCriteria; no conversionIds (right: no D2 conversion, and D3 says a migration that cannot be declarative gets a structured TODO — "NOT mechanically converted: raising the scale or deleting it are both right, and which one is the author's display choice" is that reason); no relevantWhen (always listed; the conservative default). Against the siblings: 18.currency-config-fixed-default-currency-required names the three legs an upgrade meets — authored sources, OLDER BUILT ARTIFACTS, STORED sys_metadata object rows ([metadata_spec_invalid] boot line, next save 422) — and its acceptance criteria tell the agent to look for that boot line. This entry's reason ends "Stored rows are unaffected; the engine already kept every digit" (true of DATA rows) and its acceptanceCriteria covers objectstack validate alone. Read in the live tree, the other two legs exist: a built artifact declaring scale: 4 is refused at ingest (ObjectStackDefinitionSchema.parse, plugin.ts L1383, with the parse's own message and prescription), and a stored object row registers under [metadata_spec_invalid] at the same path and is refused at its next save. Neither is silent, so this is a should-fix: add one sentence per leg to reason and the boot-line check to acceptanceCriteria, the way the sibling does. Runtime strings carry no #NNNN; "ruling 6469382727" is the house citation form the siblings use.

Should-fix summary (none blocks): ④ 3 semantic entry's stored-metadata and built-artifact legs; ③ rateField describe naming the floor (field.zod.ts L657); ③ a pin tying HOME_AMOUNT_RATE_SCALE to EXCHANGE_RATE_SCALE; ③ follow-up card for the "ISO 4217 minor unit" describes and refusal text. No must-fix.

Implemented-by: claude/issue-30103-rate-field-scale-floor
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
os-dev-report — patch round (contract review ④ 3) · 2026-11-05T21:09Z

Head fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e, one commit on 997cb02d30, normal push. Files: packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts and .changeset/30103-home-amount-rate-field-scale-floor.md. No new files.

What changed. The entry's reason no longer ends "Stored rows are unaffected". It now names two metadata legs, in the sibling 18.currency-config-fixed-default-currency-required.ts form, and limits the last sentence to stored DATA rows (record values). acceptanceCriteria gains two instructions: rebuild every built artifact, and for stored rows find the [metadata_spec_invalid] boot line at fields.AMOUNT.currencyConfig.homeAmount.rateField, then raise the scale to 12 or more (or delete it) and save. The changeset's "Unchanged" line now says stored data rows, and a new line covers both legs. The PR body's risk line and acceptance notes were patched through relay issue_patch (run 42105415589, read-back identical, ends with the signed line).

(a) Older built artifacts, verified in the tree.

  • packages/metadata/src/plugin.ts _parseAndRegisterArtifact calls a throwing ObjectStackDefinitionSchema.parse(...) on the plain-stack shape. On the environment-artifact shape it calls EnvironmentArtifactSchema.parse, whose metadata is ObjectStackDefinitionSchema.
  • That parse runs before MemoryLoader and any registration, so the whole artifact is refused and nothing in it registers.
  • The parse runs from start through _loadFromLocalFile. optional covers only a missing local file, so the thrown Zod error fails the metadata plugin's start.
  • No D2 conversion rewrites this field.
  • No dedicated error code: it is the Zod error itself, with the issue at the rateField path.

(b) Stored sys_metadata object rows, verified.

  • packages/objectql/src/registry.ts registerItem runs this.validate(type, item). For object that is ObjectSchema.parse.
  • The parse sits inside a try/catch that logs [Registry] [metadata_spec_invalid] object/NAME fails the current spec schema: ... and then registers the item anyway.
  • The next save goes through packages/metadata-protocol/src/protocol-save.ts: resolveOverlaySchema(...).safeParse (object maps to ObjectSchema in metadata-type-schemas.ts). It throws with code 'INVALID_METADATA', status 422 and the structured issues.
  • Rows are kept as authored and no conversion touches the key.

Checks on fcb86f7 (exit codes captured before any pipe):

Check Exit
src/migrations/migrations.test.ts + src/data/currency-config-home-amount.test.ts (2 files, 195/195) 0
check-adr-0087-registration --base origin/main (registered home-amount-rate-field-scale-floor) 0
dispatch-gates --commands: 103 derived 103 × 0
--ran reconciliation (103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN) 0

The container restarted twice during the run. Commands 1-85 kept their first recorded codes. Two of them (check:skill-examples, check:dual-build-cjs-loads) first answered 3 (PREREQUISITE NOT MET, dists missing in the recreated worktree). I built those dists and re-ran both: 0. Commands 86-103 ran after the restart. Dogfood was not re-run: the change is a migration-entry string and a changeset only. Out of this round, as the seat ruled: the rateField describe floor, the 12-vs-12 pin, and the ISO 4217 minor-unit lines.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e
Local-runs: none
Scope: packages/spec/src/data/currency-fraction-digits.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts + packages/metadata/src/plugin.ts (start L855, _loadFromLocalFile L1603, _parseAndRegisterArtifact L1362, _convertArtifactForward L1216), packages/metadata-protocol/src/protocol-diff-delete-load.ts (loadMetaFromDb L1077), packages/metadata-protocol/src/protocol-registry-hydration.ts (hydrateOverlayIntoRegistry, L487), packages/metadata-protocol/src/metadata-diagnostics.ts (computeMetadataDiagnostics L242), packages/metadata-protocol/src/protocol-metadata-issues.ts L249, packages/metadata-protocol/src/protocol-save.ts (L1378-1420), packages/metadata-protocol/src/protocol-meta-type-schema.ts (resolveOverlaySchema L556), packages/spec/src/kernel/metadata-type-schemas.ts L93, packages/objectql/src/registry.ts (registerItem L7122-7250, validate L7423, registerObject L4550), packages/spec/src/conversions/entries/18/field-malformed-scale-precision-removed.ts, packages/spec/src/migrations/types.ts (SemanticMigration), packages/spec/src/migrations/entries/semantic/18.currency-config-fixed-default-currency-required.ts (sibling), .github/workflows/pr-automation.yml (changeset-check, L992-1002), plus every file the first record's Scope line names, carried.

Re-review after one patch round. Inputs read: card #30103 body and all five comments (triage 6632541832, claim 6665552839, os-dev-report 6668122247, amendment 6668151276); PR #30644 body (patched), its 6-file list, the net diff against main at fcb86f79 (6 files, +194/-13) and the patch-round delta 997cb02d..fcb86f79 (one commit, 2 files: the semantic entry +13/-3 lines, the changeset +2/-1); the first ## Contract review record 6668524557 (PASS at 997cb02d); the dev's patch-round report 6669638581; the check-runs on the head. Reading time 2026-11-05T21:17Z.

Check-runs on fcb86f79a4: 69 total, 59 success, 10 skipped (opt-in and matrix placeholders), 0 failure, 0 in progress. Green among them: Check Changeset (both runs; its changeset-check job carries check-adr-0087-registration --self-test and --base MERGE_BASE, pr-automation.yml L992-1002), Lint & Repo Gates, Lint · PM dispatch-gates, Governed Surface Queue Guard, PR-body guards, Build Core, Build Docs, Spec property liveness, Dogfood Regression Gate, Type Check · workspace / source gates / consumer gates / debt ledger, Test Core 1-8 of 8, Test UI 1-16 of 16. The three Test Core and three Test UI shards still running at the first record's reading time landed green.

Unchanged hits files. git diff 997cb02d..fcb86f79 -- packages/spec/src/data/object.zod.ts packages/spec/src/data/currency-fraction-digits.ts is empty (0 bytes): both are byte-identical to the head the first record judged. The first record's ① 1-6, ④ 1 and ④ 2 on them are carried here unchanged, by citation (comment 6668524557); nothing in this round touches them, and main moved nothing under them (the merge-base diff is the same 6 files). The effort below is on the delta.

① Derived judgments

  1. Carried (comment 6668524557 ① 1-6) — the refusal arm's accept set (declared scale 0-11 refused at fields.AMOUNT.currencyConfig.homeAmount.rateField; 12+ and undeclared accepted; shared column judged once), the floor's premise, the engine untouched, the CURRENCY_FRACTION_DIGITS TSDoc and the sixteen codes, the public surface. Unchanged files, unchanged judgment.
  2. No new accept-set or public-surface change in the delta — right. The patch round edits two prose strings of a SemanticMigration object (reason, acceptanceCriteria) and two lines of a changeset. No export, schema, key, id or surface moves; conversionIds stays absent (there is still no D2 conversion of this key, see ④ 3 (b)); relevantWhen stays absent. The generated upgrade guide and spec-changes.json pick the new text up through gen:migration-registry (Lint & Repo Gates and Build Core green).
  3. The entry's new claims are claims about other packages' behaviour, read as one change with the arm. Each is judged against the live tree in ④ 3. The arm itself is what makes them true: it runs inside ObjectSchema's superRefine, so every door below parses through it.

② Semver level

Unchanged axis: .changeset/30103-home-amount-rate-field-scale-floor.md still says '@objectstack/spec': minor, feat(spec)!:, **BREAKING**, Clause-②: no (narrowing), Metadata-protocol: narrowed, one adr-0087: registered home-amount-rate-field-scale-floor marker resolving to the entry this diff adds, and the FROM → TO line. The first record's ② reasoning (AGENTS.md Post-Task Checklist step 3; check-changeset-no-major.mjs LEVEL axis; check-adr-0087-registration.mjs R2) stands. The two changed lines are prose and agree with the entry word for word: Unchanged now says "every stored data row (record values)" (the entry: "Stored DATA rows (record values) are unaffected"); the new Metadata already on disk line names the built-artifact leg (refused whole at load, ObjectStackDefinitionSchema parse fails before anything registers, fix the source and rebuild) and the stored-row leg (kept as authored, loads and registers, [metadata_spec_invalid] at the rateField path, next save 422 INVALID_METADATA until scale is raised to 12 or more or deleted). The PR body carries the same Clause-② and Metadata-protocol lines, and its risk line and acceptance notes were patched to say the same two legs (read-back matches the changeset). Check Changeset is green on the head in both runs. Level: right, unchanged.

③ Boundary flags

  • Patch-round report 6669638581 — no open_questions, no new flag, no new file, no surface breach: the two files it names are exactly the two the delta touches (git diff --stat). Its (a) and (b) accounts are checked in ④ 3; one of them is incomplete as an account of the tree but the entry's text is right either way (④ 3 (b)).
  • The three should-fixes the seat declined — the rateField describe naming the floor (field.zod.ts L657), the HOME_AMOUNT_RATE_SCALE-to-EXCHANGE_RATE_SCALE pin, and the twenty "ISO 4217 minor unit" lines (now follow-up card Published currency text says decimal places are the ISO 4217 minor unit; the platform uses CLDR fraction digits (16 codes differ, e.g. IQD 0 not 3) #30648) — are dispositioned by the seat and not re-raised. Noted only so this record is complete about the first record's four.
  • The engine pin at scale: 4 (objectql engine-home-amount-projection.test.ts L331) — unchanged, and still the right reading: registerObject (registry.ts L4550) calls neither register nor validate (re-read this round, L4550-4800), so the pin parses no ObjectSchema and keeps proving the engine-level fact for a row stored before this release.
  • security_findings: none in both reports — nothing to escalate.

④ Design judgment

  1. packages/spec/src/data/object.zod.ts — holds, carried. Byte-identical to 997cb02d; comment 6668524557 ④ 1 applies as written (arm placement, path, message, the five break attempts, no #NNNN in the message).

  2. packages/spec/src/data/currency-fraction-digits.ts — holds, carried. Byte-identical to 997cb02d; comment 6668524557 ④ 2 applies as written.

  3. packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts — holds. The ④ 3 should-fix of the first record is closed; no new finding. Judged sentence by sentence against the live tree at the head, in the sibling's form.

    (a) "OLDER BUILT ARTIFACTS: an artifact whose object declares such a rate field fails its ObjectStackDefinitionSchema parse at load, before anything in it registers, so the whole artifact is refused and the metadata plugin's start fails with the Zod issue at that path; rebuild the artifact after fixing the source." True on every clause. MetadataPlugin.start (plugin.ts L855) reaches _loadFromLocalFile in all three bootstrap modes when an artifact source is set (L884-912); _loadFromLocalFile (L1603) wraps only the file read in try, and its catch tolerates ENOENT alone, and only under optional (L1623-1632); the call to _parseAndRegisterArtifact (L1634) is outside it, and start has no try of its own around the mode branch (the only try/catch nearby, in init, wraps the ObjectQL lookup). Inside _parseAndRegisterArtifact (L1362), every shape is a throwing .parse: EnvironmentArtifactSchema.parse on the envelope shapes (L1370, L1377; its metadata is ObjectStackDefinitionSchema, carried from the first record), ObjectStackDefinitionSchema.parse on the bare shape (L1383). Each runs before new MemoryLoader() (L1399) and before the sink's first manager.register (L1448-1451), so a Zod throw leaves nothing of that artifact registered and propagates out of start. D2: _convertArtifactForward (L1216) runs applyArtifactForwardConversions first, and no conversion in packages/spec/src/conversions/entries/18/ reads rateField or homeAmount (git grep empty); the one conversion whose surface is object.fields.*.scale, field-malformed-scale-precision-removed, strips a scale only when it is non-integer or negative (Number.isInteger(value) && value >= 0 passes through), so a scale: 4 reaches the parse as authored. No dedicated error code: it is the Zod error, as the dev says. One wording nit, not a finding: "at that path" — the issue's path inside an artifact parse is prefixed (objects.N.fields… / metadata.objects.N.fields…, the first record's pin shows objects.0.fields.amount.currencyConfig.homeAmount.rateField); the sibling uses the same loose form, and an upgrader grepping the error for homeAmount.rateField finds it.

    (b) "STORED sys_metadata OBJECT ROWS are kept as authored and nothing converts one: such a row still loads and registers unchanged, boot logs it with a [metadata_spec_invalid] line at fields.AMOUNT.currencyConfig.homeAmount.rateField, and its next save is refused with 422 INVALID_METADATA until the scale is raised to 12 or more or deleted." True on every clause, and true on BOTH boot branches, which the dev's report names only one of. The boot load of stored rows is loadMetaFromDb (protocol-diff-delete-load.ts L1077). For every row it first runs host.convertStoredItem (the D2 chain; nothing converts this key, as in (a)), then computeMetadataDiagnostics(normalizedType, data) (L1172; metadata-diagnostics.ts L242 resolves getMetadataTypeSchema('object'), which is ObjectSchema at kernel/metadata-type-schemas.ts L93, and safeParses), and on valid: false logs [Protocol] [metadata_spec_invalid] stored object/NAME fails the current spec schema even after conversion (PATH: MESSAGE) (L1188) with the first issue's dotted path (protocol-metadata-issues.ts L249, path.join('.')), so the line reads fields.amount.currencyConfig.homeAmount.rateField: … — the path the entry names. That happens BEFORE the object branch splits (L1196-1282): a package-layered row goes to engine.registry.registerObject (L1266-1271; no validate, no registry line), every other row to hydrateOverlayIntoRegistry → registry.registerItem (hydration L487), whose validate (ObjectSchema.parse, registry.ts L7423-7425) is wrapped in the try/catch at L7244-7251 that logs the second line, [Registry] [metadata_spec_invalid] object/NAME …, and registers anyway. So "still loads and registers unchanged" and "boot logs it with a [metadata_spec_invalid] line at that path" hold whichever branch the row takes; the dev's (b) cites only the registry line, which a layered row never gets, but the entry's sentence does not depend on which emitter fires. Next save: protocol-save.ts L1397-1420 resolves the type's schema (resolveOverlaySchema L556 → getMetadataTypeSchema('object') → ObjectSchema), safeParses the body, maps the issues with zodIssuesToMetadataIssues and throws code 'INVALID_METADATA', status 422 (the block's docblock L1378-1381 and the throw below it). Nothing in that door grandfathers a stored row, so the save stays refused until the body parses: scale 12 or more, or absent, exactly the entry's two fixes. Tried to break it: a row whose rate field has NO scale — not refused, not logged, not an upgrade case (right, the arm reads typeof scale === 'number' only); a row refused for another reason as well — the boot line prints the FIRST issue only, so the rateField issue may not be the one shown; the _diagnostics the read carries holds all of them, and the entry's acceptance criterion sends the upgrader to the boot line, where the sibling sends them too. Not a finding; the same limit holds for every entry in the family.

    "Stored DATA rows (record values) are unaffected; the engine already kept every digit of the rate." True and now scoped right: the old sentence "Stored rows are unaffected" was the first record's objection; "DATA rows (record values)" names what it meant and the two metadata legs now sit beside it.

    acceptanceCriteria: "Rebuild every built artifact after the fix" follows from (a) (the artifact file is parsed, never rewritten). "For stored sys_metadata object rows, look for the [metadata_spec_invalid] boot line at fields.AMOUNT.currencyConfig.homeAmount.rateField: raise that rate field's scale to 12 or more, or delete it, and save the object" is the sibling's form and matches (b), including the boot line's own prescription ("correct it in Studio … or delete the sys_metadata row"). An upgrader following the entry now meets all three legs — authored source (objectstack validate), built artifact (rebuild), stored row (boot line, fix, save) — and is told nothing the tree contradicts.

    Against SemanticMigration (types.ts L99-134): id, surface, replacement, reason, acceptanceCriteria present and non-empty; conversionIds rightly absent (the entry judges no mechanical edit, and none exists); relevantWhen absent (always listed). Runtime strings carry no #NNNN (grep -E '#[0-9]{3,}' over the file is empty); "ruling 6469382727" stays the house citation form. The migrations.test.ts and currency-config-home-amount.test.ts suites the dev ran are inside Test Core, green on the head.

Should-fix summary: none new. The first record's ④ 3 is closed by this delta; its other three are dispositioned by the seat (one as card #30648). No must-fix.

Implemented-by: claude/issue-30103-rate-field-scale-floor
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

维护者速读

  • 改了什么:对象里记录币种金额的 homeAmount.rateField 若指向一个声明了 scale 0 到 11 的数字字段,建模时(os validate、os compile、保存元数据)就会被拒,报错点名字段、它的 scale、下限 12 和两种改法:改成 scale: 12 或以上,或删掉 scale。另外 CURRENCY_FRACTION_DIGITS 和币种文档写明这是 CLDR 小数位,不是 ISO 4217 最小单位,并列出两者不同的 16 个币种。
  • 为什么改:引擎存汇率一直是 12 位全精度,字段上声明的 scale: 4 只影响显示。界面显示 0.1389,实际用的是 0.138912345678,看到的汇率算不回已存的本位币金额。汇率 12 位小数由裁决 6469382727 定。
  • 风险与回滚:收窄了已发布的接受集,changeset 标 **BREAKING**,写了 FROM → TO,并登记了升级指南条目 home-amount-rate-field-scale-floor。不写 scale 或写 12 及以上照旧接受。已存的业务数据不受影响。已构建的产物里若有这种字段,加载时整份拒绝,要改源码后重新构建;库里已存的对象定义照常加载,启动日志报 [metadata_spec_invalid],下次保存时被拒(422)。回滚即 revert 本 PR。
  • 先看:packages/spec/src/data/object.zod.ts refuseDishonestHomeAmount 的 rateField 一支 → packages/spec/src/data/currency-config-home-amount.test.ts 新增的 describe → packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/currency-config-home-amount.test.ts,预期全过;去掉 scale 那一支后 3 条转红,其余 35 条照旧绿。
  • 需要你做的:无,席位落地。
  • 席位意见:接受。首轮契约复核 PASS(6668524557,Head-sha 997cb02d),有四条 should-fix:席位采纳一条,让升级指南条目补上已构建产物和库内对象定义两条路径;补丁轮后再复核 PASS(6669807764,Head-sha fcb86f79),没有新的发现。另外三条不采纳,其中一条已立后续卡 Published currency text says decimal places are the ISO 4217 minor unit; the platform uses CLDR fraction digits (16 codes differ, e.g. IQD 0 not 3) #30648,理由见第 5、6 项。

席位核对

ACCEPT · head fcb86f79a42b967c781e8ae92ab9c4e17eeb8c3e · 2026-11-05T21:20Z

  1. 形态:
    • draft,base main,正文 Fixes #30103,全文只有这一个闭合关键词。
    • 6 个文件都在认领 6665552839 的文件面内:object.zod.ts 的 rateField 一支、currency-fraction-digits.ts(只改 TSDoc)、currency.mdx 的小数位那几句、测试、changeset;语义条目由修订 6668151276 补进文件面。
    • ui/**、packages/services/**、packages/objectql/** 引擎代码、skills/**、docs/adr/**、.claude/**、AGENTS.md 都没碰,也没有 content/docs/releases/。
  2. 前提实测:
    • 在 origin/main 上,refuseDishonestHomeAmount(object.zod.ts L1990)的 rateField 一支只判只读和类型,不读 scale:main 上没有 HOME_AMOUNT_RATE_SCALE。
    • 汇率行 sys_exchange_rate.rate 是 EXCHANGE_RATE_SCALE = 12 / EXCHANGE_RATE_PRECISION = 24(sys-exchange-rate.object.ts L20、L27),与裁决 6469382727 一致。
    • 复核核到:投影把汇率行的原值不经舍入写进 rate 字段,只读字段上的 scale 不约束写入,列一律是 DECIMAL(65,30),所以声明的 scale 只影响显示。前提成立。
  3. 门禁 check-runs:
    • head fcb86f79 上 59 success、10 skipped、0 failure,全部跑完,Test Core 1-8、Test UI 1-16、Dogfood Regression Gate、Check Changeset(内含 check-adr-0087-registration)都在其中。
    • 首轮契约复核 6668524557 在 Served-tier: CONTRACT_REVIEW_TIER 下 PASS,覆盖三个 hits(object.zod.ts、currency-fraction-digits.ts、语义条目),以及汇率行、投影、记录校验、SQL 列、CLI 与产物加载门的所引代码。
    • 再复核 6669807764 PASS:两个未改动的 hits 文件与 997cb02d 逐字节相同,沿用首轮判断;条目的两条新路径逐句对照 plugin.ts、protocol-diff-delete-load.ts、registry.ts、protocol-save.ts 核实,库内对象行在两条启动分支上都会报 [metadata_spec_invalid]。
    • dev 本地两轮各 103 条派生门禁 --ran 0 NOT-MEASURED,只证本地子集;首轮六条、补丁轮两条 exit 3 都是前置包没构建,构建后重跑都是 0。
    • (narrowing) 要求的 dogfood_battery:在 997cb02d 上 command-exit 0(420 个文件通过、3 个跳过)。补丁轮只改了条目字符串和 changeset 文字,schema 行为不变,head 上的 Dogfood Regression Gate 也是 success。
    • 消融(首轮):把 rateField 那一支的条件前缀成 false 后,3 条转红(scale 4、0/11 两端、共享列),35 条保持绿;还原后 blob 等于 HEAD,git diff HEAD 为空。
  4. 席位自读(TIER_DEFAULT,hits 之外的部分):
    • 测试:新 describe 断言拒绝只有一条 custom issue,路径是 objects.0.…homeAmount.rateField,文案含改法且不含编号;0 和 11 两端各拒一条;12 及以上、不写 scale(断言夹具里确实没有 scale 键)、共享汇率列只报一次都有覆盖。
    • currency.mdx:小数位那段改成 “CLDR 小数位,不是 ISO 4217 最小单位”,16 个币种逐个列出,与 TSDoc 和 changeset 一致;USD → CNY 的例子不变。
    • changeset 逐句核过:
      • @objectstack/spec minor,feat(spec)!:,Clause-②: no (narrowing) / Metadata-protocol: narrowed,与修正后的认领一致;卡正文原写的 Metadata-protocol: none 是错的,认领已按 diff 改正。
      • 处置标记 adr-0087: registered home-amount-rate-field-scale-floor 与新条目同名。
      • “Unchanged” 一行限定为已存的业务数据;新加的 “Metadata already on disk” 一行与条目的两条路径逐字一致。
      • FROM → TO 给了两种改法。
  5. 接受的偏差:
    • 复核 should-fix:HomeAmountSchema.rateField 的 .describe()(field.zod.ts L657)没写下限 12。dropped:这句描述没有说错,不写 scale 本来就被接受;声明了低于 12 的 scale 时,每扇解析门都会当场拒绝,报错点名下限和两种改法,不会悄悄合进去。它也不在本卡文件面内。
    • 复核 should-fix:12 这个数在 spec(HOME_AMOUNT_RATE_SCALE)和 platform-objects(EXCHANGE_RATE_SCALE)各写一次,没有 pin 绑住两者。dropped:两处都引同一条裁决 6469382727,要改只能出新裁决,那时两处会一起改。加 pin 要动 platform-objects 的测试,不在文件面内。属于卫生提醒,不是缺陷,不立卡。
    • 再复核记下的措辞小瑕疵(产物解析时报错路径带 objects.N. 前缀)与兄弟条目写法相同,不算问题。
    • 补丁轮把首轮的 ④ 3 收口了;为它挪 head 并做了再复核。
    • PR 没设 assignee 和 reviewer,Tier S 不交人。
  6. 范围外去向和 Skills-docs::
  7. 落地条款:check-governed-merges.mjs --pr 30644 已在本 head 读过:6 个路径 0 个命中 governed 登记,207 行(+194 / -13)≤ 3000,属于 Tier S;落地前再读一次。CI 已全绿,经 relay pr_ready + automerge_enable,跟到 MERGED,同一动作里摘 pm:dispatched。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 21:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit b77c160 Nov 5, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30103-rate-field-scale-floor branch November 5, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant