diff --git a/package.json b/package.json
index 01c92bc..9afa2b7 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,9 @@
"typecheck": "tsc -p tsconfig.templates.json && tsc -p tsconfig.spec.json && tsc -p tsconfig.jsx.json",
"syntaxcheck": "node --check skills/webmcpify/templates/webmcpify.js",
"test": "node --test \"tests/*.test.mjs\"",
- "check": "npm run typecheck && npm run syntaxcheck && npm test"
+ "check": "npm run typecheck && npm run syntaxcheck && npm test",
+ "proof:verify": "xvfb-run -a node proof/demo/run.mjs --verify",
+ "proof:record": "xvfb-run -a node proof/demo/run.mjs --record"
},
"devDependencies": {
"@playwright/test": "^1.54.0",
diff --git a/proof/README.md b/proof/README.md
new file mode 100644
index 0000000..fcf62f3
--- /dev/null
+++ b/proof/README.md
@@ -0,0 +1,62 @@
+# WebMCPify proof pack
+
+This is a sanitized, deterministic run against a local release-notes fixture. It
+contains no customer data and performs no network mutation. The single candidate,
+`set_release_filter`, wraps the same browser-local filter path used by the visible
+buttons.
+
+## Reproduce the verification
+
+Requirements: Node 20+, dependencies from `npm ci`, Google Chrome 150+ (earlier
+releases do not expose the native `document.modelContext` surface and the run
+fails its first assertion), `xvfb-run`, and (for the derivative) ffmpeg. Chrome
+is located through Playwright's `chrome` channel; set `CHROME_BIN` to point at a
+specific binary. On a desktop session you can skip Xvfb and run
+`node proof/demo/run.mjs --verify` directly.
+
+```sh
+npm ci
+npm run proof:verify
+```
+
+The command starts a loopback-only static server, opens system Google Chrome
+headed under Xvfb with `--enable-features=WebMCP,WebMCPTesting`, proves that no
+tool exists before approval/integration, triggers the bounded integration, then:
+
+1. enumerates `set_release_filter` through native `document.modelContext.getTools()`;
+2. parses and compares its stringified schema and checks `readOnlyHint: false`;
+3. executes `{ "category": "fix" }` through native `executeTool()`;
+4. checks the result string and the visible UI delta; and
+5. confirms an invalid enum resolves the runtime's bounded `ERROR:` convention
+ without changing UI state.
+
+## Reproduce the 63-second uncut recording
+
+```sh
+npm run proof:record
+ffmpeg -y -i proof/artifacts/webmcpify-proof-source.webm \
+ -vf scale=854:-2 -c:v libx264 -preset slow -crf 28 -movflags +faststart \
+ -an proof/artifacts/webmcpify-proof-480p.mp4
+sha256sum proof/artifacts/webmcpify-proof-source.webm \
+ proof/artifacts/webmcpify-proof-480p.mp4 > proof/artifacts/SHA256SUMS
+```
+
+The recording is one continuous browser capture. The phase labels and log lines
+are advanced by `proof/demo/run.mjs`; the gate is a real click, integration is a
+real registration through the vendored runtime, and verification uses Chrome's
+native production enumeration/execution surface. Pauses are intentional so the
+workflow is legible at normal playback speed.
+
+## Sanitized before/after evidence
+
+- [`manifest.before.json`](manifest.before.json) is the inventory result at the
+ human gate. The client mutation is still `discovered` and no setup exists.
+- [`manifest.after.json`](manifest.after.json) records approval, two bounded setup
+ paths, and successful native-Chrome verification.
+- [`integration.patch`](integration.patch) is the complete conceptual app diff:
+ one module script plus one tool module; no server endpoint or unrelated UI path.
+
+The manifests contain only fixture paths, loopback URLs, and synthetic release
+notes. Video output is reproducible but binary-identical hashes can vary with the
+installed Chrome/ffmpeg builds; the checked-in `SHA256SUMS` identifies the review
+artifacts in this revision.
diff --git a/proof/artifacts/SHA256SUMS b/proof/artifacts/SHA256SUMS
new file mode 100644
index 0000000..de9c97e
--- /dev/null
+++ b/proof/artifacts/SHA256SUMS
@@ -0,0 +1,2 @@
+7e0ef201630a3231abf4b192ec48f17b78d2e9e6cd4f92b34182e49d6d41c7cd proof/artifacts/webmcpify-proof-source.webm
+8edf979fc56c9df802c65554591377fab85f35de7baa06829a19d5ea2c3df75e proof/artifacts/webmcpify-proof-480p.mp4
diff --git a/proof/artifacts/webmcpify-proof-480p.mp4 b/proof/artifacts/webmcpify-proof-480p.mp4
new file mode 100644
index 0000000..793e538
Binary files /dev/null and b/proof/artifacts/webmcpify-proof-480p.mp4 differ
diff --git a/proof/artifacts/webmcpify-proof-source.webm b/proof/artifacts/webmcpify-proof-source.webm
new file mode 100644
index 0000000..683aae8
Binary files /dev/null and b/proof/artifacts/webmcpify-proof-source.webm differ
diff --git a/proof/demo/app.js b/proof/demo/app.js
new file mode 100644
index 0000000..0250e9a
--- /dev/null
+++ b/proof/demo/app.js
@@ -0,0 +1,41 @@
+const notes = [...document.querySelectorAll('[data-category]')];
+
+export function applyFilter(category) {
+ let visible = 0;
+ for (const note of notes) {
+ const show = category === 'all' || note.dataset.category === category;
+ note.hidden = !show;
+ if (show) visible++;
+ }
+ document.querySelectorAll('[data-filter]').forEach((button) =>
+ button.setAttribute('aria-pressed', String(button.dataset.filter === category)));
+ document.querySelector('#visible-count').textContent = `${visible} release notes visible`;
+ return visible;
+}
+
+document.querySelectorAll('[data-filter]').forEach((button) =>
+ button.addEventListener('click', () => applyFilter(button.dataset.filter)));
+
+const log = document.querySelector('#log');
+window.proof = {
+ phase(name, title) {
+ document.querySelectorAll('#phases span').forEach((item) => {
+ const phases = ['inventory', 'gate', 'integrate', 'verify', 'audit'];
+ const current = phases.indexOf(name);
+ const itemIndex = phases.indexOf(item.dataset.phase);
+ item.className = itemIndex < current ? 'done' : itemIndex === current ? 'active' : '';
+ });
+ document.querySelector('#phase-label').textContent = name.toUpperCase();
+ document.querySelector('#proof-title').textContent = title;
+ },
+ line(text) { log.textContent += `${text}\n`; log.scrollTop = log.scrollHeight; },
+ manifest(show = true) { document.querySelector('#manifest').hidden = !show; },
+ gate() { document.querySelector('#gate').showModal(); },
+ check(text) { document.querySelector('#checks').insertAdjacentHTML('beforeend', `