waygraph 0.15.23 → 0.15.25

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 (65) hide show
  1. package/dist/cli/discover.d.ts +40 -0
  2. package/dist/cli/discover.js +148 -0
  3. package/dist/cli/flags.d.ts +91 -0
  4. package/dist/cli/flags.js +442 -0
  5. package/dist/cli/run-chain.d.ts +8 -0
  6. package/dist/cli/run-chain.js +67 -0
  7. package/dist/cli/usage.d.ts +4 -0
  8. package/dist/cli/usage.js +236 -0
  9. package/dist/cli/util.d.ts +4 -0
  10. package/dist/cli/util.js +24 -0
  11. package/dist/cli.js +45 -2448
  12. package/dist/commands/agent-dive.d.ts +1 -0
  13. package/dist/commands/agent-dive.js +52 -0
  14. package/dist/commands/check.d.ts +4 -0
  15. package/dist/commands/check.js +192 -0
  16. package/dist/commands/init.d.ts +1 -0
  17. package/dist/commands/init.js +54 -0
  18. package/dist/commands/list.d.ts +2 -0
  19. package/dist/commands/list.js +67 -0
  20. package/dist/commands/map.d.ts +1 -0
  21. package/dist/commands/map.js +29 -0
  22. package/dist/commands/run.d.ts +3 -0
  23. package/dist/commands/run.js +104 -0
  24. package/dist/commands/session.d.ts +2 -0
  25. package/dist/commands/session.js +600 -0
  26. package/dist/commands/test.d.ts +1 -0
  27. package/dist/commands/test.js +33 -0
  28. package/dist/commands/traverse.d.ts +1 -0
  29. package/dist/commands/traverse.js +177 -0
  30. package/dist/commands/try.d.ts +1 -0
  31. package/dist/commands/try.js +298 -0
  32. package/dist/engine/blocks/effect.d.ts +41 -0
  33. package/dist/engine/blocks/effect.js +23 -0
  34. package/dist/engine/blocks/method.d.ts +90 -0
  35. package/dist/engine/blocks/method.js +70 -0
  36. package/dist/engine/blocks/nav.d.ts +138 -0
  37. package/dist/engine/blocks/nav.js +110 -0
  38. package/dist/engine/blocks/page.d.ts +59 -0
  39. package/dist/engine/blocks/page.js +91 -0
  40. package/dist/engine/compose.d.ts +75 -0
  41. package/dist/engine/compose.js +76 -0
  42. package/dist/engine/config.d.ts +55 -0
  43. package/dist/engine/config.js +17 -0
  44. package/dist/engine/core.d.ts +92 -0
  45. package/dist/engine/core.js +256 -0
  46. package/dist/engine/engine-class.d.ts +289 -0
  47. package/dist/engine/engine-class.js +257 -0
  48. package/dist/engine/flow.d.ts +266 -0
  49. package/dist/engine/flow.js +387 -0
  50. package/dist/engine/locate.d.ts +26 -0
  51. package/dist/engine/locate.js +62 -0
  52. package/dist/engine/run-graph.d.ts +147 -0
  53. package/dist/engine/run-graph.js +223 -0
  54. package/dist/engine.d.ts +11 -1224
  55. package/dist/engine.js +12 -1550
  56. package/examples/saucedemo/.waygraph-auto/0ac58118.log +0 -0
  57. package/examples/saucedemo/.waygraph-auto/309c107f.log +0 -0
  58. package/examples/saucedemo/.waygraph-auto/3ab7c7bd.log +44 -0
  59. package/examples/saucedemo/.waygraph-auto/45864828.log +0 -0
  60. package/examples/saucedemo/.waygraph-auto/95bd8371.log +0 -0
  61. package/examples/saucedemo/.waygraph-auto/bcb9e31a.log +0 -0
  62. package/examples/saucedemo/.waygraph-auto/cd3e528c.log +0 -0
  63. package/examples/saucedemo/.waygraph-auto/dce1535d.log +0 -0
  64. package/examples/saucedemo/.waygraph-auto/e280bf20.log +0 -0
  65. package/package.json +1 -1
