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.
- package/dist/analysis/coverage-gap.d.ts +11 -0
- package/dist/analysis/coverage-gap.js +119 -0
- package/dist/analysis/graph.d.ts +92 -0
- package/dist/analysis/graph.js +467 -0
- package/dist/analysis/map-check.d.ts +18 -0
- package/dist/analysis/map-check.js +140 -0
- package/dist/analysis/practices-check.d.ts +14 -0
- package/dist/analysis/practices-check.js +195 -0
- package/dist/auto-explore.d.ts +1 -1
- package/dist/auto-explore.js +1 -1
- package/dist/auto-session/session.js +2 -2
- package/dist/cli/util.d.ts +8 -1
- package/dist/cli/util.js +9 -2
- package/dist/commands/check.js +2 -2
- package/dist/commands/map.js +1 -1
- package/dist/commands/session.js +2 -2
- package/dist/commands/traverse.js +2 -2
- package/dist/engine/branch-regression.js +2 -1
- package/dist/engine/compose.js +3 -2
- package/dist/engine/core.js +5 -4
- package/dist/engine/engine-class.js +13 -12
- package/dist/engine/flow.js +4 -3
- package/dist/engine/run-graph.js +4 -3
- package/dist/errors.d.ts +122 -0
- package/dist/errors.js +125 -0
- package/dist/highlights/shorthand.d.ts +24 -0
- package/dist/highlights/shorthand.js +30 -0
- package/dist/index.d.ts +9 -7
- package/dist/index.js +5 -4
- package/dist/mem-page.js +2 -1
- package/dist/pilot.d.ts +1 -1
- package/dist/pilot.js +1 -1
- package/dist/traverse/coverage.d.ts +59 -0
- package/dist/traverse/coverage.js +127 -0
- package/dist/traverse/lease.d.ts +18 -0
- package/dist/traverse/lease.js +62 -0
- package/dist/traverse/run.d.ts +47 -0
- package/dist/traverse/run.js +439 -0
- package/examples/saucedemo/.waygraph-auto/050e5b55.log +0 -0
- package/examples/saucedemo/.waygraph-auto/096fea05.log +20 -0
- package/examples/saucedemo/.waygraph-auto/123d1cad.log +5 -0
- package/examples/saucedemo/.waygraph-auto/12ed8e28.log +0 -0
- package/examples/saucedemo/.waygraph-auto/1cb6f44c.log +5 -0
- package/examples/saucedemo/.waygraph-auto/1fdb2100.log +0 -0
- package/examples/saucedemo/.waygraph-auto/573abedd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/9a26134c.log +20 -0
- package/examples/saucedemo/.waygraph-auto/aa226dd0.log +22 -0
- package/examples/saucedemo/.waygraph-auto/ac5dde3d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cae91469.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d846cd78.log +0 -0
- package/examples/saucedemo/.waygraph-auto/eb38d92e.log +22 -0
- package/examples/saucedemo/.waygraph-auto/ee86203e.log +0 -0
- package/package.json +1 -1
- package/templates/scaffold/scripts/README.md +13 -0
- package/templates/scaffold/src/flows/README.md +18 -0
- package/templates/scaffold/src/map/README.md +31 -0
- package/templates/scaffold/src/states/README.md +15 -0
- 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
|
+
}
|
package/dist/auto-explore.d.ts
CHANGED
|
@@ -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). */
|
package/dist/auto-explore.js
CHANGED
|
@@ -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
|
package/dist/cli/util.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
/**
|
|
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
|
-
/**
|
|
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) {
|
package/dist/commands/check.js
CHANGED
|
@@ -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);
|
package/dist/commands/map.js
CHANGED
|
@@ -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);
|
package/dist/commands/session.js
CHANGED
|
@@ -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 "../
|
|
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
|
|
6
|
-
import { runTraverse } from "../traverse
|
|
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
|
|
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,
|
package/dist/engine/compose.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
package/dist/engine/core.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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];
|
package/dist/engine/flow.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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) {
|
package/dist/engine/run-graph.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
}
|