thurview 0.17.2 → 0.19.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/dist/ui/app.js CHANGED
@@ -125,7 +125,30 @@
125
125
  });
126
126
 
127
127
  // src/ui/api.ts
128
+ var published = (() => {
129
+ const el = document.getElementById("thurview-snapshot");
130
+ return el?.textContent ? JSON.parse(el.textContent) : null;
131
+ })();
132
+ function offline(url) {
133
+ const snap = published;
134
+ const u = new URL(url, "http://published.invalid");
135
+ const sub = u.pathname.split("/").filter(Boolean)[3];
136
+ const path = u.searchParams.get("path") ?? "";
137
+ const answer = sub === void 0 ? snap.payload : sub === "commits" ? snap.commits : sub === "presence" ? snap.payload.agent : sub === "diff" ? snap.diffs[path] : sub === "file" ? sliceLines(snap.files[`${u.searchParams.get("graph")}:${path}`], u.searchParams) : void 0;
138
+ if (answer === void 0) throw new Error("not in this published copy");
139
+ return answer;
140
+ }
141
+ function sliceLines(f, q) {
142
+ if (!f) return void 0;
143
+ const from = Math.max(1, Number(q.get("from") ?? 1));
144
+ const to = Math.min(f.total, Number(q.get("to") ?? f.total));
145
+ return { ...f, from, to, lines: f.lines.slice(from - 1, to) };
146
+ }
128
147
  async function j(url, init) {
148
+ if (published) {
149
+ if (init?.method && init.method !== "GET") throw new Error("a published copy is read only");
150
+ return offline(url);
151
+ }
129
152
  const r = await fetch(url, init);
130
153
  const body = await r.json();
131
154
  if (!r.ok) throw new Error(body.error ?? r.statusText);
@@ -227,7 +250,10 @@
227
250
  return s === "accepted" || s === "closed";
228
251
  }
229
252
  function readOnly() {
230
- return isTerminal() || state.viewingRevision !== null;
253
+ return isTerminal() || !canWrite();
254
+ }
255
+ function canWrite() {
256
+ return state.viewingRevision === null && !published;
231
257
  }
232
258
 
233
259
  // src/ui/code.ts
@@ -615,13 +641,13 @@
615
641
  return table;
616
642
  }
617
643
  function startLineComment(e, target, quote) {
618
- if (state.viewingRevision !== null) return;
644
+ if (!canWrite()) return;
619
645
  const el = commentPopover(target, quote?.trim() ? quote.trim() : void 0);
620
646
  popover(el, { x: e.pageX + 8, y: e.pageY + 8 });
621
647
  }
622
648
  var rangeStart = null;
623
649
  function lineClick(e, path, side2, line, table) {
624
- if (state.viewingRevision !== null) return;
650
+ if (!canWrite()) return;
625
651
  if (e.shiftKey && rangeStart && rangeStart.path === path && rangeStart.side === side2) {
626
652
  const from = Math.min(rangeStart.line, line);
627
653
  const to = Math.max(rangeStart.line, line);
@@ -664,8 +690,7 @@
664
690
  );
665
691
  const a = rowOf(range.startContainer);
666
692
  const b = rowOf(range.endContainer);
667
- if (!a || !b || !root.contains(a) || !root.contains(b) || state.viewingRevision !== null)
668
- return remove();
693
+ if (!a || !b || !root.contains(a) || !root.contains(b) || !canWrite()) return remove();
669
694
  const table = a.closest("table");
670
695
  if (!table || table !== b.closest("table")) return remove();
671
696
  const path = root.closest("[data-path]")?.dataset["path"] ?? "";
@@ -708,7 +733,7 @@
708
733
  const sel = window.getSelection();
709
734
  if (sel && !sel.isCollapsed) return;
710
735
  const word = wordAt(e);
711
- if (!word) return;
736
+ if (!word || published) return;
712
737
  const defs = await api.symbols(state.id, word, graph);
713
738
  if (e.ctrlKey || e.metaKey) {
714
739
  const d = defs[0];
@@ -1068,7 +1093,7 @@
1068
1093
  } else {
1069
1094
  g.appendChild(svg("line", { x1, y1: y2, x2: x2 + (x2 > x1 ? -4 : 4), y2 }));
1070
1095
  }
1071
- const label2 = svg("text", {}, ...tspans(start, y2 - 6, rows[i].lines));
1096
+ const label = svg("text", {}, ...tspans(start, y2 - 6, rows[i].lines));
1072
1097
  const open = (at) => {
1073
1098
  if (m2.anchor) openAnchor(m2.anchor);
1074
1099
  else if (m2.code)
@@ -1081,17 +1106,17 @@
1081
1106
  at
1082
1107
  );
1083
1108
  };
