waygraph 0.12.12 → 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 +15 -4
- package/dist/auto-explore.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +74 -22
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/traverse-lease.d.ts +18 -0
- package/dist/traverse-lease.js +62 -0
- package/dist/traverse-run.d.ts +6 -1
- package/dist/traverse-run.js +196 -50
- package/examples/saucedemo/.waygraph-traverse/leases.json +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,10 +218,21 @@ so it drops straight into `defineFlow([start, ..., composed, ..., end])` like an
|
|
|
218
218
|
`fastForwardComposeBlock(name, steps)` is the same shape marked for **wall-clock
|
|
219
219
|
fast-forward**: one opaque demo step that **blitzes** (no per-inner gate, no smooth
|
|
220
220
|
cursor theater, Playwright slowMo off for that flow unless `WAYGRAPH_SLOWMO` is set)
|
|
221
|
-
unless
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
unless expanded. Put **several** FF units in one Flow when you need to race past auth,
|
|
222
|
+
then a heavy dashboard settle, then watch the interesting middle.
|
|
223
|
+
|
|
224
|
+
**Dispute / disable FF:** `waygraph demo --ff-disabled` (alias `--no-ff`) expands every
|
|
225
|
+
FFCompose into its inner Blocks so a broken login step is visible as its own panel row.
|
|
226
|
+
Former FF inners keep blitz pacing so wall-clock stays comparable to opaque FF (same
|
|
227
|
+
real acts). `--ff-expand` is the older expand-only flag (same flatten). Saucedemo
|
|
228
|
+
`checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
|
|
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`.
|
|
225
236
|
`spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
|
|
226
237
|
Block/Flow, in the same browser context an existing `page` already belongs to - the
|
|
227
238
|
pattern `observe()` could already reach for (it's the only phase allowed to touch
|
package/dist/auto-explore.js
CHANGED
|
@@ -251,6 +251,8 @@ export async function buildExploreMenu(page, graph, library, here) {
|
|
|
251
251
|
if (!(await navRunnable(page, entry)))
|
|
252
252
|
continue;
|
|
253
253
|
if (here === null) {
|
|
254
|
+
// Bootstrap only: URL deep-links as "Start here". Click-nav needs a live
|
|
255
|
+
// control so it cannot seed an unknown page.
|
|
254
256
|
if (!isUrlNav(entry))
|
|
255
257
|
continue;
|
|
256
258
|
push(navigate, edge);
|
|
@@ -258,6 +260,12 @@ export async function buildExploreMenu(page, graph, library, here) {
|
|
|
258
260
|
}
|
|
259
261
|
if (edge.to === here)
|
|
260
262
|
continue;
|
|
263
|
+
// Known screen: do NOT dump every defineNavBlock({ url }) under Navigate.
|
|
264
|
+
// Graph still marks NavBlocks from:"*" for pathfinding/traverse; auto menu
|
|
265
|
+
// only offers click-nav whose control is visible on this page (navRunnable).
|
|
266
|
+
// Otherwise LoginForm lists every app route + mailpit as if legal moves.
|
|
267
|
+
if (isUrlNav(entry))
|
|
268
|
+
continue;
|
|
261
269
|
if (isBackEdge(edge) && (edge.from === here || edge.from === "*")) {
|
|
262
270
|
push(back, edge);
|
|
263
271
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Primary verbs (less is more):
|
|
6
6
|
* auto Explore picker; `.flow.ts` / `--blocks From To` = run that path
|
|
7
|
-
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
7
|
+
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand` `--ff-disabled`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
10
|
* Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Primary verbs (less is more):
|
|
6
6
|
* auto Explore picker; `.flow.ts` / `--blocks From To` = run that path
|
|
7
|
-
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
7
|
+
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand` `--ff-disabled`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
10
|
* Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
|
|
@@ -2170,11 +2170,11 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2170
2170
|
if ((clearSession || r.resetSession) && i > 0) {
|
|
2171
2171
|
await resetPageState(context, page, baseURL);
|
|
2172
2172
|
}
|
|
2173
|
-
// gatesFast
|
|
2174
|
-
//
|
|
2175
|
-
|
|
2176
|
-
pacing.
|
|
2177
|
-
|
|
2173
|
+
// gatesFast / skipTheater: opaque FF, former-FF inners (--ff-disabled/--ff-expand),
|
|
2174
|
+
// or WAYGRAPH_FAST_BLOCKS. --fast alone = shorter gates only (keeps theater).
|
|
2175
|
+
pacing.gatesFast = demoFast || !!r.block.fastForward || !!r.wasFastForward;
|
|
2176
|
+
pacing.skipTheater =
|
|
2177
|
+
fastBlockNames.has(r.block.name) || !!r.block.fastForward || !!r.wasFastForward;
|
|
2178
2178
|
const fixtures = r.highlightFixtures;
|
|
2179
2179
|
stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
|
|
2180
2180
|
const isNavBlock = r.block.__waygraphKind === "nav";
|
|
@@ -2484,7 +2484,9 @@ async function main() {
|
|
|
2484
2484
|
// truly unexpected failure outside runJsonReportMode's own try/catch)
|
|
2485
2485
|
// reaches stdout.
|
|
2486
2486
|
const jsonReport = process.env.WAYGRAPH_JSON === "1";
|
|
2487
|
-
const ffExpand =
|
|
2487
|
+
const ffExpand =
|
|
2488
|
+
process.env.WAYGRAPH_FF_EXPAND === "1" || process.env.WAYGRAPH_FF_DISABLED === "1";
|
|
2489
|
+
const ffDisabled = process.env.WAYGRAPH_FF_DISABLED === "1";
|
|
2488
2490
|
/** Expand or keep fastForwardComposeBlock units for demo/run step lists. */
|
|
2489
2491
|
const flattenBlockInfos = (blockInfos) => {
|
|
2490
2492
|
const out = [];
|
|
@@ -2497,6 +2499,10 @@ async function main() {
|
|
|
2497
2499
|
block: step.block,
|
|
2498
2500
|
...(step.routes ? { routes: step.routes } : {}),
|
|
2499
2501
|
...(bi.resetSessionBefore ? { resetSessionBefore: true } : {}),
|
|
2502
|
+
// Dispute / --ff-disabled: keep blitz pacing on former FF inners so
|
|
2503
|
+
// wall-clock stays comparable to opaque FF (same real acts).
|
|
2504
|
+
wasFastForward: true,
|
|
2505
|
+
ffSource: b.name,
|
|
2500
2506
|
});
|
|
2501
2507
|
}
|
|
2502
2508
|
} else {
|
|
@@ -2529,14 +2535,20 @@ async function main() {
|
|
|
2529
2535
|
flow.expectedFailureReason && idx === blockInfos.length - 1
|
|
2530
2536
|
? flow.expectedFailureReason
|
|
2531
2537
|
: undefined,
|
|
2538
|
+
...(bi.wasFastForward ? { wasFastForward: true, ffSource: bi.ffSource } : {}),
|
|
2532
2539
|
}));
|
|
2533
2540
|
chainFlows = [flow];
|
|
2534
2541
|
if (!jsonReport) {
|
|
2535
2542
|
console.log(
|
|
2536
|
-
|
|
2543
|
+
'waygraph: running existing flow "' + bareRef + '" - ' +
|
|
2537
2544
|
resolved.map((r) => r.block.name).join(" -> ") + " (" + resolved.length + " block" +
|
|
2538
2545
|
(resolved.length === 1 ? "" : "s") + ", no chain spec needed)",
|
|
2539
2546
|
);
|
|
2547
|
+
if (ffDisabled) {
|
|
2548
|
+
console.error(
|
|
2549
|
+
"waygraph demo: --ff-disabled - FFCompose expanded to inners (dispute / step locus); blitz pacing kept on former FF steps",
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2540
2552
|
}
|
|
2541
2553
|
}
|
|
2542
2554
|
}
|
|
@@ -2623,6 +2635,7 @@ async function main() {
|
|
|
2623
2635
|
expectedFailureReason:
|
|
2624
2636
|
meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
|
|
2625
2637
|
seedMem: isFirstOfSegment ? meta.seedMem : undefined,
|
|
2638
|
+
...(bi.wasFastForward ? { wasFastForward: true, ffSource: bi.ffSource } : {}),
|
|
2626
2639
|
};
|
|
2627
2640
|
remainingInFlow -= 1;
|
|
2628
2641
|
isFirstOfSegment = false;
|
|
@@ -2644,21 +2657,27 @@ async function main() {
|
|
|
2644
2657
|
// nobody has to watch a live window for it to be right).
|
|
2645
2658
|
const headed =
|
|
2646
2659
|
process.env.WAYGRAPH_HEADED === "0" ? false : process.env.WAYGRAPH_HEADED === "1" || step;
|
|
2647
|
-
// Step mode
|
|
2648
|
-
//
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
(r) => r && r.block && r.block.fastForward === true,
|
|
2660
|
+
// Step mode: blitz Playwright slowMo when any opaque FF **or** former-FF
|
|
2661
|
+
// inners (--ff-disabled) are in the resolved list. Explicit WAYGRAPH_SLOWMO wins.
|
|
2662
|
+
const hasFfBlitz = resolved.some(
|
|
2663
|
+
(r) =>
|
|
2664
|
+
r &&
|
|
2665
|
+
((r.block && r.block.fastForward === true) || r.wasFastForward === true),
|
|
2654
2666
|
);
|
|
2655
2667
|
const slowMo = process.env.WAYGRAPH_SLOWMO !== undefined
|
|
2656
2668
|
? Number(process.env.WAYGRAPH_SLOWMO)
|
|
2657
2669
|
: step
|
|
2658
|
-
?
|
|
2670
|
+
? hasFfBlitz
|
|
2659
2671
|
? 0
|
|
2660
2672
|
: 350
|
|
2661
2673
|
: undefined;
|
|
2674
|
+
if (step && hasFfBlitz && process.env.WAYGRAPH_SLOWMO === undefined && !jsonReport) {
|
|
2675
|
+
console.error(
|
|
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",
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2662
2681
|
const baseURL = process.env.WAYGRAPH_BASE_URL;
|
|
2663
2682
|
const title = process.env.WAYGRAPH_TITLE;
|
|
2664
2683
|
// Comma-separated Block names (their real .name, e.g. "login") that
|
|
@@ -3355,6 +3374,10 @@ function parseRunFlags(argv) {
|
|
|
3355
3374
|
else if (a === "--ff-expand") {
|
|
3356
3375
|
out.ffExpand = true;
|
|
3357
3376
|
}
|
|
3377
|
+
else if (a === "--ff-disabled" || a === "--no-ff") {
|
|
3378
|
+
out.ffDisabled = true;
|
|
3379
|
+
out.ffExpand = true;
|
|
3380
|
+
}
|
|
3358
3381
|
else if (a === "--non-headless") {
|
|
3359
3382
|
out.nonHeadless = true;
|
|
3360
3383
|
}
|
|
@@ -3516,6 +3539,10 @@ function applyRunFlags(flags, opts) {
|
|
|
3516
3539
|
if (flags.ffExpand) {
|
|
3517
3540
|
process.env.WAYGRAPH_FF_EXPAND = "1";
|
|
3518
3541
|
}
|
|
3542
|
+
if (flags.ffDisabled) {
|
|
3543
|
+
process.env.WAYGRAPH_FF_DISABLED = "1";
|
|
3544
|
+
process.env.WAYGRAPH_FF_EXPAND = "1";
|
|
3545
|
+
}
|
|
3519
3546
|
}
|
|
3520
3547
|
/**
|
|
3521
3548
|
* BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set:
|
|
@@ -3674,6 +3701,7 @@ Primary (less is more):
|
|
|
3674
3701
|
--fast Shorter auto-next / Next gates (keeps smooth cursor)
|
|
3675
3702
|
--full Classic wrap-all block chips (default: carousel)
|
|
3676
3703
|
--ff-expand Expand fastForwardComposeBlock inners as separate steps
|
|
3704
|
+
--ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
|
|
3677
3705
|
--auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless by default
|
|
3678
3706
|
--auto-play-video-head Same, but keep the browser visible (--non-headless)
|
|
3679
3707
|
--video-viewport WxH Recording size (default demo: 1920x1080)
|
|
@@ -3684,11 +3712,14 @@ Primary (less is more):
|
|
|
3684
3712
|
--video [dir] Record .webm
|
|
3685
3713
|
--video-viewport WxH Recording size (default run: 1280x720)
|
|
3686
3714
|
--ff-expand Same as demo (expand FFCompose inners)
|
|
3715
|
+
--ff-disabled Same as demo (dispute: expand FF)
|
|
3687
3716
|
|
|
3688
3717
|
Also:
|
|
3689
3718
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3690
|
-
waygraph traverse [project]
|
|
3719
|
+
waygraph traverse [project] Graph crawl (Phase B+D)
|
|
3691
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
|
|
3692
3723
|
--from <Checkpoint> Seed / start checkpoint
|
|
3693
3724
|
--data '{...}' Mem seed
|
|
3694
3725
|
--max-steps N Cap Block runs (default 50)
|
|
@@ -3714,6 +3745,7 @@ Examples:
|
|
|
3714
3745
|
waygraph auto --blocks LoginPage OrderComplete
|
|
3715
3746
|
waygraph auto --blocks '/mailpit/' # Phase C filtered explore
|
|
3716
3747
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
|
|
3748
|
+
waygraph traverse --parallel 2 --session clone # Phase D clone workers
|
|
3717
3749
|
waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
|
|
3718
3750
|
|
|
3719
3751
|
Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
|
|
@@ -3808,6 +3840,8 @@ async function main() {
|
|
|
3808
3840
|
let headed = false;
|
|
3809
3841
|
let baseUrl;
|
|
3810
3842
|
let blocksFilter;
|
|
3843
|
+
let parallel;
|
|
3844
|
+
let session;
|
|
3811
3845
|
let projectDir = process.cwd();
|
|
3812
3846
|
for (let i = 0; i < rest.length; i++) {
|
|
3813
3847
|
const a = rest[i];
|
|
@@ -3833,6 +3867,20 @@ async function main() {
|
|
|
3833
3867
|
}
|
|
3834
3868
|
continue;
|
|
3835
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
|
+
}
|
|
3836
3884
|
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
3837
3885
|
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
3838
3886
|
maxSteps = Number(v);
|
|
@@ -3861,13 +3909,15 @@ async function main() {
|
|
|
3861
3909
|
if (a === "--help" || a === "-h") {
|
|
3862
3910
|
console.log(`waygraph traverse [project] [flags]
|
|
3863
3911
|
|
|
3864
|
-
|
|
3865
|
-
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.
|
|
3866
3914
|
|
|
3867
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
|
|
3868
3918
|
--from <Checkpoint> start checkpoint (optional)
|
|
3869
3919
|
--data '{...}' Mem seed JSON
|
|
3870
|
-
--max-steps N default 50
|
|
3920
|
+
--max-steps N default 50 (per worker)
|
|
3871
3921
|
--max-visits N per-node visit cap (default 2)
|
|
3872
3922
|
--max-visits-per-edge N default 1
|
|
3873
3923
|
--non-headless show browser
|
|
@@ -3875,7 +3925,7 @@ budget kill, or first broken edge.
|
|
|
3875
3925
|
|
|
3876
3926
|
Examples:
|
|
3877
3927
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts'
|
|
3878
|
-
waygraph traverse --
|
|
3928
|
+
waygraph traverse --parallel 2 --session clone --max-steps 20
|
|
3879
3929
|
|
|
3880
3930
|
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
3881
3931
|
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
@@ -3906,6 +3956,8 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
3906
3956
|
headed,
|
|
3907
3957
|
...(baseUrl ? { baseURL: baseUrl } : {}),
|
|
3908
3958
|
...(blocksSelect ? { blocksSelect } : {}),
|
|
3959
|
+
...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
|
|
3960
|
+
...(session ? { session } : {}),
|
|
3909
3961
|
});
|
|
3910
3962
|
process.exit(code);
|
|
3911
3963
|
}
|
|
@@ -4006,7 +4058,7 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
|
|
|
4006
4058
|
console.error("waygraph demo: missing flow/spec — e.g.\n" +
|
|
4007
4059
|
" waygraph demo src/flows/shop.flow.ts\n" +
|
|
4008
4060
|
" waygraph demo --blocks shopFlow\n" +
|
|
4009
|
-
" Flags: --blocks --data --auto-next --fast --full --ff-expand --auto-play-video --title --base-url --video");
|
|
4061
|
+
" Flags: --blocks --data --auto-next --fast --full --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
|
|
4010
4062
|
process.exit(1);
|
|
4011
4063
|
}
|
|
4012
4064
|
const proj = flags.blocks
|
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
|
+
}
|
package/dist/traverse-run.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/traverse-run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Phase B:
|
|
3
|
-
* Walk unused legal edges until leaf / budget / break.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|