thurview 0.12.0 → 0.13.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 +40 -19
- package/dist/cli.js +217 -96
- package/dist/cli.js.map +1 -1
- package/dist/document/compile.js +97 -13
- package/dist/document/compile.js.map +1 -1
- package/dist/store.js +1 -1
- package/dist/store.js.map +1 -1
- package/dist/ui/app.css +12 -0
- package/dist/ui/app.js +68 -44
- package/dist/ui/app.js.map +2 -2
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +18 -3
- package/skills/thurview-design/SKILL.md +265 -0
- package/skills/thurview-design/references/anchors-and-proposals.md +90 -0
package/README.md
CHANGED
|
@@ -37,8 +37,8 @@ back with changes requested.
|
|
|
37
37
|
|
|
38
38
|
<details>
|
|
39
39
|
<summary><b>Other ways to install</b> - pin the skill to a release, install the
|
|
40
|
-
command from npm, run from a checkout, session hooks, the
|
|
41
|
-
|
|
40
|
+
command from npm, run from a checkout, session hooks, the thurview-design and
|
|
41
|
+
review-fix skills</summary>
|
|
42
42
|
|
|
43
43
|
`--global` installs for your user, so one install covers every repository.
|
|
44
44
|
`universal` puts the one real copy in `~/.agents/skills/thurview`, the directory
|
|
@@ -92,10 +92,13 @@ with the reviews of its working directory. `thurview setup skill` links the
|
|
|
92
92
|
skill from this checkout instead of the `skills` CLI copy; use one or the
|
|
93
93
|
other.
|
|
94
94
|
|
|
95
|
-
Also available: `
|
|
96
|
-
[below](#
|
|
95
|
+
Also available: `thurview-design`, which authors the design kind described
|
|
96
|
+
[below](#how-it-works), and `review-fix`, the browserless companion skill
|
|
97
|
+
described [below](#review-and-fix).
|
|
97
98
|
|
|
98
99
|
```sh
|
|
100
|
+
npx skills@latest add https://github.com/Thurbeen/thurview \
|
|
101
|
+
--skill thurview-design --agent universal claude-code --global --yes
|
|
99
102
|
npx skills@latest add https://github.com/Thurbeen/thurview \
|
|
100
103
|
--skill review-fix --agent universal claude-code --global --yes
|
|
101
104
|
```
|
|
@@ -115,6 +118,12 @@ architecture well enough to spot design problems themselves. It has no diff and
|
|
|
115
118
|
nothing to approve, it surfaces structure rather than grading it, and it states
|
|
116
119
|
what it did not examine.
|
|
117
120
|
|
|
121
|
+
And it reads a plan. A **design** is the same document over a change that is
|
|
122
|
+
not written yet: one pinned commit — the code it argues from — anchors on the
|
|
123
|
+
code as it stands, and what it would build declared as proposals, each attached
|
|
124
|
+
to the code it lands in today. An anchor never points at code that does not
|
|
125
|
+
exist; the reader approves the design or sends it back.
|
|
126
|
+
|
|
118
127
|
It does not review the code for you. It helps you understand it fast enough
|
|
119
128
|
to review it yourself.
|
|
120
129
|
|
|
@@ -201,20 +210,21 @@ bar.
|
|
|
201
210
|
|
|
202
211
|
## CLI
|
|
203
212
|
|
|
204
|
-
| Command | Purpose
|
|
205
|
-
| --------------------------------------------------------------------- |
|
|
206
|
-
| `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins)
|
|
207
|
-
| `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit
|
|
208
|
-
| `thurview
|
|
209
|
-
| `thurview
|
|
210
|
-
| `thurview
|
|
211
|
-
| `thurview
|
|
212
|
-
| `thurview
|
|
213
|
-
| `thurview
|
|
214
|
-
| `thurview
|
|
215
|
-
| `thurview
|
|
216
|
-
| `thurview
|
|
217
|
-
| `thurview
|
|
213
|
+
| Command | Purpose |
|
|
214
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
215
|
+
| `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
|
|
216
|
+
| `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
|
|
217
|
+
| `thurview design [<path>] [--commit R]` | Create a design of what to build, pinned to the commit it argues from |
|
|
218
|
+
| `thurview info [--all]` | Reviews, explainers and designs bound to this worktree |
|
|
219
|
+
| `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
|
|
220
|
+
| `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
|
|
221
|
+
| `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
|
|
222
|
+
| `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
|
|
223
|
+
| `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
|
|
224
|
+
| `thurview forge status\|prior\|pass\|submit\|reply` | Read a change request through its forge, and post the review back |
|
|
225
|
+
| `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
|
|
226
|
+
| `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
|
|
227
|
+
| `thurview update` | Self-update from npm |
|
|
218
228
|
|
|
219
229
|
thurview is an [AXI](https://axi.md): built for agents that drive it through a
|
|
220
230
|
shell. Output is [TOON](https://toonformat.dev) on stdout, errors are
|
|
@@ -302,12 +312,23 @@ An explainer writes the same files, minus `interfaces`: there is no change to
|
|
|
302
312
|
derive a delta from, and `graph: base` on an anchor is an error because there
|
|
303
313
|
is one commit.
|
|
304
314
|
|
|
315
|
+
A design writes the same files, and `interfaces` means something else in it:
|
|
316
|
+
each entry is a **proposal** — what the design would add, change or remove,
|
|
317
|
+
with the anchor of the code that proposal lands in, replaces or plugs into
|
|
318
|
+
today. `graph: base` is an error for the same reason as in an explainer, a
|
|
319
|
+
`symbol:` entry is an error because no diff derived one, and a design that
|
|
320
|
+
proposes nothing is refused: that document is an explainer. In its `map.yaml`,
|
|
321
|
+
`base` is the structure as it stands and `nodes` the structure it proposes, so
|
|
322
|
+
a proposed part may own files that do not exist yet while a `base` node may
|
|
323
|
+
not.
|
|
324
|
+
|
|
305
325
|
`thurview publish` rejects an anchor whose file or lines do not exist at the
|
|
306
326
|
pinned commit, a call stack frame that claims an added or removed call the
|
|
307
327
|
diff does not show, a storage operation on an unknown field, a map edge
|
|
308
328
|
to an unknown node, an interface annotation for a symbol the change did not
|
|
309
329
|
move, a declared interface whose anchor holds no added or deleted line, and an
|
|
310
|
-
explainer that anchors nothing at all
|
|
330
|
+
explainer that anchors nothing at all, and a design that proposes nothing or
|
|
331
|
+
whose proposal names no site in the code as it stands. The full format is in
|
|
311
332
|
[skills/thurview/references](skills/thurview/references).
|
|
312
333
|
|
|
313
334
|
Optional guidance for the agent: `~/.thurview/THURVIEW.md` for you,
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { parseSubmission, longComments, buildPass } from "./forge/submission.js"
|
|
|
24
24
|
import { VERSION } from "./version.js";
|
|
25
25
|
const execFileP = promisify(execFile);
|
|
26
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
const DESCRIPTION = "Guided, evidence-anchored reviews of a change
|
|
27
|
+
const DESCRIPTION = "Guided, evidence-anchored reviews of a change, explainers of a codebase and designs of what to build, read and answered in the browser";
|
|
28
28
|
function note(msg) {
|
|
29
29
|
process.stderr.write(msg + "\n");
|
|
30
30
|
}
|
|
@@ -134,9 +134,9 @@ async function reviewRow(r, fields) {
|
|
|
134
134
|
row["binding"] = bindingLabel(r.binding);
|
|
135
135
|
if (fields.has("all") || fields.has("pins"))
|
|
136
136
|
row["pins"] =
|
|
137
|
-
kindOf(r) === "
|
|
138
|
-
? r.pins.head.slice(0, 12)
|
|
139
|
-
:
|
|
137
|
+
kindOf(r) === "review"
|
|
138
|
+
? `${r.pins.base.slice(0, 12)}..${r.pins.head.slice(0, 12)}`
|
|
139
|
+
: r.pins.head.slice(0, 12);
|
|
140
140
|
if (fields.has("all") || fields.has("worktree"))
|
|
141
141
|
row["worktree"] = r.worktree;
|
|
142
142
|
if (fields.has("all") || fields.has("inSync"))
|
|
@@ -323,6 +323,150 @@ const TEMPLATE_EXPLAIN_MAP = `# The structure of the code at the pinned commit:
|
|
|
323
323
|
nodes: []
|
|
324
324
|
edges: []
|
|
325
325
|
`;
|
|
326
|
+
const TEMPLATE_DESIGN_MD = (title) => `# ${title}
|
|
327
|
+
|
|
328
|
+
**Summary**
|
|
329
|
+
|
|
330
|
+
- The agent is still writing this design. The page offers the new revision
|
|
331
|
+
when the proposal lands.
|
|
332
|
+
`;
|
|
333
|
+
const TEMPLATE_DESIGN_DATA = `# Typed inputs for the design: actors, anchors, stores, and the proposals it
|
|
334
|
+
# makes. A design is pinned to ONE commit - the code as it stands, which the
|
|
335
|
+
# design changes - so every anchor reads that commit and \`graph: base\` is an
|
|
336
|
+
# error.
|
|
337
|
+
#
|
|
338
|
+
# An anchor is evidence, never a proposal. It points at code that exists today:
|
|
339
|
+
# what the design changes, and what constrains it.
|
|
340
|
+
#
|
|
341
|
+
# anchors:
|
|
342
|
+
# login:
|
|
343
|
+
# title: where a request is authenticated today
|
|
344
|
+
# peek: { file: src/auth.ts, from: 41, to: 58 }
|
|
345
|
+
#
|
|
346
|
+
# interfaces holds what the design WOULD add, change or remove. Each entry names
|
|
347
|
+
# the interface, what it would let a consumer do, and the anchor of the code it
|
|
348
|
+
# lands in, replaces or plugs into today. A design with no entry here proposes
|
|
349
|
+
# nothing, and publish refuses it: that document is a code explainer.
|
|
350
|
+
#
|
|
351
|
+
# interfaces:
|
|
352
|
+
# strict:
|
|
353
|
+
# name: auth.login --strict
|
|
354
|
+
# change: added
|
|
355
|
+
# capability: Rejects an empty user instead of answering false.
|
|
356
|
+
# anchor: login
|
|
357
|
+
actors: {}
|
|
358
|
+
anchors: {}
|
|
359
|
+
stores: {}
|
|
360
|
+
interfaces: {}
|
|
361
|
+
`;
|
|
362
|
+
const TEMPLATE_DESIGN_MAP = `# The structure the design proposes, with the structure as it stands under
|
|
363
|
+
# \`base\`, so the Map tab shows what it adds, changes and removes. A node under
|
|
364
|
+
# \`nodes\` may own files that do not exist yet - that is a proposed part. A node
|
|
365
|
+
# under \`base\` may not: it is a claim about today, and publish warns.
|
|
366
|
+
# Seed base from \`thurview graph architecture\`.
|
|
367
|
+
nodes: []
|
|
368
|
+
edges: []
|
|
369
|
+
`;
|
|
370
|
+
const EXPLAINER = {
|
|
371
|
+
kind: "explainer",
|
|
372
|
+
command: "explain",
|
|
373
|
+
title: (scope, worktree) => scope === "**" ? worktree.split("/").pop() || "Codebase" : scope.replace(/\/\*\*$/, ""),
|
|
374
|
+
templates: { md: TEMPLATE_EXPLAIN_MD, data: TEMPLATE_EXPLAIN_DATA, map: TEMPLATE_EXPLAIN_MAP },
|
|
375
|
+
};
|
|
376
|
+
const DESIGN = {
|
|
377
|
+
kind: "design",
|
|
378
|
+
command: "design",
|
|
379
|
+
title: (scope) => (scope === "**" ? "Design" : `${scope.replace(/\/\*\*$/, "")} design`),
|
|
380
|
+
templates: { md: TEMPLATE_DESIGN_MD, data: TEMPLATE_DESIGN_DATA, map: TEMPLATE_DESIGN_MAP },
|
|
381
|
+
};
|
|
382
|
+
/** The kind with its article, so a generated sentence reads as one: "an explainer". */
|
|
383
|
+
function kindWord(kind) {
|
|
384
|
+
return kind === "explainer" ? "an explainer" : kind === "design" ? "a design" : "a review";
|
|
385
|
+
}
|
|
386
|
+
/** The command that re-pins a document of this kind, for a message that offers it. */
|
|
387
|
+
function repinCommand(kind) {
|
|
388
|
+
return kind === "review"
|
|
389
|
+
? "thurview scaffold"
|
|
390
|
+
: `thurview ${kind === "design" ? "design" : "explain"}`;
|
|
391
|
+
}
|
|
392
|
+
async function pinOneCommit(p, k) {
|
|
393
|
+
const worktree = await worktreeOf(process.cwd());
|
|
394
|
+
if (!worktree)
|
|
395
|
+
throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
|
|
396
|
+
`Run \`thurview ${k.command}\` inside the source worktree`,
|
|
397
|
+
]);
|
|
398
|
+
const existing = bool(p, "update") || str(p, "review") ? await resolveReview(str(p, "review")) : null;
|
|
399
|
+
if (existing && kindOf(existing) !== k.kind) {
|
|
400
|
+
const other = kindOf(existing);
|
|
401
|
+
throw new AxiError(`${short(existing.id)} is ${kindWord(other)}, not ${kindWord(k.kind)}`, "VALIDATION_ERROR", [
|
|
402
|
+
`Run \`${repinCommand(other)} --update --review ${short(existing.id)}\` to re-pin that ${other}`,
|
|
403
|
+
`Run \`thurview ${k.command}\` with no --review to start ${kindWord(k.kind)}`,
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
const scope = scopeGlob(p.positional[0] ?? existing?.binding.name);
|
|
407
|
+
let commit;
|
|
408
|
+
try {
|
|
409
|
+
commit = await g.revParse(worktree, str(p, "commit") ?? "HEAD");
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
throw new AxiError(e.message, "VALIDATION_ERROR", [
|
|
413
|
+
`Pass a resolvable ref: \`thurview ${k.command} --commit <ref>\``,
|
|
414
|
+
]);
|
|
415
|
+
}
|
|
416
|
+
// A scope that matches nothing is a typo, and a document of nothing would
|
|
417
|
+
// still publish and still state honest-looking coverage of zero files.
|
|
418
|
+
const files = await g.listFiles(worktree, commit);
|
|
419
|
+
const inScope = scope === "**" ? files : files.filter((f) => globToRegExp(scope).test(f));
|
|
420
|
+
if (!inScope.length)
|
|
421
|
+
throw new AxiError(`no file matches "${scope}" at ${commit.slice(0, 12)}`, "VALIDATION_ERROR", [
|
|
422
|
+
`Pass a path that exists at that commit: \`thurview ${k.command} src/server\``,
|
|
423
|
+
`Run \`thurview ${k.command}\` with no scope for the whole repository`,
|
|
424
|
+
]);
|
|
425
|
+
const binding = { kind: "codebase", name: scope };
|
|
426
|
+
if (existing) {
|
|
427
|
+
existing.pins = { base: commit, head: commit };
|
|
428
|
+
existing.binding = binding;
|
|
429
|
+
if (str(p, "title"))
|
|
430
|
+
existing.title = str(p, "title");
|
|
431
|
+
await writeReview(existing);
|
|
432
|
+
return { review: existing, reused: false, scope, commit, worktree, inScope };
|
|
433
|
+
}
|
|
434
|
+
const match = bool(p, "new")
|
|
435
|
+
? []
|
|
436
|
+
: (await reviewsFor(worktree)).filter((r) => kindOf(r) === k.kind &&
|
|
437
|
+
r.binding.name === scope &&
|
|
438
|
+
r.status !== "accepted" &&
|
|
439
|
+
r.status !== "closed");
|
|
440
|
+
if (match.length) {
|
|
441
|
+
const review = match[0];
|
|
442
|
+
review.pins = { base: commit, head: commit };
|
|
443
|
+
await writeReview(review);
|
|
444
|
+
return { review, reused: true, scope, commit, worktree, inScope };
|
|
445
|
+
}
|
|
446
|
+
const id = newId();
|
|
447
|
+
const review = {
|
|
448
|
+
schema: SCHEMA,
|
|
449
|
+
id,
|
|
450
|
+
kind: k.kind,
|
|
451
|
+
title: str(p, "title") || k.title(scope, worktree),
|
|
452
|
+
worktree,
|
|
453
|
+
repoRoot: worktree,
|
|
454
|
+
binding,
|
|
455
|
+
pins: { base: commit, head: commit },
|
|
456
|
+
status: "draft",
|
|
457
|
+
revision: 0,
|
|
458
|
+
dismissed: false,
|
|
459
|
+
createdAt: now(),
|
|
460
|
+
updatedAt: now(),
|
|
461
|
+
};
|
|
462
|
+
await mkdir(reviewDir(id), { recursive: true });
|
|
463
|
+
await writeText(join(reviewDir(id), "review.md"), k.templates.md(review.title));
|
|
464
|
+
await writeText(join(reviewDir(id), "data.yaml"), k.templates.data);
|
|
465
|
+
await writeText(join(reviewDir(id), "map.yaml"), k.templates.map);
|
|
466
|
+
await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
|
|
467
|
+
await writeReview(review);
|
|
468
|
+
return { review, reused: false, scope, commit, worktree, inScope };
|
|
469
|
+
}
|
|
326
470
|
// ---- commands ----
|
|
327
471
|
const SPECS = {
|
|
328
472
|
scaffold: {
|
|
@@ -368,8 +512,25 @@ const SPECS = {
|
|
|
368
512
|
"thurview explain --update --review <id>",
|
|
369
513
|
],
|
|
370
514
|
},
|
|
515
|
+
design: {
|
|
516
|
+
description: "Create a design or architecture document: what to build, argued against the code as it stands",
|
|
517
|
+
args: "[<path scope>]",
|
|
518
|
+
flags: {
|
|
519
|
+
commit: { kind: "string", help: "commit to pin (default: HEAD)" },
|
|
520
|
+
title: { kind: "string", help: "initial title" },
|
|
521
|
+
new: { kind: "boolean", help: "create another design even if one matches the scope" },
|
|
522
|
+
update: { kind: "boolean", help: "re-pin an existing design to a new commit" },
|
|
523
|
+
review: { kind: "string", help: "design to update (id prefix)" },
|
|
524
|
+
},
|
|
525
|
+
examples: [
|
|
526
|
+
"thurview design",
|
|
527
|
+
"thurview design src/server",
|
|
528
|
+
"thurview design --title 'Queue the forge pass'",
|
|
529
|
+
"thurview design --update --review <id>",
|
|
530
|
+
],
|
|
531
|
+
},
|
|
371
532
|
info: {
|
|
372
|
-
description: "Reviews and
|
|
533
|
+
description: "Reviews, explainers and designs bound to this worktree (or all of them with --all)",
|
|
373
534
|
flags: {
|
|
374
535
|
all: { kind: "boolean", help: "every review, not only this worktree" },
|
|
375
536
|
fields: {
|
|
@@ -553,6 +714,7 @@ async function homeView() {
|
|
|
553
714
|
"Run `thurview scaffold` to create a review of the current branch",
|
|
554
715
|
"Run `thurview scaffold --pr <number>` for a pull request",
|
|
555
716
|
"Run `thurview explain [<path>]` to explain the codebase at HEAD instead",
|
|
717
|
+
"Run `thurview design [<path>]` to design a change before writing it",
|
|
556
718
|
],
|
|
557
719
|
};
|
|
558
720
|
const reviews = [];
|
|
@@ -719,87 +881,8 @@ const commands = {
|
|
|
719
881
|
},
|
|
720
882
|
async explain(args) {
|
|
721
883
|
const p = parseFlags("explain", args, spec("explain").flags, 1);
|
|
722
|
-
const
|
|
723
|
-
const worktree =
|
|
724
|
-
if (!worktree)
|
|
725
|
-
throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
|
|
726
|
-
"Run `thurview explain` inside the source worktree",
|
|
727
|
-
]);
|
|
728
|
-
const existing = bool(p, "update") || str(p, "review") ? await resolveReview(str(p, "review")) : null;
|
|
729
|
-
if (existing && kindOf(existing) !== "explainer")
|
|
730
|
-
throw new AxiError(`${short(existing.id)} is a review, not an explainer`, "VALIDATION_ERROR", [
|
|
731
|
-
"Run `thurview scaffold --update --review <id>` to re-pin a review",
|
|
732
|
-
"Run `thurview explain` with no --review to start an explainer",
|
|
733
|
-
]);
|
|
734
|
-
const scope = scopeGlob(p.positional[0] ?? existing?.binding.name);
|
|
735
|
-
let commit;
|
|
736
|
-
try {
|
|
737
|
-
commit = await g.revParse(worktree, str(p, "commit") ?? "HEAD");
|
|
738
|
-
}
|
|
739
|
-
catch (e) {
|
|
740
|
-
throw new AxiError(e.message, "VALIDATION_ERROR", [
|
|
741
|
-
"Pass a resolvable ref: `thurview explain --commit <ref>`",
|
|
742
|
-
]);
|
|
743
|
-
}
|
|
744
|
-
// A scope that matches nothing is a typo, and an explainer of nothing would
|
|
745
|
-
// still publish and still state honest-looking coverage of zero files.
|
|
746
|
-
const files = await g.listFiles(worktree, commit);
|
|
747
|
-
const inScope = scope === "**" ? files : files.filter((f) => globToRegExp(scope).test(f));
|
|
748
|
-
if (!inScope.length)
|
|
749
|
-
throw new AxiError(`no file matches "${scope}" at ${commit.slice(0, 12)}`, "VALIDATION_ERROR", [
|
|
750
|
-
"Pass a path that exists at that commit: `thurview explain src/server`",
|
|
751
|
-
"Run `thurview explain` with no scope for the whole repository",
|
|
752
|
-
]);
|
|
753
|
-
const binding = { kind: "codebase", name: scope };
|
|
754
|
-
const defaultTitle = scope === "**" ? worktree.split("/").pop() || "Codebase" : scope.replace(/\/\*\*$/, "");
|
|
755
|
-
let review;
|
|
756
|
-
let reused = false;
|
|
757
|
-
if (existing) {
|
|
758
|
-
review = existing;
|
|
759
|
-
review.pins = { base: commit, head: commit };
|
|
760
|
-
review.binding = binding;
|
|
761
|
-
if (str(p, "title"))
|
|
762
|
-
review.title = str(p, "title");
|
|
763
|
-
await writeReview(review);
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
const match = bool(p, "new")
|
|
767
|
-
? []
|
|
768
|
-
: (await reviewsFor(worktree)).filter((r) => kindOf(r) === "explainer" &&
|
|
769
|
-
r.binding.name === scope &&
|
|
770
|
-
r.status !== "accepted" &&
|
|
771
|
-
r.status !== "closed");
|
|
772
|
-
if (match.length) {
|
|
773
|
-
review = match[0];
|
|
774
|
-
reused = true;
|
|
775
|
-
review.pins = { base: commit, head: commit };
|
|
776
|
-
await writeReview(review);
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
const id = newId();
|
|
780
|
-
review = {
|
|
781
|
-
schema: SCHEMA,
|
|
782
|
-
id,
|
|
783
|
-
kind: "explainer",
|
|
784
|
-
title: str(p, "title") || defaultTitle,
|
|
785
|
-
worktree,
|
|
786
|
-
repoRoot: worktree,
|
|
787
|
-
binding,
|
|
788
|
-
pins: { base: commit, head: commit },
|
|
789
|
-
status: "draft",
|
|
790
|
-
revision: 0,
|
|
791
|
-
dismissed: false,
|
|
792
|
-
createdAt: now(),
|
|
793
|
-
updatedAt: now(),
|
|
794
|
-
};
|
|
795
|
-
await mkdir(reviewDir(id), { recursive: true });
|
|
796
|
-
await writeText(join(reviewDir(id), "review.md"), TEMPLATE_EXPLAIN_MD(review.title));
|
|
797
|
-
await writeText(join(reviewDir(id), "data.yaml"), TEMPLATE_EXPLAIN_DATA);
|
|
798
|
-
await writeText(join(reviewDir(id), "map.yaml"), TEMPLATE_EXPLAIN_MAP);
|
|
799
|
-
await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
|
|
800
|
-
await writeReview(review);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
884
|
+
const pinned = await pinOneCommit(p, EXPLAINER);
|
|
885
|
+
const { review, scope, commit, worktree } = pinned;
|
|
803
886
|
const dir = reviewDir(review.id);
|
|
804
887
|
return {
|
|
805
888
|
explainer: {
|
|
@@ -812,7 +895,7 @@ const commands = {
|
|
|
812
895
|
scope,
|
|
813
896
|
commit,
|
|
814
897
|
worktree,
|
|
815
|
-
reused,
|
|
898
|
+
reused: pinned.reused,
|
|
816
899
|
dir,
|
|
817
900
|
},
|
|
818
901
|
files: {
|
|
@@ -821,7 +904,7 @@ const commands = {
|
|
|
821
904
|
map: join(dir, "map.yaml"),
|
|
822
905
|
theme: join(dir, "theme.yaml"),
|
|
823
906
|
},
|
|
824
|
-
scale: { filesInScope: inScope.length },
|
|
907
|
+
scale: { filesInScope: pinned.inScope.length },
|
|
825
908
|
guidance: await guidanceFiles(worktree),
|
|
826
909
|
help: [
|
|
827
910
|
`Run \`thurview graph architecture --review ${short(review.id)}\` for the clusters, their hubs and the links between them`,
|
|
@@ -830,6 +913,40 @@ const commands = {
|
|
|
830
913
|
],
|
|
831
914
|
};
|
|
832
915
|
},
|
|
916
|
+
async design(args) {
|
|
917
|
+
const p = parseFlags("design", args, spec("design").flags, 1);
|
|
918
|
+
const pinned = await pinOneCommit(p, DESIGN);
|
|
919
|
+
const { review, scope, commit, worktree } = pinned;
|
|
920
|
+
const dir = reviewDir(review.id);
|
|
921
|
+
return {
|
|
922
|
+
design: {
|
|
923
|
+
id: short(review.id),
|
|
924
|
+
uuid: review.id,
|
|
925
|
+
kind: "design",
|
|
926
|
+
title: review.title,
|
|
927
|
+
status: review.status,
|
|
928
|
+
rev: review.revision,
|
|
929
|
+
scope,
|
|
930
|
+
commit,
|
|
931
|
+
worktree,
|
|
932
|
+
reused: pinned.reused,
|
|
933
|
+
dir,
|
|
934
|
+
},
|
|
935
|
+
files: {
|
|
936
|
+
document: join(dir, "review.md"),
|
|
937
|
+
data: join(dir, "data.yaml"),
|
|
938
|
+
map: join(dir, "map.yaml"),
|
|
939
|
+
theme: join(dir, "theme.yaml"),
|
|
940
|
+
},
|
|
941
|
+
scale: { filesInScope: pinned.inScope.length },
|
|
942
|
+
guidance: await guidanceFiles(worktree),
|
|
943
|
+
help: [
|
|
944
|
+
`Run \`thurview graph architecture --review ${short(review.id)}\` for the structure the design has to fit`,
|
|
945
|
+
`Declare in ${join(dir, "data.yaml")} what the design would add, change or remove, each anchored to the code it lands in today`,
|
|
946
|
+
`Edit ${join(dir, "review.md")}, then run \`thurview publish --review ${short(review.id)}\``,
|
|
947
|
+
],
|
|
948
|
+
};
|
|
949
|
+
},
|
|
833
950
|
async info(args) {
|
|
834
951
|
const p = parseFlags("info", args, spec("info").flags);
|
|
835
952
|
const fields = new Set((str(p, "fields") ?? "").split(",").filter(Boolean));
|
|
@@ -1003,7 +1120,7 @@ const commands = {
|
|
|
1003
1120
|
if (review.binding.kind === "codebase") {
|
|
1004
1121
|
const tip = await g.revParse(review.worktree, "HEAD").catch(() => null);
|
|
1005
1122
|
if (tip && tip !== review.pins.head)
|
|
1006
|
-
warnings.push(`HEAD has moved past the pinned commit; run
|
|
1123
|
+
warnings.push(`HEAD has moved past the pinned commit; run \`${repinCommand(kind)} --update --review ${short(review.id)}\` to re-pin`);
|
|
1007
1124
|
}
|
|
1008
1125
|
if (review.binding.kind === "branch") {
|
|
1009
1126
|
const tip = await g.revParse(review.worktree, review.binding.name).catch(() => null);
|
|
@@ -1058,7 +1175,9 @@ const commands = {
|
|
|
1058
1175
|
map: !!map,
|
|
1059
1176
|
...(kind === "explainer"
|
|
1060
1177
|
? { coverage: coverage ? coverage.verdict : "(unavailable)" }
|
|
1061
|
-
:
|
|
1178
|
+
: kind === "design"
|
|
1179
|
+
? { proposes: doc.document.interfaces?.verdict ?? "(unavailable)" }
|
|
1180
|
+
: { interfaces: doc.document.interfaces?.verdict ?? "(unavailable)" }),
|
|
1062
1181
|
theme: theme?.name ?? "default",
|
|
1063
1182
|
url: url ?? "(server not running)",
|
|
1064
1183
|
},
|
|
@@ -1263,8 +1382,8 @@ const commands = {
|
|
|
1263
1382
|
}
|
|
1264
1383
|
// A next step has to name the same commits, or it answers about another change.
|
|
1265
1384
|
const again = review ? "" : ` --base ${short(t.pins.base)} --head ${short(t.pins.head)}`;
|
|
1266
|
-
if (review && kindOf(review)
|
|
1267
|
-
throw new AxiError(`graph ${sub} compares two commits; an explainer is pinned to one`, "VALIDATION_ERROR", [
|
|
1385
|
+
if (review && kindOf(review) !== "review" && (sub === "interfaces" || sub === "impact"))
|
|
1386
|
+
throw new AxiError(`graph ${sub} compares two commits; ${kindOf(review) === "design" ? "a design" : "an explainer"} is pinned to one`, "VALIDATION_ERROR", [
|
|
1268
1387
|
`Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
|
|
1269
1388
|
`Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
|
|
1270
1389
|
]);
|
|
@@ -1337,9 +1456,11 @@ const commands = {
|
|
|
1337
1456
|
],
|
|
1338
1457
|
};
|
|
1339
1458
|
}
|
|
1340
|
-
// An explainer
|
|
1341
|
-
//
|
|
1342
|
-
|
|
1459
|
+
// An explainer and a design are both scoped to a path and pinned to one
|
|
1460
|
+
// commit, so the structure they get back is that path's, not the
|
|
1461
|
+
// repository's: for an explainer, the same bound the Coverage tab accounts
|
|
1462
|
+
// for; for a design, the structure it has to fit.
|
|
1463
|
+
if (review && kindOf(review) !== "review") {
|
|
1343
1464
|
const scope = review.binding.name;
|
|
1344
1465
|
const g0 = scopeGraph(head, scope);
|
|
1345
1466
|
const allFiles = await g.listFiles(t.worktree, t.pins.head);
|
|
@@ -1885,7 +2006,7 @@ const commands = {
|
|
|
1885
2006
|
return {
|
|
1886
2007
|
skill: installed,
|
|
1887
2008
|
help: [
|
|
1888
|
-
|
|
2009
|
+
`Invoke them as ${names.map((n) => `/${n}`).join(", ")} in Claude Code, or by name in other agents`,
|
|
1889
2010
|
"Run `thurview setup hooks` for ambient context at session start",
|
|
1890
2011
|
],
|
|
1891
2012
|
};
|