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 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 info [--all]` | Reviews bound to this worktree |
160
- | `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
161
- | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
162
- | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
163
- | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
164
- | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at the pinned commits |
165
- | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
166
- | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
167
- | `thurview update` | Self-update from npm |
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, and a declared interface whose anchor holds no added or deleted line.
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 agent-written code, read and answered in the browser";
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"] = `${r.pins.base.slice(0, 12)}..${r.pins.head.slice(0, 12)}`;
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 every review with --all)",
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
- try {
651
- interfaces = await deltaFor(review);
652
- }
653
- catch (e) {
654
- diags.push({
655
- level: "warning",
656
- file: "review.md",
657
- message: `the interface delta is unavailable: ${e.message}`,
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
- interfaces: doc.document.interfaces?.verdict ?? "(unavailable)",
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>"',