waygraph 0.7.15 → 0.10.0

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 (93) 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 +77 -7
  7. package/dist/engine.js +108 -7
  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 +186 -0
  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/cart/nav-cart.block.ts +3 -3
  25. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
  26. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  27. 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
  28. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  29. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  30. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  31. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  32. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  33. 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
  34. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  35. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  36. 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
  37. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +4 -4
  38. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +3 -3
  39. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
  40. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
  41. package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  42. package/examples/saucedemo/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  43. 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
  44. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -0
  45. package/examples/saucedemo/src/flows/cart-mem.flow.ts +1 -1
  46. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  47. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +4 -4
  48. package/examples/saucedemo/src/flows/checkout.flow.ts +4 -4
  49. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  50. package/examples/saucedemo/src/flows/shop.flow.ts +3 -3
  51. package/examples/saucedemo/src/flows/smoke.flow.ts +2 -2
  52. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  53. package/examples/saucedemo/tests/flow-block-verify.spec.ts +1 -1
  54. package/examples/saucedemo/tests/submit-login-branch.spec.ts +1 -1
  55. package/package.json +1 -1
  56. package/templates/quickstart/README-saucedemo-commands.md +72 -0
  57. package/templates/quickstart/docs/CONVENTION.md +5 -5
  58. package/templates/quickstart/docs/HELPERS.md +186 -0
  59. package/templates/quickstart/package.json +9 -5
  60. package/templates/quickstart/scripts/drive-auto-cli.mjs +93 -0
  61. package/templates/quickstart/src/blocks/SITE-MAP.md +11 -8
  62. package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +3 -3
  63. package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  64. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -3
  65. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
  66. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  67. 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
  68. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  69. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  70. package/templates/quickstart/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  71. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  72. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  73. 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
  74. package/templates/quickstart/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  75. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  76. 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
  77. package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +1 -1
  78. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +3 -3
  79. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
  80. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
  81. package/templates/quickstart/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  82. package/templates/quickstart/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  83. 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
  84. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -0
  85. package/templates/quickstart/src/flows/cart-mem.flow.ts +1 -1
  86. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  87. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +4 -4
  88. package/templates/quickstart/src/flows/checkout.flow.ts +4 -4
  89. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  90. package/templates/quickstart/src/flows/shop.flow.ts +3 -3
  91. package/templates/quickstart/src/flows/smoke.flow.ts +2 -2
  92. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  93. package/templates/scaffold/package.json +1 -1
