Skip to content

fix(spec): 别名表内不得有两个 key 共用一个 probe —— 修 4 条 + 开第三条闸门断言 (#5481) - #5516

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5481-alias-probe-collision
Aug 5, 2026
Merged

fix(spec): 别名表内不得有两个 key 共用一个 probe —— 修 4 条 + 开第三条闸门断言 (#5481)#5516
os-zhuang merged 3 commits into
mainfrom
claude/issue-5481-alias-probe-collision

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#5481

前置 #5486 已 MERGED,闸门文件 packages/spec/src/shared/alias-integrity.test.ts 已在 main,本 PR 在其上加第三条断言。

前提复核(改代码前)

issue 的行号是上午的,#5486 刚合并,所以先重新定位并实测复现,而不是照抄 issue。四处撞车在 origin/main (306ca50) 上全部属实。活缺陷的实测输出:

MESSAGE >>> Unrecognized key(s) on these snap settings: `grid`. …
Did you mean `grid` → `showGrid`?
FOLLOW-THE-ADVICE >>> Invalid input: expected boolean, received number

第二行是关键:照着提示改就是第二次被拒 —— 账本 finding 7 的形状,由这场 campaign 自己的修复把作者指进去。

另外没有只信 issue 列的四条,而是跑了一次全量普查(遍历运行时 235 张表):撞车恰好 4 条,不多不少,与 issue 一致。

成因

strictUnknownKeyErroraliasProbe(key)(转小写 + 去掉 _/-/空格)给别名表建索引。同一张表里两个 key 归一化后相同即共用一个下标,后写的静默覆盖先写的,且被覆盖的那条在任何作者能碰到它之前就已经消失。

改动

1. 活缺陷studio/flow-builder.zod.ts:删掉表尾的 grid_: 'showGrid'。它把前面的 grid: 'gridSize' 覆盖了,于是想设网格像素间距的 grid: 24 被指向布尔的 showGridvisible: 'showGrid' 本来就承载了显示/隐藏那个意图,所以删掉 grid_ 不丢覆盖面。修后 grid: 24 得到 grid → gridSize,而且照做能过(gridSize: 24 解析成功)。

2. 三条休眠冗余:rollup/rollUp(field)、object_name/objectName(webhook)、strokeDasharray/strokeDashArray(chart series)。这三条两个 key 指向同一 target,覆盖后语义不变,所以今天没人踩到。各删一条。行为不变是可证的而非推测的:probe 已经吃掉大小写和分隔符,幸存的那条接受被删那条能接受的每一种拼法 —— 这条等价性现在由 strict-object.test.ts 里一条新测试钉住(用 visible_when / VISIBLE-WHEN / Visible When / visiblewhen 四种拼法验证单条 entry 全覆盖)。

3. 第三条闸门断言:一张表内任意两个别名 key 的 probe 不得相同。失败信息点名文件、surface、两个 key、各自 target,以及幸存者是谁

为什么多了一个 shared/alias-probe.ts

断言必须用真的aliasProbe 判,不能在测试里抄一份正则。抄一份就正是 #5013 / #5481 这一类问题本身的形状 —— 哪天 probe 放宽(比如再吃掉 .),抄了旧表达式的闸门会继续"通过",而它已经看不见新出现的撞车了。

aliasProbe 原本是 suggestions.zod.ts 的模块私有 const,而该文件被 shared/index.tsexport * 导出,直接导出它会扩大 @objectstack/spec 公共 API。所以抽到一个叶子模块,并刻意不进 barrel —— 与 strict-object.tsstrictObjectDeclarations(同一类"给闸门用的内部接缝",测试按相对路径拿)完全一致的处理。放进 strict-object.ts 会形成 suggestions ↔ strict-object 循环导入,而该文件的注释记录过这个循环在 OS_EAGER_SCHEMAS=1 下真炸过,故未采用。check:api-surface 绿,证实公共面没动。

反向验证(方向是先定后跑)

预判:普通的红。 这条断言是对单张表 key 集合的纯谓词,既不喂下游计数闸门,也没有 ?? 链的先后顺序问题,所以把删掉的肢体接回去应当增加一条 finding,并点名 flow-builder 那张表。

实跑与预判一致 —— 临时把 grid_ 加回:

FAIL … no two alias keys in one table collapse onto the same probe (#5481)
+ [
+ "studio/flow-builder.zod.ts:241 — \"these snap settings\": `grid` -> `gridSize` — 2 keys share the probe `grid`: `grid` -> `gridSize`, `grid_` -> `showGrid` — only `showGrid` survives",
+ ]
Tests 1 failed | 7 passed (8)

只有新断言变红,其余 7 条不动;恢复后 8/8 绿。

消费半径走查

按"规则被谁消费"而不是"改了哪个包"来扫:全仓 grep 被删的三种拼法 + grid_,packages/spec/src 之外零命中(webhook 的 objectName 命中全部是 nav item 的合法声明键或 plugin-webhooks 内部字段,不走 WebhookSchema 的别名表);these snap settings / showGrid 在 spec 之外也无断言。故无 fixture 需要改口径。

验证

  • pnpm --filter @objectstack/spec test —— 312 files / 7965 tests 全绿(main 基线 7962,+3 为本 PR 新增断言)
  • pnpm --filter @objectstack/spec typecheck —— tsc --noEmit 无输出
  • pnpm --filter @objectstack/spec check:generated —— 9/9 up to date,无需重生成(别名表不进任何生成物)
  • node scripts/check-nul-bytes.mjs —— OK;改动文件另做了 grep -naP 越界自扫,无裸控制字节

changeset

按 PM 指示打了 @objectstack/specpatch:这是对作者可见的行为修复(grid: 24 的处方从错的变成对的),不是纯内部改动,故走 changeset 而非 skip-changeset


Generated by Claude Code

…be (#5481)
`strictUnknownKeyError` indexes the alias table by `aliasProbe(key)` —
lowercased, `_`/`-`/space stripped — so two keys in one table that
normalise identically share one index and the later entry silently
overwrites the earlier. Nothing checked for it.
Live defect: `these snap settings` carried `grid: 'gridSize'` and, at the
end of the same table, `grid_: 'showGrid'`. Writing `grid: 24` was
answered "Did you mean `grid` -> `showGrid`?", and `showGrid` is a
boolean — a second rejection for taking the advice, ledger finding 7's
shape. `grid_` is deleted; `visible` already carried that intent.
Three dormant instances pointed both keys at the same target, so the
overwrite changed nothing: `rollup`/`rollUp`, `object_name`/`objectName`,
`strokeDasharray`/`strokeDashArray`. Since the probe already folds case
and separators, the surviving entry accepts every spelling the deleted
one did, so removing them preserves behaviour.
`alias-integrity.test.ts` gains the third assertion: no two alias keys in
one table may share a probe. It imports the real `aliasProbe` — extracted
to `shared/alias-probe.ts`, a leaf module deliberately kept out of the
barrel like `strict-object.ts` — rather than transcribing the expression,
so widening the normalisation cannot leave the gate judging by an old
rule. Census over the 235 runtime tables: exactly these four, now zero.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 5, 2026 3:08pm

Request Review

@github-actions

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.

`gen:schema` rewrote the anchor when the merge base moved: `baseRev`
cdfbee2 -> 168f60f, plus the two keys that commit itself added
(`UpdateRecordConfig:multi`, `DeleteRecordConfig:multi`). Both facts come
from main, not from this PR — the alias-table change adds and removes no
authorable key.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 15:48
@os-zhuang
os-zhuang added this pull request to the merge queueAug 5, 2026
Merged via the queue into main with commit a9f32dfAug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5481-alias-probe-collision branch August 5, 2026 16:12
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…bjectstack-ai#5483) (objectstack-ai#5609)
objectstack-ai#5013 的闸门判的是 strictObject 建的表(构造期登记 {options, shape}),而 helper
之前的老接线直接调 strictUnknownKeyError、自带一份手抄 knownKeys 数组,没有 shape
可登记,于是这 44 张表在闸门的每一条判据之外。objectstack-ai#5481 把判据加到三条之后,其中第三条
(表内 aliasProbe 撞车)只读别名表本身、不依赖 shape —— 也就是说这批表在撞车维度上
一直是「未测量」,而不是 issue 正文说的「已测干净」(那次测量早于 objectstack-ai#5481)。
登记放在工厂里而不是调用点上:strictUnknownKeyError 首行把自己的
{surface, knownKeys, aliases, guidance} 记进一张独立的内部登记表
(shared/alias-table-registry.ts,不进 shared/index.ts 桶,与 strict-object.ts /
alias-probe.ts 同待遇)。44 个调用点一个都没改,objectstack-ai#5593 的迁移因此仍是一次干净可分离
的改动。strictObject 内部那次调用用 withoutDirectAliasTableRegistration 抑制登记 ——
它的表已带 shape 登记在更强的那张里,重复登记会让本表规模取决于此前有没有哪个测试
恰好触发过一次拒绝。
闸门的 forcing walk 现在也用一个合成 unrecognized_keys issue 构建 error map:
strictObject 与 data/object.zod.ts 都把 map 延迟到首次拒绝才建,对直调点而言这个
延迟就是「登记了」和「根本看不见」的区别。
首测:44 个源码调用点 → 运行时 52 张表(ui/app.zod.ts 的导航项工厂一个调用点跑九次)。
- aliasProbe 撞车 0 条(参考:已覆盖的 235 张里实测 4 条,PR objectstack-ai#5516 清零)
- 别名 key 不得是已知键 0 条
- 别名 target 必须是已知键 32 条,同一根因:NAV_ITEM_ALIASES 的四条「默认展开」
别名指向 expanded,而 expanded 只声明在 group 变体上 —— 另外八个变体把作者指向
该变体同样会拒的键。已立 objectstack-ai#5555;修法要改面向作者的文案且落在调用点里,本单不动,
这里钉成结构化的只减不增容差(≤32),第五种拼法直接红。
两处显式豁免各带一条陈旧检查:ui/app.zod.ts 六条刻意的散文式 target,以及 children
的 guidance 在 object/group 两个变体上本就合法。≤44 棘轮一字未动。
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:dataprotocol:uisize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

别名表内部撞车:aliasProbe 归一化后两个 key 相同,后者静默覆盖前者(snap.grid 被指向 showGrid)

2 participants

@os-zhuang@claude