Skip to content

fix(spec): ADR-0087 别名转换按值处置被遮蔽的旧拼法(等值删除、异值双留) (#4923) - #5731

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4923-grid-alias-ruling
Aug 6, 2026
Merged

fix(spec): ADR-0087 别名转换按值处置被遮蔽的旧拼法(等值删除、异值双留) (#4923)#5731
os-zhuang merged 2 commits into
mainfrom
claude/issue-4923-grid-alias-ruling

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4923

按 issue 线程中已记录的**维护者裁决(2026-08-04)**执行,纯执行、无自由裁量。

前提复核(对 origin/main)

裁决前提仍然成立,但 issue 正文的定位有一处过期:

另有一处仓库内的主动指认:retry-policy-converged 的注释原文写着「#4923 is queued to revisit that shadowing rule — this entry deliberately relies on the shared helper's semantics rather than open-coding its own, so it moves with that ruling」。这句话直接回答了「裁决该落在哪个 helper 上」——落在共享 helper,而不是只落在 renameConfigKey。因此实现落在 renameKey,由 renameConfigKey 委托,renameKey 的其余调用方(datasource driver-config 别名、page:header descriptionretryDelayMs)随之统一,不留第二套方言。

改了什么

按值拆分(walk.ts):renameKey 遇到两个拼法同时存在时——

  • 值结构相等 → 删掉旧拼法 + 照常发 notice。旧拼法不携带规范键没有的信息,属无损卫生,在 D2 契约内;顺带让转换在形状和 notice 上都幂等(重放命中「from 不存在」分支)。
  • 值不同 → 返回 null,双键都保留,不发 notice。作者给一个槽写了两个不同的值,这是真歧义,升级工具替客户择一等于改了客户没同意的配置。活下来的这一对,正是 strict 门能够点名双键拒绝的依据。

相等性用结构比较而非 ===:两处分别写出的 { status: 'stale' } 是同一份声明。这个谓词正是 #5005 的 compose 已经在用的那个(「same value composes fine」),抽到 shared/deep-equal.ts 共用,免得两个面对「这两个值算不算同一个」给出不同答案(未加入 shared/index,不动公共 API 面)。抽取时补了递归深度上限:两个调用点在触顶时都退化为「不相等」,即 compose 报冲突、转换保留双键——都是朝响的方向降级,不会静默丢作者的值。

liftNotifySourceShape 同理(issue 点名):嵌套部件与扁平键重复则算已交代、source 照常删除;不一致则整个节点原样不动(连无歧义的那半也不抬),让 source 完整抵达 strict 契约。wait 节点的松散键 lift 刻意不纳入,并在注释里写明理由:它是跨位置搬迁、且一个 target 对多个候选名,与「一个槽的两种拼法」不是同一个问题,扩张需要另一次裁决。

fixture 按新语义修正:原来 5 条 fixture 的 after 半边都靠「值不同」才恰好维持原状,但它们的注释写的是「canonical 已存在 → 别名原样留下」——那是旧规则的措辞,且只覆盖了两分支中的一支。逐条改为:歧义节点改用真正不同的值(object: 'ticket' 而非 'ignored',后者读起来像「这个值被忽略」,在新规则下恰是误导),并各补一个等值节点演示删除。notify 的 n3 由此从「抬一半、丢 source」变成「原样不动」,n4 新增演示等值冗余,expectedNotices 7 到 8。

批 9 guidance 同步措辞:活下来的孪生键不再是「dead」——转换现在会删掉等值的那种,所以能抵达该 parse 的键必然携带规范键没有的值。每条处方改为点名双键 + 要求作出决定(builtin-node-config.zod.ts CRUD 与 mapio-node-config.zod.ts notify 五条、schemaless-node-config.zod.ts script 与 subflow)。

Rider(#5511):protocol-15 page-component-visibility-to-visibleWhen 的就地走查换成 #5509mapPageComponents,25 行手写遍历变 2 行。

验证

先证红(方向在跑之前就写死在测试注释里)。renameKey 的规则临时还原成旧语义(保留 import,确保红是行为红不是编译红)后跑新增的 7 条:

× deletes an alias whose value EQUALS the canonical key, and emits a notice
× compares STRUCTURALLY — two separately-authored identical filters are one declaration
× is idempotent in shape AND notices — the deduped result replays to itself
✓ keeps BOTH spellings when the values differ — the upgrade tool does not choose
× judges each pair on its own value — one twin dedupes while its neighbour survives
× applies the same rule one level up, on a plain dict rename (page header)
× the surviving pair is refused by the strict gate with BOTH keys named
Tests  6 failed | 1 passed

这里要如实说明:两个方向的证红结构并不对称,报告模板预设的「两边都 before-red」在本单不成立。

  • 等值 → 删除 + notice:真的 before-red / after-green,如上 6 条。
  • 异值 → 双保留:那唯一一条 就是它。旧代码对所有成对情形都保留双键,所以「双保留」这半改前改后都是绿的——它是回归钉,不是变更证明。这一方向真正变的是理由读这条理由的处方,所以证红落在 strict 门那条上(not.toMatch(/already won and this key is dead/)),旧 guidance 的原文正是 so a surviving object means objectName already won and this key is dead,红得干净。

Rider 等价性:按 #5511 的验收口径,由既有 fixture 判定。pageComponentVisibilityToVisibleWhen 的 fixture 一个字节未动(diff 里 apply 之后直接进 fixture:),在全量跑中保持绿。路径构造也逐字相同:mapPageComponents 产出 pages[i].regions[j].components[k],与原手写的 path + .regions[ri].components[ci](path = pages[pi])一致;非数组 regions/components、非 dict region/component 的短路分支也一一对应。无 fixture 需要修改,故不触发 stop-and-report。

绿跑

pnpm --filter @objectstack/spec test
  Test Files  317 passed (317)
  Tests  8090 passed (8090)          # 基线 8082,+8 为本单新增

pnpm --filter @objectstack/spec typecheck
  tsc --noEmit && check:test-typecheck: OK

pnpm --filter @objectstack/spec check:generated
  ✓ All 10 generated artifacts are up to date.

node scripts/check-nul-bytes.mjs
  OK (scanned 5610 tracked text file(s); no raw ASCII control bytes)

消费半径已按「规则的调用方」而非「改动的包」扫过:metadata / objectql / service-automation / service-datasource / metadata-protocol / lint(即 applyConversions 系列与 normalizeStackInput 的全部调用方)。

生成物

check:generated 十项全绿,没有任何生成物移动——本单改的是转换的 apply 行为与 fixture,而 spec-changes.json / upgrade-guide 投影的是条目的 id / surface / summary / toMajor,这些一个没动。

packages/spec/authorable-surface.base.json 在本地 build 时被 gen:schema 重锚了(baseRev 指向新的 main,并带进 api/Discovery:scoping别人已合并的键)——与本单无关,按 #5358git checkout 剔除,未进提交。

风险与边界

  • 对存量元数据是「删键」:等值分支会真的从加载结果里删掉旧拼法。按定义该键不携带任何规范键没有的信息,且每次删除都发 notice(与普通改名同一种 notice,validate 输出的种类不变)。
  • 异值的存量 flow 仍会在 17 上硬停——这是裁决明确选择的结果(不替客户择一),处方现在点名双键。
  • wait / connector_action 的 lift 未纳入,已在代码注释中写明是「不同的问题」而非遗漏。

Changeset

@objectstack/spec: patch —— 没有新增/移除任何可写的键或导出,是既有 D2 转换在一个边界情形上的行为订正,与仓库里同类行为修正(如 webhook-authoring-surface-bridge 给 spec 的 patch)同级。变更在升级时对作者可见,changeset 正文写清了作者看得到的两种结果。


Generated by Claude Code

…keeping it (#4923)

`renameKey` / `renameConfigKey` did nothing whenever the canonical key was
already present, so every D2 rename left the retired spelling sitting in the
converted metadata — the conversion had not finished converting. Invisible
while the flow-node config contracts were `.strip`; an execute-time guard
refusal once #4001 batch 9 made them strict.

Per the maintainer ruling recorded on the issue, the case now splits on whether
the two values disagree:

  - equal (structural equality) -> the alias carries nothing the canonical key
    does not, so it is deleted and the usual notice fires. Lossless hygiene,
    inside the D2 contract, and idempotent in both shape and notices.
  - different -> both keys survive and nothing is emitted. Two spellings with
    two values is genuine author ambiguity; picking one would edit a config the
    customer never agreed to. The surviving pair is what lets the strict gates
    refuse with a prescription naming BOTH keys.

`renameConfigKey` now delegates to `renameKey` so the rule has one definition,
and the structural-equality predicate is the one the composer already used for
"same value composes fine" (#5005), extracted to `shared/deep-equal.ts` so the
two surfaces cannot disagree about what "the same" means.

`liftNotifySourceShape` follows the same rule: a nested part repeating its flat
counterpart is redundant (and `source` is dropped), a part that disagrees leaves
the node entirely untouched so `source` reaches the strict contract. The `wait`
node's loose-key lift is deliberately NOT covered and says so — it moves keys
between locations rather than resolving two spellings of one slot.

Batch-9 guidance reworded: a surviving twin is no longer "dead", it holds a
value the canonical key does not, so each prescription names both keys and asks
for a decision.

Also adopts `mapPageComponents` (#5509) for protocol-15
`page-component-visibility-to-visibleWhen` per #5511 — its existing fixture is
unchanged and stays green, which is the equivalence acceptance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 2:36am

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…d finish the guidance sweep (#4923)

Follow-up on the same ruling.

`renameKey(dict, k, k)` would have reached the new equal-value branch, compared
the value against itself, called it a redundant twin and deleted the only copy.
No registry pair is written that way today (all 40 literal pairs scanned), so
this is a guard rather than a fix: one added later now converts nothing instead
of erasing data. Pinned by a test on both `renameKey` and `renameConfigKey`.

Also completes the batch-9 wording sweep the ruling asked for: the notify
guidance test still asserted the old "dead twin" reading, and the `source`
prescription said DISAGREES where the rest of the family says DIFFERENT. Both
now speak one vocabulary, and the test additionally requires each prescription
to say the two values differ — not just that one of them should be deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D

Copy link
Copy Markdown
Contributor Author

二次提交后的完整复验(最终树)

e56af879b 补了一处守卫 + 收尾了 guidance 措辞,重跑全部门禁。两条独立路径(直接跑 + 排在共享验证锁里的那次)结论一致:

#### SPEC TEST ####
Test Files  317 passed (317)
Tests  8091 passed (8091)

#### SPEC TYPECHECK ####
tsc --noEmit: exit 0
check:test-typecheck: OK

#### CONSUMERS ####(转换层的全部调用方)
service-datasource   11 passed (204 tests)
metadata             24 passed (495 tests)
lint                 59 passed (1373 tests)
metadata-protocol    44 passed (407 tests)
objectql            121 passed (1970 tests)
service-automation   61 passed (730 tests)

#### GENERATED ####
✓ All 10 generated artifacts are up to date.

补的守卫

renameKey(dict, k, k) 会落进新增的等值分支——拿值和自己比、判定为「冗余孪生」、把唯一一份删掉。扫过 registry 里全部 40 条字面 pair,今天没有这样写的,所以这是守卫不是修复:以后有人加一条,结果是「不转换」而不是「抹数据」。renameKeyrenameConfigKey 两侧都有钉子。

顺带统一了 notify source 处方的用词(原写 DISAGREES,与同族的 DIFFERENT 不一致),并让 guidance 测试额外要求每条处方说清「两个值不同」,而不只是「删掉其中一个」。

一次值得记下的假红

首轮消费方扫描时 service-datasource 报了 20 条失败,全部是 driver requested but @objectstack/driver-mongodb is not installed——新工作树里没先构建被测包的依赖(AGENTS.md §9 的镜像陷阱)。pnpm --filter '<pkg>^...' build 之后 11/11 全绿,与本变更无关。

顺手记录的发现

#5732(observation 类,finding,未排期、未指派):liftWaitEventConfigconnector_action 的声明块 lift 仍是无条件遮蔽。本单刻意没扩张过去——它们是跨位置搬迁且一个 target 对多个候选名,#4923 的按值口径套不过去,需要单独判一次。代码注释与对应测试都写明了这是刻意为之,不是漏改。


Generated by Claude Code

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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0087 别名转换会把「被遮蔽的旧拼法」留在存量元数据里 —— 节点 config 收紧后它从静默丢弃变成执行期硬拒

2 participants