package/dist/engine.d.ts CHANGED
@@ -536,9 +536,79 @@ export type NavBlockOptions<Out extends Checkpoint<string>> = {
536
536
  * hand.
537
537
  * @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
538
538
  * @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
539
- * @example defineNavBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
539
+ * @example defineNavClickBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
540
+ * For click-nav in app code prefer {@link defineNavClickBlock} (url stays here).
540
541
  */
541
542
  export declare function defineNavBlock<Out extends Checkpoint<string>>(options: NavBlockOptions<Out>): NavBlock<Out>;
543
+ /**
544
+ * Click-only {@link NavBlock}. Same runtime as {@link defineNavBlock} with
545
+ * `click` - use this in app code so you never wonder whether a Nav is url or
546
+ * click (no `__waygraphNavClick` sniffing at author time). Prefer
547
+ * {@link defineNavBlock} only for `url` / `goto` deep links.
548
+ */
549
+ export type NavClickBlockOptions<Out extends Checkpoint<string>> = {
550
+ name: string;
551
+ description?: string;
552
+ checkpoint: Out["__state"];
553
+ click: string | ((mem: MemPage) => string);
554
+ requires?: readonly MemKey<any>[];
555
+ verify?: Trait[] | ((out: Out) => Trait[]);
556
+ highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
557
+ instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
558
+ };
559
+ export type NavClickBlock<Out extends Checkpoint<string>> = NavBlock<Out>;
560
+ /** @see {@link NavClickBlockOptions} */
561
+ export declare function defineNavClickBlock<Out extends Checkpoint<string>>(options: NavClickBlockOptions<Out>): NavClickBlock<Out>;
562
+ /**
563
+ * TypeScript salt over {@link defineBlock} for one-shot non-nav app steps
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.
569
+ */
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>;
542
612
  /**
543
613
  * TypeScript salt over {@link defineBlock}: same runtime Block, but the type
544
614
  * forces `requires` + `instanceOptions` so `waygraph auto` can list one menu
@@ -582,14 +652,14 @@ export declare function defineMemNavBlock<Out extends Checkpoint<string>>(option
582
652
  instanceOptions: NonNullable<Block<Checkpoint<string>, Out>["instanceOptions"]>;
583
653
  }): MemNavBlock<Out>;
584
654
  /**
585
- * "Where am I" - reverse-matches a live `page` against every NavBlock in
586
- * `library`, in order, returning the first one whose own `verify` Traits all
587
- * pass (its `checkpoint`), or `null` if none match. Deliberately NavBlocks
588
- * only: a NavBlock has exactly one fixed `checkpoint` and a `verify` list
589
- * 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
590
660
  * fingerprint - a regular `defineBlock` can branch to different `Out` tags
591
661
  * depending on runtime evidence, so its `verify` alone doesn't identify a
592
- * single state the way a NavBlock's does.
662
+ * single state the way a hub's does.
593
663
  *
594
664
  * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
595
665
  * what pages exist and how to reach them; `locate()` is how an autonomous
package/dist/engine.js CHANGED
@@ -634,7 +634,8 @@ export function defineBlock(base) {
634
634
  * hand.
635
635
  * @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
636
636
  * @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
637
- * @example defineNavBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
637
+ * @example defineNavClickBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
638
+ * For click-nav in app code prefer {@link defineNavClickBlock} (url stays here).
638
639
  */
639
640
  export function defineNavBlock(options) {
640
641
  const built = defineBlock({
@@ -682,6 +683,101 @@ export function defineNavBlock(options) {
682
683
  }
683
684
  return built;
684
685
  }
686
+ /** @see {@link NavClickBlockOptions} */
687
+ export function defineNavClickBlock(options) {
688
+ return defineNavBlock({
689
+ name: options.name,
690
+ checkpoint: options.checkpoint,
691
+ click: options.click,
692
+ ...(options.description ? { description: options.description } : {}),
693
+ ...(options.requires ? { requires: options.requires } : {}),
694
+ ...(options.verify ? { verify: options.verify } : {}),
695
+ ...(options.highlights ? { highlights: options.highlights } : {}),
696
+ ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
697
+ });
698
+ }
699
+ export function defineMethodBlock(base) {
700
+ const built = defineBlock(base);
701
+ Object.defineProperty(built, "__waygraphSalt", {
702
+ value: "method",
703
+ enumerable: false,
704
+ configurable: false,
705
+ });
706
+ return built;
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
+ }
685
781
  /**
686
782
  * {@link defineBlock} constrained for effect-style instance menus: `requires` +
687
783
  * `instanceOptions` are mandatory. Prefer this when the Block adds / removes /
@@ -695,6 +791,11 @@ export function defineEffectBlock(base) {
695
791
  enumerable: false,
696
792
  configurable: false,
697
793
  });
794
+ Object.defineProperty(built, "__waygraphSalt", {
795
+ value: "effect",
796
+ enumerable: false,
797
+ configurable: false,
798
+ });
698
799
  return built;
699
800
  }
700
801
  /** @deprecated Prefer {@link defineEffectBlock} - identical helper. */
@@ -715,14 +816,14 @@ export function defineMemNavBlock(options) {
715
816
  return built;
716
817
  }
717
818
  /**
718
- * "Where am I" - reverse-matches a live `page` against every NavBlock in
719
- * `library`, in order, returning the first one whose own `verify` Traits all
720
- * pass (its `checkpoint`), or `null` if none match. Deliberately NavBlocks
721
- * only: a NavBlock has exactly one fixed `checkpoint` and a `verify` list
722
- * 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
723
824
  * fingerprint - a regular `defineBlock` can branch to different `Out` tags
724
825
  * depending on runtime evidence, so its `verify` alone doesn't identify a
725
- * single state the way a NavBlock's does.
826
+ * single state the way a hub's does.
726
827
  *
727
828
  * The primitive `waygraph auto`'s discovered graph is *for* - the graph says
728
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|defineEffectBlock|defineMemEffectBlock|defineNavBlock|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)?Block<\s*${arg}\s*,\s*${arg}\s*>`, "g");
246
- // Matches: defineBlock | defineEffectBlock | defineMemEffectBlock
247
- // (defineMemBlock alone is not a real helper - Mem* only prefixes Effect.)
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, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
6
- export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, 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, 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).