@ash-technologia/fathom 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.
Files changed (236) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +725 -0
  4. package/dist/analyzers/architecture/graph.d.ts +29 -0
  5. package/dist/analyzers/architecture/graph.d.ts.map +1 -0
  6. package/dist/analyzers/architecture/graph.js +347 -0
  7. package/dist/analyzers/architecture/graph.js.map +1 -0
  8. package/dist/analyzers/architecture/imports.d.ts +24 -0
  9. package/dist/analyzers/architecture/imports.d.ts.map +1 -0
  10. package/dist/analyzers/architecture/imports.js +155 -0
  11. package/dist/analyzers/architecture/imports.js.map +1 -0
  12. package/dist/analyzers/architecture/index.d.ts +13 -0
  13. package/dist/analyzers/architecture/index.d.ts.map +1 -0
  14. package/dist/analyzers/architecture/index.js +217 -0
  15. package/dist/analyzers/architecture/index.js.map +1 -0
  16. package/dist/analyzers/architecture/types.d.ts +65 -0
  17. package/dist/analyzers/architecture/types.d.ts.map +1 -0
  18. package/dist/analyzers/architecture/types.js +2 -0
  19. package/dist/analyzers/architecture/types.js.map +1 -0
  20. package/dist/analyzers/cicd/index.d.ts +11 -0
  21. package/dist/analyzers/cicd/index.d.ts.map +1 -0
  22. package/dist/analyzers/cicd/index.js +157 -0
  23. package/dist/analyzers/cicd/index.js.map +1 -0
  24. package/dist/analyzers/dependencies/index.d.ts +11 -0
  25. package/dist/analyzers/dependencies/index.d.ts.map +1 -0
  26. package/dist/analyzers/dependencies/index.js +403 -0
  27. package/dist/analyzers/dependencies/index.js.map +1 -0
  28. package/dist/analyzers/dependencies/lockfiles.d.ts +19 -0
  29. package/dist/analyzers/dependencies/lockfiles.d.ts.map +1 -0
  30. package/dist/analyzers/dependencies/lockfiles.js +172 -0
  31. package/dist/analyzers/dependencies/lockfiles.js.map +1 -0
  32. package/dist/analyzers/dependencies/online.d.ts +19 -0
  33. package/dist/analyzers/dependencies/online.d.ts.map +1 -0
  34. package/dist/analyzers/dependencies/online.js +116 -0
  35. package/dist/analyzers/dependencies/online.js.map +1 -0
  36. package/dist/analyzers/dependencies/types.d.ts +49 -0
  37. package/dist/analyzers/dependencies/types.d.ts.map +1 -0
  38. package/dist/analyzers/dependencies/types.js +2 -0
  39. package/dist/analyzers/dependencies/types.js.map +1 -0
  40. package/dist/analyzers/documentation/index.d.ts +11 -0
  41. package/dist/analyzers/documentation/index.d.ts.map +1 -0
  42. package/dist/analyzers/documentation/index.js +166 -0
  43. package/dist/analyzers/documentation/index.js.map +1 -0
  44. package/dist/analyzers/git/index.d.ts +11 -0
  45. package/dist/analyzers/git/index.d.ts.map +1 -0
  46. package/dist/analyzers/git/index.js +180 -0
  47. package/dist/analyzers/git/index.js.map +1 -0
  48. package/dist/analyzers/project/index.d.ts +11 -0
  49. package/dist/analyzers/project/index.d.ts.map +1 -0
  50. package/dist/analyzers/project/index.js +95 -0
  51. package/dist/analyzers/project/index.js.map +1 -0
  52. package/dist/analyzers/quality/index.d.ts +11 -0
  53. package/dist/analyzers/quality/index.d.ts.map +1 -0
  54. package/dist/analyzers/quality/index.js +198 -0
  55. package/dist/analyzers/quality/index.js.map +1 -0
  56. package/dist/analyzers/security/index.d.ts +11 -0
  57. package/dist/analyzers/security/index.d.ts.map +1 -0
  58. package/dist/analyzers/security/index.js +236 -0
  59. package/dist/analyzers/security/index.js.map +1 -0
  60. package/dist/analyzers/testing/index.d.ts +11 -0
  61. package/dist/analyzers/testing/index.d.ts.map +1 -0
  62. package/dist/analyzers/testing/index.js +134 -0
  63. package/dist/analyzers/testing/index.js.map +1 -0
  64. package/dist/baseline/baseline.d.ts +20 -0
  65. package/dist/baseline/baseline.d.ts.map +1 -0
  66. package/dist/baseline/baseline.js +134 -0
  67. package/dist/baseline/baseline.js.map +1 -0
  68. package/dist/baseline/compare.d.ts +14 -0
  69. package/dist/baseline/compare.d.ts.map +1 -0
  70. package/dist/baseline/compare.js +113 -0
  71. package/dist/baseline/compare.js.map +1 -0
  72. package/dist/baseline/types.d.ts +25 -0
  73. package/dist/baseline/types.d.ts.map +1 -0
  74. package/dist/baseline/types.js +2 -0
  75. package/dist/baseline/types.js.map +1 -0
  76. package/dist/cli/commands.d.ts +39 -0
  77. package/dist/cli/commands.d.ts.map +1 -0
  78. package/dist/cli/commands.js +277 -0
  79. package/dist/cli/commands.js.map +1 -0
  80. package/dist/cli/index.d.ts +3 -0
  81. package/dist/cli/index.d.ts.map +1 -0
  82. package/dist/cli/index.js +29 -0
  83. package/dist/cli/index.js.map +1 -0
  84. package/dist/cli/options.d.ts +55 -0
  85. package/dist/cli/options.d.ts.map +1 -0
  86. package/dist/cli/options.js +258 -0
  87. package/dist/cli/options.js.map +1 -0
  88. package/dist/core/analyzer.d.ts +33 -0
  89. package/dist/core/analyzer.d.ts.map +1 -0
  90. package/dist/core/analyzer.js +19 -0
  91. package/dist/core/analyzer.js.map +1 -0
  92. package/dist/core/context.d.ts +90 -0
  93. package/dist/core/context.d.ts.map +1 -0
  94. package/dist/core/context.js +159 -0
  95. package/dist/core/context.js.map +1 -0
  96. package/dist/core/errors.d.ts +74 -0
  97. package/dist/core/errors.d.ts.map +1 -0
  98. package/dist/core/errors.js +109 -0
  99. package/dist/core/errors.js.map +1 -0
  100. package/dist/core/findings.d.ts +59 -0
  101. package/dist/core/findings.d.ts.map +1 -0
  102. package/dist/core/findings.js +34 -0
  103. package/dist/core/findings.js.map +1 -0
  104. package/dist/core/orchestrator.d.ts +44 -0
  105. package/dist/core/orchestrator.d.ts.map +1 -0
  106. package/dist/core/orchestrator.js +160 -0
  107. package/dist/core/orchestrator.js.map +1 -0
  108. package/dist/core/result.d.ts +169 -0
  109. package/dist/core/result.d.ts.map +1 -0
  110. package/dist/core/result.js +2 -0
  111. package/dist/core/result.js.map +1 -0
  112. package/dist/detectors/framework.d.ts +22 -0
  113. package/dist/detectors/framework.d.ts.map +1 -0
  114. package/dist/detectors/framework.js +147 -0
  115. package/dist/detectors/framework.js.map +1 -0
  116. package/dist/detectors/language.d.ts +13 -0
  117. package/dist/detectors/language.d.ts.map +1 -0
  118. package/dist/detectors/language.js +100 -0
  119. package/dist/detectors/language.js.map +1 -0
  120. package/dist/detectors/package-manager.d.ts +17 -0
  121. package/dist/detectors/package-manager.d.ts.map +1 -0
  122. package/dist/detectors/package-manager.js +67 -0
  123. package/dist/detectors/package-manager.js.map +1 -0
  124. package/dist/detectors/project-type.d.ts +20 -0
  125. package/dist/detectors/project-type.d.ts.map +1 -0
  126. package/dist/detectors/project-type.js +48 -0
  127. package/dist/detectors/project-type.js.map +1 -0
  128. package/dist/diff/analyzer.d.ts +16 -0
  129. package/dist/diff/analyzer.d.ts.map +1 -0
  130. package/dist/diff/analyzer.js +296 -0
  131. package/dist/diff/analyzer.js.map +1 -0
  132. package/dist/diff/git-diff.d.ts +44 -0
  133. package/dist/diff/git-diff.d.ts.map +1 -0
  134. package/dist/diff/git-diff.js +256 -0
  135. package/dist/diff/git-diff.js.map +1 -0
  136. package/dist/diff/types.d.ts +75 -0
  137. package/dist/diff/types.d.ts.map +1 -0
  138. package/dist/diff/types.js +2 -0
  139. package/dist/diff/types.js.map +1 -0
  140. package/dist/integrations/github/context.d.ts +22 -0
  141. package/dist/integrations/github/context.d.ts.map +1 -0
  142. package/dist/integrations/github/context.js +115 -0
  143. package/dist/integrations/github/context.js.map +1 -0
  144. package/dist/integrations/github/types.d.ts +30 -0
  145. package/dist/integrations/github/types.d.ts.map +1 -0
  146. package/dist/integrations/github/types.js +2 -0
  147. package/dist/integrations/github/types.js.map +1 -0
  148. package/dist/plugins/context.d.ts +13 -0
  149. package/dist/plugins/context.d.ts.map +1 -0
  150. package/dist/plugins/context.js +102 -0
  151. package/dist/plugins/context.js.map +1 -0
  152. package/dist/plugins/examples/react.d.ts +8 -0
  153. package/dist/plugins/examples/react.d.ts.map +1 -0
  154. package/dist/plugins/examples/react.js +129 -0
  155. package/dist/plugins/examples/react.js.map +1 -0
  156. package/dist/plugins/index.d.ts +5 -0
  157. package/dist/plugins/index.d.ts.map +1 -0
  158. package/dist/plugins/index.js +4 -0
  159. package/dist/plugins/index.js.map +1 -0
  160. package/dist/plugins/registry.d.ts +45 -0
  161. package/dist/plugins/registry.d.ts.map +1 -0
  162. package/dist/plugins/registry.js +236 -0
  163. package/dist/plugins/registry.js.map +1 -0
  164. package/dist/plugins/types.d.ts +148 -0
  165. package/dist/plugins/types.d.ts.map +1 -0
  166. package/dist/plugins/types.js +2 -0
  167. package/dist/plugins/types.js.map +1 -0
  168. package/dist/reporters/html.d.ts +11 -0
  169. package/dist/reporters/html.d.ts.map +1 -0
  170. package/dist/reporters/html.js +1352 -0
  171. package/dist/reporters/html.js.map +1 -0
  172. package/dist/reporters/json.d.ts +10 -0
  173. package/dist/reporters/json.d.ts.map +1 -0
  174. package/dist/reporters/json.js +11 -0
  175. package/dist/reporters/json.js.map +1 -0
  176. package/dist/reporters/markdown.d.ts +7 -0
  177. package/dist/reporters/markdown.d.ts.map +1 -0
  178. package/dist/reporters/markdown.js +74 -0
  179. package/dist/reporters/markdown.js.map +1 -0
  180. package/dist/reporters/sarif.d.ts +133 -0
  181. package/dist/reporters/sarif.d.ts.map +1 -0
  182. package/dist/reporters/sarif.js +268 -0
  183. package/dist/reporters/sarif.js.map +1 -0
  184. package/dist/reporters/terminal.d.ts +17 -0
  185. package/dist/reporters/terminal.d.ts.map +1 -0
  186. package/dist/reporters/terminal.js +423 -0
  187. package/dist/reporters/terminal.js.map +1 -0
  188. package/dist/rules/categories.d.ts +8 -0
  189. package/dist/rules/categories.d.ts.map +1 -0
  190. package/dist/rules/categories.js +23 -0
  191. package/dist/rules/categories.js.map +1 -0
  192. package/dist/rules/definitions.d.ts +25 -0
  193. package/dist/rules/definitions.d.ts.map +1 -0
  194. package/dist/rules/definitions.js +429 -0
  195. package/dist/rules/definitions.js.map +1 -0
  196. package/dist/rules/registry.d.ts +36 -0
  197. package/dist/rules/registry.d.ts.map +1 -0
  198. package/dist/rules/registry.js +64 -0
  199. package/dist/rules/registry.js.map +1 -0
  200. package/dist/rules/severity.d.ts +13 -0
  201. package/dist/rules/severity.d.ts.map +1 -0
  202. package/dist/rules/severity.js +15 -0
  203. package/dist/rules/severity.js.map +1 -0
  204. package/dist/scoring/confidence.d.ts +12 -0
  205. package/dist/scoring/confidence.d.ts.map +1 -0
  206. package/dist/scoring/confidence.js +22 -0
  207. package/dist/scoring/confidence.js.map +1 -0
  208. package/dist/scoring/score.d.ts +27 -0
  209. package/dist/scoring/score.d.ts.map +1 -0
  210. package/dist/scoring/score.js +81 -0
  211. package/dist/scoring/score.js.map +1 -0
  212. package/dist/scoring/weights.d.ts +11 -0
  213. package/dist/scoring/weights.d.ts.map +1 -0
  214. package/dist/scoring/weights.js +26 -0
  215. package/dist/scoring/weights.js.map +1 -0
  216. package/dist/utils/filesystem.d.ts +65 -0
  217. package/dist/utils/filesystem.d.ts.map +1 -0
  218. package/dist/utils/filesystem.js +299 -0
  219. package/dist/utils/filesystem.js.map +1 -0
  220. package/dist/utils/git.d.ts +47 -0
  221. package/dist/utils/git.d.ts.map +1 -0
  222. package/dist/utils/git.js +125 -0
  223. package/dist/utils/git.js.map +1 -0
  224. package/dist/utils/logger.d.ts +14 -0
  225. package/dist/utils/logger.d.ts.map +1 -0
  226. package/dist/utils/logger.js +25 -0
  227. package/dist/utils/logger.js.map +1 -0
  228. package/dist/utils/paths.d.ts +30 -0
  229. package/dist/utils/paths.d.ts.map +1 -0
  230. package/dist/utils/paths.js +48 -0
  231. package/dist/utils/paths.js.map +1 -0
  232. package/dist/utils/timing.d.ts +12 -0
  233. package/dist/utils/timing.d.ts.map +1 -0
  234. package/dist/utils/timing.js +19 -0
  235. package/dist/utils/timing.js.map +1 -0
  236. package/package.json +73 -0
