Skip to content

docs(adr): ADR-0104 D2 still says every action that declares no params keeps the pass-through — since #24268 a declarative operation: 'update' action's empty list is an empty accept set (Tier H, the maintainer's hand) #24393

Closed

Description

@objectstack-fleet

Blocked-by: #24268

Filing gate ①: a named location whose sentence the landing work makes untrue. This is a follow-up owed by PR #24392 (#24268, in review). reach: is the record itself, the decision every action-door change is read against (Prime Directive #13).

Filed by domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T, from the dev's open_questions[0] in report 6254612136 on #24268. Checked on origin/main cf16a0d18. ⛔ Not a claim. Lane, type and grade are triage's.

What the record says, and what #24268 changes

docs/adr/0104-field-runtime-value-shape-contract.md states D2's boundary twice:

  • Lines 195–196: "Actions declaring no params keep today's pass-through (there is nothing to check against), so existing param-less actions are untouched."
  • Lines 941–942: "Actions declaring no params keep their pass-through — that boundary is unchanged from D2's landing."

Both were written before the declarative operation: 'update' action existed (#14092, executed by #15079). Such an action has no handler. Its bag is merged over patch and written to the row, so the reason the pass-through gives ("there is nothing to check against") does not hold for it.

PR #24392 makes enforceActionParams (packages/runtime/src/action-execution.ts) judge a param-less declarative update's bag against an empty declared list. Every undeclared wire key is refused through the existing D2 path: the same 400, the same OS_ALLOW_LAX_ACTION_PARAMS hatch. Handler actions keep the pass-through. The published liveness row liveness/action.json /props/patch already says the wire cannot widen the bag ("an undeclared param is refused by ADR-0104 D2").

What the record lacks

A dated addendum (or an amended status line) of about one paragraph:

Why it waits on #24268

The record is written once PR #24392 has landed, so it never states an enforcement the code does not yet hold. The precedent is #23483, the ADR-0069 note that waited on its seams.

Reader who acts

This is one Tier H edit to docs/adr/ alone, with a PR of its own. It is a governed surface, landed only on the maintainer's approval (precedents #23483 and #23006). The seat that lands #24268 can carry it once the blocker closes, if triage routes it there.

Dedupe


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · documentation · domain:spec · pm:blocked on #24268 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T20:54Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked: #24268 landed as d79dc4883c · pm:blocked → pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-18T22:05Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-19T00:36Z · ADR-0104 D2: a dated note that scopes the param-less pass-through to actions with a handler; an operation: 'update' action's empty or absent params list is an empty accept set (PR #24392, d79dc4883c) (Tier H: the maintainer merges)
    Session: session_019oFY6Hi35J7atvgVFnmoaK (the seat; the dev is an os-dev subagent of this session)
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24393-adr-0104-d2-declarative-update
    Worktree: objectstack-issue-24393
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface (paths on origin/main ee882b845 or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24393,
      "status": "done",
      "branch": "claude/issue-24393-adr-0104-d2-declarative-update",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24416",
      "session": "session_019oFY6Hi35J7atvgVFnmoaK — subagent, so the parent seat's id; the relay resolved the same id from the container's CLAUDE_CODE_REMOTE_SESSION_ID",
      "premise_still_valid": true,
      "summary": "ADR-0104 now carries a dated note under D2 item 1, a status-line entry and a one-line pointer at the 2026-07-30 addendum's restatement. Each says what origin/main holds after d79dc4883c: the param-less pass-through is scoped to actions with a handler; a declarative operation 'update' action's empty or absent params list is an empty accept set, refused through D2's path (same 400, same OS_ALLOW_LAX_ACTION_PARAMS hatch); and at the MCP run_action door a D2 refusal carries code + status, the ADR-0112 envelope, as at the REST door. P1-P3 were all measured true on ee882b845. Two precisions are recorded in the PR body: the pass-through discriminator is operation alone, so 'handler' covers script handlers, inline bodies, flow targets and undeclared actions; and P3 is held by two pins that meet at one seam (the runtime throw in action-declarative-update.test.ts, run_action's renderer in mcp-coded-refusal.test.ts), with no committed end-to-end pin, so the note cites both halves rather than claiming one. Placement (G2): one note at D2 item 1 plus a pointer at the second sentence; neither original sentence is rewritten. files_changed: docs/adr/0104-field-runtime-value-shape-contract.md (+41/-1). deviations: none. Notes: (1) no labels were written, because the dispatch named none (the definition's 'forbidden or empty set: zero writes and report' branch). Check Changeset will read red until the seat applies skip-changeset; the diff publishes nothing. (2) The harness asked for a model-named Co-Authored-By trailer and a different PR footer; the commits carry AGENTS.md's model-free pair and the body carries AGENTS.md's session-URL footer. (3) Acceptance note 1 records that D2 item 1 spells the code VALIDATION_FAILED while both doors answer VALIDATION_ERROR. It is documentation drift with no carrier, so it is left unedited and not filed.",
      "tests": "No package touched (docs/adr only), so no build closure, no package test or typecheck, and no ablation is owed (docs are not ablated). test:repo not run locally: the diff edits none of its tests, so it is declared to CI. Union run after the final commit, at head 5823853f3 (git rev-parse --short HEAD after the merge of origin/main 97cb5c5a8): 19 of 19 derived commands exit 0. One build ran under the verify lock to satisfy a gate prerequisite: OS_VERIFY_LOCK_SLOT=issue-24393-dev os-verify-lock.sh -c 'pnpm exec turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint' gave 'Tasks: 4 successful, 4 total' and 'VERDICT command-exit 0', held 102s, waited 0s. Anchor proof: scripts/symbol-anchors.mjs extractAnchors reads all 8 new anchors (5 symbol, 3 file-level) out of the edited file, so check-adr-symbol-anchors' green judged them.",
      "gates": "Local subset, each exit code captured before any pipe, at 5823853f3: node scripts/check-adr-links.mjs 0 ('✅ check-adr-links: 770 relative link destination(s) under docs/adr/ resolve'); --self-test 0; node scripts/check-adr-symbol-anchors.mjs 0 ('✅ check-adr-symbol-anchors: 2365 anchors across 142 records resolve ... 0 line anchors survive'); --self-test 0; check-ci-filter-parity 0; check-closing-keyword-parity 0 and --self-test 0; check-comment-mask-corpus 0; pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 (on pre-merge head 4ca7af7f9 it first exited 3, PREREQUISITE NOT MET, unbuilt formula and lint; 0 after the locked build); check:adr-anchors 0 ('check-adr-anchors: OK (75 anchored file(s) ...)'); check:cross-package-test-inputs 0; check:doc-authoring 0; check:driver-memory-census 0; check:gitlink-declared 0; check:nul-bytes 0; check:pm-governed-merges 0 (self-test only); check:pm-prior-rulings 0 (self-test only); check:refd-timer-probe 0; check:watch-hint-literal 0. Derivation: dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, change set from merge base ee882b845 = 1 path, 42 changed lines, 19 commands, the same set before and after the merge. Reconciliation: 'Run reconciliation — 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ dispatch-gates --ran: 19 derived famil(ies) accounted for'. Control-byte self-scan of the ADR and PR body: zero hits. PR check-runs on 5823853f3 at report time, read once and not polled (22 runs): 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; in_progress: Governed Surface Queue Guard, Type Check · workspace / debt ledger / source gates / consumer gates, Lint · ESLint / PM dispatch-gates / docs & code guards / repo & tooling gates / CI, release & engine gates / scoped · tooling self-tests / scoped · query-options erasure / scoped · slot lookup & corpus, Check Changeset (expected red: no skip-changeset label, see summary), Check PR Size, Check Documentation Links, filter, and the claim-path, card-claims-branch and Part-of guards. Test Core and Build Core were not yet listed.",
      "mcp_calls": "0 — none",
      "api_writes": "3 endpoint writes in 2 fleet-write relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches from this container, executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24416/assignees (zhuangjianguo), run 39183166492, read-back 10670 bytes sent = stored; (2) comment = POST /repos/objectstack-ai/objectstack/issues/24393/comments (this report). git push x3 of the branch is not a REST write. Reads were single-card REST GETs.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24393,
      "status": "done",
      "branch": "claude/issue-24393-adr-0104-d2-declarative-update",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24416",
      "session": "session_019oFY6Hi35J7atvgVFnmoaK — subagent, so the parent seat's id",
      "premise_still_valid": true,
      "summary": "Patch round 1 (REWORK 6257559771, from contract review 6257540342 ③ 2). The dated note under D2 item 1 now says what a D2 refusal is on main, at both doors and for every action: one 400 VALIDATION_ERROR whose message (Invalid action params: …) joins one issue per offending param, each naming it, with no fields[]. The REST door passes no code, so buildApiError derives it through standardErrorCodeForHttpStatus(400), and the MCP throw stamps that function's answer. The note also says that item 1's 'standard 400 VALIDATION_FAILED per-field envelope' names the wrong code and the wrong shape: VALIDATION_FAILED is the ADR-0112 ledger's record-level fields[] member, which the D2 gate never builds. The pin parenthetical names the four pinned assertions, and the closing line now reads 'No other decision in D2 changes: the code and shape above correct the record, they add no rule.' The original sentence stays verbatim. Every cited line was re-measured on the merged tree 485dc4362: action-declarative-update.test.ts:326 (REST, the declared-params triage_case), :359 and :376 (REST, the param-less close_case), :390-:391 (the MCP throw's code and status); action-execution.ts:1689-:1691 and :2542; domains/actions.ts:678; error-envelope.ts:120; errors.zod.ts:184; error-code-ledger.zod.ts:302. files_changed: docs/adr/0104-field-runtime-value-shape-contract.md (this round +24/-5 in fce8981f8; +60/-1 against merge base 71fa166e9). deviations: (1) the status-line entry stated no code, so the instruction did not require a change there. I added one sentence anyway ('A D2 refusal is one 400 VALIDATION_ERROR naming every offending param, with no fields[], at both doors, which corrects item 1's \"400 VALIDATION_FAILED per-field envelope\"'), so the status line announces the correction; dropping that sentence reverts it. (2) A container restart killed the first gate run (exit 137); all 19 gates were re-run on 485dc4362 after the restart, with check-comment-mask-corpus in its own call and the same command text in ran.list. pr_body_edits: 10 pairs. Each old_str occurs exactly once in the live body, which was re-read over REST at 485dc4362 and is byte-identical to the round-0 body (10670 bytes). Applied in order, they give a 12828-byte body: no less-than character, the only closing keyword is line 1's, and no control bytes. The body was not PATCHed.",
      "tests": "No package touched (docs/adr only), so no build closure, no package test or typecheck, and no ablation is owed (docs are not ablated). test:repo is declared to CI. Gates were re-run on the final head 485dc4362 (git rev-parse --short HEAD after the merge of origin/main 71fa166e9), after the container restart: 19 of 19 exit 0. The gate prerequisite build ran under the verify lock: OS_VERIFY_LOCK_SLOT=issue-24393-dev os-verify-lock.sh -c 'pnpm exec turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint' gave 'Tasks: 4 successful, 4 total', 'Cached: 4 cached, 4 total' and 'VERDICT command-exit 0'. Anchor proof: scripts/symbol-anchors.mjs extractAnchors reads 11 anchors out of the note (7 symbol, 4 file-level; new this round: error-envelope.ts#buildApiError, errors.zod.ts#standardErrorCodeForHttpStatus, packages/types/src/validation-failure.ts). check-adr-symbol-anchors' total moved 2365 to 2368.",
      "gates": "Local, at 485dc4362, each exit code captured before any pipe: check-adr-links 0 ('✅ check-adr-links: 770 relative link destination(s) under docs/adr/ resolve') and --self-test 0; check-adr-symbol-anchors 0 ('✅ check-adr-symbol-anchors: 2368 anchors across 142 records resolve … 0 line anchors survive') and --self-test 0; check:adr-anchors 0 ('check-adr-anchors: OK (75 anchored file(s) …)'); check:doc-authoring 0 ('✓ doc authoring guard: 423 files clean'); check-ci-filter-parity 0; check-closing-keyword-parity 0 and --self-test 0; check-comment-mask-corpus 0 ('9516 files, 0 disagree'); check:doc-formula-expressions 0; check:cross-package-test-inputs 0; check:driver-memory-census 0; check:gitlink-declared 0; check:nul-bytes 0; check:pm-governed-merges 0 (self-test only); check:pm-prior-rulings 0 (self-test only); check:refd-timer-probe 0; check:watch-hint-literal 0. Derivation: dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, at commit 485dc4362; change set 1 path vs merge base 71fa166e9, 61 changed lines; 19 commands, the same set as round 0. Reconciliation: 'Run reconciliation — 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ dispatch-gates --ran: 19 derived famil(ies) accounted for'. CI on 485dc4362, read once at 2026-10-19T01:27:07Z (36 runs): 9 success (Auto Label, Dogfood Regression Gate, filter, the card-claims-branch, Part-of, single-writer-path and same-issue guards, Check PR Size, Check Documentation Links); 8 skipped (Build Core, Check Changeset (skip-changeset label present), Dogfood Verify CLI, Dogfood Regression Gate shards, Build Docs, Temporal Conformance, Console Pin Gate, Packed-tarball smoke); 19 in_progress (Test Core 1-6/6, Type Check · workspace / debt ledger / source gates / consumer gates, every Lint · job including docs & code guards and ESLint, Governed Surface Queue Guard). None failed.",
      "mcp_calls": "0 — none",
      "api_writes": "1 endpoint write this round, in 1 fleet-write relay stroke (POST /repos/objectstack-ai/objectstack/dispatches from this container, executed as objectstack-fleet[bot]): comment = POST /repos/objectstack-ai/objectstack/issues/24393/comments (this report). Plus 1 git push of the branch (5823853f3..485dc4362), which is not a REST write. The PR body was not written; reads were single-resource REST GETs.",
      "open_questions": [],
      "out_of_scope_findings": [],
      "pr_body_edits": [
        {
          "old_str": "Head: `5823853f3`.",
          "new_str": "Patch round 1 (REWORK `6257559771`, from contract review `6257540342` ③ 2) added the correction commit `fce8981f8`, then merged `origin/main` `71fa166e9` (PR #24413, which touches no file the note cites). Head: `485dc4362`."
        },
        {
          "old_str": "One file, `docs/adr/0104-field-runtime-value-shape-contract.md`, +41 / −1.",
          "new_str": "One file, `docs/adr/0104-field-runtime-value-shape-contract.md`, +60 / −1."
        },
        {
          "old_str": "**Placement (G2).**",
          "new_str": "4. **Patch round 1: what a D2 refusal is on `main`.** D2 item 1's sentence just above the note says a refusal is \"the standard `400 VALIDATION_FAILED` per-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: one `400 VALIDATION_ERROR` whose message (`Invalid action params: …`) joins one issue per offending param, each naming it, with no `fields[]`. The REST door's `deps.error(paramError, 400)` passes no code, so `buildApiError` (`packages/runtime/src/error-envelope.ts`) derives it through `standardErrorCodeForHttpStatus(400)`; the MCP throw stamps that function's answer. `VALIDATION_FAILED` is the ADR-0112 ledger's record-level validation member, which carries `fields[]` (`packages/types/src/validation-failure.ts`), and the D2 gate never builds it. Pins, re-measured on `485dc4362`: `packages/runtime/src/action-declarative-update.test.ts:326` (REST, a declared-params action), `:359` and `:376` (REST, the param-less `close_case`), `:390`–`:391` (the MCP throw's `code` and `status`). The status-line entry carries the same correction in one sentence. The original sentence stays verbatim (Prime Directive #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.\"\n\n**Placement (G2).**"
        },
        {
          "old_str": "## Local gates, at head `5823853f3`",
          "new_str": "## Local gates, at head `485dc4362`"
        },
        {
          "old_str": "Its change set, taken from merge base `ee882b845`, is this one file with 42 changed lines. It derived 19 commands, the same set before and after the merge. All 19 exited 0 on the final head:",
          "new_str": "Its change set, taken from merge base `71fa166e9`, 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:"
        },
        {
          "old_str": "`✅ check-adr-symbol-anchors: 2365 anchors across 142 records resolve … 0 line anchors survive …`",
          "new_str": "`✅ check-adr-symbol-anchors: 2368 anchors across 142 records resolve … 0 line anchors survive …`"
        },
        {
          "old_str": "reads the 8 new anchors (5 symbol, 3 file-level) out of the edited file",
          "new_str": "reads the 11 new anchors (7 symbol, 4 file-level) out of the edited file"
        },
        {
          "old_str": "- On the pre-merge head `4ca7af7f9`, `check:doc-formula-expressions` first exited 3",
          "new_str": "- In patch round 1, `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` ran under the verify lock first (4 of 4 cached, `VERDICT command-exit 0`), and `check:doc-formula-expressions` then exited 0. In round 0, on the pre-merge head `4ca7af7f9`, `check:doc-formula-expressions` first exited 3"
        },
        {
          "old_str": "1. **A code spelling D2 item 1 already gets wrong.** It says failures return \"the standard `400 VALIDATION_FAILED` per-field envelope\". The code both doors answer for a D2 refusal is `VALIDATION_ERROR`: `HttpStatusErrorCodeMap[400]`, which `standardErrorCodeForHttpStatus(400)` returns, as pinned in `action-declarative-update.test.ts`. That sentence predates this card and is not edited here. The note does not spell the code, so it adds no second, contradicting literal. Observation only (documentation drift), no carrier.",
          "new_str": "1. **A code spelling D2 item 1 gets wrong, now corrected by the note (patch round 1, What changed item 4).** It says failures return \"the standard `400 VALIDATION_FAILED` per-field envelope\". A D2 refusal is one `400 VALIDATION_ERROR` naming every offending param, with no `fields[]`, at both doors. That sentence predates this card and stays verbatim (Prime Directive #13); the note and the status-line entry state what a D2 refusal is on `main`."
        },
        {
          "old_str": "2026-07-30 附录里重复这条边界的那句话后面,加了一行指向这段说明的指引。原文一句都没删、没改。",
          "new_str": "2026-07-30 附录里重复这条边界的那句话后面,加了一行指向这段说明的指引。第 1 轮修订在这段说明里又补了一处更正:D2 第 1 条原文说拒收时返回\"`400 VALIDATION_FAILED` 逐字段信封\",但实际上 REST 和 MCP 两个入口返回的都是 `400 VALIDATION_ERROR`,一条消息里点出每个出错的参数,没有 `fields[]`。`VALIDATION_FAILED` 是记录级校验用的错误码,带 `fields[]`,D2 从来不会返回这种形状。状态行也写上了这条更正。原文一句都没删、没改。"
        }
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24416 comment 6257859051 · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T01:43Z · contract review PASS in two rounds (6257540342, 6257846085) · Tier H: awaiting an approval by os-zhuang or hotlong (both requested), or the maintainer's merge.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #24416 → 7e1ce9d945 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T04:41Z

    probe reading
    PR object approved by os-zhuang 2026-10-19T03:55Z on head 485dc43620. os-zhuang also marked it ready and enqueued it, so the seat's pr_ready + automerge_enable stroke (run 39197098282, note on the PR) found both already done. Merged 2026-10-19T04:40Z out of the merge queue, as merge commit 7e1ce9d945fcc941d781cbc72aade2ee08f360b4 on parent 2be3d7a5e6.
    origin/main git merge-base --is-ancestor 7e1ce9d945 origin/main → exit 0
    merged vs reviewed head git diff 2be3d7a5e6 7e1ce9d945 and the reviewed head 485dc43620 over its merge base 71fa166e96 share git patch-id --stable 8a0725a5. One file, docs/adr/0104-field-runtime-value-shape-contract.md, +60 / −1. Record chain: contract review PASS in two rounds, 6257540342 and 6257846085; seat record (maintainer brief and seat check), 6257859051.
    queue branch no gh-readonly-queue/main/pr-24416-* ref remains
    mis-close scan The body's only closing keyword is Fixes #24393. One other issue closed in the same second, #24306; GitHub names PR #24440 (another lane's) as what closed it, not this PR.

    What now holds on main: ADR-0104 carries a dated note under D2 item 1:

    The ADR's status line records the amendment, and the 2026-07-30 addendum's restatement points to the note.

    Downstream: no open card carries Blocked-by: #24393.

    State: the merge closed this card completed. pm:dispatched is removed in this act. Claim 6257175272 ends here.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions