Repository navigation
/data request contracts, the family remainder after PR #23571: a JSON null create still makes an empty row on both doors; the dispatcher POST /data/:object/query validates no FindDataRequestSchema #23574
Description
Filing gate ①, class (b): two /data routes do not enforce the request contract their schemas declare. Both were measured at a public door. This is the closing card for the family PR #23571 started (#23491 + #23517: each /data write door validates the assembled request as the caller sent it). One card covers every remaining location, per the report contract's family rule. ⛔ No point cards.
Filed by domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG, from PR #23571's os-dev-report (on #23491, out-of-scope findings 1 and 2, and open question 1). ⛔ Not a claim. Lane and grade are triage's.
Reader who acts: the domain:cli seat that claims it after PR #23571 merges. It edits the same two files: packages/runtime/src/domains/data.ts and the POST /data/:object handler of packages/rest/src/rest-server.ts.
Locations (enumeration pin: each closes with its own pin)
POST /data/:objectwith a JSONnullbody, both doors.reach:measured by 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's dev on PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571's rig (a real better-sqlite3 store, ObjectQL and the protocol,RestServer's handler andHttpDispatcher.dispatch()side by side), at086fa4640fand at3dd76b2a8.- Answer:
201and an empty row, onRestServerand on the dispatcher. - Mechanism:
RestServerassemblesdata: req.body ?? {}. PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571's dispatcher create gate mirrors it statement for statement, on purpose, so the two doors would not split. CreateDataRequestSchema.datadeclares a record. PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571 refuses thePATCHtwin of this case (null→400) on both doors.
- The dispatcher's
POST /data/:object/queryhas noFindDataRequestSchemagate.reach:measured by the same dev on the same rig at3dd76b2a8, through thecreateHonoAppcatch-all's body parse.- Answer:
{ limit: "ten" }and the retired{ joins: [] }answer200with the full record set on the dispatcher.RestServerrefuses both400 VALIDATION_FAILED(query.limit; thejoinstombstone's prescription). A string or array body answers the engine'sINVALID_FIELD400on the dispatcher, againstVALIDATION_FAILEDon REST.
Governing text
CreateDataRequestSchema.dataandFindDataRequestSchema/QuerySchema(@objectstack/spec/api), including thejoinsretiredKeytombstone.- 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899 (closed): a request body is checked against the schema that declares it.
- AGENTS.md Prime Directive 12: ⛔ no
??leniency in a consumer to tolerate off-contract input. - AGENTS.md 〈Route & surface ownership〉: one route's two transports answer one request alike.
Seam: spec:CreateDataRequestSchema.data (record) → rest: rest-server.ts POST /data/:object (req.body ?? {}) + runtime: domains/data.ts POST create (mirrors it) · Seam: spec:FindDataRequestSchema / QuerySchema (retiredKey joins) → runtime: domains/data.ts POST /data/:object/query (no request-schema gate).
Acceptance (a proposal; triage's call)
- Location 1: both doors refuse a
nullcreate400 VALIDATION_FAILEDnamingdata, with no row written. It is an accept-set narrowing on both doors (201→400), so it owes the contract-tier review before the queue, as PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571 does. - Location 2: the dispatcher validates the assembled
FindDataRequestSchemarequest before the engine is asked, asRestServer's twin does. A non-conforming query is refused400 VALIDATION_FAILEDand never answered with the full set. - Pins on both doors per location, beside the twin:
code+statusat least. - Order: after PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571 lands. It edits the same files.
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included:
dispatcher data query request schema gate POST /data/:object/query limit string answers 200 retired joins tombstone: 9 hits, all closed. None is this. The nearest are [finding] class closure: the rest of rest-server.ts's bare-Number() query reads. /history ?sinceSeq, /audit ?limit and /search ?perObject answer 200 on an unreadable value; close the family with one census pin #20139 (REST's bareNumber()query reads), Data query: an unknown field insidewhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534 (an unknownwherefield), and spec:retiredKey()tombstones are emitted into a repeater row'sitems.properties, so the served schema offers six columns the parse door refuses #17502 (tombstones in a repeater row).POST /data/:object JSON null body creates empty row 201 CreateDataRequestSchema data record null coerced: 2 hits (spec(automation): FlowSchema refuses a create_record, update_record or delete_record node whose static objectName is a stored-metadata family table (the save-time half of #21624) #21654, A write response still serves ORPHANED columns no metadata declares: after the read narrowing, PATCH /api/v1/data/OBJECT/ID answers 200 with a retired field's column inrecord#21613), both closed and different.
Dedupe words: dispatcher data query request schema gate; POST data object query limit string 200; create data null body empty row 201; CreateDataRequestSchema data null coerced.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p1+target:v18·domain:cli·pm:queue· land after PR #23571 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T01:53ZRead fresh. This is the
/datarequest-contract family remainder after PR #23571. Both were measured on its rig.nullcreate answers201and an empty row on both doors (req.body ?? {}), whileCreateDataRequestSchema.datadeclares a record.POST /data/:object/queryvalidates noFindDataRequestSchema.{ limit: "ten" }and the retired{ joins: [] }answer200with the full record set, whereRestServerrefuses both400 VALIDATION_FAILED.target:v18. Both fixes refuse at a/datadoor requests it accepts today (201→400,200→400), which is a data-door narrowing (filing-gate.md〈定级判据〉, 「REST/数据门收窄亦算」). It rides the major, with the contract-tier review the card names.domain:cli, the family's seat. It lands after PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571, which edits the same two files.??leniency (Prime Directive 12).findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Location 3 added to this card's enumeration (escalated to this seat by PR #23571's contract review
6182801438, ③). ⛔ Not a claim.domain:cliseat 1 ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T02:11Z.400 VALIDATION_FAILEDon the dispatcher, whose request gate runs beforecallData, where the exposure check lives. OnRestServer,enforceApiAccessruns before its request gate, so the same body answers403. This ordering is new on the dispatcher since PR fix(rest,runtime)!: both /data write doors validate the request as the caller sent it; a number expectedVersion is refused 400 #23571.PATCHandPOSTcreate.RestServermoving its gate earlier.This card also covers location 1 (the
nullcreate on both doors) and location 2 (the dispatcher's query gate), which the review asked the seat to confirm.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 (#23574: the
/datarequest-contract family remainder after PR #23571: a JSONnullcreate is refused on both doors, the dispatcher'sPOST /data/:object/queryvalidatesFindDataRequestSchema, and the exposure-check order is measured, then aligned) · 2026-10-15T02:48ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23574-data-request-contract-remainderWorktree:
objectstack-issue-23574Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface:
packages/runtime/src/domains/data.ts(thePOST /data/:object/querybranch near:194and thePOST /data/:objectcreate branch near:383onorigin/main744b73f53f, plus the order of thePATCH/POSTrequest gate againstcallData's exposure check if location 3 measures as read),packages/rest/src/rest-server.ts(thePOST /data/:objectcreate handler only, near:9016-:9072), their tests in both packages, and one changeset. Stop on a breach and explain it in the report. ⛔packages/specis read only.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 (narrowing)
Responsibility: the dispatcher's
/datadomain andRestServer's create handler, platform code in this lane | none:RestServer's create gate parsesdata: req.body ?? {}(anullreads as an empty record), and the dispatcher's query branch has no request-schema gate | any caller of the two/dataroutes on either door; locations 1 and 2 measured on PR #23571's rig (os-dev-report6181906712, findings 1 and 2); location 3 read in source onlyThread-read: 6182818417
Serial constraints cleared:
data.tsandrest-server.ts: PR #23571 (#23491 + #23517), the order triage named, landed ascac0be8cacand released both. None of the 11 open PRs edits either file. #23497 (registerFormEndpoints) and #23050's slices are hard-serial behind this claim onrest-server.ts. Seat 2's #23531 (plugin-hono-serverdeclaredEnvelopeForThrow) does not touch either file.Contract face: locations 1 and 2 narrow what a door accepts (
201→400for anullcreate on both doors;200→400for a non-conforming dispatcher query), hence the(narrowing)arm. Per triage6182587093andexecution-duties.md〈派发〉, this owes oneCONTRACT_REVIEW_TIERreview before the queue. The seat runs it after ACCEPT, in an isolated agent.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23574,
"status": "done",
"branch": "claude/issue-23574-data-request-contract-remainder",
"pr": "#23617",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the dispatching seat session; this run is its subagent)",
"premise_still_valid": true,
"summary": "All three locations, plus H4, land in draft PR #23617 (head 7def2b3, assignee os-bill, body read back byte-identical, 16077 bytes). The head is based on 744b73f with origin/main merged at 16abea8. L1: both POST /data/:object doors drop req.body ?? {} / body ?? {}, so a JSON null create answers 400 VALIDATION_FAILED [data] with no row written. L2: the dispatcher POST /data/:object/query parses the request RestServer's twin assembles ({object, query}, path object written last) with FindDataRequestSchema and returns requestSchemaRefusal, which is PR #23571's writeRequestRefusal widened to 'query'. It forwards the gated query's clauses with the path object at the request level, so the engine fallback still serves. The dispatcher-only body
queryenvelope is retired: the gate cannot see it, and the catalog declares the body to be the QueryAST. That flips one #3946 pin, which data-path-object.test.ts carried. H4 (same family, same fix): RestServer's query handler also drops req.body ?? {}, so a null query is refused 400 [query] on both doors. L3 was measured as read, with statuses 404/405 rather than 403. The dispatcher now asks the exposure gate before each request gate (PATCH, POST create, POST query) through refuseUnexposedDataAction, which is callData's inline gate moved, not copied, into an exported function that callData still asks first.","tests": "At 7def2b3: @objectstack/runtime vitest --project local: 383 files, 5819 passed, 19 skipped, exit 0. --project repo: 3 files, 808 passed, exit 0. @objectstack/rest --project local: 285 files, 5858 passed, 326 skipped, exit 0. --project repo: 5 files, 213 passed, 2 skipped, exit 0. The rest runs were at d10096d, and git diff d10096d 7def2b3 -- packages/rest is empty. Targeted at head: data-write-request-gate 73 (was 26), data-path-object 11, http-dispatcher 248, action-execution-calldata-batch-retired 8, all green. The first full runtime run at d10096d was red on 2 tests (the engine-fallback query refused
objectinside the forwarded query, and the #3946 envelope pin). b4b22b1 handles both, and the final run is green. Ablations: 10 legs, each through scripts/ablation-replace.mjs in WRAP mode against the committed implementation, anchor hit x1 and blob changed on disk, restore proven blob == HEAD and git diff HEAD empty, no dist step (both suites resolve data.ts and @objectstack/rest from src). Reds: RestServer create ?? {} 2 runtime + 1 rest conformance; dispatcher create ?? {} 2; dispatcher query gate deleted 12; RestServer query ?? {} 2 + 1 conformance; PATCH, create and query exposure call each deleted 4/4/4; forward back to {...body, object} 3; path object left inside the forwarded query 3. Measurement before/after on PR #23571's rig, both doors, at 744b73f and on this branch, is in the PR table and in before_after below.","gates": "Local, all at 7def2b3, exit codes captured before any pipe. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 96. 95 exit 0 and 1 exit 1: node scripts/check-empty-changeset.mjs --base origin/main. That is the DELIBERATE CORRECTION class, red by design until confirmed on the PR (see open_questions). --ran reconciliation: 96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). Beside it: pnpm check:durability-log-level exit 0. check:route-envelope, check:error-status-conformance and check:dispatcher-error-vocabulary are in the derived list, each exit 0. check:system-context-census exit 0 after the row-48 anchor; it was exit 1 before it. check:dual-build-cjs-loads exit 0 (measured, 110 entries). pnpm --filter @objectstack/runtime typecheck exit 0 and pnpm --filter @objectstack/rest typecheck exit 0. pnpm exec eslint packages/runtime/src packages/rest/src --no-inline-config exit 0. CI check-runs on 7def2b3 at report time: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, and the other 22 in_progress (Check Changeset, Governed Surface Queue Guard, the Lint lanes, the Type Check lanes and the rest). Check Changeset is expected red for the same deliberate correction.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (POST /repos/objectstack-ai/objectstack/dispatches, run by objectstack-fleet[bot]): (1) pr_create, which executed POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23617/assignees [os-bill]; (2) comment, which executed POST /repos//issues/23574/comments (this os-dev-report). label-write: 0, because no labels were named and a published fix owes a changeset. git push is not counted.",
"line_budget": "n/a",
"files_changed": [
"packages/runtime/src/domains/data.ts",
"packages/runtime/src/action-execution.ts",
"packages/rest/src/rest-server.ts",
"packages/runtime/src/domains/data-write-request-gate.test.ts",
"packages/runtime/src/domains/data-path-object.test.ts",
"packages/rest/src/request-schema-gate.conformance.test.ts",
"content/docs/permissions/system-context.mdx",
".changeset/23574-data-request-contract-remainder.md",
".changeset/23491-patch-data-request-gate.md"
],
"deviations": [
"File surface, beyond the claim: packages/runtime/src/action-execution.ts. callData's exposure gate is extracted verbatim into the exported refuseUnexposedDataAction (moved, not copied), because no single call existed to ask exposure before the shape gate.",
"File surface: content/docs/permissions/system-context.mdx row 48 gains the #refuseUnexposedDataAction anchor. check-system-context-census was red (exit 1) on the moved isSystem read until it was added.",
"File surface: .changeset/23491-patch-data-request-gate.md. Its pending 'Unchanged' sentence (a null POST create still answers 201) became false, so it was corrected in the same stroke. check-empty-changeset stays red by design for that class.",
"File surface: packages/runtime/src/domains/data-path-object.test.ts. The #3946 pin 'still honours an explicit query envelope' is flipped. The dispatcher-only body
queryenvelope is retired because a gate over the declared body (the QueryAST) cannot see it: {query:{limit:'ten'}} would pass the gate and be served as the full set. RestServer already answered that body 400 INVALID_FIELD. This goes beyond the card's literal enumeration; the catalog's requestSchema row declares the body to be the QueryAST.","rest-server.ts: the POST /data/:object/query handler's req.body ?? {} is also dropped. This is H4, folded in as the dispatch allows once RestServer owes a change. No other region is touched.",
"Location 3: the dispatch and the card read the RestServer answer as 403. Measured, it is 405 (apiMethods leaves the verb out) or 404 (apiEnabled false), and the pins assert those.",
"The rest suites were cited at d10096d, not at the final head, because packages/rest has no diff between the two (a declared narrowing). The runtime suites ran at the final head.",
"The rig gains each door's object-definition source (RestServer getMetaItems, dispatcher metadata.getObject), both answered from the engine registry. Without them both exposure gates fail open. Measured: RestServer answered 201 on an apiEnabled:false object."
],
"before_after": [
"L1 POST /data/task body null | RestServer 201 + empty row | dispatcher 201 + empty row | both after 400 VALIDATION_FAILED [data], no row, createData never asked",
"L2 POST /data/task/query {limit:'ten'} | RestServer 400 VALIDATION_FAILED [query.limit] | dispatcher 200 full set | both 400 VALIDATION_FAILED [query.limit], findData never asked",
"L2 {joins:[]} | RestServer 400 [query.joins] | dispatcher 200 full set | both 400 VALIDATION_FAILED [query.joins]",
"L2 42 | RestServer 400 [query] | dispatcher 200 full set | both 400 VALIDATION_FAILED [query]",
"L2 'garbage' and [{}] | RestServer 400 [query] | dispatcher 400 INVALID_FIELD (engine, findData asked) | both 400 VALIDATION_FAILED [query]",
"H4 null | RestServer 200 full set | dispatcher 200 full set | both 400 VALIDATION_FAILED [query]",
"L2 {query:{where}} | RestServer 400 INVALID_FIELD | dispatcher 200 nested query served | both 400 INVALID_FIELD",
"L2 controls {}, {where}, {limit:1}, {object:'other'} | 200 | 200 | 200 unchanged",
"L3 PATCH or POST on apiMethods:[get] object, non-record body | RestServer 405 | dispatcher 400 VALIDATION_FAILED | both 405",
"L3 PATCH or POST on apiEnabled:false object, non-record body | RestServer 404 | dispatcher 400 VALIDATION_FAILED | both 404",
"L3 POST null, record bodies, any query on either object | 405/404 | 405/404 | 405/404 unchanged"
],
"open_questions": [
{
"question": "The pending .changeset/23491-patch-data-request-gate.md (PR #23571's entry, same release) says a JSON null POST create is unchanged and answers 201. This PR makes that false and corrects the sentence in the same stroke. check-empty-changeset (in Check Changeset, not one of the five required contexts) stays red for this class until the correction is confirmed on the PR. Keep the correction?",
"options": [
"A Keep the correction, and confirm it on PR #23617. Check Changeset stays red as the gate designs; the landing is the seat's or maintainer's call.",
"B Revert the correction. The gate turns green, and the next CHANGELOG version ships both 'null create unchanged, 201' and this PR's 'null create FROM 201 TO 400'."
],
"recommendation": "A, because the gate's ruling D text says not to restore a note the PR made false. Restoring it would publish a contradiction inside one release that an upgrading agent greps."
}
],
"out_of_scope_findings": [
"class: a · reach: measured at both public doors on PR #23571's rig (RestServer's registered handler and the @objectstack/hono catch-all through HttpDispatcher.dispatch()), at 744b73f and at 7def2b3: the same exposure refusal carries two codes on one route. RestServer answers OBJECT_API_METHOD_NOT_ALLOWED (405) / OBJECT_API_DISABLED (404). The dispatcher's addressed-object gate (refuseUnexposedDataAction, formerly inline in callData) throws {statusCode, message} with no code, which the host renders as METHOD_NOT_ALLOWED / RESOURCE_NOT_FOUND. The relation-target refusal in the same file already sets the OBJECT_API_* codes. Seam: runtime: action-execution.ts refuseUnexposedDataAction (bare throw) | rest: rest-server.ts apiAccessDenialFromEnable. An AI caller reads a different code for one refusal depending on host composition. Statuses agree and are pinned here; the codes are not changed. Dedupe words: exposure refusal code dispatcher RESOURCE_NOT_FOUND OBJECT_API_DISABLED; callData exposure gate throws statusCode without code; METHOD_NOT_ALLOWED vs OBJECT_API_METHOD_NOT_ALLOWED door parity",
"carrier: none · noted in the PR Acceptance notes, not filed: checkApiExposure's docblock (api-exposure.ts) says 'The only caller is callData'. The relation-target gate in action-execution.ts already called it before this PR (doc drift, outside the surface).",
"carrier: none · noted in the PR Acceptance notes, not filed: the engine query fallback (no protocol registered) refuses a query's
objectkey as unservable, althoughobjectis a declared QueryAST member. It is unreachable from the /data door after this PR (the path object is forwarded at the request level). Observed in source and through the batch-retired pin only; no other caller was measured."]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Seat pointer: ACCEPT of PR #23617 at
7def2b3016, and this card's claim surface revised in the same round ·domain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T05:37Z · holder of claim6183265997. Thread-read: 6185063956.CONTRACT_REVIEW_TIERPASS on its head, which also confirms the pending.changeset/23491-patch-data-request-gate.mdcorrection, and a true-green head.6183265997's surface plus the four paths below. No open PR edits any of them, and nopm:dispatchedclaim names them (read at this stamp).packages/runtime/src/action-execution.ts:callData's exposure gate, moved verbatim intorefuseUnexposedDataAction.content/docs/permissions/system-context.mdx, row 48: the census anchor..changeset/23491-patch-data-request-gate.md: one sentence that this PR makes false.rest-server.tsquery handler: the conditional region the dispatch named.6182818417saysRestServeranswers403. Measured, it answers405whenapiMethodsleaves the verb out, and404forapiEnabled: false. The dev's pins assert those.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23574,
"status": "done",
"round": "patch round 1 (contract review 6185455247, VERDICT FAIL on changeset bullet 6)",
"branch": "claude/issue-23574-data-request-contract-remainder",
"pr": "#23617",
"head": "7d30b11474108982e49cd48ddb76de589b953a12",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the dispatching seat session; this run is its subagent)",
"premise_still_valid": true,
"summary": "One commit on 7def2b3, changing only .changeset/23574-data-request-contract-remainder.md (3 lines). Bullet 6 now names only PATCH /data/:object/:id with a body that is not a record (an array, a string, a number or null) and POST /data/:object with an array, a string or a number, each FROM 400 VALIDATION_FAILED TO the exposure refusal. The query-route clause is gone. It is stated instead as a fact with no FROM/TO: a null create on an exposure-denied object, and every query on one, already answered the exposure refusal, and the new query gate runs after the exposure gate, as RestServer orders it. Re-reading every sentence against origin/main found the same defect in two more places, both now corrected. (1) The opening paragraph said the dispatcher 'used to answer a malformed body with its request gate's 400', which covered the query route (no gate on main) and a null create (main's gate read null as {}, so callData answered 405/404); it now names the PATCH non-record and create array/string/number bodies only. (2) The ADR-0087 note said a query the schema does not declare 'is now refused where it was applied'; a string or array query body was already refused, by the engine with INVALID_FIELD, and the note now says so. Bullets 1 to 5 hold against origin/main and were left unchanged; the reading is in rereading below. No code change. .changeset/23491-patch-data-request-gate.md was not touched.",
"rereading": [
"Bullet 1 (both doors, null create, FROM 201 + empty row): origin/main rest-server.ts:9036 and :9049 read req.body ?? {}, and data.ts:395 reads body ?? {}. Measured 201 + one row on both doors at 744b73f. Holds.",
"Bullet 2 (both doors, null query, FROM 200 full set): origin/main rest-server.ts:9100 reads req.body ?? {}, and data.ts:213 forwards {...body, object} with no gate. Measured 200 full set on both doors. Holds on any object that exposes list; on an exposure-denied object it is 405/404 before and after, which bullet 6's new sentence now states.",
"Bullet 3 (dispatcher {limit:'ten'}, joins, number, FROM 200 full set): data.ts:213 has no gate. Measured 200 full set, the clause dropped. Holds on the protocol path every shipped host composes.",
"Bullet 4 (dispatcher string/array, FROM engine 400 INVALID_FIELD): measured as a thrown INVALID_FIELD on numbered keys, findData asked. Holds.",
"Bullet 5 (dispatcher body query key, FROM nested query 200): action-execution.ts on main reads params.query || rest. Measured 200 with the nested where applied. Holds.",
"Bullet 6: main's PATCH gate (data.ts:291) and create gate (:396) run before callData, whose exposure gate is at action-execution.ts:302. PATCH non-record bodies, null included, measured 400. Create array/string/number measured 400, while create null measured 405/404 (read as {}) and every query measured 405/404. Corrected to that set.",
"Unchanged paragraph and SDK sentence: client data.create posts JSON.stringify(data) and data.query posts JSON.stringify(query) (client/src/index.ts:7027, :6907). An empty body reaches both doors as {}. Holds."
],
"tests": "No code change in this round (git diff 7def2b3 7d30b11 touches only the changeset), so no suite was re-run. The test record at 7def2b3 stands: runtime local 383 files / 5819 passed / 19 skipped; runtime repo 808 passed; rest local 5858 passed / 326 skipped; rest repo 213 passed; typecheck both exit 0; eslint exit 0. CI on 7d30b11: Test Core success.",
"gates": "Re-derived at 7d30b11 with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: the same 96 as at 7def2b3 (9 paths vs merge base 16abea8). Run in a fresh worktree after pnpm install and a build of the runtime and rest closures, with exit codes captured before any pipe. The run stopped once at gate 80 (session limit) and was resumed from gate 81 on the same head. Result: 93 exit 0. 1 exit 1: node scripts/check-empty-changeset.mjs --base origin/main, the DELIBERATE CORRECTION class, red by design (annotation on .changeset/23491-patch-data-request-gate.md only), confirmed as option A by contract review 6185455247. 2 exit 3 PREREQUISITE NOT MET, because this fresh worktree has no dist for the client packages and ~39 others: pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads. Both exited 0 at 7def2b3, and their CI lanes are success on 7d30b11 ('Type Check · consumer gates', 'Build Core'). Beside the derivation: pnpm check:durability-log-level exit 0. --ran reconciliation: 96 derived, 94 run, 2 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN, exit 0. CI check-runs on 7d30b11: 41 completed, 35 success, 5 skipped (Console Pin Gate, Dogfood Regression Gate shard placeholder, Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance), 1 failure: Check Changeset, the same deliberate-correction annotation. The five required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard. PR mergeable true; no merge of main was needed (no conflict, no main-caused red).",
"mcp_calls": "0",
"api_writes": "1 relay stroke: comment, which executed POST /repos//issues/23574/comments (this os-dev-report). git push is not counted. No PR body PATCH; the body changes are listed in pr_body_changes for the seat.",
"line_budget": "n/a",
"files_changed": [
".changeset/23574-data-request-contract-remainder.md"
],
"files_changed_cumulative": [
"packages/runtime/src/domains/data.ts",
"packages/runtime/src/action-execution.ts",
"packages/rest/src/rest-server.ts",
"packages/runtime/src/domains/data-write-request-gate.test.ts",
"packages/runtime/src/domains/data-path-object.test.ts",
"packages/rest/src/request-schema-gate.conformance.test.ts",
"content/docs/permissions/system-context.mdx",
".changeset/23574-data-request-contract-remainder.md",
".changeset/23491-patch-data-request-gate.md"
],
"deviations": [
"Beyond bullet 6, the same over-claim was corrected in two more sentences of the same changeset: the opening paragraph's exposure sentence and the ADR-0087 note's 'refused where it was applied' clause. The dispatch asked for every bullet to be re-read against origin/main; these two are release text outside the bullet list, and both made the same kind of claim. The ADR-0087 disposition category (not-required, no-migration-prescription) is unchanged, and check-adr-0087-registration is exit 0.",
"2 derived gates are NOT MEASURED locally (exit 3, PREREQUISITE NOT MET in a fresh worktree). They are cited from CI on this head and from the local exit 0 at 7def2b3.",
"The local gate run was split in two by the session limit: gates 1 to 80, then 81 to 97, both at 7d30b11."
],
"pr_body_changes": [
{
"old": "- On an object whose API exposure denies the verb, the dispatcher answered a non-record body with its request gate's
400.","new": "- On an object whose API exposure denies the verb, the dispatcher answered a
PATCHbody that is not a record, or a create body that is an array, a string or a number, with its request gate's400(anullcreate, which the old gate read as{}, and every query, which had no gate, already reached the exposure gate)."},
{
"old": "The runtime suites, typecheck, eslint and the gate list all ran at
7def2b301, the final commit. Exit codes were captured before any pipe.","new": "The runtime suites, typecheck and eslint ran at
7def2b301.7d30b1147changes only the changeset text (the contract review's bullet-6 correction), and the gate list was re-derived and re-run there. Exit codes were captured before any pipe."},
{
"old": "derived 96 commands at
7def2b301. 95 exit 0 and 1 exit 1 (check-empty-changeset --base origin/main, by design, see below).--ranreconciliation: 96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN.","new": "derived 96 commands at
7def2b301: 95 exit 0 and 1 exit 1 (check-empty-changeset --base origin/main, by design, see below);--ran: 96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN. Re-derived at7d30b1147, the same 96: 93 exit 0, the same 1 exit 1, and 2 exit 3 PREREQUISITE NOT MET in a fresh worktree (check:skill-examples,check:dual-build-cjs-loads; both exit 0 at7def2b301, and their CI lanesType Check · consumer gatesandBuild Coreare success on7d30b1147);--ran: 96 derived, 94 run, 2 NOT-MEASURED, 0 UNRUN."}
],
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed: PR #23617 →
7c6b7f4a35(#23574). The card is closedcompleted, andpm:dispatchedis strippeddomain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T07:09Z · holder of claim6183265997, released by this landing.added_to_merge_queue06:47Z, merged 07:07Zorigin/main7c6b7f4a35: one parent (a queue squash), an ancestor oforigin/main, 9 files +420 / −74 as reviewed at7d30b11474; no queue branch leftcompletedby the PR'sFixes #23574;pm:dispatchedremoved by targeted delete6185264261.CONTRACT_REVIEW_TIERrecord FAIL6185455247(one changeset sentence) was adopted at6185488658, after which a patch round corrected the changeset text only.6186186875on7d30b11474(an independent agent; its served tier was checked against its transcript) re-confirmed the corrected pending note.changeset/23491-patch-data-request-gate.md.6186206232queued the PR past the designedCheck Changesetred under the three conditions.nullcreate or query is refused400 VALIDATION_FAILEDon both/datadoors.POST /data/:object/queryvalidatesFindDataRequestSchemaoverRestServer's assembled request, and the dispatcher-only{ query: … }envelope is retired.refuseUnexposedDataAction, moved out ofcallData) before each request gate, asRestServerdoes.packages/rest/src/rest-server.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 move-only slices are no longer hard-serial behind this claim.packages/runtime/src/domains/data.ts.packages/runtime/src/action-execution.ts: runtime: the /mcp data bridge's exposure gate (callData) refuses with a status and a message but no code, where the stdio face and REST answer OBJECT_API_DISABLED / OBJECT_API_METHOD_NOT_ALLOWED for the same declaration #23528 (domain:spec) can takerefuseUnexposedDataAction's code now (pointer6185279107).Generated by Claude Code