thurview 0.19.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.
- package/README.md +3 -5
- package/dist/cli.js +149 -42
- package/dist/cli.js.map +1 -1
- package/dist/diff.js +3 -3
- package/dist/diff.js.map +1 -1
- package/dist/document/compile.js +1 -1
- package/dist/document/compile.js.map +1 -1
- package/dist/export.js +10 -25
- package/dist/export.js.map +1 -1
- package/dist/forge/github.js +41 -0
- package/dist/forge/github.js.map +1 -1
- package/dist/forge/gitlab.js +84 -23
- package/dist/forge/gitlab.js.map +1 -1
- package/dist/forge/types.js +4 -0
- package/dist/forge/types.js.map +1 -1
- package/dist/git.js +0 -9
- package/dist/git.js.map +1 -1
- package/dist/highlight-theme.js +37 -14
- package/dist/highlight-theme.js.map +1 -1
- package/dist/highlight.js +3 -15
- package/dist/highlight.js.map +1 -1
- package/dist/pr-review/categories.js +51 -0
- package/dist/pr-review/categories.js.map +1 -0
- package/dist/pr-review/follow.js +208 -0
- package/dist/pr-review/follow.js.map +1 -0
- package/dist/pr-review/format.js +227 -0
- package/dist/pr-review/format.js.map +1 -0
- package/dist/server/server.js +32 -47
- package/dist/server/server.js.map +1 -1
- package/dist/ui/app.css +65 -30
- package/dist/ui/app.js +15 -16
- package/dist/ui/app.js.map +2 -2
- package/dist/ui/index.html +21 -0
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +10 -20
- package/skills/thurview/references/document-authoring.md +1 -2
- package/skills/thurview/references/lifecycle.md +0 -1
- package/skills/thurview-design/SKILL.md +4 -7
- package/skills/thurview-explain/SKILL.md +6 -7
- package/skills/thurview-pr-review/SKILL.md +181 -0
- package/skills/thurview-pr-review/references/forges.md +47 -0
- package/dist/theme.js +0 -290
- package/dist/theme.js.map +0 -1
- package/skills/thurview/references/theme.md +0 -89
package/README.md
CHANGED
|
@@ -74,6 +74,8 @@ Three kinds of document, and one companion that writes none:
|
|
|
74
74
|
- **`thurview-fix`** - no browser and no reader. It reviews, fixes what it is
|
|
75
75
|
sure of behind the repository's own tests and lint, and reports the rest -
|
|
76
76
|
optionally as inline comments on the change request.
|
|
77
|
+
- **`thurview-pr-review`** - reviews a change request on the forge and follows
|
|
78
|
+
it to merge: one summary edited in place, one resolvable thread per finding.
|
|
77
79
|
|
|
78
80
|
<details>
|
|
79
81
|
<summary><b>Other ways to install</b> - one skill at a time, pin to a release,
|
|
@@ -263,9 +265,7 @@ repository's Pages folder.
|
|
|
263
265
|
claims a reader is there when none is: a question asked with no agent
|
|
264
266
|
attached is queued, not lost, and reaches the agent the next time it checks.
|
|
265
267
|
- **Revisions**: every publish is sealed; switch back to earlier ones.
|
|
266
|
-
- **Theme**:
|
|
267
|
-
publishes them with the review, so each review looks like the code it
|
|
268
|
-
explains. The default skin applies when the project has none.
|
|
268
|
+
- **Theme**: light or dark, following the system until you pick one in the ⋯ menu.
|
|
269
269
|
|
|
270
270
|
Everything runs locally against your checkout. The server listens on
|
|
271
271
|
loopback and, when present, your Tailscale address, so a phone or another
|
|
@@ -373,8 +373,6 @@ The agent writes three files in `~/.thurview/reviews/<id>/`:
|
|
|
373
373
|
- `map.yaml`: the software map at head, optionally at base. In an explainer it
|
|
374
374
|
carries the breadth the prose has no room for, and a node's `files` globs are
|
|
375
375
|
what let a file count as placed rather than not examined.
|
|
376
|
-
- `theme.yaml`: the look, derived from the reviewed project's own design
|
|
377
|
-
system (tokens, fonts, shape, code palette). Empty means the default skin.
|
|
378
376
|
|
|
379
377
|
`security` is a review's own dimension, shown to the reader under the interface
|
|
380
378
|
delta rather than left to a section the agent might not write. It is
|
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,6 @@ 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
14
|
import { computeCoverage, scopeGlob } from "./coverage.js";
|
|
15
|
-
import { parseTheme, compileTheme } from "./theme.js";
|
|
16
|
-
import { registerTheme } from "./highlight.js";
|
|
17
15
|
import { replyThread, setThreadStatus, needsAgent } from "./threads.js";
|
|
18
16
|
import { targetLabel, truncate } from "./thread-state.js";
|
|
19
17
|
import { attach } from "./presence.js";
|
|
@@ -23,6 +21,9 @@ import { parseFlags, helpFor, str, bool } from "./flags.js";
|
|
|
23
21
|
import { forgeFor, repoOf, summariseCi, } from "./forge/index.js";
|
|
24
22
|
import { parseSubmission, longComments, buildPass } from "./forge/submission.js";
|
|
25
23
|
import { recordForgeFacts, ciFacts } from "./queue.js";
|
|
24
|
+
import { parsePass } from "./pr-review/format.js";
|
|
25
|
+
import { CATEGORIES, CONFIDENCE } from "./pr-review/categories.js";
|
|
26
|
+
import { readState, startReview, stopReview, sync, waitForEvent, STOP_LABEL, } from "./pr-review/follow.js";
|
|
26
27
|
import { VERSION } from "./version.js";
|
|
27
28
|
const execFileP = promisify(execFile);
|
|
28
29
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -277,18 +278,6 @@ const TEMPLATE_MAP = `# Software map: where this change landed in the system, an
|
|
|
277
278
|
nodes: []
|
|
278
279
|
edges: []
|
|
279
280
|
`;
|
|
280
|
-
const TEMPLATE_THEME = `# Look of this review, derived from the reviewed project's own design system.
|
|
281
|
-
# Leave this file empty (or delete it) for the default skin. See the thurview skill
|
|
282
|
-
# reference (references/theme.md) for every key. Example:
|
|
283
|
-
#
|
|
284
|
-
# name: acme-web
|
|
285
|
-
# source: tailwind.config.ts, src/styles/tokens.css
|
|
286
|
-
# mode: light
|
|
287
|
-
# colors: { bg: "#ffffff", bg2: "#f6f7f9", fg: "#111827", fg2: "#4b5563", muted: "#9ca3af", line: "#e5e7eb", accent: "#2563eb", link: "#2563eb", ok: "#16a34a", warn: "#d97706", del: "#dc2626" }
|
|
288
|
-
# fonts: { display: "Inter, sans-serif", body: "Inter, sans-serif", mono: "'JetBrains Mono', monospace", stylesheets: ["https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"] }
|
|
289
|
-
# shape: { radius: 8px, bevel: false, glow: false, scanlines: false, headingTransform: none }
|
|
290
|
-
# code: { keyword: "#7c3aed", string: "#15803d", function: "#b45309", variable: "#0369a1", comment: "#9ca3af" }
|
|
291
|
-
`;
|
|
292
281
|
const TEMPLATE_EXPLAIN_MD = (title) => `# ${title}
|
|
293
282
|
|
|
294
283
|
**Summary**
|
|
@@ -465,7 +454,6 @@ async function pinOneCommit(p, k) {
|
|
|
465
454
|
await writeText(join(reviewDir(id), "review.md"), k.templates.md(review.title));
|
|
466
455
|
await writeText(join(reviewDir(id), "data.yaml"), k.templates.data);
|
|
467
456
|
await writeText(join(reviewDir(id), "map.yaml"), k.templates.map);
|
|
468
|
-
await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
|
|
469
457
|
await writeReview(review);
|
|
470
458
|
return { review, reused: false, scope, commit, worktree, inScope };
|
|
471
459
|
}
|
|
@@ -543,7 +531,7 @@ const SPECS = {
|
|
|
543
531
|
examples: ["thurview info", "thurview info --all --fields pins,inSync"],
|
|
544
532
|
},
|
|
545
533
|
publish: {
|
|
546
|
-
description: "Validate review.md, data.yaml
|
|
534
|
+
description: "Validate review.md, data.yaml and map.yaml against the pins and seal a revision",
|
|
547
535
|
flags: {
|
|
548
536
|
review: {
|
|
549
537
|
kind: "string",
|
|
@@ -664,6 +652,33 @@ const SPECS = {
|
|
|
664
652
|
'thurview forge reply <threadId> --body "<answer>" --resolve --at <sha>',
|
|
665
653
|
],
|
|
666
654
|
},
|
|
655
|
+
"pr-review": {
|
|
656
|
+
description: "Follow a pull or merge request until it merges: one summary edited in place, one resolvable thread per finding",
|
|
657
|
+
args: "status|sync --file <pass.json>|wait|stop|start",
|
|
658
|
+
flags: {
|
|
659
|
+
change: { kind: "string", help: "change request number or URL" },
|
|
660
|
+
review: { kind: "string", help: "review id prefix; its binding names the change request" },
|
|
661
|
+
forge: {
|
|
662
|
+
kind: "string",
|
|
663
|
+
help: "github or gitlab, when the host is not one of the two known ones",
|
|
664
|
+
},
|
|
665
|
+
repo: { kind: "string", help: "host/path, when `origin` is not the repository to post to" },
|
|
666
|
+
file: { kind: "string", help: "sync: the JSON pass to post" },
|
|
667
|
+
"dry-run": { kind: "boolean", help: "sync: validate and render, post nothing" },
|
|
668
|
+
interval: { kind: "string", help: "wait: seconds between polls", default: "120" },
|
|
669
|
+
timeout: {
|
|
670
|
+
kind: "string",
|
|
671
|
+
help: "wait: seconds before answering `none`, so one call fits a tool limit",
|
|
672
|
+
default: "540",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
examples: [
|
|
676
|
+
"thurview pr-review status --change 123",
|
|
677
|
+
"thurview pr-review sync --change 123 --file pass.json --dry-run",
|
|
678
|
+
"thurview pr-review wait --change 123",
|
|
679
|
+
"thurview pr-review stop --change 123",
|
|
680
|
+
],
|
|
681
|
+
},
|
|
667
682
|
delete: {
|
|
668
683
|
description: "Delete a review and everything stored for it (the code is untouched)",
|
|
669
684
|
flags: { review: { kind: "string", help: "review id prefix (required)" } },
|
|
@@ -845,7 +860,6 @@ const commands = {
|
|
|
845
860
|
await writeText(join(reviewDir(id), "review.md"), TEMPLATE_MD(review.title));
|
|
846
861
|
await writeText(join(reviewDir(id), "data.yaml"), TEMPLATE_DATA);
|
|
847
862
|
await writeText(join(reviewDir(id), "map.yaml"), TEMPLATE_MAP);
|
|
848
|
-
await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
|
|
849
863
|
await writeReview(review);
|
|
850
864
|
}
|
|
851
865
|
}
|
|
@@ -871,7 +885,6 @@ const commands = {
|
|
|
871
885
|
document: join(dir, "review.md"),
|
|
872
886
|
data: join(dir, "data.yaml"),
|
|
873
887
|
map: join(dir, "map.yaml"),
|
|
874
|
-
theme: join(dir, "theme.yaml"),
|
|
875
888
|
},
|
|
876
889
|
change: stat,
|
|
877
890
|
guidance: await guidanceFiles(worktree),
|
|
@@ -907,7 +920,6 @@ const commands = {
|
|
|
907
920
|
document: join(dir, "review.md"),
|
|
908
921
|
data: join(dir, "data.yaml"),
|
|
909
922
|
map: join(dir, "map.yaml"),
|
|
910
|
-
theme: join(dir, "theme.yaml"),
|
|
911
923
|
},
|
|
912
924
|
scale: { filesInScope: pinned.inScope.length },
|
|
913
925
|
guidance: await guidanceFiles(worktree),
|
|
@@ -941,7 +953,6 @@ const commands = {
|
|
|
941
953
|
document: join(dir, "review.md"),
|
|
942
954
|
data: join(dir, "data.yaml"),
|
|
943
955
|
map: join(dir, "map.yaml"),
|
|
944
|
-
theme: join(dir, "theme.yaml"),
|
|
945
956
|
},
|
|
946
957
|
scale: { filesInScope: pinned.inScope.length },
|
|
947
958
|
guidance: await guidanceFiles(worktree),
|
|
@@ -1007,22 +1018,13 @@ const commands = {
|
|
|
1007
1018
|
}
|
|
1008
1019
|
}
|
|
1009
1020
|
const diags = [];
|
|
1010
|
-
|
|
1011
|
-
if (themeYaml)
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (!(await g.fileExists(review.worktree, review.pins.head, f.path)))
|
|
1018
|
-
diags.push({
|
|
1019
|
-
level: "error",
|
|
1020
|
-
file: "theme.yaml",
|
|
1021
|
-
message: `fonts.files: ${f.path} does not exist at the pinned head commit`,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
const themeName = theme ? await registerTheme(theme.shiki) : undefined;
|
|
1021
|
+
// an older scaffold wrote a comments-only template; only a theme someone wrote is worth a warning
|
|
1022
|
+
if (themeYaml?.replace(/^\s*#.*$/gm, "").trim())
|
|
1023
|
+
diags.push({
|
|
1024
|
+
level: "warning",
|
|
1025
|
+
file: "theme.yaml",
|
|
1026
|
+
message: "ignored: a review no longer restyles thurview's own light and dark theme; delete the file",
|
|
1027
|
+
});
|
|
1026
1028
|
const kind = kindOf(review);
|
|
1027
1029
|
const doc = await compileDocument({
|
|
1028
1030
|
cwd: review.worktree,
|
|
@@ -1030,7 +1032,6 @@ const commands = {
|
|
|
1030
1032
|
reviewMd,
|
|
1031
1033
|
dataYaml: dataYaml ?? "",
|
|
1032
1034
|
kind,
|
|
1033
|
-
...(themeName ? { themeName } : {}),
|
|
1034
1035
|
});
|
|
1035
1036
|
diags.push(...doc.diagnostics);
|
|
1036
1037
|
let map = null;
|
|
@@ -1127,9 +1128,6 @@ const commands = {
|
|
|
1127
1128
|
await writeJson(join(rdir, "map.json"), map);
|
|
1128
1129
|
await writeJson(join(rdir, "changes.json"), changes);
|
|
1129
1130
|
await writeJson(join(rdir, "coverage.json"), coverage);
|
|
1130
|
-
if (themeYaml !== null)
|
|
1131
|
-
await cp(join(dir, "theme.yaml"), join(rdir, "theme.yaml"));
|
|
1132
|
-
await writeJson(join(rdir, "theme.json"), theme);
|
|
1133
1131
|
await writeJson(join(rdir, "meta.json"), {
|
|
1134
1132
|
revision: n,
|
|
1135
1133
|
at: now(),
|
|
@@ -1137,7 +1135,6 @@ const commands = {
|
|
|
1137
1135
|
pins: review.pins,
|
|
1138
1136
|
kind,
|
|
1139
1137
|
hasMap: !!map,
|
|
1140
|
-
theme: theme?.name ?? "default",
|
|
1141
1138
|
});
|
|
1142
1139
|
review.title = doc.document.title;
|
|
1143
1140
|
review.revision = n;
|
|
@@ -1168,7 +1165,6 @@ const commands = {
|
|
|
1168
1165
|
interfaces: doc.document.interfaces?.verdict ?? "(unavailable)",
|
|
1169
1166
|
security: doc.document.security?.verdict ?? "(unavailable)",
|
|
1170
1167
|
}),
|
|
1171
|
-
theme: theme?.name ?? "default",
|
|
1172
1168
|
url: url ?? "(server not running)",
|
|
1173
1169
|
},
|
|
1174
1170
|
};
|
|
@@ -1910,6 +1906,117 @@ const commands = {
|
|
|
1910
1906
|
help: ["Run `thurview setup hooks` or `thurview setup skill` to install what is missing"],
|
|
1911
1907
|
};
|
|
1912
1908
|
},
|
|
1909
|
+
async "pr-review"(args) {
|
|
1910
|
+
const sub = args[0];
|
|
1911
|
+
const subs = ["status", "sync", "wait", "stop", "start"];
|
|
1912
|
+
if (!sub || !subs.includes(sub))
|
|
1913
|
+
throw new AxiError(`unknown pr-review command${sub ? ` ${sub}` : ""}`, "VALIDATION_ERROR", subs.map((x) => `thurview pr-review ${x} --change <ref>`));
|
|
1914
|
+
const s = spec("pr-review").flags;
|
|
1915
|
+
const p = parseFlags(`pr-review ${sub}`, args.slice(1), s);
|
|
1916
|
+
const ctx = await forgeContext(p);
|
|
1917
|
+
const where = `${ctx.repo.path}#${ctx.cr.number}`;
|
|
1918
|
+
const again = `thurview pr-review wait --change ${ctx.cr.number}`;
|
|
1919
|
+
if (sub === "status") {
|
|
1920
|
+
const st = await readState(ctx);
|
|
1921
|
+
const open = st.findings.filter((f) => f.open);
|
|
1922
|
+
return {
|
|
1923
|
+
change: {
|
|
1924
|
+
url: ctx.cr.url,
|
|
1925
|
+
state: ctx.cr.state,
|
|
1926
|
+
author: ctx.cr.author,
|
|
1927
|
+
head: ctx.cr.head,
|
|
1928
|
+
base: ctx.cr.base,
|
|
1929
|
+
baseBranch: ctx.cr.baseBranch,
|
|
1930
|
+
},
|
|
1931
|
+
review: st.summary
|
|
1932
|
+
? {
|
|
1933
|
+
state: st.summary.marker.state,
|
|
1934
|
+
reviewedHead: st.summary.marker.head,
|
|
1935
|
+
pushedSince: st.summary.marker.head !== ctx.cr.head,
|
|
1936
|
+
}
|
|
1937
|
+
: "none yet (the first pass reviews the whole change)",
|
|
1938
|
+
...(st.stop ? { stopRequested: st.stop } : {}),
|
|
1939
|
+
open: open.length
|
|
1940
|
+
? open.map((f) => ({
|
|
1941
|
+
id: f.id,
|
|
1942
|
+
category: f.category,
|
|
1943
|
+
severity: f.severity,
|
|
1944
|
+
at: f.path ? `${f.path}:${f.line ?? ""}` : "",
|
|
1945
|
+
title: f.title,
|
|
1946
|
+
}))
|
|
1947
|
+
: "0 (no open finding)",
|
|
1948
|
+
categories: Object.fromEntries(Object.entries(CATEGORIES).map(([k, v]) => [k, v.definition])),
|
|
1949
|
+
confidence: CONFIDENCE,
|
|
1950
|
+
help: [
|
|
1951
|
+
st.summary && st.summary.marker.head !== ctx.cr.head
|
|
1952
|
+
? `Review only \`git diff ${st.summary.marker.head.slice(0, 12)} ${ctx.cr.head.slice(0, 12)}\`, then decide each open finding: still valid, or fixed`
|
|
1953
|
+
: `Review \`git diff ${ctx.cr.base.slice(0, 12)} ${ctx.cr.head.slice(0, 12)}\``,
|
|
1954
|
+
`Write the pass file and run \`thurview pr-review sync --change ${ctx.cr.number} --file <pass.json> --dry-run\``,
|
|
1955
|
+
],
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
if (sub === "sync") {
|
|
1959
|
+
const file = str(p, "file");
|
|
1960
|
+
if (!file)
|
|
1961
|
+
throw new AxiError("--file is required", "VALIDATION_ERROR", [
|
|
1962
|
+
"thurview pr-review sync --change <ref> --file <pass.json>",
|
|
1963
|
+
]);
|
|
1964
|
+
const text = await readText(resolve(process.cwd(), file));
|
|
1965
|
+
if (text === null)
|
|
1966
|
+
throw new AxiError(`${file} not found`, "NOT_FOUND", []);
|
|
1967
|
+
const dry = bool(p, "dry-run");
|
|
1968
|
+
const r = await sync(ctx, parsePass(text, file), { dryRun: dry });
|
|
1969
|
+
return {
|
|
1970
|
+
pass: {
|
|
1971
|
+
change: where,
|
|
1972
|
+
head: r.head,
|
|
1973
|
+
summary: r.summary,
|
|
1974
|
+
open: r.open,
|
|
1975
|
+
blocking: r.blocking,
|
|
1976
|
+
},
|
|
1977
|
+
posted: r.posted.length ? r.posted : "0 (no new finding)",
|
|
1978
|
+
resolved: r.resolved.length ? r.resolved : "0 (no finding fixed)",
|
|
1979
|
+
duplicates: r.duplicates.length
|
|
1980
|
+
? r.duplicates
|
|
1981
|
+
: "0 (no finding already open was found again)",
|
|
1982
|
+
...(dry ? { body: r.body } : {}),
|
|
1983
|
+
help: dry
|
|
1984
|
+
? ["Nothing was posted; re-run without --dry-run to post it"]
|
|
1985
|
+
: [
|
|
1986
|
+
`Run \`${again}\` to block until the next push, merge or stop`,
|
|
1987
|
+
"Never approve, merge or push to the change request; the verdict is the summary",
|
|
1988
|
+
],
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
if (sub === "wait") {
|
|
1992
|
+
const interval = Number(str(p, "interval"));
|
|
1993
|
+
const timeout = Number(str(p, "timeout"));
|
|
1994
|
+
if (!(interval > 0) || !(timeout > 0))
|
|
1995
|
+
throw new AxiError("--interval and --timeout take seconds", "VALIDATION_ERROR", []);
|
|
1996
|
+
const ev = await waitForEvent(ctx.forge, ctx.repo, ctx.cr.number, { interval, timeout });
|
|
1997
|
+
const help = ev.event === "push"
|
|
1998
|
+
? [
|
|
1999
|
+
ev.since
|
|
2000
|
+
? `Review only \`git diff ${ev.since.slice(0, 12)} ${ev.head.slice(0, 12)}\`, then sync`
|
|
2001
|
+
: "Nothing posted yet: review the whole change, then sync",
|
|
2002
|
+
]
|
|
2003
|
+
: ev.event === "none"
|
|
2004
|
+
? [`Nothing new; run \`${again}\` again`]
|
|
2005
|
+
: ["The follow loop ends here; the summary says why"];
|
|
2006
|
+
return { event: { ...ev, change: where }, help };
|
|
2007
|
+
}
|
|
2008
|
+
if (sub === "stop") {
|
|
2009
|
+
await stopReview(ctx);
|
|
2010
|
+
return {
|
|
2011
|
+
stopped: where,
|
|
2012
|
+
help: [
|
|
2013
|
+
`Resume with \`thurview pr-review start --change ${ctx.cr.number}\`; a reader stops with the ${STOP_LABEL} label or a /thurview stop comment`,
|
|
2014
|
+
],
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
await startReview(ctx);
|
|
2018
|
+
return { started: where, help: [`Run \`${again}\``] };
|
|
2019
|
+
},
|
|
1913
2020
|
async skill(args) {
|
|
1914
2021
|
parseFlags("skill", args, {});
|
|
1915
2022
|
const skills = {};
|