1084
- label2.addEventListener("click", (e) => open({ x: e.pageX, y: e.pageY + 8 }));
1085
- label2.setAttribute("role", "button");
1086
- label2.setAttribute("tabindex", "0");
1087
- label2.addEventListener("keydown", (e) => {
1109
+ label.addEventListener("click", (e) => open({ x: e.pageX, y: e.pageY + 8 }));
1110
+ label.setAttribute("role", "button");
1111
+ label.setAttribute("tabindex", "0");
1112
+ label.addEventListener("keydown", (e) => {
1088
1113
  const k = e.key;
1089
1114
  if (k !== "Enter" && k !== " ") return;
1090
1115
  e.preventDefault();
1091
- const r = label2.getBoundingClientRect();
1116
+ const r = label.getBoundingClientRect();
1092
1117
  open({ x: r.left + window.scrollX, y: r.bottom + window.scrollY + 8 });
1093
1118
  });
1094
- g.appendChild(label2);
1119
+ g.appendChild(label);
1095
1120
  el.appendChild(g);
1096
1121
  });
1097
1122
  return h(
@@ -1255,11 +1280,11 @@
1255
1280
  )
1256
1281
  )
1257
1282
  );
1258
- for (const { edge, d, back, label: label2 } of edges) {
1283
+ for (const { edge, d, back, label } of edges) {
1259
1284
  const g = svg("g", { class: back ? "edge back" : "edge" });
1260
1285
  g.appendChild(svg("path", { d, fill: "none", "marker-end": "url(#flow-arr)" }));
1261
1286
  if (edge.case)
1262
- g.appendChild(svg("text", { x: label2.x, y: label2.y, "text-anchor": label2.at }, edge.case));
1287
+ g.appendChild(svg("text", { x: label.x, y: label.y, "text-anchor": label.at }, edge.case));
1263
1288
  el.appendChild(g);
1264
1289
  }
1265
1290
  for (const s of b.steps) {
@@ -1421,7 +1446,7 @@
1421
1446
  class: threads.length ? "count" : "",
1422
1447
  title: design ? "Comment on what this design proposes" : "Comment on the interface delta",
1423
1448
  onclick: (e) => {
1424
- if (state.viewingRevision !== null) return;
1449
+ if (!canWrite()) return;
1425
1450
  popover(commentPopover({ type: "document", blockId: DELTA_BLOCK }), {
1426
1451
  x: e.pageX + 10,
1427
1452
  y: e.pageY
@@ -1454,7 +1479,7 @@
1454
1479
  h(
1455
1480
  "p",
1456
1481
  { class: "ifd-verdict" },
1457
- delta ? delta.verdict : design ? "Unavailable: this revision declared no proposal." : "Unavailable: the code graph could not be built for these commits."
1482
+ delta ? delta.verdict : design ? "Unavailable: this revision declared no proposal." : "Unavailable: this revision recorded no interface delta."
1458
1483
  )
1459
1484
  );
1460
1485
  const entries = delta?.entries ?? [];
@@ -1491,7 +1516,7 @@
1491
1516
  class: threads.length ? "count" : "",
1492
1517
  title: "Comment on coverage",
1493
1518
  onclick: (e) => {
1494
- if (state.viewingRevision !== null) return;
1519
+ if (!canWrite()) return;
1495
1520
  popover(commentPopover({ type: "document", blockId: COVERAGE_BLOCK }), {
1496
1521
  x: e.pageX + 10,
1497
1522
  y: e.pageY
@@ -1556,7 +1581,7 @@
1556
1581
  class: threads.length ? "count" : "",
1557
1582
  title: "Comment on the trust boundaries",
1558
1583
  onclick: (e) => {
1559
- if (state.viewingRevision !== null) return;
1584
+ if (!canWrite()) return;
1560
1585
  popover(commentPopover({ type: "document", blockId: SECURITY_BLOCK }), {
1561
1586
  x: e.pageX + 10,
1562
1587
  y: e.pageY
@@ -1664,7 +1689,7 @@
1664
1689
  class: threads.length ? "count" : "",
1665
1690
  title: "Comment on this block",
1666
1691
  onclick: (e) => {
1667
- if (state.viewingRevision !== null) return;
1692
+ if (!canWrite()) return;
1668
1693
  popover(commentPopover({ type: "document", blockId: b.id }), {
1669
1694
  x: e.pageX + 10,
1670
1695
  y: e.pageY
@@ -1759,7 +1784,7 @@
1759
1784
  if (!sel || sel.isCollapsed || !sel.rangeCount) return remove();
1760
1785
  const range = sel.getRangeAt(0);
1761
1786
  const block = (range.commonAncestorContainer instanceof Element ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement)?.closest(".block");
1762
- if (!block || !docEl.contains(block) || state.viewingRevision !== null) return remove();
1787
+ if (!block || !docEl.contains(block) || !canWrite()) return remove();
1763
1788
  const quote = sel.toString().trim();
1764
1789
  if (!quote) return remove();
1765
1790
  const rect = range.getBoundingClientRect();
@@ -1897,12 +1922,12 @@
1897
1922
  { class: "muted" },
1898
1923
  `${d.hunks.reduce((n, hk) => n + hk.rows.filter((r) => r.type === "add").length, 0)} added \xB7 ${d.hunks.reduce((n, hk) => n + hk.rows.filter((r) => r.type === "del").length, 0)} deleted`
1899
1924
  ),
1900
- h(
1925
+ canWrite() ? h(
1901
1926
  "span",
1902
1927
  { class: "muted hint" },
1903
1928
  "click a line number to comment \xB7 shift-click or select text for a range"
1904
- ),
1905
- state.viewingRevision === null ? h(
1929
+ ) : null,
1930
+ canWrite() ? h(
1906
1931
  "button",
1907
1932
  {
1908
1933
  class: "small",
@@ -1959,7 +1984,7 @@
1959
1984
  }
1960
1985
  function gapRow(d, from, to, colspan, onExpand) {
1961
1986
  if (to < from) return null;
1962
- const count = to - from + 1;
1987
+ const count2 = to - from + 1;
1963
1988
  return h(
1964
1989
  "tr",
1965
1990
  { class: "gap" },
@@ -1979,7 +2004,7 @@
1979
2004
  );
1980
2005
  }
1981
2006
  },
1982
- `\u22EF ${count} unchanged line${count > 1 ? "s" : ""}`
2007
+ `\u22EF ${count2} unchanged line${count2 > 1 ? "s" : ""}`
1983
2008
  )
1984
2009
  );
1985
2010
  }
@@ -2101,9 +2126,9 @@
2101
2126
  let prevEnd = 0;
2102
2127
  const gapBoth = (from, to) => {
2103
2128
  if (to < from) return;
2104
- const count = to - from + 1;
2105
- const lg = h("tr", { class: "gap" }, h("td", { colspan: 2 }, `\u22EF ${count} unchanged`));
2106
- const rg = h("tr", { class: "gap" }, h("td", { colspan: 2 }, `\u22EF ${count} unchanged`));
2129
+ const count2 = to - from + 1;
2130
+ const lg = h("tr", { class: "gap" }, h("td", { colspan: 2 }, `\u22EF ${count2} unchanged`));
2131
+ const rg = h("tr", { class: "gap" }, h("td", { colspan: 2 }, `\u22EF ${count2} unchanged`));
2107
2132
  const expand = async () => {
2108
2133
  const f = await api.file(state.id, d.path, "head", from, to);
2109
2134
  const off = gapOffset(d, from) ?? 0;
@@ -2476,7 +2501,7 @@
2476
2501
  group(design ? "Context \u2014 untouched by the design" : "Context \u2014 unchanged here", cold)
2477
2502
  );
2478
2503
  };
2479
- const links2 = () => {
2504
+ const links = () => {
2480
2505
  const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
2481
2506
  const top = (id) => {
2482
2507
  let x = id;
@@ -2524,7 +2549,7 @@
2524
2549
  };
2525
2550
  const draw = () => {
2526
2551
  canvas.innerHTML = "";
2527
- append(canvas, [intro2(), crumbs(), board(), links2()]);
2552
+ append(canvas, [intro2(), crumbs(), board(), links()]);
2528
2553
  drawSide();
2529
2554
  };
2530
2555
  const drawSide = () => {
@@ -2601,18 +2626,21 @@
2601
2626
  );
2602
2627
  })
2603
2628
  ) : null,
2604
- h(
2629
+ canWrite() ? h(
2605
2630
  "div",
2606
2631
  { style: { marginTop: "12px" } },
2607
2632
  h(
2608
2633
  "button",
2609
2634
  {
2610
2635
  class: "small",
2611
- onclick: (e) => popover(commentPopover({ type: "map", node: n.id }), { x: e.pageX, y: e.pageY + 8 })
2636
+ onclick: (e) => popover(commentPopover({ type: "map", node: n.id }), {
2637
+ x: e.pageX,
2638
+ y: e.pageY + 8
2639
+ })
2612
2640
  },
2613
2641
  "Comment on this node"
2614
2642
  )
2615
- ),
2643
+ ) : null,
2616
2644
  threads.length ? h(
2617
2645
  "div",
2618
2646
  { class: "thread-pins", style: { marginTop: "10px" } },
@@ -2644,6 +2672,12 @@
2644
2672
  // src/ui/views/coverage.ts
2645
2673
  init_dom();
2646
2674
  var SHOWN = 8;
2675
+ var LABELS = [
2676
+ ["explained", "anchored in the document"],
2677
+ ["placed", "placed on the map only"],
2678
+ ["searched", "matched by a recorded search only"],
2679
+ ["uncovered", "not examined"]
2680
+ ];
2647
2681
  function renderCoverage(root) {
2648
2682
  const cov = state.data?.coverage;
2649
2683
  if (!cov) {
@@ -2657,29 +2691,23 @@
2657
2691
  ]);
2658
2692
  return;
2659
2693
  }
2660
- append(root, [intro(cov), clusters(cov), links(cov), sharedNames(cov), unread(cov)]);
2694
+ append(root, [intro(cov), searches(cov), clusters(cov), owners(cov)]);
2661
2695
  }
2696
+ var count = (cov, s) => cov.states[s] ?? 0;
2662
2697
  function bar(cov) {
2663
2698
  const total = Math.max(cov.files.total, 1);
2664
- const seg = (n, cls, label2) => n ? h("span", {
2699
+ const seg = (n, cls, label) => n ? h("span", {
2665
2700
  class: `cov-seg ${cls}`,
2666
2701
  style: { width: `${n / total * 100}%` },
2667
- title: `${n} ${label2}`
2702
+ title: `${n} ${label}`
2668
2703
  }) : null;
2669
2704
  return h(
2670
2705
  "div",
2671
2706
  { class: "cov-bar" },
2672
- seg(cov.states.explained, "explained", "anchored in the document"),
2673
- seg(cov.states.placed, "placed", "placed on the map only"),
2674
- seg(cov.states.uncovered, "uncovered", "not examined")
2707
+ LABELS.map(([s, label]) => seg(count(cov, s), s, label))
2675
2708
  );
2676
2709
  }
2677
2710
  function intro(cov) {
2678
- const key = [
2679
- [cov.states.explained, "explained", "anchored in the document"],
2680
- [cov.states.placed, "placed", "placed on the map only"],
2681
- [cov.states.uncovered, "uncovered", "not examined"]
2682
- ];
2683
2711
  return h(
2684
2712
  "div",
2685
2713
  { class: "cov-intro" },
@@ -2689,14 +2717,19 @@
2689
2717
  null,
2690
2718
  "A codebase does not fit in a short document, so this one selects. Here is the",
2691
2719
  " selection, counted rather than claimed: every file in scope at the pinned commit,",
2692
- " and which of the three states it is in."
2720
+ " and which of the four states it is in."
2693
2721
  ),
2694
2722
  bar(cov),
2695
2723
  h(
2696
2724
  "div",
2697
2725
  { class: "cov-key" },
2698
- key.map(
2699
- ([n, cls, label2]) => h("span", { class: "cov-key-item" }, h("i", { class: `cov-dot ${cls}` }), `${n} ${label2}`)
2726
+ LABELS.map(
2727
+ ([s, label]) => h(
2728
+ "span",
2729
+ { class: "cov-key-item" },
2730
+ h("i", { class: `cov-dot ${s}` }),
2731
+ `${count(cov, s)} ${label}`
2732
+ )
2700
2733
  )
2701
2734
  ),
2702
2735
  h(
@@ -2704,15 +2737,12 @@
2704
2737
  { class: "cov-facts" },
2705
2738
  fact("scope", cov.scope === "**" ? "the whole repository" : cov.scope),
2706
2739
  fact("commit", cov.commit.slice(0, 12)),
2707
- fact("files in scope", String(cov.files.total)),
2708
- fact("read by the code graph", `${cov.files.inGraph} of ${cov.files.total}`),
2709
- cov.unresolved ? fact("references the graph could not place", String(cov.unresolved)) : null,
2710
- cov.truncated ? fact("file list capped", "the graph is partial; treat every count as a floor") : null
2740
+ fact("files in scope", String(cov.files.total))
2711
2741
  )
2712
2742
  );
2713
2743
  }
2714
- function fact(label2, value) {
2715
- return h("div", { class: "cov-fact" }, h("b", null, value), h("span", { class: "muted" }, label2));
2744
+ function fact(label, value) {
2745
+ return h("div", { class: "cov-fact" }, h("b", null, value), h("span", { class: "muted" }, label));
2716
2746
  }
2717
2747
  function fileList(files, cls) {
2718
2748
  if (!files.length) return null;
@@ -2733,9 +2763,49 @@
2733
2763
  }
2734
2764
  return wrap;
2735
2765
  }
2766
+ function command(cov, s) {
2767
+ const quote = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
2768
+ const paths = s.paths.map((p) => ` ${quote(p)}`).join("");
2769
+ return `git grep -I -n -E -e ${quote(s.pattern)} ${cov.commit.slice(0, 12)} --${paths}`;
2770
+ }
2771
+ function searches(cov) {
2772
+ const list = cov.searches ?? [];
2773
+ if (!list.length) return null;
2774
+ return h(
2775
+ "div",
2776
+ { class: "cov-section" },
2777
+ h("h3", null, "What the agent searched"),
2778
+ h(
2779
+ "p",
2780
+ { class: "muted" },
2781
+ "Each search the agent recorded, re-run at the pinned commit. A search that matched",
2782
+ " nothing is listed too: that zero is what a claim like \u201Cnothing else calls it\u201D rests on."
2783
+ ),
2784
+ h(
2785
+ "div",
2786
+ { class: "cov-links" },
2787
+ list.map(
2788
+ (s) => h(
2789
+ "div",
2790
+ { class: "cov-link" },
2791
+ h("code", null, s.key),
2792
+ s.why ? h("span", null, s.why) : null,
2793
+ h(
2794
+ "span",
2795
+ { class: "muted mono" },
2796
+ `${s.hits} line${s.hits === 1 ? "" : "s"} in ${s.files.length} file${s.files.length === 1 ? "" : "s"}`
2797
+ ),
2798
+ h("code", { class: "muted" }, command(cov, s)),
2799
+ fileList(s.files, "searched")
2800
+ )
2801
+ )
2802
+ )
2803
+ );
2804
+ }
2736
2805
  function clusterRow(c) {
2737
2806
  const anchors = state.data?.document?.anchors ?? {};
2738
2807
  const firstAnchor = c.explained.length ? Object.values(anchors).find((a) => a.peek && c.explained.includes(a.peek.file)) : void 0;
2808
+ const badge = (files, cls, label) => files?.length ? h("span", { class: `badge cov-b-${cls}` }, `${files.length} ${label}`) : null;
2739
2809
  return h(
2740
2810
  "div",
2741
2811
  { class: "cov-cluster" },
@@ -2743,19 +2813,14 @@
2743
2813
  "div",
2744
2814
  { class: "cov-cluster-head" },
2745
2815
  h("code", { class: "cov-label" }, c.label),
2746
- h("span", { class: "muted" }, `${c.files} files \xB7 ${c.symbols} symbols`),
2816
+ h("span", { class: "muted" }, `${c.files} files`),
2747
2817
  h("span", { class: "spacer" }),
2748
- c.explained.length ? h("span", { class: "badge cov-b-explained" }, `${c.explained.length} anchored`) : null,
2749
- c.placed.length ? h("span", { class: "badge cov-b-placed" }, `${c.placed.length} placed`) : null,
2750
- c.uncovered.length ? h("span", { class: "badge cov-b-uncovered" }, `${c.uncovered.length} not examined`) : null,
2818
+ badge(c.explained, "explained", "anchored"),
2819
+ badge(c.placed, "placed", "placed"),
2820
+ badge(c.searched, "searched", "searched"),
2821
+ badge(c.uncovered, "uncovered", "not examined"),
2751
2822
  firstAnchor ? h("button", { class: "small", onclick: () => openAnchorPeek(firstAnchor) }, "Peek code \u25B8") : null
2752
2823
  ),
2753
- c.hubs.length ? h(
2754
- "div",
2755
- { class: "muted cov-hubs" },
2756
- "most referenced here: ",
2757
- c.hubs.map((n) => h("code", null, n))
2758
- ) : null,
2759
2824
  fileList(c.uncovered, "uncovered")
2760
2825
  );
2761
2826
  }
@@ -2763,153 +2828,47 @@
2763
2828
  return h(
2764
2829
  "div",
2765
2830
  { class: "cov-section" },
2766
- h("h3", null, "By part of the system"),
2831
+ h("h3", null, "By directory"),
2767
2832
  h(
2768
2833
  "p",
2769
2834
  { class: "muted" },
2770
- "Files clustered by how they reference each other at the pinned commit, largest first.",
2771
- " The files listed under a part are the ones this explainer never examined."
2835
+ "The files in scope by the directory they sit in, largest first.",
2836
+ " The files listed under a directory are the ones nothing in this explainer reached."
2772
2837
  ),
2773
2838
  cov.clusters.map(clusterRow)
2774
2839
  );
2775
2840
  }
2776
- function links(cov) {
2777
- if (!cov.links.length) return null;
2841
+ function owners(cov) {
2842
+ if (!cov.owners.length) return null;
2778
2843
  return h(
2779
2844
  "div",
2780
2845
  { class: "cov-section" },
2781
- h("h3", null, "What reaches what"),
2846
+ h("h3", null, "What each map node owns"),
2782
2847
  h(
2783
2848
  "p",
2784
2849
  { class: "muted" },
2785
- "References that cross from one part to another, with how many there are.",
2786
- " A pair marked both ways references each other in both directions."
2850
+ "A file counts as placed because a map node's globs match it. The globs are here",
2851
+ " so a broad one is visible rather than silently inflating the count."
2787
2852
  ),
2788
2853
  h(
2789
2854
  "div",
2790
2855
  { class: "cov-links" },
2791
- cov.links.slice(0, 24).map(
2792
- (l) => h(
2793
- "div",
2794
- { class: "cov-link" },
2795
- h("code", null, label(cov, l.from)),
2796
- h("span", { class: "muted" }, "\u2192"),
2797
- h("code", null, label(cov, l.to)),
2798
- h("span", { class: "muted mono" }, `${l.references} refs`),
2799
- l.bothWays ? h("span", { class: "badge" }, "both ways") : null
2800
- )
2801
- )
2802
- )
2803
- );
2804
- }
2805
- function label(cov, id) {
2806
- return cov.clusters.find((c) => c.id === id)?.label ?? id;
2807
- }
2808
- function sharedNames(cov) {
2809
- if (!cov.sharedNames.length) return null;
2810
- return h(
2811
- "div",
2812
- { class: "cov-section" },
2813
- h("h3", null, "Names defined in more than one part"),
2814
- h(
2815
- "p",
2816
- { class: "muted" },
2817
- `${cov.sharedNamesTotal} name${cov.sharedNamesTotal === 1 ? " at this commit is" : "s at this commit are"}`,
2818
- " defined in two or more parts. What that means here - the same idea in two places,",
2819
- " two different ideas sharing a word, or a name too common to mean anything - is",
2820
- " what the code says and this page does not."
2821
- ),
2822
- h(
2823
- "div",
2824
- { class: "cov-shared" },
2825
- cov.sharedNames.map(
2826
- (n) => h(
2856
+ cov.owners.map(
2857
+ (o) => h(
2827
2858
  "div",
2828
- { class: "cov-link" },
2829
- h("code", null, n.name),
2830
- h("span", { class: "muted mono" }, `${n.clusters.length} parts`),
2831
- h("span", { class: "muted" }, n.files.join(" \xB7 "))
2859
+ {
2860
+ class: "cov-link",
2861
+ onclick: () => navigate("map", { node: o.node }),
2862
+ style: { cursor: "pointer" }
2863
+ },
2864
+ h("code", null, o.node),
2865
+ h("span", { class: "muted" }, o.globs.join(" ")),
2866
+ h("span", { class: "muted mono" }, `${o.files} files`)
2832
2867
  )
2833
2868
  )
2834
2869
  )
2835
2870
  );
2836
2871
  }
2837
- function unread(cov) {
2838
- if (!cov.outsideGraph.length && !cov.files.capped && !cov.owners.length) return null;
2839
- return h(
2840
- "div",
2841
- { class: "cov-section" },
2842
- h("h3", null, "Outside the code graph"),
2843
- cov.outsideGraph.length ? h(
2844
- "div",
2845
- null,
2846
- h(
2847
- "p",
2848
- { class: "muted" },
2849
- `${cov.files.outsideGraph} files in scope are not in a language the graph reads,`,
2850
- " so they are in no part above. They are absent from the structure, not empty."
2851
- ),
2852
- h(
2853
- "div",
2854
- { class: "cov-links" },
2855
- cov.outsideGraph.slice(0, 16).map(
2856
- (e) => h(
2857
- "div",
2858
- { class: "cov-link" },
2859
- h("code", null, `.${e.extension}`),
2860
- h("span", { class: "muted mono" }, `${e.files} files`)
2861
- )
2862
- )
2863
- ),
2864
- fileList(
2865
- cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "outsideGraph").map((u) => u.file),
2866
- "uncovered"
2867
- )
2868
- ) : null,
2869
- cov.files.capped ? h(
2870
- "div",
2871
- null,
2872
- h(
2873
- "p",
2874
- { class: "muted" },
2875
- `${cov.files.capped} files in scope are in a language the graph reads, but the`,
2876
- " repo-wide file cap was hit before this scope was read, so they were never parsed."
2877
- ),
2878
- fileList(
2879
- cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "capped").map((u) => u.file),
2880
- "uncovered"
2881
- )
2882
- ) : null,
2883
- cov.owners.length ? h(
2884
- "div",
2885
- null,
2886
- h("h4", null, "What each map node owns"),
2887
- h(
2888
- "p",
2889
- { class: "muted" },
2890
- "A file counts as placed because a map node's globs match it. The globs are here",
2891
- " so a broad one is visible rather than silently inflating the count."
2892
- ),
2893
- h(
2894
- "div",
2895
- { class: "cov-links" },
2896
- cov.owners.map(
2897
- (o) => h(
2898
- "div",
2899
- {
2900
- class: "cov-link",
2901
- onclick: () => navigate("map", { node: o.node }),
2902
- style: { cursor: "pointer" }
2903
- },
2904
- h("code", null, o.node),
2905
- h("span", { class: "muted" }, o.globs.join(" ")),
2906
- h("span", { class: "muted mono" }, `${o.files} files`)
2907
- )
2908
- )
2909
- )
2910
- ) : null
2911
- );
2912
- }
2913
2872
 
2914
2873
  // src/ui/app.ts
2915
2874
  var app = document.getElementById("app");
@@ -3069,10 +3028,10 @@
3069
3028
  )
3070
3029
  );
3071
3030
  const identity = h("div", { class: "bar-row bar-identity" }, [
3072
- h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
3031
+ published ? h("span", { class: "brand" }, "thurview") : h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
3073
3032
  h("span", { class: "title", title: r.title }, r.title),
3074
3033
  h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
3075
- r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
3034
+ r.revision > 1 && !published ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
3076
3035
  h(
3077
3036
  "span",
3078
3037
  {
@@ -3088,7 +3047,7 @@
3088
3047
  "div",
3089
3048
  { class: "tabs" },
3090
3049
  tabs.map(
3091
- ([v, label2]) => h("button", { class: v === current2 ? "active" : "", onclick: () => navigate(v) }, label2)
3050
+ ([v, label]) => h("button", { class: v === current2 ? "active" : "", onclick: () => navigate(v) }, label)
3092
3051
  )
3093
3052
  ),
3094
3053
  h("span", { class: "spacer" }),
@@ -3103,12 +3062,12 @@
3103
3062
  },
3104
3063
  `Threads${open ? ` \xB7 ${open}` : ""}`
3105
3064
  ),
3106
- isTerminal() || state.viewingRevision !== null ? null : h(
3065
+ readOnly() ? null : h(
3107
3066
  "button",
3108
3067
  { class: pending ? "primary" : "ok", onclick: () => submitDialog() },
3109
3068
  pending ? `Submit${pending ? ` (${pending})` : ""}` : "Decide"
3110
3069
  ),
3111
- h(
3070
+ published ? null : h(
3112
3071
  "button",
3113
3072
  { class: "ghost bar-more", title: "More", onclick: (e) => moreMenu(e) },
3114
3073
  "\u22EF"
@@ -3215,7 +3174,12 @@
3215
3174
  banner.hidden = true;
3216
3175
  clear(banner);
3217
3176
  const d = state.data;
3218
- if (state.viewingRevision !== null) {
3177
+ if (published) {
3178
+ banner.hidden = false;
3179
+ banner.append(
3180
+ `Published copy of revision ${d.review.revision}, ${d.review.status}. Read only: nothing here reaches the agent.`
3181
+ );
3182
+ } else if (state.viewingRevision !== null) {
3219
3183
  banner.hidden = false;
3220
3184
  banner.append(
3221
3185
  `Viewing revision ${state.viewingRevision} of ${d.review.revision}. Read only.`,
@@ -3344,11 +3308,16 @@
3344
3308
  });
3345
3309
  window.matchMedia(NARROW).addEventListener("change", () => emit("view"));
3346
3310
  emit("data");
3311
+ if (published) {
3312
+ document.documentElement.classList.add("published");
3313
+ return;
3314
+ }
3347
3315
  connectEvents();
3348
3316
  pollPresence();
3349
3317
  }
3350
3318
  var m = /^\/review\/([^/]+)/.exec(location.pathname);
3351
- if (m) void reviewPage(m[1]);
3319
+ if (published) void reviewPage(published.payload.review.id);
3320
+ else if (m) void reviewPage(m[1]);
3352
3321
  else void home();
3353
3322
  })();
3354
3323
  //# sourceMappingURL=app.js.map