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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/onboarder.js +14 -0
- package/bin/postinstall.js +6 -0
- package/cli/commands.js +436 -0
- package/cli/main.js +129 -0
- package/cli/prompt.js +101 -0
- package/cli/ui.js +55 -0
- package/cli/wizard.js +325 -0
- package/package.json +48 -0
- package/public/app.js +1339 -0
- package/public/index.html +455 -0
- package/public/js/about.js +284 -0
- package/public/js/aiDraft.js +163 -0
- package/public/js/analysisPanel.js +270 -0
- package/public/js/analysisReport.js +154 -0
- package/public/js/api.js +264 -0
- package/public/js/atlas.js +98 -0
- package/public/js/blameView.js +41 -0
- package/public/js/codeTab.js +256 -0
- package/public/js/codeViewer.js +184 -0
- package/public/js/components/FileChip.js +102 -0
- package/public/js/components/MetricSparkline.js +89 -0
- package/public/js/components/RiskBadge.js +105 -0
- package/public/js/deepAnalysisView.js +496 -0
- package/public/js/diagramPane.js +154 -0
- package/public/js/diffView.js +352 -0
- package/public/js/docsView.js +566 -0
- package/public/js/fileSourceBrowser.js +47 -0
- package/public/js/flameGraph.js +75 -0
- package/public/js/forceGraph.js +592 -0
- package/public/js/heatmap.js +185 -0
- package/public/js/highlight.js +122 -0
- package/public/js/html.js +50 -0
- package/public/js/insightsView.js +236 -0
- package/public/js/inspector.js +535 -0
- package/public/js/llm.js +345 -0
- package/public/js/mapView.js +159 -0
- package/public/js/markdown.js +119 -0
- package/public/js/mindmap.js +289 -0
- package/public/js/repoFiles.js +44 -0
- package/public/js/sbomView.js +142 -0
- package/public/js/scanCache.js +76 -0
- package/public/js/search.js +874 -0
- package/public/js/serverSettings.js +169 -0
- package/public/js/state.js +192 -0
- package/public/js/tour.js +17 -0
- package/public/js/transitions.js +15 -0
- package/public/js/tree.js +200 -0
- package/public/js/workflowsView.js +95 -0
- package/public/styles.css +2767 -0
- package/public/vendor/mermaid.min.js +3587 -0
- package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/vendor/monaco/vs/loader.js +11 -0
- package/public/vendor/monaco/worker-boot.js +9 -0
- package/server/.fuse_hidden0000000800000001 +36 -0
- package/server/apiDiff.js +25 -0
- package/server/apiDocs.js +67 -0
- package/server/apiFile.js +36 -0
- package/server/apiGitBlame.js +63 -0
- package/server/apiMcp.js +110 -0
- package/server/apiScan.js +119 -0
- package/server/apiSearch.js +253 -0
- package/server/apiSettings.js +118 -0
- package/server/apiTools.js +90 -0
- package/server/config.js +227 -0
- package/server/fileSourceNode.js +44 -0
- package/server/gitClone.js +95 -0
- package/server/gitDiff.js +184 -0
- package/server/gitHistory.js +110 -0
- package/server/htmlText.js +44 -0
- package/server/http.js +55 -0
- package/server/httpGuards.js +87 -0
- package/server/index.js +156 -0
- package/server/llmProxy.js +162 -0
- package/server/logger.js +29 -0
- package/server/mcp/analysis.js +209 -0
- package/server/mcp/http.js +213 -0
- package/server/mcp/runner.js +278 -0
- package/server/mcp/server.js +241 -0
- package/server/mcp/standalone.js +42 -0
- package/server/mcp/tools.js +683 -0
- package/server/paths.js +39 -0
- package/server/router.js +218 -0
- package/server/searchIndex.js +118 -0
- package/server/sessions.js +163 -0
- package/server/static.js +59 -0
- package/server/tools/install.js +246 -0
- package/server/tools/parse.js +170 -0
- package/server/tools/platform.js +91 -0
- package/server/tools/registry.js +212 -0
- package/server/tools/scan.js +136 -0
- package/server/tools.js +212 -0
- package/server/tunnel.js +92 -0
- package/shared/analyzer/docs.js +135 -0
- package/shared/analyzer/explainLocal.js +163 -0
- package/shared/analyzer/graph.js +461 -0
- package/shared/analyzer/health.js +215 -0
- package/shared/analyzer/history.js +146 -0
- package/shared/analyzer/languages/csharp.js +39 -0
- package/shared/analyzer/languages/generic.js +131 -0
- package/shared/analyzer/languages/go.js +70 -0
- package/shared/analyzer/languages/index.js +43 -0
- package/shared/analyzer/languages/java.js +39 -0
- package/shared/analyzer/languages/javascript.js +240 -0
- package/shared/analyzer/languages/python.js +120 -0
- package/shared/analyzer/languages/rust.js +42 -0
- package/shared/analyzer/languages/typescript.js +138 -0
- package/shared/analyzer/licenses.js +151 -0
- package/shared/analyzer/metrics.js +120 -0
- package/shared/analyzer/pathUtil.js +69 -0
- package/shared/analyzer/patterns.js +272 -0
- package/shared/analyzer/scan.js +473 -0
- package/shared/analyzer/security.js +187 -0
- package/shared/analyzer/services.js +157 -0
- package/shared/analyzer/stack.js +173 -0
- package/shared/analyzer/tour.js +69 -0
- package/shared/analyzer/util.js +123 -0
- package/shared/analyzer/workflows.js +143 -0
- package/shared/diagram/aiFacts.js +145 -0
- package/shared/diagram/aiMermaid.js +59 -0
- package/shared/diagram/atlas.js +93 -0
- package/shared/diagram/mermaid.js +459 -0
- package/shared/search/query.js +518 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// Turns scan results into Mermaid flowchart source. Kept separate from the
|
|
2
|
+
// UI so the exact same diagrams can be produced in Node (tests, exports)
|
|
3
|
+
// and in the browser.
|
|
4
|
+
|
|
5
|
+
import { baseName, dirOf, topFolderOf } from '../analyzer/pathUtil.js';
|
|
6
|
+
import { factIndex, scanIndex } from '../analyzer/graph.js';
|
|
7
|
+
|
|
8
|
+
// ---- theming ---------------------------------------------------------------
|
|
9
|
+
// Every diagram embeds its own init directive, so exported SVGs carry the
|
|
10
|
+
// theme too. The app flips `currentTheme`; Node tests can do the same.
|
|
11
|
+
|
|
12
|
+
let currentTheme = 'light';
|
|
13
|
+
|
|
14
|
+
export function setDiagramTheme(theme) {
|
|
15
|
+
currentTheme = theme === 'dark' ? 'dark' : 'light';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getDiagramTheme() {
|
|
19
|
+
return currentTheme;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PALETTES = {
|
|
23
|
+
light: {
|
|
24
|
+
background: '#f7f4ed', node: '#fffdf7', border: '#3d3a33', text: '#2a2721',
|
|
25
|
+
line: '#8a8577', cluster: '#efe9dc66', clusterBorder: '#8a8577',
|
|
26
|
+
entry: 'fill:#dce8d8,stroke:#2e5d43,stroke-width:2px,color:#1d3a2a',
|
|
27
|
+
hub: 'fill:#f3e3c8,stroke:#8a5a1d,stroke-width:2px,color:#5c3a10',
|
|
28
|
+
cycle: 'fill:#f3d4c8,stroke:#a4442a,stroke-width:2px,color:#6e2c17',
|
|
29
|
+
ghost: 'fill:#efe9dc,stroke:#b5ae9c,stroke-dasharray:4 3,color:#8a8577',
|
|
30
|
+
self: 'fill:#dce8d8,stroke:#2e5d43,stroke-width:3px,color:#1d3a2a',
|
|
31
|
+
ext: 'fill:#efe9dc,stroke:#b5ae9c,stroke-dasharray:4 3,color:#8a8577',
|
|
32
|
+
blank: 'fill:transparent,stroke:transparent,color:transparent',
|
|
33
|
+
note: 'fill:transparent,stroke:transparent,color:#8a8577',
|
|
34
|
+
r0: 'fill:#dce8d8,stroke:#2e5d43,stroke-width:1.5px,color:#1d3a2a',
|
|
35
|
+
r1: 'fill:#fffdf7,stroke:#8a8577,stroke-width:1px,color:#2a2721',
|
|
36
|
+
r2: 'fill:#f3e3c8,stroke:#8a5a1d,stroke-width:1.5px,color:#5c3a10',
|
|
37
|
+
r3: 'fill:#f3d4c8,stroke:#a4442a,stroke-width:2px,color:#6e2c17',
|
|
38
|
+
},
|
|
39
|
+
dark: {
|
|
40
|
+
background: '#211e19', node: '#2b2721', border: '#b8b0a0', text: '#ece7db',
|
|
41
|
+
line: '#8a8577', cluster: '#2b272166', clusterBorder: '#6b6558',
|
|
42
|
+
entry: 'fill:#243d2c,stroke:#8fbd9f,stroke-width:2px,color:#cfe6d6',
|
|
43
|
+
hub: 'fill:#463617,stroke:#d9a75f,stroke-width:2px,color:#f0d9ae',
|
|
44
|
+
cycle: 'fill:#472418,stroke:#d97b5a,stroke-width:2px,color:#f3c4ae',
|
|
45
|
+
ghost: 'fill:#2b2721,stroke:#6b6558,stroke-dasharray:4 3,color:#9b9484',
|
|
46
|
+
self: 'fill:#243d2c,stroke:#8fbd9f,stroke-width:3px,color:#cfe6d6',
|
|
47
|
+
ext: 'fill:#2b2721,stroke:#6b6558,stroke-dasharray:4 3,color:#9b9484',
|
|
48
|
+
blank: 'fill:transparent,stroke:transparent,color:transparent',
|
|
49
|
+
note: 'fill:transparent,stroke:transparent,color:#9b9484',
|
|
50
|
+
r0: 'fill:#243d2c,stroke:#8fbd9f,stroke-width:1.5px,color:#cfe6d6',
|
|
51
|
+
r1: 'fill:#2b2721,stroke:#8a8577,stroke-width:1px,color:#ece7db',
|
|
52
|
+
r2: 'fill:#463617,stroke:#d9a75f,stroke-width:1.5px,color:#f0d9ae',
|
|
53
|
+
r3: 'fill:#472418,stroke:#d97b5a,stroke-width:2px,color:#f3c4ae',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function themeBlock() {
|
|
58
|
+
const p = PALETTES[currentTheme];
|
|
59
|
+
return `%%{init: {
|
|
60
|
+
'theme': 'base',
|
|
61
|
+
'themeVariables': {
|
|
62
|
+
'background': '${p.background}',
|
|
63
|
+
'primaryColor': '${p.node}',
|
|
64
|
+
'primaryBorderColor': '${p.border}',
|
|
65
|
+
'primaryTextColor': '${p.text}',
|
|
66
|
+
'lineColor': '${p.line}',
|
|
67
|
+
'secondaryColor': '${p.node}',
|
|
68
|
+
'tertiaryColor': '${p.node}',
|
|
69
|
+
'clusterBkg': '${p.cluster}',
|
|
70
|
+
'clusterBorder': '${p.clusterBorder}',
|
|
71
|
+
'edgeLabelBackground': '${p.background}',
|
|
72
|
+
'fontFamily': 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
73
|
+
'fontSize': '13px'
|
|
74
|
+
},
|
|
75
|
+
'flowchart': { 'curve': 'basis', 'htmlLabels': false, 'nodeSpacing': 42, 'rankSpacing': 52 }
|
|
76
|
+
}}%%`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// AI-generated diagrams get our theme injected after the fact, so a model's
|
|
80
|
+
// sketch is indistinguishable from the hand-drawn ones.
|
|
81
|
+
export function diagramThemeBlock() {
|
|
82
|
+
return themeBlock();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function classDefLines() {
|
|
86
|
+
const p = PALETTES[currentTheme];
|
|
87
|
+
return [
|
|
88
|
+
` classDef entry ${p.entry}`,
|
|
89
|
+
` classDef hub ${p.hub}`,
|
|
90
|
+
` classDef cycle ${p.cycle}`,
|
|
91
|
+
` classDef ghost ${p.ghost}`,
|
|
92
|
+
` classDef self ${p.self}`,
|
|
93
|
+
` classDef ext ${p.ext}`,
|
|
94
|
+
` classDef blank ${p.blank}`,
|
|
95
|
+
` classDef note ${p.note}`,
|
|
96
|
+
` classDef r0 ${p.r0}`,
|
|
97
|
+
` classDef r1 ${p.r1}`,
|
|
98
|
+
` classDef r2 ${p.r2}`,
|
|
99
|
+
` classDef r3 ${p.r3}`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const MAX_LABEL = 34;
|
|
104
|
+
|
|
105
|
+
export function overviewDiagram(scan, facts) {
|
|
106
|
+
const lines = [themeBlock(), 'flowchart LR'];
|
|
107
|
+
|
|
108
|
+
// Nodes: top-level folders, sized by parsed file count.
|
|
109
|
+
const folderStats = new Map();
|
|
110
|
+
for (const f of scan.files) {
|
|
111
|
+
const top = topFolderOf(f.path);
|
|
112
|
+
folderStats.set(top, (folderStats.get(top) || 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
const entryFolders = new Set(facts.entries.map((p) => topFolderOf(p)));
|
|
115
|
+
|
|
116
|
+
const ids = new Map();
|
|
117
|
+
let n = 0;
|
|
118
|
+
const sorted = [...folderStats.entries()].sort((a, b) => b[1] - a[1]);
|
|
119
|
+
for (const [folder, count] of sorted) {
|
|
120
|
+
const id = 'f' + n++;
|
|
121
|
+
ids.set(folder, id);
|
|
122
|
+
const isEntry = entryFolders.has(folder);
|
|
123
|
+
const label = esc(folder === '(root)' ? '(repo root)' : folder + '/') + '\\n' + count + (count === 1 ? ' file' : ' files');
|
|
124
|
+
if (isEntry) {
|
|
125
|
+
lines.push(` ${id}(["${label}"]):::entry`);
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(` ${id}["${label}"]`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const fe of facts.folderEdges) {
|
|
132
|
+
const a = ids.get(fe.from);
|
|
133
|
+
const b = ids.get(fe.to);
|
|
134
|
+
if (a === undefined || b === undefined) continue;
|
|
135
|
+
lines.push(` ${a} -->|${fe.count}| ${b}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lines.push(...classDefLines());
|
|
139
|
+
|
|
140
|
+
const nodes = {};
|
|
141
|
+
for (const [folder, id] of ids) {
|
|
142
|
+
nodes[id] = { kind: 'folder', path: folder === '(root)' ? '' : folder };
|
|
143
|
+
}
|
|
144
|
+
return { source: lines.join('\n'), nodes };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function folderDiagram(scan, facts, folder, opts = {}) {
|
|
148
|
+
const include = opts.include || (() => true);
|
|
149
|
+
const here = scanIndex(scan).filesIn(folder).filter((f) => include(f.path));
|
|
150
|
+
if (!here.length) return emptyDiagram('Nothing matches the filter in this folder.');
|
|
151
|
+
|
|
152
|
+
const lines = [themeBlock(), 'flowchart LR'];
|
|
153
|
+
const localSet = new Set(here.map((f) => f.path));
|
|
154
|
+
const ids = idMapFor(here.map((f) => f.path));
|
|
155
|
+
const fx = factIndex(facts);
|
|
156
|
+
|
|
157
|
+
for (const f of here) {
|
|
158
|
+
const id = ids.get(f.path);
|
|
159
|
+
const fin = facts.fanIn[f.path] || 0;
|
|
160
|
+
const isEntry = fx.entries.has(f.path);
|
|
161
|
+
const inCycle = fx.inCycle.has(f.path);
|
|
162
|
+
const cls = isEntry ? ':::entry' : inCycle ? ':::cycle' : fin >= 5 ? ':::hub' : '';
|
|
163
|
+
lines.push(` ${id}["${esc(shortName(f.name))}"]${cls}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Edges between files in this folder; edges to elsewhere get ghost nodes.
|
|
167
|
+
const ghosts = new Map(); // outside path -> id
|
|
168
|
+
let g = 0;
|
|
169
|
+
for (const e of scan.edges) {
|
|
170
|
+
const fromIn = localSet.has(e.from);
|
|
171
|
+
const toIn = localSet.has(e.to);
|
|
172
|
+
if (fromIn && toIn) {
|
|
173
|
+
lines.push(` ${ids.get(e.from)} --> ${ids.get(e.to)}`);
|
|
174
|
+
} else if (fromIn && !toIn && include(e.to)) {
|
|
175
|
+
const gid = ghostId(ghosts, e.to, () => 'g' + g++);
|
|
176
|
+
lines.push(` ${ids.get(e.from)} -.-> ${gid}`);
|
|
177
|
+
} else if (!fromIn && toIn && include(e.from)) {
|
|
178
|
+
const gid = ghostId(ghosts, e.from, () => 'g' + g++);
|
|
179
|
+
lines.push(` ${gid} -.-> ${ids.get(e.to)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const [p, gid] of ghosts) {
|
|
183
|
+
lines.push(` ${gid}["${esc(shortName(baseName(p)))}\\n${esc(dirOf(p) || '(root)')}"]:::ghost`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lines.push(...classDefLines());
|
|
187
|
+
|
|
188
|
+
const nodes = {};
|
|
189
|
+
for (const [p, id] of ids) nodes[id] = { kind: 'file', path: p };
|
|
190
|
+
for (const [p, id] of ghosts) nodes[id] = { kind: 'file', path: p };
|
|
191
|
+
return { source: lines.join('\n'), nodes };
|
|
192
|
+
}
|
|
193
|
+
export function fileDetailDiagram(scan, facts, path) {
|
|
194
|
+
const file = scanIndex(scan).fileAt(path);
|
|
195
|
+
if (!file) return emptyDiagram('File not found in the scan.');
|
|
196
|
+
|
|
197
|
+
const lines = [themeBlock(), 'flowchart LR'];
|
|
198
|
+
const importers = (facts.importers[path] || []).slice(0, 8);
|
|
199
|
+
const imports = (facts.importsOf[path] || []).slice(0, 8);
|
|
200
|
+
const ext = scan.externals.filter((x) => x.usedBy.includes(path)).slice(0, 5);
|
|
201
|
+
|
|
202
|
+
lines.push(` subgraph up["imported by"]`);
|
|
203
|
+
importers.forEach((p, i) => lines.push(` up${i}["${esc(shortName(baseName(p)))}"]`));
|
|
204
|
+
if (!importers.length) lines.push(` upnone[" "]:::blank`);
|
|
205
|
+
lines.push(' end');
|
|
206
|
+
|
|
207
|
+
lines.push(` self["${esc(shortName(file.name))}"]:::self`);
|
|
208
|
+
|
|
209
|
+
lines.push(` subgraph down["imports"]`);
|
|
210
|
+
imports.forEach((p, i) => lines.push(` dn${i}["${esc(shortName(baseName(p)))}"]`));
|
|
211
|
+
ext.forEach((x, i) => lines.push(` dx${i}["${esc(x.name)}"]:::ext`));
|
|
212
|
+
if (!imports.length && !ext.length) lines.push(` dnnone[" "]:::blank`);
|
|
213
|
+
lines.push(' end');
|
|
214
|
+
|
|
215
|
+
importers.forEach((p, i) => lines.push(` up${i} --> self`));
|
|
216
|
+
imports.forEach((p, i) => lines.push(` self --> dn${i}`));
|
|
217
|
+
ext.forEach((x, i) => lines.push(` self -.-> dx${i}`));
|
|
218
|
+
|
|
219
|
+
lines.push(...classDefLines());
|
|
220
|
+
|
|
221
|
+
const nodes = { self: { kind: 'file', path } };
|
|
222
|
+
importers.forEach((p, i) => (nodes['up' + i] = { kind: 'file', path: p }));
|
|
223
|
+
imports.forEach((p, i) => (nodes['dn' + i] = { kind: 'file', path: p }));
|
|
224
|
+
return { source: lines.join('\n'), nodes };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function callsDiagram(file) {
|
|
228
|
+
if (!file?.calls?.length) return emptyDiagram('No internal call links we can see.');
|
|
229
|
+
const lines = [themeBlock(), 'flowchart LR'];
|
|
230
|
+
const names = new Set();
|
|
231
|
+
for (const c of file.calls) {
|
|
232
|
+
names.add(c.from);
|
|
233
|
+
names.add(c.to);
|
|
234
|
+
}
|
|
235
|
+
const ids = idMapFor([...names]);
|
|
236
|
+
for (const name of names) {
|
|
237
|
+
lines.push(` ${ids.get(name)}["${esc(name)}()"]`);
|
|
238
|
+
}
|
|
239
|
+
for (const c of file.calls) {
|
|
240
|
+
lines.push(` ${ids.get(c.from)} --> ${ids.get(c.to)}`);
|
|
241
|
+
}
|
|
242
|
+
return { source: lines.join('\n'), nodes: {} };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function servicesDiagram(scan, manifest) {
|
|
246
|
+
if (!manifest?.services?.length) return emptyDiagram('No services detected.');
|
|
247
|
+
const lines = [themeBlock(), 'flowchart LR'];
|
|
248
|
+
manifest.services.forEach((s, i) => {
|
|
249
|
+
const bits = [esc(s.name)];
|
|
250
|
+
if (s.image) bits.push(esc(s.image));
|
|
251
|
+
if (s.ports?.length) bits.push(esc(s.ports.join(', ')));
|
|
252
|
+
if (s.command) bits.push(esc(shortName(s.command)));
|
|
253
|
+
lines.push(` svc${i}(["${bits.join('\\n')}"])`);
|
|
254
|
+
});
|
|
255
|
+
// Link each service to the folder it builds from, when we can see one.
|
|
256
|
+
manifest.services.forEach((s, i) => {
|
|
257
|
+
if (!s.build) return;
|
|
258
|
+
const dir = s.build.replace(/^\.\/?/, '').replace(/\/$/, '');
|
|
259
|
+
if (!dir || scan.folders.some((f) => f.path === dir)) {
|
|
260
|
+
lines.push(` svc${i} -.->|"${esc(dir || '.')}"| root${i}["${esc(dir || '(repo root)')}"]:::ghost`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
lines.push(...classDefLines());
|
|
264
|
+
return { source: lines.join('\n'), nodes: {} };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function emptyDiagram(note) {
|
|
268
|
+
const source = [themeBlock(), 'flowchart LR', ` empty["${esc(note)}"]:::note`, ...classDefLines()].join('\n');
|
|
269
|
+
return { source, nodes: {} };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The Patterns view: every file placed on its import depth — the longest
|
|
273
|
+
// chain from an entry point — so the architecture's strata are visible at a
|
|
274
|
+
// glance. Edges flow downward; anything horizontal or upward earns a look.
|
|
275
|
+
export function layersDiagram(scan, facts, layersInfo, opts = {}) {
|
|
276
|
+
const include = opts.include || (() => true);
|
|
277
|
+
const { layers, unreachable } = layersInfo;
|
|
278
|
+
const MAX_PER_LAYER = 10;
|
|
279
|
+
const MAX_EDGES = 70;
|
|
280
|
+
|
|
281
|
+
const lines = [themeBlock(), 'flowchart TD'];
|
|
282
|
+
const ids = new Map();
|
|
283
|
+
const nodes = {};
|
|
284
|
+
let n = 0;
|
|
285
|
+
|
|
286
|
+
const fx = factIndex(facts);
|
|
287
|
+
const clsFor = (p) =>
|
|
288
|
+
fx.entries.has(p) ? ':::entry'
|
|
289
|
+
: fx.inCycle.has(p) ? ':::cycle'
|
|
290
|
+
: (facts.fanIn[p] || 0) >= 5 ? ':::hub'
|
|
291
|
+
: '';
|
|
292
|
+
|
|
293
|
+
layers.forEach((files, i) => {
|
|
294
|
+
const shown = files.filter(include);
|
|
295
|
+
if (!shown.length) return;
|
|
296
|
+
const label = i === 0 ? 'the way in' : 'depth ' + i;
|
|
297
|
+
lines.push(` subgraph L${i}["${label}"]`);
|
|
298
|
+
for (const p of shown.slice(0, MAX_PER_LAYER)) {
|
|
299
|
+
const id = 'n' + n++;
|
|
300
|
+
ids.set(p, id);
|
|
301
|
+
nodes[id] = { kind: 'file', path: p };
|
|
302
|
+
lines.push(` ${id}["${esc(shortName(baseName(p)))}"]${clsFor(p)}`);
|
|
303
|
+
}
|
|
304
|
+
if (shown.length > MAX_PER_LAYER) {
|
|
305
|
+
lines.push(` moreL${i}["+ ${shown.length - MAX_PER_LAYER} more"]:::ghost`);
|
|
306
|
+
}
|
|
307
|
+
lines.push(' end');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const stray = unreachable.filter(include).slice(0, 6);
|
|
311
|
+
if (stray.length) {
|
|
312
|
+
lines.push(` subgraph LU["off the path"]`);
|
|
313
|
+
for (const p of stray) {
|
|
314
|
+
const id = 'n' + n++;
|
|
315
|
+
ids.set(p, id);
|
|
316
|
+
nodes[id] = { kind: 'file', path: p };
|
|
317
|
+
lines.push(` ${id}["${esc(shortName(baseName(p)))}"]:::ghost`);
|
|
318
|
+
}
|
|
319
|
+
if (unreachable.length > stray.length) {
|
|
320
|
+
lines.push(` moreLU["+ ${unreachable.length - stray.length} more"]:::ghost`);
|
|
321
|
+
}
|
|
322
|
+
lines.push(' end');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let edgeCount = 0;
|
|
326
|
+
for (const e of scan.edges) {
|
|
327
|
+
if (edgeCount >= MAX_EDGES) break;
|
|
328
|
+
if (!ids.has(e.from) || !ids.has(e.to)) continue;
|
|
329
|
+
lines.push(` ${ids.get(e.from)} --> ${ids.get(e.to)}`);
|
|
330
|
+
edgeCount++;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
lines.push(...classDefLines());
|
|
334
|
+
return { source: lines.join('\n'), nodes };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- helpers ---------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
function idMapFor(paths) {
|
|
340
|
+
const map = new Map();
|
|
341
|
+
paths.forEach((p, i) => map.set(p, 'n' + i));
|
|
342
|
+
return map;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ghostId(ghosts, path, next) {
|
|
346
|
+
if (!ghosts.has(path)) ghosts.set(path, next());
|
|
347
|
+
return ghosts.get(path);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Mermaid labels break on a handful of characters; keep them out.
|
|
351
|
+
function esc(s) {
|
|
352
|
+
return String(s)
|
|
353
|
+
.replace(/["<>|{}[\]()]/g, '')
|
|
354
|
+
.replace(/&/g, 'and')
|
|
355
|
+
.slice(0, 80);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function shortName(name) {
|
|
359
|
+
return name.length > MAX_LABEL ? name.slice(0, MAX_LABEL - 1) + '…' : name;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The Health view: the riskiest files as a heat map, colored by risk band and
|
|
363
|
+
// wired with their import arrows, so the dangerous neighborhood reads at a
|
|
364
|
+
// glance. Nodes stay clickable into the deep-dive.
|
|
365
|
+
export function healthDiagram(scan, facts, health, opts = {}) {
|
|
366
|
+
const include = opts.include || (() => true);
|
|
367
|
+
const MAX = 26;
|
|
368
|
+
const top = health.perFile.filter((f) => include(f.path)).slice(0, MAX);
|
|
369
|
+
const lines = [themeBlock(), 'flowchart TB', ...classDefLines()];
|
|
370
|
+
const ids = new Map();
|
|
371
|
+
const nodes = {};
|
|
372
|
+
let n = 0;
|
|
373
|
+
const band = (r) => (r >= 70 ? 'r3' : r >= 50 ? 'r2' : r >= 30 ? 'r1' : 'r0');
|
|
374
|
+
|
|
375
|
+
for (const f of top) {
|
|
376
|
+
const id = 'h' + n++;
|
|
377
|
+
ids.set(f.path, id);
|
|
378
|
+
nodes[id] = { kind: 'file', path: f.path };
|
|
379
|
+
lines.push(` ${id}["${esc(shortName(baseName(f.path)))} · ${f.risk}"]:::${band(f.risk)}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const set = new Set(top.map((f) => f.path));
|
|
383
|
+
let drawn = 0;
|
|
384
|
+
for (const e of scan.edges) {
|
|
385
|
+
if (drawn >= 60) break;
|
|
386
|
+
|
|
387
|
+
if (!set.has(e.from) || !set.has(e.to)) continue;
|
|
388
|
+
lines.push(` ${ids.get(e.from)} --> ${ids.get(e.to)}`);
|
|
389
|
+
drawn++;
|
|
390
|
+
}
|
|
391
|
+
if (!top.length) lines.push(' empty["Nothing stands out — this repo is in good shape."]:::note');
|
|
392
|
+
return { source: lines.join('\n'), nodes };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// The Security view: files carrying rule-engine findings, colored by their
|
|
396
|
+
// worst severity (critical red → info green), wired with their import arrows.
|
|
397
|
+
export function securityDiagram(scan, facts, security, opts = {}) {
|
|
398
|
+
const include = opts.include || (() => true);
|
|
399
|
+
const MAX = 26;
|
|
400
|
+
const sevClass = { critical: 'r3', high: 'r2', medium: 'r1', low: 'r0', info: 'r0' };
|
|
401
|
+
const top = security.files.filter((f) => include(f.path)).slice(0, MAX);
|
|
402
|
+
const lines = [themeBlock(), 'flowchart TB', ...classDefLines()];
|
|
403
|
+
const ids = new Map();
|
|
404
|
+
const nodes = {};
|
|
405
|
+
let n = 0;
|
|
406
|
+
for (const f of top) {
|
|
407
|
+
const id = 's' + n++;
|
|
408
|
+
ids.set(f.path, id);
|
|
409
|
+
nodes[id] = { kind: 'file', path: f.path };
|
|
410
|
+
lines.push(` ${id}["${esc(shortName(baseName(f.path)))} · ${f.count}"]:::${sevClass[f.worst]}`);
|
|
411
|
+
}
|
|
412
|
+
const set = new Set(top.map((f) => f.path));
|
|
413
|
+
let drawn = 0;
|
|
414
|
+
for (const e of scan.edges) {
|
|
415
|
+
if (drawn >= 60) break;
|
|
416
|
+
if (!set.has(e.from) || !set.has(e.to)) continue;
|
|
417
|
+
lines.push(` ${ids.get(e.from)} --> ${ids.get(e.to)}`);
|
|
418
|
+
drawn++;
|
|
419
|
+
}
|
|
420
|
+
if (!top.length) lines.push(' clean["No findings — the rule engine found nothing to flag."]:::r0');
|
|
421
|
+
return { source: lines.join('\n'), nodes };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// The History view: the hotspot cross-tab — complexity × churn — as a heat
|
|
425
|
+
// map. A file high here is both hard to change and changed constantly, which
|
|
426
|
+
// is where a newcomer's caution (and a maintainer's tests) belong. Import
|
|
427
|
+
// arrows are drawn between shown files so hot neighborhoods read at a glance.
|
|
428
|
+
// No history (browser pick, tarball, not a checkout) says why instead of
|
|
429
|
+
// drawing an empty chart.
|
|
430
|
+
export function historyDiagram(scan, facts, history, opts = {}) {
|
|
431
|
+
if (!history?.available) {
|
|
432
|
+
return emptyDiagram(history?.reason || 'No history available.');
|
|
433
|
+
}
|
|
434
|
+
const include = opts.include || (() => true);
|
|
435
|
+
const MAX = 26;
|
|
436
|
+
const band = (s) => (s >= 70 ? 'r3' : s >= 50 ? 'r2' : s >= 30 ? 'r1' : 'r0');
|
|
437
|
+
const top = history.perFile.filter((f) => include(f.path)).slice(0, MAX);
|
|
438
|
+
const lines = [themeBlock(), 'flowchart TB', ...classDefLines()];
|
|
439
|
+
const ids = new Map();
|
|
440
|
+
const nodes = {};
|
|
441
|
+
let n = 0;
|
|
442
|
+
for (const f of top) {
|
|
443
|
+
const id = 't' + n++;
|
|
444
|
+
ids.set(f.path, id);
|
|
445
|
+
nodes[id] = { kind: 'file', path: f.path };
|
|
446
|
+
lines.push(` ${id}["${esc(shortName(baseName(f.path)))} · ${f.hotspot}"]:::${band(f.hotspot)}`);
|
|
447
|
+
}
|
|
448
|
+
const set = new Set(top.map((f) => f.path));
|
|
449
|
+
let drawn = 0;
|
|
450
|
+
for (const e of scan.edges) {
|
|
451
|
+
if (drawn >= 60) break;
|
|
452
|
+
if (!set.has(e.from) || !set.has(e.to)) continue;
|
|
453
|
+
lines.push(` ${ids.get(e.from)} --> ${ids.get(e.to)}`);
|
|
454
|
+
drawn++;
|
|
455
|
+
}
|
|
456
|
+
if (!top.length) lines.push(' quiet["No file in this repo has been touched in the recent window."]:::note');
|
|
457
|
+
return { source: lines.join('\n'), nodes };
|
|
458
|
+
}
|
|
459
|
+
|