thurview 0.7.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 +34 -15
- package/dist/cli.js +270 -15
- 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 +184 -0
- package/dist/ui/app.js +455 -54
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +31 -11
- package/skills/thurview/references/code-explainer.md +166 -0
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
|
|
|
@@ -133,6 +139,12 @@ and open it.
|
|
|
133
139
|
Click an identifier to see where it is defined at that commit; Ctrl-click
|
|
134
140
|
jumps there.
|
|
135
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.
|
|
136
148
|
- **Map**: systems, containers, components and code, with what the change
|
|
137
149
|
added, removed or touched, linked to files and code.
|
|
138
150
|
- **Threads**: _Ask now_ sends a question to the agent immediately and the
|
|
@@ -153,18 +165,19 @@ bar.
|
|
|
153
165
|
|
|
154
166
|
## CLI
|
|
155
167
|
|
|
156
|
-
| Command | Purpose
|
|
157
|
-
| --------------------------------------------------------------------- |
|
|
158
|
-
| `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins)
|
|
159
|
-
| `thurview
|
|
160
|
-
| `thurview
|
|
161
|
-
| `thurview
|
|
162
|
-
| `thurview
|
|
163
|
-
| `thurview
|
|
164
|
-
| `thurview
|
|
165
|
-
| `thurview
|
|
166
|
-
| `thurview
|
|
167
|
-
| `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 |
|
|
168
181
|
|
|
169
182
|
thurview is an [AXI](https://axi.md): built for agents that drive it through a
|
|
170
183
|
shell. Output is [TOON](https://toonformat.dev) on stdout, errors are
|
|
@@ -186,16 +199,22 @@ The agent writes three files in `~/.thurview/reviews/<id>/`:
|
|
|
186
199
|
- `data.yaml`: typed inputs: `actors`, `anchors` (file, from, to, graph),
|
|
187
200
|
`stores`, `interfaces` (a capability line per derived entry, plus the
|
|
188
201
|
interfaces the graph cannot see).
|
|
189
|
-
- `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.
|
|
190
205
|
- `theme.yaml`: the look, derived from the reviewed project's own design
|
|
191
206
|
system (tokens, fonts, shape, code palette). Empty means the default skin.
|
|
192
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
|
+
|
|
193
212
|
`thurview publish` rejects an anchor whose file or lines do not exist at the
|
|
194
213
|
pinned commit, a call stack frame that claims an added or removed call the
|
|
195
214
|
diff does not show, a storage operation on an unknown field, a map edge
|
|
196
215
|
to an unknown node, an interface annotation for a symbol the change did not
|
|
197
|
-
move,
|
|
198
|
-
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
|
|
199
218
|
[skills/thurview/references](skills/thurview/references).
|
|
200
219
|
|
|
201
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"))
|
|
@@ -234,6 +239,33 @@ const TEMPLATE_THEME = `# Look of this review, derived from the reviewed project
|
|
|
234
239
|
# shape: { radius: 8px, bevel: false, glow: false, scanlines: false, headingTransform: none }
|
|
235
240
|
# code: { keyword: "#7c3aed", string: "#15803d", function: "#b45309", variable: "#0369a1", comment: "#9ca3af" }
|
|
236
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
|
+
`;
|
|
237
269
|
// ---- commands ----
|
|
238
270
|
const SPECS = {
|
|
239
271
|
scaffold: {
|
|
@@ -254,8 +286,25 @@ const SPECS = {
|
|
|
254
286
|
"thurview scaffold --update --review <id>",
|
|
255
287
|
],
|
|
256
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
|
+
},
|
|
257
306
|
info: {
|
|
258
|
-
description: "Reviews bound to this worktree (or
|
|
307
|
+
description: "Reviews and explainers bound to this worktree (or all of them with --all)",
|
|
259
308
|
flags: {
|
|
260
309
|
all: { kind: "boolean", help: "every review, not only this worktree" },
|
|
261
310
|
fields: {
|
|
@@ -395,6 +444,7 @@ async function homeView() {
|
|
|
395
444
|
help: [
|
|
396
445
|
"Run `thurview scaffold` to create a review of the current branch",
|
|
397
446
|
"Run `thurview scaffold --pr <number>` for a pull request",
|
|
447
|
+
"Run `thurview explain [<path>]` to explain the codebase at HEAD instead",
|
|
398
448
|
],
|
|
399
449
|
};
|
|
400
450
|
const reviews = [];
|
|
@@ -575,6 +625,119 @@ const commands = {
|
|
|
575
625
|
],
|
|
576
626
|
};
|
|
577
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
|
+
},
|
|
578
741
|
async info(args) {
|
|
579
742
|
const p = parseFlags("info", args, spec("info").flags);
|
|
580
743
|
const fields = new Set((str(p, "fields") ?? "").split(",").filter(Boolean));
|
|
@@ -646,22 +809,28 @@ const commands = {
|
|
|
646
809
|
}
|
|
647
810
|
}
|
|
648
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.
|
|
649
815
|
let interfaces = null;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
}
|
|
659
827
|
}
|
|
660
828
|
const doc = await compileDocument({
|
|
661
829
|
cwd: review.worktree,
|
|
662
830
|
pins: review.pins,
|
|
663
831
|
reviewMd,
|
|
664
832
|
dataYaml: dataYaml ?? "",
|
|
833
|
+
kind,
|
|
665
834
|
...(themeName ? { themeName } : {}),
|
|
666
835
|
interfaces,
|
|
667
836
|
});
|
|
@@ -673,6 +842,7 @@ const commands = {
|
|
|
673
842
|
pins: review.pins,
|
|
674
843
|
mapYaml,
|
|
675
844
|
anchors: doc.anchors,
|
|
845
|
+
kind,
|
|
676
846
|
});
|
|
677
847
|
diags.push(...m.diagnostics);
|
|
678
848
|
map = m.map;
|
|
@@ -695,7 +865,54 @@ const commands = {
|
|
|
695
865
|
],
|
|
696
866
|
};
|
|
697
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
|
+
}
|
|
698
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
|
+
}
|
|
699
916
|
if (review.binding.kind === "branch") {
|
|
700
917
|
const tip = await g.revParse(review.worktree, review.binding.name).catch(() => null);
|
|
701
918
|
if (tip && tip !== review.pins.head)
|
|
@@ -713,6 +930,7 @@ const commands = {
|
|
|
713
930
|
await writeJson(join(rdir, "document.json"), doc.document);
|
|
714
931
|
await writeJson(join(rdir, "map.json"), map);
|
|
715
932
|
await writeJson(join(rdir, "changes.json"), changes);
|
|
933
|
+
await writeJson(join(rdir, "coverage.json"), coverage);
|
|
716
934
|
if (themeYaml !== null)
|
|
717
935
|
await cp(join(dir, "theme.yaml"), join(rdir, "theme.yaml"));
|
|
718
936
|
await writeJson(join(rdir, "theme.json"), theme);
|
|
@@ -721,6 +939,7 @@ const commands = {
|
|
|
721
939
|
at: now(),
|
|
722
940
|
title: doc.document.title,
|
|
723
941
|
pins: review.pins,
|
|
942
|
+
kind,
|
|
724
943
|
hasMap: !!map,
|
|
725
944
|
theme: theme?.name ?? "default",
|
|
726
945
|
});
|
|
@@ -740,15 +959,26 @@ const commands = {
|
|
|
740
959
|
const out = {
|
|
741
960
|
published: {
|
|
742
961
|
id: short(review.id),
|
|
962
|
+
kind,
|
|
743
963
|
rev: n,
|
|
744
964
|
title: review.title,
|
|
745
965
|
status: review.status,
|
|
746
966
|
map: !!map,
|
|
747
|
-
|
|
967
|
+
...(kind === "explainer"
|
|
968
|
+
? { coverage: coverage ? coverage.verdict : "(unavailable)" }
|
|
969
|
+
: { interfaces: doc.document.interfaces?.verdict ?? "(unavailable)" }),
|
|
748
970
|
theme: theme?.name ?? "default",
|
|
749
971
|
url: url ?? "(server not running)",
|
|
750
972
|
},
|
|
751
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
|
+
};
|
|
752
982
|
if (rows.length)
|
|
753
983
|
out["diagnostics"] = rows;
|
|
754
984
|
if (warnings.length)
|
|
@@ -912,6 +1142,11 @@ const commands = {
|
|
|
912
1142
|
throw new AxiError("--graph must be head or base", "VALIDATION_ERROR", help);
|
|
913
1143
|
const graph = await import("./graph.js");
|
|
914
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
|
+
]);
|
|
915
1150
|
const dir = reviewDir(review.id);
|
|
916
1151
|
const at = (commit) => graph.graphAt(review.worktree, commit, dir);
|
|
917
1152
|
if (sub === "callers" || sub === "tests-for") {
|
|
@@ -980,6 +1215,25 @@ const commands = {
|
|
|
980
1215
|
],
|
|
981
1216
|
};
|
|
982
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
|
+
}
|
|
983
1237
|
return {
|
|
984
1238
|
...pins,
|
|
985
1239
|
...graph.architecture(base, head),
|
|
@@ -1240,6 +1494,7 @@ function topLevelHelp() {
|
|
|
1240
1494
|
examples: [
|
|
1241
1495
|
"thurview",
|
|
1242
1496
|
"thurview scaffold",
|
|
1497
|
+
"thurview explain src/server",
|
|
1243
1498
|
"thurview publish --view files --open",
|
|
1244
1499
|
"thurview wait",
|
|
1245
1500
|
'thurview threads reply <threadId> --body "<answer>"',
|