waygraph 0.12.8 → 0.12.10

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.
@@ -4,6 +4,8 @@ export interface AutoExploreOptions {
4
4
  baseURL?: string;
5
5
  /** Initial URL when the page is blank (defaults to baseURL). */
6
6
  startUrl?: string;
7
+ /** Phase C: glob / regex / bare `--blocks` file select for discovery. */
8
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
7
9
  }
8
10
  /**
9
11
  * Saucedemo-only defaults. Exact key names only — never substring `"login"`
@@ -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", () => {
@@ -490,7 +491,11 @@ export async function runAutoExplore(projectDir, options = {}) {
490
491
  const cli = options.cli === true;
491
492
  const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
492
493
  const startUrl = options.startUrl ?? baseURL;
493
- const { graph, library } = await buildExploreContext(projectDir);
494
+ const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
495
+ if (options.blocksSelect) {
496
+ console.error(`waygraph auto: --blocks ${options.blocksSelect.raw} → ` +
497
+ `${library.byName.size} block(s), ${graph.edges.length} edge(s)`);
498
+ }
494
499
  const mem = new MemPage();
495
500
  seedDefaultMem(library.byName, mem);
496
501
  const engine = cli ? new Engine({ headless: true }) : new Engine({ headless: false, slowMo: 250 });
@@ -32,7 +32,9 @@ export interface ExploreMenu {
32
32
  /**
33
33
  * Import every `*.block.ts` export keyed by runtime Block `.name`.
34
34
  */
