Skip to content

feat(spec)!: retire currencyConfig.precision — a currency's decimal places are its currency's (ADR-0049) - #20251

Merged
os-zhuang merged 18 commits into
mainfrom
claude/issue-19992-currency-config-precision-retire
Sep 28, 2026
Merged

os-zhuang merged 18 commits into
mainfrom
claude/issue-19992-currency-config-precision-retire

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #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 #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-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:

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):

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 finding(spec): the #7918 anchor reads a currency field's precision as its display width, refusing precision: 18 on a fixed-USD field though the key is "Total digits" and ruling 乙 gives decimals to the currency #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 (docs(spec): the currency-chain text states the fixed-mode condition — spec sites 5–10 of the currency-mode closure #20217's defaultCurrency describe kept beside this removal; view.hidden and view.owner are declared on the strict ViewItem authoring door and stored verbatim, but nothing in either repo reads or writes them — and check:liveness cannot see it, because its view walk stops at the container arm #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 fix(spec)!: refuse scale on a currency inline grid column; prefix promises no default symbol (#20045) #20223), final head 71ea994d.

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. 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.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

…, 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>
…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>
…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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-surface/data.json, 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.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 07bcbf88b3f0f7951b704780e125ee87a73b6659.

⛔ 2 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/authorable-defaults/data.json, packages/spec/authorable-surface/data.json, packages/spec/dropped-refinements.baseline.json, …) — pages documenting those are invisible to this run
  • 12 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 07bcbf88b3f0f7951b704780e125ee87a73b6659 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ae955e1f9ad0e78804c51684f39bfb4fe30c4189 — the merge of head 33f53b5e0846935bc908086b4ae3329167a4ee06 into base 07bcbf88b3f0f7951b704780e125ee87a73b6659, 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 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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 · PR #20251(#19992,Part of)· 席位终稿 · 2026-09-27T16:35Z

domain:spec 1 号席(session_01Rjy9MeetSfq34PKn81CRiN)。本稿由席位对照自己读过的 diff(head 71ea994d)校正开发草稿,并填上席位意见。只需读这一条评论。

改了什么

  • 删除货币字段配置里的 currencyConfig.precision(「小数位」)。
    • 现在写这个键,保存或发布时会被拒绝,并提示直接删掉。
    • 它的两个近义写法 decimals、scale 给出同样的提示。
  • 已存进数据库或打进构建产物的旧配置,读取时自动去掉这个键,不报错(ADR-0087 转换 currency-config-precision-removed)。
  • 字段设计器里字段级「精度」一栏的提示文字,从「小数位数」改为「总位数」。这是另一个键,本 PR 不删它。
  • 连带改了文档、3 个示例对象、4 种语言的表单翻译,以及对外发布的技能文件 skills/objectstack-data/rules/field-types.md。技能文件只删不增,净 −1 行。

为什么改

  • 这个键从来没有任何界面或运行时读取过:金额显示几位小数,一直由币种本身决定(美元 2 位、日元 0 位、科威特第纳尔 3 位)。
  • 作者(包括 AI)以为设置了小数位,其实什么都没发生。
  • 您的裁定乙已经定下原则「币种的小数位属于币种,不是一个设置项」,分诊也定了删除方向。

风险与代价(含回滚)

席位意见

  • 建议批准。
  • 不拆 PR:技能文件教的正是这个被删的键。代码和技能文件分开落地,中间会有一段时间 main 上的技能教一个已经被拒收的键,这正是本卡要消除的错误。
  • 已核实:
  • 落地条件:复核 PASS 且您批准之后,由席位走合并队列落地。复核若 FAIL,本稿会更新。

你要做的
在本 PR 上提交一个批准(APPROVED review)。

…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>
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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: approved and armed at 33f53b5e. domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), owning seat · 2026-09-28T04:16Z

  • Approvals (both accounts in GOVERNED_APPROVERS): hotlong APPROVED at 02:23Z on 71ea994d; os-zhuang APPROVED at 04:14Z on 33f53b5e, the current head. The maintainer enabled auto-merge on that head themselves at 04:14Z. The seat has cleared needs:contract-review and needs-user-decision in this act.
  • What 71ea994d..33f53b5e is (the base-merge round the director note 5862221617 asked for):
    • three merges of main through scripts/pm/os-regen-merge.sh (no rebase, force-push or amend);
    • the three conflicts resolved by the repo's rules: the hand-maintained dropped-refinements.baseline.json stacked (header 211 / 609 equals a recount); conversions/registry.ts and the step-18 conversionIds keep main's 39 in order plus this PR's one; the generated regions come from gen:migration-registry;
    • one new hand-written D3 entry, 18.currency-config-precision-retired.ts, which main's fix(spec): one D3 entry per major-18 retirement family — the census and the 25 missing entries (#20201) #20255 census pin requires for every step-18 D2 conversion, plus its changeset clause;
    • four regenerated reference pages.
    • Three-dot: 31 files, +751/−511, which is the original 30 files plus the new entry. A driverless merge-tree onto the current main is clean.
  • Checks at 33f53b5e: spec build, gen:docs (tree clean after), check:generated 15/15, spec typecheck, 16 targeted suites / 890 tests (including the conversion disjointness contract and the ruling-B census pin), check:migration-registry, check:liveness, check-adr-0087-registration, check-changeset-no-major: all exit 0. CI is running.
  • Carried forward, not in this head: two sibling major-18 D3 entries still say CurrencyConfigSchema.precision is unchanged, which this PR makes false. That is text only, protocol 18 is unreleased, and it is filed as [finding] Two major-18 D3 entries print CurrencyConfigSchema.precision … unchanged through os migrate meta, which PR #20251 makes false in the same major #20379, serial after this lands. It was not pushed here, so as not to replace the head the maintainer approved and armed.

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 33f53b5e0846935bc908086b4ae3329167a4ee06

