waygraph 0.15.19 → 0.15.21

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.
Files changed (47) hide show
  1. package/README.md +72 -1309
  2. package/dist/auto-session.d.ts +10 -1
  3. package/dist/auto-session.js +27 -7
  4. package/dist/cli.js +98 -31
  5. package/dist/engine.d.ts +20 -0
  6. package/dist/engine.js +18 -0
  7. package/dist/highlight-shorthand.d.ts +24 -0
  8. package/dist/highlight-shorthand.js +30 -0
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +2 -1
  11. package/dist/mem-stub.d.ts +41 -0
  12. package/dist/mem-stub.js +36 -0
  13. package/examples/saucedemo/.waygraph-auto/11c497cd.log +44 -0
  14. package/examples/saucedemo/.waygraph-auto/29d11a49.log +44 -0
  15. package/examples/saucedemo/.waygraph-auto/2c9a4003.log +44 -0
  16. package/examples/saucedemo/.waygraph-auto/32ef32a8.log +44 -0
  17. package/examples/saucedemo/.waygraph-auto/36ba01f7.log +44 -0
  18. package/examples/saucedemo/.waygraph-auto/537af83d.log +44 -0
  19. package/examples/saucedemo/.waygraph-auto/5adfacd5.log +44 -0
  20. package/examples/saucedemo/.waygraph-auto/76c7d418.log +44 -0
  21. package/examples/saucedemo/.waygraph-auto/78197960.log +44 -0
  22. package/examples/saucedemo/.waygraph-auto/7c2acbc2.log +44 -0
  23. package/examples/saucedemo/.waygraph-auto/844b63ff.log +44 -0
  24. package/examples/saucedemo/.waygraph-auto/88a4200b.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/b0eed995.log +44 -0
  26. package/examples/saucedemo/.waygraph-auto/bb69dc1a.log +44 -0
  27. package/examples/saucedemo/.waygraph-auto/bc4ae56f.log +0 -0
  28. package/examples/saucedemo/.waygraph-auto/c17253b0.log +44 -0
  29. package/examples/saucedemo/.waygraph-auto/c31a9465.log +0 -0
  30. package/examples/saucedemo/.waygraph-auto/c5ac2393.log +44 -0
  31. package/examples/saucedemo/.waygraph-auto/c701e3c2.log +0 -0
  32. package/examples/saucedemo/.waygraph-auto/d207a000.log +44 -0
  33. package/examples/saucedemo/.waygraph-auto/fa3c5655.log +44 -0
  34. package/examples/saucedemo/package-lock.json +20 -532
  35. package/examples/saucedemo/package.json +0 -1
  36. package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +23 -0
  37. package/package.json +1 -1
  38. package/templates/scaffold/README.md +21 -19
  39. package/templates/scaffold/STRUCTURE.md +37 -70
  40. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +1 -1
  41. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +1 -1
  42. package/templates/skills/waygraph-convention.skill.md +5 -1
  43. package/templates/skills/waygraph-pilot.skill.md +1 -1
  44. package/examples/saucedemo/.waygraph-auto/4adfbe91.json +0 -8
  45. package/examples/saucedemo/.waygraph-auto/9f0287e6.json +0 -8
  46. /package/examples/saucedemo/.waygraph-auto/{4adfbe91.log → 32e2c26f.log} +0 -0
  47. /package/examples/saucedemo/.waygraph-auto/{9f0287e6.log → 4c6b9e15.log} +0 -0
