waygraph 0.12.7 → 0.12.9
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/dist/auto-explore-run.js +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +150 -41
- package/dist/engine.d.ts +42 -0
- package/dist/engine.js +1 -1
- package/dist/step-overlay.d.ts +3 -3
- package/dist/step-overlay.js +60 -52
- package/dist/traverse-run.d.ts +24 -0
- package/dist/traverse-run.js +227 -0
- package/package.json +1 -1
package/dist/auto-explore-run.js
CHANGED
|
@@ -428,6 +428,7 @@ async function headfulPick(page, menu, library, mem, waitPick) {
|
|
|
428
428
|
window.__wgPositionRing(
|
|
429
429
|
{ x: r.x, y: r.y, width: r.width, height: r.height },
|
|
430
430
|
btn.textContent?.trim()?.slice(0, 80) || "target",
|
|
431
|
+
"auto",
|
|
431
432
|
);
|
|
432
433
|
});
|
|
433
434
|
btn.addEventListener("mouseleave", () => {
|
package/dist/cli.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
|
-
* Also: list / nav / validate / check / graph / init / agent-dive / try
|
|
10
|
+
* Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
|
|
11
11
|
* Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
|
|
12
12
|
*
|
|
13
13
|
* "project" defaults to cwd. Flags beat WAYGRAPH_* env.
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
|
-
* Also: list / nav / validate / check / graph / init / agent-dive / try
|
|
10
|
+
* Also: list / nav / validate / check / graph / init / agent-dive / traverse / try
|
|
11
11
|
* Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
|
|
12
12
|
*
|
|
13
13
|
* "project" defaults to cwd. Flags beat WAYGRAPH_* env.
|
|
@@ -20,6 +20,7 @@ import { spawn } from "node:child_process";
|
|
|
20
20
|
import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
21
21
|
import { runAutoExplore } from "./auto-explore-run.js";
|
|
22
22
|
import { runAgentDive } from "./agent-dive.js";
|
|
23
|
+
import { runTraverse } from "./traverse-run.js";
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Filesystem
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -395,7 +396,11 @@ function seedMemForFlow(mem, flowBlocks, json, label) {
|
|
|
395
396
|
const RING_CSS =
|
|
396
397
|
"#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
397
398
|
"border:2.5px solid #7C3AED;border-radius:10px;" +
|
|
398
|
-
"box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
|
|
399
|
+
"box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
|
|
400
|
+
// Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
|
|
401
|
+
// Automation (verify fallback, unmatched fill/click) = yellow - so operators
|
|
402
|
+
// can ignore engine checks and only watch purple narration.
|
|
403
|
+
"#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
|
|
399
404
|
// A real element, not a ::after pseudo-element - a pseudo-element's
|
|
400
405
|
// position is CSS-relative to the ring's own box (left:0 always meant
|
|
401
406
|
// "the ring's own left edge"), so it had no way to be clamped back onto
|
|
@@ -406,7 +411,8 @@ const RING_CSS =
|
|
|
406
411
|
// window.__wgPositionRing in installOverlay.
|
|
407
412
|
"#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
408
413
|
"max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
|
|
409
|
-
"font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
|
|
414
|
+
"font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
|
|
415
|
+
"#wg-ring-label[data-tone=auto]{background:#EAB308;color:#1c1917;}" +
|
|
410
416
|
// Mouse cursor icon that travels to a target before it's acted on, plus a
|
|
411
417
|
// quick expanding ripple at the moment of a click - same idea as
|
|
412
418
|
// help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
|
|
@@ -424,6 +430,7 @@ const RING_CSS =
|
|
|
424
430
|
"#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
|
|
425
431
|
"margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
|
|
426
432
|
"border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
|
|
433
|
+
"#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
|
|
427
434
|
"#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
|
|
428
435
|
"@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
|
|
429
436
|
"#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
|
|
@@ -639,26 +646,13 @@ async function installOverlay(page, title) {
|
|
|
639
646
|
const cursor = document.getElementById("wg-cursor");
|
|
640
647
|
if (cursor) cursor.style.opacity = "0";
|
|
641
648
|
};
|
|
642
|
-
window.
|
|
643
|
-
const pulse = document.getElementById("wg-click-pulse");
|
|
644
|
-
if (!pulse) return;
|
|
645
|
-
pulse.style.left = x + "px";
|
|
646
|
-
pulse.style.top = y + "px";
|
|
647
|
-
pulse.classList.remove("wg-pulse");
|
|
648
|
-
void pulse.offsetWidth;
|
|
649
|
-
pulse.classList.add("wg-pulse");
|
|
650
|
-
};
|
|
651
|
-
// Clamps the ring AND its label to stay fully on-screen, same
|
|
652
|
-
// width/height always - only the position moves. A target near a
|
|
653
|
-
// viewport edge (real case: "login success" highlight landing top
|
|
654
|
-
// right) used to just run the label text off-screen with no guard
|
|
655
|
-
// at all - same mistake as the banner had before its own
|
|
656
|
-
// left/center/right positions existed, not repeating it here by
|
|
657
|
-
// shrinking anything, only repositioning.
|
|
658
|
-
window.__wgPositionRing = (box, label) => {
|
|
649
|
+
window.__wgPositionRing = (box, label, tone) => {
|
|
659
650
|
const ring = document.getElementById("wg-ring");
|
|
660
651
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
661
652
|
if (!ring || !ringLabel) return;
|
|
653
|
+
const t = tone === "auto" ? "auto" : "planned";
|
|
654
|
+
ring.dataset.tone = t;
|
|
655
|
+
ringLabel.dataset.tone = t;
|
|
662
656
|
const margin = 6;
|
|
663
657
|
const vw = window.innerWidth;
|
|
664
658
|
const vh = window.innerHeight;
|
|
@@ -690,6 +684,16 @@ async function installOverlay(page, title) {
|
|
|
690
684
|
ringLabel.style.left = labelLeft + "px";
|
|
691
685
|
ringLabel.style.top = labelTop + "px";
|
|
692
686
|
};
|
|
687
|
+
window.__wgClickPulse = (x, y, tone) => {
|
|
688
|
+
const pulse = document.getElementById("wg-click-pulse");
|
|
689
|
+
if (!pulse) return;
|
|
690
|
+
pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
|
|
691
|
+
pulse.style.left = x + "px";
|
|
692
|
+
pulse.style.top = y + "px";
|
|
693
|
+
pulse.classList.remove("wg-pulse");
|
|
694
|
+
void pulse.offsetWidth;
|
|
695
|
+
pulse.classList.add("wg-pulse");
|
|
696
|
+
};
|
|
693
697
|
window.__wgHideRing = () => {
|
|
694
698
|
// While a narrate() call owns the ring (mid multi-step action),
|
|
695
699
|
// the auto-highlight click/fill patches' own end-of-step hide is
|
|
@@ -1030,7 +1034,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
1030
1034
|
try {
|
|
1031
1035
|
const box = await page.locator(h.selector).first().boundingBox();
|
|
1032
1036
|
if (box) {
|
|
1033
|
-
await showRing(page, box, h.label);
|
|
1037
|
+
await showRing(page, box, h.label, h.tone || "planned");
|
|
1034
1038
|
const authored = resolveFixtureDwellMs(h, { gatesFast });
|
|
1035
1039
|
const legacyMs = i === list.length - 1 ? 200 : 900;
|
|
1036
1040
|
const holdMs =
|
|
@@ -1052,7 +1056,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
1052
1056
|
if (window.__wgHideRing) window.__wgHideRing();
|
|
1053
1057
|
return;
|
|
1054
1058
|
}
|
|
1055
|
-
window.__wgPositionRing(el.getBoundingClientRect(), h.label);
|
|
1059
|
+
window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned");
|
|
1056
1060
|
};
|
|
1057
1061
|
reposition();
|
|
1058
1062
|
window.__wgRingTrack = reposition;
|
|
@@ -1296,7 +1300,10 @@ function resolveDeclaredHighlights(block, resultTag) {
|
|
|
1296
1300
|
}
|
|
1297
1301
|
}
|
|
1298
1302
|
if (!Array.isArray(highlights)) return [];
|
|
1299
|
-
return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string")
|
|
1303
|
+
return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string").map((h) => ({
|
|
1304
|
+
...h,
|
|
1305
|
+
tone: h.tone === "auto" ? "auto" : "planned",
|
|
1306
|
+
}));
|
|
1300
1307
|
}
|
|
1301
1308
|
|
|
1302
1309
|
/**
|
|
@@ -1326,12 +1333,12 @@ function extractVerifyHighlights(block, resultTag) {
|
|
|
1326
1333
|
if (typeof name !== "string") continue;
|
|
1327
1334
|
let m = /^visible\\((.+)\\)$/.exec(name);
|
|
1328
1335
|
if (m) {
|
|
1329
|
-
highlights.push({ selector: m[1], label: name });
|
|
1336
|
+
highlights.push({ selector: m[1], label: name, tone: "auto" });
|
|
1330
1337
|
continue;
|
|
1331
1338
|
}
|
|
1332
1339
|
m = /^text-equals\\((.+?),\\s*"/.exec(name);
|
|
1333
1340
|
if (m) {
|
|
1334
|
-
highlights.push({ selector: m[1], label: name });
|
|
1341
|
+
highlights.push({ selector: m[1], label: name, tone: "auto" });
|
|
1335
1342
|
continue;
|
|
1336
1343
|
}
|
|
1337
1344
|
}
|
|
@@ -1394,13 +1401,13 @@ async function markStepRunning(page, _opts) {
|
|
|
1394
1401
|
.catch(() => {});
|
|
1395
1402
|
}
|
|
1396
1403
|
|
|
1397
|
-
async function showRing(page, box, label) {
|
|
1404
|
+
async function showRing(page, box, label, tone) {
|
|
1398
1405
|
await page
|
|
1399
1406
|
.evaluate(
|
|
1400
|
-
({ box, label }) => {
|
|
1401
|
-
if (window.__wgPositionRing) window.__wgPositionRing(box, label);
|
|
1407
|
+
({ box, label, tone }) => {
|
|
1408
|
+
if (window.__wgPositionRing) window.__wgPositionRing(box, label, tone || "planned");
|
|
1402
1409
|
},
|
|
1403
|
-
{ box, label },
|
|
1410
|
+
{ box, label, tone: tone || "planned" },
|
|
1404
1411
|
)
|
|
1405
1412
|
.catch(() => {});
|
|
1406
1413
|
}
|
|
@@ -1430,8 +1437,8 @@ async function matchStubForLocator(page, locator, stubs) {
|
|
|
1430
1437
|
|
|
1431
1438
|
async function captionForLocator(page, locator, stubs, fallback) {
|
|
1432
1439
|
const matched = await matchStubForLocator(page, locator, stubs);
|
|
1433
|
-
if (matched) return formatHighlightCaption(matched);
|
|
1434
|
-
return fallback;
|
|
1440
|
+
if (matched) return { label: formatHighlightCaption(matched), tone: "planned" };
|
|
1441
|
+
return { label: fallback, tone: "auto" };
|
|
1435
1442
|
}
|
|
1436
1443
|
|
|
1437
1444
|
async function dwellMatchedStub(page, locator, stubs, pacing) {
|
|
@@ -1473,7 +1480,7 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1473
1480
|
if (s.selector) {
|
|
1474
1481
|
try {
|
|
1475
1482
|
const box = await page.locator(s.selector).first().boundingBox();
|
|
1476
|
-
if (box) await showRing(page, box, caption);
|
|
1483
|
+
if (box) await showRing(page, box, caption, "planned");
|
|
1477
1484
|
} catch {
|
|
1478
1485
|
await hideRing(page);
|
|
1479
1486
|
}
|
|
@@ -1704,11 +1711,11 @@ async function moveCursorTo(page, box, ms) {
|
|
|
1704
1711
|
return { x, y };
|
|
1705
1712
|
}
|
|
1706
1713
|
|
|
1707
|
-
async function clickPulseAt(page, x, y) {
|
|
1714
|
+
async function clickPulseAt(page, x, y, tone) {
|
|
1708
1715
|
await page
|
|
1709
|
-
.evaluate(({ x, y }) => {
|
|
1710
|
-
if (window.__wgClickPulse) window.__wgClickPulse(x, y);
|
|
1711
|
-
}, { x, y })
|
|
1716
|
+
.evaluate(({ x, y, tone }) => {
|
|
1717
|
+
if (window.__wgClickPulse) window.__wgClickPulse(x, y, tone || "planned");
|
|
1718
|
+
}, { x, y, tone: tone || "planned" })
|
|
1712
1719
|
.catch(() => {});
|
|
1713
1720
|
}
|
|
1714
1721
|
|
|
@@ -1782,9 +1789,9 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1782
1789
|
? "from mem: " + memTrack.lastKeyName
|
|
1783
1790
|
: "writing from mem";
|
|
1784
1791
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
1785
|
-
const
|
|
1792
|
+
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
1786
1793
|
await moveCursorTo(page, box, cursorMs(500));
|
|
1787
|
-
await showRing(page, box, label);
|
|
1794
|
+
await showRing(page, box, cap.label, cap.tone);
|
|
1788
1795
|
await dwellMatchedStub(page, this, stubs, pacing);
|
|
1789
1796
|
await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 200));
|
|
1790
1797
|
} else if (box) {
|
|
@@ -1823,6 +1830,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1823
1830
|
const originalClick = proto.click;
|
|
1824
1831
|
proto.click = async function (options) {
|
|
1825
1832
|
let clickPoint = null;
|
|
1833
|
+
let clickTone = "auto";
|
|
1826
1834
|
try {
|
|
1827
1835
|
await installOverlay(page);
|
|
1828
1836
|
// Wait until the target is visible BEFORE measuring - otherwise
|
|
@@ -1852,9 +1860,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1852
1860
|
/* ignore */
|
|
1853
1861
|
}
|
|
1854
1862
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
1855
|
-
const
|
|
1863
|
+
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
1864
|
+
clickTone = cap.tone;
|
|
1856
1865
|
clickPoint = await moveCursorTo(page, box, cursorMs(600));
|
|
1857
|
-
await showRing(page, box, label);
|
|
1866
|
+
await showRing(page, box, cap.label, cap.tone);
|
|
1858
1867
|
await dwellMatchedStub(page, this, stubs, pacing);
|
|
1859
1868
|
// "pop for a few seconds" - Dan's own phrase, matching the
|
|
1860
1869
|
// zsign demo-engine's ring-before-click pattern in
|
|
@@ -1864,12 +1873,13 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1864
1873
|
// Ring/caption already handled by narrate() - still move the
|
|
1865
1874
|
// cursor + pulse the click point, just skip re-showing the ring.
|
|
1866
1875
|
clickPoint = await moveCursorTo(page, box, cursorMs(200));
|
|
1876
|
+
clickTone = "planned";
|
|
1867
1877
|
}
|
|
1868
1878
|
} catch {
|
|
1869
1879
|
// best-effort - the real click below still runs either way
|
|
1870
1880
|
}
|
|
1871
1881
|
if (clickPoint) {
|
|
1872
|
-
await clickPulseAt(page, clickPoint.x, clickPoint.y);
|
|
1882
|
+
await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
|
|
1873
1883
|
// clickPulseAt only triggers the CSS animation class - it doesn't
|
|
1874
1884
|
// wait for it. Without a pause here, the real click (and any
|
|
1875
1885
|
// resulting navigation/DOM change) fires while the ripple is still
|
|
@@ -2256,6 +2266,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2256
2266
|
label: formatHighlightCaption(h),
|
|
2257
2267
|
duration: h.duration,
|
|
2258
2268
|
fastMode: h.fastMode,
|
|
2269
|
+
tone: "planned",
|
|
2259
2270
|
}));
|
|
2260
2271
|
await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
|
|
2261
2272
|
defaultHoldMs: 2000,
|
|
@@ -2328,6 +2339,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2328
2339
|
label: formatHighlightCaption(h),
|
|
2329
2340
|
duration: h.duration,
|
|
2330
2341
|
fastMode: h.fastMode,
|
|
2342
|
+
tone: "planned",
|
|
2331
2343
|
}),
|
|
2332
2344
|
);
|
|
2333
2345
|
} else {
|
|
@@ -3661,6 +3673,12 @@ Primary (less is more):
|
|
|
3661
3673
|
|
|
3662
3674
|
Also:
|
|
3663
3675
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3676
|
+
waygraph traverse [project] Serial graph crawl (Phase B)
|
|
3677
|
+
--from <Checkpoint> Seed / start checkpoint
|
|
3678
|
+
--data '{...}' Mem seed
|
|
3679
|
+
--max-steps N Cap Block runs (default 50)
|
|
3680
|
+
--max-visits N Cap visits per node (default 2)
|
|
3681
|
+
--non-headless Show browser
|
|
3664
3682
|
waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts]
|
|
3665
3683
|
Initialize coding-agent defs (Playwright init-agents analogue)
|
|
3666
3684
|
waygraph try [demo|auto|auto:cli] One-shot saucedemo in a temp dir
|
|
@@ -3759,6 +3777,97 @@ async function main() {
|
|
|
3759
3777
|
initCommand(args[1] ?? "");
|
|
3760
3778
|
break;
|
|
3761
3779
|
}
|
|
3780
|
+
case "traverse": {
|
|
3781
|
+
if (args.includes("--step") || args.includes("--auto-next") || args.includes("--autoplay")) {
|
|
3782
|
+
console.error("waygraph traverse: --step / --auto-next not supported (not a demo)");
|
|
3783
|
+
process.exit(1);
|
|
3784
|
+
}
|
|
3785
|
+
const rest = args.slice(1);
|
|
3786
|
+
let from;
|
|
3787
|
+
let data;
|
|
3788
|
+
let maxSteps;
|
|
3789
|
+
let maxVisits;
|
|
3790
|
+
let maxEdge;
|
|
3791
|
+
let headed = false;
|
|
3792
|
+
let baseUrl;
|
|
3793
|
+
let projectDir = process.cwd();
|
|
3794
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3795
|
+
const a = rest[i];
|
|
3796
|
+
if (a === "--from" || a.startsWith("--from=")) {
|
|
3797
|
+
from = a.startsWith("--from=") ? a.slice(7) : rest[++i];
|
|
3798
|
+
continue;
|
|
3799
|
+
}
|
|
3800
|
+
if (a === "--data" || a.startsWith("--data=")) {
|
|
3801
|
+
data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
|
|
3802
|
+
continue;
|
|
3803
|
+
}
|
|
3804
|
+
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
3805
|
+
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
3806
|
+
maxSteps = Number(v);
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
if (a === "--max-visits" || a.startsWith("--max-visits=")) {
|
|
3810
|
+
const v = a.startsWith("--max-visits=") ? a.slice(13) : rest[++i];
|
|
3811
|
+
maxVisits = Number(v);
|
|
3812
|
+
continue;
|
|
3813
|
+
}
|
|
3814
|
+
if (a === "--max-visits-per-edge" || a.startsWith("--max-visits-per-edge=")) {
|
|
3815
|
+
const v = a.startsWith("--max-visits-per-edge=")
|
|
3816
|
+
? a.slice("--max-visits-per-edge=".length)
|
|
3817
|
+
: rest[++i];
|
|
3818
|
+
maxEdge = Number(v);
|
|
3819
|
+
continue;
|
|
3820
|
+
}
|
|
3821
|
+
if (a === "--non-headless" || a === "--headed") {
|
|
3822
|
+
headed = true;
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
if (a === "--base-url" || a.startsWith("--base-url=")) {
|
|
3826
|
+
baseUrl = a.startsWith("--base-url=") ? a.slice(11) : rest[++i];
|
|
3827
|
+
continue;
|
|
3828
|
+
}
|
|
3829
|
+
if (a === "--help" || a === "-h") {
|
|
3830
|
+
console.log(`waygraph traverse [project] [flags]
|
|
3831
|
+
|
|
3832
|
+
Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
|
|
3833
|
+
budget kill, or first broken edge.
|
|
3834
|
+
|
|
3835
|
+
--from <Checkpoint> start checkpoint (optional)
|
|
3836
|
+
--data '{...}' Mem seed JSON
|
|
3837
|
+
--max-steps N default 50
|
|
3838
|
+
--max-visits N per-node visit cap (default 2)
|
|
3839
|
+
--max-visits-per-edge N default 1
|
|
3840
|
+
--non-headless show browser
|
|
3841
|
+
--base-url URL
|
|
3842
|
+
|
|
3843
|
+
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
3844
|
+
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
3845
|
+
`);
|
|
3846
|
+
process.exit(0);
|
|
3847
|
+
}
|
|
3848
|
+
if (!a.startsWith("-")) {
|
|
3849
|
+
projectDir = resolve(a);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
3853
|
+
console.error(`waygraph traverse: no such directory: ${projectDir}`);
|
|
3854
|
+
process.exit(1);
|
|
3855
|
+
}
|
|
3856
|
+
const code = await runTraverse(projectDir, {
|
|
3857
|
+
...(from ? { from } : {}),
|
|
3858
|
+
...(data ? { data } : {}),
|
|
3859
|
+
...(maxSteps !== undefined && Number.isFinite(maxSteps) ? { maxSteps } : {}),
|
|
3860
|
+
...(maxVisits !== undefined && Number.isFinite(maxVisits)
|
|
3861
|
+
? { maxVisitsPerNode: maxVisits }
|
|
3862
|
+
: {}),
|
|
3863
|
+
...(maxEdge !== undefined && Number.isFinite(maxEdge)
|
|
3864
|
+
? { maxVisitsPerEdge: maxEdge }
|
|
3865
|
+
: {}),
|
|
3866
|
+
headed,
|
|
3867
|
+
...(baseUrl ? { baseURL: baseUrl } : {}),
|
|
3868
|
+
});
|
|
3869
|
+
process.exit(code);
|
|
3870
|
+
}
|
|
3762
3871
|
case "agent-dive":
|
|
3763
3872
|
case "init-agents": {
|
|
3764
3873
|
// Playwright analogue: npx playwright init-agents --loop <provider>
|
package/dist/engine.d.ts
CHANGED
|
@@ -467,6 +467,48 @@ export declare class Engine {
|
|
|
467
467
|
Block<H, I>,
|
|
468
468
|
EndMarker
|
|
469
469
|
]): Flow<I>;
|
|
470
|
+
defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>>(blocks: readonly [
|
|
471
|
+
StartMarker,
|
|
472
|
+
Block<S, B>,
|
|
473
|
+
Block<B, C>,
|
|
474
|
+
Block<C, D>,
|
|
475
|
+
Block<D, E>,
|
|
476
|
+
Block<E, F>,
|
|
477
|
+
Block<F, G>,
|
|
478
|
+
Block<G, H>,
|
|
479
|
+
Block<H, I>,
|
|
480
|
+
Block<I, J>,
|
|
481
|
+
EndMarker
|
|
482
|
+
]): Flow<J>;
|
|
483
|
+
defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>, K extends Checkpoint<string>>(blocks: readonly [
|
|
484
|
+
StartMarker,
|
|
485
|
+
Block<S, B>,
|
|
486
|
+
Block<B, C>,
|
|
487
|
+
Block<C, D>,
|
|
488
|
+
Block<D, E>,
|
|
489
|
+
Block<E, F>,
|
|
490
|
+
Block<F, G>,
|
|
491
|
+
Block<G, H>,
|
|
492
|
+
Block<H, I>,
|
|
493
|
+
Block<I, J>,
|
|
494
|
+
Block<J, K>,
|
|
495
|
+
EndMarker
|
|
496
|
+
]): Flow<K>;
|
|
497
|
+
defineFlow<B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, G extends Checkpoint<string>, H extends Checkpoint<string>, I extends Checkpoint<string>, J extends Checkpoint<string>, K extends Checkpoint<string>, L extends Checkpoint<string>>(blocks: readonly [
|
|
498
|
+
StartMarker,
|
|
499
|
+
Block<S, B>,
|
|
500
|
+
Block<B, C>,
|
|
501
|
+
Block<C, D>,
|
|
502
|
+
Block<D, E>,
|
|
503
|
+
Block<E, F>,
|
|
504
|
+
Block<F, G>,
|
|
505
|
+
Block<G, H>,
|
|
506
|
+
Block<H, I>,
|
|
507
|
+
Block<I, J>,
|
|
508
|
+
Block<J, K>,
|
|
509
|
+
Block<K, L>,
|
|
510
|
+
EndMarker
|
|
511
|
+
]): Flow<L>;
|
|
470
512
|
}
|
|
471
513
|
/**
|
|
472
514
|
* Attaches routing to `block` without touching its act/observe/resolve/verify -
|
package/dist/engine.js
CHANGED
|
@@ -139,7 +139,7 @@ export async function narrate(locator, caption, action) {
|
|
|
139
139
|
.evaluate(({ box, caption }) => {
|
|
140
140
|
const w = globalThis;
|
|
141
141
|
if (w.__wgPositionRing)
|
|
142
|
-
w.__wgPositionRing(box, caption);
|
|
142
|
+
w.__wgPositionRing(box, caption, "planned");
|
|
143
143
|
// The CLI's own automatic per-fill/per-click narration checks
|
|
144
144
|
// this before showing (and overwriting) its own guess - an
|
|
145
145
|
// explicitly authored caption should win, not get immediately
|
package/dist/step-overlay.d.ts
CHANGED
|
@@ -3,18 +3,18 @@ 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. */
|
|
6
|
+
/** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
|
|
7
7
|
export declare function instrumentInteractionHighlighting(page: Page, mem: MemPage, slowMo: number): void;
|
|
8
8
|
declare global {
|
|
9
9
|
interface Window {
|
|
10
10
|
__wgMoveCursorTo?: (x: number, y: number, ms?: number) => void;
|
|
11
|
-
__wgClickPulse?: (x: number, y: number) => void;
|
|
11
|
+
__wgClickPulse?: (x: number, y: number, tone?: string) => void;
|
|
12
12
|
__wgPositionRing?: (box: {
|
|
13
13
|
x: number;
|
|
14
14
|
y: number;
|
|
15
15
|
width: number;
|
|
16
16
|
height: number;
|
|
17
|
-
}, label: string) => void;
|
|
17
|
+
}, label: string, tone?: string) => void;
|
|
18
18
|
__wgHideRing?: () => void;
|
|
19
19
|
__wgPendingNavClickLabel?: string;
|
|
20
20
|
__wgWirePanelChrome?: (panel: HTMLElement, storageKey: string, chromeTitle?: string) => void;
|
package/dist/step-overlay.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
2
|
-
"border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);
|
|
2
|
+
"border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
|
|
3
|
+
"transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
|
|
4
|
+
"#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
|
|
3
5
|
"#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
|
|
4
6
|
"white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
|
|
5
|
-
"font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease;}" +
|
|
7
|
+
"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:#EAB308;color:#1c1917;}" +
|
|
6
9
|
"#wg-cursor{position:fixed;z-index:2147483647;width:24px;height:24px;pointer-events:none;" +
|
|
7
10
|
"left:0;top:0;opacity:0;margin:0;" +
|
|
8
11
|
"transition:transform var(--wg-cursor-ms,600ms) cubic-bezier(.22,1,.36,1),opacity .2s ease;" +
|
|
@@ -10,6 +13,7 @@ const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none
|
|
|
10
13
|
"#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
|
|
11
14
|
"margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
|
|
12
15
|
"border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
|
|
16
|
+
"#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
|
|
13
17
|
"#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
|
|
14
18
|
"@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
|
|
15
19
|
"#wg-banner{position:fixed;z-index:2147483647;top:14px;left:14px;" +
|
|
@@ -24,7 +28,7 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
24
28
|
const showBanner = options.banner !== false;
|
|
25
29
|
await page.addStyleTag({ content: RING_CSS }).catch(() => { });
|
|
26
30
|
await page
|
|
27
|
-
.evaluate(({ title, favicon }) => {
|
|
31
|
+
.evaluate(({ title, favicon, showBanner }) => {
|
|
28
32
|
if (!document.getElementById("wg-ring")) {
|
|
29
33
|
const ring = document.createElement("div");
|
|
30
34
|
ring.id = "wg-ring";
|
|
@@ -57,21 +61,25 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
57
61
|
cursor.style.transform = "translate(" + x + "px," + y + "px)";
|
|
58
62
|
cursor.style.opacity = "1";
|
|
59
63
|
};
|
|
60
|
-
window.__wgClickPulse = (x, y) => {
|
|
64
|
+
window.__wgClickPulse = (x, y, tone) => {
|
|
61
65
|
const pulse = document.getElementById("wg-click-pulse");
|
|
62
66
|
if (!pulse)
|
|
63
67
|
return;
|
|
68
|
+
pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
|
|
64
69
|
pulse.style.left = x + "px";
|
|
65
70
|
pulse.style.top = y + "px";
|
|
66
71
|
pulse.classList.remove("wg-pulse");
|
|
67
72
|
void pulse.offsetWidth;
|
|
68
73
|
pulse.classList.add("wg-pulse");
|
|
69
74
|
};
|
|
70
|
-
window.__wgPositionRing = (box, label) => {
|
|
75
|
+
window.__wgPositionRing = (box, label, tone) => {
|
|
71
76
|
const ring = document.getElementById("wg-ring");
|
|
72
77
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
73
78
|
if (!ring || !ringLabel)
|
|
74
79
|
return;
|
|
80
|
+
const t = tone === "auto" ? "auto" : "planned";
|
|
81
|
+
ring.dataset.tone = t;
|
|
82
|
+
ringLabel.dataset.tone = t;
|
|
75
83
|
ring.style.left = box.x - 6 + "px";
|
|
76
84
|
ring.style.top = box.y - 6 + "px";
|
|
77
85
|
ring.style.width = box.width + 12 + "px";
|
|
@@ -111,16 +119,24 @@ async function moveCursorTo(page, box, ms) {
|
|
|
111
119
|
await new Promise((r) => setTimeout(r, ms));
|
|
112
120
|
return { x, y };
|
|
113
121
|
}
|
|
114
|
-
async function showRing(page, box, label) {
|
|
115
|
-
await page
|
|
122
|
+
async function showRing(page, box, label, tone = "auto") {
|
|
123
|
+
await page
|
|
124
|
+
.evaluate(({ box, label, tone }) => window.__wgPositionRing?.(box, label, tone), {
|
|
125
|
+
box,
|
|
126
|
+
label,
|
|
127
|
+
tone,
|
|
128
|
+
})
|
|
129
|
+
.catch(() => { });
|
|
116
130
|
}
|
|
117
131
|
async function hideRing(page) {
|
|
118
132
|
await page.evaluate(() => window.__wgHideRing?.()).catch(() => { });
|
|
119
133
|
}
|
|
120
|
-
async function clickPulseAt(page, x, y) {
|
|
121
|
-
await page
|
|
134
|
+
async function clickPulseAt(page, x, y, tone = "auto") {
|
|
135
|
+
await page
|
|
136
|
+
.evaluate(({ x, y, tone }) => window.__wgClickPulse?.(x, y, tone), { x, y, tone })
|
|
137
|
+
.catch(() => { });
|
|
122
138
|
}
|
|
123
|
-
/** Demo cursor + ring on fill/click during auto explore headful runs. */
|
|
139
|
+
/** Demo cursor + ring on fill/click during auto explore headful runs (automation = yellow). */
|
|
124
140
|
export function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
125
141
|
const typeDelay = () => (slowMo ? 0 : 30);
|
|
126
142
|
const clickPrePop = () => (slowMo ? 300 : 700);
|
|
@@ -133,89 +149,81 @@ export function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
|
133
149
|
memTrack.at = Date.now();
|
|
134
150
|
return originalGet(key);
|
|
135
151
|
};
|
|
136
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
152
|
const proto = Object.getPrototypeOf(page.locator("html"));
|
|
138
153
|
if (!proto.__wgFillPatched) {
|
|
139
154
|
proto.__wgFillPatched = true;
|
|
140
|
-
// Keep unbound — originalFill.call(this, …) must receive the Locator as `this`, not the prototype.
|
|
141
155
|
const originalFill = proto.fill;
|
|
142
156
|
proto.fill = async function (value, options) {
|
|
143
|
-
const livePage = (typeof this.page === "function" ? this.page() : page);
|
|
144
157
|
try {
|
|
145
|
-
await installDemoChrome(
|
|
158
|
+
await installDemoChrome(page, "", { banner: false });
|
|
146
159
|
const box = await this.boundingBox();
|
|
147
160
|
if (box) {
|
|
148
161
|
const label = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
|
|
149
162
|
? "from mem: " + memTrack.lastKeyName
|
|
150
|
-
: "writing";
|
|
151
|
-
await moveCursorTo(
|
|
152
|
-
await showRing(
|
|
153
|
-
await new Promise((
|
|
163
|
+
: "writing from mem";
|
|
164
|
+
await moveCursorTo(page, box, cursorMs(500));
|
|
165
|
+
await showRing(page, box, label, "auto");
|
|
166
|
+
await new Promise((res) => setTimeout(res, 200));
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
catch {
|
|
157
170
|
/* best-effort */
|
|
158
171
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
let result;
|
|
173
|
+
try {
|
|
174
|
+
await originalFill.call(this, "", {
|
|
175
|
+
timeout: options?.timeout,
|
|
176
|
+
});
|
|
177
|
+
const seqOpts = { delay: typeDelay() };
|
|
178
|
+
const to = options?.timeout;
|
|
179
|
+
if (to !== undefined)
|
|
180
|
+
seqOpts.timeout = to;
|
|
181
|
+
result = await this.pressSequentially(String(value), seqOpts);
|
|
163
182
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await originalFill.call(this, "", options);
|
|
167
|
-
const seqOpts = { delay: typeDelay() };
|
|
168
|
-
if (options?.timeout !== undefined)
|
|
169
|
-
seqOpts.timeout = options.timeout;
|
|
170
|
-
await this.pressSequentially(String(value), seqOpts);
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
await originalFill.call(this, value, options);
|
|
174
|
-
}
|
|
183
|
+
catch {
|
|
184
|
+
result = await originalFill.call(this, value, options);
|
|
175
185
|
}
|
|
176
|
-
await hideRing(
|
|
186
|
+
await hideRing(page);
|
|
187
|
+
return result;
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
if (!proto.__wgClickPatched) {
|
|
180
191
|
proto.__wgClickPatched = true;
|
|
181
192
|
const originalClick = proto.click;
|
|
182
193
|
proto.click = async function (options) {
|
|
183
|
-
const livePage = (typeof this.page === "function" ? this.page() : page);
|
|
184
194
|
let clickPoint = null;
|
|
185
195
|
try {
|
|
186
|
-
await installDemoChrome(
|
|
187
|
-
await this.waitFor({
|
|
196
|
+
await installDemoChrome(page, "", { banner: false });
|
|
197
|
+
await this.waitFor({
|
|
198
|
+
state: "visible",
|
|
199
|
+
timeout: options?.timeout || 30000,
|
|
200
|
+
}).catch(() => { });
|
|
188
201
|
const box = await this.boundingBox();
|
|
189
202
|
if (box) {
|
|
190
203
|
let label = "click";
|
|
191
204
|
try {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
label =
|
|
195
|
-
else {
|
|
196
|
-
const text = (await this.textContent())?.trim();
|
|
197
|
-
if (text && text.length <= 30)
|
|
198
|
-
label = text;
|
|
199
|
-
}
|
|
205
|
+
const text = (await this.textContent())?.trim();
|
|
206
|
+
if (text && text.length > 0 && text.length <= 30)
|
|
207
|
+
label = text;
|
|
200
208
|
}
|
|
201
209
|
catch {
|
|
202
210
|
/* ignore */
|
|
203
211
|
}
|
|
204
|
-
clickPoint = await moveCursorTo(
|
|
205
|
-
await showRing(
|
|
206
|
-
await new Promise((
|
|
212
|
+
clickPoint = await moveCursorTo(page, box, cursorMs(600));
|
|
213
|
+
await showRing(page, box, label, "auto");
|
|
214
|
+
await new Promise((res) => setTimeout(res, clickPrePop()));
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
217
|
catch {
|
|
210
218
|
/* best-effort */
|
|
211
219
|
}
|
|
212
220
|
if (clickPoint) {
|
|
213
|
-
await clickPulseAt(
|
|
214
|
-
await new Promise((
|
|
221
|
+
await clickPulseAt(page, clickPoint.x, clickPoint.y, "auto");
|
|
222
|
+
await new Promise((res) => setTimeout(res, 80));
|
|
215
223
|
}
|
|
216
224
|
const result = await originalClick.call(this, options);
|
|
217
|
-
await new Promise((
|
|
218
|
-
await hideRing(
|
|
225
|
+
await new Promise((res) => setTimeout(res, clickPostPop()));
|
|
226
|
+
await hideRing(page);
|
|
219
227
|
return result;
|
|
220
228
|
};
|
|
221
229
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TraverseOptions {
|
|
2
|
+
baseURL?: string;
|
|
3
|
+
startUrl?: string;
|
|
4
|
+
/** Seed checkpoint label (optional - otherwise detect from page / nav). */
|
|
5
|
+
from?: string;
|
|
6
|
+
/** Global Mem seed JSON (or use WAYGRAPH_DATA / WAYGRAPH_AUTO_MEM). */
|
|
7
|
+
data?: string;
|
|
8
|
+
/** Max Block runs this traverse (default 50). */
|
|
9
|
+
maxSteps?: number;
|
|
10
|
+
/** Max times a checkpoint may be visited (default 2). */
|
|
11
|
+
maxVisitsPerNode?: number;
|
|
12
|
+
/** Max times an edge key (block::instanceId) may be taken (default 1). */
|
|
13
|
+
maxVisitsPerEdge?: number;
|
|
14
|
+
/** Wall-clock budget ms (default 15m). */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Show browser (default headless). */
|
|
17
|
+
headed?: boolean;
|
|
18
|
+
/** Traverse id for greppable lines (default traverse-1). */
|
|
19
|
+
traverseId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase B: serial graph traverse (RFC traverse-ffcompose).
|
|
3
|
+
* Walk unused legal edges until leaf / budget / break. No parallel yet.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { Engine, start, end, locate } from "./engine.js";
|
|
8
|
+
import { MemPage } from "./mem-page.js";
|
|
9
|
+
import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
|
|
10
|
+
import { defaultMemValueForKey } from "./auto-explore-run.js";
|
|
11
|
+
function resolveBaseUrl(projectDir) {
|
|
12
|
+
if (process.env.WAYGRAPH_BASE_URL)
|
|
13
|
+
return process.env.WAYGRAPH_BASE_URL;
|
|
14
|
+
const pkgPath = join(projectDir, "package.json");
|
|
15
|
+
if (!existsSync(pkgPath))
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
19
|
+
return pkg.waygraph?.baseUrl;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function seedMem(library, mem, dataJson) {
|
|
26
|
+
const raw = dataJson ?? process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
|
|
27
|
+
let envParsed = {};
|
|
28
|
+
if (raw) {
|
|
29
|
+
try {
|
|
30
|
+
envParsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`waygraph traverse: --data / WAYGRAPH_DATA is not valid JSON`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const entry of library.values()) {
|
|
37
|
+
for (const k of entry.block.requires ?? []) {
|
|
38
|
+
if (mem.has(k))
|
|
39
|
+
continue;
|
|
40
|
+
if (k.name in envParsed) {
|
|
41
|
+
mem.set(k, envParsed[k.name]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const def = defaultMemValueForKey(k.name);
|
|
45
|
+
if (def !== undefined)
|
|
46
|
+
mem.set(k, def);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function missingMemKeys(entry, mem) {
|
|
51
|
+
return (entry.block.requires ?? []).filter((k) => !mem.has(k));
|
|
52
|
+
}
|
|
53
|
+
function ensureMem(entry, mem) {
|
|
54
|
+
const missing = missingMemKeys(entry, mem);
|
|
55
|
+
if (missing.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
throw new Error(`Block "${entry.block.name}" needs Mem keys: ${missing.map((k) => k.name).join(", ")} ` +
|
|
58
|
+
`(pass --data '{...}' or WAYGRAPH_DATA)`);
|
|
59
|
+
}
|
|
60
|
+
function edgeKey(edge) {
|
|
61
|
+
const id = edge.instanceOption?.id;
|
|
62
|
+
return id ? `${edge.block}::${id}` : edge.block;
|
|
63
|
+
}
|
|
64
|
+
async function detectHere(page, navBlocks) {
|
|
65
|
+
try {
|
|
66
|
+
const url = page.url();
|
|
67
|
+
if (url.includes("/login"))
|
|
68
|
+
return "PiaLogin";
|
|
69
|
+
if (url.includes("mailpit") || url.includes(":54324") || url.includes("/mailhog")) {
|
|
70
|
+
return "PiaMailpitGui";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
const tags = navBlocks.map((n) => n.block);
|
|
77
|
+
return locate(page, tags, { timeoutMs: 1500 });
|
|
78
|
+
}
|
|
79
|
+
async function runOneBlock(engine, entry, context, page, mem) {
|
|
80
|
+
const flow = engine.defineFlow([start, entry.block, end]);
|
|
81
|
+
const BLOCK_TIMEOUT_MS = 45_000;
|
|
82
|
+
let timer;
|
|
83
|
+
try {
|
|
84
|
+
const ran = await Promise.race([
|
|
85
|
+
flow.run(context, mem, { page, closeOnFinish: false }),
|
|
86
|
+
new Promise((_, reject) => {
|
|
87
|
+
timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s`)), BLOCK_TIMEOUT_MS);
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
return ran.result;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (timer)
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
|
|
99
|
+
*/
|
|
100
|
+
export async function runTraverse(projectDir, options = {}) {
|
|
101
|
+
const traverseId = options.traverseId ?? "traverse-1";
|
|
102
|
+
const maxSteps = options.maxSteps ?? 50;
|
|
103
|
+
const maxVisitsPerNode = options.maxVisitsPerNode ?? 2;
|
|
104
|
+
const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
|
|
105
|
+
const timeoutMs = options.timeoutMs ?? 15 * 60_000;
|
|
106
|
+
const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
|
|
107
|
+
const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
|
|
108
|
+
const startUrl = options.startUrl ?? baseURL;
|
|
109
|
+
const { graph, library } = await buildExploreContext(projectDir);
|
|
110
|
+
const mem = new MemPage();
|
|
111
|
+
seedMem(library.byName, mem, options.data);
|
|
112
|
+
const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
|
|
113
|
+
const { chromium } = await import("@playwright/test");
|
|
114
|
+
const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
115
|
+
const launchOpts = {
|
|
116
|
+
headless: !headed,
|
|
117
|
+
};
|
|
118
|
+
if (executablePath)
|
|
119
|
+
launchOpts.executablePath = executablePath;
|
|
120
|
+
const browser = await chromium.launch(launchOpts);
|
|
121
|
+
const contextOpts = {
|
|
122
|
+
viewport: { width: 1280, height: 720 },
|
|
123
|
+
};
|
|
124
|
+
if (baseURL)
|
|
125
|
+
contextOpts.baseURL = baseURL;
|
|
126
|
+
const context = await browser.newContext(contextOpts);
|
|
127
|
+
let page = await context.newPage();
|
|
128
|
+
const nodeVisits = new Map();
|
|
129
|
+
const edgeVisits = new Map();
|
|
130
|
+
const edgesHit = new Set();
|
|
131
|
+
let steps = 0;
|
|
132
|
+
const startedAt = Date.now();
|
|
133
|
+
let lastHere = options.from ?? null;
|
|
134
|
+
let exitCode = 0;
|
|
135
|
+
console.error(`waygraph traverse[${traverseId}]: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
|
|
136
|
+
(baseURL ? ` base=${baseURL}` : "") +
|
|
137
|
+
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`);
|
|
138
|
+
try {
|
|
139
|
+
if (startUrl) {
|
|
140
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
141
|
+
await page.waitForLoadState("load").catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
for (;;) {
|
|
144
|
+
if (Date.now() - startedAt > timeoutMs) {
|
|
145
|
+
console.log(`[Killed timeout[${traverseId}] node=${lastHere ?? "?"} wallMs=${timeoutMs}]`);
|
|
146
|
+
exitCode = 1;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (steps >= maxSteps) {
|
|
150
|
+
console.log(`[Killed max-steps[${traverseId}] node=${lastHere ?? "?"} steps=${steps} max=${maxSteps}]`);
|
|
151
|
+
exitCode = 1;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (page.isClosed()) {
|
|
155
|
+
page = await context.newPage();
|
|
156
|
+
if (startUrl) {
|
|
157
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const detected = await detectHere(page, library.navBlocks);
|
|
161
|
+
const here = lastHere ?? detected ?? options.from ?? null;
|
|
162
|
+
if (here && (nodeVisits.get(here) ?? 0) >= maxVisitsPerNode) {
|
|
163
|
+
console.log(`[Killed loop[${traverseId}] node=${here} visits=${nodeVisits.get(here)} max=${maxVisitsPerNode}]`);
|
|
164
|
+
exitCode = 1;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
const menu = await buildExploreMenu(page, graph, library, here);
|
|
168
|
+
const candidates = menu.flat.filter((edge) => {
|
|
169
|
+
const key = edgeKey(edge);
|
|
170
|
+
return (edgeVisits.get(key) ?? 0) < maxVisitsPerEdge;
|
|
171
|
+
});
|
|
172
|
+
if (candidates.length === 0) {
|
|
173
|
+
const label = here ?? detected ?? "unknown";
|
|
174
|
+
console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
|
|
175
|
+
console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const pick = [...candidates].sort((a, b) => {
|
|
179
|
+
const aWild = a.from === "*" ? 1 : 0;
|
|
180
|
+
const bWild = b.from === "*" ? 1 : 0;
|
|
181
|
+
if (aWild !== bWild)
|
|
182
|
+
return aWild - bWild;
|
|
183
|
+
return edgeKey(a).localeCompare(edgeKey(b));
|
|
184
|
+
})[0];
|
|
185
|
+
const entry = library.byName.get(pick.block);
|
|
186
|
+
if (!entry) {
|
|
187
|
+
console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${here ?? "?"} to=${pick.to} reason=not-loaded]`);
|
|
188
|
+
exitCode = 1;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const fromLabel = here ?? pick.from;
|
|
192
|
+
try {
|
|
193
|
+
if (pick.instanceOption) {
|
|
194
|
+
mem.set(pick.instanceOption.key, pick.instanceOption.value);
|
|
195
|
+
}
|
|
196
|
+
ensureMem(entry, mem);
|
|
197
|
+
console.error(`waygraph traverse[${traverseId}]: step ${steps + 1} ${fromLabel} -[${pick.block}]-> ${pick.to}` +
|
|
198
|
+
(pick.label ? ` (${pick.label})` : ""));
|
|
199
|
+
const out = await runOneBlock(engine, entry, context, page, mem);
|
|
200
|
+
const observed = typeof out === "object" && out && "__state" in out
|
|
201
|
+
? String(out.__state)
|
|
202
|
+
: pick.to;
|
|
203
|
+
const key = edgeKey(pick);
|
|
204
|
+
edgeVisits.set(key, (edgeVisits.get(key) ?? 0) + 1);
|
|
205
|
+
edgesHit.add(key);
|
|
206
|
+
if (here)
|
|
207
|
+
nodeVisits.set(here, (nodeVisits.get(here) ?? 0) + 1);
|
|
208
|
+
lastHere = observed;
|
|
209
|
+
steps += 1;
|
|
210
|
+
if (observed !== pick.to && pick.to !== "*") {
|
|
211
|
+
console.error(`waygraph traverse[${traverseId}]: WARN static to=${pick.to} observed=${observed}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
216
|
+
console.log(`[Broke at edge[${traverseId}] block=${pick.block} from=${fromLabel} to=${pick.to}] ${msg}`);
|
|
217
|
+
exitCode = 1;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await browser.close().catch(() => { });
|
|
224
|
+
}
|
|
225
|
+
console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
|
|
226
|
+
return exitCode;
|
|
227
|
+
}
|