Repository navigation
docs(spec): the currency-chain text states the fixed-mode condition — spec sites 5–10 of the currency-mode closure - #20217
Conversation
…nder fixed mode The AnalyticsResult.fields[].currency TSDoc, its AnalyticsResultResponseSchema describe mirror, the response docblock and the percent-scale header called the measure-currency chain "ADR-0053" (the date/datetime record) and stated its middle step as an unconditioned "source-field default". The chain the service runs reads the field's currencyConfig.defaultCurrency only under currencyMode 'fixed'; a dynamic field has no currency of its own and falls through to the tenant localization.currency. Text only. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…mode condition dataset.measures.currency and field.currencyConfig cited the currency chain as "ADR-0053" (the date/datetime record) with an unconditioned defaultCurrency step. Re-measured: the analytics relay (AnalyticsServicePlugin sourceFieldMeta) and objectui's resolveFieldCurrency (objectui @25c7d584, currency.ts:60) both read defaultCurrency only under currencyMode 'fixed'. Evidence re-worded and re-anchored, verifiedAt re-dated; both verdicts stay LIVE. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…tion The chart-gallery dataset comment stated the measure-currency chain with an unconditioned currencyConfig.defaultCurrency step, cited ADR-0053 (the date/datetime record), and advised "set a field currencyConfig" to pin a symbol, which pins nothing without currencyMode 'fixed'. The account and semantic-zoo field comments and the grouped-detail e2e comment named currencyConfig.defaultCurrency as the ISO channel without the mode; the fields those three describe already declare currencyMode 'fixed'. Comments only. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…ency CurrencyConfigSchema.defaultCurrency read "Default or fixed currency code", which teaches a dynamic-mode default that no reader honours: under dynamic the key is not read and amounts display in the tenant default currency. The describe now states that, matching the currencyMode describe. The #8163 test-block comment names the same nested form with its mode. Describe string and comment only; the .default('CNY') and every other line are untouched. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…scribe gen:docs output for the two CurrencyConfigSchema defaultCurrency rows. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…R token The two re-dated notes named the retired citation to say the chain is not it; a count-based closure pin cannot tell that negation from a citation. They now say the chain no longer cites the date/datetime-semantics ADR. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0358f62a9aabc9c4edbd4fdedc0c0288195e0cd4 && git checkout 0358f62a9aabc9c4edbd4fdedc0c0288195e0cd4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 585c9af587a165c066c7815185a6652c2b9c545e 6ff1a15cf4998a432bada27954b4b9bf359ca3c4 && git checkout -B drift-repro 585c9af587a165c066c7815185a6652c2b9c545e && git merge --no-ff 6ff1a15cf4998a432bada27954b4b9bf359ca3c4
node scripts/docs-audit/affected-docs.mjs --json 585c9af587a165c066c7815185a6652c2b9c545e
|
Contract reviewServed-tier: ① Derived judgmentsInputs read: card #20126 body and comments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…etire Conflicts resolved by stacking both sides: #20217's defaultCurrency describe kept beside the removed precision; #20085's view-item conversion and rationale kept beside currency-config-precision-removed; dropped-refinements totals recomputed. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…(step 4 of the os-regen merge) Both sides edited the generated CurrencyConfig table: main rewrote the defaultCurrency row (#20217), this branch removed the precision row. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…laces are its currency's (ADR-0049) (objectstack-ai#20251) Part of objectstack-ai#19992 Clause-②: no Retires `currencyConfig.precision` under ADR-0049 enforce-or-remove, in the direction triage set (`5853208433`: 「**Direction unchanged:** remove, per ADR-0049 with the ADR-0087 retirement entry, under ruling 乙's principle that a currency's decimal places are the currency's. The aliases go with the key.」) and ruling 乙 on objectstack-ai#19910 (`5805782503`: 「a currency's decimal places are the currency's, not a setting」). objectstack-ai#19992 remains open for its folded family site (`5854612946`): the FIELD-level `precision` ("Total digits") that nothing reads. That key is untouched here apart from its form help text; objectui's Studio inspector writes it, so retire-versus-enforce is still an open choice for that card. ## What changes for an author | before (17.4) | after | | --- | --- | | `currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }` parses | refused at `currencyConfig` as `unrecognized_keys`, with the prescription below | | `currencyConfig.decimals` / `currencyConfig.scale` refused with *Did you mean → `precision`* | refused with the reason, and no rename suggested | | `CurrencyConfigSchema.parse({})` → `{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}` | → `{"currencyMode":"dynamic","defaultCurrency":"CNY"}` | | authored `precision` contradicting a fixed currency's ISO 4217 digits → `custom` issue at `currencyConfig.precision` | the check is gone with the key | | field designer help text for the field-level `precision`: "Decimal places (e.g., 2 for $10.50)" | "Total digits" (the key's describe; the object designer's row already said so) | ### Refusal texts, verbatim (rendered from `src` at this head) `precision`: ```text Unrecognized key(s) on this currency configuration: `precision`. • `currencyConfig.precision` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever read it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` `decimals` (and `scale`, identical but for the key name): ```text Unrecognized key(s) on this currency configuration: `decimals`. • `currencyConfig.decimals` is not a currency configuration key, and nothing replaces it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` The trailing sentence is the schema's existing `history` (`FIELD_HISTORY`), appended by `strictObject`; it is not new text. ## Premises measured (Zone 2) 1. **No reader, anywhere** — held. Instrument: `git grep currencyConfig` (non-test) plus indirect spellings (a config held in a variable, then `.precision`), with a lit control. - objectstack `packages/**` + `examples/**` + `apps/**` at `4df101c3`: 0 readers of `precision`; the control `currencyConfig?.currencyMode` IS read (`service-analytics/src/plugin.ts:1101`). - objectui at the pin `f8a9d0fb` and at `main` `25c7d584e`: 0 readers. `CurrencyField.tsx:82` derives width as `currencyFractionDigits(currency)` on both; its `:74` comment says it never read the key. Control: `currencyMode` / `defaultCurrency` reads 7 (pin) / 10 (main). - cloud `main` `48d7066`: 0 `currencyConfig` mentions; the instrument fires on that ref (`defaultCurrency` in `connector-stripe`). 2. **Producers.** Authored: three `examples/app-showcase` objects (`account`, `field-zoo`, `semantic-zoo`), now edited. Studio inspector: none (objectui's own `ObjectFieldInspector.currencyScale-10221.test.tsx` pins that it writes the field-level key, never `currencyConfig.precision`). **Stored rows and built artifacts: nearly all of them** — the removed `.overwrite()` baked `precision: 2` into parse output, so every persisted currency config carries it unwritten. That population decides the route: a D2 conversion replayed at rest, not a bare refusal. - What a stored field experiences after upgrade: the stored-row and artifact seams replay `currency-config-precision-removed` (`includeRetired`), strip the key, and serve the row canonical; it then parses. Pinned end to end through `applyConversionsToStoredItem('object', …)` in `currency-precision-iso4217.test.ts` — the row as stored is refused by today's door, the converted row parses, and the field-level `precision: 10` survives. 3. **Aliases** — the schema is a `strictObject`, and its `aliases` are rejection-with-suggestion, not renames. With the target gone they would point at a refusal (and `alias-integrity` audits that). Precedent for a natural spelling with no landing key is `FieldSchema`'s `currency` guidance, so `decimals` / `scale` get `guidance` entries with the same reason and no command (no conversion strips them: the closed shape always refused them, so no stored row carries them). Pinned: code, path, key, text, and the absence of "Did you mean". 4. **`currencyPrecisionContradiction`** lost its only reader, and `currencyFractionDigits` was read only by it: both are removed (neither was exported from a public entry — no barrel row, no `api-surface/` row). `CURRENCY_FRACTION_DIGITS` stays: `shared/value-domain.zod.ts:160` reads its key set for the `iso_4217_currency` value domain. The module docblock now says so and keeps the CLDR provenance. 5. **Docs** — every published sentence that stated the rule is corrected: `data-modeling/field-types.mdx`, `validation-rules.mdx`, `fields.mdx`, `getting-started/common-patterns.mdx` (two `os:check` blocks), `protocol/objectql/types.mdx`, the published skill `skills/objectstack-data/rules/field-types.md`, and the generated references (`references/data/field.mdx`, `object.mdx`, `system/migration.mdx`, `shared/value-domain.mdx`). ## The retirement kit - Schema: `precision` deleted from `CurrencyConfigSchema`; `CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCE` carries the three prescriptions; the `.superRefine` and `.overwrite` are removed. Stale comments that named the twin (`FieldSchema.precision`, the objectstack-ai#20011 note, the `.overwrite` precedent notes) now say it is gone. - ADR-0087: D2 conversion `currency-config-precision-removed` (toMajor 18, `retiredFromLoadPath`, strips the key under every field's `currencyConfig` on `objects` and `objectExtensions`, 2-notice fixture), wired into step 18's `conversionIds` with the rationale extended; `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision` (one entry file, regenerated). - Generated / ledgers: `authorable-surface/data.json` row removed deliberately (gate (a) tripwire; gate (c) proof 4 — guidance route — then passes); `authorable-defaults/data.json` loses `CurrencyConfig:precision = 2` (regenerated); `dropped-refinements.baseline.json` loses the 13 sites the removed `.superRefine` produced (`data/CurrencyConfig` itself plus 12 embeddings) — the refinement was removed with its key, the projection did not learn anything. - Form + i18n: `field.form.ts` help text; the `en` bundle regenerated, `zh-CN` / `ja-JP` / `es-ES` set to the object designer row's existing translations (总位数 / 総桁数 / Total de dígitos). - `spec-changes.json` / the upgrade guide do not move: neither carries any protocol-18 entry yet (the precedent `metric-filters-removed` is absent too), and `check:spec-changes` / `check:upgrade-guide` read up to date. - Changeset: `@objectstack/spec` minor + `@objectstack/platform-objects` patch, `**BREAKING**`, FROM → TO, and `adr-0087: registered currency-config-precision-removed` (`check:adr-0087-registration` green). ## Absence half — no tree-scoped text pin, on purpose `precision` does not leave the tree: it stays the field-level total-digit count on every numeric field. What is retired is a key in a position (`fields.NAME.currencyConfig.precision`), which a grep either matches everywhere or, scoped down, only where its author already knew to look. The `dashboard-chart-structure-refusal.test.ts` precedent covers this shape; the rationale block is in the test file. Standing in its place: `tsc` (the key is off `CurrencyConfig`'s input type — Leg A below) and the closed parse door (Leg B below). ## Proofs (one-off, nothing left behind) - **Leg A — reverse verification against the rebuilt `.d.ts`.** `node scripts/ablation-replace.mjs` put `precision: 2` back into `account.object.ts` (anchor 1 → 0, blob `58a9af7e` → `700705a8`), then `tsc --noEmit` on the showcase: exit 1, `account.object.ts(71,25): error TS2353: Object literal may only specify known properties, and 'precision' does not exist in type '{ currencyMode?: …`. Restored: blob == HEAD, `git diff HEAD` empty. Control (committed tree): showcase `typecheck` exit 0. - **Leg B — ablation of the refusal.** Re-declared `precision` on the schema (blob `7d36200c` → `86fdbf2d`), then the two pin files: **9 failed | 279 passed** — every retirement pin went red; the alias pins stayed green (they pin the guidance, which the ablation leaves in place) and so did the parse-output and conversion pins (independent of the key's declaration). Restored: blob == HEAD, `git diff HEAD` empty; tree clean after both legs (0 bytes of `git diff HEAD --stat` + `git status --porcelain`). Direction observed: red, as expected. ## Verification (head `36819398`; patch round on `71ea994d` below) - `pnpm --filter @objectstack/spec build` exit 0; `check:generated` exit 0 (15 of 15 up to date, after the one stale `check:docs` was regenerated with `--fix`). - `pnpm --filter @objectstack/spec typecheck` exit 0 (includes `check:test-typecheck`, so the `@ts-expect-error` in the pin file is a used directive). - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: **546 files passed, 16062 tests passed, 2 todo**. - Consumer suites (contract-face fixture triage): `@objectstack/example-showcase` typecheck exit 0 (after its dependency closure built); `@objectstack/platform-objects` typecheck exit 0 and tests **55 files / 911 passed**; `@objectstack/service-analytics` `currency-mode-relay.test.ts` + `query-dataset.test.ts` **51 passed**; `pnpm check:i18n` exit 0 (9 packages in sync, after the gate's own prerequisite closure). - Gate union: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **120** commands on this head; all run, exit codes on disk; `--ran` reconciles **119 run, 1 NOT-MEASURED, 0 UNRUN**. - `check:skill-examples` (covers the two edited `os:check` blocks): first refused on its prerequisite (exit 3, `client-react` unbuilt); after `pnpm --filter '@objectstack/client-react...' build` it exit 0 — "259 prose examples type-check across 3 surface(s)", 109 files. - NOT MEASURED: `check:dual-build-cjs-loads` — reason: its prerequisite is every package's `dist/` (a full `pnpm build`), and this diff changes no package entry or `exports`. - Not run locally: repo-wide `pnpm lint` (CI's run). - Console Pin Gate premise: objectui at the pin `f8a9d0fb` and at `main` imports neither removed helper nor `CurrencyConfigParsed`, and writes no `currencyConfig` literal carrying `precision` (multi-line scan over every file mentioning `currencyConfig`: 0 hits; control `currencyConfig: { … defaultCurrency … }` literals: 5 files at the pin, 13 at `main`). - The showcase `^...` closure build, the i18n prerequisite closure and every heavy run went through `scripts/pm/os-verify-lock.sh`. - Merged `origin/main` (`3cb84d08`) with `scripts/pm/os-regen-merge.sh`: conflicts stacked both sides (objectstack-ai#20217's `defaultCurrency` describe kept beside this removal; objectstack-ai#20085's `view-item-owner-hidden-removed` kept beside the new conversion, in both registries); every sibling entry measured present after the merge, same counts as `origin/main`. - **Patch round (merge conflict with objectstack-ai#20223), final head `71ea994d`.** - Merged `origin/main` twice with `scripts/pm/os-regen-merge.sh`: `0d3ec471` (merge `1e6f29b2`, regeneration `0b2e3964`), then `e4621867` (merge `71ea994d`). No rebase, no force-push. - Both conflicts were in `packages/spec/dropped-refinements.baseline.json`, which is hand-maintained by rule (no `gen:` script; `build-schemas.ts` gates it). Both sides are stacked: objectstack-ai#20223's 12 `inlineColumns.element` sites and `data/InlineGridColumn` root are kept, this PR's 12 `currencyConfig` sites and `data/CurrencyConfig` root stay removed, and objectstack-ai#20205's three automation entries are kept. The header totals were recounted to 210 schemas / 591 sites, and `gen:schema` accepts the ledger (exit 0). - Interaction with objectstack-ai#20223: no contradiction. Its field/column currency-scale refusals name neither `currencyConfig` nor `precision`, and both sides prescribe 「delete the key; decimal places are the currency's ISO 4217 minor unit」. - Readings on `71ea994d` (locked): - spec build exit 0; `check:generated` exit 0 (15/15); spec typecheck exit 0; - touched suites 6 files / 350 passed; full spec `--project local` **549 files / 16138 passed, 2 todo**; - `check-adr-0087-registration`, `check-changeset-no-major` and `check:migration-registry` (265 semantic / 215 retired-key / 199 retired-def) all exit 0. - The derived gate list is byte-identical to the one reconciled at `36819398`. - CI on `71ea994d`: 35 check runs, 33 success, 2 skipped, 0 failing. ## Scope notes for the reviewer - **Tier H.** `skills/objectstack-data/rules/field-types.md` is on this diff because it taught the retired key in its field-type table and code sample; leaving it would ship a published skill that teaches a refused key. The edit is a pure removal: file 328 → 327 lines; `objectstack-data` markdown 3717 → 3716; all `SKILL.md` files 4402 → 4402. It makes this PR Tier H. - Outside the claim's listed surface, each because this change would otherwise leave it false: the `value-domain.zod.ts` docblock (cited the removed function), one comment in `conversions/registry.ts` (named the removed key's bounds), the three showcase objects, `fields.mdx` / `common-patterns.mdx` / `types.mdx`, the platform-objects translation bundles, and the generated rows above. - `packages/spec/liveness/field.json` is **not** edited. The `currencyConfig` row covers the container, asserts nothing about `precision`, and stays `live` (`defaultCurrency` is read). The container is in `undrilled-containers.baseline.json`, so no per-key row existed to remove. objectstack-ai#20217 had just re-cited that row. ## Acceptance notes - `docs/qa/platform-checklist/areas/records-forms.json` item `records-forms.field-type-constraints` still describes `f_currency` as `scale 2 currencyConfig{precision 2}`. Both halves are stale now: the `scale` half since an earlier card retired it from currency, the `precision` half since this one. Its SCALE gap probe targets a key the currency type refuses. Internal QA prose, not published. carrier: the next checklist-author sweep; 承接者:无. - Standalone `field` metadata rows are outside every field conversion's reach (the stored-row seam has no `fields` stack collection), this one included, the same as its precedents. Observation only; no producer of such rows was measured. - Two semantic ledger entries of earlier protocol-18 migrations (`18.field-scale-precision-integer-refused`, `18.ui-form-field-precision-scale-integer-refused`) say `CurrencyConfigSchema.precision` "is a different surface". That is a scope remark about those migrations, historically accurate, and left as recorded. ## 维护者速读(草稿) **改了什么**:货币字段配置里的 `currencyConfig.precision`(「小数位」)被删除。现在写这个键会在保存/发布时被拒绝,并提示直接删掉;它的两个近义写法 `decimals`、`scale` 也给出同样的说明。已经存进数据库或打进构建产物里的旧配置,读取时会自动去掉这个键,不会报错。字段设计器里「精度」一栏的提示文字从「小数位数」改成「总位数」。 **为什么改**:这个键从来没有任何界面或运行时读取过——金额显示几位小数一直由币种本身决定(美元 2 位、日元 0 位、科威特第纳尔 3 位)。作者(包括 AI)以为设了小数位,实际什么都没发生。裁定乙已定原则「币种的小数位属于币种,不是一个设置项」,分诊定了删除方向。 **风险与代价(含回滚)**:这是破坏性收窄:仍在源码里写这个键的应用,升级后会在发布时报错,需要删掉这个键(`os migrate meta --from 17` 会列出要改的地方)。已存数据不受影响(读取时自动转换)。回滚:revert 本 PR 即可,已存数据没有被改写。本 PR 改到了对外发布的技能包 `skills/`,因此属于 Tier H,需要您批准才能合并。 **席位意见**: **你要做的**:审阅后在本 PR 上给出批准(APPROVED review),或告诉席位把 `skills/` 的改动拆成单独的 PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…tstack-ai#20126) (objectstack-ai#20415) Fixes objectstack-ai#20126 Clause-②: no Remainder (c) of the currency-mode class-closure card: the family's one enumerating pin. Remainder (b) landed in PR objectstack-ai#20217, the skills half (objectstack-ai#20196) in PR objectstack-ai#20256, and the runtime member (objectstack-ai#20091) in PR objectstack-ai#20119, so this test-only PR closes the card. ## What the pin is One new test in spec's `repo` vitest project: `packages/spec/src/data/currency-mode-family-closure.pin.test.ts`. The maintainer's ruling on objectstack-ai#20088 asked for one enumerating pin in the owning package and ⛔ no new `check-*` gate. `packages/spec` owns `CurrencyConfigSchema`, and its repo project already walks `skills/**` and `content/**`. The pin enumerates the corpus, not the sites. A new site anywhere in the radius reddens it, as well as a regression of an old one. - **Rule A.** Every `defaultCurrency` in `skills/**/*.md` and `content/docs/**/*.mdx` sits in a unit that carries `fixed`, or that says it is not the displayed currency (or not read) under `dynamic`. - **Rule B.** The date record's number within five physical lines of `currenc` (any case) counts 0. The matcher reads `ADR-0053`, `ADR 0053` and the `adr/0053-` file link. The roots are `skills/**`, `content/docs/**` and `packages/services/service-analytics/src/**`. Per the family's appended sites 5-10 (ACCEPT `5853407641`, triage `5853734589`), they also include `packages/spec/src/**`, `packages/spec/liveness/**` and `examples/**`: non-code files anywhere, plus `.ts` under each app's `src/`. - **Runtime member.** It is objectstack-ai#20091's own pin, `packages/services/service-analytics/src/__tests__/currency-mode-relay.test.ts`, on `main` (`git merge-base --is-ancestor 6780e34 HEAD` exit 0). The closure pin only checks that this file still stands, and ⛔ does not re-assert the resolver. - **Site table.** Each family site (1-10, the three pages objectstack-ai#20090 landed, and 11-14) is mapped to the leg that guards it. The test asserts each file's membership in each corpus, so if a site moves out of the radius, the pin goes red. ## Outside the corpus, and why - **Released entries.** This covers every `CHANGELOG.md` and `content/docs/releases/**`. The seat ruled sites 11-14 (the released 17.0.0 entries) accepted history, since a released entry records what shipped (claim `5866047669`). The two package CHANGELOGs sit outside every root by construction. `examples/*/CHANGELOG.md` is inside a root and is skipped by name. The test asserts both exclusions, with a live control proving the by-name skip has something to skip. - **The pin's own file.** It quotes both rules and their fabricated controls. ## The three design facts (`5855122068`), each answered in the pin's header 1. **Test titles.** Rule A reads published prose only (`.md` / `.mdx`), so no test title is judged by it. The `field.test.ts` title stays outside by corpus, not by exemption. Rule B does read test files inside its roots. 2. **Negation versus citation.** Rule B does not try to tell them apart: a negation within the window counts too. Currency text has no reason to name the date record; the pin's message says to cite ADR-0104. 3. **Wrapped keys.** Both rules read logical text. A key hyphen-wrapped at a line end (`default-` / `Currency`, `ADR-` / `0053`) rejoins after the next line's comment prefix is stripped. **Unit for rule A: not the line.** A strict line reading reds on 5 correct, accepted lines in `skills/objectstack-data/rules/field-types.md` (ACCEPT `5853407641`'s pin note). Instead, the unit is: - in prose, the sentence across wrapped lines; - in a table, the row; - in a fenced comment, the sentence of its comment run; - in fenced code, the innermost object literal holding the key, or the sibling mapping in YAML. So one `fixed` field in a fence never vouches for a neighbour's bare `defaultCurrency`. There is one shape exemption: a parenthesised key list that also names `currencyMode`, `(currencyMode, defaultCurrency)`. It names keys and teaches no reading; this is `field-types.md:55`. ## Measured on this branch's base `dcd3bceaa` (assumption 1 holds) - **Rule A:** 445 files in the corpus. 34 `defaultCurrency` occurrences in 11 files; 0 failing, 1 of them the key-list exemption. - **Rule B:** 2550 files in the corpus, 0 hits. 65 date-record lines remain, all outside the window (date uses). - **No live text site:** no text site was edited in this PR. ## Declared radius (AGENTS.md: a test that reads outside its package is spelled so the gate can see it) Rule B's example-code leg is new radius for `@objectstack/spec#test:repo`. It is declared in two places: - `scripts/cross-package-test-inputs.mjs` gets `examples/*/src/**/*.ts`; - `turbo.json` gets the matching `$TURBO_ROOT$/...` input. The glob is deliberately ⛔ not `examples/**/*.ts`. That glob covers the CRM example's smoke test, which is the dispatch-gates self-test's outside-packages specimen. The declaration comment states that the glob reaches the two translation tests inside the todo app's `src/`. The pin's site table quotes the showcase dataset's path, which holds the glob on the roster. `check:cross-package-test-inputs` and `check:ci-filter-parity` are both green. ## Verification All readings are at head `7fc9f86b`, the branch's only commit; every build and test ran through `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0` each). - **The pin alone:** `pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2 src/data/currency-mode-family-closure.pin.test.ts` → `Test Files 1 passed (1)` / `Tests 6 passed (6)`. - **Ablation, one leg per rule** (from the committed state, `scripts/ablation-replace.mjs` in wrap mode, under the lock). The expected direction was red, and both legs went red: - Leg A drops ` (only under currencyMode: 'fixed')` from `skills/objectstack-ui/rules/dashboards.md`, site 2's regression. Anchor 1 → 0, blob `570341f3` → `8e0c44f3`. Rule A goes red with exactly `skills/objectstack-ui/rules/dashboards.md:307`: `Tests 1 failed / 5 passed`. Restored: blob equals HEAD `570341f3`, and `git diff HEAD` is empty. - Leg B inserts ` (ADR-0053)` after the key in `examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts`, site 10's regression and the newly declared example-code radius. Anchor 1 → 0, blob `31348e15` → `1ca43894`. Rule B goes red with exactly `examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts:47`: `Tests 1 failed / 5 passed`. Restored: blob equals HEAD `31348e15`. - The first leg-B attempt was a no-op and is not a reading: its replacement embedded its own anchor, and the tool refused (anchor 1 → 1) and restored before any test ran. It was re-run with an anchor the replacement does not contain. - Restored leg: `Tests 6 passed (6)`. `git diff HEAD` is 0 bytes and `git status --porcelain` is empty. - **spec, both projects:** - `vitest run --project repo`: `Test Files 36 passed (36)` / `Tests 640 passed (640)`. - `vitest run --project local`: `Test Files 561 passed (561)` / `Tests 16517 passed | 1 todo (16518)`. - `pnpm --filter @objectstack/spec run typecheck` exits 0, and `check:test-typecheck` is OK. `tsc -p tsconfig.test.json --listFilesOnly` lists the new file, so the typecheck really reads it. - **Builds:** `pnpm --filter @objectstack/spec build` exits 0. `turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2` gives `Tasks: 71 successful, 71 total`. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 96 commands over the 4 changed paths, and every one was run. `--ran` gives `96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN`. On the first pass, four gates exited 3 because their prerequisite packages were not built. They exited 0 after the full build: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt`. - **Readers of the edited declaration module:** each exits 0. - `node scripts/pm/dispatch-gates.mjs --self-test`: 1976 cases, including "no hint of this gate reaches a test file outside packages/**". - `node scripts/check-ci-filter-parity.mjs --self-test`. - `node scripts/partition-test-shards.mjs --self-test`. - `node scripts/check-published-list-mirrors.mjs`. - `node scripts/check-examples-live-imports.mjs`. - **Lint, narrowed** (CI owns `pnpm lint`): - `eslint --no-inline-config --format json` on the two lintable changed files gives 2 files / 0 errors / 0 warnings; `--print-config` gives them 4 and 2 active rules. - `turbo.json` and `vitest.repo-tests.json` answer "File ignored because no matching configuration", so they are outside eslint's population. - `eslint.config.mjs` sets no `parserOptions.project`, so there is no type-aware linting, and no untouched file's verdict can move. - **Ships nothing:** spec's `files[]` is `dist`, `json-schema`, `liveness`, `src/**/*.zod.ts` and so on. A grep of the published paths for the pin finds 0 files; the control grep for `CurrencyConfigSchema` in `dist` finds 30. So the diff takes `skip-changeset`. - **Base:** the branch is one commit behind `origin/main` (`2c310705`). That commit adds two `packages/spec/src/data` files with no date-record token, so rule B cannot move on the merge. Not merged here. ## Acceptance notes (not filed) - `.changeset/*.md` is outside the corpus. It is where released CHANGELOG text originates, so a pending changeset citing the date record for currency would not redden this pin before release. Carrier: none; noted only. - The `user selectable` wording that site 1 corrected is not a rule of the card's pin definition, so it is not guarded here. - Example tests (`examples/*/test/**`, `examples/*/e2e/**`) are outside rule B's example-code leg, which follows the declared radius above. - `node scripts/pm/measurement-claim-triage.mjs --self-test` (run as a reader of the edited module) is red on the base, naming only `scripts/check-dts-references.mjs:74` (UNTRIAGED), a file this PR does not touch. No workflow or package script invokes it. Carrier: none. - The objectui pointer in `5855122068` (objectui `packages/i18n/src/currency.ts`, `CurrencyField.tsx`) is another repository and is not read here. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #20126
Clause-②: no
This PR covers remainder (b) of the currency-mode class-closure card: sites 5–10, appended by the seat in comment 5853407641 and kept in this lane by triage (5853734589). It does not close the card. What stays open is listed under Remainder at the end.
What was wrong
CurrencyConfigSchemadeclares two modes. UndercurrencyMode: 'fixed', the field has one currency,defaultCurrency. Underdynamic(the default), the field has no currency of its own: amounts display in the tenant default currency (localization.currency), or as a plain number when none is set, anddefaultCurrencyis not read, including its parse defaultCNY. The value is a bare number either way (ADR-0104).The readers were re-measured for this PR, and each reads
defaultCurrencyonly under'fixed':1c8b320a8.AnalyticsServicePlugin'ssourceFieldMeta(packages/services/service-analytics/src/plugin.ts, thefixedCurrencyternary) answerscurrencyConfig.defaultCurrencyonly whencurrencyMode === 'fixed', andundefinedotherwise.queryDatasetthen resolvesmc.currency ?? meta?.defaultCurrency ?? context?.currency.context.currencyis the tenantlocalization.currency(packages/core/src/security/assemble-execution-context.ts).resolveFieldCurrency, at objectuimain25c7d584.packages/i18n/src/currency.ts:60takesconfig.defaultCurrencyonly undercurrencyMode === 'fixed'.Six texts under
packages/spec/**andexamples/**still described an unconditioneddefaultCurrencystep, or called the chain "ADR-0053". ADR-0053 is the date / datetime semantics record and never names currency.What changed: text only, site by site
Site 5,
packages/spec/src/contracts/analytics-service.ts, theAnalyticsResult.fields[].currencyTSDoc. It ships in the published.d.ts. The ADR-0053 label is gone. The middle step is now "the source field's FIXED currency (currencyConfig.defaultCurrency, read only undercurrencyMode: 'fixed'— adynamicfield has no currency of its own, so itsdefaultCurrencyis not read)". The text citesCurrencyConfigSchemaand ADR-0104, and it says the column is also absent when no step answers. The date-bucketingADR-0053 Phase 2citation in the same file (now:300) really is a date use, and is untouched.Site 6,
packages/spec/src/data/percent-scale.ts, module header. "This mirrors the ADR-0053 currency chain" now reads "This mirrors the currency chain", with the three steps and the fixed condition spelled out.Site 7,
packages/spec/src/api/analytics.zod.ts,AnalyticsResultResponseSchemadocblock. "(the ADR-0053 / percent-scale renderer chains)" now reads "(the currency / percent-scale renderer chains)".Site 7, in-class extra in the same file: the
data.fields[].currencydescribe. This is the.describe()mirror of site 5, and it said "source-field default" with no mode:Resolved ISO 4217 code for a MONETARY measure (explicit measure `currency`, then source-field default, then tenant default). Absent on non-monetary columns, which must never render a symbol.Resolved ISO 4217 code for a MONETARY measure (explicit measure `currency`, then the source field's fixed currency — its `currencyConfig.defaultCurrency`, read only under `currencyMode: 'fixed'` — then the tenant default). Absent on non-monetary columns, which must never render a symbol.Site 8,
packages/spec/liveness/dataset.json(currency) andpackages/spec/liveness/field.json(currencyConfig). Evidence text is hand-maintained.check:livenessresolves its paths and#symbolanchors, andgen:liveness-countstallies statuses only, so the owning route is a hand edit gated bycheck:liveness.dataset.json: the evidence drops the ADR-0053 label and names the middle step "the source field's FIXED currency". It gains a gate-checked anchor at the relay,packages/services/service-analytics/src/plugin.ts#AnalyticsServicePlugin.field.json: re-measured at objectui25c7d584, and the pointers moved with the code:currency.ts:32→:60, andCurrencyField.tsx:45/:51→:55/:91. At the old pin11c1e71ethe read was unconditioned, so rewording the text without re-measuring would have pinned a false sentence to that sha.verifiedAtre-dated to 2026-09-27 and a dated note. Both verdicts staylive; no measurement falsified either one.state-counts.mdis unchanged.Site 9,
packages/spec/src/data/field.zod.ts:447, thedefaultCurrencydescribe string only. The.default('CNY')and every other line of the region-split file are untouched. The diff is one line.Default or fixed currency code (ISO 4217, e.g., USD, CNY, EUR)The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency. Not read under `dynamic` (the default), where amounts display in the tenant default currency.gen:docsregenerated the two matching rows ofcontent/docs/references/data/field.mdx.check:generatedproved that one artifact stale, and no other.Site 10,
examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts. The comment stated the chain with no mode, cited ADR-0053, and spelled the key as adefault-/Currencywrap, so a line grep fordefaultCurrencymissed it. It also told authors to "set a fieldcurrencyConfig" to pin a symbol, which pins nothing withoutcurrencyMode: 'fixed'.project.budget/spentdeclare nocurrencyConfigat all, sinceField.currency()injects none. The comment now states the conditioned chain and the fixed form an author would actually write.In-class extras found by the census, fixed in place
All four conditions of the bounded in-place rule hold for these:
precisionas its display width, refusingprecision: 18on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #20011'sfield.zod.tshunks are at:29and:2306);The extras:
packages/spec/src/api/analytics.zod.ts, thefields[].currencydescribe (above).examples/app-showcase/src/data/objects/account.object.ts:63andsemantic-zoo.object.ts:53: "spec channel:currencyConfig.defaultCurrency" gains "read only undercurrencyMode: 'fixed'". Both fields already declare'fixed'.examples/app-showcase/e2e/detail-shapes.spec.ts:125: "currencyConfig.defaultCurrency drives a real $ symbol" now reads "the fixed-mode currencyConfig.defaultCurrency (USD)". The field issemantic-zoo.budget,fixed/ USD.packages/spec/src/data/field.test.ts:176: this test-block comment namescurrencyConfig.defaultCurrencyas the declarable nested form. It now adds "undercurrencyMode: 'fixed'— the only mode that reads it". It is a comment only, with no assertion change.Census: the seed for remainder (c)
Instrument: a scratchpad script (not committed) that reads each file at a git ref. The scopes are
packages/spec/src/**,packages/spec/liveness/**andexamples/**: 1881 files at both refs. Base is1c8b320a8. Head is6ff1a15c, the final head after mergingmaine7f69dbb; it reads identically to the pre-merge head452bd5d4.ADR-0053within five lines ofcurrencdefaultCurrencylinesdefault-/Currencywrapped tokens, which a line grep missesControls.
ADR-0053lines remain in these scopes at head. All are date uses outside the window:analytics-service.ts:300(date bucketing, the:294the order excluded, shifted by this diff) andexecution-context.zod.ts:91(timezone, ADR-0053 Phase 2). No genuine date-bucketing use falls inside the window.Sentence-level judgment at head. Every prose sentence naming
defaultCurrencyin these scopes carries thefixedcondition or says it is not read underdynamic:field.zod.ts:446,:447,:460–:463,:1060,:1066–:1068;analytics.zod.ts:111–:116;analytics-service.ts:79–:90;percent-scale.ts:14–:18; both ledger rows;chart-gallery.dataset.ts:44–:51;account.object.ts:63;semantic-zoo.object.ts:53;detail-shapes.spec.ts:125;field.test.ts:176aliasesmap (field.zod.ts:427), the precision-check code (:465,:505,:512,:2327), and every test fixturefield.test.ts:202, theittitle "names the declarable nested form,currencyConfig/defaultCurrency". It labels exactly what its assertions check (the refusal message contains both tokens), and its body parses thefixedform. Retitling it to claimfixedwould need a new message assertion, which is outside a text-only PR. A pin that scans test titles should decide this line deliberately.Acceptance notes (observed, not filed)
examples/app-showcase/src/data/objects/semantic-zoo.object.ts:54–:55says a bare field-levelcurrencykey "gets stripped at parse".FieldSchemarefuses it:field.test.ts:198pinssuccess: false, and the guidance message is atfield.zod.ts:1066. This is a different defect class, left untouched.packages/spec/CHANGELOG.md:28383and:66160, andpackages/services/service-analytics/CHANGELOG.md:1976and:8504, all under 17.0.0. These are release-owned. AGENTS.md routes an amendment to a dedicated docs-only PR, never a rider here.25c7d584,packages/i18n/src/currency.ts:22andpackages/fields/src/widgets/CurrencyField.tsx:52, still cite ADR-0053 for the tenant step.:30still quotes the spec's pre-docs(spec): currencyMode says what each mode does; the currency-chain text states the fixed condition #20179 "user selectable" wording. The code is right; the comments are in objectui's lane.Verification
All readings are at head
6ff1a15c: this branch plus a merge ofmaine7f69dbb. Main brought no file this PR touches. The branch delta against main is byte-identical before and after the merge:git patch-id --stablegives224b6215on both.Build and tests (one
os-verify-lock.shinvocation,VERDICT command-exit 0):turbo run build --filter=./packages/* --filter=./packages/*/* --concurrency=2: "Tasks: 71 successful, 71 total".pnpm --filter @objectstack/spec run check:generated: "All 15 generated artifacts are up to date". This is the second leg: beforegen:docs, the same command read "1 of 15 artifact(s) stale: content/docs/references/**", and exactly the 2defaultCurrencyrows moved.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: "Test Files 540 passed (540) / Tests 15878 passed | 2 todo (15880)".pnpm --filter @objectstack/spec run typecheck: exit 0 (tsc --noEmit,check:scripts-typecheck, and "check:test-typecheck: OK").pnpm --filter @objectstack/example-showcase run typecheck: exit 0.pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2: "Test Files 29 passed (29) / Tests 384 passed (384)".What ships. The measurement was taken on the built
dist, with a positive control:dist/contracts/index.d.tsanddist/percent-scale-*.d.ts/.d.mts.dist/uianddist/api.dist.field.test.tscomment is not shipped.Consumer fixture triage. No test or snapshot pins any changed string.
git grepfor the old describe texts and the old example comments outside their own files gives 0 hits, and the same search overexamples/app-showcaselights on the edited files as a control. The one generated consumer,field.mdx, is regenerated.Gates.
dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat6ff1a15cderives 109 commands (13 paths vs merge basee7f69dbb). All 109 were run, each exit code recorded before any pipe, and all 109 exit 0.--rananswers "Run reconciliation — 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero: every line carries its exit code).check-changeset-fixed, speccheck:meta-url-spelling, speccheck:spec-changes,check:authz-resolver,check:error-code-casing,check:filter-alias-parity.check:partof-closing-keywordon this body, offline viaPR_BODY: exit 0.Lint. The run is narrowed, since CI owns
pnpm lint.eslint --no-inline-config --format jsonon the 9 touched.tsfiles: 9 files, 0 errors, 0 warnings, none ignored.--print-configputs each file in eslint's population, with 4 or 5 rules each.eslint.config.mjssets noparserOptions.project, so no type-aware linting runs and this diff cannot move a verdict on an untouched file.Bytes. A control-byte scan over all 13 files gives grep exit 1 (none); the control fixture is lit.
check:nul-bytesis in the union.Ablation. None. The diff is text only: describes, TSDoc, ledger text and comments. No assertion or runtime line changed, so there is no negative pin to ablate. The
check:generatedred-then-green above is the regenerated artifact's own two-leg reading.Remainder after this PR
currencyMode: 'fixed'condition and cites ADR-0053 — remainder (a) of #20126 #20196:skills/objectstack-ui/rules/dashboards.md, the Tier H site. It is open (pm:queue) as this PR opens, per a REST read at 2026-09-27T10:34Z, so this PR isPart ofand carries no enumerating pin.packages/spec/src/**,packages/spec/liveness/**andexamples/**, and judge the sentence, not the line. The wrappeddefault-/Currencytoken this PR removed is one concrete case a line grep cannot see.Generated by Claude Code