waygraph 0.12.18 → 0.12.20
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 +6 -3
- package/dist/auto-explore-run.js +13 -6
- package/dist/cli.js +145 -9
- package/dist/highlights.d.ts +10 -0
- package/dist/highlights.js +45 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/traverse-coverage.d.ts +59 -0
- package/dist/traverse-coverage.js +127 -0
- package/dist/traverse-run.d.ts +17 -1
- package/dist/traverse-run.js +73 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,11 +228,14 @@ real acts). `--ff-expand` is the older expand-only flag (same flatten). Saucedem
|
|
|
228
228
|
`checkoutFlow` uses `ff-owner-auth` for the login prefix; `loginFlow` stays expanded for
|
|
229
229
|
Sign In narration.
|
|
230
230
|
|
|
231
|
-
`waygraph traverse` walks the Block graph (Phase B
|
|
231
|
+
`waygraph traverse` walks the Block graph (Phase B-E). `--parallel N` (default
|
|
232
232
|
`--session clone`) bootstraps one context, forks N workers with `storageState` + mem
|
|
233
233
|
snapshot, partitions edges by hash, and claims via in-process edge leases (audit under
|
|
234
|
-
`.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`.
|
|
235
|
-
`
|
|
234
|
+
`.waygraph-traverse/`). `--session inherit` is refused when `parallel > 1`. Phase E
|
|
235
|
+
writes `.waygraph-traverse/coverage.json` and prints `[Coverage edges=H/T ratio=R%...]`;
|
|
236
|
+
`--min-edge-coverage 80%` fails the suite with exit 2 when the ratio is below the gate
|
|
237
|
+
(even if every worker leaf-PASSed). `--coverage-out PATH` / `--no-coverage-report` adjust
|
|
238
|
+
the JSON. See `docs/proposals/traverse-ffcompose-rfc.md`.
|
|
236
239
|
|
|
237
240
|
**Richer demo narration:** automation rings are **gray**; authored stubs/slides can use
|
|
238
241
|
`tone: "info" | "warning" | "danger" | "success"` (iconified captions), plus
|
package/dist/auto-explore-run.js
CHANGED
|
@@ -194,20 +194,23 @@ async function runOneBlock(engine, entry, context, page, mem, headful) {
|
|
|
194
194
|
const flow = engine.defineFlow([start, entry.block, end]);
|
|
195
195
|
const BLOCK_TIMEOUT_MS = 45_000;
|
|
196
196
|
let timer;
|
|
197
|
+
let result;
|
|
197
198
|
try {
|
|
198
|
-
await Promise.race([
|
|
199
|
+
const ran = await Promise.race([
|
|
199
200
|
flow.run(context, mem, { page, closeOnFinish: false }),
|
|
200
201
|
new Promise((_, reject) => {
|
|
201
202
|
timer = setTimeout(() => reject(new Error(`Block "${entry.block.name}" timed out after ${BLOCK_TIMEOUT_MS / 1000}s - ` +
|
|
202
203
|
"click Quit and retry, or check network / mem credentials")), BLOCK_TIMEOUT_MS);
|
|
203
204
|
}),
|
|
204
205
|
]);
|
|
206
|
+
result = ran.result;
|
|
205
207
|
}
|
|
206
208
|
finally {
|
|
207
209
|
if (timer)
|
|
208
210
|
clearTimeout(timer);
|
|
209
211
|
}
|
|
210
212
|
await page.evaluate("delete window.__wgPendingNavClickLabel").catch(() => { });
|
|
213
|
+
return result;
|
|
211
214
|
}
|
|
212
215
|
const PANEL_CSS = "#wg-auto-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
|
|
213
216
|
"max-width:min(92vw,720px);max-height:calc(100vh - 24px);overflow-y:auto;box-sizing:border-box;" +
|
|
@@ -540,13 +543,16 @@ export async function runAutoExplore(projectDir, options = {}) {
|
|
|
540
543
|
}
|
|
541
544
|
wiredPage = target;
|
|
542
545
|
}
|
|
546
|
+
let here = null;
|
|
543
547
|
try {
|
|
544
548
|
for (;;) {
|
|
545
549
|
page = await ensureLivePage(context, page, startUrl);
|
|
546
550
|
await wirePage(page);
|
|
547
551
|
const pickWait = armPickWait();
|
|
548
552
|
pickSlot.resolve = pickWait.resolve;
|
|
549
|
-
|
|
553
|
+
if (here === null) {
|
|
554
|
+
here = await detectHere(page, library.navBlocks);
|
|
555
|
+
}
|
|
550
556
|
if (cli)
|
|
551
557
|
console.error("waygraph auto: scanning page for moves ...");
|
|
552
558
|
const menu = await buildExploreMenu(page, graph, library, here);
|
|
@@ -595,10 +601,11 @@ export async function runAutoExplore(projectDir, options = {}) {
|
|
|
595
601
|
}
|
|
596
602
|
const pickLabel = edge.label ?? entry.block.name;
|
|
597
603
|
console.error(`waygraph auto: running [${pick.index + 1}] ${pickLabel} ...`);
|
|
598
|
-
await runOneBlock(engine, entry, context, page, mem, !cli);
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
const result = await runOneBlock(engine, entry, context, page, mem, !cli);
|
|
605
|
+
// Trust block output over locate() - nav verify traits collide on /dashboard.
|
|
606
|
+
here = result.__state ?? edge.to;
|
|
607
|
+
lastRunNote = `${entry.block.name} -> ${here}`;
|
|
608
|
+
console.error(`waygraph auto: ran ${entry.block.name} -> ${here}`);
|
|
602
609
|
}
|
|
603
610
|
catch (err) {
|
|
604
611
|
lastRunNote = `${entry.block.name} failed`;
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./gra
|
|
|
21
21
|
import { runAutoExplore } from "./auto-explore-run.js";
|
|
22
22
|
import { runAgentDive } from "./agent-dive.js";
|
|
23
23
|
import { runTraverse } from "./traverse-run.js";
|
|
24
|
+
import { parseMinEdgeCoverage } from "./traverse-coverage.js";
|
|
24
25
|
import { isFileSelectToken, parseBlocksSelect, } from "./blocks-select.js";
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Filesystem
|
|
@@ -221,6 +222,8 @@ import {
|
|
|
221
222
|
demoPaceIsBlitz,
|
|
222
223
|
demoPaceIsFast,
|
|
223
224
|
demoPaceIsSlow,
|
|
225
|
+
formatDemoPaceBadge,
|
|
226
|
+
formatDemoPaceLabel,
|
|
224
227
|
} from "waygraph";
|
|
225
228
|
|
|
226
229
|
function walkDir(dir, pattern) {
|
|
@@ -491,6 +494,18 @@ const RING_CSS =
|
|
|
491
494
|
// block chain shows no episode heading at all.
|
|
492
495
|
"#wg-panel .wg-episode{margin:0 0 6px;font:700 15px system-ui,sans-serif;color:#fff;" +
|
|
493
496
|
"padding-bottom:6px;border-bottom:1px solid rgba(124,58,237,.35);}" +
|
|
497
|
+
// Pace chip - numbers speak in the overlay (2.5x / 4500ms / slow).
|
|
498
|
+
"#wg-panel .wg-pace{display:inline-flex;align-items:center;gap:8px;margin:0 0 10px;" +
|
|
499
|
+
"padding:6px 10px;border-radius:8px;background:rgba(124,58,237,.22);" +
|
|
500
|
+
"border:1px solid rgba(124,58,237,.45);font:600 12px/1.35 system-ui,sans-serif;color:#f0e8ff;}" +
|
|
501
|
+
"#wg-panel .wg-pace-badge{display:inline-block;padding:2px 8px;border-radius:999px;" +
|
|
502
|
+
"background:#7C3AED;color:#fff;font:800 11px/1.2 system-ui,sans-serif;letter-spacing:.04em;" +
|
|
503
|
+
"text-transform:uppercase;}" +
|
|
504
|
+
"#wg-panel .wg-pace[data-pace-kind=slow] .wg-pace-badge," +
|
|
505
|
+
"#wg-panel .wg-pace[data-pace-kind=num-slow] .wg-pace-badge{background:#EAB308;color:#1c1917;}" +
|
|
506
|
+
"#wg-panel .wg-pace[data-pace-kind=fast] .wg-pace-badge," +
|
|
507
|
+
"#wg-panel .wg-pace[data-pace-kind=blitz] .wg-pace-badge{background:#22C55E;color:#052e16;}" +
|
|
508
|
+
"#wg-panel .wg-pace[data-pace-kind=ms] .wg-pace-badge{background:#3B82F6;color:#fff;}" +
|
|
494
509
|
"#wg-panel .wg-narration{margin:0 0 12px;font:italic 14px/1.4 system-ui,sans-serif;color:#f0e8ff;}" +
|
|
495
510
|
"#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
|
|
496
511
|
"#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
|
|
@@ -957,9 +972,20 @@ async function renderBeforeStep(page, info) {
|
|
|
957
972
|
const narrationHtml = info.description
|
|
958
973
|
? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
|
|
959
974
|
: "";
|
|
975
|
+
const paceHtml =
|
|
976
|
+
info.paceBadge || info.paceLabel
|
|
977
|
+
? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
|
|
978
|
+
esc(info.paceKind || "normal") +
|
|
979
|
+
"\\"><span class=\\"wg-pace-badge\\">" +
|
|
980
|
+
esc(info.paceBadge || "1x") +
|
|
981
|
+
"</span><span>" +
|
|
982
|
+
esc(info.paceLabel || "") +
|
|
983
|
+
"</span></div>"
|
|
984
|
+
: "";
|
|
960
985
|
let html =
|
|
961
986
|
"<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
|
|
962
987
|
episodesHtml +
|
|
988
|
+
paceHtml +
|
|
963
989
|
"<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
|
|
964
990
|
"<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
|
|
965
991
|
narrationHtml;
|
|
@@ -995,7 +1021,12 @@ async function renderBeforeStep(page, info) {
|
|
|
995
1021
|
}
|
|
996
1022
|
if (window.__wgWirePanelChrome) {
|
|
997
1023
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
998
|
-
stepLabel:
|
|
1024
|
+
stepLabel:
|
|
1025
|
+
(info.index + 1) +
|
|
1026
|
+
" / " +
|
|
1027
|
+
info.total +
|
|
1028
|
+
(info.blockName ? " · " + info.blockName : "") +
|
|
1029
|
+
(info.paceBadge ? " · " + info.paceBadge : ""),
|
|
999
1030
|
forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
|
|
1000
1031
|
});
|
|
1001
1032
|
}
|
|
@@ -1214,9 +1245,20 @@ async function renderAfterStep(page, info) {
|
|
|
1214
1245
|
.join("") +
|
|
1215
1246
|
"</div>"
|
|
1216
1247
|
: "";
|
|
1248
|
+
const paceHtml =
|
|
1249
|
+
info.paceBadge || info.paceLabel
|
|
1250
|
+
? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
|
|
1251
|
+
escA(info.paceKind || "normal") +
|
|
1252
|
+
"\\"><span class=\\"wg-pace-badge\\">" +
|
|
1253
|
+
escA(info.paceBadge || "1x") +
|
|
1254
|
+
"</span><span>" +
|
|
1255
|
+
escA(info.paceLabel || "") +
|
|
1256
|
+
"</span></div>"
|
|
1257
|
+
: "";
|
|
1217
1258
|
panel.innerHTML =
|
|
1218
1259
|
"<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
|
|
1219
1260
|
episodesHtml +
|
|
1261
|
+
paceHtml +
|
|
1220
1262
|
"<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
|
|
1221
1263
|
"<h3>" + heading + "</h3>" +
|
|
1222
1264
|
resultHtml +
|
|
@@ -1227,7 +1269,12 @@ async function renderAfterStep(page, info) {
|
|
|
1227
1269
|
}
|
|
1228
1270
|
if (window.__wgWirePanelChrome) {
|
|
1229
1271
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1230
|
-
stepLabel:
|
|
1272
|
+
stepLabel:
|
|
1273
|
+
(info.index + 1) +
|
|
1274
|
+
" / " +
|
|
1275
|
+
info.total +
|
|
1276
|
+
(info.blockName ? " · " + info.blockName : "") +
|
|
1277
|
+
(info.paceBadge ? " · " + info.paceBadge : ""),
|
|
1231
1278
|
forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
|
|
1232
1279
|
});
|
|
1233
1280
|
}
|
|
@@ -1329,7 +1376,12 @@ async function renderStepError(page, info) {
|
|
|
1329
1376
|
}
|
|
1330
1377
|
if (window.__wgWirePanelChrome) {
|
|
1331
1378
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1332
|
-
stepLabel:
|
|
1379
|
+
stepLabel:
|
|
1380
|
+
(info.index + 1) +
|
|
1381
|
+
" / " +
|
|
1382
|
+
info.total +
|
|
1383
|
+
(info.blockName ? " · " + info.blockName : "") +
|
|
1384
|
+
(info.paceBadge ? " · " + info.paceBadge : ""),
|
|
1333
1385
|
forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
|
|
1334
1386
|
});
|
|
1335
1387
|
}
|
|
@@ -1537,14 +1589,13 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1537
1589
|
const episodeTitle = (opts && opts.episodeTitle) || "";
|
|
1538
1590
|
const gatesFast = !!(opts && opts.fast);
|
|
1539
1591
|
const demoPace = normalizeDemoPace((opts && opts.pace) || (gatesFast ? "fast" : "normal"));
|
|
1540
|
-
const
|
|
1592
|
+
const baseAutoplay =
|
|
1541
1593
|
opts && opts.autoplayMs
|
|
1542
1594
|
? Number(opts.autoplayMs)
|
|
1543
1595
|
: process.env.WAYGRAPH_AUTOPLAY_MS
|
|
1544
1596
|
? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
|
|
1545
|
-
:
|
|
1546
|
-
|
|
1547
|
-
: 1800;
|
|
1597
|
+
: 1800;
|
|
1598
|
+
const autoplayMs = demoPaceGateMs(demoPace, baseAutoplay);
|
|
1548
1599
|
for (let i = 0; i < slides.length; i++) {
|
|
1549
1600
|
const s = slides[i];
|
|
1550
1601
|
const caption = formatHighlightCaption(s);
|
|
@@ -1584,11 +1635,22 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1584
1635
|
(info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
|
|
1585
1636
|
" · yap slides</div>"
|
|
1586
1637
|
: "";
|
|
1638
|
+
const paceHtml =
|
|
1639
|
+
info.paceBadge || info.paceLabel
|
|
1640
|
+
? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
|
|
1641
|
+
esc(info.paceKind || "normal") +
|
|
1642
|
+
"\\"><span class=\\"wg-pace-badge\\">" +
|
|
1643
|
+
esc(info.paceBadge || "1x") +
|
|
1644
|
+
"</span><span>" +
|
|
1645
|
+
esc(info.paceLabel || "") +
|
|
1646
|
+
"</span></div>"
|
|
1647
|
+
: "";
|
|
1587
1648
|
panel.innerHTML =
|
|
1588
1649
|
"<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
|
|
1589
1650
|
"<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
|
|
1590
1651
|
"<div class=\\"wg-body\\">" +
|
|
1591
1652
|
episodeLine +
|
|
1653
|
+
paceHtml +
|
|
1592
1654
|
"<h3>Slide " +
|
|
1593
1655
|
(info.index + 1) +
|
|
1594
1656
|
" / " +
|
|
@@ -1706,6 +1768,9 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1706
1768
|
dwellMs,
|
|
1707
1769
|
episodeNumber: episodeNumber !== undefined ? episodeNumber : null,
|
|
1708
1770
|
episodeTitle,
|
|
1771
|
+
paceBadge: formatDemoPaceBadge(demoPace, baseAutoplay),
|
|
1772
|
+
paceLabel: formatDemoPaceLabel(demoPace, baseAutoplay),
|
|
1773
|
+
paceKind: demoPaceKind(demoPace),
|
|
1709
1774
|
},
|
|
1710
1775
|
);
|
|
1711
1776
|
const started = Date.now();
|
|
@@ -1738,6 +1803,25 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1738
1803
|
await hideRing(page);
|
|
1739
1804
|
}
|
|
1740
1805
|
|
|
1806
|
+
function demoPaceKind(pace) {
|
|
1807
|
+
const p = normalizeDemoPace(pace);
|
|
1808
|
+
if (typeof p === "number" && p > 20) return "ms";
|
|
1809
|
+
if (typeof p === "number") return demoPaceIsSlow(p) ? "num-slow" : demoPaceIsFast(p) ? "fast" : "normal";
|
|
1810
|
+
if (p === "blitz" || p === "fast" || p === "slow") return p;
|
|
1811
|
+
return "normal";
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function paceSpeakFields(pace, baseAutoplayMs) {
|
|
1815
|
+
const p = normalizeDemoPace(pace);
|
|
1816
|
+
const base = Number.isFinite(baseAutoplayMs) && baseAutoplayMs > 0 ? baseAutoplayMs : 1800;
|
|
1817
|
+
return {
|
|
1818
|
+
pace: p,
|
|
1819
|
+
paceBadge: formatDemoPaceBadge(p, base),
|
|
1820
|
+
paceLabel: formatDemoPaceLabel(p, base),
|
|
1821
|
+
paceKind: demoPaceKind(p),
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1741
1825
|
async function hideRing(page) {
|
|
1742
1826
|
await page
|
|
1743
1827
|
.evaluate(() => {
|
|
@@ -2302,6 +2386,20 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2302
2386
|
}
|
|
2303
2387
|
return { name: k.name, value };
|
|
2304
2388
|
});
|
|
2389
|
+
const paceSpeak = paceSpeakFields(pacing.demoPace, autoplayMs);
|
|
2390
|
+
if (process.env.WAYGRAPH_JSON !== "1") {
|
|
2391
|
+
console.log(
|
|
2392
|
+
"waygraph demo: step " +
|
|
2393
|
+
(i + 1) +
|
|
2394
|
+
"/" +
|
|
2395
|
+
resolved.length +
|
|
2396
|
+
" " +
|
|
2397
|
+
r.block.name +
|
|
2398
|
+
" · " +
|
|
2399
|
+
paceSpeak.paceLabel +
|
|
2400
|
+
(r.episodeNumber ? " · episode " + r.episodeNumber : ""),
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2305
2403
|
await renderBeforeStep(page, {
|
|
2306
2404
|
index: i,
|
|
2307
2405
|
total: resolved.length,
|
|
@@ -2318,6 +2416,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2318
2416
|
episodeTitle: r.episodeTitle,
|
|
2319
2417
|
stepperMode,
|
|
2320
2418
|
forceCollapsed,
|
|
2419
|
+
...paceSpeak,
|
|
2321
2420
|
});
|
|
2322
2421
|
const edits = await gate();
|
|
2323
2422
|
for (const k of requires) {
|
|
@@ -2487,6 +2586,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2487
2586
|
highlights,
|
|
2488
2587
|
gatesFast: pacing.gatesFast,
|
|
2489
2588
|
pace: pacing.demoPace,
|
|
2589
|
+
...paceSpeakFields(pacing.demoPace, autoplayMs),
|
|
2490
2590
|
isLast: i === resolved.length - 1,
|
|
2491
2591
|
allNames: moduleNames,
|
|
2492
2592
|
allDescriptions: moduleDescriptions,
|
|
@@ -3851,7 +3951,7 @@ Primary (less is more):
|
|
|
3851
3951
|
|
|
3852
3952
|
Also:
|
|
3853
3953
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3854
|
-
waygraph traverse [project] Graph crawl (Phase B
|
|
3954
|
+
waygraph traverse [project] Graph crawl (Phase B-E)
|
|
3855
3955
|
--blocks <glob|/re/|sub> Phase C: filter *.block.ts discovery
|
|
3856
3956
|
--parallel N Phase D: N clone workers (max 4)
|
|
3857
3957
|
--session clone|inherit Phase D: default clone
|
|
@@ -3977,6 +4077,9 @@ async function main() {
|
|
|
3977
4077
|
let blocksFilter;
|
|
3978
4078
|
let parallel;
|
|
3979
4079
|
let session;
|
|
4080
|
+
let minEdgeCoverageRaw;
|
|
4081
|
+
let coverageOut;
|
|
4082
|
+
let noCoverageReport = false;
|
|
3980
4083
|
let projectDir = process.cwd();
|
|
3981
4084
|
for (let i = 0; i < rest.length; i++) {
|
|
3982
4085
|
const a = rest[i];
|
|
@@ -4016,6 +4119,22 @@ async function main() {
|
|
|
4016
4119
|
session = v;
|
|
4017
4120
|
continue;
|
|
4018
4121
|
}
|
|
4122
|
+
if (a === "--min-edge-coverage" || a.startsWith("--min-edge-coverage=")) {
|
|
4123
|
+
minEdgeCoverageRaw = a.startsWith("--min-edge-coverage=")
|
|
4124
|
+
? a.slice("--min-edge-coverage=".length)
|
|
4125
|
+
: rest[++i];
|
|
4126
|
+
continue;
|
|
4127
|
+
}
|
|
4128
|
+
if (a === "--coverage-out" || a.startsWith("--coverage-out=")) {
|
|
4129
|
+
coverageOut = a.startsWith("--coverage-out=")
|
|
4130
|
+
? a.slice("--coverage-out=".length)
|
|
4131
|
+
: rest[++i];
|
|
4132
|
+
continue;
|
|
4133
|
+
}
|
|
4134
|
+
if (a === "--no-coverage-report") {
|
|
4135
|
+
noCoverageReport = true;
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4019
4138
|
if (a === "--max-steps" || a.startsWith("--max-steps=")) {
|
|
4020
4139
|
const v = a.startsWith("--max-steps=") ? a.slice(12) : rest[++i];
|
|
4021
4140
|
maxSteps = Number(v);
|
|
@@ -4044,12 +4163,15 @@ async function main() {
|
|
|
4044
4163
|
if (a === "--help" || a === "-h") {
|
|
4045
4164
|
console.log(`waygraph traverse [project] [flags]
|
|
4046
4165
|
|
|
4047
|
-
Graph crawl (RFC Phase B
|
|
4166
|
+
Graph crawl (RFC Phase B-E). Walks unused legal edges until a leaf,
|
|
4048
4167
|
budget kill, or first broken edge. --parallel N uses session clone + edge leases.
|
|
4049
4168
|
|
|
4050
4169
|
--blocks <glob|/regex/|substr> Phase C: filter *.block.ts discovery
|
|
4051
4170
|
--parallel N Phase D: N clone workers (default 1, max 4)
|
|
4052
4171
|
--session clone|inherit Phase D: default clone; inherit refused if parallel>1
|
|
4172
|
+
--min-edge-coverage X Phase E: suite gate 0.8 | 80% | 80 (exit 2 if below)
|
|
4173
|
+
--coverage-out PATH Phase E: write JSON (default .waygraph-traverse/coverage.json)
|
|
4174
|
+
--no-coverage-report Phase E: print Coverage line only (no JSON file)
|
|
4053
4175
|
--from <Checkpoint> start checkpoint (optional)
|
|
4054
4176
|
--data '{...}' Mem seed JSON
|
|
4055
4177
|
--max-steps N default 50 (per worker)
|
|
@@ -4061,9 +4183,11 @@ budget kill, or first broken edge. --parallel N uses session clone + edge leases
|
|
|
4061
4183
|
Examples:
|
|
4062
4184
|
waygraph traverse --blocks '**/mailpit/**/*.block.ts'
|
|
4063
4185
|
waygraph traverse --parallel 2 --session clone --max-steps 20
|
|
4186
|
+
waygraph traverse --min-edge-coverage 80% --coverage-out ./cov.json
|
|
4064
4187
|
|
|
4065
4188
|
PASS: [Reached Leaf Node[traverse-1] at=... steps=N]
|
|
4066
4189
|
FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
4190
|
+
COVER: [Coverage edges=H/T ratio=R% min=M% PASS|FAIL]
|
|
4067
4191
|
`);
|
|
4068
4192
|
process.exit(0);
|
|
4069
4193
|
}
|
|
@@ -4078,6 +4202,15 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
4078
4202
|
const blocksSelect = blocksFilter
|
|
4079
4203
|
? parseBlocksSelect(blocksFilter)
|
|
4080
4204
|
: undefined;
|
|
4205
|
+
let minEdgeCoverage;
|
|
4206
|
+
if (minEdgeCoverageRaw !== undefined) {
|
|
4207
|
+
const parsed = parseMinEdgeCoverage(minEdgeCoverageRaw);
|
|
4208
|
+
if (parsed === null) {
|
|
4209
|
+
console.error(`waygraph traverse: --min-edge-coverage expects 0.8, 80%, or 80 (got ${JSON.stringify(minEdgeCoverageRaw)})`);
|
|
4210
|
+
process.exit(1);
|
|
4211
|
+
}
|
|
4212
|
+
minEdgeCoverage = parsed;
|
|
4213
|
+
}
|
|
4081
4214
|
const code = await runTraverse(projectDir, {
|
|
4082
4215
|
...(from ? { from } : {}),
|
|
4083
4216
|
...(data ? { data } : {}),
|
|
@@ -4093,6 +4226,9 @@ FAIL: [Broke at edge[traverse-1] block=... from=... to=...]
|
|
|
4093
4226
|
...(blocksSelect ? { blocksSelect } : {}),
|
|
4094
4227
|
...(parallel !== undefined && Number.isFinite(parallel) ? { parallel } : {}),
|
|
4095
4228
|
...(session ? { session } : {}),
|
|
4229
|
+
...(minEdgeCoverage !== undefined ? { minEdgeCoverage } : {}),
|
|
4230
|
+
...(coverageOut ? { coverageOut: resolve(coverageOut) } : {}),
|
|
4231
|
+
...(noCoverageReport ? { noCoverageReport: true } : {}),
|
|
4096
4232
|
});
|
|
4097
4233
|
process.exit(code);
|
|
4098
4234
|
}
|
package/dist/highlights.d.ts
CHANGED
|
@@ -133,6 +133,16 @@ export declare function demoPaceIsFast(pace: DemoPace): boolean;
|
|
|
133
133
|
export declare function demoPaceIsSlow(pace: DemoPace): boolean;
|
|
134
134
|
/** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
|
|
135
135
|
export declare function demoPaceGateMs(pace: DemoPace, baseAutoplayMs: number): number;
|
|
136
|
+
/**
|
|
137
|
+
* Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
|
|
138
|
+
* Always printable - numbers speak in the UI, not only in code.
|
|
139
|
+
*/
|
|
140
|
+
export declare function formatDemoPaceBadge(pace: DemoPace, baseAutoplayMs?: number): string;
|
|
141
|
+
/**
|
|
142
|
+
* Loud one-line pace description for panel + console
|
|
143
|
+
* (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
|
|
144
|
+
*/
|
|
145
|
+
export declare function formatDemoPaceLabel(pace: DemoPace, baseAutoplayMs?: number): string;
|
|
136
146
|
/**
|
|
137
147
|
* Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
|
|
138
148
|
* or `null` when duration is unset (caller keeps legacy timing).
|
package/dist/highlights.js
CHANGED
|
@@ -162,6 +162,51 @@ export function demoPaceGateMs(pace, baseAutoplayMs) {
|
|
|
162
162
|
const scale = demoPaceScale(pace);
|
|
163
163
|
return Math.min(30000, Math.max(80, Math.round(baseAutoplayMs * scale)));
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
|
|
167
|
+
* Always printable - numbers speak in the UI, not only in code.
|
|
168
|
+
*/
|
|
169
|
+
export function formatDemoPaceBadge(pace, baseAutoplayMs = 1800) {
|
|
170
|
+
const p = normalizeDemoPace(pace);
|
|
171
|
+
if (typeof p === "number" && p > 20)
|
|
172
|
+
return `${Math.round(p)}ms`;
|
|
173
|
+
if (typeof p === "number") {
|
|
174
|
+
const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
|
|
175
|
+
return `${s}x`;
|
|
176
|
+
}
|
|
177
|
+
if (p === "blitz")
|
|
178
|
+
return "blitz";
|
|
179
|
+
if (p === "fast")
|
|
180
|
+
return "fast";
|
|
181
|
+
if (p === "slow")
|
|
182
|
+
return "slow";
|
|
183
|
+
void baseAutoplayMs;
|
|
184
|
+
return "1x";
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Loud one-line pace description for panel + console
|
|
188
|
+
* (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
|
|
189
|
+
*/
|
|
190
|
+
export function formatDemoPaceLabel(pace, baseAutoplayMs = 1800) {
|
|
191
|
+
const p = normalizeDemoPace(pace);
|
|
192
|
+
const gate = demoPaceGateMs(p, baseAutoplayMs);
|
|
193
|
+
const scale = demoPaceScale(p);
|
|
194
|
+
const scaleTxt = Number.isInteger(scale) ? String(scale) : (Math.round(scale * 100) / 100).toString();
|
|
195
|
+
if (typeof p === "number" && p > 20) {
|
|
196
|
+
return `pace ${Math.round(p)}ms gates (~${scaleTxt}x)`;
|
|
197
|
+
}
|
|
198
|
+
if (typeof p === "number") {
|
|
199
|
+
const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
|
|
200
|
+
return `pace ${s}x (~${gate}ms gates)`;
|
|
201
|
+
}
|
|
202
|
+
if (p === "blitz")
|
|
203
|
+
return `pace blitz (~${gate}ms, skip theater)`;
|
|
204
|
+
if (p === "fast")
|
|
205
|
+
return `pace fast (~${scaleTxt}x, ~${gate}ms gates)`;
|
|
206
|
+
if (p === "slow")
|
|
207
|
+
return `pace slow (~${scaleTxt}x, ~${gate}ms gates)`;
|
|
208
|
+
return `pace normal (1x, ~${gate}ms gates)`;
|
|
209
|
+
}
|
|
165
210
|
/**
|
|
166
211
|
* Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
|
|
167
212
|
* or `null` when duration is unset (caller keeps legacy timing).
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { connect, checkpoint } from "./types.js";
|
|
|
3
3
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
4
4
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
5
5
|
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
6
|
-
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
|
|
6
|
+
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
|
|
7
7
|
export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, } from "./highlights.js";
|
|
8
8
|
export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
|
|
9
9
|
export type { ActionPage } from "./types.js";
|
|
@@ -13,4 +13,6 @@ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp,
|
|
|
13
13
|
export type { BlocksSelect, BlocksSelectKind } from "./blocks-select.js";
|
|
14
14
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
15
15
|
export { runTraverse } from "./traverse-run.js";
|
|
16
|
-
export type { TraverseOptions, TraverseSessionMode } from "./traverse-run.js";
|
|
16
|
+
export type { TraverseOptions, TraverseSessionMode, TraverseWorkerResult, } from "./traverse-run.js";
|
|
17
|
+
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
18
|
+
export type { TraverseCoverageReport } from "./traverse-coverage.js";
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@ export { connect, checkpoint } from "./types.js";
|
|
|
2
2
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
3
3
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
4
4
|
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
5
|
-
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
|
|
5
|
+
export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
|
|
6
6
|
export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
7
7
|
export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
|
|
8
8
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
9
9
|
export { runTraverse } from "./traverse-run.js";
|
|
10
|
+
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
@@ -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
|
+
}
|
package/dist/traverse-run.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type TraverseSessionMode = "clone" | "inherit";
|
|
2
|
+
export interface TraverseWorkerResult {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
steps: number;
|
|
5
|
+
edgesHit: string[];
|
|
6
|
+
leaf?: string;
|
|
7
|
+
}
|
|
2
8
|
export interface TraverseOptions {
|
|
3
9
|
baseURL?: string;
|
|
4
10
|
startUrl?: string;
|
|
@@ -24,8 +30,18 @@ export interface TraverseOptions {
|
|
|
24
30
|
parallel?: number;
|
|
25
31
|
/** Phase D: clone (default) or inherit. Inherit refused when parallel > 1. */
|
|
26
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;
|
|
27
42
|
}
|
|
28
43
|
/**
|
|
29
|
-
* Serial or parallel graph crawl. Prints greppable PASS/FAIL
|
|
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).
|
|
30
46
|
*/
|
|
31
47
|
export declare function runTraverse(projectDir: string, options?: TraverseOptions): Promise<number>;
|
package/dist/traverse-run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { MemPage } from "./mem-page.js";
|
|
|
9
9
|
import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
|
|
10
10
|
import { defaultMemValueForKey } from "./auto-explore-run.js";
|
|
11
11
|
import { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
12
|
+
import { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, writeCoverageReport, } from "./traverse-coverage.js";
|
|
12
13
|
function resolveBaseUrl(projectDir) {
|
|
13
14
|
if (process.env.WAYGRAPH_BASE_URL)
|
|
14
15
|
return process.env.WAYGRAPH_BASE_URL;
|
|
@@ -132,6 +133,7 @@ async function runTraverseWorker(opts) {
|
|
|
132
133
|
const startedAt = Date.now();
|
|
133
134
|
let lastHere = opts.from ?? null;
|
|
134
135
|
let exitCode = 0;
|
|
136
|
+
let leaf;
|
|
135
137
|
try {
|
|
136
138
|
for (;;) {
|
|
137
139
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -171,6 +173,7 @@ async function runTraverseWorker(opts) {
|
|
|
171
173
|
});
|
|
172
174
|
if (candidates.length === 0) {
|
|
173
175
|
const label = here ?? detected ?? "unknown";
|
|
176
|
+
leaf = label;
|
|
174
177
|
console.log(`[Reached Leaf Node[${traverseId}] at=${label} steps=${steps}]`);
|
|
175
178
|
console.error(`waygraph traverse[${traverseId}]: PASS leaf edgesHit=${edgesHit.size} steps=${steps}`);
|
|
176
179
|
break;
|
|
@@ -227,10 +230,16 @@ async function runTraverseWorker(opts) {
|
|
|
227
230
|
finally {
|
|
228
231
|
console.error(`waygraph traverse[${traverseId}]: done exit=${exitCode} steps=${steps} edgesHit=${edgesHit.size}`);
|
|
229
232
|
}
|
|
230
|
-
return
|
|
233
|
+
return {
|
|
234
|
+
exitCode,
|
|
235
|
+
steps,
|
|
236
|
+
edgesHit: [...edgesHit],
|
|
237
|
+
...(leaf ? { leaf } : {}),
|
|
238
|
+
};
|
|
231
239
|
}
|
|
232
240
|
/**
|
|
233
|
-
* Serial or parallel graph crawl. Prints greppable PASS/FAIL
|
|
241
|
+
* Serial or parallel graph crawl. Prints greppable PASS/FAIL + Coverage lines.
|
|
242
|
+
* Exit: 0 leaf ok (+ coverage ok), 1 break/timeout, 2 coverage gate fail (Phase E).
|
|
234
243
|
*/
|
|
235
244
|
export async function runTraverse(projectDir, options = {}) {
|
|
236
245
|
let parallel = Math.max(1, Math.floor(options.parallel ?? 1));
|
|
@@ -250,11 +259,16 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
250
259
|
const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
|
|
251
260
|
const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
|
|
252
261
|
const startUrl = options.startUrl ?? baseURL;
|
|
262
|
+
const minEdgeCoverage = options.minEdgeCoverage != null && Number.isFinite(options.minEdgeCoverage)
|
|
263
|
+
? options.minEdgeCoverage
|
|
264
|
+
: undefined;
|
|
265
|
+
const coverageOut = options.coverageOut ?? defaultCoverageOutPath(projectDir);
|
|
253
266
|
const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
|
|
254
267
|
if (options.blocksSelect) {
|
|
255
268
|
console.error(`waygraph traverse: --blocks ${options.blocksSelect.raw} -> ` +
|
|
256
269
|
`${library.byName.size} block(s), ${graph.edges.length} edge(s)`);
|
|
257
270
|
}
|
|
271
|
+
const allEdgeKeys = collectGraphEdgeKeys(graph.edges);
|
|
258
272
|
const leases = new EdgeLeaseCoordinator({ projectDir, persist: parallel > 1 });
|
|
259
273
|
const engine = new Engine({ headless: !headed, slowMo: headed ? 100 : 0 });
|
|
260
274
|
const { chromium } = await import("@playwright/test");
|
|
@@ -268,7 +282,46 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
268
282
|
console.error(`waygraph traverse: ${graph.nodes.length} node(s), ${graph.edges.length} edge(s)` +
|
|
269
283
|
(baseURL ? ` base=${baseURL}` : "") +
|
|
270
284
|
` parallel=${parallel} session=${session}` +
|
|
271
|
-
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}`
|
|
285
|
+
` maxSteps=${maxSteps} maxVisits/node=${maxVisitsPerNode} maxVisits/edge=${maxVisitsPerEdge}` +
|
|
286
|
+
(minEdgeCoverage !== undefined
|
|
287
|
+
? ` minEdgeCoverage=${(minEdgeCoverage * 100).toFixed(1)}%`
|
|
288
|
+
: ""));
|
|
289
|
+
const emitCoverage = (workers) => {
|
|
290
|
+
const hitUnion = new Set();
|
|
291
|
+
for (const w of workers) {
|
|
292
|
+
for (const k of w.result.edgesHit)
|
|
293
|
+
hitUnion.add(k);
|
|
294
|
+
}
|
|
295
|
+
const report = buildCoverageReport({
|
|
296
|
+
projectDir,
|
|
297
|
+
parallel,
|
|
298
|
+
allEdgeKeys,
|
|
299
|
+
hitKeys: hitUnion,
|
|
300
|
+
...(minEdgeCoverage !== undefined ? { minEdgeCoverage } : {}),
|
|
301
|
+
workers: workers.map(({ traverseId, result }) => ({
|
|
302
|
+
traverseId,
|
|
303
|
+
exitCode: result.exitCode,
|
|
304
|
+
steps: result.steps,
|
|
305
|
+
edgesHit: result.edgesHit.length,
|
|
306
|
+
...(result.leaf ? { leaf: result.leaf } : {}),
|
|
307
|
+
})),
|
|
308
|
+
});
|
|
309
|
+
console.log(formatCoverageLine(report));
|
|
310
|
+
console.error(`waygraph traverse: coverage hit=${report.edgesHit}/${report.edgesTotal}` +
|
|
311
|
+
(report.missed.length
|
|
312
|
+
? ` missed=${report.missed.slice(0, 8).join(",")}${report.missed.length > 8 ? "..." : ""}`
|
|
313
|
+
: "") +
|
|
314
|
+
(options.noCoverageReport ? "" : ` report=${coverageOut}`));
|
|
315
|
+
if (!options.noCoverageReport) {
|
|
316
|
+
writeCoverageReport(coverageOut, report);
|
|
317
|
+
}
|
|
318
|
+
if (report.coveragePass === false) {
|
|
319
|
+
console.error(`waygraph traverse: FAIL coverage gate ratio=${(report.ratio * 100).toFixed(1)}%` +
|
|
320
|
+
` < min=${((report.minEdgeCoverage ?? 0) * 100).toFixed(1)}%`);
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
return 0;
|
|
324
|
+
};
|
|
272
325
|
try {
|
|
273
326
|
if (parallel <= 1) {
|
|
274
327
|
const contextOpts = {
|
|
@@ -284,8 +337,9 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
284
337
|
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
285
338
|
await page.waitForLoadState("load").catch(() => { });
|
|
286
339
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
340
|
+
const traverseId = options.traverseId ?? "traverse-1";
|
|
341
|
+
const result = await runTraverseWorker({
|
|
342
|
+
traverseId,
|
|
289
343
|
workerIndex: 0,
|
|
290
344
|
parallel: 1,
|
|
291
345
|
maxSteps,
|
|
@@ -305,7 +359,10 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
305
359
|
leases,
|
|
306
360
|
});
|
|
307
361
|
await context.close().catch(() => { });
|
|
308
|
-
|
|
362
|
+
const coverageCode = emitCoverage([{ traverseId, result }]);
|
|
363
|
+
if (result.exitCode !== 0)
|
|
364
|
+
return result.exitCode;
|
|
365
|
+
return coverageCode;
|
|
309
366
|
}
|
|
310
367
|
// Phase D: bootstrap one context for storageState, then N clones.
|
|
311
368
|
const bootOpts = {
|
|
@@ -325,7 +382,7 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
325
382
|
const memSnap = snapshotMem(library.byName, bootMem);
|
|
326
383
|
await bootContext.close().catch(() => { });
|
|
327
384
|
console.error(`waygraph traverse: forked ${parallel} clone workers (storageState + mem snapshot)`);
|
|
328
|
-
const
|
|
385
|
+
const workerMeta = await Promise.all(Array.from({ length: parallel }, async (_, i) => {
|
|
329
386
|
const traverseId = `traverse-${i + 1}`;
|
|
330
387
|
const ctxOpts = {
|
|
331
388
|
viewport: { width: 1280, height: 720 },
|
|
@@ -341,7 +398,7 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
341
398
|
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
342
399
|
}
|
|
343
400
|
try {
|
|
344
|
-
|
|
401
|
+
const result = await runTraverseWorker({
|
|
345
402
|
traverseId,
|
|
346
403
|
workerIndex: i,
|
|
347
404
|
parallel,
|
|
@@ -361,15 +418,20 @@ export async function runTraverse(projectDir, options = {}) {
|
|
|
361
418
|
mem,
|
|
362
419
|
leases,
|
|
363
420
|
});
|
|
421
|
+
return { traverseId, result };
|
|
364
422
|
}
|
|
365
423
|
finally {
|
|
366
424
|
await context.close().catch(() => { });
|
|
367
425
|
}
|
|
368
426
|
}));
|
|
369
|
-
const
|
|
370
|
-
const
|
|
427
|
+
const results = workerMeta.map((w) => w.result);
|
|
428
|
+
const worst = results.reduce((a, b) => Math.max(a, b.exitCode), 0);
|
|
429
|
+
const pass = results.filter((c) => c.exitCode === 0).length;
|
|
371
430
|
console.error(`waygraph traverse: suite parallel=${parallel} pass=${pass}/${parallel} exit=${worst}`);
|
|
372
|
-
|
|
431
|
+
const coverageCode = emitCoverage(workerMeta);
|
|
432
|
+
if (worst !== 0)
|
|
433
|
+
return worst;
|
|
434
|
+
return coverageCode;
|
|
373
435
|
}
|
|
374
436
|
finally {
|
|
375
437
|
await browser.close().catch(() => { });
|