Skip to content

feat(spec): currencyConfig.homeAmount — a record-mode amount declares its stored home-currency projection by pointer (multi-currency M2-1) - #28096

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-27191-home-amount-pointer
Oct 31, 2026
Merged

os-zhuang merged 8 commits into
mainfrom
claude/issue-27191-home-amount-pointer

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #27191

⛔ not released alone — R2: pairs with #27192 (the write-time projection that writes the home amount), #27185, #27190, #27198, #27193, #27196 (one Version Packages release; ADR-0144 D9: "the homeAmount key never ships ahead of the engine")
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here

Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.

维护者速读

What changes

  • CurrencyConfigSchema.homeAmount (packages/spec/src/data/field.zod.ts, ADR-0144 D5): a new optional key, a closed shape { field, rateDate, rateField }. All three pointers are required whenever homeAmount is declared, and none may be empty. ADR-0144 D5 corrects the card's draft rateField? to required. A missing pointer and an empty one each get their own message with a remedy. Near-miss spellings (rate, date, homeField, …) are steered to the declared pointer by the shape's aliases. refuseRecordModeKeyMismatch gains one arm: homeAmount under dynamic or fixed (including the defaulted mode) is refused at homeAmount, because only a record amount is converted.
  • refuseDishonestHomeAmount (packages/spec/src/data/object.zod.ts), in ObjectSchema's superRefine beside the currencyField arm. It judges what the pointers name, against the authored field map. There is one located issue per offending pointer, at fields.AMOUNT.currencyConfig.homeAmount.POINTER. Each issue carries a remedy and the object's candidate fields.
    • The amount must be a stored currency field. A formula amount is computed on read and never written, so it is refused at homeAmount itself.
    • field must name another declared currency field in dynamic mode. An absent currencyConfig counts as dynamic. This follows ADR-0144 D8 rule 5: "the home field is a dynamic-mode currency field", so metadata never names the home currency. A second amount may not claim a home field that another amount already writes.
    • rateDate must name a declared date or datetime field. Nothing defaults it to created_at.
    • rateField must name a declared number field. Two amounts may share one rate field only when they name the same currencyField and the same rateDate, because otherwise they would store two different rates in one column.
    • Platform-written mark. The home field and the rate field are written only by the engine. They say so with the mark the spec already has for that, readonly: true, which drops every non-system write of the field from the payload. Neither field may also declare an expression, which would be a second writer. No new key is invented.
  • Skills (skills/objectstack-data/rules/field-types.md, governed skills/**): the currency block teaches homeAmount in one example line and one paragraph; the example names are guidance, "never a contract" (ADR-0144 D8 rule 2). Paid for by deleting restatement in the same file: the intro line, the "Text with Max Length" example (a duplicate of the file's first code block), the "Master-Detail with Cascade" example (the Relational table's row says it) and a removed-key history comment. 3207 → 3181 tokens, 323 → 304 lines; bundle 219711 → 219685 tokens.
  • Changeset .changeset/27191-home-amount-pointer.md: @objectstack/spec minor and @objectstack/skills minor. The body carries Clause-②: yes (widening), Metadata-protocol: added and the R2 pairing line.
  • Generated, not hand-edited: packages/spec/authorable-surface/data.json (gen:schema) and content/docs/references/** (gen:docs).
  • ADR anchors: the ADR-0144 entries on field.zod.ts and object.zod.ts gain the D5 invariant.

Tests

  • New packages/spec/src/data/currency-config-home-amount.test.ts:
    • The config door. The accepted shape round-trips. A record config without homeAmount parses byte-identical to before. homeAmount is refused under fixed, dynamic and the defaulted mode. Each missing pointer and each empty pointer is refused at its own path. An unknown key is steered to rateField.
    • The object door. The honest shape is accepted, including datetime and an explicit dynamic home field, and parse(parse(x)) equals parse(x). A consistently shared rate field is accepted. Each refusal is pinned with code, path and the message's first clause:
      • field: points into nothing, at the amount itself, at a non-currency field (a number, a currency formula), at a fixed or record home field, or at a home field another amount already writes;
      • rateDate: points into nothing, or at a non-date field;
      • rateField: points into nothing, at a non-number field, or at a rate field shared inconsistently;
      • a home or rate field without readonly: true, or one declaring an expression;
      • a formula amount.
  • The R1 suite currency-config-record-mode.test.ts stays green, unchanged.
  • One ablation, a gate run, and the package test and typecheck readings come with the os-dev-report comment on spec(field): homeAmount: { field, rateDate, rateField? } on a record-mode amount — the stored home-currency projection declared by pointer (multi-currency M2-1, epic #8345; released with M2-2) #27191, at the final head.

Acceptance notes

Verification

Gate readings (dispatch-gates.mjs --commands on the final diff, each exit code captured before any pipe, --ran reconciliation), the package test and typecheck runs, and the ablation are reported with the head sha in the os-dev-report comment on #27191.

…}` on a record-mode amount

A `record`-mode amount declares, by three required pointers into the same
object, where the engine stores it converted into the home currency, which
document date picks the rate, and where the rate used is stored (ADR-0144 D5).

- `CurrencyConfigSchema` gains `homeAmount`, a closed shape whose three
  pointers are required and non-empty; it is refused outside `record`.
- `ObjectSchema`'s superRefine gains `refuseDishonestHomeAmount`: the amount
  is a stored `currency` field; `field` names another `dynamic`-mode
  `currency` field no other amount stores into; `rateDate` a `date` /
  `datetime` field; `rateField` a `number` field, shared only by amounts with
  the same code field and document date. The home and rate fields declare
  `readonly: true` and no `expression`. Each refusal is located at its
  pointer and carries a remedy and the object's candidates.
- ADR-0144 anchors on field.zod.ts and object.zod.ts gain the D5 invariant.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…me-amount pointer

The currency block of `objectstack-data/rules/field-types.md` gains the
`homeAmount` example and one paragraph, paid for by deleting restatement in
the same file (3207 -> 3181 tokens). The changeset declares `@objectstack/spec`
and `@objectstack/skills` minor and marks the key as released with the
write-time projection (ADR-0144 D9, R2).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/data-api.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/data-modeling/currency.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/field-types.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/fields.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/formulas.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/data-modeling/validation-rules.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/getting-started/common-patterns.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/protocol/objectql/schema.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/protocol/objectql/types.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))

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

  • content/docs/releases/v15.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; a string literal in refuseDishonestHomeAmount))
  • content/docs/releases/v17/17-5.mdx (via currencyConfig (literal, a string literal in a comment on a changed line; 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/authorable-surface/data.json) — 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 59 of 211 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 168 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 3fd4d6bafbe1c8198070fb3343322e3bff598439 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3fd4d6bafbe1c8198070fb3343322e3bff598439

⚠️ 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 3fd4d6bafbe1c8198070fb3343322e3bff598439 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…and the strictness ledger for `homeAmount`

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…e engine writes it

The form/Zod reconciliation gate refuses a key the Zod accepts and the form
does not offer. `homeAmount` is offered in no form yet, deliberately: nothing
writes the home amount until the write-time projection lands in the same
release, so the gate's ledger carries a `planned` omit, the precedent of the
other spec-first keys.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 361f8f25cca4b3635c31de3e6fa187483700dcff · 2026-10-30T15:07Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 361f8f25cca4b3635c31de3e6fa187483700dcff
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts, skills/objectstack-data/rules/field-types.md + packages/spec/src/data/currency-config-home-amount.test.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json, scripts/adr-anchors/packages__spec__src__data__object.zod.ts.json, .changeset/27191-home-amount-pointer.md, docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md (on main, the ruling this implements), packages/objectql/src/validation/record-validator.ts and FieldSchema.readonly (read for what the readonly: true mark does on the write path), packages/formula/src/formula-return.ts (read as the second consumer of CurrencyConfigSchema), content/docs/data-modeling/currency.mdx (read for the claim the PR body makes about it). The rest of the diff (generated content/docs/references/**, packages/spec/authorable-surface/data.json, the strictness-ledger counts) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs read: card #27191 (body, both comments: the epic PM's queue order and the dev's claim), PR #28096 (body, file list, net diff of all 13 files), the 50 check-runs on the head (every non-skipped run success, including Lint & Repo Gates, Check Changeset, Spec property liveness, Governed Surface Queue Guard, PR-body guards, Test Core 1-8, Type Check). No dev report comment exists on #27191 at the time of this read (see ③).

① Derived judgments

  • CurrencyConfigSchema gains an optional homeAmount key (HomeAmountSchema, a closed strictObject with field, rateDate, rateField, each a required non-empty string, with near-miss aliases). Accept-set widens by one optional key under record; a config without it parses byte-identical (pinned). Right. ADR-0144 D5 makes all three pointers required and corrects the card's rateField?; the schema does that, and both ADR-anchor invariants record it as a do-not-relax.
  • refuseRecordModeKeyMismatch refuses homeAmount under fixed, dynamic and the defaulted mode, at homeAmount, with a remedy. Right. Only a record amount is converted; anywhere else the key would be declared and read by nothing (ADR-0078). Pinned for all three modes.
  • The config door sees only names; the object door (refuseDishonestHomeAmount, in ObjectSchemaBase's superRefine beside refuseDishonestRecordCurrencyField) judges what they name. Same split as the R1 currencyField arm, same reason (only the object holds the field map), same exclusion of ObjectExtensionSchema. Right.
  • Public surface: HomeAmountSchema is a module-local const, not exported; CurrencyConfigSchema's inferred type gains homeAmount?. formula-return.ts parses the same schema for a currency formula and reads only currencyMode / currencyField, so a stray homeAmount there is inert at that reader and refused at the object door (a formula amount with homeAmount is refused at homeAmount, pinned). The analytics relay reads currencyMode / currencyField only. No consumer breaks. Right.
  • Platform-written mark = the existing readonly: true, author-declared and refused if absent; expression on the home or rate field refused. FieldSchema.readonly at the head: "a non-system write to this field is silently dropped from the payload on UPDATE and on INSERT ... Exempt from the strip on BOTH paths: isSystem writes". That is exactly the contract the card's "read-only at the door" asks for; no new key is invented (ADR-0078 would have made a new one declarable-but-inert until objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192). Right. The alternative (the parse injecting readonly onto the pointed-to fields) is not possible in a refinement and would hide the invariant from the author; refusing with a remedy is the honest shape.
  • Four refusals beyond the card's list (a formula amount; a home field claimed by two amounts; a rate field shared by amounts with different currencyField or rateDate; an expression on a home or rate field). Each refuses a shape where the engine would store a wrong value or a second writer could overwrite it; the sharing allowance (same code field, same date, hence the same rate row under D3) is sound. Right, each pinned with code, path and message head.
  • Studio field form offers no homeAmount row; the form-reconciliation ledger carries an omit entry with a why that names D9. Right (the editor is ui: home field read-only with "converted at RATE on DATE"; live preview when a rate exists; Studio editor for homeAmount (multi-currency M3-2, epic #8345; the read-only display is released with M2) #27196 M3-2).
  • Skill text teaches the key in one example line and one paragraph and pays for it by deleting a duplicated text example, a master_detail example the Relational row already states, an intro line and a removed-key history comment. No other file anchors the deleted headings (grepped skills/, docs/, content/, scripts/, .claude/). Right as a token trade; the Tier H approval of the text stays the maintainer's.

② Semver level

.changeset/27191-home-amount-pointer.md: @objectstack/spec: minor, @objectstack/skills: minor. A new optional key under an existing config, no existing config changes meaning, record without homeAmount parses as before: minor is right for spec. skills minor has precedent for a skill teaching a new capability (e.g. the count-rollup and queues changesets on main). The PR body and the changeset both carry Clause-②: yes (widening) and Metadata-protocol: added, matching the card's line and the diff. Clause-②: yes (widening) — correct. The changeset carries the "Not released alone" sentence naming ADR-0144 D9 / R2, in the same form as the R1 changesets on main.

③ Boundary flags

  1. "What holds the key from a lone release: nothing mechanical." Confirmed from the cited code: no gate, ledger or release script reads a pairing marker; the three controls (the PR-body line, the changeset sentence, the maintainer's hand on Version Packages under Prime Directive Add missing Field.phone() helper and factory methods for Action/Dashboard/Report #15) are the same three R1 used. Answered, not this PR's to fix; the epic PM and the maintainer own the pairing. The record repeats the ⛔: this changeset must not reach a Version Packages release without objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192.
  2. readonly and objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192. isSystem writes are exempt from the strip on both paths, so the projection has a documented door. Two things the describe also says, for objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 to pin: (a) "A stripped INSERT field still falls back to its defaultValue" and (b) the UPDATE-only preserveAudit exemption admits author-declared business readonly fields, so a historical import can write the home field directly. Neither is refused here. Escalated to objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 as hand-offs; see ④ should-fix 2.
  3. Refusals beyond the card's list. Judged in ①: each right.
  4. "Not touched: content/docs/data-modeling/currency.mdx." Read at the head: its "The home currency" section says consolidation "comes in a later release. Until then, total ... by grouping by its code field." That sentence turns false the moment R2 ships, and no R2 card is a docs card (migrate: adding a homeAmount field to an existing object is the normal add-column path; backfill = recompute — one proving test (multi-currency M2-4, epic #8345) #27194 is a migrate proving test). Escalated to the epic PM: name the card that rewrites that page before the R2 Version Packages release, or add it to this PR.
  5. PR body "Verification" points at an os-dev-report comment on spec(field): homeAmount: { field, rateDate, rateField? } on a record-mode amount — the stored home-currency projection declared by pointer (multi-currency M2-1, epic #8345; released with M2-2) #27191 "at the final head". No such comment exists on the card as read. The check-runs on the head are the gate verdicts and are green, so the review does not depend on it; but the implementation claim's report is outstanding. Escalated to the dev seat: post it or amend the body.
  6. ADR-0144 Open item 1 (the spelling of the two pointers) is settled by this head as rateDate / rateField, and the ADR says "This record is amended with each answer." The ADR is not amended in this PR. Escalated to the spec seat as a Tier H follow-up (should-fix 3).
  7. The two generated artefacts the drift bot could not anchor (authorable-surface/data.json) are gated by Spec property liveness, green on the head. No flag.

No open_questions block is on the PR body or the card.

④ Design judgment

packages/spec/src/data/field.zod.ts — against ADR-0144 D5, D8 rules 2 and 5, and its first consumer ObjectSchema / formula-return.ts. Holds. Tried: homeAmount under each non-record mode (refused with remedy, pinned); a missing or empty pointer (refused at its own path, pinned); a near-miss key (steered by alias, pinned); a record config without the key (byte-identical, pinned); parse(parse(x)) on the config (equal, pinned). The error-function form of z.string({ error }) is the zod 4 API the file already uses. Describe text names the three target types, readonly: true, the no-created_at rule and the D9 pairing. No finding.

packages/spec/src/data/object.zod.ts — against the same ruling and the field map it reads. Holds. Tried to break refuseDishonestHomeAmount: pointer at the amount itself, into nothing, at a number, at a currency formula, at a fixed or record home field, at a home field another amount owns, at a non-date, at a non-number, at a rate field shared with a different date; a home or rate field without readonly, or with expression; a formula amount — every one refused at the located path and pinned. Declaration order decides who owns a shared home field (first claimant keeps it) and the candidates list excludes owned fields; a consistent rate-field sharer is not re-judged for the mark because it stores into the already-judged column. readonly has .default(false) so the === true test is exact on parsed input. isSystem writes bypass the strip, so the mark does not block the engine. Two should-fixes, no must-fix:

skills/objectstack-data/rules/field-types.md — against the spec it teaches at this head (no eval prompt is on the card; the card asks for "skills guidance"). Holds: an author who follows the example line and paragraph produces metadata the two doors accept, and the paragraph states the three refusable facts ('record' only, all three required, both targets readonly: true). Two should-fixes, no must-fix:

  • Should-fix 3 (under-teaching): the paragraph says rateDate (date); the spec accepts date or datetime. Say date/datetime.
  • Should-fix 4 (stale row): the Numbers table's currency row lists currencyConfig as "(currencyMode, defaultCurrency)"; it has carried currencyField since R1 and now homeAmount. The file's own rule says config columns list real keys; the row should name all four.

Should-fix 5 (outside the hits, Tier H follow-up): amend ADR-0144 Open item 1 with the settled spelling, as the ADR says it will be.

Implemented-by: claude/issue-27191-home-amount-pointer
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

维护者速读(席位终稿 · head 361f8f25)

席位意见: 按卡交付,ACCEPT(车道部分)。建议现在就合入,并由您按住发版,因为两个后续卡要在它上面开发。

席位核对 · ACCEPT (lane part) · card #27191 · 2026-10-30T15:30Z

  1. 形态: 13 files, +607 / −42 at head 361f8f25 (5 regenerated), draft, Fixes #27191 once, Clause-②: yes (widening) / Metadata-protocol: added / Skills-docs: updated here. Changeset: @objectstack/spec and @objectstack/skills minor, plus an R2 pairing line. skills/** is governed, so this is Tier H. Below 3000 lines.
  2. 前提实测: holds. spec(field): currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1, epic #8345) #27184 is closed and PR feat(spec): currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1) #27268 merged; there was no homeAmount on main; ADR-0144 D5 makes rateField required, and the schema requires it.
  3. 门禁 check-runs: read on 361f8f25: 50 runs, 41 success, 9 skipped, 0 failed (Lint & Repo Gates, TypeScript Type Check and Test Core 1–8 all success). The seat re-reads them before any landing stroke. Dev: spec 22540 tests pass, the new suite has 30 cases (each refusal pinned on code, path and the message's first clause), typecheck exit 0, 117/118 gates exit 0 (check:dual-build-cjs-loads exit 3, unbuilt workspace), and ablation of the readonly check reds exactly its 2 cases.
  4. 接受的偏差:
    • Platform-written mark = the existing readonly: true (it drops every non-system write, the same use the master-not-writable rule makes). No new key, as the order required.
    • Studio field form: an omit entry in metadata-form-zod-reconciliation.test.ts's ledger, not a form row, because nothing writes the key until the engine lands. That is the print / record / conditionalFormatting precedent. The form row goes to ui: home field read-only with "converted at RATE on DATE"; live preview when a rate exists; Studio editor for homeAmount (multi-currency M3-2, epic #8345; the read-only display is released with M2) #27196 (the home-field UI card). The PR body's file list omits this file and the regenerated docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md; this record names both.
    • Refusals beyond the card's list, for the contract reviewer: homeAmount on a non-currency or formula amount; one home field claimed by two amounts; a rate field shared by amounts with a different code field or document date; an expression on the home or rate field.
    • Regeneration ran outside the verify lock (lock timeouts), against a dist built under it; check:generated is green.
  5. 范围外去向 + Skills-docs:: two engine notes go to objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192: readonly strips non-system writes, so the projection must write through a system context; and a rate field with scale < 12 refuses every converting write with max_scale, loudly. Also: content/docs/data-modeling/currency.mdx does not mention homeAmount yet; the docs page rides objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192, the change that makes the key live. Skills-docs: updated here holds: field-types.md teaches the key and went 3207 → 3181 tokens by deleting restatement, so the pack's value density goes up.
  6. 落地条款: owed: a same-shape CONTRACT_REVIEW_TIER PASS on the current head (packages/spec/src/**, Clause-②: yes, governed skills/**). After it, the seat hands over through pr_request_reviewers for the governed approval, and lands only once an authorized approval is on record. Release hold: manual. Nothing mechanical keeps the key out of Version Packages (chore: version packages #26517) until objectql: write-time home projection — rate lookup by document date, decimal arithmetic, minor-unit rounding, missing rate refuses, computed fields read-only (multi-currency M2-2, epic #8345) #27192 lands; the maintainer is asked to confirm that hold.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head 361f8f2

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

Seat hand-over to the approvers · 2026-10-30T16:43Z · head 361f8f25cca4b3635c31de3e6fa187483700dcff.

…me-amount-pointer

# Conflicts:
#	packages/spec/src/system/metadata-form-zod-reconciliation.test.ts
…erging main

Main's side of the four regenerated files, re-derived from the merged
tree so this branch's `homeAmount` rows sit beside main's new entries.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
@os-zhuang
os-zhuang marked this pull request as ready for review October 31, 2026 02:29
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

New head · 2026-10-31T02:34Z · cf13692214fde39ab2a9b113f1708485e382e6b5. Main moved overnight and the PR went dirty, so the seat merged origin/main through os-regen-merge.sh. One hand conflict, in packages/spec/src/system/metadata-form-zod-reconciliation.test.ts: main added omit rows for pressed (#28235), percentDisplay (#28222) and lookupAutofill (#28244) beside this PR's currencyConfig.homeAmount row; every row is kept. 5b1430f663 regenerates the generated files from the merged tree (authorable surface, field/object/migration references). cf13692214 recounts the unknown-key strictness ledger, because main's lookupAutofill adds one site under data/ (155 → 156, field.zod.ts 14 → 15). No hand-written line of this PR changed. Local: spec src/data 152 files / 4740 tests pass, tsc --noEmit exit 0, check:generated 14/14, check:liveness and check:authorable-surface exit 0.

The prior PASS 6526217217 was on 361f8f25. Regen-provenance: cannot carry it: main's edits to field.zod.ts and object.zod.ts and the hand-resolved test sit in this PR's own delta, so the recogniser reads them as unexplained. A fresh review is owed on this head; the hand-over (approval from GOVERNED_APPROVERS, and the Version Packages hold until #27192) stands as written.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-30T04:26Z) · head cf13692

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head cf13692214fde39ab2a9b113f1708485e382e6b5 · 2026-10-31T02:51Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#4 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cf13692214fde39ab2a9b113f1708485e382e6b5
Local-runs: none
Scope: packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (the one hand-resolved conflict), packages/spec/authorable-surface/data.json, content/docs/references/data/field.mdx, object.mdx, content/docs/references/system/migration.mdx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md (the two regeneration commits) + packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts, packages/spec/src/data/currency-config-home-amount.test.ts, skills/objectstack-data/rules/field-types.md, .changeset/27191-home-amount-pointer.md (unchanged by the delta; judged in record 6526217217)

Inputs: PR #28096 (body, all six comments, the file list of 13 files, the eight commits from the merge-base 3fd4d6b), the seat's hand-off 6537861938 naming this head and the one conflict, the prior record 6526217217 (triage, PASS on 361f8f2, five should-fix, each routed per the seat's hand-over 6528171740: 1 to #28205, 2 to the #27192 hand-off note, 3 to 5 to #28203), the 55 check-runs on this head (read 2026-10-31T02:59Z: all 55 runs completed, none failed, none cancelled, none in progress; success: Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts), and the tree. This is a delta review of record 6526217217 on the head that merged main: the PR-only diff at 361f8f2 (against its merge-base) and the PR-only diff at this head (against today's main 6285203) were compared line by line with index and hunk-position lines stripped, and the merge commit 6c39365 was read in combined form, which shows only the hunks that differ from both parents. The judgments of record 6526217217 on every hand-written line stand, because none moved.

① Derived judgments

  • The delta is the merge, one resolved conflict and two regenerations; no hand-written line of the PR moved - right. The interdiff of the two PR-only diffs is 858 lines against 858, differing only in (a) the strictness-ledger counts, where data/ goes 155 to 156, field.zod.ts 14 to 15 and the total 155 to 156 because main added one site under data/ for lookupAutofill (feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) #28244), and (b) context lines around the homeAmount omit row, where main's three new omit rows (pressed feat(spec): an action can show the reader already pressed it (pressed, a per-reader CEL predicate) #28235, percentDisplay feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) #28222, lookupAutofill feat(spec): a lookup field declares which fields a pick fills (lookupAutofill) #28244) now sit beside it. field.zod.ts, object.zod.ts, the test file, the skills rule and the changeset are byte-identical in both PR-only diffs.
  • The conflict in metadata-form-zod-reconciliation.test.ts keeps every row - right. The combined diff of 6c39365 shows the homeAmount omit row (kind omit, type field, path currencyConfig, the D5/D9 why) stacked above main's pressed, percentDisplay and lookupAutofill rows, all four intact and in ledger order; no row was dropped or duplicated.
  • The regenerations match the merged tree - right as far as the diff shows and as CI judges. 5b1430f touches only the authorable surface and the three reference pages the homeAmount key already touched at 361f8f2; cf13692 touches only the ledger counts file, and the +1 is main's lookupAutofill site, not this PR's. Spec property liveness, Lint · docs & code guards and Check Changeset are the gates for those files and are in the check-run line above.
  • Accept-set, refusals and public surface - as record 6526217217 found: CurrencyConfigSchema gains optional homeAmount under record mode only, the object door judges the three pointers, readonly: true is the platform-written mark, HomeAmountSchema stays module-local. Nothing in the delta touches them.
  • Check-runs as gate verdicts - read as above; every run on this head completed and none is red, cancelled or in progress at the read.

② Semver level

③ Boundary flags

④ Design judgment

  • The delta carries no design: a merge that stacks two intents in one ledger, and two regenerations whose only new count is main's own. Nothing to add to record 6526217217's ④.

Verdict reasoning. The hand-written lines of this PR are byte-identical to the ones record 6526217217 passed; the one conflict keeps every row of both sides; the regenerations are the merged tree's, with the single changed count explained by main's lookupAutofill; every check on the head is green or skipped. The prior PASS carries to this head.

Implemented-by: claude/issue-27191-home-amount-pointer
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

Merged via the queue into main with commit 73b7270 Oct 31, 2026
61 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-27191-home-amount-pointer branch October 31, 2026 03:21
os-bill pushed a commit that referenced this pull request Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants