waygraph 0.15.18 → 0.15.20

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 (45) hide show
  1. package/README.md +68 -18
  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/dist/practices-check.d.ts +1 -1
  14. package/dist/practices-check.js +36 -0
  15. package/examples/saucedemo/.waygraph-auto/11c497cd.log +44 -0
  16. package/examples/saucedemo/.waygraph-auto/29d11a49.log +44 -0
  17. package/examples/saucedemo/.waygraph-auto/2c9a4003.log +44 -0
  18. package/examples/saucedemo/.waygraph-auto/32ef32a8.log +44 -0
  19. package/examples/saucedemo/.waygraph-auto/36ba01f7.log +44 -0
  20. package/examples/saucedemo/.waygraph-auto/4c6b9e15.log +0 -0
  21. package/examples/saucedemo/.waygraph-auto/537af83d.log +44 -0
  22. package/examples/saucedemo/.waygraph-auto/5adfacd5.log +44 -0
  23. package/examples/saucedemo/.waygraph-auto/76c7d418.log +44 -0
  24. package/examples/saucedemo/.waygraph-auto/78197960.log +44 -0
  25. package/examples/saucedemo/.waygraph-auto/7c2acbc2.log +44 -0
  26. package/examples/saucedemo/.waygraph-auto/7eded669.log +0 -0
  27. package/examples/saucedemo/.waygraph-auto/844b63ff.log +44 -0
  28. package/examples/saucedemo/.waygraph-auto/88a4200b.log +0 -0
  29. package/examples/saucedemo/.waygraph-auto/b0eed995.log +44 -0
  30. package/examples/saucedemo/.waygraph-auto/bb69dc1a.log +44 -0
  31. package/examples/saucedemo/.waygraph-auto/bc4ae56f.log +0 -0
  32. package/examples/saucedemo/.waygraph-auto/c17253b0.log +44 -0
  33. package/examples/saucedemo/.waygraph-auto/c31a9465.log +0 -0
  34. package/examples/saucedemo/.waygraph-auto/c5ac2393.log +44 -0
  35. package/examples/saucedemo/.waygraph-auto/c701e3c2.log +0 -0
  36. package/examples/saucedemo/.waygraph-auto/d207a000.log +44 -0
  37. package/examples/saucedemo/.waygraph-auto/fa3c5655.log +44 -0
  38. package/examples/saucedemo/package-lock.json +20 -532
  39. package/examples/saucedemo/package.json +0 -1
  40. package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +23 -0
  41. package/package.json +1 -1
  42. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -0
  43. package/templates/skills/waygraph-convention.skill.md +18 -4
  44. package/examples/saucedemo/.waygraph-auto/4adfbe91.json +0 -8
  45. /package/examples/saucedemo/.waygraph-auto/{4adfbe91.log → 32e2c26f.log} +0 -0
package/README.md CHANGED
@@ -249,6 +249,40 @@ A Block's `requires: MemKey[]` gets checked by `preflight()` (built into `runGra
249
249
  before a tab even opens - a missing credential fails in milliseconds, not after several
250
250
  real browser actions already ran.
251
251
 
252
+ ### memStub - fake values for `requires`, opt-in per Flow
253
+
254
+ Hand-supplying `--data '{...}'` every run gets old fast for a `requires` key that's
255
+ genuinely arbitrary (a username, an email, a name) rather than something the run actually
256
+ depends on. `registerMemStub(key, fake)` registers a generator for one `MemKey` (bring your
257
+ own - Faker.js or anything else; waygraph takes no dependency on it); `withMemStub(flow)`
258
+ opts that Flow into filling any `requires` key with no `--data` coverage from that registry
259
+ instead of failing preflight. A key with no registration still fails exactly as before -
260
+ this narrows the case that needs real input, it doesn't remove the guard:
261
+
262
+ ```typescript
263
+ import { registerMemStub, withMemStub } from "waygraph";
264
+ import { faker } from "@faker-js/faker";
265
+
266
+ registerMemStub(LoginCreds.key, () => ({
267
+ username: faker.internet.userName(),
268
+ password: faker.internet.password(),
269
+ }));
270
+
271
+ export const demoLoginFlow = withMemStub(loginFlow);
272
+ ```
273
+ ```bash
274
+ waygraph run demoLoginFlow # zero --data - fills from the registry
275
+ waygraph run loginFlow --mem-stub # same effect without a Flow-level wrapper -
276
+ # --mem-stub / WAYGRAPH_MEM_STUB=1 force it
277
+ # for one CLI call, opt-in or not
278
+ ```
279
+ `--data` always wins for a key it actually covers - memStub only fills what's left over.
280
+ Registered by `MemKey` *identity*, not by name (matching `MemPage`'s own store): two keys
281
+ sharing a debug name never cross-contaminate. For a bare library `flow.run(mem)` call
282
+ outside the CLI, call `seedMemStub(mem, flow)` yourself before `run()` when
283
+ `flow.memStub` is true - `run()` never reads Flow flags itself, same as
284
+ `resetSession`/`title`/every other `with*` wrapper.
285
+
252
286
  ### Engine config and pluggable browsers
253
287
 
254
288
  `new Engine({ headless, browserName, slowMo })` configures a browser the Engine launches
@@ -298,10 +332,26 @@ code, an autonomous agent authoring Blocks) - it warns, never fails the process.
298
332
 
