@deeeed/metamask-harness 0.51.9 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +14 -0
- package/dist/index.js +2 -0
- package/dist/runtime-proof.js +54 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.52.0 - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Export portable runtime proof validation so standalone QA requires executed assertions and rejects passing packages without behavioral proof.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Align tutorials with explicit workflow invocation, static review, recipe-qa scopes and caller-owned publication/cleanup; refresh shared execution-template examples.
|
|
14
|
+
|
|
5
15
|
## 0.51.9 - 2026-09-16
|
|
6
16
|
|
|
7
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -262,3 +262,17 @@ mm-harness --version
|
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
Unset `MM_HARNESS_BIN` to return to the published installation.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
### Workflow entry points and completion
|
|
268
|
+
|
|
269
|
+
Explicitly invoke `mms-recipe-dev`, `mms-recipe-fixbug`, `mms-recipe-qa` or the team's static review skill. `mms-recipe-cook` selects or resumes their shared Markdown checklist. Matching descriptions or changed paths never activates these workflows.
|
|
270
|
+
|
|
271
|
+
Static Perps review uses a generated source checklist without installing or launching the app harness. `recipe-qa` freezes a PR, range or daily change window and requires runtime smoke plus selected recipe proof. Official release validation uses its named release profile and proof lane.
|
|
272
|
+
|
|
273
|
+
TASK.md holds the request; CHECKLIST.md is the sole step list. The frozen terminal contract and task-local mark validate completion. Publication, retained sessions and workspace cleanup belong to the caller. Farmslot handles them through its control plane; direct invocation returns the outcome to the engineer. See the [task-directory guide](site/task-dir.html) and [QA tutorial](site/tutorials/v5.html).
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
### Portable runtime proof
|
|
277
|
+
|
|
278
|
+
`validateRuntimeProof(bundle, smokeTarget)` validates a retained Recipe v1 artifact package, requiring executed behavioral assertions for its declared targets. A passing command or end node alone is insufficient. QA skills use this exported function through their locked harness installation; no gateway, farm profile or run ID is required.
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
walletFixturePath
|
|
10
10
|
} from "./paths.js";
|
|
11
11
|
import { captureActiveRecipeRecordingSnapshot } from "./run-recording.js";
|
|
12
|
+
import { validateRuntimeProof } from "./runtime-proof.js";
|
|
12
13
|
export {
|
|
13
14
|
captureActiveRecipeRecordingSnapshot,
|
|
14
15
|
createDoctorReport,
|
|
@@ -21,5 +22,6 @@ export {
|
|
|
21
22
|
loadActionManifest,
|
|
22
23
|
recipeHarnessPath,
|
|
23
24
|
validateManifest,
|
|
25
|
+
validateRuntimeProof,
|
|
24
26
|
walletFixturePath
|
|
25
27
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { normalizeRecipeRef, validateRecipeArtifactPackage } from "@farmslot/protocol";
|
|
3
|
+
function validateRuntimeProof(bundle, smokeTarget) {
|
|
4
|
+
const checked = validateRecipeArtifactPackage(bundle);
|
|
5
|
+
assert.equal(checked.status, "valid", JSON.stringify(checked.findings));
|
|
6
|
+
const { recipe, summary, trace, manifest, recipeResolution, resolvedRecipes } = bundle;
|
|
7
|
+
assert.equal(summary.status, "pass", "Runtime recipe did not pass");
|
|
8
|
+
const entries = Array.isArray(trace) ? trace : trace.entries;
|
|
9
|
+
assert(entries.length && entries.every((entry) => entry.ok === true), "Runtime trace contains failed or unknown actions");
|
|
10
|
+
assert(entries.some((entry) => entry.action === "end" && entry.status === "pass" && !String(entry.nodeId ?? entry.id).includes("/")), "Runtime recipe did not reach its passing end");
|
|
11
|
+
const documents = new Map(recipeResolution.dependencies.map((dependency) => [normalizeRecipeRef(dependency.ref), resolvedRecipes[dependency.digest]]));
|
|
12
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
13
|
+
function collect(document, prefix = "", stack = /* @__PURE__ */ new Set()) {
|
|
14
|
+
for (const [id, node] of Object.entries(document.workflow.nodes)) {
|
|
15
|
+
nodes.set(prefix + id, node);
|
|
16
|
+
if (node.action === "call" && typeof node.ref === "string") {
|
|
17
|
+
const ref = normalizeRecipeRef(node.ref);
|
|
18
|
+
assert(!stack.has(ref), "Recursive recipe proof");
|
|
19
|
+
const nested = documents.get(ref);
|
|
20
|
+
assert(nested, `Missing resolved recipe ${ref}`);
|
|
21
|
+
collect(nested, `${prefix}${id}/`, new Set(stack).add(ref));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
collect(recipe);
|
|
26
|
+
const executed = new Map(entries.map((entry, index) => [entry.nodeId ?? entry.id, { entry, index }]));
|
|
27
|
+
const assertions = /* @__PURE__ */ new Set(["assert_output", "assert_json", "ui.wait_for"]);
|
|
28
|
+
const controls = /* @__PURE__ */ new Set(["end", "wait", "manual", "switch", "call", "index_artifacts", "assert_exit_code", "assert_file", "branch", "repeat", "set", "log"]);
|
|
29
|
+
function proves(id) {
|
|
30
|
+
const node = nodes.get(id);
|
|
31
|
+
const current = executed.get(id);
|
|
32
|
+
if (!current || current.entry.action !== node.action) return false;
|
|
33
|
+
if (node.action === "call") return [...nodes.keys()].some((child) => child.startsWith(`${id}/`) && proves(child));
|
|
34
|
+
if (!assertions.has(node.action) || !current.entry.output || typeof current.entry.output !== "object") return false;
|
|
35
|
+
if (node.action === "assert_output") {
|
|
36
|
+
const prefix = id.includes("/") ? id.slice(0, id.lastIndexOf("/") + 1) : "";
|
|
37
|
+
const source = prefix + String(node.source ?? node.node ?? "");
|
|
38
|
+
const prior = executed.get(source);
|
|
39
|
+
const action = nodes.get(source)?.action;
|
|
40
|
+
return Boolean(prior && prior.index < current.index && action && action === prior.entry.action && !controls.has(action) && !assertions.has(action));
|
|
41
|
+
}
|
|
42
|
+
if (node.action === "ui.wait_for") return entries.slice(0, current.index).some((entry) => ["ui.press", "ui.key_press", "ui.set_input", "ui.swipe", "ui.drag", "ui.long_press"].includes(entry.action));
|
|
43
|
+
return typeof node.path === "string" && manifest.artifacts.some((artifact) => artifact.path === node.path && artifact.category === "proof");
|
|
44
|
+
}
|
|
45
|
+
assert(recipe.proofTargets?.length, "Runtime recipe has no behavioral proof targets");
|
|
46
|
+
for (const target of recipe.proofTargets) {
|
|
47
|
+
const linked = [...nodes].filter(([, node]) => node.proves?.includes(target.id));
|
|
48
|
+
assert(linked.some(([id]) => proves(id)), `Proof target ${target.id} has no executed runtime assertion`);
|
|
49
|
+
}
|
|
50
|
+
if (smokeTarget) assert(recipe.proofTargets.some((target) => target.id === smokeTarget), "Smoke target is absent from recipe");
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
validateRuntimeProof
|
|
54
|
+
};
|
package/package.json
CHANGED
|
@@ -391,7 +391,7 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
391
391
|
const expected = [
|
|
392
392
|
['npm i -g @deeeed/metamask-harness@latest', copied.setup],
|
|
393
393
|
['yarn skills', copied.setup],
|
|
394
|
-
['/mms-recipe-
|
|
394
|
+
['/mms-recipe-qa', copied.work.join('\n')],
|
|
395
395
|
['/mms-recipe-cook', copied.work.join('\n')],
|
|
396
396
|
['/mms-recipe-performance', copied.work.join('\n')],
|
|
397
397
|
['/mms-recipe-evidence', copied.work.join('\n')],
|
|
@@ -425,6 +425,32 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
425
425
|
|
|
426
426
|
// The walkthrough and its saved progress live one click deeper.
|
|
427
427
|
await withPage(browserEndpoint, `${base}how-it-works.html`, async (page) => {
|
|
428
|
+
const previews = await page.evaluate(String.raw`
|
|
429
|
+
(async () => {
|
|
430
|
+
document.querySelector('[data-map-key="template"]').click();
|
|
431
|
+
const checked = [];
|
|
432
|
+
for (const button of document.querySelectorAll('[data-template-example]')) {
|
|
433
|
+
button.click();
|
|
434
|
+
const dialog = document.querySelector('#template-preview');
|
|
435
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
436
|
+
if (!dialog.querySelector('.template-preview-controls').hidden) break;
|
|
437
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
438
|
+
}
|
|
439
|
+
const meta = dialog.querySelector('#template-preview-meta').textContent;
|
|
440
|
+
const raw = dialog.querySelector('#template-preview-raw').textContent;
|
|
441
|
+
const source = dialog.querySelector('#template-preview-source');
|
|
442
|
+
if (!dialog.open || !meta.includes(button.dataset.templateExample) || !raw.includes('- [ ]') || source.hidden || !/\/blob\/[a-f0-9]{40}\//.test(source.href)) {
|
|
443
|
+
throw new Error('Invalid canonical template preview: ' + button.dataset.templateExample);
|
|
444
|
+
}
|
|
445
|
+
checked.push(button.dataset.templateExample);
|
|
446
|
+
dialog.querySelector('[data-template-close]').click();
|
|
447
|
+
}
|
|
448
|
+
return checked;
|
|
449
|
+
})()
|
|
450
|
+
`);
|
|
451
|
+
if (!previews.includes('review-pr/static-perps') || !previews.includes('qa/pr.extension')) {
|
|
452
|
+
throw new Error('Static review and QA previews must both be available');
|
|
453
|
+
}
|
|
428
454
|
const progress = await page.evaluate(String.raw`
|
|
429
455
|
(() => {
|
|
430
456
|
const box = document.querySelector('.step-check');
|