waygraph 0.12.13 → 0.12.14

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 CHANGED
@@ -226,7 +226,13 @@ FFCompose into its inner Blocks so a broken login step is visible as its own pan
226
226
  Former FF inners keep blitz pacing so wall-clock stays comparable to opaque FF (same
227
227
  real acts). `--ff-expand` is the older expand-only flag (same flatten). Saucedemo
228
228
  `checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
229
- Sign In narration. See `docs/proposals/traverse-ffcompose-rfc.md`.
229
+ Sign In narration.
230
+
231
+ `waygraph traverse` walks the Block graph (Phase B+D). `--parallel N` (default
232
+ `--session clone`) bootstraps one context, forks N workers with `storageState` + mem
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`. See
235
+ `docs/proposals/traverse-ffcompose-rfc.md`.
230
236
  `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
231
237
  Block/Flow, in the same browser context an existing `page` already belongs to - the
232
238
  pattern `observe()` could already reach for (it's the only phase allowed to touch
package/dist/cli.js CHANGED
@@ -2674,8 +2674,8 @@ async function main() {
2674
2674
  if (step && hasFfBlitz && process.env.WAYGRAPH_SLOWMO === undefined && !jsonReport) {
2675
2675
  console.error(
2676
2676
  ffDisabled
2677
- ? "waygraph demo: --ff-disabled (expanded) → Playwright slowMo=0 on former FF steps (blitz; same acts as opaque FF)"
2678
- : "waygraph demo: opaque FFCompose in flow → Playwright slowMo=0 (blitz); set WAYGRAPH_SLOWMO to override",
2677
+ ? "waygraph demo: --ff-disabled (expanded) -> Playwright slowMo=0 on former FF steps (blitz; same acts as opaque FF)"
2678
+ : "waygraph demo: opaque FFCompose in flow -> Playwright slowMo=0 (blitz); set WAYGRAPH_SLOWMO to override",
2679
2679
  );
2680
2680
  }
2681
2681
  const baseURL = process.env.WAYGRAPH_BASE_URL;
@@ -3716,8 +3716,10 @@ Primary (less is more):
3716
3716
 
3717
3717
  Also:
3718
3718
  waygraph list | nav | validate | check | graph | init <name>
3719
- waygraph traverse [project] Serial graph crawl (Phase B+)
3719
+ waygraph traverse [project] Graph crawl (Phase B+D)
3720
3720
  --blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
3721
+ --parallel N Phase D: N clone workers (max 4)
3722
+ --session clone|inherit Phase D: default clone
3721
3723
  --from <Checkpoint> Seed / start checkpoint
3722
3724
  --data '{...}' Mem seed
3723
3725
  --max-steps N Cap Block runs (default 50)
@@ -3743,6 +3745,7 @@ Examples:
3743
3745
  waygraph auto --blocks LoginPage OrderComplete
3744
3746
  waygraph auto --blocks '/mailpit/' # Phase C filtered explore
3745
3747
  waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
3748
+ waygraph traverse --parallel 2 --session clone # Phase D clone workers
3746
3749
  waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
3747
3750
 
3748
3751
  Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
@@ -3837,6 +3840,8 @@ async function main() {
3837
3840
  let headed = false;
3838
3841
  let baseUrl;
3839
3842
  let blocksFilter;
3843
+ let parallel;
3844
+ let session;
3840
3845
  let projectDir = process.cwd();
3841
3846
  for (let i = 0; i < rest.length; i++) {
3842
3847
  const a = rest[i];
@@ -3862,6 +3867,20 @@ async function main() {
3862
3867
  }
3863
3868
  continue;
3864
3869
  }
3870
+ if (a === "--parallel" || a.startsWith("--parallel=")) {
3871
+ const v = a.startsWith("--parallel=") ? a.slice("--parallel=".length) : rest[++i];
3872
+ parallel = Number(v);
3873
+ continue;
3874
+ }
3875
+ if (a === "--session" || a.startsWith("--session=")) {
3876
+ const v = a.startsWith("--session=") ? a.slice("--session=".length) : rest[++i];
3877
+ if (v !== "clone" && v !== "inherit") {
3878
+ console.error('waygraph traverse: --session must be "clone" or "inherit"');
3879
+ process.exit(1);
3880
+ }
3881
+ session = v;
3882
+ continue;
3883
+ }
3865
3884
  if (a === "--max-steps" || a.startsWith("--max-steps=")) {
3866
3885
  const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
3867
3886
  maxSteps = Number(v);
@@ -3890,13 +3909,15 @@ async function main() {
3890
3909
  if (a === "--help" || a === "-h") {
3891
3910
  console.log(`waygraph traverse [project] [flags]
