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,496 @@
1
+ // The Deep Analysis view: the whole report, in one place, with every finding a
2
+ // click away from its line.
3
+ //
4
+ // The Security panel in the inspector is a summary — engines, a run button, a
5
+ // verdict. This is the other half: a full-page report you can read, filter, and
6
+ // ask about. It shares the run itself with the panel through the store in
7
+ // `analysisPanel.js`, so both surfaces always show the same report and the same
8
+ // grade; only the presentation differs.
9
+ //
10
+ // Three blocks, top to bottom:
11
+ //
12
+ // 1. **Engines** — what is installed, what ran, what it cost, and what it
13
+ // found. An engine that could not run says so here, with the reason, which
14
+ // is the difference between "clean" and "unchecked".
15
+ // 2. **What this means** — the AI. One button explains the report; the box
16
+ // answers a specific question about it. Works offline: with no endpoint
17
+ // configured it offers the drawer instead of failing silently.
18
+ // 3. **Findings** — every finding from every engine, merged, worst first,
19
+ // each row linking to `file:line` in the Code tab.
20
+ //
21
+ // The filters (severity, engine, text) live on `state.analysis` so they survive
22
+ // switching tabs within a repo, and reset with the repo.
23
+
24
+ import { escapeHtml } from './html.js';
25
+ import { mdInline } from './markdown.js';
26
+ import { streamExplain } from './api.js';
27
+ import * as llm from './llm.js';
28
+ import { state } from './state.js';
29
+ import {
30
+ filterFindings, findingsFromSecurity, tabulate, engineRows, engineSummary,
31
+ SEV_ORDER, BUILTIN_TOOL,
32
+ } from './analysisReport.js';
33
+ import {
34
+ ensureToolsStatus, installEngine, runDeepAnalysis, runOutcomeMessage, subscribeAnalysis,
35
+ } from './analysisPanel.js';
36
+
37
+ let host = null;
38
+ let hooks = {
39
+ onToast: () => {},
40
+ onOpenFile: () => {},
41
+ onOpenSettings: () => {},
42
+ repoOverview: () => '',
43
+ };
44
+
45
+ export function initDeepAnalysis(options) {
46
+ host = options.host;
47
+ hooks = { ...hooks, ...options };
48
+
49
+ host.addEventListener('click', (event) => {
50
+ const kind = event.target.closest('[data-run-kind]');
51
+ if (kind) return void doRun({ kinds: [kind.dataset.runKind] });
52
+
53
+ if (event.target.closest('[data-run-all]')) return void doRun({});
54
+
55
+ const engineRun = event.target.closest('[data-engine-run]');
56
+ if (engineRun) return void doRun({ tools: [engineRun.dataset.engineRun] });
57
+
58
+ const engineInstall = event.target.closest('[data-engine-install]');
59
+ if (engineInstall) {
60
+ const id = engineInstall.dataset.engineInstall;
61
+ return void installEngine(id).then((done) => {
62
+ if (done && done.ok) hooks.onToast(`${id} installed — press Run to use it.`);
63
+ else if (done && done.error) hooks.onToast(done.error);
64
+ });
65
+ }
66
+
67
+ if (event.target.closest('[data-sev]')) {
68
+ state.analysis.severity = event.target.closest('[data-sev]').dataset.sev;
69
+ return renderDeepAnalysis(host, state);
70
+ }
71
+ if (event.target.closest('[data-tool-filter]')) {
72
+ state.analysis.tool = event.target.closest('[data-tool-filter]').dataset.toolFilter;
73
+ return renderDeepAnalysis(host, state);
74
+ }
75
+ if (event.target.closest('[data-open-settings]')) {
76
+ hooks.onOpenSettings();
77
+ return undefined;
78
+ }
79
+ if (event.target.closest('[data-ai-explain]')) return void explainWithAI('');
80
+
81
+ const fileLink = event.target.closest('[data-goto]');
82
+ if (fileLink) {
83
+ hooks.onOpenFile(fileLink.dataset.goto, Number(fileLink.dataset.gotoLine) || 0);
84
+ }
85
+ return undefined;
86
+ });
87
+
88
+ host.addEventListener('input', (event) => {
89
+ if (event.target.matches('[data-finding-search]')) {
90
+ const caret = event.target.selectionStart;
91
+ state.analysis.q = event.target.value;
92
+ renderDeepAnalysis(host, state);
93
+ // Re-focus and restore the caret: a full repaint replaces the input, and
94
+ // typing into a box that loses focus on every keystroke is unusable.
95
+ const next = host.querySelector('[data-finding-search]');
96
+ if (next) {
97
+ next.focus();
98
+ next.setSelectionRange(caret, caret);
99
+ }
100
+ return;
101
+ }
102
+ if (event.target.matches('[data-ai-question]')) {
103
+ state.analysis.ai.question = event.target.value;
104
+ }
105
+ });
106
+
107
+ host.addEventListener('keydown', (event) => {
108
+ if (event.key === 'Enter' && event.target.matches('[data-ai-question]')) {
109
+ event.preventDefault();
110
+ explainWithAI(event.target.value);
111
+ }
112
+ });
113
+
114
+ // The Configure forms. `change` — not `input` — so a repaint never fights a
115
+ // half-typed number; the store keeps the values and the next run picks them
116
+ // up. No repaint on change either: the control itself already shows the new
117
+ // value, and redrawing would close the <details> the person just opened.
118
+ host.addEventListener('change', (event) => {
119
+ const control = event.target.closest('[data-opt]');
120
+ if (!control) return;
121
+ const { engine, key } = control.dataset;
122
+ const slot = state.tools.options[engine] || (state.tools.options[engine] = {});
123
+ if (control.type === 'checkbox') {
124
+ if (control.dataset.optValue) {
125
+ // one value of a multi-select
126
+ const list = Array.isArray(slot[key]) ? [...slot[key]] : [];
127
+ const at = list.indexOf(control.dataset.optValue);
128
+ if (control.checked && at === -1) list.push(control.dataset.optValue);
129
+ if (!control.checked && at !== -1) list.splice(at, 1);
130
+ slot[key] = list;
131
+ } else {
132
+ slot[key] = control.checked;
133
+ }
134
+ } else if (control.type === 'number') {
135
+ slot[key] = control.value === '' ? undefined : Number(control.value);
136
+ } else {
137
+ slot[key] = control.value;
138
+ }
139
+ });
140
+
141
+ // A run started from the inspector panel repaints this view too.
142
+ subscribeAnalysis(() => {
143
+ if (state.view === 'analysis') renderDeepAnalysis(host, state);
144
+ });
145
+ }
146
+
147
+ async function doRun(options) {
148
+ const report = await runDeepAnalysis(options);
149
+ hooks.onToast(runOutcomeMessage(report));
150
+ }
151
+
152
+ // ---- the AI pass ------------------------------------------------------------
153
+
154
+ // Explains the report, or answers `question` about it. Streams into
155
+ // `state.analysis.ai.text` and repaints the note in place — a full page repaint
156
+ // mid-stream would throw away every character already received.
157
+ async function explainWithAI(question) {
158
+ const ai = state.analysis.ai;
159
+ if (ai.running) return;
160
+
161
+ if (!llm.isConfigured()) {
162
+ hooks.onOpenSettings();
163
+ hooks.onToast('Add an endpoint and model first — the report is still readable without one.');
164
+ return;
165
+ }
166
+
167
+ const rows = findingsFromSecurity(state.security);
168
+ const counts = tabulate(rows);
169
+ const engines = engineRows(state.tools.status, state.tools.report);
170
+ const messages = llm.analysisMessages({
171
+ repoName: state.scan.name,
172
+ grade: state.security?.grade,
173
+ score: state.security?.score,
174
+ counts,
175
+ engines,
176
+ findings: rows,
177
+ question: (question || '').trim(),
178
+ });
179
+
180
+ const settings = llm.getSettings();
181
+ ai.running = true;
182
+ ai.text = '';
183
+ ai.error = '';
184
+ paintAI();
185
+
186
+ try {
187
+ const providerOptions = llm.isAzureHost(settings.baseUrl) ? {} : { reasoning: { exclude: true } };
188
+ for await (const delta of streamExplain({ ...settings, messages, maxTokens: 900, providerOptions })) {
189
+ ai.text += delta;
190
+ paintAI();
191
+ }
192
+ } catch (err) {
193
+ ai.error = err.message;
194
+ } finally {
195
+ ai.running = false;
196
+ paintAI();
197
+ }
198
+ }
199
+
200
+ // Repaint just the AI note, so streaming never fights the rest of the page.
201
+ function paintAI() {
202
+ if (!host) return;
203
+ const note = host.querySelector('[data-ai-note]');
204
+ if (!note) return;
205
+ const ai = state.analysis.ai;
206
+ note.innerHTML = renderAINote(ai);
207
+ }
208
+
209
+ // ---- painting ---------------------------------------------------------------
210
+
211
+ export function renderDeepAnalysis(container, appState = state) {
212
+ if (!container) return;
213
+ host = container;
214
+ const { analysis } = appState;
215
+
216
+ // A browser-picked folder has no server-side root, so there is nothing for an
217
+ // engine to read. Say that once, plainly, rather than showing an empty table
218
+ // that looks like a clean bill of health.
219
+ if (!appState.scanId) {
220
+ container.innerHTML = `<div class="analysis-layout">
221
+ <header class="analysis-head">
222
+ <h1 class="analysis-title">Deep analysis</h1>
223
+ <p class="analysis-sub">External engines — Semgrep, Gitleaks, Knip, Vulture, Depcheck.</p>
224
+ </header>
225
+ <div class="analysis-empty">This repo was picked in the browser, so its files never reached the server. An external engine needs a directory on the server to read, so deep analysis is available for folders scanned by path and for git clones. The built-in scan — every finding on the <b>Security</b> tab — still applies.</div>
226
+ </div>`;
227
+ return;
228
+ }
229
+
230
+ const engines = engineRows(appState.tools.status, appState.tools.report);
231
+ const all = findingsFromSecurity(appState.security);
232
+ const counts = tabulate(all);
233
+ const visible = filterFindings(all, analysis);
234
+ const summary = engineSummary(appState.tools.report, engines);
235
+ const running = appState.tools.running;
236
+
237
+ container.innerHTML = `<div class="analysis-layout">
238
+ ${renderHead(counts, running)}
239
+ ${renderEngineBlock(engines, summary, running, appState.tools)}
240
+ ${renderAIBlock()}
241
+ ${renderFindingsBlock(visible, counts, analysis)}
242
+ </div>`;
243
+
244
+ // The engines panel may still be loading; ask once and let the store notify.
245
+ if (!appState.tools.status && !appState.tools.loading) void ensureToolsStatus();
246
+ }
247
+
248
+ function renderHead(counts, running) {
249
+ const chip = (label, value, cls = '') => `<span class="analysis-chip ${cls}"><b>${value}</b> ${label}</span>`;
250
+ return `<header class="analysis-head">
251
+ <div class="analysis-head-text">
252
+ <h1 class="analysis-title">Deep analysis</h1>
253
+ <p class="analysis-sub">Every engine this machine has, plus the built-in scanner — merged into one list. Click any file to open it at the line.</p>
254
+ </div>
255
+ <div class="analysis-actions">
256
+ <button class="btn btn-ink" data-run-all ${running ? 'disabled' : ''}>${running ? 'Running…' : 'Run all engines'}</button>
257
+ <button class="btn btn-ghost" data-run-kind="security" ${running ? 'disabled' : ''}>Security only</button>
258
+ <button class="btn btn-ghost" data-run-kind="dead-code" ${running ? 'disabled' : ''}>Dead code only</button>
259
+ </div>
260
+ </header>
261
+ <div class="analysis-stats">
262
+ ${chip('findings', counts.total)}
263
+ ${counts.critical ? chip('critical', counts.critical, 'is-critical') : ''}
264
+ ${counts.high ? chip('high', counts.high, 'is-high') : ''}
265
+ ${chip('from engines', counts.external, counts.external ? 'is-external' : '')}
266
+ ${chip('from built-in', counts.builtin)}
267
+ </div>`;
268
+ }
269
+
270
+ function renderEngineBlock(engines, summary, running, tools) {
271
+ const rows = engines.length ? engines.map((e) => renderEngineRow(e, running)).join('') : '';
272
+ const empty = tools.loading
273
+ ? '<div class="analysis-empty">Checking which engines are installed…</div>'
274
+ : (engines.length ? '' : '<div class="analysis-empty">No engine table — the server did not answer.</div>');
275
+
276
+ return `<section class="analysis-block">
277
+ <h2 class="analysis-h">Engines <span class="analysis-h-sub">what this machine can run</span></h2>
278
+ <div class="analysis-engines">${rows}${empty}</div>
279
+ <div class="analysis-verdict is-${escapeHtml(summary.tone)}">${escapeHtml(summary.text)}</div>
280
+ </section>`;
281
+ }
282
+
283
+ function renderEngineRow(engine, running) {
284
+ const installing = state.tools.install[engine.id];
285
+ let badge;
286
+ if (installing && installing.running) badge = '<span class="tools-badge is-running">installing…</span>';
287
+ else if (!engine.available) badge = '<span class="tools-badge is-missing">not installed</span>';
288
+ else if (running) badge = '<span class="tools-badge is-running">running…</span>';
289
+ else if (engine.ran) badge = `<span class="tools-badge is-ran">${engine.findings} · ${engine.ms}ms</span>`;
290
+ else if (engine.failed) badge = '<span class="tools-badge is-failed">failed</span>';
291
+ else badge = `<span class="tools-badge is-ready">ready${engine.how ? ' · ' + escapeHtml(engine.how) : ''}</span>`;
292
+
293
+ const note = !engine.available && engine.reason
294
+ ? `<span class="tools-reason">${escapeHtml(engine.reason)}</span>`
295
+ : (engine.failure ? `<span class="tools-reason">${escapeHtml(engine.failure)}</span>` : '');
296
+
297
+ const runBtn = engine.available
298
+ ? `<button class="btn btn-ghost btn-sm" data-engine-run="${escapeHtml(engine.id)}" ${running ? 'disabled' : ''}>Run</button>`
299
+ : '';
300
+ const installBtn = !engine.available && engine.installable
301
+ ? `<button class="btn btn-ink btn-sm" data-engine-install="${escapeHtml(engine.id)}" ${installing && installing.running ? 'disabled' : ''}>${installing && installing.running ? 'Installing…' : 'Install'}</button>`
302
+ : '';
303
+
304
+ return `<div class="analysis-engine${engine.available ? '' : ' is-missing'}">
305
+ <div class="analysis-engine-top">
306
+ <span class="tools-name">${escapeHtml(engine.label)}</span>
307
+ ${badge}
308
+ <span class="analysis-engine-kind">${escapeHtml(engine.kind)}</span>
309
+ ${installBtn}
310
+ ${runBtn}
311
+ </div>
312
+ <div class="tools-purpose">${escapeHtml(engine.purpose)}</div>
313
+ ${note}
314
+ ${renderEngineOptions(engine)}
315
+ ${renderInstallConsole(installing)}
316
+ </div>`;
317
+ }
318
+
319
+ // The Configure panel: a <details> with one control per schema entry, so the
320
+ // useful surface of each engine — semgrep's rule sets, gitleaks' history
321
+ // scan, vulture's confidence floor — is a form, not a command line. Values
322
+ // live in `state.tools.options` and ride along with the next run; the server
323
+ // validates them against the same schema before they touch an argv.
324
+ function renderEngineOptions(engine) {
325
+ if (!engine.options.length) return '';
326
+ const values = state.tools.options[engine.id] || {};
327
+
328
+ const field = (opt) => {
329
+ const current = values[opt.key] !== undefined ? values[opt.key] : engine.defaults[opt.key];
330
+ let control;
331
+ if (opt.type === 'boolean') {
332
+ control = `<label class="analysis-opt-check">
333
+ <input type="checkbox" data-opt data-engine="${escapeHtml(engine.id)}" data-key="${escapeHtml(opt.key)}" ${current ? 'checked' : ''}>
334
+ <span>${escapeHtml(opt.label)}</span>
335
+ </label>`;
336
+ } else if (opt.type === 'enum') {
337
+ const options = opt.values.map((v) =>
338
+ `<option value="${escapeHtml(v)}" ${v === current ? 'selected' : ''}>${escapeHtml(v)}</option>`).join('');
339
+ control = `<label class="analysis-opt-field"><span>${escapeHtml(opt.label)}</span>
340
+ <select class="text-input" data-opt data-engine="${escapeHtml(engine.id)}" data-key="${escapeHtml(opt.key)}">${options}</select>
341
+ </label>`;
342
+ } else if (opt.type === 'multi') {
343
+ const chosen = Array.isArray(current) ? current : [];
344
+ control = `<div class="analysis-opt-field"><span>${escapeHtml(opt.label)}</span>
345
+ <div class="analysis-opt-multi">${opt.values.map((v) => `
346
+ <label class="analysis-opt-check">
347
+ <input type="checkbox" data-opt data-engine="${escapeHtml(engine.id)}" data-key="${escapeHtml(opt.key)}"
348
+ data-opt-value="${escapeHtml(v)}" ${chosen.includes(v) ? 'checked' : ''}>
349
+ <span>${escapeHtml(v)}</span>
350
+ </label>`).join('')}
351
+ </div>
352
+ </div>`;
353
+ } else { // number
354
+ control = `<label class="analysis-opt-field"><span>${escapeHtml(opt.label)}</span>
355
+ <input type="number" class="text-input" data-opt data-engine="${escapeHtml(engine.id)}" data-key="${escapeHtml(opt.key)}"
356
+ min="${opt.min}" max="${opt.max}" value="${escapeHtml(String(current ?? opt.default))}">
357
+ </label>`;
358
+ }
359
+ const hint = opt.hint ? `<span class="analysis-opt-hint">${escapeHtml(opt.hint)}</span>` : '';
360
+ return `<div class="analysis-opt">${control}${hint}</div>`;
361
+ };
362
+
363
+ return `<details class="analysis-config">
364
+ <summary>Configure</summary>
365
+ <div class="analysis-config-body">${engine.options.map(field).join('')}</div>
366
+ </details>`;
367
+ }
368
+
369
+ // The install log, live while an install runs and kept afterwards so a failed
370
+ // install leaves its reason on screen instead of in a toast that faded.
371
+ function renderInstallConsole(installing) {
372
+ if (!installing || (!installing.log.length && !installing.running && !installing.error)) return '';
373
+ const verdict = installing.running
374
+ ? ''
375
+ : installing.ok
376
+ ? '<div class="analysis-console-verdict is-ok">Installed — the engine is ready.</div>'
377
+ : `<div class="analysis-console-verdict is-err">${escapeHtml(installing.error || 'The install failed.')}</div>`;
378
+ return `<div class="analysis-console-wrap">
379
+ <pre class="analysis-console">${escapeHtml(installing.log.join('\n'))}${installing.running ? ' ▍' : ''}</pre>
380
+ ${verdict}
381
+ </div>`;
382
+ }
383
+
384
+
385
+ function renderAIBlock() {
386
+ const configured = llm.isConfigured();
387
+ const ask = escapeHtml(state.analysis.ai.question || '');
388
+ const settingsBtn = configured
389
+ ? ''
390
+ : '<button class="btn btn-ghost btn-sm" data-open-settings>API key…</button>';
391
+ return `<section class="analysis-block">
392
+ <h2 class="analysis-h">What this means <span class="analysis-h-sub">ask the AI</span></h2>
393
+ <div class="analysis-ai">
394
+ <div class="analysis-ai-bar">
395
+ <button class="btn btn-ink btn-sm" data-ai-explain>${configured ? 'Explain this analysis' : 'Explain (needs an endpoint)'}</button>
396
+ ${settingsBtn}
397
+ <span class="analysis-ai-hint">Uses the report above — engines, severities, files. Nothing is sent anywhere but your own endpoint.</span>
398
+ </div>
399
+ <div class="analysis-ai-note" data-ai-note>${renderAINote(state.analysis.ai)}</div>
400
+ <div class="analysis-ai-ask">
401
+ <input type="text" class="text-input" data-ai-question placeholder="Ask about this report — e.g. “is the eval finding reachable?”" value="${ask}" spellcheck="false" autocomplete="off">
402
+ <button class="btn btn-ghost btn-sm" data-ai-explain>Ask</button>
403
+ </div>
404
+ </div>
405
+ </section>`;
406
+ }
407
+
408
+ // The note is plain text from the model. It goes through `mdInline`, the same
409
+ // boundary helper the inspector uses: escape first, then bold and inline code.
410
+ // The prompt asks for prose, but models reach for `**bold**` and backticks
411
+ // anyway, and half-rendered asterisks look like a bug.
412
+ export function renderAINote(ai) {
413
+ if (ai.running && !ai.text) {
414
+ return '<span class="analysis-ai-waiting"><span class="search-spinner"></span> Reading the report…</span>';
415
+ }
416
+ if (ai.error) return `<span class="analysis-ai-error">${escapeHtml(ai.error)}</span>`;
417
+ if (!ai.text) {
418
+ return '<span class="analysis-ai-idle">Press <b>Explain this analysis</b> for a prioritised read of the findings above, or ask a question about any of them.</span>';
419
+ }
420
+ const html = ai.text
421
+ .split(/\n{2,}/)
422
+ .map((para) => `<p>${mdInline(para).replace(/\n/g, '<br>')}</p>`)
423
+ .join('');
424
+ const cursor = ai.running ? '<span class="analysis-ai-cursor">▍</span>' : '';
425
+ return html + cursor;
426
+ }
427
+
428
+ function renderFindingsBlock(visible, counts, analysis) {
429
+ const sevChips = ['all', ...SEV_ORDER].map((sev) => {
430
+ const n = sev === 'all' ? counts.total : (counts.bySeverity[sev] || 0);
431
+ if (sev !== 'all' && !n) return '';
432
+ const active = analysis.severity === sev ? ' is-active' : '';
433
+ return `<button class="btn btn-ghost btn-sm${active}" data-sev="${sev}">${sev} (${n})</button>`;
434
+ }).join('');
435
+
436
+ const tools = Object.keys(counts.byTool).sort((a, b) => counts.byTool[b] - counts.byTool[a]);
437
+ const toolChips = tools.length > 1
438
+ ? ['all', ...tools].map((tool) => {
439
+ const n = tool === 'all' ? counts.total : counts.byTool[tool];
440
+ const label = tool === BUILTIN_TOOL ? 'built-in scanner' : tool;
441
+ const active = analysis.tool === tool ? ' is-active' : '';
442
+ return `<button class="btn btn-ghost btn-sm${active}" data-tool-filter="${escapeHtml(tool)}">${escapeHtml(label)} (${n})</button>`;
443
+ }).join('')
444
+ : '';
445
+
446
+ const body = visible.length
447
+ ? visible.map(renderFindingRow).join('')
448
+ : `<tr><td colspan="5" class="analysis-no-rows">Nothing matches those filters.</td></tr>`;
449
+
450
+ return `<section class="analysis-block">
451
+ <h2 class="analysis-h">Findings <span class="analysis-h-sub">${visible.length} of ${counts.total} shown</span></h2>
452
+ <div class="analysis-filterbar">
453
+ <div class="analysis-filter-group">${sevChips}</div>
454
+ ${toolChips ? `<div class="analysis-filter-group">${toolChips}</div>` : ''}
455
+ <input type="text" class="text-input analysis-search" data-finding-search
456
+ placeholder="Filter by file, rule or message…" value="${escapeHtml(analysis.q || '')}" spellcheck="false">
457
+ </div>
458
+ <div class="analysis-table-wrap">
459
+ <table class="analysis-table">
460
+ <thead><tr>
461
+ <th class="col-sev">Severity</th>
462
+ <th class="col-file">File</th>
463
+ <th class="col-rule">Rule</th>
464
+ <th class="col-tool">Engine</th>
465
+ <th>Finding</th>
466
+ </tr></thead>
467
+ <tbody>${body}</tbody>
468
+ </table>
469
+ </div>
470
+ </section>`;
471
+ }
472
+
473
+ // One finding. The file cell is the point of the whole view: it carries the
474
+ // path, the line, and the click that opens the Code tab there.
475
+ function renderFindingRow(row) {
476
+ return `<tr class="analysis-row is-${escapeHtml(row.severity)}">
477
+ <td class="col-sev"><span class="sev-pill is-${escapeHtml(row.severity)}">${escapeHtml(row.severity)}</span></td>
478
+ <td class="col-file">
479
+ <button class="analysis-file" data-goto="${escapeHtml(row.path)}" data-goto-line="${row.line}"
480
+ title="Open ${escapeHtml(row.path)} at line ${row.line}">
481
+ <span class="analysis-file-name">${escapeHtml(row.name)}</span>
482
+ <span class="analysis-file-line">L${row.line}</span>
483
+ </button>
484
+ <span class="analysis-file-dir">${escapeHtml(dirOf(row.path))}</span>
485
+ </td>
486
+ <td class="col-rule"><code>${escapeHtml(row.rule)}</code></td>
487
+ <td class="col-tool"><span class="analysis-tool is-${escapeHtml(row.tool === BUILTIN_TOOL ? 'builtin' : 'external')}">${escapeHtml(row.tool === BUILTIN_TOOL ? 'built-in' : row.tool)}</span></td>
488
+ <td class="analysis-msg">${escapeHtml(row.message)}</td>
489
+ </tr>`;
490
+ }
491
+
492
+ function dirOf(path) {
493
+ const at = String(path || '').lastIndexOf('/');
494
+ return at === -1 ? '' : path.slice(0, at);
495
+ }
496
+
@@ -0,0 +1,154 @@
1
+ // Mermaid rendering plus the canvas furniture: click wiring, pan & zoom,
2
+ // fit-to-view, SVG export. Mermaid comes from the vendored UMD build and
3
+ // stays a global; everything interactive happens here.
4
+
5
+ let initialized = false;
6
+ let renderSeq = 0;
7
+
8
+ function ensureMermaid() {
9
+ if (initialized) return;
10
+ window.mermaid.initialize({
11
+ startOnLoad: false,
12
+ securityLevel: 'loose', // labels are escaped upstream; local tool, local data
13
+ flowchart: { htmlLabels: false },
14
+ });
15
+ initialized = true;
16
+ }
17
+
18
+ export async function renderInto(contentEl, source) {
19
+ ensureMermaid();
20
+ const { svg } = await window.mermaid.render('obd' + ++renderSeq, source);
21
+ contentEl.innerHTML = svg;
22
+ const el = contentEl.querySelector('svg');
23
+ // Mermaid emits `max-width` + fluid sizing, so the CSS box rarely matches
24
+ // the viewBox units — and our pan/zoom math assumes 1 unit = 1 px. Pin the
25
+ // box to the viewBox, or fit() sizes and centers against a phantom.
26
+ if (el && el.viewBox && el.viewBox.baseVal && el.viewBox.baseVal.width) {
27
+ const vb = el.viewBox.baseVal;
28
+ el.setAttribute('width', String(vb.width));
29
+ el.setAttribute('height', String(vb.height));
30
+ el.style.maxWidth = 'none';
31
+ el.style.width = `${vb.width}px`;
32
+ el.style.height = `${vb.height}px`;
33
+ }
34
+ return el;
35
+ }
36
+
37
+ // Mermaid gives every node a DOM id like "flowchart-f3-87"; the middle part
38
+ // is the id our diagram builders assigned, and `nodes` maps it back to a
39
+ // file or folder payload.
40
+ export function wireNodeClicks(contentEl, nodes, onClick) {
41
+ for (const g of contentEl.querySelectorAll('g.node')) {
42
+ const m = (g.id || '').match(/^flowchart-([A-Za-z0-9]+)-\d+$/);
43
+ if (!m || !nodes[m[1]]) continue;
44
+ g.style.cursor = 'pointer';
45
+ g.addEventListener('click', (event) => {
46
+ event.stopPropagation();
47
+ onClick(nodes[m[1]]);
48
+ });
49
+ }
50
+ }
51
+
52
+ export function makePanzoom(canvas, content) {
53
+ let scale = 1;
54
+ let tx = 0;
55
+ let ty = 0;
56
+ let dragging = false;
57
+ let lastX = 0;
58
+ let lastY = 0;
59
+
60
+ const apply = () => {
61
+ content.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
62
+ };
63
+
64
+ canvas.addEventListener('pointerdown', (event) => {
65
+ if (content.classList.contains('atlas-mode')) return; // the atlas list takes its own clicks
66
+ dragging = true;
67
+ lastX = event.clientX;
68
+ lastY = event.clientY;
69
+ canvas.classList.add('is-dragging');
70
+ canvas.setPointerCapture(event.pointerId);
71
+ });
72
+ canvas.addEventListener('pointermove', (event) => {
73
+ if (!dragging) return;
74
+ tx += event.clientX - lastX;
75
+ ty += event.clientY - lastY;
76
+ lastX = event.clientX;
77
+ lastY = event.clientY;
78
+ apply();
79
+ });
80
+ const stop = () => {
81
+ dragging = false;
82
+ canvas.classList.remove('is-dragging');
83
+ };
84
+ canvas.addEventListener('pointerup', stop);
85
+ canvas.addEventListener('pointercancel', stop);
86
+
87
+ canvas.addEventListener('wheel', (event) => {
88
+ if (content.classList.contains('atlas-mode')) return; // the atlas list scrolls
89
+ event.preventDefault();
90
+ const rect = canvas.getBoundingClientRect();
91
+ const cx = event.clientX - rect.left;
92
+ const cy = event.clientY - rect.top;
93
+ const factor = event.deltaY < 0 ? 1.12 : 1 / 1.12;
94
+ const next = Math.min(4, Math.max(0.12, scale * factor));
95
+ tx = cx - ((cx - tx) * next) / scale;
96
+ ty = cy - ((cy - ty) * next) / scale;
97
+ scale = next;
98
+ apply();
99
+ }, { passive: false });
100
+
101
+ canvas.addEventListener('dblclick', () => fit());
102
+
103
+ function fit() {
104
+ const first = content.firstElementChild;
105
+ if (!first) return;
106
+ let w = 0;
107
+ let h = 0;
108
+ if (first instanceof SVGElement) {
109
+ try {
110
+ const box = first.getBBox();
111
+ w = box.width;
112
+ h = box.height;
113
+ } catch {
114
+ return;
115
+ }
116
+ } else {
117
+ // HTML content (the tree map): offset sizes ignore the transform.
118
+ w = first.offsetWidth;
119
+ h = first.offsetHeight;
120
+ }
121
+ if (!w || !h) return;
122
+ const cw = canvas.clientWidth - 48;
123
+ const ch = canvas.clientHeight - 48;
124
+ scale = Math.min(cw / w, ch / h, 1.15);
125
+ tx = (canvas.clientWidth - w * scale) / 2;
126
+ ty = (canvas.clientHeight - h * scale) / 2;
127
+ apply();
128
+ }
129
+
130
+ // Back to the un-zoomed, un-panned resting position (the atlas list wants
131
+ // this — it is a document, not a diagram).
132
+ function home() {
133
+ scale = 1;
134
+ tx = 0;
135
+ ty = 0;
136
+ apply();
137
+ }
138
+
139
+ return { fit, reset: fit, home };
140
+ }
141
+
142
+ export function downloadSvg(contentEl, name) {
143
+ const svg = contentEl.querySelector('svg');
144
+ if (!svg) return false;
145
+ const clone = svg.cloneNode(true);
146
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
147
+ const blob = new Blob(['<?xml version="1.0"?>\n' + clone.outerHTML], { type: 'image/svg+xml' });
148
+ const a = document.createElement('a');
149
+ a.href = URL.createObjectURL(blob);
150
+ a.download = name;
151
+ a.click();
152
+ URL.revokeObjectURL(a.href);
153
+ return true;
154
+ }