thurview 0.9.0 → 0.10.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
@@ -73,7 +73,7 @@ reads the Agent Skills format:
73
73
 
74
74
  ```sh
75
75
  npx skills add Thurbeen/thurview --skill thurview
76
- npx skills add Thurbeen/thurview --skill forge-review # optional, see below
76
+ npx skills add Thurbeen/thurview --skill review-fix # optional, see below
77
77
  ```
78
78
 
79
79
  That form tracks this repository's default branch: `skills update` takes
@@ -178,7 +178,7 @@ bar.
178
178
  | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
179
179
  | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
180
180
  | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
181
- | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at the pinned commits |
181
+ | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
182
182
  | `thurview forge status\|prior\|submit\|reply` | Read a change request through its forge, and post the review back |
183
183
  | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
184
184
  | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
@@ -194,11 +194,26 @@ arguments shows live state for the current directory instead of a manual.
194
194
  `thurview <command> --help` is the fallback. Progress and diagnostics go to
195
195
  stderr.
196
196
 
197
- ## Posting the review to the forge
197
+ ## Review and fix
198
198
 
199
- A thurview review is read in the browser. When the change is a pull request on
200
- GitHub or a merge request on GitLab, the `forge-review` skill posts it there as
201
- well: inline comments anchored to lines, a summary, and a verdict.
199
+ The `review-fix` skill reviews a branch, a commit range or a pull or merge
200
+ request, fixes what it is sure of and reports the rest, with no browser and no
201
+ approval step. For each changed symbol it asks the code graph who calls it and
202
+ which tests reach it, so a finding can name a caller the diff never shows.
203
+ Fixes that pass the repository's own tests and lint land as one local commit;
204
+ nothing is pushed unless you ask.
205
+
206
+ ```sh
207
+ thurview graph impact --head HEAD # changed symbols, the callers they left alone, the tests
208
+ thurview graph callers discount --head HEAD # every call site of one symbol
209
+ ```
210
+
211
+ `--base` and `--head` ask about two commits directly; `--head` alone diffs
212
+ from where it forked from trunk. Each caller in `impact.reach` carries the line
213
+ of its call and whether any test reaches it.
214
+
215
+ With `--post`, the skill posts the findings it did not fix as inline comments
216
+ on the change request, through `thurview forge`:
202
217
 
203
218
  ```sh
204
219
  thurview forge status --change 123 # what CI actually did, and whether it is a gate at all
@@ -224,7 +239,7 @@ and gitlab.com are matched against what those CLIs are authenticated for, and
224
239
  an unmatched host is refused rather than guessed. The differences that survive
225
240
  the seam - GitLab has no changes-requested state, no atomic review and no
226
241
  multi-line comment anchor - are listed in
227
- [skills/forge-review/references/forges.md](skills/forge-review/references/forges.md).
242
+ [skills/review-fix/references/forges.md](skills/review-fix/references/forges.md).
228
243
 
229
244
  ## Authoring format
230
245
 
package/dist/cli.js CHANGED
@@ -215,22 +215,43 @@ async function ensureServer() {
215
215
  function reviewUrl(base, id, view) {
216
216
  return `${base}/review/${id}${view ? `#/${view}` : ""}`;
217
217
  }
218
+ function pinnedOf(review) {
219
+ return { worktree: review.worktree, pins: review.pins, dir: reviewDir(review.id) };
220
+ }
218
221
  /**
219
- * The interface delta at a review's pins. The graphs are cached per commit under
220
- * the review directory, so publish and `graph interfaces` build them once between
222
+ * Base and head as `--base` and `--head` name them. Head defaults to HEAD and
223
+ * base to where head forked from trunk, so a branch is diffed against what it
224
+ * branched from rather than against wherever trunk has moved since.
225
+ */
226
+ async function pinRange(worktree, base, head, usage) {
227
+ try {
228
+ const h = await g.revParse(worktree, head ?? "HEAD");
229
+ const b = base
230
+ ? await g.revParse(worktree, base)
231
+ : await g.mergeBase(worktree, await g.trunkRef(worktree), h);
232
+ return { base: b, head: h };
233
+ }
234
+ catch (e) {
235
+ throw new AxiError(e.message, "VALIDATION_ERROR", [
236
+ `Pass resolvable refs: \`${usage}\``,
237
+ ]);
238
+ }
239
+ }
240
+ /**
241
+ * The interface delta at two pinned commits. The graphs are cached per commit
242
+ * under `at.dir`, so publish and `graph interfaces` build them once between
221
243
  * them; both modules load lazily to keep tree-sitter off every other command's path.
222
244
  */
