@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.
@@ -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 offer to render it with review-intent so the reviewer adjudicates decisions instead of skimming lines.
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 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.
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
- Offer to render never auto-launch:
185
+ Render itdon'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
- > I've written the review intent to \`.review/intent.json\`. Want me to open the
189
- > side-by-side review? (\`review-intent\`)
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 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.
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.0",
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",