thurview 0.7.0 → 0.8.1
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 +34 -15
- package/dist/cli.js +270 -15
- package/dist/cli.js.map +1 -1
- package/dist/coverage.js +208 -0
- package/dist/coverage.js.map +1 -0
- package/dist/document/compile.js +25 -10
- package/dist/document/compile.js.map +1 -1
- package/dist/graph.js +43 -5
- package/dist/graph.js.map +1 -1
- package/dist/server/server.js +4 -2
- package/dist/server/server.js.map +1 -1
- package/dist/store.js +4 -0
- package/dist/store.js.map +1 -1
- package/dist/ui/app.css +184 -0
- package/dist/ui/app.js +455 -54
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +31 -11
- package/skills/thurview/references/code-explainer.md +166 -0
package/dist/ui/app.js
CHANGED
|
@@ -162,6 +162,17 @@
|
|
|
162
162
|
init_dom();
|
|
163
163
|
|
|
164
164
|
// src/ui/state.ts
|
|
165
|
+
var VIEWS = {
|
|
166
|
+
review: ["review", "commits", "files", "map"],
|
|
167
|
+
explainer: ["review", "map", "coverage"]
|
|
168
|
+
};
|
|
169
|
+
function kind() {
|
|
170
|
+
return state.data?.review.kind === "explainer" ? "explainer" : "review";
|
|
171
|
+
}
|
|
172
|
+
function view() {
|
|
173
|
+
const allowed = VIEWS[kind()];
|
|
174
|
+
return allowed.includes(state.view) ? state.view : allowed[0];
|
|
175
|
+
}
|
|
165
176
|
var NARROW = "(max-width: 900px)";
|
|
166
177
|
function isNarrow() {
|
|
167
178
|
return window.matchMedia(NARROW).matches;
|
|
@@ -196,16 +207,16 @@
|
|
|
196
207
|
if (t.type === "map") return `map \xB7 ${t.node}`;
|
|
197
208
|
return "review";
|
|
198
209
|
}
|
|
199
|
-
function navigate(
|
|
210
|
+
function navigate(view2, params = {}) {
|
|
200
211
|
const p = new URLSearchParams();
|
|
201
212
|
for (const [k, v] of Object.entries(params)) if (v !== void 0 && v !== "") p.set(k, String(v));
|
|
202
213
|
const q = p.toString();
|
|
203
|
-
location.hash = `#/${
|
|
214
|
+
location.hash = `#/${view2}${q ? `?${q}` : ""}`;
|
|
204
215
|
}
|
|
205
216
|
function readHash() {
|
|
206
217
|
const m2 = /^#\/(\w+)(?:\?(.*))?$/.exec(location.hash);
|
|
207
|
-
const
|
|
208
|
-
state.view = ["review", "commits", "files", "map"].includes(
|
|
218
|
+
const v = m2?.[1] ?? "review";
|
|
219
|
+
state.view = ["review", "commits", "files", "map", "coverage"].includes(v) ? v : "review";
|
|
209
220
|
state.params = new URLSearchParams(m2?.[2] ?? "");
|
|
210
221
|
}
|
|
211
222
|
function isTerminal() {
|
|
@@ -484,12 +495,14 @@
|
|
|
484
495
|
}
|
|
485
496
|
function submitDialog() {
|
|
486
497
|
const pending = (state.data?.threads ?? []).filter((t) => !t.submitted).length;
|
|
498
|
+
const explainer = kind() === "explainer";
|
|
499
|
+
const words = explainer ? { title: "Send this back or finish", accept: "Done reading", back: "Send it back" } : { title: "Submit review", accept: "Approve", back: "Request changes" };
|
|
487
500
|
const ta = h("textarea", { rows: 4, placeholder: "Summary for the agent (optional)" });
|
|
488
501
|
const d = dialog(
|
|
489
502
|
h(
|
|
490
503
|
"div",
|
|
491
504
|
null,
|
|
492
|
-
h("h3", null,
|
|
505
|
+
h("h3", null, words.title),
|
|
493
506
|
h(
|
|
494
507
|
"p",
|
|
495
508
|
{ class: "muted" },
|
|
@@ -502,11 +515,14 @@
|
|
|
502
515
|
h("button", { class: "ghost", onclick: () => d.close() }, "Cancel"),
|
|
503
516
|
h(
|
|
504
517
|
"button",
|
|
505
|
-
{
|
|
518
|
+
{
|
|
519
|
+
title: explainer ? "End it without reading it through" : "End the review without approving it",
|
|
520
|
+
onclick: () => decide("close")
|
|
521
|
+
},
|
|
506
522
|
"Close"
|
|
507
523
|
),
|
|
508
|
-
h("button", { onclick: () => decide("request-changes") },
|
|
509
|
-
h("button", { class: "ok", onclick: () => decide("approve") },
|
|
524
|
+
h("button", { onclick: () => decide("request-changes") }, words.back),
|
|
525
|
+
h("button", { class: "ok", onclick: () => decide("approve") }, words.accept)
|
|
510
526
|
)
|
|
511
527
|
)
|
|
512
528
|
);
|
|
@@ -788,7 +804,7 @@
|
|
|
788
804
|
const frame = h("div", { class: "code" });
|
|
789
805
|
const draw = () => {
|
|
790
806
|
frame.innerHTML = "";
|
|
791
|
-
const
|
|
807
|
+
const bar2 = (dir) => {
|
|
792
808
|
const can = dir === "up" ? p.from > 1 : p.to < p.total;
|
|
793
809
|
return h(
|
|
794
810
|
"div",
|
|
@@ -800,7 +816,7 @@
|
|
|
800
816
|
) : h("span", { class: "muted" }, dir === "up" ? "start of file" : "end of file")
|
|
801
817
|
);
|
|
802
818
|
};
|
|
803
|
-
frame.appendChild(
|
|
819
|
+
frame.appendChild(bar2("up"));
|
|
804
820
|
frame.appendChild(
|
|
805
821
|
codeTable({
|
|
806
822
|
path: p.path,
|
|
@@ -811,7 +827,7 @@
|
|
|
811
827
|
commentable: true
|
|
812
828
|
})
|
|
813
829
|
);
|
|
814
|
-
frame.appendChild(
|
|
830
|
+
frame.appendChild(bar2("down"));
|
|
815
831
|
};
|
|
816
832
|
const expand = async (dir) => {
|
|
817
833
|
if (dir === "up") {
|
|
@@ -901,12 +917,12 @@
|
|
|
901
917
|
} else {
|
|
902
918
|
g.appendChild(svg("line", { x1, y1: y, x2: x2 + (x2 > x1 ? -4 : 4), y2: y }));
|
|
903
919
|
}
|
|
904
|
-
const
|
|
920
|
+
const label2 = svg(
|
|
905
921
|
"text",
|
|
906
922
|
{ x: self ? x1 + 36 : (x1 + x2) / 2, y: y - 6, "text-anchor": self ? "start" : "middle" },
|
|
907
923
|
`${i + 1}. ${m2.label}`
|
|
908
924
|
);
|
|
909
|
-
|
|
925
|
+
label2.addEventListener("click", (e) => {
|
|
910
926
|
if (m2.anchor) openAnchor(m2.anchor);
|
|
911
927
|
else if (m2.code)
|
|
912
928
|
popover(
|
|
@@ -918,7 +934,7 @@
|
|
|
918
934
|
{ x: e.pageX, y: e.pageY + 8 }
|
|
919
935
|
);
|
|
920
936
|
});
|
|
921
|
-
g.appendChild(
|
|
937
|
+
g.appendChild(label2);
|
|
922
938
|
el.appendChild(g);
|
|
923
939
|
});
|
|
924
940
|
return h("div", { class: "diagram" }, h("div", { class: "dhead" }, b.label), el);
|
|
@@ -1067,7 +1083,9 @@
|
|
|
1067
1083
|
);
|
|
1068
1084
|
return;
|
|
1069
1085
|
}
|
|
1070
|
-
root.appendChild(
|
|
1086
|
+
root.appendChild(
|
|
1087
|
+
kind() === "explainer" ? coveragePanel(state.data?.coverage ?? null) : interfaceDelta(doc.interfaces)
|
|
1088
|
+
);
|
|
1071
1089
|
const layout = h("div", { class: "doc-layout" });
|
|
1072
1090
|
const toc = h(
|
|
1073
1091
|
"nav",
|
|
@@ -1140,6 +1158,7 @@
|
|
|
1140
1158
|
docEl.querySelectorAll(".block.heading").forEach((el) => io.observe(el));
|
|
1141
1159
|
}
|
|
1142
1160
|
var DELTA_BLOCK = "interface-delta";
|
|
1161
|
+
var COVERAGE_BLOCK = "coverage";
|
|
1143
1162
|
var DELTA_SHOWN = 12;
|
|
1144
1163
|
var CHANGE_CLASS = {
|
|
1145
1164
|
removed: "del",
|
|
@@ -1216,6 +1235,71 @@
|
|
|
1216
1235
|
wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
|
|
1217
1236
|
return wrap;
|
|
1218
1237
|
}
|
|
1238
|
+
function coveragePanel(cov) {
|
|
1239
|
+
const wrap = h("div", { class: "block ifd", "data-block": COVERAGE_BLOCK });
|
|
1240
|
+
const threads = threadsFor((t) => t.type === "document" && t.blockId === COVERAGE_BLOCK);
|
|
1241
|
+
if (threads.length) wrap.classList.add("has-threads");
|
|
1242
|
+
const actions = h(
|
|
1243
|
+
"div",
|
|
1244
|
+
{ class: "block-actions" },
|
|
1245
|
+
h(
|
|
1246
|
+
"button",
|
|
1247
|
+
{
|
|
1248
|
+
class: threads.length ? "count" : "",
|
|
1249
|
+
title: "Comment on coverage",
|
|
1250
|
+
onclick: (e) => {
|
|
1251
|
+
if (state.viewingRevision !== null) return;
|
|
1252
|
+
popover(commentPopover({ type: "document", blockId: COVERAGE_BLOCK }), {
|
|
1253
|
+
x: e.pageX + 10,
|
|
1254
|
+
y: e.pageY
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
threads.length ? String(threads.length) : "+"
|
|
1259
|
+
)
|
|
1260
|
+
);
|
|
1261
|
+
const body = h("div", { class: "ifd-body" });
|
|
1262
|
+
const toggle = h("span", { class: "heading-toggle" }, "hide");
|
|
1263
|
+
toggle.addEventListener("click", () => {
|
|
1264
|
+
const hidden = body.hidden;
|
|
1265
|
+
body.hidden = !hidden;
|
|
1266
|
+
toggle.textContent = hidden ? "hide" : "show";
|
|
1267
|
+
});
|
|
1268
|
+
wrap.appendChild(
|
|
1269
|
+
h(
|
|
1270
|
+
"div",
|
|
1271
|
+
{ class: "ifd-head" },
|
|
1272
|
+
h("span", { class: "t" }, "Coverage"),
|
|
1273
|
+
cov ? h("span", { class: "badge" }, `${cov.states.uncovered} not examined`) : null,
|
|
1274
|
+
h("span", { class: "spacer" }),
|
|
1275
|
+
toggle,
|
|
1276
|
+
actions
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
body.appendChild(
|
|
1280
|
+
h(
|
|
1281
|
+
"p",
|
|
1282
|
+
{ class: "ifd-verdict" },
|
|
1283
|
+
cov ? cov.verdict : "Unavailable: coverage could not be derived for this revision."
|
|
1284
|
+
)
|
|
1285
|
+
);
|
|
1286
|
+
body.appendChild(
|
|
1287
|
+
h(
|
|
1288
|
+
"p",
|
|
1289
|
+
{ class: "ifd-verdict" },
|
|
1290
|
+
"A codebase does not fit in one document, so this one selects. ",
|
|
1291
|
+
h(
|
|
1292
|
+
"button",
|
|
1293
|
+
{ class: "small", onclick: () => navigate("coverage") },
|
|
1294
|
+
"See what it left out \u25B8"
|
|
1295
|
+
)
|
|
1296
|
+
)
|
|
1297
|
+
);
|
|
1298
|
+
wrap.appendChild(body);
|
|
1299
|
+
if (threads.length)
|
|
1300
|
+
wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
|
|
1301
|
+
return wrap;
|
|
1302
|
+
}
|
|
1219
1303
|
function interfaceRow(e) {
|
|
1220
1304
|
return h(
|
|
1221
1305
|
"div",
|
|
@@ -1416,15 +1500,15 @@
|
|
|
1416
1500
|
list.appendChild(
|
|
1417
1501
|
h("div", { class: "empty-state" }, "No changed files: base and head are the same commit.")
|
|
1418
1502
|
);
|
|
1419
|
-
const
|
|
1420
|
-
root.appendChild(h("div", { class: "files-layout" }, list,
|
|
1421
|
-
if (path) void renderDiff(
|
|
1503
|
+
const view2 = h("div", { class: "file-view", "data-path": path });
|
|
1504
|
+
root.appendChild(h("div", { class: "files-layout" }, list, view2));
|
|
1505
|
+
if (path) void renderDiff(view2, path);
|
|
1422
1506
|
}
|
|
1423
|
-
async function renderDiff(
|
|
1507
|
+
async function renderDiff(view2, path) {
|
|
1424
1508
|
const inChanges = state.data?.changes.some((c) => c.path === path);
|
|
1425
1509
|
const line = Number(state.params.get("line") ?? 0);
|
|
1426
1510
|
const side2 = state.params.get("side") === "base" ? "base" : "head";
|
|
1427
|
-
|
|
1511
|
+
view2.appendChild(h("div", { class: "muted", style: { padding: "12px" } }, "Loading\u2026"));
|
|
1428
1512
|
let d;
|
|
1429
1513
|
try {
|
|
1430
1514
|
if (inChanges) d = await api.diff(state.id, path);
|
|
@@ -1453,11 +1537,11 @@
|
|
|
1453
1537
|
};
|
|
1454
1538
|
}
|
|
1455
1539
|
} catch (e) {
|
|
1456
|
-
|
|
1457
|
-
|
|
1540
|
+
view2.innerHTML = "";
|
|
1541
|
+
view2.appendChild(h("div", { class: "empty-state" }, e.message));
|
|
1458
1542
|
return;
|
|
1459
1543
|
}
|
|
1460
|
-
|
|
1544
|
+
view2.innerHTML = "";
|
|
1461
1545
|
const split = state.splitDiff && inChanges && !isNarrow();
|
|
1462
1546
|
const toggle = h(
|
|
1463
1547
|
"button",
|
|
@@ -1466,8 +1550,8 @@
|
|
|
1466
1550
|
onclick: () => {
|
|
1467
1551
|
state.splitDiff = !state.splitDiff;
|
|
1468
1552
|
localStorage.setItem("thurview.split", state.splitDiff ? "1" : "0");
|
|
1469
|
-
|
|
1470
|
-
void renderDiff(
|
|
1553
|
+
view2.innerHTML = "";
|
|
1554
|
+
void renderDiff(view2, path);
|
|
1471
1555
|
}
|
|
1472
1556
|
},
|
|
1473
1557
|
state.splitDiff ? "Unified" : "Split"
|
|
@@ -1475,7 +1559,7 @@
|
|
|
1475
1559
|
const fileThreads = (state.data?.threads ?? []).filter(
|
|
1476
1560
|
(t) => t.target.type === "file" && t.target.path === path && t.target.line === 0
|
|
1477
1561
|
);
|
|
1478
|
-
|
|
1562
|
+
view2.appendChild(
|
|
1479
1563
|
h(
|
|
1480
1564
|
"div",
|
|
1481
1565
|
{ class: "file-head" },
|
|
@@ -1502,7 +1586,7 @@
|
|
|
1502
1586
|
)
|
|
1503
1587
|
);
|
|
1504
1588
|
if (fileThreads.length)
|
|
1505
|
-
|
|
1589
|
+
view2.appendChild(
|
|
1506
1590
|
h(
|
|
1507
1591
|
"div",
|
|
1508
1592
|
{ class: "thread-pins", style: { margin: "8px 14px" } },
|
|
@@ -1510,15 +1594,15 @@
|
|
|
1510
1594
|
)
|
|
1511
1595
|
);
|
|
1512
1596
|
if (d.binary) {
|
|
1513
|
-
|
|
1597
|
+
view2.appendChild(h("div", { class: "empty-state" }, "Binary file"));
|
|
1514
1598
|
return;
|
|
1515
1599
|
}
|
|
1516
1600
|
const threads = (state.data?.threads ?? []).filter(
|
|
1517
1601
|
(t) => t.target.type === "file" && t.target.path === path
|
|
1518
1602
|
);
|
|
1519
1603
|
const el = split ? splitView(d, threads) : unifiedView(d, threads);
|
|
1520
|
-
|
|
1521
|
-
codeSelectionHandler(
|
|
1604
|
+
view2.appendChild(el);
|
|
1605
|
+
codeSelectionHandler(view2);
|
|
1522
1606
|
if (line) {
|
|
1523
1607
|
const tr = el.querySelector(`tr[data-${side2}="${line}"]`);
|
|
1524
1608
|
if (tr) {
|
|
@@ -1878,9 +1962,21 @@
|
|
|
1878
1962
|
var statusClass = (s) => s === "added" ? "ok" : s === "removed" ? "del" : "warn";
|
|
1879
1963
|
function renderMap(root) {
|
|
1880
1964
|
const map = state.data?.map;
|
|
1965
|
+
const explainer = kind() === "explainer";
|
|
1881
1966
|
if (!map || !map.head.nodes.length) {
|
|
1882
1967
|
append(root, [
|
|
1883
|
-
h(
|
|
1968
|
+
explainer ? h(
|
|
1969
|
+
"div",
|
|
1970
|
+
{ class: "empty-state" },
|
|
1971
|
+
h("p", null, "No map in this explainer."),
|
|
1972
|
+
h(
|
|
1973
|
+
"p",
|
|
1974
|
+
null,
|
|
1975
|
+
"The map is where an explainer carries breadth: it places the parts the prose ",
|
|
1976
|
+
"had no room for. Without one, every file the document does not anchor counts ",
|
|
1977
|
+
"as not examined on the Coverage tab."
|
|
1978
|
+
)
|
|
1979
|
+
) : h(
|
|
1884
1980
|
"div",
|
|
1885
1981
|
{ class: "empty-state" },
|
|
1886
1982
|
h("p", null, "No map in this review."),
|
|
@@ -1909,7 +2005,29 @@
|
|
|
1909
2005
|
level = id;
|
|
1910
2006
|
draw();
|
|
1911
2007
|
};
|
|
1912
|
-
const
|
|
2008
|
+
const intro2 = () => {
|
|
2009
|
+
if (explainer)
|
|
2010
|
+
return h(
|
|
2011
|
+
"div",
|
|
2012
|
+
{ class: "map-intro" },
|
|
2013
|
+
h("h2", null, "The parts of the system"),
|
|
2014
|
+
h(
|
|
2015
|
+
"p",
|
|
2016
|
+
null,
|
|
2017
|
+
"The document carries depth on the parts it selected; this carries breadth over the",
|
|
2018
|
+
" rest \u2014 what the parts are, what they own, and what they connect to at the pinned",
|
|
2019
|
+
" commit. Open a part to see its files, its code and its neighbours."
|
|
2020
|
+
),
|
|
2021
|
+
h(
|
|
2022
|
+
"div",
|
|
2023
|
+
{ class: "map-route" },
|
|
2024
|
+
h(
|
|
2025
|
+
"button",
|
|
2026
|
+
{ class: "small", onclick: () => navigate("coverage") },
|
|
2027
|
+
"What this explainer left out \u25B8"
|
|
2028
|
+
)
|
|
2029
|
+
)
|
|
2030
|
+
);
|
|
1913
2031
|
const counts = [
|
|
1914
2032
|
[map.diff.added.length, "ok", "added"],
|
|
1915
2033
|
[map.diff.changed.length, "warn", "changed"],
|
|
@@ -1965,7 +2083,7 @@
|
|
|
1965
2083
|
return h(
|
|
1966
2084
|
"div",
|
|
1967
2085
|
{
|
|
1968
|
-
class: `map-node ${n.status} ${touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
|
|
2086
|
+
class: `map-node ${n.status} ${explainer || touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
|
|
1969
2087
|
onclick: () => navigate("map", { node: n.id }),
|
|
1970
2088
|
ondblclick: () => inside && openLevel(n.id)
|
|
1971
2089
|
},
|
|
@@ -2003,6 +2121,7 @@
|
|
|
2003
2121
|
title ? h("h3", null, title) : null,
|
|
2004
2122
|
h("div", { class: "map-grid" }, items.map(card))
|
|
2005
2123
|
) : null;
|
|
2124
|
+
if (explainer) return group(null, kids) ?? h("div", null);
|
|
2006
2125
|
if (!hot.length)
|
|
2007
2126
|
return h(
|
|
2008
2127
|
"div",
|
|
@@ -2017,7 +2136,7 @@
|
|
|
2017
2136
|
group("Context \u2014 unchanged here", cold)
|
|
2018
2137
|
);
|
|
2019
2138
|
};
|
|
2020
|
-
const
|
|
2139
|
+
const links2 = () => {
|
|
2021
2140
|
const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
|
|
2022
2141
|
const top = (id) => {
|
|
2023
2142
|
let x = id;
|
|
@@ -2041,7 +2160,7 @@
|
|
|
2041
2160
|
h(
|
|
2042
2161
|
"div",
|
|
2043
2162
|
{ class: "muted map-note" },
|
|
2044
|
-
"A link touching a changed part is where the two sides can fall out of step."
|
|
2163
|
+
explainer ? "A link is a dependency the parts have on each other, drawn as the author declared it." : "A link touching a changed part is where the two sides can fall out of step."
|
|
2045
2164
|
),
|
|
2046
2165
|
rankEdges(folded, all).map(
|
|
2047
2166
|
({ edge, touchesChange }) => h(
|
|
@@ -2065,7 +2184,7 @@
|
|
|
2065
2184
|
};
|
|
2066
2185
|
const draw = () => {
|
|
2067
2186
|
canvas.innerHTML = "";
|
|
2068
|
-
append(canvas, [
|
|
2187
|
+
append(canvas, [intro2(), crumbs(), board(), links2()]);
|
|
2069
2188
|
drawSide();
|
|
2070
2189
|
};
|
|
2071
2190
|
const drawSide = () => {
|
|
@@ -2076,7 +2195,7 @@
|
|
|
2076
2195
|
h(
|
|
2077
2196
|
"div",
|
|
2078
2197
|
{ class: "muted" },
|
|
2079
|
-
"Pick a part to see what changed in it, the code behind it, and what it connects to."
|
|
2198
|
+
explainer ? "Pick a part to see what it owns, the code behind it, and what it connects to." : "Pick a part to see what changed in it, the code behind it, and what it connects to."
|
|
2080
2199
|
)
|
|
2081
2200
|
);
|
|
2082
2201
|
return;
|
|
@@ -2182,12 +2301,282 @@
|
|
|
2182
2301
|
draw();
|
|
2183
2302
|
}
|
|
2184
2303
|
|
|
2304
|
+
// src/ui/views/coverage.ts
|
|
2305
|
+
init_dom();
|
|
2306
|
+
var SHOWN = 8;
|
|
2307
|
+
function renderCoverage(root) {
|
|
2308
|
+
const cov = state.data?.coverage;
|
|
2309
|
+
if (!cov) {
|
|
2310
|
+
append(root, [
|
|
2311
|
+
h(
|
|
2312
|
+
"div",
|
|
2313
|
+
{ class: "empty-state" },
|
|
2314
|
+
h("p", null, "No coverage recorded for this revision."),
|
|
2315
|
+
h("p", null, "Coverage is derived when an explainer is published.")
|
|
2316
|
+
)
|
|
2317
|
+
]);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
append(root, [intro(cov), clusters(cov), links(cov), sharedNames(cov), unread(cov)]);
|
|
2321
|
+
}
|
|
2322
|
+
function bar(cov) {
|
|
2323
|
+
const total = Math.max(cov.files.total, 1);
|
|
2324
|
+
const seg = (n, cls, label2) => n ? h("span", {
|
|
2325
|
+
class: `cov-seg ${cls}`,
|
|
2326
|
+
style: { width: `${n / total * 100}%` },
|
|
2327
|
+
title: `${n} ${label2}`
|
|
2328
|
+
}) : null;
|
|
2329
|
+
return h(
|
|
2330
|
+
"div",
|
|
2331
|
+
{ class: "cov-bar" },
|
|
2332
|
+
seg(cov.states.explained, "explained", "anchored in the document"),
|
|
2333
|
+
seg(cov.states.placed, "placed", "placed on the map only"),
|
|
2334
|
+
seg(cov.states.uncovered, "uncovered", "not examined")
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
function intro(cov) {
|
|
2338
|
+
const key = [
|
|
2339
|
+
[cov.states.explained, "explained", "anchored in the document"],
|
|
2340
|
+
[cov.states.placed, "placed", "placed on the map only"],
|
|
2341
|
+
[cov.states.uncovered, "uncovered", "not examined"]
|
|
2342
|
+
];
|
|
2343
|
+
return h(
|
|
2344
|
+
"div",
|
|
2345
|
+
{ class: "cov-intro" },
|
|
2346
|
+
h("h2", null, "What this explainer covers"),
|
|
2347
|
+
h(
|
|
2348
|
+
"p",
|
|
2349
|
+
null,
|
|
2350
|
+
"A codebase does not fit in a short document, so this one selects. Here is the",
|
|
2351
|
+
" selection, counted rather than claimed: every file in scope at the pinned commit,",
|
|
2352
|
+
" and which of the three states it is in."
|
|
2353
|
+
),
|
|
2354
|
+
bar(cov),
|
|
2355
|
+
h(
|
|
2356
|
+
"div",
|
|
2357
|
+
{ class: "cov-key" },
|
|
2358
|
+
key.map(
|
|
2359
|
+
([n, cls, label2]) => h("span", { class: "cov-key-item" }, h("i", { class: `cov-dot ${cls}` }), `${n} ${label2}`)
|
|
2360
|
+
)
|
|
2361
|
+
),
|
|
2362
|
+
h(
|
|
2363
|
+
"div",
|
|
2364
|
+
{ class: "cov-facts" },
|
|
2365
|
+
fact("scope", cov.scope === "**" ? "the whole repository" : cov.scope),
|
|
2366
|
+
fact("commit", cov.commit.slice(0, 12)),
|
|
2367
|
+
fact("files in scope", String(cov.files.total)),
|
|
2368
|
+
fact("read by the code graph", `${cov.files.inGraph} of ${cov.files.total}`),
|
|
2369
|
+
cov.unresolved ? fact("references the graph could not place", String(cov.unresolved)) : null,
|
|
2370
|
+
cov.truncated ? fact("file list capped", "the graph is partial; treat every count as a floor") : null
|
|
2371
|
+
)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
function fact(label2, value) {
|
|
2375
|
+
return h("div", { class: "cov-fact" }, h("b", null, value), h("span", { class: "muted" }, label2));
|
|
2376
|
+
}
|
|
2377
|
+
function fileList(files, cls) {
|
|
2378
|
+
if (!files.length) return null;
|
|
2379
|
+
const wrap = h("div", { class: `cov-files ${cls}` });
|
|
2380
|
+
const chip = (f) => h("code", { class: "cov-file", title: f }, f.split("/").slice(-2).join("/"));
|
|
2381
|
+
files.slice(0, SHOWN).forEach((f) => wrap.appendChild(chip(f)));
|
|
2382
|
+
const rest = files.slice(SHOWN);
|
|
2383
|
+
if (rest.length) {
|
|
2384
|
+
const more = h(
|
|
2385
|
+
"button",
|
|
2386
|
+
{
|
|
2387
|
+
class: "small ghost",
|
|
2388
|
+
onclick: () => more.replaceWith(...rest.map(chip))
|
|
2389
|
+
},
|
|
2390
|
+
`show ${rest.length} more`
|
|
2391
|
+
);
|
|
2392
|
+
wrap.appendChild(more);
|
|
2393
|
+
}
|
|
2394
|
+
return wrap;
|
|
2395
|
+
}
|
|
2396
|
+
function clusterRow(c) {
|
|
2397
|
+
const anchors = state.data?.document?.anchors ?? {};
|
|
2398
|
+
const firstAnchor = c.explained.length ? Object.values(anchors).find((a) => a.peek && c.explained.includes(a.peek.file)) : void 0;
|
|
2399
|
+
return h(
|
|
2400
|
+
"div",
|
|
2401
|
+
{ class: "cov-cluster" },
|
|
2402
|
+
h(
|
|
2403
|
+
"div",
|
|
2404
|
+
{ class: "cov-cluster-head" },
|
|
2405
|
+
h("code", { class: "cov-label" }, c.label),
|
|
2406
|
+
h("span", { class: "muted" }, `${c.files} files \xB7 ${c.symbols} symbols`),
|
|
2407
|
+
h("span", { class: "spacer" }),
|
|
2408
|
+
c.explained.length ? h("span", { class: "badge cov-b-explained" }, `${c.explained.length} anchored`) : null,
|
|
2409
|
+
c.placed.length ? h("span", { class: "badge cov-b-placed" }, `${c.placed.length} placed`) : null,
|
|
2410
|
+
c.uncovered.length ? h("span", { class: "badge cov-b-uncovered" }, `${c.uncovered.length} not examined`) : null,
|
|
2411
|
+
firstAnchor ? h("button", { class: "small", onclick: () => openAnchorPeek(firstAnchor) }, "Peek code \u25B8") : null
|
|
2412
|
+
),
|
|
2413
|
+
c.hubs.length ? h(
|
|
2414
|
+
"div",
|
|
2415
|
+
{ class: "muted cov-hubs" },
|
|
2416
|
+
"most referenced here: ",
|
|
2417
|
+
c.hubs.map((n) => h("code", null, n))
|
|
2418
|
+
) : null,
|
|
2419
|
+
fileList(c.uncovered, "uncovered")
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
function clusters(cov) {
|
|
2423
|
+
return h(
|
|
2424
|
+
"div",
|
|
2425
|
+
{ class: "cov-section" },
|
|
2426
|
+
h("h3", null, "By part of the system"),
|
|
2427
|
+
h(
|
|
2428
|
+
"p",
|
|
2429
|
+
{ class: "muted" },
|
|
2430
|
+
"Files clustered by how they reference each other at the pinned commit, largest first.",
|
|
2431
|
+
" The files listed under a part are the ones this explainer never examined."
|
|
2432
|
+
),
|
|
2433
|
+
cov.clusters.map(clusterRow)
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
function links(cov) {
|
|
2437
|
+
if (!cov.links.length) return null;
|
|
2438
|
+
return h(
|
|
2439
|
+
"div",
|
|
2440
|
+
{ class: "cov-section" },
|
|
2441
|
+
h("h3", null, "What reaches what"),
|
|
2442
|
+
h(
|
|
2443
|
+
"p",
|
|
2444
|
+
{ class: "muted" },
|
|
2445
|
+
"References that cross from one part to another, with how many there are.",
|
|
2446
|
+
" A pair marked both ways references each other in both directions."
|
|
2447
|
+
),
|
|
2448
|
+
h(
|
|
2449
|
+
"div",
|
|
2450
|
+
{ class: "cov-links" },
|
|
2451
|
+
cov.links.slice(0, 24).map(
|
|
2452
|
+
(l) => h(
|
|
2453
|
+
"div",
|
|
2454
|
+
{ class: "cov-link" },
|
|
2455
|
+
h("code", null, label(cov, l.from)),
|
|
2456
|
+
h("span", { class: "muted" }, "\u2192"),
|
|
2457
|
+
h("code", null, label(cov, l.to)),
|
|
2458
|
+
h("span", { class: "muted mono" }, `${l.references} refs`),
|
|
2459
|
+
l.bothWays ? h("span", { class: "badge" }, "both ways") : null
|
|
2460
|
+
)
|
|
2461
|
+
)
|
|
2462
|
+
)
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
function label(cov, id) {
|
|
2466
|
+
return cov.clusters.find((c) => c.id === id)?.label ?? id;
|
|
2467
|
+
}
|
|
2468
|
+
function sharedNames(cov) {
|
|
2469
|
+
if (!cov.sharedNames.length) return null;
|
|
2470
|
+
return h(
|
|
2471
|
+
"div",
|
|
2472
|
+
{ class: "cov-section" },
|
|
2473
|
+
h("h3", null, "Names defined in more than one part"),
|
|
2474
|
+
h(
|
|
2475
|
+
"p",
|
|
2476
|
+
{ class: "muted" },
|
|
2477
|
+
`${cov.sharedNamesTotal} name${cov.sharedNamesTotal === 1 ? " at this commit is" : "s at this commit are"}`,
|
|
2478
|
+
" defined in two or more parts. What that means here - the same idea in two places,",
|
|
2479
|
+
" two different ideas sharing a word, or a name too common to mean anything - is",
|
|
2480
|
+
" what the code says and this page does not."
|
|
2481
|
+
),
|
|
2482
|
+
h(
|
|
2483
|
+
"div",
|
|
2484
|
+
{ class: "cov-shared" },
|
|
2485
|
+
cov.sharedNames.map(
|
|
2486
|
+
(n) => h(
|
|
2487
|
+
"div",
|
|
2488
|
+
{ class: "cov-link" },
|
|
2489
|
+
h("code", null, n.name),
|
|
2490
|
+
h("span", { class: "muted mono" }, `${n.clusters.length} parts`),
|
|
2491
|
+
h("span", { class: "muted" }, n.files.join(" \xB7 "))
|
|
2492
|
+
)
|
|
2493
|
+
)
|
|
2494
|
+
)
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
function unread(cov) {
|
|
2498
|
+
if (!cov.outsideGraph.length && !cov.files.capped && !cov.owners.length) return null;
|
|
2499
|
+
return h(
|
|
2500
|
+
"div",
|
|
2501
|
+
{ class: "cov-section" },
|
|
2502
|
+
h("h3", null, "Outside the code graph"),
|
|
2503
|
+
cov.outsideGraph.length ? h(
|
|
2504
|
+
"div",
|
|
2505
|
+
null,
|
|
2506
|
+
h(
|
|
2507
|
+
"p",
|
|
2508
|
+
{ class: "muted" },
|
|
2509
|
+
`${cov.files.outsideGraph} files in scope are not in a language the graph reads,`,
|
|
2510
|
+
" so they are in no part above. They are absent from the structure, not empty."
|
|
2511
|
+
),
|
|
2512
|
+
h(
|
|
2513
|
+
"div",
|
|
2514
|
+
{ class: "cov-links" },
|
|
2515
|
+
cov.outsideGraph.slice(0, 16).map(
|
|
2516
|
+
(e) => h(
|
|
2517
|
+
"div",
|
|
2518
|
+
{ class: "cov-link" },
|
|
2519
|
+
h("code", null, `.${e.extension}`),
|
|
2520
|
+
h("span", { class: "muted mono" }, `${e.files} files`)
|
|
2521
|
+
)
|
|
2522
|
+
)
|
|
2523
|
+
),
|
|
2524
|
+
fileList(
|
|
2525
|
+
cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "outsideGraph").map((u) => u.file),
|
|
2526
|
+
"uncovered"
|
|
2527
|
+
)
|
|
2528
|
+
) : null,
|
|
2529
|
+
cov.files.capped ? h(
|
|
2530
|
+
"div",
|
|
2531
|
+
null,
|
|
2532
|
+
h(
|
|
2533
|
+
"p",
|
|
2534
|
+
{ class: "muted" },
|
|
2535
|
+
`${cov.files.capped} files in scope are in a language the graph reads, but the`,
|
|
2536
|
+
" repo-wide file cap was hit before this scope was read, so they were never parsed."
|
|
2537
|
+
),
|
|
2538
|
+
fileList(
|
|
2539
|
+
cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "capped").map((u) => u.file),
|
|
2540
|
+
"uncovered"
|
|
2541
|
+
)
|
|
2542
|
+
) : null,
|
|
2543
|
+
cov.owners.length ? h(
|
|
2544
|
+
"div",
|
|
2545
|
+
null,
|
|
2546
|
+
h("h4", null, "What each map node owns"),
|
|
2547
|
+
h(
|
|
2548
|
+
"p",
|
|
2549
|
+
{ class: "muted" },
|
|
2550
|
+
"A file counts as placed because a map node's globs match it. The globs are here",
|
|
2551
|
+
" so a broad one is visible rather than silently inflating the count."
|
|
2552
|
+
),
|
|
2553
|
+
h(
|
|
2554
|
+
"div",
|
|
2555
|
+
{ class: "cov-links" },
|
|
2556
|
+
cov.owners.map(
|
|
2557
|
+
(o) => h(
|
|
2558
|
+
"div",
|
|
2559
|
+
{
|
|
2560
|
+
class: "cov-link",
|
|
2561
|
+
onclick: () => navigate("map", { node: o.node }),
|
|
2562
|
+
style: { cursor: "pointer" }
|
|
2563
|
+
},
|
|
2564
|
+
h("code", null, o.node),
|
|
2565
|
+
h("span", { class: "muted" }, o.globs.join(" ")),
|
|
2566
|
+
h("span", { class: "muted mono" }, `${o.files} files`)
|
|
2567
|
+
)
|
|
2568
|
+
)
|
|
2569
|
+
)
|
|
2570
|
+
) : null
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2185
2574
|
// src/ui/app.ts
|
|
2186
2575
|
var app = document.getElementById("app");
|
|
2187
2576
|
async function home() {
|
|
2188
2577
|
clear(app);
|
|
2189
2578
|
const reviews = await api.reviews();
|
|
2190
|
-
const el = h("div", { class: "home" }, h("h2", null, "Reviews"));
|
|
2579
|
+
const el = h("div", { class: "home" }, h("h2", null, "Reviews and explainers"));
|
|
2191
2580
|
const active = reviews.filter((r) => !r.dismissed);
|
|
2192
2581
|
const dismissed = reviews.filter((r) => r.dismissed);
|
|
2193
2582
|
const item = (r) => h(
|
|
@@ -2196,16 +2585,16 @@
|
|
|
2196
2585
|
h("span", { class: "t" }, r.title),
|
|
2197
2586
|
h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
|
|
2198
2587
|
r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
|
|
2199
|
-
h(
|
|
2200
|
-
"span",
|
|
2201
|
-
{ class: "muted mono", style: { fontSize: "12px" } },
|
|
2202
|
-
r.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name
|
|
2203
|
-
),
|
|
2588
|
+
h("span", { class: "muted mono", style: { fontSize: "12px" } }, bindingLabel(r)),
|
|
2204
2589
|
h("span", { class: "muted", style: { fontSize: "12px" } }, timeAgo(r.updatedAt))
|
|
2205
2590
|
);
|
|
2206
2591
|
if (!active.length)
|
|
2207
2592
|
el.appendChild(
|
|
2208
|
-
h(
|
|
2593
|
+
h(
|
|
2594
|
+
"div",
|
|
2595
|
+
{ class: "empty-state" },
|
|
2596
|
+
"Nothing published yet. Ask your agent for a review of a change, or an explainer of the codebase."
|
|
2597
|
+
)
|
|
2209
2598
|
);
|
|
2210
2599
|
active.forEach((r) => el.appendChild(item(r)));
|
|
2211
2600
|
if (dismissed.length) {
|
|
@@ -2214,6 +2603,11 @@
|
|
|
2214
2603
|
}
|
|
2215
2604
|
app.appendChild(el);
|
|
2216
2605
|
}
|
|
2606
|
+
function bindingLabel(r) {
|
|
2607
|
+
if (r.binding.kind === "pr") return `PR #${r.binding.name}`;
|
|
2608
|
+
if (r.binding.kind === "codebase") return r.binding.name === "**" ? "codebase" : r.binding.name;
|
|
2609
|
+
return r.binding.name;
|
|
2610
|
+
}
|
|
2217
2611
|
function statusClass2(s) {
|
|
2218
2612
|
return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
|
|
2219
2613
|
}
|
|
@@ -2253,12 +2647,16 @@
|
|
|
2253
2647
|
const r = d.review;
|
|
2254
2648
|
const pending = d.threads.filter((t) => !t.submitted).length;
|
|
2255
2649
|
const open = d.threads.filter((t) => t.status === "open").length;
|
|
2256
|
-
const
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2650
|
+
const explainer = kind() === "explainer";
|
|
2651
|
+
const labels = {
|
|
2652
|
+
review: explainer ? "Explainer" : "Review",
|
|
2653
|
+
commits: "Commits",
|
|
2654
|
+
files: `Files${d.changes.length ? ` (${d.changes.length})` : ""}`,
|
|
2655
|
+
map: "Map",
|
|
2656
|
+
coverage: "Coverage"
|
|
2657
|
+
};
|
|
2658
|
+
const current2 = view();
|
|
2659
|
+
const tabs = VIEWS[kind()].map((v) => [v, labels[v]]);
|
|
2262
2660
|
const revSel = h("select", {
|
|
2263
2661
|
class: "small",
|
|
2264
2662
|
style: { font: "inherit", fontSize: "12px" },
|
|
@@ -2289,7 +2687,7 @@
|
|
|
2289
2687
|
style: { fontSize: "12px" },
|
|
2290
2688
|
title: `${r.pins.base} \u2192 ${r.pins.head}`
|
|
2291
2689
|
},
|
|
2292
|
-
r
|
|
2690
|
+
bindingLabel(r)
|
|
2293
2691
|
)
|
|
2294
2692
|
]);
|
|
2295
2693
|
const actions = h("div", { class: "bar-row bar-actions" }, [
|
|
@@ -2297,7 +2695,7 @@
|
|
|
2297
2695
|
"div",
|
|
2298
2696
|
{ class: "tabs" },
|
|
2299
2697
|
tabs.map(
|
|
2300
|
-
([v,
|
|
2698
|
+
([v, label2]) => h("button", { class: v === current2 ? "active" : "", onclick: () => navigate(v) }, label2)
|
|
2301
2699
|
)
|
|
2302
2700
|
),
|
|
2303
2701
|
h("span", { class: "spacer" }),
|
|
@@ -2382,7 +2780,7 @@
|
|
|
2382
2780
|
h(
|
|
2383
2781
|
"div",
|
|
2384
2782
|
{ class: "item muted" },
|
|
2385
|
-
`base ${r.pins.base.slice(0, 12)} \xB7 head ${r.pins.head.slice(0, 12)}`
|
|
2783
|
+
kind() === "explainer" ? `commit ${r.pins.head.slice(0, 12)}` : `base ${r.pins.base.slice(0, 12)} \xB7 head ${r.pins.head.slice(0, 12)}`
|
|
2386
2784
|
),
|
|
2387
2785
|
h(
|
|
2388
2786
|
"div",
|
|
@@ -2395,7 +2793,7 @@
|
|
|
2395
2793
|
function renderCenter() {
|
|
2396
2794
|
const scroll = center.scrollTop;
|
|
2397
2795
|
clear(center);
|
|
2398
|
-
switch (
|
|
2796
|
+
switch (view()) {
|
|
2399
2797
|
case "review":
|
|
2400
2798
|
renderReview(center);
|
|
2401
2799
|
break;
|
|
@@ -2408,6 +2806,9 @@
|
|
|
2408
2806
|
case "map":
|
|
2409
2807
|
renderMap(center);
|
|
2410
2808
|
break;
|
|
2809
|
+
case "coverage":
|
|
2810
|
+
renderCoverage(center);
|
|
2811
|
+
break;
|
|
2411
2812
|
}
|
|
2412
2813
|
center.scrollTop = scroll;
|
|
2413
2814
|
}
|