waygraph 0.15.24 → 0.15.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/engine/blocks/effect.d.ts +41 -0
  2. package/dist/engine/blocks/effect.js +23 -0
  3. package/dist/engine/blocks/method.d.ts +90 -0
  4. package/dist/engine/blocks/method.js +70 -0
  5. package/dist/engine/blocks/nav.d.ts +138 -0
  6. package/dist/engine/blocks/nav.js +110 -0
  7. package/dist/engine/blocks/page.d.ts +59 -0
  8. package/dist/engine/blocks/page.js +91 -0
  9. package/dist/engine/compose.d.ts +75 -0
  10. package/dist/engine/compose.js +76 -0
  11. package/dist/engine/config.d.ts +55 -0
  12. package/dist/engine/config.js +17 -0
  13. package/dist/engine/core.d.ts +92 -0
  14. package/dist/engine/core.js +256 -0
  15. package/dist/engine/engine-class.d.ts +289 -0
  16. package/dist/engine/engine-class.js +257 -0
  17. package/dist/engine/flow.d.ts +266 -0
  18. package/dist/engine/flow.js +387 -0
  19. package/dist/engine/locate.d.ts +26 -0
  20. package/dist/engine/locate.js +62 -0
  21. package/dist/engine/run-graph.d.ts +147 -0
  22. package/dist/engine/run-graph.js +223 -0
  23. package/dist/engine.d.ts +11 -1224
  24. package/dist/engine.js +12 -1550
  25. package/dist/highlights/css.d.ts +14 -0
  26. package/dist/highlights/css.js +58 -0
  27. package/dist/highlights/demo-pace.d.ts +56 -0
  28. package/dist/highlights/demo-pace.js +184 -0
  29. package/dist/highlights/device.d.ts +29 -0
  30. package/dist/highlights/device.js +171 -0
  31. package/dist/highlights/merge.d.ts +8 -0
  32. package/dist/highlights/merge.js +173 -0
  33. package/dist/highlights/run-phase.d.ts +43 -0
  34. package/dist/highlights/run-phase.js +211 -0
  35. package/dist/highlights/stub-ctx.d.ts +10 -0
  36. package/dist/highlights/stub-ctx.js +281 -0
  37. package/dist/highlights/style.d.ts +27 -0
  38. package/dist/highlights/style.js +84 -0
  39. package/dist/highlights/todo-dock.d.ts +52 -0
  40. package/dist/highlights/todo-dock.js +248 -0
  41. package/dist/highlights/types.d.ts +536 -0
  42. package/dist/highlights/types.js +1 -0
  43. package/dist/highlights.d.ts +9 -734
  44. package/dist/highlights.js +10 -1416
  45. package/examples/saucedemo/.waygraph-auto/008f4e6e.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/22fefa13.log +0 -0
  47. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ import type { BrowserType } from "@playwright/test";
