waygraph 0.15.43 → 0.15.44

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 (58) hide show
  1. package/dist/analysis/coverage-gap.d.ts +11 -0
  2. package/dist/analysis/coverage-gap.js +119 -0
  3. package/dist/analysis/graph.d.ts +92 -0
  4. package/dist/analysis/graph.js +467 -0
  5. package/dist/analysis/map-check.d.ts +18 -0
  6. package/dist/analysis/map-check.js +140 -0
  7. package/dist/analysis/practices-check.d.ts +14 -0
  8. package/dist/analysis/practices-check.js +195 -0
  9. package/dist/auto-explore.d.ts +1 -1
  10. package/dist/auto-explore.js +1 -1
  11. package/dist/auto-session/session.js +2 -2
  12. package/dist/cli/util.d.ts +8 -1
  13. package/dist/cli/util.js +9 -2
  14. package/dist/commands/check.js +2 -2
  15. package/dist/commands/map.js +1 -1
  16. package/dist/commands/session.js +2 -2
  17. package/dist/commands/traverse.js +2 -2
  18. package/dist/engine/branch-regression.js +2 -1
  19. package/dist/engine/compose.js +3 -2
  20. package/dist/engine/core.js +5 -4
  21. package/dist/engine/engine-class.js +13 -12
  22. package/dist/engine/flow.js +4 -3
  23. package/dist/engine/run-graph.js +4 -3
  24. package/dist/errors.d.ts +122 -0
  25. package/dist/errors.js +125 -0
  26. package/dist/highlights/shorthand.d.ts +24 -0
  27. package/dist/highlights/shorthand.js +30 -0
  28. package/dist/index.d.ts +9 -7
  29. package/dist/index.js +5 -4
  30. package/dist/mem-page.js +2 -1
  31. package/dist/pilot.d.ts +1 -1
  32. package/dist/pilot.js +1 -1
  33. package/dist/traverse/coverage.d.ts +59 -0
  34. package/dist/traverse/coverage.js +127 -0
  35. package/dist/traverse/lease.d.ts +18 -0
  36. package/dist/traverse/lease.js +62 -0
  37. package/dist/traverse/run.d.ts +47 -0
  38. package/dist/traverse/run.js +439 -0
  39. package/examples/saucedemo/.waygraph-auto/050e5b55.log +0 -0
  40. package/examples/saucedemo/.waygraph-auto/096fea05.log +20 -0
  41. package/examples/saucedemo/.waygraph-auto/123d1cad.log +5 -0
  42. package/examples/saucedemo/.waygraph-auto/12ed8e28.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/1cb6f44c.log +5 -0
  44. package/examples/saucedemo/.waygraph-auto/1fdb2100.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/573abedd.log +0 -0
  46. package/examples/saucedemo/.waygraph-auto/9a26134c.log +20 -0
  47. package/examples/saucedemo/.waygraph-auto/aa226dd0.log +22 -0
  48. package/examples/saucedemo/.waygraph-auto/ac5dde3d.log +0 -0
  49. package/examples/saucedemo/.waygraph-auto/cae91469.log +0 -0
  50. package/examples/saucedemo/.waygraph-auto/d846cd78.log +0 -0
  51. package/examples/saucedemo/.waygraph-auto/eb38d92e.log +22 -0
  52. package/examples/saucedemo/.waygraph-auto/ee86203e.log +0 -0
  53. package/package.json +1 -1
  54. package/templates/scaffold/scripts/README.md +13 -0
  55. package/templates/scaffold/src/flows/README.md +18 -0
  56. package/templates/scaffold/src/map/README.md +31 -0
  57. package/templates/scaffold/src/states/README.md +15 -0
  58. package/templates/scaffold/tests/README.md +10 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `waygraph check` / `waygraph typecheck` practice warnings — style/typing issues
