diff --git a/examples/adversarial-implementation/README.md b/examples/adversarial-implementation/README.md index 80c727298..19dfac715 100644 --- a/examples/adversarial-implementation/README.md +++ b/examples/adversarial-implementation/README.md @@ -535,6 +535,85 @@ 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 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. + +Repeating the same request against the same hold returns its recorded successor +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 +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..e2d63ab2c 100644 --- a/examples/adversarial-implementation/components/Ticket/Advance.md +++ b/examples/adversarial-implementation/components/Ticket/Advance.md @@ -14,6 +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; +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. @@ -32,7 +34,7 @@ Selected feedback documents remain available across reviews and corrections. - + @@ -49,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} : 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/Decision.ts b/examples/adversarial-implementation/components/Ticket/Decision.ts new file mode 100644 index 000000000..708db6c26 --- /dev/null +++ b/examples/adversarial-implementation/components/Ticket/Decision.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import { envelope, state } from "./control.ts"; +import { describe, descriptor, readBounded, same, 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; + +export default function* Decision(value: unknown) { + const input = z + .object({ path: z.string().startsWith("/"), record: envelope, saved: state }) + .strict() + .parse(value); + const source = yield* readBounded(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); + } + return { + request: selected, + file: describe(input.path, source), + references: [...references.keys()], + descriptors: Object.fromEntries(references), + }; +} 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/Publish.ts b/examples/adversarial-implementation/components/Ticket/Publish.ts index 018c67791..f72409c56 100644 --- a/examples/adversarial-implementation/components/Ticket/Publish.ts +++ b/examples/adversarial-implementation/components/Ticket/Publish.ts @@ -86,14 +86,41 @@ 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 && + hold?.execution === undefined && + 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..01682012b --- /dev/null +++ b/examples/adversarial-implementation/components/Ticket/ResolveDecision.md @@ -0,0 +1,114 @@ +--- +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. 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. + + + + + + + + + + + +```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} + + + + [path,recorded.documents[path]]))} as="retained" /> + + + + + + + + +{executionRead.output}{executionJson} + + + + + 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..efc4a2f0a 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,13 @@ 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(), + execution: z.literal("failed").optional(), + }) .strict() .nullable(), plannerCall: z.string().startsWith("/").nullable().optional(), @@ -105,6 +113,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 +205,15 @@ export function confirmLinks(saved: z.infer, selected: z.infer { 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/resolve-decision.md b/examples/adversarial-implementation/resolve-decision.md new file mode 100644 index 000000000..8c71583f7 --- /dev/null +++ b/examples/adversarial-implementation/resolve-decision.md @@ -0,0 +1,11 @@ +--- +props: + type: object + properties: + record: {type: string, minLength: 1} + decision: {type: string, minLength: 1} + required: [record, decision] + additionalProperties: false +--- + + diff --git a/examples/adversarial-implementation/tests/DecisionResolution.test.md b/examples/adversarial-implementation/tests/DecisionResolution.test.md new file mode 100644 index 000000000..86066e9d7 --- /dev/null +++ b/examples/adversarial-implementation/tests/DecisionResolution.test.md @@ -0,0 +1,41 @@ +```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:"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:''} +]} /> + + + +' + 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/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 new file mode 100644 index 000000000..65f7f4b93 --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionHold.md @@ -0,0 +1,52 @@ +--- +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} + cut: {type: [object, 'null'], default: null} + 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. + + + + + +```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} + + + 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..bd98cf57b --- /dev/null +++ b/examples/adversarial-implementation/tests/components/Fixture/DecisionInterruption.md @@ -0,0 +1,183 @@ +--- +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)))}> + entry.publication !== "confirmed").every(entry => entry.status === "hold" && JSON.stringify(entry.continuation) === JSON.stringify(held.work.continuation) && entry.hold.includes(reservation))}> + + + + + +{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} +``` + + + + + + + + + + + + +{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",