waygraph 0.15.11 → 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.
Files changed (52) hide show
  1. package/README.md +10 -8
  2. package/dist/auto-explore.d.ts +7 -6
  3. package/dist/auto-explore.js +43 -32
  4. package/dist/auto-session-ipc.d.ts +13 -0
  5. package/dist/auto-session-ipc.js +119 -7
  6. package/dist/auto-session.d.ts +16 -13
  7. package/dist/auto-session.js +72 -53
  8. package/dist/block-inject.d.ts +15 -0
  9. package/dist/block-inject.js +91 -0
  10. package/dist/browser.d.ts +15 -0
  11. package/dist/browser.js +29 -0
  12. package/dist/cli.js +322 -47
  13. package/dist/coverage-gap.d.ts +11 -0
  14. package/dist/coverage-gap.js +119 -0
  15. package/dist/graph.d.ts +1 -0
  16. package/dist/graph.js +99 -91
  17. package/dist/pilot-overlay.d.ts +7 -1
  18. package/dist/pilot-overlay.js +59 -41
  19. package/dist/pilot.d.ts +7 -2
  20. package/dist/pilot.js +34 -7
  21. package/dist/practices-check.d.ts +14 -0
  22. package/dist/practices-check.js +115 -0
  23. package/examples/saucedemo/.waygraph-auto/0b937f26.log +4 -0
  24. package/examples/saucedemo/.waygraph-auto/2b608ed3.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/2c13567b.log +0 -0
  26. package/examples/saucedemo/.waygraph-auto/2d9622a5.log +4 -0
  27. package/examples/saucedemo/.waygraph-auto/305f6e5b.log +4 -0
  28. package/examples/saucedemo/.waygraph-auto/418befbf.log +4 -0
  29. package/examples/saucedemo/.waygraph-auto/483bd763.log +4 -0
  30. package/examples/saucedemo/.waygraph-auto/48e3596a.log +0 -0
  31. package/examples/saucedemo/.waygraph-auto/4adfbe91.json +8 -0
  32. package/examples/saucedemo/.waygraph-auto/4adfbe91.log +0 -0
  33. package/examples/saucedemo/.waygraph-auto/4f0e33e7.log +4 -0
  34. package/examples/saucedemo/.waygraph-auto/5bd8fedf.log +4 -0
  35. package/examples/saucedemo/.waygraph-auto/5dde8331.log +4 -0
  36. package/examples/saucedemo/.waygraph-auto/68903903.log +0 -0
  37. package/examples/saucedemo/.waygraph-auto/78bbf43c.log +0 -0
  38. package/examples/saucedemo/.waygraph-auto/7efa646f.log +4 -0
  39. package/examples/saucedemo/.waygraph-auto/9cecb0c8.log +4 -0
  40. package/examples/saucedemo/.waygraph-auto/aae57d51.log +0 -0
  41. package/examples/saucedemo/.waygraph-auto/b61a08d8.log +0 -0
  42. package/examples/saucedemo/.waygraph-auto/c946f66c.log +0 -0
  43. package/examples/saucedemo/.waygraph-auto/cd0d5b50.log +4 -0
  44. package/examples/saucedemo/.waygraph-auto/cd5b203c.log +0 -0
  45. package/examples/saucedemo/.waygraph-auto/ea2df316.log +4 -0
  46. package/examples/saucedemo/.waygraph-auto/ee60ed24.log +0 -0
  47. package/examples/saucedemo/.waygraph-auto/f2b6cb1b.log +0 -0
  48. package/examples/saucedemo/.waygraph-auto/f392cdc8.log +0 -0
  49. package/package.json +2 -2
  50. package/templates/scaffold/package.json +1 -1
  51. package/templates/skills/waygraph-convention.skill.md +11 -1
  52. package/templates/skills/waygraph-pilot.skill.md +71 -4
@@ -5,7 +5,8 @@ import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
5
5
  import { ensureLivePage, detectHere, seedDefaultMem, ensureMem, runOneBlock, resolveBaseUrl, } from "./auto-explore-run.js";
