Skip to content

fix(spec)!: refuse scale on a currency inline grid column; prefix promises no default symbol (#20045) - #20223

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20045-inline-currency-column-scale
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20045-inline-currency-column-scale

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20045
Clause-②: no (narrowing)

Clause-②: no (narrowing) per the seat's ruling 5855671317 on #20045: this diff widens nothing and only refuses a shape that parsed; the claim's yes is corrected there.

Branch claude/issue-20045-inline-currency-column-scale, base 1c8b320a89. It is merged with origin/main twice through scripts/pm/os-regen-merge.sh: first b09ce67870 as 4a6e8ed61a, then, in rework round 1, 805af4f290 as 9f01270dd2. The regeneration is its own commit, df6b175e58. Head is 4ab1d0cac2. Every reading below was taken at head 4ab1d0cac2 unless it names another commit.

What changes

Triage direction: comment 5825687357, option A. Triage read the card as inherited from ruling B 5791803339 on #19629 (scale retired from the currency field) and ruling 乙 5805782503 on #19910 (「a currency's ISO 4217 minor unit decides its display」).

  1. InlineGridColumnSchema (packages/spec/src/data/field.zod.ts) now refuses an authored scale on a column that declares type: 'currency'. The refusal is one custom issue at the column's scale, and it applies to any value, scale: 0 included, computed or not. The message carries ruling B's shape:
    • first sentence: "scale is not valid on a currency inline grid column — delete the key." This is the field refusal's first sentence with only the subject swapped;
    • remedy: the currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD) decides how the cell displays the amount and the width a computed amount is rounded to. It names no other key.
    • No alias and no grace window.
    • scale on a number column, and on a column that declares no type, is unchanged.
  2. The prefix describe no longer says 「(default '¥')」. It now reads: the symbol replaces the resolved currency's own symbol; there is no default; it replaces the symbol only. prefix is still accepted. The scale describe drops "numeric/currency" and names the currency refusal.
  3. ADR-0087: the D3 semantic entry 18.inline-grid-column-currency-scale-refused is added, and registry.ts was regenerated with gen:migration-registry.
  4. The changeset makes @objectstack/spec minor, with a BREAKING banner, a FROM → TO table and the registered disposition marker.
  5. dropped-refinements.baseline.json gains the new refinement's rows, exactly as the build's ratchet printed them: data/InlineGridColumn (root) plus one …inlineColumns.element site on each of the 12 published schemas that embed the column. The header totals moved with the body: 208 schemas, 590 sites after the merge.
  6. content/docs/references/data/field.mdx was regenerated (check:generated --fix, which proved only check:docs stale).

ADR-0087 disposition: why D3 only

Premise, measured

  • On 1c8b320a89, InlineGridColumnSchema was a bare strictObject with no refinement, and scale was declared for a "computed numeric/currency result".
  • The ablation below is the executable half of that reading. With the new refinement short-circuited, all six refusal pins go red, because every currency-plus-scale shape parses again.
  • The objectui side was read by content:
    • at objectui origin/main 25c7d58, GridField.tsx has function currencyAdornment ×1 and function currencyWidth ×1, and c.prefix || '¥' ×0;
    • at this repo's .objectui-sha pin f8a9d0fb0596, it has c.prefix || '¥' ×2 and the currency ? 2 default ×1 (see Acceptance notes).

Mechanism hypothesis H1, measured

