thurview 0.17.2 → 0.19.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
@@ -23,13 +23,14 @@ _The reader's half of it; the agent is working off camera._
23
23
  agent — at once if one is listening, queued if not — and the answer lands
24
24
  in the same thread; a comment waits for your decision. Then you approve the
25
25
  change or send it back with the comments attached.
26
- - **A code graph answers what the diff cannot.** Who calls the symbol that
27
- moved, which tests reach it, where the change landed in the system and what
28
- sits next to it. thurview builds the graph itself with tree-sitter from the
29
- pinned commits, for TypeScript, JavaScript, Python, Go, Rust, Java and Elixir.
26
+ - **The agent searches what the diff cannot show.** Who calls the symbol that
27
+ moved, which tests reach it, what imports the module it touched: the skills
28
+ give the agent `git grep` recipes run at the pinned commits, in any language,
29
+ and every claim that rests on a search carries the search, so you can run it
30
+ again.
30
31
  - **The evidence is checked, not taken on trust.** An anchor whose lines
31
32
  do not exist at the pinned commit, a call stack frame asserting a call the
32
- diff does not show, an interface annotation for a symbol the change never
33
+ diff does not show, an interface entry anchored on lines the change never
33
34
  moved, a trust boundary crossing that resolves to nothing — publishing
34
35
  rejects each one rather than rendering it.
35
36
 
@@ -115,9 +116,8 @@ Install the command itself, rather than leaving the skills to reach it through
115
116
  npm install -g thurview # or: pnpm add -g thurview
