From 0dc8e7fc75e04c9347a567380c6121821b05ea56 Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 07:38:01 +0000
Subject: [PATCH 1/7] feat: add zero-friction MachineOutcome verification lab
---
README.md | 8 +++
app.js | 105 ++++++++++++++++++++++++++++++++++++
index.html | 115 ++++++++++++++++++++++++++++++++++++++++
styles.css | 14 +++++
test/live-demo.test.mjs | 34 ++++++++++++
5 files changed, 276 insertions(+)
create mode 100644 app.js
create mode 100644 index.html
create mode 100644 styles.css
create mode 100644 test/live-demo.test.mjs
diff --git a/README.md b/README.md
index 81faab8..b955fca 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# MachineOutcome - verify what actually happened before retrying
+[**Try the live 30-second Outcome Verification Lab →**](https://samct86.github.io/machineoutcome-case-study/)
+
+No install, account, credential or real external write is required. The browser lab uses the same bounded verifier exported by this repository and runs entirely on synthetic state.
+
[](https://github.com/SamCT86/machineoutcome-case-study/actions/workflows/verify-reference.yml)
MachineOutcome is about a simple but important failure mode in automation: an API call can time out even though the external system already changed.
@@ -36,6 +40,10 @@ Start with **2–3 sentences** describing what is slow, expensive or unreliable.
## Try it
+**Fastest path:** open the [live browser lab](https://samct86.github.io/machineoutcome-case-study/) and run **Timeout, but change happened**. Then switch to **No readback** and **Wrong-attempt evidence** to see the safe action change.
+
+**Technical verification from a clean clone:**
+
```bash
git clone https://github.com/SamCT86/machineoutcome-case-study.git
cd machineoutcome-case-study
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..8164bf8
--- /dev/null
+++ b/app.js
@@ -0,0 +1,105 @@
+import { verifyMutationOutcome } from "./src/reference-outcome-verifier.mjs";
+
+const BASE = Object.freeze({
+ taskId: "task:repo-change-001",
+ evidenceTaskId: "task:repo-change-001",
+ attemptId: "attempt:001",
+ evidenceAttemptId: "attempt:001",
+ expectedPreState: "repo@before",
+ expectedPostState: "repo@after",
+ observedPreState: "repo@before",
+ observedPostState: "repo@after",
+ readbackComplete: true,
+ providerAcknowledgement: "accepted",
+});
+
+const SCENARIOS = Object.freeze({
+ ambiguousVerified: { title: "Timeout, but change happened", hint: "The acknowledgement disappears, but readback proves the expected state.", controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "expected", identity: "exact" } },
+ readbackMissing: { title: "No readback", hint: "Reality is unavailable, so retry remains blocked.", controls: { ack: "ambiguous", prestate: "match", readback: "incomplete", poststate: "expected", identity: "exact" } },
+ unchanged: { title: "Timeout, state unchanged", hint: "The provider is reachable, but the state still looks unchanged.", controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "unchanged", identity: "exact" } },
+ stalePrestate: { title: "Stale starting state", hint: "The action was prepared against an outdated state.", controls: { ack: "accepted", prestate: "stale", readback: "complete", poststate: "expected", identity: "exact" } },
+ wrongAttempt: { title: "Wrong-attempt evidence", hint: "The state looks right, but the evidence belongs to another attempt.", controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "expected", identity: "wrong-attempt" } },
+ wrongPost: { title: "Unexpected post-state", hint: "Readback is complete, but reality is not what the action was supposed to produce.", controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "unexpected", identity: "exact" } },
+});
+
+const form = document.querySelector("#lab-form");
+const presets = document.querySelector("#presets");
+const result = document.querySelector("#result");
+const validation = document.querySelector("#validation");
+const resetButton = document.querySelector("[data-reset]");
+const guidedButton = document.querySelector("[data-run-guided]");
+let selected = "ambiguousVerified";
+
+function escapeHtml(value) {
+ return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
+}
+function setControls(values) { for (const [key, value] of Object.entries(values)) form.elements[key].value = value; }
+function readControls() { return Object.fromEntries(["ack", "prestate", "readback", "poststate", "identity"].map((key) => [key, form.elements[key].value])); }
+function toVerifierInput(c) {
+ const input = { ...BASE };
+ input.providerAcknowledgement = c.ack;
+ input.observedPreState = c.prestate === "match" ? BASE.expectedPreState : "repo@stale";
+ input.readbackComplete = c.readback === "complete";
+ input.observedPostState = c.poststate === "expected" ? BASE.expectedPostState : c.poststate === "unchanged" ? input.observedPreState : "repo@unexpected";
+ if (!input.readbackComplete) input.observedPostState = null;
+ if (c.identity === "wrong-attempt") input.evidenceAttemptId = "attempt:other";
+ if (c.identity === "wrong-task") input.evidenceTaskId = "task:other";
+ return input;
+}
+function renderPresets() {
+ presets.innerHTML = "";
+ for (const [key, scenario] of Object.entries(SCENARIOS)) {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "preset";
+ button.dataset.scenario = key;
+ button.setAttribute("aria-pressed", String(key === selected));
+ button.innerHTML = `${escapeHtml(scenario.title)} ${escapeHtml(scenario.hint)} `;
+ button.addEventListener("click", () => { selected = key; setControls(scenario.controls); validation.textContent = ""; renderPresets(); });
+ presets.append(button);
+ }
+}
+function explanation(out) {
+ const table = {
+ POSTSTATE_CONFIRMED: ["Observed state matches the expected result.", "Do not retry the mutation."],
+ PRESTATE_MISMATCH: ["The starting state changed before the action could be trusted.", "Do not mutate from this stale pre-state."],
+ READBACK_INCOMPLETE: ["The external reality is not established yet.", "Reconcile state before retrying."],
+ AMBIGUOUS_MUTATION_STATE: ["The acknowledgement was ambiguous and the observed state is still unchanged.", "Keep the outcome UNKNOWN and reconcile before retrying."],
+ EVIDENCE_ATTEMPT_MISMATCH: ["The readback belongs to a different attempt.", "Reject the evidence and do not retry from it."],
+ EVIDENCE_TASK_MISMATCH: ["The readback belongs to a different task.", "Reject the evidence and do not retry from it."],
+ POSTSTATE_MISMATCH: ["Readback is complete, but reality does not match the expected result.", "Treat the outcome as failed; do not blindly replay the mutation."],
+ };
+ return table[out.reason] ?? ["The verifier refused to infer a safe outcome.", out.retry.replaceAll("_", " ").toLowerCase() + "."];
+}
+function traceRows(input, out) {
+ const rows = [
+ ["1", "IDENTITY", input.evidenceTaskId === input.taskId && input.evidenceAttemptId === input.attemptId ? "bound" : "mismatch", `${input.evidenceTaskId} / ${input.evidenceAttemptId}`],
+ ["2", "PRE-STATE", input.observedPreState === input.expectedPreState ? "match" : "mismatch", `expected ${input.expectedPreState} · observed ${input.observedPreState}`],
+ ["3", "ATTEMPT", "attempted", `provider acknowledgement: ${input.providerAcknowledgement}`],
+ ["4", "READBACK", input.readbackComplete ? "complete" : "incomplete", input.readbackComplete ? `observed ${input.observedPostState}` : "provider state not established"],
+ ["5", "OUTCOME", out.status.toLowerCase(), `${out.status} · ${out.reason}`],
+ ["6", "RETRY", out.retry === "DO_NOT_RETRY" || out.retry === "DO_NOT_MUTATE" ? "blocked" : "hold", out.retry.replaceAll("_", " ")],
+ ];
+ return rows.map(([n, stage, state, detail]) => `
${n} ${stage} ${escapeHtml(state)} ${escapeHtml(detail)}
`).join("");
+}
+function naiveReaction(input) { return input.providerAcknowledgement === "ambiguous" ? "Treat the timeout as failure and immediately repeat the external action." : "Trust the transport response without proving the downstream state."; }
+function renderResult(input, out) {
+ const [why, safe] = explanation(out);
+ const tone = out.status.toLowerCase();
+ result.innerHTML = `
+ OUTCOME
${escapeHtml(out.status)} ${escapeHtml(why)}
${escapeHtml(out.reason)}
+ Safe next action ${escapeHtml(safe)}
+ Naive reaction ${escapeHtml(naiveReaction(input))}
MachineOutcome reaction ${escapeHtml(out.retry.replaceAll("_", " "))}
+ ${traceRows(input, out)}
+ Inspect exact verifier input and output ${escapeHtml(JSON.stringify({ input, output: out }, null, 2))} `;
+}
+function evaluate(controls) {
+ validation.textContent = "";
+ try { const input = toVerifierInput(controls); const out = verifyMutationOutcome(input); renderResult(input, out); return out; }
+ catch (error) { validation.textContent = `VERIFIER_REFUSED_INPUT: ${error instanceof Error ? error.message : String(error)}`; result.innerHTML = `FAIL CLOSED
Invalid evidence contract. The public verifier rejected the input instead of guessing.
`; return null; }
+}
+form.addEventListener("submit", (event) => { event.preventDefault(); selected = "custom"; renderPresets(); evaluate(readControls()); });
+resetButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); validation.textContent = ""; result.innerHTML = `READY
Run an ambiguous action. You will get an outcome status, reason, safe retry decision, evidence trace and the exact verifier input/output.
`; });
+guidedButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); evaluate(SCENARIOS.ambiguousVerified.controls); document.querySelector("#lab").scrollIntoView({ behavior: "smooth", block: "start" }); });
+setControls(SCENARIOS.ambiguousVerified.controls);
+renderPresets();
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1fa882c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+ MachineOutcome — Outcome Verification Lab
+
+
+
+ Skip to lab
+
+
+
+
+
+
OUTCOME VERIFICATION FOR AGENTIC SYSTEMS
+
A timeout is not a failure. Prove what happened before retrying.
+
MachineOutcome demonstrates one reliability rule: when an external action has an ambiguous result, read back the real state before success or retry is trusted.
+
+
+ ✓ no credentials
+ ✓ no network calls
+ ✓ no real writes
+ ✓ canonical public verifier
+
+
+
+ attempt / repo-change-001 synthetic
+
+ 01 MUTATION attempted
+ 02 ACK timeout
+ 03 READBACK expected state
+ 04 OUTCOME verified
+ 05 RETRY blocked
+
+ transport certainty: low outcome certainty: verified by state
+
+
+
+
+
+
THE FAILURE MODE
+
+
1 Action leaves your process An agent asks another system to mutate something.
+
2 Acknowledgement disappears The request times out after the external system may already have changed.
+
3 Naive retry duplicates work Treating silence as failure can create a second write, deploy, payment or message.
+
4 Read reality first Observed post-state decides whether success or retry is safe.
+
+
+
+
+
+
+
+
TRY IT WITHOUT INSTALLING ANYTHING
+
Change the evidence. Watch the safe action change.
+
+
This lab calls the same bounded verifier exported by this public repository. It never contacts a provider and never performs a mutation.
+
+
+
+
+
+
+
+
+ READY
Run an ambiguous action. You will get an outcome status, reason, safe retry decision, evidence trace and the exact verifier input/output.
+
+
+
+
+
+
+
WHAT THIS PROVES
Small enough to inspect. Strict enough to fail closed.
+
The public lab demonstrates one invariant. It is not the private MachineOutcome runtime and it does not claim customer adoption, ROI or universal agent reliability.
+
+
+
01 Bind evidence Wrong-task and wrong-attempt evidence cannot prove the action.
+
02 Bind pre-state A stale starting state blocks the mutation boundary.
+
03 Read after ambiguity Transport timeout does not decide the real outcome.
+
04 Keep UNKNOWN Missing or ambiguous reality stays unresolved instead of being guessed into success or failure.
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..59459f5
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,14 @@
+:root{--bg:#f4f1ea;--paper:#fffdf8;--ink:#10141a;--muted:#626872;--line:#d8d0c4;--blue:#2154d8;--blue-soft:#edf2ff;--green:#08765a;--green-soft:#eaf7f1;--amber:#9a5a00;--amber-soft:#fff3d9;--red:#a03232;--red-soft:#fff0f0;--dark:#111827;--radius:18px;--shadow:0 20px 60px rgba(16,20,26,.12)}
+*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.55}a{color:inherit}button,select{font:inherit}button{cursor:pointer}.shell{width:min(1180px,calc(100% - 40px));margin-inline:auto}.skip-link{position:absolute;left:-9999px;top:0;background:white;padding:10px;z-index:99}.skip-link:focus{left:10px}
+.topbar{min-height:68px;display:flex;align-items:center;gap:28px;padding:0 max(20px,calc((100vw - 1180px)/2));border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(244,241,234,.94);backdrop-filter:blur(12px);z-index:20}.brand{font-weight:850;text-decoration:none;letter-spacing:-.03em}.topbar nav{display:flex;gap:20px;margin-right:auto}.topbar nav a{text-decoration:none;color:var(--muted);font-size:.92rem}.status-pill{font-size:.7rem;font-weight:850;letter-spacing:.09em;border:1px solid var(--line);border-radius:999px;padding:7px 10px}
+.hero{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(340px,.72fr);gap:64px;align-items:center;padding-block:92px 82px}.eyebrow{font-size:.74rem;font-weight:850;letter-spacing:.14em;color:var(--blue);margin:0 0 16px}.eyebrow.light{color:#a7c2ff}h1,h2,h3,p{margin-top:0}h1{font-size:clamp(3rem,6vw,6.2rem);line-height:.92;letter-spacing:-.065em;margin-bottom:28px;max-width:12ch}h2{font-size:clamp(2rem,3.5vw,3.6rem);line-height:1.02;letter-spacing:-.045em}h3{letter-spacing:-.025em}.lede{font-size:1.22rem;color:#363b43;max-width:720px}.hero-actions,.proof-actions,.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:26px}.button{display:inline-flex;align-items:center;justify-content:center;min-height:46px;padding:0 18px;border-radius:999px;border:1px solid transparent;font-weight:780;text-decoration:none}.primary{background:var(--ink);color:white}.primary:hover{background:#2a3038}.secondary{border-color:var(--ink);background:transparent}.quiet{border-color:var(--line);background:transparent;color:var(--muted)}.trust-row{display:flex;gap:16px;flex-wrap:wrap;margin-top:26px;color:var(--muted);font-size:.86rem}
+.preview{background:var(--dark);color:#edf4ff;border-radius:26px;padding:22px;box-shadow:var(--shadow);transform:rotate(.8deg)}.preview-head{display:flex;justify-content:space-between;color:#94a3b8;font:700 .72rem ui-monospace,monospace;text-transform:uppercase;letter-spacing:.07em}.dot{color:#8be0bf}.preview ol{list-style:none;padding:0;margin:26px 0 18px;border-top:1px solid #334155}.preview li{display:grid;grid-template-columns:32px 1fr auto;gap:10px;padding:14px 0;border-bottom:1px solid #334155;font:700 .8rem ui-monospace,monospace}.preview li span{color:#64748b}.preview em{font-style:normal;color:#cbd5e1}.preview .warn{color:#ffd089}.preview .good{color:#9ce4c7}.preview .blocked{color:#ffadad}.preview p{margin:0;color:#aab8c8;font:500 .8rem/1.55 ui-monospace,monospace}.preview strong{color:white}
+.why-band{background:var(--dark);color:white;padding:68px 0}.why-grid{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #334155;border-radius:22px;overflow:hidden}.why-grid article{padding:24px;border-right:1px solid #334155}.why-grid article:last-child{border-right:0}.why-grid span{font:700 .78rem ui-monospace,monospace;color:#8fa3bd}.why-grid h2{font-size:1.28rem;letter-spacing:-.025em;margin:16px 0 8px}.why-grid p{margin:0;color:#b8c2cf;font-size:.94rem}
+.lab,.proof{padding-block:94px}.section-heading{display:grid;grid-template-columns:1.15fr .85fr;gap:60px;align-items:end;margin-bottom:40px}.section-heading h2{max-width:16ch;margin-bottom:0}.section-heading>p{color:var(--muted);font-size:1.04rem;margin-bottom:3px}.presets{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:22px}.preset{text-align:left;border:1px solid var(--line);background:rgba(255,255,255,.35);border-radius:14px;padding:14px}.preset b{display:block;margin-bottom:4px}.preset span{display:block;color:var(--muted);font-size:.84rem;line-height:1.35}.preset[aria-pressed="true"]{border-color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink);background:var(--paper)}
+.lab-grid{display:grid;grid-template-columns:minmax(300px,.72fr) minmax(0,1.28fr);gap:22px}.controls,.result{border:1px solid var(--line);border-radius:var(--radius);background:rgba(255,255,255,.45)}.controls{padding:22px;display:grid;gap:16px;align-content:start}.panel-title{font-size:.72rem;font-weight:850;letter-spacing:.12em;color:var(--muted);padding-bottom:7px;border-bottom:1px solid var(--line)}.controls label{display:grid;gap:5px}.controls label>span{font-weight:760}.controls small{color:var(--muted)}select{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--paper);color:var(--ink)}.validation{min-height:1.2rem;margin:0;color:var(--red);font-size:.9rem}
+.result{min-height:590px;padding:28px;background:var(--paper)}.empty-state{display:grid;place-content:center;min-height:530px;max-width:520px;margin:auto;text-align:center}.empty-state h3{font-size:2rem;margin-bottom:10px}.empty-state p:last-child{color:var(--muted)}.result-head{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;padding-bottom:20px;border-bottom:1px solid var(--line)}.decision-label{font:.75rem ui-monospace,monospace;color:var(--muted);letter-spacing:.08em}.decision{font-size:2rem;margin:8px 0}.decision.verified{color:var(--green)}.decision.unknown{color:var(--amber)}.decision.failed{color:var(--red)}.result-head p{color:var(--muted);margin:0;max-width:560px}.badge{border-radius:999px;padding:8px 10px;font-size:.74rem;font-weight:850;white-space:nowrap;border:1px solid var(--line)}.badge.verified{background:var(--green-soft);color:var(--green);border-color:#b7dccc}.badge.unknown{background:var(--amber-soft);color:var(--amber);border-color:#ead29c}.badge.failed{background:var(--red-soft);color:var(--red);border-color:#e1b4b4}.safe-action{margin:20px 0;padding:16px 18px;border-left:4px solid var(--blue);background:var(--blue-soft);border-radius:0 12px 12px 0}.safe-action b{display:block;margin-bottom:4px}.comparison{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px}.comparison article{border:1px solid var(--line);border-radius:14px;padding:16px}.comparison h4{margin:0 0 6px}.comparison p{margin:0;color:var(--muted)}.comparison .guarded{background:#f1faf6;border-color:#abd3c2}
+.trace{border-top:1px solid var(--line)}.trace-step{display:grid;grid-template-columns:26px 90px 92px 1fr;gap:10px;align-items:start;padding:12px 0;border-bottom:1px solid #e4ddd2;font-size:.86rem}.trace-step>span{font-family:ui-monospace,monospace;color:#8a8f96}.trace-step b{font-size:.78rem}.trace-step em{font-style:normal;text-transform:uppercase;font:750 .68rem ui-monospace,monospace}.trace-step p{margin:0;color:var(--muted)}.match,.complete,.verified,.bound{color:var(--green)}.mismatch,.failed,.blocked{color:var(--red)}.unknown,.hold,.incomplete{color:var(--amber)}.attempted{color:var(--blue)}details{margin-top:18px}summary{cursor:pointer;font-weight:800}pre{white-space:pre-wrap;word-break:break-word;background:var(--dark);color:#dce7f3;border-radius:14px;padding:16px;font-size:.76rem;overflow:auto}
+.proof{border-top:1px solid var(--line)}.proof-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.proof-grid article{border-top:2px solid var(--ink);padding:17px 4px}.proof-grid span{font:700 .74rem ui-monospace,monospace;color:var(--blue)}.proof-grid h3{margin:13px 0 7px}.proof-grid p{color:var(--muted)}footer{border-top:1px solid var(--line);padding:28px 0}.footer-inner{display:flex;justify-content:space-between;gap:20px;align-items:center}.footer-inner div{display:grid}.footer-inner span{color:var(--muted);font-size:.84rem}.footer-inner a{font-weight:700}
+@media(max-width:920px){.topbar nav{display:none}.status-pill{margin-left:auto}.hero{grid-template-columns:1fr;padding-block:68px 56px}.preview{transform:none}.why-grid{grid-template-columns:1fr 1fr}.why-grid article:nth-child(2){border-right:0}.why-grid article:nth-child(-n+2){border-bottom:1px solid #334155}.section-heading,.lab-grid{grid-template-columns:1fr}.presets{grid-template-columns:1fr 1fr}.proof-grid{grid-template-columns:1fr 1fr}}
+@media(max-width:580px){.shell{width:min(100% - 26px,1180px)}.topbar{padding:0 13px;min-height:60px}.status-pill{display:none}.hero{padding-block:52px 44px;gap:34px}h1{font-size:clamp(2.7rem,14vw,4.2rem)}.lede{font-size:1.08rem}.hero-actions .button,.proof-actions .button{width:100%}.trust-row{display:grid;grid-template-columns:1fr 1fr;gap:7px}.why-grid,.presets,.comparison,.proof-grid{grid-template-columns:1fr}.why-grid article{border-right:0!important;border-bottom:1px solid #334155}.why-grid article:last-child{border-bottom:0}.lab,.proof{padding-block:66px}.section-heading{gap:16px}.section-heading h2{font-size:2.25rem}.result{padding:18px}.result-head{display:grid}.badge{width:max-content}.trace-step{grid-template-columns:24px 78px 76px 1fr;font-size:.8rem}.footer-inner{display:grid}}
+@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation:none!important;transition:none!important}}
diff --git a/test/live-demo.test.mjs b/test/live-demo.test.mjs
new file mode 100644
index 0000000..8b928c5
--- /dev/null
+++ b/test/live-demo.test.mjs
@@ -0,0 +1,34 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { verifyMutationOutcome } from "../src/reference-outcome-verifier.mjs";
+
+const html = await readFile(new URL("../index.html", import.meta.url), "utf8");
+const app = await readFile(new URL("../app.js", import.meta.url), "utf8");
+const css = await readFile(new URL("../styles.css", import.meta.url), "utf8");
+
+test("live demo has one primary heading, lab controls and local assets", () => {
+ assert.equal((html.match(/ {
+ assert.match(app, /from "\.\/src\/reference-outcome-verifier\.mjs"/);
+ for (const primitive of [/\bfetch\s*\(/, /\bXMLHttpRequest\b/, /\bWebSocket\b/, /\bnavigator\.sendBeacon\b/]) assert.doesNotMatch(app, primitive);
+});
+
+test("guided ambiguous-success scenario is VERIFIED by the canonical verifier", () => {
+ assert.deepEqual(verifyMutationOutcome({ taskId:"task:repo-change-001", evidenceTaskId:"task:repo-change-001", attemptId:"attempt:001", evidenceAttemptId:"attempt:001", expectedPreState:"repo@before", expectedPostState:"repo@after", observedPreState:"repo@before", observedPostState:"repo@after", readbackComplete:true, providerAcknowledgement:"ambiguous" }), { status:"VERIFIED", reason:"POSTSTATE_CONFIRMED", retry:"DO_NOT_RETRY" });
+});
+
+test("demo exposes failure presets and explicit proof boundaries", () => {
+ for (const phrase of ["Timeout, but change happened","No readback","Timeout, state unchanged","Stale starting state","Wrong-attempt evidence","Unexpected post-state"]) assert.match(app, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
+ assert.match(html, /not the private MachineOutcome runtime/i);
+ assert.match(html, /no credentials/i);
+ assert.match(html, /no network calls/i);
+});
+
+test("responsive stylesheet includes mobile and reduced-motion handling", () => {
+ assert.match(css, /@media\(max-width:580px\)/);
+ assert.match(css, /prefers-reduced-motion/);
+});
From 5e836be69b20e5203e7407b33ef400f95a66d3b2 Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 09:38:17 +0200
Subject: [PATCH 2/7] ci: add manual verified Pages deployment
---
.github/workflows/pages.yml | 63 +++++++++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
create mode 100644 .github/workflows/pages.yml
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..9bfec8b
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,63 @@
+[Reading 59 lines from start (total: 59 lines, 0 remaining)]
+
+name: Deploy MachineOutcome live demo
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ verify-and-build:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 8
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
+ with:
+ persist-credentials: false
+ - name: Setup Node
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+ with:
+ node-version: "24"
+ - name: Verify reference and live demo
+ run: npm test
+ - name: Configure Pages
+ uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b
+ with:
+ enablement: true
+ - name: Build minimal static artifact
+ run: |
+ set -euo pipefail
+ mkdir -p _site/src
+ cp index.html app.js styles.css _site/
+ cp src/reference-outcome-verifier.mjs _site/src/
+ test -s _site/index.html
+ test -s _site/app.js
+ test -s _site/styles.css
+ test -s _site/src/reference-outcome-verifier.mjs
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b
+ with:
+ path: _site
+
+ deploy:
+ needs: verify-and-build
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-24.04
+ timeout-minutes: 8
+ steps:
+ - name: Deploy Pages
+ id: deployment
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
+
+[executed on device: ubuntu (45c66b70-fab9-48d7-937c-fdf5e7e8a749)]
\ No newline at end of file
From 0ad4440c6f0f633a8c40a4c1a9d81b57c2e7f9cc Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 07:41:33 +0000
Subject: [PATCH 3/7] fix: evaluate MachineOutcome presets immediately
---
app.js | 2 +-
test/live-demo.test.mjs | 4 ++++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/app.js b/app.js
index 8164bf8..80dfbf6 100644
--- a/app.js
+++ b/app.js
@@ -55,7 +55,7 @@ function renderPresets() {
button.dataset.scenario = key;
button.setAttribute("aria-pressed", String(key === selected));
button.innerHTML = `${escapeHtml(scenario.title)} ${escapeHtml(scenario.hint)} `;
- button.addEventListener("click", () => { selected = key; setControls(scenario.controls); validation.textContent = ""; renderPresets(); });
+ button.addEventListener("click", () => { selected = key; setControls(scenario.controls); validation.textContent = ""; renderPresets(); evaluate(scenario.controls); });
presets.append(button);
}
}
diff --git a/test/live-demo.test.mjs b/test/live-demo.test.mjs
index 8b928c5..b30624e 100644
--- a/test/live-demo.test.mjs
+++ b/test/live-demo.test.mjs
@@ -17,6 +17,10 @@ test("browser demo imports canonical verifier and contains no network primitive"
for (const primitive of [/\bfetch\s*\(/, /\bXMLHttpRequest\b/, /\bWebSocket\b/, /\bnavigator\.sendBeacon\b/]) assert.doesNotMatch(app, primitive);
});
+test("preset selection immediately evaluates the selected scenario so stale results cannot remain visible", () => {
+ assert.match(app, /button\.addEventListener\("click",[\s\S]*evaluate\(scenario\.controls\)/);
+});
+
test("guided ambiguous-success scenario is VERIFIED by the canonical verifier", () => {
assert.deepEqual(verifyMutationOutcome({ taskId:"task:repo-change-001", evidenceTaskId:"task:repo-change-001", attemptId:"attempt:001", evidenceAttemptId:"attempt:001", expectedPreState:"repo@before", expectedPostState:"repo@after", observedPreState:"repo@before", observedPostState:"repo@after", readbackComplete:true, providerAcknowledgement:"ambiguous" }), { status:"VERIFIED", reason:"POSTSTATE_CONFIRMED", retry:"DO_NOT_RETRY" });
});
From 9a10cda04a8c0cfb9543dd20519ef616339926a4 Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 07:43:07 +0000
Subject: [PATCH 4/7] fix: reevaluate MachineOutcome on manual input changes
---
app.js | 6 ++++++
test/live-demo.test.mjs | 4 ++++
2 files changed, 10 insertions(+)
diff --git a/app.js b/app.js
index 80dfbf6..3aae1a2 100644
--- a/app.js
+++ b/app.js
@@ -98,6 +98,12 @@ function evaluate(controls) {
try { const input = toVerifierInput(controls); const out = verifyMutationOutcome(input); renderResult(input, out); return out; }
catch (error) { validation.textContent = `VERIFIER_REFUSED_INPUT: ${error instanceof Error ? error.message : String(error)}`; result.innerHTML = `FAIL CLOSED
Invalid evidence contract. The public verifier rejected the input instead of guessing.
`; return null; }
}
+form.addEventListener("change", (event) => {
+ if (!(event.target instanceof HTMLSelectElement)) return;
+ selected = "custom";
+ renderPresets();
+ evaluate(readControls());
+});
form.addEventListener("submit", (event) => { event.preventDefault(); selected = "custom"; renderPresets(); evaluate(readControls()); });
resetButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); validation.textContent = ""; result.innerHTML = `READY
Run an ambiguous action. You will get an outcome status, reason, safe retry decision, evidence trace and the exact verifier input/output.
`; });
guidedButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); evaluate(SCENARIOS.ambiguousVerified.controls); document.querySelector("#lab").scrollIntoView({ behavior: "smooth", block: "start" }); });
diff --git a/test/live-demo.test.mjs b/test/live-demo.test.mjs
index b30624e..6209f2c 100644
--- a/test/live-demo.test.mjs
+++ b/test/live-demo.test.mjs
@@ -21,6 +21,10 @@ test("preset selection immediately evaluates the selected scenario so stale resu
assert.match(app, /button\.addEventListener\("click",[\s\S]*evaluate\(scenario\.controls\)/);
});
+test("manual select changes immediately reevaluate current controls so visible outcomes never stay stale", () => {
+ assert.match(app, /form\.addEventListener\("change",[\s\S]*HTMLSelectElement[\s\S]*evaluate\(readControls\(\)\)/);
+});
+
test("guided ambiguous-success scenario is VERIFIED by the canonical verifier", () => {
assert.deepEqual(verifyMutationOutcome({ taskId:"task:repo-change-001", evidenceTaskId:"task:repo-change-001", attemptId:"attempt:001", evidenceAttemptId:"attempt:001", expectedPreState:"repo@before", expectedPostState:"repo@after", observedPreState:"repo@before", observedPostState:"repo@after", readbackComplete:true, providerAcknowledgement:"ambiguous" }), { status:"VERIFIED", reason:"POSTSTATE_CONFIRMED", retry:"DO_NOT_RETRY" });
});
From f55fbba11d00f428d27fdf0d024482287d24f5e4 Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 09:45:29 +0200
Subject: [PATCH 5/7] fix: scope Pages permissions to deployment job
---
.github/workflows/pages.yml | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 9bfec8b..7ae09ce 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -1,5 +1,3 @@
-[Reading 59 lines from start (total: 59 lines, 0 remaining)]
-
name: Deploy MachineOutcome live demo
on:
@@ -7,8 +5,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
concurrency:
group: pages
@@ -23,16 +19,15 @@ jobs:
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
persist-credentials: false
+
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "24"
+
- name: Verify reference and live demo
run: npm test
- - name: Configure Pages
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b
- with:
- enablement: true
+
- name: Build minimal static artifact
run: |
set -euo pipefail
@@ -43,6 +38,7 @@ jobs:
test -s _site/app.js
test -s _site/styles.css
test -s _site/src/reference-outcome-verifier.mjs
+
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b
with:
@@ -50,14 +46,20 @@ jobs:
deploy:
needs: verify-and-build
+ permissions:
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-24.04
timeout-minutes: 8
steps:
+ - name: Configure Pages
+ uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b
+ with:
+ enablement: true
+
- name: Deploy Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
-
-[executed on device: ubuntu (45c66b70-fab9-48d7-937c-fdf5e7e8a749)]
\ No newline at end of file
From ee01520b5735ad97744c36ab75aab2f68bb6b529 Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 08:53:09 +0000
Subject: [PATCH 6/7] test: execute MachineOutcome demo scenarios end to end
---
README.md | 6 +-
app.js | 127 +++++++++++++++------------------------
src/demo-model.mjs | 120 ++++++++++++++++++++++++++++++++++++
test/demo-model.test.mjs | 39 ++++++++++++
test/live-demo.test.mjs | 20 +++++-
5 files changed, 229 insertions(+), 83 deletions(-)
create mode 100644 src/demo-model.mjs
create mode 100644 test/demo-model.test.mjs
diff --git a/README.md b/README.md
index b955fca..abb431b 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# MachineOutcome - verify what actually happened before retrying
-[**Try the live 30-second Outcome Verification Lab →**](https://samct86.github.io/machineoutcome-case-study/)
+[**Browser lab for this release →**](https://samct86.github.io/machineoutcome-case-study/)
-No install, account, credential or real external write is required. The browser lab uses the same bounded verifier exported by this repository and runs entirely on synthetic state.
+After the Pages deployment for this release completes, no install, account, credential or real external write is required. The browser lab uses the same bounded verifier exported by this repository and runs entirely on synthetic state.
[](https://github.com/SamCT86/machineoutcome-case-study/actions/workflows/verify-reference.yml)
@@ -40,7 +40,7 @@ Start with **2–3 sentences** describing what is slow, expensive or unreliable.
## Try it
-**Fastest path:** open the [live browser lab](https://samct86.github.io/machineoutcome-case-study/) and run **Timeout, but change happened**. Then switch to **No readback** and **Wrong-attempt evidence** to see the safe action change.
+**Fastest path after the Pages deployment completes:** open the [browser lab](https://samct86.github.io/machineoutcome-case-study/) and run **Timeout, but change happened**. Then switch to **No readback** and **Wrong-attempt evidence** to see the safe action change.
**Technical verification from a clean clone:**
diff --git a/app.js b/app.js
index 3aae1a2..bbd45bd 100644
--- a/app.js
+++ b/app.js
@@ -1,26 +1,5 @@
import { verifyMutationOutcome } from "./src/reference-outcome-verifier.mjs";
-
-const BASE = Object.freeze({
- taskId: "task:repo-change-001",
- evidenceTaskId: "task:repo-change-001",
- attemptId: "attempt:001",
- evidenceAttemptId: "attempt:001",
- expectedPreState: "repo@before",
- expectedPostState: "repo@after",
- observedPreState: "repo@before",
- observedPostState: "repo@after",
- readbackComplete: true,
- providerAcknowledgement: "accepted",
-});
-
-const SCENARIOS = Object.freeze({
- ambiguousVerified: { title: "Timeout, but change happened", hint: "The acknowledgement disappears, but readback proves the expected state.", controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "expected", identity: "exact" } },
- readbackMissing: { title: "No readback", hint: "Reality is unavailable, so retry remains blocked.", controls: { ack: "ambiguous", prestate: "match", readback: "incomplete", poststate: "expected", identity: "exact" } },
- unchanged: { title: "Timeout, state unchanged", hint: "The provider is reachable, but the state still looks unchanged.", controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "unchanged", identity: "exact" } },
- stalePrestate: { title: "Stale starting state", hint: "The action was prepared against an outdated state.", controls: { ack: "accepted", prestate: "stale", readback: "complete", poststate: "expected", identity: "exact" } },
- wrongAttempt: { title: "Wrong-attempt evidence", hint: "The state looks right, but the evidence belongs to another attempt.", controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "expected", identity: "wrong-attempt" } },
- wrongPost: { title: "Unexpected post-state", hint: "Readback is complete, but reality is not what the action was supposed to produce.", controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "unexpected", identity: "exact" } },
-});
+import { SCENARIOS, escapeHtml, evaluateControls } from "./src/demo-model.mjs";
const form = document.querySelector("#lab-form");
const presets = document.querySelector("#presets");
@@ -30,22 +9,14 @@ const resetButton = document.querySelector("[data-reset]");
const guidedButton = document.querySelector("[data-run-guided]");
let selected = "ambiguousVerified";
-function escapeHtml(value) {
- return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
+function setControls(values) {
+ for (const [key, value] of Object.entries(values)) form.elements[key].value = value;
}
-function setControls(values) { for (const [key, value] of Object.entries(values)) form.elements[key].value = value; }
-function readControls() { return Object.fromEntries(["ack", "prestate", "readback", "poststate", "identity"].map((key) => [key, form.elements[key].value])); }
-function toVerifierInput(c) {
- const input = { ...BASE };
- input.providerAcknowledgement = c.ack;
- input.observedPreState = c.prestate === "match" ? BASE.expectedPreState : "repo@stale";
- input.readbackComplete = c.readback === "complete";
- input.observedPostState = c.poststate === "expected" ? BASE.expectedPostState : c.poststate === "unchanged" ? input.observedPreState : "repo@unexpected";
- if (!input.readbackComplete) input.observedPostState = null;
- if (c.identity === "wrong-attempt") input.evidenceAttemptId = "attempt:other";
- if (c.identity === "wrong-task") input.evidenceTaskId = "task:other";
- return input;
+
+function readControls() {
+ return Object.fromEntries(["ack", "prestate", "readback", "poststate", "identity"].map((key) => [key, form.elements[key].value]));
}
+
function renderPresets() {
presets.innerHTML = "";
for (const [key, scenario] of Object.entries(SCENARIOS)) {
@@ -54,58 +25,60 @@ function renderPresets() {
button.className = "preset";
button.dataset.scenario = key;
button.setAttribute("aria-pressed", String(key === selected));
- button.innerHTML = `${escapeHtml(scenario.title)} ${escapeHtml(scenario.hint)} `;
- button.addEventListener("click", () => { selected = key; setControls(scenario.controls); validation.textContent = ""; renderPresets(); evaluate(scenario.controls); });
+ button.innerHTML = '' + escapeHtml(scenario.title) + ' ' + escapeHtml(scenario.hint) + ' ';
+ button.addEventListener("click", () => {
+ selected = key;
+ setControls(scenario.controls);
+ validation.textContent = "";
+ renderPresets();
+ evaluate(scenario.controls);
+ });
presets.append(button);
}
}
-function explanation(out) {
- const table = {
- POSTSTATE_CONFIRMED: ["Observed state matches the expected result.", "Do not retry the mutation."],
- PRESTATE_MISMATCH: ["The starting state changed before the action could be trusted.", "Do not mutate from this stale pre-state."],
- READBACK_INCOMPLETE: ["The external reality is not established yet.", "Reconcile state before retrying."],
- AMBIGUOUS_MUTATION_STATE: ["The acknowledgement was ambiguous and the observed state is still unchanged.", "Keep the outcome UNKNOWN and reconcile before retrying."],
- EVIDENCE_ATTEMPT_MISMATCH: ["The readback belongs to a different attempt.", "Reject the evidence and do not retry from it."],
- EVIDENCE_TASK_MISMATCH: ["The readback belongs to a different task.", "Reject the evidence and do not retry from it."],
- POSTSTATE_MISMATCH: ["Readback is complete, but reality does not match the expected result.", "Treat the outcome as failed; do not blindly replay the mutation."],
- };
- return table[out.reason] ?? ["The verifier refused to infer a safe outcome.", out.retry.replaceAll("_", " ").toLowerCase() + "."];
-}
-function traceRows(input, out) {
- const rows = [
- ["1", "IDENTITY", input.evidenceTaskId === input.taskId && input.evidenceAttemptId === input.attemptId ? "bound" : "mismatch", `${input.evidenceTaskId} / ${input.evidenceAttemptId}`],
- ["2", "PRE-STATE", input.observedPreState === input.expectedPreState ? "match" : "mismatch", `expected ${input.expectedPreState} · observed ${input.observedPreState}`],
- ["3", "ATTEMPT", "attempted", `provider acknowledgement: ${input.providerAcknowledgement}`],
- ["4", "READBACK", input.readbackComplete ? "complete" : "incomplete", input.readbackComplete ? `observed ${input.observedPostState}` : "provider state not established"],
- ["5", "OUTCOME", out.status.toLowerCase(), `${out.status} · ${out.reason}`],
- ["6", "RETRY", out.retry === "DO_NOT_RETRY" || out.retry === "DO_NOT_MUTATE" ? "blocked" : "hold", out.retry.replaceAll("_", " ")],
- ];
- return rows.map(([n, stage, state, detail]) => `${n} ${stage} ${escapeHtml(state)} ${escapeHtml(detail)}
`).join("");
-}
-function naiveReaction(input) { return input.providerAcknowledgement === "ambiguous" ? "Treat the timeout as failure and immediately repeat the external action." : "Trust the transport response without proving the downstream state."; }
-function renderResult(input, out) {
- const [why, safe] = explanation(out);
- const tone = out.status.toLowerCase();
- result.innerHTML = `
- OUTCOME
${escapeHtml(out.status)} ${escapeHtml(why)}
${escapeHtml(out.reason)}
- Safe next action ${escapeHtml(safe)}
- Naive reaction ${escapeHtml(naiveReaction(input))}
MachineOutcome reaction ${escapeHtml(out.retry.replaceAll("_", " "))}
- ${traceRows(input, out)}
- Inspect exact verifier input and output ${escapeHtml(JSON.stringify({ input, output: out }, null, 2))} `;
-}
+
function evaluate(controls) {
validation.textContent = "";
- try { const input = toVerifierInput(controls); const out = verifyMutationOutcome(input); renderResult(input, out); return out; }
- catch (error) { validation.textContent = `VERIFIER_REFUSED_INPUT: ${error instanceof Error ? error.message : String(error)}`; result.innerHTML = `FAIL CLOSED
Invalid evidence contract. The public verifier rejected the input instead of guessing.
`; return null; }
+ try {
+ const evaluation = evaluateControls(controls, verifyMutationOutcome);
+ result.innerHTML = evaluation.markup;
+ return evaluation.output;
+ } catch (error) {
+ validation.textContent = "VERIFIER_REFUSED_INPUT: " + (error instanceof Error ? error.message : String(error));
+ result.innerHTML = 'FAIL CLOSED
Invalid evidence contract. The public verifier rejected the input instead of guessing.
';
+ return null;
+ }
}
+
form.addEventListener("change", (event) => {
if (!(event.target instanceof HTMLSelectElement)) return;
selected = "custom";
renderPresets();
evaluate(readControls());
});
-form.addEventListener("submit", (event) => { event.preventDefault(); selected = "custom"; renderPresets(); evaluate(readControls()); });
-resetButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); validation.textContent = ""; result.innerHTML = `READY
Run an ambiguous action. You will get an outcome status, reason, safe retry decision, evidence trace and the exact verifier input/output.
`; });
-guidedButton.addEventListener("click", () => { selected = "ambiguousVerified"; setControls(SCENARIOS.ambiguousVerified.controls); renderPresets(); evaluate(SCENARIOS.ambiguousVerified.controls); document.querySelector("#lab").scrollIntoView({ behavior: "smooth", block: "start" }); });
+
+form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ selected = "custom";
+ renderPresets();
+ evaluate(readControls());
+});
+
+resetButton.addEventListener("click", () => {
+ selected = "ambiguousVerified";
+ setControls(SCENARIOS.ambiguousVerified.controls);
+ renderPresets();
+ validation.textContent = "";
+ result.innerHTML = 'READY
Run an ambiguous action. You will get an outcome status, reason, safe retry decision, evidence trace and the exact verifier input/output.
';
+});
+
+guidedButton.addEventListener("click", () => {
+ selected = "ambiguousVerified";
+ setControls(SCENARIOS.ambiguousVerified.controls);
+ renderPresets();
+ evaluate(SCENARIOS.ambiguousVerified.controls);
+ document.querySelector("#lab").scrollIntoView({ behavior: "smooth", block: "start" });
+});
+
setControls(SCENARIOS.ambiguousVerified.controls);
renderPresets();
diff --git a/src/demo-model.mjs b/src/demo-model.mjs
new file mode 100644
index 0000000..a94c8c4
--- /dev/null
+++ b/src/demo-model.mjs
@@ -0,0 +1,120 @@
+export const BASE = Object.freeze({
+ taskId: "task:repo-change-001",
+ evidenceTaskId: "task:repo-change-001",
+ attemptId: "attempt:001",
+ evidenceAttemptId: "attempt:001",
+ expectedPreState: "repo@before",
+ expectedPostState: "repo@after",
+ observedPreState: "repo@before",
+ observedPostState: "repo@after",
+ readbackComplete: true,
+ providerAcknowledgement: "accepted",
+});
+
+export const SCENARIOS = Object.freeze({
+ ambiguousVerified: {
+ title: "Timeout, but change happened",
+ hint: "The acknowledgement disappears, but readback proves the expected state.",
+ controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "expected", identity: "exact" },
+ },
+ readbackMissing: {
+ title: "No readback",
+ hint: "Reality is unavailable, so retry remains blocked.",
+ controls: { ack: "ambiguous", prestate: "match", readback: "incomplete", poststate: "expected", identity: "exact" },
+ },
+ unchanged: {
+ title: "Timeout, state unchanged",
+ hint: "The provider is reachable, but the state still looks unchanged.",
+ controls: { ack: "ambiguous", prestate: "match", readback: "complete", poststate: "unchanged", identity: "exact" },
+ },
+ stalePrestate: {
+ title: "Stale starting state",
+ hint: "The action was prepared against an outdated state.",
+ controls: { ack: "accepted", prestate: "stale", readback: "complete", poststate: "expected", identity: "exact" },
+ },
+ wrongAttempt: {
+ title: "Wrong-attempt evidence",
+ hint: "The state looks right, but the evidence belongs to another attempt.",
+ controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "expected", identity: "wrong-attempt" },
+ },
+ wrongPost: {
+ title: "Unexpected post-state",
+ hint: "Readback is complete, but reality is not what the action was supposed to produce.",
+ controls: { ack: "accepted", prestate: "match", readback: "complete", poststate: "unexpected", identity: "exact" },
+ },
+});
+
+export function escapeHtml(value) {
+ return String(value)
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+export function toVerifierInput(c) {
+ const input = { ...BASE };
+ input.providerAcknowledgement = c.ack;
+ input.observedPreState = c.prestate === "match" ? BASE.expectedPreState : "repo@stale";
+ input.readbackComplete = c.readback === "complete";
+ input.observedPostState =
+ c.poststate === "expected" ? BASE.expectedPostState :
+ c.poststate === "unchanged" ? input.observedPreState :
+ "repo@unexpected";
+ if (!input.readbackComplete) input.observedPostState = null;
+ if (c.identity === "wrong-attempt") input.evidenceAttemptId = "attempt:other";
+ if (c.identity === "wrong-task") input.evidenceTaskId = "task:other";
+ return input;
+}
+
+export function explanation(out) {
+ const table = {
+ POSTSTATE_CONFIRMED: ["Observed state matches the expected result.", "Do not retry the mutation."],
+ PRESTATE_MISMATCH: ["The starting state changed before the action could be trusted.", "Do not mutate from this stale pre-state."],
+ READBACK_INCOMPLETE: ["The external reality is not established yet.", "Reconcile state before retrying."],
+ AMBIGUOUS_MUTATION_STATE: ["The acknowledgement was ambiguous and the observed state is still unchanged.", "Keep the outcome UNKNOWN and reconcile before retrying."],
+ EVIDENCE_ATTEMPT_MISMATCH: ["The readback belongs to a different attempt.", "Reject the evidence and do not retry from it."],
+ EVIDENCE_TASK_MISMATCH: ["The readback belongs to a different task.", "Reject the evidence and do not retry from it."],
+ POSTSTATE_MISMATCH: ["Readback is complete, but reality does not match the expected result.", "Treat the outcome as failed; do not blindly replay the mutation."],
+ };
+ return table[out.reason] ?? ["The verifier refused to infer a safe outcome.", out.retry.replaceAll("_", " ").toLowerCase() + "."];
+}
+
+export function naiveReaction(input) {
+ return input.providerAcknowledgement === "ambiguous"
+ ? "Treat the timeout as failure and immediately repeat the external action."
+ : "Trust the transport response without proving the downstream state.";
+}
+
+export function traceRows(input, out) {
+ return [
+ ["1", "IDENTITY", input.evidenceTaskId === input.taskId && input.evidenceAttemptId === input.attemptId ? "bound" : "mismatch", input.evidenceTaskId + " / " + input.evidenceAttemptId],
+ ["2", "PRE-STATE", input.observedPreState === input.expectedPreState ? "match" : "mismatch", "expected " + input.expectedPreState + " · observed " + input.observedPreState],
+ ["3", "ATTEMPT", "attempted", "provider acknowledgement: " + input.providerAcknowledgement],
+ ["4", "READBACK", input.readbackComplete ? "complete" : "incomplete", input.readbackComplete ? "observed " + input.observedPostState : "provider state not established"],
+ ["5", "OUTCOME", out.status.toLowerCase(), out.status + " · " + out.reason],
+ ["6", "RETRY", out.retry === "DO_NOT_RETRY" || out.retry === "DO_NOT_MUTATE" ? "blocked" : "hold", out.retry.replaceAll("_", " ")],
+ ];
+}
+
+export function renderResultMarkup(input, out) {
+ const [why, safe] = explanation(out);
+ const tone = out.status.toLowerCase();
+ const trace = traceRows(input, out)
+ .map(([n, stage, state, detail]) => '' + n + ' ' + stage + ' ' + escapeHtml(state) + ' ' + escapeHtml(detail) + '
')
+ .join("");
+ return [
+ 'OUTCOME
' + escapeHtml(out.status) + ' ' + escapeHtml(why) + '
' + escapeHtml(out.reason) + ' ',
+ 'Safe next action ' + escapeHtml(safe) + '
',
+ 'Naive reaction ' + escapeHtml(naiveReaction(input)) + '
MachineOutcome reaction ' + escapeHtml(out.retry.replaceAll("_", " ")) + '
',
+ '' + trace + '
',
+ 'Inspect exact verifier input and output ' + escapeHtml(JSON.stringify({ input, output: out }, null, 2)) + ' ',
+ ].join("");
+}
+
+export function evaluateControls(controls, verifier) {
+ const input = toVerifierInput(controls);
+ const output = verifier(input);
+ return { input, output, markup: renderResultMarkup(input, output) };
+}
diff --git a/test/demo-model.test.mjs b/test/demo-model.test.mjs
new file mode 100644
index 0000000..86b8655
--- /dev/null
+++ b/test/demo-model.test.mjs
@@ -0,0 +1,39 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { verifyMutationOutcome } from "../src/reference-outcome-verifier.mjs";
+import { SCENARIOS, evaluateControls, escapeHtml, toVerifierInput } from "../src/demo-model.mjs";
+
+const EXPECTED = {
+ ambiguousVerified: { status: "VERIFIED", reason: "POSTSTATE_CONFIRMED", retry: "DO_NOT_RETRY" },
+ readbackMissing: { status: "UNKNOWN", reason: "READBACK_INCOMPLETE", retry: "RECONCILE_BEFORE_RETRY" },
+ unchanged: { status: "UNKNOWN", reason: "AMBIGUOUS_MUTATION_STATE", retry: "RECONCILE_BEFORE_RETRY" },
+ stalePrestate: { status: "FAILED", reason: "PRESTATE_MISMATCH", retry: "DO_NOT_MUTATE" },
+ wrongAttempt: { status: "FAILED", reason: "EVIDENCE_ATTEMPT_MISMATCH", retry: "DO_NOT_RETRY" },
+ wrongPost: { status: "FAILED", reason: "POSTSTATE_MISMATCH", retry: "DO_NOT_RETRY" },
+};
+
+for (const [name, scenario] of Object.entries(SCENARIOS)) {
+ test("scenario " + name + " executes through canonical verifier and renders the exact outcome", () => {
+ const { input, output, markup } = evaluateControls(scenario.controls, verifyMutationOutcome);
+ assert.deepEqual(output, EXPECTED[name]);
+ assert.match(markup, new RegExp(">" + output.status + "<"));
+ assert.match(markup, new RegExp(output.reason));
+ assert.match(markup, new RegExp(output.retry.replaceAll("_", " ")));
+ assert.match(markup, /Safe next action/);
+ assert.match(markup, /Inspect exact verifier input and output/);
+ assert.equal(input.taskId, "task:repo-change-001");
+ });
+}
+
+test("wrong-task manual control is executable and fails closed", () => {
+ const input = toVerifierInput({ ack:"accepted", prestate:"match", readback:"complete", poststate:"expected", identity:"wrong-task" });
+ assert.deepEqual(verifyMutationOutcome(input), {
+ status: "FAILED",
+ reason: "EVIDENCE_TASK_MISMATCH",
+ retry: "DO_NOT_RETRY",
+ });
+});
+
+test("rendered synthetic values are escaped before HTML insertion", () => {
+ assert.equal(escapeHtml(' '), "<img src=x onerror="alert(1)">");
+});
diff --git a/test/live-demo.test.mjs b/test/live-demo.test.mjs
index 6209f2c..9631141 100644
--- a/test/live-demo.test.mjs
+++ b/test/live-demo.test.mjs
@@ -5,11 +5,14 @@ import { verifyMutationOutcome } from "../src/reference-outcome-verifier.mjs";
const html = await readFile(new URL("../index.html", import.meta.url), "utf8");
const app = await readFile(new URL("../app.js", import.meta.url), "utf8");
+const model = await readFile(new URL("../src/demo-model.mjs", import.meta.url), "utf8");
const css = await readFile(new URL("../styles.css", import.meta.url), "utf8");
test("live demo has one primary heading, lab controls and local assets", () => {
assert.equal((html.match(/ {
@@ -26,11 +29,22 @@ test("manual select changes immediately reevaluate current controls so visible o
});
test("guided ambiguous-success scenario is VERIFIED by the canonical verifier", () => {
- assert.deepEqual(verifyMutationOutcome({ taskId:"task:repo-change-001", evidenceTaskId:"task:repo-change-001", attemptId:"attempt:001", evidenceAttemptId:"attempt:001", expectedPreState:"repo@before", expectedPostState:"repo@after", observedPreState:"repo@before", observedPostState:"repo@after", readbackComplete:true, providerAcknowledgement:"ambiguous" }), { status:"VERIFIED", reason:"POSTSTATE_CONFIRMED", retry:"DO_NOT_RETRY" });
+ assert.deepEqual(verifyMutationOutcome({
+ taskId:"task:repo-change-001",
+ evidenceTaskId:"task:repo-change-001",
+ attemptId:"attempt:001",
+ evidenceAttemptId:"attempt:001",
+ expectedPreState:"repo@before",
+ expectedPostState:"repo@after",
+ observedPreState:"repo@before",
+ observedPostState:"repo@after",
+ readbackComplete:true,
+ providerAcknowledgement:"ambiguous",
+ }), { status:"VERIFIED", reason:"POSTSTATE_CONFIRMED", retry:"DO_NOT_RETRY" });
});
test("demo exposes failure presets and explicit proof boundaries", () => {
- for (const phrase of ["Timeout, but change happened","No readback","Timeout, state unchanged","Stale starting state","Wrong-attempt evidence","Unexpected post-state"]) assert.match(app, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
+ for (const phrase of ["Timeout, but change happened","No readback","Timeout, state unchanged","Stale starting state","Wrong-attempt evidence","Unexpected post-state"]) assert.ok(model.includes(phrase), phrase);
assert.match(html, /not the private MachineOutcome runtime/i);
assert.match(html, /no credentials/i);
assert.match(html, /no network calls/i);
From 967fb7e1da404687e886222b418506f63fe83f6a Mon Sep 17 00:00:00 2001
From: Sarmad Tawfeek <125739417+SamCT86@users.noreply.github.com>
Date: Wed, 7 Oct 2026 10:53:35 +0200
Subject: [PATCH 7/7] fix: publish browser demo model in Pages artifact
---
.github/workflows/pages.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 7ae09ce..3033516 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -33,11 +33,12 @@ jobs:
set -euo pipefail
mkdir -p _site/src
cp index.html app.js styles.css _site/
- cp src/reference-outcome-verifier.mjs _site/src/
+ cp src/reference-outcome-verifier.mjs src/demo-model.mjs _site/src/
test -s _site/index.html
test -s _site/app.js
test -s _site/styles.css
test -s _site/src/reference-outcome-verifier.mjs
+ test -s _site/src/demo-model.mjs
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b