Authored instances (H2), census on 1c8b320a89

  • git grep inlineColumns over the whole tree gives 30 files. Exactly one authored block: examples/app-showcase/src/data/objects/invoice.object.ts, with 7 identity-only { name } columns, 0 declaring type and 0 declaring scale. This is the control: the same instrument counts 7 column entries in that block.
  • Platform objects, skills/**, content/docs/** prose and examples, and JSON/YAML fixtures: 0 inline grid columns.
  • Objectui at the pin: 0 authored inlineColumns fixtures. Its only hits are the two renderer files.
  • Test fixtures: 1 carried scale: 2 on a currency column (inline-related-columns.test.ts, the "every renderer-read key" case). It was re-judged: scale was deleted there, and the key stays covered by the identity-only computed amount column in the same fixture.
  • Deployed metadata: NOT MEASURED, because no instrument reaches it.

Tests, at head 4ab1d0cac2

All heavy runs went through scripts/pm/os-verify-lock.sh under one stable slot, and each verdict below is read from its VERDICT line.

  • @objectstack/spec build: exit 0.
  • check:generated: "All 15 generated artifacts are up to date", including ✓ check:migration-registry.
  • registry.ts was regenerated with gen:migration-registry from the merged entries/ directory. The output is byte-identical to the merge. Quoted-exact git grep -c -F on 4ab1d0cac2, 1 hit each:
  • check-adr-0087-registration --base origin/main: exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered inline-grid-column-currency-scale-refused".
  • check-changeset-no-major --base origin/main --event with THIS body: exit 0, "✓ LEVEL AXIS: this PR declares clause-② no (narrowing)".
  • Targeted, the 5 files (the new file, inline-related-columns, field-currency-scale-refused, validation-boundary-description, authoring-key-lint): 5 files / 71 passed.
  • spec vitest --project local: 545 files passed; 16045 passed, 2 todo.
  • spec vitest --project repo: 32 files / 586 passed.
  • spec typecheck: exit 0 (check:test-typecheck: OK — 53 file(s) / 255 error(s) / 142 pinned signature(s) held).
  • The new file inline-grid-column-currency-scale-refused.test.ts covers:
    • refusal through FieldSchema, ObjectSchema and the standalone column, located at …inlineColumns[i].scale, with code: 'custom';
    • every value, computed or not;
    • controls: a number column, an untyped column, prefix accepted, and the remedy parsing and re-parsing unchanged;
    • field/column shape parity;
    • the two describes;
    • the D3 entry being registered.

Ablation, from committed state aa4b6ca14f, run with scripts/ablation-replace.mjs:

  • The anchor if (column.type === 'currency' && column.scale !== undefined) { was hit ×1 and went ×1 → ×0. The replacement if (false && … went ×0 → ×1. The blob changed from 147220988d04 to ddf1ebbf0437.
  • Result: 6 failed | 22 passed (28). All six refusal/parity pins went red, and the controls stayed green, which is the expected direction.
  • Restore: blob 147220988d04 == HEAD, and git diff HEAD is empty.
  • The subject is imported from src (./field.zod), so no dist rebuild was involved.

Gates. dispatch-gates.mjs --commands was re-derived on 4ab1d0cac2 and gave 111 families, the same list as on 4a6e8ed61a. All 111 were re-run on 4ab1d0cac2. --ran reconciliation: "111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED".

  • NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET. Packages have no dist/, and only a full-repo build satisfies that. This diff changes no entry point, export map or build config.
  • NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET (@objectstack/driver-turso has no built type entry point in this worktree). Its --re-measure otherwise starts a workspace-closure build outside the verify lock. This diff changes no exported type (check:api-surface is green) and touches none of the four DEBT-ledgered packages.

Rework round 1, diff 4a6e8ed61a → 4ab1d0cac2 (107 files, +6088/−682):

  • From main (b09ce67870 → 805af4f290):
    • 102 files are byte-identical to 805af4f290;
    • in the 4 files this branch also touches, the change set equals main's own line for line (field.zod.ts 2 lines, registry.ts 224, field.mdx 4, dropped-refinements.baseline.json main's rows).
  • Regenerated:
    • registry.ts, by gen:migration-registry, byte-identical to the merge;
    • field.mdx, by gen:schema && gen:docs (df6b175e58). The os-regen driver had kept the branch side and dropped main's two defaultCurrency rows, and the regeneration restores them;
    • the ledger header totals, recounted from the merged body: 588 → 601 sites, 208 schemas.
  • Anything else: exactly one line, the changeset's Clause-②: yes (narrowing) → Clause-②: no (narrowing). The PR body's Clause-② line moved in this same edit.

Deviations

Acceptance notes

  • Reach of the refusal. Only a DECLARED column type is judged. A column that declares no type gets its type from the child field when objectui hydrates it (deriveMasterDetail.ts hydrateColumns, which spreads the authored column). So { name: 'amount', scale: 2 } over a currency child field still parses. The column schema cannot see the child object. Once the objectui follow-up stops reading an authored scale on currency columns, that key is ignored on such a column. That lands on the objectui follow-up card (triage note 4), not here.
  • Pinned console vs this describe. The new prefix describe ("No default") is true of objectui main (25c7d58). The console bundled at .objectui-sha f8a9d0fb0596 still falls back to ¥ and rounds a computed currency with no scale to 2. The describe becomes true of the bundled console when the pin moves past objectui PR objectstack init scaffolds a project whose pnpm install exits 1 on pnpm 11 — its rendered pnpm-workspace.yaml omits allowBuilds #10405. Deleting scale loses no decimals under either console: the pin falls back to 2, and main to the ISO minor unit. So no zero-decimal window opens.
  • Stale notes outside this surface, recorded only (carrier: none):
    • packages/spec/liveness/field.json → inlineColumns.children.prefix / .scale notes still cite c.prefix || '¥' and c.scale ?? currency default 2;
    • the packages/spec/src/data/field-scale.ts docblock sentence "The one 2 that looks like a currency default belongs to an inline grid COLUMN's rounding" describes the pre-objectui#10355 grid.
  • The objectui GridField follow-up (drop the authored-scale read on currency columns) is objectui's own card, per triage note 4. Nothing in objectui changes here.

Generated by Claude Code

… promises no default symbol

InlineGridColumnSchema now refuses an authored `scale` on a column that
declares `type: 'currency'`, carrying the currency-field refusal's first
sentence and remedy (delete the key; the currency's ISO 4217 minor unit
decides). `scale` on a number column, and on a column that declares no
type, is unchanged. The `prefix` describe no longer promises a yen
default: it replaces the resolved currency's symbol and has none.

ADR-0087 D3 semantic entry inline-grid-column-currency-scale-refused
registered under protocol major 18; registry regenerated.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…nt in the dropped-refinements ledger

The new InlineGridColumnSchema refinement has no JSON Schema form, so the
build's dropped-refinement ratchet names it: one new row for
data/InlineGridColumn (site at the root) and one `inlineColumns.element`
site on each of the twelve published schemas that embed the column,
exactly as the gate printed them; header totals moved with the body.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…'s prefix and scale describes

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…line column scale refusal

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…line-currency-column-scale

# Conflicts:
#	packages/spec/dropped-refinements.baseline.json
…line-currency-column-scale

# Conflicts:
#	packages/spec/dropped-refinements.baseline.json
The os-regen merge driver kept the branch's bytes of field.mdx and dropped
main's CurrencyConfig.defaultCurrency describe rows; regenerated from the
merged sources (gen:schema then gen:docs), which restores main's rows and
keeps this branch's inline-column prefix/scale rows. registry.ts was
regenerated with gen:migration-registry from the merged entries/ directory
and came out byte-identical to the merge.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
…e column scale refusal

Per the seat's ruling 5855671317 on #20045: this diff widens nothing and
only refuses a shape that parsed. The narrowing arm, the level (minor) and
the ADR-0087 disposition are unchanged.

Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 805af4f290565955d6e6b56ee46fed45f721d955 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from aa222be880efabd8465cf039476ee80be53d8e82 — the merge of head 4ab1d0cac243ff74c4ec7951e3471c718974fb55 into base 805af4f290565955d6e6b56ee46fed45f721d955, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin aa222be880efabd8465cf039476ee80be53d8e82 && git checkout aa222be880efabd8465cf039476ee80be53d8e82
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 805af4f290565955d6e6b56ee46fed45f721d955 4ab1d0cac243ff74c4ec7951e3471c718974fb55 && git checkout -B drift-repro 805af4f290565955d6e6b56ee46fed45f721d955 && git merge --no-ff 4ab1d0cac243ff74c4ec7951e3471c718974fb55

node scripts/docs-audit/affected-docs.mjs --json 805af4f290565955d6e6b56ee46fed45f721d955

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4ab1d0cac243ff74c4ec7951e3471c718974fb55

① Derived judgments

Diff read three-dot against objectstack origin/main 66e266c931 (merge-base 805af4f290): 8 files, +388 / −10 — the changeset, content/docs/references/data/field.mdx, packages/spec/dropped-refinements.baseline.json, packages/spec/src/data/field.zod.ts, the new pin inline-grid-column-currency-scale-refused.test.ts, one fixture line in inline-related-columns.test.ts, the D3 entry 18.inline-grid-column-currency-scale-refused.ts and its registry.ts row.

  1. One accept-set change, a narrowing, exactly as declared. InlineGridColumnSchema gains a .superRefine that adds one custom issue at path: ['scale'] when column.type === 'currency' && column.scale !== undefined. Nothing else in the strict object moves: scale's own rule (int().nonnegative().max(MAX_RENDERABLE_SCALE)) and prefix: z.string().optional() are unchanged. The condition reads only the DECLARED type, so an identity-only column, whose type the console derives from the child field at hydration, keeps parsing; the docblock, the changeset and the D3 entry all say so under "Reach". scale has no schema default, so undefined always means "not authored" and parse(parse(x)) is stable; the pin's remedy test asserts the re-parse.
  2. The message is the field refusal carried, not reworded. INLINE_GRID_CURRENCY_SCALE_REFUSAL opens with FieldSchema's currency refusal (field.zod.ts:2301 on main) with "currency field" → "currency inline grid column", then the same ISO 4217 minor-unit clause and the same remedy (delete the key, nothing replaces it, scale stays valid on a number column). The pin proves parity by String.replace of the field sentence, not by copying the constant, and refuses currencyConfig|precision in either message.
  3. Published text is true. prefix's describe drops "(default '¥')" for "No default: when omitted, the cell shows the symbol of the currency it resolves" — objectui GridField.tsx:1132-1133 reads currencyAdornment(c, currency, locale), "never a default ¥" (objectui#10355), so the old sentence described a fallback the renderer no longer has. scale's describe now says "computed numeric result" and names the currency refusal. field.mdx updates the same two rows in both InlineGridColumn tables (:209, :219 and :359, :369) byte-for-byte to the describes.
  4. The pin can fail, and its controls are the right ones. Refusal via FieldSchema (the card's repro, computed scale: 4, one issue at ['inlineColumns', 1, 'scale']), every declared value including scale: 0 computed or not, via ObjectSchema (['fields', 'invoice', 'inlineColumns', 0, 'scale']), and standalone; controls: number column keeps scale: 3, undeclared-type column keeps scale: 2, prefix round-trips on a currency column, the remedy (delete scale, add nothing) parses and re-parses unchanged; shape parity; both describes; the D3 entry under major 18 with a replacement that says DELETE and names no carrier. Reverting the .superRefine turns the four refusal tests green-to-red (the safeParse succeeds), and the old strings fail the describe tests. inline-related-columns.test.ts drops scale: 2 from its currency column with a comment naming the pin.
  5. The JSON Schema ledger grows by the one route it allows. dropped-refinements.baseline.json: +1 schema (data/InlineGridColumn), +13 sites (588 → 601, 207 → 208), each new site an …inlineColumns.element under a schema that already lists …currencyConfig at the same depth — the same class as the currency FIELD refusal. The projection closed list (required-one-of, non-blank-string, dependent-required, banned-keys, banned-key-pattern) has no conditional-forbid pattern, so the ledger is the correct carrier and build-schemas.ts accepted the moved lines (Build Core green). Consequence, stated: the published JSON Schema still accepts scale on a currency column; the parse refuses it — the ledger's own description names this class. ③.
  6. Population, re-measured at 66e266c931. inlineColumns authors outside packages/spec/src, tests and generated references: the showcase invoice.object.ts (identity-only columns), prose in docs, ADR-0035, audits, CHANGELOGs, the liveness and authorable-surface ledgers, builtin-column-collision.ts, the census script and the relationships skill rule. A scale on a type: 'currency' column appears in none of them (the one scale: 2 near an example, fields.mdx:67, is Field.percent). Matches the changeset's census; deployed metadata is NOT MEASURED and the changeset says so.
  7. ADR-0087. The D3 entry and the step18 registry row carry the same id, surface, replacement, reason and acceptanceCriteria; the changeset carries <!-- adr-0087: registered inline-grid-column-currency-scale-refused -->; Check Changeset, Lint & Repo Gates and Spec property liveness are green at head.
  8. Mergeability and CI. git merge-tree --write-tree origin/main refs/pull/20223/head at 66e266c931 exits 0 with no conflict. Check runs on 4ab1d0cac2: 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke — both on the expected-skip roster), 0 red.

② Semver level

@objectstack/spec: minor with a **BREAKING** banner, a FROM → TO table and the registered marker — the launch-window grade for an accept-set narrowing, and every changeset sentence checks out against items 1-7. The changeset's Clause-②: no (narrowing) is exactly the clause2-line.mjs:95 arm: not a widening, but breaking. The PR body's line reads a bare Clause-②: yes, copied, as the body says, from the claim comment 5854472496; a bare yes declares a widening this diff does not contain. Both spellings take at least minor and both mark the change breaking (banner, arm), so no gate outcome differs and the published text is the right one. Recorded, not blocking; the claim's spelling was the seat's line, not the dev's.

③ Boundary flags

  • Dev flag, stale notes outside this surface (carrier: none): packages/spec/liveness/field.json notes on inlineColumns.children.prefix / .scale still cite c.prefix || '¥' and c.scale ?? 2; the field-scale.ts docblock's "one 2 that looks like a currency default belongs to an inline grid COLUMN's rounding" describes the pre-objectui#10355 grid. Both describe objectui code that has moved and neither is published text; correctly kept out of this PR. The director seat attaches a carrier card with this record (filed, or an existing one if the board already holds it).
  • Item 5: the JSON-schema silence for this refinement is the ledger's known class, with the currency FIELD refusal as precedent; adding a conditional-forbid arm to the projection list is a public-contract decision, not this PR's.
  • The objectui GridField follow-up (drop the authored-scale read on currency columns) is objectui's card per triage note 4; nothing in objectui changes here.
  • NOT GOVERNED paths; Fixes #20045 closes only the intended card.

Implemented-by: claude/issue-20045-inline-currency-column-scale
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

Rendered at tier by the director seat (summon #30 续) on the fetched head. Not a governed path; the landing pre-checks are met on this head (fresh PASS, CI green, no conflict), so needs:contract-review comes off with this record and ready + auto-merge follow through the relay.


Generated by Claude Code

os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…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>
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…(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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nd the 25 missing entries (objectstack-ai#20201) (objectstack-ai#20255)

Fixes objectstack-ai#20201
Clause-②: no

## What

Ruling B on objectstack-ai#17152 (director `5615360777`, restated `5634031140`, on the
maintainer's objectstack-ai#15954 authority `5559778263`): every retirement family
carries ONE ADR-0087 D3 (`semantic`) entry, even when a lossless D2
conversion repairs its data; D2 carries the mechanical repair only. This
PR takes the major-18 family census the card asks for, adds the 25 D3
entries it found missing, corrects the prose that justified their
absence, and pins the census in the registry's own test.

- **25 new D3 entries** under
`packages/spec/src/migrations/entries/semantic/18.*.ts`, one per
D2-backed family that had none. Each names its family and its D2
conversion id, says what D2 already repairs, and says what judgment the
consumer still owes (the `reason`), with an `acceptanceCriteria` the
consumer can check. None is a placeholder: every one states a residue
specific to its family (a unit only the author knows, a belief the
platform never honoured, a shape the conversion deliberately leaves
alone, code the chain cannot reach).
- **Prose corrected** at the sites of `5854110917` and `5854456343`,
plus step 18's rationale and step 17's docblock fact (list below).
- **Census pin** in the existing
`packages/spec/src/migrations/migrations.test.ts` (registry integrity).
No new check script.
- `MIGRATIONS_BY_MAJOR[18].semantic` 186 → 211 entries at the census
base; after merging `main` (four sibling D3 entries landed meanwhile,
none with a D2 conversion) the generated region holds 215.

## The census (the card's main deliverable)

**Tree.** `objectstack-ai/objectstack` at `3cb84d084` (this branch's
fork point; it already contains objectstack-ai#20227's view-item retirement). Major-18
population there: **185** `retired-keys`, **146** `retired-defs`,
**186** `semantic` entry files, and **36** D2 conversions graduated into
step 18. (The card measured 181 / 130 / 175 at `d7c024133e`.)

**Grouping rule (ruling B's unit, held constant).** Where a D2
conversion exists, the conversion is the family: every retired key or
def it repairs belongs to it, and a D3 entry may cover more than one
conversion only where one judgment covers them (the pre-existing
`element-filter-and-form-node-refused` covers `element-filter-removed`
and `element-form-removed`). Every new entry here covers exactly one
conversion. Where no conversion exists, the records are grouped by the
D3 entry that names them.

**Method.**
1. Mechanical pass (scratch scripts, not committed): for each of the 36
conversions, the major-18 `semantic/` entry files that name its id as a
whole id (comment or field); for each retired key, the conversion its
own comment names, else the major-18 entries naming it as `cat/Def:key`,
`Def.path` / `Def:path`, or the def name plus the leaf key; for each
retired def, the entries naming the def.
2. Reading pass, where a string match cannot decide: (a) ownership: an
entry that names a conversion only in passing is not that family's entry
(this is how `metric-filters-removed` was classed missing although
`analytics-authorable-unknown-keys-refused` names it); (b) 14 records
matched by more than one entry, each placed by its own comment (for
example `kernel/PluginStartupResult:plugin`, which goes to
`startup-orchestrator-retired`); (c) 9 records whose comment names no
conversion but which belong to a D2 family
(`integration/DeclarativeConnectorEntry:connectionTimeoutMs` and
`:errorMapping`, the three error-mapping defs, the four responsive-shape
defs), plus `integration/Connector:connectionTimeoutMs`, whose comment
names two conversion ids and belongs to
`connector-connection-timeout-ms-removed` (it names the permission
conversion only as a comparison; round 1's Table 1 placed it wrongly,
corrected in patch round 1); (d) 5 theme sub-block defs, named by the
theme family's entry as its sub-blocks.

**Control.** The pairing sees a family that has its entry: 11 of the 36
conversions pair with a pre-existing entry, among them
`cube-join-sql-and-relationship-removed` with
`cube-join-sql-and-relationship-retired` (which the pin's own control
test also asserts), and the whole-id matcher refuses a prefix
(`record-chatter-position-vocabulary` is a prefix of its entry's own id
and matches only the entry's real citation). Evaluated at the base with
the pin's logic: **24** conversions named by no major-18 entry; the
reading pass adds `metric-filters-removed` for **25**. Evaluated at this
head: **0**.

**Result.** 331 records (185 keys + 146 defs) plus 36 conversions:

- **36 D2-backed families** covering 58 records: 11 had their D3 entry,
**25 had none**. Table 1.
- **273 D2-less records** in 68 groups: every one is named by an
existing D3 entry. Table 2. None missing, as expected: before ruling B,
a retirement with no conversion needed a D3 entry anyway.

The card's grep for 「lossless」 found the `tenancy.organizationField`
site and step 17. The census finds 25 major-18 families, most of them
with no 「lossless」 wording at all.

### Table 1 — D2-backed families (step 18 `conversionIds`, in order)

| # | D2 conversion (the family) | registered records | D3 entry at base
| D3 entry after |
|---|---|---|---|---|
| 1 | `field-malformed-scale-precision-removed` | none (value or
strict-key retirement, not in the two tables) |
`field-scale-precision-integer-refused` | unchanged |
| 2 | `record-chatter-position-vocabulary` | none (value or strict-key
retirement, not in the two tables) |
`record-chatter-position-vocabulary-converged` | unchanged |
| 3 | `element-input-target-variable-removed` |
`ui/ElementRecordPickerProps:targetVariable`,
`ui/ElementTextInputProps:targetVariable` | MISSING |
`element-input-target-variable-retired` (new) |
| 4 | `element-filter-removed` | `ui/ElementFilterProps:aria`,
`ui/ElementFilterProps:fields`, `ui/ElementFilterProps:layout`,
`ui/ElementFilterProps:object`, `ui/ElementFilterProps:showSearch`,
`ui/ElementFilterProps:targetVariable` |
`element-filter-and-form-node-refused` | unchanged |
| 5 | `element-form-removed` | `ui/ElementFormProps:aria`,
`ui/ElementFormProps:fields`, `ui/ElementFormProps:mode`,
`ui/ElementFormProps:object`, `ui/ElementFormProps:onSubmit`,
`ui/ElementFormProps:submitLabel` |
`element-filter-and-form-node-refused` | unchanged |
| 6 | `field-column-lists-canonicalized` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`field-inline-and-related-list-columns-closed` (new) |
| 7 | `metric-filters-removed` | `data/Metric:filters` | MISSING (named
only in passing by `analytics-authorable-unknown-keys-refused`) |
`cube-metric-filters-retired` (new) |
| 8 | `cube-sub-day-granularities-removed` | none (value or strict-key
retirement, not in the two tables) |
`time-update-interval-sub-day-retired` | unchanged |
| 9 | `cube-join-sql-and-relationship-removed` |
`data/CubeJoin:relationship`, `data/CubeJoin:sql` |
`cube-join-sql-and-relationship-retired` | unchanged |
| 10 | `record-highlights-field-icon-removed` |
`ui/RecordHighlightsField:icon` | MISSING |
`record-highlights-field-icon-retired` (new) |
| 11 | `mapping-lookup-params-removed` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`mapping-lookup-params-retired` (new) |
| 12 | `translation-component-submit-label-removed` | none (value or
strict-key retirement, not in the two tables) | MISSING |
`translation-component-submit-label-retired` (new) |
| 13 | `page-component-responsive-removed` |
`ui/PageComponent:responsive`, `ui/BreakpointColumnMap`,
`ui/BreakpointName`, `ui/BreakpointOrderMap`, `ui/ResponsiveConfig` |
MISSING | `page-component-responsive-retired` (new) |
| 14 | `object-grid-default-sort-removed` |
`ui/ObjectGridProps:defaultSort` | MISSING |
`object-grid-default-sort-retired` (new) |
| 15 | `object-kanban-quick-add-removed` |
`ui/ObjectKanbanProps:quickAdd` | MISSING |
`object-kanban-quick-add-retired` (new) |
| 16 | `permission-allow-restore-purge-removed` |
`security/EffectiveObjectPermission:allowPurge`,
`security/EffectiveObjectPermission:allowRestore`,
`security/ObjectPermission:allowPurge`,
`security/ObjectPermission:allowRestore` | MISSING |
`permission-restore-purge-bits-retired` (new) |
| 17 | `form-view-option-default-removed` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`form-view-option-default-retired` (new) |
| 18 | `field-reference-to-alias` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`field-reference-to-spelling-retired` (new) |
| 19 | `connector-error-mapping-removed` |
`integration/Connector:errorMapping`,
`integration/DeclarativeConnectorEntry:errorMapping`,
`integration/ConnectorErrorCategory`, `integration/ErrorMappingConfig`,
`integration/ErrorMappingRule` | MISSING |
`connector-error-mapping-retired` (new) |
| 20 | `connector-connection-timeout-ms-removed` |
`integration/Connector:connectionTimeoutMs`,
`integration/DeclarativeConnectorEntry:connectionTimeoutMs` |
`connector-provider-context-connection-timeout-ms-retired` | unchanged |
| 21 | `hook-timeout-to-timeout-ms` | none (value or strict-key
retirement, not in the two tables) | MISSING |
`hook-timeout-unit-in-key` (new) |
| 22 | `job-timeout-to-timeout-ms` | `system/Job:timeout` | MISSING |
`job-timeout-unit-in-key` (new) |
| 23 | `api-endpoint-cache-ttl-to-cache-ttl-seconds` |
`api/ApiEndpoint:cacheTtl` | MISSING |
`api-endpoint-cache-ttl-unit-in-key` (new) |
| 24 | `dashboard-refresh-interval-to-refresh-interval-seconds` |
`ui/Dashboard:refreshInterval` | MISSING |
`dashboard-refresh-interval-unit-in-key` (new) |
| 25 | `connector-health-and-trigger-durations-unit-in-key` |
`integration/CircuitBreakerConfig:monitoringWindow`,
`integration/ConnectorTrigger:interval` | MISSING |
`connector-resilience-durations-unit-in-key` (new) |
| 26 | `memory-persistence-auto-save-interval-to-ms` |
`data/AutoPersistenceConfig:autoSaveInterval`,
`data/FilePersistenceConfig:autoSaveInterval` | MISSING |
`memory-persistence-auto-save-interval-unit-in-key` (new) |
| 27 | `turso-config-timeout-to-timeout-ms` | `data/TursoConfig:timeout`
| MISSING | `turso-config-timeout-unit-in-key` (new) |
| 28 | `view-page-mount-removed` | `ui/ListView:pageName`,
`ui/ObjectListView:pageName` | MISSING | `list-view-page-mount-retired`
(new) |
| 29 | `list-view-sort-string-clause-to-array` | none (value or
strict-key retirement, not in the two tables) | MISSING |
`list-view-sort-string-clause-retired` (new) |
| 30 | `page-assigned-profiles-removed` | `ui/Page:assignedProfiles` |
`page-assigned-profiles-audience-to-permission-set` | unchanged |
| 31 | `chart-config-aria-removed` | `ui/ChartConfig:aria`,
`ui/ReportChart:aria` | MISSING | `chart-config-aria-retired` (new) |
| 32 | `dashboard-widget-chart-config-structure-removed` |
`ui/DashboardWidgetChartConfig:series`,
`ui/DashboardWidgetChartConfig:type`,
`ui/DashboardWidgetChartConfig:xAxis`,
`ui/DashboardWidgetChartConfig:yAxis` |
`dashboard-widget-chart-config-structure-refused` | unchanged |
| 33 | `translation-per-app-settings-removed` | none (value or
strict-key retirement, not in the two tables) |
`translation-per-app-settings-platform-only` | unchanged |
| 34 | `object-tenancy-organization-field-removed` |
`data/TenancyConfig:organizationField` | MISSING |
`object-tenancy-organization-field-retired` (new) |
| 35 | `page-component-filter-record-to-rule-array` | none (value or
strict-key retirement, not in the two tables) |
`element-data-source-and-object-block-filter-rule-array`,
`object-grid-default-filters-rule-array` | unchanged |
| 36 | `view-item-owner-hidden-removed` | `ui/ViewItemWire:hidden`,
`ui/ViewItemWire:owner`, `ui/ViewItem:hidden`, `ui/ViewItem:owner` |
MISSING | `view-item-owner-hidden-retired` (new) |

### Table 2 — D2-less records, grouped by the existing D3 entry that
names them

| D3 entry (existing) | records it names |
|---|---|
| `advanced-plugin-lifecycle-config-retired` |
`kernel/AdvancedPluginLifecycleConfig`, `kernel/GracefulDegradation`,
`kernel/PluginUpdateStrategy` |
| `ai-conversation-analytics-duration-unit-in-key` |
`ai/ConversationAnalytics:duration` |
| `api-error-retry-after-unit-in-key` |
`api/EnhancedApiError:retryAfter` |
| `api-runtime-config-durations-unit-in-key` |
`api/DataLoaderConfig:cacheTtl`, `api/RouteDefinition:timeout` |
| `automation-flow-list-route-retired` | `api/FlowSummary`,
`api/ListFlowsRequest`, `api/ListFlowsResponse` |
| `automation-runs-cursor-retired` | `api/ListRunsRequest:cursor` |
| `branded-identifier-schemas-retired` | `shared/AppName`,
`shared/FieldName`, `shared/FlowName`, `shared/ObjectName`,
`shared/RoleName`, `shared/ViewName` |
| `change-management-duration-keys-retired` |
`system/ChangeImpact:downtime.durationMinutes`,
`system/ChangeRequest:implementation.steps.estimatedMinutes`,
`system/RollbackPlan:steps.estimatedMinutes` |
| `change-management-family-retired` | `system/ChangeImpact`,
`system/ChangePriority`, `system/ChangeRequest`, `system/ChangeStatus`,
`system/ChangeType`, `system/RollbackPlan` |
| `cli-command-contribution-retired` | `kernel/CLICommandContribution` |
| `cloud-subpath-retired` | 62 records, all `cloud/` defs |
| `data-file-value-duration-unit-in-key` | `data/FileValue:duration` |
| `data-nosql-query-options-timeout-unit-in-key` |
`data/NoSQLQueryOptions:timeout` |
| `device-request-response-interval-unit-in-key` |
`api/DeviceRequestResponse:interval` |
| `driver-options-timeout-to-timeout-ms` | `data/DriverOptions:timeout`
|
| `epoch-instant-keys-renamed` | `api/SimplePresenceState:lastSeen`,
`api/WebSocketEvent:timestamp`, `kernel/HealthStatus:timestamp`,
`kernel/KernelContext:startTime`,
`kernel/TenantRuntimeContext:startTime` |
| `esignature-config-deadline-keys-retired` |
`data/ESignatureConfig:expirationDays`,
`data/ESignatureConfig:reminderDays` |
| `event-name-schema-retired` | `shared/EventName` |
| `export-job-family-retired` | 13 records, all `api/`, `automation/`
defs |
| `hot-reload-inert-state-strategies-retired` |
`kernel/DistributedStateConfig` |
| `hot-reload-watch-placeholder-retired` |
`kernel/HotReloadConfig:watchPatterns` |
| `identity-api-key-schema-retired` | `identity/ApiKey` |
| `incident-response-deadline-keys-retired` |
`system/IncidentNotificationMatrix:escalationTimeoutMinutes`,
`system/IncidentNotificationRule:regulatorDeadlineHours`,
`system/IncidentNotificationRule:withinMinutes`,
`system/IncidentResponsePhase:targetHours`,
`system/IncidentResponsePolicy:retentionDays`,
`system/IncidentResponsePolicy:triageDeadlineHours` |
| `incident-response-family-retired` | `system/Incident`,
`system/IncidentCategory`, `system/IncidentNotificationMatrix`,
`system/IncidentNotificationRule`, `system/IncidentResponsePhase`,
`system/IncidentResponsePolicy`, `system/IncidentSeverity`,
`system/IncidentStatus` |
| `kernel-compatibility-matrix-estimated-migration-time-unit-in-key` |
`kernel/CompatibilityMatrixEntry:estimatedMigrationTime` |
| `kernel-context-preview-mode-retired` |
`kernel/KernelContext:previewMode`, `kernel/PreviewModeConfig`,
`kernel/TenantRuntimeContext:previewMode` |
| `kernel-event-bus-retention-unit-in-key` |
`kernel/EventPersistence:retention`,
`kernel/EventSourcingConfig:retention` |
| `kernel-health-check-and-hot-reload-durations-unit-in-key` |
`kernel/HotReloadConfig:debounceDelay`,
`kernel/PluginHealthCheck:interval`, `kernel/PluginHealthCheck:timeout`
|
| `kernel-package-lifecycle-durations-unit-in-key` |
`kernel/MultiVersionSupport:rollout.duration`,
`kernel/PackageDependencyResolutionResult:resolvedIn`,
`kernel/UpgradePlan:estimatedDuration` |
| `kernel-plugin-health-report-durations-unit-in-key` |
`kernel/PluginHealthReport:metrics.responseTime`,
`kernel/PluginHealthReport:metrics.uptime` |
| `kernel-plugin-security-durations-unit-in-key` |
`kernel/KernelSecurityPolicy:auditLog.retention`,
`kernel/KernelSecurityPolicy:authentication.tokenExpiration`,
`kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` |
| `kernel-runtime-config-timeout-unit-in-key` |
`kernel/RuntimeConfig:resourceLimits.timeout`,
`kernel/SandboxConfig:process.timeout` |
| `kernel-startup-orchestrator-durations-unit-in-key` |
`kernel/PluginStartupResult:duration`, `kernel/StartupOptions:timeout`,
`kernel/StartupOrchestrationResult:totalDuration` |
| `list-view-navigation-view-retired` | `ui/NavigationConfig:view` |
| `logging-durations-unit-in-key` |
`system/HttpDestinationConfig:batch.flushInterval`,
`system/HttpDestinationConfig:retry.initialDelay`,
`system/HttpDestinationConfig:timeout`,
`system/LoggingConfig:buffer.flushInterval` |
| `metadata-changed-event-payload-retired` |
`kernel/MetadataChangeOperation`, `kernel/MetadataChangedEventPayload` |
| `metadata-customization-protocol-retired` | 13 records, all `api/`,
`kernel/` defs |
| `metadata-manager-config-cache-ttl-unit-in-key` |
`kernel/MetadataManagerConfig:cache.ttl` |
| `metadata-manager-config-inert-cache-keys-retired` |
`kernel/MetadataManagerConfig:cache.enabled`,
`kernel/MetadataManagerConfig:cache.maxSize`,
`kernel/MetadataManagerConfig:cache.ttlSeconds` |
| `metadata-plugin-additional-types-retired` |
`kernel/MetadataPluginConfig:additionalTypes` |
| `package-rollback-response-retired` | `api/PackageRollbackResponse` |
| `packages-list-pagination-retired` |
`api/ListInstalledPackagesRequest:cursor`,
`api/ListInstalledPackagesRequest:limit` |
| `plugin-auto-restart-never-reinitialised` |
`kernel/PluginHealthCheck:autoRestart`,
`kernel/PluginHealthCheck:maxRestartAttempts`,
`kernel/PluginHealthCheck:restartBackoff` |
| `plugin-manifest-contributes-dead-members-retired` |
`kernel/Manifest:contributes.actions`,
`kernel/Manifest:contributes.commands`,
`kernel/Manifest:contributes.drivers`,
`kernel/Manifest:contributes.events`,
`kernel/Manifest:contributes.fieldTypes`,
`kernel/Manifest:contributes.functions`,
`kernel/Manifest:contributes.menus`,
`kernel/Manifest:contributes.themes`,
`kernel/Manifest:contributes.translations` |
| `plugin-manifest-contributes-routes-retired` |
`kernel/Manifest:contributes.routes` |
| `plugin-manifest-dead-containers-retired` |
`kernel/Manifest:capabilities`, `kernel/Manifest:configuration`,
`kernel/Manifest:extensions` |
| `plugin-manifest-kind-globs-retired` |
`kernel/Manifest:contributes.kinds.globs` |
| `plugin-security-scan-result-surface-retired` |
`kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`,
`kernel/PluginQualityMetrics:securityScan`,
`kernel/PluginSecurityManifest:scanResults`,
`kernel/PluginSecurityManifest:vulnerabilities` |
| `rest-api-endpoint-handler-status-retired` | `api/HandlerStatus`,
`api/RestApiEndpoint:handlerStatus`, `api/RouteCoverageEntry`,
`api/RouteCoverageReport` |
| `rest-api-plugin-durations-unit-in-key` |
`api/RestApiEndpoint:cacheTtl`, `api/RestApiEndpoint:timeout`,
`api/RestApiPluginConfig:performance.defaultCacheTtl` |
| `rest-server-config-dead-keys-retired` | 11 records, all `api/` defs |
| `session-user-language-retired` | `api/SessionUser:language` |
| `stack-themes-carrier-retired` | `ui/BorderRadius`, `ui/ColorPalette`,
`ui/Shadow`, `ui/Theme`, `ui/ThemeMode`, `ui/Typography` |
| `startup-orchestrator-retired` | `kernel/HealthStatus`,
`kernel/PluginStartupResult:health`,
`kernel/PluginStartupResult:plugin`,
`kernel/PluginStartupResult:startTime`, `kernel/StartupOptions`,
`kernel/StartupOrchestrationResult` |
| `system-cache-durations-unit-in-key` |
`system/CacheAvalanchePrevention:circuitBreaker.resetTimeout`,
`system/CacheTier:ttl` |
| `system-collaboration-durations-unit-in-key` |
`system/CollaborationSessionConfig:idleTimeout`,
`system/CollaborationSessionConfig:snapshot.interval` |
| `system-failover-health-check-interval-unit-in-key` |
`system/FailoverConfig:healthCheckInterval` |
| `system-metrics-jsdoc-durations-unit-in-key` |
`system/MetricDefinition:summary.maxAge`,
`system/MetricExportConfig:interval`,
`system/MetricsConfig:collectionInterval`,
`system/MetricsConfig:retention.period`,
`system/ServiceLevelObjective:errorBudget.burnRateWindows.window` |
| `system-metrics-window-durations-unit-in-key` |
`system/MetricAggregationConfig:window.size`,
`system/ServiceLevelIndicator:window.size`,
`system/ServiceLevelObjective:period.duration` |
| `system-object-storage-durations-unit-in-key` |
`system/AccessControlConfig:maxAge`, `system/StorageConnection:timeout`
|
| `system-registry-config-durations-unit-in-key` |
`system/RegistryConfig:cache.ttl`,
`system/RegistryUpstream:syncInterval`,
`system/RegistryUpstream:timeout` |
| `system-tracing-otel-exporter-durations-unit-in-key` |
`system/OpenTelemetryCompatibility:exporter.batch.exportTimeout`,
`system/OpenTelemetryCompatibility:exporter.batch.scheduledDelay`,
`system/OpenTelemetryCompatibility:exporter.timeout`,
`system/TracingConfig:performance.exportInterval` |
| `system-tracing-span-duration-unit-in-key` | `system/Span:duration` |
| `system-worker-queue-rate-limit-duration-unit-in-key` |
`system/QueueConfig:rateLimit.duration` |
| `tenant-schema-cache-ttl-unit-in-key` |
`system/SchemaLevelIsolationStrategy:performance.schemaCacheTTL` |
| `training-deadline-keys-retired` |
`system/TrainingCourse:durationMinutes`,
`system/TrainingCourse:validityDays`,
`system/TrainingPlan:gracePeriodDays`,
`system/TrainingPlan:recertificationIntervalDays`,
`system/TrainingPlan:reminderDaysBefore` |
| `training-family-retired` | `system/TrainingCategory`,
`system/TrainingCompletionStatus`, `system/TrainingCourse`,
`system/TrainingPlan`, `system/TrainingRecord` |
| `websocket-durations-unit-in-key` |
`api/WebSocketConfig:pingInterval`,
`api/WebSocketConfig:reconnectInterval`, `api/WebSocketConfig:timeout`,
`api/WebSocketServerConfig:heartbeatInterval` |

## Prose corrected (the single-entry sites of `5854110917` /
`5854456343`, and the rationale sentences)

| site (at this head) | was | now |
|---|---|---|
| `packages/spec/src/migrations/registry.ts:78–86` (step 17 docblock,
fact correction only) | 「Mechanical, and mechanical only … there is no
semantic residue and the `semantic` list is deliberately empty」 | the
three renames replay losslessly as D2; they carry no D3 entry because
step 17 shipped before the rule and was not back-filled; the `semantic`
list is NOT empty. ⛔ No step-17 entry added. |
| `packages/spec/src/migrations/registry.ts:5256` (step 18 rationale,
`tenancy.organizationField`) | 「The conversion is a lossless delete and
there is no semantic residue」 | a lossless delete still leaves the
author a judgment, carried by
`object-tenancy-organization-field-retired` |
| `packages/spec/src/conversions/registry.ts:3460`
(`datasource-driver-mongo-to-mongodb`, protocol 17) | 「Why D2 and not
D3」 | 「Why the data repair is D2」, plus: losslessness does not decide
whether a family owes D3; this one is protocol 17 and has none |
| `packages/spec/src/conversions/registry.ts:9312`
(`api-endpoint-cache-ttl-to-cache-ttl-seconds`) | 「gets a conversion
rather than a semantic entry」 | 「also gets a conversion」, and names its
D3 entry |
| `packages/spec/src/conversions/registry.ts:9804`
(`list-view-sort-string-clause-to-array`) | 「which is why this is a D2
conversion rather than a semantic TODO」 | the data repair is D2; the
family's D3 entry carries the clauses the rewrite leaves alone |
|
`packages/spec/src/migrations/entries/retired-keys/18.api__ApiEndpoint__cacheTtl.ts:11–19`
| 「a D2 CONVERSION rather than a semantic entry」 | also a D2 conversion,
and names the D3 entry |
|
`packages/spec/src/migrations/entries/semantic/18.metadata-endpoints-switch-radius-repartitioned.ts:11–13`
| 「exactly the residue D2 cannot express, which is why this is a
semantic entry」 | that residue is why there is no D2 at all; the D3
entry is owed either way |
| `packages/spec/scripts/build-migration-registry.ts:276` | 「a major
whose semantic residue is genuinely nil (protocol 14)」 | an empty region
is a real state (a freshly opened step, or protocol 14's, which predated
the rule) |

⛔ Not touched: the governed texts (ADR-0087,
`.claude/skills/spec-property-retirement/SKILL.md` §3), which are
objectstack-ai#20188's.

## The census pin

**Where:** `packages/spec/src/migrations/migrations.test.ts` › `registry
integrity`: `from protocol 18 on, every graduated D2 conversion is named
by a D3 entry of its own step (ruling B)`, plus a control test. It reads
the major's `entries/semantic/` files (comment and literal) inside its
own package; `check:migration-registry` already proves those files and
the generated region are one set.

**What it asserts:** for every step whose `toMajor` is 18 or later,
every id in `conversionIds` appears as a whole id in at least one
`semantic/` entry of that major. A new major-18 (or later) retirement
that lands a D2 conversion with no D3 entry naming it goes red, naming
the conversion.

**What it cannot see**, stated so a green run is not over-read: (1)
whether the naming entry is that family's OWN (a passing mention
satisfies it; the census judged ownership by reading); (2) a family
retired with no conversion at all (no machine-readable link joins a
retired key or def to its D3 entry; the census paired those by reading,
and found none missing). Protocol 17 is outside the pin by design:
measured with the same logic, 53 of its 57 graduated conversions are
named by no step-17 entry, and step-17 backfill is out of scope (triage
`5854164872`).

**Reverse verification (one-shot, no permanent test file).** At
`c8656ad35`, with the entries committed: deleted
`18.object-tenancy-organization-field-retired.ts` (absence confirmed on
disk before the run), ran the pin: `× from protocol 18 on …` with `+
"protocol 18: object-tenancy-organization-field-removed"`, `Tests 1
failed | 140 skipped`. Restored with `git checkout HEAD -- PATH` (that
path) inside a `trap … EXIT INT TERM`: blob `2cf3007d9fff` equals
HEAD's, `git diff HEAD` empty. Direction observed: red, the expected
one. No build involved: the test imports `src/` and reads the entry
files directly.

## Patch round 1 (contract review `5857834457`: FAIL at `3197fce29`)

**Blocking: fixed.** `Lint & Repo Gates` step 189
(`check-issue-citations.mjs`, judging pass) was red. Four bare citations
this PR added answer 404 on the board: objectstack-ai#10329, objectstack-ai#10926, objectstack-ai#12868 and
objectstack-ai#14676. Each was in an entry's leading comment, and again in the
regenerated region. `--probe-cause` classes all four as **deleted** (the
web endpoint also answers 404, so none was transferred), so none of them
is a reference to another repository to qualify. Each comment now
anchors to the commit in this repository's history that retired the
family, and says in words what that commit decided. That is the
precedent of commit `66e266c93` (ruling C+D on objectstack-ai#19123). Every sha is an
ancestor of `origin/main`:

| entry | was | now anchored to |
|---|---|---|
| `mapping-lookup-params-retired` | objectstack-ai#10329 | commit `15d58dbf1` (the
import path never read the four lookup steering params) |
| `translation-component-submit-label-retired` | objectstack-ai#10926 | commit
`d173125fb` (the copy key left with its only declarer, `element:form`) |
| `form-view-option-default-retired` | objectstack-ai#12868 | commit `c459da6bc` (the
ruled narrowing: the form-view face drops per-option `default`, the
object-field face keeps it enforced) |
| `connector-error-mapping-retired` | objectstack-ai#14676 | commit `13c48c2a5`
(eleven inert keys, one spelled like the live `userMessage` channel) |

Only the comments changed; no string an author is shown moves. The
region was regenerated with `gen:migration-registry`. The round-1
report's `pnpm check:issue-citations :: exit 0` was the package script,
which runs only the `--self-test`. The judging pass CI runs was exit 2
at `3197fce29` (8 findings = 4 numbers × 2 sites) and is exit 0 now
(below).

**Pin message.** The census pin's assertion now names the unnamed
`protocol N: conversion-id` pairs and the remedy: add a D3 `semantic`
entry of that step whose text names the conversion id as a whole word.
Its logic and scope are unchanged. Shown firing at `21418c4d2` with one
entry removed (trap-guarded restore, blob equal to HEAD's, `git diff
HEAD` empty): `AssertionError: graduated D2 conversion(s) named by no D3
entry of their own step: protocol 18:
object-tenancy-organization-field-removed. Remedy: add a D3 semantic
entry of that step …`, `Tests 1 failed | 140 passed`.

**Body.** Table 1: `integration/Connector:connectionTimeoutMs` moved
from row 16 to row 20. Its own comment names
`connector-connection-timeout-ms-removed`; the permission id appears
there only as a comparison. The code was already right.

## Sibling PRs

- **PR objectstack-ai#20238** (objectstack-ai#20161) has since LANDED as `6a6a17b62`, with the D2
conversion `report-joined-chart-removed` and
`18.ui-report-joined-chart-retired.ts`, which names that id. The union
of this head with `main` at `6a6a17b62` is clean and passes the pin (37
pairs, 0 unnamed; delta review `5859315908`).
- `main` was merged three times with `os-regen-merge.sh`, and never by
hand in a generated region: at `a70cd62e5` (objectstack-ai#20223 and objectstack-ai#20245), at
`21418c4d2` (`cel-predicate-one-value-comparand-refused` and
`filter-query-face-comparands-refused-at-save`) and at `a930cacea`
(step-17 rationale prose from objectstack-ai#20268). Each is D3-only or prose, with no
new step-18 conversion. Regeneration produced a commit only after the
first merge (`3197fce29`) and changed nothing after the other two. Every
sibling entry id was verified present.

## Verification (head `a930cacea`)

- `pnpm check:issue-citations && node
scripts/check-issue-citations.mjs`, exactly as CI runs it (base
`origin/main`): **exit 0**, 112 citations across 29 files: 106 resolve,
6 cross-repo unjudged, 0 findings. At `3197fce29` the same command
exited 2.
- `pnpm --filter @objectstack/spec build` under the verify lock: ok.
`check:generated`: all 15 generated artifacts up to date.
`check:migration-registry`: current (292 semantic, 214 retired-key, 199
retired-def). `spec-changes.json` and `docs/protocol-upgrade-guide.md`
do not move: they project up to the current protocol major, and step 18
is beyond it.
- `pnpm --filter @objectstack/spec exec vitest run --project local`:
**552 files, 16257 passed, 1 todo**. The `src/migrations/` directory
alone: 3 files, 151 passed.
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, test layer)
at `21418c4d2`, the head before the last merge, which brought only
another PR's prose into this diff's files: exit 0, test-typecheck debt
unchanged (53 files / 255 errors / 142 signatures).
- `node scripts/pm/dispatch-gates.mjs --commands` (no paths) at
`a930cacea`, every command run and its exit recorded, reconciled with
`--ran`: **89 derived, 87 run (all exit 0), 2 NOT MEASURED**.
`check:dual-build-cjs-loads` and `check:type-check-debt` exited 3
(PREREQUISITE NOT MET: the full 86-package workspace build does not fit
the foreground cap on this shared box). CI runs both.
`check:pm-dispatch-gates` finished this time: exit 0, in 907.6 s.
- ESLint, narrowed and proven: all 31 changed `.ts` files,
`--no-inline-config --format json`: 0 errors, 0 warnings, none reported
ignored. `eslint.config.mjs` enables no type-aware linting (its own
statement at `eslint.config.mjs:326–328`), so this diff cannot move any
untouched file's verdict.
- Changeset: `@objectstack/spec` `patch`. The published registry text
changes; no accept set moves.

## Acceptance notes (observed, not filed)

- `registry.ts:108` (released step-17 text) still says the sharing-rule
`full` conversion 「leaves no semantic residue」: triage scoped step-17
backfill out, so it is left as is.
- New entries keep tracker numbers in their `//` comments only, never in
the strings an author is shown (AGENTS.md runtime-strings rule). Several
older entries do cite numbers in `reason`; not touched.
- `dashboard-refresh-interval-unit-in-key` states the console renderer's
release lag as a verification step, not as a present fact: this
container has no objectui checkout at the pin to measure it.
- `main` moved after the last merge (`a930cacea`). objectstack-ai#20285 (`2aa25efb4`,
prose in five semantic entries) and objectstack-ai#20238 (`6a6a17b62`, a new step-18
conversion with its entry) landed under `migrations/` and
`conversions/`. The delta review merged this head onto `6a6a17b62`:
clean, with all six regions still mirrored. The queue verifies the
merged generation. (Corrected by the seat at 2026-09-27T19:57Z; the
earlier wording said nothing under those paths had moved.)

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

2 participants