thurview 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -18
- package/dist/cli.js +273 -16
- package/dist/cli.js.map +1 -1
- package/dist/coverage.js +208 -0
- package/dist/coverage.js.map +1 -0
- package/dist/document/compile.js +25 -10
- package/dist/document/compile.js.map +1 -1
- package/dist/graph.js +25 -4
- package/dist/graph.js.map +1 -1
- package/dist/server/server.js +4 -2
- package/dist/server/server.js.map +1 -1
- package/dist/store.js +4 -0
- package/dist/store.js.map +1 -1
- package/dist/ui/app.css +282 -5
- package/dist/ui/app.js +718 -190
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +43 -15
- package/skills/thurview/references/code-explainer.md +166 -0
- package/skills/thurview/references/software-map.md +71 -10
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@ and serves it in your browser: the walkthrough, live code peeks, the diff,
|
|
|
9
9
|
commits, and a software map. You ask questions, leave anchored comments, and
|
|
10
10
|
approve or request changes. The agent answers and republishes.
|
|
11
11
|
|
|
12
|
+
It also explains a codebase. A **code explainer** is the same document over a
|
|
13
|
+
different unit: one pinned commit instead of a range, so a reader can see the
|
|
14
|
+
architecture well enough to spot design problems themselves. It has no diff and
|
|
15
|
+
nothing to approve, it surfaces structure rather than grading it, and it states
|
|
16
|
+
what it did not examine.
|
|
17
|
+
|
|
12
18
|
It does not review the code for you. It helps you understand it fast enough
|
|
13
19
|
to review it yourself.
|
|
14
20
|
|
|
@@ -32,10 +38,11 @@ The diff at the pinned commits, commenting on a selected line range:
|
|
|
32
38
|

|
|
34
40
|
|
|
35
|
-
The software map
|
|
41
|
+
The software map: where the change landed in the system, and what sits next to
|
|
42
|
+
it — the question the diff cannot answer:
|
|
36
43
|
|
|
37
|
-

