Repository navigation
docs(adr): ADR-0104 D2 — the param-less pass-through is scoped to actions with a handler; a declarative update's empty params list is an empty accept set - #24416
Conversation
…with a handler A dated note under D2 item 1, a status-line entry, and a one-line pointer at the 2026-07-30 addendum's restatement: a declarative operation 'update' action's empty or absent params list is an empty accept set, refused through D2's own path, and at the MCP run_action door a D2 refusal carries code + status, as at the REST door. Measured on origin/main ee882b8, after merge commit d79dc48. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Rules face, ① Derived judgmentsEach rule the note states, named right or wrong against the code it cites.
Prime Directive #10 clause by clause: no sentence of the note states as fact a behaviour the code or its pins do not hold. ② Semver levelDocs only: the diff is one file under ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 (seat check) · PR #24416 ·
|
The dated note under D2 item 1 now says a D2 refusal is one 400 VALIDATION_ERROR naming every offending param, with no fields[], at the REST and MCP doors, and that item 1's "400 VALIDATION_FAILED per-field envelope" names the ledger's record-level fields[] member instead. The status-line entry carries the same correction. The original sentence stays verbatim. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round 2, rules face, ① Derived judgmentsEach sentence the delta adds, named right or wrong against the code it cites.
Prime Directive #10 clause by clause: no sentence the delta adds states as fact a behaviour the code or its pins do not hold; the two places where the claim rests on the code's construction rather than on a door-level pin are named in ③ 2 and ③ 3 and are not claimed as pins by the note. ② Semver levelDocs only: the diff is one file under ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读 · PR #24416 ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, assignee zhuangjianguo. Line 1 is Fixes #24393, and Clause-②: no starts a line; there is no other closing keyword. One file, +60/−1, governed (check-governed-merges: 1 of 1 path hits the register). skip-changeset (docs only). mergeable_state: clean. |
| Premises measured | P1: the two pass-through sentences were present on main. P2: enforceActionParams judges a param-less declarative update against []. P3: at MCP run_action, a D2 refusal carries code + status. All were measured by the dev, and every cited line was re-read by both review rounds. |
| Gates (check-runs on the head) | 46 runs: 35 success, 11 skipped. check-expected-skips: OK. Dev: 19 derived families, all exit 0, reconciled with --ran. |
| Accepted deviations | The status-line sentence that announces the code correction. The review judged it true and in place. |
| Out of scope | Two absence-of-pin observations from round 2 (③ 2, ③ 3): an MCP door-level pin for a declared-params refusal, and an assertion that no fields[] appears. Both are held by construction and stated no wider than their pins. They are not filed: no producer is harmed. Dropped. |
| Landing | After an approval by os-zhuang or hotlong, or the maintainer's merge: pr_ready then automerge_enable. The merge closes #24393. |
Generated by Claude Code
|
Approval in, seat landing ·
Generated by Claude Code |
Fixes #24393
Clause-②: no
Dispatched by the
domain:specseat 2 PM (session_019oFY6Hi35J7atvgVFnmoaK, claim comment 6257175272 on #24393). This dev ran as anos-devsubagent of that session. Branched fromorigin/mainee882b845. Before the final push,origin/main97cb5c5a8was merged in (PR #24408, which touches ADR-0095 and ADR-0090 only). Patch round 1 (REWORK6257559771, from contract review6257540342③ 2) added the correction commitfce8981f8, then mergedorigin/main71fa166e9(PR #24413, which touches no file the note cites). Head:485dc4362.This is a Tier H governed surface (
docs/adr/**). The PR stays draft and lands only by the maintainer's hand. No changeset: the diff is one ADR file and publishes nothing.What changed
One file,
docs/adr/0104-field-runtime-value-shape-contract.md, +60 / −1. No decision is added. Every sentence is a measurement oforigin/mainafter merge commitd79dc4883c(PR #24392, card #24268).Status line. One
· **Amended** (2026-10-19, #24268, PR #24392, merge commitd79dc48) — …entry. It uses the list-form spelling of ADR-0103 and ADR-0091, with ADR-0090's em dash.A dated blockquote under D2 item 1. It sits right after the sentence at the old
:195–:196, which stays as written:> **Amended 2026-10-19 (…) — the param-less pass-through is scoped to actions with a handler.**It states the three points of the card's "What the record lacks", each with its symbol anchor, and names the pins that hold them.A one-line pointer at the 2026-07-30 addendum's restatement (the old
:941–:942). It uses this ADR's own inline form,*(Amended …: …)*, as its earlier refinements do: the boundary holds for actions with a handler; a declarative update's empty or absent list is an empty accept set; see the note under D2 item 1.Patch round 1: what a D2 refusal is on
main. D2 item 1's sentence just above the note says a refusal is "the standard400 VALIDATION_FAILEDper-field envelope", and the note said "the same 400" right under it. The note now says what a D2 refusal is, at both doors and for every action: one400 VALIDATION_ERRORwhose message (Invalid action params: …) joins one issue per offending param, each naming it, with nofields[]. The REST door'sdeps.error(paramError, 400)passes no code, sobuildApiError(packages/runtime/src/error-envelope.ts) derives it throughstandardErrorCodeForHttpStatus(400); the MCP throw stamps that function's answer.VALIDATION_FAILEDis the ADR-0112 ledger's record-level validation member, which carriesfields[](packages/types/src/validation-failure.ts), and the D2 gate never builds it. Pins, re-measured on485dc4362:packages/runtime/src/action-declarative-update.test.ts:326(REST, a declared-params action),:359and:376(REST, the param-lessclose_case),:390–:391(the MCP throw'scodeandstatus). The status-line entry carries the same correction in one sentence. The original sentence stays verbatim (Prime Directive [WIP] Add Chinese version of the documentation #13), and the note's closing line now reads "No other decision in D2 changes: the code and shape above correct the record, they add no rule."Placement (G2). The note sits once, at D2 item 1, where the boundary is decided. The second sentence gets a pointer, not a second copy, so the record says it in one place. Neither original sentence is rewritten (Prime Directive #13: extend the record, never reverse it).
Premises, each measured on
origin/mainee882b845before writingparamskeep today's pass-through (there is nothing to check against), so existing param-less actions are untouched." Lines 941–942 read: "Actions declaring noparamskeep their pass-through — that boundary is unchanged from D2's landing." A grep of this ADR forpass-through,param-lessand "declaring no" finds no third statement. The same grep over the seven other ADRs that cite ADR-0104 finds none.enforceActionParams(packages/runtime/src/action-execution.ts) opens withif (!declaresParams && !isDeclarativeUpdateAction(action)) return null;. For a declarative update,resolveDeclaredActionParamsreturns[].validateActionParams(packages/spec/src/ui/action-params.zod.ts) then refuses every key outsideACTION_PARAM_BUILTIN_KEYS(recordId,objectName,_selectedIds) asunknown_field.Invalid action params: …string every D2 refusal returns. The REST door answers it withdeps.error(paramError, 400)(packages/runtime/src/domains/actions.ts).OS_ALLOW_LAX_ACTION_PARAMS=1(laxActionParams()) warns once and accepts.packages/runtime/src/action-params-enforcement.test.ts: both spellings of "declares none" (absent and[]) refuse and name the key; the empty bag and the built-ins are accepted; the hatch warns and accepts.packages/runtime/src/action-declarative-update.test.ts, describe#24268: REST answers 400VALIDATION_ERRORwith zero updates, for an undeclared key and for apatchkey sent as a param; MCP refuses the same way; an empty bag writes the patch at both doors; a declared param wins overpatch; the handler controlnudge_casereaches the registry.isDeclarativeUpdateActionisaction?.operation === 'update'and nothing else. So "a handler action keeps the pass-through" covers every param-less action that is not a declarative update: a script handler, an inlinebody, aflowtarget, and an action with no declaration at all. The note names the first three.invokeBusinessActionthrows every D2 refusal at that door asObject.assign(new Error(paramError), { code: standardErrorCodeForHttpStatus(400), status: 400 }). The runtime bridge'srunAction(packages/runtime/src/domains/mcp.ts) returns that call with no catch of its own. Pinned inaction-declarative-update.test.ts, "MCPrun_action: the same undeclared key is refused withcode+status, before any write":err.codeisVALIDATION_ERRORanderr.statusis 400.run_action(packages/mcp/src/mcp-http-tools.ts) catches witherrorResultFromThrown. That renders any thrown stringcodethroughthrownRefusalEnvelopeas{ error: { code, message, status } }withisError: true. Pinned inpackages/mcp/src/mcp-coded-refusal.test.ts, therun_actionrow over HTTP/mcp, where a fake bridge throwsRECORD_NOT_FOUND.{"error":{"code":"VALIDATION_ERROR",…,"status":400}}). The note states the clause as the two pinned halves and cites both pins. It does not claim an end-to-end pin.Local gates, at head
485dc4362node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackran with no paths. Its change set, taken from merge base71fa166e9, is this one file with 61 changed lines. It derived 19 commands, the same set as in round 0. All 19 exited 0 on the patch-round-1 head, re-run after a container restart:node scripts/check-adr-links.mjs→ exit 0node scripts/check-adr-links.mjs --self-test→ exit 0node scripts/check-adr-symbol-anchors.mjs→ exit 0node scripts/check-adr-symbol-anchors.mjs --self-test→ exit 0node scripts/check-ci-filter-parity.mjs→ exit 0node scripts/check-closing-keyword-parity.mjs→ exit 0node scripts/check-closing-keyword-parity.mjs --self-test→ exit 0node scripts/check-comment-mask-corpus.mjs→ exit 0pnpm --filter @objectstack/lint run check:doc-formula-expressions→ exit 0pnpm check:adr-anchors→ exit 0pnpm check:cross-package-test-inputs→ exit 0pnpm check:doc-authoring→ exit 0pnpm check:driver-memory-census→ exit 0pnpm check:gitlink-declared→ exit 0pnpm check:nul-bytes→ exit 0pnpm check:pm-governed-merges→ exit 0pnpm check:pm-prior-rulings→ exit 0pnpm check:refd-timer-probe→ exit 0pnpm check:watch-hint-literal→ exit 0dispatch-gates --ran:19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN.Verdict lines:
✅ check-adr-symbol-anchors: 2368 anchors across 142 records resolve … 0 line anchors survive …;✅ check-adr-links: 770 relative link destination(s) under docs/adr/ resolve;check-adr-anchors: OK (75 anchored file(s), …);check-nul-bytes: OK (…);✓ doc authoring guard: 423 files clean.scripts/symbol-anchors.mjs#extractAnchorsreads the 11 new anchors (7 symbol, 4 file-level) out of the edited file, so the anchor gate's green above judged them.In patch round 1,
turbo run build --filter=@objectstack/formula --filter=@objectstack/lintran under the verify lock first (4 of 4 cached,VERDICT command-exit 0), andcheck:doc-formula-expressionsthen exited 0. In round 0, on the pre-merge head4ca7af7f9,check:doc-formula-expressionsfirst exited 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintwere not built). It exited 0 afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintran under the verify lock. That first exit is not a finding.No package is touched, so no build closure, package test or typecheck is owed. The repo-wide scans are CI's.
Acceptance notes
400 VALIDATION_FAILEDper-field envelope". A D2 refusal is one400 VALIDATION_ERRORnaming every offending param, with nofields[], at both doors. That sentence predates this card and stays verbatim (Prime Directive [WIP] Add Chinese version of the documentation #13); the note and the status-line entry state what a D2 refusal is onmain.recordId,objectNameand_selectedIdsstay allowlisted by the D2 validator and still reachdeclarativeUpdateWrite's bag (PR fix(runtime)!: a param-less declarative update refuses undeclared wire params at both action doors #24392's acceptance note 3). The note says every key "except the dispatcher built-ins" is refused. It does not describe what the write does with them.Check Changesetreads red withoutskip-changeset. The diff publishes nothing (docs/adr/**only).维护者速读(草稿)
改了什么:只改了一个 ADR 文件(ADR-0104),一共加了三处。D2 第 1 条下面加了一段带日期的说明。状态行加了一条 Amended 记录。2026-07-30 附录里重复这条边界的那句话后面,加了一行指向这段说明的指引。第 1 轮修订在这段说明里又补了一处更正:D2 第 1 条原文说拒收时返回"
400 VALIDATION_FAILED逐字段信封",但实际上 REST 和 MCP 两个入口返回的都是400 VALIDATION_ERROR,一条消息里点出每个出错的参数,没有fields[]。VALIDATION_FAILED是记录级校验用的错误码,带fields[],D2 从来不会返回这种形状。状态行也写上了这条更正。原文一句都没删、没改。为什么改:ADR-0104 D2 原来写的是"没有声明 params 的动作保持放行"。写这句话的时候,每个动作都有自己的处理代码,由作者自己决定怎么处理传进来的参数。后来有了声明式的
operation: 'update'动作。它没有处理代码,传进来的参数会直接写进记录。PR #24392 已经让这种动作在参数列表为空或缺省时,拒收所有没声明的参数。代码已经变了,ADR 还是旧说法。以后改动作入口的人都会拿 ADR 当依据,照旧说法判断就会判错。风险与代价(含回滚):只改文档,不改任何代码,也不改任何行为。主要风险是措辞不准,所以每一句都对照了
origin/main上的代码和测试。有一处要说明:MCP 入口的返回格式由两个测试分别守住,一个守运行时抛出的错误,一个守 MCP 的渲染。没有一个端到端的测试把两半连起来,所以说明里按两半来写,没有说成一个端到端的保证。回滚:revert 这一个提交即可。席位意见:
你要做的:读一遍 D2 第 1 条下面那段说明。同意就批准这个 PR。这是 Tier H 文件,要由你批准后才能合并。
Generated by Claude Code