thurview 0.5.1 → 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/README.md +30 -20
- package/dist/cli.js +72 -4
- package/dist/cli.js.map +1 -1
- package/dist/document/compile.js +81 -1
- package/dist/document/compile.js.map +1 -1
- package/dist/document/schema.js +25 -0
- package/dist/document/schema.js.map +1 -1
- package/dist/interfaces.js +241 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/ui/app.css +211 -5
- package/dist/ui/app.js +367 -143
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +34 -9
- package/skills/thurview/references/components.md +34 -0
- package/skills/thurview/references/document-authoring.md +34 -1
- package/skills/thurview/references/software-map.md +71 -10
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
|
|
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 (!
|
|
978
|
-
if (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 =
|
|
999
|
+
const hit = touched2.get(`${sid}.${cid}`);
|
|
1000
1000
|
return h(
|
|
1001
1001
|
"div",
|
|
1002
1002
|
{ class: `table ${hit ? "hit" : ""}` },
|
|
@@ -1067,6 +1067,7 @@
|
|
|
1067
1067
|
);
|
|
1068
1068
|
return;
|
|
1069
1069
|
}
|
|
1070
|
+
root.appendChild(interfaceDelta(doc.interfaces));
|
|
1070
1071
|
const layout = h("div", { class: "doc-layout" });
|
|
1071
1072
|
const toc = h(
|
|
1072
1073
|
"nav",
|
|
@@ -1138,6 +1139,102 @@
|
|
|
1138
1139
|
);
|
|
1139
1140
|
docEl.querySelectorAll(".block.heading").forEach((el) => io.observe(el));
|
|
1140
1141
|
}
|
|
1142
|
+
var DELTA_BLOCK = "interface-delta";
|
|
1143
|
+
var DELTA_SHOWN = 12;
|
|
1144
|
+
var CHANGE_CLASS = {
|
|
1145
|
+
removed: "del",
|
|
1146
|
+
changed: "warn",
|
|
1147
|
+
added: "ok"
|
|
1148
|
+
};
|
|
1149
|
+
function interfaceDelta(delta) {
|
|
1150
|
+
const wrap = h("div", { class: "block ifd", "data-block": DELTA_BLOCK });
|
|
1151
|
+
const threads = threadsFor((t) => t.type === "document" && t.blockId === DELTA_BLOCK);
|
|
1152
|
+
if (threads.length) wrap.classList.add("has-threads");
|
|
1153
|
+
const actions = h(
|
|
1154
|
+
"div",
|
|
1155
|
+
{ class: "block-actions" },
|
|
1156
|
+
h(
|
|
1157
|
+
"button",
|
|
1158
|
+
{
|
|
1159
|
+
class: threads.length ? "count" : "",
|
|
1160
|
+
title: "Comment on the interface delta",
|
|
1161
|
+
onclick: (e) => {
|
|
1162
|
+
if (state.viewingRevision !== null) return;
|
|
1163
|
+
popover(commentPopover({ type: "document", blockId: DELTA_BLOCK }), {
|
|
1164
|
+
x: e.pageX + 10,
|
|
1165
|
+
y: e.pageY
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
threads.length ? String(threads.length) : "+"
|
|
1170
|
+
)
|
|
1171
|
+
);
|
|
1172
|
+
const body = h("div", { class: "ifd-body" });
|
|
1173
|
+
const toggle = h("span", { class: "heading-toggle" }, "hide");
|
|
1174
|
+
toggle.addEventListener("click", () => {
|
|
1175
|
+
const hidden = body.hidden;
|
|
1176
|
+
body.hidden = !hidden;
|
|
1177
|
+
toggle.textContent = hidden ? "hide" : "show";
|
|
1178
|
+
});
|
|
1179
|
+
const counts = delta ? ["removed", "changed", "added"].map((c) => [c, delta.entries.filter((e) => e.change === c).length]).filter(([, n]) => n > 0) : [];
|
|
1180
|
+
wrap.appendChild(
|
|
1181
|
+
h(
|
|
1182
|
+
"div",
|
|
1183
|
+
{ class: "ifd-head" },
|
|
1184
|
+
h("span", { class: "t" }, "Interface delta"),
|
|
1185
|
+
counts.map(([c, n]) => h("span", { class: `badge ${CHANGE_CLASS[c]}` }, `${n} ${c}`)),
|
|
1186
|
+
h("span", { class: "spacer" }),
|
|
1187
|
+
toggle,
|
|
1188
|
+
actions
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
body.appendChild(
|
|
1192
|
+
h(
|
|
1193
|
+
"p",
|
|
1194
|
+
{ class: "ifd-verdict" },
|
|
1195
|
+
delta ? delta.verdict : "Unavailable: the code graph could not be built for these commits."
|
|
1196
|
+
)
|
|
1197
|
+
);
|
|
1198
|
+
const entries = delta?.entries ?? [];
|
|
1199
|
+
const rest = entries.slice(DELTA_SHOWN);
|
|
1200
|
+
for (const e of entries.slice(0, DELTA_SHOWN)) body.appendChild(interfaceRow(e));
|
|
1201
|
+
if (rest.length) {
|
|
1202
|
+
const more = h(
|
|
1203
|
+
"button",
|
|
1204
|
+
{
|
|
1205
|
+
class: "small ghost",
|
|
1206
|
+
onclick: () => {
|
|
1207
|
+
more.replaceWith(...rest.map(interfaceRow));
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
`show ${rest.length} more`
|
|
1211
|
+
);
|
|
1212
|
+
body.appendChild(more);
|
|
1213
|
+
}
|
|
1214
|
+
wrap.appendChild(body);
|
|
1215
|
+
if (threads.length)
|
|
1216
|
+
wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
|
|
1217
|
+
return wrap;
|
|
1218
|
+
}
|
|
1219
|
+
function interfaceRow(e) {
|
|
1220
|
+
return h(
|
|
1221
|
+
"div",
|
|
1222
|
+
{
|
|
1223
|
+
class: `ifd-entry ifd-${e.change}`,
|
|
1224
|
+
title: "Open in Files",
|
|
1225
|
+
onclick: () => navigate("files", { path: e.file, line: e.line, side: e.graph })
|
|
1226
|
+
},
|
|
1227
|
+
h("span", { class: `badge ${CHANGE_CLASS[e.change]}` }, e.change),
|
|
1228
|
+
h(
|
|
1229
|
+
"div",
|
|
1230
|
+
{ class: "ifd-what" },
|
|
1231
|
+
e.was ? h("div", { class: "ifd-was" }, h("code", null, e.was)) : null,
|
|
1232
|
+
h("code", { class: "ifd-name" }, e.name),
|
|
1233
|
+
e.capability ? h("div", { class: "ifd-cap" }, e.capability) : null
|
|
1234
|
+
),
|
|
1235
|
+
h("span", { class: "ifd-where mono" }, `${e.file}:${e.line}`)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1141
1238
|
function scrollToBlock(id) {
|
|
1142
1239
|
const el = document.querySelector(`[data-block="${id}"]`);
|
|
1143
1240
|
if (!el) return;
|
|
@@ -1700,22 +1797,12 @@
|
|
|
1700
1797
|
|
|
1701
1798
|
// src/ui/views/map.ts
|
|
1702
1799
|
init_dom();
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
{ class: "empty-state" },
|
|
1710
|
-
"No software map in this revision. The agent authors ",
|
|
1711
|
-
h("code", null, "map.yaml"),
|
|
1712
|
-
" and publishes again."
|
|
1713
|
-
)
|
|
1714
|
-
);
|
|
1715
|
-
return;
|
|
1716
|
-
}
|
|
1717
|
-
const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
|
|
1718
|
-
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) {
|
|
1719
1806
|
const all = /* @__PURE__ */ new Map();
|
|
1720
1807
|
for (const n of map.head.nodes)
|
|
1721
1808
|
all.set(n.id, {
|
|
@@ -1723,145 +1810,262 @@
|
|
|
1723
1810
|
status: map.diff.added.includes(n.id) ? "added" : map.diff.changed.includes(n.id) ? "changed" : ""
|
|
1724
1811
|
});
|
|
1725
1812
|
for (const id of map.diff.removed) {
|
|
1726
|
-
const n =
|
|
1813
|
+
const n = (map.base?.nodes ?? []).find((x) => x.id === id);
|
|
1727
1814
|
if (n) all.set(id, { ...n, status: "removed" });
|
|
1728
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) {
|
|
1729
1864
|
const edges = [...map.head.edges];
|
|
1730
1865
|
for (const e of map.base?.edges ?? [])
|
|
1731
1866
|
if (!edges.some((x) => x.from === e.from && x.to === e.to)) edges.push(e);
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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;
|
|
1740
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) : "";
|
|
1741
1905
|
const canvas = h("div", { class: "map-canvas" });
|
|
1742
1906
|
const side2 = h("div", { class: "map-side" });
|
|
1743
1907
|
root.appendChild(h("div", { class: "map-layout" }, canvas, side2));
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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"),
|
|
1749
1923
|
h(
|
|
1750
|
-
"
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
"
|
|
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
|
+
)
|
|
1758
1947
|
)
|
|
1759
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"));
|
|
1760
1954
|
parts.forEach((_, i) => {
|
|
1761
1955
|
const id = parts.slice(0, i + 1).join(".");
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
h(
|
|
1765
|
-
"span",
|
|
1766
|
-
{
|
|
1767
|
-
onclick: () => {
|
|
1768
|
-
level = id;
|
|
1769
|
-
draw();
|
|
1770
|
-
}
|
|
1771
|
-
},
|
|
1772
|
-
all.get(id)?.label ?? id
|
|
1773
|
-
)
|
|
1774
|
-
);
|
|
1956
|
+
el.appendChild(document.createTextNode(" \u203A "));
|
|
1957
|
+
el.appendChild(h("span", { onclick: () => openLevel(id) }, all.get(id)?.label ?? id));
|
|
1775
1958
|
});
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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(
|
|
1779
1966
|
"div",
|
|
1780
|
-
{
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
h("span", { class:
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
h(
|
|
1795
|
-
"
|
|
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",
|
|
1796
1983
|
{
|
|
1797
|
-
class:
|
|
1798
|
-
onclick: () => {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
ondblclick: () => {
|
|
1802
|
-
if (nk) {
|
|
1803
|
-
level = n.id;
|
|
1804
|
-
draw();
|
|
1805
|
-
}
|
|
1984
|
+
class: "small drill",
|
|
1985
|
+
onclick: (e) => {
|
|
1986
|
+
e.stopPropagation();
|
|
1987
|
+
openLevel(n.id);
|
|
1806
1988
|
}
|
|
1807
1989
|
},
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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)
|
|
1826
2012
|
);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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));
|
|
1832
2022
|
const top = (id) => {
|
|
1833
2023
|
let x = id;
|
|
1834
2024
|
while (x && !here.has(x)) x = parentOf(x);
|
|
1835
2025
|
return x;
|
|
1836
2026
|
};
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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(
|
|
1841
2048
|
"div",
|
|
1842
|
-
{ class: "
|
|
1843
|
-
h(
|
|
1844
|
-
|
|
1845
|
-
(
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
"b",
|
|
1856
|
-
{ onclick: () => navigate("map", { node: e.to }) },
|
|
1857
|
-
all.get(e.to)?.label ?? e.to
|
|
1858
|
-
),
|
|
1859
|
-
h("span", { class: "muted" }, e.label ?? "")
|
|
1860
|
-
)
|
|
1861
|
-
)
|
|
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 ?? "")
|
|
1862
2062
|
)
|
|
1863
|
-
)
|
|
1864
|
-
|
|
2063
|
+
)
|
|
2064
|
+
);
|
|
2065
|
+
};
|
|
2066
|
+
const draw = () => {
|
|
2067
|
+
canvas.innerHTML = "";
|
|
2068
|
+
append(canvas, [intro(), crumbs(), board(), links()]);
|
|
1865
2069
|
drawSide();
|
|
1866
2070
|
};
|
|
1867
2071
|
const drawSide = () => {
|
|
@@ -1872,7 +2076,7 @@
|
|
|
1872
2076
|
h(
|
|
1873
2077
|
"div",
|
|
1874
2078
|
{ class: "muted" },
|
|
1875
|
-
"
|
|
2079
|
+
"Pick a part to see what changed in it, the code behind it, and what it connects to."
|
|
1876
2080
|
)
|
|
1877
2081
|
);
|
|
1878
2082
|
return;
|
|
@@ -1880,19 +2084,14 @@
|
|
|
1880
2084
|
const anchor = n.anchor ? state.data?.document?.anchors[n.anchor] : null;
|
|
1881
2085
|
const files = map.filesByNode[n.id] ?? [];
|
|
1882
2086
|
const threads = threadsFor((t) => t.type === "map" && t.node === n.id);
|
|
2087
|
+
const neighbours = neighboursOf(map, n.id);
|
|
1883
2088
|
append(side2, [
|
|
1884
2089
|
h(
|
|
1885
2090
|
"div",
|
|
1886
2091
|
{ class: "kind muted", style: { fontSize: "11px", textTransform: "uppercase" } },
|
|
1887
2092
|
n.kind,
|
|
1888
2093
|
" ",
|
|
1889
|
-
n.status ? h(
|
|
1890
|
-
"span",
|
|
1891
|
-
{
|
|
1892
|
-
class: `badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
|
|
1893
|
-
},
|
|
1894
|
-
n.status
|
|
1895
|
-
) : ""
|
|
2094
|
+
n.status ? h("span", { class: `badge ${statusClass(n.status)}` }, n.status) : ""
|
|
1896
2095
|
),
|
|
1897
2096
|
h("h3", { style: { margin: "2px 0 6px" } }, n.label),
|
|
1898
2097
|
h("div", { class: "mono muted", style: { fontSize: "11px" } }, n.id),
|
|
@@ -1918,6 +2117,31 @@
|
|
|
1918
2117
|
(f) => h("span", { class: "file", onclick: () => navigate("files", { path: f }) }, f)
|
|
1919
2118
|
)
|
|
1920
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,
|
|
1921
2145
|
h(
|
|
1922
2146
|
"div",
|
|
1923
2147
|
{ style: { marginTop: "12px" } },
|
|
@@ -1970,7 +2194,7 @@
|
|
|
1970
2194
|
"div",
|
|
1971
2195
|
{ class: "item", onclick: () => location.href = `/review/${r.id}` },
|
|
1972
2196
|
h("span", { class: "t" }, r.title),
|
|
1973
|
-
h("span", { class: `badge ${
|
|
2197
|
+
h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
|
|
1974
2198
|
r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
|
|
1975
2199
|
h(
|
|
1976
2200
|
"span",
|
|
@@ -1990,7 +2214,7 @@
|
|
|
1990
2214
|
}
|
|
1991
2215
|
app.appendChild(el);
|
|
1992
2216
|
}
|
|
1993
|
-
function
|
|
2217
|
+
function statusClass2(s) {
|
|
1994
2218
|
return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
|
|
1995
2219
|
}
|
|
1996
2220
|
var center;
|
|
@@ -2056,7 +2280,7 @@
|
|
|
2056
2280
|
const identity = h("div", { class: "bar-row bar-identity" }, [
|
|
2057
2281
|
h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
|
|
2058
2282
|
h("span", { class: "title", title: r.title }, r.title),
|
|
2059
|
-
h("span", { class: `badge ${
|
|
2283
|
+
h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
|
|
2060
2284
|
r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
|
|
2061
2285
|
h(
|
|
2062
2286
|
"span",
|