|
|
39
46
|
|
|
40
47
|
Threads: a question the agent already answered, and a comment held for the
|
|
41
48
|
decision:
|
|
@@ -132,6 +139,12 @@ and open it.
|
|
|
132
139
|
Click an identifier to see where it is defined at that commit; Ctrl-click
|
|
133
140
|
jumps there.
|
|
134
141
|
- **Commits**: the commits between base and head.
|
|
142
|
+
- **Coverage** (explainers): every file in scope at the pinned commit, in one
|
|
143
|
+
of three states - anchored in the document, placed on the map only, or not
|
|
144
|
+
examined - with the parts of the system they belong to, the references that
|
|
145
|
+
cross between those parts, and the names defined in more than one of them.
|
|
146
|
+
Derived at publish, so what the explainer skipped is a stated fact rather
|
|
147
|
+
than something the reader has to infer.
|
|
135
148
|
- **Map**: systems, containers, components and code, with what the change
|
|
136
149
|
added, removed or touched, linked to files and code.
|
|
137
150
|
- **Threads**: _Ask now_ sends a question to the agent immediately and the
|
|
@@ -152,18 +165,19 @@ bar.
|
|
|
152
165
|
|
|
153
166
|
## CLI
|
|
154
167
|
|
|
155
|
-
| Command | Purpose
|
|
156
|
-
| --------------------------------------------------------------------- |
|
|
157
|
-
| `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins)
|
|
158
|
-
| `thurview
|
|
159
|
-
| `thurview
|
|
160
|
-
| `thurview
|
|
161
|
-
| `thurview
|
|
162
|
-
| `thurview
|
|
163
|
-
| `thurview
|
|
164
|
-
| `thurview
|
|
165
|
-
| `thurview
|
|
166
|
-
| `thurview
|
|
168
|
+
| Command | Purpose |
|
|
169
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
170
|
+
| `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
|
|
171
|
+
| `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
|
|
172
|
+
| `thurview info [--all]` | Reviews bound to this worktree |
|
|
173
|
+
| `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
|
|
174
|
+
| `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
|
|
175
|
+
| `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
|
|
176
|
+
| `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
|
|
177
|
+
| `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at the pinned commits |
|
|
178
|
+
| `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
|
|
179
|
+
| `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
|
|
180
|
+
| `thurview update` | Self-update from npm |
|
|
167
181
|
|
|
168
182
|
thurview is an [AXI](https://axi.md): built for agents that drive it through a
|
|
169
183
|
shell. Output is [TOON](https://toonformat.dev) on stdout, errors are
|
|
@@ -185,16 +199,22 @@ The agent writes three files in `~/.thurview/reviews/<id>/`:
|
|
|
185
199
|
- `data.yaml`: typed inputs: `actors`, `anchors` (file, from, to, graph),
|
|
186
200
|
`stores`, `interfaces` (a capability line per derived entry, plus the
|
|
187
201
|
interfaces the graph cannot see).
|
|
188
|
-
- `map.yaml`: the software map at head, optionally at base.
|
|
202
|
+
- `map.yaml`: the software map at head, optionally at base. In an explainer it
|
|
203
|
+
carries the breadth the prose has no room for, and a node's `files` globs are
|
|
204
|
+
what let a file count as placed rather than not examined.
|
|
189
205
|
- `theme.yaml`: the look, derived from the reviewed project's own design
|
|
190
206
|
system (tokens, fonts, shape, code palette). Empty means the default skin.
|
|
191
207
|
|
|
208
|
+
An explainer writes the same files, minus `interfaces`: there is no change to
|
|
209
|
+
derive a delta from, and `graph: base` on an anchor is an error because there
|
|
210
|
+
is one commit.
|
|
211
|
+
|
|
192
212
|
`thurview publish` rejects an anchor whose file or lines do not exist at the
|
|
193
213
|
pinned commit, a call stack frame that claims an added or removed call the
|
|
194
214
|
diff does not show, a storage operation on an unknown field, a map edge
|
|
195
215
|
to an unknown node, an interface annotation for a symbol the change did not
|
|
196
|
-
move,
|
|
197
|
-
The full format is in
|
|
216
|
+
move, a declared interface whose anchor holds no added or deleted line, and an
|
|
217
|
+
explainer that anchors nothing at all. The full format is in
|
|
198
218
|
[skills/thurview/references](skills/thurview/references).
|
|
199
219
|
|
|
200
220
|
Optional guidance for the agent: `~/.thurview/THURVIEW.md` for you,
|
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,9 @@ import { join, dirname, resolve } from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import open from "open";
|
|
11
11
|
import * as g from "./git.js";
|
|
12
|
-
import { SCHEMA, home, newId, now, readReview, writeReview, listReviews, reviewsFor, reviewDir, revisionDir, readThreads, readText, writeText, writeJson, readJson, serverStateFile, deleteReview, } from "./store.js";
|
|
13
|
-
import { compileDocument, compileMap } from "./document/compile.js";
|
|
12
|
+
import { SCHEMA, home, newId, now, readReview, writeReview, listReviews, reviewsFor, reviewDir, revisionDir, readThreads, readText, writeText, writeJson, readJson, serverStateFile, deleteReview, kindOf, } from "./store.js";
|
|
13
|
+
import { compileDocument, compileMap, globToRegExp } from "./document/compile.js";
|
|
14
|
+
import { computeCoverage, scopeGlob, scopeGraph, scopeTruncated, } from "./coverage.js";
|
|
14
15
|
import { parseTheme, compileTheme } from "./theme.js";
|
|
15
16
|
import { registerTheme } from "./highlight.js";
|
|
16
17
|
import { replyThread, setThreadStatus, needsAgent } from "./threads.js";
|
|
@@ -19,7 +20,7 @@ import { parseFlags, helpFor, str, bool } from "./flags.js";
|
|
|
19
20
|
import { VERSION } from "./version.js";
|
|
20
21
|
const execFileP = promisify(execFile);
|
|
21
22
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const DESCRIPTION = "Guided, evidence-anchored reviews of
|
|
23
|
+
const DESCRIPTION = "Guided, evidence-anchored reviews of a change and explainers of a codebase, read and answered in the browser";
|
|
23
24
|
function note(msg) {
|
|
24
25
|
process.stderr.write(msg + "\n");
|
|
25
26
|
}
|
|
@@ -86,6 +87,7 @@ async function reviewRow(r, fields) {
|
|
|
86
87
|
const t = await readThreads(r.id);
|
|
87
88
|
const row = {
|
|
88
89
|
id: short(r.id),
|
|
90
|
+
kind: kindOf(r),
|
|
89
91
|
title: r.title,
|
|
90
92
|
status: r.status,
|
|
91
93
|
rev: r.revision,
|
|
@@ -95,7 +97,10 @@ async function reviewRow(r, fields) {
|
|
|
95
97
|
if (fields.has("all") || fields.has("binding"))
|
|
96
98
|
row["binding"] = r.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name;
|
|
97
99
|
if (fields.has("all") || fields.has("pins"))
|
|
98
|
-
row["pins"] =
|
|
100
|
+
row["pins"] =
|
|
101
|
+
kindOf(r) === "explainer"
|
|
102
|
+
? r.pins.head.slice(0, 12)
|
|
103
|
+
: `${r.pins.base.slice(0, 12)}..${r.pins.head.slice(0, 12)}`;
|
|
99
104
|
if (fields.has("all") || fields.has("worktree"))
|
|
100
105
|
row["worktree"] = r.worktree;
|
|
101
106
|
if (fields.has("all") || fields.has("inSync"))
|
|
@@ -216,7 +221,9 @@ anchors: {}
|
|
|
216
221
|
stores: {}
|
|
217
222
|
interfaces: {}
|
|
218
223
|
`;
|
|
219
|
-
const TEMPLATE_MAP = `# Software map:
|
|
224
|
+
const TEMPLATE_MAP = `# Software map: where this change landed in the system, and what sits next to it.
|
|
225
|
+
# People, systems, containers, components, code. Leave nodes empty when the change
|
|
226
|
+
# lands in one place and the Files tab already answers that.
|
|
220
227
|
nodes: []
|
|
221
228
|
edges: []
|
|
222
229
|
`;
|
|
@@ -232,6 +239,33 @@ const TEMPLATE_THEME = `# Look of this review, derived from the reviewed project
|
|
|
232
239
|
# shape: { radius: 8px, bevel: false, glow: false, scanlines: false, headingTransform: none }
|
|
233
240
|
# code: { keyword: "#7c3aed", string: "#15803d", function: "#b45309", variable: "#0369a1", comment: "#9ca3af" }
|
|
234
241
|
`;
|
|
242
|
+
const TEMPLATE_EXPLAIN_MD = (title) => `# ${title}
|
|
243
|
+
|
|
244
|
+
**Summary**
|
|
245
|
+
|
|
246
|
+
- The agent is still writing this explainer. The Coverage tab already states
|
|
247
|
+
what it has and has not examined at the pinned commit; this page offers the
|
|
248
|
+
new revision when the walkthrough lands.
|
|
249
|
+
`;
|
|
250
|
+
const TEMPLATE_EXPLAIN_DATA = `# Typed inputs for the explainer: actors, anchors and stores. An explainer has
|
|
251
|
+
# one pinned commit, so every anchor reads that commit and \`graph: base\` is an
|
|
252
|
+
# error. It has no interface delta: there is no change to take one from.
|
|
253
|
+
#
|
|
254
|
+
# anchors:
|
|
255
|
+
# dispatch:
|
|
256
|
+
# title: where a request picks its handler
|
|
257
|
+
# peek: { file: src/server/router.ts, from: 41, to: 58 }
|
|
258
|
+
actors: {}
|
|
259
|
+
anchors: {}
|
|
260
|
+
stores: {}
|
|
261
|
+
`;
|
|
262
|
+
const TEMPLATE_EXPLAIN_MAP = `# The structure of the code at the pinned commit: systems, containers,
|
|
263
|
+
# components, code. The map carries breadth so the prose can carry depth, and a
|
|
264
|
+
# node's \`files\` globs are what tell the Coverage tab a file was at least placed.
|
|
265
|
+
# Seed it from \`thurview graph architecture\`.
|
|
266
|
+
nodes: []
|
|
267
|
+
edges: []
|
|
268
|
+
`;
|
|
235
269
|
// ---- commands ----
|
|
236
270
|
const SPECS = {
|
|
237
271
|
scaffold: {
|
|
@@ -252,8 +286,25 @@ const SPECS = {
|
|
|
252
286
|
"thurview scaffold --update --review <id>",
|
|
253
287
|
],
|
|
254
288
|
},
|
|
289
|
+
explain: {
|
|
290
|
+
description: "Create a code explainer pinned to one commit: a whole codebase, or one subsystem of it",
|
|
291
|
+
args: "[<path scope>]",
|
|
292
|
+
flags: {
|
|
293
|
+
commit: { kind: "string", help: "commit to pin (default: HEAD)" },
|
|
294
|
+
title: { kind: "string", help: "initial title" },
|
|
295
|
+
new: { kind: "boolean", help: "create another explainer even if one matches the scope" },
|
|
296
|
+
update: { kind: "boolean", help: "re-pin an existing explainer to a new commit" },
|
|
297
|
+
review: { kind: "string", help: "explainer to update (id prefix)" },
|
|
298
|
+
},
|
|
299
|
+
examples: [
|
|
300
|
+
"thurview explain",
|
|
301
|
+
"thurview explain src/server",
|
|
302
|
+
"thurview explain --commit v1.2.0",
|
|
303
|
+
"thurview explain --update --review <id>",
|
|
304
|
+
],
|
|
305
|
+
},
|
|
255
306
|
info: {
|
|
256
|
-
description: "Reviews bound to this worktree (or
|
|
307
|
+
description: "Reviews and explainers bound to this worktree (or all of them with --all)",
|
|
257
308
|
flags: {
|
|
258
309
|
all: { kind: "boolean", help: "every review, not only this worktree" },
|
|
259
310
|
fields: {
|
|
@@ -393,6 +444,7 @@ async function homeView() {
|
|
|
393
444
|
help: [
|
|
394
445
|
"Run `thurview scaffold` to create a review of the current branch",
|
|
395
446
|
"Run `thurview scaffold --pr <number>` for a pull request",
|
|
447
|
+
"Run `thurview explain [<path>]` to explain the codebase at HEAD instead",
|
|
396
448
|
],
|
|
397
449
|
};
|
|
398
450
|
const reviews = [];
|
|
@@ -573,6 +625,119 @@ const commands = {
|
|
|
573
625
|
],
|
|
574
626
|
};
|
|
575
627
|
},
|
|
628
|
+
async explain(args) {
|
|
629
|
+
const p = parseFlags("explain", args, spec("explain").flags, 1);
|
|
630
|
+
const cwd = process.cwd();
|
|
631
|
+
const worktree = await worktreeOf(cwd);
|
|
632
|
+
if (!worktree)
|
|
633
|
+
throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
|
|
634
|
+
"Run `thurview explain` inside the source worktree",
|
|
635
|
+
]);
|
|
636
|
+
const existing = bool(p, "update") || str(p, "review") ? await resolveReview(str(p, "review")) : null;
|
|
637
|
+
if (existing && kindOf(existing) !== "explainer")
|
|
638
|
+
throw new AxiError(`${short(existing.id)} is a review, not an explainer`, "VALIDATION_ERROR", [
|
|
639
|
+
"Run `thurview scaffold --update --review <id>` to re-pin a review",
|
|
640
|
+
"Run `thurview explain` with no --review to start an explainer",
|
|
641
|
+
]);
|
|
642
|
+
const scope = scopeGlob(p.positional[0] ?? existing?.binding.name);
|
|
643
|
+
let commit;
|
|
644
|
+
try {
|
|
645
|
+
commit = await g.revParse(worktree, str(p, "commit") ?? "HEAD");
|
|
646
|
+
}
|
|
647
|
+
catch (e) {
|
|
648
|
+
throw new AxiError(e.message, "VALIDATION_ERROR", [
|
|
649
|
+
"Pass a resolvable ref: `thurview explain --commit <ref>`",
|
|
650
|
+
]);
|
|
651
|
+
}
|
|
652
|
+
// A scope that matches nothing is a typo, and an explainer of nothing would
|
|
653
|
+
// still publish and still state honest-looking coverage of zero files.
|
|
654
|
+
const files = await g.listFiles(worktree, commit);
|
|
655
|
+
const inScope = scope === "**" ? files : files.filter((f) => globToRegExp(scope).test(f));
|
|
656
|
+
if (!inScope.length)
|
|
657
|
+
throw new AxiError(`no file matches "${scope}" at ${commit.slice(0, 12)}`, "VALIDATION_ERROR", [
|
|
658
|
+
"Pass a path that exists at that commit: `thurview explain src/server`",
|
|
659
|
+
"Run `thurview explain` with no scope for the whole repository",
|
|
660
|
+
]);
|
|
661
|
+
const binding = { kind: "codebase", name: scope };
|
|
662
|
+
const defaultTitle = scope === "**" ? worktree.split("/").pop() || "Codebase" : scope.replace(/\/\*\*$/, "");
|
|
663
|
+
let review;
|
|
664
|
+
let reused = false;
|
|
665
|
+
if (existing) {
|
|
666
|
+
review = existing;
|
|
667
|
+
review.pins = { base: commit, head: commit };
|
|
668
|
+
review.binding = binding;
|
|
669
|
+
if (str(p, "title"))
|
|
670
|
+
review.title = str(p, "title");
|
|
671
|
+
await writeReview(review);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
const match = bool(p, "new")
|
|
675
|
+
? []
|
|
676
|
+
: (await reviewsFor(worktree)).filter((r) => kindOf(r) === "explainer" &&
|
|
677
|
+
r.binding.name === scope &&
|
|
678
|
+
r.status !== "accepted" &&
|
|
679
|
+
r.status !== "closed");
|
|
680
|
+
if (match.length) {
|
|
681
|
+
review = match[0];
|
|
682
|
+
reused = true;
|
|
683
|
+
review.pins = { base: commit, head: commit };
|
|
684
|
+
await writeReview(review);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const id = newId();
|
|
688
|
+
review = {
|
|
689
|
+
schema: SCHEMA,
|
|
690
|
+
id,
|
|
691
|
+
kind: "explainer",
|
|
692
|
+
title: str(p, "title") || defaultTitle,
|
|
693
|
+
worktree,
|
|
694
|
+
repoRoot: worktree,
|
|
695
|
+
binding,
|
|
696
|
+
pins: { base: commit, head: commit },
|
|
697
|
+
status: "draft",
|
|
698
|
+
revision: 0,
|
|
699
|
+
dismissed: false,
|
|
700
|
+
createdAt: now(),
|
|
701
|
+
updatedAt: now(),
|
|
702
|
+
};
|
|
703
|
+
await mkdir(reviewDir(id), { recursive: true });
|
|
704
|
+
await writeText(join(reviewDir(id), "review.md"), TEMPLATE_EXPLAIN_MD(review.title));
|
|
705
|
+
await writeText(join(reviewDir(id), "data.yaml"), TEMPLATE_EXPLAIN_DATA);
|
|
706
|
+
await writeText(join(reviewDir(id), "map.yaml"), TEMPLATE_EXPLAIN_MAP);
|
|
707
|
+
await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
|
|
708
|
+
await writeReview(review);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const dir = reviewDir(review.id);
|
|
712
|
+
return {
|
|
713
|
+
explainer: {
|
|
714
|
+
id: short(review.id),
|
|
715
|
+
uuid: review.id,
|
|
716
|
+
kind: "explainer",
|
|
717
|
+
title: review.title,
|
|
718
|
+
status: review.status,
|
|
719
|
+
rev: review.revision,
|
|
720
|
+
scope,
|
|
721
|
+
commit,
|
|
722
|
+
worktree,
|
|
723
|
+
reused,
|
|
724
|
+
dir,
|
|
725
|
+
},
|
|
726
|
+
files: {
|
|
727
|
+
document: join(dir, "review.md"),
|
|
728
|
+
data: join(dir, "data.yaml"),
|
|
729
|
+
map: join(dir, "map.yaml"),
|
|
730
|
+
theme: join(dir, "theme.yaml"),
|
|
731
|
+
},
|
|
732
|
+
scale: { filesInScope: inScope.length },
|
|
733
|
+
guidance: await guidanceFiles(worktree),
|
|
734
|
+
help: [
|
|
735
|
+
`Run \`thurview graph architecture --review ${short(review.id)}\` for the clusters, their hubs and the links between them`,
|
|
736
|
+
`Author ${join(dir, "map.yaml")} first: it carries the breadth the prose cannot`,
|
|
737
|
+
`Edit ${join(dir, "review.md")} and data.yaml, then run \`thurview publish --review ${short(review.id)}\``,
|
|
738
|
+
],
|
|
739
|
+
};
|
|
740
|
+
},
|
|
576
741
|
async info(args) {
|
|
577
742
|
const p = parseFlags("info", args, spec("info").flags);
|
|
578
743
|
const fields = new Set((str(p, "fields") ?? "").split(",").filter(Boolean));
|
|
@@ -644,22 +809,28 @@ const commands = {
|
|
|
644
809
|
}
|
|
645
810
|
}
|
|
646
811
|
const themeName = theme ? await registerTheme(theme.shiki) : undefined;
|
|
812
|
+
const kind = kindOf(review);
|
|
813
|
+
// An explainer has one pinned commit, so there is no delta to derive: the
|
|
814
|
+
// panel above its document states coverage instead.
|
|
647
815
|
let interfaces = null;
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
816
|
+
if (kind === "review") {
|
|
817
|
+
try {
|
|
818
|
+
interfaces = await deltaFor(review);
|
|
819
|
+
}
|
|
820
|
+
catch (e) {
|
|
821
|
+
diags.push({
|
|
822
|
+
level: "warning",
|
|
823
|
+
file: "review.md",
|
|
824
|
+
message: `the interface delta is unavailable: ${e.message}`,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
657
827
|
}
|
|
658
828
|
const doc = await compileDocument({
|
|
659
829
|
cwd: review.worktree,
|
|
660
830
|
pins: review.pins,
|
|
661
831
|
reviewMd,
|
|
662
832
|
dataYaml: dataYaml ?? "",
|
|
833
|
+
kind,
|
|
663
834
|
...(themeName ? { themeName } : {}),
|
|
664
835
|
interfaces,
|
|
665
836
|
});
|
|
@@ -671,6 +842,7 @@ const commands = {
|
|
|
671
842
|
pins: review.pins,
|
|
672
843
|
mapYaml,
|
|
673
844
|
anchors: doc.anchors,
|
|
845
|
+
kind,
|
|
674
846
|
});
|
|
675
847
|
diags.push(...m.diagnostics);
|
|
676
848
|
map = m.map;
|
|
@@ -693,7 +865,54 @@ const commands = {
|
|
|
693
865
|
],
|
|
694
866
|
};
|
|
695
867
|
}
|
|
868
|
+
// Coverage is derived, not claimed: every file in scope at the pinned commit
|
|
869
|
+
// is accounted for, so the reader is told what the prose never reached.
|
|
870
|
+
let coverage = null;
|
|
871
|
+
if (kind === "explainer") {
|
|
872
|
+
try {
|
|
873
|
+
const graph = await import("./graph.js");
|
|
874
|
+
const g0 = await graph.graphAt(review.worktree, review.pins.head, dir);
|
|
875
|
+
coverage = computeCoverage({
|
|
876
|
+
commit: review.pins.head,
|
|
877
|
+
scope: review.binding.name,
|
|
878
|
+
allFiles: await g.listFiles(review.worktree, review.pins.head),
|
|
879
|
+
graph: g0,
|
|
880
|
+
anchored: Object.values(doc.document.anchors)
|
|
881
|
+
.map((a) => a.peek?.file)
|
|
882
|
+
.filter((f) => !!f),
|
|
883
|
+
owners: (map?.head.nodes ?? [])
|
|
884
|
+
.filter((n) => n.files?.length)
|
|
885
|
+
.map((n) => ({ node: n.id, globs: n.files })),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
diags.push({
|
|
890
|
+
level: "error",
|
|
891
|
+
file: "review.md",
|
|
892
|
+
message: `coverage is unavailable, so the explainer cannot state what it skipped: ${e.message}`,
|
|
893
|
+
});
|
|
894
|
+
process.exitCode = 1;
|
|
895
|
+
return {
|
|
896
|
+
error: "publish failed: coverage could not be derived",
|
|
897
|
+
code: "PUBLISH_FAILED",
|
|
898
|
+
diagnostics: diags.map((d) => ({
|
|
899
|
+
level: d.level,
|
|
900
|
+
file: d.file,
|
|
901
|
+
line: d.line ?? "",
|
|
902
|
+
message: d.message,
|
|
903
|
+
})),
|
|
904
|
+
help: [`Run \`thurview publish --review ${short(review.id)}\` again`],
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
}
|
|
696
908
|
const warnings = [];
|
|
909
|
+
if (kind === "explainer" && !map)
|
|
910
|
+
warnings.push("this explainer has no map, so every file it does not anchor counts as not examined; author map.yaml to place the rest");
|
|
911
|
+
if (review.binding.kind === "codebase") {
|
|
912
|
+
const tip = await g.revParse(review.worktree, "HEAD").catch(() => null);
|
|
913
|
+
if (tip && tip !== review.pins.head)
|
|
914
|
+
warnings.push(`HEAD has moved past the pinned commit; run \`thurview explain --update --review ${short(review.id)}\` to re-pin`);
|
|
915
|
+
}
|
|
697
916
|
if (review.binding.kind === "branch") {
|
|
698
917
|
const tip = await g.revParse(review.worktree, review.binding.name).catch(() => null);
|
|
699
918
|
if (tip && tip !== review.pins.head)
|
|
@@ -711,6 +930,7 @@ const commands = {
|
|
|
711
930
|
await writeJson(join(rdir, "document.json"), doc.document);
|
|
712
931
|
await writeJson(join(rdir, "map.json"), map);
|
|
713
932
|
await writeJson(join(rdir, "changes.json"), changes);
|
|
933
|
+
await writeJson(join(rdir, "coverage.json"), coverage);
|
|
714
934
|
if (themeYaml !== null)
|
|
715
935
|
await cp(join(dir, "theme.yaml"), join(rdir, "theme.yaml"));
|
|
716
936
|
await writeJson(join(rdir, "theme.json"), theme);
|
|
@@ -719,6 +939,7 @@ const commands = {
|
|
|
719
939
|
at: now(),
|
|
720
940
|
title: doc.document.title,
|
|
721
941
|
pins: review.pins,
|
|
942
|
+
kind,
|
|
722
943
|
hasMap: !!map,
|
|
723
944
|
theme: theme?.name ?? "default",
|
|
724
945
|
});
|
|
@@ -738,15 +959,26 @@ const commands = {
|
|
|
738
959
|
const out = {
|
|
739
960
|
published: {
|
|
740
961
|
id: short(review.id),
|
|
962
|
+
kind,
|
|
741
963
|
rev: n,
|
|
742
964
|
title: review.title,
|
|
743
965
|
status: review.status,
|
|
744
966
|
map: !!map,
|
|
745
|
-
|
|
967
|
+
...(kind === "explainer"
|
|
968
|
+
? { coverage: coverage ? coverage.verdict : "(unavailable)" }
|
|
969
|
+
: { interfaces: doc.document.interfaces?.verdict ?? "(unavailable)" }),
|
|
746
970
|
theme: theme?.name ?? "default",
|
|
747
971
|
url: url ?? "(server not running)",
|
|
748
972
|
},
|
|
749
973
|
};
|
|
974
|
+
if (coverage)
|
|
975
|
+
out["notExamined"] = {
|
|
976
|
+
files: coverage.states.uncovered,
|
|
977
|
+
first: coverage.uncovered.slice(0, 8),
|
|
978
|
+
byPart: coverage.clusters
|
|
979
|
+
.filter((c) => c.uncovered.length)
|
|
980
|
+
.map((c) => ({ part: c.label, files: c.uncovered.length })),
|
|
981
|
+
};
|
|
750
982
|
if (rows.length)
|
|
751
983
|
out["diagnostics"] = rows;
|
|
752
984
|
if (warnings.length)
|
|
@@ -910,6 +1142,11 @@ const commands = {
|
|
|
910
1142
|
throw new AxiError("--graph must be head or base", "VALIDATION_ERROR", help);
|
|
911
1143
|
const graph = await import("./graph.js");
|
|
912
1144
|
const review = await resolveReview(str(p, "review"));
|
|
1145
|
+
if (kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
|
|
1146
|
+
throw new AxiError(`graph ${sub} compares two commits; an explainer is pinned to one`, "VALIDATION_ERROR", [
|
|
1147
|
+
`Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
|
|
1148
|
+
`Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
|
|
1149
|
+
]);
|
|
913
1150
|
const dir = reviewDir(review.id);
|
|
914
1151
|
const at = (commit) => graph.graphAt(review.worktree, commit, dir);
|
|
915
1152
|
if (sub === "callers" || sub === "tests-for") {
|
|
@@ -978,6 +1215,25 @@ const commands = {
|
|
|
978
1215
|
],
|
|
979
1216
|
};
|
|
980
1217
|
}
|
|
1218
|
+
// An explainer is scoped to a path, so its structure is that path's, not the
|
|
1219
|
+
// repository's: the same bound the Coverage tab accounts for.
|
|
1220
|
+
if (kindOf(review) === "explainer") {
|
|
1221
|
+
const scope = review.binding.name;
|
|
1222
|
+
const g0 = scopeGraph(head, scope);
|
|
1223
|
+
const allFiles = await g.listFiles(review.worktree, review.pins.head);
|
|
1224
|
+
const { diff: _diff, truncated: _truncated, ...rest } = graph.architecture(g0, g0);
|
|
1225
|
+
return {
|
|
1226
|
+
commit: short(head.commit),
|
|
1227
|
+
scope,
|
|
1228
|
+
languages: pins.languages,
|
|
1229
|
+
truncated: scopeTruncated(allFiles, head, scope),
|
|
1230
|
+
...rest,
|
|
1231
|
+
help: [
|
|
1232
|
+
"Seed map.yaml nodes from communities, their `files` from a community's files, and edges from edges",
|
|
1233
|
+
"A file in no community is outside the languages the graph reads; `thurview publish` counts those",
|
|
1234
|
+
],
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
981
1237
|
return {
|
|
982
1238
|
...pins,
|
|
983
1239
|
...graph.architecture(base, head),
|
|
@@ -1238,6 +1494,7 @@ function topLevelHelp() {
|
|
|
1238
1494
|
examples: [
|
|
1239
1495
|
"thurview",
|
|
1240
1496
|
"thurview scaffold",
|
|
1497
|
+
"thurview explain src/server",
|
|
1241
1498
|
"thurview publish --view files --open",
|
|
1242
1499
|
"thurview wait",
|
|
1243
1500
|
'thurview threads reply <threadId> --body "<answer>"',
|