116
117
  ```
117
118
 
118
- Nothing else needs installing: the code graph is built from the pinned commits
119
- with tree-sitter. `thurview graph` answers which interfaces the change moved,
120
- what it reaches, who calls a symbol, what tests cover it and how files cluster.
119
+ Nothing else needs installing beyond `git`: callers, tests and importers are
120
+ found by the agent's own search at the pinned commits.
121
121
 
122
122
  To run from a checkout instead:
123
123
 
@@ -224,13 +224,21 @@ Approve, or send it back with the comments:
224
224
  ![The submit dialog, one pending comment, Approve or Request
225
225
  changes](./media/review-decision.png)
226
226
 
227
+ To share a document with someone who will not run thurview,
228
+ `thurview export --out review.html` writes the published revision as one HTML
229
+ file: the same page, read only, with every snippet, diagram and diff resolved
230
+ at the pinned commits and the reader's threads shown as notes (`--no-threads`
231
+ leaves them out). It opens from disk, fetches nothing, and names no server and
232
+ no local path. Point `--out` at a folder for `<folder>/index.html`, such as a
233
+ repository's Pages folder.
234
+
227
235
  ## What's in a review
228
236
 
229
237
  - **Interface delta**: above the document, what the change added to, changed
230
238
  in or removed from the surfaces other code can reach - exported functions
231
- and types, plus the CLI flags, routes, config keys and formats the agent
232
- declares. Derived from the code graph at both pinned commits, so a change
233
- that moved no surface says exactly that instead of inventing a feature.
239
+ and types, CLI flags, routes, config keys and formats. The agent declares
240
+ each one, and publish holds it to an anchor on lines the diff really moved,
241
+ so an entry cannot invent a feature the change did not deliver.
234
242
  - **Review**: the document with a table of contents. Anchor links open the
235
243
  exact code beside the text; peeks show it inline. Sequence diagrams, call
236
244
  stack diffs and storage views are clickable down to the line.
@@ -240,11 +248,11 @@ changes](./media/review-decision.png)
240
248
  jumps there.
241
249
  - **Commits**: the commits between base and head.
242
250
  - **Coverage** (explainers): every file in scope at the pinned commit, in one
243
- of three states - anchored in the document, placed on the map only, or not
244
- examined - with the parts of the system they belong to, the references that
245
- cross between those parts, and the names defined in more than one of them.
246
- Derived at publish, so what the explainer skipped is a stated fact rather
247
- than something the reader has to infer.
251
+ of four states - anchored in the document, placed on the map only, matched
252
+ by a search the agent recorded, or not examined - grouped by directory, with
253
+ each search and what it matched. Derived at publish, which re-runs every
254
+ recorded search with `git grep` at the pinned commit, so what the explainer
255
+ skipped is a stated fact rather than something the reader has to infer.
248
256
  - **Map**: systems, containers, components and code, with what the change
249
257
  added, removed or touched, linked to files and code.
250
258
  - **Threads**: _Send to the agent_ delivers a question at once and the answer
@@ -268,21 +276,21 @@ bar.
268
276
 
269
277
  ## CLI
270
278
 
271
- | Command | Purpose |
272
- | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
273
- | `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
274
- | `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
275
- | `thurview design [<path>] [--commit R]` | Create a design of what to build, pinned to the commit it argues from |
276
- | `thurview info [--all]` | Reviews, explainers and designs bound to this worktree |
277
- | `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
278
- | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
279
- | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
280
- | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
281
- | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
282
- | `thurview forge status\|prior\|pass\|submit\|reply` | Read a change request through its forge, and post the review back |
283
- | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
284
- | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
285
- | `thurview update` | Self-update from npm |
279
+ | Command | Purpose |
280
+ | ------------------------------------------------------- | --------------------------------------------------------------------- |
281
+ | `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
282
+ | `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
283
+ | `thurview design [<path>] [--commit R]` | Create a design of what to build, pinned to the commit it argues from |
284
+ | `thurview info [--all]` | Reviews, explainers and designs bound to this worktree |
285
+ | `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
286
+ | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
287
+ | `thurview export --review ID --out PATH [--no-threads]` | Write the published revision as one static HTML file, no server |
288
+ | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
289
+ | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
290
+ | `thurview forge status\|prior\|pass\|submit\|reply` | Read a change request through its forge, and post the review back |
291
+ | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
292
+ | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
293
+ | `thurview update` | Self-update from npm |
286
294
 
287
295
  thurview is an [AXI](https://axi.md): built for agents that drive it through a
288
296
  shell. Output is [TOON](https://toonformat.dev) on stdout, errors are
@@ -298,19 +306,19 @@ stderr.
298
306
 
299
307
  The `thurview-fix` skill reviews a branch, a commit range or a pull or merge
300
308
  request, fixes what it is sure of and reports the rest, with no browser and no
301
- approval step. For each changed symbol it asks the code graph who calls it and
302
- which tests reach it, so a finding can name a caller the diff never shows.
309
+ approval step. For each changed symbol it searches, at the pinned commits, who
310
+ calls it and which tests name it, so a finding can name a caller the diff never
311
+ shows.
303
312
  Fixes that pass the repository's own tests and lint land as one local commit;
304
313
  nothing is pushed unless you ask.
305
314
 
306
315
  ```sh
307
- thurview graph impact --head HEAD # changed symbols, the callers they left alone, the tests
308
- thurview graph callers discount --head HEAD # every call site of one symbol
316
+ git grep -n -E -e '\bdiscount *\(' <head> -- # every call site of one symbol
317
+ git grep -n -E -e '\bdiscount\b' <head> -- '*test*' '*spec*' # the tests that name it
309
318
  ```
310
319
 
311
- `--base` and `--head` ask about two commits directly; `--head` alone diffs
312
- from where it forked from trunk. Each caller in `impact.reach` carries the line
313
- of its call and whether any test reaches it.
320
+ Each finding carries the search behind it, so "no other caller" is a line you
321
+ can run.
314
322
 
315
323
  With `--post`, the skill posts the findings it did not fix as inline comments
316
324
  on the change request, through `thurview forge`:
@@ -358,9 +366,10 @@ The agent writes three files in `~/.thurview/reviews/<id>/`:
358
366
  blocks `peek`, `sequence`, `flow`, `callstack` and `database` add components.
359
367
  `## Heading {collapsed}` folds a section by default.
