feat(spec)!: retire currencyConfig.precision — a currency's decimal places are its currency's (ADR-0049) - #20251
Conversation
…stry, pins Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…, form help text; changeset Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…pped-refinement sites leave with the key Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…; ledger entry for the removed refinement Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…sion Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…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>
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…ecision help text Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…etire dropped-refinements.baseline.json resolved by the file's own rule (hand-maintained, gated by build-schemas.ts): #20223's inlineColumns.element sites and data/InlineGridColumn root kept, this branch's currencyConfig sites and data/CurrencyConfig root stay removed; header totals recounted from the merged body. 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 field reference: main's #20223 rewrote the inline grid column prefix/scale rows, this branch removed CurrencyConfig's precision row and narrowed the currencyConfig type signature. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…recision-retire dropped-refinements.baseline.json: header-only conflict with #20205's three automation entries; totals recounted from the merged body (210 schemas, 591 sites). Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 ae955e1f9ad0e78804c51684f39bfb4fe30c4189 && git checkout ae955e1f9ad0e78804c51684f39bfb4fe30c4189
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 07bcbf88b3f0f7951b704780e125ee87a73b6659 33f53b5e0846935bc908086b4ae3329167a4ee06 && git checkout -B drift-repro 07bcbf88b3f0f7951b704780e125ee87a73b6659 && git merge --no-ff 33f53b5e0846935bc908086b4ae3329167a4ee06
node scripts/docs-audit/affected-docs.mjs --json 07bcbf88b3f0f7951b704780e125ee87a73b6659
|
维护者速读 · PR #20251(#19992,
|
…D3 entry
The merge of main brought the ruling-B census pin in migrations.test.ts
("from protocol 18 on, every graduated D2 conversion is named by a D3 entry
of its own step"). This branch's D2 conversion currency-config-precision-removed
had a retired-key entry but no semantic entry, so the merged tree failed that
pin. Adds entries/semantic/18.currency-config-precision-retired.ts naming the
conversion and the two judgments the lossless strip cannot make, regenerates
the step-18 semantic region with gen:migration-registry, and names the new
entry in the changeset's registry bullet.
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
…rrency-config-precision-retire
The os-regen driver kept the branch side of content/docs/references/{data/field,
data/object, shared/value-domain, system/migration}.mdx in the merge of main
(2972097); main had re-derived their page descriptions. Regenerated with
gen:schema + gen:docs from the merged tree: main's derived descriptions plus
this branch's retired currencyConfig.precision rows. The resulting tree equals a
driverless merge-tree of main and the pre-merge tip (5431f62).
Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
…igrate sentence retired-key-migrate-sentence.test.ts (repo project) requires every `os migrate meta --from N` prescription to be the house sentence, last in its literal. The new currency-config-precision-retired entry ended on a combined sentence; the stored-row command now precedes the house sentence. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…recision-retire Two hand-resolved conflicts, both intents stacked: - conversions/registry.ts: main's form-layout-inline-grid-to-vertical definition kept, currency-config-precision-removed appended after it; CONVERSIONS_BY_MAJOR keeps main's order and appends this branch's entry. - migrations/registry.ts: step-18 rationale keeps main's text and appends this branch's sentence; conversionIds keeps main's list in order and appends currency-config-precision-removed. Generated regions are regenerated after. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
|
Seat note: approved and armed at
|
Contract reviewServed-tier: Round 2 at the new head, adopting the round-1 PASS record ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…om the merged tree The merge took main's generated regions; gen:migration-registry puts this branch's two entries back (retired-key security/RowLevelSecurityPolicy:tags and the D3 semantic entry permission-rls-tags-retired) beside #20251's. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
objectstack-ai#20168) (objectstack-ai#20279) Fixes objectstack-ai#20168 Clause-②: no This PR carries ruling `5856786357` into `packages/spec`: batch objectstack-ai#227 item 4, letter **A**, put in force by `5856865124`. A `decision` node that declares a NON-EMPTY `conditions` list together with `mode` is now refused at authoring. The refusal names the ruled way out: drop `mode` (a conditions list is first-match on its own), or move the branches onto the edges and keep `mode`. `mode` belongs to the edge-branched decision alone. Dispatched by the `domain:spec` seat 4 PM, session `session_01CiCTczDo7tGhafXjf61dUJ`. The claim is comment `5857419814`. Branch base `e46218674`; every reading below is at head `ae8185032` unless it says otherwise. ## What changes 1. **`DecisionConfigSchema`** (`packages/spec/src/automation/schemaless-node-config.zod.ts`) gains a `.superRefine`. When `mode` is authored beside a non-empty `conditions` array, it adds one `custom` issue at `['mode']`. The message comes from a new helper, `decisionModeWithConditionsRefusal()`, which sits beside the existing `decisionModePrescription()` and echoes the value the same way. Both members are refused alike, because it is the key that has no reader here, not the value. 2. Out of scope, by the ruling: `mode` beside an empty list, `mode` with `conditions` absent, and a list without `mode`. All three parse exactly as before. A `mode` outside the closed pair still gets its own value refusal first. A base-type issue aborts the object's refinement, so the author sees one issue, never two. 3. The `mode` `.describe()` and docblocks now state the refusal. `content/docs/references/automation/schemaless-node-config.mdx` is regenerated with `gen:docs`, because `check:generated` proved only `check:docs` stale. 4. **`dropped-refinements.baseline.json`** gains exactly the row the build's ratchet printed: `automation/DecisionConfig`, with one root site (the empty path). No arm of the projection list in `shared/refinement-projection.ts` fits "this key forbids that one when the list is non-empty". That rule is value-conditional, and `dependent-required` / `banned-keys` are presence-only. Adding an arm would be a public-contract decision, so the one route the ruling allows was taken. The header totals were recounted from the body: 211 → **212** schemas and 604 → **605** sites. The built `json-schema/automation/DecisionConfig.json` carries `x-dropped-refinements: [{ at: '', type: 'object', count: 1 }]`. 5. The changeset `.changeset/20168-decision-mode-beside-conditions-refused.md` bumps `@objectstack/spec` as `patch`, with `Clause-②: no`. ## Timing and grade: measured at landing, not assumed (ruling item 2) | reading | value | source | |:--|:--|:--| | npm `latest` `@objectstack/spec` | `17.4.0` | `npm view @objectstack/spec dist-tags`, 2026-09-27T16:12Z, and again at 18:03Z | | `mode` in the published contract | **absent**: `json-schema/automation/DecisionConfig.json` in the 17.4.0 tarball declares `conditions` only, with `additionalProperties: false`. In `dist/automation/index.js`, the control string `first true expression wins` has 2 hits and the test string `edge-branched decision` has 0 | `npm pack @objectstack/spec@17.4.0` | | Version Packages PR objectstack-ai#17076 | `open`, `merged: false` | REST, 16:12Z and 18:03Z | | pending changeset for `mode` | `.changeset/19867-decision-config-mode.md` is still in `.changeset/`, so it is unconsumed | tree at `ae8185032` | ⇒ **Unreleased.** Per ruling item 2 this is `patch`, `Clause-②: no`, and no ADR-0087 entry. `mode` reaches its first release together with this refusal, so no published accept set narrows. Against the published 17.4.0 contract, the release still only widens. The same reading and its source are written into the changeset. ## Which doors parse `DecisionConfigSchema` today (PM mechanism assumption 3, measured) - **Direct parse**: yes, through the export and through the `SCHEMALESS_NODE_CONFIG_SCHEMAS.decision` handle (one object). Both are pinned. - **Flow registration**: no. `validateNodeConfigKeys` (`engine.ts`) skips a node whose descriptor publishes no `configSchema` (`if (!schema) continue;`), and `decision` publishes none by design. - **`FlowSchema` / `defineFlow`**: no. `FlowNodeSchema.config` is a `z.record(z.string(), z.unknown())`, and the node-level config pass parses only an `end` node (`parseEndNodeConfig`). - **`os validate`**: no. `lint-flow-patterns.ts` reads `config.conditions` ad hoc (labels, emptiness) and never parses the schema. `git grep` on `DecisionConfigSchema|SCHEMALESS_NODE_CONFIG_SCHEMAS|getSchemalessNodeConfigJsonSchemas` outside `packages/spec` finds three places. `metadata-protocol/src/reference-sites.ts` is a JSON-projection walk, where a refinement projects byte-identically. `service-automation`'s `config-expression-ledger.test.ts` reads the projection. `config-expression-ledger.test.ts:325` mentions the schema in a comment only. - **Published JSON Schema**: it cannot state the rule. The rule is declared dropped instead, in the ledger and on the artifact, as item 4 above describes. ⇒ No door that answers in the ADR-0112 envelope (a `code` and a `status`) parses this schema yet. The envelope arrives with the registration-time reader that objectstack-ai#15429 adds, which is the ruling's item 3. This PR pins the parse door, the by-node-type registry handle that reader will look up, and the per-parse `objectStackErrorMap` a validator may pass. Mechanism assumption 1 held (`:471` / `:486` / `:206` on `e46218674`). So did assumption 2: the projection drops the refinement, and the ledger row is registered. ## For objectstack-ai#15429's acceptance list (the `domain:services` seat) The refusal that the registration reader must surface: - issue `code: 'custom'`, `path: ['mode']`; exactly one issue for a config with a legal `mode` beside a non-empty `conditions`. - message first sentence, verbatim (the value is echoed): ``` `mode: 'inclusive'` is not valid on a decision that declares a `conditions` list — `mode` belongs to the edge-branched decision alone. ``` - the two remedies in the same message: ``Either delete `mode` and keep the list`` … ``move the branches onto the out-edges (a `condition` on each branch edge, `isDefault: true` on the fallback), delete `conditions`, and keep `mode`.`` - the message carries no tracker number. - to leave alone: `{ conditions: [], mode }`, `{ mode }`, and `{ conditions: [...] }` without `mode`. ## Pins, and the ablation `packages/spec/src/automation/schemaless-node-config.test.ts`: - The old pin *"…and alongside a branch list, which the key does not forbid"* asserted the accept-both shape. It is replaced by the ruled semantics. - A new describe block covers: - `{ conditions: [one], mode: 'inclusive' | 'exclusive' }` and the same with a two-entry list: refused, one `custom` issue at `['mode']`, ruled first sentence and both remedies. - The same refusal through `SCHEMALESS_NODE_CONFIG_SCHEMAS.decision`, and under `objectStackErrorMap`. - An illegal value beside a list: the value refusal only. - Controls, each a full `safeParse` success that round-trips: `{ conditions: [], mode }` for both members, `{ mode }` with `conditions` absent for both members, and a list without `mode`. Also a test that following either remedy parses. **Ablation** (one-shot, run from the committed state; no permanent test file). The test imports the schema by relative path, so the source is what is resolved and no `dist/` is in the path. `node scripts/ablation-replace.mjs` replaced the refinement's `if (...)` guard with `if (false)`: - mutation: anchor 1 → 0, blob `70f2ae5d5b01` → `1de6a6511010`; - run: **7 failed / 43 passed (50)**. All six refusal pins went red, plus *following either remedy parses*, whose first assertion is the refusal. The controls and the value-refusal-first test stayed green. That is the expected direction, and it was observed; - restore: blob back to `70f2ae5d5b01` == HEAD blob, `git diff HEAD` empty. ## Flipped-semantics sweep (card clause) No fixture, example, doc or skill authors `conditions` + `mode` together. The sweep grepped for `mode: 'inclusive'|'exclusive'` and the double-quoted forms: - `examples/**`, `skills/**` and `content/docs/**`: 0 hits. The six files carrying `type: 'decision'` were checked for any `mode:`, and the only two hits are a screen node's `mode: 'create'` and a comment. - `packages/services`, `packages/lint`, `packages/cli`, `packages/metadata`, `packages/metadata-protocol`: 0 hits. - `/home/user/hotcrm` at `2f7b2326` (read-only): 0 hits across its 14 decision-bearing files. The control `isDefault` hits. objectui was not checked out in this container, so its designer form is **NOT MEASURED** here. objectui#10750 stays the coordination card (ruling item 4). ## Verification at `ae8185032` | command | result | |:--|:--| | `pnpm --filter @objectstack/spec build` | exit 0; the dropped-refinement ratchet passes with the new row. Without the row it printed `+ automation/DecisionConfig (1 site(s))` and exited 1 | | `pnpm --filter @objectstack/spec test` | exit 0: 549 files, 16159 passed, 2 todo | | `pnpm --filter @objectstack/spec typecheck` | exit 0 (`tsc`, `check:scripts-typecheck`, `check:test-typecheck`) | | `pnpm --filter @objectstack/spec check:generated` | 14/15 current plus `check:docs` stale. After `gen:docs`, `check:docs` gives exit 0, "226 generated files in sync" | | consumer closure `turbo run build --only` (service-automation / lint / metadata-protocol closures, plus client and client-react, excluding spec) | exit 0 | | `@objectstack/service-automation` vitest | exit 0: 146 files, 1757 passed | | `@objectstack/lint` vitest | exit 0: 110 files, 4258 passed | | `@objectstack/metadata-protocol` vitest | exit 0: 189 passed, 3 skipped files; 2715 passed, 19 skipped | | `dispatch-gates --commands` → each run → `--ran` | 107 derived, **105 exit 0**, 2 NOT MEASURED, 0 unrun | | eslint (`--no-inline-config --format json`) on the 2 touched `.ts` files | exit 0, 2 files, 0 errors, 0 warnings | | `check:nul-bytes` plus a control-byte self-scan of the 4 hand-edited files | exit 0; 0 matches | - **NOT MEASURED, declared.** `check:dual-build-cjs-loads` and `check:type-check-debt` both exited 3 (PREREQUISITE NOT MET): they need every workspace package built, which is 86 packages without `dist` here, and lint.yml's own prerequisite is a full `turbo build` of all packages. CI runs both on the PR. - **Consumer sweep direction.** I ran the direct importers of the schema family found by `git grep` (upstream of nothing; downstream of spec), plus `@objectstack/lint` as the dispatch named it. I did not run all of `...@objectstack/spec`: the public types are byte-unchanged (`check:api-surface` exit 0 with no regeneration; `z.input`/`z.infer` are unaffected by a refinement), so only the parse accept set of this one schema narrows. - **eslint narrowing.** The population is the two `.ts` files. The `.json`, `.md` and `.mdx` files match no eslint config object. The count comes from the JSON output. It is invariant for untouched files, because `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`). ## Acceptance notes - The pending objectstack-ai#19867 changeset still says "A `conditions` list is unaffected". It is left as written, because it is another PR's input. This PR's changeset states the refusal, and both reach the same release's CHANGELOG. If the release compiler wants one sentence, the edit is to append "— and `mode` beside a non-empty list is refused" to that bullet. - Ledger contention: PR objectstack-ai#20251 also edits `dropped-refinements.baseline.json`. `origin/main` `17bd31877` has not moved the ledger since `e46218674`. Whichever PR lands second re-merges with `bash scripts/pm/os-regen-merge.sh` and recounts both header totals from the body. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…(ADR-0049) (objectstack-ai#20398) Fixes objectstack-ai#20323 Clause-②: yes ## What this does Retires `action.aria` under ADR-0049 enforce-or-remove, following the triage direction on the card (RETIRE, on the chart-config precedent `2bf6ef18d`). No maintainer word reversed it to ENFORCE. - **Schema.** `ActionSchema.aria` becomes a `retiredKey()` tombstone. It is a `tsc` error (the input type is `never`) and a parse error that carries the prescription. The accessible name that IS applied is the action's required `label`, and the placing node's `aria` block (`page.components[].aria` or the list view `aria`) names the region. `AriaPropsSchema` is untouched: this is a key retirement, not a def retirement. - **ADR-0087.** The D2 conversion `action-aria-removed` (protocol 18, `retiredFromLoadPath`) strips the key from `actions[]` and `objects[].actions[]` as a lossless delete. The D3 semantic entry `action-aria-retired` is its own family, per the one-entry-per-family rule. The retired key `ui/Action:aria` is registered under 18, the major of the chart-config sibling. - **Ledger.** `liveness/action.json` regrades `aria` from `live` to `dead`. Its `REMOVED` note records that the uncited 「PARTIAL — honored by a few objectui renderers」 claim had no reader behind it. The undrilled-container row `action/aria` goes, and `state-counts.md` is regenerated. - **Docs.** Two hand-written pages taught `aria` on an action and are corrected: `protocol/objectui/actions.mdx` and `widget-contract.mdx`. The three reference pages that render `ActionSchema` are regenerated. - **Changeset.** `minor`, BREAKING, with `Clause-②: yes`, the FROM → TO table and the ADR-0087 `registered` marker. ## Premise, re-measured on this checkout - **objectui at the `.objectui-sha` pin `f8a9d0fb05`.** A reader grep for an action's `aria` (`action`, `actionDef`, `def`, `spec`, `btn`, `a`, `act`, `item` followed by `.aria`) over `packages/**` and `apps/**` non-test sources hits **0** lines. The control, the same grep for `.variant`, hits **18** files. Every `schema.aria` reader there is a placing node: the `record:*` components, `ListView`, `ObjectView` and `element:button`'s props. - **Icon-only reversal condition.** Triage named "icon-only actions have no accessible name" as the condition that would reverse this to ENFORCE. It does not hold. `action-icon.tsx:243` renders `aria-label={schema.label || schema.name}`, `action-menu.tsx:341` renders `aria-label={schema.label || moreActionsLabel}`, and `action-button.tsx:346` renders `{schema.label}` as the visible text. - **Framework.** A grep for `.aria` over `packages/**` non-test TS outside `packages/spec` hits **0** lines. `action.form.ts` has 0 `aria` rows; its one hit is the `variant` substring. - **Authors in this repo.** Across `examples/**`, the published `skills/**` and `packages/**` fixtures, **0** actions author `aria`. The control is **15** `variant:` lines in `examples/**`. - **Authors in HotCRM `2f7b2326`.** **0** actions author `aria`. Its 6 `aria:` blocks are all page-level `page.aria`, which stays live. The control: 7 files under `src/**/actions/` declare `locations:`, 17 times. - **Served schema.** `metadata-protocol` drops a tombstone's `{ not: {} }` node from the served JSON Schema, via `stripUnauthorableProperties`. So the Studio "More fields" form stops offering `aria` once this ships. - **Pinned sibling.** objectui's `ActionRunner.ts:410` mirrors the key as `aria?: SpecActionInput['aria']`. A type probe against this branch's built `dist` compiles that mirror at exit 0, because it evaluates to `undefined`. An authored block on it is refused with TS2322, and the control leg without `@ts-expect-error` exits 2. So the Console Pin Gate's objectui build is not broken by this change. ## Hand-over review (the stopped run's six commits, read hunk by hunk) | commit | verdict | | --- | --- | | `0c5dbbff` sources, ledger, tests, changeset | **kept, with corrections.** The prescription said "removed in @objectstack/spec 17"; it now says `17.5.0`, the spelling every sibling retirement on this line uses. The refusal pin in `action.test.ts` now also asserts the tombstone's own issue kind (`invalid_type` at `aria`, and no root `unrecognized_keys`, which is what a bare deletion would answer instead). The changeset's BREAKING sentence now names the replacement. The HotCRM control is re-measured: 7 files and 17 lines, not "5 action files". | | `6334c137` regenerated artifacts | kept, and re-derived after both merges | | `bfc86375` the stored-row pin uses a parseable script action | kept | | `0f20c62f` the `action/aria` undrilled-container row goes | kept; `check:liveness` is green | | `8cbcfa46` merge of `main` | kept | | `90b8fcdc` regenerated state counts | kept; superseded by the post-merge regeneration | Added in this round: - `33407119` and `c38c18ae` merge `origin/main` through `scripts/pm/os-regen-merge.sh`. Both merges stopped on the two registries. Each was settled by stacking both sides: main's conversions and rationale paragraphs first, then this branch's. The merges brought in objectstack-ai#20262, objectstack-ai#20352, objectstack-ai#20251 and objectstack-ai#20353. - `2b15085d` and `7faf0e9f` are the deferred regenerations. - `68b038ef` carries the corrections above, plus two more: - a new pin that the stored-row seam reaches an `object` row's nested action (the changeset's second at-rest coordinate); - one sentence on the `action` row of `liveness/README.md`. That row said "makes the dead set three", which this change makes false in a published file (`liveness` is in `@objectstack/spec`'s `files`). - `8dd3a2ce` regenerates the reference pages for the 17.5.0 prescription. ## Verification Head `7faf0e9f`, on `origin/main` `15bf186f`: - `pnpm --filter @objectstack/spec test`: `Test Files 558 passed (558)` · `Tests 16509 passed | 1 todo (16510)`. - `pnpm --filter @objectstack/spec test:repo`: `Test Files 35 passed (35)` · `Tests 634 passed (634)`. - `pnpm --filter @objectstack/spec check:generated`: all 15 generated artifacts up to date, measured over a spec build made on this head. - Gates: `node scripts/pm/dispatch-gates.mjs --commands` derives 114 commands. All 114 exit 0, and `--ran` reconciles them as `114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN`. The run covers every package's build closure, rebuilt on this head, so the gates that read `dist` measured it. Head `8dd3a2ce`, before the second `main` merge, on `origin/main` `862b6ce8`. The second merge touched none of these packages' interaction with this diff; the incoming commits retire other keys and touch no action surface. | package | command | Test Files | Tests | | --- | --- | --- | --- | | `@objectstack/lint` | `vitest run` | 112 passed | 4640 passed | | `@objectstack/cli` | `vitest run --project unit` | 231 passed | 3309 passed | | `@objectstack/runtime` | `vitest run --project local` | 282 passed | 4059 passed, 1 skipped | | `@objectstack/metadata-protocol` | `vitest run` | 189 passed, 3 skipped | 2736 passed, 19 skipped | | `@objectstack/metadata-core` | `vitest run` | 16 passed | 285 passed | | `@objectstack/objectql` | `vitest run --project local` | 322 passed | 5857 passed | `pnpm --filter @objectstack/spec typecheck` was green at the same head. The cli `integration` layer is declared to CI: this diff touches no spawn entry and no integration file. **Ablation**, from the committed state at `8dd3a2ce`. The blobs of `action.zod.ts` and of the three pin files are byte-identical at `7faf0e9f`. - The mutation goes through `scripts/ablation-replace.mjs`: the anchor ` aria: retiredKey(` becomes ` aria: z.any().optional().describe(`, so the key is accepted again. The tool reports the anchor going from 1 to 0, the replacement from 0 to 1, and the blob from `2e0a17b14b06` to `45e6bd9b1156`. The wrapper also arms a `trap` that restores the file. - The control leg runs the three pin files on the committed state: `Tests 166 passed (166)`. - The mutant leg: `Tests 4 failed | 162 passed (166)`. The four red tests: - `action.test.ts` · refuses an action carrying `aria`; - `aria-carrier-tombstones.test.ts` · the action tombstone fires and prescribes; - `aria-carrier-tombstones.test.ts` · the object-nested coordinate; - `action-aria-removed.test.ts` · the stored-row seam. - The restore is proven by content, not by an exit code. The file's blob equals HEAD's blob `2e0a17b14b06`, `git diff HEAD` is empty, and the porcelain status has 0 lines. - The pins import `./action.zod` and `../ui/action.zod.js` relatively. They read `src`, not `dist`, so this ablation has no dist leg. ## Acceptance notes - **objectui, owned by seat 4 after landing; not in this PR.** `ActionDefaultInspector` should list `aria` in its `RETIRED_FIELDS`, per triage note 3. `ActionRunner.ts:410` should drop its `aria?: SpecActionInput['aria']` mirror, which evaluates to `undefined` once this ships and still compiles (the probe above). - **objectstack-ai#19332, owned by seat 4 after landing.** The disposition of `action.aria` goes to objectstack-ai#19332's item that waits on this card. - **Advisory lint.** No `lint-liveness-properties` non-warn pin is added. The `aria` row never carried `authorWarn`, so the advisory lint's behaviour is unchanged: it was silent before and is silent now. - **Aliases.** `ActionSchema` never aliased `accessibility` or `ariaProps` onto `aria`, unlike the chart config. A probe shows both already refused as `unrecognized_keys`, so no alias refusal pin is owed. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… `listViews` entries (objectstack-ai#20357) Part of objectstack-ai#20301 Clause-②: no (narrowing) Stage 1 of a staged card: the list view's own `tabs` key only. objectstack-ai#20301 remains open for its second key, the view container's body `name`, which waits until cloud's writer stops sending it (triage note 2). This PR does not touch the container's `name`. ## What `ListViewSchema.tabs` (and the same key on `ObjectListViewSchema`, a view item record's list `config`, and the flattened list overlay at `PUT /api/v1/meta/view`) is retired under ADR-0049 enforce-or-remove. The triage verdict was RETIRE under the maintainer's family criterion: named-view switching is mainstream and is already delivered here by `listViews`, rendered by the saved-view switcher (`ViewTabBar`). - **Tombstone.** `tabs` becomes a `retiredKey()` tombstone inside `ListViewShapeSchema` only, beside the `pageName` tombstone on the same strict shape. Authoring it is a `tsc` error (input type `never`) and a parse error that carries the prescription: delete the key, and move each tab to a named `listViews` entry (tab `name` becomes the entry key, `label` the entry `label`, its `filter` rules join the view's own `filter`, copy `columns`). - **ADR-0087.** D2 conversion `view-list-tabs-removed` (protocol 18, retired from the load path) strips the key from every list payload in `stack.views[]`, in all three persisted spellings. **D2 chosen**: the delete is lossless in pixels because nothing ever read or drew the key, so a stored `view` row replays without it at every rehydration seam and re-saves through the strict write door instead of being refused there over a key that never had an effect. One D3 entry, `list-view-tabs-retired`, per ruling B on objectstack-ai#17152, with no tracker number in any author-shown field. `RETIRED_KEYS_BY_MAJOR[18]` gains `ui/ListView:tabs` and `ui/ObjectListView:tabs`. Declared boundary (same as the three sibling list-view conversions): an object's own `listViews` is reached by no conversion, so such an object is refused at its own door. - **Ledger.** The row `view.json` `/props/list/children/tabs` stays `dead` (tombstone discipline), re-verified 2026-09-27, with a REMOVED note. The stale `view/list.tabs` row leaves `undrilled-containers.baseline.json` (the tombstone is no longer a container, which `check:liveness` reported). - **Kit.** The metadata form's `tabs` repeater leaves with the key; the extracted form-label bundles are regenerated (pure deletion, 4 files). Reference docs, `authorable-surface/ui.json` (two rows become `[RETIRED]`) and the migration registry are regenerated by their generators. The published `objectstack-ui` skill's list-view rules stop teaching the key. `layout-dsl.mdx` stops pointing `ViewTab` at list views. Changeset: `@objectstack/spec` minor, BREAKING, FROM to TO table. - **Tests that pinned the retired acceptance** were flipped or re-pointed: `view.test.ts` drops "should accept list view with tabs" and the key from the full-parity fixture; two wire-door tests move their tab carrier to the surviving `userFilters.tabs`. `ViewTabSchema` is **not** retired: the page-only `userFilters.tabs` preset bar reuses it and renders. Pinned as a boundary. ## Measured first (premise holds) - What is true, stated first: **a list view's own `tabs` has no reader**, and objectui's `TabBar`, the one component that would draw it, has zero production mounts. `userFilters.tabs` is a different key with the same element type (`z.array(ViewTabSchema)`): objectui's `UserFilters` passes it to `TabFilters`, which renders it in production as a page list's preset bar, and it stays. - objectui at the pin `f8a9d0fb` (`.objectui-sha`): `git grep -E` for a `TabBar` JSX mount = 10 hits in 2 files, both test files (`components/__tests__/TabBar.i18nLabel.test.tsx`, `__tests__/icon-seam-fallbacks-5935.test.tsx`); every `TabBar` import is a test's. **Lit control**, same instrument and corpus: a `ViewTabBar` JSX mount in production source = 1 file, `packages/app-shell/src/views/ObjectView.tsx` (`views={viewTabItems}`, built from `objectDef.listViews`). No production `.tabs` read of a list view anywhere in `packages/*/src` at the pin. objectui's type mirror (`types/src/objectql.ts`, `tabs?: ListViewSchema['tabs']`) and its zod twin import the spec field by reference, so the tombstone flows in on the next pin bump, the `pageName` precedent; no export is removed here, so the Console Pin Gate build is unaffected. - This repo at base `4e0f72e8d`: zero list-view `tabs` authorings in `examples/` or in platform sources (the 2 `tabs:` hits in `examples/` are a page `userFilters` preset bar and a record-page slot); **control**: `listViews:` authored in 9 example files. The one published skill example that taught the key (`skills/objectstack-ui/rules/list-views.md`) is corrected here. No platform writer sends it. - Readers in this repo: two author-time reference walks read the key off RAW input (`packages/lint/src/validate-list-view-field-refs.ts#checkTabs`, `computeViewReferenceDiagnostics` in `packages/metadata-protocol/src/metadata-diagnostics.ts`). Neither draws a tab bar; the ledger already graded them as not delivering the key's effect. Left in place, see Acceptance notes. `packages/lint/src/validate-capability-references.ts:186` (a generic `rec.tabs` walk) does NOT reach a list view: its walk starts at `stack.apps[*]` and descends only an app node's `navigation` / `areas` / `tabs` / `children` / `items`, so it never visits a view. A third reader, found by CI and not by the pre-change census: the derived reference-site index behind `findReferencesToMeta` (the Studio "Used by" panel) walks each type's schema, and `listViews.*.tabs[].view` (a `ViewTab` naming a list view) was the ONLY property under `object` spelled as a `view` reference. With the key retired the walk finds none, so a view's "Used by" scan no longer reads `object` rows. Measured by an ablation of the tombstone over the `object` input schema: 1 view-spelled path now (`listViews.*.navigation.view`, itself retired), 2 with the tombstone removed (plus `listViews.*.tabs[].view`). No such reference is authorable any more; a stored object row still carrying one is refused at its own door (no conversion reaches an object's `listViews`). - hotcrm's inert-key test (objectstack-ai#16094) is inherited, NOT MEASURED here (out of this seat's repo scope). Cloud: NOT MEASURED here; the card's census at cloud `96eb092` names cloud writers for `view.name` only. ## Ablation (the tombstone is what shuts every door) Run on committed HEAD `74f8ab9727` with `node scripts/ablation-replace.mjs` (wrap mode, restore armed on EXIT/INT/TERM) against `packages/spec/src/ui/view.zod.ts`: - mutation landed on disk: anchor ` tabs: retiredKey(` x1 to x0, replacement (`tabs` back to a plain `ViewTab` array, the tombstone parked on a dummy key) x0 to x1, blob `b64e9996f3fc` to `90f0a141e429`; - `vitest run src/ui/view-list-tabs-retirement.test.ts`: **11 failed, 18 passed (29)**. Red: the refusal at all seven doors, the empty-array pin, the prescription pin, the parse-channel half of the tsc pin, and the object-door boundary. Still green, as expected: the D2 conversion, registration, form, `userFilters.tabs` boundary and absence pins, which do not depend on the tombstone. Direction observed: turned red; - restore proven by bytes, not exit code: blob after restore `b64e9996f3fc` equals the HEAD blob, `git diff HEAD` empty, tombstone anchor count 1, mutant count 0. The tsc door is proven by the positive run: `check:test-typecheck` is green with an `@ts-expect-error` on the `tabs` line of a `defineView` call, and an unneeded directive is itself a TS2578 in that same program. It was not separately ablated under tsc. ## Verification **Rework round 1, head `606046e96a`.** The at-tier review failed on the truth of two sentences; the retirement itself stood. (1) "The one component that reads a `ViewTab[]` has zero production mounts" was false, because `userFilters.tabs` is a `ViewTab[]` that `TabFilters` renders. It now reads: a list view's own `tabs` has no reader, and `TabBar`, the one component that would draw it, has zero production mounts; `userFilters.tabs` is read, rendered, and stays. Corrected in the changeset, the tombstone docblock in `view.zod.ts`, both retired-key entries, the conversion docblock, the step-18 rationale, the ledger note, the pin's header, and this body. (2) "examples, skills or platform sources" now reads "examples or platform sources", plus the note that the one published skill example that taught it is corrected here. `main` merged at `d0003a10c7` with `scripts/pm/os-regen-merge.sh`: the hand-written registry tails conflicted with objectstack-ai#20251 and both sides were kept, main's first. That also repaired a join from the previous merge that had dropped a period and space before this PR's rationale sentence. The object reference page was regenerated at `8845cb564e`, and the prose corrections landed at `c9f81fa132`. `main` moved again (the RLS policy `tags` retirement) and was merged at `606046e96a`, again keeping both registry tails, main's first; the migration registry regenerated byte-identically. Checks at `c9f81fa132`: spec build + `check:generated` all 15 up to date; `check:liveness`, `check:adr-0087-registration`, `check-changeset-no-major`, `check-empty-changeset`, `check:doc-authoring`, `check:nul-bytes`, `check:issue-citations`, spec docblock anchors, comment-mask adoption and `check:cross-package-test-inputs` all exit 0; the changeset's `Clause-②: no (narrowing)` line reads back as declared. Targeted spec tests at `c9f81fa132`: 5 files / 863 tests passed (`view-list-tabs-retirement`, `conversions`, `migrations`, `view`, `retired-key-migrate-sentence`). At `606046e96a`: the source-reading spec gates (`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:liveness`, `check:authorable-surface`, `check:docs`), `check-comment-mask-corpus` and `check:query-options-erasure` all exit 0. The shared verify lock was not acquired for a local post-merge spec rebuild. CI on `606046e96a`: 35 check runs, 33 success and 2 skipped, including TypeScript Type Check (the generated-artifact gates), Test Core 6/6 and Lint & Repo Gates. Derived gates at `606046e96a`: 122, 114 run green, 8 NOT MEASURED. Five of those lost their build prerequisites when a local turbo rebuild was OOM-killed on the shared box: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift` and `check:lean-entry-closure`, all green at `a3f8054dcc` and covered by CI. The other three are `check:i18n`, `check:dual-build-cjs-loads` and `check:type-check-debt`. **CI repair round, head `a3f8054dcc`** (main merged again at `bfa765ca08`, reference pages regenerated at `c13e610ab5`). Four consumer pins of the retired key, in packages the first round did not run, moved to the post-retirement truth, none weakened: `repeater-row-properties.test.ts` now asserts `view:tabs` ABSENT beside the two surviving view repeaters; `object-lifecycle-panel-echo-decisions.test.ts` positive control 608 to 598 (the repeater's label plus its nine row labels), with the reason in the pin's comment; `protocol.graft-normalized-operators.test.ts` keeps its view-filter leg and moves the nested `ViewTab.filter` leg to the surviving `userFilters.tabs` carrier; `protocol.read-seam-empty-accumulator.test.ts` reads the consulted source types from the derived index and pins `object` absent (see Measured first). Full suites at `a3f8054dcc`: `@objectstack/platform-objects` 55 files / 911 tests passed; `@objectstack/metadata-protocol` 189 files passed, 3 skipped (pre-existing) / 2736 tests passed; `@objectstack/lint` 112 files / 4640 tests passed; `@objectstack/objectql` `metadata-diagnostics.test.ts` 10 passed. Derived gates: 122, 119 run green, 3 NOT MEASURED (`check:i18n`: its CLI prerequisite build never acquired the shared lock, green at `baa383008b` on the same bundles; `check:dual-build-cjs-loads`; `check:type-check-debt`). First round, at merged HEAD `baa383008b` unless stated. - `pnpm --filter @objectstack/spec build` then `check:generated`: **All 15 generated artifacts are up to date** (migration registry, spec-changes, upgrade guide, authorable surface, api-surface, docs, liveness counts, test-typecheck and the rest). - `pnpm --filter @objectstack/spec typecheck`: exit 0 (tsc, scripts typecheck, and test typecheck: 53 files / 255 errors / 142 pinned signatures held, unchanged). - `pnpm --filter @objectstack/spec test` (local project): **554 files passed, 16426 tests passed**, 1 todo. - Repo project: full `test:repo` at `74f8ab9727`: **34 files, 633 tests passed**. After merging `main` again (the flattened-overlay `owner` / `hidden` retirement landed and conflicted only in the two hand-written registry tails; both sides kept), targeted rerun of the two repo-project retirement pins at `baa383008b`: 2 files, 47 tests passed. Declared narrowing: the other 32 repo-project files were not rerun after that merge; CI runs them. - i18n: `node scripts/check-i18n-bundles.mjs --write` regenerated the four `metadata-forms` bundles; `pnpm check:i18n` exit 0. - Derived gate families (`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, reconciled with `--ran`): **121 derived, 119 run, 2 NOT MEASURED, 0 unrun**. Named in the dispatch and green: `check:liveness`, `check:migration-registry`, `check:generated`, `check:spec-changes`. - NOT MEASURED: `check:dual-build-cjs-loads` (exit 3, needs a full workspace build), `check:type-check-debt` (repo-wide tsc re-measure of every package exceeded the foreground cap on a shared box). - `check:dts-closure` exit 1 on tree state, not on this diff: five packages this PR does not touch (cloud-connection, organizations, plugin-approvals, plugin-dev, verify) hold JS-only `dist/` in this worktree after a shared turbo cache replay during an unrelated closure build; the same gate was exit 0 at `74f8ab9727` before that build. CI builds fresh. - Lint, narrowed and proven: eslint `--no-inline-config --format json` over the 15 changed JS/TS files: 15 files, 0 errors, 0 warnings, none ignored. Population is `pnpm lint` (`eslint . --no-inline-config`, `eslint.config.mjs`); invariance: that config enables no type-aware linting (no `parserOptions.project`, stated in `eslint.config.mjs`), so this diff cannot move any untouched file's verdict. ## Governed surface: Tier H `skills/objectstack-ui/rules/list-views.md` is under `skills/**`, so this PR is **Tier H**: it lands only by the maintainer's hand or an authorized approval. No generated file under `skills/**` changed. Readings for the `skills/**` edit (lines): the edited file 309 to 302 (net -7, pure deletion: the list-level `tabs` example and a rule that described a tab bar that never rendered, replaced by a 3-line `listViews` pointer). Whole pack, all 10 `skills/**/SKILL.md`: 4402 to 4402 (no SKILL.md edited). ## Acceptance notes Out of this card's file surface, noted and not filed (dead code class; carrier named): 1. The two author-time walks above still read a list view's `tabs` off raw input, and their test fixtures still author it (`packages/lint/src/validate-list-view-field-refs.test.ts`, `packages/objectql/src/metadata-diagnostics.test.ts`, plus the CLI's negative i18n pin `packages/cli/test/i18n-tab-coverage.test.ts`). After this PR the parse refuses the key first, so those branches are unreachable for valid input. The tree-scoped absence pin declares these three files as self-expiring residue: each is asserted to still hold an offender, so the day a follow-up deletes the walks, the pin forces the entry out. Carrier: none named. 2. `packages/cli/src/utils/i18n-extract.ts` and `packages/lint/src/validate-translation-references.ts` carry prose that says the list view's own `tabs` "has no reader"; still true, now for a stronger reason. Carrier: none named. 3. objectui at its next pin bump: `types/src/objectql.ts` keeps `tabs?: ListViewSchema['tabs']` (becomes `undefined`-typed) and its parity tests may want the row updated. Carrier: the objectui pin-bump PR. ## 维护者速读(草稿) **改了什么** 列表视图上的 `tabs` 字段被正式退役。以后在列表视图里写 `tabs`,类型检查和保存都会直接报错,错误信息会告诉作者改用 `listViews`(每个命名视图就是切换条上的一个页签)。存量数据里残留的这个字段,升级时会被自动删掉。 **为什么改** 这个字段一直能写、能存,但没有任何代码读它,界面上也从来没有画出过任何东西:本来要画它的那个页签组件在正式代码里一次都没被用上;真正显示在记录列表上方的页签条,是按 `listViews` 逐个列出的已存视图。页面筛选栏里的 `userFilters.tabs` 是另一个字段,照常显示,不受影响。同一个能力两种写法,其中一种完全无效,AI 和作者都容易写错。按“主流平台已有、本平台已交付 ⇒ 只保留一种写法”的判据退役。 **风险与代价(含回滚)** 属于破坏性变更:外部仓库里若有人写了这个字段,升级后会被拒绝(报错里带修改方法)。本仓示例与平台代码实测零使用,唯一教过这种写法的已发布技能示例已在本 PR 中改正;外部使用面未测量。存量数据自动清理,不影响任何已有画面。回滚:revert 本 PR 即可恢复该字段。 **席位意见** **你要做的** 本 PR 触及已发布技能目录(`skills/**`),属于 Tier H,需要你本人批准后才能合并。 --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #19992
Clause-②: no
Retires
currencyConfig.precisionunder 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 #19910 (5805782503: 「a currency's decimal places are the currency's, not a setting」).#19992 remains open for its folded family site (
5854612946): the FIELD-levelprecision("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
currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }parsescurrencyConfigasunrecognized_keys, with the prescription belowcurrencyConfig.decimals/currencyConfig.scalerefused with Did you mean →precisionCurrencyConfigSchema.parse({})→{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}{"currencyMode":"dynamic","defaultCurrency":"CNY"}precisioncontradicting a fixed currency's ISO 4217 digits →customissue atcurrencyConfig.precisionprecision: "Decimal places (e.g., 2 for $10.50)"Refusal texts, verbatim (rendered from
srcat this head)precision:decimals(andscale, identical but for the key name):The trailing sentence is the schema's existing
history(FIELD_HISTORY), appended bystrictObject; it is not new text.Premises measured (Zone 2)
git grep currencyConfig(non-test) plus indirect spellings (a config held in a variable, then.precision), with a lit control.packages/**+examples/**+apps/**at4df101c3: 0 readers ofprecision; the controlcurrencyConfig?.currencyModeIS read (service-analytics/src/plugin.ts:1101).f8a9d0fband atmain25c7d584e: 0 readers.CurrencyField.tsx:82derives width ascurrencyFractionDigits(currency)on both; its:74comment says it never read the key. Control:currencyMode/defaultCurrencyreads 7 (pin) / 10 (main).main48d7066: 0currencyConfigmentions; the instrument fires on that ref (defaultCurrencyinconnector-stripe).examples/app-showcaseobjects (account,field-zoo,semantic-zoo), now edited. Studio inspector: none (objectui's ownObjectFieldInspector.currencyScale-10221.test.tsxpins that it writes the field-level key, nevercurrencyConfig.precision). Stored rows and built artifacts: nearly all of them — the removed.overwrite()bakedprecision: 2into 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.currency-config-precision-removed(includeRetired), strip the key, and serve the row canonical; it then parses. Pinned end to end throughapplyConversionsToStoredItem('object', …)incurrency-precision-iso4217.test.ts— the row as stored is refused by today's door, the converted row parses, and the field-levelprecision: 10survives.strictObject, and itsaliasesare rejection-with-suggestion, not renames. With the target gone they would point at a refusal (andalias-integrityaudits that). Precedent for a natural spelling with no landing key isFieldSchema'scurrencyguidance, sodecimals/scalegetguidanceentries 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".currencyPrecisionContradictionlost its only reader, andcurrencyFractionDigitswas read only by it: both are removed (neither was exported from a public entry — no barrel row, noapi-surface/row).CURRENCY_FRACTION_DIGITSstays:shared/value-domain.zod.ts:160reads its key set for theiso_4217_currencyvalue domain. The module docblock now says so and keeps the CLDR provenance.data-modeling/field-types.mdx,validation-rules.mdx,fields.mdx,getting-started/common-patterns.mdx(twoos:checkblocks),protocol/objectql/types.mdx, the published skillskills/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
precisiondeleted fromCurrencyConfigSchema;CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCEcarries the three prescriptions; the.superRefineand.overwriteare removed. Stale comments that named the twin (FieldSchema.precision, the finding(spec): the #7918 anchor reads a currency field'sprecisionas its display width, refusingprecision: 18on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #20011 note, the.overwriteprecedent notes) now say it is gone.currency-config-precision-removed(toMajor 18,retiredFromLoadPath, strips the key under every field'scurrencyConfigonobjectsandobjectExtensions, 2-notice fixture), wired into step 18'sconversionIdswith the rationale extended;RETIRED_KEYS_BY_MAJOR[18]gainsdata/CurrencyConfig:precision(one entry file, regenerated).authorable-surface/data.jsonrow removed deliberately (gate (a) tripwire; gate (c) proof 4 — guidance route — then passes);authorable-defaults/data.jsonlosesCurrencyConfig:precision = 2(regenerated);dropped-refinements.baseline.jsonloses the 13 sites the removed.superRefineproduced (data/CurrencyConfigitself plus 12 embeddings) — the refinement was removed with its key, the projection did not learn anything.field.form.tshelp text; theenbundle regenerated,zh-CN/ja-JP/es-ESset 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 precedentmetric-filters-removedis absent too), andcheck:spec-changes/check:upgrade-guideread up to date.@objectstack/specminor +@objectstack/platform-objectspatch,**BREAKING**, FROM → TO, andadr-0087: registered currency-config-precision-removed(check:adr-0087-registrationgreen).Absence half — no tree-scoped text pin, on purpose
precisiondoes 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. Thedashboard-chart-structure-refusal.test.tsprecedent covers this shape; the rationale block is in the test file. Standing in its place:tsc(the key is offCurrencyConfig's input type — Leg A below) and the closed parse door (Leg B below).Proofs (one-off, nothing left behind)
.d.ts.node scripts/ablation-replace.mjsputprecision: 2back intoaccount.object.ts(anchor 1 → 0, blob58a9af7e→700705a8), thentsc --noEmiton 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 HEADempty. Control (committed tree): showcasetypecheckexit 0.precisionon the schema (blob7d36200c→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 HEADempty; tree clean after both legs (0 bytes ofgit diff HEAD --stat+git status --porcelain). Direction observed: red, as expected.Verification (head
36819398; patch round on71ea994dbelow)pnpm --filter @objectstack/spec buildexit 0;check:generatedexit 0 (15 of 15 up to date, after the one stalecheck:docswas regenerated with--fix).pnpm --filter @objectstack/spec typecheckexit 0 (includescheck:test-typecheck, so the@ts-expect-errorin 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.@objectstack/example-showcasetypecheck exit 0 (after its dependency closure built);@objectstack/platform-objectstypecheck exit 0 and tests 55 files / 911 passed;@objectstack/service-analyticscurrency-mode-relay.test.ts+query-dataset.test.ts51 passed;pnpm check:i18nexit 0 (9 packages in sync, after the gate's own prerequisite closure).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 120 commands on this head; all run, exit codes on disk;--ranreconciles 119 run, 1 NOT-MEASURED, 0 UNRUN.check:skill-examples(covers the two editedos:checkblocks): first refused on its prerequisite (exit 3,client-reactunbuilt); afterpnpm --filter '@objectstack/client-react...' buildit exit 0 — "259 prose examples type-check across 3 surface(s)", 109 files.check:dual-build-cjs-loads— reason: its prerequisite is every package'sdist/(a fullpnpm build), and this diff changes no package entry orexports.pnpm lint(CI's run).f8a9d0fband atmainimports neither removed helper norCurrencyConfigParsed, and writes nocurrencyConfigliteral carryingprecision(multi-line scan over every file mentioningcurrencyConfig: 0 hits; controlcurrencyConfig: { … defaultCurrency … }literals: 5 files at the pin, 13 atmain).^...closure build, the i18n prerequisite closure and every heavy run went throughscripts/pm/os-verify-lock.sh.origin/main(3cb84d08) withscripts/pm/os-regen-merge.sh: conflicts stacked both sides (docs(spec): the currency-chain text states the fixed-mode condition — spec sites 5–10 of the currency-mode closure #20217'sdefaultCurrencydescribe kept beside this removal;view.hiddenandview.ownerare declared on the strictViewItemauthoring door and stored verbatim, but nothing in either repo reads or writes them — andcheck:livenesscannot see it, because its view walk stops at the container arm #20085'sview-item-owner-hidden-removedkept beside the new conversion, in both registries); every sibling entry measured present after the merge, same counts asorigin/main.scaleon a currency inline grid column;prefixpromises no default symbol (#20045) #20223), final head71ea994d.origin/maintwice withscripts/pm/os-regen-merge.sh:0d3ec471(merge1e6f29b2, regeneration0b2e3964), thene4621867(merge71ea994d). No rebase, no force-push.packages/spec/dropped-refinements.baseline.json, which is hand-maintained by rule (nogen:script;build-schemas.tsgates it). Both sides are stacked: fix(spec)!: refusescaleon a currency inline grid column;prefixpromises no default symbol (#20045) #20223's 12inlineColumns.elementsites anddata/InlineGridColumnroot are kept, this PR's 12currencyConfigsites anddata/CurrencyConfigroot stay removed, and feat(spec,automation): create_record / update_record fields.* accept the CEL value envelope — declared and evaluated together #20205's three automation entries are kept. The header totals were recounted to 210 schemas / 591 sites, andgen:schemaaccepts the ledger (exit 0).scaleon a currency inline grid column;prefixpromises no default symbol (#20045) #20223: no contradiction. Its field/column currency-scale refusals name neithercurrencyConfignorprecision, and both sides prescribe 「delete the key; decimal places are the currency's ISO 4217 minor unit」.71ea994d(locked):check:generatedexit 0 (15/15); spec typecheck exit 0;--project local549 files / 16138 passed, 2 todo;check-adr-0087-registration,check-changeset-no-majorandcheck:migration-registry(265 semantic / 215 retired-key / 199 retired-def) all exit 0.36819398.71ea994d: 35 check runs, 33 success, 2 skipped, 0 failing.Scope notes for the reviewer
skills/objectstack-data/rules/field-types.mdis 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-datamarkdown 3717 → 3716; allSKILL.mdfiles 4402 → 4402. It makes this PR Tier H.value-domain.zod.tsdocblock (cited the removed function), one comment inconversions/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.jsonis not edited. ThecurrencyConfigrow covers the container, asserts nothing aboutprecision, and stayslive(defaultCurrencyis read). The container is inundrilled-containers.baseline.json, so no per-key row existed to remove. docs(spec): the currency-chain text states the fixed-mode condition — spec sites 5–10 of the currency-mode closure #20217 had just re-cited that row.Acceptance notes
docs/qa/platform-checklist/areas/records-forms.jsonitemrecords-forms.field-type-constraintsstill describesf_currencyasscale 2 currencyConfig{precision 2}. Both halves are stale now: thescalehalf since an earlier card retired it from currency, theprecisionhalf 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; 承接者:无.fieldmetadata rows are outside every field conversion's reach (the stored-row seam has nofieldsstack collection), this one included, the same as its precedents. Observation only; no producer of such rows was measured.18.field-scale-precision-integer-refused,18.ui-form-field-precision-scale-integer-refused) sayCurrencyConfigSchema.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