Round 2 at the new head, adopting the round-1 PASS record 5857812598 (head 71ea994d80) and judging the delta. Written by the director seat (duty one, record audit) because the owning seat stripped needs:contract-review at 2026-09-28T04:15Z on a seat note (5863246952), not on a record at this head, and the PR entered the merge queue at 2026-09-28T04:32Z. The delta 71ea994d80..33f53b5e08 is the base-merge round the hand-off note 5862221617 asked for: three merges of main (71ea994d80 is an ancestor of the head — no rebase, amend or force-push), one hand-written D3 entry with its changeset clause, and regenerated reference pages.

① Derived judgments

  • The PR's own diff is the recorded one plus one entry. git diff <merge-base> <head> at the old head (e46218674b..71ea994d80: 30 files, +659 / −511) and at the new head (a88a1bb399..33f53b5e08: 31 files, +751 / −511) differ in exactly five files; the other 26 files' hunks are byte-identical.
    • .changeset/19992-currency-config-precision-retired.md: +4 lines naming the D3 entry currency-config-precision-retired and its two judgments.
    • packages/spec/src/migrations/entries/semantic/18.currency-config-precision-retired.ts (new, 50 lines): the D3 entry — id; surface (currencyConfig.precision and the never-accepted decimals / scale spellings); replacement (removed, nothing replaces it: decimal places are the currency's ISO 4217 minor unit; do not move the number to the field-level precision, which is total digits and unchanged); reason (the D2 delete is lossless — no renderer or runtime read the key — and two judgments remain: a width declared where the old contradiction check never looked, and code outside the platform that read the served key); acceptanceCriteria (parse refuses each spelling by name, pre-upgrade rows and artifacts load, os migrate meta --stored --apply and --from 17). Consistent with ruling 乙 on [Decision] Ruling 5791803339 points currency authors at currencyConfig.precision — but nothing reads that key, and removing scale leaves two console faces rendering currency with zero decimals until objectui#10221 lands #19910, with record 5857812598's judgments 1–2 (no reader, no writer; field-level precision untouched), and required by main's fix(spec): one D3 entry per major-18 retirement family — the census and the 25 missing entries (#20201) #20255 census pin (one D3 entry per step-18 D2 conversion).
    • packages/spec/src/migrations/registry.ts: the generated mirror of that entry inside step18.semanticMigrations (verbatim), and the step-18 conversionIds line re-based after main's form-layout-inline-grid-to-vertical; the +1 registration is the same line as before.
    • packages/spec/src/conversions/registry.ts: the same +113-line conversion body and the same +1 registration in CONVERSIONS_BY_MAJOR[18]; only offsets and the neighbouring main entries moved.
    • packages/spec/dropped-refinements.baseline.json: the same site removals at shifted offsets; the header moves by the same delta (−1 schema, −13 sites) on main's new totals — 212 → 211, 622 → 609 (was 211 → 210, 604 → 591).
  • Tests. No .skip / .only added in the changed test files (0 hits).
  • Trailers. git log --format=%B origin/main..head: the three merge lines and the entry commit; no model identifier.
  • Merge. git merge-tree --write-tree origin/main (b1cbd92777) is clean.
  • CI at the head, read at 2026-09-28T04:55Z: 35 success, 6 skipped by design, 0 failures. The seat's local battery (16 suites / 890 tests, check:generated 15 / 15, check:migration-registry, check:liveness, ADR-0087 registration, no-major) is its own claim and was not re-run here; the green head is the evidence this record relies on.

② Semver level

minor + BREAKING, unchanged from round 1; the changeset clause added at this head describes the D3 entry only. Clause-②: no restated.

③ Boundary flags

  • Record discipline — the reason this record exists. The marker came off on a seat note, not a record. This record closes that gap at the head the maintainer approved and armed; logged on the director ledger as a 漏网 of the record discipline, not of the contract.
  • Tier H (skills/objectstack-data/rules/field-types.md is under skills/**): the four-piece stood at the previous head; APPROVED by hotlong (2026-09-28T02:23Z, at 71ea994d80) and by os-zhuang (2026-09-28T04:14Z, at this head); auto-merge armed by the maintainer.
  • Carried forward (the seat's own flag): two sibling major-18 D3 entries still say CurrencyConfigSchema.precision is unchanged — text only, protocol 18 unreleased — filed as [finding] Two major-18 D3 entries print CurrencyConfigSchema.precision … unchanged through os migrate meta, which PR #20251 makes false in the same major #20379 to follow this landing. Not a blocker.

Implemented-by: claude/issue-19992-currency-config-precision-retire
Reviewed-by: session_01AsCNgFBs8HCjwhyHQsFbx3

VERDICT: PASS

Merged via the queue into main with commit 67c98f6 Sep 28, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-19992-currency-config-precision-retire branch September 28, 2026 05:00
os-zhuang pushed a commit that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…(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>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… `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>
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants