Skip to content

fix(metadata-protocol): an undeclared hook abort is HOOK_ABORTED on the per-object batch row too - #29977

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-maj60f
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-maj60f

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29894

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:钩子用自由文本中止写入且未声明错误码时,POST /api/v1/data/:object/batch 的行错误码现在是 HOOK_ABORTED,不再是 VALIDATION_ERROR,与 /data、/createMany、/batch 一致。
  • 为什么改:error codes: an undeclared hook onError abort answers the data door with a code-less 400; give it a ledgered ADR-0112 code (#27508 code half) #27597 给这种中止定了唯一线上码 HOOK_ABORTED,但行映射 toRowApiError 仍按 400 推导出 VALIDATION_ERROR,批量客户端会把业务拒绝当成输入格式错误。
  • 风险与回滚:只换这一种中止的行码(文案、httpStatus 不变);钩子声明了码的照旧。回滚即 revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-client-failures.ts 的 toRowApiError / isUndeclaredHookAbort;新 pin packages/qa/dogfood/test/batch-row-hook-aborted.dogfood.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/batch-row-hook-aborted.dogfood.test.ts,预期 14 passed。
  • 需要你做的:无,席位落地。

What changes

Same-pattern sweep (what I checked)

  • protocol-batch-data.ts: the create/upsert loop (:586), the update loop (:887) and the delete loop (:1079) all call toRowApiError, so one fix covers them. The atomic siblings stay NOT_ATTEMPTED / ROLLED_BACK, and the causal row now carries HOOK_ABORTED (pinned live).
  • clientFacingFailureCode (same file): it builds the metadata failed[] code limb. No hook abort reaches it, so it is left as is.
  • Import rows (POST /data/:object/import, core/src/utils/import-runner.ts toFailedResult) do not share the mapper. Measured on the same boot, the row answers { error: <sentence>, code: "IMPORT_ROW_FAILED" }, which is the import report's own generic code and not VALIDATION_ERROR. This PR leaves it alone, because the order scopes the sweep to doors that share the mapper. Whether the import row should also say HOOK_ABORTED is reported to the seat as a possible follow-up.

Pins

  • New, live: packages/qa/dogfood/test/batch-row-hook-aborted.dogfood.test.ts. It boots once (14 cases). §1 covers the card's three whole-request doors (/data, /createMany, /batch): 400, the sentence, and HOOK_ABORTED. §2 covers the per-object batch row: create, create atomic, create continueOnError, update, upsert (existing id), delete, each with the sentence and HOOK_ABORTED, and nothing written. §3 is the control: a hook that declares RECORD_LOCKED keeps it on all four doors.
  • Moved: packages/metadata-protocol/src/protocol.batch-row-sandbox-refusal.test.ts §1/§2 (8 cases) now expect HOOK_ABORTED, and §1 compares the row's code to the /data door's own body.code instead of the code its status derives. §3's unregistered-code case still pins VALIDATION_ERROR and is unchanged.

Ablation, once. I restored protocol-client-failures.ts from origin/main and rebuilt metadata-protocol. The dogfood file then went 6 failed / 8 passed: exactly the six §2 rows, each expected 'VALIDATION_ERROR' to be 'HOOK_ABORTED', while §1 and the §3 controls stayed green. The unit file went 8 failed / 12 passed. After restoring the fix and rebuilding: 14/14 and 20/20.

Verification

  • pnpm --filter @objectstack/metadata-protocol test: exit 0 (312 files, 22339 passed). typecheck: exit 0.
  • pnpm --filter @objectstack/dogfood typecheck: exit 0. The new dogfood file: 14 passed.
  • pnpm lint: exit 0.
  • Build: turbo run build --filter=@objectstack/dogfood^... exit 0, plus the seven dists check:dual-build-cjs-loads reads.
  • node scripts/pm/dispatch-gates.mjs --commands on f286c443: 63 derived, 63 run, all exit 0. --ran reports "63 run, 0 NOT-MEASURED". check:dual-build-cjs-loads exited 3 until those dists were built, then 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzeV9ZbECNr1Z2G2Puu

…he batch row too

The per-object batch row (POST /data/:object/batch, every verb and mode, and
updateManyData/deleteManyData rows) derived a sandboxed free-text refusal's
code from its 400 (VALIDATION_ERROR), while /data, /createMany and /batch
answer HOOK_ABORTED since #27597. toRowApiError now picks
UNDECLARED_HOOK_REFUSAL_CODE for that case; a declared code still wins.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzeV9ZbECNr1Z2G2Puu
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: TIER_DEFAULT
Head-sha: f286c443d998ab9483cae649e4de29c606d81553
Local-runs: none

Face: declaration (Clause-②: no, Metadata-protocol: none). No published schema and no governed path in the committed tree (4 files). Read: the full diff, card #29894 and its triage audit 6601871119, the precedent #27597 / #27672 (PR #27898) declarations, and the dev report.

① Derived judgments

② Semver level

'@objectstack/metadata-protocol': patch. No exported surface moves (isUndeclaredHookAbort is module-private). Consistent with both declaration lines and the precedent.

③ Boundary flags

  • Out of scope, filed by the seat: POST /data/:object/import answers the same abort as row code IMPORT_ROW_FAILED (a different mapper, core import-runner toFailedResult), measured live by the dev.
  • Pre-existing, noted: a declared-code sandbox refusal logs [REST] Unhandled error: SandboxError on /data. This is log noise, not a wire defect.

Implemented-by: claude/project-thread-maj60f
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head f286c443d998ab9483cae649e4de29c606d81553 · 2026-11-03T21:16Z

  • Shape: 4 files: the metadata-protocol row mapper, its unit cases, a new dogfood pin, and one patch changeset. No published schema, no governed path ⇒ not Tier H. TIER_DEFAULT review PASS 6615620869 at this head.
  • Premise measured: the dogfood pin reproduces the card's four-door split on one boot. With main's mapper restored, §2 went 6 red with VALIDATION_ERROR.
  • Gates: metadata-protocol 22339 pass, typecheck 0, dogfood typecheck 0, pnpm lint 0, dispatch-gates 63/63 exit 0 (--ran 0 NOT-MEASURED). Landing waits for every check on this head to be green or an expected skip.
  • Accepted deviations: harness-pinned branch claude/project-thread-maj60f. The import-row divergence is left out of scope and filed separately by the seat. The signed trailer is appended by the seat through the relay.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 5 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError), data.batch (sdk, the route ledger binds it to POST /api/v1/data/:object/batch, selected by route anchor /data/:object/batch))
  • content/docs/api/data-api.mdx (via /data/:object/batch (route, a path literal in a comment on a changed line))
  • content/docs/api/error-catalog.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))
  • content/docs/api/wire-format.mdx (via /data/:object/batch (route, a path literal in a comment on a changed line))
  • content/docs/plugins/development.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))
  • content/docs/protocol/kernel/error-handling.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))
  • content/docs/protocol/kernel/http-protocol.mdx (via /data/:object/batch (route, a path literal in a comment on a changed line))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via /data/:object/batch (route, a path literal in a comment on a changed line))
  • content/docs/releases/v16.mdx (via data.batch (sdk, the route ledger binds it to POST /api/v1/data/:object/batch, selected by route anchor /data/:object/batch))
  • content/docs/releases/v17/17-0.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))
  • content/docs/releases/v17/17-4.mdx (via /data/:object/batch (route, a path literal in a comment on a changed line))
  • content/docs/releases/v18/18-2.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))
  • content/docs/releases/v18/18-3.mdx (via INTERNAL_ERROR (literal, a string literal in toRowApiError))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 10 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 c92b88728cb533338759c3d700a683f29db1ee60 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c107ae076c675caeea186ce6850245b01602bdb — the merge of head 73cf31de6e575b40c8b8ea21082672194d02a342 into base c92b88728cb533338759c3d700a683f29db1ee60, 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 6c107ae076c675caeea186ce6850245b01602bdb && git checkout 6c107ae076c675caeea186ce6850245b01602bdb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c92b88728cb533338759c3d700a683f29db1ee60 73cf31de6e575b40c8b8ea21082672194d02a342 && git checkout -B drift-repro c92b88728cb533338759c3d700a683f29db1ee60 && git merge --no-ff 73cf31de6e575b40c8b8ea21082672194d02a342

node scripts/docs-audit/affected-docs.mjs --json c92b88728cb533338759c3d700a683f29db1ee60

⚠️ 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 c92b88728cb533338759c3d700a683f29db1ee60 → 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

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Head moved · 2026-11-03T22:30Z · f286c443 → 73cf31de, moved by the seat through the relay pr_update_branch as a clean merge of main (c92b8872). On f286c443 the only red was the lint docs-guard job's checkout timeout (runner slowness, a lint.yml run the relay cannot rerun).

  • The PR's own diff is identical: the single commit f286c443 is the merge's first parent, and the stat against main is 4 files, +205/−9. The TIER_DEFAULT PASS 6615620869 and ACCEPT 6615666459 carry over to this head.
  • Landing: on all-green at 73cf31de, pr_ready + automerge_enable through the relay.

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

1 participant