tech-debt-visualizer 0.2.1 → 0.2.2

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 CHANGED
@@ -68,6 +68,8 @@ So: **static metrics + git → score & tier → optional LLM explanations and co
68
68
  | **Global (after publish)** | `npm install -g tech-debt-visualizer` then `tech-debt analyze [path]` |
69
69
  | **No install (after publish)** | `npx tech-debt-visualizer analyze [path]` |
70
70
 
71
+ Use **`tech-debt-visualizer`** in the command (not `tech-debt`); `npx tech-debt` runs a different npm package. From this repo you can also run `npm run analyze` or `node dist/cli.js analyze .`.
72
+
71
73
  Requires **Node 18+**.
72
74
 
73
75
  ---
package/dist/llm.js CHANGED
@@ -70,9 +70,10 @@ function parseFileAssessmentResponse(raw) {
70
70
  }
71
71
  /** Resolve provider and auth from config + env. Explicit baseURL = OpenAI-compatible; else key format or env picks provider. */
72
72
  export function resolveLLMConfig(config = {}) {
73
+ const trim = (s) => (typeof s === "string" ? s.trim() : undefined) || undefined;
73
74
  const explicitBase = (config.baseURL ?? process.env.OPENAI_BASE_URL)?.replace(/\/$/, "");
74
- const cliKey = config.apiKey;
75
- const openaiKey = cliKey ?? process.env.OPENAI_API_KEY ?? process.env.ANTHROPIC_API_KEY;
75
+ const cliKey = trim(config.apiKey);
76
+ const openaiKey = trim(cliKey ?? process.env.OPENAI_API_KEY ?? process.env.ANTHROPIC_API_KEY);
76
77
  if (explicitBase && openaiKey) {
77
78
  return {
78
79
  provider: "openai",
@@ -99,20 +100,20 @@ export function resolveLLMConfig(config = {}) {
99
100
  };
100
101
  }
101
102
  }
102
- if (cliKey ?? process.env.OPENROUTER_API_KEY) {
103
- const key = cliKey ?? process.env.OPENROUTER_API_KEY;
103
+ const openRouterKey = trim(cliKey ?? process.env.OPENROUTER_API_KEY);
104
+ if (openRouterKey) {
104
105
  return {
105
106
  provider: "openrouter",
106
- apiKey: key,
107
+ apiKey: openRouterKey,
107
108
  baseURL: config.baseURL ?? process.env.OPENROUTER_BASE_URL ?? OPENROUTER_BASE,
108
109
  model: config.model ?? process.env.OPENROUTER_MODEL ?? OPENROUTER_DEFAULT_MODEL,
109
110
  };
110
111
  }
111
- if (cliKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_GENAI_API_KEY) {
112
- const key = cliKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_GENAI_API_KEY;
112
+ const geminiKey = trim(cliKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_GENAI_API_KEY);
113
+ if (geminiKey) {
113
114
  return {
114
115
  provider: "gemini",
115
- apiKey: key,
116
+ apiKey: geminiKey,
116
117
  baseURL: GEMINI_BASE,
117
118
  model: config.model ?? process.env.GEMINI_MODEL ?? GEMINI_DEFAULT_MODEL,
118
119
  };
@@ -1,141 +1,297 @@
1
- /* Technical Debt Report - theme and layout */
1
+ /* Technical Debt Report — Grafana-style dashboard */
2
2
  :root {
3
- --bg: #fff;
4
- --surface: #fff;
5
- --border: #ccc;
6
- --text: #222;
7
- --text-muted: #666;
8
- --link: #0568c2;
3
+ --bg: #0b0c0e;
4
+ --bg-elevated: #111214;
5
+ --surface: #181b1f;
6
+ --surface-hover: #1e2127;
7
+ --border: #2c3239;
8
+ --border-subtle: #252a30;
9
+ --text: #e8e9ea;
10
+ --text-muted: #8b9199;
11
+ --link: #5794f2;
12
+ --radius: 4px;
13
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
9
14
  }
10
15
 
11
- [data-theme="dark"] {
12
- --bg: #1a1a1a;
13
- --surface: #252525;
14
- --border: #404040;
15
- --text: #e0e0e0;
16
- --text-muted: #999;
17
- --link: #6eb3f7;
16
+ [data-theme="light"] {
17
+ --bg: #f4f5f5;
18
+ --bg-elevated: #fff;
19
+ --surface: #fff;
20
+ --surface-hover: #f9fafb;
21
+ --border: #d8d9da;
22
+ --border-subtle: #e8e9ea;
23
+ --text: #1e2022;
24
+ --text-muted: #6b7077;
25
+ --link: #3274d9;
26
+ --shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
18
27
  }
19
28
 
20
29
  * { box-sizing: border-box; }
21
30
 
22
31
  body {
23
32
  margin: 0;
24
- font-family: system-ui, sans-serif;
33
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
25
34
  background: var(--bg);
26
35
  color: var(--text);
27
36
  min-height: 100vh;
28
- font-size: 14px;
37
+ font-size: 13px;
29
38
  line-height: 1.5;
39
+ -webkit-font-smoothing: antialiased;
30
40
  }
31
41
 
32
- .container {
33
- max-width: 900px;
34
- margin: 0 auto;
35
- padding: 1.5rem 1.25rem;
42
+ body.dashboard-page {
43
+ padding-bottom: 2rem;
36
44
  }
37
45
 
38
- /* Banner */
39
- .no-llm-banner {
40
- width: 100%;
41
- background: var(--surface);
46
+ /* ——— Dashboard header (Grafana-style top bar) ——— */
47
+ .dashboard-header {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ flex-wrap: wrap;
52
+ gap: 0.75rem;
53
+ padding: 0.75rem 1.5rem;
54
+ background: var(--bg-elevated);
42
55
  border-bottom: 1px solid var(--border);
43
- padding: 0.5rem 1rem;
44
- text-align: center;
56
+ position: sticky;
57
+ top: 0;
58
+ z-index: 10;
45
59
  }
46
60
 
47
- .no-llm-banner .no-llm-cta {
48
- font-size: 14px;
49
- font-weight: normal;
61
+ .dashboard-header-left {
62
+ display: flex;
63
+ align-items: baseline;
64
+ gap: 0.75rem;
65
+ min-width: 0;
66
+ }
67
+
68
+ .dashboard-title {
69
+ margin: 0;
70
+ font-size: 1.125rem;
71
+ font-weight: 600;
72
+ letter-spacing: -0.01em;
50
73
  color: var(--text);
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ text-overflow: ellipsis;
77
+ }
78
+
79
+ .dashboard-meta {
80
+ font-size: 12px;
81
+ color: var(--text-muted);
82
+ white-space: nowrap;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ }
86
+
87
+ .dashboard-header-right {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 1rem;
91
+ flex-shrink: 0;
92
+ }
93
+
94
+ .dashboard-score {
95
+ display: inline-flex;
96
+ align-items: baseline;
97
+ gap: 0.2rem;
98
+ padding: 0.35rem 0.65rem;
99
+ border-radius: var(--radius);
100
+ font-weight: 600;
101
+ }
102
+
103
+ .dashboard-score-value {
104
+ font-size: 1.25rem;
105
+ line-height: 1;
106
+ }
107
+
108
+ .dashboard-score-of {
109
+ font-size: 0.85rem;
110
+ font-weight: 500;
111
+ color: var(--text-muted);
112
+ }
113
+
114
+ .dashboard-score-label {
115
+ font-size: 11px;
116
+ font-weight: 500;
117
+ text-transform: uppercase;
118
+ letter-spacing: 0.04em;
119
+ margin-left: 0.35rem;
120
+ opacity: 0.95;
121
+ }
122
+
123
+ .dashboard-score.tier-1 { background: rgba(204, 0, 0, 0.2); color: #f66; }
124
+ .dashboard-score.tier-2 { background: rgba(232, 93, 0, 0.2); color: #f90; }
125
+ .dashboard-score.tier-3 { background: rgba(184, 134, 11, 0.2); color: #db9; }
126
+ .dashboard-score.tier-4 { background: rgba(0, 102, 153, 0.2); color: #6cf; }
127
+ .dashboard-score.tier-5 { background: rgba(10, 107, 10, 0.2); color: #6c6; }
128
+
129
+ .dashboard-date {
130
+ font-size: 12px;
131
+ color: var(--text-muted);
132
+ }
133
+
134
+ /* ——— Main content & grid ——— */
135
+ .dashboard-main {
136
+ max-width: 1200px;
137
+ margin: 0 auto;
138
+ padding: 1.25rem 1.5rem;
139
+ }
140
+
141
+ .dashboard-grid {
142
+ display: grid;
143
+ gap: 1rem;
144
+ margin-bottom: 1rem;
145
+ }
146
+
147
+ .dashboard-grid-stats {
148
+ grid-template-columns: repeat(4, 1fr);
149
+ }
150
+
151
+ .dashboard-grid-half {
152
+ grid-template-columns: repeat(2, 1fr);
153
+ }
154
+
155
+ @media (max-width: 900px) {
156
+ .dashboard-grid-stats { grid-template-columns: repeat(2, 1fr); }
157
+ .dashboard-grid-half { grid-template-columns: 1fr; }
158
+ }
159
+
160
+ @media (max-width: 560px) {
161
+ .dashboard-header { padding: 0.6rem 1rem; }
162
+ .dashboard-main { padding: 1rem; }
163
+ .dashboard-grid-stats { grid-template-columns: 1fr; }
164
+ }
165
+
166
+ /* ——— Panels (Grafana-style) ——— */
167
+ .panel {
168
+ background: var(--surface);
169
+ border: 1px solid var(--border);
170
+ border-radius: var(--radius);
171
+ box-shadow: var(--shadow);
172
+ overflow: hidden;
173
+ }
174
+
175
+ .panel-header {
176
+ padding: 0.75rem 1rem;
177
+ border-bottom: 1px solid var(--border-subtle);
178
+ background: rgba(0, 0, 0, 0.03);
179
+ }
180
+
181
+ [data-theme="light"] .panel-header {
182
+ background: rgba(0, 0, 0, 0.02);
183
+ }
184
+
185
+ .panel-title {
51
186
  margin: 0;
187
+ font-size: 13px;
188
+ font-weight: 600;
189
+ color: var(--text);
190
+ letter-spacing: -0.01em;
52
191
  }
53
192
 
54
- /* Hero / score */
55
- .hero-caption {
193
+ .panel-desc {
194
+ margin: 0.25rem 0 0;
56
195
  font-size: 12px;
57
196
  color: var(--text-muted);
58
- margin: 0 0 0.5rem;
59
- text-align: center;
197
+ line-height: 1.4;
60
198
  }
61
199
 
62
- .hero {
200
+ .panel-body {
201
+ padding: 1rem;
202
+ }
203
+
204
+ .panel-body-center {
63
205
  text-align: center;
64
- margin-bottom: 1.5rem;
65
206
  }
66
207
 
67
- .score-badge {
208
+ /* Stat panels (KPI row) */
209
+ .stat-panel .panel-body {
210
+ padding: 1rem 1.25rem;
211
+ }
212
+
213
+ .stat-value {
214
+ font-size: 1.75rem;
215
+ font-weight: 700;
216
+ line-height: 1.2;
217
+ color: var(--text);
218
+ letter-spacing: -0.02em;
219
+ }
220
+
221
+ .stat-label {
222
+ font-size: 11px;
223
+ font-weight: 500;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.04em;
226
+ color: var(--text-muted);
227
+ margin-top: 0.25rem;
228
+ }
229
+
230
+ .stat-panel-warn .stat-value { color: #f44; }
231
+
232
+ /* Score panel (cleanliness gauge) */
233
+ .panel-score .panel-body-center {
234
+ padding: 1.25rem 1rem;
235
+ }
236
+
237
+ .panel-score .score-badge {
68
238
  display: inline-block;
69
239
  margin-bottom: 0.75rem;
70
240
  line-height: 0;
71
241
  }
72
242
 
73
- .score-badge-svg {
243
+ .panel-score .score-badge-svg {
74
244
  display: block;
75
- width: 160px;
245
+ width: 120px;
76
246
  height: auto;
77
- max-width: min(160px, 40vw);
78
247
  }
79
248
 
80
- .score-badge-svg .score-badge-num {
81
- font-family: system-ui, -apple-system, sans-serif;
82
- font-size: 56px;
249
+ .panel-score .score-badge-svg .score-badge-num {
250
+ font-size: 42px;
83
251
  font-weight: 800;
84
252
  letter-spacing: -0.02em;
85
253
  }
86
254
 
87
- .score-badge-svg .score-badge-of {
88
- font-family: system-ui, -apple-system, sans-serif;
89
- font-size: 14px;
255
+ .panel-score .score-badge-svg .score-badge-of {
256
+ font-size: 12px;
90
257
  font-weight: 700;
91
258
  letter-spacing: 0.08em;
92
259
  text-transform: uppercase;
93
260
  opacity: 0.95;
94
261
  }
95
262
 
96
- .hero .score-label { font-size: 1.1rem; font-weight: bold; color: var(--text); margin: 0 0 0.2rem; }
97
- .hero .score-desc { font-size: 13px; color: var(--text-muted); margin: 0 0 0.35rem; max-width: 360px; margin-left: auto; margin-right: auto; }
98
- .hero .report-meta { font-size: 12px; color: var(--text-muted); }
99
-
100
- .hero.tier-1 { --tier-bg: #c00; --tier-num: #fff; }
101
- .hero.tier-2 { --tier-bg: #e85d00; --tier-num: #fff; }
102
- .hero.tier-3 { --tier-bg: #b8860b; --tier-num: #fff; }
103
- .hero.tier-4 { --tier-bg: #069; --tier-num: #fff; }
104
- .hero.tier-5 { --tier-bg: #0a6b0a; --tier-num: #fff; }
105
-
106
- /* Summary cards */
107
- .summary-cards {
108
- display: grid;
109
- grid-template-columns: repeat(4, 1fr);
110
- gap: 0.75rem;
111
- margin-bottom: 1.5rem;
263
+ .panel-score .score-desc {
264
+ font-size: 13px;
265
+ color: var(--text-muted);
266
+ margin: 0;
267
+ max-width: 320px;
268
+ margin-left: auto;
269
+ margin-right: auto;
270
+ line-height: 1.45;
112
271
  }
113
272
 
114
- @media (max-width: 640px) {
115
- .summary-cards { grid-template-columns: repeat(2, 1fr); }
273
+ /* LLM panel accent */
274
+ .panel-llm {
275
+ border-left: 3px solid var(--link);
116
276
  }
117
277
 
118
- .card {
278
+ /* ——— Banner ——— */
279
+ .no-llm-banner {
280
+ width: 100%;
119
281
  background: var(--surface);
120
- border: 1px solid var(--border);
121
- padding: 0.75rem 1rem;
282
+ border-bottom: 1px solid var(--border);
283
+ padding: 0.5rem 1rem;
284
+ text-align: center;
122
285
  }
123
286
 
124
- .card .value { font-size: 1.35rem; font-weight: bold; color: var(--text); }
125
- .card .label { font-size: 12px; color: var(--text-muted); margin-top: 0.15rem; }
126
-
127
- /* Sections */
128
- .section {
129
- background: var(--surface);
130
- border: 1px solid var(--border);
131
- padding: 1rem 1.25rem;
132
- margin-bottom: 1rem;
287
+ .no-llm-banner .no-llm-cta {
288
+ font-size: 13px;
289
+ font-weight: normal;
290
+ color: var(--text-muted);
291
+ margin: 0;
133
292
  }
134
293
 
135
- .section h2 { font-size: 1rem; margin: 0 0 0.75rem; color: var(--text); font-weight: bold; }
136
- .section-desc { font-size: 13px; color: var(--text-muted); margin: -0.35rem 0 0.75rem; line-height: 1.4; }
137
-
138
- /* Treemap */
294
+ /* ——— Treemap ——— */
139
295
  #treemap {
140
296
  display: flex;
141
297
  flex-wrap: wrap;
@@ -154,10 +310,14 @@ body {
154
310
  overflow: hidden;
155
311
  text-overflow: ellipsis;
156
312
  white-space: nowrap;
157
- border: 1px solid rgba(0,0,0,0.1);
313
+ border: 1px solid rgba(0, 0, 0, 0.15);
314
+ border-radius: 2px;
158
315
  }
159
316
 
160
- .treemap-cell:hover { outline: 2px solid var(--text); outline-offset: 1px; }
317
+ .treemap-cell:hover {
318
+ outline: 2px solid var(--link);
319
+ outline-offset: 1px;
320
+ }
161
321
 
162
322
  .treemap-cell[data-severity="critical"] { background: #c00; color: #fff; border-color: #900; }
163
323
  .treemap-cell[data-severity="high"] { background: #e85d00; color: #fff; border-color: #b84a00; }
@@ -165,37 +325,77 @@ body {
165
325
  .treemap-cell[data-severity="low"] { background: #0a6b0a; color: #fff; border-color: #064906; }
166
326
  .treemap-cell[data-severity="none"] { background: var(--border); color: var(--text-muted); border-color: var(--border); }
167
327
 
168
- /* Legend */
169
- .legend { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; margin-bottom: 0.75rem; font-size: 12px; color: var(--text-muted); }
170
- .legend span { display: inline-flex; align-items: center; gap: 0.25rem; }
171
- .legend .swatch { width: 10px; height: 10px; }
328
+ .legend {
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ gap: 1rem;
332
+ align-items: center;
333
+ margin-bottom: 0.75rem;
334
+ font-size: 12px;
335
+ color: var(--text-muted);
336
+ }
337
+
338
+ .legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
339
+ .legend .swatch { width: 10px; height: 10px; border-radius: 2px; }
172
340
  .legend .swatch-crit { background: #c00; }
173
341
  .legend .swatch-high { background: #e85d00; }
174
342
  .legend .swatch-med { background: #b8860b; }
175
343
  .legend .swatch-low { background: #0a6b0a; }
176
344
  .legend .swatch-none { background: var(--border); }
177
345
 
178
- /* Debt list */
179
- .debt-list { list-style: none; padding: 0; margin: 0; }
346
+ /* ——— Priority lists ——— */
347
+ .priority-list {
348
+ list-style: none;
349
+ padding: 0;
350
+ margin: 0;
351
+ font-size: 12px;
352
+ }
353
+
354
+ .priority-list li {
355
+ padding: 0.4rem 0;
356
+ border-bottom: 1px solid var(--border-subtle);
357
+ color: var(--text);
358
+ font-family: ui-monospace, monospace;
359
+ }
360
+
361
+ .priority-list li:last-child { border-bottom: none; }
362
+
363
+ /* ——— Debt list ——— */
364
+ .debt-list {
365
+ list-style: none;
366
+ padding: 0;
367
+ margin: 0;
368
+ }
180
369
 
181
370
  .debt-list li {
182
- border-bottom: 1px solid var(--border);
183
- padding: 0.6rem 0;
371
+ border-bottom: 1px solid var(--border-subtle);
372
+ padding: 0.65rem 0;
184
373
  cursor: pointer;
374
+ transition: background 0.12s ease;
185
375
  }
186
376
 
187
377
  .debt-list li:last-child { border-bottom: none; }
188
- .debt-list li:hover { background: var(--bg); }
189
- .debt-list .title { font-weight: bold; margin-bottom: 0.2rem; }
190
- .debt-list .meta { font-size: 12px; color: var(--text-muted); display: block; margin-top: 0.2rem; }
378
+ .debt-list li:hover { background: var(--surface-hover); }
379
+
380
+ .debt-list .title { font-weight: 600; margin-bottom: 0.2rem; }
381
+ .debt-list .meta { font-size: 12px; color: var(--text-muted); display: block; margin-top: 0.25rem; font-family: ui-monospace, monospace; }
191
382
  .debt-list .insight { font-size: 13px; color: var(--text-muted); margin-top: 0.25rem; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
192
383
  .debt-list-ratings { display: flex; align-items: center; gap: 1rem; margin: 0.25rem 0; flex-wrap: wrap; }
193
384
  .debt-list-rating { display: inline-flex; align-items: center; gap: 0.35rem; }
194
385
  .debt-list-rating-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
195
- .debt-list-llm-none { color: var(--text-muted); font-size: 13px; }
386
+ .debt-list-llm-none { color: var(--text-muted); font-size: 12px; }
196
387
 
197
388
  /* Badges */
198
- .badge { display: inline-block; padding: 0.15em 0.4em; font-size: 11px; font-weight: bold; }
389
+ .badge {
390
+ display: inline-block;
391
+ padding: 0.15em 0.45em;
392
+ font-size: 10px;
393
+ font-weight: 600;
394
+ text-transform: uppercase;
395
+ letter-spacing: 0.03em;
396
+ border-radius: 3px;
397
+ }
398
+
199
399
  .badge-critical { background: #c00; color: #fff; }
200
400
  .badge-high { background: #e85d00; color: #fff; }
201
401
  .badge-medium { background: #b8860b; color: #fff; }
@@ -203,11 +403,11 @@ body {
203
403
  .badge-none { background: var(--border); color: var(--text-muted); }
204
404
  .badge-llm { background: var(--link); color: #fff; }
205
405
 
206
- /* Detail modal */
406
+ /* ——— Detail modal ——— */
207
407
  #detail {
208
408
  position: fixed;
209
409
  inset: 0;
210
- background: rgba(0,0,0,0.5);
410
+ background: rgba(0, 0, 0, 0.55);
211
411
  display: none;
212
412
  align-items: center;
213
413
  justify-content: center;
@@ -220,7 +420,9 @@ body {
220
420
  #detail .panel {
221
421
  background: var(--surface);
222
422
  border: 1px solid var(--border);
223
- max-width: 520px;
423
+ border-radius: var(--radius);
424
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
425
+ max-width: 540px;
224
426
  width: 100%;
225
427
  max-height: 90vh;
226
428
  overflow-y: auto;
@@ -229,7 +431,7 @@ body {
229
431
  -webkit-overflow-scrolling: touch;
230
432
  }
231
433
 
232
- #detail .panel h3 { margin: 0 0 0.35rem; font-size: 1rem; }
434
+ #detail .panel h3 { margin: 0 0 0.35rem; font-size: 14px; font-weight: 600; }
233
435
  #detail .panel .file { font-family: ui-monospace, monospace; font-size: 13px; color: var(--link); }
234
436
  #detail .panel .close-hint { margin-top: 0.75rem; font-size: 12px; color: var(--text-muted); }
235
437
 
@@ -250,6 +452,10 @@ body {
250
452
  #detail .panel .detail-explanation .detail-llm-label { margin-top: 0.5rem; margin-bottom: 0.25rem; }
251
453
  #detail .panel .detail-explanation .detail-no-llm { font-style: italic; color: var(--text-muted); }
252
454
  #detail .panel .detail-explanation .detail-static-desc { margin-top: 0.5rem; font-size: 12px; color: var(--text-muted); }
455
+ #detail .panel .detail-explanation .detail-issues-list { margin: 0.25rem 0 0 0; padding-left: 1.25rem; list-style: none; }
456
+ #detail .panel .detail-explanation .detail-issues-list > li { margin-bottom: 0.35rem; }
457
+ #detail .panel .detail-explanation .detail-issues-list .detail-issue-desc { margin: 0.15rem 0 0 1rem; font-size: 11px; opacity: 0.9; }
458
+ #detail .panel .detail-explanation .detail-issues-list .detail-line { color: var(--text-muted); font-size: 11px; }
253
459
 
254
460
  #detail .panel .file-assessment {
255
461
  margin-top: 0.75rem;
@@ -265,14 +471,14 @@ body {
265
471
  #detail .panel .file-assessment strong { color: var(--text); font-size: 12px; }
266
472
  #detail .panel .suggested-code { margin-top: 0.75rem; }
267
473
 
268
- /* Code blocks - shared */
474
+ /* ——— Code blocks ——— */
269
475
  .code-block {
270
476
  margin: 0.5rem 0;
271
477
  padding: 0.75rem 1rem;
272
478
  background: var(--bg);
273
479
  border: 1px solid var(--border);
274
- border-radius: 6px;
275
- font-size: 13px;
480
+ border-radius: var(--radius);
481
+ font-size: 12px;
276
482
  overflow-x: auto;
277
483
  overflow-y: auto;
278
484
  max-height: 20em;
@@ -280,43 +486,36 @@ body {
280
486
 
281
487
  .code-block .lang-label {
282
488
  display: block;
283
- font-size: 11px;
489
+ font-size: 10px;
284
490
  font-family: system-ui, sans-serif;
285
491
  color: var(--text-muted);
286
492
  margin-bottom: 0.5rem;
287
493
  text-transform: uppercase;
288
- letter-spacing: 0.03em;
494
+ letter-spacing: 0.04em;
289
495
  }
290
496
 
291
497
  .code-block pre { margin: 0; white-space: pre; overflow-x: auto; min-width: min-content; }
292
498
  .code-block code {
293
- font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
499
+ font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
294
500
  font-size: 12px;
295
501
  line-height: 1.5;
296
502
  color: var(--text);
297
503
  display: block;
298
504
  }
299
505
 
300
- #detail .panel .code-block {
301
- max-height: 16em;
302
- }
303
-
506
+ #detail .panel .code-block { max-height: 16em; }
304
507
  #detail .panel .suggested-code .code-block { margin: 0.35rem 0 0; background: var(--surface); }
305
508
  #detail .panel .file-assessment .code-block { margin: 0.5rem 0 0; background: var(--surface); }
306
509
 
307
- /* LLM sections */
308
- .llm-overall { border-left: 3px solid var(--link); padding-left: 0.5rem; margin-left: -0.5rem; }
309
- .llm-overall h2 { color: var(--text); font-size: 1rem; }
310
- .llm-overall .llm-overall-text { margin: 0; color: var(--text); line-height: 1.5; font-size: 14px; white-space: pre-wrap; word-break: break-word; }
510
+ /* ——— LLM output ——— */
511
+ .llm-output .llm-prose {
512
+ margin: 0 0 0.75rem;
513
+ line-height: 1.55;
514
+ white-space: pre-wrap;
515
+ word-break: break-word;
516
+ color: var(--text);
517
+ }
311
518
 
312
- .llm-output .llm-prose { margin: 0 0 0.75rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
313
519
  .llm-output .llm-prose:last-child { margin-bottom: 0; }
314
520
 
315
- .llm-next-steps h2 { color: var(--text); font-size: 1rem; }
316
- .llm-next-steps ul { margin: 0; padding-left: 1.25rem; color: var(--text); line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
317
-
318
- /* Priority matrix */
319
- .priority-matrix { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 0.75rem; }
320
- .priority-matrix .quadrant { padding: 0.75rem; border: 1px solid var(--border); }
321
- .priority-matrix .quadrant h4 { margin: 0 0 0.35rem; font-size: 0.9rem; font-weight: bold; }
322
- .priority-matrix .quadrant p { margin: 0; font-size: 13px; color: var(--text-muted); }
521
+ .panel-llm .panel-body .llm-output { margin: 0; }
@@ -166,58 +166,82 @@ document.getElementById("q2").innerHTML = highImpact
166
166
  .map(function (d) { return '<li style="font-size:0.8rem">' + escapeHtml(d.file) + "</li>"; })
167
167
  .join("");
168
168
 
169
- // Debt list: static and LLM ratings side by side (LLM = file's rating, one per file)
169
+ // Debt list: one row per file; static = worst severity across that file's issues, LLM = file's rating
170
170
  var sev = { critical: 4, high: 3, medium: 2, low: 1 };
171
171
  var list = document.getElementById("debtList");
172
- DATA.debtItems.sort(function (a, b) {
173
- return (sev[b.severity] || 0) - (sev[a.severity] || 0);
174
- }).forEach(function (d) {
175
- var li = document.createElement("li");
176
- var fileM = DATA.fileMetrics.find(function (m) { return m.file === d.file; });
172
+ var filesWithDebt = Array.from(debtByFile.keys()).sort(function (fa, fb) {
173
+ var itemsA = debtByFile.get(fa);
174
+ var itemsB = debtByFile.get(fb);
175
+ var worstA = itemsA.length ? Math.max.apply(null, itemsA.map(function (d) { return sev[d.severity] || 0; })) : 0;
176
+ var worstB = itemsB.length ? Math.max.apply(null, itemsB.map(function (d) { return sev[d.severity] || 0; })) : 0;
177
+ if (worstB !== worstA) return worstB - worstA;
178
+ return fa.localeCompare(fb);
179
+ });
180
+ filesWithDebt.forEach(function (file) {
181
+ var items = debtByFile.get(file);
182
+ var worstSeverityVal = items.length ? items.reduce(function (best, d) {
183
+ return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
184
+ }, items[0]) : null;
185
+ var staticSeverity = worstSeverityVal ? worstSeverityVal.severity : "low";
186
+ var fileM = DATA.fileMetrics.find(function (m) { return m.file === file; });
177
187
  var fileLlmSeverity = fileM && fileM.llmSeverity ? fileM.llmSeverity : null;
178
- var staticBadge = '<span class="badge badge-' + d.severity + '" title="Static analysis">' + d.severity + "</span>";
188
+ var staticBadge = '<span class="badge badge-' + staticSeverity + '" title="Static analysis (worst of ' + items.length + ' issue(s))">' + staticSeverity + "</span>";
179
189
  var llmRating = fileLlmSeverity
180
190
  ? '<span class="badge badge-' + fileLlmSeverity + '" title="LLM rating for this file">' + fileLlmSeverity + "</span>"
181
191
  : '<span class="debt-list-llm-none">—</span>';
192
+ var titleText = items.length === 1
193
+ ? items[0].title
194
+ : worstSeverityVal.title + " (+" + (items.length - 1) + " more)";
182
195
  var ratingsRow =
183
196
  '<div class="debt-list-ratings">' +
184
197
  '<span class="debt-list-rating"><span class="debt-list-rating-label">Static</span> ' + staticBadge + "</span>" +
185
198
  '<span class="debt-list-rating"><span class="debt-list-rating-label">LLM</span> ' + llmRating + "</span>" +
186
199
  "</div>";
200
+ var li = document.createElement("li");
187
201
  li.innerHTML =
188
202
  '<span class="title">' +
189
- escapeHtml(d.title) +
203
+ escapeHtml(titleText) +
190
204
  "</span> " +
191
205
  ratingsRow +
192
- '<span class="meta">' +
193
- escapeHtml(d.file) +
194
- (d.line ? ":" + d.line : "") +
195
- "</span>";
196
- li.addEventListener("click", function () { showDetail(d.file, [d]); });
206
+ '<span class="meta">' + escapeHtml(file) + "</span>";
207
+ li.addEventListener("click", function () { showDetail(file, items); });
197
208
  list.appendChild(li);
198
209
  });
199
210
 
200
211
  function showDetail(file, items) {
201
212
  var panel = document.getElementById("detail");
202
- var item = items.length ? items[0] : null;
203
213
  var fileMetric = DATA.fileMetrics.find(function (m) { return m.file === file; });
214
+ var worstItem = items.length ? items.reduce(function (best, d) {
215
+ return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
216
+ }, items[0]) : null;
217
+ var staticSeverity = worstItem ? worstItem.severity : "low";
204
218
 
205
- document.getElementById("detailTitle").textContent = item ? item.title : "No debt items";
219
+ document.getElementById("detailTitle").textContent = items.length === 1
220
+ ? (items[0].title || "Debt item")
221
+ : items.length + " static issues";
206
222
  document.getElementById("detailFile").textContent = file;
207
223
 
208
224
  var explanationEl = document.getElementById("detailExplanation");
209
225
  var parts = [];
210
- var staticSev = item ? item.severity : "—";
211
- var llmSev = fileMetric && fileMetric.llmSeverity ? fileMetric.llmSeverity : "—";
212
226
  parts.push(
213
227
  '<div class="detail-severities">' +
214
- '<span class="detail-sev"><strong>Static</strong> <span class="badge badge-' + (item ? item.severity : "low") + '">' + staticSev + "</span></span> " +
228
+ '<span class="detail-sev"><strong>Static</strong> <span class="badge badge-' + staticSeverity + '">' + staticSeverity + "</span> (worst of " + items.length + ")</span> " +
215
229
  '<span class="detail-sev"><strong>LLM</strong> ' +
216
230
  (fileMetric && fileMetric.llmSeverity ? '<span class="badge badge-' + fileMetric.llmSeverity + '">' + fileMetric.llmSeverity + "</span>" : "<span class=\"debt-list-llm-none\">—</span>") +
217
231
  "</span></div>"
218
232
  );
219
- if (item && item.description)
220
- parts.push('<div class="detail-static-desc"><strong>Static description</strong><br>' + escapeHtml(item.description).replace(/\n/g, "<br>") + "</div>");
233
+ parts.push('<div class="detail-static-desc"><strong>Static issues</strong><ul class="detail-issues-list">');
234
+ items.forEach(function (item) {
235
+ parts.push(
236
+ '<li><span class="badge badge-' + item.severity + '">' + item.severity + "</span> " +
237
+ escapeHtml(item.title || "Issue") +
238
+ (item.line ? " <span class=\"detail-line\">line " + item.line + "</span>" : "")
239
+ );
240
+ if (item.description)
241
+ parts.push('<div class="detail-issue-desc">' + escapeHtml(item.description).replace(/\n/g, "<br>") + "</div>");
242
+ parts.push("</li>");
243
+ });
244
+ parts.push("</ul></div>");
221
245
  parts.push('<div class="detail-llm-label"><strong>LLM assessment</strong></div>');
222
246
  if (fileMetric && (fileMetric.llmRawAssessment || fileMetric.llmAssessment)) {
223
247
  if (fileMetric.llmRawAssessment)
@@ -78,17 +78,20 @@ function buildHtml(run, title, darkMode, css, script) {
78
78
  };
79
79
  const tierColor = tierColors[cleanliness.tier] ?? "#666";
80
80
  const scoreBadgeSvg = buildScoreBadgeSvg(cleanliness.tier, tierColor);
81
- const llmOverallSection = run.llmOverallAssessment || run.llmOverallRaw
81
+ const llmPanelHtml = run.llmOverallAssessment || run.llmOverallRaw
82
82
  ? `
83
- <div class="section llm-overall">
84
- <h2>LLM overall assessment</h2>
85
- <div class="llm-output">${run.llmOverallAssessment
83
+ <div class="panel panel-llm">
84
+ <div class="panel-header">
85
+ <h2 class="panel-title">LLM overall assessment</h2>
86
+ </div>
87
+ <div class="panel-body">
88
+ <div class="llm-output">${run.llmOverallAssessment
86
89
  ? renderLlmOutputToHtml(run.llmOverallAssessment)
87
90
  : '<div class="llm-prose">' +
88
91
  escapeHtml(stripTrailingSeverityAndScore(run.llmOverallRaw ?? "")).replace(/\n/g, "<br>") +
89
92
  "</div>"}</div>
90
- </div>
91
- `
93
+ </div>
94
+ </div>`
92
95
  : "";
93
96
  return `<!DOCTYPE html>
94
97
  <html lang="en" data-theme="${theme}">
@@ -104,54 +107,113 @@ function buildHtml(run, title, darkMode, css, script) {
104
107
  <meta name="twitter:description" content="${escapeHtml(cleanliness.label)}: ${escapeHtml(cleanliness.description)}" />
105
108
  <style>${css}</style>
106
109
  </head>
107
- <body>
110
+ <body class="dashboard-page">
108
111
  ${!hasLlm ? `<div class="no-llm-banner"><p class="no-llm-cta">Analysis run without LLM — for full results, run with LLM</p></div>` : ""}
109
- <div class="container">
110
- <p class="hero-caption">Technical Debt Cleanliness Score</p>
111
- <div class="hero tier-${cleanliness.tier}">
112
- <div class="score-badge" aria-label="Score ${cleanliness.tier} of 5">${scoreBadgeSvg}</div>
113
- <div class="score-label">${escapeHtml(cleanliness.label)}</div>
114
- <p class="score-desc">${escapeHtml(cleanliness.description)}</p>
115
- <p class="report-meta">${escapeHtml(run.repoPath)} · ${run.completedAt ?? run.startedAt}</p>
112
+ <header class="dashboard-header">
113
+ <div class="dashboard-header-left">
114
+ <h1 class="dashboard-title">${escapeHtml(title)}</h1>
115
+ <span class="dashboard-meta">${escapeHtml(run.repoPath)}</span>
116
+ </div>
117
+ <div class="dashboard-header-right">
118
+ <div class="dashboard-score tier-${cleanliness.tier}" aria-label="Score ${cleanliness.tier} of 5">
119
+ <span class="dashboard-score-value">${cleanliness.tier}</span>
120
+ <span class="dashboard-score-of">/ 5</span>
121
+ <span class="dashboard-score-label">${escapeHtml(cleanliness.label)}</span>
122
+ </div>
123
+ <span class="dashboard-date">${run.completedAt ?? run.startedAt}</span>
116
124
  </div>
125
+ </header>
117
126
 
118
- <div class="summary-cards">
119
- <div class="card"><div class="value">${run.fileMetrics.length}</div><div class="label">Files</div></div>
120
- <div class="card"><div class="value">${run.debtItems.length}</div><div class="label">Debt items</div></div>
121
- <div class="card"><div class="value">${highCriticalCount}</div><div class="label">High / Critical</div></div>
122
- <div class="card"><div class="value">${hotspotCount}</div><div class="label">Hotspots</div></div>
127
+ <main class="dashboard-main">
128
+ <div class="dashboard-grid dashboard-grid-stats">
129
+ <div class="panel stat-panel">
130
+ <div class="panel-body">
131
+ <div class="stat-value">${run.fileMetrics.length}</div>
132
+ <div class="stat-label">Files analyzed</div>
133
+ </div>
134
+ </div>
135
+ <div class="panel stat-panel">
136
+ <div class="panel-body">
137
+ <div class="stat-value">${run.debtItems.length}</div>
138
+ <div class="stat-label">Debt items</div>
139
+ </div>
140
+ </div>
141
+ <div class="panel stat-panel stat-panel-warn">
142
+ <div class="panel-body">
143
+ <div class="stat-value">${highCriticalCount}</div>
144
+ <div class="stat-label">High / Critical</div>
145
+ </div>
146
+ </div>
147
+ <div class="panel stat-panel">
148
+ <div class="panel-body">
149
+ <div class="stat-value">${hotspotCount}</div>
150
+ <div class="stat-label">Hotspots</div>
151
+ </div>
152
+ </div>
123
153
  </div>
124
154
 
125
- ${llmOverallSection}
155
+ <div class="dashboard-grid">
156
+ <div class="panel panel-score tier-${cleanliness.tier}">
157
+ <div class="panel-header">
158
+ <h2 class="panel-title">Cleanliness score</h2>
159
+ </div>
160
+ <div class="panel-body panel-body-center">
161
+ <div class="score-badge" aria-hidden="true">${scoreBadgeSvg}</div>
162
+ <p class="score-desc">${escapeHtml(cleanliness.description)}</p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ ${llmPanelHtml}
126
168
 
127
- <div class="section">
128
- <h2>Files by debt</h2>
129
- <p class="section-desc">Size = complexity + churn. Color = LLM severity per file (when LLM used). Click for details.</p>
130
- <div class="legend">
131
- <span><span class="swatch swatch-crit"></span> Critical</span>
132
- <span><span class="swatch swatch-high"></span> High</span>
133
- <span><span class="swatch swatch-med"></span> Medium</span>
134
- <span><span class="swatch swatch-low"></span> Low</span>
135
- <span><span class="swatch swatch-none"></span> No debt</span>
169
+ <div class="panel">
170
+ <div class="panel-header">
171
+ <h2 class="panel-title">Files by debt</h2>
172
+ <p class="panel-desc">Size = complexity + churn. Color = LLM severity. Click for details.</p>
173
+ </div>
174
+ <div class="panel-body">
175
+ <div class="legend">
176
+ <span><span class="swatch swatch-crit"></span> Critical</span>
177
+ <span><span class="swatch swatch-high"></span> High</span>
178
+ <span><span class="swatch swatch-med"></span> Medium</span>
179
+ <span><span class="swatch swatch-low"></span> Low</span>
180
+ <span><span class="swatch swatch-none"></span> No debt</span>
181
+ </div>
182
+ <div id="treemap"></div>
136
183
  </div>
137
- <div id="treemap"></div>
138
184
  </div>
139
185
 
140
- <div class="section">
141
- <h2>Prioritized recommendations</h2>
142
- <p class="section-desc">Focus on high-impact items first.</p>
143
- <div class="priority-matrix">
144
- <div class="quadrant"><h4>High impact, easier</h4><p>High severity in smaller files.</p><ul id="q1"></ul></div>
145
- <div class="quadrant"><h4>High impact, harder</h4><p>Critical or hotspot files.</p><ul id="q2"></ul></div>
186
+ <div class="dashboard-grid dashboard-grid-half">
187
+ <div class="panel">
188
+ <div class="panel-header">
189
+ <h2 class="panel-title">High impact, easier</h2>
190
+ <p class="panel-desc">High severity in smaller files.</p>
191
+ </div>
192
+ <div class="panel-body">
193
+ <ul id="q1" class="priority-list"></ul>
194
+ </div>
195
+ </div>
196
+ <div class="panel">
197
+ <div class="panel-header">
198
+ <h2 class="panel-title">High impact, harder</h2>
199
+ <p class="panel-desc">Critical or hotspot files.</p>
200
+ </div>
201
+ <div class="panel-body">
202
+ <ul id="q2" class="priority-list"></ul>
203
+ </div>
146
204
  </div>
147
205
  </div>
148
206
 
149
- <div class="section">
150
- <h2>All debt items</h2>
151
- <p class="section-desc">Static (analysis) and LLM ratings shown side by side. Click a row for details.</p>
152
- <ul class="debt-list" id="debtList"></ul>
207
+ <div class="panel">
208
+ <div class="panel-header">
209
+ <h2 class="panel-title">All debt items</h2>
210
+ <p class="panel-desc">Static and LLM ratings. Click a row for details.</p>
211
+ </div>
212
+ <div class="panel-body">
213
+ <ul class="debt-list" id="debtList"></ul>
214
+ </div>
153
215
  </div>
154
- </div>
216
+ </main>
155
217
 
156
218
  <div id="detail">
157
219
  <div class="panel">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-debt-visualizer",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Language-agnostic CLI that analyzes repos and generates interactive technical debt visualizations with AI-powered insights",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",