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 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 **demo pacing**: one
219
- opaque step (no per-inner gate) unless `waygraph demo --ff-expand`. Saucedemo
220
- `checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
221
- Sign In narration. Traverse (later) reuses FF as a seed prefix - see
222
- `docs/proposals/traverse-ffcompose-rfc.md`.
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"`
@@ -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 });
@@ -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
  // ---------------------------------------------------------------------------
@@ -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 only): near-zero cursor / pop /
1748
- // typing - blow past this block. --fast sets pacing.gatesFast instead
1749
- // and must NOT land here, or demo looks like plain waygraph run.
1750
- // stubBeforeRef.current = resolved stubBefore slots for the active block.
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
- const cursorMs = (full) => (skipTheater() ? Math.min(120, full) : full);
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 none of the overlay's
1987
- // own added dwell while the rest of the chain keeps the full theatrical
1988
- // pace. Playwright's own slowMo is process-wide and untouched by this -
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: named blocks still skip theater (old blow-past).
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
- pacing.gatesFast || pacing.skipTheater ? Math.min(400, autoplayMs) : autoplayMs;
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: only WAYGRAPH_FAST_BLOCKS - never --fast, never FF by default.
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 (each fill/click actually shows on
2641
- // screen instead of snapping in) unless the human overrides it - an
2642
- // explicit WAYGRAPH_SLOWMO=0 still means "off".
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
- ? 350
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
- // Playwright's real slowMo is untouched; this only trims what waygraph
2655
- // itself adds on top of it. Distinct from --fast (gates only).
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** (no per-inner gate/ring)
355
- * unless `--ff-expand` / `WAYGRAPH_FF_EXPAND=1`. Inner acts still run for real;
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) on demo Flows.
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): 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";
@@ -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.9",
3
+ "version": "0.12.11",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",