waygraph 0.15.43 → 0.15.45
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/helpers.d.ts +2 -2
- package/dist/auto-session/session.js +4 -4
- package/dist/cli/usage.d.ts +1 -1
- 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 +7 -7
- 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 +13 -11
- package/dist/index.js +7 -6
- package/dist/mem-page.js +2 -1
- package/dist/pilot.d.ts +1 -1
- package/dist/pilot.js +1 -1
- package/dist/session/auto-explore-run.d.ts +37 -0
- package/dist/session/auto-explore-run.js +670 -0
- package/dist/session/auto-explore.d.ts +57 -0
- package/dist/session/auto-explore.js +376 -0
- package/dist/session/auto-session-ipc.d.ts +150 -0
- package/dist/session/auto-session-ipc.js +499 -0
- package/dist/session/block-inject.d.ts +21 -0
- package/dist/session/block-inject.js +97 -0
- package/dist/session/browser.d.ts +15 -0
- package/dist/session/browser.js +29 -0
- package/dist/session/overlay-beacon.d.ts +44 -0
- package/dist/session/overlay-beacon.js +85 -0
- package/dist/session/pilot.d.ts +54 -0
- package/dist/session/pilot.js +80 -0
- 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/00b0c55c.log +22 -0
- package/examples/saucedemo/.waygraph-auto/00e5d037.log +5 -0
- package/examples/saucedemo/.waygraph-auto/015be8f6.log +6 -0
- package/examples/saucedemo/.waygraph-auto/026e188e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/050e5b55.log +0 -0
- package/examples/saucedemo/.waygraph-auto/07845ed1.log +0 -0
- package/examples/saucedemo/.waygraph-auto/096fea05.log +20 -0
- package/examples/saucedemo/.waygraph-auto/0c19e232.log +5 -0
- package/examples/saucedemo/.waygraph-auto/1122c13c.log +0 -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/2b9b56fc.log +20 -0
- package/examples/saucedemo/.waygraph-auto/30a5c848.log +20 -0
- package/examples/saucedemo/.waygraph-auto/33a50dc0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/3a208bb8.log +20 -0
- package/examples/saucedemo/.waygraph-auto/3e3c0f3b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/573abedd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/756fd326.log +20 -0
- package/examples/saucedemo/.waygraph-auto/78043767.log +20 -0
- package/examples/saucedemo/.waygraph-auto/976394cd.log +20 -0
- package/examples/saucedemo/.waygraph-auto/9a26134c.log +20 -0
- package/examples/saucedemo/.waygraph-auto/9d827954.log +20 -0
- package/examples/saucedemo/.waygraph-auto/a4f8d355.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/af2fae12.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b0752c49.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b14811d7.log +6 -0
- package/examples/saucedemo/.waygraph-auto/c9d76e32.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cae91469.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cc989184.log +20 -0
- package/examples/saucedemo/.waygraph-auto/d846cd78.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e5127c03.log +22 -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/agents/waygraph-author.agent.md +24 -51
- package/templates/agents/waygraph-planner.agent.md +12 -5
- 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,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
import type { Page } from "@playwright/test";
|
|
13
|
+
export declare const WG_UI_SEL = "[data-wg-ui=\"1\"]";
|
|
14
|
+
export declare const WG_READY_PANEL_SEL = "[data-wg-modal=\"panel\"][data-wg-ready=\"1\"]";
|
|
15
|
+
export declare const WG_READY_AUTO_SEL = "[data-wg-modal=\"auto-panel\"][data-wg-ready=\"1\"]";
|
|
16
|
+
export declare const WG_READY_BANNER_SEL = "[data-wg-modal=\"banner\"][data-wg-ready=\"1\"]";
|
|
17
|
+
export type WgModalKind = "panel" | "banner" | "auto-panel";
|
|
18
|
+
export type WgBeaconRow = {
|
|
19
|
+
id: string | null;
|
|
20
|
+
modal: string | null;
|
|
21
|
+
ready: boolean;
|
|
22
|
+
phase: string | null;
|
|
23
|
+
step: string | null;
|
|
24
|
+
block: string | null;
|
|
25
|
+
collapsed: boolean;
|
|
26
|
+
opacity: string;
|
|
27
|
+
textLen: number;
|
|
28
|
+
w: number;
|
|
29
|
+
h: number;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
33
|
+
export declare function readWgOverlayBeaconInPage(): WgBeaconRow[];
|
|
34
|
+
export declare function readWgOverlayBeacon(page: Page): Promise<WgBeaconRow[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
37
|
+
* Use after demo/auto paints its first overlay.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertWgOverlayReady(page: Page, opts?: {
|
|
40
|
+
modal?: WgModalKind;
|
|
41
|
+
minTextLen?: number;
|
|
42
|
+
/** When true, collapsed mini chrome is allowed (still needs visible box + text). */
|
|
43
|
+
allowCollapsed?: boolean;
|
|
44
|
+
}): Promise<WgBeaconRow>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
export const WG_UI_SEL = '[data-wg-ui="1"]';
|
|
13
|
+
export const WG_READY_PANEL_SEL = '[data-wg-modal="panel"][data-wg-ready="1"]';
|
|
14
|
+
export const WG_READY_AUTO_SEL = '[data-wg-modal="auto-panel"][data-wg-ready="1"]';
|
|
15
|
+
export const WG_READY_BANNER_SEL = '[data-wg-modal="banner"][data-wg-ready="1"]';
|
|
16
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
17
|
+
export function readWgOverlayBeaconInPage() {
|
|
18
|
+
const w = globalThis;
|
|
19
|
+
if (typeof w.__wgOverlayBeacon === "function")
|
|
20
|
+
return w.__wgOverlayBeacon();
|
|
21
|
+
return Array.from(document.querySelectorAll(WG_UI_SEL)).map((el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
const st = getComputedStyle(el);
|
|
24
|
+
return {
|
|
25
|
+
id: el.id || null,
|
|
26
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
27
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
28
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
29
|
+
step: el.getAttribute("data-wg-step"),
|
|
30
|
+
block: el.getAttribute("data-wg-block"),
|
|
31
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
32
|
+
opacity: st.opacity,
|
|
33
|
+
textLen: (el.textContent || "").trim().length,
|
|
34
|
+
w: Math.round(r.width),
|
|
35
|
+
h: Math.round(r.height),
|
|
36
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function readWgOverlayBeacon(page) {
|
|
41
|
+
return page.evaluate(() => {
|
|
42
|
+
const w = window;
|
|
43
|
+
if (typeof w.__wgOverlayBeacon === "function") {
|
|
44
|
+
return w.__wgOverlayBeacon();
|
|
45
|
+
}
|
|
46
|
+
return Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
|
|
47
|
+
const r = el.getBoundingClientRect();
|
|
48
|
+
const st = getComputedStyle(el);
|
|
49
|
+
return {
|
|
50
|
+
id: el.id || null,
|
|
51
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
52
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
53
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
54
|
+
step: el.getAttribute("data-wg-step"),
|
|
55
|
+
block: el.getAttribute("data-wg-block"),
|
|
56
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
57
|
+
opacity: st.opacity,
|
|
58
|
+
textLen: (el.textContent || "").trim().length,
|
|
59
|
+
w: Math.round(r.width),
|
|
60
|
+
h: Math.round(r.height),
|
|
61
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
68
|
+
* Use after demo/auto paints its first overlay.
|
|
69
|
+
*/
|
|
70
|
+
export async function assertWgOverlayReady(page, opts) {
|
|
71
|
+
const modal = opts?.modal ?? "panel";
|
|
72
|
+
const minText = opts?.minTextLen ?? 12;
|
|
73
|
+
const rows = await readWgOverlayBeacon(page);
|
|
74
|
+
const hit = rows.find((r) => r.modal === modal &&
|
|
75
|
+
r.ready &&
|
|
76
|
+
r.visible &&
|
|
77
|
+
r.textLen >= minText &&
|
|
78
|
+
(opts?.allowCollapsed || !r.collapsed || r.textLen >= minText));
|
|
79
|
+
if (!hit) {
|
|
80
|
+
throw new Error(`waygraph overlay NOT ready (modal=${modal}, minTextLen=${minText}). ` +
|
|
81
|
+
`beacons=${JSON.stringify(rows)}. ` +
|
|
82
|
+
`Do not claim stepper/modal works.`);
|
|
83
|
+
}
|
|
84
|
+
return hit;
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waygraph Pilot: the one bootstrap call an agent needs to start driving a
|
|
3
|
+
* real, persistent, inspectable browser session toward a natural-language
|
|
4
|
+
* goal it plans and executes itself.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT a resolver that maps one ask to one Block
|
|
7
|
+
* (`resolveAsk`/`pilotNarrate`/`pilotAct`, the v1 shape this replaced - see
|
|
8
|
+
* git tag `waygraph-pilot-v1-logs-prettified` for that history). Real
|
|
9
|
+
* requests are multi-step ("log in and buy the backpack for me", "make a
|
|
10
|
+
* signature draft request") and need an agent that plans a whole sequence
|
|
11
|
+
* against the graph, not a single best-matching edge. `pilotStart` does not
|
|
12
|
+
* plan, resolve, narrate, or act on anything - it only combines two
|
|
13
|
+
* already-proven, separately-existing primitives into one payload:
|
|
14
|
+
*
|
|
15
|
+
* - `spawnDetachedSession` (same mechanism as `auto --cli --detach`) - a
|
|
16
|
+
* real, persistent, visible-if-headful Playwright session an agent can
|
|
17
|
+
* keep driving across many calls, not a fresh session per request.
|
|
18
|
+
* - `discoverGraph` (same mechanism as `waygraph graph`) - the WHOLE
|
|
19
|
+
* project's Checkpoint/edge/description graph, not just what's reachable
|
|
20
|
+
* from the session's current position, because planning a multi-step
|
|
21
|
+
* route needs to see steps ahead, not just the next one.
|
|
22
|
+
*
|
|
23
|
+
* Driving the session afterward - `auto send <id> "<pick>"`, `auto status
|
|
24
|
+
* <id>`, `auto dom <id>`, `auto trace <id>` - is left entirely to whatever
|
|
25
|
+
* agent is holding it. All four already existed before this file did; see
|
|
26
|
+
* openspec/changes/waygraph-pilot/design.md for the worked example (a real
|
|
27
|
+
* login + add-to-cart + checkout run against examples/saucedemo, driven by
|
|
28
|
+
* hand-picking indices from each status response) that proved this needs no
|
|
29
|
+
* new execution primitive.
|
|
30
|
+
*/
|
|
31
|
+
import { type WaygraphGraph } from "../analysis/graph.js";
|
|
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";
|
|
35
|
+
export interface PilotStartResult {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
socketPath: string;
|
|
38
|
+
headless: boolean;
|
|
39
|
+
/** The whole project's Block graph - every Checkpoint/edge/description, not just what's reachable right now. */
|
|
40
|
+
graph: WaygraphGraph;
|
|
41
|
+
/** The session's starting position, or `null` if the very first `status` read failed. */
|
|
42
|
+
snapshot: SessionSnapshot | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Starts a detached session and reads back its own graph + starting
|
|
46
|
+
* snapshot in one call, so an agent bootstraps with everything it needs
|
|
47
|
+
* (a live session it can keep driving, and the full map of what exists) in
|
|
48
|
+
* one round trip instead of separately knowing to chain `auto --cli
|
|
49
|
+
* --detach` and `waygraph graph` itself.
|
|
50
|
+
*/
|
|
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 };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waygraph Pilot: the one bootstrap call an agent needs to start driving a
|
|
3
|
+
* real, persistent, inspectable browser session toward a natural-language
|
|
4
|
+
* goal it plans and executes itself.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT a resolver that maps one ask to one Block
|
|
7
|
+
* (`resolveAsk`/`pilotNarrate`/`pilotAct`, the v1 shape this replaced - see
|
|
8
|
+
* git tag `waygraph-pilot-v1-logs-prettified` for that history). Real
|
|
9
|
+
* requests are multi-step ("log in and buy the backpack for me", "make a
|
|
10
|
+
* signature draft request") and need an agent that plans a whole sequence
|
|
11
|
+
* against the graph, not a single best-matching edge. `pilotStart` does not
|
|
12
|
+
* plan, resolve, narrate, or act on anything - it only combines two
|
|
13
|
+
* already-proven, separately-existing primitives into one payload:
|
|
14
|
+
*
|
|
15
|
+
* - `spawnDetachedSession` (same mechanism as `auto --cli --detach`) - a
|
|
16
|
+
* real, persistent, visible-if-headful Playwright session an agent can
|
|
17
|
+
* keep driving across many calls, not a fresh session per request.
|
|
18
|
+
* - `discoverGraph` (same mechanism as `waygraph graph`) - the WHOLE
|
|
19
|
+
* project's Checkpoint/edge/description graph, not just what's reachable
|
|
20
|
+
* from the session's current position, because planning a multi-step
|
|
21
|
+
* route needs to see steps ahead, not just the next one.
|
|
22
|
+
*
|
|
23
|
+
* Driving the session afterward - `auto send <id> "<pick>"`, `auto status
|
|
24
|
+
* <id>`, `auto dom <id>`, `auto trace <id>` - is left entirely to whatever
|
|
25
|
+
* agent is holding it. All four already existed before this file did; see
|
|
26
|
+
* openspec/changes/waygraph-pilot/design.md for the worked example (a real
|
|
27
|
+
* login + add-to-cart + checkout run against examples/saucedemo, driven by
|
|
28
|
+
* hand-picking indices from each status response) that proved this needs no
|
|
29
|
+
* new execution primitive.
|
|
30
|
+
*/
|
|
31
|
+
import { discoverGraph } from "../analysis/graph.js";
|
|
32
|
+
import { requestSession, listBrowserSessions, resolveSessionMeta, } from "./auto-session-ipc.js";
|
|
33
|
+
import { browserStart } from "./browser.js";
|
|
34
|
+
/**
|
|
35
|
+
* Starts a detached session and reads back its own graph + starting
|
|
36
|
+
* snapshot in one call, so an agent bootstraps with everything it needs
|
|
37
|
+
* (a live session it can keep driving, and the full map of what exists) in
|
|
38
|
+
* one round trip instead of separately knowing to chain `auto --cli
|
|
39
|
+
* --detach` and `waygraph graph` itself.
|
|
40
|
+
*/
|
|
41
|
+
export async function pilotStart(init) {
|
|
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
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
sessionId: meta.sessionId,
|
|
74
|
+
socketPath: meta.socketPath,
|
|
75
|
+
headless: meta.headless,
|
|
76
|
+
graph,
|
|
77
|
+
snapshot: status.snapshot,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export { listBrowserSessions };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface TraverseCoverageReport {
|
|
2
|
+
version: 1;
|
|
3
|
+
generatedAt: string;
|
|
4
|
+
projectDir: string;
|
|
5
|
+
parallel: number;
|
|
6
|
+
/** Unique edge keys in the discovered graph (block names). */
|
|
7
|
+
edgesTotal: number;
|
|
8
|
+
/** Graph edges successfully walked this suite (after instance normalize). */
|
|
9
|
+
edgesHit: number;
|
|
10
|
+
/** edgesHit / edgesTotal (0 when total is 0). */
|
|
11
|
+
ratio: number;
|
|
12
|
+
/** Optional suite gate that was applied (0-1). */
|
|
13
|
+
minEdgeCoverage?: number;
|
|
14
|
+
/** true when minEdgeCoverage was set and ratio met it. */
|
|
15
|
+
coveragePass?: boolean;
|
|
16
|
+
/** Sorted graph edge keys that were hit. */
|
|
17
|
+
hit: string[];
|
|
18
|
+
/** Sorted graph edge keys never walked. */
|
|
19
|
+
missed: string[];
|
|
20
|
+
/** Raw worker hit keys (may include block::instanceId). */
|
|
21
|
+
rawHit?: string[];
|
|
22
|
+
workers?: Array<{
|
|
23
|
+
traverseId: string;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
steps: number;
|
|
26
|
+
edgesHit: number;
|
|
27
|
+
leaf?: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/** Static graph edge key = Block runtime name. */
|
|
31
|
+
export declare function edgeKeyFromGraphEdge(edge: {
|
|
32
|
+
block: string;
|
|
33
|
+
}): string;
|
|
34
|
+
/** Collect unique edge keys from a WaygraphGraph-like edges list. */
|
|
35
|
+
export declare function collectGraphEdgeKeys(edges: ReadonlyArray<{
|
|
36
|
+
block: string;
|
|
37
|
+
}>): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Map a worker hit key onto the static graph key it covers.
|
|
40
|
+
* `add-to-cart::sku-1` -> `add-to-cart` when that block is in the graph.
|
|
41
|
+
*/
|
|
42
|
+
export declare function normalizeHitKey(hitKey: string, graphKeys: ReadonlySet<string>): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Parse --min-edge-coverage: `0.8`, `80%`, `80` (as percent when > 1).
|
|
45
|
+
* Returns 0-1 fraction, or null when unset/invalid.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseMinEdgeCoverage(raw: unknown): number | null;
|
|
48
|
+
export declare function buildCoverageReport(opts: {
|
|
49
|
+
projectDir: string;
|
|
50
|
+
parallel: number;
|
|
51
|
+
allEdgeKeys: readonly string[];
|
|
52
|
+
hitKeys: ReadonlySet<string> | readonly string[];
|
|
53
|
+
minEdgeCoverage?: number | null;
|
|
54
|
+
workers?: TraverseCoverageReport["workers"];
|
|
55
|
+
}): TraverseCoverageReport;
|
|
56
|
+
/** Greppable one-liner for agents/CI. */
|
|
57
|
+
export declare function formatCoverageLine(report: TraverseCoverageReport): string;
|
|
58
|
+
export declare function defaultCoverageOutPath(projectDir: string): string;
|
|
59
|
+
export declare function writeCoverageReport(path: string, report: TraverseCoverageReport): void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase E: traverse edge coverage report + suite gate.
|
|
3
|
+
* Coverage is independent of leaf PASS - a green leaf can still fail CI
|
|
4
|
+
* when --min-edge-coverage is set and ratio is too low.
|
|
5
|
+
*
|
|
6
|
+
* Denominator = unique static graph edge block names (FF inners stay as their
|
|
7
|
+
* own graph edges when discovered; RFC open Q deferred - no covered-via-prefix yet).
|
|
8
|
+
* A hit of `block::instanceId` counts as covering `block`.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
/** Static graph edge key = Block runtime name. */
|
|
13
|
+
export function edgeKeyFromGraphEdge(edge) {
|
|
14
|
+
return edge.block;
|
|
15
|
+
}
|
|
16
|
+
/** Collect unique edge keys from a WaygraphGraph-like edges list. */
|
|
17
|
+
export function collectGraphEdgeKeys(edges) {
|
|
18
|
+
const set = new Set();
|
|
19
|
+
for (const e of edges)
|
|
20
|
+
set.add(edgeKeyFromGraphEdge(e));
|
|
21
|
+
return [...set].sort();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map a worker hit key onto the static graph key it covers.
|
|
25
|
+
* `add-to-cart::sku-1` -> `add-to-cart` when that block is in the graph.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeHitKey(hitKey, graphKeys) {
|
|
28
|
+
if (graphKeys.has(hitKey))
|
|
29
|
+
return hitKey;
|
|
30
|
+
const base = hitKey.includes("::") ? hitKey.slice(0, hitKey.indexOf("::")) : hitKey;
|
|
31
|
+
if (graphKeys.has(base))
|
|
32
|
+
return base;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse --min-edge-coverage: `0.8`, `80%`, `80` (as percent when > 1).
|
|
37
|
+
* Returns 0-1 fraction, or null when unset/invalid.
|
|
38
|
+
*/
|
|
39
|
+
export function parseMinEdgeCoverage(raw) {
|
|
40
|
+
if (raw === undefined || raw === null || raw === "")
|
|
41
|
+
return null;
|
|
42
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
43
|
+
if (raw < 0)
|
|
44
|
+
return null;
|
|
45
|
+
if (raw <= 1)
|
|
46
|
+
return raw;
|
|
47
|
+
if (raw <= 100)
|
|
48
|
+
return raw / 100;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (typeof raw !== "string")
|
|
52
|
+
return null;
|
|
53
|
+
const t = raw.trim().toLowerCase();
|
|
54
|
+
if (!t)
|
|
55
|
+
return null;
|
|
56
|
+
if (t.endsWith("%")) {
|
|
57
|
+
const n = Number(t.slice(0, -1));
|
|
58
|
+
if (!Number.isFinite(n) || n < 0 || n > 100)
|
|
59
|
+
return null;
|
|
60
|
+
return n / 100;
|
|
61
|
+
}
|
|
62
|
+
const n = Number(t);
|
|
63
|
+
if (!Number.isFinite(n) || n < 0)
|
|
64
|
+
return null;
|
|
65
|
+
if (n <= 1)
|
|
66
|
+
return n;
|
|
67
|
+
if (n <= 100)
|
|
68
|
+
return n / 100;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export function buildCoverageReport(opts) {
|
|
72
|
+
const all = [...new Set(opts.allEdgeKeys)].sort();
|
|
73
|
+
const graphSet = new Set(all);
|
|
74
|
+
const rawList = Array.isArray(opts.hitKeys) ? [...opts.hitKeys] : [...opts.hitKeys];
|
|
75
|
+
const covered = new Set();
|
|
76
|
+
for (const raw of rawList) {
|
|
77
|
+
const n = normalizeHitKey(raw, graphSet);
|
|
78
|
+
if (n)
|
|
79
|
+
covered.add(n);
|
|
80
|
+
}
|
|
81
|
+
const hit = all.filter((k) => covered.has(k));
|
|
82
|
+
const missed = all.filter((k) => !covered.has(k));
|
|
83
|
+
const edgesTotal = all.length;
|
|
84
|
+
const edgesHit = hit.length;
|
|
85
|
+
const ratio = edgesTotal === 0 ? 0 : edgesHit / edgesTotal;
|
|
86
|
+
const min = opts.minEdgeCoverage != null && Number.isFinite(opts.minEdgeCoverage)
|
|
87
|
+
? Math.min(1, Math.max(0, opts.minEdgeCoverage))
|
|
88
|
+
: undefined;
|
|
89
|
+
const coveragePass = min === undefined ? undefined : ratio + 1e-12 >= min;
|
|
90
|
+
const coverageExtra = min !== undefined && coveragePass !== undefined
|
|
91
|
+
? { minEdgeCoverage: min, coveragePass }
|
|
92
|
+
: {};
|
|
93
|
+
return {
|
|
94
|
+
version: 1,
|
|
95
|
+
generatedAt: new Date().toISOString(),
|
|
96
|
+
projectDir: opts.projectDir,
|
|
97
|
+
parallel: opts.parallel,
|
|
98
|
+
edgesTotal,
|
|
99
|
+
edgesHit,
|
|
100
|
+
ratio: Math.round(ratio * 10000) / 10000,
|
|
101
|
+
...coverageExtra,
|
|
102
|
+
hit,
|
|
103
|
+
missed,
|
|
104
|
+
rawHit: [...new Set(rawList)].sort(),
|
|
105
|
+
...(opts.workers ? { workers: opts.workers } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Greppable one-liner for agents/CI. */
|
|
109
|
+
export function formatCoverageLine(report) {
|
|
110
|
+
const pct = (report.ratio * 100).toFixed(1);
|
|
111
|
+
const minPart = report.minEdgeCoverage !== undefined
|
|
112
|
+
? ` min=${(report.minEdgeCoverage * 100).toFixed(1)}%` +
|
|
113
|
+
(report.coveragePass === false
|
|
114
|
+
? " FAIL"
|
|
115
|
+
: report.coveragePass === true
|
|
116
|
+
? " PASS"
|
|
117
|
+
: "")
|
|
118
|
+
: "";
|
|
119
|
+
return `[Coverage edges=${report.edgesHit}/${report.edgesTotal} ratio=${pct}%${minPart}]`;
|
|
120
|
+
}
|
|
121
|
+
export function defaultCoverageOutPath(projectDir) {
|
|
122
|
+
return join(projectDir, ".waygraph-traverse", "coverage.json");
|
|
123
|
+
}
|
|
124
|
+
export function writeCoverageReport(path, report) {
|
|
125
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
126
|
+
writeFileSync(path, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface EdgeLeaseRecord {
|
|
2
|
+
workerId: string;
|
|
3
|
+
claimedAt: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class EdgeLeaseCoordinator {
|
|
6
|
+
private readonly held;
|
|
7
|
+
private readonly dir;
|
|
8
|
+
constructor(opts?: {
|
|
9
|
+
projectDir?: string;
|
|
10
|
+
persist?: boolean;
|
|
11
|
+
});
|
|
12
|
+
/** True if this worker now owns `edgeKey` (or already owned it). */
|
|
13
|
+
tryClaim(edgeKey: string, workerId: string): boolean;
|
|
14
|
+
release(edgeKey: string, workerId: string): void;
|
|
15
|
+
/** Stable partition: same key always maps to the same worker index. */
|
|
16
|
+
static partitionIndex(edgeKey: string, parallel: number): number;
|
|
17
|
+
private flush;
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase D: edge leases so parallel traverse workers never claim the same edge.
|
|
3
|
+
* In-process Map is authoritative for `--parallel` (same Node process).
|
|
4
|
+
* Optional JSON under `.waygraph-traverse/` is an audit trail only.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
export class EdgeLeaseCoordinator {
|
|
9
|
+
held = new Map();
|
|
10
|
+
dir;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
if (opts?.persist && opts.projectDir) {
|
|
13
|
+
this.dir = join(opts.projectDir, ".waygraph-traverse");
|
|
14
|
+
if (!existsSync(this.dir))
|
|
15
|
+
mkdirSync(this.dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.dir = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** True if this worker now owns `edgeKey` (or already owned it). */
|
|
22
|
+
tryClaim(edgeKey, workerId) {
|
|
23
|
+
const cur = this.held.get(edgeKey);
|
|
24
|
+
if (cur && cur.workerId !== workerId)
|
|
25
|
+
return false;
|
|
26
|
+
this.held.set(edgeKey, { workerId, claimedAt: Date.now() });
|
|
27
|
+
this.flush();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
release(edgeKey, workerId) {
|
|
31
|
+
const cur = this.held.get(edgeKey);
|
|
32
|
+
if (cur && cur.workerId === workerId) {
|
|
33
|
+
this.held.delete(edgeKey);
|
|
34
|
+
this.flush();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Stable partition: same key always maps to the same worker index. */
|
|
38
|
+
static partitionIndex(edgeKey, parallel) {
|
|
39
|
+
if (parallel <= 1)
|
|
40
|
+
return 0;
|
|
41
|
+
let h = 2166136261;
|
|
42
|
+
for (let i = 0; i < edgeKey.length; i++) {
|
|
43
|
+
h ^= edgeKey.charCodeAt(i);
|
|
44
|
+
h = Math.imul(h, 16777619);
|
|
45
|
+
}
|
|
46
|
+
return (h >>> 0) % parallel;
|
|
47
|
+
}
|
|
48
|
+
flush() {
|
|
49
|
+
if (!this.dir)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const payload = {
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
leases: Object.fromEntries(this.held),
|
|
55
|
+
};
|
|
56
|
+
writeFileSync(join(this.dir, "leases.json"), JSON.stringify(payload, null, 2));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* audit only */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type TraverseSessionMode = "clone" | "inherit";
|
|
2
|
+
export interface TraverseWorkerResult {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
steps: number;
|
|
5
|
+
edgesHit: string[];
|
|
6
|
+
leaf?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TraverseOptions {
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
startUrl?: string;
|
|
11
|
+
/** Seed checkpoint label (optional - otherwise detect from page / nav). */
|
|
12
|
+
from?: string;
|
|
13
|
+
/** Global Mem seed JSON (or use WAYGRAPH_DATA / WAYGRAPH_AUTO_MEM). */
|
|
14
|
+
data?: string;
|
|
15
|
+
/** Max Block runs this traverse (default 50). */
|
|
16
|
+
maxSteps?: number;
|
|
17
|
+
/** Max times a checkpoint may be visited (default 2). */
|
|
18
|
+
maxVisitsPerNode?: number;
|
|
19
|
+
/** Max times an edge key (block::instanceId) may be taken (default 1). */
|
|
20
|
+
maxVisitsPerEdge?: number;
|
|
21
|
+
/** Wall-clock budget ms (default 15m). */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** Show browser (default headless). */
|
|
24
|
+
headed?: boolean;
|
|
25
|
+
/** Traverse id for greppable lines (default traverse-1). */
|
|
26
|
+
traverseId?: string;
|
|
27
|
+
/** Phase C: glob / regex / bare `--blocks` file select. */
|
|
28
|
+
blocksSelect?: import("../blocks-select.js").BlocksSelect;
|
|
29
|
+
/** Phase D: parallel workers (default 1). Cap 4. */
|
|
30
|
+
parallel?: number;
|
|
31
|
+
/** Phase D: clone (default) or inherit. Inherit refused when parallel > 1. */
|
|
32
|
+
session?: TraverseSessionMode;
|
|
33
|
+
/**
|
|
34
|
+
* Phase E: fail suite (exit 2) when hit/total ratio is below this 0-1 fraction.
|
|
35
|
+
* Leaf PASS alone does not satisfy the gate.
|
|
36
|
+
*/
|
|
37
|
+
minEdgeCoverage?: number;
|
|
38
|
+
/** Phase E: write coverage JSON here (default `.waygraph-traverse/coverage.json`). */
|
|
39
|
+
coverageOut?: string;
|
|
40
|
+
/** Phase E: skip writing coverage.json (still prints the Coverage line). */
|
|
41
|
+
noCoverageReport?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Serial or parallel graph crawl. Prints greppable PASS/FAIL + Coverage lines.
|
|
45
|
+
* Exit: 0 leaf ok (+ coverage ok), 1 break/timeout, 2 coverage gate fail (Phase E).
|
|
46
|
+
*/
|
|
47
|
+
export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
|