waygraph 0.12.13 → 0.12.15
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 +12 -1
- package/dist/cli.js +115 -27
- package/dist/engine.d.ts +17 -0
- package/dist/engine.js +25 -0
- package/dist/highlights.d.ts +44 -2
- package/dist/highlights.js +88 -13
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/step-overlay.d.ts +1 -1
- package/dist/step-overlay.js +34 -6
- 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/dist/types.d.ts +5 -0
- package/examples/saucedemo/.waygraph-traverse/leases.json +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,7 +226,18 @@ 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.
|
|
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`.
|
|
236
|
+
|
|
237
|
+
**Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
|
|
238
|
+
`tone: "info" | "warning" | "danger" | "success"` (iconified captions). Episode pacing:
|
|
239
|
+
`withDemoPace(flow, "fast" | "slow" | "normal" | "blitz")` or `withBlockPace(block, ...)`
|
|
240
|
+
(FFCompose stays blitz). Living objectives: `docs/proposals/OBJECTIVES.md`.
|
|
230
241
|
`spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
|
|
231
242
|
Block/Flow, in the same browser context an existing `page` already belongs to - the
|
|
232
243
|
pattern `observe()` could already reach for (it's the only phase allowed to touch
|
package/dist/cli.js
CHANGED
|
@@ -211,6 +211,9 @@ import {
|
|
|
211
211
|
formatHighlightCaption,
|
|
212
212
|
hasAuthoredStubAfter,
|
|
213
213
|
hasAuthoredStubOnError,
|
|
214
|
+
normalizeHighlightTone,
|
|
215
|
+
normalizeDemoPace,
|
|
216
|
+
resolveStepDemoPace,
|
|
214
217
|
} from "waygraph";
|
|
215
218
|
|
|
216
219
|
function walkDir(dir, pattern) {
|
|
@@ -399,9 +402,13 @@ const RING_CSS =
|
|
|
399
402
|
"border:2.5px solid #7C3AED;border-radius:10px;" +
|
|
400
403
|
"box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
|
|
401
404
|
// Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
|
|
402
|
-
// Automation (verify fallback, unmatched fill/click) =
|
|
403
|
-
// can ignore engine checks and
|
|
404
|
-
"#wg-ring[data-tone=auto]{border-color:#
|
|
405
|
+
// Automation (verify fallback, unmatched fill/click) = gray - operators
|
|
406
|
+
// can ignore engine checks and watch purple + semantic tones.
|
|
407
|
+
"#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
|
|
408
|
+
"#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
|
|
409
|
+
"#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
|
|
410
|
+
"#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
|
|
411
|
+
"#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
|
|
405
412
|
// A real element, not a ::after pseudo-element - a pseudo-element's
|
|
406
413
|
// position is CSS-relative to the ring's own box (left:0 always meant
|
|
407
414
|
// "the ring's own left edge"), so it had no way to be clamped back onto
|
|
@@ -413,7 +420,11 @@ const RING_CSS =
|
|
|
413
420
|
"#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
414
421
|
"max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
|
|
415
422
|
"font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
|
|
416
|
-
"#wg-ring-label[data-tone=auto]{background:#
|
|
423
|
+
"#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
|
|
424
|
+
"#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
|
|
425
|
+
"#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
|
|
426
|
+
"#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
|
|
427
|
+
"#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
|
|
417
428
|
// Mouse cursor icon that travels to a target before it's acted on, plus a
|
|
418
429
|
// quick expanding ripple at the moment of a click - same idea as
|
|
419
430
|
// help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
|
|
@@ -431,7 +442,11 @@ const RING_CSS =
|
|
|
431
442
|
"#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
|
|
432
443
|
"margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
|
|
433
444
|
"border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
|
|
434
|
-
"#wg-click-pulse[data-tone=auto]{border-color:#
|
|
445
|
+
"#wg-click-pulse[data-tone=auto]{border-color:#9CA3AF;background:rgba(156,163,175,.28);}" +
|
|
446
|
+
"#wg-click-pulse[data-tone=info]{border-color:#3B82F6;background:rgba(59,130,246,.28);}" +
|
|
447
|
+
"#wg-click-pulse[data-tone=warning]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
|
|
448
|
+
"#wg-click-pulse[data-tone=danger]{border-color:#EF4444;background:rgba(239,68,68,.28);}" +
|
|
449
|
+
"#wg-click-pulse[data-tone=success]{border-color:#22C55E;background:rgba(34,197,94,.28);}" +
|
|
435
450
|
"#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
|
|
436
451
|
"@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
|
|
437
452
|
"#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
|
|
@@ -651,7 +666,11 @@ async function installOverlay(page, title) {
|
|
|
651
666
|
const ring = document.getElementById("wg-ring");
|
|
652
667
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
653
668
|
if (!ring || !ringLabel) return;
|
|
654
|
-
const
|
|
669
|
+
const raw = (tone || "planned") + "";
|
|
670
|
+
const t =
|
|
671
|
+
raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
|
|
672
|
+
? raw
|
|
673
|
+
: "planned";
|
|
655
674
|
ring.dataset.tone = t;
|
|
656
675
|
ringLabel.dataset.tone = t;
|
|
657
676
|
const margin = 6;
|
|
@@ -688,7 +707,11 @@ async function installOverlay(page, title) {
|
|
|
688
707
|
window.__wgClickPulse = (x, y, tone) => {
|
|
689
708
|
const pulse = document.getElementById("wg-click-pulse");
|
|
690
709
|
if (!pulse) return;
|
|
691
|
-
|
|
710
|
+
const raw = (tone || "planned") + "";
|
|
711
|
+
pulse.dataset.tone =
|
|
712
|
+
raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
|
|
713
|
+
? raw
|
|
714
|
+
: "planned";
|
|
692
715
|
pulse.style.left = x + "px";
|
|
693
716
|
pulse.style.top = y + "px";
|
|
694
717
|
pulse.classList.remove("wg-pulse");
|
|
@@ -1303,7 +1326,7 @@ function resolveDeclaredHighlights(block, resultTag) {
|
|
|
1303
1326
|
if (!Array.isArray(highlights)) return [];
|
|
1304
1327
|
return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string").map((h) => ({
|
|
1305
1328
|
...h,
|
|
1306
|
-
tone: h.tone
|
|
1329
|
+
tone: normalizeHighlightTone(h.tone),
|
|
1307
1330
|
}));
|
|
1308
1331
|
}
|
|
1309
1332
|
|
|
@@ -1438,14 +1461,22 @@ async function matchStubForLocator(page, locator, stubs) {
|
|
|
1438
1461
|
|
|
1439
1462
|
async function captionForLocator(page, locator, stubs, fallback) {
|
|
1440
1463
|
const matched = await matchStubForLocator(page, locator, stubs);
|
|
1441
|
-
if (matched)
|
|
1464
|
+
if (matched) {
|
|
1465
|
+
return {
|
|
1466
|
+
label: formatHighlightCaption(matched),
|
|
1467
|
+
tone: normalizeHighlightTone(matched.tone),
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1442
1470
|
return { label: fallback, tone: "auto" };
|
|
1443
1471
|
}
|
|
1444
1472
|
|
|
1445
1473
|
async function dwellMatchedStub(page, locator, stubs, pacing) {
|
|
1446
1474
|
const matched = await matchStubForLocator(page, locator, stubs);
|
|
1447
1475
|
if (!matched) return;
|
|
1448
|
-
const ms = resolveFixtureDwellMs(matched, {
|
|
1476
|
+
const ms = resolveFixtureDwellMs(matched, {
|
|
1477
|
+
gatesFast: !!(pacing && pacing.gatesFast),
|
|
1478
|
+
pace: pacing && pacing.demoPace,
|
|
1479
|
+
});
|
|
1449
1480
|
if (ms != null && ms > 0) {
|
|
1450
1481
|
await new Promise((res) => setTimeout(res, ms));
|
|
1451
1482
|
}
|
|
@@ -1466,22 +1497,26 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1466
1497
|
const episodeNumber = opts && opts.episodeNumber;
|
|
1467
1498
|
const episodeTitle = (opts && opts.episodeTitle) || "";
|
|
1468
1499
|
const gatesFast = !!(opts && opts.fast);
|
|
1500
|
+
const demoPace = normalizeDemoPace((opts && opts.pace) || (gatesFast ? "fast" : "normal"));
|
|
1469
1501
|
const autoplayMs =
|
|
1470
1502
|
opts && opts.autoplayMs
|
|
1471
1503
|
? Number(opts.autoplayMs)
|
|
1472
1504
|
: process.env.WAYGRAPH_AUTOPLAY_MS
|
|
1473
1505
|
? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
|
|
1474
|
-
:
|
|
1506
|
+
: demoPace === "slow"
|
|
1507
|
+
? 3500
|
|
1508
|
+
: 1800;
|
|
1475
1509
|
for (let i = 0; i < slides.length; i++) {
|
|
1476
1510
|
const s = slides[i];
|
|
1477
1511
|
const caption = formatHighlightCaption(s);
|
|
1512
|
+
const slideTone = normalizeHighlightTone(s.tone);
|
|
1478
1513
|
const isLast = i === slides.length - 1;
|
|
1479
|
-
const dwellMs = resolveFixtureDwellMs(s, { gatesFast });
|
|
1514
|
+
const dwellMs = resolveFixtureDwellMs(s, { gatesFast, pace: demoPace });
|
|
1480
1515
|
await installOverlay(page, title);
|
|
1481
1516
|
if (s.selector) {
|
|
1482
1517
|
try {
|
|
1483
1518
|
const box = await page.locator(s.selector).first().boundingBox();
|
|
1484
|
-
if (box) await showRing(page, box, caption,
|
|
1519
|
+
if (box) await showRing(page, box, caption, slideTone);
|
|
1485
1520
|
} catch {
|
|
1486
1521
|
await hideRing(page);
|
|
1487
1522
|
}
|
|
@@ -1996,7 +2031,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
1996
2031
|
// inside the FF (that was the "30s login / cursor gone but still slow" bug).
|
|
1997
2032
|
const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
|
|
1998
2033
|
const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
|
|
1999
|
-
const pacing = { gatesFast: demoFast, skipTheater: false };
|
|
2034
|
+
const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
|
|
2000
2035
|
const stubBeforeRef = { current: [] };
|
|
2001
2036
|
instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
|
|
2002
2037
|
// Force the panel checkbox from this process's flags/env at run start.
|
|
@@ -2087,7 +2122,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2087
2122
|
? Math.min(80, autoplayMs)
|
|
2088
2123
|
: pacing.gatesFast
|
|
2089
2124
|
? Math.min(400, autoplayMs)
|
|
2090
|
-
:
|
|
2125
|
+
: pacing.gatesSlow
|
|
2126
|
+
? Math.max(autoplayMs * 2, 3500)
|
|
2127
|
+
: autoplayMs;
|
|
2091
2128
|
let elapsed = 0;
|
|
2092
2129
|
for (;;) {
|
|
2093
2130
|
// Re-read the checkbox EVERY loop tick, not once up front - a human
|
|
@@ -2171,10 +2208,33 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2171
2208
|
await resetPageState(context, page, baseURL);
|
|
2172
2209
|
}
|
|
2173
2210
|
// gatesFast / skipTheater: opaque FF, former-FF inners (--ff-disabled/--ff-expand),
|
|
2174
|
-
// or
|
|
2175
|
-
|
|
2211
|
+
// WAYGRAPH_FAST_BLOCKS, or withDemoPace/withBlockPace. --fast alone = shorter
|
|
2212
|
+
// gates only (keeps theater). Episode pace: block > flow > CLI --fast.
|
|
2213
|
+
const stepPace = resolveStepDemoPace({
|
|
2214
|
+
blockPace: r.block.demoPace,
|
|
2215
|
+
flowPace: r.demoPace,
|
|
2216
|
+
fastForward: !!r.block.fastForward,
|
|
2217
|
+
wasFastForward: !!r.wasFastForward,
|
|
2218
|
+
});
|
|
2219
|
+
const cliFast = demoFast || fastBlockNames.has(r.block.name);
|
|
2220
|
+
pacing.demoPace =
|
|
2221
|
+
stepPace === "normal" && cliFast
|
|
2222
|
+
? fastBlockNames.has(r.block.name)
|
|
2223
|
+
? "blitz"
|
|
2224
|
+
: "fast"
|
|
2225
|
+
: stepPace;
|
|
2226
|
+
pacing.gatesFast =
|
|
2227
|
+
pacing.demoPace === "blitz" ||
|
|
2228
|
+
pacing.demoPace === "fast" ||
|
|
2229
|
+
!!r.block.fastForward ||
|
|
2230
|
+
!!r.wasFastForward ||
|
|
2231
|
+
demoFast;
|
|
2232
|
+
pacing.gatesSlow = pacing.demoPace === "slow";
|
|
2176
2233
|
pacing.skipTheater =
|
|
2177
|
-
|
|
2234
|
+
pacing.demoPace === "blitz" ||
|
|
2235
|
+
fastBlockNames.has(r.block.name) ||
|
|
2236
|
+
!!r.block.fastForward ||
|
|
2237
|
+
!!r.wasFastForward;
|
|
2178
2238
|
const fixtures = r.highlightFixtures;
|
|
2179
2239
|
stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
|
|
2180
2240
|
const isNavBlock = r.block.__waygraphKind === "nav";
|
|
@@ -2273,7 +2333,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2273
2333
|
label: formatHighlightCaption(h),
|
|
2274
2334
|
duration: h.duration,
|
|
2275
2335
|
fastMode: h.fastMode,
|
|
2276
|
-
tone:
|
|
2336
|
+
tone: normalizeHighlightTone(h.tone),
|
|
2277
2337
|
}));
|
|
2278
2338
|
await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
|
|
2279
2339
|
defaultHoldMs: 2000,
|
|
@@ -2333,6 +2393,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2333
2393
|
title,
|
|
2334
2394
|
blockName: r.block.name,
|
|
2335
2395
|
fast: pacing.gatesFast,
|
|
2396
|
+
pace: pacing.demoPace,
|
|
2336
2397
|
episodeNumber: r.episodeNumber,
|
|
2337
2398
|
episodeTitle: r.episodeTitle,
|
|
2338
2399
|
autoplayMs,
|
|
@@ -2346,7 +2407,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2346
2407
|
label: formatHighlightCaption(h),
|
|
2347
2408
|
duration: h.duration,
|
|
2348
2409
|
fastMode: h.fastMode,
|
|
2349
|
-
tone:
|
|
2410
|
+
tone: normalizeHighlightTone(h.tone),
|
|
2350
2411
|
}),
|
|
2351
2412
|
);
|
|
2352
2413
|
} else {
|
|
@@ -2531,6 +2592,7 @@ async function main() {
|
|
|
2531
2592
|
exportName: bi.name,
|
|
2532
2593
|
seedMem: idx === 0 ? seedMem : undefined,
|
|
2533
2594
|
highlightFixtures: flow.highlightFixtures,
|
|
2595
|
+
demoPace: flow.demoPace,
|
|
2534
2596
|
expectedFailureReason:
|
|
2535
2597
|
flow.expectedFailureReason && idx === blockInfos.length - 1
|
|
2536
2598
|
? flow.expectedFailureReason
|
|
@@ -2599,6 +2661,7 @@ async function main() {
|
|
|
2599
2661
|
episodeTitle: flow.title || seg.ref,
|
|
2600
2662
|
expectedFailureReason: flow.expectedFailureReason,
|
|
2601
2663
|
highlightFixtures: flow.highlightFixtures,
|
|
2664
|
+
demoPace: flow.demoPace,
|
|
2602
2665
|
seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
|
|
2603
2666
|
});
|
|
2604
2667
|
} else {
|
|
@@ -2608,6 +2671,7 @@ async function main() {
|
|
|
2608
2671
|
episodeNumber: undefined,
|
|
2609
2672
|
episodeTitle: undefined,
|
|
2610
2673
|
highlightFixtures: undefined,
|
|
2674
|
+
demoPace: undefined,
|
|
2611
2675
|
seedMem: () => seedMemForBlock(mem, r, seg.json),
|
|
2612
2676
|
});
|
|
2613
2677
|
}
|
|
@@ -2632,6 +2696,7 @@ async function main() {
|
|
|
2632
2696
|
episodeNumber: meta.episodeNumber,
|
|
2633
2697
|
episodeTitle: meta.episodeTitle,
|
|
2634
2698
|
highlightFixtures: meta.highlightFixtures,
|
|
2699
|
+
demoPace: meta.demoPace,
|
|
2635
2700
|
expectedFailureReason:
|
|
2636
2701
|
meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
|
|
2637
2702
|
seedMem: isFirstOfSegment ? meta.seedMem : undefined,
|
|
@@ -2674,8 +2739,8 @@ async function main() {
|
|
|
2674
2739
|
if (step && hasFfBlitz && process.env.WAYGRAPH_SLOWMO === undefined && !jsonReport) {
|
|
2675
2740
|
console.error(
|
|
2676
2741
|
ffDisabled
|
|
2677
|
-
? "waygraph demo: --ff-disabled (expanded)
|
|
2678
|
-
: "waygraph demo: opaque FFCompose in flow
|
|
2742
|
+
? "waygraph demo: --ff-disabled (expanded) -> Playwright slowMo=0 on former FF steps (blitz; same acts as opaque FF)"
|
|
2743
|
+
: "waygraph demo: opaque FFCompose in flow -> Playwright slowMo=0 (blitz); set WAYGRAPH_SLOWMO to override",
|
|
2679
2744
|
);
|
|
2680
2745
|
}
|
|
2681
2746
|
const baseURL = process.env.WAYGRAPH_BASE_URL;
|
|
@@ -3716,8 +3781,10 @@ Primary (less is more):
|
|
|
3716
3781
|
|
|
3717
3782
|
Also:
|
|
3718
3783
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3719
|
-
waygraph traverse [project]
|
|
3784
|
+
waygraph traverse [project] Graph crawl (Phase B+D)
|
|
3720
3785
|
--blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
|
|
3786
|
+
--parallel N Phase D: N clone workers (max 4)
|
|
3787
|
+
--session clone|inherit Phase D: default clone
|
|
3721
3788
|
--from <Checkpoint> Seed / start checkpoint
|
|
3722
3789
|
--data '{...}' Mem seed
|
|
3723
3790
|
--max-steps N Cap Block runs (default 50)
|
|
@@ -3743,6 +3810,7 @@ Examples:
|
|
|
3743
3810
|
waygraph auto --blocks LoginPage OrderComplete
|
|
3744
3811
|
waygraph auto --blocks '/mailpit/' # Phase C filtered explore
|
|
3745
3812
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
|
|
3813
|
+
waygraph traverse --parallel 2 --session clone # Phase D clone workers
|
|
3746
3814
|
waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
|
|
3747
3815
|
|
|
3748
3816
|
Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
|
|
@@ -3837,6 +3905,8 @@ async function main() {
|
|
|
3837
3905
|
let headed = false;
|
|
3838
3906
|
let baseUrl;
|
|
3839
3907
|
let blocksFilter;
|
|
3908
|
+
let parallel;
|
|
3909
|
+
let session;
|
|
3840
3910
|
let projectDir = process.cwd();
|
|
3841
3911
|
for (let i = 0; i < rest.length; i++) {
|
|
3842
3912
|
const a = rest[i];
|
|
@@ -3862,6 +3932,20 @@ async function main() {
|
|
|
3862
3932
|
}
|
|
3863
3933
|
continue;
|
|
3864
3934
|
}
|
|
3935
|
+
if (a === "--parallel" || a.startsWith("--parallel=")) {
|
|
3936
|
+
const v = a.startsWith("--parallel=") ? a.slice("--parallel=".length) : rest[++i];
|
|
3937
|
+
parallel = Number(v);
|
|
3938
|
+
continue;
|
|
3939
|
+
}
|
|
3940
|
+
if (a === "--session" || a.startsWith("--session=")) {
|
|
3941
|
+
const v = a.startsWith("--session=") ? a.slice("--session=".length) : rest[++i];
|
|
3942
|
+
if (v !== "clone" && v !== "inherit") {
|
|
3943
|
+
console.error('waygraph traverse: --session must be "clone" or "inherit"');
|
|
3944
|
+
process.exit(1);
|
|
3945
|
+
}
|
|
3946
|
+
session = v;
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3865
3949
|
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
3866
3950
|
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
3867
3951
|
maxSteps = Number(v);
|
|
@@ -3890,13 +3974,15 @@ async function main() {
|
|
|
3890
3974
|
if (a === "--help" || a === "-h") {
|
|
3891
3975
|
console.log(`waygraph traverse [project] [flags]
|
|
3892
3976
|
|
|
3893
|
-
|
|
3894
|
-
budget kill, or first broken edge.
|
|
3977
|
+
Graph crawl (RFC Phase B+D). Walks unused legal edges until a leaf,
|
|
3978
|
+
budget kill, or first broken edge. --parallel N uses session clone + edge leases.
|
|
3895
3979
|
|
|
3896
3980
|
--blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
|
|
3981
|
+
--parallel N Phase D: N clone workers (default 1, max 4)
|
|
3982
|
+
--session clone|inherit Phase D: default clone; inherit refused if parallel>1
|
|
3897
3983
|
--from <Checkpoint> start checkpoint (optional)
|
|
3898
3984
|
--data '{...}' Mem seed JSON
|
|
3899
|
-
--max-steps N default 50
|
|
3985
|
+
--max-steps N default 50 (per worker)
|
|
3900
3986
|
--max-visits N per-node visit cap (default 2)
|
|
3901
3987
|
--max-visits-per-edge N default 1
|
|
3902
3988
|
--non-headless show browser
|
|
@@ -3904,7 +3990,7 @@ budget kill, or first broken edge.
|
|
|
3904
3990
|
|
|
3905
3991
|
Examples:
|
|
3906
3992
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts'
|
|
3907
|
-
waygraph traverse --
|
|
3993
|
+
waygraph traverse --parallel 2 --session clone --max-steps 20
|
|
3908
3994
|
|
|
3909
3995
|
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
3910
3996
|
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
@@ -3935,6 +4021,8 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
3935
4021
|
headed,
|
|
3936
4022
|
...(baseUrl ? { baseURL: baseUrl } : {}),
|
|
3937
4023
|
...(blocksSelect ? { blocksSelect } : {}),
|
|
4024
|
+
...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
|
|
4025
|
+
...(session ? { session } : {}),
|
|
3938
4026
|
});
|
|
3939
4027
|
process.exit(code);
|
|
3940
4028
|
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -186,6 +186,11 @@ export interface Flow<Out extends Checkpoint<string>> {
|
|
|
186
186
|
* block.name. `run()` never reads this; step/`demo` merges onto block stubs.
|
|
187
187
|
*/
|
|
188
188
|
readonly highlightFixtures?: import("./highlights.js").HighlightFixtureMap;
|
|
189
|
+
/**
|
|
190
|
+
* Demo pacing for this Flow's episode (`withDemoPace`).
|
|
191
|
+
* `run()` ignores; step/demo applies per Block in the episode.
|
|
192
|
+
*/
|
|
193
|
+
readonly demoPace?: import("./highlights.js").DemoPace;
|
|
189
194
|
/**
|
|
190
195
|
* This Flow's constituent Blocks, in order between `start`/`end`, as plain
|
|
191
196
|
* data - for introspection/visualization tools, without running anything.
|
|
@@ -253,6 +258,18 @@ export declare function withSessionReset<Out extends Checkpoint<string>>(flow: F
|
|
|
253
258
|
* @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
|
|
254
259
|
*/
|
|
255
260
|
export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out>, title: string): Flow<Out>;
|
|
261
|
+
/**
|
|
262
|
+
* Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
|
|
263
|
+
* Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
|
|
264
|
+
* use this when an entire episode should be fast or slow.
|
|
265
|
+
* @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
|
|
266
|
+
*/
|
|
267
|
+
export declare function withDemoPace<Out extends Checkpoint<string>>(flow: Flow<Out>, pace: import("./highlights.js").DemoPace): Flow<Out>;
|
|
268
|
+
/**
|
|
269
|
+
* Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
|
|
270
|
+
* @example withBlockPace(composeBlock("gap-review", [...]), "slow")
|
|
271
|
+
*/
|
|
272
|
+
export declare function withBlockPace<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, pace: import("./highlights.js").DemoPace): Block<In, Out>;
|
|
256
273
|
/**
|
|
257
274
|
* Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
|
|
258
275
|
* login attempt genuinely failing, demonstrating the app's own real
|
package/dist/engine.js
CHANGED
|
@@ -351,6 +351,31 @@ export function withTitle(flow, title) {
|
|
|
351
351
|
modBlockVerify: (block, nameOrIndex, newCheck) => withTitle(flow.modBlockVerify(block, nameOrIndex, newCheck), title),
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
|
|
356
|
+
* Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
|
|
357
|
+
* use this when an entire episode should be fast or slow.
|
|
358
|
+
* @example withDemoPace(withTitle(gapFlow, "Episode 2 - gaps"), "slow")
|
|
359
|
+
*/
|
|
360
|
+
export function withDemoPace(flow, pace) {
|
|
361
|
+
const demoPace = pace;
|
|
362
|
+
return {
|
|
363
|
+
...flow,
|
|
364
|
+
demoPace,
|
|
365
|
+
withBlockVerify: (block, verify) => withDemoPace(flow.withBlockVerify(block, verify), demoPace),
|
|
366
|
+
modBlockVerify: (block, nameOrIndex, newCheck) => withDemoPace(flow.modBlockVerify(block, nameOrIndex, newCheck), demoPace),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Demo pacing for one Block or compose unit (not FF - use fastForwardComposeBlock for blitz).
|
|
371
|
+
* @example withBlockPace(composeBlock("gap-review", [...]), "slow")
|
|
372
|
+
*/
|
|
373
|
+
export function withBlockPace(block, pace) {
|
|
374
|
+
return {
|
|
375
|
+
...block,
|
|
376
|
+
demoPace: pace,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
354
379
|
/**
|
|
355
380
|
* Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
|
|
356
381
|
* login attempt genuinely failing, demonstrating the app's own real
|
package/dist/highlights.d.ts
CHANGED
|
@@ -33,9 +33,16 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
|
|
|
33
33
|
detail?: string;
|
|
34
34
|
/** Short badge, e.g. AC / BUG / GATE / YAP. */
|
|
35
35
|
tag?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Demo ring color + icon:
|
|
38
|
+
* - `planned` purple (authored narration, default)
|
|
39
|
+
* - `auto` gray (engine automation / verify fallback)
|
|
40
|
+
* - `info` blue · `warning` yellow · `danger` red · `success` green
|
|
41
|
+
*/
|
|
42
|
+
tone?: HighlightTone;
|
|
36
43
|
}
|
|
37
44
|
/** Flow fixture patch: label required; selector optional (inherits from stub). */
|
|
38
|
-
export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag"> & FixtureDurationFields;
|
|
45
|
+
export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone"> & FixtureDurationFields;
|
|
39
46
|
export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
|
|
40
47
|
export type HighlightStubPhaseOrFn<Out extends Checkpoint<string>> = HighlightStubPhase | ((out: Out) => HighlightStubPhase);
|
|
41
48
|
/**
|
|
@@ -49,11 +56,32 @@ export interface WaygraphSlide extends FixtureDurationFields {
|
|
|
49
56
|
tag?: string;
|
|
50
57
|
/** Optional ring while this slide is showing. */
|
|
51
58
|
selector?: string;
|
|
59
|
+
tone?: HighlightTone;
|
|
52
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Demo ring tones (iconified).
|
|
63
|
+
* Automation (`auto`) is gray so authored planned/info/warning/danger/success stand out.
|
|
64
|
+
*/
|
|
65
|
+
export type HighlightTone = "planned" | "auto" | "info" | "warning" | "danger" | "success";
|
|
66
|
+
/** Map author aliases (`error` -> danger) to a canonical tone. */
|
|
67
|
+
export declare function normalizeHighlightTone(raw: unknown): HighlightTone;
|
|
68
|
+
/** Prefix label with a short ASCII icon for semantic tones. */
|
|
69
|
+
export declare function toneIconPrefix(tone: HighlightTone): string;
|
|
70
|
+
/**
|
|
71
|
+
* Demo pacing for a Flow episode or a Block/compose group.
|
|
72
|
+
* - `blitz` - FF-like: skip theater + short gates (same as fastForwardComposeBlock)
|
|
73
|
+
* - `fast` - shorter gates, keep cursor theater
|
|
74
|
+
* - `normal` - default demo pace
|
|
75
|
+
* - `slow` - longer autoplay + fixture dwell (gaps / wrongs review)
|
|
76
|
+
*/
|
|
77
|
+
export type DemoPace = "blitz" | "fast" | "normal" | "slow";
|
|
78
|
+
export declare function normalizeDemoPace(raw: unknown): DemoPace;
|
|
53
79
|
/** Default fixture dwell when `duration: true`. */
|
|
54
80
|
export declare const FIXTURE_DURATION_MS_DEFAULT = 2000;
|
|
55
81
|
/** Default fixture dwell under `--fast` when duration is enabled. */
|
|
56
82
|
export declare const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
|
|
83
|
+
/** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
|
|
84
|
+
export declare const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
|
|
57
85
|
/** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
|
|
58
86
|
export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
|
|
59
87
|
/** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
|
|
@@ -64,11 +92,23 @@ export declare const SLIDE_DURATION_FAST_MS_DEFAULT = 600;
|
|
|
64
92
|
*/
|
|
65
93
|
export declare function resolveFixtureDwellMs(fixture: FixtureDurationFields, opts?: {
|
|
66
94
|
gatesFast?: boolean;
|
|
95
|
+
pace?: DemoPace;
|
|
67
96
|
}): number | null;
|
|
68
97
|
/** @deprecated Prefer {@link resolveFixtureDwellMs} */
|
|
69
98
|
export declare function resolveSlideDwellMs(slide: FixtureDurationFields, opts?: {
|
|
70
99
|
gatesFast?: boolean;
|
|
100
|
+
pace?: DemoPace;
|
|
71
101
|
}): number | null;
|
|
102
|
+
/**
|
|
103
|
+
* Effective demo pace for a step: block override > flow episode > normal.
|
|
104
|
+
* Opaque FF / wasFastForward always win as blitz.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveStepDemoPace(opts: {
|
|
107
|
+
blockPace?: DemoPace | string | null;
|
|
108
|
+
flowPace?: DemoPace | string | null;
|
|
109
|
+
fastForward?: boolean;
|
|
110
|
+
wasFastForward?: boolean;
|
|
111
|
+
}): DemoPace;
|
|
72
112
|
export type WaygraphSlidesOrFn<Out extends Checkpoint<string>> = readonly WaygraphSlide[] | ((out: Out) => readonly WaygraphSlide[]);
|
|
73
113
|
/** Demo stub phase names (block instruction + flow fixtures). */
|
|
74
114
|
export type HighlightStubPhaseName = "stubBefore" | "stubAfter" | "stubOnError";
|
|
@@ -85,6 +125,7 @@ export type HighlightFixtureMap = Record<string, {
|
|
|
85
125
|
export interface WaygraphHighlight {
|
|
86
126
|
selector: string;
|
|
87
127
|
label: string;
|
|
128
|
+
tone?: HighlightTone;
|
|
88
129
|
}
|
|
89
130
|
export type ResolvedHighlight = WaygraphHighlightStub;
|
|
90
131
|
/**
|
|
@@ -104,12 +145,13 @@ export declare function resolveSlides(block: Block<any, any>, opts?: {
|
|
|
104
145
|
out?: Checkpoint<string>;
|
|
105
146
|
fixtures?: HighlightFixtureMap;
|
|
106
147
|
}): WaygraphSlide[];
|
|
107
|
-
/** Caption for ring / slide UI (label|caption + optional detail/tag). */
|
|
148
|
+
/** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
|
|
108
149
|
export declare function formatHighlightCaption(h: {
|
|
109
150
|
label?: string;
|
|
110
151
|
caption?: string;
|
|
111
152
|
detail?: string;
|
|
112
153
|
tag?: string;
|
|
154
|
+
tone?: HighlightTone | string;
|
|
113
155
|
}): string;
|
|
114
156
|
/**
|
|
115
157
|
* True when the block authored a non-empty stubAfter (after resolve + shim),
|
package/dist/highlights.js
CHANGED
|
@@ -8,10 +8,56 @@
|
|
|
8
8
|
* All fixture shapes share optional {@link FixtureDurationFields} so demo dwell
|
|
9
9
|
* before advancing is authored the same way on stubs, flow fixtures, and slides.
|
|
10
10
|
*/
|
|
11
|
+
const TONE_ICONS = {
|
|
12
|
+
planned: "",
|
|
13
|
+
auto: "",
|
|
14
|
+
info: "i",
|
|
15
|
+
warning: "!",
|
|
16
|
+
danger: "x",
|
|
17
|
+
success: "+",
|
|
18
|
+
};
|
|
19
|
+
/** Map author aliases (`error` -> danger) to a canonical tone. */
|
|
20
|
+
export function normalizeHighlightTone(raw) {
|
|
21
|
+
if (typeof raw !== "string")
|
|
22
|
+
return "planned";
|
|
23
|
+
const t = raw.trim().toLowerCase();
|
|
24
|
+
if (t === "auto" || t === "automation" || t === "engine")
|
|
25
|
+
return "auto";
|
|
26
|
+
if (t === "info" || t === "blue")
|
|
27
|
+
return "info";
|
|
28
|
+
if (t === "warning" || t === "warn" || t === "yellow")
|
|
29
|
+
return "warning";
|
|
30
|
+
if (t === "danger" || t === "error" || t === "red")
|
|
31
|
+
return "danger";
|
|
32
|
+
if (t === "success" || t === "ok" || t === "green")
|
|
33
|
+
return "success";
|
|
34
|
+
if (t === "planned" || t === "purple" || t === "authored")
|
|
35
|
+
return "planned";
|
|
36
|
+
return "planned";
|
|
37
|
+
}
|
|
38
|
+
/** Prefix label with a short ASCII icon for semantic tones. */
|
|
39
|
+
export function toneIconPrefix(tone) {
|
|
40
|
+
const icon = TONE_ICONS[tone];
|
|
41
|
+
return icon ? `[${icon}] ` : "";
|
|
42
|
+
}
|
|
43
|
+
export function normalizeDemoPace(raw) {
|
|
44
|
+
if (typeof raw !== "string")
|
|
45
|
+
return "normal";
|
|
46
|
+
const t = raw.trim().toLowerCase();
|
|
47
|
+
if (t === "blitz" || t === "ff")
|
|
48
|
+
return "blitz";
|
|
49
|
+
if (t === "fast" || t === "quick")
|
|
50
|
+
return "fast";
|
|
51
|
+
if (t === "slow" || t === "careful" || t === "review")
|
|
52
|
+
return "slow";
|
|
53
|
+
return "normal";
|
|
54
|
+
}
|
|
11
55
|
/** Default fixture dwell when `duration: true`. */
|
|
12
56
|
export const FIXTURE_DURATION_MS_DEFAULT = 2000;
|
|
13
57
|
/** Default fixture dwell under `--fast` when duration is enabled. */
|
|
14
58
|
export const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
|
|
59
|
+
/** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
|
|
60
|
+
export const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
|
|
15
61
|
/** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
|
|
16
62
|
export const SLIDE_DURATION_MS_DEFAULT = FIXTURE_DURATION_MS_DEFAULT;
|
|
17
63
|
/** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
|
|
@@ -23,23 +69,40 @@ export const SLIDE_DURATION_FAST_MS_DEFAULT = FIXTURE_DURATION_FAST_MS_DEFAULT;
|
|
|
23
69
|
export function resolveFixtureDwellMs(fixture, opts) {
|
|
24
70
|
if (fixture.duration === undefined || fixture.duration === false)
|
|
25
71
|
return null;
|
|
26
|
-
const
|
|
72
|
+
const pace = normalizeDemoPace(opts?.pace ?? (opts?.gatesFast ? "fast" : "normal"));
|
|
27
73
|
const fastMs = typeof fixture.fastMode === "number" && Number.isFinite(fixture.fastMode) && fixture.fastMode >= 0
|
|
28
74
|
? fixture.fastMode
|
|
29
75
|
: FIXTURE_DURATION_FAST_MS_DEFAULT;
|
|
30
|
-
if (fast)
|
|
76
|
+
if (pace === "blitz" || pace === "fast" || opts?.gatesFast)
|
|
31
77
|
return fastMs;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
const normalMs = fixture.duration === true
|
|
79
|
+
? FIXTURE_DURATION_MS_DEFAULT
|
|
80
|
+
: typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0
|
|
81
|
+
? fixture.duration
|
|
82
|
+
: FIXTURE_DURATION_MS_DEFAULT;
|
|
83
|
+
if (pace === "slow")
|
|
84
|
+
return Math.max(normalMs, FIXTURE_DURATION_SLOW_MS_DEFAULT);
|
|
85
|
+
return normalMs;
|
|
38
86
|
}
|
|
39
87
|
/** @deprecated Prefer {@link resolveFixtureDwellMs} */
|
|
40
88
|
export function resolveSlideDwellMs(slide, opts) {
|
|
41
89
|
return resolveFixtureDwellMs(slide, opts);
|
|
42
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Effective demo pace for a step: block override > flow episode > normal.
|
|
93
|
+
* Opaque FF / wasFastForward always win as blitz.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveStepDemoPace(opts) {
|
|
96
|
+
if (opts.fastForward || opts.wasFastForward)
|
|
97
|
+
return "blitz";
|
|
98
|
+
if (opts.blockPace != null && opts.blockPace !== "") {
|
|
99
|
+
return normalizeDemoPace(opts.blockPace);
|
|
100
|
+
}
|
|
101
|
+
if (opts.flowPace != null && opts.flowPace !== "") {
|
|
102
|
+
return normalizeDemoPace(opts.flowPace);
|
|
103
|
+
}
|
|
104
|
+
return "normal";
|
|
105
|
+
}
|
|
43
106
|
function isStubPhase(v) {
|
|
44
107
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
45
108
|
}
|
|
@@ -65,6 +128,7 @@ function mergeSlot(base, patch) {
|
|
|
65
128
|
label: patch.label,
|
|
66
129
|
...(patch.detail ? { detail: patch.detail } : {}),
|
|
67
130
|
...(patch.tag ? { tag: patch.tag } : {}),
|
|
131
|
+
...(patch.tone ? { tone: normalizeHighlightTone(patch.tone) } : {}),
|
|
68
132
|
...dwell,
|
|
69
133
|
};
|
|
70
134
|
}
|
|
@@ -79,6 +143,11 @@ function mergeSlot(base, patch) {
|
|
|
79
143
|
? { detail: base.detail }
|
|
80
144
|
: {}),
|
|
81
145
|
...(patch.tag !== undefined ? { tag: patch.tag } : base.tag ? { tag: base.tag } : {}),
|
|
146
|
+
...(patch.tone !== undefined
|
|
147
|
+
? { tone: normalizeHighlightTone(patch.tone) }
|
|
148
|
+
: base.tone
|
|
149
|
+
? { tone: normalizeHighlightTone(base.tone) }
|
|
150
|
+
: {}),
|
|
82
151
|
...dwell,
|
|
83
152
|
};
|
|
84
153
|
}
|
|
@@ -88,7 +157,11 @@ function phaseFromLegacyHighlights(highlights) {
|
|
|
88
157
|
return out;
|
|
89
158
|
highlights.forEach((h, i) => {
|
|
90
159
|
if (h && typeof h.selector === "string" && typeof h.label === "string") {
|
|
91
|
-
out[String(i)] = {
|
|
160
|
+
out[String(i)] = {
|
|
161
|
+
selector: h.selector,
|
|
162
|
+
label: h.label,
|
|
163
|
+
...(h.tone ? { tone: normalizeHighlightTone(h.tone) } : {}),
|
|
164
|
+
};
|
|
92
165
|
}
|
|
93
166
|
});
|
|
94
167
|
return out;
|
|
@@ -169,13 +242,15 @@ export function resolveSlides(block, opts) {
|
|
|
169
242
|
return [];
|
|
170
243
|
return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
|
|
171
244
|
}
|
|
172
|
-
/** Caption for ring / slide UI (label|caption + optional detail/tag). */
|
|
245
|
+
/** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
|
|
173
246
|
export function formatHighlightCaption(h) {
|
|
174
|
-
const
|
|
247
|
+
const tone = normalizeHighlightTone(h.tone);
|
|
248
|
+
const icon = toneIconPrefix(tone);
|
|
249
|
+
const primary = (h.label ?? h.caption ?? "").trim();
|
|
175
250
|
const tag = h.tag ? `[${h.tag}] ` : "";
|
|
176
251
|
if (h.detail)
|
|
177
|
-
return `${tag}${primary} - ${h.detail}`;
|
|
178
|
-
return `${tag}${primary}`;
|
|
252
|
+
return `${icon}${tag}${primary} - ${h.detail}`;
|
|
253
|
+
return `${icon}${tag}${primary}`;
|
|
179
254
|
}
|
|
180
255
|
/**
|
|
181
256
|
* True when the block authored a non-empty stubAfter (after resolve + shim),
|
package/dist/index.d.ts
CHANGED
|
@@ -2,12 +2,15 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
|
|
|
2
2
|
export { connect, checkpoint } from "./types.js";
|
|
3
3
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
4
4
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
5
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
6
|
-
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";
|
|
7
|
-
export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, } from "./highlights.js";
|
|
5
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
6
|
+
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
|
|
7
|
+
export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, DemoPace, } from "./highlights.js";
|
|
8
8
|
export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
|
|
9
9
|
export type { ActionPage } from "./types.js";
|
|
10
10
|
export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { connect, checkpoint } from "./types.js";
|
|
2
2
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
3
3
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
4
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
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";
|
|
4
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
5
|
+
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, toneIconPrefix, resolveStepDemoPace, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_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";
|
package/dist/step-overlay.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { MemPage } from "./mem-page.js";
|
|
|
3
3
|
export declare function installDemoChrome(page: Page, title?: string, options?: {
|
|
4
4
|
banner?: boolean;
|
|
5
5
|
}): Promise<void>;
|
|
6
|
-
/** Demo cursor + ring on fill/click during auto explore headful runs (automation =
|
|
6
|
+
/** Demo cursor + ring on fill/click during auto explore headful runs (automation = gray). */
|
|
7
7
|
export declare function instrumentInteractionHighlighting(page: Page, mem: MemPage, slowMo: number): void;
|
|
8
8
|
declare global {
|
|
9
9
|
interface Window {
|
package/dist/step-overlay.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
2
2
|
"border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
|
|
3
3
|
"transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
|
|
4
|
-
|
|
4
|
+
// Automation = gray; semantic tones for authored highlights.
|
|
5
|
+
"#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
|
|
6
|
+
"#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
|
|
7
|
+
"#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
|
|
8
|
+
"#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
|
|
9
|
+
"#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
|
|
5
10
|
"#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
6
11
|
"white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
|
|
7
12
|
"font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
|
|
8
|
-
"#wg-ring-label[data-tone=auto]{background:#
|
|
13
|
+
"#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
|
|
14
|
+
"#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
|
|
15
|
+
"#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
|
|
16
|
+
"#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
|
|
17
|
+
"#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
|
|
9
18
|
"#wg-cursor{position:fixed;z-index:2147483647;width:24px;height:24px;pointer-events:none;" +
|
|
10
19
|
"left:0;top:0;opacity:0;margin:0;" +
|
|
11
20
|
"transition:transform var(--wg-cursor-ms,600ms) cubic-bezier(.22,1,.36,1),opacity .2s ease;" +
|
|
@@ -13,7 +22,11 @@ const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none
|
|
|
13
22
|
"#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
|
|
14
23
|
"margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
|
|
15
24
|
"border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
|
|
16
|
-
"#wg-click-pulse[data-tone=auto]{border-color:#
|
|
25
|
+
"#wg-click-pulse[data-tone=auto]{border-color:#9CA3AF;background:rgba(156,163,175,.28);}" +
|
|
26
|
+
"#wg-click-pulse[data-tone=info]{border-color:#3B82F6;background:rgba(59,130,246,.28);}" +
|
|
27
|
+
"#wg-click-pulse[data-tone=warning]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
|
|
28
|
+
"#wg-click-pulse[data-tone=danger]{border-color:#EF4444;background:rgba(239,68,68,.28);}" +
|
|
29
|
+
"#wg-click-pulse[data-tone=success]{border-color:#22C55E;background:rgba(34,197,94,.28);}" +
|
|
17
30
|
"#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
|
|
18
31
|
"@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
|
|
19
32
|
"#wg-banner{position:fixed;z-index:2147483647;top:14px;left:14px;" +
|
|
@@ -65,7 +78,15 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
65
78
|
const pulse = document.getElementById("wg-click-pulse");
|
|
66
79
|
if (!pulse)
|
|
67
80
|
return;
|
|
68
|
-
|
|
81
|
+
const raw = (tone || "planned") + "";
|
|
82
|
+
pulse.dataset.tone =
|
|
83
|
+
raw === "auto" ||
|
|
84
|
+
raw === "info" ||
|
|
85
|
+
raw === "warning" ||
|
|
86
|
+
raw === "danger" ||
|
|
87
|
+
raw === "success"
|
|
88
|
+
? raw
|
|
89
|
+
: "planned";
|
|
69
90
|
pulse.style.left = x + "px";
|
|
70
91
|
pulse.style.top = y + "px";
|
|
71
92
|
pulse.classList.remove("wg-pulse");
|
|
@@ -77,7 +98,14 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
77
98
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
78
99
|
if (!ring || !ringLabel)
|
|
79
100
|
return;
|
|
80
|
-
const
|
|
101
|
+
const raw = (tone || "planned") + "";
|
|
102
|
+
const t = raw === "auto" ||
|
|
103
|
+
raw === "info" ||
|
|
104
|
+
raw === "warning" ||
|
|
105
|
+
raw === "danger" ||
|
|
106
|
+
raw === "success"
|
|
107
|
+
? raw
|
|
108
|
+
: "planned";
|
|
81
109
|
ring.dataset.tone = t;
|
|
82
110
|
ringLabel.dataset.tone = t;
|
|
83
111
|
ring.style.left = box.x - 6 + "px";
|
|
@@ -136,7 +164,7 @@ async function clickPulseAt(page, x, y, tone = "auto") {
|
|
|
136
164
|
.evaluate(({ x, y, tone }) => window.__wgClickPulse?.(x, y, tone), { x, y, tone })
|
|
137
165
|
.catch(() => { });
|
|
138
166
|
}
|
|
139
|
-
/** Demo cursor + ring on fill/click during auto explore headful runs (automation =
|
|
167
|
+
/** Demo cursor + ring on fill/click during auto explore headful runs (automation = gray). */
|
|
140
168
|
export function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
141
169
|
const typeDelay = () => (slowMo ? 0 : 30);
|
|
142
170
|
const clickPrePop = () => (slowMo ? 300 : 700);
|
|
@@ -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
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -209,6 +209,11 @@ export interface Block<In extends Checkpoint<string>, Out extends Checkpoint<str
|
|
|
209
209
|
* @example requires: [LoginCreds.key]
|
|
210
210
|
*/
|
|
211
211
|
requires?: readonly MemKey<any>[];
|
|
212
|
+
/**
|
|
213
|
+
* Demo pacing for this Block or compose group (`withBlockPace`).
|
|
214
|
+
* Demo/step only - `runGraph` ignores. Prefer `fastForwardComposeBlock` for blitz.
|
|
215
|
+
*/
|
|
216
|
+
demoPace?: import("./highlights.js").DemoPace;
|
|
212
217
|
/**
|
|
213
218
|
* Optional. When this Block's real choices depend on the live page
|
|
214
219
|
* (which of several items to add to cart, which of several rows to
|
|
@@ -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
|
+
}
|