waygraph 0.12.19 → 0.12.21
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/README.md +6 -3
- package/dist/auto-explore-run.js +13 -6
- package/dist/cli.js +39 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/traverse-coverage.d.ts +59 -0
- package/dist/traverse-coverage.js +127 -0
- package/dist/traverse-run.d.ts +17 -1
- package/dist/traverse-run.js +73 -11
- package/examples/saucedemo/README.md +4 -0
- package/examples/saucedemo/package.json +3 -0
- package/package.json +2 -2
- package/templates/quickstart/package.json +4 -1
- package/templates/scaffold/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,11 +228,14 @@ real acts). `--ff-expand` is the older expand-only flag (same flatten). Saucedem
|
|
|
228
228
|
`checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
|
|
229
229
|
Sign In narration.
|
|
230
230
|
|
|
231
|
-
`waygraph traverse` walks the Block graph (Phase B
|
|
231
|
+
`waygraph traverse` walks the Block graph (Phase B-E). `--parallel N` (default
|
|
232
232
|
`--session clone`) bootstraps one context, forks N workers with `storageState` + mem
|
|
233
233
|
snapshot, partitions edges by hash, and claims via in-process edge leases (audit under
|
|
234
|
-
`.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`.
|
|
235
|
-
`
|
|
234
|
+
`.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. Phase E
|
|
235
|
+
writes `.waygraph-traverse/coverage.json` and prints `[Coverage edges=H/T ratio=R%...]`;
|
|
236
|
+
`--min-edge-coverage 80%` fails the suite with exit 2 when the ratio is below the gate
|
|
237
|
+
(even if every worker leaf-PASSed). `--coverage-out PATH` / `--no-coverage-report` adjust
|
|
238
|
+
the JSON. See `docs/proposals/traverse-ffcompose-rfc.md`.
|
|
236
239
|
|
|
237
240
|
**Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
|
|
238
241
|
`tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
|
package/dist/auto-explore-run.js
CHANGED
|
@@ -194,20 +194,23 @@ async function runOneBlock(engine, entry, context, page, mem, headful) {
|
|
|
194
194
|
const flow = engine.defineFlow([start, entry.block, end]);
|
|
195
195
|
const BLOCK_TIMEOUT_MS = 45_000;
|
|
196
196
|
let timer;
|
|
197
|
+
let result;
|
|
197
198
|
try {
|
|
198
|
-
await Promise.race([
|
|
199
|
+
const ran = await Promise.race([
|
|
199
200
|
flow.run(context, mem, { page, closeOnFinish: false }),
|
|
200
201
|
new Promise((_, reject) => {
|
|
201
202
|
timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s - ` +
|
|
202
203
|
"click Quit and retry, or check network / mem credentials")), BLOCK_TIMEOUT_MS);
|
|
203
204
|
}),
|
|
204
205
|
]);
|
|
206
|
+
result = ran.result;
|
|
205
207
|
}
|
|
206
208
|
finally {
|
|
207
209
|
if (timer)
|
|
208
210
|
clearTimeout(timer);
|
|
209
211
|
}
|
|
210
212
|
await page.evaluate("delete window.__wgPendingNavClickLabel").catch(() => { });
|
|
213
|
+
return result;
|
|
211
214
|
}
|
|
212
215
|
const PANEL_CSS = "#wg-auto-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
|
|
213
216
|
"max-width:min(92vw,720px);max-height:calc(100vh - 24px);overflow-y:auto;box-sizing:border-box;" +
|
|
@@ -540,13 +543,16 @@ export async function runAutoExplore(projectDir, options = {}) {
|
|
|
540
543
|
}
|
|
541
544
|
wiredPage = target;
|
|
542
545
|
}
|
|
546
|
+
let here = null;
|
|
543
547
|
try {
|
|
544
548
|
for (;;) {
|
|
545
549
|
page = await ensureLivePage(context, page, startUrl);
|
|
546
550
|
await wirePage(page);
|
|
547
551
|
const pickWait = armPickWait();
|
|
548
552
|
pickSlot.resolve = pickWait.resolve;
|
|
549
|
-
|
|
553
|
+
if (here === null) {
|
|
554
|
+
here = await detectHere(page, library.navBlocks);
|
|
555
|
+
}
|
|
550
556
|
if (cli)
|
|
551
557
|
console.error("waygraph auto: scanning page for moves ...");
|
|
552
558
|
const menu = await buildExploreMenu(page, graph, library, here);
|
|
@@ -595,10 +601,11 @@ export async function runAutoExplore(projectDir, options = {}) {
|
|
|
595
601
|
}
|
|
596
602
|
const pickLabel = edge.label ?? entry.block.name;
|
|
597
603
|
console.error(`waygraph auto: running [${pick.index + 1}] ${pickLabel} ...`);
|
|
598
|
-
await runOneBlock(engine, entry, context, page, mem, !cli);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
const result = await runOneBlock(engine, entry, context, page, mem, !cli);
|
|
605
|
+
// Trust block output over locate() - nav verify traits collide on /dashboard.
|
|
606
|
+
here = result.__state ?? edge.to;
|
|
607
|
+
lastRunNote = `${entry.block.name} -> ${here}`;
|
|
608
|
+
console.error(`waygraph auto: ran ${entry.block.name} -> ${here}`);
|
|
602
609
|
}
|
|
603
610
|
catch (err) {
|
|
604
611
|
lastRunNote = `${entry.block.name} failed`;
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./gra
|
|
|
21
21
|
import { runAutoExplore } from "./auto-explore-run.js";
|
|
22
22
|
import { runAgentDive } from "./agent-dive.js";
|
|
23
23
|
import { runTraverse } from "./traverse-run.js";
|
|
24
|
+
import { parseMinEdgeCoverage } from "./traverse-coverage.js";
|
|
24
25
|
import { isFileSelectToken, parseBlocksSelect, } from "./blocks-select.js";
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Filesystem
|
|
@@ -3950,7 +3951,7 @@ Primary (less is more):
|
|
|
3950
3951
|
|
|
3951
3952
|
Also:
|
|
3952
3953
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3953
|
-
waygraph traverse [project] Graph crawl (Phase B
|
|
3954
|
+
waygraph traverse [project] Graph crawl (Phase B-E)
|
|
3954
3955
|
--blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
|
|
3955
3956
|
--parallel N Phase D: N clone workers (max 4)
|
|
3956
3957
|
--session clone|inherit Phase D: default clone
|
|
@@ -4076,6 +4077,9 @@ async function main() {
|
|
|
4076
4077
|
let blocksFilter;
|
|
4077
4078
|
let parallel;
|
|
4078
4079
|
let session;
|
|
4080
|
+
let minEdgeCoverageRaw;
|
|
4081
|
+
let coverageOut;
|
|
4082
|
+
let noCoverageReport = false;
|
|
4079
4083
|
let projectDir = process.cwd();
|
|
4080
4084
|
for (let i = 0; i < rest.length; i++) {
|
|
4081
4085
|
const a = rest[i];
|
|
@@ -4115,6 +4119,22 @@ async function main() {
|
|
|
4115
4119
|
session = v;
|
|
4116
4120
|
continue;
|
|
4117
4121
|
}
|
|
4122
|
+
if (a === "--min-edge-coverage" || a.startsWith("--min-edge-coverage=")) {
|
|
4123
|
+
minEdgeCoverageRaw = a.startsWith("--min-edge-coverage=")
|
|
4124
|
+
? a.slice("--min-edge-coverage=".length)
|
|
4125
|
+
: rest[++i];
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
if (a === "--coverage-out" || a.startsWith("--coverage-out=")) {
|
|
4129
|
+
coverageOut = a.startsWith("--coverage-out=")
|
|
4130
|
+
? a.slice("--coverage-out=".length)
|
|
4131
|
+
: rest[++i];
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
if (a === "--no-coverage-report") {
|
|
4135
|
+
noCoverageReport = true;
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4118
4138
|
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
4119
4139
|
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
4120
4140
|
maxSteps = Number(v);
|
|
@@ -4143,12 +4163,15 @@ async function main() {
|
|
|
4143
4163
|
if (a === "--help" || a === "-h") {
|
|
4144
4164
|
console.log(`waygraph traverse [project] [flags]
|
|
4145
4165
|
|
|
4146
|
-
Graph crawl (RFC Phase B
|
|
4166
|
+
Graph crawl (RFC Phase B-E). Walks unused legal edges until a leaf,
|
|
4147
4167
|
budget kill, or first broken edge. --parallel N uses session clone + edge leases.
|
|
4148
4168
|
|
|
4149
4169
|
--blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
|
|
4150
4170
|
--parallel N Phase D: N clone workers (default 1, max 4)
|
|
4151
4171
|
--session clone|inherit Phase D: default clone; inherit refused if parallel>1
|
|
4172
|
+
--min-edge-coverage X Phase E: suite gate 0.8 | 80% | 80 (exit 2 if below)
|
|
4173
|
+
--coverage-out PATH Phase E: write JSON (default .waygraph-traverse/coverage.json)
|
|
4174
|
+
--no-coverage-report Phase E: print Coverage line only (no JSON file)
|
|
4152
4175
|
--from <Checkpoint> start checkpoint (optional)
|
|
4153
4176
|
--data '{...}' Mem seed JSON
|
|
4154
4177
|
--max-steps N default 50 (per worker)
|
|
@@ -4160,9 +4183,11 @@ budget kill, or first broken edge. --parallel N uses session clone + edge leases
|
|
|
4160
4183
|
Examples:
|
|
4161
4184
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts'
|
|
4162
4185
|
waygraph traverse --parallel 2 --session clone --max-steps 20
|
|
4186
|
+
waygraph traverse --min-edge-coverage 80% --coverage-out ./cov.json
|
|
4163
4187
|
|
|
4164
4188
|
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
4165
4189
|
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
4190
|
+
COVER: [Coverage edges=H/T ratio=R% min=M% PASS|FAIL]
|
|
4166
4191
|
`);
|
|
4167
4192
|
process.exit(0);
|
|
4168
4193
|
}
|
|
@@ -4177,6 +4202,15 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
4177
4202
|
const blocksSelect = blocksFilter
|
|
4178
4203
|
? parseBlocksSelect(blocksFilter)
|
|
4179
4204
|
: undefined;
|
|
4205
|
+
let minEdgeCoverage;
|
|
4206
|
+
if (minEdgeCoverageRaw !== undefined) {
|
|
4207
|
+
const parsed = parseMinEdgeCoverage(minEdgeCoverageRaw);
|
|
4208
|
+
if (parsed === null) {
|
|
4209
|
+
console.error(`waygraph traverse: --min-edge-coverage expects 0.8, 80%, or 80 (got ${JSON.stringify(minEdgeCoverageRaw)})`);
|
|
4210
|
+
process.exit(1);
|
|
4211
|
+
}
|
|
4212
|
+
minEdgeCoverage = parsed;
|
|
4213
|
+
}
|
|
4180
4214
|
const code = await runTraverse(projectDir, {
|
|
4181
4215
|
...(from ? { from } : {}),
|
|
4182
4216
|
...(data ? { data } : {}),
|
|
@@ -4192,6 +4226,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
4192
4226
|
...(blocksSelect ? { blocksSelect } : {}),
|
|
4193
4227
|
...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
|
|
4194
4228
|
...(session ? { session } : {}),
|
|
4229
|
+
...(minEdgeCoverage !== undefined ? { minEdgeCoverage } : {}),
|
|
4230
|
+
...(coverageOut ? { coverageOut: resolve(coverageOut) } : {}),
|
|
4231
|
+
...(noCoverageReport ? { noCoverageReport: true } : {}),
|
|
4195
4232
|
});
|
|
4196
4233
|
process.exit(code);
|
|
4197
4234
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,6 @@ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp,
|
|
|
13
13
|
export type { BlocksSelect, BlocksSelectKind } from "./blocks-select.js";
|
|
14
14
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
15
15
|
export { runTraverse } from "./traverse-run.js";
|
|
16
|
-
export type { TraverseOptions, TraverseSessionMode } from "./traverse-run.js";
|
|
16
|
+
export type { TraverseOptions, TraverseSessionMode, TraverseWorkerResult, } from "./traverse-run.js";
|
|
17
|
+
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
18
|
+
export type { TraverseCoverageReport } from "./traverse-coverage.js";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./gra
|
|
|
7
7
|
export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
|
|
8
8
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
9
9
|
export { runTraverse } from "./traverse-run.js";
|
|
10
|
+
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface TraverseCoverageReport {
|
|
2
|
+
version: 1;
|
|
3
|
+
generatedAt: string;
|
|
4
|
+
projectDir: string;
|
|
5
|
+
parallel: number;
|
|
6
|
+
/** Unique edge keys in the discovered graph (block names). */
|
|
7
|
+
edgesTotal: number;
|
|
8
|
+
/** Graph edges successfully walked this suite (after instance normalize). */
|
|
9
|
+
edgesHit: number;
|
|
10
|
+
/** edgesHit / edgesTotal (0 when total is 0). */
|
|
11
|
+
ratio: number;
|
|
12
|
+
/** Optional suite gate that was applied (0-1). */
|
|
13
|
+
minEdgeCoverage?: number;
|
|
14
|
+
/** true when minEdgeCoverage was set and ratio met it. */
|
|
15
|
+
coveragePass?: boolean;
|
|
16
|
+
/** Sorted graph edge keys that were hit. */
|
|
17
|
+
hit: string[];
|
|
18
|
+
/** Sorted graph edge keys never walked. */
|
|
19
|
+
missed: string[];
|
|
20
|
+
/** Raw worker hit keys (may include block::instanceId). */
|
|
21
|
+
rawHit?: string[];
|
|
22
|
+
workers?: Array<{
|
|
23
|
+
traverseId: string;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
steps: number;
|
|
26
|
+
edgesHit: number;
|
|
27
|
+
leaf?: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** Static graph edge key = Block runtime name. */
|
|
31
|
+
export declare function edgeKeyFromGraphEdge(edge: {
|
|
32
|
+
block: string;
|
|
33
|
+
}): string;
|
|
34
|
+
/** Collect unique edge keys from a WaygraphGraph-like edges list. */
|
|
35
|
+
export declare function collectGraphEdgeKeys(edges: ReadonlyArray<{
|
|
36
|
+
block: string;
|
|
37
|
+
}>): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Map a worker hit key onto the static graph key it covers.
|
|
40
|
+
* `add-to-cart::sku-1` -> `add-to-cart` when that block is in the graph.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeHitKey(hitKey: string, graphKeys: ReadonlySet<string>): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Parse --min-edge-coverage: `0.8`, `80%`, `80` (as percent when > 1).
|
|
45
|
+
* Returns 0-1 fraction, or null when unset/invalid.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseMinEdgeCoverage(raw: unknown): number | null;
|
|
48
|
+
export declare function buildCoverageReport(opts: {
|
|
49
|
+
projectDir: string;
|
|
50
|
+
parallel: number;
|
|
51
|
+
allEdgeKeys: readonly string[];
|
|
52
|
+
hitKeys: ReadonlySet<string> | readonly string[];
|
|
53
|
+
minEdgeCoverage?: number | null;
|
|
54
|
+
workers?: TraverseCoverageReport["workers"];
|
|
55
|
+
}): TraverseCoverageReport;
|
|
56
|
+
/** Greppable one-liner for agents/CI. */
|
|
57
|
+
export declare function formatCoverageLine(report: TraverseCoverageReport): string;
|
|
58
|
+
export declare function defaultCoverageOutPath(projectDir: string): string;
|
|
59
|
+
export declare function writeCoverageReport(path: string, report: TraverseCoverageReport): void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase E: traverse edge coverage report + suite gate.
|
|
3
|
+
* Coverage is independent of leaf PASS - a green leaf can still fail CI
|
|
4
|
+
* when --min-edge-coverage is set and ratio is too low.
|
|
5
|
+
*
|
|
6
|
+
* Denominator = unique static graph edge block names (FF inners stay as their
|
|
7
|
+
* own graph edges when discovered; RFC open Q deferred - no covered-via-prefix yet).
|
|
8
|
+
* A hit of `block::instanceId` counts as covering `block`.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
/** Static graph edge key = Block runtime name. */
|
|
13
|
+
export function edgeKeyFromGraphEdge(edge) {
|
|
14
|
+
return edge.block;
|
|
15
|
+
}
|
|
16
|
+
/** Collect unique edge keys from a WaygraphGraph-like edges list. */
|
|
17
|
+
export function collectGraphEdgeKeys(edges) {
|
|
18
|
+
const set = new Set();
|
|
19
|
+
for (const e of edges)
|
|
20
|
+
set.add(edgeKeyFromGraphEdge(e));
|
|
21
|
+
return [...set].sort();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map a worker hit key onto the static graph key it covers.
|
|
25
|
+
* `add-to-cart::sku-1` -> `add-to-cart` when that block is in the graph.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeHitKey(hitKey, graphKeys) {
|
|
28
|
+
if (graphKeys.has(hitKey))
|
|
29
|
+
return hitKey;
|
|
30
|
+
const base = hitKey.includes("::") ? hitKey.slice(0, hitKey.indexOf("::")) : hitKey;
|
|
31
|
+
if (graphKeys.has(base))
|
|
32
|
+
return base;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse --min-edge-coverage: `0.8`, `80%`, `80` (as percent when > 1).
|
|
37
|
+
* Returns 0-1 fraction, or null when unset/invalid.
|
|
38
|
+
*/
|
|
39
|
+
export function parseMinEdgeCoverage(raw) {
|
|
40
|
+
if (raw === undefined || raw === null || raw === "")
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
43
|
+
if (raw < 0)
|
|
44
|
+
return null;
|
|
45
|
+
if (raw <= 1)
|
|
46
|
+
return raw;
|
|
47
|
+
if (raw <= 100)
|
|
48
|
+
return raw / 100;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (typeof raw !== "string")
|
|
52
|
+
return null;
|
|
53
|
+
const t = raw.trim().toLowerCase();
|
|
54
|
+
if (!t)
|
|
55
|
+
return null;
|
|
56
|
+
if (t.endsWith("%")) {
|
|
57
|
+
const n = Number(t.slice(0, -1));
|
|
58
|
+
if (!Number.isFinite(n) || n < 0 || n > 100)
|
|
59
|
+
return null;
|
|
60
|
+
return n / 100;
|
|
61
|
+
}
|
|
62
|
+
const n = Number(t);
|
|
63
|
+
if (!Number.isFinite(n) || n < 0)
|
|
64
|
+
return null;
|
|
65
|
+
if (n <= 1)
|
|
66
|
+
return n;
|
|
67
|
+
if (n <= 100)
|
|
68
|
+
return n / 100;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export function buildCoverageReport(opts) {
|
|
72
|
+
const all = [...new Set(opts.allEdgeKeys)].sort();
|
|
73
|
+
const graphSet = new Set(all);
|
|
74
|
+
const rawList = Array.isArray(opts.hitKeys) ? [...opts.hitKeys] : [...opts.hitKeys];
|
|
75
|
+
const covered = new Set();
|
|
76
|
+
for (const raw of rawList) {
|
|
77
|
+
const n = normalizeHitKey(raw, graphSet);
|
|
78
|
+
if (n)
|
|
79
|
+
covered.add(n);
|
|
80
|
+
}
|
|
81
|
+
const hit = all.filter((k) => covered.has(k));
|
|
82
|
+
const missed = all.filter((k) => !covered.has(k));
|
|
83
|
+
const edgesTotal = all.length;
|
|
84
|
+
const edgesHit = hit.length;
|
|
85
|
+
const ratio = edgesTotal === 0 ? 0 : edgesHit / edgesTotal;
|
|
86
|
+
const min = opts.minEdgeCoverage != null && Number.isFinite(opts.minEdgeCoverage)
|
|
87
|
+
? Math.min(1, Math.max(0, opts.minEdgeCoverage))
|
|
88
|
+
: undefined;
|
|
89
|
+
const coveragePass = min === undefined ? undefined : ratio + 1e-12 >= min;
|
|
90
|
+
const coverageExtra = min !== undefined && coveragePass !== undefined
|
|
91
|
+
? { minEdgeCoverage: min, coveragePass }
|
|
92
|
+
: {};
|
|
93
|
+
return {
|
|
94
|
+
version: 1,
|
|
95
|
+
generatedAt: new Date().toISOString(),
|
|
96
|
+
projectDir: opts.projectDir,
|
|
97
|
+
parallel: opts.parallel,
|
|
98
|
+
edgesTotal,
|
|
99
|
+
edgesHit,
|
|
100
|
+
ratio: Math.round(ratio * 10000) / 10000,
|
|
101
|
+
...coverageExtra,
|
|
102
|
+
hit,
|
|
103
|
+
missed,
|
|
104
|
+
rawHit: [...new Set(rawList)].sort(),
|
|
105
|
+
...(opts.workers ? { workers: opts.workers } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Greppable one-liner for agents/CI. */
|
|
109
|
+
export function formatCoverageLine(report) {
|
|
110
|
+
const pct = (report.ratio * 100).toFixed(1);
|
|
111
|
+
const minPart = report.minEdgeCoverage !== undefined
|
|
112
|
+
? ` min=${(report.minEdgeCoverage * 100).toFixed(1)}%` +
|
|
113
|
+
(report.coveragePass === false
|
|
114
|
+
? " FAIL"
|
|
115
|
+
: report.coveragePass === true
|
|
116
|
+
? " PASS"
|
|
117
|
+
: "")
|
|
118
|
+
: "";
|
|
119
|
+
return `[Coverage edges=${report.edgesHit}/${report.edgesTotal} ratio=${pct}%${minPart}]`;
|
|
120
|
+
}
|
|
121
|
+
export function defaultCoverageOutPath(projectDir) {
|
|
122
|
+
return join(projectDir, ".waygraph-traverse", "coverage.json");
|
|
123
|
+
}
|
|
124
|
+
export function writeCoverageReport(path, report) {
|
|
125
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
126
|
+
writeFileSync(path, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
127
|
+
}
|
package/dist/traverse-run.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type TraverseSessionMode = "clone" | "inherit";
|
|
2
|
+
export interface TraverseWorkerResult {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
steps: number;
|
|
5
|
+
edgesHit: string[];
|
|
6
|
+
leaf?: string;
|
|
7
|
+
}
|
|
2
8
|
export interface TraverseOptions {
|
|
3
9
|
baseURL?: string;
|
|
4
10
|
startUrl?: string;
|
|
@@ -24,8 +30,18 @@ export interface TraverseOptions {
|
|
|
24
30
|
parallel?: number;
|
|
25
31
|
/** Phase D: clone (default) or inherit. Inherit refused when parallel > 1. */
|
|
26
32
|
session?: TraverseSessionMode;
|
|
33
|
+
/**
|
|
34
|
+
* Phase E: fail suite (exit 2) when hit/total ratio is below this 0-1 fraction.
|
|
35
|
+
* Leaf PASS alone does not satisfy the gate.
|
|
36
|
+
*/
|
|
37
|
+
minEdgeCoverage?: number;
|
|
38
|
+
/** Phase E: write coverage JSON here (default `.waygraph-traverse/coverage.json`). */
|
|
39
|
+
coverageOut?: string;
|
|
40
|
+
/** Phase E: skip writing coverage.json (still prints the Coverage line). */
|
|
41
|
+
noCoverageReport?: boolean;
|
|
27
42
|
}
|
|
28
43
|
/**
|
|
29
|
-
* Serial or parallel graph crawl. Prints greppable PASS/FAIL
|
|
44
|
+
* Serial or parallel graph crawl. Prints greppable PASS/FAIL + Coverage lines.
|
|
45
|
+
* Exit: 0 leaf ok (+ coverage ok), 1 break/timeout, 2 coverage gate fail (Phase E).
|
|
30
46
|
*/
|
|
31
47
|
export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
|
package/dist/traverse-run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { MemPage } from "./mem-page.js";
|
|
|
9
9
|
import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
|
|
10
10
|
import { defaultMemValueForKey } from "./auto-explore-run.js";
|
|
11
11
|
import { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
12
|
+
import { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, writeCoverageReport, } from "./traverse-coverage.js";
|
|
12
13
|
function resolveBaseUrl(projectDir) {
|
|
13
14
|
if (process.env.WAYGRAPH_BASE_URL)
|
|
14
15
|
return process.env.WAYGRAPH_BASE_URL;
|
|
@@ -132,6 +133,7 @@ async function runTraverseWorker(opts) {
|
|
|
132
133
|
const startedAt = Date.now();
|
|
133
134
|
let lastHere = opts.from ?? null;
|
|
134
135
|
let exitCode = 0;
|
|
136
|
+
let leaf;
|
|
135
137
|
try {
|
|
136
138
|
for (;;) {
|
|
137
139
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -171,6 +173,7 @@ async function runTraverseWorker(opts) {
|
|
|
171
173
|
});
|
|
172
174
|
if (candidates.length === 0) {
|
|
173
175
|
const label = here ?? detected ?? "unknown";
|
|
176
|
+
leaf = label;
|
|
174
177
|
console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
|
|
175
178
|
console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
|
|
176
179
|
break;
|
|
@@ -227,10 +230,16 @@ async function runTraverseWorker(opts) {
|
|
|
227
230
|
finally {
|
|
228
231
|
console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
|
|
229
232
|
}
|
|
230
|
-
return
|
|
233
|
+
return {
|
|
234
|
+
exitCode,
|
|
235
|
+
steps,
|
|
236
|
+
edgesHit: [...edgesHit],
|
|
237
|
+
...(leaf ? { leaf } : {}),
|
|
238
|
+
};
|
|
231
239
|
}
|
|
232
240
|
/**
|
|
233
|
-
* Serial or parallel graph crawl. Prints greppable PASS/FAIL
|
|
241
|
+
* Serial or parallel graph crawl. Prints greppable PASS/FAIL + Coverage lines.
|
|
242
|
+
* Exit: 0 leaf ok (+ coverage ok), 1 break/timeout, 2 coverage gate fail (Phase E).
|
|
234
243
|
*/
|
|
235
244
|
export async function runTraverse(projectDir, options = {}) {
|
|
236
245
|
let parallel = Math.max(1, Math.floor(options.parallel ?? 1));
|
|
@@ -250,11 +259,16 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
250
259
|
const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
|
|
251
260
|
const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
|
|
252
261
|
const startUrl = options.startUrl ?? baseURL;
|
|
262
|
+
const minEdgeCoverage = options.minEdgeCoverage != null && Number.isFinite(options.minEdgeCoverage)
|
|
263
|
+
? options.minEdgeCoverage
|
|
264
|
+
: undefined;
|
|
265
|
+
const coverageOut = options.coverageOut ?? defaultCoverageOutPath(projectDir);
|
|
253
266
|
const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
|
|
254
267
|
if (options.blocksSelect) {
|
|
255
268
|
console.error(`waygraph traverse: --blocks ${options.blocksSelect.raw} -> ` +
|
|
256
269
|
`${library.byName.size} block(s), ${graph.edges.length} edge(s)`);
|
|
257
270
|
}
|
|
271
|
+
const allEdgeKeys = collectGraphEdgeKeys(graph.edges);
|
|
258
272
|
const leases = new EdgeLeaseCoordinator({ projectDir, persist: parallel > 1 });
|
|
259
273
|
const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
|
|
260
274
|
const { chromium } = await import("@playwright/test");
|
|
@@ -268,7 +282,46 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
268
282
|
console.error(`waygraph traverse: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
|
|
269
283
|
(baseURL ? ` base=${baseURL}` : "") +
|
|
270
284
|
` parallel=${parallel} session=${session}` +
|
|
271
|
-
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`
|
|
285
|
+
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}` +
|
|
286
|
+
(minEdgeCoverage !== undefined
|
|
287
|
+
? ` minEdgeCoverage=${(minEdgeCoverage * 100).toFixed(1)}%`
|
|
288
|
+
: ""));
|
|
289
|
+
const emitCoverage = (workers) => {
|
|
290
|
+
const hitUnion = new Set();
|
|
291
|
+
for (const w of workers) {
|
|
292
|
+
for (const k of w.result.edgesHit)
|
|
293
|
+
hitUnion.add(k);
|
|
294
|
+
}
|
|
295
|
+
const report = buildCoverageReport({
|
|
296
|
+
projectDir,
|
|
297
|
+
parallel,
|
|
298
|
+
allEdgeKeys,
|
|
299
|
+
hitKeys: hitUnion,
|
|
300
|
+
...(minEdgeCoverage !== undefined ? { minEdgeCoverage } : {}),
|
|
301
|
+
workers: workers.map(({ traverseId, result }) => ({
|
|
302
|
+
traverseId,
|
|
303
|
+
exitCode: result.exitCode,
|
|
304
|
+
steps: result.steps,
|
|
305
|
+
edgesHit: result.edgesHit.length,
|
|
306
|
+
...(result.leaf ? { leaf: result.leaf } : {}),
|
|
307
|
+
})),
|
|
308
|
+
});
|
|
309
|
+
console.log(formatCoverageLine(report));
|
|
310
|
+
console.error(`waygraph traverse: coverage hit=${report.edgesHit}/${report.edgesTotal}` +
|
|
311
|
+
(report.missed.length
|
|
312
|
+
? ` missed=${report.missed.slice(0, 8).join(",")}${report.missed.length > 8 ? "..." : ""}`
|
|
313
|
+
: "") +
|
|
314
|
+
(options.noCoverageReport ? "" : ` report=${coverageOut}`));
|
|
315
|
+
if (!options.noCoverageReport) {
|
|
316
|
+
writeCoverageReport(coverageOut, report);
|
|
317
|
+
}
|
|
318
|
+
if (report.coveragePass === false) {
|
|
319
|
+
console.error(`waygraph traverse: FAIL coverage gate ratio=${(report.ratio * 100).toFixed(1)}%` +
|
|
320
|
+
` < min=${((report.minEdgeCoverage ?? 0) * 100).toFixed(1)}%`);
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
return 0;
|
|
324
|
+
};
|
|
272
325
|
try {
|
|
273
326
|
if (parallel <= 1) {
|
|
274
327
|
const contextOpts = {
|
|
@@ -284,8 +337,9 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
284
337
|
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
285
338
|
await page.waitForLoadState("load").catch(() => { });
|
|
286
339
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
340
|
+
const traverseId = options.traverseId ?? "traverse-1";
|
|
341
|
+
const result = await runTraverseWorker({
|
|
342
|
+
traverseId,
|
|
289
343
|
workerIndex: 0,
|
|
290
344
|
parallel: 1,
|
|
291
345
|
maxSteps,
|
|
@@ -305,7 +359,10 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
305
359
|
leases,
|
|
306
360
|
});
|
|
307
361
|
await context.close().catch(() => { });
|
|
308
|
-
|
|
362
|
+
const coverageCode = emitCoverage([{ traverseId, result }]);
|
|
363
|
+
if (result.exitCode !== 0)
|
|
364
|
+
return result.exitCode;
|
|
365
|
+
return coverageCode;
|
|
309
366
|
}
|
|
310
367
|
// Phase D: bootstrap one context for storageState, then N clones.
|
|
311
368
|
const bootOpts = {
|
|
@@ -325,7 +382,7 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
325
382
|
const memSnap = snapshotMem(library.byName, bootMem);
|
|
326
383
|
await bootContext.close().catch(() => { });
|
|
327
384
|
console.error(`waygraph traverse: forked ${parallel} clone workers (storageState + mem snapshot)`);
|
|
328
|
-
const
|
|
385
|
+
const workerMeta = await Promise.all(Array.from({ length: parallel }, async (_, i) => {
|
|
329
386
|
const traverseId = `traverse-${i + 1}`;
|
|
330
387
|
const ctxOpts = {
|
|
331
388
|
viewport: { width: 1280, height: 720 },
|
|
@@ -341,7 +398,7 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
341
398
|
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
342
399
|
}
|
|
343
400
|
try {
|
|
344
|
-
|
|
401
|
+
const result = await runTraverseWorker({
|
|
345
402
|
traverseId,
|
|
346
403
|
workerIndex: i,
|
|
347
404
|
parallel,
|
|
@@ -361,15 +418,20 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
361
418
|
mem,
|
|
362
419
|
leases,
|
|
363
420
|
});
|
|
421
|
+
return { traverseId, result };
|
|
364
422
|
}
|
|
365
423
|
finally {
|
|
366
424
|
await context.close().catch(() => { });
|
|
367
425
|
}
|
|
368
426
|
}));
|
|
369
|
-
const
|
|
370
|
-
const
|
|
427
|
+
const results = workerMeta.map((w) => w.result);
|
|
428
|
+
const worst = results.reduce((a, b) => Math.max(a, b.exitCode), 0);
|
|
429
|
+
const pass = results.filter((c) => c.exitCode === 0).length;
|
|
371
430
|
console.error(`waygraph traverse: suite parallel=${parallel} pass=${pass}/${parallel} exit=${worst}`);
|
|
372
|
-
|
|
431
|
+
const coverageCode = emitCoverage(workerMeta);
|
|
432
|
+
if (worst !== 0)
|
|
433
|
+
return worst;
|
|
434
|
+
return coverageCode;
|
|
373
435
|
}
|
|
374
436
|
finally {
|
|
375
437
|
await browser.close().catch(() => { });
|
|
@@ -34,6 +34,9 @@ npx playwright install chromium
|
|
|
34
34
|
| Headless suite | `npm test` |
|
|
35
35
|
| Interactive explore (headed panel) | `npm run auto` |
|
|
36
36
|
| Interactive explore (terminal menu) | `npm run auto:cli` |
|
|
37
|
+
| Graph crawl (Phase B-E) | `npm run traverse` |
|
|
38
|
+
| Parallel crawl + leases | `npm run traverse:parallel` |
|
|
39
|
+
| Coverage JSON + min gate (10%) | `npm run traverse:coverage` |
|
|
37
40
|
| Shop demo stepper | `npm run demo:step` |
|
|
38
41
|
| Shop demo autoplay | `npm run demo:autoplay` |
|
|
39
42
|
| **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
|
|
@@ -47,6 +50,7 @@ npx playwright install chromium
|
|
|
47
50
|
| **`run --blocks`** | `npm run run:bulk` — Block names + `--data` | No |
|
|
48
51
|
| **`demo --blocks`** | `npm run demo:bulk` — named flow + `--data` | Optional (`cartBulkFlow`) |
|
|
49
52
|
| **`auto`** | `npm run auto` / `auto:cli` | No — live menu |
|
|
53
|
+
| **`traverse`** | `npm run traverse` / `traverse:parallel` / `traverse:coverage` | No — graph crawl + `.waygraph-traverse/coverage.json` |
|
|
50
54
|
| **`auto --blocks From To`** | path-find on graph | No — checkpoints only |
|
|
51
55
|
|
|
52
56
|
QA watch+record: `npm run demo:bulk:video` (`--auto-play-video`).
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"auto": "waygraph auto",
|
|
15
15
|
"auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
16
16
|
"prove:auto-cli": "node scripts/drive-auto-cli.mjs",
|
|
17
|
+
"traverse": "waygraph traverse --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
18
|
+
"traverse:parallel": "waygraph traverse --parallel 2 --session clone --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 30",
|
|
19
|
+
"traverse:coverage": "waygraph traverse --min-edge-coverage 10% --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
17
20
|
"graph": "waygraph graph",
|
|
18
21
|
"graph:mermaid": "waygraph graph --mermaid",
|
|
19
22
|
"demo:stub-on-error": "waygraph demo --blocks stubOnErrorFlow --auto-next --fast --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --title \"stubOnError prove\"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waygraph",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"tsx": "^4.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@playwright/test": "^1.
|
|
44
|
+
"@playwright/test": "^1.63.0",
|
|
45
45
|
"@types/node": "^26.0.0",
|
|
46
46
|
"typescript": "^5.6.0"
|
|
47
47
|
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"check": "waygraph check",
|
|
10
10
|
"auto": "waygraph auto",
|
|
11
11
|
"auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
12
|
+
"traverse": "waygraph traverse --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
13
|
+
"traverse:parallel": "waygraph traverse --parallel 2 --session clone --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 30",
|
|
14
|
+
"traverse:coverage": "waygraph traverse --min-edge-coverage 10% --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
12
15
|
"graph": "waygraph graph",
|
|
13
16
|
"graph:mermaid": "waygraph graph --mermaid",
|
|
14
17
|
"demo": "waygraph demo --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
@@ -23,7 +26,7 @@
|
|
|
23
26
|
"baseUrl": "https://www.saucedemo.com"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
|
-
"waygraph": "^0.
|
|
29
|
+
"waygraph": "^0.12.20"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@playwright/test": "^1.55.0",
|