From 7aa1b67a644a30a3094ea53e975c3a7117a312fd Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 15:28:12 -0500 Subject: [PATCH 1/4] Resolve a retained decision hold and resume its original owners Add Ticket.ResolveDecision and resolve-decision.md. A ticket-decision.v1 request binds the work, exact held continuation and held result to an accept/amend disposition with decision, provenance and accepted-input descriptors. Resolution validates the held result, reserves a receipt under the hold's continuation digest, prepares the original owner selected by the result and publishes the successor without calling a role; ordinary continue.md dispatches it. Confirmed repeats return the recorded successor, conflicting decisions and excluded holds refuse, and interrupted reservations stay held. Eligible decision holds advertise the resolve-decision entry. DR1-DR7 acceptance scenarios and a native identity witness cover routing, refusal, interruption and inherited input boundaries. Refs #1165 Co-Authored-By: Claude Opus 5.5 --- examples/adversarial-implementation/README.md | 75 +++++++++ .../components/Entry/ResolveDecision.md | 19 +++ .../components/Ticket/Advance.md | 3 +- .../components/Ticket/Decision.ts | 110 ++++++++++++ .../components/Ticket/Publish.ts | 36 +++- .../components/Ticket/ResolveDecision.md | 104 ++++++++++++ .../components/Ticket/control.ts | 19 ++- .../resolve-decision.md | 11 ++ .../tests/DecisionResolution.test.md | 40 +++++ .../tests/agents/decision-planner-held.md | 4 + .../tests/agents/decision-planner.md | 9 + .../components/Fixture/DecisionBoundary.md | 95 +++++++++++ .../tests/components/Fixture/DecisionHold.md | 36 ++++ .../Fixture/DecisionInterruption.md | 157 ++++++++++++++++++ .../components/Fixture/DecisionJourney.md | 108 ++++++++++++ .../components/Fixture/DecisionRefusal.md | 97 +++++++++++ .../components/Fixture/DecisionRequest.md | 30 ++++ .../tests/components/Fixture/Execution.md | 5 +- .../components/Fixture/InitialPlanning.md | 2 +- .../tests/components/Fixture/Refused.md | 20 +++ .../tests/components/Phase/Role.md | 7 +- .../tests/decision-cut.md | 13 ++ .../tests/decision-native-cases.md | 67 ++++++++ .../tests/decision-native-journey.md | 32 ++++ .../adversarial-decision-resolution.test.ts | 7 + packages/cli/tests/fixtures/decision-cut.mjs | 28 ++++ .../cli/tests/support/adversarial-journey.ts | 2 +- scripts/tests/runtime-exclusions.test.ts | 1 + 28 files changed, 1125 insertions(+), 12 deletions(-) create mode 100644 examples/adversarial-implementation/components/Entry/ResolveDecision.md create mode 100644 examples/adversarial-implementation/components/Ticket/Decision.ts create mode 100644 examples/adversarial-implementation/components/Ticket/ResolveDecision.md create mode 100644 examples/adversarial-implementation/resolve-decision.md create mode 100644 examples/adversarial-implementation/tests/DecisionResolution.test.md create mode 100644 examples/adversarial-implementation/tests/agents/decision-planner-held.md create mode 100644 examples/adversarial-implementation/tests/agents/decision-planner.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionBoundary.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionJourney.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionRefusal.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionRequest.md create mode 100644 examples/adversarial-implementation/tests/components/Fixture/Refused.md create mode 100644 examples/adversarial-implementation/tests/decision-cut.md create mode 100644 examples/adversarial-implementation/tests/decision-native-cases.md create mode 100644 examples/adversarial-implementation/tests/decision-native-journey.md create mode 100644 packages/cli/tests/adversarial-decision-resolution.test.ts create mode 100644 packages/cli/tests/fixtures/decision-cut.mjs diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 80c727298..a182a0f3f 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -535,6 +535,81 @@ custom/workflow provider storage contract or migration of old inline records. External reviewer models and publication use their existing workflows. The phase entry retains the implementation workflow's role and model policy. +### Resolve a decision hold + +A Planner or Architect that returns `needs_decision`, and an Implementor that +returns `needs_discovery` naming the Architect, hold the ticket for an accepted +decision. When the hold is at the clean launch revision before implementation +feedback, its envelope names the supported operation: + +```json +{"entry": "resolve-decision.md", "gate": "planning", "action": "resolve_decision", + "result": "/absolute/path/to/work/attempts/0000/result.json", "input": "ticket-decision.v1"} +``` + +Save the decision and its provenance as text files, then save one request that +binds them to this exact hold. Every field is required and no other field is +accepted: + +```json +{ + "schema": "ticket-decision.v1", + "work": "selected-task", + "continuation": {"path": "/absolute/path/to/work/continuations/HASH.json", "utf8Bytes": 13240, "sha256": "HASH"}, + "heldResult": {"path": "/absolute/path/to/work/attempts/0000/result.json", "utf8Bytes": 2210, "sha256": "RESULT_HASH"}, + "disposition": "accept", + "decision": {"path": "/absolute/path/to/decision.md", "utf8Bytes": 412, "sha256": "DECISION_HASH"}, + "provenance": {"path": "/absolute/path/to/provenance.md", "utf8Bytes": 96, "sha256": "PROVENANCE_HASH"}, + "acceptedInputs": [] +} +``` + +`continuation` copies the held envelope's descriptor and `heldResult` the +retained descriptor of its final call's result. Each artifact descriptor names an +absolute path and its exact UTF-8 byte length and SHA-256. The decision and +provenance are always selected for the routed owner, followed by +`acceptedInputs` in order. A recorded reference keeps its first text; give changed +text a fresh path. Provenance states the actual authority, such as standing +coordinator authority. It is evidence for the routed owner; it neither overrides +a permission nor records a new human answer. + +```bash +deno task xmd run --allow-absolute examples/adversarial-implementation/resolve-decision.md \ + --props-record /absolute/path/to/held-record.json \ + --props-decision /absolute/path/to/decision.json \ + --include examples/adversarial-implementation/components --raw +``` + +Resolution calls no role. It validates the held result, records the decision under +the hold's continuation digest, prepares the original owner and publishes the +successor. Continue the returned envelope with `continue.md --props-steps 1`; that +ordinary step dispatches the owner through the existing source, identity, input +and publication checks: + +| Held result | Disposition | Next owner | Then | +| --- | --- | --- | --- | +| discovery Planner `needs_decision` | `accept` | original Planner | original Implementor | +| discovery Planner `needs_decision` | `amend` | original Architect | retained Planner, then original Implementor | +| discovery Architect `needs_decision` | `accept` | same Architect | retained Planner, then original Implementor | +| implementation Implementor `needs_discovery` naming the Architect | either | original Architect | retained Planner, then original Implementor | + +The original task, sessions, calls, results, delivered inputs, source anchor and +correction count are unchanged; resolution grants no further corrections. Every +other hold refuses, including an Architect amendment, an API conflict routed to +the Planner, execution failure, uncertain native work, changed input, source +mismatch, an exhausted correction bound and publication failure. A dirty or moved +checkout, prior implementation feedback, a stale or foreign binding and a changed +input also refuse. A refusal returns a held envelope carrying the failure and +leaves the managed ticket unchanged; keep the original envelope. + +Repeating the same request against the same hold returns its recorded successor +without publishing again. A different decision for a resolved hold refuses. If +reservation, publication or the receipt is interrupted, the hold stays held and +its reservation directory preserves the first failure for operator disposition. A +matching successor file alone does not confirm the interrupted write. Repeated +continuation of an older envelope reuses the existing call reservation and resends +no role effect. + ## Supervise a live REPL phase Keep the original ACP call pending, or reconnect passively with the existing diff --git a/examples/adversarial-implementation/components/Entry/ResolveDecision.md b/examples/adversarial-implementation/components/Entry/ResolveDecision.md new file mode 100644 index 000000000..8b09a79d8 --- /dev/null +++ b/examples/adversarial-implementation/components/Entry/ResolveDecision.md @@ -0,0 +1,19 @@ +--- +props: + type: object + properties: + record: {type: string} + decision: {type: string} + required: [record, decision] + additionalProperties: false +returns: {type: object} +--- + +{text} + + + +{resolved.output} + + !/^\s+at /.test(line)).join("\n").slice(0,1024),data:null}}} as="bounded" /> + diff --git a/examples/adversarial-implementation/components/Ticket/Advance.md b/examples/adversarial-implementation/components/Ticket/Advance.md index 892ec4ce4..68cb9cc64 100644 --- a/examples/adversarial-implementation/components/Ticket/Advance.md +++ b/examples/adversarial-implementation/components/Ticket/Advance.md @@ -14,6 +14,7 @@ returns: {type: object} --- A decision or contract conflict holds the ticket for its existing approval gate. +An implementation discovery hold keeps its validated next owner as a routing fact. A correction clears prior verdicts. Readiness requires every selected reviewer to PASS the same signed implementation SHA; it does not authorize delivery or merge. Selected feedback documents remain available across reviews and corrections. @@ -49,6 +50,6 @@ Selected feedback documents remain available across reviews and corrections. - 0 ? [{role:call.role,revision:result.revision,result:props.pending.resultPath,requests:result.discoveryRequests ?? [],gaps:result.discoveryGaps ?? []}] : [])],feedback:result.outcome === "complete" && call.role === "implementor" ? [props.pending.resultPath,props.observation.path] : state.feedback,hold:held ? {kind:correction ? "correction_bound" : result.outcome,reason:result.handoff} : null}} /> + 0 ? [{role:call.role,revision:result.revision,result:props.pending.resultPath,requests:result.discoveryRequests ?? [],gaps:result.discoveryGaps ?? []}] : [])],feedback:result.outcome === "complete" && call.role === "implementor" ? [props.pending.resultPath,props.observation.path] : state.feedback,hold:held ? {kind:correction ? "correction_bound" : result.outcome,reason:result.handoff,...(result.outcome === "needs_discovery" ? {next:result.next} : {})} : null}} /> diff --git a/examples/adversarial-implementation/components/Ticket/Decision.ts b/examples/adversarial-implementation/components/Ticket/Decision.ts new file mode 100644 index 000000000..c80e163e3 --- /dev/null +++ b/examples/adversarial-implementation/components/Ticket/Decision.ts @@ -0,0 +1,110 @@ +import { parseFilesFailure } from "@executablemd/runtime"; +import { z } from "zod"; +import { envelope, state } from "./control.ts"; +import { describe, descriptor, read, stateLimit, textLimit } from "./text-storage.ts"; + +export const props = { + type: "object", + properties: { + path: { type: "string", pattern: "^/" }, + record: { type: "object" }, + saved: { type: "object" }, + }, + required: ["path", "record", "saved"], + additionalProperties: false, +}; +const reference = descriptor.extend({ path: z.string().startsWith("/") }).strict(); +const text = reference.extend({ utf8Bytes: z.number().int().min(1).max(textLimit) }).strict(); +const request = z + .object({ + schema: z.literal("ticket-decision.v1"), + work: z.string().min(1), + continuation: reference, + heldResult: reference, + disposition: z.enum(["accept", "amend"]), + decision: text, + provenance: text, + acceptedInputs: z.array(reference), + }) + .strict(); +type Reference = z.infer; + +function same(left: Reference, right: { path: string; utf8Bytes: number; sha256: string }) { + return ( + left.path === right.path && left.utf8Bytes === right.utf8Bytes && left.sha256 === right.sha256 + ); +} + +function* readText(path: string, limit: number) { + try { + return yield* read("/", path, limit); + } catch (error) { + const failure = parseFilesFailure(error); + if (failure === undefined) { + throw error; + } + throw new Error( + "Cannot read " + + path + + " within " + + limit + + " UTF-8 bytes under current File permission: " + + failure.reason + + ". No resolution was recorded.", + ); + } +} + +export default function* Decision(value: unknown) { + const input = z + .object({ path: z.string().startsWith("/"), record: envelope, saved: state }) + .strict() + .parse(value); + const source = yield* readText(input.path, stateLimit); + const selected = request.parse(JSON.parse(source)); + const { saved } = input; + if (selected.work !== saved.manifest.work) { + throw new Error("The decision names another work item; no resolution was recorded."); + } + if ( + input.record.continuation === null || + !same(selected.continuation, input.record.continuation) + ) { + throw new Error( + "The decision is bound to another or stale continuation; bind it to this exact confirmed hold.", + ); + } + const call = saved.calls.at(-1); + const result = typeof call?.result === "string" ? saved.documents[call.result] : undefined; + if (result === undefined || !same(selected.heldResult, result)) { + throw new Error( + "The decision names another held result; bind it to the retained result of the final call.", + ); + } + const references = new Map(); + for (const entry of [selected.decision, selected.provenance, ...selected.acceptedInputs]) { + const prior = references.get(entry.path); + if (prior !== undefined && !same(prior, entry)) { + throw new Error("One decision reference names different contents: " + entry.path); + } + references.set(entry.path, entry); + } + for (const entry of references.values()) { + const retained = saved.documents[entry.path]; + const current = retained ?? describe(entry.path, yield* readText(entry.path, textLimit)); + if (!same(entry, current)) { + throw new Error( + (retained === undefined + ? "Decision input differs from its descriptor: " + : "A recorded reference keeps its first text; give changed text a fresh reference: ") + + entry.path, + ); + } + } + return { + request: selected, + file: describe(input.path, source), + references: [...references.keys()], + descriptors: Object.fromEntries(references), + }; +} diff --git a/examples/adversarial-implementation/components/Ticket/Publish.ts b/examples/adversarial-implementation/components/Ticket/Publish.ts index 018c67791..3437fe3b1 100644 --- a/examples/adversarial-implementation/components/Ticket/Publish.ts +++ b/examples/adversarial-implementation/components/Ticket/Publish.ts @@ -86,14 +86,40 @@ export default function* Publish(value: unknown) { remainingRequiredDeliveryWork: ["independent-review", "required-ci", "main-health"], } : {}; + const last = record.calls.at(-1); + const hold = record.checkpoint.hold; + const decision = + !record.checkpoint.dispatchable && + record.checkpoint.status === "hold" && + record.checkpoint.revision === record.manifest.assignment.revision && + record.checkpoint.feedback.length === 0 && + last?.status === "accepted" && + typeof last.result === "string" && + ((hold?.kind === "needs_decision" && + last.assignment.phase === "discovery" && + last.role !== "implementor") || + (hold?.kind === "needs_discovery" && + last.role === "implementor" && + hold.next?.phase === "discovery" && + hold.next.role === "architect")) + ? last.result + : null; const nextAction = record.checkpoint.status === "feedback_ready" && typeof deliveryDirectory === "string" ? { entry: "delivery.md", record: deliveryDirectory + "/requirements.json", action: "review" } - : { - entry: "continue.md", - gate: record.checkpoint.gate, - action: record.checkpoint.dispatchable ? "continue_feedback" : "resolve_feedback_hold", - }; + : decision !== null + ? { + entry: "resolve-decision.md", + gate: record.checkpoint.gate, + action: "resolve_decision", + result: decision, + input: "ticket-decision.v1", + } + : { + entry: "continue.md", + gate: record.checkpoint.gate, + action: record.checkpoint.dispatchable ? "continue_feedback" : "resolve_feedback_hold", + }; return { ...presentation, nextAction, diff --git a/examples/adversarial-implementation/components/Ticket/ResolveDecision.md b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md new file mode 100644 index 000000000..b796cca5b --- /dev/null +++ b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md @@ -0,0 +1,104 @@ +--- +description: >- + Record an accepted decision for one exact retained hold and publish the prepared + original owner without calling a role. Use , then continue the returned + envelope normally. +props: + type: object + properties: + record: {type: object} + decision: {type: string, pattern: '^/'} + required: [record, decision] + additionalProperties: false +returns: {type: object} +--- + +Resolution covers a decision or discovery hold at the clean unchanged launch +revision, before implementation feedback. The held result selects the original +owner: an accepted Planner decision returns to that Planner, an amended Planner +decision or an accepted Architect decision returns to the Architect, and an +Implementor's discovery hold returns to the Architect it named. The decision and +its evidence enter that owner's selected inputs; the original sessions, calls, +delivered inputs, source anchor and correction count stay unchanged. + +The reservation under the hold's continuation digest admits one resolution. A +confirmed repeat returns the recorded successor, and a different decision for that +hold refuses. A reservation without its confirmed receipt stays held: matching +files do not establish an unknown earlier write. + + + + + + + + + + + +```bash exec as="existing" +if test -e {quoted}/receipt.json; then printf confirmed; elif test -e {quoted} || test -L {quoted}; then printf reserved; else printf none; fi +``` + + + + + + +{receiptText} + + + + + + + + + + + saved.manifest.maxCorrections}> + + + + + + + + + + + + "'" + (saved.manifest.directory + part + id).replaceAll("'","'\"'\"'") + "'").join(" ")} /> +```bash exec as="unstarted" +for path in {unstartedPaths}; do if test -e "$path" || test -L "$path"; then exit 1; fi; done +``` + + +```bash exec as="reserved" +mkdir -p -- {parent} && mkdir -- {quoted} +``` + + + + + + + + + documents[path].utf8Bytes !== bound.descriptors[path].utf8Bytes || documents[path].sha256 !== bound.descriptors[path].sha256)}> + + + + + + + + + + + + + + + diff --git a/examples/adversarial-implementation/components/Ticket/control.ts b/examples/adversarial-implementation/components/Ticket/control.ts index 15a5331b7..cb7857367 100644 --- a/examples/adversarial-implementation/components/Ticket/control.ts +++ b/examples/adversarial-implementation/components/Ticket/control.ts @@ -43,6 +43,8 @@ export const envelope = z record: z.string().optional(), gate: z.string().nullable().optional(), action: z.string(), + result: z.string().optional(), + input: z.literal("ticket-decision.v1").optional(), }) .strict() .optional(), @@ -97,7 +99,12 @@ export const state = z .object({ architect: z.boolean(), planner: z.boolean(), implementor: z.boolean() }) .strict(), hold: z - .object({ kind: z.string(), reason: z.string(), receipt: z.string().optional() }) + .object({ + kind: z.string(), + reason: z.string(), + receipt: z.string().optional(), + next: z.object({ phase: z.string(), role: role.nullable() }).strict().optional(), + }) .strict() .nullable(), plannerCall: z.string().startsWith("/").nullable().optional(), @@ -105,6 +112,7 @@ export const state = z discovery: z.array(jsonObject).optional(), exactApproval: z.json().optional(), exactReview: z.json().optional(), + decisions: z.array(z.string()).optional(), }) .strict(), calls: z.array( @@ -196,6 +204,15 @@ export function confirmLinks(saved: z.infer, selected: z.infer + diff --git a/examples/adversarial-implementation/tests/DecisionResolution.test.md b/examples/adversarial-implementation/tests/DecisionResolution.test.md new file mode 100644 index 000000000..3727bf2dc --- /dev/null +++ b/examples/adversarial-implementation/tests/DecisionResolution.test.md @@ -0,0 +1,40 @@ +```bash exec as="location" +pwd -P +``` + +```bash exec as="commandSetting" +printf '%s' "$XMD_EXAMPLE_COMMAND" +``` + +'}, + {name:"DR2 A2 amending a Planner decision routes original Architect, retained Planner, then Implementor",fixture:''}, + {name:"DR3 A2 accepting an Architect decision returns to that Architect before the retained Planner",fixture:''}, + {name:"DR4 A3 an Implementor discovery hold at clean source routes Architect, retained Planner, original Implementor",fixture:''}, + {name:"DR5 A4 A6 stale, foreign, unsupported and excluded resolutions refuse without dispatch or managed change",fixture:''}, + {name:"DR6 A5 repeats, contenders and interrupted publication never duplicate or adopt effects",fixture:''}, + {name:"DR7 A6 decision inputs keep snapshot, descriptor, protection, permission and capacity boundaries",fixture:''} +]} /> + + + +' + selected.fixture.replaceAll("ROOT",JSON.stringify(root)) + ''} /> + "'" + value.replaceAll("'","'\"'\"'") + "'").join(" ")} /> +```bash exec as="ran" +{runner} {caseArguments} +``` + + + + + +```bash exec as="testCommandSetting" +printf '%s' "$XMD_EXAMPLE_TEST_COMMAND" +``` + + "'" + value.replaceAll("'","'\"'\"'") + "'").join(" ")} /> +```bash exec as="native" +{testRunner} {nativeArguments} +``` + + diff --git a/examples/adversarial-implementation/tests/agents/decision-planner-held.md b/examples/adversarial-implementation/tests/agents/decision-planner-held.md new file mode 100644 index 000000000..b7197ad34 --- /dev/null +++ b/examples/adversarial-implementation/tests/agents/decision-planner-held.md @@ -0,0 +1,4 @@ + + +{text} + diff --git a/examples/adversarial-implementation/tests/agents/decision-planner.md b/examples/adversarial-implementation/tests/agents/decision-planner.md new file mode 100644 index 000000000..38527ce4b --- /dev/null +++ b/examples/adversarial-implementation/tests/agents/decision-planner.md @@ -0,0 +1,9 @@ + + +{text} + + + + +{text} + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionBoundary.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionBoundary.md new file mode 100644 index 000000000..587ce78a7 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionBoundary.md @@ -0,0 +1,95 @@ +--- +description: >- + Keep the inherited snapshot, descriptor, protection, permission and capacity refusals + for decision inputs. Use . +props: + type: object + properties: + root: {type: string} + required: [root] + additionalProperties: false +--- + + + + + + + + + +```bash exec as="altered" +cp -- {snapshotPath} {snapshotPath}.original +printf 'altered retained result' > {snapshotPath} +``` + + +```bash exec as="removed" +rm -- {snapshotPath} +``` + + +```bash exec as="restored" +mv -- {snapshotPath}.original {snapshotPath} +``` + + +Rewritten after the request was bound. + + + + + + +```bash exec as="described" +set -e +printf '%s%s%s' ghp _ abcdefghijklmnopqrstuvwxyz0123456789 > {secretQuoted} +printf 'Readable only before its permission was withdrawn.' > {deniedQuoted} +digest() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"; else shasum -a 256 "$1"; fi | cut -d' ' -f1; } +printf '{"secret":{"utf8Bytes":%s,"sha256":"%s"},"denied":{"utf8Bytes":%s,"sha256":"%s"}}' "$(wc -c < {secretQuoted} | tr -d ' ')" "$(digest {secretQuoted})" "$(wc -c < {deniedQuoted} | tr -d ' ')" "$(digest {deniedQuoted})" +chmod 000 {deniedQuoted} +``` + +{described.stdout} + + + + + + + + + +{oversizedText} + + +```bash exec as="permitted" +chmod 644 {deniedQuoted} +``` + + + +Accepted scope rewritten after launch.Accepted scope rewritten after launch. + + + + + +{child.stdout} + + + + + +{child.stdout} + + + +{calledText} + input.path === held.evidence)?.content,called.inputs.find(input => input.path === revised.path)?.content]} expected={["architect",originalEvidence,"Accepted scope rewritten after launch."]} /> + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md new file mode 100644 index 000000000..3e4ed6130 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md @@ -0,0 +1,36 @@ +--- +description: >- + Launch a deterministic ordinary ticket until its first hold. Use inside a TempDir. +props: + type: object + properties: + root: {type: string} + scenario: {type: string} + ready: {type: boolean, default: false} + required: [root, scenario] + additionalProperties: false +returns: {type: object} +--- + + + +format() { printf '%s' "$1"; } + +```bash exec as="base" +git add api.sh +git -c commit.gpgsign=false commit -qm 'ordinary unsigned planning base' +git rev-parse HEAD +``` + + + +Accepted ordinary planning scope and frozen checks. + + + + +{child.stdout} + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md new file mode 100644 index 000000000..fdbe8c621 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md @@ -0,0 +1,157 @@ +--- +description: >- + Repeat, contend for and interrupt decision resolution, publication and continuation. + Use . +props: + type: object + properties: + root: {type: string} + required: [root] + additionalProperties: false +--- + +```bash exec as="commandSetting" +printf '%s' "$XMD_EXAMPLE_COMMAND" +``` + + + + + + + + + + +```bash exec as="uncertain" +mkdir attempts/0001 +``` + + + + +```bash exec as="cleared" +rmdir attempts/0001 +test ! -e decisions +``` + + + + + quote + value.replaceAll(quote,quote + '"' + quote + '"' + quote) + quote).join(" ")} /> + + +```bash exec as="contended" +{runner} {entry} > {output}/first.json 2> {output}/first.err & +first=$! +{runner} {entry} > {output}/second.json 2> {output}/second.err & +second=$! +wait "$first"; firstStatus=$? +wait "$second"; secondStatus=$? +printf '%s %s' "$firstStatus" "$secondStatus" +``` + + + + +{firstText} +{secondText} + + entry.publication === "confirmed")} /> += 1 && winners.every(entry => JSON.stringify(entry.continuation) === JSON.stringify(winners[0].continuation))} expected={true} /> + entry.publication !== "confirmed").every(entry => entry.status === "hold" && JSON.stringify(entry.continuation) === JSON.stringify(held.work.continuation) && entry.hold.includes(reservation))} expected={true} /> + + + + + +{child.stdout} + + + + + + + + + + + + +{child.stdout} + + +{child.stdout} + + + +{child.stdout} + + +{child.stdout} + + + + + + + + + + + + + + + + + + quote + value.replaceAll(quote,quote + '"' + quote + '"' + quote) + quote).join(" ")} /> + +```bash exec as="child" +XMD_DECISION_CUT_MODE={cut.mode} XMD_DECISION_CUT_MATCH={cut.match} {runner} {cutArguments} +``` + + +{child.stdout} + + +{interrupted.output} + + + + + + + + + +{repeat.stdout} + + + + + + + + + + +{child.stdout} + + +```bash exec as="uncertain" +mkdir calls/0001 +``` + + +{child.stdout} + + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionJourney.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionJourney.md new file mode 100644 index 000000000..cda68bd83 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionJourney.md @@ -0,0 +1,108 @@ +--- +description: >- + Resolve one prospective managed hold and continue it through its original owners. + Use . +props: + type: object + properties: + root: {type: string} + scenario: {type: string} + disposition: {enum: [accept, amend]} + route: {type: array, items: {enum: [architect, planner, implementor]}} + required: [root, scenario, disposition, route] + additionalProperties: false +--- + + + + + + + + +```bash exec as="checkout" +git rev-parse HEAD +git status --porcelain --untracked-files=no +``` + + + + +{heldResultText} + + + + + +Selected accepted input for the routed owner. + + + + + + +{refusedChild.stdout} + + + + + +{child.stdout} + + + + + + + + + + 0} expected={true} /> + + + + + [resolved.documents[path].utf8Bytes,resolved.documents[path].sha256,resolved.documents[path].observation])} expected={[[decision.request.decision.utf8Bytes,decision.request.decision.sha256,"file"],[decision.request.provenance.utf8Bytes,decision.request.provenance.sha256,"file"],[selected.utf8Bytes,selected.sha256,"file"]]} /> + + + +{receiptText} + + + + + + + + + +{continuation.stdout} + + + + + + call.role)} expected={props.route} /> + call.session === saved.manifest.roles[call.role].session && call.provider === saved.manifest.roles[call.role].provider)} expected={true} /> + call.initialize)} expected={props.route.map((role,index) => !saved.checkpoint.initialized[role] && props.route.indexOf(role) === index)} /> + call.assignment.phase)} expected={props.route.map(role => role === "implementor" ? "implementation" : "discovery")} /> + call.status)} expected={props.route.map(() => "accepted")} /> + + + input.path === held.evidence)} expected={!(saved.deliveredInputs[first.session] ?? []).includes(held.evidence)} /> + first.inputs.some(input => input.path === path))} expected={true} /> + +{calledText} + input.path)]} /> + + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionRefusal.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionRefusal.md new file mode 100644 index 000000000..6dfd1dfb5 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionRefusal.md @@ -0,0 +1,97 @@ +--- +description: >- + Refuse stale, conflicting, foreign and excluded decision resolutions without a role + call or managed change. Use . +props: + type: object + properties: + root: {type: string} + required: [root] + additionalProperties: false +--- + + + + + + + + + + + + + + + + + + + + + + + + + +```bash exec as="dirty" +printf 'edited during the hold\n' > legacy.fixture +``` + + + + +```bash exec as="moved" +set -e +git checkout -q -- legacy.fixture +git -c commit.gpgsign=false commit -q --allow-empty -m 'moved during the hold' +``` + + + + + +```bash exec as="restored" +git reset -q --hard {held.task.revision} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionRequest.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionRequest.md new file mode 100644 index 000000000..c8c2139e7 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionRequest.md @@ -0,0 +1,30 @@ +--- +description: >- + Save decision texts and one ticket-decision.v1 request bound to a held ticket. Use + . +props: + type: object + properties: + held: {type: object} + name: {type: string} + disposition: {type: string, default: accept} + acceptedInputs: {type: array, default: []} + override: {type: object, default: {}} + required: [held, name] + additionalProperties: false +returns: {type: object} +--- + + + + +{decisionText} +Standing coordinator authority recorded in the accepted contract; no new human answer is recorded. + + + + + + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/Execution.md b/examples/adversarial-implementation/tests/components/Fixture/Execution.md index 4b957fd37..c8508bfec 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/Execution.md +++ b/examples/adversarial-implementation/tests/components/Fixture/Execution.md @@ -2,11 +2,12 @@ props: type: object properties: - entry: {enum: [run, continue]} + entry: {enum: [run, continue, resolve-decision]} value: {type: object} repository: {type: string} root: {type: string} steps: {type: integer, minimum: 1} + decision: {type: string} required: [entry, value, repository, root] additionalProperties: false returns: {type: object} @@ -19,7 +20,7 @@ pwd -P - + diff --git a/examples/adversarial-implementation/tests/components/Fixture/InitialPlanning.md b/examples/adversarial-implementation/tests/components/Fixture/InitialPlanning.md index 03cd2b795..b90525516 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/InitialPlanning.md +++ b/examples/adversarial-implementation/tests/components/Fixture/InitialPlanning.md @@ -80,7 +80,7 @@ git rev-parse HEAD - + {text} diff --git a/examples/adversarial-implementation/tests/components/Fixture/Refused.md b/examples/adversarial-implementation/tests/components/Fixture/Refused.md new file mode 100644 index 000000000..070af96d9 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/Refused.md @@ -0,0 +1,20 @@ +--- +description: >- + Require a refused decision envelope that keeps the prior continuation. Use + . +props: + type: object + properties: + output: {type: string} + continuation: {type: object} + expected: {type: string} + required: [output, continuation, expected] + additionalProperties: false +returns: {type: object} +--- +{props.output} + + + + diff --git a/examples/adversarial-implementation/tests/components/Phase/Role.md b/examples/adversarial-implementation/tests/components/Phase/Role.md index fc6d64bcb..18fb4cb72 100644 --- a/examples/adversarial-implementation/tests/components/Phase/Role.md +++ b/examples/adversarial-implementation/tests/components/Phase/Role.md @@ -41,7 +41,7 @@ printf '%s' 'Evidence B: mutable original after planning.' > {evidencePath} Changed during read-only discovery. - + @@ -56,6 +56,9 @@ git rev-parse HEAD^ + + + ```bash exec as="check" @@ -117,3 +120,5 @@ git rev-parse HEAD + + diff --git a/examples/adversarial-implementation/tests/decision-cut.md b/examples/adversarial-implementation/tests/decision-cut.md new file mode 100644 index 000000000..1dfc7e048 --- /dev/null +++ b/examples/adversarial-implementation/tests/decision-cut.md @@ -0,0 +1,13 @@ +--- +props: + type: object + properties: + record: {type: string} + decision: {type: string} + required: [record, decision] + additionalProperties: false +--- + +{text} + + diff --git a/examples/adversarial-implementation/tests/decision-native-cases.md b/examples/adversarial-implementation/tests/decision-native-cases.md new file mode 100644 index 000000000..1b11092ba --- /dev/null +++ b/examples/adversarial-implementation/tests/decision-native-cases.md @@ -0,0 +1,67 @@ +```bash exec as="location" +pwd -P +``` + + + + + +```bash exec as="repositoryPrepared" +set -eu +mkdir repository +git -C repository init -q +git -C repository config user.name Fixture +git -C repository config user.email fixture@example.invalid +git -C repository config commit.gpgsign false +pwd -P +``` + + +Follow the accepted fixture assignment. +Prepare the accepted fixture plan. +Preserve the accepted fixture architecture. +Implement the accepted fixture plan. +```bash exec as="base" +git add AGENTS.md .agents +git commit -qm 'fixture instructions' +git rev-parse HEAD +``` + +Accepted decision-resolution fixture scope. + + + + + + + + + + +{stdout} + + + +{journeyText} +{journey.identityBefore} + + + + + + + + + +{deliveryText} + input.path)]} expected={["decision-planner",[fixture.root + "/decision.md",fixture.root + "/provenance.md"]]} /> + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/decision-native-journey.md b/examples/adversarial-implementation/tests/decision-native-journey.md new file mode 100644 index 000000000..9268bf8d7 --- /dev/null +++ b/examples/adversarial-implementation/tests/decision-native-journey.md @@ -0,0 +1,32 @@ +--- +props: + type: object + properties: + manifest: {type: string} + root: {type: string} + variant: {enum: [continued, missing]} + required: [manifest, root, variant] +--- + +{text} + + + + +Decision native: accept the held proposal.Standing coordinator authority; no new human answer is recorded. + + + [path,{path,utf8Bytes:document.utf8Bytes,sha256:document.sha256}]))} /> + + + + + +```bash exec as="removed" +rm -- {quoted} +``` + + + + + diff --git a/packages/cli/tests/adversarial-decision-resolution.test.ts b/packages/cli/tests/adversarial-decision-resolution.test.ts new file mode 100644 index 000000000..922181a4a --- /dev/null +++ b/packages/cli/tests/adversarial-decision-resolution.test.ts @@ -0,0 +1,7 @@ +import { describe, it } from "@executablemd/test-support/bdd"; +import { runAdversarialJourney } from "./support/adversarial-journey.ts"; + +describe("adversarial implementation example", () => { + it("runs the DecisionResolution Markdown journeys without native models", () => + runAdversarialJourney("DecisionResolution")); +}); diff --git a/packages/cli/tests/fixtures/decision-cut.mjs b/packages/cli/tests/fixtures/decision-cut.mjs new file mode 100644 index 000000000..5f1743a72 --- /dev/null +++ b/packages/cli/tests/fixtures/decision-cut.mjs @@ -0,0 +1,28 @@ +import { Plugin } from "@executablemd/core/api"; +import { API, env } from "@executablemd/runtime"; + +// Fail the first host commit whose target contains XMD_DECISION_CUT_MATCH, either before +// its bytes exist or after the rename completed without acknowledging it to the writer. +export default Plugin({ + name: "decision-cut", + *install() { + const mode = yield* env("XMD_DECISION_CUT_MODE"); + const match = yield* env("XMD_DECISION_CUT_MATCH"); + if (mode === undefined || match === undefined) { + return; + } + let cut = false; + yield* API.Fs.around({ + *rename([from, to], next) { + if (!cut && String(to).includes(match)) { + cut = true; + if (mode === "after") { + yield* next(from, to); + } + throw new Error("synthetic " + mode + " cut at " + String(to)); + } + return yield* next(from, to); + }, + }); + }, +}); diff --git a/packages/cli/tests/support/adversarial-journey.ts b/packages/cli/tests/support/adversarial-journey.ts index 14d1a9467..c4a97f469 100644 --- a/packages/cli/tests/support/adversarial-journey.ts +++ b/packages/cli/tests/support/adversarial-journey.ts @@ -57,7 +57,7 @@ export function* runAdversarialJourney(suite: string) { ? preparationTimeout : suite === "Friction" || suite === "Implementation" ? 180_000 - : suite === "Delivery" + : suite === "Delivery" || suite === "DecisionResolution" ? 300_000 : 120_000, env: { diff --git a/scripts/tests/runtime-exclusions.test.ts b/scripts/tests/runtime-exclusions.test.ts index 700af2514..06eadfb62 100644 --- a/scripts/tests/runtime-exclusions.test.ts +++ b/scripts/tests/runtime-exclusions.test.ts @@ -155,6 +155,7 @@ describe("the applicable corpus", () => { const wrappers = [ "adversarial-artifact-return-recovery", "adversarial-artifact-return", + "adversarial-decision-resolution", "adversarial-delivery", "adversarial-friction", "adversarial-implementation", From 7dfe3e919f9e5ece4245e19d0fafaa8bc97a71dd Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 15:45:31 -0500 Subject: [PATCH 2/4] Refuse decision resolution for a failed held execution Phase.Run can save a valid needs_decision result and then fail; settlement keeps the call accepted with a failed execution record. Ticket.ResolveDecision now requires the held call's execution record to show completion before any reservation or publication, and refuses failed, missing or uncertain execution. Advance records hold.execution="failed" so Publish no longer advertises resolve-decision for that hold. The new DR5 regression saves a Planner decision, fails the same phase on its response-acceptance write, and asserts refusal with unchanged managed bytes, retained first failure and no successor, reservation or role call. Refs #1165 Co-Authored-By: Claude Opus 5.5 --- examples/adversarial-implementation/README.md | 4 +- .../components/Ticket/Advance.md | 7 +-- .../components/Ticket/Publish.ts | 1 + .../components/Ticket/ResolveDecision.md | 7 ++- .../components/Ticket/control.ts | 1 + .../tests/DecisionResolution.test.md | 1 + .../Fixture/DecisionFailedExecution.md | 54 +++++++++++++++++++ .../tests/components/Fixture/DecisionHold.md | 16 ++++++ 8 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 examples/adversarial-implementation/tests/components/Fixture/DecisionFailedExecution.md diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index a182a0f3f..09e2cdcee 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -597,7 +597,9 @@ The original task, sessions, calls, results, delivered inputs, source anchor and correction count are unchanged; resolution grants no further corrections. Every other hold refuses, including an Architect amendment, an API conflict routed to the Planner, execution failure, uncertain native work, changed input, source -mismatch, an exhausted correction bound and publication failure. A dirty or moved +mismatch, an exhausted correction bound and publication failure. A held result +whose phase execution failed after the result was saved also refuses; resolution +requires the call's execution record to show completion. A dirty or moved checkout, prior implementation feedback, a stale or foreign binding and a changed input also refuse. A refusal returns a held envelope carrying the failure and leaves the managed ticket unchanged; keep the original envelope. diff --git a/examples/adversarial-implementation/components/Ticket/Advance.md b/examples/adversarial-implementation/components/Ticket/Advance.md index 68cb9cc64..e2d63ab2c 100644 --- a/examples/adversarial-implementation/components/Ticket/Advance.md +++ b/examples/adversarial-implementation/components/Ticket/Advance.md @@ -14,7 +14,8 @@ returns: {type: object} --- A decision or contract conflict holds the ticket for its existing approval gate. -An implementation discovery hold keeps its validated next owner as a routing fact. +An implementation discovery hold keeps its validated next owner as a routing fact; +a hold whose phase execution failed after saving its result records that failure. A correction clears prior verdicts. Readiness requires every selected reviewer to PASS the same signed implementation SHA; it does not authorize delivery or merge. Selected feedback documents remain available across reviews and corrections. @@ -33,7 +34,7 @@ Selected feedback documents remain available across reviews and corrections. - + @@ -50,6 +51,6 @@ Selected feedback documents remain available across reviews and corrections. - 0 ? [{role:call.role,revision:result.revision,result:props.pending.resultPath,requests:result.discoveryRequests ?? [],gaps:result.discoveryGaps ?? []}] : [])],feedback:result.outcome === "complete" && call.role === "implementor" ? [props.pending.resultPath,props.observation.path] : state.feedback,hold:held ? {kind:correction ? "correction_bound" : result.outcome,reason:result.handoff,...(result.outcome === "needs_discovery" ? {next:result.next} : {})} : null}} /> + 0 ? [{role:call.role,revision:result.revision,result:props.pending.resultPath,requests:result.discoveryRequests ?? [],gaps:result.discoveryGaps ?? []}] : [])],feedback:result.outcome === "complete" && call.role === "implementor" ? [props.pending.resultPath,props.observation.path] : state.feedback,hold:held ? {kind:correction ? "correction_bound" : result.outcome,reason:result.handoff,...(result.outcome === "needs_discovery" ? {next:result.next} : {}),...(props.pending.execution.status === "failed" ? {execution:"failed"} : {})} : null}} /> diff --git a/examples/adversarial-implementation/components/Ticket/Publish.ts b/examples/adversarial-implementation/components/Ticket/Publish.ts index 3437fe3b1..f72409c56 100644 --- a/examples/adversarial-implementation/components/Ticket/Publish.ts +++ b/examples/adversarial-implementation/components/Ticket/Publish.ts @@ -93,6 +93,7 @@ export default function* Publish(value: unknown) { record.checkpoint.status === "hold" && record.checkpoint.revision === record.manifest.assignment.revision && record.checkpoint.feedback.length === 0 && + hold?.execution === undefined && last?.status === "accepted" && typeof last.result === "string" && ((hold?.kind === "needs_decision" && diff --git a/examples/adversarial-implementation/components/Ticket/ResolveDecision.md b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md index b796cca5b..cc9d11383 100644 --- a/examples/adversarial-implementation/components/Ticket/ResolveDecision.md +++ b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md @@ -25,7 +25,8 @@ delivered inputs, source anchor and correction count stay unchanged. The reservation under the hold's continuation digest admits one resolution. A confirmed repeat returns the recorded successor, and a different decision for that hold refuses. A reservation without its confirmed receipt stays held: matching -files do not establish an unknown earlier write. +files do not establish an unknown earlier write. A held call whose execution +record is failed, missing or uncertain refuses, even when its result was saved. @@ -54,6 +55,10 @@ if test -e {quoted}/receipt.json; then printf confirmed; elif test -e {quoted} | + + +{executionRead.output}{executionJson} + diff --git a/examples/adversarial-implementation/components/Ticket/control.ts b/examples/adversarial-implementation/components/Ticket/control.ts index cb7857367..efc4a2f0a 100644 --- a/examples/adversarial-implementation/components/Ticket/control.ts +++ b/examples/adversarial-implementation/components/Ticket/control.ts @@ -104,6 +104,7 @@ export const state = z reason: z.string(), receipt: z.string().optional(), next: z.object({ phase: z.string(), role: role.nullable() }).strict().optional(), + execution: z.literal("failed").optional(), }) .strict() .nullable(), diff --git a/examples/adversarial-implementation/tests/DecisionResolution.test.md b/examples/adversarial-implementation/tests/DecisionResolution.test.md index 3727bf2dc..86066e9d7 100644 --- a/examples/adversarial-implementation/tests/DecisionResolution.test.md +++ b/examples/adversarial-implementation/tests/DecisionResolution.test.md @@ -12,6 +12,7 @@ printf '%s' "$XMD_EXAMPLE_COMMAND" {name:"DR3 A2 accepting an Architect decision returns to that Architect before the retained Planner",fixture:''}, {name:"DR4 A3 an Implementor discovery hold at clean source routes Architect, retained Planner, original Implementor",fixture:''}, {name:"DR5 A4 A6 stale, foreign, unsupported and excluded resolutions refuse without dispatch or managed change",fixture:''}, + {name:"DR5 A4 a saved decision result whose phase execution then failed refuses before reservation",fixture:''}, {name:"DR6 A5 repeats, contenders and interrupted publication never duplicate or adopt effects",fixture:''}, {name:"DR7 A6 decision inputs keep snapshot, descriptor, protection, permission and capacity boundaries",fixture:''} ]} /> diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionFailedExecution.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionFailedExecution.md new file mode 100644 index 000000000..4180fb09d --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionFailedExecution.md @@ -0,0 +1,54 @@ +--- +description: >- + Refuse a decision for an accepted needs_decision result whose phase execution failed + after saving it. Use . +props: + type: object + properties: + root: {type: string} + required: [root] + additionalProperties: false +--- + + + + + + + + +{executionText} + + +{resultText} + + + + + + + + + + +```bash exec as="hidden" +mv -- {executionPath} {executionPath}.retained +``` + + +```bash exec as="restored" +mv -- {executionPath}.retained {executionPath} +``` + + + + + + + + + + + + + diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md index 3e4ed6130..65f7f4b93 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md @@ -8,6 +8,7 @@ props: root: {type: string} scenario: {type: string} ready: {type: boolean, default: false} + cut: {type: [object, 'null'], default: null} required: [root, scenario] additionalProperties: false returns: {type: object} @@ -28,7 +29,22 @@ git rev-parse HEAD Accepted ordinary planning scope and frozen checks. + + +```bash exec as="commandSetting" +printf '%s' "$XMD_EXAMPLE_COMMAND" +``` + + + "'" + value.replaceAll("'","'\"'\"'") + "'").join(" ")} /> + +```bash exec as="child" +XMD_DECISION_CUT_MODE={props.cut.mode} XMD_DECISION_CUT_MATCH={props.cut.match} {runner} {runArguments} +``` + + + {child.stdout} From c6f995bc26cfe8aa1e3cedc350ca9deef6fc89ab Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 16:29:41 -0500 Subject: [PATCH 3/4] test: expose decision interruption assertion evidence --- .../tests/components/Fixture/DecisionInterruption.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md index fdbe8c621..264de819a 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md @@ -56,10 +56,13 @@ printf '%s %s' "$firstStatus" "$secondStatus" {firstText} {secondText} + + + entry.publication === "confirmed")} /> -= 1 && winners.every(entry => JSON.stringify(entry.continuation) === JSON.stringify(winners[0].continuation))} expected={true} /> - entry.publication !== "confirmed").every(entry => entry.status === "hold" && JSON.stringify(entry.continuation) === JSON.stringify(held.work.continuation) && entry.hold.includes(reservation))} expected={true} /> += 1 && winners.every(entry => JSON.stringify(entry.continuation) === JSON.stringify(winners[0].continuation)))}> + entry.publication !== "confirmed").every(entry => entry.status === "hold" && JSON.stringify(entry.continuation) === JSON.stringify(held.work.continuation) && entry.hold.includes(reservation))}> @@ -72,7 +75,7 @@ printf '%s %s' "$firstStatus" "$secondStatus" - + @@ -135,7 +138,7 @@ XMD_DECISION_CUT_MODE={cut.mode} XMD_DECISION_CUT_MATCH={cut.match} {runner} {cu - + From 48e75b99d5c98b5eff3ff044af201fdd400722cd Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 8 Nov 2026 18:19:05 -0500 Subject: [PATCH 4/4] Return a confirmed decision repeat from its retained evidence ResolveDecision bound the request and reread every new decision input from its original file before inspecting the confirmed receipt, so an identical repeat refused once an origin was rewritten or removed, even though the successor retained the accepted snapshots. Ticket.Decision now only parses the request and binds it to the exact hold. A confirmed matching repeat loads its recorded successor and hydrates the retained decision evidence under current permission and protection. The new Ticket.DecisionInputs reads origins only for a first resolution. Conflict refusal, first-resolution descriptor checks, the execution-failure exclusion and unknown-publication holds are unchanged. DR6 now rewrites and removes the decision origins after the confirmed resolution and requires the identical repeat to return the same successor with no new publication or role call; a corrupted retained snapshot still refuses. Refs #1165 Co-Authored-By: Claude Opus 5.5 --- examples/adversarial-implementation/README.md | 4 +- .../components/Ticket/Decision.ts | 43 +------------------ .../components/Ticket/DecisionInputs.ts | 34 +++++++++++++++ .../components/Ticket/ResolveDecision.md | 7 ++- .../components/Ticket/text-storage.ts | 29 +++++++++++++ .../Fixture/DecisionInterruption.md | 23 ++++++++++ 6 files changed, 97 insertions(+), 43 deletions(-) create mode 100644 examples/adversarial-implementation/components/Ticket/DecisionInputs.ts diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 09e2cdcee..19dfac715 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -605,7 +605,9 @@ input also refuse. A refusal returns a held envelope carrying the failure and leaves the managed ticket unchanged; keep the original envelope. Repeating the same request against the same hold returns its recorded successor -without publishing again. A different decision for a resolved hold refuses. If +without publishing again. The repeat checks the evidence the successor retained, +under current File permission and protection; it does not reread the decision's +original files, so they may later change or disappear. A different decision for a resolved hold refuses. If reservation, publication or the receipt is interrupted, the hold stays held and its reservation directory preserves the first failure for operator disposition. A matching successor file alone does not confirm the interrupted write. Repeated diff --git a/examples/adversarial-implementation/components/Ticket/Decision.ts b/examples/adversarial-implementation/components/Ticket/Decision.ts index c80e163e3..708db6c26 100644 --- a/examples/adversarial-implementation/components/Ticket/Decision.ts +++ b/examples/adversarial-implementation/components/Ticket/Decision.ts @@ -1,7 +1,6 @@ -import { parseFilesFailure } from "@executablemd/runtime"; import { z } from "zod"; import { envelope, state } from "./control.ts"; -import { describe, descriptor, read, stateLimit, textLimit } from "./text-storage.ts"; +import { describe, descriptor, readBounded, same, stateLimit, textLimit } from "./text-storage.ts"; export const props = { type: "object", @@ -29,38 +28,12 @@ const request = z .strict(); type Reference = z.infer; -function same(left: Reference, right: { path: string; utf8Bytes: number; sha256: string }) { - return ( - left.path === right.path && left.utf8Bytes === right.utf8Bytes && left.sha256 === right.sha256 - ); -} - -function* readText(path: string, limit: number) { - try { - return yield* read("/", path, limit); - } catch (error) { - const failure = parseFilesFailure(error); - if (failure === undefined) { - throw error; - } - throw new Error( - "Cannot read " + - path + - " within " + - limit + - " UTF-8 bytes under current File permission: " + - failure.reason + - ". No resolution was recorded.", - ); - } -} - export default function* Decision(value: unknown) { const input = z .object({ path: z.string().startsWith("/"), record: envelope, saved: state }) .strict() .parse(value); - const source = yield* readText(input.path, stateLimit); + const source = yield* readBounded(input.path, stateLimit); const selected = request.parse(JSON.parse(source)); const { saved } = input; if (selected.work !== saved.manifest.work) { @@ -89,18 +62,6 @@ export default function* Decision(value: unknown) { } references.set(entry.path, entry); } - for (const entry of references.values()) { - const retained = saved.documents[entry.path]; - const current = retained ?? describe(entry.path, yield* readText(entry.path, textLimit)); - if (!same(entry, current)) { - throw new Error( - (retained === undefined - ? "Decision input differs from its descriptor: " - : "A recorded reference keeps its first text; give changed text a fresh reference: ") + - entry.path, - ); - } - } return { request: selected, file: describe(input.path, source), diff --git a/examples/adversarial-implementation/components/Ticket/DecisionInputs.ts b/examples/adversarial-implementation/components/Ticket/DecisionInputs.ts new file mode 100644 index 000000000..32e006299 --- /dev/null +++ b/examples/adversarial-implementation/components/Ticket/DecisionInputs.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; +import { describe, descriptor, readBounded, same, snapshot, textLimit } from "./text-storage.ts"; + +export const props = { + type: "object", + properties: { descriptors: { type: "object" }, documents: { type: "object" } }, + required: ["descriptors", "documents"], + additionalProperties: false, +}; + +// A first resolution observes each decision input once: a recorded reference keeps +// its retained text, and a new reference must match its origin's current bytes. +export default function* DecisionInputs(value: unknown) { + const { descriptors, documents } = z + .object({ + descriptors: z.record(z.string(), descriptor), + documents: z.record(z.string(), snapshot), + }) + .strict() + .parse(value); + for (const entry of Object.values(descriptors)) { + const retained = documents[entry.path]; + const current = retained ?? describe(entry.path, yield* readBounded(entry.path, textLimit)); + if (!same(entry, current)) { + throw new Error( + (retained === undefined + ? "Decision input differs from its descriptor: " + : "A recorded reference keeps its first text; give changed text a fresh reference: ") + + entry.path, + ); + } + } + return Object.keys(descriptors); +} diff --git a/examples/adversarial-implementation/components/Ticket/ResolveDecision.md b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md index cc9d11383..01682012b 100644 --- a/examples/adversarial-implementation/components/Ticket/ResolveDecision.md +++ b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md @@ -25,7 +25,9 @@ delivered inputs, source anchor and correction count stay unchanged. The reservation under the hold's continuation digest admits one resolution. A confirmed repeat returns the recorded successor, and a different decision for that hold refuses. A reservation without its confirmed receipt stays held: matching -files do not establish an unknown earlier write. A held call whose execution +files do not establish an unknown earlier write. The repeat uses the evidence the +successor retained, so later changes to the decision's original files do not +affect it. A held call whose execution record is failed, missing or uncertain refuses, even when its result was saved. @@ -50,8 +52,11 @@ if test -e {quoted}/receipt.json; then printf confirmed; elif test -e {quoted} | {receiptText} + + [path,recorded.documents[path]]))} as="retained" /> + diff --git a/examples/adversarial-implementation/components/Ticket/text-storage.ts b/examples/adversarial-implementation/components/Ticket/text-storage.ts index e99aa6196..93b5e6b9e 100644 --- a/examples/adversarial-implementation/components/Ticket/text-storage.ts +++ b/examples/adversarial-implementation/components/Ticket/text-storage.ts @@ -78,3 +78,32 @@ export function unused(repository: string, path: string): Operation { return !result.ok && parseFilesFailure(result.error)?.reason === "missing"; }); } + +export function same( + left: { path: string; utf8Bytes: number; sha256: string }, + right: { path: string; utf8Bytes: number; sha256: string }, +) { + return ( + left.path === right.path && left.utf8Bytes === right.utf8Bytes && left.sha256 === right.sha256 + ); +} + +export function* readBounded(path: string, limit: number): Operation { + try { + return yield* read("/", path, limit); + } catch (error) { + const failure = parseFilesFailure(error); + if (failure === undefined) { + throw error; + } + throw new Error( + "Cannot read " + + path + + " within " + + limit + + " UTF-8 bytes under current File permission: " + + failure.reason + + ". No resolution was recorded.", + ); + } +} diff --git a/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md index 264de819a..bd98cf57b 100644 --- a/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md @@ -70,6 +70,29 @@ printf '%s %s' "$firstStatus" "$secondStatus" {child.stdout} + + + + + +```bash exec as="origins" +set -e +printf 'The original pathname was reused after the confirmed resolution.' > {decisionOrigin} +rm -- {provenanceOrigin} +``` + +{child.stdout} + +```bash exec as="corrupted" +cp -- {retainedDecision} {retainedDecision}.retained +printf 'altered retained decision' > {retainedDecision} +``` + + +```bash exec as="retainedRestored" +mv -- {retainedDecision}.retained {retainedDecision} +``` +