@christianmorup/review-intent 0.1.0 → 0.1.3
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 +11 -8
- package/dist/render.js +1306 -259
- package/dist/review-order.js +78 -0
- package/dist/skill.js +18 -13
- package/dist/themes.js +176 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isNoisePath } from "./scorecard.js";
|
|
2
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
3
|
+
/** Stable, unique, deterministic anchor id for a file section. */
|
|
4
|
+
export function fileSlug(index) {
|
|
5
|
+
return `file-${index}`;
|
|
6
|
+
}
|
|
7
|
+
/** Highest CCN of a measured hotspot matching this file, or null. Matches
|
|
8
|
+
* lizard's (possibly differently-rooted) path by suffix/basename — same
|
|
9
|
+
* heuristic the change-map uses. */
|
|
10
|
+
function hotspotCcn(model, path) {
|
|
11
|
+
const cx = model.complexity;
|
|
12
|
+
if (!cx.available || cx.hotspots.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const p = norm(path);
|
|
15
|
+
const base = p.split("/").pop() ?? p;
|
|
16
|
+
let max = null;
|
|
17
|
+
for (const h of cx.hotspots) {
|
|
18
|
+
const hp = norm(h.file);
|
|
19
|
+
const hit = hp === p ||
|
|
20
|
+
hp.endsWith("/" + p) ||
|
|
21
|
+
p.endsWith("/" + hp) ||
|
|
22
|
+
(hp.split("/").pop() ?? hp) === base;
|
|
23
|
+
if (hit)
|
|
24
|
+
max = max === null ? h.ccn : Math.max(max, h.ccn);
|
|
25
|
+
}
|
|
26
|
+
return max;
|
|
27
|
+
}
|
|
28
|
+
/** Pure: one signal record per changed file, in original diff order. */
|
|
29
|
+
export function collectSignals(model) {
|
|
30
|
+
return model.files.map((f, i) => {
|
|
31
|
+
let added = 0;
|
|
32
|
+
let removed = 0;
|
|
33
|
+
for (const h of f.hunks)
|
|
34
|
+
for (const l of h.lines) {
|
|
35
|
+
if (l.type === "add")
|
|
36
|
+
added++;
|
|
37
|
+
else if (l.type === "del")
|
|
38
|
+
removed++;
|
|
39
|
+
}
|
|
40
|
+
const fanIn = model.reach.edges.reduce((n, e) => (norm(e.to) === norm(f.path) ? n + 1 : n), 0);
|
|
41
|
+
const maxCcn = hotspotCcn(model, f.path);
|
|
42
|
+
const missingIntent = !f.why || f.hunks.some((h) => h.intents.length === 0);
|
|
43
|
+
return {
|
|
44
|
+
path: f.path,
|
|
45
|
+
index: i,
|
|
46
|
+
slug: fileSlug(i),
|
|
47
|
+
status: f.status,
|
|
48
|
+
added,
|
|
49
|
+
removed,
|
|
50
|
+
churn: added + removed,
|
|
51
|
+
hunks: f.hunks.length,
|
|
52
|
+
fanIn,
|
|
53
|
+
hotspot: maxCcn !== null,
|
|
54
|
+
maxCcn,
|
|
55
|
+
missingIntent,
|
|
56
|
+
isNoise: isNoisePath(f.path),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Pure: files ranked by review priority (most attention-worthy first). Score
|
|
61
|
+
* blends normalized churn + reach with flat bonuses for a complexity hotspot
|
|
62
|
+
* and for unexplained changes, then demotes noise. Ties break by churn, then
|
|
63
|
+
* original diff order — fully deterministic. */
|
|
64
|
+
export function reviewOrder(model) {
|
|
65
|
+
const sig = collectSignals(model);
|
|
66
|
+
const maxChurn = Math.max(1, ...sig.map((s) => s.churn));
|
|
67
|
+
const maxFan = Math.max(1, ...sig.map((s) => s.fanIn));
|
|
68
|
+
const sq = (v, max) => Math.sqrt(v) / Math.sqrt(max);
|
|
69
|
+
const scored = sig.map((s, i) => {
|
|
70
|
+
const base = sq(s.churn, maxChurn) +
|
|
71
|
+
sq(s.fanIn, maxFan) +
|
|
72
|
+
(s.hotspot ? 0.6 : 0) +
|
|
73
|
+
(s.missingIntent ? 0.5 : 0);
|
|
74
|
+
return { sig: s, i, score: s.isNoise ? base * 0.25 : base };
|
|
75
|
+
});
|
|
76
|
+
scored.sort((a, b) => b.score - a.score || b.sig.churn - a.sig.churn || a.i - b.i);
|
|
77
|
+
return scored.map((x, idx) => ({ ...x.sig, score: x.score, rank: idx + 1 }));
|
|
78
|
+
}
|
package/dist/skill.js
CHANGED
|
@@ -8,7 +8,7 @@ const SKILL_NAME = "review-intent-authoring";
|
|
|
8
8
|
// against it (line-ending-normalized) to decide whether the file is still ours.
|
|
9
9
|
export const SKILL_CONTENT = `---
|
|
10
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
|
|
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 render it with review-intent (run it, don't ask) so the reviewer adjudicates decisions instead of skimming lines.
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Authoring an honest intent artifact
|
|
@@ -131,13 +131,12 @@ This is the reviewer's highest-value target, so write it to honesty-rule #3:
|
|
|
131
131
|
"low risk" while the scorecard flags \`touches auth/, 0 test files\` or a
|
|
132
132
|
changed function jumps to CCN 30 and you never mention it, the contradiction is
|
|
133
133
|
visible at a glance.
|
|
134
|
-
- **There is
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
the
|
|
139
|
-
|
|
140
|
-
the place to justify it.
|
|
134
|
+
- **There is a "change map"** that plots each changed file by *measured*
|
|
135
|
+
downstream reach (how many files import it) against *measured* churn, flagging
|
|
136
|
+
the ones that also carry a complexity hotspot. The biggest, most depended-on
|
|
137
|
+
files land in the top-right — that's where a reviewer looks first, so those are
|
|
138
|
+
the files whose *why* had better be airtight. If a hunk added real
|
|
139
|
+
branching/complexity, the *why* is the place to justify it.
|
|
141
140
|
|
|
142
141
|
### The tests section (\`tests\`)
|
|
143
142
|
|
|
@@ -183,13 +182,19 @@ don't draw a trivial two-box diagram to fill the slot.
|
|
|
183
182
|
|
|
184
183
|
## After writing it
|
|
185
184
|
|
|
186
|
-
|
|
185
|
+
Render it — don't ask first. You're inside this skill because a review is
|
|
186
|
+
wanted; writing the artifact and *then* asking "should I open it?" just adds a
|
|
187
|
+
round-trip the user did not want. Run \`review-intent\` via Bash from the repo
|
|
188
|
+
root — it diffs the current branch against main, writes \`review.html\`, and opens
|
|
189
|
+
it in the browser. Then tell the user you've opened it (and where the file is);
|
|
190
|
+
don't ask permission to.
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
The only times you don't run it: the change was trivial/mechanical so you
|
|
193
|
+
skipped the artifact entirely (say so in chat), or the user has explicitly
|
|
194
|
+
declined review-intent for this change set.
|
|
190
195
|
|
|
191
|
-
If the
|
|
192
|
-
|
|
196
|
+
If the completeness gate refuses to render because intent is missing, fix the
|
|
197
|
+
gaps and run again — do **not** reach for \`--allow-gaps\` to get past it.
|
|
193
198
|
|
|
194
199
|
## Why this exists
|
|
195
200
|
|
package/dist/themes.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Pure: visual theme catalog + CSS emitter. No I/O, no Date/random.
|
|
2
|
+
* `paper` (the default) lives in render.ts `:root`; this module supplies
|
|
3
|
+
* every *other* theme as a `[data-theme="id"]` override block. */
|
|
4
|
+
/** Canonical token contract — every theme must define all of these. */
|
|
5
|
+
export const TOKEN_KEYS = [
|
|
6
|
+
"--paper", "--surface", "--surface-2", "--ink", "--ink-soft", "--muted",
|
|
7
|
+
"--line", "--line-2", "--accent", "--accent-soft",
|
|
8
|
+
"--add", "--add-soft", "--del", "--del-soft", "--warn", "--warn-soft",
|
|
9
|
+
"--add-border", "--del-border", "--warn-border", "--accent-border",
|
|
10
|
+
"--accent-shadow", "--on-accent", "--glass", "--code-add", "--code-del",
|
|
11
|
+
"--viz-add", "--viz-add-ink", "--viz-del", "--viz-del-ink", "--viz-warn",
|
|
12
|
+
"--viz-accent", "--viz-line", "--viz-node", "--viz-node-stroke",
|
|
13
|
+
"--viz-accent-stroke", "--viz-cell-stroke", "--viz-noise", "--viz-other",
|
|
14
|
+
"--viz-cell-label", "--viz-zone", "--kind-e2e",
|
|
15
|
+
"--viz-s1", "--viz-s2", "--viz-s3", "--viz-s4",
|
|
16
|
+
"--viz-s5", "--viz-s6", "--viz-s7", "--viz-s8",
|
|
17
|
+
];
|
|
18
|
+
const DEFAULT_SERIES = [
|
|
19
|
+
"#5b7db1", "#5fa389", "#b08a5a", "#a07ba6",
|
|
20
|
+
"#c47d72", "#7fa86a", "#d0a85a", "#7a93b8",
|
|
21
|
+
];
|
|
22
|
+
/** Expand core palette into the full token record. Derived tokens alias core
|
|
23
|
+
* values (no color math) so a theme stays ~16 lines; any token can still be
|
|
24
|
+
* overridden by adding it to the returned record after the fact. */
|
|
25
|
+
export function makeTheme(id, label, group, c) {
|
|
26
|
+
const s = c.series ?? DEFAULT_SERIES;
|
|
27
|
+
const tokens = {
|
|
28
|
+
"--paper": c.paper, "--surface": c.surface, "--surface-2": c.surface2,
|
|
29
|
+
"--ink": c.ink, "--ink-soft": c.inkSoft, "--muted": c.muted,
|
|
30
|
+
"--line": c.line, "--line-2": c.line2,
|
|
31
|
+
"--accent": c.accent, "--accent-soft": c.accentSoft,
|
|
32
|
+
"--add": c.add, "--add-soft": c.addSoft,
|
|
33
|
+
"--del": c.del, "--del-soft": c.delSoft,
|
|
34
|
+
"--warn": c.warn, "--warn-soft": c.warnSoft,
|
|
35
|
+
"--add-border": c.add, "--del-border": c.del,
|
|
36
|
+
"--warn-border": c.warn, "--accent-border": c.accent,
|
|
37
|
+
"--accent-shadow": c.accentShadow ?? "rgba(0,0,0,.2)",
|
|
38
|
+
"--on-accent": c.onAccent ?? "#fff",
|
|
39
|
+
"--glass": c.glass ?? c.surface,
|
|
40
|
+
"--code-add": c.codeAdd ?? c.add,
|
|
41
|
+
"--code-del": c.codeDel ?? c.del,
|
|
42
|
+
"--viz-add": c.add, "--viz-add-ink": c.add,
|
|
43
|
+
"--viz-del": c.del, "--viz-del-ink": c.del,
|
|
44
|
+
"--viz-warn": c.warn, "--viz-accent": c.accent, "--viz-line": c.line,
|
|
45
|
+
"--viz-node": c.surface, "--viz-node-stroke": c.line2,
|
|
46
|
+
"--viz-accent-stroke": c.accent, "--viz-cell-stroke": c.surface,
|
|
47
|
+
"--viz-noise": c.muted, "--viz-other": c.muted,
|
|
48
|
+
"--viz-cell-label": c.cellLabel ?? c.ink,
|
|
49
|
+
"--viz-zone": c.delSoft, "--kind-e2e": c.accent,
|
|
50
|
+
"--viz-s1": s[0], "--viz-s2": s[1], "--viz-s3": s[2], "--viz-s4": s[3],
|
|
51
|
+
"--viz-s5": s[4], "--viz-s6": s[5], "--viz-s7": s[6], "--viz-s8": s[7],
|
|
52
|
+
};
|
|
53
|
+
if (c.sans)
|
|
54
|
+
tokens["--sans"] = c.sans;
|
|
55
|
+
if (c.mono)
|
|
56
|
+
tokens["--mono"] = c.mono;
|
|
57
|
+
return { id, label, group, tokens };
|
|
58
|
+
}
|
|
59
|
+
export const THEMES = [
|
|
60
|
+
makeTheme("dark", "Dark", "Playful", {
|
|
61
|
+
paper: "#1b1a17", surface: "#232220", surface2: "#2c2a26",
|
|
62
|
+
ink: "#ece9e1", inkSoft: "#b8b2a6", muted: "#847d6f",
|
|
63
|
+
line: "#34322c", line2: "#44413a",
|
|
64
|
+
accent: "#6fa3e0", accentSoft: "#20303f",
|
|
65
|
+
add: "#56c07a", addSoft: "#18301f", del: "#e8786c", delSoft: "#3a201d",
|
|
66
|
+
warn: "#d6a64a", warnSoft: "#332a16", onAccent: "#000",
|
|
67
|
+
}),
|
|
68
|
+
makeTheme("hacker", "Hacker", "Playful", {
|
|
69
|
+
paper: "#000800", surface: "#021202", surface2: "#001a00",
|
|
70
|
+
ink: "#33ff66", inkSoft: "#1faf47", muted: "#0f7a2e",
|
|
71
|
+
line: "#093d12", line2: "#0e5a1c",
|
|
72
|
+
accent: "#7dff7d", accentSoft: "#00270c",
|
|
73
|
+
add: "#39ff77", addSoft: "#002a0e", del: "#ff5f56", delSoft: "#2a0a08",
|
|
74
|
+
warn: "#d4ff3a", warnSoft: "#1d2a00", onAccent: "#000",
|
|
75
|
+
mono: 'ui-monospace, "JetBrains Mono", "Cascadia Code", Consolas, monospace',
|
|
76
|
+
}),
|
|
77
|
+
makeTheme("solarized-light", "Solarized Light", "Dev favorites", {
|
|
78
|
+
paper: "#fdf6e3", surface: "#fdf6e3", surface2: "#eee8d5",
|
|
79
|
+
ink: "#586e75", inkSoft: "#657b83", muted: "#6c7b7b",
|
|
80
|
+
line: "#eee8d5", line2: "#d6cfbb",
|
|
81
|
+
accent: "#268bd2", accentSoft: "#dcebf2",
|
|
82
|
+
add: "#5f6d00", addSoft: "#eef0d8", del: "#dc322f", delSoft: "#f6e0db",
|
|
83
|
+
warn: "#8a6700", warnSoft: "#f3ead0", onAccent: "#000",
|
|
84
|
+
}),
|
|
85
|
+
makeTheme("solarized-dark", "Solarized Dark", "Dev favorites", {
|
|
86
|
+
paper: "#002b36", surface: "#073642", surface2: "#003744",
|
|
87
|
+
ink: "#93a1a1", inkSoft: "#839496", muted: "#6c8088",
|
|
88
|
+
line: "#0a4250", line2: "#135561",
|
|
89
|
+
accent: "#268bd2", accentSoft: "#03323e",
|
|
90
|
+
add: "#859900", addSoft: "#14331f", del: "#dc322f", delSoft: "#33161a",
|
|
91
|
+
warn: "#b58900", warnSoft: "#2e2812", onAccent: "#000",
|
|
92
|
+
}),
|
|
93
|
+
makeTheme("nord", "Nord", "Dev favorites", {
|
|
94
|
+
paper: "#2e3440", surface: "#3b4252", surface2: "#434c5e",
|
|
95
|
+
ink: "#eceff4", inkSoft: "#d8dee9", muted: "#9aa5b5",
|
|
96
|
+
line: "#434c5e", line2: "#4c566a",
|
|
97
|
+
accent: "#88c0d0", accentSoft: "#2b333f",
|
|
98
|
+
add: "#a3be8c", addSoft: "#2c3a2e", del: "#bf616a", delSoft: "#3a2a2c",
|
|
99
|
+
warn: "#ebcb8b", warnSoft: "#3a3424", onAccent: "#000",
|
|
100
|
+
}),
|
|
101
|
+
makeTheme("gruvbox", "Gruvbox", "Dev favorites", {
|
|
102
|
+
paper: "#282828", surface: "#32302f", surface2: "#3c3836",
|
|
103
|
+
ink: "#ebdbb2", inkSoft: "#d5c4a1", muted: "#a89984",
|
|
104
|
+
line: "#3c3836", line2: "#504945",
|
|
105
|
+
accent: "#83a598", accentSoft: "#2b3331",
|
|
106
|
+
add: "#b8bb26", addSoft: "#2f3318", del: "#fb4934", delSoft: "#3a201c",
|
|
107
|
+
warn: "#fabd2f", warnSoft: "#3a3014", onAccent: "#000",
|
|
108
|
+
}),
|
|
109
|
+
makeTheme("catppuccin", "Catppuccin", "Dev favorites", {
|
|
110
|
+
paper: "#1e1e2e", surface: "#313244", surface2: "#45475a",
|
|
111
|
+
ink: "#cdd6f4", inkSoft: "#bac2de", muted: "#9399b2",
|
|
112
|
+
line: "#313244", line2: "#585b70",
|
|
113
|
+
accent: "#89b4fa", accentSoft: "#2a2c41",
|
|
114
|
+
add: "#a6e3a1", addSoft: "#26342a", del: "#f38ba8", delSoft: "#361f2a",
|
|
115
|
+
warn: "#f9e2af", warnSoft: "#36321f", onAccent: "#000",
|
|
116
|
+
}),
|
|
117
|
+
makeTheme("github", "GitHub", "Professional", {
|
|
118
|
+
paper: "#ffffff", surface: "#ffffff", surface2: "#f6f8fa",
|
|
119
|
+
ink: "#1f2328", inkSoft: "#656d76", muted: "#8c959f",
|
|
120
|
+
line: "#d0d7de", line2: "#afb8c1",
|
|
121
|
+
accent: "#0969da", accentSoft: "#ddf4ff",
|
|
122
|
+
add: "#1a7f37", addSoft: "#dafbe1", del: "#cf222e", delSoft: "#ffebe9",
|
|
123
|
+
warn: "#9a6700", warnSoft: "#fff8c5",
|
|
124
|
+
}),
|
|
125
|
+
makeTheme("high-contrast", "High Contrast", "Professional", {
|
|
126
|
+
paper: "#ffffff", surface: "#ffffff", surface2: "#f0f0f0",
|
|
127
|
+
ink: "#000000", inkSoft: "#000000", muted: "#3a3a3a",
|
|
128
|
+
line: "#000000", line2: "#000000",
|
|
129
|
+
accent: "#0000cc", accentSoft: "#e6e6ff",
|
|
130
|
+
add: "#006400", addSoft: "#e6f5e6", del: "#b00000", delSoft: "#ffe6e6",
|
|
131
|
+
warn: "#7a4d00", warnSoft: "#fff3e0",
|
|
132
|
+
}),
|
|
133
|
+
makeTheme("blueprint", "Blueprint", "Professional", {
|
|
134
|
+
paper: "#0d2747", surface: "#123257", surface2: "#16395f",
|
|
135
|
+
ink: "#dbeafe", inkSoft: "#a9c7ee", muted: "#6f93c0",
|
|
136
|
+
line: "#1d4373", line2: "#2a558c",
|
|
137
|
+
accent: "#5ad1ff", accentSoft: "#0e2c4d",
|
|
138
|
+
add: "#5ee0a0", addSoft: "#103a2c", del: "#ff8a8a", delSoft: "#3a1d22",
|
|
139
|
+
warn: "#ffd166", warnSoft: "#33301a", onAccent: "#000",
|
|
140
|
+
}),
|
|
141
|
+
makeTheme("newsprint", "Newsprint", "Editorial", {
|
|
142
|
+
paper: "#f7f5ef", surface: "#ffffff", surface2: "#ece9e1",
|
|
143
|
+
ink: "#111111", inkSoft: "#333333", muted: "#6b6b6b",
|
|
144
|
+
line: "#cfcabf", line2: "#b3ada0",
|
|
145
|
+
accent: "#8a1f11", accentSoft: "#f1e3e0",
|
|
146
|
+
add: "#1a6b34", addSoft: "#e6f0e8", del: "#a31d12", delSoft: "#f6e3e0",
|
|
147
|
+
warn: "#8a6400", warnSoft: "#f1ead4",
|
|
148
|
+
sans: 'Iowan Old Style, Georgia, "Times New Roman", Times, serif',
|
|
149
|
+
}),
|
|
150
|
+
makeTheme("sepia", "Sepia", "Editorial", {
|
|
151
|
+
paper: "#e9dcc3", surface: "#f3e9d6", surface2: "#ddcfb2",
|
|
152
|
+
ink: "#4a3b28", inkSoft: "#5f4d34", muted: "#776344",
|
|
153
|
+
line: "#d4c4a3", line2: "#c4b08a",
|
|
154
|
+
accent: "#8a5a2b", accentSoft: "#ead9bf",
|
|
155
|
+
add: "#4f5e24", addSoft: "#dfe0bf", del: "#9c3b28", delSoft: "#ecd5cc",
|
|
156
|
+
warn: "#7e5e22", warnSoft: "#ece0c0",
|
|
157
|
+
}),
|
|
158
|
+
makeTheme("synthwave", "Synthwave", "Playful", {
|
|
159
|
+
paper: "#1a1033", surface: "#251447", surface2: "#2f1a57",
|
|
160
|
+
ink: "#f6e6ff", inkSoft: "#d3b8f0", muted: "#9a7fc0",
|
|
161
|
+
line: "#3a2470", line2: "#4d2f8f",
|
|
162
|
+
accent: "#ff5fd2", accentSoft: "#2e1450",
|
|
163
|
+
add: "#36f9c5", addSoft: "#0e3a32", del: "#ff5f6d", delSoft: "#3a1622",
|
|
164
|
+
warn: "#ffd23f", warnSoft: "#332a10", onAccent: "#000",
|
|
165
|
+
}),
|
|
166
|
+
];
|
|
167
|
+
/** Emit a `[data-theme="id"]{…}` block per theme. `paper` (default) is NOT
|
|
168
|
+
* emitted — it lives in render.ts `:root`. Deterministic string output. */
|
|
169
|
+
export function themeCss() {
|
|
170
|
+
return THEMES.map((t) => {
|
|
171
|
+
const decls = Object.entries(t.tokens)
|
|
172
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
173
|
+
.join("\n");
|
|
174
|
+
return `[data-theme="${t.id}"] {\n${decls}\n}`;
|
|
175
|
+
}).join("\n");
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@christianmorup/review-intent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Render the diff between the current branch and main as an intent-annotated HTML review page with mermaid class & sequence diagrams.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|