thurview 0.6.0 → 0.7.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
@@ -970,12 +970,12 @@
970
970
  );
971
971
  const uc = b.usecases[active];
972
972
  body.innerHTML = "";
973
- const touched = /* @__PURE__ */ new Map();
973
+ const touched2 = /* @__PURE__ */ new Map();
974
974
  for (const op of uc.ops) {
975
975
  const [s, c, f] = op.store.split(".");
976
976
  const key = `${s}.${c}`;
977
- if (!touched.has(key)) touched.set(key, /* @__PURE__ */ new Set());
978
- if (f) touched.get(key).add(f);
977
+ if (!touched2.has(key)) touched2.set(key, /* @__PURE__ */ new Set());
978
+ if (f) touched2.get(key).add(f);
979
979
  }
980
980
  const actorIds = [...new Set(uc.ops.map((o) => o.actor))];
981
981
  body.appendChild(
@@ -996,7 +996,7 @@
996
996
  { class: "store" },
997
997
  h("div", { class: "sname" }, s.label, " ", h("span", { class: "badge" }, s.kind)),
998
998
  Object.entries(colls).map(([cid, c]) => {
999
- const hit = touched.get(`${sid}.${cid}`);
999
+ const hit = touched2.get(`${sid}.${cid}`);
1000
1000
  return h(
1001
1001
  "div",
1002
1002
  { class: `table ${hit ? "hit" : ""}` },
@@ -1797,22 +1797,12 @@
1797
1797
 
1798
1798
  // src/ui/views/map.ts
1799
1799
  init_dom();
1800
- function renderMap(root) {
1801
- const map = state.data?.map;
1802
- if (!map) {
1803
- root.appendChild(
1804
- h(
1805
- "div",
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]));
1800
+
1801
+ // src/ui/views/map-model.ts
1802
+ function parentOf(id) {
1803
+ return id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1804
+ }
1805
+ function collect(map) {
1816
1806
  const all = /* @__PURE__ */ new Map();
1817
1807
  for (const n of map.head.nodes)
1818
1808
  all.set(n.id, {
@@ -1820,145 +1810,262 @@
1820
1810
  status: map.diff.added.includes(n.id) ? "added" : map.diff.changed.includes(n.id) ? "changed" : ""
1821
1811
  });
1822
1812
  for (const id of map.diff.removed) {
1823
- const n = baseNodes.get(id);
1813
+ const n = (map.base?.nodes ?? []).find((x) => x.id === id);
1824
1814
  if (n) all.set(id, { ...n, status: "removed" });
1825
1815
  }
1816
+ return all;
1817
+ }
1818
+ function changedInside(all, id) {
1819
+ let n = 0;
1820
+ for (const item of all.values()) if (item.id.startsWith(id + ".") && item.status) n++;
1821
+ return n;
1822
+ }
1823
+ function touched(all, item) {
1824
+ return !!item.status || changedInside(all, item.id) > 0;
1825
+ }
1826
+ function childrenOf(all, parent, filesByNode) {
1827
+ const kids = [...all.values()].filter((n) => parentOf(n.id) === parent);
1828
+ const weight = (n) => (filesByNode[n.id]?.length ?? 0) + changedInside(all, n.id);
1829
+ return kids.map((n, i) => ({ n, i })).sort((a, b) => {
1830
+ const t = Number(touched(all, b.n)) - Number(touched(all, a.n));
1831
+ if (t) return t;
1832
+ const w = weight(b.n) - weight(a.n);
1833
+ return w || a.i - b.i;
1834
+ }).map((x) => x.n);
1835
+ }
1836
+ function routeIn(all, filesByNode) {
1837
+ const changed = [...all.values()].filter((n) => n.status);
1838
+ const hasChild = (id) => [...all.keys()].some((k) => parentOf(k) === id);
1839
+ const leaves = changed.filter((n) => !hasChild(n.id));
1840
+ const pool = leaves.length ? leaves : changed;
1841
+ let best = null;
1842
+ let bestFiles = -1;
1843
+ for (const n of pool) {
1844
+ const files = filesByNode[n.id]?.length ?? 0;
1845
+ if (files > bestFiles) {
1846
+ best = n;
1847
+ bestFiles = files;
1848
+ }
1849
+ }
1850
+ return best;
1851
+ }
1852
+ function neighboursOf(map, id) {
1853
+ const out = [];
1854
+ const into = [];
1855
+ for (const e of allEdges(map)) {
1856
+ if (e.from === id)
1857
+ out.push({ dir: "out", other: e.to, ...e.label ? { label: e.label } : {} });
1858
+ if (e.to === id)
1859
+ into.push({ dir: "in", other: e.from, ...e.label ? { label: e.label } : {} });
1860
+ }
1861
+ return [...out, ...into];
1862
+ }
1863
+ function allEdges(map) {
1826
1864
  const edges = [...map.head.edges];
1827
1865
  for (const e of map.base?.edges ?? [])
1828
1866
  if (!edges.some((x) => x.from === e.from && x.to === e.to)) edges.push(e);
1829
- const selected = state.params.get("node") ?? "";
1830
- const parentOf = (id) => id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1831
- let level = selected && all.has(selected) ? childrenOf(selected).length ? selected : parentOf(selected) : "";
1832
- function childrenOf(p) {
1833
- return [...all.values()].filter((n) => parentOf(n.id) === p);
1834
- }
1835
- function descendantsChanged(id) {
1836
- return [...all.values()].filter((n) => n.id.startsWith(id + ".") && n.status).length;
1867
+ return edges;
1868
+ }
1869
+ function rankEdges(edges, all) {
1870
+ const hot = (id) => {
1871
+ const item = all.get(id);
1872
+ return item && touched(all, item) ? 1 : 0;
1873
+ };
1874
+ 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 }));
1875
+ }
1876
+
1877
+ // src/ui/views/map.ts
1878
+ var statusClass = (s) => s === "added" ? "ok" : s === "removed" ? "del" : "warn";
1879
+ function renderMap(root) {
1880
+ const map = state.data?.map;
1881
+ if (!map || !map.head.nodes.length) {
1882
+ append(root, [
1883
+ h(
1884
+ "div",
1885
+ { class: "empty-state" },
1886
+ h("p", null, "No map in this review."),
1887
+ h(
1888
+ "p",
1889
+ null,
1890
+ "The map places a change inside the system and shows what sits next to it. ",
1891
+ "A change that lands in one place, where the Files tab already answers that, ships without one."
1892
+ )
1893
+ )
1894
+ ]);
1895
+ return;
1837
1896
  }
1897
+ const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
1898
+ const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
1899
+ const all = collect(map);
1900
+ const edges = allEdges(map);
1901
+ const route = routeIn(all, map.filesByNode);
1902
+ const selected = state.params.get("node") ?? "";
1903
+ const hasChildren = (id) => [...all.keys()].some((k) => parentOf(k) === id);
1904
+ let level = selected && all.has(selected) ? hasChildren(selected) ? selected : parentOf(selected) : "";
1838
1905
  const canvas = h("div", { class: "map-canvas" });
1839
1906
  const side2 = h("div", { class: "map-side" });
1840
1907
  root.appendChild(h("div", { class: "map-layout" }, canvas, side2));
1841
- const draw = () => {
1842
- canvas.innerHTML = "";
1843
- const crumbs = h("div", { class: "crumbs" });
1844
- const parts = level ? level.split(".") : [];
1845
- crumbs.appendChild(
1908
+ const openLevel = (id) => {
1909
+ level = id;
1910
+ draw();
1911
+ };
1912
+ const intro = () => {
1913
+ const counts = [
1914
+ [map.diff.added.length, "ok", "added"],
1915
+ [map.diff.changed.length, "warn", "changed"],
1916
+ [map.diff.removed.length, "del", "removed"]
1917
+ ];
1918
+ const marks = counts.filter(([n]) => n > 0);
1919
+ return h(
1920
+ "div",
1921
+ { class: "map-intro" },
1922
+ h("h2", null, "Where the change landed"),
1846
1923
  h(
1847
- "span",
1848
- {
1849
- onclick: () => {
1850
- level = "";
1851
- draw();
1852
- }
1853
- },
1854
- "System"
1924
+ "p",
1925
+ null,
1926
+ "The Files tab has the lines. This has the parts of the system they landed in, and what",
1927
+ " those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change;",
1928
+ " the rest is the context you need to judge it."
1929
+ ),
1930
+ h(
1931
+ "div",
1932
+ { class: "map-route" },
1933
+ 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."),
1934
+ route ? h(
1935
+ "button",
1936
+ {
1937
+ class: "small",
1938
+ onclick: () => navigate("map", { node: route.id })
1939
+ },
1940
+ `Start at ${route.label} \u203A`
1941
+ ) : null,
1942
+ map.base ? null : h(
1943
+ "span",
1944
+ { class: "muted" },
1945
+ "No base map, so changed means touched by the diff and nothing reads as added or removed."
1946
+ )
1855
1947
  )
1856
1948
  );
1949
+ };
1950
+ const crumbs = () => {
1951
+ const el = h("div", { class: "crumbs" });
1952
+ const parts = level ? level.split(".") : [];
1953
+ el.appendChild(h("span", { onclick: () => openLevel("") }, "System"));
1857
1954
  parts.forEach((_, i) => {
1858
1955
  const id = parts.slice(0, i + 1).join(".");
1859
- crumbs.appendChild(document.createTextNode(" \u203A "));
1860
- crumbs.appendChild(
1861
- h(
1862
- "span",
1863
- {
1864
- onclick: () => {
1865
- level = id;
1866
- draw();
1867
- }
1868
- },
1869
- all.get(id)?.label ?? id
1870
- )
1871
- );
1956
+ el.appendChild(document.createTextNode(" \u203A "));
1957
+ el.appendChild(h("span", { onclick: () => openLevel(id) }, all.get(id)?.label ?? id));
1872
1958
  });
1873
- canvas.appendChild(crumbs);
1874
- const kids = childrenOf(level);
1875
- const legend = h(
1959
+ return el;
1960
+ };
1961
+ const card = (n) => {
1962
+ const inside = [...all.keys()].filter((k) => parentOf(k) === n.id).length;
1963
+ const hidden = changedInside(all, n.id);
1964
+ const files = map.filesByNode[n.id]?.length ?? 0;
1965
+ return h(
1876
1966
  "div",
1877
- { class: "muted", style: { fontSize: "12px", marginBottom: "10px" } },
1878
- h("span", { class: "badge ok" }, `${map.diff.added.length} added`),
1879
- " ",
1880
- h("span", { class: "badge del" }, `${map.diff.removed.length} removed`),
1881
- " ",
1882
- h("span", { class: "badge warn" }, `${map.diff.changed.length} changed`),
1883
- map.base ? "" : " (no base map: changed = touched by the diff)"
1884
- );
1885
- canvas.appendChild(legend);
1886
- const grid = h("div", { class: "map-grid" });
1887
- for (const n of kids) {
1888
- const nk = childrenOf(n.id).length;
1889
- const files = map.filesByNode[n.id]?.length ?? 0;
1890
- grid.appendChild(
1891
- h(
1892
- "div",
1967
+ {
1968
+ class: `map-node ${n.status} ${touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
1969
+ onclick: () => navigate("map", { node: n.id }),
1970
+ ondblclick: () => inside && openLevel(n.id)
1971
+ },
1972
+ n.status ? h("span", { class: `status badge ${statusClass(n.status)}` }, n.status) : null,
1973
+ h("div", { class: "kind" }, n.kind),
1974
+ h("div", { class: "label" }, n.label),
1975
+ n.description ? h("div", { class: "desc" }, n.description) : null,
1976
+ h(
1977
+ "div",
1978
+ { class: "meta" },
1979
+ files ? h("span", { class: "warn-text" }, `${files} changed file${files > 1 ? "s" : ""}`) : null,
1980
+ hidden ? h("span", { class: "warn-text" }, `${hidden} changed inside`) : null,
1981
+ inside ? h(
1982
+ "button",
1893
1983
  {
1894
- class: `map-node ${n.status} ${n.id === selected ? "selected" : ""}`,
1895
- onclick: () => {
1896
- navigate("map", { node: n.id });
1897
- },
1898
- ondblclick: () => {
1899
- if (nk) {
1900
- level = n.id;
1901
- draw();
1902
- }
1984
+ class: "small drill",
1985
+ onclick: (e) => {
1986
+ e.stopPropagation();
1987
+ openLevel(n.id);
1903
1988
  }
1904
1989
  },
1905
- n.status ? h(
1906
- "span",
1907
- {
1908
- class: `status badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1909
- },
1910
- n.status
1911
- ) : null,
1912
- h("div", { class: "kind" }, n.kind),
1913
- h("div", { class: "label" }, n.label),
1914
- n.description ? h("div", { class: "desc" }, n.description) : null,
1915
- h(
1916
- "div",
1917
- { class: "children" },
1918
- nk ? `${nk} inside \xB7 dbl-click to open` : "",
1919
- nk && descendantsChanged(n.id) ? ` \xB7 ${descendantsChanged(n.id)} changed` : "",
1920
- files ? ` \xB7 ${files} changed file${files > 1 ? "s" : ""}` : ""
1921
- )
1922
- )
1990
+ `Open ${inside} inside \u203A`
1991
+ ) : null
1992
+ )
1993
+ );
1994
+ };
1995
+ const board = () => {
1996
+ const kids = childrenOf(all, level, map.filesByNode);
1997
+ if (!kids.length) return h("div", { class: "empty-state" }, "Nothing at this level.");
1998
+ const hot = kids.filter((n) => touched(all, n));
1999
+ const cold = kids.filter((n) => !touched(all, n));
2000
+ const group = (title, items) => items.length ? h(
2001
+ "div",
2002
+ { class: "map-group" },
2003
+ title ? h("h3", null, title) : null,
2004
+ h("div", { class: "map-grid" }, items.map(card))
2005
+ ) : null;
2006
+ if (!hot.length)
2007
+ return h(
2008
+ "div",
2009
+ null,
2010
+ h("div", { class: "map-note muted" }, "Nothing here was touched. The change is elsewhere."),
2011
+ group(null, cold)
1923
2012
  );
1924
- }
1925
- if (!kids.length)
1926
- grid.appendChild(h("div", { class: "empty-state" }, "Nothing at this level."));
1927
- canvas.appendChild(grid);
1928
- const here = new Set(kids.map((k) => k.id));
2013
+ return h(
2014
+ "div",
2015
+ null,
2016
+ group(cold.length ? "What the change touched" : null, hot),
2017
+ group("Context \u2014 unchanged here", cold)
2018
+ );
2019
+ };
2020
+ const links = () => {
2021
+ const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
1929
2022
  const top = (id) => {
1930
2023
  let x = id;
1931
2024
  while (x && !here.has(x)) x = parentOf(x);
1932
2025
  return x;
1933
2026
  };
1934
- const shown = edges.filter((e) => here.has(top(e.from)) || here.has(top(e.to)));
1935
- if (shown.length) {
1936
- canvas.appendChild(
1937
- h(
2027
+ const folded = [];
2028
+ for (const e of edges) {
2029
+ const from = top(e.from) || e.from;
2030
+ const to = top(e.to) || e.to;
2031
+ if (from === to) continue;
2032
+ if (!here.has(from) && !here.has(to)) continue;
2033
+ if (folded.some((x) => x.from === from && x.to === to)) continue;
2034
+ folded.push({ from, to, ...e.label ? { label: e.label } : {} });
2035
+ }
2036
+ if (!folded.length) return null;
2037
+ return h(
2038
+ "div",
2039
+ { class: "map-edges" },
2040
+ h("h3", null, "What connects to what here"),
2041
+ h(
2042
+ "div",
2043
+ { class: "muted map-note" },
2044
+ "A link touching a changed part is where the two sides can fall out of step."
2045
+ ),
2046
+ rankEdges(folded, all).map(
2047
+ ({ edge, touchesChange }) => h(
1938
2048
  "div",
1939
- { class: "map-edges" },
1940
- h("div", { class: "muted", style: { marginBottom: "4px" } }, "Relationships"),
1941
- shown.map(
1942
- (e) => h(
1943
- "div",
1944
- { class: "edge" },
1945
- h(
1946
- "b",
1947
- { onclick: () => navigate("map", { node: e.from }) },
1948
- all.get(e.from)?.label ?? e.from
1949
- ),
1950
- "\u2192",
1951
- h(
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
- )
2049
+ { class: `edge ${touchesChange ? "hot" : ""}` },
2050
+ h(
2051
+ "b",
2052
+ { onclick: () => navigate("map", { node: edge.from }) },
2053
+ all.get(edge.from)?.label ?? edge.from
2054
+ ),
2055
+ "\u2192",
2056
+ h(
2057
+ "b",
2058
+ { onclick: () => navigate("map", { node: edge.to }) },
2059
+ all.get(edge.to)?.label ?? edge.to
2060
+ ),
2061
+ h("span", { class: "muted" }, edge.label ?? "")
1959
2062
  )
1960
- );
1961
- }
2063
+ )
2064
+ );
2065
+ };
2066
+ const draw = () => {
2067
+ canvas.innerHTML = "";
2068
+ append(canvas, [intro(), crumbs(), board(), links()]);
1962
2069
  drawSide();
1963
2070
  };
1964
2071
  const drawSide = () => {
@@ -1969,7 +2076,7 @@
1969
2076
  h(
1970
2077
  "div",
1971
2078
  { class: "muted" },
1972
- "Select a node to see its files, code and relationships. Double-click to drill in."
2079
+ "Pick a part to see what changed in it, the code behind it, and what it connects to."
1973
2080
  )
1974
2081
  );
1975
2082
  return;
@@ -1977,19 +2084,14 @@
1977
2084
  const anchor = n.anchor ? state.data?.document?.anchors[n.anchor] : null;
1978
2085
  const files = map.filesByNode[n.id] ?? [];
1979
2086
  const threads = threadsFor((t) => t.type === "map" && t.node === n.id);
2087
+ const neighbours = neighboursOf(map, n.id);
1980
2088
  append(side2, [
1981
2089
  h(
1982
2090
  "div",
1983
2091
  { class: "kind muted", style: { fontSize: "11px", textTransform: "uppercase" } },
1984
2092
  n.kind,
1985
2093
  " ",
1986
- n.status ? h(
1987
- "span",
1988
- {
1989
- class: `badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1990
- },
1991
- n.status
1992
- ) : ""
2094
+ n.status ? h("span", { class: `badge ${statusClass(n.status)}` }, n.status) : ""
1993
2095
  ),
1994
2096
  h("h3", { style: { margin: "2px 0 6px" } }, n.label),
1995
2097
  h("div", { class: "mono muted", style: { fontSize: "11px" } }, n.id),
@@ -2015,6 +2117,31 @@
2015
2117
  (f) => h("span", { class: "file", onclick: () => navigate("files", { path: f }) }, f)
2016
2118
  )
2017
2119
  ) : null,
2120
+ neighbours.length ? h(
2121
+ "div",
2122
+ { class: "neighbours", style: { marginTop: "12px" } },
2123
+ h("b", null, "Next to it"),
2124
+ h(
2125
+ "div",
2126
+ { class: "muted", style: { fontSize: "11px" } },
2127
+ "\u2192 this reaches \xB7 \u2190 reaches this"
2128
+ ),
2129
+ neighbours.map((x) => {
2130
+ const other = all.get(x.other);
2131
+ return h(
2132
+ "div",
2133
+ { class: "neighbour" },
2134
+ h("span", { class: "mono muted" }, x.dir === "out" ? "\u2192" : "\u2190"),
2135
+ h(
2136
+ "span",
2137
+ { class: "nb", onclick: () => navigate("map", { node: x.other }) },
2138
+ other?.label ?? x.other
2139
+ ),
2140
+ other?.status ? h("span", { class: `badge ${statusClass(other.status)}` }, other.status) : null,
2141
+ x.label ? h("span", { class: "muted" }, x.label) : null
2142
+ );
2143
+ })
2144
+ ) : null,
2018
2145
  h(
2019
2146
  "div",
2020
2147
  { style: { marginTop: "12px" } },
@@ -2067,7 +2194,7 @@
2067
2194
  "div",
2068
2195
  { class: "item", onclick: () => location.href = `/review/${r.id}` },
2069
2196
  h("span", { class: "t" }, r.title),
2070
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2197
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2071
2198
  r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
2072
2199
  h(
2073
2200
  "span",
@@ -2087,7 +2214,7 @@
2087
2214
  }
2088
2215
  app.appendChild(el);
2089
2216
  }
2090
- function statusClass(s) {
2217
+ function statusClass2(s) {
2091
2218
  return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
2092
2219
  }
2093
2220
  var center;
@@ -2153,7 +2280,7 @@
2153
2280
  const identity = h("div", { class: "bar-row bar-identity" }, [
2154
2281
  h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
2155
2282
  h("span", { class: "title", title: r.title }, r.title),
2156
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2283
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2157
2284
  r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
2158
2285
  h(
2159
2286
  "span",