@@ -0,0 +1,1352 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { CATEGORY_LABELS } from '../rules/categories.js';
4
+ import { bandLabel } from '../scoring/score.js';
5
+ /**
6
+ * HTML reporter — generates a 100% self-contained developer dashboard.
7
+ * Zero external CDNs, zero fonts, zero tracking, zero network requests.
8
+ */
9
+ export class HtmlReporter {
10
+ async report(result, outputPath) {
11
+ const html = generateDashboardHtml(result);
12
+ await fs.writeFile(outputPath, html, 'utf8');
13
+ }
14
+ }
15
+ export function escapeHtml(str) {
16
+ if (str === null || str === undefined)
17
+ return '';
18
+ return String(str)
19
+ .replace(/&/g, '&')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;')
23
+ .replace(/'/g, '&#39;');
24
+ }
25
+ function scoreColor(score) {
26
+ if (score >= 90)
27
+ return '#10b981'; // emerald-500
28
+ if (score >= 75)
29
+ return '#22c55e'; // green-500
30
+ if (score >= 60)
31
+ return '#f59e0b'; // amber-500
32
+ if (score >= 40)
33
+ return '#f97316'; // orange-500
34
+ return '#ef4444'; // red-500
35
+ }
36
+ function severityColor(severity) {
37
+ switch (severity) {
38
+ case 'critical':
39
+ return '#dc2626'; // red-600
40
+ case 'high':
41
+ return '#ef4444'; // red-500
42
+ case 'medium':
43
+ return '#f59e0b'; // amber-500
44
+ case 'low':
45
+ return '#3b82f6'; // blue-500
46
+ case 'info':
47
+ return '#94a3b8'; // slate-400
48
+ default:
49
+ return '#64748b';
50
+ }
51
+ }
52
+ export function generateDashboardHtml(result) {
53
+ const overallColor = scoreColor(result.score.overall);
54
+ const catScores = result.score.categories.filter((c) => c.weight > 0);
55
+ const findings = result.findings;
56
+ const criticalAndHigh = findings.filter((f) => f.severity === 'critical' || f.severity === 'high');
57
+ // Severity counts
58
+ const severityCounts = {
59
+ critical: findings.filter((f) => f.severity === 'critical').length,
60
+ high: findings.filter((f) => f.severity === 'high').length,
61
+ medium: findings.filter((f) => f.severity === 'medium').length,
62
+ low: findings.filter((f) => f.severity === 'low').length,
63
+ info: findings.filter((f) => f.severity === 'info').length,
64
+ };
65
+ const totalFindings = findings.length;
66
+ // Analyzer metrics maps
67
+ const analyzerMap = new Map(result.analyzers.map((a) => [a.analyzerId, a]));
68
+ const archMetrics = analyzerMap.get('architecture')?.metrics ?? {};
69
+ const depMetrics = analyzerMap.get('dependencies')?.metrics ?? {};
70
+ const testMetrics = analyzerMap.get('testing')?.metrics ?? {};
71
+ const gitMetrics = analyzerMap.get('git')?.metrics ?? {};
72
+ const docMetrics = analyzerMap.get('documentation')?.metrics ?? {};
73
+ const cicdMetrics = analyzerMap.get('cicd')?.metrics ?? {};
74
+ // Detected tech stack tags
75
+ const stack = [];
76
+ const projMetrics = analyzerMap.get('project')?.metrics ?? {};
77
+ if (Array.isArray(projMetrics['languages'])) {
78
+ stack.push(...projMetrics['languages'].map(String));
79
+ }
80
+ if (Array.isArray(projMetrics['frameworks'])) {
81
+ stack.push(...projMetrics['frameworks'].map(String));
82
+ }
83
+ if (Array.isArray(projMetrics['packageManagers'])) {
84
+ stack.push(...projMetrics['packageManagers'].map(String));
85
+ }
86
+ // Parse architecture layers and warnings
87
+ let archLayers = [];
88
+ let archWarnings = [];
89
+ if (typeof archMetrics['layers'] === 'string') {
90
+ try {
91
+ archLayers = JSON.parse(archMetrics['layers']);
92
+ }
93
+ catch {
94
+ archLayers = [];
95
+ }
96
+ }
97
+ if (typeof archMetrics['warnings'] === 'string') {
98
+ try {
99
+ archWarnings = JSON.parse(archMetrics['warnings']);
100
+ }
101
+ catch {
102
+ archWarnings = [];
103
+ }
104
+ }
105
+ // Sanitize findings payload for embedded client-side search & filtering
106
+ const findingsJson = JSON.stringify(findings.map((f) => ({
107
+ id: f.id,
108
+ ruleId: f.ruleId,
109
+ category: f.category,
110
+ categoryLabel: CATEGORY_LABELS[f.category] ?? f.category,
111
+ severity: f.severity,
112
+ title: f.title,
113
+ description: f.description,
114
+ recommendation: f.recommendation,
115
+ confidence: f.confidence,
116
+ file: f.location?.file ?? '',
117
+ line: f.location?.line ?? 0,
118
+ evidence: f.evidence ?? '',
119
+ references: f.references ?? [],
120
+ autoFixable: f.autoFixable,
121
+ }))).replace(/</g, '\\u003c');
122
+ const repoName = path.basename(result.repositoryPath) || 'Repository';
123
+ return `<!DOCTYPE html>
124
+ <html lang="en">
125
+ <head>
126
+ <meta charset="UTF-8">
127
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
128
+ <title>Fathom Dashboard — ${escapeHtml(repoName)}</title>
129
+ <style>
130
+ :root {
131
+ --bg: #090d16;
132
+ --card-bg: #0f172a;
133
+ --card-hover: #162036;
134
+ --surface: #1e293b;
135
+ --border: #334155;
136
+ --border-subtle: #1e293b;
137
+ --text: #f8fafc;
138
+ --text-muted: #94a3b8;
139
+ --text-dim: #64748b;
140
+ --primary: #38bdf8;
141
+ --primary-dim: #0284c7;
142
+ --critical: #dc2626;
143
+ --high: #ef4444;
144
+ --medium: #f59e0b;
145
+ --low: #3b82f6;
146
+ --info: #94a3b8;
147
+ --success: #10b981;
148
+ }
149
+
150
+ *, *::before, *::after {
151
+ box-sizing: border-box;
152
+ margin: 0;
153
+ padding: 0;
154
+ }
155
+
156
+ body {
157
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
158
+ background: var(--bg);
159
+ color: var(--text);
160
+ min-height: 100vh;
161
+ line-height: 1.5;
162
+ -webkit-font-smoothing: antialiased;
163
+ }
164
+
165
+ a {
166
+ color: var(--primary);
167
+ text-decoration: none;
168
+ }
169
+ a:hover {
170
+ text-decoration: underline;
171
+ }
172
+
173
+ .container {
174
+ max-width: 1200px;
175
+ margin: 0 auto;
176
+ padding: 2rem 1.5rem 4rem;
177
+ }
178
+
179
+ /* Header */
180
+ header {
181
+ display: flex;
182
+ justify-content: space-between;
183
+ align-items: center;
184
+ flex-wrap: wrap;
185
+ gap: 1rem;
186
+ padding-bottom: 2rem;
187
+ border-bottom: 1px solid var(--border-subtle);
188
+ margin-bottom: 2rem;
189
+ }
190
+
191
+ .brand {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 0.75rem;
195
+ }
196
+
197
+ .brand-logo {
198
+ font-size: 1.75rem;
199
+ font-weight: 900;
200
+ letter-spacing: 0.15em;
201
+ background: linear-gradient(135deg, #38bdf8 0%, #818cf8 100%);
202
+ -webkit-background-clip: text;
203
+ -webkit-text-fill-color: transparent;
204
+ }
205
+
206
+ .brand-tagline {
207
+ font-size: 0.85rem;
208
+ color: var(--text-dim);
209
+ }
210
+
211
+ .meta-badges {
212
+ display: flex;
213
+ flex-wrap: wrap;
214
+ gap: 0.5rem;
215
+ align-items: center;
216
+ }
217
+
218
+ .meta-pill {
219
+ background: var(--card-bg);
220
+ border: 1px solid var(--border);
221
+ color: var(--text-muted);
222
+ border-radius: 6px;
223
+ padding: 0.35rem 0.75rem;
224
+ font-size: 0.8rem;
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 0.4rem;
228
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
229
+ }
230
+
231
+ /* Sections */
232
+ .section {
233
+ margin-bottom: 2.5rem;
234
+ }
235
+
236
+ .section-header {
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: space-between;
240
+ margin-bottom: 1.25rem;
241
+ padding-bottom: 0.5rem;
242
+ border-bottom: 1px solid var(--border-subtle);
243
+ }
244
+
245
+ .section-title {
246
+ font-size: 1.1rem;
247
+ font-weight: 700;
248
+ text-transform: uppercase;
249
+ letter-spacing: 0.06em;
250
+ color: var(--text-muted);
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 0.5rem;
254
+ }
255
+
256
+ .section-badge {
257
+ font-size: 0.75rem;
258
+ padding: 0.2rem 0.5rem;
259
+ background: var(--surface);
260
+ border-radius: 9999px;
261
+ color: var(--text);
262
+ }
263
+
264
+ /* Top Grid: Health Score + Category Overview */
265
+ .hero-grid {
266
+ display: grid;
267
+ grid-template-columns: 320px 1fr;
268
+ gap: 1.5rem;
269
+ margin-bottom: 2rem;
270
+ }
271
+
272
+ @media (max-width: 860px) {
273
+ .hero-grid {
274
+ grid-template-columns: 1fr;
275
+ }
276
+ }
277
+
278
+ .score-card {
279
+ background: var(--card-bg);
280
+ border: 1px solid var(--border);
281
+ border-radius: 12px;
282
+ padding: 2rem;
283
+ display: flex;
284
+ flex-direction: column;
285
+ align-items: center;
286
+ justify-content: center;
287
+ text-align: center;
288
+ position: relative;
289
+ overflow: hidden;
290
+ }
291
+
292
+ .score-glow {
293
+ position: absolute;
294
+ width: 140px;
295
+ height: 140px;
296
+ border-radius: 50%;
297
+ background: ${overallColor}22;
298
+ filter: blur(40px);
299
+ z-index: 0;
300
+ }
301
+
302
+ .score-number {
303
+ font-size: 5.5rem;
304
+ font-weight: 900;
305
+ line-height: 1;
306
+ color: ${overallColor};
307
+ z-index: 1;
308
+ font-feature-settings: "tnum";
309
+ }
310
+
311
+ .score-denom {
312
+ font-size: 1.1rem;
313
+ color: var(--text-dim);
314
+ margin-top: 0.25rem;
315
+ z-index: 1;
316
+ }
317
+
318
+ .score-band-badge {
319
+ margin-top: 1rem;
320
+ padding: 0.4rem 1.25rem;
321
+ background: ${overallColor}18;
322
+ border: 1px solid ${overallColor}55;
323
+ color: ${overallColor};
324
+ border-radius: 9999px;
325
+ font-size: 0.9rem;
326
+ font-weight: 700;
327
+ text-transform: uppercase;
328
+ letter-spacing: 0.06em;
329
+ z-index: 1;
330
+ }
331
+
332
+ .score-subtext {
333
+ margin-top: 0.85rem;
334
+ font-size: 0.8rem;
335
+ color: var(--text-dim);
336
+ z-index: 1;
337
+ }
338
+
339
+ .categories-card {
340
+ background: var(--card-bg);
341
+ border: 1px solid var(--border);
342
+ border-radius: 12px;
343
+ padding: 1.75rem;
344
+ display: flex;
345
+ flex-direction: column;
346
+ justify-content: space-between;
347
+ }
348
+
349
+ .cat-table {
350
+ width: 100%;
351
+ display: flex;
352
+ flex-direction: column;
353
+ gap: 0.8rem;
354
+ }
355
+
356
+ .cat-row {
357
+ display: grid;
358
+ grid-template-columns: 140px 1fr 45px 60px;
359
+ align-items: center;
360
+ gap: 1rem;
361
+ font-size: 0.9rem;
362
+ }
363
+
364
+ .cat-name {
365
+ color: var(--text);
366
+ font-weight: 500;
367
+ white-space: nowrap;
368
+ }
369
+
370
+ .progress-track {
371
+ background: var(--surface);
372
+ height: 8px;
373
+ border-radius: 4px;
374
+ overflow: hidden;
375
+ }
376
+
377
+ .progress-bar {
378
+ height: 100%;
379
+ border-radius: 4px;
380
+ transition: width 0.3s ease;
381
+ }
382
+
383
+ .cat-val {
384
+ text-align: right;
385
+ font-weight: 700;
386
+ font-family: ui-monospace, SFMono-Regular, monospace;
387
+ }
388
+
389
+ .cat-findings-count {
390
+ text-align: right;
391
+ font-size: 0.75rem;
392
+ color: var(--text-dim);
393
+ }
394
+
395
+ /* Severity Distribution Bar */
396
+ .distribution-card {
397
+ background: var(--card-bg);
398
+ border: 1px solid var(--border);
399
+ border-radius: 12px;
400
+ padding: 1.5rem;
401
+ margin-bottom: 2rem;
402
+ }
403
+
404
+ .dist-bar {
405
+ display: flex;
406
+ height: 14px;
407
+ border-radius: 7px;
408
+ overflow: hidden;
409
+ margin-bottom: 1.25rem;
410
+ background: var(--surface);
411
+ }
412
+
413
+ .dist-segment {
414
+ height: 100%;
415
+ transition: width 0.3s ease;
416
+ }
417
+
418
+ .dist-legend {
419
+ display: flex;
420
+ flex-wrap: wrap;
421
+ gap: 1.5rem;
422
+ }
423
+
424
+ .legend-item {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 0.5rem;
428
+ font-size: 0.85rem;
429
+ color: var(--text-muted);
430
+ }
431
+
432
+ .legend-dot {
433
+ width: 10px;
434
+ height: 10px;
435
+ border-radius: 50%;
436
+ }
437
+
438
+ .legend-count {
439
+ font-weight: 700;
440
+ color: var(--text);
441
+ font-family: ui-monospace, SFMono-Regular, monospace;
442
+ }
443
+
444
+ /* Top Priorities */
445
+ .top-priorities-grid {
446
+ display: grid;
447
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
448
+ gap: 1rem;
449
+ }
450
+
451
+ .priority-card {
452
+ background: var(--card-bg);
453
+ border-left: 4px solid var(--border);
454
+ border-radius: 8px;
455
+ padding: 1.25rem;
456
+ border-top: 1px solid var(--border);
457
+ border-right: 1px solid var(--border);
458
+ border-bottom: 1px solid var(--border);
459
+ }
460
+
461
+ .priority-header {
462
+ display: flex;
463
+ justify-content: space-between;
464
+ align-items: center;
465
+ margin-bottom: 0.5rem;
466
+ }
467
+
468
+ .priority-rule {
469
+ font-size: 0.75rem;
470
+ font-weight: 700;
471
+ padding: 0.2rem 0.5rem;
472
+ border-radius: 4px;
473
+ background: var(--surface);
474
+ font-family: ui-monospace, SFMono-Regular, monospace;
475
+ }
476
+
477
+ .priority-loc {
478
+ font-size: 0.75rem;
479
+ color: var(--text-dim);
480
+ font-family: ui-monospace, SFMono-Regular, monospace;
481
+ }
482
+
483
+ .priority-title {
484
+ font-weight: 600;
485
+ font-size: 0.95rem;
486
+ color: var(--text);
487
+ margin-bottom: 0.5rem;
488
+ }
489
+
490
+ .priority-rec {
491
+ font-size: 0.85rem;
492
+ color: var(--primary);
493
+ line-height: 1.4;
494
+ }
495
+
496
+ /* Comparison / Regressions */
497
+ .comparison-banner {
498
+ border-radius: 12px;
499
+ padding: 1.5rem;
500
+ margin-bottom: 2rem;
501
+ border: 1px solid var(--border);
502
+ background: var(--card-bg);
503
+ }
504
+
505
+ .comp-header {
506
+ display: flex;
507
+ justify-content: space-between;
508
+ align-items: center;
509
+ flex-wrap: wrap;
510
+ gap: 1rem;
511
+ margin-bottom: 1.25rem;
512
+ }
513
+
514
+ .comp-badge {
515
+ padding: 0.4rem 1rem;
516
+ border-radius: 6px;
517
+ font-size: 0.85rem;
518
+ font-weight: 800;
519
+ letter-spacing: 0.05em;
520
+ }
521
+
522
+ .badge-regression {
523
+ background: #7f1d1d;
524
+ color: #fca5a5;
525
+ border: 1px solid #ef4444;
526
+ }
527
+
528
+ .badge-clean {
529
+ background: #064e3b;
530
+ color: #6ee7b7;
531
+ border: 1px solid #10b981;
532
+ }
533
+
534
+ .comp-metrics {
535
+ display: grid;
536
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
537
+ gap: 1rem;
538
+ }
539
+
540
+ .comp-metric-box {
541
+ background: var(--bg);
542
+ border: 1px solid var(--border-subtle);
543
+ border-radius: 8px;
544
+ padding: 1rem;
545
+ text-align: center;
546
+ }
547
+
548
+ .comp-metric-label {
549
+ font-size: 0.75rem;
550
+ color: var(--text-dim);
551
+ margin-bottom: 0.25rem;
552
+ }
553
+
554
+ .comp-metric-val {
555
+ font-size: 1.5rem;
556
+ font-weight: 800;
557
+ color: var(--text);
558
+ font-family: ui-monospace, SFMono-Regular, monospace;
559
+ }
560
+
561
+ /* Subsystem Metric Cards Grid */
562
+ .subsystems-grid {
563
+ display: grid;
564
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
565
+ gap: 1.25rem;
566
+ margin-bottom: 2rem;
567
+ }
568
+
569
+ .subsystem-card {
570
+ background: var(--card-bg);
571
+ border: 1px solid var(--border);
572
+ border-radius: 10px;
573
+ padding: 1.25rem;
574
+ display: flex;
575
+ flex-direction: column;
576
+ justify-content: space-between;
577
+ }
578
+
579
+ .subsystem-title {
580
+ font-size: 0.9rem;
581
+ font-weight: 700;
582
+ color: var(--text-muted);
583
+ margin-bottom: 1rem;
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 0.5rem;
587
+ }
588
+
589
+ .subsystem-list {
590
+ list-style: none;
591
+ font-size: 0.85rem;
592
+ display: flex;
593
+ flex-direction: column;
594
+ gap: 0.5rem;
595
+ }
596
+
597
+ .subsystem-item {
598
+ display: flex;
599
+ justify-content: space-between;
600
+ align-items: center;
601
+ color: var(--text-muted);
602
+ }
603
+
604
+ .subsystem-item-val {
605
+ font-weight: 600;
606
+ color: var(--text);
607
+ font-family: ui-monospace, SFMono-Regular, monospace;
608
+ }
609
+
610
+ /* Architecture Specific */
611
+ .arch-layers-grid {
612
+ display: grid;
613
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
614
+ gap: 1rem;
615
+ margin-top: 1rem;
616
+ }
617
+
618
+ .arch-layer-box {
619
+ background: var(--bg);
620
+ border: 1px solid var(--border);
621
+ border-radius: 8px;
622
+ padding: 1rem;
623
+ }
624
+
625
+ .arch-layer-name {
626
+ font-size: 0.85rem;
627
+ font-weight: 700;
628
+ color: var(--primary);
629
+ margin-bottom: 0.5rem;
630
+ }
631
+
632
+ .arch-components {
633
+ list-style: none;
634
+ font-size: 0.8rem;
635
+ color: var(--text-muted);
636
+ }
637
+
638
+ .arch-components li {
639
+ margin-bottom: 0.25rem;
640
+ }
641
+
642
+ /* Finding Explorer */
643
+ .explorer-card {
644
+ background: var(--card-bg);
645
+ border: 1px solid var(--border);
646
+ border-radius: 12px;
647
+ padding: 1.75rem;
648
+ }
649
+
650
+ .explorer-toolbar {
651
+ display: flex;
652
+ flex-wrap: wrap;
653
+ gap: 1rem;
654
+ align-items: center;
655
+ justify-content: space-between;
656
+ margin-bottom: 1.5rem;
657
+ }
658
+
659
+ .search-box {
660
+ flex: 1;
661
+ min-width: 240px;
662
+ position: relative;
663
+ }
664
+
665
+ .search-input {
666
+ width: 100%;
667
+ background: var(--bg);
668
+ border: 1px solid var(--border);
669
+ border-radius: 8px;
670
+ padding: 0.6rem 1rem 0.6rem 2.25rem;
671
+ font-size: 0.9rem;
672
+ color: var(--text);
673
+ outline: none;
674
+ }
675
+ .search-input:focus {
676
+ border-color: var(--primary);
677
+ }
678
+
679
+ .search-icon {
680
+ position: absolute;
681
+ left: 0.8rem;
682
+ top: 50%;
683
+ transform: translateY(-50%);
684
+ color: var(--text-dim);
685
+ font-size: 0.85rem;
686
+ }
687
+
688
+ .filter-controls {
689
+ display: flex;
690
+ flex-wrap: wrap;
691
+ gap: 0.75rem;
692
+ align-items: center;
693
+ }
694
+
695
+ .select-control {
696
+ background: var(--bg);
697
+ border: 1px solid var(--border);
698
+ color: var(--text);
699
+ border-radius: 8px;
700
+ padding: 0.6rem 0.85rem;
701
+ font-size: 0.85rem;
702
+ outline: none;
703
+ cursor: pointer;
704
+ }
705
+
706
+ .pills-group {
707
+ display: flex;
708
+ flex-wrap: wrap;
709
+ gap: 0.35rem;
710
+ }
711
+
712
+ .pill-btn {
713
+ background: var(--bg);
714
+ border: 1px solid var(--border);
715
+ color: var(--text-muted);
716
+ border-radius: 6px;
717
+ padding: 0.4rem 0.75rem;
718
+ font-size: 0.8rem;
719
+ cursor: pointer;
720
+ font-weight: 500;
721
+ transition: all 0.15s ease;
722
+ }
723
+ .pill-btn:hover {
724
+ background: var(--surface);
725
+ color: var(--text);
726
+ }
727
+ .pill-btn.active {
728
+ background: var(--primary-dim);
729
+ border-color: var(--primary);
730
+ color: #ffffff;
731
+ }
732
+
733
+ .findings-container {
734
+ display: flex;
735
+ flex-direction: column;
736
+ gap: 0.75rem;
737
+ }
738
+
739
+ .finding-item {
740
+ background: var(--bg);
741
+ border: 1px solid var(--border-subtle);
742
+ border-radius: 8px;
743
+ padding: 1.25rem;
744
+ transition: border-color 0.15s ease;
745
+ }
746
+ .finding-item:hover {
747
+ border-color: var(--border);
748
+ }
749
+
750
+ .finding-meta-row {
751
+ display: flex;
752
+ align-items: center;
753
+ justify-content: space-between;
754
+ flex-wrap: wrap;
755
+ gap: 0.5rem;
756
+ margin-bottom: 0.5rem;
757
+ }
758
+
759
+ .badge-severity {
760
+ font-size: 0.7rem;
761
+ font-weight: 800;
762
+ text-transform: uppercase;
763
+ letter-spacing: 0.06em;
764
+ padding: 0.2rem 0.5rem;
765
+ border-radius: 4px;
766
+ }
767
+
768
+ .badge-rule {
769
+ font-size: 0.75rem;
770
+ font-family: ui-monospace, SFMono-Regular, monospace;
771
+ color: var(--text-muted);
772
+ background: var(--surface);
773
+ padding: 0.2rem 0.5rem;
774
+ border-radius: 4px;
775
+ }
776
+
777
+ .badge-category {
778
+ font-size: 0.75rem;
779
+ color: var(--text-dim);
780
+ background: var(--surface);
781
+ padding: 0.2rem 0.5rem;
782
+ border-radius: 4px;
783
+ }
784
+
785
+ .badge-loc {
786
+ font-size: 0.8rem;
787
+ font-family: ui-monospace, SFMono-Regular, monospace;
788
+ color: var(--primary);
789
+ }
790
+
791
+ .finding-main-title {
792
+ font-size: 1rem;
793
+ font-weight: 600;
794
+ color: var(--text);
795
+ margin-bottom: 0.35rem;
796
+ }
797
+
798
+ .finding-main-desc {
799
+ font-size: 0.875rem;
800
+ color: var(--text-muted);
801
+ margin-bottom: 0.75rem;
802
+ line-height: 1.5;
803
+ }
804
+
805
+ .details-box {
806
+ margin-top: 0.75rem;
807
+ padding-top: 0.75rem;
808
+ border-top: 1px dashed var(--border);
809
+ }
810
+
811
+ .rec-box {
812
+ display: flex;
813
+ gap: 0.5rem;
814
+ font-size: 0.85rem;
815
+ color: #67e8f9;
816
+ background: #083344;
817
+ padding: 0.6rem 0.85rem;
818
+ border-radius: 6px;
819
+ margin-bottom: 0.5rem;
820
+ }
821
+
822
+ .evidence-block {
823
+ background: #030712;
824
+ border: 1px solid var(--border);
825
+ border-radius: 6px;
826
+ padding: 0.75rem;
827
+ font-family: ui-monospace, SFMono-Regular, monospace;
828
+ font-size: 0.8rem;
829
+ color: #e2e8f0;
830
+ overflow-x: auto;
831
+ margin-top: 0.5rem;
832
+ }
833
+
834
+ .ref-links {
835
+ margin-top: 0.5rem;
836
+ font-size: 0.8rem;
837
+ color: var(--text-dim);
838
+ }
839
+
840
+ .no-results {
841
+ text-align: center;
842
+ padding: 3rem;
843
+ color: var(--text-dim);
844
+ font-size: 0.95rem;
845
+ }
846
+
847
+ /* Analyzer Status Grid */
848
+ .analyzer-status-grid {
849
+ display: grid;
850
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
851
+ gap: 0.75rem;
852
+ }
853
+
854
+ .analyzer-pill {
855
+ background: var(--card-bg);
856
+ border: 1px solid var(--border);
857
+ border-radius: 8px;
858
+ padding: 0.75rem 1rem;
859
+ display: flex;
860
+ align-items: center;
861
+ gap: 0.75rem;
862
+ }
863
+
864
+ .analyzer-status-icon {
865
+ font-size: 1.1rem;
866
+ }
867
+
868
+ .analyzer-info-name {
869
+ font-size: 0.85rem;
870
+ font-weight: 600;
871
+ color: var(--text);
872
+ }
873
+
874
+ .analyzer-info-dur {
875
+ font-size: 0.75rem;
876
+ color: var(--text-dim);
877
+ }
878
+
879
+ footer {
880
+ text-align: center;
881
+ padding: 3rem 0 1rem;
882
+ color: var(--text-dim);
883
+ font-size: 0.8rem;
884
+ border-top: 1px solid var(--border-subtle);
885
+ }
886
+ </style>
887
+ </head>
888
+ <body>
889
+ <div class="container">
890
+
891
+ <!-- Header -->
892
+ <header>
893
+ <div class="brand">
894
+ <div>
895
+ <div class="brand-logo">FATHOM</div>
896
+ <div class="brand-tagline">Repository Intelligence &amp; Architectural Deep Dive</div>
897
+ </div>
898
+ </div>
899
+ <div class="meta-badges">
900
+ <span class="meta-pill">📁 ${escapeHtml(result.repositoryPath)}</span>
901
+ <span class="meta-pill">🕐 ${escapeHtml(new Date(result.timestamp).toUTCString())}</span>
902
+ <span class="meta-pill">⚡ ${escapeHtml(String(result.durationMs))}ms</span>
903
+ <span class="meta-pill">v${escapeHtml(result.fathomVersion)}</span>
904
+ </div>
905
+ </header>
906
+
907
+ <!-- Section 5: Comparison / Regression Banner (if exists) -->
908
+ ${result.comparison
909
+ ? `
910
+ <section class="section comparison-banner">
911
+ <div class="comp-header">
912
+ <div>
913
+ <span class="comp-badge ${result.comparison.isRegression ? 'badge-regression' : 'badge-clean'}">
914
+ ${result.comparison.isRegression ? '⚠️ REGRESSION DETECTED' : '✓ CLEAN: NO REGRESSIONS'}
915
+ </span>
916
+ </div>
917
+ <div style="font-size: 0.85rem; color: var(--text-muted);">
918
+ Comparing current state against baseline from ${escapeHtml(result.comparison.baselineTimestamp)}
919
+ </div>
920
+ </div>
921
+ <div class="comp-metrics">
922
+ <div class="comp-metric-box">
923
+ <div class="comp-metric-label">Baseline Score</div>
924
+ <div class="comp-metric-val">${result.comparison.baselineScore}</div>
925
+ </div>
926
+ <div class="comp-metric-box">
927
+ <div class="comp-metric-label">Current Score</div>
928
+ <div class="comp-metric-val">${result.comparison.currentScore}</div>
929
+ </div>
930
+ <div class="comp-metric-box">
931
+ <div class="comp-metric-label">Score Shift</div>
932
+ <div class="comp-metric-val" style="color: ${result.comparison.scoreDelta >= 0 ? 'var(--success)' : 'var(--high)'}">
933
+ ${result.comparison.scoreDelta > 0 ? '+' : ''}${result.comparison.scoreDelta}
934
+ </div>
935
+ </div>
936
+ <div class="comp-metric-box">
937
+ <div class="comp-metric-label">New Findings</div>
938
+ <div class="comp-metric-val" style="color: ${result.comparison.newFindings.length > 0 ? 'var(--high)' : 'var(--success)'}">
939
+ ${result.comparison.newFindings.length}
940
+ </div>
941
+ </div>
942
+ <div class="comp-metric-box">
943
+ <div class="comp-metric-label">Resolved Findings</div>
944
+ <div class="comp-metric-val" style="color: var(--success)">
945
+ ${result.comparison.resolvedFindings.length}
946
+ </div>
947
+ </div>
948
+ </div>
949
+ </section>`
950
+ : ''}
951
+
952
+ <!-- Section 1 & 2: Health Hero & Category Scores -->
953
+ <div class="hero-grid">
954
+ <section class="score-card" id="section-overall-score">
955
+ <div class="score-glow"></div>
956
+ <div class="score-number">${result.score.overall}</div>
957
+ <div class="score-denom">/ 100</div>
958
+ <div class="score-band-badge">${escapeHtml(bandLabel(result.score.band))}</div>
959
+ <div class="score-subtext">Overall Repository Health</div>
960
+ </section>
961
+
962
+ <section class="categories-card" id="section-category-scores">
963
+ <div class="section-title">Category Health Breakdown</div>
964
+ <div class="cat-table">
965
+ ${catScores
966
+ .map((cs) => {
967
+ const label = CATEGORY_LABELS[cs.category] ?? cs.category;
968
+ const col = scoreColor(cs.score);
969
+ return `
970
+ <div class="cat-row">
971
+ <span class="cat-name">${escapeHtml(label)}</span>
972
+ <div class="progress-track">
973
+ <div class="progress-bar" style="width:${cs.score}%; background:${col};"></div>
974
+ </div>
975
+ <span class="cat-val" style="color:${col}">${cs.score}</span>
976
+ <span class="cat-findings-count">${cs.findingCount} issue${cs.findingCount === 1 ? '' : 's'}</span>
977
+ </div>`;
978
+ })
979
+ .join('')}
980
+ </div>
981
+ </section>
982
+ </div>
983
+
984
+ <!-- Section 3: Severity Distribution -->
985
+ <section class="distribution-card" id="section-severity-distribution">
986
+ <div class="section-title" style="margin-bottom: 1rem;">Finding Severity Distribution (${totalFindings} total)</div>
987
+ <div class="dist-bar">
988
+ ${totalFindings > 0
989
+ ? `
990
+ <div class="dist-segment" style="width:${(severityCounts.critical / totalFindings) * 100}%; background:var(--critical);" title="Critical: ${severityCounts.critical}"></div>
991
+ <div class="dist-segment" style="width:${(severityCounts.high / totalFindings) * 100}%; background:var(--high);" title="High: ${severityCounts.high}"></div>
992
+ <div class="dist-segment" style="width:${(severityCounts.medium / totalFindings) * 100}%; background:var(--medium);" title="Medium: ${severityCounts.medium}"></div>
993
+ <div class="dist-segment" style="width:${(severityCounts.low / totalFindings) * 100}%; background:var(--low);" title="Low: ${severityCounts.low}"></div>
994
+ <div class="dist-segment" style="width:${(severityCounts.info / totalFindings) * 100}%; background:var(--info);" title="Info: ${severityCounts.info}"></div>
995
+ `
996
+ : '<div class="dist-segment" style="width:100%; background:var(--success);" title="All clear"></div>'}
997
+ </div>
998
+ <div class="dist-legend">
999
+ <div class="legend-item"><span class="legend-dot" style="background:var(--critical);"></span>Critical: <span class="legend-count">${severityCounts.critical}</span></div>
1000
+ <div class="legend-item"><span class="legend-dot" style="background:var(--high);"></span>High: <span class="legend-count">${severityCounts.high}</span></div>
1001
+ <div class="legend-item"><span class="legend-dot" style="background:var(--medium);"></span>Medium: <span class="legend-count">${severityCounts.medium}</span></div>
1002
+ <div class="legend-item"><span class="legend-dot" style="background:var(--low);"></span>Low: <span class="legend-count">${severityCounts.low}</span></div>
1003
+ <div class="legend-item"><span class="legend-dot" style="background:var(--info);"></span>Info: <span class="legend-count">${severityCounts.info}</span></div>
1004
+ </div>
1005
+ </section>
1006
+
1007
+ <!-- Section 4: Top Actionable Priorities -->
1008
+ <section class="section" id="section-top-priorities">
1009
+ <div class="section-header">
1010
+ <h2 class="section-title">🚨 Top Actionable Priorities</h2>
1011
+ <span class="section-badge">${criticalAndHigh.length} Urgent</span>
1012
+ </div>
1013
+ ${criticalAndHigh.length === 0
1014
+ ? `
1015
+ <div style="background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; text-align: center; color: var(--success);">
1016
+ ✓ No critical or high severity issues detected in this repository!
1017
+ </div>`
1018
+ : `
1019
+ <div class="top-priorities-grid">
1020
+ ${criticalAndHigh
1021
+ .slice(0, 4)
1022
+ .map((f) => `
1023
+ <div class="priority-card" style="border-left-color: ${severityColor(f.severity)};">
1024
+ <div class="priority-header">
1025
+ <span class="priority-rule" style="color: ${severityColor(f.severity)};">${escapeHtml(f.ruleId)}</span>
1026
+ ${f.location?.file ? `<span class="priority-loc">${escapeHtml(f.location.file)}${f.location.line ? ':' + String(f.location.line) : ''}</span>` : ''}
1027
+ </div>
1028
+ <div class="priority-title">${escapeHtml(f.title)}</div>
1029
+ <div class="priority-rec">→ ${escapeHtml(f.recommendation)}</div>
1030
+ </div>`)
1031
+ .join('')}
1032
+ </div>`}
1033
+ </section>
1034
+
1035
+ <!-- Subsystems Grid: Sections 6, 7, 8, 9, 10, 11 -->
1036
+ <div class="subsystems-grid">
1037
+
1038
+ <!-- Section 6: Architecture Overview -->
1039
+ <section class="subsystem-card" id="section-architecture">
1040
+ <div>
1041
+ <h3 class="subsystem-title">🏗️ Architecture</h3>
1042
+ <ul class="subsystem-list">
1043
+ <li class="subsystem-item"><span>Graph Nodes</span><span class="subsystem-item-val">${escapeHtml(String(archMetrics['nodeCount'] ?? 0))}</span></li>
1044
+ <li class="subsystem-item"><span>Import Edges</span><span class="subsystem-item-val">${escapeHtml(String(archMetrics['edgeCount'] ?? 0))}</span></li>
1045
+ <li class="subsystem-item"><span>Circular Loops</span><span class="subsystem-item-val" style="color: ${(archMetrics['circularDependencyCount'] ?? 0) ? 'var(--high)' : 'var(--success)'}">${escapeHtml(String(archMetrics['circularDependencyCount'] ?? 0))}</span></li>
1046
+ <li class="subsystem-item"><span>Detected Layers</span><span class="subsystem-item-val">${archLayers.length}</span></li>
1047
+ </ul>
1048
+ </div>
1049
+ ${archWarnings.length > 0
1050
+ ? `
1051
+ <div style="margin-top: 0.75rem; font-size: 0.75rem; color: var(--high);">
1052
+ ⚠️ ${archWarnings.length} architectural warning(s) detected
1053
+ </div>`
1054
+ : ''}
1055
+ </section>
1056
+
1057
+ <!-- Section 7: Dependency Summary -->
1058
+ <section class="subsystem-card" id="section-dependencies">
1059
+ <div>
1060
+ <h3 class="subsystem-title">📦 Dependencies</h3>
1061
+ <ul class="subsystem-list">
1062
+ <li class="subsystem-item"><span>Total Manifest Deps</span><span class="subsystem-item-val">${escapeHtml(String(depMetrics['totalDependencies'] ?? 0))}</span></li>
1063
+ <li class="subsystem-item"><span>Direct Dependencies</span><span class="subsystem-item-val">${escapeHtml(String(depMetrics['directDependencies'] ?? depMetrics['totalDependencies'] ?? 0))}</span></li>
1064
+ <li class="subsystem-item"><span>Transitive Deps</span><span class="subsystem-item-val">${escapeHtml(String(depMetrics['transitiveDependencies'] ?? 0))}</span></li>
1065
+ <li class="subsystem-item"><span>Duplicate Versions</span><span class="subsystem-item-val" style="color: ${(depMetrics['duplicateCount'] ?? 0) ? 'var(--medium)' : 'var(--text)'}">${escapeHtml(String(depMetrics['duplicateCount'] ?? 0))}</span></li>
1066
+ </ul>
1067
+ </div>
1068
+ </section>
1069
+
1070
+ <!-- Section 8: Testing Maturity -->
1071
+ <section class="subsystem-card" id="section-testing">
1072
+ <div>
1073
+ <h3 class="subsystem-title">🧪 Testing Maturity</h3>
1074
+ <ul class="subsystem-list">
1075
+ <li class="subsystem-item"><span>Test Files</span><span class="subsystem-item-val">${escapeHtml(String(testMetrics['testFiles'] ?? 0))}</span></li>
1076
+ <li class="subsystem-item"><span>Source Files</span><span class="subsystem-item-val">${escapeHtml(String(testMetrics['sourceFiles'] ?? 0))}</span></li>
1077
+ <li class="subsystem-item"><span>Test-to-Source Ratio</span><span class="subsystem-item-val">${escapeHtml(String(Math.round(Number(testMetrics['testToSourceRatio'] ?? 0) * 100)))}%</span></li>
1078
+ <li class="subsystem-item"><span>Test Script in Manifest</span><span class="subsystem-item-val">${testMetrics['hasTestScript'] ? '✓ Yes' : '✗ No'}</span></li>
1079
+ </ul>
1080
+ </div>
1081
+ </section>
1082
+
1083
+ <!-- Section 9: Git Hygiene -->
1084
+ <section class="subsystem-card" id="section-git-hygiene">
1085
+ <div>
1086
+ <h3 class="subsystem-title">🌱 Git Hygiene</h3>
1087
+ <ul class="subsystem-list">
1088
+ <li class="subsystem-item"><span>Git Repository</span><span class="subsystem-item-val">${result.repository.git.isRepo ? '✓ Initialized' : '✗ No'}</span></li>
1089
+ <li class="subsystem-item"><span>.gitignore Present</span><span class="subsystem-item-val">${result.repository.git.hasGitignore ? '✓ Yes' : '✗ Missing'}</span></li>
1090
+ <li class="subsystem-item"><span>Uncommitted Changes</span><span class="subsystem-item-val">${gitMetrics['uncommittedChanges'] ? 'Pending' : 'Clean'}</span></li>
1091
+ <li class="subsystem-item"><span>Large Files (>10MB)</span><span class="subsystem-item-val">${escapeHtml(String(gitMetrics['largeFilesCount'] ?? 0))}</span></li>
1092
+ </ul>
1093
+ </div>
1094
+ </section>
1095
+
1096
+ <!-- Section 10: Documentation -->
1097
+ <section class="subsystem-card" id="section-documentation">
1098
+ <div>
1099
+ <h3 class="subsystem-title">📚 Documentation</h3>
1100
+ <ul class="subsystem-list">
1101
+ <li class="subsystem-item"><span>README</span><span class="subsystem-item-val">${docMetrics['hasReadme'] ? '✓ Present' : '✗ Missing'}</span></li>
1102
+ <li class="subsystem-item"><span>LICENSE</span><span class="subsystem-item-val">${docMetrics['hasLicense'] ? '✓ Present' : '✗ Missing'}</span></li>
1103
+ <li class="subsystem-item"><span>CONTRIBUTING</span><span class="subsystem-item-val">${docMetrics['hasContributing'] ? '✓ Present' : '✗ Missing'}</span></li>
1104
+ <li class="subsystem-item"><span>SECURITY Policy</span><span class="subsystem-item-val">${docMetrics['hasSecurityPolicy'] ? '✓ Present' : '✗ Missing'}</span></li>
1105
+ </ul>
1106
+ </div>
1107
+ </section>
1108
+
1109
+ <!-- Section 11: CI/CD -->
1110
+ <section class="subsystem-card" id="section-cicd">
1111
+ <div>
1112
+ <h3 class="subsystem-title">🚀 CI / CD</h3>
1113
+ <ul class="subsystem-list">
1114
+ <li class="subsystem-item"><span>CI Configuration</span><span class="subsystem-item-val">${cicdMetrics['hasCIConfig'] ? '✓ Present' : '✗ None'}</span></li>
1115
+ <li class="subsystem-item"><span>GitHub Actions</span><span class="subsystem-item-val">${cicdMetrics['hasGitHubActions'] ? '✓ Active' : '✗ None'}</span></li>
1116
+ <li class="subsystem-item"><span>Workflows Detected</span><span class="subsystem-item-val">${escapeHtml(String(cicdMetrics['ciWorkflowCount'] ?? 0))}</span></li>
1117
+ <li class="subsystem-item"><span>Automated Test Run</span><span class="subsystem-item-val">${cicdMetrics['hasTestStep'] ? '✓ Yes' : '—'}</span></li>
1118
+ </ul>
1119
+ </div>
1120
+ </section>
1121
+
1122
+ </div>
1123
+
1124
+ <!-- Section 12: Finding Explorer -->
1125
+ <section class="explorer-card" id="section-finding-explorer">
1126
+ <div class="section-header" style="border: none; margin-bottom: 1rem;">
1127
+ <h2 class="section-title">🔍 Finding Explorer</h2>
1128
+ <span class="section-badge" id="results-counter">${findings.length} findings</span>
1129
+ </div>
1130
+
1131
+ <!-- Toolbar: Search, Filters, Sort -->
1132
+ <div class="explorer-toolbar">
1133
+ <div class="search-box">
1134
+ <span class="search-icon">🔎</span>
1135
+ <input type="text" id="filter-search" class="search-input" placeholder="Search by rule, title, description, path..." aria-label="Search findings" />
1136
+ </div>
1137
+
1138
+ <div class="filter-controls">
1139
+ <select id="filter-category" class="select-control" aria-label="Filter by category">
1140
+ <option value="all">All Categories</option>
1141
+ <option value="project">Project</option>
1142
+ <option value="git">Git</option>
1143
+ <option value="security">Security</option>
1144
+ <option value="dependencies">Dependencies</option>
1145
+ <option value="quality">Code Quality</option>
1146
+ <option value="testing">Testing</option>
1147
+ <option value="documentation">Documentation</option>
1148
+ <option value="cicd">CI/CD</option>
1149
+ <option value="architecture">Architecture</option>
1150
+ </select>
1151
+
1152
+ <select id="sort-order" class="select-control" aria-label="Sort findings">
1153
+ <option value="severity-desc">Severity (High to Low)</option>
1154
+ <option value="severity-asc">Severity (Low to High)</option>
1155
+ <option value="ruleId">Rule ID (A to Z)</option>
1156
+ <option value="file">File Path (A to Z)</option>
1157
+ <option value="title">Title (A to Z)</option>
1158
+ </select>
1159
+
1160
+ <div class="pills-group" id="severity-pills">
1161
+ <button type="button" class="pill-btn active" data-severity="all">All</button>
1162
+ <button type="button" class="pill-btn" data-severity="critical">Critical</button>
1163
+ <button type="button" class="pill-btn" data-severity="high">High</button>
1164
+ <button type="button" class="pill-btn" data-severity="medium">Medium</button>
1165
+ <button type="button" class="pill-btn" data-severity="low">Low</button>
1166
+ <button type="button" class="pill-btn" data-severity="info">Info</button>
1167
+ </div>
1168
+ </div>
1169
+ </div>
1170
+
1171
+ <!-- Findings Container -->
1172
+ <div class="findings-container" id="findings-list">
1173
+ <!-- Injected and filtered via inline JavaScript -->
1174
+ </div>
1175
+ </section>
1176
+
1177
+ <!-- Analyzer Execution Breakdown -->
1178
+ <section class="section" style="margin-top: 2.5rem;">
1179
+ <div class="section-title">Analyzers &amp; Execution Timers</div>
1180
+ <div class="analyzer-status-grid">
1181
+ ${result.analyzers
1182
+ .map((a) => `
1183
+ <div class="analyzer-pill">
1184
+ <span class="analyzer-status-icon">${a.status === 'success' ? '🟢' : '🔴'}</span>
1185
+ <div>
1186
+ <div class="analyzer-info-name">${escapeHtml(a.analyzerName)}</div>
1187
+ <div class="analyzer-info-dur">${a.status === 'failed' ? escapeHtml(a.error ?? 'Failed') : `${String(a.durationMs)}ms`}</div>
1188
+ </div>
1189
+ </div>`)
1190
+ .join('')}
1191
+ </div>
1192
+ </section>
1193
+
1194
+ <footer>
1195
+ Fathom v${escapeHtml(result.fathomVersion)} · Completely Offline &amp; Self-Contained · Zero Telemetry
1196
+ </footer>
1197
+
1198
+ </div>
1199
+
1200
+ <!-- Client-side Interactivity Script (No External Dependencies) -->
1201
+ <script>
1202
+ (function() {
1203
+ const allFindings = ${findingsJson};
1204
+
1205
+ const severityWeights = {
1206
+ critical: 0,
1207
+ high: 1,
1208
+ medium: 2,
1209
+ low: 3,
1210
+ info: 4
1211
+ };
1212
+
1213
+ const searchInput = document.getElementById('filter-search');
1214
+ const categorySelect = document.getElementById('filter-category');
1215
+ const sortSelect = document.getElementById('sort-order');
1216
+ const severityPills = document.querySelectorAll('#severity-pills .pill-btn');
1217
+ const findingsList = document.getElementById('findings-list');
1218
+ const resultsCounter = document.getElementById('results-counter');
1219
+
1220
+ let activeSeverity = 'all';
1221
+
1222
+ function escapeText(str) {
1223
+ if (!str) return '';
1224
+ const div = document.createElement('div');
1225
+ div.textContent = String(str);
1226
+ return div.innerHTML;
1227
+ }
1228
+
1229
+ function getSeverityColor(sev) {
1230
+ switch (sev) {
1231
+ case 'critical': return '#dc2626';
1232
+ case 'high': return '#ef4444';
1233
+ case 'medium': return '#f59e0b';
1234
+ case 'low': return '#3b82f6';
1235
+ default: return '#94a3b8';
1236
+ }
1237
+ }
1238
+
1239
+ function render() {
1240
+ const query = (searchInput.value || '').toLowerCase().trim();
1241
+ const cat = categorySelect.value;
1242
+ const sort = sortSelect.value;
1243
+
1244
+ let filtered = allFindings.filter(function(item) {
1245
+ if (activeSeverity !== 'all' && item.severity !== activeSeverity) {
1246
+ return false;
1247
+ }
1248
+ if (cat !== 'all' && item.category !== cat) {
1249
+ return false;
1250
+ }
1251
+ if (query) {
1252
+ const matches =
1253
+ item.ruleId.toLowerCase().includes(query) ||
1254
+ item.title.toLowerCase().includes(query) ||
1255
+ item.description.toLowerCase().includes(query) ||
1256
+ item.file.toLowerCase().includes(query) ||
1257
+ item.recommendation.toLowerCase().includes(query);
1258
+ if (!matches) return false;
1259
+ }
1260
+ return true;
1261
+ });
1262
+
1263
+ // Sort
1264
+ filtered.sort(function(a, b) {
1265
+ if (sort === 'severity-desc') {
1266
+ return (severityWeights[a.severity] || 9) - (severityWeights[b.severity] || 9);
1267
+ }
1268
+ if (sort === 'severity-asc') {
1269
+ return (severityWeights[b.severity] || 9) - (severityWeights[a.severity] || 9);
1270
+ }
1271
+ if (sort === 'ruleId') {
1272
+ return a.ruleId.localeCompare(b.ruleId);
1273
+ }
1274
+ if (sort === 'file') {
1275
+ return a.file.localeCompare(b.file);
1276
+ }
1277
+ if (sort === 'title') {
1278
+ return a.title.localeCompare(b.title);
1279
+ }
1280
+ return 0;
1281
+ });
1282
+
1283
+ resultsCounter.textContent = filtered.length + ' finding' + (filtered.length === 1 ? '' : 's');
1284
+
1285
+ if (filtered.length === 0) {
1286
+ findingsList.innerHTML = '<div class="no-results">No findings match the active search or filters.</div>';
1287
+ return;
1288
+ }
1289
+
1290
+ let html = '';
1291
+ for (let i = 0; i < filtered.length; i++) {
1292
+ const f = filtered[i];
1293
+ const color = getSeverityColor(f.severity);
1294
+ const loc = f.file ? escapeText(f.file) + (f.line ? ':' + f.line : '') : '';
1295
+
1296
+ html += '<div class="finding-item" style="border-left: 4px solid ' + color + ';">';
1297
+ html += ' <div class="finding-meta-row">';
1298
+ html += ' <div style="display:flex;gap:0.5rem;align-items:center;">';
1299
+ html += ' <span class="badge-severity" style="background:' + color + '22;color:' + color + ';">' + escapeText(f.severity) + '</span>';
1300
+ html += ' <span class="badge-rule">' + escapeText(f.ruleId) + '</span>';
1301
+ html += ' <span class="badge-category">' + escapeText(f.categoryLabel) + '</span>';
1302
+ html += ' </div>';
1303
+ if (loc) {
1304
+ html += ' <span class="badge-loc">' + loc + '</span>';
1305
+ }
1306
+ html += ' </div>';
1307
+ html += ' <div class="finding-main-title">' + escapeText(f.title) + '</div>';
1308
+ html += ' <div class="finding-main-desc">' + escapeText(f.description) + '</div>';
1309
+ html += ' <details class="details-box">';
1310
+ html += ' <summary style="cursor:pointer;font-size:0.8rem;color:var(--text-dim);margin-bottom:0.5rem;">View Recommendation &amp; Evidence</summary>';
1311
+ if (f.recommendation) {
1312
+ html += ' <div class="rec-box">💡 <span>' + escapeText(f.recommendation) + '</span></div>';
1313
+ }
1314
+ if (f.evidence) {
1315
+ html += ' <pre class="evidence-block"><code>' + escapeText(f.evidence) + '</code></pre>';
1316
+ }
1317
+ if (f.references && f.references.length > 0) {
1318
+ html += ' <div class="ref-links">Documentation: ';
1319
+ for (let r = 0; r < f.references.length; r++) {
1320
+ const url = f.references[r];
1321
+ html += '<a href="' + escapeText(url) + '" target="_blank" rel="noopener noreferrer">' + escapeText(url) + '</a> ';
1322
+ }
1323
+ html += ' </div>';
1324
+ }
1325
+ html += ' </details>';
1326
+ html += '</div>';
1327
+ }
1328
+
1329
+ findingsList.innerHTML = html;
1330
+ }
1331
+
1332
+ searchInput.addEventListener('input', render);
1333
+ categorySelect.addEventListener('change', render);
1334
+ sortSelect.addEventListener('change', render);
1335
+
1336
+ severityPills.forEach(function(btn) {
1337
+ btn.addEventListener('click', function() {
1338
+ severityPills.forEach(function(b) { b.classList.remove('active'); });
1339
+ btn.classList.add('active');
1340
+ activeSeverity = btn.getAttribute('data-severity');
1341
+ render();
1342
+ });
1343
+ });
1344
+
1345
+ // Initial render
1346
+ render();
1347
+ })();
1348
+ </script>
1349
+ </body>
1350
+ </html>`;
1351
+ }
1352
+ //# sourceMappingURL=html.js.map