@@ -31,8 +31,17 @@ export type ParsedPick = PickResult | {
31
31
  /**
32
32
  * Same number/`q`/range rules `cliPick` already applies, but returning a
33
33
  * result instead of looping to re-prompt - one request gets one answer.
34
+ * Also accepts a Block name (exact, case-sensitive) as a shorthand for its
35
+ * menu index - `send <id> "submit-login"` instead of hunting the number.
36
+ * Ambiguous (the same Block name appearing more than once in one menu)
37
+ * refuses rather than guessing which occurrence was meant - a pick runs a
38
+ * real browser action, not something to silently get wrong.
34
39
  */
35
- export declare function parsePick(raw: string, menuLength: number): ParsedPick;
40
+ export declare function parsePick(raw: string, menu: {
41
+ readonly flat: readonly {
42
+ readonly block: string;
43
+ }[];
44
+ }): ParsedPick;
36
45
  export interface DomSnapshot {
37
46
  mode: "aria" | "full";
38
47
  selector?: string;
@@ -37,18 +37,38 @@ export function buildSessionSnapshot(menu, library, here, lastRunNote) {
37
37
  /**
38
38
  * Same number/`q`/range rules `cliPick` already applies, but returning a
39
39
  * result instead of looping to re-prompt - one request gets one answer.
40
+ * Also accepts a Block name (exact, case-sensitive) as a shorthand for its
41
+ * menu index - `send <id> "submit-login"` instead of hunting the number.
42
+ * Ambiguous (the same Block name appearing more than once in one menu)
43
+ * refuses rather than guessing which occurrence was meant - a pick runs a
44
+ * real browser action, not something to silently get wrong.
40
45
  */
41
- export function parsePick(raw, menuLength) {
42
- const ans = raw.trim().toLowerCase();
43
- if (ans === "q" || ans === "quit")
46
+ export function parsePick(raw, menu) {
47
+ const ans = raw.trim();
48
+ const lower = ans.toLowerCase();
49
+ if (lower === "q" || lower === "quit")
44
50
  return { type: "quit" };
45
- const n = Number(ans);
46
- if (Number.isInteger(n) && n >= 1 && n <= menuLength) {
51
+ const n = Number(lower);
52
+ if (Number.isInteger(n) && n >= 1 && n <= menu.flat.length) {
47
53
  return { type: "pick", index: n - 1 };
48
54
  }
55
+ const matches = [];
56
+ menu.flat.forEach((edge, i) => {
57
+ if (edge.block === ans)
58
+ matches.push(i);
59
+ });
60
+ if (matches.length === 1) {
61
+ return { type: "pick", index: matches[0] };
62
+ }
63
+ if (matches.length > 1) {
64
+ return {
65
+ type: "invalid",
66
+ reason: `"${raw}" matches ${matches.length} menu entries - use the numeric index instead of the Block name`,
67
+ };
68
+ }
49
69
  return {
50
70
  type: "invalid",
51
- reason: `"${raw}" is not a valid pick - enter a number from 1 to ${menuLength}, or "q" to quit`,
71
+ reason: `"${raw}" is not a valid pick - enter a number from 1 to ${menu.flat.length}, a Block name, or "q" to quit`,
52
72
  };
53
73
  }
54
74
  const FULL_MODE_DEFAULT_DEPTH = 12;
@@ -499,7 +519,7 @@ export class AutoSession {
499
519
  /** Applies exactly one pick: runs at most one Block, returns the resulting state. */
500
520
  async applyPick(raw) {
501
521
  const menu = await this.currentMenu();
502
- const pick = parsePick(raw, menu.flat.length);
522
+ const pick = parsePick(raw, menu);
503
523
  if (pick.type === "invalid")
504
524
  return { ok: false, error: pick.reason };
505
525
  if (pick.type === "quit") {
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ import { runAgentDive, printSkillIndex, readSkillMarkdown, SKILL_FLAG_MAP, } fro
31
31
  import { runTraverse } from "./traverse-run.js";
32
32
  import { parseMinEdgeCoverage } from "./traverse-coverage.js";
33
33
  import { isFileSelectToken, parseBlocksSelect, } from "./blocks-select.js";
34
+ import { parseHighlightShorthand } from "./highlight-shorthand.js";
34
35
  // ---------------------------------------------------------------------------
35
36
  // Filesystem
36
37
  // ---------------------------------------------------------------------------
@@ -242,6 +243,7 @@ import {
242
243
  resolveDeviceState,
243
244
  resolveTodoDockUi,
244
245
  WAYGRAPH_RING_CSS,
246
+ getMemStub,
245
247
  } from "waygraph";
246
248
 
247
249
  function walkDir(dir, pattern) {
@@ -341,7 +343,7 @@ function parseChainSpec(spec) {
341
343
  });
342
344
  }
343
345
 
344
- function seedMemFromRequires(mem, requires, json, label) {
346
+ function seedMemFromRequires(mem, requires, json, label, stubActive) {
345
347
  if (requires.length === 0) {
346
348
  if (json !== undefined) {
347
349
  throw new Error(
@@ -353,21 +355,34 @@ function seedMemFromRequires(mem, requires, json, label) {
353
355
  if (json === undefined) {
354
356
  // --data / WAYGRAPH_DATA supplies the same payload shape as blockName({...}).
355
357
  const fromEnv = process.env.WAYGRAPH_DATA;
356
- if (fromEnv && fromEnv.trim()) {
357
- json = fromEnv;
358
- } else {
358
+ if (fromEnv && fromEnv.trim()) json = fromEnv;
359
+ }
360
+
361
+ let parsed;
362
+ let haveParsed = false;
363
+ if (json !== undefined) {
364
+ try {
365
+ parsed = JSON.parse(json);
366
+ haveParsed = true;
367
+ } catch (err) {
368
+ throw new Error("waygraph chain: \\"" + label + "\\" payload is not valid JSON - " + String(err));
369
+ }
370
+ }
371
+
372
+ if (!haveParsed) {
373
+ // No payload at all - only a hard failure when memStub isn't active for
374
+ // this flow; when it is, fall through with an empty object so the
375
+ // per-key loop below gets a chance to fill each one from the registry.
376
+ if (!stubActive) {
359
377
  throw new Error(
360
378
  "waygraph chain: \\"" + label + "\\" requires " + requires.map((k) => k.name).join(", ") +
361
- " - give a JSON payload (inline blockName({...}) or --data '{...}')",
379
+ " - give a JSON payload (inline blockName({...}) or --data '{...}'), or enable --mem-stub" +
380
+ " with registerMemStub for these keys",
362
381
  );
363
382
  }
383
+ parsed = {};
364
384
  }
365
- let parsed;
366
- try {
367
- parsed = JSON.parse(json);
368
- } catch (err) {
369
- throw new Error("waygraph chain: \\"" + label + "\\" payload is not valid JSON - " + String(err));
370
- }
385
+
371
386
  // Keyed-by-name when every require name is a top-level key (preferred for
372
387
  // --data and for {"saucedemo.credentials":{...}} even with one require).
373
388
  if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
@@ -377,26 +392,42 @@ function seedMemFromRequires(mem, requires, json, label) {
377
392
  return;
378
393
  }
379
394
  }
380
- if (requires.length === 1) {
395
+ // Single-key shorthand: the whole payload IS that one key's value - only
396
+ // when a real payload was actually given (the stub-only "{}" fallback
397
+ // above must fall through to per-key stub resolution instead).
398
+ if (haveParsed && requires.length === 1) {
381
399
  mem.set(requires[0], parsed);
382
400
  return;
383
401
  }
384
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
402
+ if (haveParsed && (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))) {
385
403
  throw new Error(
386
404
  "waygraph chain: \\"" + label + "\\" requires " + requires.length + " keys (" +
387
405
  requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name",
388
406
  );
389
407
  }
408
+ const stillMissing = [];
390
409
  for (const k of requires) {
391
- if (!(k.name in parsed)) {
392
- throw new Error("waygraph chain: \\"" + label + "\\" payload is missing required key \\"" + k.name + "\\"");
410
+ if (k.name in parsed) {
411
+ mem.set(k, parsed[k.name]);
412
+ continue;
413
+ }
414
+ const fake = stubActive ? getMemStub(k) : undefined;
415
+ if (fake) {
416
+ mem.set(k, fake());
417
+ continue;
393
418
  }
394
- mem.set(k, parsed[k.name]);
419
+ stillMissing.push(k);
420
+ }
421
+ if (stillMissing.length > 0) {
422
+ throw new Error(
423
+ "waygraph chain: \\"" + label + "\\" payload is missing required key(s): " +
424
+ stillMissing.map((k) => "\\"" + k.name + "\\"").join(", "),
425
+ );
395
426
  }
396
427
  }
397
428
 
398
- function seedMemForBlock(mem, resolved, json) {
399
- seedMemFromRequires(mem, resolved.block.requires ?? [], json, resolved.exportName);
429
+ function seedMemForBlock(mem, resolved, json, stubActive) {
430
+ seedMemFromRequires(mem, resolved.block.requires ?? [], json, resolved.exportName, stubActive);
400
431
  }
401
432
 
402
433
  /**
@@ -406,14 +437,14 @@ function seedMemForBlock(mem, resolved, json) {
406
437
  * name) and seeds them all from one JSON payload, same keyed-by-name shape
407
438
  * as a multi-key Block payload already uses.
408
439
  */
409
- function seedMemForFlow(mem, flowBlocks, json, label) {
440
+ function seedMemForFlow(mem, flowBlocks, json, label, stubActive) {
410
441
  const seen = new Map();
411
442
  for (const bi of flowBlocks) {
412
443
  for (const k of bi.block.requires ?? []) {
413
444
  if (!seen.has(k.name)) seen.set(k.name, k);
414
445
  }
415
446
  }
416
- seedMemFromRequires(mem, Array.from(seen.values()), json, label);
447
+ seedMemFromRequires(mem, Array.from(seen.values()), json, label, stubActive);
417
448
  }
418
449
 
419
450
  // ---------------------------------------------------------------------------
@@ -5898,7 +5929,8 @@ async function main() {
5898
5929
  // Same --data / WAYGRAPH_DATA seeding as the multi-segment path.
5899
5930
  // Without this, \`waygraph run shop.flow.ts --data '{...}'\` (and bare
5900
5931
  // export names) hit preflight with an empty MemPage.
5901
- const seedMem = () => seedMemForFlow(mem, blockInfos, undefined, bareRef);
5932
+ const stubActive = flow.memStub === true || process.env.WAYGRAPH_MEM_STUB === "1";
5933
+ const seedMem = () => seedMemForFlow(mem, blockInfos, undefined, bareRef, stubActive);
5902
5934
  resolved = blockInfos.map((bi, idx) => ({
5903
5935
  block: bi.block,
5904
5936
  exportName: bi.name,
@@ -5969,6 +6001,7 @@ async function main() {
5969
6001
  if (flow && typeof flow.blocks === "function") {
5970
6002
  flows.push(flow);
5971
6003
  episodeCounter += 1;
6004
+ const stubActive = flow.memStub === true || process.env.WAYGRAPH_MEM_STUB === "1";
5972
6005
  flowMeta.push({
5973
6006
  episodeNumber: episodeCounter,
5974
6007
  episodeTitle: flow.title || seg.ref,
@@ -5976,18 +6009,21 @@ async function main() {
5976
6009
  highlightFixtures: flow.highlightFixtures,
5977
6010
  demoPace: flow.demoPace,
5978
6011
  highlightStyle: flow.highlightStyle,
5979
- seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
6012
+ seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref, stubActive),
5980
6013
  });
5981
6014
  } else {
5982
6015
  const r = await findBlock(projectDir, seg.ref);
5983
6016
  flows.push(wrapEngine.defineFlow([start, r.block, end]));
6017
+ // A bare Block segment (no Flow, no withMemStub to opt in) only gets
6018
+ // memStub via the global --mem-stub/WAYGRAPH_MEM_STUB override.
6019
+ const stubActive = process.env.WAYGRAPH_MEM_STUB === "1";
5984
6020
  flowMeta.push({
5985
6021
  episodeNumber: undefined,
5986
6022
  episodeTitle: undefined,
5987
6023
  highlightFixtures: undefined,
5988
6024
  demoPace: undefined,
5989
6025
  highlightStyle: undefined,
5990
- seedMem: () => seedMemForBlock(mem, r, seg.json),
6026
+ seedMem: () => seedMemForBlock(mem, r, seg.json, stubActive),
5991
6027
  });
5992
6028
  }
5993
6029
  }
@@ -6889,6 +6925,9 @@ function parseRunFlags(argv) {
6889
6925
  out.data = t.value;
6890
6926
  i = t.nextI;
6891
6927
  }
6928
+ else if (a === "--mem-stub") {
6929
+ out.memStub = true;
6930
+ }
6892
6931
  else if (a === "--video" || a.startsWith("--video=")) {
6893
6932
  if (a.startsWith("--video=")) {
6894
6933
  out.video = a.slice("--video=".length);
@@ -7016,6 +7055,9 @@ function applyRunFlags(flags, opts) {
7016
7055
  if (flags.data !== undefined) {
7017
7056
  process.env.WAYGRAPH_DATA = flags.data;
7018
7057
  }
7058
+ if (flags.memStub) {
7059
+ process.env.WAYGRAPH_MEM_STUB = "1";
7060
+ }
7019
7061
  if (flags.fast) {
7020
7062
  process.env.WAYGRAPH_DEMO_FAST = "1";
7021
7063
  }
@@ -7233,7 +7275,12 @@ Primary (less is more):
7233
7275
  --cli --detach --non-headless Detached session with a real visible browser
7234
7276
  (same --non-headless flag run/demo already use)
7235
7277
  waygraph auto send <sessionId> "<pick>" Send one pick to a --detach session, print
7236
- the resulting state as JSON (no TTY needed)
7278
+ the resulting state as JSON (no TTY needed).
7279
+ <pick> is a 1-based menu index, a Block name
7280
+ (exact match; ambiguous names refuse rather than
7281
+ guess), or "q"/"quit". Same command as
7282
+ browser send <sessionId> "<pick>" (prefer that
7283
+ prefix for a --detach/persistent session).
7237
7284
  --timeout <ms> Override the 15s default wait for this one call -
7238
7285
  a single Block can legitimately run a slow real
7239
7286
  interaction (e.g. a multi-step mouse drag)
@@ -7297,6 +7344,15 @@ Primary (less is more):
7297
7344
  label,tone?,size?,weight?}], todos[], todoIndex?,
7298
7345
  todoTitle?, holdMs? (0=until next), clear:true.
7299
7346
  Missing selectors listed in response, not fatal.
7347
+ Same command as browser highlight <sessionId>
7348
+ '<json>' (prefer that prefix for a --detach/
7349
+ persistent session).
7350
+ <selector>|<label>[|<tone>] Shorthand for 1-3 rings, no JSON braces/quotes
7351
+ to escape - rings separated by ";", e.g.
7352
+ "#x|Login button|warning; .err|Error banner|danger".
7353
+ Covers selector/label/tone only; anything else
7354
+ (size/weight/zoom/focus/todos/...) needs the JSON
7355
+ form above.
7300
7356
  waygraph browser Show browser subcommands
7301
7357
  waygraph browser start Open a new session (headful by default, about:blank)
7302
7358
  --inject preset|path Merge an external Block library (e.g. saucedemo)
@@ -7316,6 +7372,10 @@ Primary (less is more):
7316
7372
  layer; use browser/pilot for agent-driven sessions.
7317
7373
  waygraph demo [--blocks <flow|file|spec>] Watch with step overlay (QA path)
7318
7374
  --data '{...}' Mem seed JSON (or inline flow({...}))
7375
+ --mem-stub Fill any requires key --data didn't cover from
7376
+ registerMemStub's registry (needs the key
7377
+ registered, or the flow's own withMemStub - a
7378
+ key with neither still fails preflight)
7319
7379
  --auto-next Auto-advance steps (alias: --autoplay)
7320
7380
  --fast Shorter auto-next / Next gates (keeps smooth cursor)
7321
7381
  --full Classic wrap-all block chips (default: carousel)
@@ -7332,6 +7392,7 @@ Primary (less is more):
7332
7392
  --title / --base-url
7333
7393
  waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
7334
7394
  --data '{...}'
7395
+ --mem-stub Same as demo's --mem-stub
7335
7396
  --non-headless Show browser
7336
7397
  --video [dir] Record .webm
7337
7398
  --video-viewport WxH Recording size (default run: 1280x720)
@@ -7460,7 +7521,7 @@ async function main() {
7460
7521
  " waygraph run src/flows/shop.flow.ts\n" +
7461
7522
  " waygraph run --blocks shopFlow\n" +
7462
7523
  " waygraph list # file → export map\n" +
7463
- " Flags: --blocks --data --non-headless --video [dir] --step/--no-step");
7524
+ " Flags: --blocks --data --mem-stub --non-headless --video [dir] --step/--no-step");
7464
7525
  process.exit(1);
7465
7526
  }
7466
7527
  const proj = flags.blocks
@@ -7779,7 +7840,7 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
7779
7840
  console.error("waygraph demo: missing flow/spec — e.g.\n" +
7780
7841
  " waygraph demo src/flows/shop.flow.ts\n" +
7781
7842
  " waygraph demo --blocks shopFlow\n" +
7782
- " Flags: --blocks --data --auto-next --fast --full --mini --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
7843
+ " Flags: --blocks --data --mem-stub --auto-next --fast --full --mini --ff-expand --ff-disabled --auto-play-video --title --base-url --video");
7783
7844
  process.exit(1);
7784
7845
  }
7785
7846
  const proj = flags.blocks
@@ -8223,19 +8284,25 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
8223
8284
  if (sub === "highlight") {
8224
8285
  const raw = args[3];
8225
8286
  if (raw === undefined) {
8226
- console.error('waygraph auto highlight: usage: waygraph auto highlight <sessionId> \'{"rings":[{"selector":"#x","label":"X"}]}\'');
8287
+ console.error(`waygraph ${command} highlight: usage:\n` +
8288
+ ` waygraph ${command} highlight <sessionId> '{"rings":[{"selector":"#x","label":"X"}]}'\n` +
8289
+ ` waygraph ${command} highlight <sessionId> "#x|X" (shorthand: <selector>|<label>[|<tone>], rings separated by ";")`);
8227
8290
  process.exit(1);
8228
8291
  }
8229
8292
  let fixtures;
8230
8293
  try {
8231
8294
  fixtures = JSON.parse(raw);
8232
8295
  }
8233
- catch {
8234
- console.error("waygraph auto highlight: body must be valid JSON");
8235
- process.exit(1);
8296
+ catch (jsonErr) {
8297
+ const shorthand = parseHighlightShorthand(raw);
8298
+ if (shorthand.type === "error") {
8299
+ console.error(`waygraph ${command} highlight: body is neither valid JSON (${jsonErr instanceof Error ? jsonErr.message : String(jsonErr)}) nor valid shorthand (${shorthand.reason})`);
8300
+ process.exit(1);
8301
+ }
8302
+ fixtures = shorthand.fixtures;
8236
8303
  }
8237
8304
  if (fixtures.op !== undefined && fixtures.op !== "highlight") {
8238
- console.error('waygraph auto highlight: do not set "op" (or set it to "highlight")');
8305
+ console.error(`waygraph ${command} highlight: do not set "op" (or set it to "highlight")`);
8239
8306
  process.exit(1);
8240
8307
  }
8241
8308
  const { op: _ignore, ...rest } = fixtures;
package/dist/engine.d.ts CHANGED
@@ -251,6 +251,15 @@ export interface Flow<Out extends Checkpoint<string>> {
251
251
  * Per-slot stub/fixture values win. `run()` ignores.
252
252
  */
253
253
  readonly highlightStyle?: import("./highlights.js").HighlightStyleDefaults;
254
+ /**
255
+ * Set once this Flow has been wrapped in {@link withMemStub} - opts into
256
+ * auto-filling `requires` keys with registered fake values
257
+ * (`registerMemStub`, `src/mem-stub.ts`) instead of failing preflight when
258
+ * the caller didn't supply them. `run()` never reads this - the CLI's own
259
+ * `--data`/`--mem-stub` seeding, or a bare-library caller's own explicit
260
+ * `seedMemStub(mem, flow)` call, is what actually fills mem before `run()`.
261
+ */
262
+ readonly memStub?: boolean;
254
263
  /**
255
264
  * This Flow's constituent Blocks, in order between `start`/`end`, as plain
256
265
  * data - for introspection/visualization tools, without running anything.
@@ -318,6 +327,17 @@ export declare function withSessionReset<Out extends Checkpoint<string>>(flow: F
318
327
  * @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
319
328
  */
320
329
  export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out>, title: string): Flow<Out>;
330
+ /**
331
+ * Opts this Flow into memStub: `requires` keys the caller didn't supply get
332
+ * filled from `registerMemStub`'s registry (`src/mem-stub.ts`) instead of
333
+ * failing preflight, for any key that has a registered fake generator - a
334
+ * required key with no registration still fails exactly as before.
335
+ * Non-destructive, same pattern as {@link withTitle}. `run()` never reads
336
+ * this itself - the CLI's `--data`/`--mem-stub` seeding, or a bare-library
337
+ * caller's own explicit `seedMemStub(mem, flow)` call, does the filling.
338
+ * @example const demoLogin = withMemStub(loginFlow);
339
+ */
340
+ export declare function withMemStub<Out extends Checkpoint<string>>(flow: Flow<Out>): Flow<Out>;
321
341
  /**
322
342
  * Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
323
343
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
package/dist/engine.js CHANGED
@@ -401,6 +401,24 @@ export function withTitle(flow, title) {
401
401
  modBlockVerify: (block, nameOrIndex, newCheck) => withTitle(flow.modBlockVerify(block, nameOrIndex, newCheck), title),
402
402
  };
403
403
  }
404
+ /**
405
+ * Opts this Flow into memStub: `requires` keys the caller didn't supply get
406
+ * filled from `registerMemStub`'s registry (`src/mem-stub.ts`) instead of
407
+ * failing preflight, for any key that has a registered fake generator - a
408
+ * required key with no registration still fails exactly as before.
409
+ * Non-destructive, same pattern as {@link withTitle}. `run()` never reads
410
+ * this itself - the CLI's `--data`/`--mem-stub` seeding, or a bare-library
411
+ * caller's own explicit `seedMemStub(mem, flow)` call, does the filling.
412
+ * @example const demoLogin = withMemStub(loginFlow);
413
+ */
414
+ export function withMemStub(flow) {
415
+ return {
416
+ ...flow,
417
+ memStub: true,
418
+ withBlockVerify: (block, verify) => withMemStub(flow.withBlockVerify(block, verify)),
419
+ modBlockVerify: (block, nameOrIndex, newCheck) => withMemStub(flow.modBlockVerify(block, nameOrIndex, newCheck)),
420
+ };
421
+ }
404
422
  /**
405
423
  * Demo pacing for a whole Flow episode (fast review vs slow gap/wrong walkthrough).
406
424
  * Non-destructive. `fastForwardComposeBlock` is always blitz for its own step;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Compact alternative to full-JSON `browser highlight` payloads, for the
3
+ * common case (a couple of rings, each just selector + label + maybe tone).
4
+ * Full JSON stays fully supported for anything needing size/weight/zoom/
5
+ * focus/detail/tag/todos - this only ever covers a subset.
6
+ *
7
+ * Grammar: `<selector>|<label>[|<tone>]` rings joined by `;`.
8
+ * "#submit|Login button|warning; .error|Error banner|danger"
9
+ *
10
+ * `;` and `|` were picked because neither is valid inside a CSS/Playwright
11
+ * selector or an ordinary label - unlike `,`/`:`, which real selectors use
12
+ * constantly (`:nth-child(2)`, `:not(.x)`, `[href*="mailto:"]`, compound
13
+ * `a, b` lists). A selector/label containing a literal `;` or `|` needs the
14
+ * full-JSON form instead - documented, not silently mishandled.
15
+ */
16
+ import type { PilotHighlightFixtures } from "./pilot-overlay.js";
17
+ export type ParsedHighlightShorthand = {
18
+ type: "ok";
19
+ fixtures: PilotHighlightFixtures;
20
+ } | {
21
+ type: "error";
22
+ reason: string;
23
+ };
24
+ export declare function parseHighlightShorthand(raw: string): ParsedHighlightShorthand;
@@ -0,0 +1,30 @@
1
+ export function parseHighlightShorthand(raw) {
2
+ const tokens = raw
3
+ .split(";")
4
+ .map((t) => t.trim())
5
+ .filter((t) => t.length > 0);
6
+ if (tokens.length === 0) {
7
+ return { type: "error", reason: "empty highlight shorthand - give at least one <selector>|<label> ring" };
8
+ }
9
+ const rings = [];
10
+ for (let i = 0; i < tokens.length; i++) {
11
+ const token = tokens[i];
12
+ const parts = token.split("|").map((p) => p.trim());
13
+ if (parts.length < 2 || parts.length > 3) {
14
+ return {
15
+ type: "error",
16
+ reason: `ring ${i + 1} ("${token}") must be <selector>|<label> or <selector>|<label>|<tone> ` +
17
+ `- got ${parts.length} part(s) separated by "|"`,
18
+ };
19
+ }
20
+ const [selector, label, tone] = parts;
21
+ if (!selector) {
22
+ return { type: "error", reason: `ring ${i + 1} ("${token}") has an empty selector` };
23
+ }
24
+ if (!label) {
25
+ return { type: "error", reason: `ring ${i + 1} ("${token}") has an empty label` };
26
+ }
27
+ rings.push(tone ? { selector, label, tone } : { selector, label });
28
+ }
29
+ return { type: "ok", fixtures: { rings } };
30
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInstanceOption } from "./types.js";
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
+ export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
4
5
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
5
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
6
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
6
7
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
7
8
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, TodoDockUiResolved, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
8
9
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./engine.js";
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
+ export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
3
4
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
4
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
5
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
5
6
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
6
7
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
8
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * memStub: a registry of caller-supplied FAKE VALUES for required MemKeys, so
3
+ * a Flow can run/demo without hand-seeding every credential/name/email every
4
+ * time. Bring your own generator (Faker.js or anything else) - waygraph takes
5
+ * no dependency on one, it only stores and invokes an opaque `() => T`.
6
+ *
7
+ * "Stub" here is unrelated to the other two meanings already in this
8
+ * codebase: `stubBefore`/`stubAfter`/`stubOnError` (demo narration fixtures,
9
+ * see highlights.ts's `runStubPhase`) and `upload`'s `stub: "image"|"pdf"|
10
+ * "video"` (a placeholder file). This is a third, separate "stub."
11
+ *
12
+ * Opt-in only, per Flow (`withMemStub`, engine.ts) or per-run (`--mem-stub` /
13
+ * WAYGRAPH_MEM_STUB=1) - a required key with no registered generator still
14
+ * fails preflight exactly as before. Registered per MemKey *instance*
15
+ * (identity, not `.name`), matching MemPage's own store - two keys sharing a
16
+ * debug name must never cross-contaminate here either.
17
+ */
18
+ import type { MemKey } from "./mem-page.js";
19
+ import type { MemPage } from "./mem-page.js";
20
+ import type { Flow } from "./engine.js";
21
+ /**
22
+ * Registers a fake-value generator for one required MemKey.
23
+ * @example registerMemStub(LoginCreds.key, () => ({
24
+ * username: faker.internet.userName(),
25
+ * password: faker.internet.password(),
26
+ * }));
27
+ */
28
+ export declare function registerMemStub<T>(key: MemKey<T>, fake: () => T): void;
29
+ /** Used by waygraph's own `--mem-stub`-aware CLI seeding and by {@link seedMemStub} - flow authors normally use {@link registerMemStub}/{@link withMemStub} instead. */
30
+ export declare function getMemStub<T>(key: MemKey<T>): (() => T) | undefined;
31
+ /**
32
+ * For a bare library `flow.run(mem, ...)` call outside the CLI (which has its
33
+ * own `--mem-stub`-aware seeding) - call this yourself before `run()` when
34
+ * `flow.memStub` is true, the same way external tooling already reads
35
+ * `flow.resetSession`/`flow.title`/etc. (`run()` itself never reads Flow
36
+ * flags - see the `with*` helpers in engine.ts).
37
+ * @example
38
+ * if (flow.memStub) seedMemStub(mem, flow);
39
+ * await flow.run(mem, config);
40
+ */
41
+ export declare function seedMemStub(mem: MemPage, flow: Flow<any>): void;
@@ -0,0 +1,36 @@
1
+ const registry = new Map();
2
+ /**
3
+ * Registers a fake-value generator for one required MemKey.
4
+ * @example registerMemStub(LoginCreds.key, () => ({
5
+ * username: faker.internet.userName(),
6
+ * password: faker.internet.password(),
7
+ * }));
8
+ */
9
+ export function registerMemStub(key, fake) {
10
+ registry.set(key, fake);
11
+ }
12
+ /** Used by waygraph's own `--mem-stub`-aware CLI seeding and by {@link seedMemStub} - flow authors normally use {@link registerMemStub}/{@link withMemStub} instead. */
13
+ export function getMemStub(key) {
14
+ return registry.get(key);
15
+ }
16
+ /**
17
+ * For a bare library `flow.run(mem, ...)` call outside the CLI (which has its
18
+ * own `--mem-stub`-aware seeding) - call this yourself before `run()` when
19
+ * `flow.memStub` is true, the same way external tooling already reads
20
+ * `flow.resetSession`/`flow.title`/etc. (`run()` itself never reads Flow
21
+ * flags - see the `with*` helpers in engine.ts).
22
+ * @example
23
+ * if (flow.memStub) seedMemStub(mem, flow);
24
+ * await flow.run(mem, config);
25
+ */
26
+ export function seedMemStub(mem, flow) {
27
+ for (const bi of flow.blocks()) {
28
+ for (const k of bi.block.requires ?? []) {
29
+ if (mem.has(k))
30
+ continue;
31
+ const fake = registry.get(k);
32
+ if (fake)
33
+ mem.set(k, fake());
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,44 @@
1
+ waygraph auto: skip src/blocks/saucedemo-web/cart/nav-cart.block.ts: Requiring @playwright/test second time,
2
+ waygraph auto: skip src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts: Requiring @playwright/test second time,
3
+ waygraph auto: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts: Requiring @playwright/test second time,
4
+ waygraph auto: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts: Requiring @playwright/test second time,
5
+ waygraph auto: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts: Requiring @playwright/test second time,
6
+ waygraph auto: skip src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts: Requiring @playwright/test second time,
7
+ waygraph auto: skip src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts: Requiring @playwright/test second time,
8
+ waygraph auto: skip src/blocks/saucedemo-web/ff-owner-auth.block.ts: Requiring @playwright/test second time,
9
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/inventory.page.block.ts: Requiring @playwright/test second time,
10
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts: Requiring @playwright/test second time,
11
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts: Requiring @playwright/test second time,
12
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts: Requiring @playwright/test second time,
13
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/methods/remove-from-cart.effect.block.ts: Requiring @playwright/test second time,
14
+ waygraph auto: skip src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts: Requiring @playwright/test second time,
15
+ waygraph auto: skip src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts: Requiring @playwright/test second time,
16
+ waygraph auto: skip src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts: Requiring @playwright/test second time,
17
+ waygraph auto: skip src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts: Requiring @playwright/test second time,
18
+ waygraph auto: skip src/blocks/saucedemo-web/methods/fill-password.method.block.ts: Requiring @playwright/test second time,
19
+ waygraph auto: skip src/blocks/saucedemo-web/methods/fill-username.method.block.ts: Requiring @playwright/test second time,
20
+ waygraph auto: skip src/blocks/saucedemo-web/methods/submit-login.method.block.ts: Requiring @playwright/test second time,
21
+ waygraph auto: skip src/blocks/saucedemo-web/methods/submit-logout.method.block.ts: Requiring @playwright/test second time,
22
+ waygraph auto: skip src/blocks/saucedemo-web/nav-login.block.ts: Requiring @playwright/test second time,
23
+ waygraph graph: skip src/blocks/saucedemo-web/cart/nav-cart.block.ts: Requiring @playwright/test second time,
24
+ waygraph graph: skip src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts: Requiring @playwright/test second time,
25
+ waygraph graph: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-first-name.method.block.ts: Requiring @playwright/test second time,
26
+ waygraph graph: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-last-name.method.block.ts: Requiring @playwright/test second time,
27
+ waygraph graph: skip src/blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.ts: Requiring @playwright/test second time,
28
+ waygraph graph: skip src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts: Requiring @playwright/test second time,
29
+ waygraph graph: skip src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts: Requiring @playwright/test second time,
30
+ waygraph graph: skip src/blocks/saucedemo-web/ff-owner-auth.block.ts: Requiring @playwright/test second time,
31
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/inventory.page.block.ts: Requiring @playwright/test second time,
32
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts: Requiring @playwright/test second time,
33
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts: Requiring @playwright/test second time,
34
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts: Requiring @playwright/test second time,
35
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/methods/remove-from-cart.effect.block.ts: Requiring @playwright/test second time,
36
+ waygraph graph: skip src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts: Requiring @playwright/test second time,
37
+ waygraph graph: skip src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts: Requiring @playwright/test second time,
38
+ waygraph graph: skip src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts: Requiring @playwright/test second time,
39
+ waygraph graph: skip src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts: Requiring @playwright/test second time,
40
+ waygraph graph: skip src/blocks/saucedemo-web/methods/fill-password.method.block.ts: Requiring @playwright/test second time,
41
+ waygraph graph: skip src/blocks/saucedemo-web/methods/fill-username.method.block.ts: Requiring @playwright/test second time,
42
+ waygraph graph: skip src/blocks/saucedemo-web/methods/submit-login.method.block.ts: Requiring @playwright/test second time,
43
+ waygraph graph: skip src/blocks/saucedemo-web/methods/submit-logout.method.block.ts: Requiring @playwright/test second time,
44
+ waygraph graph: skip src/blocks/saucedemo-web/nav-login.block.ts: Requiring @playwright/test second time,