waygraph 0.12.9 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/auto-explore-run.d.ts +2 -0
- package/dist/auto-explore-run.js +5 -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 +73 -22
- package/dist/engine.d.ts +6 -3
- 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/traverse-run.d.ts +2 -0
- package/dist/traverse-run.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,11 +215,13 @@ overridable, not one giant Block with no per-step confirmation. `composedBlock.w
|
|
|
215
215
|
`modStepVerify` patch one step from outside, addressed the same three ways
|
|
216
216
|
`withBlockVerify`/`modBlockVerify` already are - a `composeBlock` result is a plain Block,
|
|
217
217
|
so it drops straight into `defineFlow([start, ..., composed, ..., end])` like any other.
|
|
218
|
-
`fastForwardComposeBlock(name, steps)` is the same shape marked for **
|
|
219
|
-
opaque step (no per-inner gate
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
`fastForwardComposeBlock(name, steps)` is the same shape marked for **wall-clock
|
|
219
|
+
fast-forward**: one opaque demo step that **blitzes** (no per-inner gate, no smooth
|
|
220
|
+
cursor theater, Playwright slowMo off for that flow unless `WAYGRAPH_SLOWMO` is set)
|
|
221
|
+
unless `waygraph demo --ff-expand`. Put **several** FF units in one Flow when you need
|
|
222
|
+
to race past auth, then a heavy dashboard settle, then watch the interesting middle.
|
|
223
|
+
Saucedemo `checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays
|
|
224
|
+
expanded for Sign In narration. See `docs/proposals/traverse-ffcompose-rfc.md`.
|
|
223
225
|
`spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
|
|
224
226
|
Block/Flow, in the same browser context an existing `page` already belongs to - the
|
|
225
227
|
pattern `observe()` could already reach for (it's the only phase allowed to touch
|
|
@@ -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
|
@@ -491,7 +491,11 @@ export async function runAutoExplore(projectDir, options = {}) {
|
|
|
491
491
|
const cli = options.cli === true;
|
|
492
492
|
const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
|
|
493
493
|
const startUrl = options.startUrl ?? baseURL;
|
|
494
|
-
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
|
+
}
|
|
495
499
|
const mem = new MemPage();
|
|
496
500
|
seedDefaultMem(library.byName, mem);
|
|
497
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
|
// ---------------------------------------------------------------------------
|
|
@@ -1744,10 +1745,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1744
1745
|
// seconds. When slowMo is already doing the pacing, add none of our own;
|
|
1745
1746
|
// only fall back to a small typing delay when slowMo is off entirely.
|
|
1746
1747
|
//
|
|
1747
|
-
// pacing.skipTheater (WAYGRAPH_FAST_BLOCKS
|
|
1748
|
-
// typing -
|
|
1749
|
-
// and must NOT
|
|
1750
|
-
//
|
|
1748
|
+
// pacing.skipTheater (WAYGRAPH_FAST_BLOCKS **or** FFCompose): near-zero
|
|
1749
|
+
// cursor / pop / typing - blitz this step. --fast alone sets gatesFast only
|
|
1750
|
+
// (shorter Next/autoplay gates) and must NOT skip theater, or the whole demo
|
|
1751
|
+
// looks like plain waygraph run.
|
|
1751
1752
|
const skipTheater = () => !!pacing.skipTheater;
|
|
1752
1753
|
const typeDelay = () => (skipTheater() ? 0 : slowMo ? 0 : 30);
|
|
1753
1754
|
// Click is a single action, not per-character, so it doesn't compound
|
|
@@ -1756,7 +1757,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
1756
1757
|
// rather than stacking a full 1.2s on top of that.
|
|
1757
1758
|
const clickPrePop = () => (skipTheater() ? 0 : slowMo ? 300 : 700);
|
|
1758
1759
|
const clickPostPop = () => (skipTheater() ? 0 : slowMo ? 200 : 500);
|
|
1759
|
-
|
|
1760
|
+
// FF / FAST_BLOCKS: no cursor travel animation (0). Plain theater keeps full.
|
|
1761
|
+
const cursorMs = (full) => (skipTheater() ? 0 : full);
|
|
1760
1762
|
// Locator.fill()/click() only ever see a raw call, no context of where
|
|
1761
1763
|
// the value came from. Patching mem.get() to remember the most recently
|
|
1762
1764
|
// read key's name (Blocks read-then-immediately-fill, e.g. const { email
|
|
@@ -1983,14 +1985,15 @@ async function resetPageState(context, page, baseURL) {
|
|
|
1983
1985
|
async function runStepMode(engine, start, end, context, page, mem, resolved, slowMo, title, fastBlockNames, clearSession, baseURL) {
|
|
1984
1986
|
// A shared, mutable pacing knob the interaction patches read live (per
|
|
1985
1987
|
// call, not once at setup) - flipped per-block below so one block (e.g.
|
|
1986
|
-
// WAYGRAPH_FAST_BLOCKS=login) can run through with
|
|
1987
|
-
// own added dwell while the rest of the chain keeps
|
|
1988
|
-
//
|
|
1989
|
-
// only OUR added pauses (ring pop, cursor travel, typing delay) shrink
|
|
1990
|
-
// when skipTheater is on.
|
|
1988
|
+
// WAYGRAPH_FAST_BLOCKS=login or an FFCompose unit) can run through with
|
|
1989
|
+
// none of the overlay's own added dwell while the rest of the chain keeps
|
|
1990
|
+
// the full theatrical pace.
|
|
1991
1991
|
// --fast / WAYGRAPH_DEMO_FAST: shorter auto-next / Next gates ONLY -
|
|
1992
1992
|
// keeps smooth cursor travel (not akin to waygraph run).
|
|
1993
|
-
// WAYGRAPH_FAST_BLOCKS:
|
|
1993
|
+
// FFCompose + WAYGRAPH_FAST_BLOCKS: skipTheater (blitz overlay delays).
|
|
1994
|
+
// When the flow has any opaque FF unit and WAYGRAPH_SLOWMO is unset,
|
|
1995
|
+
// launch slowMo is 0 so Playwright itself does not re-tax every click
|
|
1996
|
+
// inside the FF (that was the "30s login / cursor gone but still slow" bug).
|
|
1994
1997
|
const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
|
|
1995
1998
|
const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
|
|
1996
1999
|
const pacing = { gatesFast: demoFast, skipTheater: false };
|
|
@@ -2080,8 +2083,11 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2080
2083
|
// gates too, not just skip its interaction dwell - otherwise autoplay
|
|
2081
2084
|
// still stalls the full autoplayMs admiring a step that intentionally
|
|
2082
2085
|
// ran too fast to watch.
|
|
2083
|
-
const ms =
|
|
2084
|
-
|
|
2086
|
+
const ms = pacing.skipTheater
|
|
2087
|
+
? Math.min(80, autoplayMs)
|
|
2088
|
+
: pacing.gatesFast
|
|
2089
|
+
? Math.min(400, autoplayMs)
|
|
2090
|
+
: autoplayMs;
|
|
2085
2091
|
let elapsed = 0;
|
|
2086
2092
|
for (;;) {
|
|
2087
2093
|
// Re-read the checkbox EVERY loop tick, not once up front - a human
|
|
@@ -2165,9 +2171,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2165
2171
|
await resetPageState(context, page, baseURL);
|
|
2166
2172
|
}
|
|
2167
2173
|
// gatesFast: --fast (whole run) or FFCompose opaque steps (one short gate).
|
|
2168
|
-
// skipTheater:
|
|
2174
|
+
// skipTheater: WAYGRAPH_FAST_BLOCKS **or** FFCompose — wall-clock blitz
|
|
2175
|
+
// (overlay delays). --fast alone never sets skipTheater.
|
|
2169
2176
|
pacing.gatesFast = demoFast || !!r.block.fastForward;
|
|
2170
|
-
pacing.skipTheater = fastBlockNames.has(r.block.name);
|
|
2177
|
+
pacing.skipTheater = fastBlockNames.has(r.block.name) || !!r.block.fastForward;
|
|
2171
2178
|
const fixtures = r.highlightFixtures;
|
|
2172
2179
|
stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
|
|
2173
2180
|
const isNavBlock = r.block.__waygraphKind === "nav";
|
|
@@ -2637,13 +2644,20 @@ async function main() {
|
|
|
2637
2644
|
// nobody has to watch a live window for it to be right).
|
|
2638
2645
|
const headed =
|
|
2639
2646
|
process.env.WAYGRAPH_HEADED === "0" ? false : process.env.WAYGRAPH_HEADED === "1" || step;
|
|
2640
|
-
// Step mode defaults to a visible pace (
|
|
2641
|
-
//
|
|
2642
|
-
//
|
|
2647
|
+
// Step mode defaults to a visible Playwright pace (slowMo 350) UNLESS the
|
|
2648
|
+
// flow has an opaque fastForwardComposeBlock — then default slowMo 0 so FF
|
|
2649
|
+
// auth/seed actually blitzes (<10s target) instead of only hiding the cursor
|
|
2650
|
+
// while every click still pays 350ms. Non-FF steps keep overlay theater
|
|
2651
|
+
// (cursor/pop) for watchability. Explicit WAYGRAPH_SLOWMO always wins.
|
|
2652
|
+
const hasOpaqueFf = resolved.some(
|
|
2653
|
+
(r) => r && r.block && r.block.fastForward === true,
|
|
2654
|
+
);
|
|
2643
2655
|
const slowMo = process.env.WAYGRAPH_SLOWMO !== undefined
|
|
2644
2656
|
? Number(process.env.WAYGRAPH_SLOWMO)
|
|
2645
2657
|
: step
|
|
2646
|
-
?
|
|
2658
|
+
? hasOpaqueFf
|
|
2659
|
+
? 0
|
|
2660
|
+
: 350
|
|
2647
2661
|
: undefined;
|
|
2648
2662
|
const baseURL = process.env.WAYGRAPH_BASE_URL;
|
|
2649
2663
|
const title = process.env.WAYGRAPH_TITLE;
|
|
@@ -2651,8 +2665,8 @@ async function main() {
|
|
|
2651
2665
|
// should blow past the overlay's own added dwell (ring pop, cursor
|
|
2652
2666
|
// travel, typing delay, and that block's own gates) - "I want the login
|
|
2653
2667
|
// block to be faster than the rest of the demo," Dan's own phrase.
|
|
2654
|
-
//
|
|
2655
|
-
//
|
|
2668
|
+
// FFCompose blocks get the same skipTheater automatically (no need to
|
|
2669
|
+
// list them here). Distinct from --fast (gates only).
|
|
2656
2670
|
const fastBlockNames = new Set(
|
|
2657
2671
|
(process.env.WAYGRAPH_FAST_BLOCKS || "")
|
|
2658
2672
|
.split(",")
|
|
@@ -3673,7 +3687,8 @@ Primary (less is more):
|
|
|
3673
3687
|
|
|
3674
3688
|
Also:
|
|
3675
3689
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3676
|
-
waygraph traverse [project] Serial graph crawl (Phase B)
|
|
3690
|
+
waygraph traverse [project] Serial graph crawl (Phase B+)
|
|
3691
|
+
--blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
|
|
3677
3692
|
--from <Checkpoint> Seed / start checkpoint
|
|
3678
3693
|
--data '{...}' Mem seed
|
|
3679
3694
|
--max-steps N Cap Block runs (default 50)
|
|
@@ -3697,6 +3712,8 @@ Examples:
|
|
|
3697
3712
|
waygraph auto src/flows/shop.flow.ts --data '{...}' # run by file (same as run)
|
|
3698
3713
|
waygraph auto --cli --data '{"saucedemo.credentials":{...}}'
|
|
3699
3714
|
waygraph auto --blocks LoginPage OrderComplete
|
|
3715
|
+
waygraph auto --blocks '/mailpit/' # Phase C filtered explore
|
|
3716
|
+
waygraph traverse --blocks '**/mailpit/**/*.block.ts' # Phase C filtered crawl
|
|
3700
3717
|
waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
|
|
3701
3718
|
|
|
3702
3719
|
Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
|
|
@@ -3790,6 +3807,7 @@ async function main() {
|
|
|
3790
3807
|
let maxEdge;
|
|
3791
3808
|
let headed = false;
|
|
3792
3809
|
let baseUrl;
|
|
3810
|
+
let blocksFilter;
|
|
3793
3811
|
let projectDir = process.cwd();
|
|
3794
3812
|
for (let i = 0; i < rest.length; i++) {
|
|
3795
3813
|
const a = rest[i];
|
|
@@ -3801,6 +3819,20 @@ async function main() {
|
|
|
3801
3819
|
data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
|
|
3802
3820
|
continue;
|
|
3803
3821
|
}
|
|
3822
|
+
if (a === "--blocks" || a.startsWith("--blocks=")) {
|
|
3823
|
+
if (a.startsWith("--blocks=")) {
|
|
3824
|
+
blocksFilter = a.slice("--blocks=".length);
|
|
3825
|
+
}
|
|
3826
|
+
else {
|
|
3827
|
+
const v = rest[++i];
|
|
3828
|
+
if (!v || v.startsWith("-")) {
|
|
3829
|
+
console.error("waygraph traverse: --blocks needs a glob, /regex/, or path substring");
|
|
3830
|
+
process.exit(1);
|
|
3831
|
+
}
|
|
3832
|
+
blocksFilter = v;
|
|
3833
|
+
}
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3804
3836
|
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
3805
3837
|
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
3806
3838
|
maxSteps = Number(v);
|
|
@@ -3832,6 +3864,7 @@ async function main() {
|
|
|
3832
3864
|
Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
|
|
3833
3865
|
budget kill, or first broken edge.
|
|
3834
3866
|
|
|
3867
|
+
--blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
|
|
3835
3868
|
--from <Checkpoint> start checkpoint (optional)
|
|
3836
3869
|
--data '{...}' Mem seed JSON
|
|
3837
3870
|
--max-steps N default 50
|
|
@@ -3840,6 +3873,10 @@ budget kill, or first broken edge.
|
|
|
3840
3873
|
--non-headless show browser
|
|
3841
3874
|
--base-url URL
|
|
3842
3875
|
|
|
3876
|
+
Examples:
|
|
3877
|
+
waygraph traverse --blocks '**/mailpit/**/*.block.ts'
|
|
3878
|
+
waygraph traverse --blocks '/mailpit|login/'
|
|
3879
|
+
|
|
3843
3880
|
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
3844
3881
|
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
3845
3882
|
`);
|
|
@@ -3853,6 +3890,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
3853
3890
|
console.error(`waygraph traverse: no such directory: ${projectDir}`);
|
|
3854
3891
|
process.exit(1);
|
|
3855
3892
|
}
|
|
3893
|
+
const blocksSelect = blocksFilter
|
|
3894
|
+
? parseBlocksSelect(blocksFilter)
|
|
3895
|
+
: undefined;
|
|
3856
3896
|
const code = await runTraverse(projectDir, {
|
|
3857
3897
|
...(from ? { from } : {}),
|
|
3858
3898
|
...(data ? { data } : {}),
|
|
@@ -3865,6 +3905,7 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
3865
3905
|
: {}),
|
|
3866
3906
|
headed,
|
|
3867
3907
|
...(baseUrl ? { baseURL: baseUrl } : {}),
|
|
3908
|
+
...(blocksSelect ? { blocksSelect } : {}),
|
|
3868
3909
|
});
|
|
3869
3910
|
process.exit(code);
|
|
3870
3911
|
}
|
|
@@ -4024,13 +4065,23 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
|
|
|
4024
4065
|
process.exit(1);
|
|
4025
4066
|
}
|
|
4026
4067
|
// auto --blocks From To (graph path-find + run)
|
|
4068
|
+
// auto --blocks '/regex/' or '**/*.block.ts' (Phase C file select → explore)
|
|
4027
4069
|
if (command === "auto" && (flags.blocksFromTo || flags.blocks)) {
|
|
4028
4070
|
if (flags.blocksFromTo) {
|
|
4029
4071
|
const [fromTag, toTag] = flags.blocksFromTo;
|
|
4030
4072
|
await runChainAuto(proj, fromTag, toTag);
|
|
4031
4073
|
break;
|
|
4032
4074
|
}
|
|
4075
|
+
if (flags.blocks && isFileSelectToken(flags.blocks)) {
|
|
4076
|
+
const blocksSelect = parseBlocksSelect(flags.blocks);
|
|
4077
|
+
const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
|
|
4078
|
+
await runAutoExplore(proj, baseURL
|
|
4079
|
+
? { cli: cliPicker, baseURL, blocksSelect }
|
|
4080
|
+
: { cli: cliPicker, blocksSelect });
|
|
4081
|
+
break;
|
|
4082
|
+
}
|
|
4033
4083
|
console.error("waygraph auto --blocks expects <fromCheckpoint> <toCheckpoint>\n" +
|
|
4084
|
+
" or file select: --blocks '**/mailpit/**/*.block.ts' / --blocks '/mailpit/'\n" +
|
|
4034
4085
|
' e.g. waygraph auto --blocks LoginPage OrderComplete\n' +
|
|
4035
4086
|
' for a hand-named chain use: waygraph run --blocks "a then b"\n' +
|
|
4036
4087
|
" or: waygraph auto src/flows/shop.flow.ts");
|
package/dist/engine.d.ts
CHANGED
|
@@ -351,10 +351,13 @@ export declare function composeBlock<In extends Checkpoint<string>, B extends Ch
|
|
|
351
351
|
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): ComposedBlock<In, Out>;
|
|
352
352
|
/**
|
|
353
353
|
* Same shape as {@link composeBlock}, marked {@link FastForwardComposedBlock.fastForward}
|
|
354
|
-
* so demo/run/traverse treat it as **one opaque step**
|
|
355
|
-
*
|
|
354
|
+
* so demo/run/traverse treat it as **one opaque step** that **blitzes wall-clock**
|
|
355
|
+
* (skip overlay theater + no Playwright slowMo on that step's acts) unless
|
|
356
|
+
* `--ff-expand` / `WAYGRAPH_FF_EXPAND=1`. Inner acts still run for real;
|
|
356
357
|
* failures are prefixed `ff-name > …` so the locus names both the FF unit and
|
|
357
|
-
* the inner step. Prefer this for boring prefixes (auth, seed
|
|
358
|
+
* the inner step. Prefer this for boring or heavy-check prefixes (auth, seed,
|
|
359
|
+
* dashboard settle) — you may put **several** FF units in one Flow with normal
|
|
360
|
+
* Blocks between them.
|
|
358
361
|
* @example fastForwardComposeBlock("ff-owner-auth", [NavLoginBlock, SubmitLoginForFlow])
|
|
359
362
|
*/
|
|
360
363
|
export interface FastForwardComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends ComposedBlock<In, Out> {
|
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/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 });
|