360
368
  - `data.yaml`: typed inputs: `actors`, `anchors` (file, from, to, graph),
361
- `stores`, `interfaces` (a capability line per derived entry, plus the
362
- interfaces the graph cannot see), and `security`, where a review says where
363
- the change lets input cross a trust boundary.
369
+ `stores`, `interfaces` (one entry per interface the change moved, each
370
+ anchored on the lines that moved it), `security`, where a review says where
371
+ the change lets input cross a trust boundary, and `searches`, where an
372
+ explainer records what it searched for its Coverage tab.
364
373
  - `map.yaml`: the software map at head, optionally at base. In an explainer it
365
374
  carries the breadth the prose has no room for, and a node's `files` globs are
366
375
  what let a file count as placed rather than not examined.
@@ -377,14 +386,14 @@ defined in one place — the `thurview-fix` skill's finding rules — and
377
386
  nothing else restates it.
378
387
 
379
388
  An explainer writes the same files, minus `interfaces` and `security`: there is
380
- no change to derive a delta from or to carry input across a boundary, and
389
+ no change to take a delta from or to carry input across a boundary, and
381
390
  `graph: base` on an anchor is an error because there is one commit.
382
391
 
383
392
  A design writes the same files, and `interfaces` means something else in it:
384
393
  each entry is a **proposal** — what the design would add, change or remove,
385
394
  with the anchor of the code that proposal lands in, replaces or plugs into
386
395
  today. `graph: base` and `security` are errors for the same reason as in an
387
- explainer, a `symbol:` entry is an error because no diff derived one, and a
396
+ explainer, `searches` is an error because a design has no Coverage tab, and a
388
397
  design that proposes nothing is refused: that document is an explainer. In its
389
398
  `map.yaml`, `base` is the structure as it stands and `nodes` the structure it
390
399
  proposes, so a proposed part may own files that do not exist yet while a `base`
package/dist/cli.js CHANGED
@@ -11,13 +11,14 @@ import open from "open";
11
11
  import * as g from "./git.js";
12
12
  import { SCHEMA, home, newId, now, readReview, writeReview, listReviews, reviewsFor, reviewDir, revisionDir, passFile, readThreads, readText, writeText, writeJson, readJson, serverStateFile, deleteReview, kindOf, } from "./store.js";
13
13
  import { compileDocument, compileMap, globToRegExp } from "./document/compile.js";
14
- import { computeCoverage, scopeGlob, scopeGraph, scopeTruncated, } from "./coverage.js";
14
+ import { computeCoverage, scopeGlob } from "./coverage.js";
15
15
  import { parseTheme, compileTheme } from "./theme.js";
16
16
  import { registerTheme } from "./highlight.js";
17
17
  import { replyThread, setThreadStatus, needsAgent } from "./threads.js";
18
18
  import { targetLabel, truncate } from "./thread-state.js";
19
19
  import { attach } from "./presence.js";
20
20
  import { startServer } from "./server/server.js";
21
+ import { exportReview, localTarget } from "./export.js";
21
22
  import { parseFlags, helpFor, str, bool } from "./flags.js";
22
23
  import { forgeFor, repoOf, summariseCi, } from "./forge/index.js";
23
24
  import { parseSubmission, longComments, buildPass } from "./forge/submission.js";
