waygraph 0.7.16 → 0.10.1

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 (83) hide show
  1. package/README.md +90 -52
  2. package/dist/auto-explore-run.js +13 -7
  3. package/dist/auto-explore.js +47 -36
  4. package/dist/cli.d.ts +8 -12
  5. package/dist/cli.js +229 -249
  6. package/dist/engine.d.ts +53 -11
  7. package/dist/engine.js +81 -8
  8. package/dist/graph.d.ts +2 -1
  9. package/dist/graph.js +51 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1 -1
  12. package/examples/saucedemo/README.md +19 -5
  13. package/examples/saucedemo/docs/CONVENTION.md +22 -18
  14. package/examples/saucedemo/docs/HELPERS.md +152 -8
  15. package/examples/saucedemo/package-lock.json +1 -1
  16. package/examples/saucedemo/package.json +11 -5
  17. package/examples/saucedemo/scripts/capture-wrong-login.mjs +1 -1
  18. package/examples/saucedemo/scripts/drive-auto-cli.mjs +93 -0
  19. package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +1 -1
  20. package/examples/saucedemo/scripts/show-auto-menu.mjs +72 -0
  21. package/examples/saucedemo/src/blocks/SITE-MAP.md +11 -8
  22. package/examples/saucedemo/src/blocks/saucedemo-web/NAV.md +3 -3
  23. package/examples/saucedemo/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  24. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  25. package/{templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
  26. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  27. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  28. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  29. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  30. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  31. package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
  32. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  33. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  34. package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
  35. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +4 -4
  36. package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  37. package/examples/saucedemo/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  38. package/{templates/quickstart/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
  39. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -0
  40. package/examples/saucedemo/src/flows/cart-mem.flow.ts +1 -1
  41. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  42. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +4 -4
  43. package/examples/saucedemo/src/flows/checkout.flow.ts +4 -4
  44. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  45. package/examples/saucedemo/src/flows/shop.flow.ts +3 -3
  46. package/examples/saucedemo/src/flows/smoke.flow.ts +2 -2
  47. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  48. package/examples/saucedemo/tests/flow-block-verify.spec.ts +1 -1
  49. package/examples/saucedemo/tests/submit-login-branch.spec.ts +1 -1
  50. package/package.json +1 -1
  51. package/templates/quickstart/README-saucedemo-commands.md +72 -0
  52. package/templates/quickstart/docs/CONVENTION.md +5 -5
  53. package/templates/quickstart/docs/HELPERS.md +152 -8
  54. package/templates/quickstart/package.json +9 -5
  55. package/templates/quickstart/scripts/drive-auto-cli.mjs +93 -0
  56. package/templates/quickstart/src/blocks/SITE-MAP.md +11 -8
  57. package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +3 -3
  58. package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  59. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  60. package/{examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
  61. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  62. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  63. package/templates/quickstart/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  64. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  65. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  66. package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
  67. package/templates/quickstart/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  68. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  69. package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
  70. package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +1 -1
  71. package/templates/quickstart/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  72. package/templates/quickstart/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  73. package/{examples/saucedemo/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
  74. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -0
  75. package/templates/quickstart/src/flows/cart-mem.flow.ts +1 -1
  76. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  77. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +4 -4
  78. package/templates/quickstart/src/flows/checkout.flow.ts +4 -4
  79. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  80. package/templates/quickstart/src/flows/shop.flow.ts +3 -3
  81. package/templates/quickstart/src/flows/smoke.flow.ts +2 -2
  82. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  83. package/templates/scaffold/package.json +1 -1
package/dist/engine.d.ts CHANGED
@@ -561,12 +561,54 @@ export type NavClickBlock<Out extends Checkpoint<string>> = NavBlock<Out>;
561
561
  export declare function defineNavClickBlock<Out extends Checkpoint<string>>(options: NavClickBlockOptions<Out>): NavClickBlock<Out>;
562
562
  /**
563
563
  * TypeScript salt over {@link defineBlock} for one-shot non-nav app steps
564
- * (submit, finish, logout). Same runtime Block; marks `__waygraphSalt = "action"`
565
- * so auto/docs can tell Action from bare {@link defineBlock} (tests / unsure).
566
- * Does not force `instanceOptions` - use {@link defineEffectBlock} for that.
564
+ * (submit, upload, finish, logout) - **methods** on a page. Same runtime Block;
565
+ * marks `__waygraphSalt = "method"` so auto/docs can tell Method from bare
566
+ * {@link defineBlock}. Does not force `instanceOptions` - use
567
+ * {@link defineEffectBlock} when the auto menu should spawn one row per live
568
+ * DOM instance. Prefer hanging methods off a {@link definePageBlock} hub.
567
569
  */
568
- export type ActionBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> = DefinedBlock<In, Out>;
569
- export declare function defineActionBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: Parameters<typeof defineBlock<In, Out>>[0]): ActionBlock<In, Out>;
570
+ export type MethodBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> = DefinedBlock<In, Out>;
571
+ /** @deprecated Prefer {@link MethodBlock} - same type; Action was the 0.7 name. */
572
+ export type ActionBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> = MethodBlock<In, Out>;
573
+ export declare function defineMethodBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: Parameters<typeof defineBlock<In, Out>>[0]): MethodBlock<In, Out>;
574
+ /** @deprecated Prefer {@link defineMethodBlock} - identical helper (0.7 name). */
575
+ export declare const defineActionBlock: typeof defineMethodBlock;
576
+ /** One Block (or lazy factory) registered on a {@link PageBlock}. */
577
+ export type PageMethodEntry = DefinedBlock<any, any> | (() => DefinedBlock<any, any>);
578
+ export type PageBlockOptions<Out extends Checkpoint<string>> = {
579
+ name: string;
580
+ description?: string;
581
+ /** Checkpoint tag for this screen (hub). */
582
+ checkpoint: Out["__state"];
583
+ verify?: Trait[] | ((out: Out) => Trait[]);
584
+ highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
585
+ requires?: readonly MemKey<any>[];
586
+ /**
587
+ * Methods available on this page. Array, or a record of Blocks / `() => Block`
588
+ * factories (lazy so the page object can colocate method definitions).
589
+ */
590
+ methods?: readonly PageMethodEntry[] | Record<string, PageMethodEntry>;
591
+ /**
592
+ * Optional deep-link onto this page (`goto`). XOR with `click`. Omit when the
593
+ * hub is arrival-only (previous Block already resolved here).
594
+ */
595
+ url?: string | ((mem: MemPage) => string);
596
+ /** Optional click-nav onto this page. XOR with `url`. */
597
+ click?: string | ((mem: MemPage) => string);
598
+ instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
599
+ };
600
+ /**
601
+ * Page hub Block: one Checkpoint = this screen; methods hang off the hub for
602
+ * readability / auto grouping. Runtime is still a Block (`__waygraphKind = "page"`).
603
+ * `act` is goto/click when `url`/`click` is set, otherwise a no-op (already here).
604
+ * Child methods stay ordinary Blocks for graph edges - the page only registers them.
605
+ */
606
+ export type PageBlock<Out extends Checkpoint<string>> = NavBlock<Out> & {
607
+ /** Resolved method Blocks registered at define time. */
608
+ readonly methods: readonly DefinedBlock<any, any>[];
609
+ };
610
+ /** @see {@link PageBlockOptions} */
611
+ export declare function definePageBlock<Out extends Checkpoint<string>>(options: PageBlockOptions<Out>): PageBlock<Out>;
570
612
  /**
571
613
  * TypeScript salt over {@link defineBlock}: same runtime Block, but the type
572
614
  * forces `requires` + `instanceOptions` so `waygraph auto` can list one menu
@@ -610,14 +652,14 @@ export declare function defineMemNavBlock<Out extends Checkpoint<string>>(option
610
652
  instanceOptions: NonNullable<Block<Checkpoint<string>, Out>["instanceOptions"]>;
611
653
  }): MemNavBlock<Out>;
612
654
  /**
613
- * "Where am I" - reverse-matches a live `page` against every NavBlock in
614
- * `library`, in order, returning the first one whose own `verify` Traits all
615
- * pass (its `checkpoint`), or `null` if none match. Deliberately NavBlocks
616
- * only: a NavBlock has exactly one fixed `checkpoint` and a `verify` list
617
- * whose whole job is already "confirm arrival here," so it's a reliable
655
+ * "Where am I" - reverse-matches a live `page` against every NavBlock /
656
+ * PageBlock in `library`, in order, returning the first one whose own `verify`
657
+ * Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
658
+ * Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
659
+ * list whose whole job is already "confirm arrival here," so it's a reliable
618
660
  * fingerprint - a regular `defineBlock` can branch to different `Out` tags
619
661
  * depending on runtime evidence, so its `verify` alone doesn't identify a
620
- * single state the way a NavBlock's does.
662
+ * single state the way a hub's does.
621
663
  *
622
664
  * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
623
665
  * what pages exist and how to reach them; `locate()` is how an autonomous
package/dist/engine.js CHANGED
@@ -696,15 +696,88 @@ export function defineNavClickBlock(options) {
696
696
  ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
697
697
  });
698
698
  }
699
- export function defineActionBlock(base) {
699
+ export function defineMethodBlock(base) {
700
700
  const built = defineBlock(base);
701
701
  Object.defineProperty(built, "__waygraphSalt", {
702
- value: "action",
702
+ value: "method",
703
703
  enumerable: false,
704
704
  configurable: false,
705
705
  });
706
706
  return built;
707
707
  }
708
+ /** @deprecated Prefer {@link defineMethodBlock} - identical helper (0.7 name). */
709
+ export const defineActionBlock = defineMethodBlock;
710
+ function resolvePageMethods(methods) {
711
+ if (!methods)
712
+ return [];
713
+ const entries = Array.isArray(methods) ? methods : Object.values(methods);
714
+ return entries.map((entry) => (typeof entry === "function" ? entry() : entry));
715
+ }
716
+ /** @see {@link PageBlockOptions} */
717
+ export function definePageBlock(options) {
718
+ if (options.url !== undefined && options.click !== undefined) {
719
+ throw new Error(`definePageBlock("${options.name}"): pass url XOR click (or neither for arrival-only hub)`);
720
+ }
721
+ const methodList = resolvePageMethods(options.methods);
722
+ const built = defineBlock({
723
+ name: options.name,
724
+ ...(options.description ? { description: options.description } : {}),
725
+ ...(options.requires ? { requires: options.requires } : {}),
726
+ ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
727
+ instruction: {
728
+ async act(page, _input, mem) {
729
+ if (options.url !== undefined) {
730
+ const url = typeof options.url === "function" ? options.url(mem) : options.url;
731
+ await page.goto(url);
732
+ }
733
+ else if (options.click !== undefined) {
734
+ const selector = typeof options.click === "function" ? options.click(mem) : options.click;
735
+ await page.locator(selector).click();
736
+ }
737
+ // else: arrival-only hub — previous Block already landed here
738
+ },
739
+ resolve: () => checkpoint(options.checkpoint),
740
+ ...(options.verify ? { verify: options.verify } : {}),
741
+ ...(options.highlights ? { highlights: options.highlights } : {}),
742
+ },
743
+ });
744
+ Object.defineProperty(built, "__waygraphKind", {
745
+ value: "page",
746
+ enumerable: false,
747
+ configurable: false,
748
+ });
749
+ Object.defineProperty(built, "__waygraphSalt", {
750
+ value: "page",
751
+ enumerable: false,
752
+ configurable: false,
753
+ });
754
+ Object.defineProperty(built, "methods", {
755
+ value: Object.freeze([...methodList]),
756
+ enumerable: true,
757
+ configurable: false,
758
+ writable: false,
759
+ });
760
+ Object.defineProperty(built, "__waygraphMethods", {
761
+ value: methodList.map((m) => m.name),
762
+ enumerable: false,
763
+ configurable: false,
764
+ });
765
+ if (options.url !== undefined || options.click !== undefined) {
766
+ Object.defineProperty(built, "__waygraphPageDeepLink", {
767
+ value: true,
768
+ enumerable: false,
769
+ configurable: false,
770
+ });
771
+ }
772
+ if (options.click !== undefined) {
773
+ Object.defineProperty(built, "__waygraphNavClick", {
774
+ value: options.click,
775
+ enumerable: false,
776
+ configurable: false,
777
+ });
778
+ }
779
+ return built;
780
+ }
708
781
  /**
709
782
  * {@link defineBlock} constrained for effect-style instance menus: `requires` +
710
783
  * `instanceOptions` are mandatory. Prefer this when the Block adds / removes /
@@ -743,14 +816,14 @@ export function defineMemNavBlock(options) {
743
816
  return built;
744
817
  }
745
818
  /**
746
- * "Where am I" - reverse-matches a live `page` against every NavBlock in
747
- * `library`, in order, returning the first one whose own `verify` Traits all
748
- * pass (its `checkpoint`), or `null` if none match. Deliberately NavBlocks
749
- * only: a NavBlock has exactly one fixed `checkpoint` and a `verify` list
750
- * whose whole job is already "confirm arrival here," so it's a reliable
819
+ * "Where am I" - reverse-matches a live `page` against every NavBlock /
820
+ * PageBlock in `library`, in order, returning the first one whose own `verify`
821
+ * Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
822
+ * Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
823
+ * list whose whole job is already "confirm arrival here," so it's a reliable
751
824
  * fingerprint - a regular `defineBlock` can branch to different `Out` tags
752
825
  * depending on runtime evidence, so its `verify` alone doesn't identify a
753
- * single state the way a NavBlock's does.
826
+ * single state the way a hub's does.
754
827
  *
755
828
  * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
756
829
  * what pages exist and how to reach them; `locate()` is how an autonomous
package/dist/graph.d.ts CHANGED
@@ -48,7 +48,8 @@ export declare function toMermaid(graph: WaygraphGraph): string;
48
48
  /**
49
49
  * Blocks present on disk but never wired into a `.flow.ts` defineFlow array.
50
50
  * Required before `chain auto` can build a path from the discovered graph -
51
- * orphan Blocks are intentionally excluded from auto shorthand.
51
+ * orphan Blocks are intentionally excluded from auto shorthand. Arrival-only
52
+ * Page hubs and Blocks listed on a Page's `methods` are not orphans.
52
53
  */
53
54
  export declare function findOrphanBlocks(projectDir: string): Promise<OrphanBlock[]>;
54
55
  /**
package/dist/graph.js CHANGED
@@ -77,7 +77,8 @@ function exportNameToBlockName(exportName) {
77
77
  /**
78
78
  * Blocks present on disk but never wired into a `.flow.ts` defineFlow array.
79
79
  * Required before `chain auto` can build a path from the discovered graph -
80
- * orphan Blocks are intentionally excluded from auto shorthand.
80
+ * orphan Blocks are intentionally excluded from auto shorthand. Arrival-only
81
+ * Page hubs and Blocks listed on a Page's `methods` are not orphans.
81
82
  */
82
83
  export async function findOrphanBlocks(projectDir) {
83
84
  const blockFiles = discoverBlocks(projectDir);
@@ -88,6 +89,27 @@ export async function findOrphanBlocks(projectDir) {
88
89
  referenced.add(ref);
89
90
  }
90
91
  }
92
+ const pageMethodNames = new Set();
93
+ for (const file of blockFiles) {
94
+ let mod;
95
+ try {
96
+ mod = await importModule(file);
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ for (const exported of Object.values(mod)) {
102
+ if (!exported || typeof exported !== "object")
103
+ continue;
104
+ if (exported.__waygraphKind !== "page")
105
+ continue;
106
+ const names = exported.__waygraphMethods;
107
+ if (Array.isArray(names)) {
108
+ for (const n of names)
109
+ pageMethodNames.add(n);
110
+ }
111
+ }
112
+ }
91
113
  const orphans = [];
92
114
  for (const file of blockFiles) {
93
115
  const relFile = relative(projectDir, file);
@@ -98,7 +120,11 @@ export async function findOrphanBlocks(projectDir) {
98
120
  const exportName = m[1];
99
121
  if (referenced.has(exportName))
100
122
  continue;
101
- if (!/defineBlock|defineActionBlock|defineEffectBlock|defineMemEffectBlock|defineNavBlock|defineNavClickBlock|defineMemNavBlock/.test(src))
123
+ if (!/defineBlock|defineMethodBlock|defineActionBlock|definePageBlock|defineEffectBlock|defineMemEffectBlock|defineNavBlock|defineNavClickBlock|defineMemNavBlock/.test(src))
124
+ continue;
125
+ // Arrival-only Page hubs are never orphans (even if import fails without tsx).
126
+ const assignSlice = src.slice(m.index, m.index + exportName.length + 80);
127
+ if (/=\s*definePageBlock\b/.test(assignSlice))
102
128
  continue;
103
129
  let block = exportNameToBlockName(exportName);
104
130
  try {
@@ -106,6 +132,13 @@ export async function findOrphanBlocks(projectDir) {
106
132
  const exported = mod[exportName];
107
133
  if (isBlockLike(exported))
108
134
  block = exported.name;
135
+ if (exported &&
136
+ typeof exported === "object" &&
137
+ exported.__waygraphKind === "page") {
138
+ continue;
139
+ }
140
+ if (pageMethodNames.has(block))
141
+ continue;
109
142
  }
110
143
  catch {
111
144
  // import may fail in partial projects - keep derived name
@@ -155,7 +188,8 @@ function isBlockLike(val) {
155
188
  }
156
189
  /** Set by `defineNavBlock` (non-enumerable) - see src/engine.ts. */
157
190
  function isNavBlockMarked(val) {
158
- return val.__waygraphKind === "nav";
191
+ const kind = val.__waygraphKind;
192
+ return kind === "nav" || kind === "page";
159
193
  }
160
194
  /**
161
195
  * Resolves `typeName` to every literal Checkpoint tag it can mean, within
@@ -242,9 +276,9 @@ function extractDefineBlockGenerics(src) {
242
276
  // alternation so both shapes come out of the same two capture groups.
243
277
  // Effect helpers are TypeScript salt over defineBlock - same In/Out edges.
244
278
  const arg = String.raw `(Checkpoint<[^>]*>|[A-Za-z_$][\w]*)`;
245
- const re = new RegExp(String.raw `define(?:(?:Mem)?Effect|Action)?Block<\s*${arg}\s*,\s*${arg}\s*>`, "g");
246
- // Matches: defineBlock | defineActionBlock | defineEffectBlock | defineMemEffectBlock
247
- // (Nav / NavClick use runtime markers, not this regex.)
279
+ const re = new RegExp(String.raw `define(?:(?:Mem)?Effect|Method|Action)?Block<\s*${arg}\s*,\s*${arg}\s*>`, "g");
280
+ // Matches: defineBlock | defineMethodBlock | defineActionBlock | defineEffectBlock |
281
+ // defineMemEffectBlock (Nav / NavClick / Page use runtime markers, not this regex.)
248
282
  const calls = [];
249
283
  let m;
250
284
  while ((m = re.exec(src)) !== null) {
@@ -289,16 +323,22 @@ export async function discoverGraph(projectDir) {
289
323
  const blockName = exported.name;
290
324
  if (isNavBlockMarked(exported)) {
291
325
  try {
292
- // NavBlock's own resolve() ignores whatever it's called with -
293
- // always exactly `checkpoint(options.checkpoint)`.
326
+ // NavBlock / PageBlock resolve() ignores input - always checkpoint(tag).
294
327
  const resolveFn = exported.instruction.resolve;
295
328
  const resolved = await resolveFn(undefined);
296
329
  const to = resolved.__state;
297
330
  nodeTags.add(to);
298
- edges.push({ block: blockName, file: relFile, from: "*", to, kind: "nav" });
331
+ // Arrival-only Page hubs register the Checkpoint node but do not emit
332
+ // a Start-here edge (no deep link). Methods own the action edges.
333
+ const isPage = exported.__waygraphKind === "page";
334
+ const deepLink = !isPage ||
335
+ exported.__waygraphPageDeepLink === true;
336
+ if (deepLink) {
337
+ edges.push({ block: blockName, file: relFile, from: "*", to, kind: "nav" });
338
+ }
299
339
  }
300
340
  catch {
301
- skipped.push({ block: blockName, file: relFile, reason: "NavBlock's own resolve() threw" });
341
+ skipped.push({ block: blockName, file: relFile, reason: "NavBlock/PageBlock resolve() threw" });
302
342
  }
303
343
  continue;
304
344
  }
@@ -307,7 +347,7 @@ export async function discoverGraph(projectDir) {
307
347
  skipped.push({
308
348
  block: blockName,
309
349
  file: relFile,
310
- reason: "no defineBlock/defineEffectBlock<In, Out> generic call found",
350
+ reason: "no defineBlock/defineMethodBlock/defineEffectBlock<In, Out> generic call found",
311
351
  });
312
352
  continue;
313
353
  }
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineActionBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
6
- export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, ActionBlock, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
5
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
6
+ export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
7
7
  export type { ActionPage } from "./types.js";
8
8
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
9
9
  export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./graph.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineActionBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
4
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
5
5
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
@@ -32,11 +32,25 @@ npx playwright install chromium
32
32
  | Want | Command |
33
33
  |------|---------|
34
34
  | Headless suite | `npm test` |
35
- | Interactive explore | `npm run auto` |
36
- | Stepper (manual Next) | `npm run demo:step` |
37
- | Stepper (Auto-advance) | `npm run demo:autoplay` |
35
+ | Interactive explore (headed panel) | `npm run auto` |
36
+ | Interactive explore (terminal menu) | `npm run auto:cli` |
37
+ | Shop demo stepper | `npm run demo:step` |
38
+ | Shop demo autoplay | `npm run demo:autoplay` |
39
+ | **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
40
+ | Cart bulk stepper | `npm run demo:bulk:step` |
38
41
  | List flows | `npm run flows` |
39
42
 
43
+ **Demo vs auto vs run for cart bulk**
44
+
45
+ | | CLI | Needs `.flow.ts`? |
46
+ |--|-----|-------------------|
47
+ | **`run --blocks`** | `npm run run:bulk` — Block names + `--data` | No |
48
+ | **`demo --blocks`** | `npm run demo:bulk` — named flow + `--data` | Optional (`cartBulkFlow`) |
49
+ | **`auto`** | `npm run auto` / `auto:cli` | No — live menu |
50
+ | **`auto --blocks From To`** | path-find on graph | No — checkpoints only |
51
+
52
+ QA watch+record: `npm run demo:bulk:video` (`--auto-play-video`).
53
+
40
54
  ## Layout (short)
41
55
 
42
56
  ```text
@@ -45,10 +59,10 @@ src/blocks/
45
59
  saucedemo-web/ # URL /
46
60
  NAV.md
47
61
  nav-login.block.ts
48
- actions/ # submit-login, logout, …
62
+ methods/ # submit-login, logout, …
49
63
  inventory/ # URL /inventory.html
50
64
  nav-item-detail.block.ts # MemNav
51
- actions/*.effect.block.ts # Add / Remove
65
+ methods/*.effect.block.ts # Add / Remove
52
66
  cart/ # URL /cart.html
53
67
  checkout-step-one/
54
68
  checkout-step-two/
@@ -1,11 +1,10 @@
1
1
  # saucedemo waygraph conventions
2
2
 
3
- Atomic Blocks - one nav / effect / action per real URL or on-page mutation so
3
+ Atomic Blocks - one nav / effect / method per real URL or on-page mutation so
4
4
  `waygraph auto` can discover a real state graph (not a fat multi-page Block).
5
5
 
6
- **TypeScript salt only.** `defineNavBlock`, `defineEffectBlock`, and plain
7
- `defineBlock` are helpers that force a useful shape. At runtime everything is
8
- still a `Block` - there is no separate Effect or Nav engine.
6
+ **TypeScript salt only.** Helpers force a useful shape. At runtime everything is
7
+ still a `Block` - there is no separate Page / Effect / Nav engine.
9
8
 
10
9
  **Multi-route consumers:** folder layout must mirror Next.js `app/` page routes -
11
10
  see package docs `docs/consumer.html` (no invented `root/`, `landing/`, or
@@ -15,14 +14,18 @@ see package docs `docs/consumer.html` (no invented `root/`, `landing/`, or
15
14
 
16
15
  | Kind | File | Helper | `page.goto`? |
17
16
  |------|------|--------|--------------|
17
+ | Page | `*.page.block.ts` | `definePageBlock` | Optional deep-link; usually arrival-only hub |
18
18
  | Nav | `nav-*.block.ts` | `defineNavBlock` / `defineMemNavBlock` | Yes (`url` XOR `click`) |
19
- | Effect | `*.effect.block.ts` | `defineEffectBlock` | No - mutates a live instance (add/remove/toggle) via mem + `instanceOptions` |
20
- | Action | `*.action.block.ts` | `defineBlock` | No - forms, submit, logout, etc. |
19
+ | Effect | `*.effect.block.ts` in `methods/` | `defineEffectBlock` | No - mutates a live instance via mem + `instanceOptions` |
20
+ | Method | `*.method.block.ts` in `methods/` | `defineMethodBlock` | No - forms, submit, logout, predefined queues |
21
21
 
22
- Only an explicit `page.goto()` (or NavBlock `click`) counts as navigation - an
23
- action/effect that fills a form or clicks a button stays non-nav even when the
22
+ Only an explicit `page.goto()` (or NavBlock `click`) counts as navigation - a
23
+ method/effect that fills a form or clicks a button stays non-nav even when the
24
24
  app changes URL as a side effect of `#continue`.
25
25
 
26
+ **Selectors:** put DOM strings in `*Sel` next to the page (static + `(id) => …`).
27
+ Mem keys store values, not selectors.
28
+
26
29
  ## Layout
27
30
 
28
31
  ```text
@@ -31,15 +34,16 @@ src/blocks/
31
34
  saucedemo-web/ # THIS FOLDER IS URL "/"
32
35
  NAV.md
33
36
  nav-login.block.ts
34
- actions/
35
- submit-login.action.block.ts
37
+ methods/
38
+ submit-login.method.block.ts
36
39
  submit-login-for-flow.ts
37
- submit-logout.action.block.ts
40
+ submit-logout.method.block.ts
38
41
  inventory/ # /inventory.html
39
42
  NAV.md
43
+ inventory.page.block.ts # Page hub + methods registry
40
44
  nav-item-detail.block.ts
41
- actions/
42
- inventory-items.ts
45
+ methods/
46
+ inventory-items.ts # InventorySel + collectors
43
47
  add-to-cart.effect.block.ts
44
48
  remove-from-cart.effect.block.ts
45
49
  inventory-item/ # /inventory-item.html
@@ -53,18 +57,18 @@ src/blocks/
53
57
  nav-checkout-info.block.ts
54
58
  checkout-step-one/ # /checkout-step-one.html
55
59
  NAV.md
56
- actions/
57
- submit-checkout-info.action.block.ts
60
+ methods/
61
+ submit-checkout-info.method.block.ts
58
62
  checkout-step-two/ # /checkout-step-two.html
59
63
  NAV.md
60
- actions/
61
- finish-order.action.block.ts
64
+ methods/
65
+ finish-order.method.block.ts
62
66
  src/states/
63
67
  src/flows/
64
68
  ```
65
69
 
66
70
  `waygraph check` should report zero warnings - every navigation goes through
67
- a `defineNavBlock`, nothing else calls `page.goto`.
71
+ a `defineNavBlock` / Page deep-link, nothing else calls `page.goto`.
68
72
 
69
73
  `waygraph auto` should discover Checkpoints as nodes and these Blocks as
70
74
  edges (including cycles via logout / continue-shopping / back-to-products).
@@ -4,23 +4,76 @@ Runtime is always a `Block`. Helpers are TypeScript salt + clear intent.
4
4
 
5
5
  | Helper | Use when | File |
6
6
  |--------|----------|------|
7
+ | `definePageBlock` | Screen hub (checkpoint + methods + optional Sel) | `*.page.block.ts` |
7
8
  | `defineNavBlock` | URL / `goto` only | `nav-*.block.ts` |
8
9
  | `defineNavClickBlock` | Click-to-navigate only | `nav-*.block.ts` |
9
10
  | `defineMemNavBlock` | Nav + per-row `instanceOptions` | `nav-*.block.ts` |
10
- | `defineActionBlock` | One-shot non-nav app step | `*.action.block.ts` |
11
- | `defineEffectBlock` | Instance mutate + auto menu rows | `*.effect.block.ts` |
11
+ | `defineMethodBlock` | One-shot non-nav app step | `*.method.block.ts` under `methods/` |
12
+ | `defineEffectBlock` | Instance mutate + auto menu rows | `*.effect.block.ts` under `methods/` |
12
13
  | `defineBlock` | Escape hatch: tests, unsure, probes | any |
14
+ | `defineActionBlock` | **Deprecated** alias of `defineMethodBlock` | — |
13
15
 
14
16
  `defineNavBlock({ click })` still works (compat) - prefer `defineNavClickBlock` in app code.
15
17
 
16
- ## Branching (not Mem)
18
+ ---
17
19
 
18
- Mem / `instanceOptions` = pick *which instance* before run.
20
+ ## Page hub + methods + Sel
19
21
 
20
- Conditional outcomes = `observe` + union `Out` (already supported, not a new Block kind):
22
+ One **PageBlock** = this screen. Methods hang off it. DOM strings live in `*Sel` (not in mem).
21
23
 
22
24
  ```ts
23
- defineActionBlock<LoginPage, LoggedIn | LoginPage>({
25
+ export const InventorySel = {
26
+ list: ".inventory_list",
27
+ addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
28
+ // …
29
+ };
30
+
31
+ export const InventoryPageBlock = definePageBlock({
32
+ name: "page-inventory",
33
+ checkpoint: "LoggedIn",
34
+ verify: [Trait.url({ pathname: "/inventory.html" }), Trait.visible(InventorySel.list)],
35
+ methods: {
36
+ addToCart: () => AddToCartBlock,
37
+ removeFromCart: () => RemoveFromCartBlock,
38
+ addAllToCart: () => AddAllToCartBlock,
39
+ removeAllFromCart: () => RemoveAllFromCartBlock,
40
+ openDetail: () => NavItemDetailBlock,
41
+ },
42
+ });
43
+ ```
44
+
45
+ | Piece | Stores |
46
+ |-------|--------|
47
+ | `*Sel` | DOM selectors only (static + `(id) => …`) |
48
+ | Mem keys | Typed values (which item / queue) — not selectors |
49
+ | `instanceOptions` | Live DOM → menu rows → `mem.set` on pick |
50
+ | Predefined method | No `instanceOptions`; flow seeds mem first |
51
+
52
+ Live: `inventory/inventory.page.block.ts`, `inventory/methods/`.
53
+
54
+ **Round-trip (not forward-only):** cart Methods/Effects use `In = Checkpoint<string>`
55
+ so the graph edge is `from: "*"`. Auto lists them whenever the live page has matching
56
+ buttons (Add vs Remove), including after continue-shopping / back to inventory. Bulk
57
+ `add-all-to-cart` / `remove-all-from-cart` are one menu row each (no `instanceOptions`);
58
+ Out is `ItemInCart` vs `LoggedIn` from cart emptiness.
59
+
60
+ ---
61
+
62
+ ## Checkpoint vs `branch()`
63
+
64
+ | | **Checkpoint** | **`branch()`** |
65
+ |--|----------------|----------------|
66
+ | Answers | *Where* are you? (`LoggedIn`, `LoginPage`) | *Which Block runs next?* |
67
+ | How | `observe` + `resolve` -> union `Out` | `branch(block, { Tag: nextBlock \| null })` |
68
+ | Alone can | Label state; graph edges; verify | Route a `runGraph` walk |
69
+ | Alone cannot | Pick the next Block | Decide the state |
70
+
71
+ Checkpoint = place on the map. `branch()` = which road to take from that place.
72
+
73
+ Conditional Out (no `branch()` needed for auto/graph):
74
+
75
+ ```ts
76
+ defineMethodBlock<LoginPage, LoggedIn | LoginPage>({
24
77
  name: "submit-login",
25
78
  instruction: {
26
79
  async act(page, _in, mem) { /* fill + click */ },
@@ -37,6 +90,97 @@ defineActionBlock<LoginPage, LoggedIn | LoginPage>({
37
90
  });
38
91
  ```
39
92
 
40
- Graph expands the union Out into two edges. Optional flow routing: `branch(block, { LoggedIn: next, LoginPage: null })`.
93
+ Optional routing for `runGraph`:
94
+
95
+ ```ts
96
+ branch(SubmitLoginActionBlock, {
97
+ LoggedIn: AddToCartBlock,
98
+ LoginPage: null, // stop
99
+ });
100
+ ```
101
+
102
+ Linear `defineFlow([start, …, end])` usually skips `branch()` (one happy path).
103
+
104
+ ---
105
+
106
+ ## Mem / `instanceOptions` (spawns menu rows)
107
+
108
+ Not a separate runtime type. One Block + `requires` + `instanceOptions(page)` ->
109
+ `waygraph auto` lists **one row per live DOM match**. On pick: `mem.set(key, value)`
110
+ then run the Block (act reads mem as usual).
111
+
112
+ ### Effect - spawn Method-like rows (Add / Remove)
113
+
114
+ ```ts
115
+ defineEffectBlock<Checkpoint<string>, ItemInCart>({
116
+ name: "add-to-cart",
117
+ requires: [SelectedItem.key],
118
+ instruction: {
119
+ async act(page, _in, mem) {
120
+ const { id } = mem.get(SelectedItem.key);
121
+ await page.locator(InventorySel.addBtn(id)).click();
122
+ },
123
+ resolve: () => checkpoint("ItemInCart"),
124
+ },
125
+ async instanceOptions(page) {
126
+ const items = await collectAddableItems(page);
127
+ return items.map((item) => ({
128
+ id: item.id,
129
+ label: `Add "${item.name}" to cart`,
130
+ key: SelectedItem.key,
131
+ value: item,
132
+ highlight: InventorySel.addBtn(item.id),
133
+ }));
134
+ },
135
+ });
136
+ ```
137
+
138
+ Auto shows: `Add "Sauce Labs Backpack" to cart`, `Add "Bike Light"…`, …
139
+
140
+ ### MemNav - spawn Nav rows (Open details)
141
+
142
+ ```ts
143
+ defineMemNavBlock<ItemDetailPage>({
144
+ name: "nav-item-detail",
145
+ checkpoint: "ItemDetailPage",
146
+ requires: [SelectedItem.key],
147
+ click: (mem) => InventorySel.title(mem.get(SelectedItem.key).id),
148
+ verify: [Trait.url({ pathname: "/inventory-item.html" })],
149
+ async instanceOptions(page) {
150
+ const items = await collectInventoryItems(page);
151
+ return items.map((item) => ({
152
+ id: item.id,
153
+ label: `Open "${item.name}" details`,
154
+ key: SelectedItem.key,
155
+ value: item,
156
+ highlight: InventorySel.title(item.id),
157
+ }));
158
+ },
159
+ });
160
+ ```
161
+
162
+ Auto shows: `Open "Backpack" details`, … - still a NavBlock (click nav), salt only.
163
+
164
+ | | Effect | MemNav |
165
+ |--|--------|--------|
166
+ | Helper | `defineEffectBlock` | `defineMemNavBlock` |
167
+ | Spawns | mutate rows | navigate rows |
168
+ | `act` / nav | click Add/Remove | `click: (mem) => …` title link |
169
+ | Mem | same pattern: `requires` + row `key`/`value` | same |
170
+
171
+ Live source: `inventory/methods/add-to-cart.effect.block.ts`,
172
+ `inventory/nav-item-detail.block.ts`.
173
+
174
+ ---
175
+
176
+ ## Cart bulk: demo / run / auto
177
+
178
+ | | Command |
179
+ |--|---------|
180
+ | **Run (Block names)** | `npm run run:bulk` — `run --blocks "loginFlow then add-all…" --data '{…}'` |
181
+ | **Demo (named flow)** | `npm run demo:bulk` — `--blocks cartBulkFlow --data '{…}'` |
182
+ | **Demo QA video** | `npm run demo:bulk:video` — `--auto-play-video` |
183
+ | **Auto headed / CLI** | `npm run auto` / `auto:cli` |
184
+ | **Auto path-find** | `waygraph auto --blocks LoginPage OrderComplete` |
185
+
41
186
 
42
- No breaking change for atomics - document observe/resolve (+ optional `branch`) as the Branch pattern.