3892
3911
 
3893
- Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
3894
- budget kill, or first broken edge.
3912
+ Graph crawl (RFC Phase B+D). Walks unused legal edges until a leaf,
3913
+ budget kill, or first broken edge. --parallel N uses session clone + edge leases.
3895
3914
 
3896
3915
  --blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
3916
+ --parallel N Phase D: N clone workers (default 1, max 4)
3917
+ --session clone|inherit Phase D: default clone; inherit refused if parallel>1
3897
3918
  --from <Checkpoint> start checkpoint (optional)
3898
3919
  --data '{...}' Mem seed JSON
3899
- --max-steps N default 50
3920
+ --max-steps N default 50 (per worker)
3900
3921
  --max-visits N per-node visit cap (default 2)
3901
3922
  --max-visits-per-edge N default 1
3902
3923
  --non-headless show browser
@@ -3904,7 +3925,7 @@ budget kill, or first broken edge.
3904
3925
 
3905
3926
  Examples:
3906
3927
  waygraph traverse --blocks '**/mailpit/**/*.block.ts'
3907
- waygraph traverse --blocks '/mailpit|login/'
3928
+ waygraph traverse --parallel 2 --session clone --max-steps 20
3908
3929
 
3909
3930
  PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
3910
3931
  FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
@@ -3935,6 +3956,8 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3935
3956
  headed,
3936
3957
  ...(baseUrl ? { baseURL: baseUrl } : {}),
3937
3958
  ...(blocksSelect ? { blocksSelect } : {}),
3959
+ ...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
3960
+ ...(session ? { session } : {}),
3938
3961
  });
3939
3962
  process.exit(code);
3940
3963
  }
package/dist/index.d.ts CHANGED
@@ -11,3 +11,6 @@ export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./gra
11
11
  export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./graph.js";
12
12
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
13
13
  export type { BlocksSelect, BlocksSelectKind } from "./blocks-select.js";
14
+ export { EdgeLeaseCoordinator } from "./traverse-lease.js";
15
+ export { runTraverse } from "./traverse-run.js";
16
+ export type { TraverseOptions, TraverseSessionMode } from "./traverse-run.js";
package/dist/index.js CHANGED
@@ -5,3 +5,5 @@ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, mo
5
5
  export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