223
- async function deltaFor(review, base, head) {
245
+ async function deltaFor(at, base, head) {
224
246
  const graph = await import("./graph.js");
225
247
  const { interfaceDelta } = await import("./interfaces.js");
226
- const dir = reviewDir(review.id);
227
- const b = base ?? (await graph.graphAt(review.worktree, review.pins.base, dir));
228
- const h = head ?? (await graph.graphAt(review.worktree, review.pins.head, dir));
229
- const changes = await g.lineChanges(review.worktree, review.pins.base, review.pins.head);
230
- const changed = await g.changedFiles(review.worktree, review.pins.base, review.pins.head);
248
+ const b = base ?? (await graph.graphAt(at.worktree, at.pins.base, at.dir));
249
+ const h = head ?? (await graph.graphAt(at.worktree, at.pins.head, at.dir));
250
+ const changes = await g.lineChanges(at.worktree, at.pins.base, at.pins.head);
251
+ const changed = await g.changedFiles(at.worktree, at.pins.base, at.pins.head);
231
252
  return interfaceDelta({
232
- cwd: review.worktree,
233
- pins: review.pins,
253
+ cwd: at.worktree,
254
+ pins: at.pins,
234
255
  base: b,
235
256
  head: h,
236
257
  impact: graph.impact(b, h, changes, 1),
@@ -438,12 +459,18 @@ const SPECS = {
438
459
  args: "interfaces|impact|callers <name>|tests-for <name>|architecture",
439
460
  flags: {
440
461
  review: { kind: "string", help: "review id prefix" },
462
+ base: {
463
+ kind: "string",
464
+ help: "instead of --review: base revision (default: trunk fork point)",
465
+ },
466
+ head: { kind: "string", help: "instead of --review: head revision (default: HEAD)" },
441
467
  graph: { kind: "string", help: "callers, tests-for: head or base", default: "head" },
442
468
  depth: { kind: "string", help: "how many caller hops to follow", default: "2" },
443
469
  },
444
470
  examples: [
445
471
  "thurview graph interfaces",
446
472
  "thurview graph impact",
473
+ "thurview graph impact --base main --head HEAD",
447
474
  "thurview graph callers login",
448
475
  "thurview graph tests-for login --graph base",
449
476
  "thurview graph architecture",
@@ -590,17 +617,7 @@ const commands = {
590
617
  const [bb, hh] = b?.kind === "range" && !str(p, "base") && !str(p, "head")
591
618
  ? b.name.split("..")
592
619
  : [str(p, "base"), str(p, "head")];
593
- try {
594
- head = await g.revParse(worktree, hh ?? "HEAD");
595
- base = bb
596
- ? await g.revParse(worktree, bb)
597
- : await g.mergeBase(worktree, await g.trunkRef(worktree), head);
598
- }
599
- catch (e) {
600
- throw new AxiError(e.message, "VALIDATION_ERROR", [
601
- "Pass resolvable refs: `thurview scaffold --base <ref> --head <ref>`",
602
- ]);
603
- }
620
+ ({ base, head } = await pinRange(worktree, bb, hh, "thurview scaffold --base <ref> --head <ref>"));
604
621
  binding = { kind: "range", name: `${base.slice(0, 12)}..${head.slice(0, 12)}` };
605
622
  }
606
623
  else {
@@ -896,7 +913,7 @@ const commands = {
896
913
  let interfaces = null;
897
914
  if (kind === "review") {
898
915
  try {
899
- interfaces = await deltaFor(review);
916
+ interfaces = await deltaFor(pinnedOf(review));
900
917
  }
901
918
  catch (e) {
902
919
  diags.push({
@@ -1229,17 +1246,37 @@ const commands = {
1229
1246
  const side = str(p, "graph") ?? "head";
1230
1247
  if (side !== "head" && side !== "base")
1231
1248
  throw new AxiError("--graph must be head or base", "VALIDATION_ERROR", help);
1249
+ const baseRef = str(p, "base");
1250
+ const headRef = str(p, "head");
1251
+ const commits = baseRef !== undefined || headRef !== undefined;
1252
+ if (commits && str(p, "review"))
1253
+ throw new AxiError("pass --review or --base/--head, not both", "VALIDATION_ERROR", help);
1232
1254
  const graph = await import("./graph.js");
1233
- const review = await resolveReview(str(p, "review"));
1234
- if (kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
1255
+ const review = commits ? null : await resolveReview(str(p, "review"));
1256
+ let t;
1257
+ if (review)
1258
+ t = pinnedOf(review);
1259
+ else {
1260
+ const worktree = await worktreeOf(process.cwd());
1261
+ if (!worktree)
1262
+ throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
1263
+ "Run inside the source worktree, or pass --review <id>",
1264
+ ]);
1265
+ // No review directory owns these graphs, and a commit's graph is the same
1266
+ // whoever asks, so they share one cache under the thurview home.
1267
+ const pins = await pinRange(worktree, baseRef, headRef, "thurview graph impact --base <ref>");
1268
+ t = { worktree, pins, dir: home() };
1269
+ }
1270
+ // A next step has to name the same commits, or it answers about another change.
1271
+ const again = review ? "" : ` --base ${short(t.pins.base)} --head ${short(t.pins.head)}`;
1272
+ if (review && kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
1235
1273
  throw new AxiError(`graph ${sub} compares two commits; an explainer is pinned to one`, "VALIDATION_ERROR", [
1236
1274
  `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
1237
1275
  `Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
1238
1276
  ]);
1239
- const dir = reviewDir(review.id);
1240
- const at = (commit) => graph.graphAt(review.worktree, commit, dir);
1277
+ const at = (commit) => graph.graphAt(t.worktree, commit, t.dir);
1241
1278
  if (sub === "callers" || sub === "tests-for") {
1242
- const g = await at(side === "base" ? review.pins.base : review.pins.head);
1279
+ const g = await at(side === "base" ? t.pins.base : t.pins.head);
1243
1280
  const pins = {
1244
1281
  graph: side,
1245
1282
  commit: short(g.commit),
@@ -1251,25 +1288,25 @@ const commands = {
1251
1288
  ...pins,
1252
1289
  symbol: name,
1253
1290
  callers: graph.callers(g, name),
1254
- help: [`Run \`thurview graph tests-for ${name}\` to see what exercises it`],
1291
+ help: [`Run \`thurview graph tests-for ${name}${again}\` to see what exercises it`],
1255
1292
  };
1256
1293
  return {
1257
1294
  ...pins,
1258
1295
  symbol: name,
1259
1296
  depth,
1260
1297
  tests: graph.testsFor(g, name, depth),
1261
- help: [`Run \`thurview graph callers ${name}\` for every reference`],
1298
+ help: [`Run \`thurview graph callers ${name}${again}\` for every reference`],
1262
1299
  };
1263
1300
  }
1264
- const base = await at(review.pins.base);
1265
- const head = await at(review.pins.head);
1301
+ const base = await at(t.pins.base);
1302
+ const head = await at(t.pins.head);
1266
1303
  const pins = {
1267
1304
  base: short(base.commit),
1268
1305
  head: short(head.commit),
1269
1306
  languages: graph.LANGUAGES.join(","),
1270
1307
  };
1271
1308
  if (sub === "interfaces") {
1272
- const delta = await deltaFor(review, base, head);
1309
+ const delta = await deltaFor(t, base, head);
1273
1310
  return {
1274
1311
  ...pins,
1275
1312
  verdict: delta.verdict,
@@ -1287,29 +1324,31 @@ const commands = {
1287
1324
  unreadable: delta.unreadable,
1288
1325
  truncated: delta.truncated,
1289
1326
  help: [
1290
- "Write one capability line per entry in data.yaml under `interfaces`, keyed by id",
1291
- "Run `thurview graph callers <name>` to see who a removed or changed interface reached",
1327
+ ...(review
1328
+ ? ["Write one capability line per entry in data.yaml under `interfaces`, keyed by id"]
1329
+ : []),
1330
+ `Run \`thurview graph callers <name>${again}\` to see who a removed or changed interface reached`,
1292
1331
  ],
1293
1332
  };
1294
1333
  }
1295
1334
  if (sub === "impact") {
1296
- const changes = await g.lineChanges(review.worktree, review.pins.base, review.pins.head);
1335
+ const changes = await g.lineChanges(t.worktree, t.pins.base, t.pins.head);
1297
1336
  return {
1298
1337
  ...pins,
1299
1338
  depth,
1300
1339
  ...graph.impact(base, head, changes, depth),
1301
1340
  help: [
1302
- "Run `thurview graph callers <name>` to follow one symbol",
1303
- "Run `thurview graph architecture` for the module structure and its diff",
1341
+ `Run \`thurview graph callers <name>${again}\` to follow one symbol`,
1342
+ `Run \`thurview graph architecture${again}\` for the module structure and its diff`,
1304
1343
  ],
1305
1344
  };
1306
1345
  }
1307
1346
  // An explainer is scoped to a path, so its structure is that path's, not the
1308
1347
  // repository's: the same bound the Coverage tab accounts for.
1309
- if (kindOf(review) === "explainer") {
1348
+ if (review && kindOf(review) === "explainer") {
1310
1349
  const scope = review.binding.name;
1311
1350
  const g0 = scopeGraph(head, scope);
1312
- const allFiles = await g.listFiles(review.worktree, review.pins.head);
1351
+ const allFiles = await g.listFiles(t.worktree, t.pins.head);
1313
1352
  const { diff: _diff, truncated: _truncated, ...rest } = graph.architecture(g0, g0);
1314
1353
  return {
1315
1354
  commit: short(head.commit),
@@ -1787,7 +1826,7 @@ const commands = {
1787
1826
  return {
1788
1827
  skill: installed,
1789
1828
  help: [
1790
- "Invoke them as /thurview and /forge-review in Claude Code, or by name in other agents",
1829
+ "Invoke them as /thurview and /review-fix in Claude Code, or by name in other agents",
1791
1830
  "Run `thurview setup hooks` for ambient context at session start",
1792
1831
  ],
1793
1832
  };