@@ -0,0 +1,70 @@
1
+ // Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { checkpoint } from "../../types.js";
3
+ import { defineBlock } from "../core.js";
4
+ export function defineMethodBlock(base) {
5
+ const built = defineBlock(base);
6
+ Object.defineProperty(built, "__waygraphSalt", {
7
+ value: "method",
8
+ enumerable: false,
9
+ configurable: false,
10
+ });
11
+ return built;
12
+ }
13
+ /** @deprecated Prefer {@link defineMethodBlock} - identical helper (0.7 name). */
14
+ export const defineActionBlock = defineMethodBlock;
15
+ /**
16
+ * Sugar for a self-loop-only "pure assertion" Block: no hand-written
17
+ * `act`/`resolve`, just a name, a `checkpoint` tag, and a `verify` array.
18
+ * `resolve` is generated from the one `checkpoint` field given, closing the
19
+ * copy/rename footgun where a hand-restated checkpoint string can silently
20
+ * drift wrong across a copy/pasted file. Mirrors `definePageBlock`'s own
21
+ * construction pattern (flat options in, `defineBlock` built internally).
22
+ * See openspec/changes/waygraph-agent-skill-hardening/spec.md.
23
+ * @example
24
+ * defineAssertBlock({
25
+ * name: "assert-raw-materials-table",
26
+ * checkpoint: "RawMaterials",
27
+ * waitForHeading: "Raw Materials & Lead Times",
28
+ * verify: [Trait.visible(RawMaterialsSel.reorderBadge)],
29
+ * });
30
+ */
31
+ export function defineAssertBlock(options) {
32
+ const built = defineMethodBlock({
33
+ name: options.name,
34
+ ...(options.description ? { description: options.description } : {}),
35
+ ...(options.requires ? { requires: options.requires } : {}),
36
+ instruction: {
37
+ async act(page) {
38
+ if (options.waitForHeading !== undefined) {
39
+ await page.getByRole("heading", { name: options.waitForHeading }).waitFor();
40
+ }
41
+ },
42
+ resolve: () => checkpoint(options.checkpoint),
43
+ verify: options.verify,
44
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
45
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
46
+ ...(options.stubOnError ? { stubOnError: options.stubOnError } : {}),
47
+ ...(options.slides ? { slides: options.slides } : {}),
48
+ },
49
+ });
50
+ // Real bug this closes: `waygraph graph`'s static discovery only knows
51
+ // how to read a Block's Checkpoint tags from an EXPLICIT `<In, Out>`
52
+ // generic written in the source text (or, for Nav/Page Blocks, by calling
53
+ // their own input-independent `resolve()` directly). `defineAssertBlock`'s
54
+ // whole point is skipping that generic - called exactly as documented
55
+ // (`defineAssertBlock({ name, checkpoint, verify })`, no generics), it was
56
+ // silently invisible to both `waygraph graph` and the live Pilot menu
57
+ // ("2 Block(s) skipped (Out not resolvable)", confirmed live against a
58
+ // real veciro-waygraph session). `resolve()` here is ALSO input-
59
+ // independent (always `checkpoint(options.checkpoint)`), so the same
60
+ // resolve()-calling path Nav/Page Blocks already use is reused via this
61
+ // marker instead of requiring authors to write generics just to be found.
62
+ Object.defineProperty(built, "__waygraphKind", {
63
+ value: "assert",
64
+ enumerable: false,
65
+ configurable: false,
66
+ });
67
+ // Brand for map().assert() - distinct from MethodBlock so .method(assert)
68
+ // is a compile error even though runtime still carries method salt.
69
+ return built;
70
+ }
@@ -0,0 +1,138 @@
1
+ import type { Block, Checkpoint, DefinedBlock, WaygraphHighlight } from "../../types.js";
2
+ import type { Trait } from "../../trait.js";
3
+ import { MemPage } from "../../mem-page.js";
4
+ import type { MemKey } from "../../mem-page.js";
5
+ /**
6
+ * A Block whose only possible action is navigating to a URL - built via
7
+ * {@link defineNavBlock}, never with a hand-authored `act()`. Extends `Block`
8
+ * (same shape `DefinedBlock`/`ComposedBlock` already use), so it drops into
9
+ * `defineFlow([...])`, `connect()`, `composeBlock()` exactly like any other
10
+ * Block - no special-casing needed anywhere that only expects a `Block`.
11
+ * See `openspec/specs/nav-block-and-check/spec.md` for why this exists.
12
+ *
13
+ * `__wgFactory` is a TypeScript-only brand so {@link MapBuilder.gotoPage} /
14
+ * {@link MapBuilder.gotoExternal} can reject Assert/Method Blocks at compile
15
+ * time (runtime still checks `__waygraphKind`). Decorate methods are re-stated
16
+ * (via Omit) so `.stubBefore()` / `.withVerify()` keep the brand.
17
+ */
18
+ export type NavBlock<Out extends Checkpoint<string>> = Omit<DefinedBlock<Checkpoint<string>, Out>, "withVerify" | "modVerify" | "modVerifyAll" | "stubBefore" | "stubAfter" | "stubOnError"> & {
19
+ readonly __wgFactory?: "nav";
20
+ withVerify(verify: Trait[] | ((out: Out) => Trait[])): NavBlock<Out>;
21
+ modVerify(nameOrIndex: string | number, newCheck: Trait["check"] | Trait): NavBlock<Out>;
22
+ modVerifyAll(patches: Record<string, Trait["check"] | Trait>): NavBlock<Out>;
23
+ stubBefore(stub: import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
24
+ stubBefore(stub: import("../../highlights.js").StubLifecycleFn<Out>): NavBlock<Out>;
25
+ stubBefore(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
26
+ stubAfter(stub: import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
27
+ stubAfter(stub: import("../../highlights.js").StubLifecycleFn<Out>): NavBlock<Out>;
28
+ stubAfter(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
29
+ stubOnError(stub: import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
30
+ stubOnError(stub: import("../../highlights.js").StubLifecycleFn<Out>): NavBlock<Out>;
31
+ stubOnError(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): NavBlock<Out>;
32
+ };
33
+ /**
34
+ * `defineNavBlock`'s own options - exactly one of `url`/`click` required,
35
+ * enforced at compile time (giving both, or neither, is a type error - see
36
+ * `typecheck/nav-block.types.ts`). `checkpoint` is the tag this NavBlock
37
+ * resolves to once navigation completes - a NavBlock never branches on
38
+ * observed evidence the way a regular Block can, so there's no `resolve`
39
+ * logic to author; it always just declares "arrived."
40
+ */
41
+ export type NavBlockOptions<Out extends Checkpoint<string>> = {
42
+ name: string;
43
+ description?: string;
44
+ checkpoint: Out["__state"];
45
+ requires?: readonly MemKey<any>[];
46
+ verify?: Trait[] | ((out: Out) => Trait[]);
47
+ stubBefore?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
48
+ stubAfter?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
49
+ stubOnError?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
50
+ /** @deprecated Prefer stubAfter. */
51
+ highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
52
+ /**
53
+ * Live per-instance menu rows for `waygraph auto` (see {@link Block.instanceOptions}).
54
+ * When set, the explore menu lists one row per option instead of a single
55
+ * generic nav edge - used by {@link defineMemNavBlock}.
56
+ */
57
+ instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
58
+ } & ({
59
+ /**
60
+ * Direct URL navigation. Accepts a plain string or a function
61
+ * `(mem) => string` for parameterized routes (e.g.
62
+ * `/dashboard/requests/:id` with a real id read from mem). The
63
+ * escape hatch for the few real cases with no click path to get
64
+ * there (a deep link from an email, a public share link) - not for
65
+ * skipping past navigation an agent didn't feel like finding. Prefer
66
+ * `click` whenever the target app actually has one.
67
+ */
68
+ url: string | ((mem: MemPage) => string);
69
+ click?: never;
70
+ } | {
71
+ /**
72
+ * Navigates by clicking a real element already on the page (a nav
73
+ * link, a sidebar item) instead of teleporting straight to a URL -
74
+ * the opinionated default. Accepts a Playwright selector string, or
75
+ * a function `(mem) => selector` for a target that depends on mem
76
+ * (e.g. clicking the row for a specific id). Verifying arrival is
77
+ * `verify`'s job (typically `Trait.url(...)`), same as a `url`-based
78
+ * NavBlock - the click itself is only ever awaited for its own
79
+ * completion, not for whatever navigation it triggers.
80
+ */
81
+ click: string | ((mem: MemPage) => string);
82
+ url?: never;
83
+ });
84
+ /**
85
+ * Builds a {@link NavBlock}. The generated `act()` is always exactly
86
+ * `page.goto(url)` or `page.locator(click).click()` - never anything else,
87
+ * never author-supplied. Callers get a real `Page` here internally (the one
88
+ * legitimate place navigation belongs); `defineBlock`'s own `ActionPage`
89
+ * narrowing is what discourages navigation everywhere else, and never
90
+ * applies to this generated function since no author ever writes it by
91
+ * hand.
92
+ * @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
93
+ * @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
94
+ * @example defineNavClickBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
95
+ * For click-nav in app code prefer {@link defineNavClickBlock} (url stays here).
96
+ */
97
+ export declare function defineNavBlock<Out extends Checkpoint<string>>(options: NavBlockOptions<Out>): NavBlock<Out>;
98
+ /**
99
+ * Click-only {@link NavBlock}. Same runtime as {@link defineNavBlock} with
100
+ * `click` - use this in app code so you never wonder whether a Nav is url or
101
+ * click (no `__waygraphNavClick` sniffing at author time). Prefer
102
+ * {@link defineNavBlock} only for `url` / `goto` deep links.
103
+ */
104
+ export type NavClickBlockOptions<Out extends Checkpoint<string>> = {
105
+ name: string;
106
+ description?: string;
107
+ checkpoint: Out["__state"];
108
+ click: string | ((mem: MemPage) => string);
109
+ requires?: readonly MemKey<any>[];
110
+ verify?: Trait[] | ((out: Out) => Trait[]);
111
+ stubBefore?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
112
+ stubAfter?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
113
+ stubOnError?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
114
+ /** @deprecated Prefer stubAfter. */
115
+ highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
116
+ instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
117
+ };
118
+ export type NavClickBlock<Out extends Checkpoint<string>> = NavBlock<Out>;
119
+ /** @see {@link NavClickBlockOptions} */
120
+ export declare function defineNavClickBlock<Out extends Checkpoint<string>>(options: NavClickBlockOptions<Out>): NavClickBlock<Out>;
121
+ /**
122
+ * TypeScript salt over {@link defineNavBlock}: mem-picked destination
123
+ * (which product row / which request id). Still a NavBlock / Block at runtime.
124
+ */
125
+ export type MemNavBlock<Out extends Checkpoint<string>> = NavBlock<Out> & {
126
+ requires: readonly MemKey<any>[];
127
+ instanceOptions: NonNullable<Block<Checkpoint<string>, Out>["instanceOptions"]>;
128
+ };
129
+ /**
130
+ * {@link defineNavBlock} constrained to mem-picked navigation: requires +
131
+ * instanceOptions mandatory, and `click`/`url` should depend on that mem
132
+ * (typically `click: (mem) => …`). Prefer this when the Block opens one of
133
+ * several live rows/items rather than a fixed nav target. Still a Block.
134
+ */
135
+ export declare function defineMemNavBlock<Out extends Checkpoint<string>>(options: NavBlockOptions<Out> & {
136
+ requires: readonly MemKey<any>[];
137
+ instanceOptions: NonNullable<Block<Checkpoint<string>, Out>["instanceOptions"]>;
138
+ }): MemNavBlock<Out>;
@@ -0,0 +1,110 @@
1
+ // Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { checkpoint } from "../../types.js";
3
+ import { defineBlock } from "../core.js";
4
+ /**
5
+ * Builds a {@link NavBlock}. The generated `act()` is always exactly
6
+ * `page.goto(url)` or `page.locator(click).click()` - never anything else,
7
+ * never author-supplied. Callers get a real `Page` here internally (the one
8
+ * legitimate place navigation belongs); `defineBlock`'s own `ActionPage`
9
+ * narrowing is what discourages navigation everywhere else, and never
10
+ * applies to this generated function since no author ever writes it by
11
+ * hand.
12
+ * @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
13
+ * @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
14
+ * @example defineNavClickBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
15
+ * For click-nav in app code prefer {@link defineNavClickBlock} (url stays here).
16
+ */
17
+ export function defineNavBlock(options) {
18
+ const built = defineBlock({
19
+ name: options.name,
20
+ ...(options.description ? { description: options.description } : {}),
21
+ ...(options.requires ? { requires: options.requires } : {}),
22
+ ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
23
+ instruction: {
24
+ async act(page, _input, mem) {
25
+ if (options.url !== undefined) {
26
+ const url = typeof options.url === "function" ? options.url(mem) : options.url;
27
+ await page.goto(url);
28
+ }
29
+ else {
30
+ // Always Locator.click() so step-mode's demo cursor/pulse patch
31
+ // (instrumentInteractionHighlighting) sees NavBlock click-nav -
32
+ // never page.click() / goto teleport.
33
+ const selector = typeof options.click === "function" ? options.click(mem) : options.click;
34
+ await page.locator(selector).click();
35
+ }
36
+ },
37
+ resolve: () => checkpoint(options.checkpoint),
38
+ ...(options.verify ? { verify: options.verify } : {}),
39
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
40
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
41
+ ...(options.stubOnError ? { stubOnError: options.stubOnError } : {}),
42
+ ...(options.highlights ? { highlights: options.highlights } : {}),
43
+ },
44
+ });
45
+ // Non-enumerable so it never shows up in Object.keys/JSON/autocomplete -
46
+ // purely a runtime marker `waygraph check` (or anything else walking a
47
+ // project's Blocks) can test for to tell a NavBlock apart from a regular
48
+ // one, since the TypeScript type alone (NavBlock = DefinedBlock) doesn't
49
+ // survive to runtime.
50
+ Object.defineProperty(built, "__waygraphKind", {
51
+ value: "nav",
52
+ enumerable: false,
53
+ configurable: false,
54
+ });
55
+ // Demo/step tooling: remember the click selector (or factory) so
56
+ // overlays can label "nav click" without re-parsing source.
57
+ if (options.click !== undefined) {
58
+ Object.defineProperty(built, "__waygraphNavClick", {
59
+ value: options.click,
60
+ enumerable: false,
61
+ configurable: false,
62
+ });
63
+ }
64
+ // Same idea, for the url case - only when it's a plain string (a
65
+ // function URL is mem-dependent/dynamic, can't be compared statically
66
+ // without running it). Real, direct request this responds to: an agent
67
+ // driving Blind Pilot had no structured way to ask "which of the links
68
+ // on THIS page are already covered by a NavBlock" without re-parsing
69
+ // every nav.block.ts's own source - this is what `AutoSession` now reads
70
+ // to warn about real, unmapped nav links found live on the page.
71
+ if (typeof options.url === "string") {
72
+ Object.defineProperty(built, "__waygraphNavUrl", {
73
+ value: options.url,
74
+ enumerable: false,
75
+ configurable: false,
76
+ });
77
+ }
78
+ return built;
79
+ }
80
+ /** @see {@link NavClickBlockOptions} */
81
+ export function defineNavClickBlock(options) {
82
+ return defineNavBlock({
83
+ name: options.name,
84
+ checkpoint: options.checkpoint,
85
+ click: options.click,
86
+ ...(options.description ? { description: options.description } : {}),
87
+ ...(options.requires ? { requires: options.requires } : {}),
88
+ ...(options.verify ? { verify: options.verify } : {}),
89
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
90
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
91
+ ...(options.stubOnError ? { stubOnError: options.stubOnError } : {}),
92
+ ...(options.highlights ? { highlights: options.highlights } : {}),
93
+ ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
94
+ });
95
+ }
96
+ /**
97
+ * {@link defineNavBlock} constrained to mem-picked navigation: requires +
98
+ * instanceOptions mandatory, and `click`/`url` should depend on that mem
99
+ * (typically `click: (mem) => …`). Prefer this when the Block opens one of
100
+ * several live rows/items rather than a fixed nav target. Still a Block.
101
+ */
102
+ export function defineMemNavBlock(options) {
103
+ const built = defineNavBlock(options);
104
+ Object.defineProperty(built, "__waygraphMemKind", {
105
+ value: "nav",
106
+ enumerable: false,
107
+ configurable: false,
108
+ });
109
+ return built;
110
+ }
@@ -0,0 +1,59 @@
1
+ import type { Block, Checkpoint, DefinedBlock, WaygraphHighlight } from "../../types.js";
2
+ import type { Trait } from "../../trait.js";
3
+ import { MemPage } from "../../mem-page.js";
4
+ import type { MemKey } from "../../mem-page.js";
5
+ /** One Block (or lazy factory) registered on a {@link PageBlock}. */
6
+ export type PageMethodEntry = DefinedBlock<any, any> | (() => DefinedBlock<any, any>);
7
+ export type PageBlockOptions<Out extends Checkpoint<string>> = {
8
+ name: string;
9
+ description?: string;
10
+ /** Checkpoint tag for this screen (hub). */
11
+ checkpoint: Out["__state"];
12
+ verify?: Trait[] | ((out: Out) => Trait[]);
13
+ stubBefore?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
14
+ stubAfter?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
15
+ stubOnError?: import("../../highlights.js").HighlightStubPhaseOrFn<Out>;
16
+ /** @deprecated Prefer stubAfter. */
17
+ highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
18
+ requires?: readonly MemKey<any>[];
19
+ /**
20
+ * Methods available on this page. Array, or a record of Blocks / `() => Block`
21
+ * factories (lazy so the page object can colocate method definitions).
22
+ */
23
+ methods?: readonly PageMethodEntry[] | Record<string, PageMethodEntry>;
24
+ /**
25
+ * Optional deep-link onto this page (`goto`). XOR with `click`. Omit when the
26
+ * hub is arrival-only (previous Block already resolved here).
27
+ */
28
+ url?: string | ((mem: MemPage) => string);
29
+ /** Optional click-nav onto this page. XOR with `url`. */
30
+ click?: string | ((mem: MemPage) => string);
31
+ instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
32
+ };
33
+ /**
34
+ * Page hub Block: one Checkpoint = this screen; methods hang off the hub for
35
+ * readability / auto grouping. Runtime is still a Block (`__waygraphKind = "page"`).
36
+ * `act` is goto/click when `url`/`click` is set, otherwise a no-op (already here).
37
+ * Child methods stay ordinary Blocks for graph edges - the page only registers them.
38
+ * Branded `__wgFactory: "page"` (not `"nav"`) - both are accepted by
39
+ * {@link MapBuilder.gotoPage} / {@link MapBuilder.gotoExternal}.
40
+ */
41
+ export type PageBlock<Out extends Checkpoint<string>> = Omit<DefinedBlock<Checkpoint<string>, Out>, "withVerify" | "modVerify" | "modVerifyAll" | "stubBefore" | "stubAfter" | "stubOnError"> & {
42
+ readonly __wgFactory?: "page";
43
+ /** Resolved method Blocks registered at define time. */
44
+ readonly methods: readonly DefinedBlock<any, any>[];
45
+ withVerify(verify: Trait[] | ((out: Out) => Trait[])): PageBlock<Out>;
46
+ modVerify(nameOrIndex: string | number, newCheck: Trait["check"] | Trait): PageBlock<Out>;
47
+ modVerifyAll(patches: Record<string, Trait["check"] | Trait>): PageBlock<Out>;
48
+ stubBefore(stub: import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
49
+ stubBefore(stub: import("../../highlights.js").StubLifecycleFn<Out>): PageBlock<Out>;
50
+ stubBefore(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
51
+ stubAfter(stub: import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
52
+ stubAfter(stub: import("../../highlights.js").StubLifecycleFn<Out>): PageBlock<Out>;
53
+ stubAfter(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
54
+ stubOnError(stub: import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
55
+ stubOnError(stub: import("../../highlights.js").StubLifecycleFn<Out>): PageBlock<Out>;
56
+ stubOnError(stub: (out: Out) => import("../../highlights.js").HighlightStubPhase): PageBlock<Out>;
57
+ };
58
+ /** @see {@link PageBlockOptions} */
59
+ export declare function definePageBlock<Out extends Checkpoint<string>>(options: PageBlockOptions<Out>): PageBlock<Out>;
@@ -0,0 +1,91 @@
1
+ // Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { checkpoint } from "../../types.js";
3
+ import { defineBlock } from "../core.js";
4
+ function resolvePageMethods(methods) {
5
+ if (!methods)
6
+ return [];
7
+ const entries = Array.isArray(methods) ? methods : Object.values(methods);
8
+ return entries.map((entry) => (typeof entry === "function" ? entry() : entry));
9
+ }
10
+ /** @see {@link PageBlockOptions} */
11
+ export function definePageBlock(options) {
12
+ if (options.url !== undefined && options.click !== undefined) {
13
+ throw new Error(`definePageBlock("${options.name}"): pass url XOR click (or neither for arrival-only hub)`);
14
+ }
15
+ const methodList = resolvePageMethods(options.methods);
16
+ const built = defineBlock({
17
+ name: options.name,
18
+ ...(options.description ? { description: options.description } : {}),
19
+ ...(options.requires ? { requires: options.requires } : {}),
20
+ ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
21
+ instruction: {
22
+ async act(page, _input, mem) {
23
+ if (options.url !== undefined) {
24
+ const url = typeof options.url === "function" ? options.url(mem) : options.url;
25
+ await page.goto(url);
26
+ }
27
+ else if (options.click !== undefined) {
28
+ const selector = typeof options.click === "function" ? options.click(mem) : options.click;
29
+ await page.locator(selector).click();
30
+ }
31
+ // else: arrival-only hub — previous Block already landed here
32
+ },
33
+ resolve: () => checkpoint(options.checkpoint),
34
+ ...(options.verify ? { verify: options.verify } : {}),
35
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
36
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
37
+ ...(options.stubOnError ? { stubOnError: options.stubOnError } : {}),
38
+ ...(options.highlights ? { highlights: options.highlights } : {}),
39
+ },
40
+ });
41
+ Object.defineProperty(built, "__waygraphKind", {
42
+ value: "page",
43
+ enumerable: false,
44
+ configurable: false,
45
+ });
46
+ Object.defineProperty(built, "__waygraphSalt", {
47
+ value: "page",
48
+ enumerable: false,
49
+ configurable: false,
50
+ });
51
+ Object.defineProperty(built, "methods", {
52
+ value: Object.freeze([...methodList]),
53
+ enumerable: true,
54
+ configurable: false,
55
+ writable: false,
56
+ });
57
+ Object.defineProperty(built, "__waygraphMethods", {
58
+ value: methodList.map((m) => m.name),
59
+ enumerable: false,
60
+ configurable: false,
61
+ });
62
+ // Same marker `defineNavBlock` sets, same reason - a page hub with a
63
+ // `url` (deep-link) is exactly as "known, coverable" as a plain
64
+ // `defineNavBlock`, and in practice most real projects' own nav Blocks
65
+ // ARE page hubs (methods hanging off them), not bare NavBlocks - the real
66
+ // saucedemo fixture this whole package tests against uses definePageBlock
67
+ // for every one of its own nav-* Blocks. Missing this here meant the
68
+ // marker existed but was never actually set on any real project's Blocks.
69
+ if (typeof options.url === "string") {
70
+ Object.defineProperty(built, "__waygraphNavUrl", {
71
+ value: options.url,
72
+ enumerable: false,
73
+ configurable: false,
74
+ });
75
+ }
76
+ if (options.url !== undefined || options.click !== undefined) {
77
+ Object.defineProperty(built, "__waygraphPageDeepLink", {
78
+ value: true,
79
+ enumerable: false,
80
+ configurable: false,
81
+ });
82
+ }
83
+ if (options.click !== undefined) {
84
+ Object.defineProperty(built, "__waygraphNavClick", {
85
+ value: options.click,
86
+ enumerable: false,
87
+ configurable: false,
88
+ });
89
+ }
90
+ return built;
91
+ }
@@ -0,0 +1,75 @@
1
+ import type { BlockInfo } from "./flow.js";
2
+ import type { Block, Checkpoint } from "../types.js";
3
+ import type { Trait } from "../trait.js";
4
+ /**
5
+ * A Block guaranteed to have `withStepVerify`/`modStepVerify` attached - what
6
+ * {@link composeBlock} returns. Extends plain `Block`, so it drops straight into
7
+ * `defineFlow([start, ..., composed, ..., end])` like any other Block; no special
8
+ * casing needed anywhere that only expects a `Block`.
9
+ */
10
+ export interface ComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends Block<In, Out> {
11
+ /**
12
+ * This composed Block's constituent steps, in order, as plain data - same
13
+ * shape and purpose as {@link Flow.blocks}.
14
+ * @example GovFormBlock.steps() // [{ name: "step-1-name" }, { name: "step-2-address" }, ...]
15
+ */
16
+ steps(): readonly BlockInfo[];
17
+ /**
18
+ * Replaces one step's whole verify list, wherever it sits in this composed
19
+ * Block - same rules as {@link withVerify}. Throws if no step matches.
20
+ * @example GovFormBlock.withStepVerify("step-2-address", [])
21
+ */
22
+ withStepVerify(step: Block<any, any> | string | number, verify: Trait[] | ((out: any) => Trait[])): ComposedBlock<In, Out>;
23
+ /**
24
+ * Replaces one Trait inside one step's verify list - same rules as
25
+ * {@link modVerify}. Throws if no step matches, or that step has no verify
26
+ * trait at the given name/index.
27
+ * @example GovFormBlock.modStepVerify("step-2-address", "zip-visible", Trait.visible("#zip-confirmed"))
28
+ */
29
+ modStepVerify(step: Block<any, any> | string | number, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): ComposedBlock<In, Out>;
30
+ }
31
+ /**
32
+ * Chains several Blocks into one named, exported unit - what a naturally
33
+ * multi-step feature (a stepper form with several pages, say) wants instead of
34
+ * either one giant Block with no per-step confirmation, or several separately
35
+ * exported Blocks the caller has to wire together by hand every time. Each
36
+ * step keeps its own real `act`/`resolve`/`verify`, chained the same way
37
+ * `connect()` already chains any two Blocks - `composeBlock` is not a new
38
+ * execution mechanism, just `connect()` reduced over the list plus a name and
39
+ * the same step-list closure `Flow` already keeps for its own Block list, so
40
+ * `withStepVerify`/`modStepVerify` can patch one step from outside without the
41
+ * caller needing to hand-rebuild the chain. Resolution is by `.name`, not
42
+ * object identity, so a step reference stays valid even after an earlier patch
43
+ * swapped that slot for a new object - see {@link Flow.withBlockVerify} for
44
+ * the identical guarantee one level up. Each call returns a new composed
45
+ * Block; the original is untouched.
46
+ * @example composeBlock("gov-form", [step1, step2, step3, step4])
47
+ */
48
+ export declare function composeBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, Out>]): ComposedBlock<In, Out>;
49
+ export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, Out>]): ComposedBlock<In, Out>;
50
+ export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, Out>]): ComposedBlock<In, Out>;
51
+ export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, Out>]): ComposedBlock<In, Out>;
52
+ export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, Out>]): ComposedBlock<In, Out>;
53
+ export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): ComposedBlock<In, Out>;
54
+ /**
55
+ * Same shape as {@link composeBlock}, marked {@link FastForwardComposedBlock.fastForward}
56
+ * so demo/run/traverse treat it as **one opaque step** that **blitzes wall-clock**
57
+ * (skip overlay theater + no Playwright slowMo on that step's acts) unless
58
+ * `--ff-expand` / `WAYGRAPH_FF_EXPAND=1`. Inner acts still run for real;
59
+ * failures are prefixed `ff-name > …` so the locus names both the FF unit and
60
+ * the inner step. Prefer this for boring or heavy-check prefixes (auth, seed,
61
+ * dashboard settle) — you may put **several** FF units in one Flow with normal
62
+ * Blocks between them.
63
+ * @example fastForwardComposeBlock("ff-owner-auth", [NavLoginBlock, SubmitLoginForFlow])
64
+ */
65
+ export interface FastForwardComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends ComposedBlock<In, Out> {
66
+ readonly fastForward: true;
67
+ }
68
+ /** True when `block` came from {@link fastForwardComposeBlock}. */
69
+ export declare function isFastForwardBlock(block: Block<any, any>): block is FastForwardComposedBlock<any, any>;
70
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, Out>]): FastForwardComposedBlock<In, Out>;
71
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, Out>]): FastForwardComposedBlock<In, Out>;
72
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, Out>]): FastForwardComposedBlock<In, Out>;
73
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, Out>]): FastForwardComposedBlock<In, Out>;
74
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, Out>]): FastForwardComposedBlock<In, Out>;
75
+ export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): FastForwardComposedBlock<In, Out>;
@@ -0,0 +1,76 @@
1
+ import { connect } from "../types.js";
2
+ import { findBlockIndex, modVerify, withVerify } from "./core.js";
3
+ export function composeBlock(name, steps) {
4
+ if (steps.length === 0) {
5
+ throw new Error(`composeBlock: "${name}" needs at least one step`);
6
+ }
7
+ const chain = steps.reduce((a, b) => connect(a, b));
8
+ return {
9
+ ...chain,
10
+ name,
11
+ steps: () => steps.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
12
+ withStepVerify(step, verify) {
13
+ const index = findBlockIndex(steps, step);
14
+ const patched = [...steps];
15
+ patched[index] = withVerify(steps[index], verify);
16
+ return composeBlock(name, patched);
17
+ },
18
+ modStepVerify(step, nameOrIndex, newCheck) {
19
+ const index = findBlockIndex(steps, step);
20
+ const patched = [...steps];
21
+ patched[index] = modVerify(steps[index], nameOrIndex, newCheck);
22
+ return composeBlock(name, patched);
23
+ },
24
+ };
25
+ }
26
+ /** True when `block` came from {@link fastForwardComposeBlock}. */
27
+ export function isFastForwardBlock(block) {
28
+ return (block.fastForward === true &&
29
+ typeof block.steps === "function");
30
+ }
31
+ function wrapFfError(ffName, err) {
32
+ const msg = err instanceof Error ? err.message : String(err);
33
+ if (msg.startsWith(`${ffName} > `)) {
34
+ return err instanceof Error ? err : new Error(msg);
35
+ }
36
+ const wrapped = new Error(`${ffName} > ${msg}`);
37
+ if (err instanceof Error && err.stack) {
38
+ wrapped.stack = err.stack;
39
+ }
40
+ return wrapped;
41
+ }
42
+ export function fastForwardComposeBlock(name, steps) {
43
+ if (steps.length === 0) {
44
+ throw new Error(`fastForwardComposeBlock: "${name}" needs at least one step`);
45
+ }
46
+ const base = composeBlock(name, steps);
47
+ const origAct = base.instruction.act.bind(base.instruction);
48
+ const ff = {
49
+ ...base,
50
+ fastForward: true,
51
+ instruction: {
52
+ ...base.instruction,
53
+ async act(page, input, mem) {
54
+ try {
55
+ await origAct(page, input, mem);
56
+ }
57
+ catch (e) {
58
+ throw wrapFfError(name, e);
59
+ }
60
+ },
61
+ },
62
+ withStepVerify(step, verify) {
63
+ const index = findBlockIndex(steps, step);
64
+ const patched = [...steps];
65
+ patched[index] = withVerify(steps[index], verify);
66
+ return fastForwardComposeBlock(name, patched);
67
+ },
68
+ modStepVerify(step, nameOrIndex, newCheck) {
69
+ const index = findBlockIndex(steps, step);
70
+ const patched = [...steps];
71
+ patched[index] = modVerify(steps[index], nameOrIndex, newCheck);
72
+ return fastForwardComposeBlock(name, patched);
73
+ },
74
+ };
75
+ return ff;
76
+ }