@@ -205,9 +206,6 @@ async function ensureServer() {
205
206
  function reviewUrl(base, id, view) {
206
207
  return `${base}/review/${id}${view ? `#/${view}` : ""}`;
207
208
  }
208
- function pinnedOf(review) {
209
- return { worktree: review.worktree, pins: review.pins, dir: reviewDir(review.id) };
210
- }
211
209
  /**
212
210
  * Base and head as `--base` and `--head` name them. Head defaults to HEAD and
213
211
  * base to where head forked from trunk, so a branch is diffed against what it
@@ -227,28 +225,6 @@ async function pinRange(worktree, base, head, usage) {
227
225
  ]);
228
226
  }
229
227
  }
230
- /**
231
- * The interface delta at two pinned commits. The graphs are cached per commit
232
- * under `at.dir`, so publish and `graph interfaces` build them once between
233
- * them; both modules load lazily to keep tree-sitter off every other command's path.
234
- */
235
- async function deltaFor(at, base, head) {
236
- const graph = await import("./graph.js");
237
- const { interfaceDelta } = await import("./interfaces.js");
238
- const b = base ?? (await graph.graphAt(at.worktree, at.pins.base, at.dir));
239
- const h = head ?? (await graph.graphAt(at.worktree, at.pins.head, at.dir));
240
- const changes = await g.lineChanges(at.worktree, at.pins.base, at.pins.head);
241
- const changed = await g.changedFiles(at.worktree, at.pins.base, at.pins.head);
242
- return interfaceDelta({
243
- cwd: at.worktree,
244
- pins: at.pins,
245
- base: b,
246
- head: h,
247
- impact: graph.impact(b, h, changes, 1),
248
- changes,
249
- changed,
250
- });
251
- }
252
228
  async function guidanceFiles(repoRoot) {
253
229
  return [join(home(), "THURVIEW.md"), join(repoRoot, "THURVIEW.md")].filter((p) => existsSync(p));
254
230
  }
@@ -272,8 +248,13 @@ const TEMPLATE_DATA = `# Typed inputs for review.md: actors, anchors and stores.
272
248
  # title: PTY spawn site
273
249
  # peek: { file: src/pty.ts, from: 214, to: 223 } # add graph: base for the old side
274
250
  #
275
- # interfaces holds one capability line per interface the change moved. thurview
276
- # derives the list itself; run \`thurview graph interfaces\` for the ids.
251
+ # interfaces holds one entry per interface the change added, changed or removed:
252
+ # a CLI flag, an exported function, an HTTP route, a config key. Name it, say what
253
+ # it lets a consumer do, and anchor it on the lines that moved it (a base-side
254
+ # anchor for a removed one). Find what depends on it with git grep at the pins.
255
+ #
256
+ # interfaces:
257
+ # shellFlag: { name: --shell, change: added, capability: Runs in the named shell., anchor: spawn }
277
258
  #
278
259
  # security says where this change lets input cross a trust boundary. Leave it
279
260
  # out (or write \`security: pending\`) until you have looked and the document says
@@ -324,14 +305,23 @@ const TEMPLATE_EXPLAIN_DATA = `# Typed inputs for the explainer: actors, anchors
324
305
  # dispatch:
325
306
  # title: where a request picks its handler
326
307
  # peek: { file: src/server/router.ts, from: 41, to: 58 }
308
+ #
309
+ # searches records the searches you ran to find callers, tests and importers.
310
+ # publish re-runs each one with \`git grep -E\` at the pinned commit, and the
311
+ # Coverage tab counts the files it matches as searched. A search that matched
312
+ # nothing is stated too.
313
+ #
314
+ # searches:
315
+ # dispatchCallers: { pattern: '\\bdispatch\\(', paths: ["src/"], why: who routes a request }
327
316
  actors: {}
328
317
  anchors: {}
329
318
  stores: {}
319
+ searches: {}
330
320
  `;
331
321
  const TEMPLATE_EXPLAIN_MAP = `# The structure of the code at the pinned commit: systems, containers,
332
322
  # components, code. The map carries breadth so the prose can carry depth, and a
333
323
  # node's \`files\` globs are what tell the Coverage tab a file was at least placed.
334
- # Seed it from \`thurview graph architecture\`.
324
+ # Seed it from the directories: \`git ls-tree -d -r --name-only <commit>\`.
335
325
  nodes: []
336
326
  edges: []
337
327
  `;
@@ -375,7 +365,7 @@ const TEMPLATE_DESIGN_MAP = `# The structure the design proposes, with the struc
375
365
  # \`base\`, so the Map tab shows what it adds, changes and removes. A node under
376
366
  # \`nodes\` may own files that do not exist yet - that is a proposed part. A node
377
367
  # under \`base\` may not: it is a claim about today, and publish warns.
378
- # Seed base from \`thurview graph architecture\`.
368
+ # Seed base from the directories: \`git ls-tree -d -r --name-only <commit>\`.
379
369
  nodes: []
380
370
  edges: []
381
371
  `;
@@ -576,6 +566,27 @@ const SPECS = {
576
566
  },
577
567
  examples: ["thurview open", "thurview open --review <id> --view map --no-browser"],
578
568
  },
569
+ export: {
570
+ description: "Write a published document to one self-contained HTML file that opens with no server",
571
+ flags: {
572
+ review: {
573
+ kind: "string",
574
+ help: "review id prefix (default: the review for this worktree)",
575
+ },
576
+ out: {
577
+ kind: "string",
578
+ help: "a .html file, or a folder to write index.html into (a Pages folder works)",
579
+ },
580
+ threads: {
581
+ kind: "boolean",
582
+ help: "show the reader's threads as static notes (use --no-threads to leave them out)",
583
+ },
584
+ },
585
+ examples: [
586
+ "thurview export --out review.html",
587
+ "thurview export --review <id> --out docs/reviews/auth --no-threads",
588
+ ],
589
+ },
579
590
  serve: {
580
591
  description: "Run the review server in the foreground",
581
592
  flags: {
@@ -616,28 +627,6 @@ const SPECS = {
616
627
  "thurview threads resolve <threadId>",
617
628
  ],
618
629
  },
619
- graph: {
620
- description: "Ask the code graph at the pinned commits: the interface delta, what the change reaches, callers, tests, architecture",
621
- args: "interfaces|impact|callers <name>|tests-for <name>|architecture",
622
- flags: {
623
- review: { kind: "string", help: "review id prefix" },
624
- base: {
625
- kind: "string",
626
- help: "instead of --review: base revision (default: trunk fork point)",
627
- },
628
- head: { kind: "string", help: "instead of --review: head revision (default: HEAD)" },
629
- graph: { kind: "string", help: "callers, tests-for: head or base", default: "head" },
630
- depth: { kind: "string", help: "how many caller hops to follow", default: "2" },
631
- },
632
- examples: [
633
- "thurview graph interfaces",
634
- "thurview graph impact",
635
- "thurview graph impact --base main --head HEAD",
636
- "thurview graph callers login",
637
- "thurview graph tests-for login --graph base",
638
- "thurview graph architecture",
639
- ],
640
- },
641
630
  forge: {
642
631
  description: "Read a pull or merge request through its forge, and post the review back to it",
643
632
  args: 'status|prior|pass|submit --file <path>|reply <threadId> --body "<text>"',
@@ -888,7 +877,7 @@ const commands = {
888
877
  guidance: await guidanceFiles(worktree),
889
878
  help: [
890
879
  `Edit ${join(dir, "review.md")} and data.yaml, then run \`thurview publish --review ${short(review.id)}\``,
891
- `Run \`thurview graph impact --review ${short(review.id)}\` to see what the change reaches`,
880
+ `Run \`git grep -n -w <symbol> ${head.slice(0, 12)} --\` in ${worktree} for who reaches a symbol the change moved`,
892
881
  stat.additions + stat.deletions < 300
893
882
  ? `Small change: run \`thurview publish --review ${short(review.id)} --view files --open\` now, then write the document`
894
883
  : `Run \`git diff ${base.slice(0, 12)} ${head.slice(0, 12)}\` in ${worktree} to study the change`,
@@ -923,7 +912,7 @@ const commands = {
923
912
  scale: { filesInScope: pinned.inScope.length },
924
913
  guidance: await guidanceFiles(worktree),
925
914
  help: [
926
- `Run \`thurview graph architecture --review ${short(review.id)}\` for the clusters, their hubs and the links between them`,
915
+ `Run \`git ls-tree -d -r --name-only ${commit.slice(0, 12)}\` in ${worktree} for the directories in scope`,
927
916
  `Author ${join(dir, "map.yaml")} first: it carries the breadth the prose cannot`,
928
917
  `Edit ${join(dir, "review.md")} and data.yaml, then run \`thurview publish --review ${short(review.id)}\``,
929
918
  ],
@@ -957,7 +946,7 @@ const commands = {
957
946
  scale: { filesInScope: pinned.inScope.length },
958
947
  guidance: await guidanceFiles(worktree),
959
948
  help: [
960
- `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure the design has to fit`,
949
+ `Run \`git grep -n -w <symbol> ${commit.slice(0, 12)} --\` in ${worktree} for who depends on what the design changes`,
961
950
  `Declare in ${join(dir, "data.yaml")} what the design would add, change or remove, each anchored to the code it lands in today`,
962
951
  `Edit ${join(dir, "review.md")}, then run \`thurview publish --review ${short(review.id)}\``,
963
952
  ],
@@ -1035,21 +1024,6 @@ const commands = {
1035
1024
  }
1036
1025
  const themeName = theme ? await registerTheme(theme.shiki) : undefined;
1037
1026
  const kind = kindOf(review);
1038
- // An explainer has one pinned commit, so there is no delta to derive: the
1039
- // panel above its document states coverage instead.
1040
- let interfaces = null;
1041
- if (kind === "review") {
1042
- try {
1043
- interfaces = await deltaFor(pinnedOf(review));
1044
- }
1045
- catch (e) {
1046
- diags.push({
1047
- level: "warning",
1048
- file: "review.md",
1049
- message: `the interface delta is unavailable: ${e.message}`,
1050
- });
1051
- }
1052
- }
1053
1027
  const doc = await compileDocument({
1054
1028
  cwd: review.worktree,
1055
1029
  pins: review.pins,
@@ -1057,7 +1031,6 @@ const commands = {
1057
1031
  dataYaml: dataYaml ?? "",
1058
1032
  kind,
1059
1033
  ...(themeName ? { themeName } : {}),
1060
- interfaces,
1061
1034
  });
1062
1035
  diags.push(...doc.diagnostics);
1063
1036
  let map = null;
@@ -1095,13 +1068,11 @@ const commands = {
1095
1068
  let coverage = null;
1096
1069
  if (kind === "explainer") {
1097
1070
  try {
1098
- const graph = await import("./graph.js");
1099
- const g0 = await graph.graphAt(review.worktree, review.pins.head, dir);
1100
1071
  coverage = computeCoverage({
1101
1072
  commit: review.pins.head,
1102
1073
  scope: review.binding.name,
1103
1074
  allFiles: await g.listFiles(review.worktree, review.pins.head),
1104
- graph: g0,
1075
+ searches: doc.searches,
1105
1076
  anchored: Object.values(doc.document.anchors)
1106
1077
  .map((a) => a.peek?.file)
1107
1078
  .filter((f) => !!f),
@@ -1241,6 +1212,35 @@ const commands = {
1241
1212
  ],
1242
1213
  };
1243
1214
  },
1215
+ async export(args) {
1216
+ const p = parseFlags("export", args, spec("export").flags);
1217
+ const out = str(p, "out");
1218
+ if (!out)
1219
+ throw new AxiError("--out is required", "VALIDATION_ERROR", [
1220
+ "Run `thurview export --out review.html`, or `--out <folder>` for <folder>/index.html",
1221
+ ]);
1222
+ const review = await resolveReview(str(p, "review"), { terminal: true });
1223
+ if (!review.revision)
1224
+ throw new AxiError(`review ${short(review.id)} is not published yet`, "VALIDATION_ERROR", [
1225
+ `Run \`thurview publish --review ${short(review.id)}\` first`,
1226
+ ]);
1227
+ const threads = p.flags["threads"] !== false;
1228
+ const html = await exportReview(review, { threads });
1229
+ const file = await localTarget(out).write(html);
1230
+ return {
1231
+ exported: {
1232
+ id: short(review.id),
1233
+ revision: review.revision,
1234
+ file,
1235
+ bytes: Buffer.byteLength(html),
1236
+ threads,
1237
+ },
1238
+ help: [
1239
+ "Open the file in any browser; it needs no thurview server and fetches nothing",
1240
+ "Commit it to a Pages folder or copy it to a static host to share it",
1241
+ ],
1242
+ };
1243
+ },
1244
1244
  async serve(args) {
1245
1245
  const p = parseFlags("serve", args, spec("serve").flags);
1246
1246
  const port = str(p, "port");
@@ -1358,154 +1358,6 @@ const commands = {
1358
1358
  await (handedQuestion ? listening.handOff() : listening.stop());
1359
1359
  }
1360
1360
  },
1361
- async graph(args) {
1362
- const sub = args[0];
1363
- const rest = args.slice(1);
1364
- const s = spec("graph").flags;
1365
- const help = [
1366
- "thurview graph interfaces",
1367
- "thurview graph impact",
1368
- "thurview graph callers <name> [--graph base]",
1369
- "thurview graph tests-for <name> [--graph base]",
1370
- "thurview graph architecture",
1371
- ];
1372
- if (!sub || !["interfaces", "impact", "callers", "tests-for", "architecture"].includes(sub))
1373
- throw new AxiError(`unknown graph command${sub ? ` ${sub}` : ""}`, "VALIDATION_ERROR", help);
1374
- const named = sub === "callers" || sub === "tests-for";
1375
- const p = parseFlags(`graph ${sub}`, rest, s, named ? 1 : 0);
1376
- const name = p.positional[0];
1377
- if (named && !name)
1378
- throw new AxiError(`graph ${sub} needs a symbol name`, "VALIDATION_ERROR", help);
1379
- const depth = Number(str(p, "depth") ?? "2");
1380
- if (!Number.isInteger(depth) || depth < 1)
1381
- throw new AxiError("--depth must be a positive integer", "VALIDATION_ERROR", help);
1382
- const side = str(p, "graph") ?? "head";
1383
- if (side !== "head" && side !== "base")
1384
- throw new AxiError("--graph must be head or base", "VALIDATION_ERROR", help);
1385
- const baseRef = str(p, "base");
1386
- const headRef = str(p, "head");
1387
- const commits = baseRef !== undefined || headRef !== undefined;
1388
- if (commits && str(p, "review"))
1389
- throw new AxiError("pass --review or --base/--head, not both", "VALIDATION_ERROR", help);
1390
- const graph = await import("./graph.js");
1391
- const review = commits ? null : await resolveReview(str(p, "review"));
1392
- let t;
1393
- if (review)
1394
- t = pinnedOf(review);
1395
- else {
1396
- const worktree = await worktreeOf(process.cwd());
1397
- if (!worktree)
1398
- throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
1399
- "Run inside the source worktree, or pass --review <id>",
1400
- ]);
1401
- // No review directory owns these graphs, and a commit's graph is the same
1402
- // whoever asks, so they share one cache under the thurview home.
1403
- const pins = await pinRange(worktree, baseRef, headRef, "thurview graph impact --base <ref>");
1404
- t = { worktree, pins, dir: home() };
1405
- }
1406
- // A next step has to name the same commits, or it answers about another change.
1407
- const again = review ? "" : ` --base ${short(t.pins.base)} --head ${short(t.pins.head)}`;
1408
- if (review && kindOf(review) !== "review" && (sub === "interfaces" || sub === "impact"))
1409
- throw new AxiError(`graph ${sub} compares two commits; ${kindOf(review) === "design" ? "a design" : "an explainer"} is pinned to one`, "VALIDATION_ERROR", [
1410
- `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
1411
- `Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
1412
- ]);
1413
- const at = (commit) => graph.graphAt(t.worktree, commit, t.dir);
1414
- if (sub === "callers" || sub === "tests-for") {
1415
- const g = await at(side === "base" ? t.pins.base : t.pins.head);
1416
- const pins = {
1417
- graph: side,
1418
- commit: short(g.commit),
1419
- languages: graph.LANGUAGES.join(","),
1420
- truncated: g.truncated,
1421
- };
1422
- if (sub === "callers")
1423
- return {
1424
- ...pins,
1425
- symbol: name,
1426
- callers: graph.callers(g, name),
1427
- help: [`Run \`thurview graph tests-for ${name}${again}\` to see what exercises it`],
1428
- };
1429
- return {
1430
- ...pins,
1431
- symbol: name,
1432
- depth,
1433
- tests: graph.testsFor(g, name, depth),
1434
- help: [`Run \`thurview graph callers ${name}${again}\` for every reference`],
1435
- };
1436
- }
1437
- const base = await at(t.pins.base);
1438
- const head = await at(t.pins.head);
1439
- const pins = {
1440
- base: short(base.commit),
1441
- head: short(head.commit),
1442
- languages: graph.LANGUAGES.join(","),
1443
- };
1444
- if (sub === "interfaces") {
1445
- const delta = await deltaFor(t, base, head);
1446
- return {
1447
- ...pins,
1448
- verdict: delta.verdict,
1449
- interfaces: delta.entries.map((e) => ({
1450
- id: e.id,
1451
- change: e.change,
1452
- name: e.name,
1453
- was: e.was,
1454
- kind: e.kind,
1455
- file: e.file,
1456
- line: e.line,
1457
- graph: e.graph,
1458
- })),
1459
- internal: delta.internal,
1460
- unreadable: delta.unreadable,
1461
- truncated: delta.truncated,
1462
- help: [
1463
- ...(review
1464
- ? ["Write one capability line per entry in data.yaml under `interfaces`, keyed by id"]
1465
- : []),
1466
- `Run \`thurview graph callers <name>${again}\` to see who a removed or changed interface reached`,
1467
- ],
1468
- };
1469
- }
1470
- if (sub === "impact") {
1471
- const changes = await g.lineChanges(t.worktree, t.pins.base, t.pins.head);
1472
- return {
1473
- ...pins,
1474
- depth,
1475
- ...graph.impact(base, head, changes, depth),
1476
- help: [
1477
- `Run \`thurview graph callers <name>${again}\` to follow one symbol`,
1478
- `Run \`thurview graph architecture${again}\` for the module structure and its diff`,
1479
- ],
1480
- };
1481
- }
1482
- // An explainer and a design are both scoped to a path and pinned to one
1483
- // commit, so the structure they get back is that path's, not the
1484
- // repository's: for an explainer, the same bound the Coverage tab accounts
1485
- // for; for a design, the structure it has to fit.
1486
- if (review && kindOf(review) !== "review") {
1487
- const scope = review.binding.name;
1488
- const g0 = scopeGraph(head, scope);
1489
- const allFiles = await g.listFiles(t.worktree, t.pins.head);
1490
- const { diff: _diff, truncated: _truncated, ...rest } = graph.architecture(g0, g0);
1491
- return {
1492
- commit: short(head.commit),
1493
- scope,
1494
- languages: pins.languages,
1495
- truncated: scopeTruncated(allFiles, head, scope),
1496
- ...rest,
1497
- help: [
1498
- "Seed map.yaml nodes from communities, their `files` from a community's files, and edges from edges",
1499
- "A file in no community is outside the languages the graph reads; `thurview publish` counts those",
1500
- ],
1501
- };
1502
- }
1503
- return {
1504
- ...pins,
1505
- ...graph.architecture(base, head),
1506
- help: ["Seed map.yaml nodes from communities and edges from diff.added"],
1507
- };
1508
- },
1509
1361
  async threads(args) {
1510
1362
  const sub = args[0];
1511
1363
  const rest = args.slice(1);