299
333
  **`waygraph typecheck [project]`** runs `tsc --noEmit` then the same **bad-practice** scan
300
334
  (wildcard `Checkpoint<string>`, assert blocks missing type args, multi-`.fill()` Methods,
301
- fill+click in one Method). Practices warn only unless `tsc` fails; use `--no-practices` to
302
- skip the scan. Opt out per file: `// waygraph-ignore-practices` or
303
- `// waygraph-ignore: multi-input, combined-action`. Print full agent guidance:
304
- `waygraph --skill-convention`.
335
+ fill+click in one Method, empty `verify` on a transition Block or `defineAssertBlock`).
336
+ Practices warn only unless `tsc` fails; use `--no-practices` to skip the scan. Opt out per
337
+ file: `// waygraph-ignore-practices` or `// waygraph-ignore: multi-input, combined-action,
338
+ empty-verify`. Print full agent guidance: `waygraph --skill-convention`.
339
+
340
+ **Empty verify is the one that matters most for codegen'd Blocks.** A framework can enforce
341
+ that a check *exists* and stays structurally honest (typed, wired, right file) - it cannot
342
+ author the semantic content of that check, since only someone who knows what "success" means
343
+ for this specific app can write it. That's a correct place to draw the line, but it shifts
344
+ all the risk onto whether anything catches a Block left with a placeholder `verify: []` by
345
+ the time it ships - which would otherwise pass every static check, the orphan scan, and
346
+ `tsc`, and run green in CI forever, because an empty array isn't a wrong check, it's the
347
+ *absence* of one. `waygraph check`/`typecheck` now flag exactly that: a `defineMethodBlock`/
348
+ `defineEffectBlock` whose `In`/`Out` Checkpoints actually differ (a real transition, not a
349
+ self-loop like `fill-username`) with no `verify` key or a literal `verify: []`, and the same
350
+ for `defineAssertBlock` (whose entire job is checking). Silence it deliberately, the same as
351
+ any other practice warning, with `// waygraph-ignore: empty-verify` - e.g. when a downstream
352
+ `defineAssertBlock` right after it in the `.flow.ts` is what actually confirms the transition
353
+ (see `templates/scaffold`'s own `open-message.method.block.ts`, confirmed by
354
+ `AssertEmailContentBlock` right after it, not by its own `verify`).
305
355
 
306
356
  ### Demo narration
307
357
 
@@ -998,25 +1048,22 @@ really a Nav/Page/Assert/Method from this package's own factories.
998
1048
  |------|---------|
999
1049
  | Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
1000
1050
  | Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
1001
- | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx create-waygraph my-app` **or** `npx waygraph init my-app` |
1051
+ | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx waygraph init my-app` |
1002
1052
  | Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
1003
1053
  | Print a packaged agent skill to stdout | `npx waygraph --skill` (list) / `--skill-pilot` / `--skill-pilot-blind` / `--skill-convention` |
1004
1054
  | Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
1005
1055
 
1006
- **Scaffold is not hidden inside waygraph alone** - the offline starter also ships as
1007
- [`create-waygraph`](https://github.com/deviate-dv8/create-waygraph) on npm (same template
1008
- as `waygraph init` since 0.7.5). Use whichever entry you already have:
1009
- `npx create-waygraph my-app` when reading the GitHub Pages docs, or `npx waygraph init my-app`
1010
- when the CLI is already installed. After either:
1011
-
1012
1056
  ```bash
1057
+ npx waygraph init my-app
1013
1058
  cd my-app && npm install && npx playwright install chromium && npm test
1014
1059
  ```
1015
1060
 
1016
1061
  `try demo` is different: live saucedemo.com, step-through **Sign In -> Shop & Checkout ->
1017
1062
  blocked Viewer login**, then headless tests. Permanent full example:
1018
- [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init` /
1019
- `create-waygraph`.
1063
+ [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init`.
1064
+
1065
+ **`create-waygraph` (npm) is deprecated** - it was the identical `init` template shipped as
1066
+ a second package for no real benefit. Use `npx waygraph init my-app` instead.
1020
1067
 
1021
1068
  ```bash
1022
1069
  # From this checkout, after npm run build:
@@ -1090,8 +1137,8 @@ suite - it forwards to the local `node_modules/.bin/playwright` (falling back to
1090
1137
  `npx playwright` if the project hasn't installed it locally yet), so `waygraph test` behaves
1091
1138
  exactly like `playwright test`, and anything after it (`--grep`, a spec path, `--headed`, …)
1092
1139
  passes straight through. `waygraph test ui` (or `waygraph test --ui`) launches Playwright's
1093
- interactive UI Mode the same way. Scaffolded projects (`waygraph init`/`create-waygraph`)
1094
- wire this up as `npm test` / `npm run test:ui`, and ship `trace: "retain-on-failure"` plus
1140
+ interactive UI Mode the same way. Scaffolded projects (`waygraph init`) wire this up as
1141
+ `npm test` / `npm run test:ui`, and ship `trace: "retain-on-failure"` plus
1095
1142
  the `html` reporter in `playwright.config.ts`, so a failing `waygraph test` already has a
1096
1143
  trace waiting - `waygraph test report` opens the HTML report, and
1097
1144
  `waygraph test show-trace <trace.zip>` opens one trace directly (both forward to Playwright's
@@ -1122,7 +1169,9 @@ Flows are files (0.10.5+):
1122
1169
  | Attach to existing session (graph only) | `waygraph pilot attach <sessionId>` |
1123
1170
  | Path-find | `waygraph auto --blocks LoginPage OrderComplete` |
1124
1171
  | Run `--cli` as a background session (legacy) | `waygraph auto --cli --detach` -> `{sessionId, socketPath}` |
1125
- | Drive a detached session (no TTY) | `waygraph browser send <sessionId> "<pick>"` -> JSON state |
1172
+ | Drive a detached session (no TTY) | `waygraph browser send <sessionId> "<pick>"` -> JSON state (`<pick>` is a menu index or a Block name) |
1173
+ | Paint highlight rings (shorthand) | `waygraph browser highlight <sessionId> "#x\|Label\|tone; .y\|Label2"` |
1174
+ | Paint highlight rings (full JSON) | `waygraph browser highlight <sessionId> '{"rings":[{"selector":"#x","label":"X"}]}'` |
1126
1175
  | Read a detached session's state | `waygraph browser status <sessionId>` (no side effects) |
1127
1176
  | Reattach a terminal to a detached session | `waygraph browser attach <sessionId>` |
1128
1177
  | Read the live page (aria, small/default) | `waygraph auto dom <sessionId>` |
@@ -1161,7 +1210,8 @@ Long FR/AC checklists used to bury highlight rings. **Smart defaults are on:**
1161
1210
 
1162
1211
  Per-knob env also: `WAYGRAPH_TODO_COMPACT`, `WAYGRAPH_TODO_CAP` (default 5),
1163
1212
  `WAYGRAPH_TODO_EXPAND_CAP` (default 14). Author API: `ctx.todoDockUi({ ... })`.
1164
- Pilot `auto highlight` accepts the same knobs as `todoUi` on the JSON body.
1213
+ `browser highlight` (same as `auto`/`pilot highlight` - identical command, see "auto vs
1214
+ browser vs pilot" above) accepts the same knobs as `todoUi` on the JSON body.
1165
1215
 
1166
1216
  Run `waygraph --help` (or any subcommand with no args) for the full flag reference kept
1167
1217
  in `src/cli.ts`'s own `usage()` - that's the source of truth for flags, this table is the
@@ -1321,7 +1371,7 @@ src/
1321
1371
  index.ts public barrel
1322
1372
  examples/saucedemo/ live Sauce Demo (Page + methods/ + Sel)
1323
1373
  templates/quickstart/ try demo / init template (mirrors sauce)
1324
- templates/scaffold/ offline init/create-waygraph scaffold template
1374
+ templates/scaffold/ offline waygraph init scaffold template
1325
1375
  templates/agents/ waygraph agent-dive coding-agent definitions
1326
1376
  docs/ GitHub Pages static HTML + docs/proposals/ RFCs
1327
1377
  openspec/ spec-driven planning: specs/ (current), changes/ (in-flight),
@@ -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";