6
6
  import { runStubPhase } from "./highlights.js";
7
7
  import { findBlockPathDetailed } from "./graph.js";
8
- import { updatePilotOverlay, showPilotActivity, showPilotVision, showPilotFixtures } from "./pilot-overlay.js";
8
+ import { updatePilotOverlay, showPilotActivity, showPilotVision, showPilotFixtures, installPersistentPilotOverlay, } from "./pilot-overlay.js";
9
+ import { collectKnownInteractionSelectors, unmappedInteractionsPageScript, } from "./coverage-gap.js";
9
10
  /** True when a stub-phase result actually carries authored content worth keeping. */
10
11
  function stubPhaseHasContent(r) {
11
12
  return (Object.keys(r.highlights).length > 0 ||
@@ -142,6 +143,7 @@ const CONSOLE_LOG_MAX_ENTRIES = 200;
142
143
  */
143
144
  export class AutoSession {
144
145
  projectDir;
146
+ inject;
145
147
  blocksSelect;
146
148
  graph;
147
149
  library;
@@ -156,8 +158,12 @@ export class AutoSession {
156
158
  lastRunNote = null;
157
159
  trace = [];
158
160
  consoleLog = [];
159
- constructor(projectDir, blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, sessionId) {
161
+ knownInteractionSelectors;
162
+ headless;
163
+ lastOverlayInfo = null;
164
+ constructor(projectDir, inject, blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, sessionId, knownInteractionSelectors, headless) {
160
165
  this.projectDir = projectDir;
166
+ this.inject = inject;
161
167
  this.blocksSelect = blocksSelect;
162
168
  this.graph = graph;
163
169
  this.library = library;
@@ -168,11 +174,18 @@ export class AutoSession {
168
174
  this.page = page;
169
175
  this.startUrl = startUrl;
170
176
  this.sessionId = sessionId;
177
+ this.knownInteractionSelectors = knownInteractionSelectors;
178
+ this.headless = headless;
171
179
  }
172
180
  static async start(init) {
173
181
  const baseURL = init.baseURL ?? resolveBaseUrl(init.projectDir);
174
- const startUrl = init.startUrl ?? baseURL;
175
- const { graph, library } = await buildExploreContext(init.projectDir, init.blocksSelect ? { blocksSelect: init.blocksSelect } : undefined);
182
+ const skipNav = init.skipInitialNavigation === true;
183
+ const startUrl = skipNav ? undefined : (init.startUrl ?? baseURL);
184
+ const exploreOpts = {
185
+ ...(init.blocksSelect ? { blocksSelect: init.blocksSelect } : {}),
186
+ ...(init.inject?.length ? { inject: init.inject } : {}),
187
+ };
188
+ const { graph, library } = await buildExploreContext(init.projectDir, Object.keys(exploreOpts).length ? exploreOpts : undefined);
176
189
  const mem = new MemPage();
177
190
  seedDefaultMem(library.byName, mem);
178
191
  const headless = init.headless ?? true;
@@ -188,6 +201,9 @@ export class AutoSession {
188
201
  // headless keeps the fixed viewport since there's no real window to size.
189
202
  const launchOpts = {
190
203
  headless,
204
+ // Keep Playwright's default --no-startup-window for headful: without it
205
+ // Chromium opens its own "New Tab" window AND our context.newPage() opens
206
+ // a second about:blank window. bringToFront() below surfaces the real page.
191
207
  args: headless ? [] : ["--start-maximized"],
192
208
  };
193
209
  if (executablePath)
@@ -199,12 +215,24 @@ export class AutoSession {
199
215
  if (baseURL)
200
216
  contextOpts.baseURL = baseURL;
201
217
  const context = await browser.newContext(contextOpts);
218
+ await installPersistentPilotOverlay(context);
202
219
  const page = await context.newPage();
203
220
  if (startUrl) {
204
221
  await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
205
222
  await page.waitForLoadState("load").catch(() => { });
206
223
  }
207
- const session = new AutoSession(init.projectDir, init.blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, init.sessionId);
224
+ else {
225
+ await page.goto("about:blank").catch(() => { });
226
+ }
227
+ const knownInteractionSelectors = collectKnownInteractionSelectors(init.projectDir, library, init.inject);
228
+ const session = new AutoSession(init.projectDir, init.inject, init.blocksSelect, graph, library, mem, engine, browser, context, page, startUrl, init.sessionId, knownInteractionSelectors, headless);
229
+ page.on("load", () => {
230
+ void session.repaintOverlayAfterNavigation();
231
+ });
232
+ if (!headless) {
233
+ await session.bootstrapOverlay().catch(() => { });
234
+ await page.bringToFront().catch(() => { });
235
+ }
208
236
  // Attached once, on the page created here - `ensureLivePage` only swaps
209
237
  // to a fresh Page if the current one closed (a rare case, e.g. the human
210
238
  // closing the tab), not on ordinary same-page SPA navigation, which is
@@ -277,65 +305,52 @@ export class AutoSession {
277
305
  // whole point is a human watching the screen sees the SAME state a
278
306
  // concurrent API caller just got back, not a stale frame that catches
279
307
  // up moments later.
280
- await updatePilotOverlay(this.page, {
308
+ this.lastOverlayInfo = {
281
309
  sessionId: this.sessionId,
282
310
  snapshot: buildSessionSnapshot(menu, this.library.byName, this.here, this.lastRunNote),
283
311
  graph: this.graph,
284
- });
285
- await this.warnUnmappedLinks();
312
+ };
313
+ await updatePilotOverlay(this.page, this.lastOverlayInfo);
314
+ await this.warnUnmappedInteractions();
286
315
  return menu;
287
316
  }
317
+ /** Paint the overlay on about:blank / immediately after session start (headful browser). */
318
+ async bootstrapOverlay() {
319
+ await this.currentMenu();
320
+ }
321
+ /** Re-apply cached overlay state after navigation (init script restores shell only). */
322
+ async repaintOverlayAfterNavigation() {
323
+ if (this.lastOverlayInfo) {
324
+ await updatePilotOverlay(this.page, this.lastOverlayInfo).catch(() => { });
325
+ return;
326
+ }
327
+ if (!this.headless) {
328
+ await this.bootstrapOverlay().catch(() => { });
329
+ }
330
+ }
288
331
  /**
289
- * Real, direct user request: a Blind Pilot agent had no way to ask "which
290
- * of the links visible on THIS page are already covered by a NavBlock" -
291
- * only manual DOM inspection + guessing hrefs, exactly what this session
292
- * had been doing by hand the whole time this feature was built. Compares
293
- * every same-origin `<a href>` on the page against every NavBlock's own
294
- * static url (`__waygraphNavUrl` - see `defineNavBlock`'s own comment for
295
- * why only plain-string urls qualify), and `console.warn()`s each pathname
296
- * with no NavBlock at all - surfaced through the same real console capture
297
- * `auto console` already reads, not a separate channel. Deduped per path
298
- * per page load (a `Set` living on `window`, so it survives repeated
299
- * `currentMenu()` calls but resets on a real navigation) - once is enough
300
- * to flag a gap, repeating it on every status/send call would just be noise.
332
+ * Flags same-origin `<a href>` paths and visible buttons with no NavBlock
333
+ * URL / Block selector in the loaded library — read via `auto console`.
334
+ * Deduped per page load so repeated status/send calls stay quiet.
301
335
  */
302
- async warnUnmappedLinks() {
303
- const known = new Set();
336
+ async warnUnmappedInteractions() {
337
+ const knownPathnames = [];
304
338
  for (const entry of this.library.navBlocks) {
305
339
  const url = entry.block.__waygraphNavUrl;
306
340
  if (!url)
307
341
  continue;
308
342
  try {
309
- known.add(new URL(url, this.page.url()).pathname);
343
+ knownPathnames.push(new URL(url, this.page.url()).pathname);
310
344
  }
311
345
  catch {
312
346
  /* not a resolvable URL (relative to an unset base, etc.) - skip */
313
347
  }
314
348
  }
315
- await this.page
316
- .evaluate((knownPathnames) => {
317
- const w = window;
318
- w.__wgWarnedPaths ??= new Set();
319
- const seenThisPass = new Set();
320
- for (const a of Array.from(document.querySelectorAll("a[href]"))) {
321
- let url;
322
- try {
323
- url = new URL(a.getAttribute("href") || "", location.href);
324
- }
325
- catch {
326
- continue;
327
- }
328
- if (url.origin !== location.origin)
329
- continue;
330
- const path = url.pathname;
331
- if (seenThisPass.has(path) || knownPathnames.includes(path) || w.__wgWarnedPaths.has(path))
332
- continue;
333
- seenThisPass.add(path);
334
- w.__wgWarnedPaths.add(path);
335
- console.warn(`[waygraph] unmapped nav link on this page: ${path} - no NavBlock covers this URL`);
336
- }
337
- }, Array.from(known))
338
- .catch(() => { });
349
+ const payload = {
350
+ knownPathnames,
351
+ knownSelectors: this.knownInteractionSelectors,
352
+ };
353
+ await this.page.evaluate(unmappedInteractionsPageScript, payload).catch(() => { });
339
354
  }
340
355
  /** Pure getter - no Block runs, no mem/page mutation. */
341
356
  async currentSnapshot() {
@@ -514,7 +529,11 @@ export class AutoSession {
514
529
  async reloadLibrary() {
515
530
  if (this.page)
516
531
  await showPilotActivity(this.page, "Running: reload");
517
- const { graph, library } = await buildExploreContext(this.projectDir, this.blocksSelect ? { blocksSelect: this.blocksSelect } : undefined);
532
+ const exploreOpts = {
533
+ ...(this.blocksSelect ? { blocksSelect: this.blocksSelect } : {}),
534
+ ...(this.inject?.length ? { inject: this.inject } : {}),
535
+ };
536
+ const { graph, library } = await buildExploreContext(this.projectDir, Object.keys(exploreOpts).length ? exploreOpts : undefined);
518
537
  this.graph = graph;
519
538
  this.library = library;
520
539
  if (this.page)
@@ -561,7 +580,7 @@ export class AutoSession {
561
580
  */
562
581
  async rawClick(selector) {
563
582
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
564
- await showPilotActivity(this.page, `Running (Dom): click "${selector}"`);
583
+ await showPilotActivity(this.page, `Running (no Block): click "${selector}"`);
565
584
  const locator = this.page.locator(selector).first();
566
585
  if ((await locator.count()) === 0) {
567
586
  return { ok: false, error: `no element matches selector "${selector}"` };
@@ -579,7 +598,7 @@ export class AutoSession {
579
598
  }
580
599
  async rawType(selector, text) {
581
600
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
582
- await showPilotActivity(this.page, `Running (Dom): type into "${selector}"`);
601
+ await showPilotActivity(this.page, `Running (no Block): type into "${selector}"`);
583
602
  const locator = this.page.locator(selector).first();
584
603
  if ((await locator.count()) === 0) {
585
604
  return { ok: false, error: `no element matches selector "${selector}"` };
@@ -604,7 +623,7 @@ export class AutoSession {
604
623
  */
605
624
  async rawPress(selector, key) {
606
625
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
607
- await showPilotActivity(this.page, `Running (Dom): press "${key}" on "${selector}"`);
626
+ await showPilotActivity(this.page, `Running (no Block): press "${key}" on "${selector}"`);
608
627
  const locator = this.page.locator(selector).first();
609
628
  if ((await locator.count()) === 0) {
610
629
  return { ok: false, error: `no element matches selector "${selector}"` };
@@ -622,7 +641,7 @@ export class AutoSession {
622
641
  }
623
642
  async rawGoto(url) {
624
643
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
625
- await showPilotActivity(this.page, `Running (Dom): goto "${url}"`);
644
+ await showPilotActivity(this.page, `Running (no Block): goto "${url}"`);
626
645
  try {
627
646
  await this.page.goto(url, { waitUntil: "domcontentloaded" });
628
647
  }
@@ -649,7 +668,7 @@ export class AutoSession {
649
668
  this.page = await ensureLivePage(this.context, this.page, this.startUrl);
650
669
  const filePath = typeof stub === "string" ? stubFilePath(stub) : stub.filePath;
651
670
  const label = typeof stub === "string" ? stub : filePath;
652
- await showPilotActivity(this.page, `Running (Dom): upload ${label} into "${selector}"`);
671
+ await showPilotActivity(this.page, `Running (no Block): upload ${label} into "${selector}"`);
653
672
  const locator = this.page.locator(selector).first();
654
673
  if ((await locator.count()) === 0) {
655
674
  return { ok: false, error: `no element matches selector "${selector}"` };
@@ -0,0 +1,15 @@
1
+ export declare function packageRoot(): string;
2
+ /** Human label for an injected root (used in merged `file` paths). */
3
+ export declare function injectLabel(root: string): string;
4
+ /**
5
+ * Display path for a Block discovered under an inject root (not the host cwd).
6
+ * Primary project files stay relative as-is; injected ones are tagged `@label/...`.
7
+ */
8
+ export declare function injectTaggedFile(hostDir: string, root: string, relFile: string): string;
9
+ /**
10
+ * Resolve one `--inject` token to an absolute directory (preset name or path).
11
+ */
12
+ export declare function resolveInjectRoot(token: string, cwd?: string): string;
13
+ export declare function resolveInjectRoots(tokens: string[], cwd?: string): string[];
14
+ /** Roots to scan: host project first, then each inject path (deduped). */
15
+ export declare function exploreRoots(projectDir: string, inject?: string[]): string[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Merge Block libraries / graphs from extra project roots into a host session
3
+ * (waygraph browser `--inject`). Presets name bundled or well-known checkouts.
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { homedir } from "node:os";
9
+ export function packageRoot() {
10
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
+ }
12
+ /** Human label for an injected root (used in merged `file` paths). */
13
+ export function injectLabel(root) {
14
+ return basename(root.replace(/\/$/, ""));
15
+ }
16
+ /**
17
+ * Display path for a Block discovered under an inject root (not the host cwd).
18
+ * Primary project files stay relative as-is; injected ones are tagged `@label/...`.
19
+ */
20
+ export function injectTaggedFile(hostDir, root, relFile) {
21
+ const host = resolve(hostDir);
22
+ const r = resolve(root);
23
+ if (r === host)
24
+ return relFile.replace(/\\/g, "/");
25
+ return `@${injectLabel(r)}/${relFile.replace(/\\/g, "/")}`;
26
+ }
27
+ const PRESETS = {
28
+ saucedemo: () => join(packageRoot(), "examples", "saucedemo"),
29
+ demosauce: () => join(packageRoot(), "examples", "saucedemo"),
30
+ veciro: () => {
31
+ if (process.env.WAYGRAPH_INJECT_VECIRO) {
32
+ return resolve(process.env.WAYGRAPH_INJECT_VECIRO);
33
+ }
34
+ for (const c of [
35
+ join(homedir(), "Desktop", "Work", "veciro-waygraph"),
36
+ join(homedir(), "Desktop", "Projects", "veciro-waygraph"),
37
+ ]) {
38
+ if (existsSync(c))
39
+ return c;
40
+ }
41
+ return null;
42
+ },
43
+ };
44
+ /**
45
+ * Resolve one `--inject` token to an absolute directory (preset name or path).
46
+ */
47
+ export function resolveInjectRoot(token, cwd = process.cwd()) {
48
+ const raw = token.trim();
49
+ if (!raw)
50
+ throw new Error("waygraph inject: empty --inject value");
51
+ const preset = PRESETS[raw.toLowerCase()];
52
+ if (preset) {
53
+ const dir = preset();
54
+ if (!dir || !existsSync(dir)) {
55
+ throw new Error(`waygraph inject: preset "${raw}" not found on disk (pass an absolute path to the Block library root)`);
56
+ }
57
+ return resolve(dir);
58
+ }
59
+ const abs = resolve(cwd, raw);
60
+ if (!existsSync(abs)) {
61
+ throw new Error(`waygraph inject: no such directory: ${abs}`);
62
+ }
63
+ return abs;
64
+ }
65
+ export function resolveInjectRoots(tokens, cwd = process.cwd()) {
66
+ const seen = new Set();
67
+ const out = [];
68
+ for (const t of tokens) {
69
+ const abs = resolveInjectRoot(t, cwd);
70
+ if (seen.has(abs))
71
+ continue;
72
+ seen.add(abs);
73
+ out.push(abs);
74
+ }
75
+ return out;
76
+ }
77
+ /** Roots to scan: host project first, then each inject path (deduped). */
78
+ export function exploreRoots(projectDir, inject) {
79
+ const host = resolve(projectDir);
80
+ const extra = inject ?? [];
81
+ const seen = new Set([host]);
82
+ const roots = [host];
83
+ for (const r of extra) {
84
+ const abs = resolve(r);
85
+ if (seen.has(abs))
86
+ continue;
87
+ seen.add(abs);
88
+ roots.push(abs);
89
+ }
90
+ return roots;
91
+ }
@@ -0,0 +1,15 @@
1
+ import type { AutoSessionInit } from "./auto-session.js";
2
+ export interface BrowserStartInit extends AutoSessionInit {
3
+ /** Preset names or paths; resolved before spawn. */
4
+ injectTokens?: string[];
5
+ }
6
+ export interface BrowserStartResult {
7
+ sessionId: string;
8
+ socketPath: string;
9
+ headless: boolean;
10
+ /** Resolved inject roots merged into this session (host project excluded). */
11
+ inject: string[];
12
+ }
13
+ /** Always spawns a new detached session — does not stop or reuse existing ones. */
14
+ export declare function browserStart(init: BrowserStartInit): Promise<BrowserStartResult>;
15
+ export { listBrowserSessions, stopBrowserSession, stopAllBrowserSessions, type SessionMeta, } from "./auto-session-ipc.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `waygraph browser` — persistent Playwright session with the project's Block
3
+ * library loaded (and optional `--inject` roots). Pilot drives these sessions
4
+ * via `browser send|status|highlight|…`; it does not spawn its own browser.
5
+ *
6
+ * Session lifecycle is explicit: `browser sessions` lists what's live;
7
+ * `browser start` always opens another; `browser attach` / `browser stop` pick one up or shut it down.
8
+ */
9
+ import { spawnDetachedSession } from "./auto-session-ipc.js";
10
+ import { resolveInjectRoots } from "./block-inject.js";
11
+ /** Always spawns a new detached session — does not stop or reuse existing ones. */
12
+ export async function browserStart(init) {
13
+ const inject = init.inject ??
14
+ (init.injectTokens?.length ? resolveInjectRoots(init.injectTokens, init.projectDir) : []);
15
+ const headless = init.headless ?? false;
16
+ const meta = await spawnDetachedSession({
17
+ ...init,
18
+ inject,
19
+ headless,
20
+ skipInitialNavigation: init.skipInitialNavigation ?? true,
21
+ });
22
+ return {
23
+ sessionId: meta.sessionId,
24
+ socketPath: meta.socketPath,
25
+ headless: meta.headless,
26
+ inject,
27
+ };
28
+ }
29
+ export { listBrowserSessions, stopBrowserSession, stopAllBrowserSessions, } from "./auto-session-ipc.js";