codebase-onboarder 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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. package/shared/search/query.js +518 -0
@@ -0,0 +1,535 @@
1
+ // The inspector: everything we know about the thing you clicked, written up
2
+ // like margin notes. Explanations come from the static graph; the AI button
3
+ // hands off to the LLM when one is configured.
4
+
5
+ import { roleOf, scanIndex } from '/shared/analyzer/graph.js';
6
+ import { explainFile, explainFolder, explainOverview } from '/shared/analyzer/explainLocal.js';
7
+ import { baseName } from '/shared/analyzer/pathUtil.js';
8
+ import { languageLabel } from '/shared/analyzer/languages/index.js';
9
+ import { escapeHtml } from './html.js';
10
+ import { mdLite, mdInline } from './markdown.js';
11
+ import { renderAnalysisPanel } from './analysisPanel.js';
12
+
13
+ const el = {};
14
+ ['inspectorEmpty', 'inspectorBody', 'inspTitle', 'inspRole', 'inspPath', 'inspStats', 'inspExplain', 'inspLists', 'aiExplainBtn', 'askInput', 'inspBack']
15
+ .forEach((id) => (el[id] = document.getElementById(id)));
16
+
17
+ let askAIHandler = null;
18
+ let clearFocusHandler = null;
19
+ let inspHooks = {
20
+ onOpenCode: () => {},
21
+ onDeepDive: () => {},
22
+ onToast: () => {},
23
+ };
24
+
25
+ export function initInspector(options = {}) {
26
+ const { onAskAI, onClearFocus, ...rest } = options;
27
+ askAIHandler = onAskAI;
28
+ clearFocusHandler = onClearFocus;
29
+ inspHooks = { ...inspHooks, ...rest };
30
+ el.aiExplainBtn.addEventListener('click', () => askAIHandler?.());
31
+ el.inspBack.addEventListener('click', () => clearFocusHandler?.());
32
+
33
+ el.inspectorBody.addEventListener('click', (e) => {
34
+ const btn = e.target.closest('button[data-insp-action]');
35
+ if (!btn) return;
36
+ const action = btn.dataset.inspAction;
37
+ const targetPath = btn.dataset.path || el.inspPath.textContent;
38
+ if (action === 'code') {
39
+ inspHooks.onOpenCode?.(targetPath);
40
+ } else if (action === 'dive') {
41
+ inspHooks.onDeepDive?.(targetPath);
42
+ } else if (action === 'copy') {
43
+ if (navigator.clipboard) {
44
+ navigator.clipboard.writeText(targetPath)
45
+ .then(() => inspHooks.onToast?.('Path copied to clipboard'))
46
+ .catch(() => inspHooks.onToast?.('Failed to copy path'));
47
+ }
48
+ }
49
+ });
50
+ }
51
+
52
+ // Show/hide the small "‹ back" control that returns to the view's own panel.
53
+ function setBack(label) {
54
+ if (label) {
55
+ el.inspBack.hidden = false;
56
+ el.inspBack.textContent = '‹ ' + label;
57
+ } else {
58
+ el.inspBack.hidden = true;
59
+ }
60
+ }
61
+
62
+ export function clearInspector() {
63
+ el.inspectorEmpty.hidden = false;
64
+ el.inspectorBody.hidden = true;
65
+ }
66
+
67
+ export function showFile(path, state) {
68
+ const { scan, facts, history, backLabel } = state;
69
+ const file = scanIndex(scan).fileAt(path);
70
+ const role = roleOf(path, facts);
71
+ setBack(backLabel);
72
+
73
+ el.inspTitle.textContent = baseName(path);
74
+ el.inspRole.textContent = role;
75
+ el.inspRole.className = 'role-chip is-' + role;
76
+ el.inspPath.textContent = path;
77
+ el.askInput.placeholder = `Ask about ${baseName(path)}…`;
78
+
79
+ const fin = facts.fanIn[path] || 0;
80
+ const fout = facts.fanOut[path] || 0;
81
+ const hist = history?.byPath?.[path];
82
+
83
+ const actionsHtml = `
84
+ <div class="insp-actions" style="display:flex; gap:6px; margin: 6px 0 10px;">
85
+ <button class="btn btn-ghost btn-sm" data-insp-action="code" data-path="${escapeHtml(path)}" title="Open file in Code view">Code</button>
86
+ <button class="btn btn-ghost btn-sm" data-insp-action="dive" data-path="${escapeHtml(path)}" title="Open Deep Dive flowchart">Deep Dive →</button>
87
+ <button class="btn btn-ghost btn-sm" data-insp-action="copy" data-path="${escapeHtml(path)}" title="Copy relative path">Copy Path</button>
88
+ </div>
89
+ `;
90
+
91
+ el.inspStats.innerHTML = [
92
+ chip(`<b>${fin}</b> dependents`),
93
+ chip(`<b>${fout}</b> imports`),
94
+ hist ? chip(`<b>${hist.churn}</b> commit${hist.churn === 1 ? '' : 's'}`) : '',
95
+ hist?.hotspot ? chip(`hotspot <b>${hist.hotspot}</b>`) : '',
96
+ hist?.solo && hist.churn >= 2 ? chip(`<b>solo</b> author`) : '',
97
+ file ? chip(`<b>${file.functions.length}</b> functions`) : '',
98
+ file ? chip(`<b>${(file.size / 1024).toFixed(1)}</b> kb`) : '',
99
+ file ? chip(langName(file.lang)) : '',
100
+ file && file.cognitive !== undefined ? chip(`<b>${file.cognitive}</b> cognitive cx`) : '',
101
+ file && file.maintainability !== undefined ? chip(`<b>${file.maintainability}</b> MI`) : '',
102
+ facts.communities && facts.communities[path] ? chip(`community <b>${facts.communities[path]}</b>`) : '',
103
+ ].join('');
104
+
105
+ if (file && state && state.health) {
106
+ const riskData = state.health.perFile?.find(x => x.path === path);
107
+ if (riskData) {
108
+ el.inspStats.innerHTML += ` <risk-badge score="${riskData.risk}" style="vertical-align: middle; margin-left: 8px; --risk-size: 32px;"></risk-badge>`;
109
+ }
110
+ }
111
+
112
+ setExplain(actionsHtml + mdLite(explainFile(path, file, facts)), 'From the static analysis — no AI involved.');
113
+
114
+ el.inspLists.innerHTML = '';
115
+ addPathList('Imports', facts.importsOf[path] || [], scan);
116
+ addPathList('Imported by', facts.importers[path] || [], scan);
117
+ if (file?.exports.length) {
118
+ addList('Exports', file.exports.map((e) => ({ text: e.name, kind: e.kind })));
119
+ }
120
+ if (file?.functions.length) {
121
+ addList('Functions', file.functions.map((f) => ({ text: f.name + '()', kind: f.kind + ' · line ' + f.line })));
122
+ }
123
+
124
+ el.inspectorEmpty.hidden = true;
125
+ el.inspectorBody.hidden = false;
126
+ }
127
+
128
+ export function showFolder(folder, { scan, facts, backLabel }) {
129
+ setBack(backLabel);
130
+ el.inspTitle.textContent = (folder || '(repo root)') + '/';
131
+ el.inspRole.textContent = 'folder';
132
+ el.inspRole.className = 'role-chip is-test';
133
+ el.inspPath.textContent = folder || '(the top of the repository)';
134
+ el.askInput.placeholder = folder ? `Ask about ${folder}/…` : 'Ask anything about this repo…';
135
+ const here = scanIndex(scan).filesIn(folder);
136
+ const count = here.length;
137
+ el.inspStats.innerHTML = chip(`<b>${count}</b> parsed files`);
138
+ setExplain(mdLite(explainFolder(folder, scan, facts)), 'From the static analysis — no AI involved.');
139
+ el.inspLists.innerHTML = '';
140
+ addPathList('Files here', here.map((f) => f.path), scan);
141
+ el.inspectorEmpty.hidden = true;
142
+ el.inspectorBody.hidden = false;
143
+ }
144
+
145
+ export function showOverview({ scan, facts, manifest, history }) {
146
+ setBack();
147
+ el.inspTitle.textContent = scan.name;
148
+ el.inspRole.textContent = 'repo';
149
+ el.inspRole.className = 'role-chip is-entry';
150
+ el.inspPath.textContent = scan.root;
151
+ el.askInput.placeholder = 'Ask anything about this repo…';
152
+ const conf = scan.stats.imports?.confidence;
153
+ el.inspStats.innerHTML = [
154
+ chip(`<b>${scan.stats.filesParsed}</b> files${scan.stats.truncated ? ' (partial)' : ''}`),
155
+ chip(`<b>${scan.stats.edgeCount}</b> connections`),
156
+ chip(`<b>${facts.entries.length}</b> entries`),
157
+ chip(`<b>${facts.hubs.length}</b> hubs`),
158
+ history?.available ? chip(`<b>${history.commitCount}</b> commits`) : '',
159
+ // How much of the import graph we actually placed. Worth a permanent slot:
160
+ // every other number on this page is downstream of it.
161
+ typeof conf === 'number' ? chip(`<b>${conf}%</b> of imports placed`) : '',
162
+ ].join('');
163
+ setExplain(mdLite(explainOverview(scan, facts, manifest)), 'From the static analysis — no AI involved.');
164
+ el.inspLists.innerHTML = '';
165
+ addPathList('Entry points', facts.entries, scan);
166
+ addList('Load-bearing files', facts.hubs.slice(0, 8).map((h) => ({ text: h.path, kind: h.fanIn + ' dependents', path: h.path })), scan);
167
+ if (history?.available) {
168
+ if (history.perFile?.length) {
169
+ const topHotspots = history.perFile.filter((f) => f.hotspot > 10).slice(0, 6);
170
+ if (topHotspots.length) {
171
+ addList('Hotspots (churn × complexity)', topHotspots.map((f) => ({ text: f.path, kind: `hotspot ${f.hotspot} · ${f.churn} commits`, path: f.path })));
172
+ }
173
+ }
174
+ if (history.coChanged?.length) {
175
+ addList('Co-changed pairs', history.coChanged.slice(0, 5).map((p) => ({ text: `${p.a} + ${p.b}`, kind: `${p.count} joint commits` })));
176
+ }
177
+ }
178
+ if (scan.externals.length) {
179
+ addList('Outside packages', scan.externals.slice(0, 10).map((x) => ({ text: x.name, kind: 'used by ' + x.usedBy.length })));
180
+ }
181
+ el.inspectorEmpty.hidden = true;
182
+ el.inspectorBody.hidden = false;
183
+ }
184
+
185
+ // The Patterns view panel: written observations plus the folder-traffic grid.
186
+ export function showPatterns({ scan, patterns }) {
187
+ setBack();
188
+ const { layersInfo, findings, coupling } = patterns;
189
+ el.inspTitle.textContent = 'The shape of it';
190
+ el.inspRole.textContent = 'patterns';
191
+ el.inspRole.className = 'role-chip is-entry';
192
+ el.inspPath.textContent = `${scan.name} — ${layersInfo.layers.length} layers deep from the door`;
193
+ el.askInput.placeholder = 'Ask about the architecture…';
194
+ el.inspStats.innerHTML = [
195
+ chip(`<b>${layersInfo.layers.length}</b> layers`),
196
+ chip(`<b>${layersInfo.unreachable.length}</b> off the path`),
197
+ chip(`<b>${findings.length}</b> observations`),
198
+ ].join('');
199
+
200
+ el.inspExplain.innerHTML =
201
+ findings.map((f) => `
202
+ <div class="finding is-${f.tone}">
203
+ <h4>${escapeHtml(f.title)}</h4>
204
+ <p>${mdInline(f.detail)}</p>
205
+ ${f.paths && f.paths.length ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px;">${f.paths.slice(0, 4).map((p) => `<button class="role-chip" data-goto="${escapeHtml(p)}" style="cursor:pointer;font-size:11px;">${escapeHtml(p)}</button>`).join('')}${f.paths.length > 4 ? `<span style="font-size:11px;color:var(--faint);align-self:center;">+${f.paths.length - 4} more</span>` : ''}</div>` : ''}
206
+ </div>`).join('') +
207
+ '<p class="explain-src">Read straight from the import graph — no AI involved.</p>';
208
+
209
+ el.inspLists.innerHTML = '';
210
+ if (coupling.folders.length) {
211
+ const h = document.createElement('h3');
212
+ h.className = 'inspector-h';
213
+ h.textContent = 'Folder traffic';
214
+ el.inspLists.appendChild(h);
215
+ el.inspLists.appendChild(heatGrid(coupling));
216
+ const note = document.createElement('p');
217
+ note.className = 'explain-src';
218
+ note.textContent = 'Rows pull in columns; darker means more.';
219
+ el.inspLists.appendChild(note);
220
+ }
221
+ el.inspectorEmpty.hidden = true;
222
+ el.inspectorBody.hidden = false;
223
+ }
224
+
225
+ // The Health view panel: the repo's grade and score with an itemized
226
+ // breakdown, a legend for the metrics, and the riskiest files — clickable.
227
+ export function showHealth({ scan, health }) {
228
+ setBack();
229
+ const t = health.totals;
230
+ el.inspTitle.textContent = 'Health';
231
+ el.inspRole.textContent = 'grade ' + health.grade;
232
+ el.inspRole.className = 'role-chip is-' + (health.score >= 70 ? 'entry' : health.score >= 60 ? 'hub' : 'cycle');
233
+ el.inspPath.textContent = `${scan.name} — ${health.score}/100`;
234
+ el.askInput.placeholder = 'Ask about the riskiest files…';
235
+ el.inspStats.innerHTML = [
236
+ chip(`<b>${health.score}</b> / 100`),
237
+ chip(`<b>${t.crit}</b> critical`),
238
+ chip(`<b>${t.high}</b> high risk`),
239
+ chip(`avg cx <b>${t.avgCx}</b>`),
240
+ ].join('');
241
+
242
+ const top = health.perFile[0];
243
+ let note = `<b>${escapeHtml(scan.name)}</b> grades <b>${health.grade}</b> (${health.score}/100).`;
244
+ if (health.score >= 90) note += ' Clean dependency shape — keep it that way.';
245
+ if (t.crit) note += ` ${t.crit} file${t.crit === 1 ? ' sits' : 's sit'} in the critical band — change those with care.`;
246
+ else if (t.high) note += ` ${t.high} file${t.high === 1 ? '' : 's'} in the high band.`;
247
+ if (top) note += ` The single riskiest file is <b>${escapeHtml(top.name)}</b> (risk ${top.risk}${top.inCycle ? ', in a cycle' : ''}, ${top.fanIn} dependents, complexity ${top.complexity}).`;
248
+
249
+ const breakdownHtml = health.breakdown.length
250
+ ? `<div class="score-bd"><h4>Why ${health.score}/100</h4><ul>`
251
+ + health.breakdown.map((b) => `<li><span>${escapeHtml(b.label)}</span><b>${b.points}</b></li>`).join('')
252
+ + '</ul></div>'
253
+ : '<p class="explain-src">A perfect run — nothing was deducted.</p>';
254
+
255
+ const legendHtml = `<div class="score-bd"><h4>How the risk score works</h4><ul class="legend">`
256
+ + `<li><b>centrality</b> — how much of the repo flows through a file (PageRank)</li>`
257
+ + (health.blastExact === false
258
+ ? `<li><b>blast radius</b> — direct dependents only: this repo is over 2,500 files, where the transitive walk is too slow to run</li>`
259
+ : `<li><b>blast radius</b> — how many files break if it does</li>`)
260
+ + `<li><b>complexity</b> — decision points; how hard it is to change safely</li>`
261
+ + `<li><b>cycles</b> — circular dependencies, the sharpest edge</li>`
262
+ + '</ul></div>';
263
+
264
+ el.inspExplain.innerHTML = `<p>${note}</p>` + breakdownHtml + legendHtml
265
+ + '<p class="explain-src">Scored from centrality, blast radius, complexity and cycles — no AI involved.</p>';
266
+ el.inspLists.innerHTML = '';
267
+ addList('Riskiest files', health.perFile.map((f) => ({ text: f.name, kind: `risk ${f.risk} · blast ${f.blast} · cx ${f.complexity}`, path: f.path })), scan);
268
+ el.inspectorEmpty.hidden = true;
269
+ el.inspectorBody.hidden = false;
270
+ }
271
+
272
+ // The Security view panel: severity counts, a written summary, and the
273
+ // findings list — each one clickable into its file's deep-dive.
274
+ export function showSecurity({ scan, security }) {
275
+ setBack();
276
+ const c = security.counts;
277
+ el.inspTitle.textContent = 'Security';
278
+ el.inspRole.textContent = 'grade ' + security.grade;
279
+ el.inspRole.className = 'role-chip is-' + (security.score >= 75 ? 'entry' : security.score >= 60 ? 'hub' : 'cycle');
280
+ el.inspPath.textContent = `${scan.name} — ${security.score}/100 · ${security.total} findings`;
281
+ el.askInput.placeholder = 'Ask about the security findings…';
282
+ el.inspStats.innerHTML = [
283
+ chip(`<b>${c.critical}</b> critical`),
284
+ chip(`<b>${c.high}</b> high`),
285
+ chip(`<b>${c.medium}</b> medium`),
286
+ chip(`<b>${c.low + c.info}</b> low/info`),
287
+ ].join('');
288
+
289
+ let note = `<b>${escapeHtml(scan.name)}</b> grades <b>${security.grade}</b> for security (${security.score}/100) — ${security.total} finding${security.total === 1 ? '' : 's'} across ${security.files.length} file${security.files.length === 1 ? '' : 's'}.`;
290
+ if (!security.total) note += ' The rule engine found nothing to flag — a clean bill.';
291
+ else {
292
+ const worst = security.files[0];
293
+ note += ` The loudest file is <b>${escapeHtml(worst.name)}</b> (${worst.count} finding${worst.count === 1 ? '' : 's'}, worst: ${worst.worst}).`;
294
+ if (c.critical) note += ` ${c.critical} critical — fix those first.`;
295
+ else if (c.high) note += ` ${c.high} high — worth a pass soon.`;
296
+ }
297
+
298
+ const sevLegend = `<div class="score-bd"><h4>What the severities mean</h4><ul class="legend">`
299
+ + `<li><b class="sev-critical">critical</b> — secrets &amp; keys committed; fix now</li>`
300
+ + `<li><b class="sev-high">high</b> — injection &amp; unsafe deserialization; sanitize, parameterize</li>`
301
+ + `<li><b class="sev-medium">medium</b> — XSS sinks, weak crypto, open redirects, swallowed errors</li>`
302
+ + `<li><b class="sev-low">low / info</b> — config, debug leftovers, style — tidy-up</li>`
303
+ + '</ul></div>';
304
+
305
+ const catEntries = Object.entries(security.byCat).sort((a, b) => b[1] - a[1]);
306
+ const catHtml = catEntries.length
307
+ ? `<div class="score-bd"><h4>By category</h4><ul>`
308
+ + catEntries.map(([cat, n]) => `<li><span>${escapeHtml(cat)}</span><b>${n}</b></li>`).join('')
309
+ + '</ul></div>'
310
+ : '';
311
+
312
+ // Attribution: the security story should say which engine told it. Built-in
313
+ // findings carry no `source`; external ones carry the tool's name.
314
+ const externalCount = security.files.reduce((n, f) => n + f.findings.filter((x) => x.source === 'external').length, 0);
315
+ const sourceLine = externalCount
316
+ ? `From the built-in rule engine plus ${externalCount} finding${externalCount === 1 ? '' : 's'} from external engines (see the list below).`
317
+ : 'From the built-in rule engine — no external scanner, no AI.';
318
+
319
+ el.inspExplain.innerHTML = `<p>${note}</p>` + catHtml + sevLegend
320
+ + `<p class="explain-src">${sourceLine}</p>`;
321
+
322
+ // The deep-analysis panel: which engines are installed, and the button that
323
+ // runs them. Rendered on every pass so a finished run re-paints in place.
324
+ renderAnalysisPanel();
325
+
326
+ el.inspLists.innerHTML = '';
327
+ const rows = security.files.flatMap((f) => f.findings.slice(0, 6).map((x) => ({ text: `${f.name}:${x.line} — ${x.message}`, kind: x.severity, path: f.path })));
328
+ addList('Findings', rows, scan);
329
+ el.inspectorEmpty.hidden = true;
330
+ el.inspectorBody.hidden = false;
331
+ }
332
+
333
+ // The History view panel: what the git log says — churn, authorship, the
334
+ // hotspot cross-tab — with the top of each list clickable into the file's
335
+ // deep-dive. When there is no history it says why rather than showing zeros.
336
+ export function showHistory({ scan, history }) {
337
+ setBack();
338
+ el.inspTitle.textContent = 'History';
339
+ if (!history?.available) {
340
+ el.inspRole.textContent = 'unavailable';
341
+ el.inspRole.className = 'role-chip is-test';
342
+ el.inspPath.textContent = scan.name;
343
+ el.askInput.placeholder = 'Ask anything about this repo…';
344
+ el.inspStats.innerHTML = '';
345
+ el.inspExplain.innerHTML =
346
+ `<p>${escapeHtml(history?.reason || 'No history available.')}</p>`
347
+ + '<p class="explain-src">History needs a checkout with commits — a git URL clone has one; a folder picked in the browser or a plain directory may not.</p>';
348
+ el.inspLists.innerHTML = '';
349
+ el.inspectorEmpty.hidden = true;
350
+ el.inspectorBody.hidden = false;
351
+ return;
352
+ }
353
+
354
+ const hotspots = history.perFile.filter((f) => f.hotspot > 0);
355
+ const top = hotspots[0];
356
+ el.inspRole.textContent = history.truncated
357
+ ? `${history.commitCount} of ${history.totalCommits} commits`
358
+ : `${history.commitCount} commits`;
359
+ el.inspRole.className = 'role-chip is-entry';
360
+ el.inspPath.textContent = `${scan.name} — ${span(history.firstCommitAt, history.lastCommitAt)}`;
361
+ el.askInput.placeholder = 'Ask about the hotspots…';
362
+ el.inspStats.innerHTML = [
363
+ chip(`<b>${history.authors.length}</b> ${history.authors.length === 1 ? 'author' : 'authors'}`),
364
+ chip(`<b>${hotspots.length}</b> ${hotspots.length === 1 ? 'hotspot' : 'hotspots'}`),
365
+ history.soloFiles ? chip(`<b>${history.soloFiles}</b> solo-owned`) : '',
366
+ history.pathsGone ? chip(`<b>${history.pathsGone}</b> files gone`) : '',
367
+ ].join('');
368
+
369
+ let note = `<b>${escapeHtml(scan.name)}</b> shows ${history.commitCount} commits by ${history.authors.length} ${history.authors.length === 1 ? 'person' : 'people'}, ${span(history.firstCommitAt, history.lastCommitAt)}.`;
370
+ if (top) {
371
+ note += ` The sharpest hotspot is <b>${escapeHtml(top.name)}</b> — touched ${top.churn} ${top.churn === 1 ? 'time' : 'times'} at complexity ${top.complexity}`;
372
+ note += top.solo ? ', always by the same person.' : ` by ${top.authors} ${top.authors === 1 ? 'person' : 'people'}.`;
373
+ } else {
374
+ note += ' Nothing stands out — no file is both complex and frequently changed.';
375
+ }
376
+ if (history.truncated) note += ` The window was capped at ${history.commitCount} of ${history.totalCommits} total commits.`;
377
+
378
+ const legendHtml = `<div class="score-bd"><h4>How the hotspot score works</h4><ul class="legend">`
379
+ + `<li><b>churn</b> — how many commits have touched the file</li>`
380
+ + `<li><b>complexity</b> — decision points, the same number Health uses</li>`
381
+ + `<li><b>hotspot</b> — both at once, each against the repo's own worst: where change and difficulty meet</li>`
382
+ + `<li><b>solo</b> — only one person has ever touched it; knowledge leaves with them</li>`
383
+ + '</ul></div>';
384
+
385
+ el.inspExplain.innerHTML = `<p>${note}</p>` + legendHtml
386
+ + '<p class="explain-src">From the git log — authors keyed by email, no AI involved.</p>';
387
+ el.inspLists.innerHTML = '';
388
+ addList('Hotspots', hotspots.map((f) => ({
389
+ text: f.name,
390
+ kind: `hotspot ${f.hotspot} · ${f.churn} commits · cx ${f.complexity}${f.solo ? ' · solo' : ''}`,
391
+ path: f.path,
392
+ })), scan);
393
+ if (history.coChanged?.length) {
394
+ addList('Change together, unconnected', history.coChanged.map((p) => ({
395
+ text: p.a.split('/').pop() + ' + ' + p.b.split('/').pop(),
396
+ kind: p.count + '× together',
397
+ path: p.a,
398
+ title: p.a + ' ↔ ' + p.b,
399
+ })), scan);
400
+ }
401
+ el.inspectorEmpty.hidden = true;
402
+ el.inspectorBody.hidden = false;
403
+ }
404
+
405
+ function span(firstIso, lastIso) {
406
+ const fmt = (iso) => {
407
+ if (!iso) return '?';
408
+ const d = new Date(iso);
409
+ if (Number.isNaN(d.getTime())) return iso.slice(0, 10);
410
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
411
+ };
412
+ return fmt(firstIso) + ' → ' + fmt(lastIso);
413
+ }
414
+
415
+ function heatGrid(coupling) {
416
+ const { folders, counts, max } = coupling;
417
+ const grid = document.createElement('div');
418
+ grid.className = 'heat';
419
+ grid.style.gridTemplateColumns = `64px repeat(${folders.length}, 1fr)`;
420
+
421
+ grid.appendChild(document.createElement('span'));
422
+ for (const c of folders) {
423
+ const label = document.createElement('span');
424
+ label.className = 'heat-col-label';
425
+ label.textContent = shortFolder(c);
426
+ label.title = c;
427
+ grid.appendChild(label);
428
+ }
429
+ for (const r of folders) {
430
+ const label = document.createElement('span');
431
+ label.className = 'heat-label';
432
+ label.textContent = shortFolder(r);
433
+ label.title = r;
434
+ grid.appendChild(label);
435
+ for (const c of folders) {
436
+ const cell = document.createElement('span');
437
+ cell.className = 'heat-cell';
438
+ const n = counts.get(r + '->' + c) || 0;
439
+ if (n > 0) {
440
+ const pct = Math.max(14, Math.round((n / max) * 86));
441
+ cell.style.background = `color-mix(in srgb, var(--accent) ${pct}%, transparent)`;
442
+ cell.textContent = n;
443
+ cell.title = `${r} → ${c}: ${n} ${n === 1 ? 'import' : 'imports'}`;
444
+ }
445
+ grid.appendChild(cell);
446
+ }
447
+ }
448
+ return grid;
449
+ }
450
+
451
+ function shortFolder(name) {
452
+ const clean = name === '(root)' ? 'root' : name;
453
+ return clean.length > 9 ? clean.slice(0, 8) + '…' : clean;
454
+ }
455
+
456
+ // The AI flow reuses the same box: streaming text lands here. `headerHtml`
457
+ // pins a line above the answer — the question being answered, for instance.
458
+ export function setExplain(html, sourceNote) {
459
+ el.inspExplain.innerHTML = html + (sourceNote ? `<p class="explain-src">${escapeHtml(sourceNote)}</p>` : '');
460
+ }
461
+
462
+ export function explainStreamWriter(headerHtml = '') {
463
+ let text = '';
464
+ el.inspExplain.innerHTML = headerHtml + '<p><span class="caret"></span></p>';
465
+ return {
466
+ push(delta) {
467
+ text += delta;
468
+ el.inspExplain.innerHTML = headerHtml + mdLite(text) + '<span class="caret"></span>';
469
+ },
470
+ finish(note) {
471
+ el.inspExplain.innerHTML = headerHtml + mdLite(text) + (note ? `<p class="explain-src">${escapeHtml(note)}</p>` : '');
472
+ },
473
+ fail(message) {
474
+ el.inspExplain.innerHTML = headerHtml + `<p style="color:var(--warn)">${escapeHtml(message)}</p>`;
475
+ },
476
+ };
477
+ }
478
+
479
+ // ---- helpers ---------------------------------------------------------------
480
+
481
+ function addPathList(title, paths, scan) {
482
+ if (!paths.length) return;
483
+ addList(title, paths.map((p) => ({ text: p, path: p })), scan);
484
+ }
485
+
486
+ function addList(title, items, scan) {
487
+ if (!items || !items.length) return;
488
+ const h = document.createElement('h3');
489
+ h.className = 'inspector-h';
490
+ h.textContent = title;
491
+ const ul = document.createElement('ul');
492
+ ul.className = 'link-list';
493
+ for (const item of items.slice(0, 14)) {
494
+ const li = document.createElement('li');
495
+ if (item.path) {
496
+ const b = document.createElement('button');
497
+ b.textContent = item.text;
498
+ b.dataset.goto = item.path;
499
+ if (item.title) b.title = item.title;
500
+ li.appendChild(b);
501
+ } else {
502
+ li.textContent = item.text;
503
+ }
504
+ if (item.kind) {
505
+ const k = document.createElement('span');
506
+ k.className = 'fn-kind';
507
+ k.textContent = item.kind;
508
+ li.appendChild(k);
509
+ }
510
+ ul.appendChild(li);
511
+ }
512
+ if (items.length > 14) {
513
+ const more = document.createElement('li');
514
+ more.textContent = `…and ${items.length - 14} more`;
515
+ more.style.color = 'var(--faint)';
516
+ ul.appendChild(more);
517
+ }
518
+ el.inspLists.append(h, ul);
519
+ }
520
+
521
+ // Clicking a path inside the inspector navigates to it (wired in app.js).
522
+ export function onInspectorNavigate(handler) {
523
+ el.inspLists.addEventListener('click', (event) => {
524
+ const b = event.target.closest('button[data-goto]');
525
+ if (b) handler(b.dataset.goto);
526
+ });
527
+ }
528
+
529
+ function chip(html) {
530
+ return `<span class="stat-chip">${html}</span>`;
531
+ }
532
+
533
+ function langName(id) {
534
+ return languageLabel(id, { short: true });
535
+ }