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,120 @@
1
+ import { blankComments } from './util.js';
2
+
3
+ const DECISIONS = {
4
+ javascript: /\b(if|for|while|case|catch|do|else if)\b/g,
5
+ python: /\b(if|elif|for|while|except)\b/g,
6
+ go: /\b(if|for|case|go|select)\b/g,
7
+ };
8
+ const GENERIC = /\b(if|elif|for|while|case|catch|except|switch)\b/g;
9
+
10
+ export function codeStats(source, lang = '') {
11
+ const lines = String(source).split('\n');
12
+ let blank = 0;
13
+ let comment = 0;
14
+ let code = 0;
15
+ let inBlock = false;
16
+ const isHashComment = ['python', 'ruby', 'yaml', 'toml', 'shell', 'bash', 'sh'].includes(lang)
17
+ || (!lang && (source.includes('#!') || /\b(def|import|class)\b/.test(source)));
18
+
19
+ for (const raw of lines) {
20
+ const l = raw.trim();
21
+ if (!l) { blank++; continue; }
22
+ if (inBlock) { comment++; if (l.includes('*/')) inBlock = false; continue; }
23
+ if (l.startsWith('//') || (isHashComment && l.startsWith('#'))) { comment++; continue; }
24
+ if (l.startsWith('/*')) { comment++; if (!l.includes('*/')) inBlock = true; continue; }
25
+ code++;
26
+ }
27
+ return { lines: lines.length, code, comment, blank };
28
+ }
29
+
30
+ export function complexityOf(source, lang) {
31
+ const isHash = ['python', 'ruby', 'yaml', 'toml', 'shell'].includes(lang);
32
+ const clean = blankComments(String(source), { lineChar: isHash ? '#' : '//' });
33
+ const rx = DECISIONS[lang] || GENERIC;
34
+ let score = 1;
35
+ const decisions = clean.match(rx);
36
+ score += decisions ? decisions.length : 0;
37
+ const bools = clean.match(/&&|\|\||\?/g); // boolean ops + ternaries
38
+ score += bools ? bools.length : 0;
39
+ return score;
40
+ }
41
+
42
+ export function halsteadMetrics(source, lang) {
43
+ const isHash = ['python', 'ruby', 'yaml', 'toml', 'shell'].includes(lang);
44
+ const clean = blankComments(String(source), { lineChar: isHash ? '#' : '//' });
45
+
46
+ const ops = ['\+', '-', '\*', '/', '=', '==', '===', '!=', '!==', '<', '>', '<=', '>=', '&&', '\|\|', '!', '%', '\*\*', '\+\+', '--', '\+=', '-=', '\*=', '/=', '=>', '\?\?', '\?\.', '\.\.\.'];
47
+ const opPattern = new RegExp(ops.map(o => o.replace(/[.*+?^$\\{}()|[\]\\]/g, '\\$&')).join('|'), 'g');
48
+
49
+ const operators = new Set();
50
+ let N1 = 0;
51
+ for (const m of clean.matchAll(opPattern)) {
52
+ operators.add(m[0]);
53
+ N1++;
54
+ }
55
+ const n1 = operators.size;
56
+
57
+ const operands = new Set();
58
+ let N2 = 0;
59
+ for (const m of clean.matchAll(/\b[a-zA-Z_][a-zA-Z0-9_]*\b|\b\d+\b|['"][^'"]*['"]/g)) {
60
+ const val = m[0];
61
+ if (["if", "else", "for", "while", "return", "function", "class", "import", "export", "var", "let", "const", "true", "false", "null"].includes(val)) continue;
62
+ operands.add(val);
63
+ N2++;
64
+ }
65
+ const n2 = operands.size;
66
+
67
+ const vocabulary = n1 + n2;
68
+ const length = N1 + N2;
69
+ const volume = vocabulary === 0 ? 0 : length * Math.log2(vocabulary);
70
+ const difficulty = (n2 === 0 || n1 === 0) ? 0 : (n1 / 2) * (N2 / n2);
71
+ const effort = difficulty * volume;
72
+
73
+ return { n1, n2, N1, N2, vocabulary, length, volume, difficulty, effort };
74
+ }
75
+
76
+ export function cognitiveComplexity(source, lang) {
77
+ const isHash = ['python', 'ruby', 'yaml', 'toml', 'shell'].includes(lang);
78
+ const clean = blankComments(String(source), { lineChar: isHash ? '#' : '//' });
79
+
80
+ let score = 0;
81
+ let nesting = 0;
82
+ let inSwitch = false;
83
+
84
+ const lines = clean.split('\n');
85
+ for (const line of lines) {
86
+ const l = line.trim();
87
+ if (l.includes('{')) {
88
+ if (/\b(if|for|while|do|switch|catch)\b/.test(l)) {
89
+ if (/\bswitch\b/.test(l)) {
90
+ inSwitch = true;
91
+ score += (1 + nesting);
92
+ nesting++;
93
+ } else {
94
+ score += (1 + nesting);
95
+ nesting++;
96
+ }
97
+ } else {
98
+ nesting++;
99
+ }
100
+ }
101
+ if (l.includes('}')) {
102
+ nesting = Math.max(0, nesting - 1);
103
+ }
104
+ if (!l.includes('{') && /\b(if|else if|for|while|do|catch)\b/.test(l)) {
105
+ score += (1 + nesting);
106
+ }
107
+ if (/\belse\b(?!\s+if)/.test(l)) {
108
+ score += 1;
109
+ }
110
+ const ops = l.match(/&&|\|\||\?/g);
111
+ if (ops) score += ops.length;
112
+ }
113
+ return score;
114
+ }
115
+
116
+ export function maintainabilityIndex(halstead, complexity, loc) {
117
+ if (halstead.volume === 0 || loc === 0) return 100;
118
+ let MI = (171 - 5.2 * Math.log(halstead.volume) - 0.23 * complexity - 16.2 * Math.log(loc)) * 100 / 171;
119
+ return Math.max(0, Math.min(100, MI));
120
+ }
@@ -0,0 +1,69 @@
1
+ // Repo-relative path helpers. Everything here is POSIX-style with forward
2
+ // slashes, no leading slash, no trailing slash. Both file source adapters
3
+ // normalize to this convention before the analyzer sees a path.
4
+
5
+ export function normalize(path) {
6
+ const out = [];
7
+ for (const part of String(path).split('/')) {
8
+ if (!part || part === '.') continue;
9
+ if (part === '..') {
10
+ out.pop();
11
+ continue;
12
+ }
13
+ out.push(part);
14
+ }
15
+ return out.join('/');
16
+ }
17
+
18
+ export function joinPath(...parts) {
19
+ return normalize(parts.filter(Boolean).join('/'));
20
+ }
21
+
22
+ export function dirOf(path) {
23
+ const i = path.lastIndexOf('/');
24
+ return i === -1 ? '' : path.slice(0, i);
25
+ }
26
+
27
+ export function baseName(path) {
28
+ const i = path.lastIndexOf('/');
29
+ return path.slice(i + 1);
30
+ }
31
+
32
+ export function extOf(path) {
33
+ const base = baseName(path);
34
+ const i = base.lastIndexOf('.');
35
+ return i <= 0 ? '' : base.slice(i).toLowerCase();
36
+ }
37
+
38
+ export function depthOf(path) {
39
+ if (!path) return 0;
40
+ return path.split('/').length;
41
+ }
42
+
43
+ export function topFolderOf(path) {
44
+ const i = path.indexOf('/');
45
+ return i === -1 ? '(root)' : path.slice(0, i);
46
+ }
47
+
48
+ // Does this path look like a test? Naming, not content — there is no reliable
49
+ // way to tell from a path alone, and every repo names them differently. The
50
+ // alternatives are bounded by the separators: `foo.test.js`, `foo_spec.rb`,
51
+ // `tests/foo.py`, `__tests__/foo.jsx`. A word merely *containing* "test"
52
+ // (`contest.js`, `latest.md`, `protester.go`) must not match, which is what the
53
+ // separator groups on either side are for.
54
+ export const TEST_PATH_RE = /(^|[._\-/])(test|tests|spec|specs|__tests__|__test__)([._\-/]|$)/i;
55
+
56
+ export function isTestPath(path) {
57
+ return TEST_PATH_RE.test(path);
58
+ }
59
+
60
+ // The stage filters, as a predicate over paths — or null when nothing is being
61
+ // filtered at all. Null rather than `() => true` on purpose: every diagram
62
+ // builder takes an `include` option and skips the whole filtering pass when it is
63
+ // absent, so the common case costs nothing. Callers must keep treating a missing
64
+ // filter as "everything", not as "nothing".
65
+ export function pathFilter({ text = '', showTests = true } = {}) {
66
+ const needle = text.trim().toLowerCase();
67
+ if (!needle && showTests) return null;
68
+ return (p) => (showTests || !isTestPath(p)) && (!needle || p.toLowerCase().includes(needle));
69
+ }
@@ -0,0 +1,272 @@
1
+ // Pattern reading: turns the raw import graph into observations about the
2
+ // repo's shape — its depth layers, its load-bearing files, its smells.
3
+ // Everything here is pure functions over scan + facts.
4
+
5
+ // Assigns every reachable file a depth: the length of the longest import
6
+ // chain from an entry point. Files no entry can reach land in `unreachable`.
7
+ // Cycles are survived by capping how often a file may be relaxed.
8
+ export function computeLayers(scan, facts) {
9
+ let seeds = facts.entries.slice();
10
+ if (!seeds.length) {
11
+ // No recognized entry points — start from the tops of the import chains:
12
+ // files that pull others in but are pulled in by nothing.
13
+ seeds = scan.files
14
+ .filter((f) => (facts.fanOut[f.path] || 0) > 0 && (facts.fanIn[f.path] || 0) === 0)
15
+ .map((f) => f.path)
16
+ .slice(0, 5);
17
+ }
18
+ if (!seeds.length) {
19
+ // Last resort: the most depended-upon files.
20
+ seeds = facts.hubs.slice(0, 3).map((h) => h.path);
21
+ }
22
+
23
+ const depth = new Map();
24
+ const visits = new Map();
25
+ const queue = [];
26
+ for (const s of seeds) {
27
+ depth.set(s, 0);
28
+ queue.push(s);
29
+ }
30
+
31
+ while (queue.length) {
32
+ const cur = queue.shift();
33
+ const d = depth.get(cur);
34
+ visits.set(cur, (visits.get(cur) || 0) + 1);
35
+ if (visits.get(cur) > 4) continue; // circular chains give up here
36
+ const next = d + 1;
37
+ if (next > MAX_DEPTH) continue; // and nobody sinks past the basement
38
+ for (const nxt of facts.importsOf[cur] || []) {
39
+ if ((depth.get(nxt) ?? -1) < next) {
40
+ depth.set(nxt, next);
41
+ queue.push(nxt);
42
+ }
43
+ }
44
+ }
45
+
46
+ // Depths can skip values (cycle members sink several floors at once), and
47
+ // for..of over a sparse array hands holes to sort() — compact first.
48
+ const sparse = [];
49
+ for (const [path, d] of depth) {
50
+ (sparse[d] ||= []).push(path);
51
+ }
52
+ const layers = sparse.filter(Boolean);
53
+ for (const l of layers) l.sort((a, b) => a.localeCompare(b));
54
+
55
+ const unreachable = scan.files
56
+ .map((f) => f.path)
57
+ .filter((p) => !depth.has(p))
58
+ .sort();
59
+
60
+ return { layers, unreachable, seeds };
61
+ }
62
+
63
+ const MAX_DEPTH = 12;
64
+
65
+ // Written observations, in the order a senior dev would mention them.
66
+ // Each: { id, tone: 'good' | 'note' | 'warn', severity: 'info' | 'low' | 'medium' | 'high', paths: string[], title, detail }.
67
+ export function detectPatterns(scan, facts, manifest, layersInfo) {
68
+ const out = [];
69
+ const topHub = facts.hubs[0];
70
+
71
+ if (topHub && topHub.fanIn >= 5) {
72
+ out.push({
73
+ id: 'hub-and-spoke',
74
+ tone: 'note',
75
+ severity: 'info',
76
+ paths: [topHub.path],
77
+ title: 'Hub and spoke',
78
+ detail: `\`${topHub.path}\` holds ${topHub.fanIn} dependents — this repo orbits a core. Learn that file and most of the rest makes sense.`,
79
+ });
80
+ }
81
+
82
+ const god = scan.files
83
+ .filter((f) => (facts.fanIn[f.path] || 0) + (facts.fanOut[f.path] || 0) >= 12)
84
+ .sort((a, b) => ((facts.fanIn[b.path] || 0) + (facts.fanOut[b.path] || 0)) - ((facts.fanIn[a.path] || 0) + (facts.fanOut[a.path] || 0)));
85
+
86
+ if (god.length) {
87
+ out.push({
88
+ id: 'god-files',
89
+ tone: 'warn',
90
+ severity: 'medium',
91
+ paths: god.map((f) => f.path),
92
+ title: god.length > 1 ? 'God files' : 'God file',
93
+ detail: `${god.slice(0, 3).map((f) => `\`${f.path}\``).join(', ')} — heavily connected in both directions. Everyone's problem, nobody's owner. Touch gently.`,
94
+ });
95
+ }
96
+
97
+ if (layersInfo && layersInfo.layers.length >= 3) {
98
+ out.push({
99
+ id: 'layered-shape',
100
+ tone: 'good',
101
+ severity: 'info',
102
+ paths: [],
103
+ title: 'A layered shape',
104
+ detail: `The code reads in ${layersInfo.layers.length} strata from the door to the deepest leaf. That is an architecture someone can draw — always a good sign.`,
105
+ });
106
+ if (layersInfo.unreachable.length) {
107
+ out.push({
108
+ id: 'unreachable',
109
+ tone: 'note',
110
+ severity: 'info',
111
+ paths: layersInfo.unreachable,
112
+ title: 'Off the beaten path',
113
+ detail: `${layersInfo.unreachable.length} files can't be reached from any entry point — utilities awaiting adoption, or furniture from an older layout.`,
114
+ });
115
+ }
116
+ }
117
+
118
+ if (facts.cycles.length) {
119
+ out.push({
120
+ id: 'cycles',
121
+ tone: 'warn',
122
+ severity: 'high',
123
+ paths: Array.from(new Set(facts.cycles.flat())),
124
+ title: 'Circular dependencies',
125
+ detail: `${facts.cycles.length} ${facts.cycles.length === 1 ? 'loop' : 'loops'} where files import each other; the biggest tangles ${facts.cycles[0].length} files. Untangle before a big refactor.`,
126
+ });
127
+ }
128
+
129
+ const barrels = scan.files.filter(
130
+ (f) =>
131
+ /^index\.[jt]sx?$/.test(f.name) &&
132
+ f.functions.length === 0 &&
133
+ (facts.fanIn[f.path] || 0) >= 2 &&
134
+ (facts.fanOut[f.path] || 0) >= 2
135
+ );
136
+ if (barrels.length) {
137
+ out.push({
138
+ id: 'barrels',
139
+ tone: 'note',
140
+ severity: 'info',
141
+ paths: barrels.map((f) => f.path),
142
+ title: 'Barrel files',
143
+ detail: `${barrels.slice(0, 3).map((f) => `\`${f.path}\``).join(', ')} mostly re-export other modules. Convenient for imports, one extra hop when you're hunting a definition.`,
144
+ });
145
+ }
146
+
147
+ const testCount = scan.files.filter((f) => /(^|[._-])(test|spec|tests|__tests__)([._-]|\/|$)/i.test(f.path)).length;
148
+ if (testCount > 0) {
149
+ const pct = Math.round((testCount / Math.max(1, scan.stats.filesParsed)) * 100);
150
+ out.push({
151
+ id: 'test-shadow',
152
+ tone: 'good',
153
+ severity: 'info',
154
+ paths: [],
155
+ title: 'A test shadow',
156
+ detail: `${testCount} test ${testCount === 1 ? 'file' : 'files'} against ${scan.stats.filesParsed} code files (~${pct}%). Read tests to learn intended behavior, code to learn actual behavior.`,
157
+ });
158
+ } else {
159
+ out.push({
160
+ id: 'no-tests',
161
+ tone: 'warn',
162
+ severity: 'medium',
163
+ paths: [],
164
+ title: 'No tests in sight',
165
+ detail: 'Nothing matches the usual test/spec naming. Onboarding risk: intended behavior lives only inside the code.',
166
+ });
167
+ }
168
+
169
+ if (facts.untestedHubs?.length) {
170
+ out.push({
171
+ id: 'untested-hubs',
172
+ tone: 'warn',
173
+ severity: 'high',
174
+ paths: facts.untestedHubs.map((h) => h.path),
175
+ title: 'Untested hubs',
176
+ detail: `${facts.untestedHubs.length} load-bearing ${facts.untestedHubs.length === 1 ? 'file is' : 'files are'} not reached by any test. Changing them has wide blast radius with no test safety net.`,
177
+ });
178
+ }
179
+
180
+ if (facts.deadExports?.length) {
181
+ const deadFiles = Array.from(new Set(facts.deadExports.map((d) => d.file)));
182
+ out.push({
183
+ id: 'dead-exports',
184
+ tone: 'note',
185
+ severity: 'low',
186
+ paths: deadFiles,
187
+ title: 'Unused exports',
188
+ detail: `${facts.deadExports.length} exported ${facts.deadExports.length === 1 ? 'symbol is' : 'symbols are'} never imported anywhere in the project. Potential dead code or unexposed public API.`,
189
+ });
190
+ }
191
+
192
+ if (facts.depsDrift?.undeclaredImported?.length) {
193
+ out.push({
194
+ id: 'undeclared-deps',
195
+ tone: 'warn',
196
+ severity: 'high',
197
+ paths: [],
198
+ title: 'Undeclared dependencies',
199
+ detail: `Imported packages not listed in package manifest: ${facts.depsDrift.undeclaredImported.slice(0, 4).map((d) => `\`${d}\``).join(', ')}. Risk of build failure in CI/clean environments.`,
200
+ });
201
+ }
202
+
203
+ if (facts.depsDrift?.unusedDeclared?.length) {
204
+ out.push({
205
+ id: 'unused-deps',
206
+ tone: 'note',
207
+ severity: 'low',
208
+ paths: [],
209
+ title: 'Unused dependencies',
210
+ detail: `Declared dependencies not directly imported in source: ${facts.depsDrift.unusedDeclared.slice(0, 5).map((d) => `\`${d}\``).join(', ')}.`,
211
+ });
212
+ }
213
+
214
+ if (facts.orphans.length) {
215
+ out.push({
216
+ id: 'orphans',
217
+ tone: 'note',
218
+ severity: 'info',
219
+ paths: facts.orphans,
220
+ title: 'Unclaimed territory',
221
+ detail: `${facts.orphans.length} ${facts.orphans.length === 1 ? 'file is' : 'files are'} imported by nothing. Dead code, or doors the naming rules didn't recognize.`,
222
+ });
223
+ }
224
+
225
+ const topExt = scan.externals[0];
226
+ if (topExt && topExt.usedBy.length >= 3) {
227
+ out.push({
228
+ id: 'top-external',
229
+ tone: 'note',
230
+ severity: 'info',
231
+ paths: topExt.usedBy,
232
+ title: 'Heaviest outside anchor',
233
+ detail: `\`${topExt.name}\` is pulled in by ${topExt.usedBy.length} files — the external dependency this repo would miss most.`,
234
+ });
235
+ }
236
+
237
+ if (manifest?.services?.length) {
238
+ out.push({
239
+ id: 'services',
240
+ tone: 'note',
241
+ severity: 'info',
242
+ paths: [],
243
+ title: 'Runs as services',
244
+ detail: `${manifest.services.length} ${manifest.services.length === 1 ? 'service' : 'services'} declared: ${manifest.services.map((s) => s.name).join(', ')}.`,
245
+ });
246
+ }
247
+
248
+ return out;
249
+ }
250
+
251
+ // Folder-to-folder traffic as a matrix for the heat grid. Folders are the
252
+ // top-level ones, ordered by how much traffic they see in total.
253
+ export function couplingMatrix(scan, facts, maxFolders = 8) {
254
+ const traffic = new Map();
255
+ for (const fe of facts.folderEdges) {
256
+ traffic.set(fe.from, (traffic.get(fe.from) || 0) + fe.count);
257
+ traffic.set(fe.to, (traffic.get(fe.to) || 0) + fe.count);
258
+ }
259
+ const folders = [...traffic.entries()]
260
+ .sort((a, b) => b[1] - a[1])
261
+ .slice(0, maxFolders)
262
+ .map(([name]) => name);
263
+
264
+ const counts = new Map();
265
+ let max = 1;
266
+ for (const fe of facts.folderEdges) {
267
+ if (!folders.includes(fe.from) || !folders.includes(fe.to)) continue;
268
+ counts.set(fe.from + '->' + fe.to, fe.count);
269
+ max = Math.max(max, fe.count);
270
+ }
271
+ return { folders, counts, max };
272
+ }