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/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(view, params = {}) {
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 = `#/${view}${q ? `?${q}` : ""}`;
214
+ location.hash = `#/${view2}${q ? `?${q}` : ""}`;
204
215
  }
205
216
  function readHash() {
206
217
  const m2 = /^#\/(\w+)(?:\?(.*))?$/.exec(location.hash);
207
- const view = m2?.[1] ?? "review";
208
- state.view = ["review", "commits", "files", "map"].includes(view) ? view : "review";
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, "Submit review"),
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
- { title: "End the review without approving it", onclick: () => decide("close") },
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") }, "Request changes"),
509
- h("button", { class: "ok", onclick: () => decide("approve") }, "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 bar = (dir) => {
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(bar("up"));
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(bar("down"));
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 label = svg(
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
- label.addEventListener("click", (e) => {
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(label);
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(interfaceDelta(doc.interfaces));
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 view = h("div", { class: "file-view", "data-path": path });
1420
- root.appendChild(h("div", { class: "files-layout" }, list, view));
1421
- if (path) void renderDiff(view, path);
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(view, path) {
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
- view.appendChild(h("div", { class: "muted", style: { padding: "12px" } }, "Loading\u2026"));
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
- view.innerHTML = "";
1457
- view.appendChild(h("div", { class: "empty-state" }, e.message));
1540
+ view2.innerHTML = "";
1541
+ view2.appendChild(h("div", { class: "empty-state" }, e.message));
1458
1542
  return;
1459
1543
  }
1460
- view.innerHTML = "";
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
- view.innerHTML = "";
1470
- void renderDiff(view, path);
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
- view.appendChild(
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
- view.appendChild(
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
- view.appendChild(h("div", { class: "empty-state" }, "Binary file"));
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
- view.appendChild(el);
1521
- codeSelectionHandler(view);
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 intro = () => {
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 links = () => {
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, [intro(), crumbs(), board(), links()]);
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("div", { class: "empty-state" }, "No reviews. Ask your agent to scaffold and publish one.")
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 tabs = [
2257
- ["review", "Review"],
2258
- ["commits", "Commits"],
2259
- ["files", `Files${d.changes.length ? ` (${d.changes.length})` : ""}`],
2260
- ["map", "Map"]
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.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name
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, label]) => h("button", { class: v === state.view ? "active" : "", onclick: () => navigate(v) }, label)
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 (state.view) {
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
  }