3
+ * that compile but erode the typed graph. Warnings only (never fail exit code)
4
+ * unless paired with `tsc` errors in `waygraph typecheck`.
5
+ */
6
+ import { readFileSync, readdirSync } from "node:fs";
7
+ import { dirname, relative } from "node:path";
8
+ /** define*Block<…Checkpoint<string>…> — prefer project Checkpoint types. */
9
+ const WILDCARD_CHECKPOINT = /\bdefine(?:Method|Effect|Page|Nav|MemNav|NavClick|Action)Block\s*<[^>]*\bCheckpoint\s*<\s*string\s*>/;
10
+ /** defineAssertBlock({ … }) with no explicit Out type — defaults to wildcard. */
11
+ const ASSERT_NO_TYPE_ARG = /\bdefineAssertBlock\s*\(\s*\{/;
12
+ /** defineAssertBlock, any form - used to scope the empty-verify check below. */
13
+ const IS_ASSERT_BLOCK = /\bdefineAssertBlock\s*[<(]/;
14
+ /**
15
+ * define(Method|Effect)Block<In, Out> with two BARE identifier type args
16
+ * (never a nested generic like Checkpoint<string> - that's WILDCARD_CHECKPOINT's
17
+ * own warning) - lets a real transition (In !== Out) be told apart from a
18
+ * self-loop (In === Out, e.g. fill-username) using only source text.
19
+ */
20
+ const METHOD_EFFECT_GENERIC = /\bdefine(?:Method|Effect)Block\s*<\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*>/;
21
+ /** `verify: []` - a literal empty array, the codegen-stub shape this check exists for. */
22
+ const EMPTY_VERIFY_ARRAY = /\bverify\s*:\s*\[\s*\]/;
23
+ /** Any `verify:` key at all, empty or not. */
24
+ const HAS_VERIFY_KEY = /\bverify\s*:/;
25
+ const SCAN_GLOB = /\.(block|flow)\.ts$/;
26
+ /**
27
+ * A selector string reaching for a regex-based Playwright text pseudo-class (`:has-text(/…/)`,
28
+ * `:text-matches(…)`) or a plain XPath - real, recurring pattern: an LLM asked to target a simple
29
+ * element (one class, one id, one data-attribute) instead writes a regex/XPath from scratch. Not a
30
+ * type error (Playwright's own selector syntax accepts all of this), so nothing else catches it.
31
+ */
32
+ const TEXT_MATCHES_PSEUDO = /:text-matches\(/;
33
+ const HAS_TEXT_REGEX_PSEUDO = /:has-text\(\s*\//;
34
+ const XPATH_LOCATOR = /\.locator\(\s*["'`](?:xpath=)?\/\//;
35
+ /** A selector string literal inline in a Block file (`.locator("…")`, `click: "…"`, `url: "…"`) -
36
+ * scoped to simple, single-token selectors (id/class/attr/tag) so this doesn't fire on the
37
+ * legitimate multi-part selectors `.sel.ts` files themselves are made of. */
38
+ const INLINE_SIMPLE_SELECTOR = /(?:\.locator|\bclick|\burl)\s*[:(]\s*["'`](#|\.)[\w-]+["'`]/;
39
+ const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
40
+ const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
41
+ /** Whole-file opt-out: `// waygraph-ignore-practices` */
42
+ const IGNORE_ALL = /waygraph-ignore-practices/;
43
+ /** Per-kind opt-out: `// waygraph-ignore: multi-input, combined-action` */
44
+ const IGNORE_KINDS = /waygraph-ignore:\s*([^\n*]+)/;
45
+ function walkBlockAndFlowFiles(projectDir, walkDir) {
46
+ return walkDir(projectDir, SCAN_GLOB);
47
+ }
48
+ function ignoredKindsForFile(src) {
49
+ if (IGNORE_ALL.test(src))
50
+ return "all";
51
+ const m = IGNORE_KINDS.exec(src);
52
+ if (!m)
53
+ return new Set();
54
+ const kinds = new Set();
55
+ for (const token of m[1].split(/[,;\s]+/)) {
56
+ const t = token.trim();
57
+ if (t)
58
+ kinds.add(t);
59
+ }
60
+ return kinds;
61
+ }
62
+ function isIgnored(kind, ignore) {
63
+ return ignore === "all" || ignore.has(kind);
64
+ }
65
+ function countMatches(src, re) {
66
+ re.lastIndex = 0;
67
+ return (src.match(re) ?? []).length;
68
+ }
69
+ export function collectPracticeWarnings(projectDir, walkDir, opts) {
70
+ if (opts?.disabled)
71
+ return [];
72
+ const warnings = [];
73
+ for (const file of walkBlockAndFlowFiles(projectDir, walkDir)) {
74
+ const rel = relative(projectDir, file).replace(/\\/g, "/");
75
+ const src = readFileSync(file, "utf-8");
76
+ const ignore = ignoredKindsForFile(src);
77
+ if (!isIgnored("wildcard-checkpoint", ignore) && WILDCARD_CHECKPOINT.test(src)) {
78
+ warnings.push({
79
+ kind: "wildcard-checkpoint",
80
+ file: rel,
81
+ detail: 'uses Checkpoint<string> in a Block helper generic — prefer a typed Checkpoint from src/states/ (e.g. defineMethodBlock<LoginPage, LoginPage>)',
82
+ });
83
+ }
84
+ if (!isIgnored("assert-no-type-arg", ignore) && ASSERT_NO_TYPE_ARG.test(src)) {
85
+ warnings.push({
86
+ kind: "assert-no-type-arg",
87
+ file: rel,
88
+ detail: "defineAssertBlock({…}) without an explicit type argument — add defineAssertBlock<YourCheckpoint>({…}) so defineFlow typing stays honest",
89
+ });
90
+ }
91
+ const isMethodOrEffect = METHOD_OR_EFFECT_FILE.test(rel) || METHOD_OR_EFFECT_DEFINE.test(src);
92
+ if (isMethodOrEffect) {
93
+ const fills = countMatches(src, /\.fill\s*\(/g);
94
+ const clicks = countMatches(src, /\.click\s*\(/g);
95
+ if (!isIgnored("multi-input", ignore) && fills >= 2) {
96
+ warnings.push({
97
+ kind: "multi-input",
98
+ file: rel,
99
+ detail: `Method/Effect act() has ${fills} .fill() calls — one distinct input per Block (split into separate defineMethodBlock files, e.g. fill-username + fill-password + submit-login)`,
100
+ });
101
+ }
102
+ if (!isIgnored("combined-action", ignore) && fills >= 1 && clicks >= 1) {
103
+ warnings.push({
104
+ kind: "combined-action",
105
+ file: rel,
106
+ detail: "Method/Effect act() mixes .fill() and .click() — fill and submit must be separate Blocks (see examples/saucedemo login split)",
107
+ });
108
+ }
109
+ const genericMatch = METHOD_EFFECT_GENERIC.exec(src);
110
+ const isTransition = !!genericMatch && genericMatch[1] !== genericMatch[2];
111
+ if (isTransition &&
112
+ !isIgnored("empty-verify", ignore) &&
113
+ (!HAS_VERIFY_KEY.test(src) || EMPTY_VERIFY_ARRAY.test(src))) {
114
+ warnings.push({
115
+ kind: "empty-verify",
116
+ file: rel,
117
+ detail: `transition Block (${genericMatch[1]} → ${genericMatch[2]}) has no verify checks — resolve() moving ` +
118
+ "to a new Checkpoint is never confirmed by anything; add a Trait (or defineAssertBlock right after it), " +
119
+ "or opt out explicitly with // waygraph-ignore: empty-verify if that's intentional",
120
+ });
121
+ }
122
+ }
123
+ if (!isIgnored("overcomplex-selector", ignore) &&
124
+ (TEXT_MATCHES_PSEUDO.test(src) || HAS_TEXT_REGEX_PSEUDO.test(src) || XPATH_LOCATOR.test(src))) {
125
+ const via = TEXT_MATCHES_PSEUDO.test(src)
126
+ ? ":text-matches(...)"
127
+ : HAS_TEXT_REGEX_PSEUDO.test(src)
128
+ ? ":has-text(/regex/)"
129
+ : "an XPath locator";
130
+ warnings.push({
131
+ kind: "overcomplex-selector",
132
+ file: rel,
133
+ detail: `uses ${via} — reach for a plain CSS selector (id/class/data-attribute, via a _sel.ts/` +
134
+ "*.sel.ts export) instead of a regex/XPath match unless the DOM genuinely has no stable selector",
135
+ });
136
+ }
137
+ if (!isIgnored("inline-selector-should-use-sel-file", ignore) && INLINE_SIMPLE_SELECTOR.test(src)) {
138
+ // Two real, coexisting conventions: the Map layout's own `_sel.ts` (one per page folder, next
139
+ // to `_nav`/`_page`/`_methods`) and the freeform layout's per-concern `<name>.sel.ts`.
140
+ const siblingSelFile = readdirSync(dirname(file)).some((f) => f === "_sel.ts" || f.endsWith(".sel.ts"));
141
+ if (siblingSelFile) {
142
+ warnings.push({
143
+ kind: "inline-selector-should-use-sel-file",
144
+ file: rel,
145
+ detail: "a selector string is inlined here, but this directory already has a _sel.ts / *.sel.ts file — " +
146
+ "add/reuse the selector there instead of a new one-off literal",
147
+ });
148
+ }
149
+ }
150
+ if (!isIgnored("empty-verify", ignore) && IS_ASSERT_BLOCK.test(src) && EMPTY_VERIFY_ARRAY.test(src)) {
151
+ warnings.push({
152
+ kind: "empty-verify",
153
+ file: rel,
154
+ detail: "defineAssertBlock's verify array is empty — an assert Block whose only job is checking will pass " +
155
+ "trivially forever; fill in real Traits or remove the stub",
156
+ });
157
+ }
158
+ }
159
+ return warnings;
160
+ }
161
+ export function practiceKindLabel(kind) {
162
+ switch (kind) {
163
+ case "wildcard-checkpoint":
164
+ return "wildcard Checkpoint<string>";
165
+ case "assert-no-type-arg":
166
+ return "assert block missing type arg";
167
+ case "multi-input":
168
+ return "multiple inputs in one Block";
169
+ case "combined-action":
170
+ return "fill and click in one Block";
171
+ case "empty-verify":
172
+ return "empty verify — transition unconfirmed";
173
+ case "overcomplex-selector":
174
+ return "regex/XPath selector where a plain one would do";
175
+ case "inline-selector-should-use-sel-file":
176
+ return "inline selector — a _sel.ts/*.sel.ts file already exists here";
177
+ default: {
178
+ const _exhaustive = kind;
179
+ return _exhaustive;
180
+ }
181
+ }
182
+ }
183
+ export function formatPracticeWarnings(projectDir, warnings) {
184
+ const lines = [];
185
+ for (const w of warnings) {
186
+ lines.push(`waygraph practices: ${w.file} (${practiceKindLabel(w.kind)}) — ${w.detail}`);
187
+ }
188
+ if (warnings.length === 0) {
189
+ lines.push(`waygraph practices: no bad-practice patterns under ${projectDir}`);
190
+ }
191
+ else {
192
+ lines.push(`waygraph practices: ${warnings.length} bad-practice warning${warnings.length === 1 ? "" : "s"}`);
193
+ }
194
+ return lines;
195
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Page } from "@playwright/test";
2
2
  import type { Block, Checkpoint, WaygraphInstanceOption } from "./types.js";
3
- import { type WaygraphGraph, type WaygraphEdge } from "./graph.js";
3
+ import { type WaygraphGraph, type WaygraphEdge } from "./analysis/graph.js";
4
4
  export interface ExploreContextOpts {
5
5
  blocksSelect?: import("./blocks-select.js").BlocksSelect;
6
6
  /** Absolute paths of extra Block trees to merge (host projectDir is always first). */
@@ -1,6 +1,6 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
- import { discoverGraph } from "./graph.js";
3
+ import { discoverGraph } from "./analysis/graph.js";
4
4
  import { exploreRoots, injectTaggedFile } from "./block-inject.js";
5
5
  function walkDir(dir, pattern) {
6
6
  const results = [];
@@ -5,10 +5,10 @@ import { buildExploreContext, buildExploreMenu } from "../auto-explore.js";
5
5
  import { MemPage } from "../mem-page.js";
6
6
  import { Engine } from "../engine.js";
7
7
  import { detectHere, ensureLivePage, ensureMem, resolveBaseUrl, runOneBlock, seedDefaultMem } from "../auto-explore-run.js";
8
- import { collectKnownInteractionSelectors, unmappedInteractionsPageScript } from "../coverage-gap.js";
8
+ import { collectKnownInteractionSelectors, unmappedInteractionsPageScript } from "../analysis/coverage-gap.js";
9
9
  import { runStubPhase } from "../highlights.js";
10
10
  import { verboseLog } from "../verbose-log.js";
11
- import { findBlockPathDetailed } from "../graph.js";
11
+ import { findBlockPathDetailed } from "../analysis/graph.js";
12
12
  /**
13
13
  * Non-interactive explore session: same graph/library/mem/browser setup
14
14
  * `runAutoExplore` runs in `--cli` mode, driven by `applyPick` instead of a
@@ -1,4 +1,11 @@
1
- /** This package's own installed root - dist/cli.js -> .. */
1
+ /**
2
+ * This package's own installed root (contains `templates/`, sibling to `dist/`) - this file
3
+ * compiles to `dist/cli/util.js`, so it's TWO levels up, not one. Real regression once, from the
4
+ * 0.15.24 cli.ts split: this function was copied verbatim from its old top-level `src/cli.ts`
5
+ * location (one level: `dist/cli.js` -> `..`) into `src/cli/util.ts` (compiles one level deeper)
6
+ * without adjusting the path math - silently broke `waygraph init`/`try` (no build-level test
7
+ * caught it, since tests never ran the actual compiled dist/cli.js for these two commands).
8
+ */
2
9
  export declare function packageRoot(): string;
3
10
  /** Spawns with inherited stdio (the real npm/playwright output, visible) and resolves to its exit code. */
4
11
  export declare function runInherited(cmd: string, args: string[], cwd: string): Promise<number>;
package/dist/cli/util.js CHANGED
@@ -10,9 +10,16 @@ import { spawn } from "node:child_process";
10
10
  // experience in under a minute, then gets pointed at the plain waygraph
11
11
  // source that produced it.
12
12
  // ---------------------------------------------------------------------------
13
- /** This package's own installed root - dist/cli.js -> .. */
13
+ /**
14
+ * This package's own installed root (contains `templates/`, sibling to `dist/`) - this file
15
+ * compiles to `dist/cli/util.js`, so it's TWO levels up, not one. Real regression once, from the
16
+ * 0.15.24 cli.ts split: this function was copied verbatim from its old top-level `src/cli.ts`
17
+ * location (one level: `dist/cli.js` -> `..`) into `src/cli/util.ts` (compiles one level deeper)
18
+ * without adjusting the path math - silently broke `waygraph init`/`try` (no build-level test
19
+ * caught it, since tests never ran the actual compiled dist/cli.js for these two commands).
20
+ */
14
21
  export function packageRoot() {
15
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
22
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
23
  }
17
24
  /** Spawns with inherited stdio (the real npm/playwright output, visible) and resolves to its exit code. */
18
25
  export function runInherited(cmd, args, cwd) {
@@ -1,10 +1,10 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { INLINE_SELECTOR_CALL, NAV_METHOD_CALLS, discoverBlocks, discoverFlows, importFlowFile, importModule, isBlockLike, isFlowLike, isNavBlockMarked, walkDir } from "../cli/discover.js";
3
3
  import { basename, relative, resolve } from "node:path";
4
- import { discoverGraph, findBlockPath, findOrphanBlocks } from "../graph.js";
4
+ import { discoverGraph, findBlockPath, findOrphanBlocks } from "../analysis/graph.js";
5
5
  import { parseRunFlags, resolveBaseUrl } from "../cli/flags.js";
6
6
  import { runChain } from "../cli/run-chain.js";
7
- import { collectPracticeWarnings, practiceKindLabel } from "../practices-check.js";
7
+ import { collectPracticeWarnings, practiceKindLabel } from "../analysis/practices-check.js";
8
8
  import { readFileSync } from "node:fs";
9
9
  async function validateFlows(projectDir) {
10
10
  const files = discoverFlows(projectDir);
@@ -1,6 +1,6 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { relative, resolve } from "node:path";
3
- import { checkMap } from "../map-check.js";
3
+ import { checkMap } from "../analysis/map-check.js";
4
4
  export async function mapCase(args) {
5
5
  const proj = resolve(args[1] ?? process.cwd());
6
6
  const result = await checkMap(proj);
@@ -9,10 +9,10 @@ import { existsSync, statSync } from "node:fs";
9
9
  import { resolveInjectRoots } from "../block-inject.js";
10
10
  import { requestSession, resolveSessionMeta, runAttachLoop, runAutoServeCommand, spawnDetachedSession } from "../auto-session-ipc.js";
11
11
  import { pilotAttach, pilotStart } from "../pilot.js";
12
- import { parseHighlightShorthand } from "../highlight-shorthand.js";
12
+ import { parseHighlightShorthand } from "../highlights/shorthand.js";
13
13
  import { isFileSelectToken, parseBlocksSelect } from "../blocks-select.js";
14
14
  import { runAutoExplore } from "../auto-explore-run.js";
15
- import { discoverGraph, findOrphanBlocks, toMermaid } from "../graph.js";
15
+ import { discoverGraph, findOrphanBlocks, toMermaid } from "../analysis/graph.js";
16
16
  export async function sessionCase(args, command) {
17
17
  if (command === "browser") {
18
18
  const sub = args[1];
@@ -2,8 +2,8 @@
2
2
  import { resolve } from "node:path";
3
3
  import { existsSync, statSync } from "node:fs";
4
4
  import { parseBlocksSelect } from "../blocks-select.js";
5
- import { parseMinEdgeCoverage } from "../traverse-coverage.js";
6
- import { runTraverse } from "../traverse-run.js";
5
+ import { parseMinEdgeCoverage } from "../traverse/coverage.js";
6
+ import { runTraverse } from "../traverse/run.js";
7
7
  export async function traverseCase(args) {
8
8
  if (args.includes("--step") || args.includes("--auto-next") || args.includes("--autoplay")) {
9
9
  console.error("waygraph traverse: --step / --auto-next not supported (not a demo)");
@@ -1,3 +1,4 @@
1
+ import { WaygraphError } from "../errors.js";
1
2
  import { MemPage } from "../mem-page.js";
2
3
  import { seedMemStub } from "../mem-stub.js";
3
4
  function branchInfo(flow) {
@@ -47,7 +48,7 @@ export async function runBranchRegression(flow, context, mem = new MemPage(), op
47
48
  const results = [];
48
49
  const browser = cloneSession ? context.browser() : null;
49
50
  if (cloneSession && !browser) {
50
- throw new Error("Waygraph runBranchRegression: cloneSession needs context.browser() (a persistent context " +
51
+ throw new WaygraphError("WG_BRANCH_REGRESSION_NO_BROWSER", "Waygraph runBranchRegression: cloneSession needs context.browser() (a persistent context " +
51
52
  "has none) - pass { cloneSession: false } to run the single live path in this context instead.");
52
53
  }
53
54
  /** `entryPage`, when given, is already positioned (cloned session + navigated) - reused as-is,
@@ -1,8 +1,9 @@
1
1
  import { connect } from "../types.js";
2
+ import { WaygraphError } from "../errors.js";
2
3
  import { findBlockIndex, modVerify, withVerify } from "./core.js";
3
4
  export function composeBlock(name, steps) {
4
5
  if (steps.length === 0) {
5
- throw new Error(`composeBlock: "${name}" needs at least one step`);
6
+ throw new WaygraphError("WG_COMPOSE_EMPTY", `composeBlock: "${name}" needs at least one step`);
6
7
  }
7
8
  const chain = steps.reduce((a, b) => connect(a, b));
8
9
  return {
@@ -41,7 +42,7 @@ function wrapFfError(ffName, err) {
41
42
  }
42
43
  export function fastForwardComposeBlock(name, steps) {
43
44
  if (steps.length === 0) {
44
- throw new Error(`fastForwardComposeBlock: "${name}" needs at least one step`);
45
+ throw new WaygraphError("WG_FASTFORWARD_EMPTY", `fastForwardComposeBlock: "${name}" needs at least one step`);
45
46
  }
46
47
  const base = composeBlock(name, steps);
47
48
  const origAct = base.instruction.act.bind(base.instruction);
@@ -1,14 +1,15 @@
1
+ import { WaygraphError } from "../errors.js";
1
2
  export function findBlockIndex(middle, block) {
2
3
  if (typeof block === "number") {
3
4
  if (block < 0 || block >= middle.length) {
4
- throw new Error(`Flow: no Block at index ${block} in this flow (has ${middle.length})`);
5
+ throw new WaygraphError("WG_FLOW_BLOCK_NOT_FOUND", `Flow: no Block at index ${block} in this flow (has ${middle.length})`);
5
6
  }
6
7
  return block;
7
8
  }
8
9
  const blockName = typeof block === "string" ? block : block.name;
9
10
  const index = middle.findIndex((b) => b.name === blockName);
10
11
  if (index === -1) {
11
- throw new Error(`Flow: no Block named "${blockName}" in this flow (has: ${middle.map((b) => b.name).join(", ")})`);
12
+ throw new WaygraphError("WG_FLOW_BLOCK_NOT_FOUND", `Flow: no Block named "${blockName}" in this flow (has: ${middle.map((b) => b.name).join(", ")})`);
12
13
  }
13
14
  return index;
14
15
  }
@@ -228,13 +229,13 @@ export function withStubOnError(block, stub) {
228
229
  export function modVerify(block, nameOrIndex, newCheck) {
229
230
  const verify = block.instruction.verify;
230
231
  if (typeof verify === "function") {
231
- 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`);
232
+ throw new WaygraphError("WG_MODVERIFY_FUNCTION_FORM", `modVerify: "${block.name}" has a function-form verify, which has no fixed list to address into - use withVerify to replace it wholesale instead`);
232
233
  }
233
234
  const traits = verify ?? [];
234
235
  const index = typeof nameOrIndex === "number" ? nameOrIndex : traits.findIndex((t) => t.name === nameOrIndex);
235
236
  const existing = traits[index];
236
237
  if (!existing) {
237
- throw new Error(typeof nameOrIndex === "number"
238
+ throw new WaygraphError("WG_MODVERIFY_NOT_FOUND", typeof nameOrIndex === "number"
238
239
  ? `modVerify: "${block.name}" has no verify trait at index ${nameOrIndex} (has ${traits.length})`
239
240
  : `modVerify: "${block.name}" has no verify trait named "${nameOrIndex}" (has: ${traits.map((t) => t.name).join(", ") || "none"})`);
240
241
  }
@@ -1,3 +1,4 @@
1
+ import { WaygraphError } from "../errors.js";
1
2
  import { end, start } from "./run-graph.js";
2
3
  import { buildFlow } from "./flow.js";
3
4
  import { MemPage } from "../mem-page.js";
@@ -66,7 +67,7 @@ function assertMapKind(block, allowed, method) {
66
67
  return;
67
68
  const wanted = allowed.map((k) => MAP_KIND_FACTORY_HINT[k] ?? k).join(" or ");
68
69
  const found = kind ? `a Block of kind "${kind}"` : "an object with no waygraph kind marker at all";
69
- throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
70
+ throw new WaygraphError("WG_MAP_WRONG_KIND", `Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
70
71
  `(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
71
72
  "of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
72
73
  }
@@ -76,7 +77,7 @@ function assertMapSalt(block, allowed, method) {
76
77
  return;
77
78
  const wanted = allowed.map((s) => MAP_SALT_FACTORY_HINT[s] ?? s).join(" or ");
78
79
  const found = salt ? `a Block salted "${salt}"` : "an object with no waygraph salt marker at all";
79
- throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
80
+ throw new WaygraphError("WG_MAP_WRONG_SALT", `Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
80
81
  `(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
81
82
  "of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
82
83
  }
@@ -97,11 +98,11 @@ function assertMapOrigin(block, homeOrigin, expect, method) {
97
98
  }
98
99
  const isExternal = targetOrigin !== wantOrigin;
99
100
  if (expect === "internal" && isExternal) {
100
- throw new Error(`Waygraph map: .gotoPage("${block.name}") targets ${targetOrigin}, which is NOT this map's ` +
101
+ throw new WaygraphError("WG_MAP_WRONG_ORIGIN", `Waygraph map: .gotoPage("${block.name}") targets ${targetOrigin}, which is NOT this map's ` +
101
102
  `home origin (${wantOrigin}) - use .gotoExternal() for a genuinely cross-origin destination.`);
102
103
  }
103
104
  if (expect === "external" && !isExternal) {
104
- throw new Error(`Waygraph map: .gotoExternal("${block.name}") targets ${targetOrigin}, which IS this map's ` +
105
+ throw new WaygraphError("WG_MAP_WRONG_ORIGIN", `Waygraph map: .gotoExternal("${block.name}") targets ${targetOrigin}, which IS this map's ` +
105
106
  `home origin (${wantOrigin}) - use .gotoPage() for an internal destination.`);
106
107
  }
107
108
  }
@@ -114,7 +115,7 @@ function assertMapOrigin(block, homeOrigin, expect, method) {
114
115
  function buildBranchedFlow(before, routes) {
115
116
  const run = (async (contextOrMem, memOrConfig, options) => {
116
117
  if (contextOrMem instanceof MemPage) {
117
- throw new Error("Waygraph map: a .branch()-ed Flow can't run via run(mem, config) - that convenience form " +
118
+ throw new WaygraphError("WG_MAP_BRANCH_SHARED_SESSION_UNSUPPORTED", "Waygraph map: a .branch()-ed Flow can't run via run(mem, config) - that convenience form " +
118
119
  "always closes its own browser/page before a branch's tag is even known. Call " +
119
120
  "run(context, mem[, options]) instead, keeping the same page across the branch.");
120
121
  }
@@ -204,7 +205,7 @@ export class MapBuilder {
204
205
  */
205
206
  ffStart(name) {
206
207
  if (this.ff) {
207
- throw new Error(`Waygraph map: .ffStart() while already inside "${this.ff.name}" - call .ffEnd() first`);
208
+ throw new WaygraphError("WG_MAP_FF_ALREADY_OPEN", `Waygraph map: .ffStart() while already inside "${this.ff.name}" - call .ffEnd() first`);
208
209
  }
209
210
  const ffName = typeof name === "string" && name.trim()
210
211
  ? name.trim()
@@ -220,10 +221,10 @@ export class MapBuilder {
220
221
  */
221
222
  ffEnd() {
222
223
  if (!this.ff) {
223
- throw new Error("Waygraph map: .ffEnd() with no open .ffStart()");
224
+ throw new WaygraphError("WG_MAP_FF_NOT_OPEN", "Waygraph map: .ffEnd() with no open .ffStart()");
224
225
  }
225
226
  if (this.ff.buffer.length === 0) {
226
- throw new Error(`Waygraph map: .ffEnd() for "${this.ff.name}" has no steps - add .gotoPage/.method/… inside the window`);
227
+ throw new WaygraphError("WG_MAP_FF_EMPTY", `Waygraph map: .ffEnd() for "${this.ff.name}" has no steps - add .gotoPage/.method/… inside the window`);
227
228
  }
228
229
  const composed = fastForwardComposeBlock(this.ff.name,
229
230
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -279,7 +280,7 @@ export class MapBuilder {
279
280
  */
280
281
  method(block) {
281
282
  if (mapKindOf(block) === "assert") {
282
- throw new Error(`Waygraph map: .method("${block.name}") got a defineAssertBlock - use .assert() for ` +
283
+ throw new WaygraphError("WG_MAP_WRONG_KIND", `Waygraph map: .method("${block.name}") got a defineAssertBlock - use .assert() for ` +
283
284
  "self-loop verify steps (Assert Blocks share method salt internally but are not methods).");
284
285
  }
285
286
  assertMapSalt(block, ["method", "effect"], "method");
@@ -319,7 +320,7 @@ export class MapBuilder {
319
320
  */
320
321
  branch(routes) {
321
322
  if (this.steps.length === 0) {
322
- throw new Error("Waygraph map: .branch() needs at least one prior step - nothing to branch from. " +
323
+ throw new WaygraphError("WG_MAP_BRANCH_NO_PRIOR_STEP", "Waygraph map: .branch() needs at least one prior step - nothing to branch from. " +
323
324
  "Add .gotoPage()/.gotoExternal()/.method()/.assert() first.");
324
325
  }
325
326
  const before = this.end();
@@ -339,10 +340,10 @@ export class MapBuilder {
339
340
  */
340
341
  end() {
341
342
  if (this.ff) {
342
- throw new Error(`Waygraph map: .end() while .ffStart("${this.ff.name}") is still open - call .ffEnd() first`);
343
+ throw new WaygraphError("WG_MAP_FF_NOT_CLOSED", `Waygraph map: .end() while .ffStart("${this.ff.name}") is still open - call .ffEnd() first`);
343
344
  }
344
345
  if (this.steps.length === 0) {
345
- throw new Error("Waygraph map: .end() called with zero steps - add at least one .gotoPage()/.gotoExternal()/" +
346
+ throw new WaygraphError("WG_MAP_EMPTY", "Waygraph map: .end() called with zero steps - add at least one .gotoPage()/.gotoExternal()/" +
346
347
  ".assert()/.method() before .end()");
347
348
  }
348
349
  const chain = [start, ...this.steps, end];
@@ -1,5 +1,6 @@
1
1
  // Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { connect } from "../types.js";
3
+ import { WaygraphError } from "../errors.js";
3
4
  import { preflight, runGraph } from "./run-graph.js";
4
5
  import { resolveLauncher } from "./config.js";
5
6
  import { MemPage } from "../mem-page.js";
@@ -267,7 +268,7 @@ function locateInChain(flows, block) {
267
268
  return { flowIndex: fi, localIndex: remaining };
268
269
  remaining -= len;
269
270
  }
270
- throw new Error(`chainFlow: no Block at index ${block} across ${flows.length} chained flows`);
271
+ throw new WaygraphError("WG_FLOW_BLOCK_NOT_FOUND", `chainFlow: no Block at index ${block} across ${flows.length} chained flows`);
271
272
  }
272
273
  const blockName = typeof block === "string" ? block : block.name;
273
274
  for (let fi = 0; fi < flows.length; fi++) {
@@ -275,7 +276,7 @@ function locateInChain(flows, block) {
275
276
  if (localIndex !== -1)
276
277
  return { flowIndex: fi, localIndex };
277
278
  }
278
- throw new Error(`chainFlow: no Block named "${blockName}" in any chained flow`);
279
+ throw new WaygraphError("WG_FLOW_BLOCK_NOT_FOUND", `chainFlow: no Block named "${blockName}" in any chained flow`);
279
280
  }
280
281
  async function clearSessionState(context, page) {
281
282
  await context.clearCookies().catch(() => { });
@@ -311,7 +312,7 @@ async function clearSessionState(context, page) {
311
312
  */
312
313
  export function chainFlow(...flows) {
313
314
  if (flows.length === 0) {
314
- throw new Error("chainFlow: give at least one Flow");
315
+ throw new WaygraphError("WG_CHAINFLOW_EMPTY", "chainFlow: give at least one Flow");
315
316
  }
316
317
  const run = async function run(contextOrMem, memOrConfig, options) {
317
318
  if (contextOrMem instanceof MemPage) {
@@ -1,3 +1,4 @@
1
+ import { WaygraphError } from "../errors.js";
1
2
  import { checkpoint } from "../types.js";
2
3
  import { runPrecondition, runVerify } from "../trait.js";
3
4
  /**
@@ -11,7 +12,7 @@ import { runPrecondition, runVerify } from "../trait.js";
11
12
  export function preflight(mem, block) {
12
13
  const missing = (block.requires ?? []).filter((k) => !mem.has(k));
13
14
  if (missing.length > 0) {
14
- throw new Error(`preflight: MemPage is missing required key(s) before "${block.name}" can run: ${missing
15
+ throw new WaygraphError("WG_PREFLIGHT_MISSING_KEY", `preflight: MemPage is missing required key(s) before "${block.name}" can run: ${missing
15
16
  .map((k) => `"${k.name}"`)
16
17
  .join(", ")}`);
17
18
  }
@@ -85,7 +86,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
85
86
  let steps = 0;
86
87
  for (;;) {
87
88
  if (++steps > maxSteps) {
88
- throw new Error(`runGraph: exceeded ${maxSteps} steps - check for an unintended self-loop`);
89
+ throw new WaygraphError("WG_RUNGRAPH_MAX_STEPS", `runGraph: exceeded ${maxSteps} steps - check for an unintended self-loop`);
89
90
  }
90
91
  await runPrecondition(current.instruction.precondition, input, page, mem, current.name);
91
92
  await current.instruction.act(page, input, mem);
@@ -98,7 +99,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
98
99
  const next = current.next?.(checkpoint);
99
100
  if (!next) {
100
101
  if (terminals && !terminals.has(checkpoint.__state)) {
101
- throw new Error(`runGraph: "${current.name}" resolved to "${checkpoint.__state}", which is not a registered terminal (expected one of: ${[...terminals].join(", ")})`);
102
+ throw new WaygraphError("WG_RUNGRAPH_NOT_TERMINAL", `runGraph: "${current.name}" resolved to "${checkpoint.__state}", which is not a registered terminal (expected one of: ${[...terminals].join(", ")})`);
102
103
  }
103
104
  return checkpoint;
104
105
  }