35
- export declare function loadBlockLibrary(projectDir: string): Promise<{
35
+ export declare function loadBlockLibrary(projectDir: string, opts?: {
36
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
37
+ }): Promise<{
36
38
  byName: Map<string, BlockEntry>;
37
39
  navBlocks: BlockEntry[];
38
40
  }>;
@@ -46,7 +48,9 @@ export declare function exploreChoices(graph: WaygraphGraph, here: string | null
46
48
  export declare function buildExploreMenu(page: Page, graph: WaygraphGraph, library: {
47
49
  byName: Map<string, BlockEntry>;
48
50
  }, here: string | null): Promise<ExploreMenu>;
49
- export declare function buildExploreContext(projectDir: string): Promise<{
51
+ export declare function buildExploreContext(projectDir: string, opts?: {
52
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
53
+ }): Promise<{
50
54
  graph: WaygraphGraph;
51
55
  library: Awaited<ReturnType<typeof loadBlockLibrary>>;
52
56
  }>;
@@ -81,11 +81,17 @@ async function wildcardActionRunnable(page, entry, here) {
81
81
  /**
82
82
  * Import every `*.block.ts` export keyed by runtime Block `.name`.
83
83
  */
84
- export async function loadBlockLibrary(projectDir) {
84
+ export async function loadBlockLibrary(projectDir, opts) {
85
+ const { matchBlocksSelect } = await import("./blocks-select.js");
85
86
  const byName = new Map();
86
87
  const navBlocks = [];
88
+ const select = opts?.blocksSelect;
87
89
  for (const file of walkDir(projectDir, /\.block\.ts$/)) {
88
- const relFile = relative(projectDir, file);
90
+ const relFile = relative(projectDir, file).replace(/\\/g, "/");
91
+ // Glob/bare: path filter before import. Regex: need block name — filter after.
92
+ if (select && select.kind !== "regex" && !matchBlocksSelect(select, { relFile })) {
93
+ continue;
94
+ }
89
95
  let mod;
90
96
  try {
91
97
  mod = await importModule(file);
@@ -100,6 +106,10 @@ export async function loadBlockLibrary(projectDir) {
100
106
  for (const [exportName, exported] of Object.entries(mod)) {
101
107
  if (!isBlockLike(exported))
102
108
  continue;
109
+ if (select &&
110
+ !matchBlocksSelect(select, { relFile, blockName: exported.name })) {
111
+ continue;
112
+ }
103
113
  const kind = isNavBlockMarked(exported) ? "nav" : "action";
104
114
  const entry = {
105
115
  block: exported,
@@ -317,11 +327,11 @@ export async function buildExploreMenu(page, graph, library, here) {
317
327
  }
318
328
  return { here, sections, flat };
319
329
  }
320
- export async function buildExploreContext(projectDir) {
330
+ export async function buildExploreContext(projectDir, opts) {
321
331
  // Sequential: parallel import of the same *.block.ts tree raced Playwright's
322
332
  // dual-copy guard in some installs and left graph edges without library entries
323
333
  // (LoginPage → "No moves").
324
- const library = await loadBlockLibrary(projectDir);
325
- const graph = await discoverGraph(projectDir);
334
+ const library = await loadBlockLibrary(projectDir, opts);
335
+ const graph = await discoverGraph(projectDir, opts);
326
336
  return { graph, library };
327
337
  }
@@ -0,0 +1,22 @@
1
+ export type BlocksSelectKind = "regex" | "glob" | "bare";
2
+ export interface BlocksSelect {
3
+ kind: BlocksSelectKind;
4
+ /** Original token. */
5
+ raw: string;
6
+ /** For regex: compiled without wrapping slashes. For glob: pattern as given. */
7
+ pattern: string;
8
+ re?: RegExp;
9
+ }
10
+ /** True when the token is file-select (glob/regex), not a bare flow/checkpoint name. */
11
+ export declare function isFileSelectToken(token: string): boolean;
12
+ export declare function parseBlocksSelect(token: string): BlocksSelect;
13
+ /** Glob → RegExp (simple: ** / * / ? only; no brace expansion). */
14
+ export declare function globToRegExp(glob: string): RegExp;
15
+ export declare function matchBlocksSelect(select: BlocksSelect, opts: {
16
+ relFile: string;
17
+ blockName?: string;
18
+ }): boolean;
19
+ export declare function filterByBlocksSelect<T extends {
20
+ file: string;
21
+ block?: string;
22
+ }>(items: T[], select: BlocksSelect | undefined, projectDir: string): T[];
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared `--blocks` select grammar (RFC Phase C).
3
+ *
4
+ * - `/pattern/` -> regex on relative file path OR block name
5
+ * - has `*`/`**` -> glob on relative file path (POSIX-ish)
6
+ * - otherwise -> bare token (flow export / checkpoint / block name) — caller decides
7
+ */
8
+ import { relative } from "node:path";
9
+ /** True when the token is file-select (glob/regex), not a bare flow/checkpoint name. */
10
+ export function isFileSelectToken(token) {
11
+ if (!token)
12
+ return false;
13
+ if (token.length >= 2 && token.startsWith("/") && token.endsWith("/") && token.length > 2) {
14
+ return true;
15
+ }
16
+ return token.includes("*");
17
+ }
18
+ export function parseBlocksSelect(token) {
19
+ const raw = token.trim();
20
+ if (raw.length >= 2 && raw.startsWith("/") && raw.endsWith("/")) {
21
+ const body = raw.slice(1, -1);
22
+ return { kind: "regex", raw, pattern: body, re: new RegExp(body) };
23
+ }
24
+ if (raw.includes("*")) {
25
+ return { kind: "glob", raw, pattern: raw };
26
+ }
27
+ return { kind: "bare", raw, pattern: raw };
28
+ }
29
+ /** Glob → RegExp (simple: ** / * / ? only; no brace expansion). */
30
+ export function globToRegExp(glob) {
31
+ let out = "";
32
+ for (let i = 0; i < glob.length; i++) {
33
+ const c = glob[i];
34
+ if (c === "*" && glob[i + 1] === "*") {
35
+ out += ".*";
36
+ i++;
37
+ if (glob[i + 1] === "/")
38
+ i++;
39
+ continue;
40
+ }
41
+ if (c === "*") {
42
+ out += "[^/]*";
43
+ continue;
44
+ }
45
+ if (c === "?") {
46
+ out += "[^/]";
47
+ continue;
48
+ }
49
+ if (".+^${}()|[]\\".includes(c))
50
+ out += "\\" + c;
51
+ else
52
+ out += c;
53
+ }
54
+ return new RegExp("^" + out + "$");
55
+ }
56
+ export function matchBlocksSelect(select, opts) {
57
+ const file = opts.relFile.replace(/\\/g, "/");
58
+ const name = opts.blockName ?? "";
59
+ if (select.kind === "regex") {
60
+ const re = select.re ?? new RegExp(select.pattern);
61
+ return re.test(file) || (name !== "" && re.test(name));
62
+ }
63
+ if (select.kind === "glob") {
64
+ return globToRegExp(select.pattern.replace(/\\/g, "/")).test(file);
65
+ }
66
+ // bare: match block name equality (case-sensitive) or path substring
67
+ if (name && name === select.pattern)
68
+ return true;
69
+ if (file.includes(select.pattern))
70
+ return true;
71
+ return false;
72
+ }
73
+ export function filterByBlocksSelect(items, select, projectDir) {
74
+ if (!select)
75
+ return items;
76
+ return items.filter((it) => {
77
+ const relFile = it.file.startsWith(projectDir)
78
+ ? relative(projectDir, it.file)
79
+ : it.file;
80
+ return matchBlocksSelect(select, {
81
+ relFile: relFile.replace(/\\/g, "/"),
82
+ ...(it.block !== undefined ? { blockName: it.block } : {}),
83
+ });
84
+ });
85
+ }
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./gra
21
21
  import { runAutoExplore } from "./auto-explore-run.js";
22
22
  import { runAgentDive } from "./agent-dive.js";
23
23
  import { runTraverse } from "./traverse-run.js";
24
+ import { isFileSelectToken, parseBlocksSelect, } from "./blocks-select.js";
24
25
  // ---------------------------------------------------------------------------
25
26
  // Filesystem
26
27
  // ---------------------------------------------------------------------------
@@ -396,7 +397,11 @@ function seedMemForFlow(mem, flowBlocks, json, label) {
396
397
  const RING_CSS =
397
398
  "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
398
399
  "border:2.5px solid #7C3AED;border-radius:10px;" +
399
- "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
400
+ "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
401
+ // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
402
+ // Automation (verify fallback, unmatched fill/click) = yellow - so operators
403
+ // can ignore engine checks and only watch purple narration.
404
+ "#wg-ring[data-tone=auto]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
400
405
  // A real element, not a ::after pseudo-element - a pseudo-element's
401
406
  // position is CSS-relative to the ring's own box (left:0 always meant
402
407
  // "the ring's own left edge"), so it had no way to be clamped back onto
@@ -407,7 +412,8 @@ const RING_CSS =
407
412
  // window.__wgPositionRing in installOverlay.
408
413
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
409
414
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
410
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
415
+ "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:#EAB308;color:#1c1917;}" +
411
417
  // Mouse cursor icon that travels to a target before it's acted on, plus a
412
418
  // quick expanding ripple at the moment of a click - same idea as
413
419
  // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
@@ -425,6 +431,7 @@ const RING_CSS =
425
431
  "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
426
432
  "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
427
433
  "border:2px solid #7C3AED;background:rgba(124,58,237,.25);}" +
434
+ "#wg-click-pulse[data-tone=auto]{border-color:#EAB308;background:rgba(234,179,8,.28);}" +
428
435
  "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
429
436
  "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
430
437
  "#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
@@ -640,26 +647,13 @@ async function installOverlay(page, title) {
640
647
  const cursor = document.getElementById("wg-cursor");
641
648
  if (cursor) cursor.style.opacity = "0";
642
649
  };
643
- window.__wgClickPulse = (x, y) => {
644
- const pulse = document.getElementById("wg-click-pulse");
645
- if (!pulse) return;
646
- pulse.style.left = x + "px";
647
- pulse.style.top = y + "px";
648
- pulse.classList.remove("wg-pulse");
649
- void pulse.offsetWidth;
650
- pulse.classList.add("wg-pulse");
651
- };
652
- // Clamps the ring AND its label to stay fully on-screen, same
653
- // width/height always - only the position moves. A target near a
654
- // viewport edge (real case: "login success" highlight landing top
655
- // right) used to just run the label text off-screen with no guard
656
- // at all - same mistake as the banner had before its own
657
- // left/center/right positions existed, not repeating it here by
658
- // shrinking anything, only repositioning.
659
- window.__wgPositionRing = (box, label) => {
650
+ window.__wgPositionRing = (box, label, tone) => {
660
651
  const ring = document.getElementById("wg-ring");
661
652
  const ringLabel = document.getElementById("wg-ring-label");
662
653
  if (!ring || !ringLabel) return;
654
+ const t = tone === "auto" ? "auto" : "planned";
655
+ ring.dataset.tone = t;
656
+ ringLabel.dataset.tone = t;
663
657
  const margin = 6;
664
658
  const vw = window.innerWidth;
665
659
  const vh = window.innerHeight;
@@ -691,6 +685,16 @@ async function installOverlay(page, title) {
691
685
  ringLabel.style.left = labelLeft + "px";
692
686
  ringLabel.style.top = labelTop + "px";
693
687
  };
688
+ window.__wgClickPulse = (x, y, tone) => {
689
+ const pulse = document.getElementById("wg-click-pulse");
690
+ if (!pulse) return;
691
+ pulse.dataset.tone = tone === "auto" ? "auto" : "planned";
692
+ pulse.style.left = x + "px";
693
+ pulse.style.top = y + "px";
694
+ pulse.classList.remove("wg-pulse");
695
+ void pulse.offsetWidth;
696
+ pulse.classList.add("wg-pulse");
697
+ };
694
698
  window.__wgHideRing = () => {
695
699
  // While a narrate() call owns the ring (mid multi-step action),
696
700
  // the auto-highlight click/fill patches' own end-of-step hide is
@@ -1031,7 +1035,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1031
1035
  try {
1032
1036
  const box = await page.locator(h.selector).first().boundingBox();
1033
1037
  if (box) {
1034
- await showRing(page, box, h.label);
1038
+ await showRing(page, box, h.label, h.tone || "planned");
1035
1039
  const authored = resolveFixtureDwellMs(h, { gatesFast });
1036
1040
  const legacyMs = i === list.length - 1 ? 200 : 900;
1037
1041
  const holdMs =
@@ -1053,7 +1057,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1053
1057
  if (window.__wgHideRing) window.__wgHideRing();
1054
1058
  return;
1055
1059
  }
1056
- window.__wgPositionRing(el.getBoundingClientRect(), h.label);
1060
+ window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned");
1057
1061
  };
1058
1062
  reposition();
1059
1063
  window.__wgRingTrack = reposition;
@@ -1297,7 +1301,10 @@ function resolveDeclaredHighlights(block, resultTag) {
1297
1301
  }
1298
1302
  }
1299
1303
  if (!Array.isArray(highlights)) return [];
1300
- return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string");
1304
+ return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string").map((h) => ({
1305
+ ...h,
1306
+ tone: h.tone === "auto" ? "auto" : "planned",
1307
+ }));
1301
1308
  }
1302
1309
 
1303
1310
  /**
@@ -1327,12 +1334,12 @@ function extractVerifyHighlights(block, resultTag) {
1327
1334
  if (typeof name !== "string") continue;
1328
1335
  let m = /^visible\\((.+)\\)$/.exec(name);
1329
1336
  if (m) {
1330
- highlights.push({ selector: m[1], label: name });
1337
+ highlights.push({ selector: m[1], label: name, tone: "auto" });
1331
1338
  continue;
1332
1339
  }
1333
1340
  m = /^text-equals\\((.+?),\\s*"/.exec(name);
1334
1341
  if (m) {
1335
- highlights.push({ selector: m[1], label: name });
1342
+ highlights.push({ selector: m[1], label: name, tone: "auto" });
1336
1343
  continue;
1337
1344
  }
1338
1345
  }
@@ -1395,13 +1402,13 @@ async function markStepRunning(page, _opts) {
1395
1402
  .catch(() => {});
1396
1403
  }
1397
1404
 
1398
- async function showRing(page, box, label) {
1405
+ async function showRing(page, box, label, tone) {
1399
1406
  await page
1400
1407
  .evaluate(
1401
- ({ box, label }) => {
1402
- if (window.__wgPositionRing) window.__wgPositionRing(box, label);
1408
+ ({ box, label, tone }) => {
1409
+ if (window.__wgPositionRing) window.__wgPositionRing(box, label, tone || "planned");
1403
1410
  },
1404
- { box, label },
1411
+ { box, label, tone: tone || "planned" },
1405
1412
  )
1406
1413
  .catch(() => {});
1407
1414
  }
@@ -1431,8 +1438,8 @@ async function matchStubForLocator(page, locator, stubs) {
1431
1438
 
1432
1439
  async function captionForLocator(page, locator, stubs, fallback) {
1433
1440
  const matched = await matchStubForLocator(page, locator, stubs);
1434
- if (matched) return formatHighlightCaption(matched);
1435
- return fallback;
1441
+ if (matched) return { label: formatHighlightCaption(matched), tone: "planned" };
1442
+ return { label: fallback, tone: "auto" };
1436
1443
  }
1437
1444
 
1438
1445
  async function dwellMatchedStub(page, locator, stubs, pacing) {
@@ -1474,7 +1481,7 @@ async function presentSlides(page, slides, _gate, opts) {
1474
1481
  if (s.selector) {
1475
1482
  try {
1476
1483
  const box = await page.locator(s.selector).first().boundingBox();
1477
- if (box) await showRing(page, box, caption);
1484
+ if (box) await showRing(page, box, caption, "planned");
1478
1485
  } catch {
1479
1486
  await hideRing(page);
1480
1487
  }
@@ -1705,11 +1712,11 @@ async function moveCursorTo(page, box, ms) {
1705
1712
  return { x, y };
1706
1713
  }
1707
1714
 
1708
- async function clickPulseAt(page, x, y) {
1715
+ async function clickPulseAt(page, x, y, tone) {
1709
1716
  await page
1710
- .evaluate(({ x, y }) => {
1711
- if (window.__wgClickPulse) window.__wgClickPulse(x, y);
1712
- }, { x, y })
1717
+ .evaluate(({ x, y, tone }) => {
1718
+ if (window.__wgClickPulse) window.__wgClickPulse(x, y, tone || "planned");
1719
+ }, { x, y, tone: tone || "planned" })
1713
1720
  .catch(() => {});
1714
1721
  }
1715
1722
 
@@ -1783,9 +1790,9 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1783
1790
  ? "from mem: " + memTrack.lastKeyName
1784
1791
  : "writing from mem";
1785
1792
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1786
- const label = await captionForLocator(page, this, stubs, fallback);
1793
+ const cap = await captionForLocator(page, this, stubs, fallback);
1787
1794
  await moveCursorTo(page, box, cursorMs(500));
1788
- await showRing(page, box, label);
1795
+ await showRing(page, box, cap.label, cap.tone);
1789
1796
  await dwellMatchedStub(page, this, stubs, pacing);
1790
1797
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 200));
1791
1798
  } else if (box) {
@@ -1824,6 +1831,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1824
1831
  const originalClick = proto.click;
1825
1832
  proto.click = async function (options) {
1826
1833
  let clickPoint = null;
1834
+ let clickTone = "auto";
1827
1835
  try {
1828
1836
  await installOverlay(page);
1829
1837
  // Wait until the target is visible BEFORE measuring - otherwise
@@ -1853,9 +1861,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1853
1861
  /* ignore */
1854
1862
  }
1855
1863
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1856
- const label = await captionForLocator(page, this, stubs, fallback);
1864
+ const cap = await captionForLocator(page, this, stubs, fallback);
1865
+ clickTone = cap.tone;
1857
1866
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
1858
- await showRing(page, box, label);
1867
+ await showRing(page, box, cap.label, cap.tone);
1859
1868
  await dwellMatchedStub(page, this, stubs, pacing);
1860
1869
  // "pop for a few seconds" - Dan's own phrase, matching the
1861
1870
  // zsign demo-engine's ring-before-click pattern in
@@ -1865,12 +1874,13 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
1865
1874
  // Ring/caption already handled by narrate() - still move the
1866
1875
  // cursor + pulse the click point, just skip re-showing the ring.
1867
1876
  clickPoint = await moveCursorTo(page, box, cursorMs(200));
1877
+ clickTone = "planned";
1868
1878
  }
1869
1879
  } catch {
1870
1880
  // best-effort - the real click below still runs either way
1871
1881
  }
1872
1882
  if (clickPoint) {
1873
- await clickPulseAt(page, clickPoint.x, clickPoint.y);
1883
+ await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
1874
1884
  // clickPulseAt only triggers the CSS animation class - it doesn't
1875
1885
  // wait for it. Without a pause here, the real click (and any
1876
1886
  // resulting navigation/DOM change) fires while the ripple is still
@@ -2257,6 +2267,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2257
2267
  label: formatHighlightCaption(h),
2258
2268
  duration: h.duration,
2259
2269
  fastMode: h.fastMode,
2270
+ tone: "planned",
2260
2271
  }));
2261
2272
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
2262
2273
  defaultHoldMs: 2000,
@@ -2329,6 +2340,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2329
2340
  label: formatHighlightCaption(h),
2330
2341
  duration: h.duration,
2331
2342
  fastMode: h.fastMode,
2343
+ tone: "planned",
2332
2344
  }),
2333
2345
  );
2334
2346
  } else {
@@ -3662,7 +3674,8 @@ Primary (less is more):
3662
3674
 
3663
3675
  Also:
3664
3676
  waygraph list | nav | validate | check | graph | init <name>
3665
- waygraph traverse [project] Serial graph crawl (Phase B)
3677
+ waygraph traverse [project] Serial graph crawl (Phase B+)
3678
+ --blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
3666
3679
  --from <Checkpoint> Seed / start checkpoint
3667
3680
  --data '{...}' Mem seed
3668
3681
  --max-steps N Cap Block runs (default 50)
@@ -3686,6 +3699,8 @@ Examples:
3686
3699
  waygraph auto src/flows/shop.flow.ts --data '{...}' # run by file (same as run)
3687
3700
  waygraph auto --cli --data '{"saucedemo.credentials":{...}}'
3688
3701
  waygraph auto --blocks LoginPage OrderComplete
3702
+ waygraph auto --blocks '/mailpit/' # Phase C filtered explore
3703
+ waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
3689
3704
  waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
3690
3705
 
3691
3706
  Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
@@ -3779,6 +3794,7 @@ async function main() {
3779
3794
  let maxEdge;
3780
3795
  let headed = false;
3781
3796
  let baseUrl;
3797
+ let blocksFilter;
3782
3798
  let projectDir = process.cwd();
3783
3799
  for (let i = 0; i < rest.length; i++) {
3784
3800
  const a = rest[i];
@@ -3790,6 +3806,20 @@ async function main() {
3790
3806
  data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
3791
3807
  continue;
3792
3808
  }
3809
+ if (a === "--blocks" || a.startsWith("--blocks=")) {
3810
+ if (a.startsWith("--blocks=")) {
3811
+ blocksFilter = a.slice("--blocks=".length);
3812
+ }
3813
+ else {
3814
+ const v = rest[++i];
3815
+ if (!v || v.startsWith("-")) {
3816
+ console.error("waygraph traverse: --blocks needs a glob, /regex/, or path substring");
3817
+ process.exit(1);
3818
+ }
3819
+ blocksFilter = v;
3820
+ }
3821
+ continue;
3822
+ }
3793
3823
  if (a === "--max-steps" || a.startsWith("--max-steps=")) {
3794
3824
  const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
3795
3825
  maxSteps = Number(v);
@@ -3821,6 +3851,7 @@ async function main() {
3821
3851
  Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
3822
3852
  budget kill, or first broken edge.
3823
3853
 
3854
+ --blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
3824
3855
  --from <Checkpoint> start checkpoint (optional)
3825
3856
  --data '{...}' Mem seed JSON
3826
3857
  --max-steps N default 50
@@ -3829,6 +3860,10 @@ budget kill, or first broken edge.
3829
3860
  --non-headless show browser
3830
3861
  --base-url URL
3831
3862
 
3863
+ Examples:
3864
+ waygraph traverse --blocks '**/mailpit/**/*.block.ts'
3865
+ waygraph traverse --blocks '/mailpit|login/'
3866
+
3832
3867
  PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
3833
3868
  FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3834
3869
  `);
@@ -3842,6 +3877,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3842
3877
  console.error(`waygraph traverse: no such directory: ${projectDir}`);
3843
3878
  process.exit(1);
3844
3879
  }
3880
+ const blocksSelect = blocksFilter
3881
+ ? parseBlocksSelect(blocksFilter)
3882
+ : undefined;
3845
3883
  const code = await runTraverse(projectDir, {
3846
3884
  ...(from ? { from } : {}),
3847
3885
  ...(data ? { data } : {}),
@@ -3854,9 +3892,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3854
3892
  : {}),
3855
3893
  headed,
3856
3894
  ...(baseUrl ? { baseURL: baseUrl } : {}),
3895
+ ...(blocksSelect ? { blocksSelect } : {}),
3857
3896
  });
3858
- process.exitCode = code;
3859
- break;
3897
+ process.exit(code);
3860
3898
  }
3861
3899
  case "agent-dive":
3862
3900
  case "init-agents": {
@@ -4014,13 +4052,23 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
4014
4052
  process.exit(1);
4015
4053
  }
4016
4054
  // auto --blocks From To (graph path-find + run)
4055
+ // auto --blocks '/regex/' or '**/*.block.ts' (Phase C file select → explore)
4017
4056
  if (command === "auto" && (flags.blocksFromTo || flags.blocks)) {
4018
4057
  if (flags.blocksFromTo) {
4019
4058
  const [fromTag, toTag] = flags.blocksFromTo;
4020
4059
  await runChainAuto(proj, fromTag, toTag);
4021
4060
  break;
4022
4061
  }
4062
+ if (flags.blocks && isFileSelectToken(flags.blocks)) {
4063
+ const blocksSelect = parseBlocksSelect(flags.blocks);
4064
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
4065
+ await runAutoExplore(proj, baseURL
4066
+ ? { cli: cliPicker, baseURL, blocksSelect }
4067
+ : { cli: cliPicker, blocksSelect });
4068
+ break;
4069
+ }
4023
4070
  console.error("waygraph auto --blocks expects <fromCheckpoint> <toCheckpoint>\n" +
4071
+ " or file select: --blocks '**/mailpit/**/*.block.ts' / --blocks '/mailpit/'\n" +
4024
4072
  ' e.g. waygraph auto --blocks LoginPage OrderComplete\n' +
4025
4073
  ' for a hand-named chain use: waygraph run --blocks "a then b"\n' +
4026
4074
  " or: waygraph auto src/flows/shop.flow.ts");
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/graph.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type BlocksSelect } from "./blocks-select.js";
1
2
  /**
2
3
  * The state-machine shape `waygraph auto` discovers from a project's own
3
4
  * Blocks - Checkpoints are nodes, Blocks are edges. Kept as plain data (not
@@ -67,4 +68,6 @@ export declare function findBlockPath(graph: WaygraphGraph, fromTag: string, toT
67
68
  * already is for `check`, unlike `chain`, which spawns a child process
68
69
  * specifically because it also EXECUTES Blocks, not just imports them.
69
70
  */
70
- export declare function discoverGraph(projectDir: string): Promise<WaygraphGraph>;
71
+ export declare function discoverGraph(projectDir: string, opts?: {
72
+ blocksSelect?: BlocksSelect;
73
+ }): Promise<WaygraphGraph>;
package/dist/graph.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join, dirname, relative, resolve } from "node:path";
3
+ import { matchBlocksSelect, } from "./blocks-select.js";
3
4
  /**
4
5
  * Renders a `WaygraphGraph` as Mermaid `stateDiagram-v2` text - paste
5
6
  * straight into a `.md` file's ```mermaid fence or a live editor. `"*"`
@@ -296,8 +297,15 @@ function extractDefineBlockGenerics(src) {
296
297
  * already is for `check`, unlike `chain`, which spawns a child process
297
298
  * specifically because it also EXECUTES Blocks, not just imports them.
298
299
  */
299
- export async function discoverGraph(projectDir) {
300
- const files = discoverBlocks(projectDir);
300
+ export async function discoverGraph(projectDir, opts) {
301
+ let files = discoverBlocks(projectDir);
302
+ const select = opts?.blocksSelect;
303
+ // Glob/bare: drop files early. Regex needs block names — filter per export below.
304
+ if (select && select.kind !== "regex") {
305
+ files = files.filter((file) => matchBlocksSelect(select, {
306
+ relFile: relative(projectDir, file).replace(/\\/g, "/"),
307
+ }));
308
+ }
301
309
  const nodeTags = new Set();
302
310
  const edges = [];
303
311
  const skipped = [];
@@ -326,6 +334,13 @@ export async function discoverGraph(projectDir) {
326
334
  if (!isBlockLike(exported))
327
335
  continue;
328
336
  const blockName = exported.name;
337
+ if (select &&
338
+ !matchBlocksSelect(select, {
339
+ relFile: relFile.replace(/\\/g, "/"),
340
+ blockName,
341
+ })) {
342
+ continue;
343
+ }
329
344
  if (isNavBlockMarked(exported)) {
330
345
  try {
331
346
  // NavBlock / PageBlock resolve() ignores input - always checkpoint(tag).
package/dist/index.d.ts CHANGED
@@ -9,3 +9,5 @@ export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGr
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
+ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
13
+ export type { BlocksSelect, BlocksSelectKind } from "./blocks-select.js";
package/dist/index.js CHANGED
@@ -4,3 +4,4 @@ export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
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
5
  export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
+ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
@@ -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;
@@ -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);transition:opacity .3s ease;}" +
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.evaluate(({ box, label }) => window.__wgPositionRing?.(box, label), { box, label }).catch(() => { });
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.evaluate(({ x, y }) => window.__wgClickPulse?.(x, y), { x, y }).catch(() => { });
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(livePage);
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(livePage, box, cursorMs(500));
152
- await showRing(livePage, box, label);
153
- await new Promise((r) => setTimeout(r, 200));
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
- // Prefer a single fill - clear+pressSequentially can hang when the auto
160
- // panel steals focus mid-type (looks stuck on "Running: submit-login").
161
- if (slowMo || typeDelay() === 0) {
162
- await originalFill.call(this, value, options);
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
- else {
165
- try {
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(livePage);
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(livePage);
187
- await this.waitFor({ state: "visible", timeout: options?.timeout ?? 15000 }).catch(() => { });
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 navLabel = await livePage.evaluate(() => window.__wgPendingNavClickLabel ?? null);
193
- if (navLabel)
194
- label = String(navLabel);
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(livePage, box, cursorMs(600));
205
- await showRing(livePage, box, label);
206
- await new Promise((r) => setTimeout(r, clickPrePop()));
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(livePage, clickPoint.x, clickPoint.y);
214
- await new Promise((r) => setTimeout(r, 200));
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((r) => setTimeout(r, clickPostPop()));
218
- await hideRing(livePage).catch(() => { });
225
+ await new Promise((res) => setTimeout(res, clickPostPop()));
226
+ await hideRing(page);
219
227
  return result;
220
228
  };
221
229
  }
@@ -17,6 +17,8 @@ export interface TraverseOptions {
17
17
  headed?: boolean;
18
18
  /** Traverse id for greppable lines (default traverse-1). */
19
19
  traverseId?: string;
20
+ /** Phase C: glob / regex / bare `--blocks` file select. */
21
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
20
22
  }
21
23
  /**
22
24
  * Serial graph crawl. Prints greppable PASS/FAIL lines; non-zero exit on break.
@@ -106,7 +106,11 @@ export async function runTraverse(projectDir, options = {}) {
106
106
  const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
107
107
  const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
108
108
  const startUrl = options.startUrl ?? baseURL;
109
- const { graph, library } = await buildExploreContext(projectDir);
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
+ }
110
114
  const mem = new MemPage();
111
115
  seedMem(library.byName, mem, options.data);
112
116
  const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",