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,145 @@
1
+ // The fact sheets the AI draws from.
2
+ //
3
+ // Every "AI draft" of a diagram is a prompt plus a list of plain-English facts
4
+ // pulled out of the graph. Keeping the sheets here, apart from the streaming and
5
+ // the button states, has two payoffs: the wording is what the model actually
6
+ // sees and can be read without a browser, and the fiddly question of *which*
7
+ // sheet a given view wants — an opened atlas card borrows another view's — is a
8
+ // pure function with a test rather than a temporary mutation of app state.
9
+ //
10
+ // No DOM, no state import, no network: importable in Node, like everything
11
+ // under shared/.
12
+
13
+ import { topFolderOf } from '../analyzer/pathUtil.js';
14
+ import { scanIndex } from '../analyzer/graph.js';
15
+
16
+ // How much source text a file sheet may carry. Enough for the model to see the
17
+ // shape of a normal module; short enough to leave room for its answer.
18
+ const SOURCE_BUDGET = 9000;
19
+
20
+ // The repo-relative subject of the current view, and which sheet it wants.
21
+ // `view` here is the sheet's name, not the app's: an atlas card mirrors another
22
+ // view, and the files/deep-dive distinction becomes 'file' or 'folder'.
23
+ export function diagramTarget(s) {
24
+ if (s.view === 'atlas' && s.atlasOpen) {
25
+ const it = s.atlasOpen;
26
+ // A card shows exactly what its own view would show, so it asks for the
27
+ // same sheet. This used to be done by assigning to state.view, recursing,
28
+ // and putting the old values back in a `finally`.
29
+ if (it.kind === 'file') return { view: 'file', path: it.path };
30
+ if (it.kind === 'folder') return { view: 'folder', path: it.path };
31
+ if (it.kind === 'layers') return { view: 'patterns', path: '' };
32
+ if (it.kind === 'overview') return { view: 'map', path: '' };
33
+ return { view: it.kind, path: '' };
34
+ }
35
+ if (s.view === 'tour') return { view: 'tour', path: s.tour.stops[s.tour.idx]?.path || '' };
36
+ if (s.view === 'map' || s.view === 'patterns' || s.view === 'services') {
37
+ return { view: s.view, path: '' };
38
+ }
39
+ // Everything else is the files view or one of its neighbours: a file if one
40
+ // is open in the deep-dive, otherwise the folder being shown.
41
+ return s.detail ? { view: 'file', path: s.detail } : { view: 'folder', path: s.folder };
42
+ }
43
+
44
+ // The third-party packages one file uses, by name.
45
+ export function externalsFor(scan, path) {
46
+ return (scan.externals || []).filter((x) => x.usedBy.includes(path)).map((x) => x.name);
47
+ }
48
+
49
+ export function overviewFacts(scan, facts) {
50
+ const counts = new Map();
51
+ for (const f of scan.files) {
52
+ const top = topFolderOf(f.path);
53
+ counts.set(top, (counts.get(top) || 0) + 1);
54
+ }
55
+ return {
56
+ kind: 'overview map',
57
+ facts: [
58
+ 'Top folders (parsed file count): ' + [...counts.entries()].map(([k, v]) => `${k} (${v})`).join(', '),
59
+ 'Dependencies between folders (from -> to: edge count): ' + (facts.folderEdges.map((e) => `${e.from} -> ${e.to}: ${e.count}`).join('; ') || 'none'),
60
+ 'Entry points: ' + (facts.entries.join(', ') || 'none found'),
61
+ 'Most depended-on files (fan-in): ' + (facts.hubs.slice(0, 6).map((h) => `${h.path} (${h.fanIn})`).join(', ') || 'none'),
62
+ ],
63
+ };
64
+ }
65
+
66
+ export function layersFacts(facts, patterns) {
67
+ const L = patterns.layersInfo;
68
+ return {
69
+ kind: 'architecture layers diagram',
70
+ facts: [
71
+ 'Files grouped by import depth (longest chain from an entry point):',
72
+ ...L.layers.map((l, i) => ` depth ${i}: ${l.slice(0, 14).join(', ')}${l.length > 14 ? ` …+${l.length - 14} more` : ''}`),
73
+ 'Not reachable from any entry: ' + (L.unreachable.slice(0, 10).join(', ') || 'none'),
74
+ 'Circular dependency loops: ' + (facts.cycles.map((c) => c.slice(0, 6).join(' <-> ')).join('; ') || 'none'),
75
+ 'Hubs (fan-in): ' + (facts.hubs.slice(0, 6).map((h) => `${h.path} (${h.fanIn})`).join(', ') || 'none'),
76
+ ],
77
+ };
78
+ }
79
+
80
+ export function servicesFacts(scan, manifest) {
81
+ return {
82
+ kind: 'services diagram',
83
+ facts: [
84
+ 'Services (name | image | build | ports | command): ' +
85
+ ((manifest.services || []).map((s) => [s.name, s.image, s.build, (s.ports || []).join('+'), s.command].filter(Boolean).join(' | ')).join('; ') || 'none detected'),
86
+ 'Top folders: ' + scan.folders.filter((f) => f.depth === 1).slice(0, 8).map((f) => f.path).join(', '),
87
+ ],
88
+ };
89
+ }
90
+
91
+ // The deep-dive sheet: one file, its neighbours, and its source if we have it.
92
+ export function fileFacts({ ctx, externals }) {
93
+ return {
94
+ kind: 'file deep-dive flowchart',
95
+ facts: [
96
+ `File: ${ctx.path} (role: ${ctx.role})`,
97
+ `Imported by ${ctx.fanIn} files: ${ctx.importers.join(', ') || 'none'}`,
98
+ `Imports ${ctx.fanOut} files: ${ctx.imports.join(', ') || 'none'}`,
99
+ `External packages: ${externals.join(', ') || 'none'}`,
100
+ ctx.functions?.length ? `Functions: ${ctx.functions.join(', ')}` : '',
101
+ ctx.source ? `Source, possibly truncated:\n${ctx.source.slice(0, SOURCE_BUDGET)}` : '(source unavailable)',
102
+ ].filter(Boolean),
103
+ };
104
+ }
105
+
106
+ // The same file, but framed as a stop on the newcomer's tour: the model is told
107
+ // where it is in the sequence and why the guide stopped, which is what turns a
108
+ // dependency picture into an explanation.
109
+ export function tourStopFacts({ ctx, externals, stop, index, total }) {
110
+ if (!stop) return { kind: 'tour stop diagram', facts: ['No stop is selected.'] };
111
+ return {
112
+ kind: 'guided-tour stop diagram',
113
+ facts: [
114
+ `This is stop ${index + 1} of ${total} on a newcomer's guided tour of the repo.`,
115
+ `Why the guide stops here: ${stop.why}`,
116
+ `File: ${ctx.path} (role: ${ctx.role})`,
117
+ `Imported by ${ctx.fanIn} files: ${ctx.importers.join(', ') || 'none'}`,
118
+ `Imports ${ctx.fanOut} files: ${ctx.imports.join(', ') || 'none'}`,
119
+ `External packages: ${externals.join(', ') || 'none'}`,
120
+ ctx.functions?.length ? `Functions: ${ctx.functions.join(', ')}` : '',
121
+ ctx.source ? `Source, possibly truncated:\n${ctx.source.slice(0, SOURCE_BUDGET)}` : '(source unavailable)',
122
+ 'Draw what a guide would point at: how this file connects to the repo around it, and the flow between its main parts.',
123
+ ].filter(Boolean),
124
+ };
125
+ }
126
+
127
+ export function folderFacts(scan, facts, folder) {
128
+ const here = scanIndex(scan).filesIn(folder);
129
+ const localSet = new Set(here.map((f) => f.path));
130
+ const internal = scan.edges.filter((e) => localSet.has(e.from) && localSet.has(e.to));
131
+ const outward = scan.edges.filter((e) => localSet.has(e.from) && !localSet.has(e.to));
132
+ const inward = scan.edges.filter((e) => !localSet.has(e.from) && localSet.has(e.to));
133
+ return {
134
+ kind: 'folder import graph',
135
+ facts: [
136
+ `Folder: ${folder || '(repo root)'} — ${here.length} parsed files`,
137
+ 'Files (fan-in/fan-out): ' + here.map((f) => `${f.path} (${facts.fanIn[f.path] || 0}/${facts.fanOut[f.path] || 0})`).join(', '),
138
+ 'Edges inside the folder: ' + (internal.map((e) => `${e.from} -> ${e.to}`).join('; ') || 'none'),
139
+ // Capped: a folder that everything imports would otherwise fill the
140
+ // prompt with edges and crowd out the rest of the sheet.
141
+ 'Reaching out of it: ' + (outward.slice(0, 12).map((e) => `${e.from} -> ${e.to}`).join('; ') || 'none'),
142
+ 'Pulled in from outside: ' + (inward.slice(0, 12).map((e) => `${e.from} -> ${e.to}`).join('; ') || 'none'),
143
+ ],
144
+ };
145
+ }
@@ -0,0 +1,59 @@
1
+ // Turns an LLM's raw answer into trustworthy Mermaid source.
2
+ // Models wrap, apologize, and explain — this strips all of that back to the
3
+ // diagram itself plus the one-line caption we ask for.
4
+
5
+ // Pulls `{ caption, body }` out of `text`; returns null when there is no
6
+ // flowchart to be found. `body` starts at the `flowchart`/`graph` line and
7
+ // has trailing prose trimmed away.
8
+ export function extractAIDiagram(text) {
9
+ if (!text || typeof text !== 'string') return null;
10
+
11
+ // Fences gone first — ```mermaid and plain ``` alike.
12
+ const unfenced = text.replace(/```(?:mermaid)?/gi, '\n');
13
+
14
+ const capMatch = unfenced.match(/^%%\s*caption:\s*(.+)$/m);
15
+ const caption = capMatch ? capMatch[1].trim() : '';
16
+
17
+ const start = unfenced.search(/^\s*(flowchart|graph)\b/m);
18
+ if (start === -1) return null;
19
+
20
+ const lines = unfenced.slice(start).split('\n');
21
+ // Trim trailing prose: walk back past anything that doesn't look like
22
+ // Mermaid. Labels, edges, subgraph blocks and classDefs all survive.
23
+ const MERMAID_HINT = /-->|---|\[|\]|\(|\)|\bsubgraph\b|\bend\b|classDef|^class\b|%%|:::|^\s*[\w$]+\s*:/;
24
+ while (lines.length) {
25
+ const tail = lines[lines.length - 1].trim();
26
+ if (!tail) {
27
+ lines.pop();
28
+ continue;
29
+ }
30
+ if (MERMAID_HINT.test(tail)) break;
31
+ lines.pop(); // a sentence, not a diagram line
32
+ }
33
+
34
+ const body = lines.join('\n').trim();
35
+ const meaningful = lines.filter((l) => l.trim() && !l.trim().startsWith('%%')).length;
36
+ if (meaningful < 2) return null;
37
+
38
+ return { caption, body };
39
+ }
40
+
41
+ // AI nodes carry no stable ids, so clicks are matched by their text: a full
42
+ // path hit first, then a unique-basename hit. Imperfect and honest about it.
43
+ export function matchNodeText(text, files) {
44
+ const t = String(text || '').trim();
45
+ if (!t) return null;
46
+ const exact = files.find((f) => t === f.path || t.endsWith('/' + f.path) || t === f.name && false);
47
+ if (exact) return exact.path;
48
+ let hit = null;
49
+ for (const f of files) {
50
+ if (!t.includes(f.name)) continue;
51
+ if (hit && hit !== f.path) {
52
+ // same basename twice — ambiguous only if the paths differ
53
+ const dup = files.filter((x) => x.name === f.name);
54
+ if (dup.length > 1) return null;
55
+ }
56
+ hit = f.path;
57
+ }
58
+ return hit;
59
+ }
@@ -0,0 +1,93 @@
1
+ // The atlas: every map the app knows how to draw, listed in one place.
2
+ //
3
+ // The list is a catalogue of *specs* — kind, subject, title, subtitle — and
4
+ // nothing more. Drawing is deliberately separate: `atlasDiagram` turns one spec
5
+ // into Mermaid when a card is actually opened. The list used to carry the
6
+ // generated source for all of its entries, which meant opening the atlas built
7
+ // three whole-repo maps, 120 folder graphs and 200 file graphs up front, to show
8
+ // a page of headings. On a mid-size repo that is a visible stall for work the
9
+ // reader will use one card's worth of.
10
+ //
11
+ // Pure, like everything under shared/: no DOM, no app state, no network.
12
+
13
+ import {
14
+ overviewDiagram, folderDiagram, fileDetailDiagram, servicesDiagram, layersDiagram,
15
+ } from './mermaid.js';
16
+ import { isTestPath } from '../analyzer/pathUtil.js';
17
+
18
+ // Both caps exist to keep the card list scrollable rather than endless. Files
19
+ // are ranked by fan-in first, so the 200 that survive are the ones the rest of
20
+ // the repo actually depends on — the cap trims the leaves, not the hubs.
21
+ const MAX_FOLDERS = 120;
22
+ const MAX_FILES = 200;
23
+
24
+ const plural = (n, word) => `${n} ${n === 1 ? word : word + 's'}`;
25
+
26
+ const strata = (patterns) => {
27
+ const n = patterns.layersInfo.layers.length;
28
+ return `${n} ${n === 1 ? 'stratum' : 'strata'}`;
29
+ };
30
+
31
+ // The catalogue, in the order it is shown: the whole-repo maps first, then one
32
+ // card per folder, then one per file.
33
+ export function atlasSpecs(scan, facts, manifest, patterns) {
34
+ const specs = [
35
+ { kind: 'overview', path: '', title: 'Overview map', sub: plural(scan.stats.filesParsed, 'file') },
36
+ { kind: 'layers', path: '', title: 'Architecture layers', sub: strata(patterns) },
37
+ ];
38
+
39
+ // A services map only means something once a manifest declared some.
40
+ if (manifest?.services?.length) {
41
+ specs.push({
42
+ kind: 'services', path: '', title: 'Services',
43
+ sub: `${manifest.services.length} declared`,
44
+ });
45
+ }
46
+
47
+ const counts = new Map();
48
+ for (const f of scan.files) counts.set(f.dir, (counts.get(f.dir) || 0) + 1);
49
+ for (const folder of [...counts.keys()].sort().slice(0, MAX_FOLDERS)) {
50
+ specs.push({
51
+ kind: 'folder', path: folder,
52
+ title: (folder || '(repo root)') + '/',
53
+ sub: plural(counts.get(folder), 'file'),
54
+ });
55
+ }
56
+
57
+ const ranked = scan.files.slice()
58
+ .sort((a, b) => (facts.fanIn[b.path] || 0) - (facts.fanIn[a.path] || 0)
59
+ || a.path.localeCompare(b.path))
60
+ .slice(0, MAX_FILES);
61
+ for (const f of ranked) {
62
+ specs.push({
63
+ kind: 'file', path: f.path, title: f.path,
64
+ sub: plural(facts.fanIn[f.path] || 0, 'dependent'),
65
+ });
66
+ }
67
+
68
+ return specs;
69
+ }
70
+
71
+ // Draws one card. Called on open, so a card the reader never touches costs
72
+ // nothing but its heading.
73
+ export function atlasDiagram(spec, { scan, facts, manifest, patterns }) {
74
+ switch (spec.kind) {
75
+ case 'overview': return overviewDiagram(scan, facts);
76
+ case 'layers': return layersDiagram(scan, facts, patterns.layersInfo);
77
+ case 'services': return servicesDiagram(scan, manifest);
78
+ case 'file': return fileDetailDiagram(scan, facts, spec.path);
79
+ default: return folderDiagram(scan, facts, spec.path);
80
+ }
81
+ }
82
+
83
+ // The stage filters, applied to the catalogue. Matching is on the title so that
84
+ // typing "overview" finds the overview card, which has no path of its own; the
85
+ // tests toggle is by path, and only files have one worth testing.
86
+ export function filterAtlasItems(specs, { text = '', showTests = true } = {}) {
87
+ const needle = text.trim().toLowerCase();
88
+ if (!needle && showTests) return specs;
89
+ return specs.filter((it) => {
90
+ if (!showTests && it.kind === 'file' && isTestPath(it.path)) return false;
91
+ return !needle || it.title.toLowerCase().includes(needle);
92
+ });
93
+ }