Uh oh!
There was an error while loading. Please reload this page.
fix(approvals): refuse loudly when a successful mutation's read-back is org-filtered out - #13181
fix(approvals): refuse loudly when a successful mutation's read-back is org-filtered out#13181os-elon wants to merge 2 commits into
Conversation
…-filtered out Fixes#12769 The ten result sites in ApprovalService (decide/decideNode, recall, sendBack, resubmit, reassign, remind, requestInfo, comment) read the row they just mutated back through the caller's org narrowing and asserted the result non-null with `fresh!`. For an org-less request row — which every schedule / time-relative / api trigger run produces by construction (#10131; pinned rather than repaired by #9132) — an org-scoped caller's read-back matches nothing, and the assertion shipped a success envelope whose declared-non-null `request` was `null`. `readBackRequest` now throws `READ_BACK_FAILED: …` when the read-back returns nothing: the write is recorded, the echo cannot be built, and that state is reported as the loud refusal it is instead of a malformed 200. All ten `fresh!` assertions are gone; the declared result types are untouched and now always true. The caller-org narrowing in `loadRequest` (tenancy wall) is deliberately unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
The TEST_DEBT re-measure type-checks this package's test layer under lib ES2021, where Array.prototype.at is a TS2550 — the new case would have raised the frozen pile 345 -> 346. Patch changeset for the behaviour change (loud READ_BACK_FAILED refusal instead of 200 with request:null). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 21d5a357e086e804de2c03e83de670929377ad6f && git checkout 21d5a357e086e804de2c03e83de670929377ad6f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 596b20a6ac26c94e185f0fb36b7b5f30cb56e7d0 b096be1ec0ea3ca70d972789bad65d431cb1df2f && git checkout -B drift-repro 596b20a6ac26c94e185f0fb36b7b5f30cb56e7d0 && git merge --no-ff b096be1ec0ea3ca70d972789bad65d431cb1df2f
node scripts/docs-audit/affected-docs.mjs --json 596b20a6ac26c94e185f0fb36b7b5f30cb56e7d0
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12769
What
ApprovalServiceread the row a write path had just mutated back through the caller's organization narrowing and asserted the result non-null at ten result sites (request: fresh!) — decideNode (both legs), recall, sendBack (both legs), resubmit, reassign, remind, requestInfo, comment. For an org-less request row, an org-scoped caller's read-back matches nothing, so a call that SUCCEEDED shipped a success envelope whose declared-non-nullrequestwasnull— HTTP 200 with a nullrequestthrough the REST pass-through, and the card's measured client crash on readingrequest.status.Org-less rows are live by construction: every schedule / time-relative / api trigger run produces one (#10131; #9132 deliberately pinned that behaviour rather than repairing it, and the write-side repair waits on the #8778 ruling). Re-confirmed on this branch's merge base: ten
request: fresh!sites (reverse control:readBackRequestspelled 11 times), all sharing one unguarded shape, none pre-guarded.The fix — the loud-refusal branch, per the dispatch ruling
readBackRequest— the single private helper behind all ten sites — now throwsREAD_BACK_FAILED: ...when the read-back returns nothing. The message states that the write was recorded and is NOT rolled back, names the request id, and points at the org-scope cause. All tenfresh!assertions are deleted; the helper's return type loses its null arm.ApprovalRecallResultand siblings live inpackages/spec(enumerated in the published surface manifest), which is out of this lane's file surface — and no spec edit turned out to be needed: the declared non-nullrequestis now always true, because a result that cannot be built is never returned. The spec contract already documents this family's throw-instead-of-malformed-result pattern (the RESUME_FAILED / RESUME_TARGET_LOST precedent on these very methods); the new throw follows it.loadRequest(tenancy wall) is byte-identical.handleApprovalErrormapping (packages/restis outside this card's declared surface). Unmapped, it reaches the client through each route's existing terminal 500 arm —APPROVAL_RECALL_FAILEDand siblings, already ledger-registered codes — with theREAD_BACK_FAILED:sentence in the body. Whether the prefix deserves its own mapped row (status + wire code + error-code-ledger entry) is flagged for the contract-review seat in the report.Tests
New describe block in
approval-service.test.ts(6 cases), reusing the file's existing fake engine — no new double, the engine-double ledger is untouched:READ_BACK_FAILED:prefix. The decide case uses an ordinary org-scoped slot holder — no admin override involved, so the miss is not an override-only corner.Reverse verification, from the committed state: with the pre-fix
approval-service.tsrestored from the merge base (tests kept), exactly the 4 refusal cases fail — the calls RESOLVE with a nullrequestinstead of throwing — while both controls and all 292 pre-existing cases stay green. The restore was then verified by hashing the work-tree file against the HEAD blob (match).Verification
At head b096be1, clean tree:
pnpm --filter @objectstack/plugin-approvals test— Test Files 34 passed, Tests 632 passed.pnpm --filter @objectstack/plugin-approvals typecheck— clean. Measured with tsc --listFiles: the package's tsc programs includeapproval-service.tsand exclude the test layer, so for the test file this typecheck is NOT a reading; the test layer is covered by the TEST_DEBT re-measure below and at runtime by vitest.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, 3 paths vs merge base 1a540e8): all 27 matched families plus the convention-triggered ones run locally and green, with two NOT MEASURED locally, named:scripts/pm/check-half-states.mjs(exit 3, PREREQUISITE NOT MET — needs a real GitHub credential this container does not hold; CI's half-state patrol carries it) andscripts/check-test-completeness.mjs(consumes CI's turbo test-log artifact; usage refusal locally — the gate body never ran).pnpm check:type-check-debtwas the one real local red on the first pass: the new test's.at(-1)raised the frozen TEST_DEBT pile 345 to 346 under its ES2021 re-measure (TS2550). Repaired by spelling the index read; re-measured green — "none above its recorded number".eslint.config.mjsand enables type-aware linting for NO file (the config's own recorded measurement);pnpm exec eslint --no-inline-config --format jsonover the two changed TS files reports 2 files linted, 0 errors, 0 warnings. With no type-aware linting, this diff cannot move any untouched file's verdict; the repo-widepnpm lintremains CI's run.check-adr-0087-registrationgreen — "this PR adds no declared-breaking changeset". The loud-refusal route changes no published type, so no ADR-0087 registration is due.Changeset: patch on
@objectstack/plugin-approvals.Landing posture
Clause-② card (accept/reject behaviour change on a published contract's method family): this PR parks as a DRAFT at the contract-review gate by design. The dispatching seat is below the review tier, so the review chain belongs to another seat — please do not flip it ready or arm auto-merge from this lane. If
needs:contract-reviewis applied now that a reviewable diff exists, that is the intended flow.Related cards, all deliberately left as they are: #12775 waits behind this card in the same file; #9132 (merged) pinned the org-less write side; the #8778 ruling is still pending elsewhere; #10131 is prior history. None of them is affected by this PR's landing.
Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code