diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..3033516
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,66 @@
+name: Deploy MachineOutcome live demo
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+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: 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 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
+ with:
+ path: _site
+
+ 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
diff --git a/README.md b/README.md
index 81faab8..abb431b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# MachineOutcome - verify what actually happened before retrying
+[**Browser lab for this release →**](https://samct86.github.io/machineoutcome-case-study/)
+
+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)
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 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:**
+
```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..bbd45bd
--- /dev/null
+++ b/app.js
@@ -0,0 +1,84 @@
+import { verifyMutationOutcome } from "./src/reference-outcome-verifier.mjs";
+import { SCENARIOS, escapeHtml, evaluateControls } from "./src/demo-model.mjs";
+
+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 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 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();
+ evaluate(scenario.controls);
+ });
+ presets.append(button);
+ }
+}
+
+function evaluate(controls) {
+ validation.textContent = "";
+ 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" });
+});
+
+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/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/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/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
new file mode 100644
index 0000000..9631141
--- /dev/null
+++ b/test/live-demo.test.mjs
@@ -0,0 +1,56 @@
+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 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(/ {
+ 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("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("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" });
+});
+
+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.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);
+});
+
+test("responsive stylesheet includes mobile and reduced-motion handling", () => {
+ assert.match(css, /@media\(max-width:580px\)/);
+ assert.match(css, /prefers-reduced-motion/);
+});