Skip to content

fix(runtime): 退役 callData 的 batch 分支 —— 唯一一支「返回成功」的未实现 action (#5856) + 401 信封注释纠正 (#5800) - #6244

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-5856-calldata-batch-branch
Aug 7, 2026
Merged

fix(runtime): 退役 callData 的 batch 分支 —— 唯一一支「返回成功」的未实现 action (#5856) + 401 信封注释纠正 (#5800)#6244
qq9340100 merged 2 commits into
mainfrom
claude/issue-5856-calldata-batch-branch

Conversation

@qq9340100

@qq9340100 qq9340100 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #5856
Fixes #5800

主体是 #5856(删除 callDatabatch 分支);#5800搭车的一行注释纠正,理由见第 7 节。

按分诊倾向走 选项 1(删除该分支):callDataaction === 'batch' 分支落到默认的 throw { statusCode: 400, message: 'Unknown data action: batch' },与同函数其它未知 action 完全同形;domains/data.ts 注释里的 batch 一并清掉。实施过程中没有发现任何真实接线计划的证据(依据见第 3 节),因此不改走选项 2(501)。

1. 前提重验(against origin/main @ 8e2bbba24)

前提 结论
分支仍在、仍返回成功 packages/runtime/src/action-execution.ts:350(立单时行号即当前行号),return { object: params.object, results: [] }
它是该函数唯一「返回成功」的未实现分支 ✅ 其它未知 action → 400;aggregate 引擎缺能力 → 503;仅此一支返回 200
当前不可达 ✅ 见下方枚举
domains/data.ts// 1. Custom Actions (query, batch) 仍在 ✅ 已清理

2. 不可达性枚举 —— 「删除安全」的论证主体

callData全部入口,以及每个入口能送进来的 action 取值:

入口 送进来的 action 为什么送不进 batch
domains/data.ts(/data 域) 字面量 query / get / create / update / delete 路由表只把 parts[1] 与字面量 'query' 比较;其余分支把 parts[1] 当作记录 id 读,从不当作 action 转发。POST /data/:object/batch 在本域不匹配任何分支,返回 handled: false
domains/mcp.ts(MCP 桥 / run_action) 字面量 query / get / aggregate / create / update / delete const callData = actionExec.callData.bind(null, deps, context) 之后每个调用点都是硬编码字面量
domains/actions.ts:300action-execution.tsinvokeBusinessAction 字面量 get 同上
endpoint-executor.ts(声明式端点,在 dispatcher-plugin.ts:1434 绑定) 每个 ObjectOperation 一个字面量 ObjectOperation 就是 ApiEndpointSchema.objectParams.operation封闭枚举 find/get/create/update/delete;executeObjectOperation 的兜底分支是 delete,不是透传
包外调用者 callData 不在 packages/runtime/src/index.ts 的导出面上,@objectstack/runtimeexports 只有 . 一个入口

这份枚举里的两条结构性约束已经写成可执行断言(见新增测试第 3 组):/data 域拒收 /data/:object/batch,以及 ApiEndpointSchema 拒收 operation: 'batch'

⚠️ 一处必须说清楚的否定之否定:POST /data/:object/batch 是真实存在的端点 —— 由 @objectstack/restregisterBatchEndpoints 挂载(rest-server.ts:8406),连同跨对象的 POST /batch(rest-server.ts:8236)。本 PR 钉的是 dispatcher 的 /data 域不是这条路径的第二个 owner(route-ownership 规则 1),而不是「这条路径不存在」。批处理的 owner 唯一且不变。

3. 选项 1 的依据

  1. ADR-0049 enforce-or-remove:没有生产者、没有消费者、没有路由的能力,不留在代码里假装存在;
  2. SDK 的 client.capabilities 声明为 WellKnownCapabilities,但两个 discovery 生产者填的是互不相交的键集 #5672 同向:该 host 的 capabilities.transactionalBatch: false 正是「本 face 不交付 /batch」,分支即便可达也不开事务、不写入 —— 它是该结论的证据而非反例;
  3. 501 的「响亮」在这里兑现不了:501 只有在有人来探测时才有价值,而枚举证明没有任何入口能探测到它;留着一个 501 分支,等于把「接线」这件事继续挂在半空;
  4. ADR-0076 D11 worklist 第 (1) 条原文即 “kill or delegate the dead callable branches”;
  5. 200 + { results: [] } 是「declared ≠ enforced」里最难查的一种:消费者无法把它与「批处理成功但零匹配」区分开,重试 / 幂等 / 审计三条链路都会记成一次成功的空操作。

没有找到接线计划的证据(找过的地方,均无命中):docs/adr/ 全文(ADR-0119 是治理 /batch 原子性的那一本,通篇不提 dispatcher / callData)、ROADMAP.md、该分支的 git 历史、以及仓库内 open issue(搜 callData / action-execution / dispatcher batch,除 #5856 外无相关单)。

4. 改动面

文件 改动
packages/runtime/src/action-execution.ts 删除 batch 分支;在默认 400 处留下墓碑注释(为什么删、枚举摘要、批处理的真正 owner、钉子测试位置)
packages/runtime/src/domains/data.ts // 1. Custom Actions (query, batch)(query),并写明「在这里重新加 batch 是给 REST 已服务的路径造第二个实现」
packages/runtime/src/http-dispatcher.ts 超出既定文件面,故说明:transactionalBatch 那段 #5672 注释逐字描述了我删掉的分支(“callData's vestigial action === 'batch' arm … returns { results: [] }”)。删完不改,它就是一句事实错误的注释 —— 正是本仓库反复付学费的那类漂移。只改这段描述,transactionalBatch: false 的裁定与理由一字未动
packages/runtime/src/action-execution-calldata-batch-retired.test.ts 新增钉子(8 个用例,见第 6 节)
.changeset/calldata-batch-arm-retired.md @objectstack/runtime patch(只描述 #5856)
packages/runtime/src/endpoint-policy.ts #5800 搭车,仅注释,见第 7 节

在线行为零变化:没有任何可达请求曾经拿到过那个响应。变的只是「第一个真的写出 batch 的调用者」拿到什么 —— 从静默成功变成响亮的 400。

5. 反向验证(先预测方向,再跑)

预测(动手前写进测试文件头):恢复被删的三行 ⇒ 第 1 组(batch is refused …)转红,失败形态为「promise 解析成 { object: 'task', results: [] } 而非 reject」;第 2 组(阳性对照)与第 3 组(枚举)保持绿 —— 因为它们描述的是那条分支从未坐落其上的路径。

实测(临时恢复分支后跑同一文件,随后已还原,grep -c "action === 'batch'" packages/runtime/src/action-execution.ts = 0):

× batch is refused … > without the protocol slot (ObjectQL fallback) → 400 Unknown data action: batch
   → promise resolved "{ object: 'task', results: [] }" instead of rejecting
× batch is refused … > with the protocol slot → 400 Unknown data action: batch
   → promise resolved "{ object: 'task', results: [] }" instead of rejecting
× batch is refused … > answers `batch` in the SAME shape as any other unknown action
× batch is refused … > never answers a 200 whose body reads as "the batch ran and matched nothing"
✓ the served actions still answer (positive control) > without the protocol slot → query lists, get reads
✓ the served actions still answer (positive control) > with the protocol slot → query lists, get reads
✓ nothing upstream can spell `batch` … > the dispatcher's `/data` domain declines /data/:object/batch
✓ nothing upstream can spell `batch` … > a declared endpoint cannot ask for `batch`
 Tests  4 failed | 4 passed (8)

方向与预测一致(常规红向)。一处与模板的偏差,如实记:预测写的是「第 1 组转红」,该组实际有 4 个用例(2 个参数化 + 2 个),4/4 全红,不是我脑子里默数的 3 个;其余 4/4 全绿。

6. 测试与门禁

新增钉子 packages/runtime/src/action-execution-calldata-batch-retired.test.ts,三组:

  1. 钉子:batch有 protocol slot无 protocol slot 两种部署下都 reject { statusCode: 400, message: 'Unknown data action: batch' }(被删的分支坐在两条路径之后,所以两种都要钉);并断言它与任意其它未知 action(frobnicate)的拒绝形状逐字段一致,只有 action 名不同 —— 即「batch 不再是任何东西的特例」;
  2. 阳性对照:query / get 在两种部署下照常返回;
  3. 枚举的可执行部分:/data 域拒收 /data/:object/batch(同时用 /data/:object/query 走通作对照,防止「整个域坏掉」也能让断言通过);ApiEndpointSchema 拒收 operation: 'batch'、接受 create
pnpm --filter @objectstack/runtime test      → Test Files 106 passed (106) / Tests 1514 passed (1514)
pnpm --filter @objectstack/runtime typecheck → tsc --noEmit,无输出
npx eslint --no-inline-config(改动的 5 个 .ts) → 无输出

(两次都跑过:#5856 提交后一次,#5800 搭车提交后再一次,结果相同。)

ESLint job 内的家族门禁本地逐条跑过,全 PASS:check:nul-bytescheck:engine-double-contractcheck:route-envelopecheck:error-code-casingcheck:wildcard-fallthroughcheck:adr-anchorscheck:durability-log-levelcheck:startup-registry-verdictcheck:slot-lookupcheck:query-options-erasure

新增的 fake engine 只声明 find / findOne,不声明任何写动词 —— check:engine-double-contract 的检查面是 double 的 delete / update,一个用不到却要跟着契约走的写动词本身就是它治理的对象。

7. 搭车项:#5800(endpoint-policy.ts 的 401 信封注释)

为什么搭车:#5800 的分诊建议即为搭车,且维护者本轮示意加速 —— 处置是一行注释、零行为变更、零测试、零 changeset 条目,单独跑一趟 CI 的成本远高于其收益,而它与本 PR 同包(packages/runtime)、同「注释与事实漂移」类别。

改了什么:packages/runtime/src/endpoint-policy.tsanonymousDenial() docstring 原文是 “The 401 every seam on this platform answers — same code, same message, same envelope”。前两项成立,第三项不成立:此处的 apiErrorResponse 产出的是 dispatcher 的 wrapper 信封 { success: false, error: { code, message, httpStatus } },而 REST seam(@objectstack/rest enforceAuthANONYMOUS_DENY_BODY)写的是扁平 { error, message }。新注释说明:code / message 与其它 seam 一致,envelope 是 dispatcher 自己的 wrapper,两个信封都是 ADR-0112 2026-07-30 修正案(#4007)记录在案的 live 信封,收敛归 #3843 family;完整的两信封对照表继续单源@objectstack/coresecurity/anonymous-deny.ts(#5632 已窄化的那段),此处只指过去、不复制,措辞与其保持一致。

这正是 #5632 修掉的同一条谎言长在产出 wrapper 的那一侧,而那一侧读起来更像权威 —— 也是不该让它继续躺着的原因。packages/runtime 的行为一行未动。

…ction that answered success (#5856)

`callData`'s `action === 'batch'` arm returned `{ object, results: [] }`: an
HTTP 200 a consumer cannot tell apart from "the batch ran and matched nothing",
with no transaction opened and nothing written. It was the only arm in that
function answering an unimplemented action with success — every other unhandled
action throws `400 Unknown data action: …`, `aggregate` throws `503`.

Its safety lived upstream, in a route table that happens not to spell `batch`,
not in any guard of its own. Every entry point was enumerated before removal
(`/data` compares `parts[1]` against the literal 'query'; the MCP bridge, the
actions domain and `invokeBusinessAction` pass literals; the declarative
endpoint executor is bounded by ApiEndpointSchema.objectParams.operation, a
closed enum; `callData` is not exported from this package), so the arm is
removed under ADR-0049 enforce-or-remove rather than converted to a 501 nobody
would ever receive. `domains/data.ts`'s `// Custom Actions (query, batch)`
comment — the last trace of a wiring that never happened — goes with it, and
`http-dispatcher.ts`'s #5672 capability comment is updated to describe the code
that now exists.

No reachable request produced that response, so no online behaviour changes.
Batching keeps its single owner: `@objectstack/rest`'s `registerBatchEndpoints`
serves both `POST /batch` and `POST /data/:object/batch` (ADR-0119).

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

vercel Bot commented Aug 7, 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 7, 2026 12:04pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

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 7, 2026
…atcher envelope, not "the platform's" (#5800)

`anonymousDenial()`'s docstring claimed "same code, same message, same
envelope". The first two hold; the third does not: `apiErrorResponse` builds
the dispatcher wrapper `{ success: false, error: { code, message, httpStatus } }`,
while the REST seam (`@objectstack/rest` `enforceAuth` → `ANONYMOUS_DENY_BODY`)
answers the flat `{ error, message }`. Two live, sanctioned envelopes per
ADR-0112's 2026-07-30 amendment (#4007).

This is the same false claim #5632 narrowed on `ANONYMOUS_DENY_BODY`, surviving
on the side that PRODUCES the wrapper — where a reader (especially an AI author)
takes it as authoritative. Comment only; the wire body is untouched, and the
two-envelope table stays single-sourced in `security/anonymous-deny.ts`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbxm29qPKnLf44AbSxizqW
@qq9340100 qq9340100 changed the title fix(runtime): 退役 callData 的 batch 分支 —— 唯一一支「返回成功」的未实现 action (#5856) fix(runtime): 退役 callData 的 batch 分支 —— 唯一一支「返回成功」的未实现 action (#5856) + 401 信封注释纠正 (#5800) Aug 7, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 7, 2026 12:28
@qq9340100
qq9340100 added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit f7d80f4 Aug 7, 2026
25 checks passed
@qq9340100
qq9340100 deleted the claude/issue-5856-calldata-batch-branch branch August 7, 2026 12:41
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

2 participants