@christianmorup/review-intent 0.1.0 → 0.1.1

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
@@ -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
 
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.1",
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",