2
+ import type { Layout } from "./run-graph.js";
3
+ /**
4
+ * Resolves which `BrowserType` actually launches for a `mem`-only run (no
5
+ * caller-supplied context) - `config.browsers[browserName]` if given,
6
+ * otherwise the real `@playwright/test` export. Waygraph is deliberately
7
+ * "just an opinionated Playwright" - it doesn't own browser automation
8
+ * itself, so it shouldn't force every consumer onto the vanilla launcher.
9
+ * A stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
10
+ * plus a stealth plugin) is a drop-in replacement for `chromium`/`firefox`/
11
+ * `webkit` - same `.launch()` shape - so accepting one here costs nothing for
12
+ * a consumer who never sets it.
13
+ */
14
+ export declare function resolveLauncher(browserName: "chromium" | "firefox" | "webkit", config: EngineConfig | undefined): BrowserType;
15
+ /** Configures how an Engine launches its own browser - only used by `Flow.run(mem)` (no context given); ignored by `Flow.run(context, mem)`, since that context is already launched. */
16
+ export interface EngineConfig {
17
+ /** Default `true`, matching Playwright's own default. */
18
+ headless?: boolean;
19
+ /** Default `"chromium"`. */
20
+ browserName?: "chromium" | "firefox" | "webkit";
21
+ /** Milliseconds Playwright pauses before each operation - for watching a non-headless run with your own eyes, not for real runs. Default `0`. */
22
+ slowMo?: number;
23
+ /**
24
+ * Overrides which `BrowserType` actually launches, per browser name -
25
+ * default is the real `@playwright/test` export for whichever one
26
+ * `browserName` picks. Accepts anything shaped like Playwright's own
27
+ * `chromium`/`firefox`/`webkit` (same `.launch()` signature) - a
28
+ * stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
29
+ * plus a stealth plugin) drops in here unmodified. Puppeteer is a
30
+ * different `Page`/`BrowserContext` shape entirely and isn't supported by
31
+ * this seam.
32
+ * @example new Engine({ browsers: { chromium: stealthChromium } })
33
+ */
34
+ browsers?: Partial<Record<"chromium" | "firefox" | "webkit", BrowserType>>;
35
+ /**
36
+ * Playwright `recordVideo` - saves a `.webm` when the context closes.
37
+ * Works headless. Lighter than a full clip-engine pipeline (no overlay/ffmpeg).
38
+ * @example new Engine({ recordVideo: { dir: "./videos" } })
39
+ */
40
+ recordVideo?: {
41
+ dir: string;
42
+ size?: {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ };
47
+ /**
48
+ * Cross-cutting, automatically-enforced verify (see {@link defineLayout})
49
+ * - applies to every Flow this Engine defines. Works the same regardless
50
+ * of folder convention (Waygraph Map or freeform "manual mode") since
51
+ * matching is by Checkpoint tag, never by file path.
52
+ * @example new Engine({ layouts: [AppShellLayout] })
53
+ */
54
+ layouts?: readonly Layout[];
55
+ }
@@ -0,0 +1,17 @@
1
+ // Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ import { chromium, firefox, webkit } from "@playwright/test";
3
+ const LAUNCHERS = { chromium, firefox, webkit };
4
+ /**
5
+ * Resolves which `BrowserType` actually launches for a `mem`-only run (no
6
+ * caller-supplied context) - `config.browsers[browserName]` if given,
7
+ * otherwise the real `@playwright/test` export. Waygraph is deliberately
8
+ * "just an opinionated Playwright" - it doesn't own browser automation
9
+ * itself, so it shouldn't force every consumer onto the vanilla launcher.
10
+ * A stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
11
+ * plus a stealth plugin) is a drop-in replacement for `chromium`/`firefox`/
12
+ * `webkit` - same `.launch()` shape - so accepting one here costs nothing for
13
+ * a consumer who never sets it.
14
+ */
15
+ export function resolveLauncher(browserName, config) {
16
+ return config?.browsers?.[browserName] ?? LAUNCHERS[browserName];
17
+ }
@@ -0,0 +1,92 @@
1
+ import type { ActionPage, Block, Checkpoint, DefinedBlock, Instruction } from "../types.js";
2
+ import { MemPage } from "../mem-page.js";
3
+ import type { Trait } from "../trait.js";
4
+ export declare function findBlockIndex(middle: readonly Block<any, any>[], block: Block<any, any> | string | number): number;
5
+ /**
6
+ * Attaches routing to `block` without touching its act/observe/resolve/verify -
7
+ * `routes` must cover every tag of `block`'s output (a missing route is a compile
8
+ * error), and the returned Block runs exactly as `block` always did, just followed
9
+ * by whichever next Block the resolved tag routes to. `null` for a tag means that
10
+ * tag is terminal - the run stops there, same as a Block with no routing at all.
11
+ * A route pointing back at `block` itself is a self-loop; see `runGraph`'s
12
+ * `maxSteps` for its safety net. The return type is deliberately `Block<In, any>` -
13
+ * a route can lead anywhere, including back through itself, so there is no fixed
14
+ * Out type to express; state the expected outcomes to `runGraph` via `terminals`
15
+ * instead.
16
+ */
17
+ export declare function branch<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, routes: {
18
+ [K in Out["__state"]]: Block<Extract<Out, Checkpoint<K>>, any> | null;
19
+ }): DefinedBlock<In, any>;
20
+ /**
21
+ * Builds a Block with `withVerify`/`modVerify`/`modVerifyAll` attached as
22
+ * methods - `LoginBlock.withVerify([...])`, discoverable by typing
23
+ * `LoginBlock.` in an editor, not just callable as `withVerify(LoginBlock, ...)`
24
+ * if you already know it exists. A raw object literal is still a perfectly
25
+ * valid Block (every test fixture in this codebase is one) - it just won't have
26
+ * these attached. The methods are thin: they delegate to the identically-named
27
+ * free functions, which remain the one place this logic actually lives.
28
+ */
29
+ export declare function defineBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: {
30
+ name: string;
31
+ description?: Block<In, Out>["description"];
32
+ instruction: Omit<Instruction<In, Out, any>, "act"> & {
33
+ act(page: ActionPage, input: In, mem: MemPage): Promise<void>;
34
+ };
35
+ next?: Block<In, Out>["next"];
36
+ requires?: Block<In, Out>["requires"];
37
+ routes?: Block<In, Out>["routes"];
38
+ instanceOptions?: Block<In, Out>["instanceOptions"];
39
+ }): DefinedBlock<In, Out>;
40
+ /**
41
+ * Returns a new Block that runs exactly like `block` - same act/observe/resolve,
42
+ * same routing - but with `verify` replaced. For giving one flow stricter (or
43
+ * looser, or just different) confirmation on a Block it didn't author, without
44
+ * that Block needing to have been written as a parameterized factory in
45
+ * anticipation. Same move as `branch()`: decorate an existing Block, don't ask
46
+ * every Block to pre-build its own override mechanism.
47
+ *
48
+ * Wrap the one Block you want to change, in place, wherever it already sits in
49
+ * a `defineFlow([...])` array - every other Block stays untouched:
50
+ * `defineFlow([start, LoginBlock, withVerify(AddToCartBlock, [...]), end])`.
51
+ * There is no separate positional array to keep in sync and no `null`
52
+ * placeholder for "skip this one" - `defineFlow` has no per-slot verify
53
+ * argument at all, only whatever `verify` each Block's own `instruction` (or
54
+ * a `withVerify(...)` wrapping it) carries with it.
55
+ */
56
+ export declare function withVerify<B extends Block<any, any>>(block: B, verify: Trait[] | ((out: any) => Trait[])): B;
57
+ type StubPhaseFn<Out extends Checkpoint<string>> = import("../highlights.js").HighlightStubPhaseOrFn<Out>;
58
+ /**
59
+ * Attach / chain a stubBefore lifecycle on a Block. Prefer
60
+ * `(ctx) => { ctx.ring(...); const x = ctx.mem?.get(Key); }`.
61
+ * @example FillUsernameBlock.stubBefore((ctx) => { ctx.ring("username", { ... }) })
62
+ */
63
+ export declare function withStubBefore<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
64
+ /** @see {@link withStubBefore} */
65
+ export declare function withStubAfter<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
66
+ /** @see {@link withStubBefore} */
67
+ export declare function withStubOnError<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
68
+ /**
69
+ * Replaces one Trait inside `block`'s verify list, leaving every other check
70
+ * untouched - the v1/v2/legacy-variant case, where one check needs a different
71
+ * selector or URL pattern but the rest of the Block is identical. Addressed by
72
+ * the Trait's own `name` (recommended - inserting or reordering other checks
73
+ * can't silently retarget it) or, if you genuinely know the position and don't
74
+ * need that safety, a numeric index. Either way a bad address fails loud
75
+ * immediately instead of quietly doing nothing. `newCheck` can be a bare
76
+ * `check` function (keeps the existing name) or a full `Trait` (to rename too).
77
+ * Only works on a flat `verify` array - a function-form `verify` (from
78
+ * `verify: (out) => Trait[]`) has no fixed list to address into, so this
79
+ * throws rather than guess; use `withVerify` to replace it wholesale instead.
80
+ */
81
+ export declare function modVerify<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): DefinedBlock<In, Out>;
82
+ /**
83
+ * `modVerify`, but for several named checks in one call - a v2 variant that
84
+ * differs in five ways from v1 shouldn't need five nested `modVerify(modVerify(
85
+ * modVerify(...)))` calls to track by eye. `patches` keys are plain strings (a
86
+ * Trait's `name`, not a `MemKey` instance), so an ordinary object literal is
87
+ * fine here - unlike `MemPage`, there's no identity-collision reason to need
88
+ * `[name, check]` pairs. Applies each patch in turn via `modVerify`, so the
89
+ * same fail-loud-on-unknown-name behavior holds for every entry.
90
+ */
91
+ export declare function modVerifyAll<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, patches: Record<string, Trait["check"] | Trait>): DefinedBlock<In, Out>;
92
+ export {};
@@ -0,0 +1,256 @@
1
+ export function findBlockIndex(middle, block) {
2
+ if (typeof block === "number") {
3
+ if (block < 0 || block >= middle.length) {
4
+ throw new Error(`Flow: no Block at index ${block} in this flow (has ${middle.length})`);
5
+ }
6
+ return block;
7
+ }
8
+ const blockName = typeof block === "string" ? block : block.name;
9
+ const index = middle.findIndex((b) => b.name === blockName);
10
+ if (index === -1) {
11
+ throw new Error(`Flow: no Block named "${blockName}" in this flow (has: ${middle.map((b) => b.name).join(", ")})`);
12
+ }
13
+ return index;
14
+ }
15
+ /**
16
+ * Attaches routing to `block` without touching its act/observe/resolve/verify -
17
+ * `routes` must cover every tag of `block`'s output (a missing route is a compile
18
+ * error), and the returned Block runs exactly as `block` always did, just followed
19
+ * by whichever next Block the resolved tag routes to. `null` for a tag means that
20
+ * tag is terminal - the run stops there, same as a Block with no routing at all.
21
+ * A route pointing back at `block` itself is a self-loop; see `runGraph`'s
22
+ * `maxSteps` for its safety net. The return type is deliberately `Block<In, any>` -
23
+ * a route can lead anywhere, including back through itself, so there is no fixed
24
+ * Out type to express; state the expected outcomes to `runGraph` via `terminals`
25
+ * instead.
26
+ */
27
+ export function branch(block, routes) {
28
+ const base = stripMethods(block);
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ let routed;
31
+ const routesAsData = {};
32
+ for (const [tag, target] of Object.entries(routes)) {
33
+ routesAsData[tag] = target === null ? null : target === block ? base.name : target.name;
34
+ }
35
+ routed = defineBlock({
36
+ ...base,
37
+ next: (checkpoint) => {
38
+ const target = routes[checkpoint.__state];
39
+ if (target === undefined || target === null)
40
+ return undefined;
41
+ // A route naming `block` itself (the only self-reference in scope when
42
+ // `routes` was written) means "loop back with routing intact" - substitute
43
+ // the routed Block, not the original with no `next` of its own.
44
+ return target === block ? routed : target;
45
+ },
46
+ routes: routesAsData,
47
+ });
48
+ return routed;
49
+ }
50
+ /**
51
+ * Strips `withVerify`/`modVerify`/`modVerifyAll` off a Block, keeping everything
52
+ * else - `name`, `instruction`, `next`, `requires`. Every function below that
53
+ * returns a new Block calls this on its input first, then rebuilds through
54
+ * `defineBlock`. Without it, a Block's attached methods close over the shape it
55
+ * had *when they were attached* - call `.withVerify()` on a Block that
56
+ * `branch()` added routing to afterward, and without this step the result would
57
+ * silently drop that routing, since the stale closure never knew about it.
58
+ */
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ function stripMethods(block) {
61
+ return {
62
+ name: block.name,
63
+ instruction: block.instruction,
64
+ ...(block.description ? { description: block.description } : {}),
65
+ ...(block.next ? { next: block.next } : {}),
66
+ ...(block.requires ? { requires: block.requires } : {}),
67
+ ...(block.routes ? { routes: block.routes } : {}),
68
+ ...(block.instanceOptions ? { instanceOptions: block.instanceOptions } : {}),
69
+ };
70
+ }
71
+ /** Copy non-enumerable waygraph markers so decorate/withVerify keep map() kind checks. */
72
+ function copyWaygraphRuntime(from, to) {
73
+ for (const key of [
74
+ "__waygraphKind",
75
+ "__waygraphSalt",
76
+ "__waygraphNavUrl",
77
+ "__waygraphNavClick",
78
+ ]) {
79
+ const desc = Object.getOwnPropertyDescriptor(from, key);
80
+ if (!desc)
81
+ continue;
82
+ // Re-define as configurable so later decorate chains can copy again
83
+ // (factory markers are non-configurable on the original).
84
+ Object.defineProperty(to, key, {
85
+ value: desc.value,
86
+ enumerable: false,
87
+ writable: false,
88
+ configurable: true,
89
+ });
90
+ }
91
+ const ff = from.fastForward;
92
+ if (ff === true) {
93
+ to.fastForward = true;
94
+ }
95
+ const pace = from.demoPace;
96
+ if (pace !== undefined) {
97
+ to.demoPace = pace;
98
+ }
99
+ }
100
+ /**
101
+ * Builds a Block with `withVerify`/`modVerify`/`modVerifyAll` attached as
102
+ * methods - `LoginBlock.withVerify([...])`, discoverable by typing
103
+ * `LoginBlock.` in an editor, not just callable as `withVerify(LoginBlock, ...)`
104
+ * if you already know it exists. A raw object literal is still a perfectly
105
+ * valid Block (every test fixture in this codebase is one) - it just won't have
106
+ * these attached. The methods are thin: they delegate to the identically-named
107
+ * free functions, which remain the one place this logic actually lives.
108
+ */
109
+ export function defineBlock(base) {
110
+ const plain = {
111
+ name: base.name,
112
+ instruction: base.instruction,
113
+ ...(base.description ? { description: base.description } : {}),
114
+ ...(base.next ? { next: base.next } : {}),
115
+ ...(base.requires ? { requires: base.requires } : {}),
116
+ ...(base.routes ? { routes: base.routes } : {}),
117
+ ...(base.instanceOptions ? { instanceOptions: base.instanceOptions } : {}),
118
+ };
119
+ // Methods must close over `defined` (this object), not `plain` - factories
120
+ // attach __waygraphKind/__waygraphSalt on the returned object after
121
+ // defineBlock returns; decorate/withVerify need those markers.
122
+ const defined = {
123
+ ...plain,
124
+ withVerify: (verify) => withVerify(defined, verify),
125
+ modVerify: (nameOrIndex, newCheck) => modVerify(defined, nameOrIndex, newCheck),
126
+ modVerifyAll: (patches) => modVerifyAll(defined, patches),
127
+ stubBefore: (stub) => withStubBefore(defined, stub),
128
+ stubAfter: (stub) => withStubAfter(defined, stub),
129
+ stubOnError: (stub) => withStubOnError(defined, stub),
130
+ };
131
+ return defined;
132
+ }
133
+ /**
134
+ * Returns a new Block that runs exactly like `block` - same act/observe/resolve,
135
+ * same routing - but with `verify` replaced. For giving one flow stricter (or
136
+ * looser, or just different) confirmation on a Block it didn't author, without
137
+ * that Block needing to have been written as a parameterized factory in
138
+ * anticipation. Same move as `branch()`: decorate an existing Block, don't ask
139
+ * every Block to pre-build its own override mechanism.
140
+ *
141
+ * Wrap the one Block you want to change, in place, wherever it already sits in
142
+ * a `defineFlow([...])` array - every other Block stays untouched:
143
+ * `defineFlow([start, LoginBlock, withVerify(AddToCartBlock, [...]), end])`.
144
+ * There is no separate positional array to keep in sync and no `null`
145
+ * placeholder for "skip this one" - `defineFlow` has no per-slot verify
146
+ * argument at all, only whatever `verify` each Block's own `instruction` (or
147
+ * a `withVerify(...)` wrapping it) carries with it.
148
+ */
149
+ export function withVerify(block, verify) {
150
+ const base = stripMethods(block);
151
+ const next = defineBlock({ ...base, instruction: { ...base.instruction, verify } });
152
+ copyWaygraphRuntime(block, next);
153
+ return next;
154
+ }
155
+ function isHighlightStubPhase(v) {
156
+ return !!v && typeof v === "object" && !Array.isArray(v);
157
+ }
158
+ /** Run a prior stub (fn or slot map) into an open ctx so decorate can chain. */
159
+ async function applyStubIntoCtx(raw, ctx) {
160
+ if (raw === undefined)
161
+ return;
162
+ if (typeof raw === "function") {
163
+ const ret = await raw(ctx);
164
+ if (isHighlightStubPhase(ret)) {
165
+ for (const [id, stub] of Object.entries(ret)) {
166
+ if (stub)
167
+ ctx.ring(id, stub);
168
+ }
169
+ }
170
+ return;
171
+ }
172
+ if (isHighlightStubPhase(raw)) {
173
+ for (const [id, stub] of Object.entries(raw)) {
174
+ if (stub)
175
+ ctx.ring(id, stub);
176
+ }
177
+ }
178
+ }
179
+ function withStubPhase(block, phase, stub) {
180
+ const base = stripMethods(block);
181
+ const instr = base.instruction;
182
+ const prev = instr[phase];
183
+ const chained = async (ctx) => {
184
+ await applyStubIntoCtx(prev, ctx);
185
+ await applyStubIntoCtx(stub, ctx);
186
+ };
187
+ const next = defineBlock({
188
+ ...base,
189
+ instruction: { ...base.instruction, [phase]: chained },
190
+ });
191
+ copyWaygraphRuntime(block, next);
192
+ return next;
193
+ }
194
+ /**
195
+ * Attach / chain a stubBefore lifecycle on a Block. Prefer
196
+ * `(ctx) => { ctx.ring(...); const x = ctx.mem?.get(Key); }`.
197
+ * @example FillUsernameBlock.stubBefore((ctx) => { ctx.ring("username", { ... }) })
198
+ */
199
+ export function withStubBefore(block, stub) {
200
+ return withStubPhase(block, "stubBefore", stub);
201
+ }
202
+ /** @see {@link withStubBefore} */
203
+ export function withStubAfter(block, stub) {
204
+ return withStubPhase(block, "stubAfter", stub);
205
+ }
206
+ /** @see {@link withStubBefore} */
207
+ export function withStubOnError(block, stub) {
208
+ return withStubPhase(block, "stubOnError", stub);
209
+ }
210
+ /**
211
+ * Replaces one Trait inside `block`'s verify list, leaving every other check
212
+ * untouched - the v1/v2/legacy-variant case, where one check needs a different
213
+ * selector or URL pattern but the rest of the Block is identical. Addressed by
214
+ * the Trait's own `name` (recommended - inserting or reordering other checks
215
+ * can't silently retarget it) or, if you genuinely know the position and don't
216
+ * need that safety, a numeric index. Either way a bad address fails loud
217
+ * immediately instead of quietly doing nothing. `newCheck` can be a bare
218
+ * `check` function (keeps the existing name) or a full `Trait` (to rename too).
219
+ * Only works on a flat `verify` array - a function-form `verify` (from
220
+ * `verify: (out) => Trait[]`) has no fixed list to address into, so this
221
+ * throws rather than guess; use `withVerify` to replace it wholesale instead.
222
+ */
223
+ export function modVerify(block, nameOrIndex, newCheck) {
224
+ const verify = block.instruction.verify;
225
+ if (typeof verify === "function") {
226
+ throw new Error(`modVerify: "${block.name}" has a function-form verify, which has no fixed list to address into - use withVerify to replace it wholesale instead`);
227
+ }
228
+ const traits = verify ?? [];
229
+ const index = typeof nameOrIndex === "number" ? nameOrIndex : traits.findIndex((t) => t.name === nameOrIndex);
230
+ const existing = traits[index];
231
+ if (!existing) {
232
+ throw new Error(typeof nameOrIndex === "number"
233
+ ? `modVerify: "${block.name}" has no verify trait at index ${nameOrIndex} (has ${traits.length})`
234
+ : `modVerify: "${block.name}" has no verify trait named "${nameOrIndex}" (has: ${traits.map((t) => t.name).join(", ") || "none"})`);
235
+ }
236
+ const replacement = typeof newCheck === "function"
237
+ ? { name: typeof nameOrIndex === "string" ? nameOrIndex : existing.name, check: newCheck }
238
+ : newCheck;
239
+ const updated = [...traits];
240
+ updated[index] = replacement;
241
+ return withVerify(block, updated);
242
+ }
243
+ /**
244
+ * `modVerify`, but for several named checks in one call - a v2 variant that
245
+ * differs in five ways from v1 shouldn't need five nested `modVerify(modVerify(
246
+ * modVerify(...)))` calls to track by eye. `patches` keys are plain strings (a
247
+ * Trait's `name`, not a `MemKey` instance), so an ordinary object literal is
248
+ * fine here - unlike `MemPage`, there's no identity-collision reason to need
249
+ * `[name, check]` pairs. Applies each patch in turn via `modVerify`, so the
250
+ * same fail-loud-on-unknown-name behavior holds for every entry.
251
+ */
252
+ export function modVerifyAll(block, patches) {
253
+ const seed = defineBlock(stripMethods(block));
254
+ copyWaygraphRuntime(block, seed);
255
+ return Object.entries(patches).reduce((current, [name, newCheck]) => modVerify(current, name, newCheck), seed);
256
+ }