thurview 0.6.0 → 0.8.0
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 +38 -18
- package/dist/cli.js +273 -16
- 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 +25 -4
- 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 +282 -5
- package/dist/ui/app.js +718 -190
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +43 -15
- package/skills/thurview/references/code-explainer.md +166 -0
- package/skills/thurview/references/software-map.md +71 -10
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);
|
|
@@ -970,12 +986,12 @@
|
|
|
970
986
|
);
|
|
971
987
|
const uc = b.usecases[active];
|
|
972
988
|
body.innerHTML = "";
|
|
973
|
-
const
|
|
989
|
+
const touched2 = /* @__PURE__ */ new Map();
|
|
974
990
|
for (const op of uc.ops) {
|
|
975
991
|
const [s, c, f] = op.store.split(".");
|
|
976
992
|
const key = `${s}.${c}`;
|
|
977
|
-
if (!
|
|
978
|
-
if (f)
|
|
993
|
+
if (!touched2.has(key)) touched2.set(key, /* @__PURE__ */ new Set());
|
|
994
|
+
if (f) touched2.get(key).add(f);
|
|
979
995
|
}
|
|
980
996
|
const actorIds = [...new Set(uc.ops.map((o) => o.actor))];
|
|
981
997
|
body.appendChild(
|
|
@@ -996,7 +1012,7 @@
|
|
|
996
1012
|
{ class: "store" },
|
|
997
1013
|
h("div", { class: "sname" }, s.label, " ", h("span", { class: "badge" }, s.kind)),
|
|
998
1014
|
Object.entries(colls).map(([cid, c]) => {
|
|
999
|
-
const hit =
|
|
1015
|
+
const hit = touched2.get(`${sid}.${cid}`);
|
|
1000
1016
|
return h(
|
|
1001
1017
|
"div",
|
|
1002
1018
|
{ class: `table ${hit ? "hit" : ""}` },
|
|
@@ -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) {
|
|
@@ -1797,22 +1881,12 @@
|
|
|
1797
1881
|
|
|
1798
1882
|
// src/ui/views/map.ts
|
|
1799
1883
|
init_dom();
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
{ class: "empty-state" },
|
|
1807
|
-
"No software map in this revision. The agent authors ",
|
|
1808
|
-
h("code", null, "map.yaml"),
|
|
1809
|
-
" and publishes again."
|
|
1810
|
-
)
|
|
1811
|
-
);
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
|
|
1815
|
-
const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
|
|
1884
|
+
|
|
1885
|
+
// src/ui/views/map-model.ts
|
|
1886
|
+
function parentOf(id) {
|
|
1887
|
+
return id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
|
|
1888
|
+
}
|
|
1889
|
+
function collect(map) {
|
|
1816
1890
|
const all = /* @__PURE__ */ new Map();
|
|
1817
1891
|
for (const n of map.head.nodes)
|
|
1818
1892
|
all.set(n.id, {
|
|
@@ -1820,145 +1894,297 @@
|
|
|
1820
1894
|
status: map.diff.added.includes(n.id) ? "added" : map.diff.changed.includes(n.id) ? "changed" : ""
|
|
1821
1895
|
});
|
|
1822
1896
|
for (const id of map.diff.removed) {
|
|
1823
|
-
const n =
|
|
1897
|
+
const n = (map.base?.nodes ?? []).find((x) => x.id === id);
|
|
1824
1898
|
if (n) all.set(id, { ...n, status: "removed" });
|
|
1825
1899
|
}
|
|
1900
|
+
return all;
|
|
1901
|
+
}
|
|
1902
|
+
function changedInside(all, id) {
|
|
1903
|
+
let n = 0;
|
|
1904
|
+
for (const item of all.values()) if (item.id.startsWith(id + ".") && item.status) n++;
|
|
1905
|
+
return n;
|
|
1906
|
+
}
|
|
1907
|
+
function touched(all, item) {
|
|
1908
|
+
return !!item.status || changedInside(all, item.id) > 0;
|
|
1909
|
+
}
|
|
1910
|
+
function childrenOf(all, parent, filesByNode) {
|
|
1911
|
+
const kids = [...all.values()].filter((n) => parentOf(n.id) === parent);
|
|
1912
|
+
const weight = (n) => (filesByNode[n.id]?.length ?? 0) + changedInside(all, n.id);
|
|
1913
|
+
return kids.map((n, i) => ({ n, i })).sort((a, b) => {
|
|
1914
|
+
const t = Number(touched(all, b.n)) - Number(touched(all, a.n));
|
|
1915
|
+
if (t) return t;
|
|
1916
|
+
const w = weight(b.n) - weight(a.n);
|
|
1917
|
+
return w || a.i - b.i;
|
|
1918
|
+
}).map((x) => x.n);
|
|
1919
|
+
}
|
|
1920
|
+
function routeIn(all, filesByNode) {
|
|
1921
|
+
const changed = [...all.values()].filter((n) => n.status);
|
|
1922
|
+
const hasChild = (id) => [...all.keys()].some((k) => parentOf(k) === id);
|
|
1923
|
+
const leaves = changed.filter((n) => !hasChild(n.id));
|
|
1924
|
+
const pool = leaves.length ? leaves : changed;
|
|
1925
|
+
let best = null;
|
|
1926
|
+
let bestFiles = -1;
|
|
1927
|
+
for (const n of pool) {
|
|
1928
|
+
const files = filesByNode[n.id]?.length ?? 0;
|
|
1929
|
+
if (files > bestFiles) {
|
|
1930
|
+
best = n;
|
|
1931
|
+
bestFiles = files;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return best;
|
|
1935
|
+
}
|
|
1936
|
+
function neighboursOf(map, id) {
|
|
1937
|
+
const out = [];
|
|
1938
|
+
const into = [];
|
|
1939
|
+
for (const e of allEdges(map)) {
|
|
1940
|
+
if (e.from === id)
|
|
1941
|
+
out.push({ dir: "out", other: e.to, ...e.label ? { label: e.label } : {} });
|
|
1942
|
+
if (e.to === id)
|
|
1943
|
+
into.push({ dir: "in", other: e.from, ...e.label ? { label: e.label } : {} });
|
|
1944
|
+
}
|
|
1945
|
+
return [...out, ...into];
|
|
1946
|
+
}
|
|
1947
|
+
function allEdges(map) {
|
|
1826
1948
|
const edges = [...map.head.edges];
|
|
1827
1949
|
for (const e of map.base?.edges ?? [])
|
|
1828
1950
|
if (!edges.some((x) => x.from === e.from && x.to === e.to)) edges.push(e);
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1951
|
+
return edges;
|
|
1952
|
+
}
|
|
1953
|
+
function rankEdges(edges, all) {
|
|
1954
|
+
const hot = (id) => {
|
|
1955
|
+
const item = all.get(id);
|
|
1956
|
+
return item && touched(all, item) ? 1 : 0;
|
|
1957
|
+
};
|
|
1958
|
+
return edges.map((edge, i) => ({ edge, i, ends: hot(edge.from) + hot(edge.to) })).sort((a, b) => b.ends - a.ends || a.i - b.i).map(({ edge, ends }) => ({ edge, touchesChange: ends > 0 }));
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// src/ui/views/map.ts
|
|
1962
|
+
var statusClass = (s) => s === "added" ? "ok" : s === "removed" ? "del" : "warn";
|
|
1963
|
+
function renderMap(root) {
|
|
1964
|
+
const map = state.data?.map;
|
|
1965
|
+
const explainer = kind() === "explainer";
|
|
1966
|
+
if (!map || !map.head.nodes.length) {
|
|
1967
|
+
append(root, [
|
|
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(
|
|
1980
|
+
"div",
|
|
1981
|
+
{ class: "empty-state" },
|
|
1982
|
+
h("p", null, "No map in this review."),
|
|
1983
|
+
h(
|
|
1984
|
+
"p",
|
|
1985
|
+
null,
|
|
1986
|
+
"The map places a change inside the system and shows what sits next to it. ",
|
|
1987
|
+
"A change that lands in one place, where the Files tab already answers that, ships without one."
|
|
1988
|
+
)
|
|
1989
|
+
)
|
|
1990
|
+
]);
|
|
1991
|
+
return;
|
|
1837
1992
|
}
|
|
1993
|
+
const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
|
|
1994
|
+
const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
|
|
1995
|
+
const all = collect(map);
|
|
1996
|
+
const edges = allEdges(map);
|
|
1997
|
+
const route = routeIn(all, map.filesByNode);
|
|
1998
|
+
const selected = state.params.get("node") ?? "";
|
|
1999
|
+
const hasChildren = (id) => [...all.keys()].some((k) => parentOf(k) === id);
|
|
2000
|
+
let level = selected && all.has(selected) ? hasChildren(selected) ? selected : parentOf(selected) : "";
|
|
1838
2001
|
const canvas = h("div", { class: "map-canvas" });
|
|
1839
2002
|
const side2 = h("div", { class: "map-side" });
|
|
1840
2003
|
root.appendChild(h("div", { class: "map-layout" }, canvas, side2));
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2004
|
+
const openLevel = (id) => {
|
|
2005
|
+
level = id;
|
|
2006
|
+
draw();
|
|
2007
|
+
};
|
|
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
|
+
);
|
|
2031
|
+
const counts = [
|
|
2032
|
+
[map.diff.added.length, "ok", "added"],
|
|
2033
|
+
[map.diff.changed.length, "warn", "changed"],
|
|
2034
|
+
[map.diff.removed.length, "del", "removed"]
|
|
2035
|
+
];
|
|
2036
|
+
const marks = counts.filter(([n]) => n > 0);
|
|
2037
|
+
return h(
|
|
2038
|
+
"div",
|
|
2039
|
+
{ class: "map-intro" },
|
|
2040
|
+
h("h2", null, "Where the change landed"),
|
|
1846
2041
|
h(
|
|
1847
|
-
"
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
"
|
|
2042
|
+
"p",
|
|
2043
|
+
null,
|
|
2044
|
+
"The Files tab has the lines. This has the parts of the system they landed in, and what",
|
|
2045
|
+
" those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change;",
|
|
2046
|
+
" the rest is the context you need to judge it."
|
|
2047
|
+
),
|
|
2048
|
+
h(
|
|
2049
|
+
"div",
|
|
2050
|
+
{ class: "map-route" },
|
|
2051
|
+
marks.length ? marks.map(([n, cls, word]) => h("span", { class: `badge ${cls}` }, `${n} ${word}`)) : h("span", { class: "muted" }, "This change touched no part of the map."),
|
|
2052
|
+
route ? h(
|
|
2053
|
+
"button",
|
|
2054
|
+
{
|
|
2055
|
+
class: "small",
|
|
2056
|
+
onclick: () => navigate("map", { node: route.id })
|
|
2057
|
+
},
|
|
2058
|
+
`Start at ${route.label} \u203A`
|
|
2059
|
+
) : null,
|
|
2060
|
+
map.base ? null : h(
|
|
2061
|
+
"span",
|
|
2062
|
+
{ class: "muted" },
|
|
2063
|
+
"No base map, so changed means touched by the diff and nothing reads as added or removed."
|
|
2064
|
+
)
|
|
1855
2065
|
)
|
|
1856
2066
|
);
|
|
2067
|
+
};
|
|
2068
|
+
const crumbs = () => {
|
|
2069
|
+
const el = h("div", { class: "crumbs" });
|
|
2070
|
+
const parts = level ? level.split(".") : [];
|
|
2071
|
+
el.appendChild(h("span", { onclick: () => openLevel("") }, "System"));
|
|
1857
2072
|
parts.forEach((_, i) => {
|
|
1858
2073
|
const id = parts.slice(0, i + 1).join(".");
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
h(
|
|
1862
|
-
"span",
|
|
1863
|
-
{
|
|
1864
|
-
onclick: () => {
|
|
1865
|
-
level = id;
|
|
1866
|
-
draw();
|
|
1867
|
-
}
|
|
1868
|
-
},
|
|
1869
|
-
all.get(id)?.label ?? id
|
|
1870
|
-
)
|
|
1871
|
-
);
|
|
2074
|
+
el.appendChild(document.createTextNode(" \u203A "));
|
|
2075
|
+
el.appendChild(h("span", { onclick: () => openLevel(id) }, all.get(id)?.label ?? id));
|
|
1872
2076
|
});
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2077
|
+
return el;
|
|
2078
|
+
};
|
|
2079
|
+
const card = (n) => {
|
|
2080
|
+
const inside = [...all.keys()].filter((k) => parentOf(k) === n.id).length;
|
|
2081
|
+
const hidden = changedInside(all, n.id);
|
|
2082
|
+
const files = map.filesByNode[n.id]?.length ?? 0;
|
|
2083
|
+
return h(
|
|
1876
2084
|
"div",
|
|
1877
|
-
{
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
h("span", { class:
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
h(
|
|
1892
|
-
"
|
|
2085
|
+
{
|
|
2086
|
+
class: `map-node ${n.status} ${explainer || touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
|
|
2087
|
+
onclick: () => navigate("map", { node: n.id }),
|
|
2088
|
+
ondblclick: () => inside && openLevel(n.id)
|
|
2089
|
+
},
|
|
2090
|
+
n.status ? h("span", { class: `status badge ${statusClass(n.status)}` }, n.status) : null,
|
|
2091
|
+
h("div", { class: "kind" }, n.kind),
|
|
2092
|
+
h("div", { class: "label" }, n.label),
|
|
2093
|
+
n.description ? h("div", { class: "desc" }, n.description) : null,
|
|
2094
|
+
h(
|
|
2095
|
+
"div",
|
|
2096
|
+
{ class: "meta" },
|
|
2097
|
+
files ? h("span", { class: "warn-text" }, `${files} changed file${files > 1 ? "s" : ""}`) : null,
|
|
2098
|
+
hidden ? h("span", { class: "warn-text" }, `${hidden} changed inside`) : null,
|
|
2099
|
+
inside ? h(
|
|
2100
|
+
"button",
|
|
1893
2101
|
{
|
|
1894
|
-
class:
|
|
1895
|
-
onclick: () => {
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
ondblclick: () => {
|
|
1899
|
-
if (nk) {
|
|
1900
|
-
level = n.id;
|
|
1901
|
-
draw();
|
|
1902
|
-
}
|
|
2102
|
+
class: "small drill",
|
|
2103
|
+
onclick: (e) => {
|
|
2104
|
+
e.stopPropagation();
|
|
2105
|
+
openLevel(n.id);
|
|
1903
2106
|
}
|
|
1904
2107
|
},
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2108
|
+
`Open ${inside} inside \u203A`
|
|
2109
|
+
) : null
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
2112
|
+
};
|
|
2113
|
+
const board = () => {
|
|
2114
|
+
const kids = childrenOf(all, level, map.filesByNode);
|
|
2115
|
+
if (!kids.length) return h("div", { class: "empty-state" }, "Nothing at this level.");
|
|
2116
|
+
const hot = kids.filter((n) => touched(all, n));
|
|
2117
|
+
const cold = kids.filter((n) => !touched(all, n));
|
|
2118
|
+
const group = (title, items) => items.length ? h(
|
|
2119
|
+
"div",
|
|
2120
|
+
{ class: "map-group" },
|
|
2121
|
+
title ? h("h3", null, title) : null,
|
|
2122
|
+
h("div", { class: "map-grid" }, items.map(card))
|
|
2123
|
+
) : null;
|
|
2124
|
+
if (explainer) return group(null, kids) ?? h("div", null);
|
|
2125
|
+
if (!hot.length)
|
|
2126
|
+
return h(
|
|
2127
|
+
"div",
|
|
2128
|
+
null,
|
|
2129
|
+
h("div", { class: "map-note muted" }, "Nothing here was touched. The change is elsewhere."),
|
|
2130
|
+
group(null, cold)
|
|
1923
2131
|
);
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2132
|
+
return h(
|
|
2133
|
+
"div",
|
|
2134
|
+
null,
|
|
2135
|
+
group(cold.length ? "What the change touched" : null, hot),
|
|
2136
|
+
group("Context \u2014 unchanged here", cold)
|
|
2137
|
+
);
|
|
2138
|
+
};
|
|
2139
|
+
const links2 = () => {
|
|
2140
|
+
const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
|
|
1929
2141
|
const top = (id) => {
|
|
1930
2142
|
let x = id;
|
|
1931
2143
|
while (x && !here.has(x)) x = parentOf(x);
|
|
1932
2144
|
return x;
|
|
1933
2145
|
};
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2146
|
+
const folded = [];
|
|
2147
|
+
for (const e of edges) {
|
|
2148
|
+
const from = top(e.from) || e.from;
|
|
2149
|
+
const to = top(e.to) || e.to;
|
|
2150
|
+
if (from === to) continue;
|
|
2151
|
+
if (!here.has(from) && !here.has(to)) continue;
|
|
2152
|
+
if (folded.some((x) => x.from === from && x.to === to)) continue;
|
|
2153
|
+
folded.push({ from, to, ...e.label ? { label: e.label } : {} });
|
|
2154
|
+
}
|
|
2155
|
+
if (!folded.length) return null;
|
|
2156
|
+
return h(
|
|
2157
|
+
"div",
|
|
2158
|
+
{ class: "map-edges" },
|
|
2159
|
+
h("h3", null, "What connects to what here"),
|
|
2160
|
+
h(
|
|
2161
|
+
"div",
|
|
2162
|
+
{ class: "muted map-note" },
|
|
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."
|
|
2164
|
+
),
|
|
2165
|
+
rankEdges(folded, all).map(
|
|
2166
|
+
({ edge, touchesChange }) => h(
|
|
1938
2167
|
"div",
|
|
1939
|
-
{ class: "
|
|
1940
|
-
h(
|
|
1941
|
-
|
|
1942
|
-
(
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
"b",
|
|
1953
|
-
{ onclick: () => navigate("map", { node: e.to }) },
|
|
1954
|
-
all.get(e.to)?.label ?? e.to
|
|
1955
|
-
),
|
|
1956
|
-
h("span", { class: "muted" }, e.label ?? "")
|
|
1957
|
-
)
|
|
1958
|
-
)
|
|
2168
|
+
{ class: `edge ${touchesChange ? "hot" : ""}` },
|
|
2169
|
+
h(
|
|
2170
|
+
"b",
|
|
2171
|
+
{ onclick: () => navigate("map", { node: edge.from }) },
|
|
2172
|
+
all.get(edge.from)?.label ?? edge.from
|
|
2173
|
+
),
|
|
2174
|
+
"\u2192",
|
|
2175
|
+
h(
|
|
2176
|
+
"b",
|
|
2177
|
+
{ onclick: () => navigate("map", { node: edge.to }) },
|
|
2178
|
+
all.get(edge.to)?.label ?? edge.to
|
|
2179
|
+
),
|
|
2180
|
+
h("span", { class: "muted" }, edge.label ?? "")
|
|
1959
2181
|
)
|
|
1960
|
-
)
|
|
1961
|
-
|
|
2182
|
+
)
|
|
2183
|
+
);
|
|
2184
|
+
};
|
|
2185
|
+
const draw = () => {
|
|
2186
|
+
canvas.innerHTML = "";
|
|
2187
|
+
append(canvas, [intro2(), crumbs(), board(), links2()]);
|
|
1962
2188
|
drawSide();
|
|
1963
2189
|
};
|
|
1964
2190
|
const drawSide = () => {
|
|
@@ -1969,7 +2195,7 @@
|
|
|
1969
2195
|
h(
|
|
1970
2196
|
"div",
|
|
1971
2197
|
{ class: "muted" },
|
|
1972
|
-
"
|
|
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."
|
|
1973
2199
|
)
|
|
1974
2200
|
);
|
|
1975
2201
|
return;
|
|
@@ -1977,19 +2203,14 @@
|
|
|
1977
2203
|
const anchor = n.anchor ? state.data?.document?.anchors[n.anchor] : null;
|
|
1978
2204
|
const files = map.filesByNode[n.id] ?? [];
|
|
1979
2205
|
const threads = threadsFor((t) => t.type === "map" && t.node === n.id);
|
|
2206
|
+
const neighbours = neighboursOf(map, n.id);
|
|
1980
2207
|
append(side2, [
|
|
1981
2208
|
h(
|
|
1982
2209
|
"div",
|
|
1983
2210
|
{ class: "kind muted", style: { fontSize: "11px", textTransform: "uppercase" } },
|
|
1984
2211
|
n.kind,
|
|
1985
2212
|
" ",
|
|
1986
|
-
n.status ? h(
|
|
1987
|
-
"span",
|
|
1988
|
-
{
|
|
1989
|
-
class: `badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
|
|
1990
|
-
},
|
|
1991
|
-
n.status
|
|
1992
|
-
) : ""
|
|
2213
|
+
n.status ? h("span", { class: `badge ${statusClass(n.status)}` }, n.status) : ""
|
|
1993
2214
|
),
|
|
1994
2215
|
h("h3", { style: { margin: "2px 0 6px" } }, n.label),
|
|
1995
2216
|
h("div", { class: "mono muted", style: { fontSize: "11px" } }, n.id),
|
|
@@ -2015,6 +2236,31 @@
|
|
|
2015
2236
|
(f) => h("span", { class: "file", onclick: () => navigate("files", { path: f }) }, f)
|
|
2016
2237
|
)
|
|
2017
2238
|
) : null,
|
|
2239
|
+
neighbours.length ? h(
|
|
2240
|
+
"div",
|
|
2241
|
+
{ class: "neighbours", style: { marginTop: "12px" } },
|
|
2242
|
+
h("b", null, "Next to it"),
|
|
2243
|
+
h(
|
|
2244
|
+
"div",
|
|
2245
|
+
{ class: "muted", style: { fontSize: "11px" } },
|
|
2246
|
+
"\u2192 this reaches \xB7 \u2190 reaches this"
|
|
2247
|
+
),
|
|
2248
|
+
neighbours.map((x) => {
|
|
2249
|
+
const other = all.get(x.other);
|
|
2250
|
+
return h(
|
|
2251
|
+
"div",
|
|
2252
|
+
{ class: "neighbour" },
|
|
2253
|
+
h("span", { class: "mono muted" }, x.dir === "out" ? "\u2192" : "\u2190"),
|
|
2254
|
+
h(
|
|
2255
|
+
"span",
|
|
2256
|
+
{ class: "nb", onclick: () => navigate("map", { node: x.other }) },
|
|
2257
|
+
other?.label ?? x.other
|
|
2258
|
+
),
|
|
2259
|
+
other?.status ? h("span", { class: `badge ${statusClass(other.status)}` }, other.status) : null,
|
|
2260
|
+
x.label ? h("span", { class: "muted" }, x.label) : null
|
|
2261
|
+
);
|
|
2262
|
+
})
|
|
2263
|
+
) : null,
|
|
2018
2264
|
h(
|
|
2019
2265
|
"div",
|
|
2020
2266
|
{ style: { marginTop: "12px" } },
|
|
@@ -2055,30 +2301,300 @@
|
|
|
2055
2301
|
draw();
|
|
2056
2302
|
}
|
|
2057
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
|
+
|
|
2058
2574
|
// src/ui/app.ts
|
|
2059
2575
|
var app = document.getElementById("app");
|
|
2060
2576
|
async function home() {
|
|
2061
2577
|
clear(app);
|
|
2062
2578
|
const reviews = await api.reviews();
|
|
2063
|
-
const el = h("div", { class: "home" }, h("h2", null, "Reviews"));
|
|
2579
|
+
const el = h("div", { class: "home" }, h("h2", null, "Reviews and explainers"));
|
|
2064
2580
|
const active = reviews.filter((r) => !r.dismissed);
|
|
2065
2581
|
const dismissed = reviews.filter((r) => r.dismissed);
|
|
2066
2582
|
const item = (r) => h(
|
|
2067
2583
|
"div",
|
|
2068
2584
|
{ class: "item", onclick: () => location.href = `/review/${r.id}` },
|
|
2069
2585
|
h("span", { class: "t" }, r.title),
|
|
2070
|
-
h("span", { class: `badge ${
|
|
2586
|
+
h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
|
|
2071
2587
|
r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
|
|
2072
|
-
h(
|
|
2073
|
-
"span",
|
|
2074
|
-
{ class: "muted mono", style: { fontSize: "12px" } },
|
|
2075
|
-
r.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name
|
|
2076
|
-
),
|
|
2588
|
+
h("span", { class: "muted mono", style: { fontSize: "12px" } }, bindingLabel(r)),
|
|
2077
2589
|
h("span", { class: "muted", style: { fontSize: "12px" } }, timeAgo(r.updatedAt))
|
|
2078
2590
|
);
|
|
2079
2591
|
if (!active.length)
|
|
2080
2592
|
el.appendChild(
|
|
2081
|
-
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
|
+
)
|
|
2082
2598
|
);
|
|
2083
2599
|
active.forEach((r) => el.appendChild(item(r)));
|
|
2084
2600
|
if (dismissed.length) {
|
|
@@ -2087,7 +2603,12 @@
|
|
|
2087
2603
|
}
|
|
2088
2604
|
app.appendChild(el);
|
|
2089
2605
|
}
|
|
2090
|
-
function
|
|
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
|
+
}
|
|
2611
|
+
function statusClass2(s) {
|
|
2091
2612
|
return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
|
|
2092
2613
|
}
|
|
2093
2614
|
var center;
|
|
@@ -2126,12 +2647,16 @@
|
|
|
2126
2647
|
const r = d.review;
|
|
2127
2648
|
const pending = d.threads.filter((t) => !t.submitted).length;
|
|
2128
2649
|
const open = d.threads.filter((t) => t.status === "open").length;
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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]]);
|
|
2135
2660
|
const revSel = h("select", {
|
|
2136
2661
|
class: "small",
|
|
2137
2662
|
style: { font: "inherit", fontSize: "12px" },
|
|
@@ -2153,7 +2678,7 @@
|
|
|
2153
2678
|
const identity = h("div", { class: "bar-row bar-identity" }, [
|
|
2154
2679
|
h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
|
|
2155
2680
|
h("span", { class: "title", title: r.title }, r.title),
|
|
2156
|
-
h("span", { class: `badge ${
|
|
2681
|
+
h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
|
|
2157
2682
|
r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
|
|
2158
2683
|
h(
|
|
2159
2684
|
"span",
|
|
@@ -2162,7 +2687,7 @@
|
|
|
2162
2687
|
style: { fontSize: "12px" },
|
|
2163
2688
|
title: `${r.pins.base} \u2192 ${r.pins.head}`
|
|
2164
2689
|
},
|
|
2165
|
-
r
|
|
2690
|
+
bindingLabel(r)
|
|
2166
2691
|
)
|
|
2167
2692
|
]);
|
|
2168
2693
|
const actions = h("div", { class: "bar-row bar-actions" }, [
|
|
@@ -2170,7 +2695,7 @@
|
|
|
2170
2695
|
"div",
|
|
2171
2696
|
{ class: "tabs" },
|
|
2172
2697
|
tabs.map(
|
|
2173
|
-
([v,
|
|
2698
|
+
([v, label2]) => h("button", { class: v === current2 ? "active" : "", onclick: () => navigate(v) }, label2)
|
|
2174
2699
|
)
|
|
2175
2700
|
),
|
|
2176
2701
|
h("span", { class: "spacer" }),
|
|
@@ -2255,7 +2780,7 @@
|
|
|
2255
2780
|
h(
|
|
2256
2781
|
"div",
|
|
2257
2782
|
{ class: "item muted" },
|
|
2258
|
-
`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)}`
|
|
2259
2784
|
),
|
|
2260
2785
|
h(
|
|
2261
2786
|
"div",
|
|
@@ -2268,7 +2793,7 @@
|
|
|
2268
2793
|
function renderCenter() {
|
|
2269
2794
|
const scroll = center.scrollTop;
|
|
2270
2795
|
clear(center);
|
|
2271
|
-
switch (
|
|
2796
|
+
switch (view()) {
|
|
2272
2797
|
case "review":
|
|
2273
2798
|
renderReview(center);
|
|
2274
2799
|
break;
|
|
@@ -2281,6 +2806,9 @@
|
|
|
2281
2806
|
case "map":
|
|
2282
2807
|
renderMap(center);
|
|
2283
2808
|
break;
|
|
2809
|
+
case "coverage":
|
|
2810
|
+
renderCoverage(center);
|
|
2811
|
+
break;
|
|
2284
2812
|
}
|
|
2285
2813
|
center.scrollTop = scroll;
|
|
2286
2814
|
}
|