Repository navigation
fix(metadata-protocol,rest,runtime,mcp): the remaining write doors answer a write that landed under a failing roll-up with their own success - #28843
Conversation
…swer a write that landed under a failing roll-up with their own success Co-Authored-By: Claude <noreply@anthropic.com>
…/catch Co-Authored-By: Claude <noreply@anthropic.com>
…ccess Co-Authored-By: Claude <noreply@anthropic.com>
…engine dispatch asserts; ledger learns it Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 48 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1503479768c6c86078ffad44bc9ac1cda3116bd2 && git checkout 1503479768c6c86078ffad44bc9ac1cda3116bd2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 839aae58c21df11ecafabe631e661c2cfad69199 dce87858c2b0c9a3a2c22c6d393b1e4f2cad032e && git checkout -B drift-repro 839aae58c21df11ecafabe631e661c2cfad69199 && git merge --no-ff dce87858c2b0c9a3a2c22c6d393b1e4f2cad032e
node scripts/docs-audit/affected-docs.mjs --json 839aae58c21df11ecafabe631e661c2cfad69199
|
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. 席位意见:与 diff 相符。写入已落库、只是父记录汇总重算失败时,表单提交、批量创建、非原子批量写、clone、callData 回退和 MCP create_record 不再答 500,而是答各自原有的成功结果;原子批量照旧整体回滚,那里本来就没有落库。
|
…ord stale-summary test Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt 席位核对 · re-landACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. 席位意见:与上次 ACCEPT(6565651976,head
|
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt
Requested by Leehom · project thread
Fixes #28782
Clause-②: no
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches what a write door answers when a parent roll-up cannot be recomputed after the write.维护者速读
ERR_SUMMARY_RECOMPUTE)时,fix(lint,metadata-protocol,rest,runtime): refuse a sum/avg roll-up the engine cannot recompute, and answer a landed child write's success #28781 只修了 REST 和分发器的单条 POST/PATCH/DELETE;表单提交、createMany、clone 仍答 500,batch/updateMany/deleteMany 把已存的行报成失败并停下,客户端重试会产生重复记录。options.atomic和跨对象POST /batch)不变:拒绝发生在事务里,整批回滚,什么都没落库,回滚应答本来就是事实。packages/spec/**。update_record/delete_record也走同一路径,本卡只点名了create_record,未处理(见 Acceptance notes)。What changes
One reading for every remaining door, through #28781's carrier in
@objectstack/types(withEarnedAnswer/earnedAnswerOf/staleSummaryAnswerLines); no new carrier, code, member, status or header.createDatadoes since fix(lint,metadata-protocol,rest,runtime): refuse a sum/avg roll-up the engine cannot recompute, and answer a landed child write's success #28781, so a caller that does not read it still gets the refusal:packages/metadata-protocol:createManyData(protocol-batch-data.ts; one multi-rowengine.insert, sowrittenis every row) andcloneData(protocol.ts).packages/runtime:callData's ObjectQL fallback (action-execution.ts), its create, update and delete arms — the dispatcher's/datadoor (callDataWrite, fix(lint,metadata-protocol,rest,runtime): refuse a sum/avg roll-up the engine cannot recompute, and answer a landed child write's success #28781) already reads the answer.packages/mcp: the stdio bridge'screate(stdio-data-bridge.ts,engine.insertdirect).packages/rest: public form submit (form-routes.ts, at thecreateDatacall, then the door's own201 { id, fields? }projection),createMany(batch-routes.ts) and clone (data-action-routes.ts) answer201with the earned body.stale-summary-answer.tsgainsearnedAnswerLogged(log + answer, no dropped-fields header — these doors' success paths set none);earnedWriteDespiteStaleSummaryis now that plus the header.packages/mcp: thecreate_recordtool (mcp-http-tools.ts), for both bridges — the runtime bridge reaches the protocol'screateDatathroughcallData('create').runBatchDataLoop,runUpdateManyLoop,runDeleteManyLoopinprotocol-batch-data.ts): eachengine.insert/update/deletegets.catch(landedDespiteStaleSummary), which on the non-atomic arm hands back the refusal'swrittenso the row is built by the arm's own success code ({ id, success: true, data, index }, the delete arm'ssuccess), and the loop goes on. On the atomic arm it rethrows: the refusal is raised inside the batch transaction and the rollback answer is the truth. These loops answer a normal success, so they log the stale column themselves ([Protocol], same line).What
writtencarries. Measured in objectql: a multi-row insert throws once, after every row is stored, withwritten= all rows (engine.tsinsert path); a per-row loop's refusal belongs to that row only. Nothing throws after a partial set.Tests
Each new file red on
origin/mainc9a9981da(implementation reverted on disk,metadata-protocolrebuilt) and green on the head.packages/rest—data-write-summary-recompute-other-doors.test.ts(9,RestServer→ protocol →ObjectQL→ realSqlDriveron better-sqlite3): form submit201 { id }, row stored once;createMany201 { object, records, count }; per-objectbatchcreate,updateMany,deleteMany(non-atomic) all rowssuccess,succeeded: 2, values stored once andchild_sumrecomputed; clone201; an atomic batch still rolls back with nothing stored; a pre-write refusal (missing required parent) answers as before; oneerrorline. Base: 7 red, 2 green (the atomic and pre-write guards).packages/runtime—domains/data-write-summary-recompute-fallback.test.ts(5): the dispatcher with noprotocolservice, POST 201 / PATCH 200 / DELETE 200, stored once, roll-ups recomputed, pre-write refusal unchanged, one host-logger line. Base: 4 red.packages/mcp—mcp-create-record-stale-summary.test.ts(4, over the HTTP/mcptransport): stdio bridge success body with the row stored once; runtime-bridge shape relays the attached answer; a pre-write refusal is stillisError; one log line. Base: 3 red.git diff HEADempty after each): form reading off → 1 red;createManyreading off → 1 red; clone reading off → 1 red; bulk-loop helper rethrowing always (mp rebuilt) → 3 red (batch, updateMany, deleteMany);createManyDatathrowing the bare refusal (rebuilt) → 1 red;cloneDatathe same (rebuilt) → 1 red; fallback throwing the bare refusal → runtime 4 red; MCP tool reading off → 3 red; stdio bridge throwing the bare refusal → 1 red.6e3035f3:@objectstack/metadata-protocol22074 passed (26 skipped),@objectstack/rest7217 passed (352 skipped),@objectstack/runtime9242 passed (19 skipped),@objectstack/mcp635/635.6e3035f3(after mergingmain, closure rebuilt): Test Files 380 passed, 3 skipped (383); Tests 3114 passed, 28 skipped (3142).Acceptance notes
@objectstack/typesis untouched.options.atomicbatches and the cross-objectPOST /batchraise the refusal insideengine.transaction; the loop's failure rolls the batch back, so no row landed and a retry duplicates nothing. Committing them under a stale roll-up would be a new behaviour, not the reading this card asks for.update_record/delete_record(samecallDatapath, not named by the card),insertManyData(the import runner's partial create, ruling 3),import-runner.ts,seed-loader.ts.packages/spec/**edit; the code is read inline (check:error-code-provenanceexit 0).Verification
Live probe, base vs head, each on a fresh showcase boot. To store the stale roll-up the head's save gate refuses, both boots ran with
@objectstack/lint's roll-up entry off at the runtime door (runtimeTypes: [], restored after); base =metadata-protocol/rest/runtime/mcpatorigin/main, head = this branch.batch(create ×2, non-atomic)200, row 0success:falseERR_SUMMARY_RECOMPUTE, row 1NOT_ATTEMPTED; b1 stored, b2 not200,succeeded: 2, both rowssuccess:true, b1 and b2 stored oncecreateMany500 INTERNAL_ERROR, m1 stored201 { object, records, count }, m1 stored once500 INTERNAL_ERROR, k1 stored201 { object, id, sourceId, record }, k1 stored once400 reference_not_foundbefore the writeThe form door does not reach the post-write path live: it runs as the anonymous caller even with a session cookie, and the reference check refuses a master-detail parent that caller cannot see, before the write, on base and head alike. Its post-write answer is pinned on a real store by the REST test above. The head boot logged one
errorline forqsf_parent.probe_sf.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 71 commands on the diff (63 before the engine-double ledger change widened it); all 71 exit 0 (check:engine-double-contractfirst read exit 1 on the new MCP double, fixed by routing it through the engine dispatch asserts and--write), and--ranreconciles 71 derived, 71 run, 0 NOT-MEASURED.pnpm --filter @objectstack/spec check:error-code-provenance: exit 0. Scoped eslint over the 12 changed.tsfiles: exit 0.pnpm check:nul-bytes: exit 0.tsc --noEmitofmetadata-protocol,rest,runtime,mcp: exit 0 each. After the final merge ofmain(a4d7f538):check:engine-double-contractandcheck:error-code-provenanceexit 0, closures rebuilt, the new and #28781 door files green (rest 14, runtime 9, mcp 36 with the stdio exposure suite),metadata-protocoltypecheck exit 0; the full dogfood above ran on6e3035f3, before that merge.Generated by Claude Code