Skip to content

fix(metadata-core): delete 判定末尾改真值测试,假值标量 where.id 不再答 by-id (#5747) - #5999

Queued
baozhoutao wants to merge 2 commits into
mainfrom
claude/issue-5747-delete-falsy-scalar-id
Queued

fix(metadata-core): delete 判定末尾改真值测试,假值标量 where.id 不再答 by-id (#5747)#5999
baozhoutao wants to merge 2 commits into
mainfrom
claude/issue-5747-delete-falsy-scalar-id

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5747

前提复核(实测,不是照抄 issue)

issue 正文的落点是旧世界(packages/objectql),判定与 CASES 已随 PR #5871 迁到 packages/metadata-core/src/engine-delete-dispatch.ts。在新落点上用记录型 driver 驱动真实引擎逐条实测,前提成立:

调用 真实 ObjectQL.delete 判定(改动前) scalarDeleteId
{ where: { id: 0 } } reject by-id 0
{ where: { id: '' } } reject by-id ''
{ where: { id: 0 }, multi: true } multi by-id 0
{ where: { id: '' }, multi: true } multi by-id ''
{ where: { id: 'rec_1' } }(对照) by-id by-id 'rec_1'

两侧问的不是同一个问题:判定读 scalarDeleteId(...) !== undefined,而 engine.ts 把判定结果落进 id 后按 if (hookContext.input.id) 分支 —— 真值测试,0 / '' 落到 multi/reject 阶梯。

后果正是本模块存在的理由(#4434 形状):按 assertEngineDeleteDispatch(options) 钉死的替身接受 delete(o, { where: { id: '' } }),而真服务器抛 Delete requires an ID or options.multi=true。pinned 替身在这一个输入上仍比生产者宽松。id: ''(路径段为空 / 表单字段未填直传 where.id)是可达形状。

改了什么

按分诊裁定实施 A,一行行为改动:

- if (id !== undefined) return { kind: 'by-id', id };
+ if (id) return { kind: 'by-id', id };

改的是判定,不是引擎。 resolveEngineDeleteDispatch 是对 ObjectQL.delete 的描述,错的是描述:delete(o, { where: { id: 0 } }) 改动前抛错,改动后照样抛错,生产者行为零变化,engine.ts 一字未动。B 方案(让 { id: 0 } 变成真的按 id 删)是改生产者行为,明确不取,并在模块头写明为什么。

其余为文档与用例:

  • 模块头「The contract, normatively」补第 2 点(真值而非 !== undefined),与 update 侧孪生模块的第 3 点同形,便于对读;
  • ENGINE_DELETE_DISPATCH_CASES 补四例({id:0} / {id:''} × 有/无 multi)—— 此前这套「真实引擎 == 判定」的逐例对照结构上够不到这个输入(merge.os-regen.driver 指向「上一个装过依赖的 worktree」的绝对路径 —— 该 worktree 一删,全容器的生成物合并驱动就坏了 #4868 家族:一次逐例跑不可能反驳一个没人列出来的输入),这才是判定能安静漂移的原因;
  • scalarDeleteId 保持值忠实({ where: { id: 0 } } 仍返回 0),真值测试只加在判定这一层。与 update 侧 scalarUpdateId 的分法一致:提取器回答「那个是不是标量」,判定回答「这个调用是什么」。把提取器收窄成「真值标量」会让它和自己的名字打架。

反向验证(方向先预测,后运行)

if (id) 改回 if (id !== undefined) 并重建 —— 四行预测,四行全中:

测试 预测 实测
4 条新 CASES(逐例对照) ,且红在 predicate 断言上 AssertionError: predicate: expected 'by-id' to be 'reject' / to be 'multi'
branches on TRUTHINESS expected 'by-id' to be 'reject'
a fake pinned to assertEngineDeleteDispatch… expected [Function] to throw an error
falsy-id multi 仍被 where 限定 绿 ✅ 6 failed / 17 passed,该例在 17 里

第四行是关键的一行:它证明那条测试钉的是引擎而不是判定 —— { id: 0 } 走 multi 是 engine.ts 自己的 if (input.id) 决定的,与判定返回什么无关。所以它在回退下不该变红,也确实没有。

第三行是本单的靶心:回退后 assertEngineDeleteDispatch({ where: { id: '' } }) 不抛,这就是替身比生产者宽松的那一格。

一个如实交代的次生差异

engine.tsconst id = dispatch.kind === 'by-id' ? dispatch.id : undefined 意味着 hookContext.input.id 由判定派生。收紧判定后,{ where: { id: 0 } }input.id0 变为 undefined。逐处核对了它的下游,没有一处改变行为:if (!id)(AST 播种)、if (id && wantsPreImage)if (hookContext.input.id) 三处对 0undefined 同答;唯一的 ??(hookContext.input.id ?? resultId)只在 isPredicateWrite === false 时可达,而那蕴含 input.id 为真,故对假值 id 结构上不可达。剩下的是 beforeDelete 钩子看到的 input.id:引擎本就拒绝把它当 id,现在钩子看到的和引擎的判断一致了 —— 这是消除不自洽,不是引入行为。

测试

  • packages/metadata-core:103/103;packages/objectql:2138/2138(新增 4 条 CASES + 3 条命名测试)
  • 消费半径扫荡(judged by 门禁列出的 delete 侧替身所在包,34 个包):0 失败。含 metadata-protocol 的 13 个接线 fake(49 文件 / 486 测试全绿)—— PM 要求的自证:全仓 grep 假值标量 id 字面量,除 update 侧自己的用例外无任何 fixture 驱动这个形状,故新语义对它们无命中
  • pnpm typecheck(metadata-core + objectql):Done
  • node scripts/check-engine-double-contract.mjs:OK — 72 pinned, 133 in the DEBT ledger, 2 exempt.
  • node scripts/check-nul-bytes.mjs:OK
  • 推送前已 git merge origin/main(⛔ 未 rebase)并全量重建 + 重跑,全绿

changeset

@objectstack/metadata-core patch —— 写明「修的是判定对生产者的忠实度,生产者行为零变化」。


Generated by Claude Code

claude added 2 commits August 6, 2026 14:00
…patch (#5747)

`resolveEngineDeleteDispatch` answered `by-id` for a falsy scalar `where.id`
(`0`, `''`) while `ObjectQL.delete` — which branches on
`if (hookContext.input.id)` — routes the same call to multi/reject. Measured
against the real engine with a recording driver:

  {where:{id:0}}              engine=reject  predicate=by-id
  {where:{id:''}}             engine=reject  predicate=by-id
  {where:{id:0},multi:true}   engine=multi   predicate=by-id
  {where:{id:''},multi:true}  engine=multi   predicate=by-id

So every fake engine pinned to `assertEngineDeleteDispatch` ACCEPTED
`delete(o, { where: { id: '' } })` while a running server throws — the #4434
shape this module exists to remove, on the one input the shared case-set
could not reach (#4868 family).

This changes the PREDICATE, never the engine: the predicate is a description
of `ObjectQL.delete` and the description was wrong. `engine.ts` is untouched
and `delete(o, {where:{id:0}})` throws before and after. Option B (make
`{id:0}` a real by-id delete) would have changed producer behaviour and is
deliberately not taken.

`ENGINE_DELETE_DISPATCH_CASES` gains the four falsy shapes, so the real-engine
parity run now reaches them. `scalarDeleteId` stays value-faithful — it
answers "is that VALUE a scalar", the truthiness test belongs to the dispatch
— matching how `scalarUpdateId` and the update twin already split it (#5748).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@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:11pm

Request Review

@github-actions github-actions Bot added the size/m 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/metadata-core.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-core)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata-core)
  • content/docs/releases/v12.mdx (via @objectstack/metadata-core)

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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 14:25
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31111166282 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (2/3) — 失败步骤: Run this shard's tests

    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires a legacy global unique index and replaces it with the composite
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mretires the legacy `uniq_<table>_<col>` index left by the drift rebuild path
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2ma
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] product: cannot tighten 'uniq_product_organization_id_code' as UNIQUE (COALESCE(organization_id, '__global__'), code) — existing rows already violate the NULL-safe unique cons
    �[90mstderr�[2m | src/sql-driver-unique-tenancy.test.ts�[2m > �[22m�[2mSqlDriver unique × tenancy (#3696)�[2m > �[22m�[2mbare-composite tightening + duplicate pre-flight (ADR-0120 D4)�[2m > �[22m�[2mB
    �[22m�[39m[schema-drift] REFUSING to rebuild 'uniq_product_organization_id_code' on 'product' as a NULL-safe unique — 1 duplicate group(s) violate it (e.g. organization_id="__global__", code="DUP" × 2
    �[41m�[1m FAIL �[22m�[49m src/__tests__/datasource-pool-support.test.ts�[2m > �[22m#5714 — the driver factory rejects a pool it cannot honour�[2m > �[22msqlite WITHOUT a pool still builds exactly as b
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 9 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Copy link
Copy Markdown
Contributor

队列管家拦截:⛔ 不原样重投 —— 台账命中 objectstack 表第 2 行「已修签名」(作用域外的新落点)

本 PR 于 14:30Z 被踢出合并队列。红与本 PR 的 diff 无关(你改的是 packages/metadata-core),但按 #5810 签名台账 objectstack 表第 2 行的判定,这条签名属于已修签名再现 ⇒ 不重投、交车道重新诊断,所以队列管家没有替你重挂 auto-merge。是否入队、何时入队仍归本 PR 所属车道决定 —— 重投在技术上可行,但那只是把同一颗骰子再掷一次(见下面的命中率),不修根因。

完整签名(取完整日志归档,非 tail —— SKILL note 7):

  • run 31111166282(14:30:31Z,队列条目 pr-5999-44106d9e…)→ job Test Core (2/3) → 步骤 Run this shard's tests,包 @objectstack/service-datasource
  • FAIL src/__tests__/datasource-pool-support.test.ts > #5714 — the driver factory rejects a pool it cannot honour > sqlite WITHOUT a pool still builds exactly as before
  • Error: Test timed out in 5000ms. @ src/__tests__/datasource-pool-support.test.ts:122:3,该文件实测 5202ms;同 run import 22.27s
  • 同 job 内 Test Files 1 failed | 11 passed (12) / Tests 1 failed | 229 passed (230) —— 只有这一条红

note 7 的两个陷阱都现身、都已绕过:同 job 内 Failed: @objectstack/service-datasource#test 紧邻的其它包输出是 turbo --concurrency 并发相邻,非因果;下游 Test Core 聚合 job 的 aggregate result: failure 是后果不是原因。

判读:这是 #4796 家族那条 5000ms 超时签名的作用域外新落点,不是「修好的东西又坏了」,也不是本 PR 的回归。#4856testTimeout: 60_000包级修法,只落在 packages/spec/vitest.config.ts@objectstack/service-datasource 无 vitest 配置,仍跑默认 5000ms,而 :122 那条用例做真实 sqlite 驱动构建却未挂显式超时(同包 default-datasource-driver-factory.test.ts 的同类用例挂了 }, 30_000))。根因分析与两条 XS 量级修法已于 14:36Z 送达 services 车道锚点 #5714根因知会),本轮不重复展开。

这是同一签名在 40 分钟内踢掉的第二个不相干 PR:13:52Z 踢 #5973(diff 只碰 packages/spec/src/contracts/sharing-service.ts),14:30Z 踢本 PR。⇒ 共享损伤,受害者与 diff 无关,修法不在被踢的 PR 这一侧。频率如实给数:属边界抖动(5202ms vs 5000ms),非必现。

让行判据:处置前读本 PR 最近 30 分钟评论,仅有 merge-queue-triage workflow 的自动分诊评论(14:43:48Z)与两条 bot 评论,无车道 PM 在处置 ⇒ 让行不成立,按拦截分支动作。

⛔ 本条为止:未重投、未入队/撤队、未重跑、未改代码、未动认领。


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31117284898 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 22 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31120011988 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 4 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 40 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine-delete-dispatch 的共享判定与 ObjectQL.delete 在「假值标量 id」上不一致 —— where: { id: 0 } 判定答 by-id,引擎却 reject

3 participants