You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
runtime: the dispatcher's PATCH /data/:object/:id has no request-schema gate, so a non-record body answers 500 with the engine's own error, where REST answers 400 VALIDATION_FAILED #23491
Filing gate ①, class (a): a caller's malformed request is answered as an internal fault, and one route's two transports answer it differently. It was measured at a public door.
reach: HTTP, measured once by #23462's dev (os-dev-report6169083704, out-of-scope finding 1). The setup was a real @objectstack/honocreateHonoApp over the runtime dispatcher (dist built at PR #23490's 19d1b8dfdd).
PATCH /api/v1/data/task/ID with a JSON array, a JSON string or JSON null as the body answered 500 INTERNAL_ERROR, nothing written. The engine's own error came back: "no such column: 0", "Cannot use 'in' operator to search for 'organization_id' in text", "Cannot read properties of null (reading 'id')".
Twin:RestServer's PATCH handler answered 400 VALIDATION_FAILED for an array, string or number. For null it answered 200, patching as {}.
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU, at PR #23490's ACCEPT. ⛔ Not a claim. Lane and grade are triage's.
Reader who acts: triage grades it. The landing is the dispatcher's PATCH branch in packages/runtime/src/domains/data.ts (:163). It is domain:cli by the lane table.
What happens
REST:RestServer's PATCH /data/:object/:id (packages/rest/src/rest-server.ts, about :9031-:9050, read on main0d3ea295d1) validates the assembled UpdateDataRequestSchema request. That gate is 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899's request-side fix ("a non-record body 400s instead of reaching the engine").
Sibling, read in source and unmeasured: the dispatcher's POST /data/:object create branch (data.ts:240) has no CreateDataRequestSchema gate either, while REST's twin does (rest-server.ts:8902).
Governing text
UpdateDataRequestSchema.data is a record (@objectstack/spec/api).
AGENTS.md 〈Route & surface ownership〉: one door's two transports answer one request alike.
Seam: spec:UpdateDataRequestSchema.data (record) → runtime: domains/data.ts PATCH branch (no schema gate) (consumer: every createHonoApp host's /data update).
Acceptance (a proposal; triage's call)
The dispatcher's PATCH validates the same assembled request REST validates. A non-record body is refused 400 VALIDATION_FAILED before the engine is asked.
The null body answers as REST's twin does, or both doors converge on one answer. Measure first, then pick one, ⛔ not two.
Measure the POST create sibling. Fold it in if it has the same gap.
Pins through dispatch(), beside RestServer, for an array, a string, a number and null.
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included: runtime dispatcher PATCH data non-record body array string null answers 500 instead of 400 VALIDATION_FAILED UpdateDataRequestSchema. It returned 3 hits: #23462 (the version-token card, not this), and #4435 and #3398 (closed, not this).
Dedupe words: dispatcher PATCH non-record body 500 VALIDATION_FAILED; UpdateDataRequestSchema data record dispatcher createHonoApp; dispatcher data write body schema gate RestServer parity.
Read fresh. The dispatcher's PATCH /data/:object/:id (packages/runtime/src/domains/data.ts:163) has no request-schema gate. REST's twin validates UpdateDataRequestSchema (#3899).
priority:p2: a malformed body is already refused (nothing is written), but as 500 with the engine's internal error text instead of 400 VALIDATION_FAILED.
Not in the narrowing class: no request that succeeds today starts failing.
REST's null → 200 (patching {}) is the twin's own question; the fixing seat aligns both and states the answer.
Claim: PM loop round 1 (#23491, chain head of a family fold with #23517: both PATCH /data/:object/:id doors check UpdateDataRequestSchema against the request as the caller sent it) · 2026-10-14T23:40Z
Session: session_01FLwgbTSmGMrqsXkBDsiwfG
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23491-patch-data-request-gate
Worktree: objectstack-issue-23491
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface: packages/runtime/src/domains/data.ts (the PATCH /data/:object/:id branch near :194-:238 on origin/main086fa4640f, its local versionTokenRefusal near :74, and the POST /data/:object create branch only if measurement shows the same gap), packages/rest/src/rest-server.ts (the PATCH /data/:object/:id handler only, near :9128-:9210, and its module-level versionTokenRefusal near :1779), their tests in both packages, and one changeset. Stop on a breach and explain it in the report. ⛔ packages/spec is read only: UpdateDataRequestSchema does not change.
Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier --repo objectstack-ai/objectstack packages/runtime/src/domains/data.ts packages/rest/src/rest-server.ts: "no path-derived mandate … The tier stays the PM's per-card judgment call")
Clause-②: no
Responsibility: the dispatcher's PATCH branch (@objectstack/runtime) and RestServer's PATCH handler (@objectstack/rest), platform code in this lane | none: the dispatcher has no request-schema gate, and RestServer's gate parses a coerced copy (data ?? {}, String(expectedVersion)) | any caller of a /data update on either door; measured by #23462's dev (6169083704, dispatcher: a non-record body answers 500) and #23492's dev (both doors: a numeric expectedVersion is judged, not refused). No caller that relies on either answer was measured.
Thread-read: 6170089295
Serial constraints cleared: none of the 8 open PRs edits data.ts or rest-server.ts, and none of the 16 pm:dispatched claims names either file. The last landings there are PR #23515 (#23492, 1f3ac4f52), the order #23517 named, and PR #23490 (#23462, 650933ec7). rest-server.ts is free in seat 2's serial queue (#22648 §3). #23050's slice 1 landed (PR #23116), and no slice is claimed. Under this seat's same-file HARD SERIAL ruling, #23497 (registerFormEndpoints) and any #23050 slice wait for this PR's merge.
Family fold, five gates (execution-duties.md), answered: FOLD.
The PR body carries one Fixes line per member, with one commit per card. Member #23517 carries its own claim, naming this branch.
Contract face: a JSON null body on RestServer's PATCH answers 200 today (patched as {}). If the fold converges both doors on 400, that narrows what RestServer accepts, back to the declared record type. That is Clause-②: no. Under references/execution-duties.md 〈派发〉 and dispatch-gates' Clause-② line, it owes one CONTRACT_REVIEW_TIER review before the queue. The seat runs that review after ACCEPT, in an isolated agent.
os-dev-report
{
"issue": 23491,
"status": "done",
"branch": "claude/issue-23491-patch-data-request-gate",
"pr": "#23571",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the dispatching seat session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Family fold #23491 (chain head) + #23517 (member) in one draft PR, #23571, with one commit per card: 90424dd is #23491 and 3dd76b2 is #23517 (plus the one changeset). #23491: the runtime dispatcher PATCH /data/:object/:id now parses the assembled UpdateDataRequestSchema request (the patch as sent, token stripped, nothing defaulted) before callData and returns 400 VALIDATION_FAILED with error.details.fields. RestServer PATCH dropped data ?? {} from the gate input and the forward. H4 measured the dispatcher POST create with the same gap and a worse one (an array created one row per element, 201; a number created an empty row, 201), so it is folded in with CreateDataRequestSchema. NULL ANSWER PICKED: 400 on both PATCH doors. Measured at 086fa46: RestServer answered null 200, called updateData with {}, and moved updated_at (…26.716Z to …26.723Z) with no field changed; the dispatcher answered 500. After the fix, both doors answer 400 with fields [data] and the version is unchanged. This narrows RestServer (200 to 400), and the changeset and PR body name it. A null CREATE is left as both doors answer it today (201, an empty row): RestServer create is outside the surface, and the dispatcher gate mirrors its assembled request statement for statement, including body ?? {}. That is the one ?? default this diff adds, deliberately, so the doors do not split; see open question 1. #23517: versionTokenRefusal, both copies (function bodies diff-identical), drops the finite-number arm, so a body expectedVersion number (5, 0) is refused 400 VALIDATION_FAILED before the protocol, beside a stale or the current If-Match, on both doors (was 409, forwarded as the strings 5 and 0). Both PATCH doors gate and forward the raw token. DELETE ?expectedVersion and If-Match are strings by transport and are unchanged. PR BODY ERRATUM (the dev does not PATCH; the seat writes it): the last Acceptance-notes bullet is wrong. Replace it with: "Check order differs on an object whose API exposure denies the update: RestServer runs enforceApiAccess before its request gate (403), while the dispatcher runs its gate before callData, whose exposure check is inside (400). Before this PR the dispatcher answered that exposure refusal too, so this ordering is new on the dispatcher. Read in source, not measured."",
"tests": "All on 3dd76b2, exit codes captured before any pipe. @objectstack/rest vitest --project local: 284 files, 5856 passed, 326 skipped, REST_TEST_EXIT=0. @objectstack/runtime vitest --project local: 382 files, 5673 passed, 19 skipped, RUNTIME_TEST_EXIT=0. Targeted (JSON reporter): data-write-request-gate.test.ts 26/26 (new), data-update-expected-version.test.ts 65/65, data-delete-expected-version.test.ts 34/34, rest request-schema-gate.conformance.test.ts 16/16. Typecheck: rest exit 0, runtime exit 0 (test layer compiles; debt ledger unchanged). Flipped pins (repo-wide grep found 2 numeric-token hits, both in data-update-expected-version.test.ts, and no null-PATCH-200 pin): the body-token-0 control now asserts 400 with updateData never asked beside the CURRENT If-Match, and EDGE_SHAPES number-token row moved into the new [#23517] block. ABLATIONS via scripts/ablation-replace.mjs, restore proven blob==HEAD each time. These suites resolve data.ts and @objectstack/rest from src (runtime vitest alias), so no dist leg. (1) dispatcher PATCH gate line deleted: 8/26 red. (2) RestServer gate input back to data ?? {}: 2/26 red plus 1/16 red in the conformance file. (3a) dispatcher number arm restored alone: 4/65 red, ONLY the parity-sentence rows. The raw-token schema gate still refused the number 400, so the per-door rows stayed green: the second guard holding, reported as observed. (3b) number arm plus gate input back to String(expectedVersion): 9/65 red (4 dispatcher rows, 4 parity rows, the flipped 0 control). (4) dispatcher create gate deleted: 6/26 red. Before/after door measurements (probe, never committed): see the PR body table.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 3dd76b2 (no paths, after the last commit): 65 lines. Each was run with its exit code recorded before any pipe: 64 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs every package dist): NOT MEASURED locally, CI check-run is the reading. --ran with the exit-coded record: 65 derived, 64 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. Lane blind spot: eslint.js packages/rest/src packages/runtime/src --no-inline-config, exit 0, no findings; repo-wide pnpm lint is left to CI. PR check-runs on 3dd76b2, read once at open: 37 runs, 12 success, 6 skipped, 19 in_progress. Governed Surface Queue Guard success; Lint · PM dispatch-gates success; Check PR Size success; The card this PR closes must claim this branch success; No other open PR may claim the same issue success. Build Core, Test Core 1-6/6, Check Changeset, Lint · ESLint, Lint · repo and tooling gates, Lint · docs and code guards, Lint · CI release and engine gates, Type Check · workspace, source gates, consumer gates and debt ledger: in_progress (not waited on).",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23571/assignees (os-bill), read back identical at 11561 bytes; (2) comment POST /repos//issues/23491/comments (this report); (3) comment POST /repos//issues/23517/comments (the pointer). No label writes: none were named, and a published fix owes a changeset. git push is not counted here: 3 pushes, one of them --force-with-lease=claude/issue-23491-patch-data-request-gate:4c97ac5c0 before the PR existed.",
"line_budget": "n/a",
"files_changed": [
".changeset/23491-patch-data-request-gate.md (new, rest+runtime minor, fix(rest,runtime)!, BREAKING accept-set narrowing, adr-0087 not-required marker, Clause-②: no)",
"packages/runtime/src/domains/data.ts (PATCH gate, POST create gate, writeRequestRefusal helper, versionTokenRefusal number arm removed, raw token)",
"packages/rest/src/rest-server.ts (PATCH handler: raw data and raw token in gate and forward; versionTokenRefusal number arm removed and its docblock; no other region)",
"packages/runtime/src/domains/data-write-request-gate.test.ts (new, 26 tests)",
"packages/runtime/src/domains/data-update-expected-version.test.ts ([#23517] block, 0-control flipped, number row moved)",
"packages/rest/src/request-schema-gate.conformance.test.ts (null joins the PATCH violating bodies)"
],
"deviations": [
"Not re-merged with origin/main before opening (AGENTS §10). The branch is 3 commits behind 252775d. Those commits touch packages/cli, packages/metadata-protocol and packages/spec tests, none of packages/rest or packages/runtime, and git merge-tree is clean. Skipped so the history stays one commit per card; CI runs the merge ref.",
"Commit 2 was amended once to carry the changeset and re-pushed with --force-with-lease=claude/issue-23491-patch-data-request-gate:4c97ac5c0 before any PR existed. All five unshared-branch conditions held: claude/issue-*, created by this worktree, only this author in origin/main..branch, no PR, lease spelled.",
"PR body erratum in the last Acceptance-notes bullet: the replacement text is in summary. Written once, and not PATCHed by the dev.",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a claude.com PR footer. I used the model-free pair (Co-Authored-By: Claude, Claude-Session) and the AGENTS.md session-URL footer, as the dispatch and AGENTS.md require.",
"One locked run (the rest and runtime suites sequenced in one command) held the verify lock 17m14s, and the lock printed its holder-starvation warning. Later runs were split."
],
"open_questions": [
{
"question": "POST /data/:object with a JSON null body: both doors read it as {} and create an empty row (201), against CreateDataRequestSchema.data (a record). Converge on 400 as the PATCH doors now do?",
"options": [
"A: a follow-up card in this family. RestServer create createInput/createRequest drop req.body ?? {}, and the dispatcher create drops its mirrored body ?? {}, plus null pins on both doors. About 4 lines, one more accept-set narrowing, landing behind this PR under the same-file serial rule.",
"B: widen this PR now to the RestServer create handler. That breaches the claim surface (no other region of rest-server.ts) and needs the seat to re-scope.",
"C: keep both doors lenient. This leaves a declared contract unenforced on both doors, plus a ?? default in the consumer."
],
"recommendation": "A, because the contract (record) and Prime Directive 12 argue for 400, while the serial rule holds the REST create region. One small follow-up converges both doors at once, which is the only way to retire it without splitting one route into two answers."
},
{
"question": "Clause-② arm. The PR body carries the bare line Clause-②: no, as the dispatch ordered, and the changeset declares the break through the summary bang, the BREAKING banner and the adr-0087 marker. The direct sibling #23492 (same class: a 200 or judged token becoming 400) wrote Clause-②: no (narrowing). Should this PR carry the arm too?",
"options": [
"A: keep it bare, as dispatched.",
"B: add (narrowing) to the PR body line (a seat write) and to the changeset line (one-line dev or seat edit)."
],
"recommendation": "B, because the arm is the closed-token carrier for exactly this measured narrowing (RestServer PATCH null 200 to 400, and dispatcher create array or number 201 to 400), and it matches the #23492 precedent. It changes no gate verdict: the changeset is already read as breaking."
}
],
"out_of_scope_findings": [
"class: b · reach: measured through HttpDispatcher.dispatch() with the createHonoApp catch-all body parse, over a real better-sqlite3 store, ObjectQL and protocol (this PR rig), at 3dd76b2 · evidence: POST /data/task/query with { limit: "ten" } and with the retired { joins: [] } answers 200 with the full record set on the dispatcher, while RestServer answers 400 VALIDATION_FAILED (fields query.limit; the joins tombstone prescription). The string or array body answers the engine INVALID_FIELD 400 on the dispatcher versus VALIDATION_FAILED on REST. Seam: spec:FindDataRequestSchema / QuerySchema (retiredKey joins) → runtime: domains/data.ts POST /data/:object/query (no request-schema gate) · same family as this fold (dispatcher /data request-schema gates): fold into the family closure card rather than a single-point card · dedupe words: dispatcher data query request schema gate; POST data object query limit string 200 dispatcher; retired joins tombstone dispatcher query",
"class: b · reach: measured on both doors (this rig) at 086fa46 and 3dd76b2 · evidence: POST /data/:object with a JSON null body answers 201 and creates an empty row on RestServer (data: req.body ?? {}) and on the dispatcher (whose new gate mirrors it), where CreateDataRequestSchema.data declares a record. Seam: spec:CreateDataRequestSchema.data (record) → rest: rest-server.ts POST /data/:object createInput/createRequest + runtime: domains/data.ts POST create createInput · same family; carrier: open question 1 (seat) · dedupe words: create data null body empty row 201; CreateDataRequestSchema data null coerced; POST data object null body",
"carrier: none (noted in the PR Acceptance notes, not filed) · on an object whose API exposure denies update or create, a non-record body now answers 400 on the dispatcher (request gate before callData exposure check) and 403 on RestServer (enforceApiAccess first). Read in source, not measured."
]
}
ACCEPT → PR #23571 comment 6181996238 (seat record ## 席位核对, head 3dd76b2a81, the family fold #23491 + #23517). It queues after one CONTRACT_REVIEW_TIER PASS and a green head. The family remainder is filed as #23574. domain:cli seat 1 · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T01:05Z.
Landed: PR #23571 → cac0be8cac (the family fold #23491 + #23517). Both cards are closed completed, and pm:dispatched is stripped from both
domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T02:45Z · holder of claims 6180923601 (#23491) and 6180931308 (#23517), released by this landing.
reading
value
queue
added_to_merge_queue 02:14Z
origin/main
cac0be8cac, one parent (a queue squash), an ancestor of origin/main
cards
#23491 and #23517 closed completed by the PR's two Fixes lines; pm:dispatched removed from each by targeted delete
Review chain: ACCEPT 6181996238. The CONTRACT_REVIEW_TIER record PASS on 3dd76b2a81 (6182801438, an independent agent; the served model was checked against its transcript) was adopted in 6182829379. Every check on the head was success or a selector skip.
What landed: both /data write doors validate the request as the caller sent it.
A non-record PATCH or POST body is refused 400 VALIDATION_FAILED on the dispatcher, before the engine is asked.
A JSON nullPATCH body is refused 400 on both doors. It was 200 on RestServer, with updated_at moved.
A numeric body expectedVersion is refused 400 on both doors. It used to be judged 409.
Activity
objectstack-fleet commentedon Oct 14, 2026
Triage: first grade ·
bug·priority:p2·domain:cli·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T13:57ZRead fresh. The dispatcher's
PATCH /data/:object/:id(packages/runtime/src/domains/data.ts:163) has no request-schema gate. REST's twin validatesUpdateDataRequestSchema(#3899).priority:p2: a malformed body is already refused (nothing is written), but as500with the engine's internal error text instead of400 VALIDATION_FAILED.null→200(patching{}) is the twin's own question; the fixing seat aligns both and states the answer.domain:cli, with runtime: the dispatcher'sPATCH /data/:object/:iddropsIf-Match(a stale update is applied) and refuses a bodyexpectedVersionas an unknown field, where REST refuses the stale update 409 #23462 (same branch).findingleaves on grading.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 (#23491, chain head of a family fold with #23517: both
PATCH /data/:object/:iddoors checkUpdateDataRequestSchemaagainst the request as the caller sent it) · 2026-10-14T23:40ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23491-patch-data-request-gateWorktree:
objectstack-issue-23491Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface:
packages/runtime/src/domains/data.ts(thePATCH /data/:object/:idbranch near:194-:238onorigin/main086fa4640f, its localversionTokenRefusalnear:74, and thePOST /data/:objectcreate branch only if measurement shows the same gap),packages/rest/src/rest-server.ts(thePATCH /data/:object/:idhandler only, near:9128-:9210, and its module-levelversionTokenRefusalnear:1779), their tests in both packages, and one changeset. Stop on a breach and explain it in the report. ⛔packages/specis read only:UpdateDataRequestSchemadoes not change.Container & model:
M,mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier --repo objectstack-ai/objectstack packages/runtime/src/domains/data.ts packages/rest/src/rest-server.ts: "no path-derived mandate … The tier stays the PM's per-card judgment call")Clause-②: no
Responsibility: the dispatcher's
PATCHbranch (@objectstack/runtime) andRestServer'sPATCHhandler (@objectstack/rest), platform code in this lane | none: the dispatcher has no request-schema gate, andRestServer's gate parses a coerced copy (data ?? {},String(expectedVersion)) | any caller of a/dataupdate on either door; measured by #23462's dev (6169083704, dispatcher: a non-record body answers500) and #23492's dev (both doors: a numericexpectedVersionis judged, not refused). No caller that relies on either answer was measured.Thread-read: 6170089295
Serial constraints cleared: none of the 8 open PRs edits
data.tsorrest-server.ts, and none of the 16pm:dispatchedclaims names either file. The last landings there are PR #23515 (#23492,1f3ac4f52), the order #23517 named, and PR #23490 (#23462,650933ec7).rest-server.tsis free in seat 2's serial queue (#22648 §3). #23050's slice 1 landed (PR #23116), and no slice is claimed. Under this seat's same-file HARD SERIAL ruling, #23497 (registerFormEndpoints) and any #23050 slice wait for this PR's merge.Family fold, five gates (
execution-duties.md), answered: FOLD.PATCHdoor does not enforceUpdateDataRequestSchemaon the request as the caller sent it. The dispatcher has no gate at all (runtime: the dispatcher'sPATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491: a non-record body answers500).RestServergates a coerced copy:String(expectedVersion)(rest, runtime: a numeric bodyexpectedVersiononPATCH /data/:object/:idis stringified and judged, thoughUpdateDataRequestSchema.expectedVersiondeclares a string #23517: a numeric token is judged409, not refused) anddata ?? {}(runtime: the dispatcher'sPATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491'snulltwin:200). The fix is one gate over the raw assembled request, run before any coercion, on both doors.PATCHbranch ofdata.tsand thePATCHhandler ofrest-server.ts. One worktree, one changeset, one queue slot.PATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491 (6170089295) and rest, runtime: a numeric bodyexpectedVersiononPATCH /data/:object/:idis stringified and judged, thoughUpdateDataRequestSchema.expectedVersiondeclares a string #23517 (6173773327). Neither is in the decision box.PATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491 is checked by an array, a string, a number andnullbody throughdispatch()besideRestServer. rest, runtime: a numeric bodyexpectedVersiononPATCH /data/:object/:idis stringified and judged, thoughUpdateDataRequestSchema.expectedVersiondeclares a string #23517 is checked byexpectedVersion: 5and0on both doors, beside a stale and a currentIf-Match.409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531 (the Hono409 CONCURRENT_UPDATEbody, a different producer). rest(public forms):GET /forms/:sluglocalizes the object schema but returns the form's section labels untranslated, so an anonymous zh-CN visitor reads English section titles (the producer behind objectui#12333) #23497 (the same file, a different route and defect; hard-serial behind this). runtime: the dispatcher'sPATCH /data/:object/:iddropsIf-Match(a stale update is applied) and refuses a bodyexpectedVersionas an unknown field, where REST refuses the stale update 409 #23462, rest, runtime: a bodyexpectedVersionof''orfalsehidesIf-MatchonPATCH /data/:object/:id, so an update sent with a staleIf-Matchis applied unguarded, on both doors #23492 and runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265 (closed: the token reads this edits).The PR body carries one
Fixesline per member, with one commit per card. Member #23517 carries its own claim, naming this branch.Contract face: a JSON
nullbody onRestServer'sPATCHanswers200today (patched as{}). If the fold converges both doors on400, that narrows whatRestServeraccepts, back to the declared record type. That isClause-②: no. Underreferences/execution-duties.md〈派发〉 anddispatch-gates' Clause-② line, it owes oneCONTRACT_REVIEW_TIERreview before the queue. The seat runs that review after ACCEPT, in an isolated agent.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23491,
"status": "done",
"branch": "claude/issue-23491-patch-data-request-gate",
"pr": "#23571",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the dispatching seat session; this run is its subagent)",
"premise_still_valid": true,
"summary": "Family fold #23491 (chain head) + #23517 (member) in one draft PR, #23571, with one commit per card: 90424dd is #23491 and 3dd76b2 is #23517 (plus the one changeset). #23491: the runtime dispatcher PATCH /data/:object/:id now parses the assembled UpdateDataRequestSchema request (the patch as sent, token stripped, nothing defaulted) before callData and returns 400 VALIDATION_FAILED with error.details.fields. RestServer PATCH dropped data ?? {} from the gate input and the forward. H4 measured the dispatcher POST create with the same gap and a worse one (an array created one row per element, 201; a number created an empty row, 201), so it is folded in with CreateDataRequestSchema. NULL ANSWER PICKED: 400 on both PATCH doors. Measured at 086fa46: RestServer answered null 200, called updateData with {}, and moved updated_at (…26.716Z to …26.723Z) with no field changed; the dispatcher answered 500. After the fix, both doors answer 400 with fields [data] and the version is unchanged. This narrows RestServer (200 to 400), and the changeset and PR body name it. A null CREATE is left as both doors answer it today (201, an empty row): RestServer create is outside the surface, and the dispatcher gate mirrors its assembled request statement for statement, including body ?? {}. That is the one ?? default this diff adds, deliberately, so the doors do not split; see open question 1. #23517: versionTokenRefusal, both copies (function bodies diff-identical), drops the finite-number arm, so a body expectedVersion number (5, 0) is refused 400 VALIDATION_FAILED before the protocol, beside a stale or the current If-Match, on both doors (was 409, forwarded as the strings 5 and 0). Both PATCH doors gate and forward the raw token. DELETE ?expectedVersion and If-Match are strings by transport and are unchanged. PR BODY ERRATUM (the dev does not PATCH; the seat writes it): the last Acceptance-notes bullet is wrong. Replace it with: "Check order differs on an object whose API exposure denies the update: RestServer runs enforceApiAccess before its request gate (403), while the dispatcher runs its gate before callData, whose exposure check is inside (400). Before this PR the dispatcher answered that exposure refusal too, so this ordering is new on the dispatcher. Read in source, not measured."",
"tests": "All on 3dd76b2, exit codes captured before any pipe. @objectstack/rest vitest --project local: 284 files, 5856 passed, 326 skipped, REST_TEST_EXIT=0. @objectstack/runtime vitest --project local: 382 files, 5673 passed, 19 skipped, RUNTIME_TEST_EXIT=0. Targeted (JSON reporter): data-write-request-gate.test.ts 26/26 (new), data-update-expected-version.test.ts 65/65, data-delete-expected-version.test.ts 34/34, rest request-schema-gate.conformance.test.ts 16/16. Typecheck: rest exit 0, runtime exit 0 (test layer compiles; debt ledger unchanged). Flipped pins (repo-wide grep found 2 numeric-token hits, both in data-update-expected-version.test.ts, and no null-PATCH-200 pin): the body-token-0 control now asserts 400 with updateData never asked beside the CURRENT If-Match, and EDGE_SHAPES number-token row moved into the new [#23517] block. ABLATIONS via scripts/ablation-replace.mjs, restore proven blob==HEAD each time. These suites resolve data.ts and @objectstack/rest from src (runtime vitest alias), so no dist leg. (1) dispatcher PATCH gate line deleted: 8/26 red. (2) RestServer gate input back to data ?? {}: 2/26 red plus 1/16 red in the conformance file. (3a) dispatcher number arm restored alone: 4/65 red, ONLY the parity-sentence rows. The raw-token schema gate still refused the number 400, so the per-door rows stayed green: the second guard holding, reported as observed. (3b) number arm plus gate input back to String(expectedVersion): 9/65 red (4 dispatcher rows, 4 parity rows, the flipped 0 control). (4) dispatcher create gate deleted: 6/26 red. Before/after door measurements (probe, never committed): see the PR body table.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 3dd76b2 (no paths, after the last commit): 65 lines. Each was run with its exit code recorded before any pipe: 64 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs every package dist): NOT MEASURED locally, CI check-run is the reading. --ran with the exit-coded record: 65 derived, 64 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN. Lane blind spot: eslint.js packages/rest/src packages/runtime/src --no-inline-config, exit 0, no findings; repo-wide pnpm lint is left to CI. PR check-runs on 3dd76b2, read once at open: 37 runs, 12 success, 6 skipped, 19 in_progress. Governed Surface Queue Guard success; Lint · PM dispatch-gates success; Check PR Size success; The card this PR closes must claim this branch success; No other open PR may claim the same issue success. Build Core, Test Core 1-6/6, Check Changeset, Lint · ESLint, Lint · repo and tooling gates, Lint · docs and code guards, Lint · CI release and engine gates, Type Check · workspace, source gates, consumer gates and debt ledger: in_progress (not waited on).",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23571/assignees (os-bill), read back identical at 11561 bytes; (2) comment POST /repos//issues/23491/comments (this report); (3) comment POST /repos//issues/23517/comments (the pointer). No label writes: none were named, and a published fix owes a changeset. git push is not counted here: 3 pushes, one of them --force-with-lease=claude/issue-23491-patch-data-request-gate:4c97ac5c0 before the PR existed.",
"line_budget": "n/a",
"files_changed": [
".changeset/23491-patch-data-request-gate.md (new, rest+runtime minor, fix(rest,runtime)!, BREAKING accept-set narrowing, adr-0087 not-required marker, Clause-②: no)",
"packages/runtime/src/domains/data.ts (PATCH gate, POST create gate, writeRequestRefusal helper, versionTokenRefusal number arm removed, raw token)",
"packages/rest/src/rest-server.ts (PATCH handler: raw data and raw token in gate and forward; versionTokenRefusal number arm removed and its docblock; no other region)",
"packages/runtime/src/domains/data-write-request-gate.test.ts (new, 26 tests)",
"packages/runtime/src/domains/data-update-expected-version.test.ts ([#23517] block, 0-control flipped, number row moved)",
"packages/rest/src/request-schema-gate.conformance.test.ts (null joins the PATCH violating bodies)"
],
"deviations": [
"Not re-merged with origin/main before opening (AGENTS §10). The branch is 3 commits behind 252775d. Those commits touch packages/cli, packages/metadata-protocol and packages/spec tests, none of packages/rest or packages/runtime, and git merge-tree is clean. Skipped so the history stays one commit per card; CI runs the merge ref.",
"Commit 2 was amended once to carry the changeset and re-pushed with --force-with-lease=claude/issue-23491-patch-data-request-gate:4c97ac5c0 before any PR existed. All five unshared-branch conditions held: claude/issue-*, created by this worktree, only this author in origin/main..branch, no PR, lease spelled.",
"PR body erratum in the last Acceptance-notes bullet: the replacement text is in summary. Written once, and not PATCHed by the dev.",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a claude.com PR footer. I used the model-free pair (Co-Authored-By: Claude, Claude-Session) and the AGENTS.md session-URL footer, as the dispatch and AGENTS.md require.",
"One locked run (the rest and runtime suites sequenced in one command) held the verify lock 17m14s, and the lock printed its holder-starvation warning. Later runs were split."
],
"open_questions": [
{
"question": "POST /data/:object with a JSON null body: both doors read it as {} and create an empty row (201), against CreateDataRequestSchema.data (a record). Converge on 400 as the PATCH doors now do?",
"options": [
"A: a follow-up card in this family. RestServer create createInput/createRequest drop req.body ?? {}, and the dispatcher create drops its mirrored body ?? {}, plus null pins on both doors. About 4 lines, one more accept-set narrowing, landing behind this PR under the same-file serial rule.",
"B: widen this PR now to the RestServer create handler. That breaches the claim surface (no other region of rest-server.ts) and needs the seat to re-scope.",
"C: keep both doors lenient. This leaves a declared contract unenforced on both doors, plus a ?? default in the consumer."
],
"recommendation": "A, because the contract (record) and Prime Directive 12 argue for 400, while the serial rule holds the REST create region. One small follow-up converges both doors at once, which is the only way to retire it without splitting one route into two answers."
},
{
"question": "Clause-② arm. The PR body carries the bare line Clause-②: no, as the dispatch ordered, and the changeset declares the break through the summary bang, the BREAKING banner and the adr-0087 marker. The direct sibling #23492 (same class: a 200 or judged token becoming 400) wrote Clause-②: no (narrowing). Should this PR carry the arm too?",
"options": [
"A: keep it bare, as dispatched.",
"B: add (narrowing) to the PR body line (a seat write) and to the changeset line (one-line dev or seat edit)."
],
"recommendation": "B, because the arm is the closed-token carrier for exactly this measured narrowing (RestServer PATCH null 200 to 400, and dispatcher create array or number 201 to 400), and it matches the #23492 precedent. It changes no gate verdict: the changeset is already read as breaking."
}
],
"out_of_scope_findings": [
"class: b · reach: measured through HttpDispatcher.dispatch() with the createHonoApp catch-all body parse, over a real better-sqlite3 store, ObjectQL and protocol (this PR rig), at 3dd76b2 · evidence: POST /data/task/query with { limit: "ten" } and with the retired { joins: [] } answers 200 with the full record set on the dispatcher, while RestServer answers 400 VALIDATION_FAILED (fields query.limit; the joins tombstone prescription). The string or array body answers the engine INVALID_FIELD 400 on the dispatcher versus VALIDATION_FAILED on REST. Seam: spec:FindDataRequestSchema / QuerySchema (retiredKey joins) → runtime: domains/data.ts POST /data/:object/query (no request-schema gate) · same family as this fold (dispatcher /data request-schema gates): fold into the family closure card rather than a single-point card · dedupe words: dispatcher data query request schema gate; POST data object query limit string 200 dispatcher; retired joins tombstone dispatcher query",
"class: b · reach: measured on both doors (this rig) at 086fa46 and 3dd76b2 · evidence: POST /data/:object with a JSON null body answers 201 and creates an empty row on RestServer (data: req.body ?? {}) and on the dispatcher (whose new gate mirrors it), where CreateDataRequestSchema.data declares a record. Seam: spec:CreateDataRequestSchema.data (record) → rest: rest-server.ts POST /data/:object createInput/createRequest + runtime: domains/data.ts POST create createInput · same family; carrier: open question 1 (seat) · dedupe words: create data null body empty row 201; CreateDataRequestSchema data null coerced; POST data object null body",
"carrier: none (noted in the PR Acceptance notes, not filed) · on an object whose API exposure denies update or create, a non-record body now answers 400 on the dispatcher (request gate before callData exposure check) and 403 on RestServer (enforceApiAccess first). Read in source, not measured."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23571 comment 6181996238 (seat record
## 席位核对, head3dd76b2a81, the family fold #23491 + #23517). It queues after oneCONTRACT_REVIEW_TIERPASS and a green head. The family remainder is filed as #23574.domain:cliseat 1 · sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T01:05Z.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed: PR #23571 →
cac0be8cac(the family fold #23491 + #23517). Both cards are closedcompleted, andpm:dispatchedis stripped from bothdomain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T02:45Z · holder of claims6180923601(#23491) and6180931308(#23517), released by this landing.added_to_merge_queue02:14Zorigin/maincac0be8cac, one parent (a queue squash), an ancestor oforigin/maincompletedby the PR's twoFixeslines;pm:dispatchedremoved from each by targeted delete6181996238. TheCONTRACT_REVIEW_TIERrecord PASS on3dd76b2a81(6182801438, an independent agent; the served model was checked against its transcript) was adopted in6182829379. Every check on the head was success or a selector skip./datawrite doors validate the request as the caller sent it.PATCHorPOSTbody is refused400 VALIDATION_FAILEDon the dispatcher, before the engine is asked.nullPATCHbody is refused400on both doors. It was200onRestServer, withupdated_atmoved.expectedVersionis refused400on both doors. It used to be judged409.nullcreate still makes an empty row on both doors; the dispatcherPOST /data/:object/queryvalidates noFindDataRequestSchema#23574 (anullcreate on both doors, the dispatcher's query gate, and the exposure-check order). It is ordered after this landing and is now unblocked.PATCHhandler ofpackages/rest/src/rest-server.ts, and thePATCH/POSTbranches ofpackages/runtime/src/domains/data.ts. rest(public forms):GET /forms/:sluglocalizes the object schema but returns the form's section labels untranslated, so an anonymous zh-CN visitor reads English section titles (the producer behind objectui#12333) #23497 (registerFormEndpoints) and [maintainer] refactor(rest): splitpackages/rest/src/rest-server.ts(13,701 lines, 70 commits in 30 days) along itsregister*Endpointsseams, one move-only slice per PR,/metaafter #22877 stage 2 #23050's slices are no longer hard-serial behind this PR.Generated by Claude Code