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.
- package/dist/auto-explore-run.d.ts +2 -0
- package/dist/auto-explore-run.js +6 -1
- package/dist/auto-explore.d.ts +6 -2
- package/dist/auto-explore.js +15 -5
- package/dist/blocks-select.d.ts +22 -0
- package/dist/blocks-select.js +85 -0
- package/dist/cli.js +91 -43
- package/dist/engine.d.ts +42 -0
- package/dist/engine.js +1 -1
- package/dist/graph.d.ts +4 -1
- package/dist/graph.js +17 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/step-overlay.d.ts +3 -3
- package/dist/step-overlay.js +60 -52
- package/dist/traverse-run.d.ts +2 -0
- package/dist/traverse-run.js +5 -1
- package/package.json +1 -1
|
@@ -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"`
|
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", () => {
|
|
@@ -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 });
|
package/dist/auto-explore.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}>;
|
package/dist/auto-explore.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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";
|
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
|
}
|
package/dist/traverse-run.d.ts
CHANGED
|
@@ -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.
|
package/dist/traverse-run.js
CHANGED
|
@@ -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 });
|