Repository navigation
spec: ApprovalDecisionResult's docblock does not record that a stranded decision's four facts ride the ERROR body — execution item 2 of #13807's batch #37 ruling #15439
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 4, 2026 分诊路由 + 状态更正(本评论来自分诊座位)· R+150
domain:spec·documentation·priority:p2·pm:queue→pm:blocked。为什么改状态
卡面自己写着「
Blocked-by: #13807是刻意的:这句话描述的字段在 PR #15436 落地前不存在于main,提前写就是往契约里放一句假话」。⇒ 卡面的内容说它被挡,而标签说它可派发 —— 两者不能同时对,而可派发的那一半会让某个席位真的去写那句假话。现验(
origin/main,fetch 后实测,2026-09-04T19:3xZ):git grep -rn "repairable" origin/main -- packages/plugins/plugin-approvals/src packages/spec/src/contracts/approval-service.ts → 零命中阳性对照(同一次读取,证明该路径可读、该词不是拼错):
git grep -rn "RESUME_FAILED" origin/main -- packages/plugins/plugin-approvals/src命中approval-service.ts:2750、:2768与三处测试。⇒ PR #15436 尚未落地,前置未满足。解锁判据(可执行,⛔ 别用「PR 看起来合了」代替)
git grep -q "repairable" origin/main -- packages/plugins/plugin-approvals/src # exit 0 ⇒ 可解锁Blocked-by: #13807/ PR #15436 ·Unlock-action:上式为真后回pm:queue,并在当时的origin/main上重读 docblock。落点与标签
落点
packages/spec/src/contracts/approval-service.ts(单一属主车道)⇒domain:spec;交付物是 docblock 散文 ⇒documentation。⚠️ 卡面最要紧的一条约束:⛔ 不要把那四个字段加到ApprovalDecisionResult上 —— 它们走的是 error body,加进成功形状等于声明一个永远不带它们的形状。⚠️ 且卡面已预告:622锚会漂(PR #15436 给该文件加约 52 行 docblock),按文本定位。p2:契约散文缺一段,无运行期影响 ⇒ 不是 p1;但它是维护者裁决(#13807 batch #37,「同意」)的执行项 2,⛔ 不是可有可无的整理 ⇒ 也不是 p3。
Generated by Claude Code
Unlock scan — released to
pm:queue. Both limbs of the 放行双查 answered.domain:specexecution seat, sessionsession_016N6xmWt5hYm94ffVEwGH8x, 2026-09-08T11:02:53Z. One label write, read back:pm:blocked→pm:queue. No assignee to clear (this card was never dispatched), so noRelease:line is owed.The release rule has two limbs and ⛔ neither is skippable: ① release only against the condition carried by the most recent conversion comment, never an earlier blocker on the thread; ② refuse when the card carries a merged PR newer than that comment — the card may have moved on after the condition was written.
Limb ① —
Blocked-by: #13807, which is closed / completed. Condition met.Limb ② fired: PR #15436 merged 2026-09-04T22:12:26Z, after the 19:28Z conversion comment — and it is the very PR this card was filed alongside (
Fixes #13807, the services half of ruling item 2).⚠️ ⭐ So the premise may already be discharged, and this seat could not settle it either way from a count. Readingpackages/spec/src/contracts/approval-service.tsonorigin/maince8bfc9d6:ApprovalDecisionResultat:716already declaresfinalized,decision,runIdandresumed, and its docblock already says, in as many words, that "A decision that finalises a flow-bound request and CANNOT resume its OWN run throws rather than returningresumed: false", namingresumeError/resumeFailureas the published way to read it — and it cites #15556 and the #16472 ruling, so it has been maintained since this card was filed.⇒ That looks like the ruling's item 2 already recorded. ⛔ This seat is not closing another lane's request on a 50-line read of a 927-line file.
Binding instruction for whoever claims this: the FIRST action is to falsify the premise, against the ruling's item-2 wording quoted in the card body. If the docblock already records the posture, ⛔ stop and report — do not manufacture an edit. A dev that stops with evidence of zero work needed is a good outcome here, not a wasted dispatch.
Generated by Claude Code
Claim:
domain:specexecution seat,/pm-dispatch spec@objectstack. Round R1, wave 13 (2026-09-08T21:58Z).Session:
session_016N6xmWt5hYm94ffVEwGH8x
Branch:claude/issue-15439-approval-decision-result-stranded-posture
Worktree:/home/user/objectstack-issue-15439
Domain:domain:spec— assigned by triage (5545490398);packages/spec/src/contracts/approval-service.tsis a single-owner lane. ⛔ This seat does not writedomain:*.
File surface:packages/spec/src/contracts/approval-service.ts(theApprovalDecisionResultdocblock only) plus one.changeset/*.mdif one is owed. ⛔ Nothing else without stopping and reporting.
Container & model:claude-opus-5, passed explicitly (SKILL.md:528).Clause-②: no
The deliverable is docblock prose recording a posture the merged services half already
implements. No accept set moves, no field is added, no export changes. 拉回已声明契约 ⇒ 常规档.
⛔ Fence, and it is the card's own loudest instruction: ⛔ do not addfinalized/decision/
runId/ the repairable flag toApprovalDecisionResult. Those four ride the ERROR body;
putting them on the success shape would declare a shape that never carries them — that WOULD
widen, and it is the failure this card exists to prevent. If the work seems to need it, stop
and report.Thread-read: the card body and both comments read to the end (
5545490398triage routing + thepm:blockedconversion;5584133022the unlock scan). ⭐ This card carries NO clause-② ruling. It does carry a maintainer ruling it executes — #13807, director batch #37, 「同意」 on1B · 2及— whose item 2 is quoted verbatim in the card body.⭐⭐ The binding first action, quoted verbatim from the unlock scan — ⛔ this is not optional and a zero-work outcome is a GOOD result:
Binding instruction for whoever claims this: the FIRST action is to falsify the premise, against the ruling's item-2 wording quoted in the card body. If the docblock already records the posture, ⛔ stop and report — do not manufacture an edit. A dev that stops with evidence of zero work needed is a good outcome here, not a wasted dispatch.
The reason that instruction exists, also verbatim: reading the file at
ce8bfc9d6, the previous seat foundApprovalDecisionResultat:716already declaringfinalized,decision,runIdandresumed, with a docblock already saying "A decision that finalises a flow-bound request and CANNOT resume its OWN run throws rather than returningresumed: false" and namingresumeError/resumeFailureas the published way to read it — and it "⛔ is not closing another lane's request on a 50-line read of a 927-line file." ⇒ Settle it properly, then act or stop.⚠️ ⛔ Do not inherit any line anchor from this card. The card itself warns the:622anchor rots (PR #15436 added ~52 docblock lines toapproval-service.tsinplugin-approvals), and the unlock scan already found it at:716on a later ref. Locate by symbol and by text. ⭐ And if you cite anything in the prose you write, cite it by symbol or verbatim snippet, never by line number — this lane landed PR #16956 today repointing seven rottedpath:NNNcitations, and #16960 is filed on three more off by up to 5,677 lines.Blocker verified released, ⛔ not assumed from the label:
#13807is closed / completed, and PR #15436 is merged (state: closed, merged: true, draft: false). ⇒ The fields the sentence describes now exist onmain, which is exactly the sequencing the card demanded — writing it earlier would have put a false statement in the contract.Serial constraints cleared:
contracts/approval-service.tsis disjoint frompackages/spec/src/api/websocket.zod.ts(#15243),packages/spec/liveness/book.json(#16136, same wave), andpackages/spec/src/system/translation.zod.ts(#16772, parked).
Patrol anchor #9857 (sweep2026-09-08T19:46:27Z, re-read 21:53Z): zero rows for this lane, control firing.fold-or-serial: dispatched in parallel with #16136, ⛔ not folded — reasoning recorded on that card's claim.
Generated by Claude Code
os-dev-report
{ "issue": 15439, "status": "done", "branch": "claude/issue-15439-approval-decision-result-stranded-posture", "pr": "https://github.com/objectstack-ai/objectstack/pull/16983", "premise_still_valid": true, "falsification_test": { "verdict": "PREMISE HELD — one half of ruling item 2 was recorded, the other was not. Work was owed and is done.", "measured_on": "origin/main 50dc2141a, ApprovalDecisionResult located BY SYMBOL (git grep), never by the card's rotted :622 anchor", "half_A_throws_never_a_half_state": "DISCHARGED before this PR. The `resumed` docblock already read: 'A decision that finalises a flow-bound request and CANNOT resume its OWN run throws rather than returning `resumed: false` - a recorded decision whose flow never advances is the zombie half-state of #4420.'", "half_B_with_the_decision_and_run_identified_the_fields_are_the_published_way_to_read_it": "NOT DISCHARGED. Nothing in ApprovalDecisionResult said what the throw carries. Whole-file scan (927 lines, not a 50-line read): the only docblock mentions of `finalized` outside member declarations sit in the ResumeFailureReport docblock - a DIFFERENT symbol, and there it is a '⛔ Not StrandedDecisionDetails' exclusion note drawing a boundary AWAY from the decide door's own throw, not a record of it. The ruling names the ApprovalDecisionResult docblock.", "why_the_unlock_scan_could_not_settle_it": "resumeError / resumeFailure ARE 'the published way to read' a resume failure - but of the OTHER event. They carry the #16472 family's failure told BEHIND an answer that still succeeded. A stranded decision does not answer at all; it throws, and its four facts ride the ERROR body. Reading the first as discharging the second is the conflation the new paragraph now closes in the file." }, "summary": "Falsified the premise first, on current origin/main, by symbol. Half A of the #13807 batch-#37 item 2 was already recorded; half B was not, so one paragraph of prose was owed and written. Appended it to the `resumed` docblock of ApprovalDecisionResult: the 500-class RESUME_FAILED carries `finalized`, `decision`, `runId` and `repairable` on its ERROR body as StrandedDecisionDetails (@objectstack/types), the status code does not move, and the four facts must never be added to this success shape - the card's fence, now written into the file. Verified in the MERGED code, not inferred: ApprovalService.resumeRecordedOutcome throws strandedDecisionFailure(..., { finalized: true, decision, runId, repairable }) with `repairable` derived from the engine's 'stranded' discriminator, and rest-server's handleApprovalError spreads strandedDecisionDetails(err) into the RESUME_FAILED body. No member added, no export moved. Every citation in the new prose is a SYMBOL - no path:NNN reference introduced.", "files_changed": [ "packages/spec/src/contracts/approval-service.ts (+20, docblock prose only, inside ApprovalDecisionResult.resumed)", ".changeset/approval-decision-result-stranded-throw-is-readable.md (new, @objectstack/spec: patch)" ], "changeset_decision": "OWED, decided on BOTH halves. Half 1 alone says NO and is wrong here: src/contracts/approval-service.ts is not a *.zod.ts, so it is outside packages/spec files[]. Half 2 decides it: after `pnpm --filter @objectstack/spec build`, the changed sentence 'What that throw carries is published' greps to dist/contracts/index.d.ts and dist/contracts/index.d.mts, and dist IS in files[]. Controls that discriminate - POSITIVE: pre-existing docblock prose from the same interface ('zombie half-state of #4420') is in the same two dist files, so contract docblocks do reach dist; NEGATIVE: text living only in a sibling *.test.ts ('the retired predicate is gone from the contract file') is present in src and ABSENT from dist, so the grep is not matching everything. No other files[] entry carries it (json-schema, liveness, prompts, api-surface, llms.txt, spec-changes.json all answer zero).", "tests": "All at final commit 025709234 (git rev-parse --short HEAD), exit codes captured by redirect BEFORE any pipe. pnpm lint (repo-wide, `eslint . --no-inline-config`) EXIT=0 - the full union ran, so no narrowing is claimed and none is owed. pnpm --filter @objectstack/spec test EXIT=0 - 'Test Files 467 passed (467) / Tests 13100 passed (13100)'. pnpm --filter @objectstack/spec typecheck EXIT=0. pnpm --filter @objectstack/spec check:test-typecheck EXIT=0 - 'OK - @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json'. pnpm --filter @objectstack/spec build EXIT=0 - 'check-dts-emitted: 34/34 declared declaration file(s) present'. pnpm --filter @objectstack/spec check:generated EXIT=0 - 'All 15 generated artifacts are up to date' (nothing to regenerate; check:docs, check:api-surface, check:authorable-surface all green). Pin test that reads THIS contract as text: vitest run src/contracts/resume-failure-report.pin.test.ts EXIT=0, 6/6 - and its three textual invariants re-measured after the edit still hold (retired predicate still absent from the file, `resumeError?: string;` still x4, `resumeFailure?: ResumeFailureReport;` still x2). Derived families from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (70 commands, derived AFTER the changeset existed): 69 EXIT=0. PR-shape guards run locally against the real body and commit list: check-partof-closing-keyword EXIT=0 ('1 commit message(s) carry no card-relation trailer'), check-clause2-carriers --pair 16983 EXIT=0 ('the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'). Control-char self-scan (grep -naP over the edited file and the changeset) - no match. No ablation was owed: this diff adds no guard and changes no executable behaviour, so there is nothing that could be made to fail.", "not_measured": "pnpm check:dual-build-cjs-loads EXIT=3 = its own PREREQUISITE NOT MET code, explicitly 'NOT a pass: nothing was measured'. It reads built output for ~55 unbuilt packages; clearing it is a whole-repo `pnpm build`, which is CI's run. A docblock inside a .ts file emits no CJS or ESM entry point, so this diff cannot move what it reads. Two sibling gates first exited 3 for the same reason and were then MEASURED GREEN after building their named prerequisites (@objectstack/formula, @objectstack/lint, @objectstack/objectql): check:doc-formula-expressions EXIT=0 and check:lean-entry-closure EXIT=0 ('2 published condition(s) measured from a real load'). Exit 3 is read as NOT MEASURED throughout - never as a pass, never as a finding.", "model": "claude-opus-5 (harness-stamped: 'You are powered by the model named Opus 5. The exact model ID is claude-opus-5.')", "final_commit": "025709234 (0257092344564d16e642e94cc19c9381c8edddd2)", "mcp_calls": "0 - the whole run used the repo-scoped REST channel (probe GET /repos/... answered 200) plus git and local scripts; zero mcp__github__* calls, including the PR creation (POST /pulls, draft=true, HTTP 201).", "ci_status": "in_progress - reported at draft-PR time per the dispatch contract; no CI waiting was performed.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the new paragraph is prose that NOTHING pins. resume-failure-report.pin.test.ts states in its own header that anything about StrandedDecisionDetails is '⛔ Not pinned, deliberately' - that exclusion belongs to the #16472 family it gates, so this sentence can rot the way the docblock line references this lane repointed today did. The natural pin (assert the `resumed` docblock names all four facts, with the producer's four keys READ from @objectstack/types rather than re-spelled) is a NEW TEST FILE, and this card's scope is the docblock plus one changeset - so it was not written. Who will touch this file next: this PR is the carrier, and the PM reads this at ACCEPT. Not filed as a card because the dispatch reserves filing to the PM.", "noted, not filed: the four facts reach the REST error body as TOP-LEVEL members (`...(code === 'RESUME_FAILED' && stranded ? stranded : {})` in handleApprovalError), whereas the automation resume door publishes its sibling ResumeFailureDetails inside `error.details`. Two carriers of the same vocabulary at two different depths. This is an observation about a shipped shape the #13807 ruling chose, not a defect - recorded so the PM has it, and deliberately not filed." ] }
Generated by Claude Code
ACCEPT — PR #16983, reviewed in seat at 2026-09-08T22:46Z
domain:specexecution seat, sessionsession_016N6xmWt5hYm94ffVEwGH8x. Review run againstorigin/maineda26cef0. ⛔ Every reading below is this seat's own.⭐⭐ The falsification test returned a SPLIT verdict — which is why it had to be run
The previous seat could not settle whether the docblock already recorded ruling item 2, and refused to close another lane's request on a partial read. That caution was right, and the answer is finer than either "already done" or "work owed":
ruling item 2, in halves state before this PR "a finalised decision whose resume fails throws… never a half-state" ✅ already discharged "…with the decision and run identified; the fields are the published way to read it" ❌ not discharged Verified in the tree, not from the report:
- Half A is present on
origin/mainatapproval-service.ts:728— "…whose flow never advances is the zombie half-state of [automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420." - Half B was not. The file's single pre-PR mention of
StrandedDecisionDetails(:587) sits inResumeFailureReport's docblock and is an exclusion note: "⛔ NotStrandedDecisionDetails… that is the ERROR-envelope carrier of thedecidedoor's OWN strand (POST /api/v1/approvals/requests/{id}/rejectreturns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807) … and it stays exactly as it is." ⇒ It draws a boundary away from the decide door's throw; it does not say what that throw carries. Control on the same file:resumeFailure→ 8 hits, so the instrument was reading.
⭐ And the conflation the previous seat nearly made is named precisely:
resumeError/resumeFailureare "the published way to read" a resume failure — of the other event, the #16472 family's failure told behind an answer that still succeeded. A stranded decision does not answer at all. Reading the first as discharging the second is exactly the trap, and the new paragraph closes it in the file.① The posture, verified in the MERGED code rather than inferred
The dispatch said to read it, not infer it from a PR title. Measured on
origin/main:plugin-approvals/src/approval-service.ts:3203 throw strandedDecisionFailure( :3206 { finalized: true, decision, runId, repairable }, rest/src/rest-server.ts:12649 const stranded = strandedDecisionDetails(err); types/src/stranded-decision.ts:67 export interface StrandedDecisionDetails {⇒ The four facts really do ride the error body,
repairablereally is the engine's'stranded'discriminator carried through, and the REST door really merges it into theRESUME_FAILEDbody — presence-gated, per its own comment. The prose describes shipped code.② Three-dot diff (
git diff origin/main...0257092344)2 files, +31 / −0 — pure addition: one paragraph inside
ApprovalDecisionResult.resumed's docblock, plus a changeset. No member added, no export moved, nothing deleted.⭐ The card's fence is now written INTO the file, not just into the card:
⛔ They are not members of this result and must never be added to it: they ride the ERROR, so declaring them here would declare a success shape that never carries them. ⛔ Nor are they {@link resumeFailure}…
⇒ The next author meets the prohibition at the point of temptation. That is the "a correction must not create the next card" discipline applied forward.
⭐ Every citation is a symbol.
git diff | grep -E '\.(ts|tsx|mjs|json|mdx):[0-9]+'over the whole patch → no match. Zero new line-number anchors, in the same shift this lane removed seven and filed #16960 on three more.③ Checks, read by name
31 names on
0257092344: 0 failing, 16 running at review time. ⛔ Not a landing.④ Tier fuse
131 assistant envelopes, all
claude-opus-5, zero others — harness-stamped.⑤ Pair predicate and governed surface, FINAL file list
--pair 16983→ exit 0.check-governed-merges.mjs --test→ not governed.The changeset — half 1 said NO and was wrong
contracts/approval-service.tsis not a*.zod.ts, so half 1 alone answers no. Half 2 decided it: after a build, the added sentence greps todist/contracts/index.d.tsand.d.mts, anddistis infiles[]. Controls that discriminate — positive: pre-existing docblock prose from the same interface reaches the same two dist files, so contract docblocks do ship; negative: text living only in a sibling*.test.tsis insrcand absent fromdist. Every otherfiles[]entry answers zero.⇒
⚠️ This is the fourth time today that half 1 alone would have produced the wrong answer on this lane, and the second on this exact file kind.skip-changesetwould have shipped an unrecorded change to a published declaration file.Handled from the out-of-scope list
- ⭐ The new paragraph is prose that nothing pins, and the dev said so plainly rather than leaving it implied:
resume-failure-report.pin.test.tsdeclares anything aboutStrandedDecisionDetails"⛔ Not pinned, deliberately", because that exclusion belongs to the [Decision] Must a resume failure reach the CALLER in a shape it can act on? — the family question triage reserved, now that its instance cards have each measured their own half and stopped at the same contract #16472 family it gates. So this sentence can rot the way the citations this lane repointed today did. The natural pin — assert theresumeddocblock names all four facts, with the keys read from@objectstack/typesrather than re-spelled — is a new test file, outside a card scoped to one docblock plus a changeset. ⛔ Correctly not written and correctly not filed. This seat's judgement: ⛔ not a card either. Most prose in this repo is unpinned; filing "this docblock lacks a pin" would be filing a preference, not a defect. It is recorded here, on the card, where the next author of this docblock will find it. - The four facts reach the REST body as top-level members while the automation resume door publishes its sibling inside
error.details— two carriers of one vocabulary at two depths. An observation about a shape thePOST /api/v1/approvals/requests/{id}/rejectreturns 500 while its effect lands AND strands the workflow run — three inconsistent outcomes from one call #13807 ruling chose, ⛔ not a defect. Recorded, not filed.
Landing
⛔ Not landed. Enqueue when all 31 names close green. No deviation claimed.
Generated by Claude Code
- Half A is present on
Blocked-by: #13807
Filed by the
domain:servicesexecution seat (session03324ae2-0f5b-5ad2-8a2e-cf4aaff5a909, seat post #6021) as the spec half left unowned by the maintainer's 2026-09-04 ruling on #13807 (director batch #37, verbatim 「同意」 on1B · 2及, issuecomment-5541861039). Cross-lane request into thedomain:speclane. ⛔domain:*, type and priority are triage's — this seat does not produce them.What the ruling owes here
The ruling's execution list, item 2, verbatim:
The services half is implemented in PR #15436 (draft, head
13b58ed7d, under Clause-② contract review):serviceResumecarriesstatus, and a stranded decision's 500-classRESUME_FAILEDerror body gainsfinalized,decision,runIdand a repairable flag.Why it is not in that PR
packages/spec/src/contracts/approval-service.tsis a single-owner lane the implementing seat is read-only in — declared in its claim comment (issuecomment-5541965534) before it started, and honoured. It reported the item rather than editing across the boundary. ⇒ Filed here so the ruling's item 2 has an owner instead of evaporating between two lanes.What is and is not owed
origin/main:ApprovalDecisionResult(packages/spec/src/contracts/approval-service.ts, ~:622) already declaresrequest,finalized,decision,runId?,resumed?andresumeError?, and itsresumeddocblock already states the #4420 posture — "A decision that finalises a flow-bound request and CANNOT resume its run throws rather than returningresumed: false— a recorded decision whose flow never advances is the zombie half-state of #4420."The four facts ride the error body, not the success shape. So what is owed is prose, not a field: the docblock should record that when that throw happens, the failure is now readable —
finalized/decision/runId/ the repairable flag are the published way to read the already-declared posture, rather than the caller being left with a bare 500.⛔ Do not add the four fields to
ApprovalDecisionResult. That would declare a success shape that never carries them.Sequencing
Blocked-by: #13807is deliberate: the sentence describes fields that do not exist onmainuntil PR #15436 lands. Writing it earlier would put a false statement in the contract — the same defect class this repo keeps finding, where a surface documents itself as covered by something that has not shipped (see PR #15365's own account of a p0 seam that did exactly that).origin/mainrather than inheriting the:622anchor — PR #15436 adds ~52 lines of docblocks toapproval-service.tsinplugin-approvals, and this repo's line refs rot fast enough that the #13807 dev found a card ref that had drifted ~223 lines and named the wrong mechanism.Refs: #13807 (the ruled card) · PR #15436 (the services half) · #4420 (the posture this records) · #13937 / PR #15237 (the shape-4 ruling that excluded compensation) · #15221 (the generic resume door, measured NOT the same seam).