waygraph 0.12.9 → 0.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@ export interface AutoExploreOptions {
4
4
  baseURL?: string;
5
5
  /** Initial URL when the page is blank (defaults to baseURL). */
6
6
  startUrl?: string;
7
+ /** Phase C: glob / regex / bare `--blocks` file select for discovery. */
8
+ blocksSelect?: import("./blocks-select.js").BlocksSelect;
7
9
  }
8
10
  /**
9
11
  * Saucedemo-only defaults. Exact key names only — never substring `"login"`
@@ -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
  // ---------------------------------------------------------------------------
@@ -3673,7 +3674,8 @@ Primary (less is more):
3673
3674
 
3674
3675
  Also:
3675
3676
  waygraph list | nav | validate | check | graph | init <name>
3676
- 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
3677
3679
  --from <Checkpoint> Seed / start checkpoint
3678
3680
  --data '{...}' Mem seed
3679
3681
  --max-steps N Cap Block runs (default 50)
@@ -3697,6 +3699,8 @@ Examples:
3697
3699
  waygraph auto src/flows/shop.flow.ts --data '{...}' # run by file (same as run)
3698
3700
  waygraph auto --cli --data '{"saucedemo.credentials":{...}}'
3699
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
3700
3704
  waygraph run --blocks "loginFlow then add-all-to-cart" --data '{...}' --video
3701
3705
 
3702
3706
  Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto --blocks A B;
@@ -3790,6 +3794,7 @@ async function main() {
3790
3794
  let maxEdge;
3791
3795
  let headed = false;
3792
3796
  let baseUrl;
3797
+ let blocksFilter;
3793
3798
  let projectDir = process.cwd();
3794
3799
  for (let i = 0; i < rest.length; i++) {
3795
3800
  const a = rest[i];
@@ -3801,6 +3806,20 @@ async function main() {
3801
3806
  data = a.startsWith("--data=") ? a.slice(7) : rest[++i];
3802
3807
  continue;
3803
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
+ }
3804
3823
  if (a === "--max-steps" || a.startsWith("--max-steps=")) {
3805
3824
  const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
3806
3825
  maxSteps = Number(v);
@@ -3832,6 +3851,7 @@ async function main() {
3832
3851
  Serial graph crawl (RFC Phase B). Walks unused legal edges until a leaf,
3833
3852
  budget kill, or first broken edge.
3834
3853
 
3854
+ --blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
3835
3855
  --from <Checkpoint> start checkpoint (optional)
3836
3856
  --data '{...}' Mem seed JSON
3837
3857
  --max-steps N default 50
@@ -3840,6 +3860,10 @@ budget kill, or first broken edge.
3840
3860
  --non-headless show browser
3841
3861
  --base-url URL
3842
3862
 
3863
+ Examples:
3864
+ waygraph traverse --blocks '**/mailpit/**/*.block.ts'
3865
+ waygraph traverse --blocks '/mailpit|login/'
3866
+
3843
3867
  PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
3844
3868
  FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3845
3869
  `);
@@ -3853,6 +3877,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3853
3877
  console.error(`waygraph traverse: no such directory: ${projectDir}`);
3854
3878
  process.exit(1);
3855
3879
  }
3880
+ const blocksSelect = blocksFilter
3881
+ ? parseBlocksSelect(blocksFilter)
3882
+ : undefined;
3856
3883
  const code = await runTraverse(projectDir, {
3857
3884
  ...(from ? { from } : {}),
3858
3885
  ...(data ? { data } : {}),
@@ -3865,6 +3892,7 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
3865
3892
  : {}),
3866
3893
  headed,
3867
3894
  ...(baseUrl ? { baseURL: baseUrl } : {}),
3895
+ ...(blocksSelect ? { blocksSelect } : {}),
3868
3896
  });
3869
3897
  process.exit(code);
3870
3898
  }
@@ -4024,13 +4052,23 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
4024
4052
  process.exit(1);
4025
4053
  }
4026
4054
  // auto --blocks From To (graph path-find + run)
4055
+ // auto --blocks '/regex/' or '**/*.block.ts' (Phase C file select → explore)
4027
4056
  if (command === "auto" && (flags.blocksFromTo || flags.blocks)) {
4028
4057
  if (flags.blocksFromTo) {
4029
4058
  const [fromTag, toTag] = flags.blocksFromTo;
4030
4059
  await runChainAuto(proj, fromTag, toTag);
4031
4060
  break;
4032
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
+ }
4033
4070
  console.error("waygraph auto --blocks expects <fromCheckpoint> <toCheckpoint>\n" +
4071
+ " or file select: --blocks '**/mailpit/**/*.block.ts' / --blocks '/mailpit/'\n" +
4034
4072
  ' e.g. waygraph auto --blocks LoginPage OrderComplete\n' +
4035
4073
  ' for a hand-named chain use: waygraph run --blocks "a then b"\n' +
4036
4074
  " or: waygraph auto src/flows/shop.flow.ts");
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.10",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",