waygraph 0.15.10 → 0.15.14
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/README.md +10 -8
- package/dist/auto-explore.d.ts +7 -6
- package/dist/auto-explore.js +43 -32
- package/dist/auto-session-ipc.d.ts +13 -0
- package/dist/auto-session-ipc.js +119 -7
- package/dist/auto-session.d.ts +16 -13
- package/dist/auto-session.js +72 -53
- package/dist/block-inject.d.ts +15 -0
- package/dist/block-inject.js +91 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.js +29 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +323 -47
- package/dist/coverage-gap.d.ts +11 -0
- package/dist/coverage-gap.js +119 -0
- package/dist/graph.d.ts +1 -0
- package/dist/graph.js +99 -91
- package/dist/pilot-overlay.d.ts +7 -1
- package/dist/pilot-overlay.js +59 -41
- package/dist/pilot.d.ts +7 -2
- package/dist/pilot.js +34 -7
- package/dist/practices-check.d.ts +14 -0
- package/dist/practices-check.js +115 -0
- package/examples/saucedemo/.waygraph-auto/0b937f26.log +4 -0
- package/examples/saucedemo/.waygraph-auto/2b608ed3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2c13567b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2d9622a5.log +4 -0
- package/examples/saucedemo/.waygraph-auto/305f6e5b.log +4 -0
- package/examples/saucedemo/.waygraph-auto/418befbf.log +4 -0
- package/examples/saucedemo/.waygraph-auto/483bd763.log +4 -0
- package/examples/saucedemo/.waygraph-auto/48e3596a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4adfbe91.json +8 -0
- package/examples/saucedemo/.waygraph-auto/4adfbe91.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4f0e33e7.log +4 -0
- package/examples/saucedemo/.waygraph-auto/5bd8fedf.log +4 -0
- package/examples/saucedemo/.waygraph-auto/5dde8331.log +4 -0
- package/examples/saucedemo/.waygraph-auto/68903903.log +0 -0
- package/examples/saucedemo/.waygraph-auto/78bbf43c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/7efa646f.log +4 -0
- package/examples/saucedemo/.waygraph-auto/9cecb0c8.log +4 -0
- package/examples/saucedemo/.waygraph-auto/aae57d51.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b61a08d8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c946f66c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cd0d5b50.log +4 -0
- package/examples/saucedemo/.waygraph-auto/cd5b203c.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ea2df316.log +4 -0
- package/examples/saucedemo/.waygraph-auto/ee60ed24.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f2b6cb1b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f392cdc8.log +0 -0
- package/package.json +2 -2
- package/templates/scaffold/package.json +1 -1
- package/templates/skills/waygraph-convention.skill.md +11 -1
- package/templates/skills/waygraph-pilot.skill.md +104 -10
package/dist/graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join, dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { matchBlocksSelect, } from "./blocks-select.js";
|
|
4
|
+
import { exploreRoots, injectTaggedFile } from "./block-inject.js";
|
|
4
5
|
/**
|
|
5
6
|
* Renders a `WaygraphGraph` as Mermaid `stateDiagram-v2` text - paste
|
|
6
7
|
* straight into a `.md` file's ```mermaid fence or a live editor. `"*"`
|
|
@@ -343,110 +344,117 @@ function extractDefineBlockGenerics(src) {
|
|
|
343
344
|
* specifically because it also EXECUTES Blocks, not just imports them.
|
|
344
345
|
*/
|
|
345
346
|
export async function discoverGraph(projectDir, opts) {
|
|
346
|
-
let files = discoverBlocks(projectDir);
|
|
347
347
|
const select = opts?.blocksSelect;
|
|
348
|
-
// Glob/bare: drop files early. Regex needs block names — filter per export below.
|
|
349
|
-
if (select && select.kind !== "regex") {
|
|
350
|
-
files = files.filter((file) => matchBlocksSelect(select, {
|
|
351
|
-
relFile: relative(projectDir, file).replace(/\\/g, "/"),
|
|
352
|
-
}));
|
|
353
|
-
}
|
|
354
348
|
const nodeTags = new Set();
|
|
355
349
|
const edges = [];
|
|
356
350
|
const skipped = [];
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
let
|
|
360
|
-
|
|
361
|
-
|
|
351
|
+
const edgeSeen = new Set();
|
|
352
|
+
for (const root of exploreRoots(projectDir, opts?.inject)) {
|
|
353
|
+
let files = discoverBlocks(root);
|
|
354
|
+
// Glob/bare: drop files early. Regex needs block names — filter per export below.
|
|
355
|
+
if (select && select.kind !== "regex") {
|
|
356
|
+
files = files.filter((file) => matchBlocksSelect(select, {
|
|
357
|
+
relFile: relative(root, file).replace(/\\/g, "/"),
|
|
358
|
+
}));
|
|
362
359
|
}
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
360
|
+
for (const file of files) {
|
|
361
|
+
const relFile = injectTaggedFile(projectDir, root, relative(root, file).replace(/\\/g, "/"));
|
|
362
|
+
let mod;
|
|
363
|
+
try {
|
|
364
|
+
mod = await importModule(file);
|
|
368
365
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// handled specially here.
|
|
376
|
-
const calls = extractDefineBlockGenerics(src);
|
|
377
|
-
let callIndex = 0;
|
|
378
|
-
for (const exported of Object.values(mod)) {
|
|
379
|
-
if (!isBlockLike(exported))
|
|
380
|
-
continue;
|
|
381
|
-
const blockName = exported.name;
|
|
382
|
-
if (select &&
|
|
383
|
-
!matchBlocksSelect(select, {
|
|
384
|
-
relFile: relFile.replace(/\\/g, "/"),
|
|
385
|
-
blockName,
|
|
386
|
-
})) {
|
|
366
|
+
catch (err) {
|
|
367
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
368
|
+
// Dual @playwright/test installs fail here - surface instead of empty graph.
|
|
369
|
+
if (/second time|playwright/i.test(msg)) {
|
|
370
|
+
console.error(`waygraph graph: skip ${relFile}: ${msg.split("\n")[0]}`);
|
|
371
|
+
}
|
|
387
372
|
continue;
|
|
388
373
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
374
|
+
const src = readFileSync(file, "utf-8");
|
|
375
|
+
// Assumes source order matches export-object iteration order - true for
|
|
376
|
+
// this codebase's own convention of one Block per file; a file with
|
|
377
|
+
// several regular Blocks in an unusual order is a known edge case, not
|
|
378
|
+
// handled specially here.
|
|
379
|
+
const calls = extractDefineBlockGenerics(src);
|
|
380
|
+
let callIndex = 0;
|
|
381
|
+
for (const exported of Object.values(mod)) {
|
|
382
|
+
if (!isBlockLike(exported))
|
|
383
|
+
continue;
|
|
384
|
+
const blockName = exported.name;
|
|
385
|
+
if (select &&
|
|
386
|
+
!matchBlocksSelect(select, {
|
|
387
|
+
relFile: relFile.replace(/\\/g, "/"),
|
|
388
|
+
blockName,
|
|
389
|
+
})) {
|
|
390
|
+
continue;
|
|
401
391
|
}
|
|
402
|
-
|
|
403
|
-
|
|
392
|
+
if (isAssertBlockMarked(exported)) {
|
|
393
|
+
try {
|
|
394
|
+
// Same input-independent resolve() shape Nav/Page Blocks already
|
|
395
|
+
// get this shortcut for - defineAssertBlock's own resolve() is
|
|
396
|
+
// always `() => checkpoint(options.checkpoint)`, so no <In, Out>
|
|
397
|
+
// generic needs to appear in source text for this to be found.
|
|
398
|
+
const resolveFn = exported.instruction.resolve;
|
|
399
|
+
const resolved = await resolveFn(undefined);
|
|
400
|
+
const to = resolved.__state;
|
|
401
|
+
nodeTags.add(to);
|
|
402
|
+
// Self-loop by construction (defineAssertBlock is self-loop-only).
|
|
403
|
+
edges.push({ block: blockName, file: relFile, from: to, to, kind: "action" });
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
skipped.push({ block: blockName, file: relFile, reason: "assertBlock resolve() threw" });
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
404
409
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
410
|
+
if (isNavBlockMarked(exported)) {
|
|
411
|
+
try {
|
|
412
|
+
// NavBlock / PageBlock resolve() ignores input - always checkpoint(tag).
|
|
413
|
+
const resolveFn = exported.instruction.resolve;
|
|
414
|
+
const resolved = await resolveFn(undefined);
|
|
415
|
+
const to = resolved.__state;
|
|
416
|
+
nodeTags.add(to);
|
|
417
|
+
// Arrival-only Page hubs register the Checkpoint node but do not emit
|
|
418
|
+
// a Start-here edge (no deep link). Methods own the action edges.
|
|
419
|
+
const isPage = exported.__waygraphKind === "page";
|
|
420
|
+
const deepLink = !isPage ||
|
|
421
|
+
exported.__waygraphPageDeepLink === true;
|
|
422
|
+
if (deepLink) {
|
|
423
|
+
edges.push({ block: blockName, file: relFile, from: "*", to, kind: "nav" });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
skipped.push({ block: blockName, file: relFile, reason: "NavBlock/PageBlock resolve() threw" });
|
|
421
428
|
}
|
|
429
|
+
continue;
|
|
422
430
|
}
|
|
423
|
-
|
|
424
|
-
|
|
431
|
+
const call = calls[callIndex++];
|
|
432
|
+
if (!call) {
|
|
433
|
+
skipped.push({
|
|
434
|
+
block: blockName,
|
|
435
|
+
file: relFile,
|
|
436
|
+
reason: "no defineBlock/defineMethodBlock/defineEffectBlock<In, Out> generic call found",
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
425
439
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
for (const from of fromTags) {
|
|
445
|
-
for (const to of toTags) {
|
|
446
|
-
if (from !== "*")
|
|
447
|
-
nodeTags.add(from);
|
|
448
|
-
nodeTags.add(to);
|
|
449
|
-
edges.push({ block: blockName, file: relFile, from, to, kind: "action" });
|
|
440
|
+
const fromTags = resolveGenericArg(file, call.in);
|
|
441
|
+
const toTags = resolveGenericArg(file, call.out);
|
|
442
|
+
if (fromTags.length === 0 || toTags.length === 0) {
|
|
443
|
+
const badName = fromTags.length === 0 ? call.in : call.out;
|
|
444
|
+
skipped.push({ block: blockName, file: relFile, reason: `couldn't resolve "${badName}" to a Checkpoint tag` });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
for (const from of fromTags) {
|
|
448
|
+
for (const to of toTags) {
|
|
449
|
+
if (from !== "*")
|
|
450
|
+
nodeTags.add(from);
|
|
451
|
+
nodeTags.add(to);
|
|
452
|
+
const key = `${blockName}\0${from}\0${to}`;
|
|
453
|
+
if (edgeSeen.has(key))
|
|
454
|
+
continue;
|
|
455
|
+
edgeSeen.add(key);
|
|
456
|
+
edges.push({ block: blockName, file: relFile, from, to, kind: "action" });
|
|
457
|
+
}
|
|
450
458
|
}
|
|
451
459
|
}
|
|
452
460
|
}
|
package/dist/pilot-overlay.d.ts
CHANGED
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
* dispatch as a side effect of loading a library module - unsafe. This
|
|
17
17
|
* ships its own small, purpose-built badge + panel instead.
|
|
18
18
|
*/
|
|
19
|
-
import type { Page } from "@playwright/test";
|
|
19
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
20
20
|
import type { SessionSnapshot } from "./auto-session.js";
|
|
21
21
|
import { type HighlightTone, type DevicePreset, type DeviceState, type TodoDockUiOpts } from "./highlights.js";
|
|
22
|
+
/**
|
|
23
|
+
* Re-install the overlay shell on every navigation (about:blank, goto, etc.)
|
|
24
|
+
* so the badge/panel persist outside the app's own DOM — not lost when the
|
|
25
|
+
* page is blank or replaced.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installPersistentPilotOverlay(context: BrowserContext): Promise<void>;
|
|
22
28
|
/**
|
|
23
29
|
* Bottom-left transient toast, separate from the persistent bottom-right
|
|
24
30
|
* badge - real, direct user request: something that visibly flags "a
|
package/dist/pilot-overlay.js
CHANGED
|
@@ -129,53 +129,67 @@ const OVERLAY_CSS = `
|
|
|
129
129
|
#wg-pilot-fx-device[data-preset=tablet]{border-color:#22C55E;}
|
|
130
130
|
#wg-pilot-fx-device[data-preset=desktop]{border-color:#9CA3AF;}
|
|
131
131
|
` + WAYGRAPH_RING_CSS;
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
'<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
document.getElementById("wg-pilot-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
document.getElementById("wg-pilot-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
document.getElementById("wg-pilot-panel-current").style.display = "none";
|
|
168
|
-
});
|
|
132
|
+
/** Browser-side shell install — runs on every document via context.addInitScript. */
|
|
133
|
+
function installPilotOverlayShell(css) {
|
|
134
|
+
if (document.getElementById("wg-pilot-overlay"))
|
|
135
|
+
return;
|
|
136
|
+
const style = document.createElement("style");
|
|
137
|
+
style.id = "wg-pilot-overlay-css";
|
|
138
|
+
style.textContent = css;
|
|
139
|
+
document.documentElement.appendChild(style);
|
|
140
|
+
const root = document.createElement("div");
|
|
141
|
+
root.id = "wg-pilot-overlay";
|
|
142
|
+
root.innerHTML =
|
|
143
|
+
'<div id="wg-pilot-badge">Waygraph Pilot</div>' +
|
|
144
|
+
'<div id="wg-pilot-panel">' +
|
|
145
|
+
'<div id="wg-pilot-panel-toggle"><button id="wg-pilot-view-current" class="wg-pilot-tab-active">Here</button>' +
|
|
146
|
+
'<button id="wg-pilot-view-graph">All nodes</button></div>' +
|
|
147
|
+
'<div id="wg-pilot-panel-current"></div>' +
|
|
148
|
+
'<div id="wg-pilot-panel-graph" style="display:none"></div>' +
|
|
149
|
+
"</div>";
|
|
150
|
+
document.documentElement.appendChild(root);
|
|
151
|
+
document.getElementById("wg-pilot-badge").addEventListener("click", () => {
|
|
152
|
+
document.getElementById("wg-pilot-panel").classList.toggle("wg-pilot-open");
|
|
153
|
+
});
|
|
154
|
+
document.getElementById("wg-pilot-view-current").addEventListener("click", () => {
|
|
155
|
+
document.getElementById("wg-pilot-view-current").classList.add("wg-pilot-tab-active");
|
|
156
|
+
document.getElementById("wg-pilot-view-graph").classList.remove("wg-pilot-tab-active");
|
|
157
|
+
document.getElementById("wg-pilot-panel-current").style.display = "";
|
|
158
|
+
document.getElementById("wg-pilot-panel-graph").style.display = "none";
|
|
159
|
+
});
|
|
160
|
+
document.getElementById("wg-pilot-view-graph").addEventListener("click", () => {
|
|
161
|
+
document.getElementById("wg-pilot-view-graph").classList.add("wg-pilot-tab-active");
|
|
162
|
+
document.getElementById("wg-pilot-view-current").classList.remove("wg-pilot-tab-active");
|
|
163
|
+
document.getElementById("wg-pilot-panel-graph").style.display = "";
|
|
164
|
+
document.getElementById("wg-pilot-panel-current").style.display = "none";
|
|
165
|
+
});
|
|
166
|
+
if (!document.getElementById("wg-pilot-activity")) {
|
|
169
167
|
const toast = document.createElement("div");
|
|
170
168
|
toast.id = "wg-pilot-activity";
|
|
171
169
|
document.documentElement.appendChild(toast);
|
|
170
|
+
}
|
|
171
|
+
if (!document.getElementById("wg-ring")) {
|
|
172
172
|
const ring = document.createElement("div");
|
|
173
173
|
ring.id = "wg-ring";
|
|
174
|
+
document.documentElement.appendChild(ring);
|
|
175
|
+
}
|
|
176
|
+
if (!document.getElementById("wg-ring-label")) {
|
|
174
177
|
const ringLabel = document.createElement("div");
|
|
175
178
|
ringLabel.id = "wg-ring-label";
|
|
176
|
-
document.documentElement.appendChild(ring);
|
|
177
179
|
document.documentElement.appendChild(ringLabel);
|
|
178
|
-
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Re-install the overlay shell on every navigation (about:blank, goto, etc.)
|
|
184
|
+
* so the badge/panel persist outside the app's own DOM — not lost when the
|
|
185
|
+
* page is blank or replaced.
|
|
186
|
+
*/
|
|
187
|
+
export async function installPersistentPilotOverlay(context) {
|
|
188
|
+
await context.addInitScript(installPilotOverlayShell, OVERLAY_CSS);
|
|
189
|
+
}
|
|
190
|
+
/** Idempotent - safe to call before every update, matching narrate mode's own precedent. */
|
|
191
|
+
async function ensureInstalled(page) {
|
|
192
|
+
await page.evaluate(installPilotOverlayShell, OVERLAY_CSS).catch(() => { });
|
|
179
193
|
}
|
|
180
194
|
/**
|
|
181
195
|
* Bottom-left transient toast, separate from the persistent bottom-right
|
|
@@ -630,10 +644,14 @@ export async function updatePilotOverlay(page, info) {
|
|
|
630
644
|
if (!badge || !current || !graphPanel)
|
|
631
645
|
return;
|
|
632
646
|
const idPart = sessionId ? `session ${sessionId}` : "no session id";
|
|
633
|
-
|
|
647
|
+
const onBlank = location.href === "about:blank" || location.protocol === "about:";
|
|
648
|
+
const hereLabel = snapshot.here ?? (onBlank ? "(blank page)" : "(unknown)");
|
|
649
|
+
badge.textContent = `Waygraph Pilot - ${idPart} - ${hereLabel}`;
|
|
634
650
|
const totalEdges = snapshot.sections.reduce((n, s) => n + s.edges.length, 0);
|
|
635
651
|
if (totalEdges === 0) {
|
|
636
|
-
current.innerHTML =
|
|
652
|
+
current.innerHTML = onBlank
|
|
653
|
+
? '<div class="wg-pilot-empty">Blank page — no Block moves yet. Use auto goto or a nav Block when ready.</div>'
|
|
654
|
+
: '<div class="wg-pilot-empty">No Block moves from here right now.</div>';
|
|
637
655
|
}
|
|
638
656
|
else {
|
|
639
657
|
current.innerHTML = snapshot.sections
|
package/dist/pilot.d.ts
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
* new execution primitive.
|
|
30
30
|
*/
|
|
31
31
|
import { type WaygraphGraph } from "./graph.js";
|
|
32
|
-
import
|
|
32
|
+
import { listBrowserSessions, type SessionMeta } from "./auto-session-ipc.js";
|
|
33
|
+
import { type BrowserStartInit } from "./browser.js";
|
|
34
|
+
import type { SessionSnapshot } from "./auto-session.js";
|
|
33
35
|
export interface PilotStartResult {
|
|
34
36
|
sessionId: string;
|
|
35
37
|
socketPath: string;
|
|
@@ -46,4 +48,7 @@ export interface PilotStartResult {
|
|
|
46
48
|
* one round trip instead of separately knowing to chain `auto --cli
|
|
47
49
|
* --detach` and `waygraph graph` itself.
|
|
48
50
|
*/
|
|
49
|
-
export declare function pilotStart(init:
|
|
51
|
+
export declare function pilotStart(init: BrowserStartInit): Promise<PilotStartResult>;
|
|
52
|
+
/** Reattach to an existing browser session — does not spawn a new browser. */
|
|
53
|
+
export declare function pilotAttach(projectDir: string, sessionId: string, inject?: string[]): Promise<PilotStartResult>;
|
|
54
|
+
export { listBrowserSessions, type SessionMeta };
|
package/dist/pilot.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* new execution primitive.
|
|
30
30
|
*/
|
|
31
31
|
import { discoverGraph } from "./graph.js";
|
|
32
|
-
import {
|
|
32
|
+
import { requestSession, listBrowserSessions, resolveSessionMeta, } from "./auto-session-ipc.js";
|
|
33
|
+
import { browserStart } from "./browser.js";
|
|
33
34
|
/**
|
|
34
35
|
* Starts a detached session and reads back its own graph + starting
|
|
35
36
|
* snapshot in one call, so an agent bootstraps with everything it needs
|
|
@@ -38,16 +39,42 @@ import { spawnDetachedSession, requestSession } from "./auto-session-ipc.js";
|
|
|
38
39
|
* --detach` and `waygraph graph` itself.
|
|
39
40
|
*/
|
|
40
41
|
export async function pilotStart(init) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const { resolveInjectRoots } = await import("./block-inject.js");
|
|
43
|
+
const exploreInject = init.inject ??
|
|
44
|
+
(init.injectTokens?.length ? resolveInjectRoots(init.injectTokens, init.projectDir) : undefined);
|
|
45
|
+
const started = await browserStart({
|
|
46
|
+
...init,
|
|
47
|
+
...(exploreInject?.length ? { inject: exploreInject } : {}),
|
|
48
|
+
// Pilot navigates to baseURL on start (unlike `browser`, which defaults to about:blank).
|
|
49
|
+
skipInitialNavigation: init.skipInitialNavigation ?? false,
|
|
50
|
+
});
|
|
51
|
+
const graph = await discoverGraph(init.projectDir, exploreInject?.length ? { inject: exploreInject } : undefined);
|
|
52
|
+
const status = await requestSession(init.projectDir, started.sessionId, { op: "status" });
|
|
53
|
+
return {
|
|
54
|
+
sessionId: started.sessionId,
|
|
55
|
+
socketPath: started.socketPath,
|
|
56
|
+
headless: started.headless,
|
|
57
|
+
graph,
|
|
58
|
+
snapshot: status.ok ? status.snapshot : null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Reattach to an existing browser session — does not spawn a new browser. */
|
|
62
|
+
export async function pilotAttach(projectDir, sessionId, inject) {
|
|
63
|
+
const meta = resolveSessionMeta(sessionId, projectDir);
|
|
64
|
+
if (!meta) {
|
|
65
|
+
throw new Error(`waygraph pilot attach: no such session "${sessionId}" — run \`waygraph pilot sessions\` from the project dir`);
|
|
66
|
+
}
|
|
67
|
+
const graph = await discoverGraph(meta.projectDir, inject?.length ? { inject } : undefined);
|
|
68
|
+
const status = await requestSession(meta.projectDir, sessionId, { op: "status" });
|
|
69
|
+
if (!status.ok) {
|
|
70
|
+
throw new Error(`waygraph pilot attach: session "${sessionId}" not reachable (${status.error})`);
|
|
71
|
+
}
|
|
46
72
|
return {
|
|
47
73
|
sessionId: meta.sessionId,
|
|
48
74
|
socketPath: meta.socketPath,
|
|
49
75
|
headless: meta.headless,
|
|
50
76
|
graph,
|
|
51
|
-
snapshot: status.
|
|
77
|
+
snapshot: status.snapshot,
|
|
52
78
|
};
|
|
53
79
|
}
|
|
80
|
+
export { listBrowserSessions };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type PracticeKind = "wildcard-checkpoint" | "assert-no-type-arg" | "multi-input" | "combined-action";
|
|
2
|
+
export interface PracticeWarning {
|
|
3
|
+
kind: PracticeKind;
|
|
4
|
+
file: string;
|
|
5
|
+
/** Short hint for humans/agents. */
|
|
6
|
+
detail: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PracticeCheckOptions {
|
|
9
|
+
/** Skip all practice scanning (CLI `--no-practices`). */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function collectPracticeWarnings(projectDir: string, walkDir: (dir: string, re: RegExp) => string[], opts?: PracticeCheckOptions): PracticeWarning[];
|
|
13
|
+
export declare function practiceKindLabel(kind: PracticeKind): string;
|
|
14
|
+
export declare function formatPracticeWarnings(projectDir: string, warnings: PracticeWarning[]): string[];
|
|
@@ -0,0 +1,115 @@
|
|
|
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 } from "node:fs";
|
|
7
|
+
import { 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
|
+
const SCAN_GLOB = /\.(block|flow)\.ts$/;
|
|
13
|
+
const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
|
|
14
|
+
const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
|
|
15
|
+
/** Whole-file opt-out: `// waygraph-ignore-practices` */
|
|
16
|
+
const IGNORE_ALL = /waygraph-ignore-practices/;
|
|
17
|
+
/** Per-kind opt-out: `// waygraph-ignore: multi-input, combined-action` */
|
|
18
|
+
const IGNORE_KINDS = /waygraph-ignore:\s*([^\n*]+)/;
|
|
19
|
+
function walkBlockAndFlowFiles(projectDir, walkDir) {
|
|
20
|
+
return walkDir(projectDir, SCAN_GLOB);
|
|
21
|
+
}
|
|
22
|
+
function ignoredKindsForFile(src) {
|
|
23
|
+
if (IGNORE_ALL.test(src))
|
|
24
|
+
return "all";
|
|
25
|
+
const m = IGNORE_KINDS.exec(src);
|
|
26
|
+
if (!m)
|
|
27
|
+
return new Set();
|
|
28
|
+
const kinds = new Set();
|
|
29
|
+
for (const token of m[1].split(/[,;\s]+/)) {
|
|
30
|
+
const t = token.trim();
|
|
31
|
+
if (t)
|
|
32
|
+
kinds.add(t);
|
|
33
|
+
}
|
|
34
|
+
return kinds;
|
|
35
|
+
}
|
|
36
|
+
function isIgnored(kind, ignore) {
|
|
37
|
+
return ignore === "all" || ignore.has(kind);
|
|
38
|
+
}
|
|
39
|
+
function countMatches(src, re) {
|
|
40
|
+
re.lastIndex = 0;
|
|
41
|
+
return (src.match(re) ?? []).length;
|
|
42
|
+
}
|
|
43
|
+
export function collectPracticeWarnings(projectDir, walkDir, opts) {
|
|
44
|
+
if (opts?.disabled)
|
|
45
|
+
return [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
for (const file of walkBlockAndFlowFiles(projectDir, walkDir)) {
|
|
48
|
+
const rel = relative(projectDir, file).replace(/\\/g, "/");
|
|
49
|
+
const src = readFileSync(file, "utf-8");
|
|
50
|
+
const ignore = ignoredKindsForFile(src);
|
|
51
|
+
if (!isIgnored("wildcard-checkpoint", ignore) && WILDCARD_CHECKPOINT.test(src)) {
|
|
52
|
+
warnings.push({
|
|
53
|
+
kind: "wildcard-checkpoint",
|
|
54
|
+
file: rel,
|
|
55
|
+
detail: 'uses Checkpoint<string> in a Block helper generic — prefer a typed Checkpoint from src/states/ (e.g. defineMethodBlock<LoginPage, LoginPage>)',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!isIgnored("assert-no-type-arg", ignore) && ASSERT_NO_TYPE_ARG.test(src)) {
|
|
59
|
+
warnings.push({
|
|
60
|
+
kind: "assert-no-type-arg",
|
|
61
|
+
file: rel,
|
|
62
|
+
detail: "defineAssertBlock({…}) without an explicit type argument — add defineAssertBlock<YourCheckpoint>({…}) so defineFlow typing stays honest",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const isMethodOrEffect = METHOD_OR_EFFECT_FILE.test(rel) || METHOD_OR_EFFECT_DEFINE.test(src);
|
|
66
|
+
if (isMethodOrEffect) {
|
|
67
|
+
const fills = countMatches(src, /\.fill\s*\(/g);
|
|
68
|
+
const clicks = countMatches(src, /\.click\s*\(/g);
|
|
69
|
+
if (!isIgnored("multi-input", ignore) && fills >= 2) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
kind: "multi-input",
|
|
72
|
+
file: rel,
|
|
73
|
+
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)`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (!isIgnored("combined-action", ignore) && fills >= 1 && clicks >= 1) {
|
|
77
|
+
warnings.push({
|
|
78
|
+
kind: "combined-action",
|
|
79
|
+
file: rel,
|
|
80
|
+
detail: "Method/Effect act() mixes .fill() and .click() — fill and submit must be separate Blocks (see examples/saucedemo login split)",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return warnings;
|
|
86
|
+
}
|
|
87
|
+
export function practiceKindLabel(kind) {
|
|
88
|
+
switch (kind) {
|
|
89
|
+
case "wildcard-checkpoint":
|
|
90
|
+
return "wildcard Checkpoint<string>";
|
|
91
|
+
case "assert-no-type-arg":
|
|
92
|
+
return "assert block missing type arg";
|
|
93
|
+
case "multi-input":
|
|
94
|
+
return "multiple inputs in one Block";
|
|
95
|
+
case "combined-action":
|
|
96
|
+
return "fill and click in one Block";
|
|
97
|
+
default: {
|
|
98
|
+
const _exhaustive = kind;
|
|
99
|
+
return _exhaustive;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function formatPracticeWarnings(projectDir, warnings) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
for (const w of warnings) {
|
|
106
|
+
lines.push(`waygraph practices: ${w.file} (${practiceKindLabel(w.kind)}) — ${w.detail}`);
|
|
107
|
+
}
|
|
108
|
+
if (warnings.length === 0) {
|
|
109
|
+
lines.push(`waygraph practices: no bad-practice patterns under ${projectDir}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
lines.push(`waygraph practices: ${warnings.length} bad-practice warning${warnings.length === 1 ? "" : "s"}`);
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1438382) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1438382) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1432340) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1432340) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1419780) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1419780) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1429820) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1429820) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1432363) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1432363) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(node:1419814) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
|
+
(node:1419814) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
4
|
+
(Use `node --trace-warnings ...` to show where the warning was created)
|