thurview 0.20.0 → 0.21.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.
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { AxiError } from "axi-sdk-js";
4
+ import { CATEGORIES, CATEGORY_IDS, SEVERITIES, } from "./categories.js";
5
+ /**
6
+ * What a change request review posts, and how it reads it back. Everything a
7
+ * follow loop needs to resume lives in two hidden markers on the forge - one
8
+ * on the summary, one on each finding's thread - so a restart reads the state
9
+ * back from the change request and never from this machine.
10
+ */
11
+ /** The summary's word budget, the counts table and the marker excluded. */
12
+ export const SUMMARY_WORDS = 120;
13
+ /** A finding's visible lines, its suggestion block excluded and its sign-off included. */
14
+ export const FINDING_LINES = 5;
15
+ const SUMMARY_TAG = "thurview-pr-review";
16
+ const FINDING_TAG = "thurview-finding";
17
+ const Finding = z.object({
18
+ id: z
19
+ .string()
20
+ .regex(/^[a-z0-9-]+$/)
21
+ .optional(),
22
+ category: z.enum(CATEGORY_IDS),
23
+ severity: z.enum(SEVERITIES),
24
+ path: z.string().min(1),
25
+ line: z.number().int().positive(),
26
+ startLine: z.number().int().positive().optional(),
27
+ side: z.enum(["head", "base"]).optional(),
28
+ title: z.string().min(1),
29
+ body: z.string().default(""),
30
+ suggestion: z.string().optional(),
31
+ });
32
+ const PassSchema = z.object({
33
+ /** The head this pass reviewed; `sync` refuses it once the change request moved on. */
34
+ head: z.string().min(7),
35
+ confidence: z.number().int().min(1).max(5),
36
+ reason: z.string().min(1),
37
+ risk: z.array(z.string().min(1)).min(1).max(5),
38
+ change: z.string().min(1),
39
+ reviewUrl: z.string().url().optional(),
40
+ signoff: z.string().optional(),
41
+ findings: z.array(Finding).default([]),
42
+ fixed: z.array(z.string()).default([]),
43
+ });
44
+ export function parsePass(text, file) {
45
+ let raw;
46
+ try {
47
+ raw = JSON.parse(text);
48
+ }
49
+ catch (e) {
50
+ throw new AxiError(`${file} is not valid JSON: ${e.message}`, "VALIDATION_ERROR", [
51
+ "Read `thurview pr-review --help` for the pass file's shape",
52
+ ]);
53
+ }
54
+ const parsed = PassSchema.safeParse(raw);
55
+ if (!parsed.success)
56
+ throw new AxiError(`${file} is not a pass: ${parsed.error.issues
57
+ .map((i) => `${i.path.join(".") || "(root)"} ${i.message}`)
58
+ .join("; ")}`, "VALIDATION_ERROR", [
59
+ `category is one of ${CATEGORY_IDS.join(", ")}; severity is one of ${SEVERITIES.join(", ")}`,
60
+ "confidence is 1-5; risk holds 1 to 5 bullets",
61
+ ]);
62
+ checkPass(parsed.data);
63
+ return parsed.data;
64
+ }
65
+ /** The rules no schema says: one line per title, and the comment budget. */
66
+ export function checkPass(p) {
67
+ // A newline would let a line pass as a table row or the marker, and so
68
+ // slip past the word budget; every one of these is one line by design.
69
+ const oneLine = [
70
+ ["reason", p.reason],
71
+ ["change", p.change],
72
+ ["signoff", p.signoff],
73
+ ...p.risk.map((r, i) => [`risk[${i}]`, r]),
74
+ ];
75
+ for (const [field, text] of oneLine)
76
+ if (text && /[\r\n]/.test(text.trim()))
77
+ throw new AxiError(`${field} is more than one line`, "VALIDATION_ERROR", [
78
+ "reason, each risk bullet, change and signoff are one line each",
79
+ ]);
80
+ for (const f of p.findings ?? []) {
81
+ const at = `${f.path}:${f.line}`;
82
+ if (f.title.includes("\n"))
83
+ throw new AxiError(`${at}: the title is more than one line`, "VALIDATION_ERROR", [
84
+ "The title is the claim, in one line; the fix goes in body",
85
+ ]);
86
+ if (f.startLine && f.startLine > f.line)
87
+ throw new AxiError(`${at}: startLine ${f.startLine} is after line`, "VALIDATION_ERROR", [
88
+ "line is the LAST line of the range and startLine the first",
89
+ ]);
90
+ const lines = visibleLines(f, p.signoff);
91
+ if (lines > FINDING_LINES)
92
+ throw new AxiError(`${at}: the comment is ${lines} lines, over the ${FINDING_LINES}-line budget`, "VALIDATION_ERROR", ["One point per comment: cut it to the claim and one fix, or split it in two"]);
93
+ }
94
+ }
95
+ function visibleLines(f, signoff) {
96
+ const body = f.body?.trim() ? f.body.trim().split("\n").length : 0;
97
+ return 1 + body + (signoff ? 1 : 0);
98
+ }
99
+ /** Stable across passes, so the same finding found twice is recognised as one. */
100
+ export function findingId(f) {
101
+ if (f.id)
102
+ return f.id;
103
+ const h = createHash("sha1").update(`${f.category}\n${f.path}\n${f.title.trim()}`);
104
+ return h.digest("hex").slice(0, 10);
105
+ }
106
+ function marker(tag, data) {
107
+ return `<!-- ${tag} ${JSON.stringify(data)} -->`;
108
+ }
109
+ function readMarker(tag, body) {
110
+ const m = new RegExp(`^<!-- ${tag} (\\{.*?\\}) -->`).exec(body);
111
+ if (!m)
112
+ return null;
113
+ try {
114
+ return JSON.parse(m[1]);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ export function readSummaryMarker(body) {
121
+ return readMarker(SUMMARY_TAG, body);
122
+ }
123
+ export function readFindingMarker(body) {
124
+ const m = readMarker(FINDING_TAG, body);
125
+ if (!m || !(m.category in CATEGORIES) || !SEVERITIES.includes(m.severity))
126
+ return null;
127
+ const heading = body.split("\n")[1] ?? "";
128
+ return { ...m, title: heading.replace(/^.*?:\*\* /, "") };
129
+ }
130
+ /**
131
+ * One finding as an inline comment: the claim, the fix, a suggestion the
132
+ * author can apply. GitHub anchors a range and replaces all of it; GitLab
133
+ * anchors the range's last line, so its suggestion reaches back over the rest.
134
+ */
135
+ export function renderFinding(f, signoff, forge) {
136
+ const label = CATEGORIES[f.category].label;
137
+ const head = f.severity === "nit"
138
+ ? `nit: **${label}:** ${f.title.trim()}`
139
+ : `**${label} · ${f.severity}:** ${f.title.trim()}`;
140
+ const parts = [
141
+ marker(FINDING_TAG, { id: findingId(f), category: f.category, severity: f.severity }),
142
+ head,
143
+ ];
144
+ if (f.body?.trim())
145
+ parts.push(f.body.trim());
146
+ if (f.suggestion !== undefined) {
147
+ const above = f.startLine && f.startLine < f.line ? f.line - f.startLine : 0;
148
+ const fence = forge === "gitlab" ? `\`\`\`suggestion:-${above}+0` : "```suggestion";
149
+ parts.push(`${fence}\n${f.suggestion.replace(/\n$/, "")}\n\`\`\``);
150
+ }
151
+ if (signoff)
152
+ parts.push(signoff);
153
+ return parts.join("\n");
154
+ }
155
+ function table(open) {
156
+ if (!open.length)
157
+ return "No open findings.";
158
+ const rows = CATEGORY_IDS.filter((c) => open.some((f) => f.category === c)).map((c) => {
159
+ const n = (s) => open.filter((f) => f.category === c && f.severity === s).length;
160
+ return `| ${CATEGORIES[c].label} | ${SEVERITIES.map(n).join(" | ")} |`;
161
+ });
162
+ return [
163
+ `| Open findings | ${SEVERITIES.join(" | ")} |`,
164
+ `| --- | ${SEVERITIES.map(() => "---").join(" | ")} |`,
165
+ ...rows,
166
+ ].join("\n");
167
+ }
168
+ function headline(p, author, open) {
169
+ const blocking = open.filter((f) => f.severity === "blocking").length;
170
+ if (blocking)
171
+ return `Next: @${author} — fix the ${blocking} blocking finding${blocking === 1 ? "" : "s"}.`;
172
+ if (p.confidence >= 4)
173
+ return "Next: merge";
174
+ return `Next: @${author} — answer the risk below.`;
175
+ }
176
+ /** The line a stopped or finished review opens with instead of a next step. */
177
+ export function statusLine(m) {
178
+ const at = m.head.slice(0, 7);
179
+ if (m.state === "stopped")
180
+ return `Review stopped at ${at}. Remove the \`thurview:stop\` label and run \`thurview pr-review start\` to resume.`;
181
+ if (m.state === "merged" || m.state === "closed")
182
+ return `Review ended: ${m.state} at ${at}.`;
183
+ return null;
184
+ }
185
+ /** Words a reader reads: the marker and the counts table are not prose. */
186
+ export function summaryWords(body) {
187
+ return body
188
+ .split("\n")
189
+ .filter((l) => !l.startsWith("<!--") && !l.startsWith("|"))
190
+ .join(" ")
191
+ .split(/\s+/)
192
+ .filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
193
+ }
194
+ /**
195
+ * The one summary a change request carries: the next step, the confidence
196
+ * with its reason, the risk, the change in short, the open findings by
197
+ * category and severity, and a link to the full review when there is one.
198
+ */
199
+ export function renderSummary(p, m, author, open) {
200
+ const tail = [`Reviewed \`${m.head.slice(0, 7)}\``];
201
+ if (p.reviewUrl)
202
+ tail.push(`[Full review](${p.reviewUrl})`);
203
+ const parts = [
204
+ `${marker(SUMMARY_TAG, m)}\n${statusLine(m) ?? headline(p, author, open)}`,
205
+ `**Confidence ${p.confidence}/5:** ${p.reason.trim()}`,
206
+ `**Risk**\n${p.risk.map((r) => `- ${r.trim()}`).join("\n")}`,
207
+ `**Change:** ${p.change.trim()}`,
208
+ table(open),
209
+ tail.join(" · "),
210
+ ];
211
+ if (p.signoff)
212
+ parts.push(p.signoff);
213
+ const body = parts.join("\n\n");
214
+ const words = summaryWords(body);
215
+ if (words > SUMMARY_WORDS)
216
+ throw new AxiError(`the summary is ${words} words, over the ${SUMMARY_WORDS}-word budget`, "VALIDATION_ERROR", ["Cut reason, risk and change to what the author acts on; the full review holds the rest"]);
217
+ return body;
218
+ }
219
+ /** The same summary under a new marker and opening line; the rest is kept as written. */
220
+ export function restate(body, m, opening) {
221
+ const lines = body.split("\n");
222
+ return [marker(SUMMARY_TAG, m), opening, ...lines.slice(2)].join("\n");
223
+ }
224
+ export function bareSummary(m, opening) {
225
+ return `${marker(SUMMARY_TAG, m)}\n${opening}`;
226
+ }
227
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/pr-review/format.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thurview",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Guided, evidence-anchored reviews of agent-written code. A coding agent authors the review; you read, ask, comment and decide in the browser.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: thurview-pr-review
3
+ description: Review a pull or merge request on the forge itself and follow it until it merges - one short summary with a confidence score that is edited in place, one resolvable inline thread per finding in a fixed set of categories, threads resolved as their findings are fixed, and only what each push changed re-reviewed. Use when the user asks to review a PR or MR and post the review on it, to watch or follow a PR until it lands, for a Greptile-style or bot-style review, or invokes /thurview-pr-review. Not for a review the reader opens in the browser, which is the thurview skill, nor for fixing what a review finds, which is thurview-fix.
4
+ user-invocable: true
5
+ argument-hint: "<PR or MR number or URL> [--once] [--stop]"
6
+ ---
7
+
8
+ # thurview-pr-review
9
+
10
+ Review a change request where its author already is, and keep the review true
11
+ until the change request is merged or closed. The forge holds all the state:
12
+ one summary note with a hidden marker, one thread per finding with a hidden
13
+ marker. Nothing is kept on this machine, so the loop can die and resume
14
+ anywhere.
15
+
16
+ ```mermaid
17
+ flowchart LR
18
+ W[wait] -->|push| R[review only what moved]
19
+ R --> S[sync: new threads, resolve fixed, edit summary]
20
+ S --> W
21
+ W -->|merged, closed, stopped| E[summary says so; stop]
22
+ W -->|none| W
23
+ ```
24
+
25
+ Run the CLI as `thurview`, or `npx -y thurview` when it is not on PATH. It
26
+ prints TOON, and exit code 2 is a usage error. If it answers `unknown flag` or
27
+ `unknown command` for something below, run `thurview update` and retry once.
28
+
29
+ ## Request
30
+
31
+ $ARGUMENTS
32
+
33
+ `--once` posts one pass and stops there. `--stop` runs
34
+ `thurview pr-review stop --change <ref>` and nothing else.
35
+
36
+ ## Never
37
+
38
+ - Never approve, merge, close or push to the change request. The verdict is
39
+ the summary's confidence. The command has no verb for any of those, and you
40
+ do not reach around it with `gh` or `glab`.
41
+ - Never post a second summary. `sync` edits the one it finds.
42
+ - Never post a finding you are not sure of. Leave it out and say in a risk
43
+ bullet that something was not confirmed.
44
+ - Never post what you would not sign. Follow the user's own rules for text
45
+ posted in their name when they keep any - a sign-off line goes in `signoff`.
46
+
47
+ ## 1. Where the review stands
48
+
49
+ ```sh
50
+ thurview pr-review status --change <ref>
51
+ ```
52
+
53
+ It prints the change request's head and base, the head the last pass reviewed
54
+ (`review.reviewedHead`), every open finding with its `id`, and the categories
55
+ and confidence scale below. A stopped review stays stopped: do not sync it.
56
+
57
+ ## 2. Review only what moved
58
+
59
+ - No summary yet: review `git diff <base> <head>`, the whole change.
60
+ - A summary at an older head: review `git diff <reviewedHead> <head>`. When
61
+ `git merge-base --is-ancestor <reviewedHead> <head>` fails, the branch was
62
+ rewritten: review the whole change again.
63
+
64
+ Fetch the head first (`gh pr checkout <n>` or `glab mr checkout <n>`, or
65
+ `git fetch origin <fetchRef>`), and search at the pinned commits with the
66
+ recipes in the `thurview` skill's Searching the code reference
67
+ (`thurview skill` prints its path). The evidence rules are `thurview-fix`'s:
68
+ a problem just as present at the base is not this change's finding, and a
69
+ finding that rests on "no callers" says which search found none.
70
+
71
+ Then decide each open finding against the new head: **fixed**, or **still
72
+ valid** (do nothing - its thread stays open and is not posted again).
73
+
74
+ ## 3. Findings
75
+
76
+ One point per finding, on a line the diff touches - the forge refuses any
77
+ other line. Prefer one real defect over many nits.
78
+
79
+ | Category | Definition |
80
+ | ----------------- | ------------------------------------------------------------------------------ |
81
+ | `bug` | The code does the wrong thing for an input it accepts. |
82
+ | `security` | An attacker gains access, data or execution they should not have. |
83
+ | `performance` | Time, memory or calls grow worse than the change needs. |
84
+ | `reliability` | A failure, retry, timeout or race is handled wrongly or not at all. |
85
+ | `compatibility` | A public API, schema, config, flag or data format breaks its callers. |
86
+ | `maintainability` | The next change here is harder: duplication, dead code, a misleading name. |
87
+ | `tests` | A behaviour the change adds or alters is not tested, or a test proves nothing. |
88
+ | `docs` | A comment, README or doc now says something the code does not do. |
89
+
90
+ Severity is `blocking` (must be fixed before merge), `non-blocking` (worth
91
+ fixing) or `nit` (taste, and the comment says `nit:`).
92
+
93
+ Confidence that the change is safe to merge:
94
+
95
+ | Score | Meaning |
96
+ | ----- | ----------------------------------------------------------- |
97
+ | 5 | Safe to merge; nothing open beyond nits. |
98
+ | 4 | Safe to merge; non-blocking findings are worth a look. |
99
+ | 3 | Unsure: a risk is named that the review could not rule out. |
100
+ | 2 | Not yet: a blocking finding is open. |
101
+ | 1 | Do not merge: it breaks something that works today. |
102
+
103
+ An open blocking finding caps confidence at 2; `sync` refuses more.
104
+
105
+ ## 4. Sync
106
+
107
+ Write `pass.json` for this head. `findings` holds only what is new; `fixed`
108
+ holds the ids of open findings this push fixed:
109
+
110
+ ```json
111
+ {
112
+ "head": "<the full sha you reviewed>",
113
+ "confidence": 2,
114
+ "reason": "Safe once the retry loop stops on a 4xx.",
115
+ "risk": ["Every upload goes through the changed retry path.", "No test covers a 4xx."],
116
+ "change": "Retries a failed upload three times with exponential backoff.",
117
+ "reviewUrl": "https://reviews.example.com/pr-7/",
118
+ "signoff": "<the user's sign-off line, when they keep one>",
119
+ "findings": [
120
+ {
121
+ "category": "bug",
122
+ "severity": "blocking",
123
+ "path": "src/upload.ts",
124
+ "line": 42,
125
+ "startLine": 40,
126
+ "title": "The retry loop never stops on a 4xx.",
127
+ "body": "Return the response on any status below 500.",
128
+ "suggestion": "if (res.status < 500) return res;"
129
+ }
130
+ ],
131
+ "fixed": ["3f2a9c1b07"]
132
+ }
133
+ ```
134
+
135
+ - `head` is the commit you reviewed. `sync` refuses the pass when the change
136
+ request moved on since: review what the new push added, then sync again.
137
+ - `reason`, `change`, `signoff` and each `risk` bullet are one line each;
138
+ `sync` refuses a line break in any of them. `reason` is one sentence;
139
+ `risk` is one to five bullets naming what could break, the blast radius, and
140
+ anything touching security, data, infra or a public API; `change` is two or
141
+ three sentences.
142
+ - The summary is capped at 120 words, the counts table aside. `sync` refuses
143
+ more: cut to what the author acts on.
144
+ - A finding's `title` is the claim in one line, `body` the fix. Title, body
145
+ and sign-off together fit five lines. `suggestion` replaces the lines from
146
+ `startLine` to `line` and is optional.
147
+ - `reviewUrl` links the full rendered review. When the user has a publish
148
+ target - `thurview export --out <folder>` into a Pages folder or a static
149
+ host they serve - publish there and pass its URL. With none, leave it out.
150
+
151
+ ```sh
152
+ thurview pr-review sync --change <ref> --file pass.json --dry-run
153
+ thurview pr-review sync --change <ref> --file pass.json
154
+ ```
155
+
156
+ The dry run prints the summary as it will read. A finding already open is
157
+ reported under `duplicates` and not posted twice.
158
+
159
+ ## 5. Follow
160
+
161
+ ```sh
162
+ thurview pr-review wait --change <ref> # --interval 120 --timeout 540 by default
163
+ ```
164
+
165
+ It blocks until there is something to do and prints one `event`:
166
+
167
+ | Event | Do |
168
+ | ----------------------------- | -------------------------------------------- |
169
+ | `push` | back to step 2, with `since` as the old head |
170
+ | `none` | run `wait` again |
171
+ | `merged`, `closed`, `stopped` | stop: the summary already says so |
172
+
173
+ With `--once`, stop after the first sync.
174
+
175
+ A reader stops the loop with the `thurview:stop` label or a comment that
176
+ starts `/thurview stop`; you stop it with `thurview pr-review stop`. A stopped
177
+ review resumes with `thurview pr-review start --change <ref>` once the label
178
+ is gone. Report what you posted and the summary's link when the loop ends.
179
+
180
+ How GitHub and GitLab differ, and what each supports, is in
181
+ [Forges](references/forges.md).
@@ -0,0 +1,47 @@
1
+ # Forges
2
+
3
+ `thurview pr-review` drives the same seam as `thurview forge`: `gh api` for
4
+ GitHub and `glab api` for GitLab, a self-hosted host owned by whichever CLI is
5
+ authenticated for it. The seam has no merge, close or push, so neither does
6
+ this command.
7
+
8
+ ## What each forge does
9
+
10
+ | Step | GitHub | GitLab |
11
+ | ----------------- | ------------------------------------------ | --------------------------------------------------- |
12
+ | The summary | one issue comment, `PATCH`ed in place | one merge request note, `PUT` in place |
13
+ | A finding | one review comment, a resolvable thread | one diff discussion, a resolvable thread |
14
+ | A line range | anchored as the range | anchored at its last line |
15
+ | A suggestion | ` ```suggestion ` over the range | ` ```suggestion:-N+0 ` reaching back over the range |
16
+ | Resolving a fixed | a reply, then `resolveReviewThread` | a reply, then `PUT .../discussions/<id>` resolved |
17
+ | The stop label | `thurview:stop` among the pull's labels | `thurview:stop` among the merge request's labels |
18
+ | The stop command | an issue comment starting `/thurview stop` | a top-level note starting `/thurview stop` |
19
+ | Merged or closed | `merged`, or `state: closed` | `state: merged` or `closed` |
20
+
21
+ ## Where the state lives
22
+
23
+ The summary carries
24
+ `<!-- thurview-pr-review {"head":"<sha>","state":"active","seen":"<note id>"} -->`
25
+ and each finding's first comment
26
+ `<!-- thurview-finding {"id":"<id>","category":"bug","severity":"blocking"} -->`.
27
+ `state` is `active`, `stopped`, `merged` or `closed`. `seen` is the newest
28
+ note already read, so a `/thurview stop` posted before a `start` stops nothing.
29
+ Only a summary posted by the account the CLI is logged in as counts, so a
30
+ pasted marker changes nothing. A finding's id is the `id` given in the pass, or a hash of its category, path
31
+ and title, which is how the same finding found on the next push is recognised.
32
+
33
+ ## Gaps
34
+
35
+ - No forge approve. A forge approve can arm an auto-merge, and the verdict
36
+ lives in the summary; `thurview forge submit` is the command for one.
37
+ - GitHub reads the newest 100 review threads (`reviewThreads(last:100)`), the
38
+ same limit `thurview forge prior` has; GitLab reads every page.
39
+ - Every call asks the forge who it is (`gh api user`, `glab api user`) to
40
+ tell its own markers from pasted ones, so it needs a token that can read its
41
+ own user; a GitHub App installation token cannot.
42
+ - A finding must sit on a line the diff touches. A finding on an untouched
43
+ caller goes in a risk bullet.
44
+ - GitLab reports no per-thread staleness, and its adapter is driven by stub
45
+ tests but has not been run against a live instance.
46
+ - The full-review link is whatever URL the pass names; this command publishes
47
+ nothing itself.