@christianmorup/review-intent 0.1.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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/artifact.js +49 -0
- package/dist/cli.js +151 -0
- package/dist/completeness.js +36 -0
- package/dist/complexity.js +139 -0
- package/dist/config.js +58 -0
- package/dist/diff-parser.js +51 -0
- package/dist/git.js +59 -0
- package/dist/match.js +64 -0
- package/dist/reach.js +138 -0
- package/dist/render.js +982 -0
- package/dist/scorecard.js +101 -0
- package/dist/skill.js +257 -0
- package/dist/types.js +65 -0
- package/package.json +53 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const TEST_RE = /(?:[._-](?:test|spec)\.)|(?:^|\/)(?:tests?|__tests__|spec)\//i;
|
|
2
|
+
const CODE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|cs|go|py|java|rb|rs|cpp|cc|c|h|hpp|php|vue|svelte|kt|swift|scala)$/i;
|
|
3
|
+
/** Lockfiles, generated/minified output, build dirs, and binaries — high churn,
|
|
4
|
+
* low review value. Flagged so a reviewer knows how much of the diff is noise. */
|
|
5
|
+
const NOISE_RE = /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json)$|\.(?:lock|min\.js|min\.css|map|snap)$|(?:^|\/)(?:dist|build|node_modules|out)\/|\.(?:png|jpe?g|gif|svg|ico|webp|woff2?|ttf|eot|pdf)$/i;
|
|
6
|
+
/** Debt/debug markers a reviewer should never let through unnoticed. */
|
|
7
|
+
const DEBT_RE = /\b(?:TODO|FIXME|HACK|XXX)\b|console\.log|debugger\b/;
|
|
8
|
+
export function isTestPath(path) {
|
|
9
|
+
return TEST_RE.test(path);
|
|
10
|
+
}
|
|
11
|
+
export function isCodePath(path) {
|
|
12
|
+
return CODE_RE.test(path);
|
|
13
|
+
}
|
|
14
|
+
export function isNoisePath(path) {
|
|
15
|
+
return NOISE_RE.test(path);
|
|
16
|
+
}
|
|
17
|
+
/** Pure: derive the objective surface-area scorecard from the parsed diff. */
|
|
18
|
+
export function buildScorecard(diff, config) {
|
|
19
|
+
const byStatus = {};
|
|
20
|
+
let hunks = 0;
|
|
21
|
+
let added = 0;
|
|
22
|
+
let removed = 0;
|
|
23
|
+
let testFiles = 0;
|
|
24
|
+
let codeFiles = 0;
|
|
25
|
+
let testLines = 0;
|
|
26
|
+
let codeLines = 0;
|
|
27
|
+
let debtMarkers = 0;
|
|
28
|
+
let noiseFiles = 0;
|
|
29
|
+
let largestFile = null;
|
|
30
|
+
for (const file of diff) {
|
|
31
|
+
byStatus[file.status] = (byStatus[file.status] ?? 0) + 1;
|
|
32
|
+
hunks += file.hunks.length;
|
|
33
|
+
let fileChurn = 0;
|
|
34
|
+
for (const hunk of file.hunks) {
|
|
35
|
+
for (const line of hunk.lines) {
|
|
36
|
+
if (line.type === "add") {
|
|
37
|
+
added++;
|
|
38
|
+
fileChurn++;
|
|
39
|
+
if (DEBT_RE.test(line.content))
|
|
40
|
+
debtMarkers++;
|
|
41
|
+
}
|
|
42
|
+
else if (line.type === "del") {
|
|
43
|
+
removed++;
|
|
44
|
+
fileChurn++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const isTest = isTestPath(file.path);
|
|
49
|
+
if (isTest) {
|
|
50
|
+
testFiles++;
|
|
51
|
+
testLines += fileChurn;
|
|
52
|
+
}
|
|
53
|
+
else if (isCodePath(file.path)) {
|
|
54
|
+
codeFiles++;
|
|
55
|
+
codeLines += fileChurn;
|
|
56
|
+
}
|
|
57
|
+
if (isNoisePath(file.path))
|
|
58
|
+
noiseFiles++;
|
|
59
|
+
if (!largestFile || fileChurn > largestFile.churn) {
|
|
60
|
+
largestFile = { path: file.path, churn: fileChurn };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const badges = [];
|
|
64
|
+
// Code changed but no test files touched — the headline review smell.
|
|
65
|
+
if (codeFiles > 0 && testFiles === 0) {
|
|
66
|
+
badges.push({ label: "no test changes", tone: "danger" });
|
|
67
|
+
}
|
|
68
|
+
// Sensitive paths (repo policy), de-duplicated by label.
|
|
69
|
+
const paths = diff.map((f) => f.path);
|
|
70
|
+
for (const sp of config.sensitivePaths) {
|
|
71
|
+
let re;
|
|
72
|
+
try {
|
|
73
|
+
re = new RegExp(sp.pattern, "i");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue; // a bad policy regex shouldn't crash the scorecard
|
|
77
|
+
}
|
|
78
|
+
if (paths.some((p) => re.test(p))) {
|
|
79
|
+
badges.push({ label: `touches ${sp.label}`, tone: "danger" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Large change set (churn).
|
|
83
|
+
if (diff.length > config.churnFiles || added + removed > config.churnLines) {
|
|
84
|
+
badges.push({ label: "large change set", tone: "warn" });
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
filesChanged: diff.length,
|
|
88
|
+
byStatus,
|
|
89
|
+
hunks,
|
|
90
|
+
added,
|
|
91
|
+
removed,
|
|
92
|
+
testFiles,
|
|
93
|
+
codeFiles,
|
|
94
|
+
testLines,
|
|
95
|
+
codeLines,
|
|
96
|
+
debtMarkers,
|
|
97
|
+
noiseFiles,
|
|
98
|
+
largestFile,
|
|
99
|
+
badges,
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/skill.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const SKILL_NAME = "review-intent-authoring";
|
|
5
|
+
// Embedded content for the Claude Code skill that teaches the change-making
|
|
6
|
+
// agent to author an honest .review/intent.json before review. Single source
|
|
7
|
+
// of truth — the installer writes this byte-for-byte, and uninstall checks
|
|
8
|
+
// against it (line-ending-normalized) to decide whether the file is still ours.
|
|
9
|
+
export const SKILL_CONTENT = `---
|
|
10
|
+
name: review-intent-authoring
|
|
11
|
+
description: Use when you have just finished a set of code changes on a branch and the user (or another reviewer) is about to review the diff. Author a .review/intent.json that captures the genuine intent behind the changes — why, what you rejected, what it rests on — keyed to files and hunks, plus mermaid class and sequence diagrams. Then offer to render it with review-intent so the reviewer adjudicates decisions instead of skimming lines.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Authoring an honest intent artifact
|
|
15
|
+
|
|
16
|
+
A diff records *what* changed and erases *why*. \`review-intent\` renders your
|
|
17
|
+
intent side-by-side with the diff so the reviewer adjudicates decisions, not
|
|
18
|
+
lines. Your job is to write \`.review/intent.json\` — and to write it **honestly**,
|
|
19
|
+
because a fluent rationalization is worse than nothing: it lowers the reviewer's
|
|
20
|
+
guard while adding no real signal.
|
|
21
|
+
|
|
22
|
+
## When to author
|
|
23
|
+
|
|
24
|
+
After you finish a logical change set on a branch and before you hand it back
|
|
25
|
+
for review. One artifact per branch / review.
|
|
26
|
+
|
|
27
|
+
## When NOT to author
|
|
28
|
+
|
|
29
|
+
- A trivial or purely mechanical change (rename, formatting, dependency bump)
|
|
30
|
+
where the diff already tells the whole story. Say so in chat; don't manufacture
|
|
31
|
+
intent.
|
|
32
|
+
- You are mid-implementation, not at a reviewable stopping point.
|
|
33
|
+
- The user has declined review-intent for this change set.
|
|
34
|
+
|
|
35
|
+
## The honesty contract (read this before writing a word)
|
|
36
|
+
|
|
37
|
+
These rules exist to keep the artifact from becoming post-hoc theater:
|
|
38
|
+
|
|
39
|
+
1. **Intent is the reason you chose this, not a description of what it does.**
|
|
40
|
+
"Adds a cache" is not intent. "Chose an in-memory cache over Redis because the
|
|
41
|
+
data is request-scoped and we have one process" is intent.
|
|
42
|
+
2. **Name what you rejected — truthfully.** If there was a real alternative, state
|
|
43
|
+
it and why you didn't take it. If there genuinely wasn't one, write "no real
|
|
44
|
+
alternative considered" — do **not** invent a rejected option to look thorough.
|
|
45
|
+
3. **State what the change rests on.** The assumptions that, if false, make this
|
|
46
|
+
change wrong. This is the reviewer's highest-value target.
|
|
47
|
+
4. **Mark incidental changes as incidental.** If a hunk is a mechanical
|
|
48
|
+
consequence (a signature changed so callers had to), say that plainly instead
|
|
49
|
+
of inventing a decision for it.
|
|
50
|
+
5. **If you are uncertain, say you are uncertain.** "I think X but didn't verify
|
|
51
|
+
under concurrency" is more useful than false confidence.
|
|
52
|
+
|
|
53
|
+
If following these makes a section short or admits a gap, that is a success, not
|
|
54
|
+
a failure. The gap is the signal the reviewer needs.
|
|
55
|
+
|
|
56
|
+
## Completeness is mandatory (this is enforced)
|
|
57
|
+
|
|
58
|
+
Every changed file needs a \`what\` and a \`why\`, and **every hunk needs a \`what\`
|
|
59
|
+
and a \`why\`**. \`review-intent\` runs a completeness gate and **refuses to render**
|
|
60
|
+
if any changed file or hunk is missing intent — it prints the exact gaps. Do not
|
|
61
|
+
hand back a change set with empty intent; fill it before you offer the review.
|
|
62
|
+
(\`--allow-gaps\` exists for an explicit work-in-progress draft, and even then the
|
|
63
|
+
gaps render as red markers — it is not a way to skip the work.) review-intent
|
|
64
|
+
also renders an **intent-coverage gauge** — the measured share of files and hunks
|
|
65
|
+
you annotated — so partial coverage is visible at a glance, draft or not.
|
|
66
|
+
|
|
67
|
+
\`what\` vs \`why\`: \`what\` is a one-line description of the change (cheap — write it
|
|
68
|
+
first). \`why\` is the decision behind it and must not restate the what. "Renamed
|
|
69
|
+
\`x\` to \`y\`" is a what; "renamed for consistency with the \`z\` convention so callers
|
|
70
|
+
don't guess" is a why. For a mechanical hunk, an honest why is "incidental —
|
|
71
|
+
forced by the signature change above"; write that rather than leaving it blank.
|
|
72
|
+
|
|
73
|
+
## The artifact: \`.review/intent.json\`
|
|
74
|
+
|
|
75
|
+
Write it at the repo root. \`title\`, \`tldr\`, and \`overall\` are required, and so
|
|
76
|
+
are \`what\`/\`why\` on every file and hunk you list (see the completeness gate
|
|
77
|
+
above). The \`tldr\` is a five-second read shown as a lede at the top — the single
|
|
78
|
+
headline (what + the most important why); \`overall\` is the fuller story beneath
|
|
79
|
+
it. Don't make the tldr a copy of the title or just the "what" — it must carry a
|
|
80
|
+
why.
|
|
81
|
+
|
|
82
|
+
\`\`\`jsonc
|
|
83
|
+
{
|
|
84
|
+
"title": "Short change-set title",
|
|
85
|
+
"tldr": "One or two sentences a reviewer can read in five seconds: what this does and the single most important why.",
|
|
86
|
+
"overall": "Why this change set exists. What you rejected and why. What it rests on (assumptions that, if false, break it). Markdown.",
|
|
87
|
+
"risks": [
|
|
88
|
+
{ "assumption": "What the change rests on",
|
|
89
|
+
"ifFalse": "What breaks if it does not hold",
|
|
90
|
+
"howYoudKnow": "How a reviewer could check (optional)" }
|
|
91
|
+
],
|
|
92
|
+
"tests": [
|
|
93
|
+
{ "describes": "Plain-language sentence: what this test proves.",
|
|
94
|
+
"name": "RealTestIdentifier (optional, for cross-reference)",
|
|
95
|
+
"kind": "unit | integration | e2e | manual (optional)" }
|
|
96
|
+
],
|
|
97
|
+
"diagrams": {
|
|
98
|
+
"class": "classDiagram\\n ...", // structures you added/changed
|
|
99
|
+
"sequence": "sequenceDiagram\\n ..." // a flow the change affects; highlight changed steps
|
|
100
|
+
},
|
|
101
|
+
"files": [
|
|
102
|
+
{
|
|
103
|
+
"path": "src/foo.ts",
|
|
104
|
+
"what": "What changed in this file (one line is fine).",
|
|
105
|
+
"why": "Why — the decision behind it. REQUIRED for every changed file.",
|
|
106
|
+
"hunks": [
|
|
107
|
+
{ "anchor": 42,
|
|
108
|
+
"what": "What this specific change does.",
|
|
109
|
+
"why": "Why this specific change. REQUIRED. Anchor = a line number in the NEW file." }
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
### The blast radius (\`risks\`)
|
|
117
|
+
|
|
118
|
+
The \`risks\` array is the change's blast radius — one row per thing it rests on.
|
|
119
|
+
This is the reviewer's highest-value target, so write it to honesty-rule #3:
|
|
120
|
+
|
|
121
|
+
- Each row is an **assumption** (what must be true), the **ifFalse** consequence
|
|
122
|
+
(what breaks if it isn't), and optionally **howYoudKnow** (how to check).
|
|
123
|
+
- If the change genuinely rests on nothing, leave \`risks\` empty — but know that
|
|
124
|
+
review-intent renders an explicit "No risks declared" nudge, because a truly
|
|
125
|
+
assumption-free change is rare. An empty ledger is a claim, not a free pass.
|
|
126
|
+
- **review-intent independently measures several signals and renders them next
|
|
127
|
+
to your ledger — you cannot edit them.** A surface-area scorecard (files,
|
|
128
|
+
±lines, test-vs-code, debt/debug markers), a file-level reach graph, and the
|
|
129
|
+
**cyclomatic complexity of the changed functions** (hotspots above the repo
|
|
130
|
+
threshold are flagged, via \`lizard\`) all sit beside your claims. If you claim
|
|
131
|
+
"low risk" while the scorecard flags \`touches auth/, 0 test files\` or a
|
|
132
|
+
changed function jumps to CCN 30 and you never mention it, the contradiction is
|
|
133
|
+
visible at a glance.
|
|
134
|
+
- **There is an "honesty quadrant"** that plots your *claimed* candor (declared
|
|
135
|
+
risks + intent coverage) against the *measured* blast radius (churn + reach). A
|
|
136
|
+
large, far-reaching change that declared little lands in a red corner. Padding
|
|
137
|
+
the ledger with throwaway risks to nudge the dot is pointless — a human reads
|
|
138
|
+
the prose. Write a ledger the measured facts won't embarrass: address the gap,
|
|
139
|
+
or name it as a risk. If a hunk added real branching/complexity, the *why* is
|
|
140
|
+
the place to justify it.
|
|
141
|
+
|
|
142
|
+
### The tests section (\`tests\`)
|
|
143
|
+
|
|
144
|
+
Optional. List the test cases that cover this change, each described in **plain
|
|
145
|
+
language** — the value is a reviewer reading "Cache returns null on a miss"
|
|
146
|
+
instead of decoding \`CacheMiss_ReturnsNull\`. Each entry needs a \`describes\`
|
|
147
|
+
sentence; \`name\` (the real test identifier) and \`kind\` (\`unit\`, \`integration\`,
|
|
148
|
+
\`e2e\`, \`manual\`) are optional — known kinds group the list.
|
|
149
|
+
|
|
150
|
+
Honesty applies here too: describe the tests that **actually exist**, not the
|
|
151
|
+
ones you wish you'd written. If a behaviour is untested, don't invent a case —
|
|
152
|
+
either leave it out or, if it matters, name the gap in the risk ledger
|
|
153
|
+
(\`howYoudKnow\` is often exactly that test). \`describes\` is what the test proves
|
|
154
|
+
for a reader, not a restatement of its name. Omit the section entirely for a
|
|
155
|
+
change with no meaningful tests rather than padding it.
|
|
156
|
+
|
|
157
|
+
### Anchors
|
|
158
|
+
|
|
159
|
+
\`anchor\` is a line number in the **new** version of the file (the right side of
|
|
160
|
+
the diff). review-intent attaches the note to whichever hunk's new-line range
|
|
161
|
+
contains that anchor. Pick a line inside the change you are explaining. If you
|
|
162
|
+
get it wrong, the note still shows — under "Notes not matched to a hunk" — so it
|
|
163
|
+
is never lost, but aim to land it in the hunk.
|
|
164
|
+
|
|
165
|
+
### Diagrams (mermaid, authored by you)
|
|
166
|
+
|
|
167
|
+
- **Class diagram**: the types/modules you added or reshaped and their relations.
|
|
168
|
+
Keep it to what the change touches, not the whole system.
|
|
169
|
+
- **Sequence diagram**: a flow the change participates in. Highlight the steps
|
|
170
|
+
that changed using a \`rect\` block or a \`Note\`, e.g.:
|
|
171
|
+
|
|
172
|
+
\`\`\`
|
|
173
|
+
sequenceDiagram
|
|
174
|
+
Caller->>Service: request()
|
|
175
|
+
rect rgb(40,80,50)
|
|
176
|
+
Note over Service: CHANGED: now validates input first
|
|
177
|
+
end
|
|
178
|
+
Service-->>Caller: result
|
|
179
|
+
\`\`\`
|
|
180
|
+
|
|
181
|
+
Omit a diagram if the change genuinely has no structural or sequential story —
|
|
182
|
+
don't draw a trivial two-box diagram to fill the slot.
|
|
183
|
+
|
|
184
|
+
## After writing it
|
|
185
|
+
|
|
186
|
+
Offer to render — never auto-launch:
|
|
187
|
+
|
|
188
|
+
> I've written the review intent to \`.review/intent.json\`. Want me to open the
|
|
189
|
+
> side-by-side review? (\`review-intent\`)
|
|
190
|
+
|
|
191
|
+
If the user accepts, run \`review-intent\` via Bash from the repo root. It diffs
|
|
192
|
+
the current branch against main and opens the rendered page in the browser.
|
|
193
|
+
|
|
194
|
+
## Why this exists
|
|
195
|
+
|
|
196
|
+
The friction of hand-writing code used to carry reflection along for free. With
|
|
197
|
+
that friction gone, intent has to be chosen on purpose. This artifact is where
|
|
198
|
+
you pay for it — deliberately, in the reviewer's currency: why, rejected
|
|
199
|
+
alternatives, and assumptions. Render quality is not the hard part; the honesty
|
|
200
|
+
of what you write here is.
|
|
201
|
+
`;
|
|
202
|
+
export function skillFile(opts = {}) {
|
|
203
|
+
const scope = opts.scope ?? "user";
|
|
204
|
+
const root = scope === "local"
|
|
205
|
+
? (opts.cwd ?? process.cwd())
|
|
206
|
+
: (opts.home ?? os.homedir());
|
|
207
|
+
return path.join(root, ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
208
|
+
}
|
|
209
|
+
export async function installSkill(opts = {}) {
|
|
210
|
+
const file = skillFile(opts);
|
|
211
|
+
const existing = await readOrNull(file);
|
|
212
|
+
if (existing !== null && canonical(existing) === canonical(SKILL_CONTENT)) {
|
|
213
|
+
return "already";
|
|
214
|
+
}
|
|
215
|
+
if (existing !== null && !opts.force) {
|
|
216
|
+
return "conflict";
|
|
217
|
+
}
|
|
218
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
219
|
+
await fs.writeFile(file, SKILL_CONTENT, "utf8");
|
|
220
|
+
return existing === null ? "installed" : "updated";
|
|
221
|
+
}
|
|
222
|
+
export async function uninstallSkill(opts = {}) {
|
|
223
|
+
const file = skillFile(opts);
|
|
224
|
+
const existing = await readOrNull(file);
|
|
225
|
+
if (existing === null)
|
|
226
|
+
return "not-installed";
|
|
227
|
+
if (canonical(existing) !== canonical(SKILL_CONTENT) && !opts.force) {
|
|
228
|
+
return "modified";
|
|
229
|
+
}
|
|
230
|
+
await fs.rm(file);
|
|
231
|
+
// Best-effort cleanup of the now-empty skill directory; leave it alone if the
|
|
232
|
+
// user dropped other files in there. Only swallow the expected codes.
|
|
233
|
+
try {
|
|
234
|
+
await fs.rmdir(path.dirname(file));
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const code = err.code;
|
|
238
|
+
if (code !== "ENOTEMPTY" && code !== "ENOENT" && code !== "EEXIST")
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
return "removed";
|
|
242
|
+
}
|
|
243
|
+
// Normalize CRLF so an editor that rewrote line endings doesn't flip a clean
|
|
244
|
+
// install into a "conflict" / "modified" state.
|
|
245
|
+
function canonical(s) {
|
|
246
|
+
return s.replace(/\r\n/g, "\n");
|
|
247
|
+
}
|
|
248
|
+
async function readOrNull(p) {
|
|
249
|
+
try {
|
|
250
|
+
return await fs.readFile(p, "utf8");
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err.code === "ENOENT")
|
|
254
|
+
return null;
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The agent-authored artifact contract. The agent that made the changes writes
|
|
4
|
+
* this file (default `./.review/intent.json`); the CLI only ever reads it.
|
|
5
|
+
*/
|
|
6
|
+
export const HunkIntentSchema = z.object({
|
|
7
|
+
/** A line number in the NEW version of the file. The CLI attaches this note
|
|
8
|
+
* to whichever diff hunk's new-line range contains the anchor. */
|
|
9
|
+
anchor: z.number().int().positive(),
|
|
10
|
+
/** What this specific change does. */
|
|
11
|
+
what: z.string().min(1),
|
|
12
|
+
/** Why it was made — the decision, not a restatement of the what. */
|
|
13
|
+
why: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
export const FileIntentSchema = z.object({
|
|
16
|
+
path: z.string().min(1),
|
|
17
|
+
/** What changed in this file. */
|
|
18
|
+
what: z.string().min(1),
|
|
19
|
+
/** Why this file changed — the decision behind it (markdown). */
|
|
20
|
+
why: z.string().min(1),
|
|
21
|
+
hunks: z.array(HunkIntentSchema).optional().default([]),
|
|
22
|
+
});
|
|
23
|
+
export const DiagramsSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
/** Mermaid `classDiagram` source, authored by the agent. */
|
|
26
|
+
class: z.string().optional(),
|
|
27
|
+
/** Mermaid `sequenceDiagram` source; changed steps highlighted by the agent. */
|
|
28
|
+
sequence: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
.optional()
|
|
31
|
+
.default({});
|
|
32
|
+
/** One row of the agent-authored risk ledger (the "blast radius"). */
|
|
33
|
+
export const RiskSchema = z.object({
|
|
34
|
+
/** Something the change rests on; if false, the change is wrong. */
|
|
35
|
+
assumption: z.string().min(1),
|
|
36
|
+
/** What breaks if the assumption does not hold. */
|
|
37
|
+
ifFalse: z.string().min(1),
|
|
38
|
+
/** How a reviewer could find out whether it holds. Optional. */
|
|
39
|
+
howYoudKnow: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
/** One agent-described test case. Pure prose — the renderer never parses or
|
|
42
|
+
* measures it; it sits on the "claimed" side, next to the measured test count. */
|
|
43
|
+
export const TestCaseSchema = z.object({
|
|
44
|
+
/** A short, human-readable sentence: what the test proves. The only required
|
|
45
|
+
* field — the point is a reviewer reading it instead of a cryptic test name. */
|
|
46
|
+
describes: z.string().min(1),
|
|
47
|
+
/** The real test identifier, for cross-reference (e.g. `CacheMiss_ReturnsNull`). */
|
|
48
|
+
name: z.string().optional(),
|
|
49
|
+
/** Free-form kind (e.g. `unit`, `integration`, `e2e`, `manual`). Known kinds
|
|
50
|
+
* get a coloured tag and drive grouping; anything else is shown as-is. Kept a
|
|
51
|
+
* free string so an unusual kind never rejects the artifact. */
|
|
52
|
+
kind: z.string().optional(),
|
|
53
|
+
});
|
|
54
|
+
export const ArtifactSchema = z.object({
|
|
55
|
+
title: z.string().min(1),
|
|
56
|
+
/** One- or two-sentence executive summary, shown as a lede above `overall`. */
|
|
57
|
+
tldr: z.string().min(1),
|
|
58
|
+
/** Why this change set exists, what was rejected, what it rests on (markdown). */
|
|
59
|
+
overall: z.string().min(1),
|
|
60
|
+
diagrams: DiagramsSchema,
|
|
61
|
+
risks: z.array(RiskSchema).optional().default([]),
|
|
62
|
+
/** Human-readable descriptions of the test cases covering the change (claimed). */
|
|
63
|
+
tests: z.array(TestCaseSchema).optional().default([]),
|
|
64
|
+
files: z.array(FileIntentSchema).optional().default([]),
|
|
65
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@christianmorup/review-intent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render the diff between the current branch and main as an intent-annotated HTML review page with mermaid class & sequence diagrams.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Christian Mørup <cmo@immeo.dk>",
|
|
8
|
+
"homepage": "https://github.com/ChristianMorup/review-intent#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ChristianMorup/review-intent.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ChristianMorup/review-intent/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"code-review",
|
|
18
|
+
"diff",
|
|
19
|
+
"git",
|
|
20
|
+
"mermaid",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"review-intent": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "node --experimental-strip-types src/cli.ts",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"sample": "tsc && node scripts/gen-sample.mjs",
|
|
35
|
+
"prepare": "npm run build"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.17"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"open": "^10.1.0",
|
|
45
|
+
"parse-diff": "^0.11.1",
|
|
46
|
+
"zod": "^3.23.8"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"typescript": "^5.6.0",
|
|
51
|
+
"vitest": "^2.1.0"
|
|
52
|
+
}
|
|
53
|
+
}
|