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,135 @@
|
|
|
1
|
+
// Documentation written from the import graph alone.
|
|
2
|
+
//
|
|
3
|
+
// Two halves live here. The first parses the model's answer back into rows. The
|
|
4
|
+
// second writes the documentation *without* a model — plain sentences about what
|
|
5
|
+
// a file is and what leans on it. That half is the offline floor: it is what the
|
|
6
|
+
// docs page shows before anyone presses "Generate", and what it keeps showing if
|
|
7
|
+
// no endpoint is ever configured. It used to sit in app.js, where it could not
|
|
8
|
+
// be tested; it is pure text-from-facts, so it belongs next to `explainLocal`.
|
|
9
|
+
|
|
10
|
+
import { factIndex, scanIndex } from './graph.js';
|
|
11
|
+
|
|
12
|
+
// Parses the model's folder-brief answer, which we ask to be shaped like:
|
|
13
|
+
//
|
|
14
|
+
// FOLDER: two plain sentences about what lives here and why.
|
|
15
|
+
// FILE: scan.js: one sentence about this file.
|
|
16
|
+
// FILE: graph.js: one sentence about this file.
|
|
17
|
+
//
|
|
18
|
+
// Deliberately forgiving: missing markers fall back to taking the first
|
|
19
|
+
// couple of non-empty lines as the folder brief.
|
|
20
|
+
export function parseFolderDoc(text) {
|
|
21
|
+
const out = { folderBrief: '', files: new Map() };
|
|
22
|
+
if (!text) return out;
|
|
23
|
+
|
|
24
|
+
const fallback = [];
|
|
25
|
+
for (const raw of text.split('\n')) {
|
|
26
|
+
const line = raw.trim();
|
|
27
|
+
if (!line) continue;
|
|
28
|
+
|
|
29
|
+
const folderMatch = line.match(/^FOLDER:\s*(.+)$/i);
|
|
30
|
+
if (folderMatch) {
|
|
31
|
+
out.folderBrief = (out.folderBrief ? out.folderBrief + ' ' : '') + folderMatch[1].trim();
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const fileMatch = line.match(/^FILE:\s*([^:]+?):\s+(.+)$/i);
|
|
35
|
+
if (fileMatch) {
|
|
36
|
+
out.files.set(fileMatch[1].trim(), fileMatch[2].trim());
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
fallback.push(line);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!out.folderBrief && fallback.length) {
|
|
43
|
+
out.folderBrief = fallback.slice(0, 2).join(' ');
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- documentation from the graph, no model involved -----------------------
|
|
49
|
+
|
|
50
|
+
// The one-line facts strip under a file's name in the generated docs.
|
|
51
|
+
export function fileFactsLine(path, scan, facts) {
|
|
52
|
+
const f = scanIndex(scan).fileAt(path);
|
|
53
|
+
if (!f) return 'not parsed';
|
|
54
|
+
const fx = factIndex(facts);
|
|
55
|
+
const fin = facts.fanIn[path] || 0;
|
|
56
|
+
const fout = facts.fanOut[path] || 0;
|
|
57
|
+
const bits = [];
|
|
58
|
+
if (fx.entries.has(path)) bits.push('entry');
|
|
59
|
+
if (fin >= 5) bits.push('hub');
|
|
60
|
+
if (fx.inCycle.has(path)) bits.push('in a cycle');
|
|
61
|
+
bits.push(`${fin} dependents · pulls in ${fout}`);
|
|
62
|
+
if (f.functions.length) bits.push(`${f.functions.length} functions`);
|
|
63
|
+
return bits.join(' · ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A paragraph about one file. The branches are ordered by how much they tell
|
|
67
|
+
// you: being an entry point is the most useful thing to say about a file, being
|
|
68
|
+
// load-bearing is next, and "pulls in N, answers to M" is the fallback that
|
|
69
|
+
// always holds.
|
|
70
|
+
export function fileStaticDoc(path, scan, facts) {
|
|
71
|
+
const f = scanIndex(scan).fileAt(path);
|
|
72
|
+
if (!f) return 'Not parsed — kept as an asset or data file.';
|
|
73
|
+
const fin = facts.fanIn[path] || 0;
|
|
74
|
+
const fout = facts.fanOut[path] || 0;
|
|
75
|
+
const importers = (facts.importers[path] || []).map((p) => p.split('/').pop());
|
|
76
|
+
let s;
|
|
77
|
+
if (factIndex(facts).entries.has(path)) {
|
|
78
|
+
s = `${f.name} is one of the ways into the codebase — it pulls in ${fout} other ${fout === 1 ? 'file' : 'files'} and answers to nothing above it.`;
|
|
79
|
+
} else if (fin >= 5) {
|
|
80
|
+
s = `${f.name} is load-bearing: ${fin} files import it directly${importers.length ? `, ${importers.slice(0, 2).join(', ')} among them` : ''}.`;
|
|
81
|
+
} else if (!fin && !fout) {
|
|
82
|
+
s = `${f.name} stands alone — nothing imports it and it imports nothing we can see.`;
|
|
83
|
+
} else if (!fout) {
|
|
84
|
+
s = `${f.name} sits at the end of a chain — it imports nothing and is leaned on by ${fin} ${fin === 1 ? 'file' : 'files'}.`;
|
|
85
|
+
} else if (!fin) {
|
|
86
|
+
s = `${f.name} pulls in ${fout} ${fout === 1 ? 'file' : 'files'} but nothing imports it — a door we didn't recognize, or dead code.`;
|
|
87
|
+
} else {
|
|
88
|
+
s = `${f.name} pulls in ${fout} and answers to ${fin}.`;
|
|
89
|
+
}
|
|
90
|
+
if (f.functions.length) {
|
|
91
|
+
s += ` It defines ${f.functions.length} function${f.functions.length === 1 ? '' : 's'} — ${f.functions.slice(0, 4).map((fn) => fn.name).join(', ')}${f.functions.length > 4 ? `, and ${f.functions.length - 4} more` : ''}.`;
|
|
92
|
+
}
|
|
93
|
+
if (f.exports.length) {
|
|
94
|
+
s += ` Exports: ${f.exports.slice(0, 4).map((e) => e.name).join(', ')}${f.exports.length > 4 ? ', …' : ''}.`;
|
|
95
|
+
}
|
|
96
|
+
return s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A paragraph about one folder. `subfolderCount` is passed rather than read off
|
|
100
|
+
// a tree node, so nothing in the shared layer needs to know the view's tree
|
|
101
|
+
// shape.
|
|
102
|
+
export function folderStaticDoc(folder, scan, facts, subfolderCount = 0) {
|
|
103
|
+
const here = scanIndex(scan).filesIn(folder);
|
|
104
|
+
let s = `${here.length} parsed ${here.length === 1 ? 'file lives' : 'files live'} here`;
|
|
105
|
+
if (subfolderCount) s += `, with ${subfolderCount} subfolder${subfolderCount === 1 ? '' : 's'} inside`;
|
|
106
|
+
s += '.';
|
|
107
|
+
const heaviest = here
|
|
108
|
+
.map((f) => ({ name: f.name, fanIn: facts.fanIn[f.path] || 0 }))
|
|
109
|
+
.sort((a, b) => b.fanIn - a.fanIn)[0];
|
|
110
|
+
if (heaviest && heaviest.fanIn >= 3) {
|
|
111
|
+
s += ` The one everything leans on is ${heaviest.name}, with ${heaviest.fanIn} dependents.`;
|
|
112
|
+
}
|
|
113
|
+
const entries = factIndex(facts).entries;
|
|
114
|
+
const entriesHere = here.filter((f) => entries.has(f.path));
|
|
115
|
+
if (entriesHere.length) s += ` The way in: ${entriesHere.map((f) => f.name).join(', ')}.`;
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The facts about one file as the prompts want them. Two identical copies of
|
|
120
|
+
// this used to sit in app.js — one for a single doc tab, one for the
|
|
121
|
+
// folder-by-folder pass — and they had to agree for the two pages to describe
|
|
122
|
+
// the same file the same way.
|
|
123
|
+
export function docFileRow(path, scan, facts) {
|
|
124
|
+
const f = scanIndex(scan).fileAt(path);
|
|
125
|
+
const fanIn = facts.fanIn[path] || 0;
|
|
126
|
+
return {
|
|
127
|
+
path,
|
|
128
|
+
name: path.split('/').pop(),
|
|
129
|
+
parsed: Boolean(f),
|
|
130
|
+
fanIn,
|
|
131
|
+
fanOut: facts.fanOut[path] || 0,
|
|
132
|
+
role: factIndex(facts).entries.has(path) ? 'entry' : fanIn >= 5 ? 'hub' : 'module',
|
|
133
|
+
functions: f ? f.functions.map((fn) => fn.name) : [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Offline explanations. When no LLM key is configured these are what the
|
|
2
|
+
// user reads, so they're written like a colleague's notes, not a template
|
|
3
|
+
// dump. Every claim comes straight from the static graph.
|
|
4
|
+
|
|
5
|
+
import { baseName } from './pathUtil.js';
|
|
6
|
+
import { roleOf, factIndex, scanIndex } from './graph.js';
|
|
7
|
+
import { languageLabel } from './languages/index.js';
|
|
8
|
+
|
|
9
|
+
const ROLE_LINES = {
|
|
10
|
+
entry: 'This is one of the ways into the codebase — a good place to start reading.',
|
|
11
|
+
hub: 'A lot of the codebase leans on this file. Changes here ripple outward.',
|
|
12
|
+
leaf: 'Nothing this file uses lives in the repo — it sits at the end of a chain.',
|
|
13
|
+
test: 'Test file. It verifies behavior rather than providing it.',
|
|
14
|
+
config: 'Configuration — it shapes how the rest of the code runs.',
|
|
15
|
+
module: 'A working part of the whole.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function explainFile(path, file, facts) {
|
|
19
|
+
const role = roleOf(path, facts);
|
|
20
|
+
const fin = facts.fanIn[path] || 0;
|
|
21
|
+
const fout = facts.fanOut[path] || 0;
|
|
22
|
+
const paras = [];
|
|
23
|
+
|
|
24
|
+
paras.push(`**${baseName(path)}** — ${ROLE_LINES[role]}`);
|
|
25
|
+
|
|
26
|
+
const parts = [];
|
|
27
|
+
if (fout > 0) parts.push(`pulls in ${fout} other ${fout === 1 ? 'file' : 'files'}`);
|
|
28
|
+
if (fin > 0) parts.push(`is pulled in by ${fin}`);
|
|
29
|
+
if (fin === 0 && fout === 0 && role !== 'entry') parts.push('isn’t connected to anything else we can see');
|
|
30
|
+
if (parts.length) paras.push(`It ${parts.join(' and ')}.`);
|
|
31
|
+
|
|
32
|
+
const fns = (file?.functions || []).filter((f) => f.kind !== 'method');
|
|
33
|
+
const methods = (file?.functions || []).filter((f) => f.kind === 'method');
|
|
34
|
+
if (fns.length) {
|
|
35
|
+
paras.push(
|
|
36
|
+
`Defines ${fns.length} ${fns.length === 1 ? 'function' : 'functions'} — ` +
|
|
37
|
+
`${fns.slice(0, 6).map((f) => '`' + f.name + '`').join(', ')}` +
|
|
38
|
+
(fns.length > 6 ? ` and ${fns.length - 6} more` : '') + '.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (methods.length) {
|
|
42
|
+
paras.push(`Plus ${methods.length} ${methods.length === 1 ? 'method' : 'methods'} on its classes.`);
|
|
43
|
+
}
|
|
44
|
+
if (file?.classes?.length) {
|
|
45
|
+
paras.push(`Classes: ${file.classes.map((c) => '`' + c.name + '`').join(', ')}.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const importers = (facts.importers[path] || []).slice(0, 5);
|
|
49
|
+
if (importers.length) {
|
|
50
|
+
paras.push(
|
|
51
|
+
`If it broke, the first to notice would be ${importers.map((p) => '`' + baseName(p) + '`').join(', ')}` +
|
|
52
|
+
(fin > 5 ? ` and ${fin - 5} others` : '') + '.'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (factIndex(facts).inCycle.has(path)) {
|
|
57
|
+
const cycle = facts.cycles.find((c) => c.includes(path));
|
|
58
|
+
if (cycle) {
|
|
59
|
+
paras.push(
|
|
60
|
+
`⚠ It sits in a circular dependency (${cycle.length} files importing each other in a loop). Worth knowing before you refactor.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return paras.join('\n\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function explainFolder(path, scan, facts) {
|
|
69
|
+
const here = scanIndex(scan).filesIn(path);
|
|
70
|
+
const entries = factIndex(facts).entries;
|
|
71
|
+
const entriesHere = here.filter((f) => entries.has(f.path));
|
|
72
|
+
const hubsHere = here
|
|
73
|
+
.map((f) => ({ path: f.path, fanIn: facts.fanIn[f.path] || 0 }))
|
|
74
|
+
.filter((h) => h.fanIn >= 3)
|
|
75
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
76
|
+
|
|
77
|
+
const paras = [];
|
|
78
|
+
paras.push(`**${path}/** — ${here.length} parsed ${here.length === 1 ? 'file' : 'files'}.`);
|
|
79
|
+
if (entriesHere.length) {
|
|
80
|
+
paras.push(`The way in: ${entriesHere.map((f) => '`' + f.name + '`').join(', ')}.`);
|
|
81
|
+
}
|
|
82
|
+
if (hubsHere.length) {
|
|
83
|
+
paras.push(
|
|
84
|
+
`The load-bearing file${hubsHere.length > 1 ? 's' : ''}: ` +
|
|
85
|
+
hubsHere.slice(0, 3).map((h) => `\`${baseName(h.path)}\` (${h.fanIn} dependents)`).join(', ') + '.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const internal = scan.edges.filter((e) => e.from.startsWith(path + '/') && e.to.startsWith(path + '/')).length;
|
|
89
|
+
const outward = scan.edges.filter((e) => e.from.startsWith(path + '/') && !e.to.startsWith(path + '/')).length;
|
|
90
|
+
if (internal || outward) {
|
|
91
|
+
paras.push(`${internal} internal connections, ${outward} reaching out to other folders.`);
|
|
92
|
+
}
|
|
93
|
+
return paras.join('\n\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function explainOverview(scan, facts, manifest) {
|
|
97
|
+
const paras = [];
|
|
98
|
+
const langs = Object.entries(scan.stats.languages)
|
|
99
|
+
.sort((a, b) => b[1] - a[1])
|
|
100
|
+
.map(([k, v]) => `${v} ${langLabel(k)}`)
|
|
101
|
+
.join(', ');
|
|
102
|
+
paras.push(
|
|
103
|
+
`**${scan.name}** — ${scan.stats.filesParsed} code files (${langs}), ${scan.stats.edgeCount} import connections.`
|
|
104
|
+
);
|
|
105
|
+
if (facts.entries.length) {
|
|
106
|
+
paras.push(`Start reading at ${facts.entries.slice(0, 3).map((p) => '`' + p + '`').join(', ')}.`);
|
|
107
|
+
}
|
|
108
|
+
if (facts.hubs.length) {
|
|
109
|
+
const top = facts.hubs[0];
|
|
110
|
+
paras.push(`The file everyone depends on is \`${top.path}\` (${top.fanIn} dependents).`);
|
|
111
|
+
}
|
|
112
|
+
if (manifest?.services?.length) {
|
|
113
|
+
paras.push(`Runs as ${manifest.services.length} ${manifest.services.length === 1 ? 'service' : 'services'}: ${manifest.services.map((s) => s.name).join(', ')}.`);
|
|
114
|
+
}
|
|
115
|
+
if (facts.cycles.length) {
|
|
116
|
+
paras.push(`⚠ ${facts.cycles.length} circular ${facts.cycles.length === 1 ? 'dependency' : 'dependencies'} detected.`);
|
|
117
|
+
}
|
|
118
|
+
if (facts.orphans.length) {
|
|
119
|
+
paras.push(`${facts.orphans.length} files aren’t imported by anything — dead code, or entry points we didn’t recognize.`);
|
|
120
|
+
}
|
|
121
|
+
paras.push(...scanCaveats(scan));
|
|
122
|
+
return paras.join('\n\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// What the scan knows it doesn't know. These go last, in the same panel as the
|
|
126
|
+
// findings, because a number you can't trust is worse than no number — and
|
|
127
|
+
// every one of these changes how the rest of the page should be read.
|
|
128
|
+
export function scanCaveats(scan) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const t = scan.stats.truncated;
|
|
131
|
+
if (t) {
|
|
132
|
+
out.push(
|
|
133
|
+
`⚠ This is a partial scan. It stopped at ${t.atFiles} files with ${t.dirsQueued} ` +
|
|
134
|
+
`${t.dirsQueued === 1 ? 'directory' : 'directories'} still unvisited, so hubs look smaller than they are ` +
|
|
135
|
+
`and the unconnected-files count is inflated. Treat the health grade as indicative, not final.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const imp = scan.stats.imports;
|
|
139
|
+
if (imp && imp.total) {
|
|
140
|
+
if (imp.confidence < 90) {
|
|
141
|
+
const worst = imp.worst.slice(0, 3).map((w) => '`' + w.spec + '`').join(', ');
|
|
142
|
+
out.push(
|
|
143
|
+
`${imp.confidence}% of the ${imp.total} imports found were placed — ${imp.unresolved} ` +
|
|
144
|
+
`couldn’t be matched to a file or a package${worst ? ', most often ' + worst : ''}. ` +
|
|
145
|
+
`Imports are read with regexes, so unresolved usually means a path alias, a generated ` +
|
|
146
|
+
`file, or code quoted inside a string.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const s = scan.stats.skips;
|
|
151
|
+
if (s?.readFailed || s?.analyzeFailed || s?.listFailed) {
|
|
152
|
+
const bits = [];
|
|
153
|
+
if (s.listFailed) bits.push(`${s.listFailed} ${s.listFailed === 1 ? 'directory' : 'directories'} wouldn’t list`);
|
|
154
|
+
if (s.readFailed) bits.push(`${s.readFailed} ${s.readFailed === 1 ? 'file' : 'files'} wouldn’t read`);
|
|
155
|
+
if (s.analyzeFailed) bits.push(`${s.analyzeFailed} failed to parse`);
|
|
156
|
+
out.push(`Not everything could be read: ${bits.join(', ')}. Those files are missing from the graph.`);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function langLabel(id) {
|
|
162
|
+
return languageLabel(id, { short: true });
|
|
163
|
+
}
|