8
+ export { EdgeLeaseCoordinator } from "./traverse-lease.js";
9
+ export { runTraverse } from "./traverse-run.js";
@@ -0,0 +1,18 @@
1
+ export interface EdgeLeaseRecord {
2
+ workerId: string;
3
+ claimedAt: number;
4
+ }
5
+ export declare class EdgeLeaseCoordinator {
6
+ private readonly held;
7
+ private readonly dir;
8
+ constructor(opts?: {
9
+ projectDir?: string;
10
+ persist?: boolean;
11
+ });
12
+ /** True if this worker now owns `edgeKey` (or already owned it). */
13
+ tryClaim(edgeKey: string, workerId: string): boolean;
14
+ release(edgeKey: string, workerId: string): void;
15
+ /** Stable partition: same key always maps to the same worker index. */
16
+ static partitionIndex(edgeKey: string, parallel: number): number;
17
+ private flush;
18
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Phase D: edge leases so parallel traverse workers never claim the same edge.
3
+ * In-process Map is authoritative for `--parallel` (same Node process).
4
+ * Optional JSON under `.waygraph-traverse/` is an audit trail only.
5
+ */
6
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ export class EdgeLeaseCoordinator {
9
+ held = new Map();
10
+ dir;
11
+ constructor(opts) {
12
+ if (opts?.persist && opts.projectDir) {
13
+ this.dir = join(opts.projectDir, ".waygraph-traverse");
14
+ if (!existsSync(this.dir))
15
+ mkdirSync(this.dir, { recursive: true });
16
+ }
17
+ else {
18
+ this.dir = null;
19
+ }
20
+ }
21
+ /** True if this worker now owns `edgeKey` (or already owned it). */
22
+ tryClaim(edgeKey, workerId) {
23
+ const cur = this.held.get(edgeKey);
24
+ if (cur && cur.workerId !== workerId)
25
+ return false;
26
+ this.held.set(edgeKey, { workerId, claimedAt: Date.now() });
27
+ this.flush();
28
+ return true;
29
+ }
30
+ release(edgeKey, workerId) {
31
+ const cur = this.held.get(edgeKey);
32
+ if (cur && cur.workerId === workerId) {
33
+ this.held.delete(edgeKey);
34
+ this.flush();
35
+ }
36
+ }
37
+ /** Stable partition: same key always maps to the same worker index. */
38
+ static partitionIndex(edgeKey, parallel) {
39
+ if (parallel <= 1)
40
+ return 0;
41
+ let h = 2166136261;
42
+ for (let i = 0; i < edgeKey.length; i++) {
43
+ h ^= edgeKey.charCodeAt(i);
44
+ h = Math.imul(h, 16777619);
45
+ }
46
+ return (h >>> 0) % parallel;
47
+ }
48
+ flush() {
49
+ if (!this.dir)
50
+ return;
51
+ try {
52
+ const payload = {
53
+ updatedAt: new Date().toISOString(),
54
+ leases: Object.fromEntries(this.held),
55
+ };
56
+ writeFileSync(join(this.dir, "leases.json"), JSON.stringify(payload, null, 2));
57
+ }
58
+ catch {
59
+ /* audit only */
60
+ }
61
+ }
62
+ }
@@ -1,3 +1,4 @@
1
+ export type TraverseSessionMode = "clone" | "inherit";
1
2
  export interface TraverseOptions {
2
3
  baseURL?: string;
3
4
  startUrl?: string;
@@ -19,8 +20,12 @@ export interface TraverseOptions {
19
20
  traverseId?: string;
20
21
  /** Phase C: glob / regex / bare `--blocks` file select. */
21
22
  blocksSelect?: import("./blocks-select.js").BlocksSelect;
23
+ /** Phase D: parallel workers (default 1). Cap 4. */
24
+ parallel?: number;
25
+ /** Phase D: clone (default) or inherit. Inherit refused when parallel > 1. */
26
+ session?: TraverseSessionMode;
22
27
  }
23
28
  /**
24
- * Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
29
+ * Serial or parallel graph crawl. Prints greppable PASS/FAIL lines; non-zero on break.
25
30
  */
26
31
  export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Phase B: serial graph traverse (RFC traverse-ffcompose).
3
- * Walk unused legal edges until leaf / budget / break. No parallel yet.
2
+ * Phase B+D: graph traverse (serial or --parallel with --session clone).
3
+ * Walk unused legal edges until leaf / budget / break.
4
4
  */
5
5
  import { readFileSync, existsSync } from "node:fs";
6
6
  import { join } from "node:path";
@@ -8,6 +8,7 @@ import { Engine, start, end, locate } from "./engine.js";
8
8
  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
+ import { EdgeLeaseCoordinator } from "./traverse-lease.js";
11
12
  function resolveBaseUrl(projectDir) {
12
13
  if (process.env.WAYGRAPH_BASE_URL)
13
14
  return process.env.WAYGRAPH_BASE_URL;
@@ -47,6 +48,32 @@ function seedMem(library, mem, dataJson) {
47
48
  }
48
49
  }
49
50
  }
51
+ function snapshotMem(library, mem) {
52
+ const out = {};
53
+ for (const entry of library.values()) {
54
+ for (const k of entry.block.requires ?? []) {
55
+ if (mem.has(k) && !(k.name in out)) {
56
+ try {
57
+ out[k.name] = mem.get(k);
58
+ }
59
+ catch {
60
+ /* skip */
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ function restoreMem(library, mem, snap, dataJson) {
68
+ seedMem(library, mem, dataJson);
69
+ for (const entry of library.values()) {
70
+ for (const k of entry.block.requires ?? []) {
71
+ if (k.name in snap) {
72
+ mem.set(k, snap[k.name]);
73
+ }
74
+ }
75
+ }
76
+ }
50
77
  function missingMemKeys(entry, mem) {
51
78
  return (entry.block.requires ?? []).filter((k) => !mem.has(k));
52
79
  }
@@ -94,56 +121,18 @@ async function runOneBlock(engine, entry, context, page, mem) {
94
121
  clearTimeout(timer);
95
122
  }
96
123
  }
97
- /**
98
- * Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
99
- */
100
- export async function runTraverse(projectDir, options = {}) {
101
- const traverseId = options.traverseId ?? "traverse-1";
102
- const maxSteps = options.maxSteps ?? 50;
103
- const maxVisitsPerNode = options.maxVisitsPerNode ?? 2;
104
- const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
105
- const timeoutMs = options.timeoutMs ?? 15 * 60_000;
106
- const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
107
- const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
108
- const startUrl = options.startUrl ?? baseURL;
109
- const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
110
- if (options.blocksSelect) {
111
- console.error(`waygraph traverse: --blocks ${options.blocksSelect.raw} → ` +
112
- `${library.byName.size} block(s), ${graph.edges.length} edge(s)`);
113
- }
114
- const mem = new MemPage();
115
- seedMem(library.byName, mem, options.data);
116
- const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
117
- const { chromium } = await import("@playwright/test");
118
- const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
119
- const launchOpts = {
120
- headless: !headed,
121
- };
122
- if (executablePath)
123
- launchOpts.executablePath = executablePath;
124
- const browser = await chromium.launch(launchOpts);
125
- const contextOpts = {
126
- viewport: { width: 1280, height: 720 },
127
- };
128
- if (baseURL)
129
- contextOpts.baseURL = baseURL;
130
- const context = await browser.newContext(contextOpts);
131
- let page = await context.newPage();
124
+ async function runTraverseWorker(opts) {
125
+ const { traverseId, workerIndex, parallel, maxSteps, maxVisitsPerNode, maxVisitsPerEdge, timeoutMs, graph, library, engine, context, mem, leases, } = opts;
126
+ let page = opts.page;
127
+ const startUrl = opts.startUrl;
132
128
  const nodeVisits = new Map();
133
129
  const edgeVisits = new Map();
134
130
  const edgesHit = new Set();
135
131
  let steps = 0;
136
132
  const startedAt = Date.now();
137
- let lastHere = options.from ?? null;
133
+ let lastHere = opts.from ?? null;
138
134
  let exitCode = 0;
139
- console.error(`waygraph traverse[${traverseId}]: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
140
- (baseURL ? ` base=${baseURL}` : "") +
141
- ` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`);
142
135
  try {
143
- if (startUrl) {
144
- await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
145
- await page.waitForLoadState("load").catch(() => { });
146
- }
147
136
  for (;;) {
148
137
  if (Date.now() - startedAt > timeoutMs) {
149
138
  console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
@@ -162,7 +151,7 @@ export async function runTraverse(projectDir, options = {}) {
162
151
  }
163
152
  }
164
153
  const detected = await detectHere(page, library.navBlocks);
165
- const here = lastHere ?? detected ?? options.from ?? null;
154
+ const here = lastHere ?? detected ?? opts.from ?? null;
166
155
  if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
167
156
  console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
168
157
  exitCode = 1;
@@ -171,7 +160,14 @@ export async function runTraverse(projectDir, options = {}) {
171
160
  const menu = await buildExploreMenu(page, graph, library, here);
172
161
  const candidates = menu.flat.filter((edge) => {
173
162
  const key = edgeKey(edge);
174
- return (edgeVisits.get(key) ?? 0) < maxVisitsPerEdge;
163
+ if ((edgeVisits.get(key) ?? 0) >= maxVisitsPerEdge)
164
+ return false;
165
+ if (parallel > 1) {
166
+ if (EdgeLeaseCoordinator.partitionIndex(key, parallel) !== workerIndex) {
167
+ return false;
168
+ }
169
+ }
170
+ return true;
175
171
  });
176
172
  if (candidates.length === 0) {
177
173
  const label = here ?? detected ?? "unknown";
@@ -186,6 +182,12 @@ export async function runTraverse(projectDir, options = {}) {
186
182
  return aWild - bWild;
187
183
  return edgeKey(a).localeCompare(edgeKey(b));
188
184
  })[0];
185
+ const key = edgeKey(pick);
186
+ if (!leases.tryClaim(key, traverseId)) {
187
+ // Another worker won a race (instance fan-out); treat as unavailable.
188
+ edgeVisits.set(key, maxVisitsPerEdge);
189
+ continue;
190
+ }
189
191
  const entry = library.byName.get(pick.block);
190
192
  if (!entry) {
191
193
  console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
@@ -204,7 +206,6 @@ export async function runTraverse(projectDir, options = {}) {
204
206
  const observed = typeof out === "object" && out && "__state" in out
205
207
  ? String(out.__state)
206
208
  : pick.to;
207
- const key = edgeKey(pick);
208
209
  edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
209
210
  edgesHit.add(key);
210
211
  if (here)
@@ -224,8 +225,153 @@ export async function runTraverse(projectDir, options = {}) {
224
225
  }
225
226
  }
226
227
  finally {
227
- await browser.close().catch(() => { });
228
+ console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
228
229
  }
229
- console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
230
230
  return exitCode;
231
231
  }
232
+ /**
233
+ * Serial or parallel graph crawl. Prints greppable PASS/FAIL lines; non-zero on break.
234
+ */
235
+ export async function runTraverse(projectDir, options = {}) {
236
+ let parallel = Math.max(1, Math.floor(options.parallel ?? 1));
237
+ if (parallel > 4) {
238
+ console.error(`waygraph traverse: --parallel ${parallel} capped to 4`);
239
+ parallel = 4;
240
+ }
241
+ const session = options.session ?? (parallel > 1 ? "clone" : "clone");
242
+ if (parallel > 1 && session === "inherit") {
243
+ console.error("waygraph traverse: --session inherit is refused with --parallel > 1 (races); use --session clone");
244
+ return 1;
245
+ }
246
+ const maxSteps = options.maxSteps ?? 50;
247
+ const maxVisitsPerNode = options.maxVisitsPerNode ?? 2;
248
+ const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
249
+ const timeoutMs = options.timeoutMs ?? 15 * 60_000;
250
+ const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
251
+ const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
252
+ const startUrl = options.startUrl ?? baseURL;
253
+ const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
254
+ if (options.blocksSelect) {
255
+ console.error(`waygraph traverse: --blocks ${options.blocksSelect.raw} -> ` +
256
+ `${library.byName.size} block(s), ${graph.edges.length} edge(s)`);
257
+ }
258
+ const leases = new EdgeLeaseCoordinator({ projectDir, persist: parallel > 1 });
259
+ const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
260
+ const { chromium } = await import("@playwright/test");
261
+ const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
262
+ const launchOpts = {
263
+ headless: !headed,
264
+ };
265
+ if (executablePath)
266
+ launchOpts.executablePath = executablePath;
267
+ const browser = await chromium.launch(launchOpts);
268
+ console.error(`waygraph traverse: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
269
+ (baseURL ? ` base=${baseURL}` : "") +
270
+ ` parallel=${parallel} session=${session}` +
271
+ ` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`);
272
+ try {
273
+ if (parallel <= 1) {
274
+ const contextOpts = {
275
+ viewport: { width: 1280, height: 720 },
276
+ };
277
+ if (baseURL)
278
+ contextOpts.baseURL = baseURL;
279
+ const context = await browser.newContext(contextOpts);
280
+ const page = await context.newPage();
281
+ const mem = new MemPage();
282
+ seedMem(library.byName, mem, options.data);
283
+ if (startUrl) {
284
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
285
+ await page.waitForLoadState("load").catch(() => { });
286
+ }
287
+ const code = await runTraverseWorker({
288
+ traverseId: options.traverseId ?? "traverse-1",
289
+ workerIndex: 0,
290
+ parallel: 1,
291
+ maxSteps,
292
+ maxVisitsPerNode,
293
+ maxVisitsPerEdge,
294
+ timeoutMs,
295
+ ...(options.from ? { from: options.from } : {}),
296
+ ...(startUrl ? { startUrl } : {}),
297
+ ...(baseURL ? { baseURL } : {}),
298
+ ...(options.data ? { data: options.data } : {}),
299
+ graph,
300
+ library,
301
+ engine,
302
+ context,
303
+ page,
304
+ mem,
305
+ leases,
306
+ });
307
+ await context.close().catch(() => { });
308
+ return code;
309
+ }
310
+ // Phase D: bootstrap one context for storageState, then N clones.
311
+ const bootOpts = {
312
+ viewport: { width: 1280, height: 720 },
313
+ };
314
+ if (baseURL)
315
+ bootOpts.baseURL = baseURL;
316
+ const bootContext = await browser.newContext(bootOpts);
317
+ const bootPage = await bootContext.newPage();
318
+ const bootMem = new MemPage();
319
+ seedMem(library.byName, bootMem, options.data);
320
+ if (startUrl) {
321
+ await bootPage.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
322
+ await bootPage.waitForLoadState("load").catch(() => { });
323
+ }
324
+ const storageState = await bootContext.storageState();
325
+ const memSnap = snapshotMem(library.byName, bootMem);
326
+ await bootContext.close().catch(() => { });
327
+ console.error(`waygraph traverse: forked ${parallel} clone workers (storageState + mem snapshot)`);
328
+ const codes = await Promise.all(Array.from({ length: parallel }, async (_, i) => {
329
+ const traverseId = `traverse-${i + 1}`;
330
+ const ctxOpts = {
331
+ viewport: { width: 1280, height: 720 },
332
+ storageState,
333
+ };
334
+ if (baseURL)
335
+ ctxOpts.baseURL = baseURL;
336
+ const context = await browser.newContext(ctxOpts);
337
+ const page = await context.newPage();
338
+ const mem = new MemPage();
339
+ restoreMem(library.byName, mem, memSnap, options.data);
340
+ if (startUrl) {
341
+ await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
342
+ }
343
+ try {
344
+ return await runTraverseWorker({
345
+ traverseId,
346
+ workerIndex: i,
347
+ parallel,
348
+ maxSteps,
349
+ maxVisitsPerNode,
350
+ maxVisitsPerEdge,
351
+ timeoutMs,
352
+ ...(options.from ? { from: options.from } : {}),
353
+ ...(startUrl ? { startUrl } : {}),
354
+ ...(baseURL ? { baseURL } : {}),
355
+ ...(options.data ? { data: options.data } : {}),
356
+ graph,
357
+ library,
358
+ engine,
359
+ context,
360
+ page,
361
+ mem,
362
+ leases,
363
+ });
364
+ }
365
+ finally {
366
+ await context.close().catch(() => { });
367
+ }
368
+ }));
369
+ const worst = codes.reduce((a, b) => Math.max(a, b), 0);
370
+ const pass = codes.filter((c) => c === 0).length;
371
+ console.error(`waygraph traverse: suite parallel=${parallel} pass=${pass}/${parallel} exit=${worst}`);
372
+ return worst;
373
+ }
374
+ finally {
375
+ await browser.close().catch(() => { });
376
+ }
377
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "updatedAt": "2026-09-17T11:18:26.658Z",
3
+ "leases": {
4
+ "submit-login": {
5
+ "workerId": "traverse-2",
6
+ "claimedAt": 1789643826618
7
+ },
8
+ "add-all-to-cart": {
9
+ "workerId": "traverse-2",
10
+ "claimedAt": 1789643827327
11
+ },
12
+ "nav-cart": {
13
+ "workerId": "traverse-2",
14
+ "claimedAt": 1789643829570
15
+ },
16
+ "nav-continue-shopping": {
17
+ "workerId": "traverse-2",
18
+ "claimedAt": 1789643855282
19
+ },
20
+ "nav-item-detail::sauce-labs-backpack": {
21
+ "workerId": "traverse-2",
22
+ "claimedAt": 1789643857049
23
+ },
24
+ "nav-back-to-inventory": {
25
+ "workerId": "traverse-2",
26
+ "claimedAt": 1789643906658
27
+ }
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.13",
3
+ "version": "0.12.14",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",