ai-mind-map 1.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/change-tracker/change-log.d.ts +160 -0
  4. package/dist/change-tracker/change-log.d.ts.map +1 -0
  5. package/dist/change-tracker/change-log.js +507 -0
  6. package/dist/change-tracker/change-log.js.map +1 -0
  7. package/dist/change-tracker/diff-engine.d.ts +149 -0
  8. package/dist/change-tracker/diff-engine.d.ts.map +1 -0
  9. package/dist/change-tracker/diff-engine.js +530 -0
  10. package/dist/change-tracker/diff-engine.js.map +1 -0
  11. package/dist/change-tracker/watcher.d.ts +137 -0
  12. package/dist/change-tracker/watcher.d.ts.map +1 -0
  13. package/dist/change-tracker/watcher.js +300 -0
  14. package/dist/change-tracker/watcher.js.map +1 -0
  15. package/dist/cli.d.ts +20 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +937 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config.d.ts +38 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +222 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/context/compressor.d.ts +49 -0
  24. package/dist/context/compressor.d.ts.map +1 -0
  25. package/dist/context/compressor.js +769 -0
  26. package/dist/context/compressor.js.map +1 -0
  27. package/dist/context/progressive-disclosure.d.ts +71 -0
  28. package/dist/context/progressive-disclosure.d.ts.map +1 -0
  29. package/dist/context/progressive-disclosure.js +470 -0
  30. package/dist/context/progressive-disclosure.js.map +1 -0
  31. package/dist/context/token-budget.d.ts +121 -0
  32. package/dist/context/token-budget.d.ts.map +1 -0
  33. package/dist/context/token-budget.js +282 -0
  34. package/dist/context/token-budget.js.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +944 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/install.d.ts +66 -0
  40. package/dist/install.d.ts.map +1 -0
  41. package/dist/install.js +946 -0
  42. package/dist/install.js.map +1 -0
  43. package/dist/knowledge-graph/architecture.d.ts +213 -0
  44. package/dist/knowledge-graph/architecture.d.ts.map +1 -0
  45. package/dist/knowledge-graph/architecture.js +585 -0
  46. package/dist/knowledge-graph/architecture.js.map +1 -0
  47. package/dist/knowledge-graph/cypher.d.ts +113 -0
  48. package/dist/knowledge-graph/cypher.d.ts.map +1 -0
  49. package/dist/knowledge-graph/cypher.js +1051 -0
  50. package/dist/knowledge-graph/cypher.js.map +1 -0
  51. package/dist/knowledge-graph/dead-code.d.ts +121 -0
  52. package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
  53. package/dist/knowledge-graph/dead-code.js +331 -0
  54. package/dist/knowledge-graph/dead-code.js.map +1 -0
  55. package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
  56. package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
  57. package/dist/knowledge-graph/flow-analyzer.js +739 -0
  58. package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
  59. package/dist/knowledge-graph/graph.d.ts +291 -0
  60. package/dist/knowledge-graph/graph.d.ts.map +1 -0
  61. package/dist/knowledge-graph/graph.js +978 -0
  62. package/dist/knowledge-graph/graph.js.map +1 -0
  63. package/dist/knowledge-graph/index.d.ts +17 -0
  64. package/dist/knowledge-graph/index.d.ts.map +1 -0
  65. package/dist/knowledge-graph/index.js +14 -0
  66. package/dist/knowledge-graph/index.js.map +1 -0
  67. package/dist/knowledge-graph/indexer.d.ts +112 -0
  68. package/dist/knowledge-graph/indexer.d.ts.map +1 -0
  69. package/dist/knowledge-graph/indexer.js +506 -0
  70. package/dist/knowledge-graph/indexer.js.map +1 -0
  71. package/dist/knowledge-graph/pagerank.d.ts +141 -0
  72. package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
  73. package/dist/knowledge-graph/pagerank.js +493 -0
  74. package/dist/knowledge-graph/pagerank.js.map +1 -0
  75. package/dist/knowledge-graph/parser.d.ts +55 -0
  76. package/dist/knowledge-graph/parser.d.ts.map +1 -0
  77. package/dist/knowledge-graph/parser.js +1090 -0
  78. package/dist/knowledge-graph/parser.js.map +1 -0
  79. package/dist/knowledge-graph/snapshot.d.ts +107 -0
  80. package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
  81. package/dist/knowledge-graph/snapshot.js +435 -0
  82. package/dist/knowledge-graph/snapshot.js.map +1 -0
  83. package/dist/memory/decision-log.d.ts +151 -0
  84. package/dist/memory/decision-log.d.ts.map +1 -0
  85. package/dist/memory/decision-log.js +482 -0
  86. package/dist/memory/decision-log.js.map +1 -0
  87. package/dist/memory/persistent-memory.d.ts +182 -0
  88. package/dist/memory/persistent-memory.d.ts.map +1 -0
  89. package/dist/memory/persistent-memory.js +579 -0
  90. package/dist/memory/persistent-memory.js.map +1 -0
  91. package/dist/memory/session-memory.d.ts +165 -0
  92. package/dist/memory/session-memory.d.ts.map +1 -0
  93. package/dist/memory/session-memory.js +382 -0
  94. package/dist/memory/session-memory.js.map +1 -0
  95. package/dist/stress-test.d.ts +10 -0
  96. package/dist/stress-test.d.ts.map +1 -0
  97. package/dist/stress-test.js +258 -0
  98. package/dist/stress-test.js.map +1 -0
  99. package/dist/tools/advanced-tools.d.ts +32 -0
  100. package/dist/tools/advanced-tools.d.ts.map +1 -0
  101. package/dist/tools/advanced-tools.js +480 -0
  102. package/dist/tools/advanced-tools.js.map +1 -0
  103. package/dist/tools/change-tools.d.ts +76 -0
  104. package/dist/tools/change-tools.d.ts.map +1 -0
  105. package/dist/tools/change-tools.js +93 -0
  106. package/dist/tools/change-tools.js.map +1 -0
  107. package/dist/tools/context-tools.d.ts +68 -0
  108. package/dist/tools/context-tools.d.ts.map +1 -0
  109. package/dist/tools/context-tools.js +141 -0
  110. package/dist/tools/context-tools.js.map +1 -0
  111. package/dist/tools/debug-tools.d.ts +25 -0
  112. package/dist/tools/debug-tools.d.ts.map +1 -0
  113. package/dist/tools/debug-tools.js +286 -0
  114. package/dist/tools/debug-tools.js.map +1 -0
  115. package/dist/tools/evolving-tools.d.ts +23 -0
  116. package/dist/tools/evolving-tools.d.ts.map +1 -0
  117. package/dist/tools/evolving-tools.js +207 -0
  118. package/dist/tools/evolving-tools.js.map +1 -0
  119. package/dist/tools/flow-tools.d.ts +24 -0
  120. package/dist/tools/flow-tools.d.ts.map +1 -0
  121. package/dist/tools/flow-tools.js +265 -0
  122. package/dist/tools/flow-tools.js.map +1 -0
  123. package/dist/tools/graph-tools.d.ts +71 -0
  124. package/dist/tools/graph-tools.d.ts.map +1 -0
  125. package/dist/tools/graph-tools.js +165 -0
  126. package/dist/tools/graph-tools.js.map +1 -0
  127. package/dist/tools/memory-tools.d.ts +62 -0
  128. package/dist/tools/memory-tools.d.ts.map +1 -0
  129. package/dist/tools/memory-tools.js +195 -0
  130. package/dist/tools/memory-tools.js.map +1 -0
  131. package/dist/tools/smart-tools.d.ts +23 -0
  132. package/dist/tools/smart-tools.d.ts.map +1 -0
  133. package/dist/tools/smart-tools.js +482 -0
  134. package/dist/tools/smart-tools.js.map +1 -0
  135. package/dist/tools/snapshot-tools.d.ts +19 -0
  136. package/dist/tools/snapshot-tools.d.ts.map +1 -0
  137. package/dist/tools/snapshot-tools.js +149 -0
  138. package/dist/tools/snapshot-tools.js.map +1 -0
  139. package/dist/types.d.ts +181 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +45 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/utils/logger.d.ts +59 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +142 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/dist/utils/token-counter.d.ts +51 -0
  148. package/dist/utils/token-counter.d.ts.map +1 -0
  149. package/dist/utils/token-counter.js +181 -0
  150. package/dist/utils/token-counter.js.map +1 -0
  151. package/install.ps1 +321 -0
  152. package/install.sh +345 -0
  153. package/package.json +94 -0
  154. package/setup.bat +62 -0
@@ -0,0 +1,739 @@
1
+ /**
2
+ * AI Mind Map — Flow Analyzer & Interaction Map
3
+ *
4
+ * Maps the BEHAVIORAL flow of an application:
5
+ * Button click → event handler → API call → DB write → UI update
6
+ *
7
+ * This goes beyond structural analysis (what calls what) to map the
8
+ * full user-interaction pipeline:
9
+ * - UI events → handlers → side effects
10
+ * - API routes → controllers → services → data layer
11
+ * - State changes → re-renders → UI updates
12
+ * - Event emitters → listeners → cascading effects
13
+ *
14
+ * The AI agent can ask "what happens when the user clicks Save?"
15
+ * and get the full pipeline without reading any code.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ import { relative, basename } from 'node:path';
19
+ /** All classification signals, organized by source */
20
+ const CLASSIFICATION_SIGNALS = [
21
+ // ────── CONTENT PATTERNS (weight: 3) ──────
22
+ // Route definitions
23
+ { source: 'content', weight: 3, layer: 'route', patterns: [
24
+ /\.(get|post|put|delete|patch|all|use)\s*\(\s*['"`]/, /router\.(get|post|put|delete|patch)/,
25
+ /app\.(get|post|put|delete|patch)/, /@(Get|Post|Put|Delete|Patch|All)\s*\(/,
26
+ /@app\.(route|get|post|put|delete)\s*\(/, /@(api_view|action)\s*\(/,
27
+ ] },
28
+ // UI events
29
+ { source: 'content', weight: 3, layer: 'ui_event', patterns: [
30
+ /on(Click|Change|Submit|Press|Focus|Blur|Drag|Drop|Key|Mouse|Touch|Scroll)/,
31
+ /addEventListener\s*\(\s*['"`]/, /\$\.(on|click|submit|change|keydown|keyup)\s*\(/,
32
+ /@click|@submit|@change|v-on:/, /\(click\)|\(submit\)|\(change\)/,
33
+ /on:click|on:submit|on:change/,
34
+ /_Click\b|_Loaded\b|_Closing\b|_Closed\b|_KeyDown\b|_KeyUp\b|_MouseDown\b|_MouseUp\b|_SelectionChanged\b|_TextChanged\b|_Checked\b|_Unchecked\b|_DragEnter\b|_Drop\b|_PreviewKeyDown\b|_GotFocus\b|_LostFocus\b|_SizeChanged\b|_Toggled\b/,
35
+ /RoutedEventHandler|EventHandler|\+=.*EventHandler/,
36
+ ] },
37
+ // State management
38
+ { source: 'content', weight: 3, layer: 'state_update', patterns: [
39
+ /setState\s*\(/, /useState|useReducer|useContext/, /dispatch\s*\(/,
40
+ /store\.(commit|dispatch|state)\s*\(/, /\$store\.(commit|dispatch)/,
41
+ /writable\s*\(|derived\s*\(/, /signal\s*\(|computed\s*\(/,
42
+ /atom\s*\(|selector\s*\(/, /createSlice|createAsyncThunk/,
43
+ /INotifyPropertyChanged|OnPropertyChanged|RaisePropertyChanged/,
44
+ /DependencyProperty\.Register|SetValue\(|GetValue\(/,
45
+ /ObservableCollection|BindingList/,
46
+ ] },
47
+ // API/HTTP calls
48
+ { source: 'content', weight: 3, layer: 'api_call', patterns: [
49
+ /fetch\s*\(\s*['"`]/, /axios\.(get|post|put|delete|patch|request)\s*\(/,
50
+ /\$http\.(get|post|put|delete)/, /HttpClient/,
51
+ /useSWR|useQuery|useMutation/, /api\.(get|post|put|delete|patch)\s*\(/,
52
+ /request\s*\(\s*['"`](GET|POST|PUT|DELETE)/,
53
+ /WebClient|HttpWebRequest|RestClient|HttpRequestMessage/,
54
+ /\.GetAsync\(|\.PostAsync\(|\.PutAsync\(|\.DeleteAsync\(/,
55
+ ] },
56
+ // Database
57
+ { source: 'content', weight: 3, layer: 'database', patterns: [
58
+ /\.(find|findOne|findMany|create|update|delete|upsert|aggregate)\s*\(/,
59
+ /\.(select|insert|update|delete|where|from)\s*\(/, /db\.(query|execute|prepare|run|all|get)\s*\(/,
60
+ /Model\.(find|create|update|destroy)/, /getRepository|createQueryBuilder/,
61
+ /SELECT\s|INSERT\s|UPDATE\s|DELETE\s/, /collection\.(find|insert|update|delete)/,
62
+ /SqlConnection|SqlCommand|DbContext|DbSet|ExecuteNonQuery|ExecuteReader|ExecuteScalar/,
63
+ /SQLiteConnection|SQLiteCommand|DatabaseHelper/,
64
+ ] },
65
+ // Middleware
66
+ { source: 'content', weight: 3, layer: 'middleware', patterns: [
67
+ /app\.use\s*\(/, /router\.use\s*\(/, /@UseGuards|@UseInterceptors|@UsePipes/,
68
+ /middleware\s*=\s*\[/,
69
+ ] },
70
+ // Validation
71
+ { source: 'content', weight: 3, layer: 'validator', patterns: [
72
+ /validate|sanitize|schema\.parse|Joi\.|yup\.|z\.object/,
73
+ /IsNotEmpty|IsEmail|IsString|MinLength/, /body\(\s*['"`]|param\(\s*['"`]|query\(\s*['"`]/,
74
+ /DataAnnotations|Required|StringLength|RegularExpression|Range\[/,
75
+ /FluentValidation|AbstractValidator|RuleFor/,
76
+ ] },
77
+ // UI components
78
+ { source: 'content', weight: 3, layer: 'ui_component', patterns: [
79
+ /function\s+\w+\s*\([^)]*\)\s*{[^}]*return\s*\(?</,
80
+ /export\s+default\s+\{[^}]*template\s*:/, /@Component\s*\(\s*{/,
81
+ /React\.createElement|jsx|tsx/,
82
+ /UserControl|Window|Page|ContentControl|ItemsControl|DependencyObject/,
83
+ /partial class.*:.*Window|partial class.*:.*UserControl|partial class.*:.*Page/,
84
+ /InitializeComponent\(\)/,
85
+ ] },
86
+ // ────── PATH PATTERNS (weight: 2) ──────
87
+ { source: 'path', weight: 2, layer: 'ui_component', patterns: [/Window/i, /Control/i, /View(?!Model)/i, /Style/i, /Theme/i, /^App\./i, /\.xaml$/i] },
88
+ { source: 'path', weight: 2, layer: 'controller', patterns: [/ViewModel/i, /EventHandler/i, /Interaction/i, /WndProc/i] },
89
+ { source: 'path', weight: 2, layer: 'service', patterns: [/Manager/i, /Service/i, /Engine/i, /Provider/i, /Helper/i, /Tool/i, /Crypto/i, /Clock/i, /Secrets?/i, /Daemon/i, /Scheduler/i, /Discovery/i, /Auth/i] },
90
+ { source: 'path', weight: 2, layer: 'database', patterns: [/Database/i, /Storage/i, /Cache/i, /Persist/i] },
91
+ { source: 'path', weight: 2, layer: 'validator', patterns: [/Validator/i, /Converter/i] },
92
+ { source: 'path', weight: 2, layer: 'util', patterns: [/Matcher/i, /Comparer/i, /Sorter/i, /Logger/i, /Profiler/i, /Tracker/i, /Queue/i, /Diagnostic/i, /Telemetry/i, /NativeMethods/i] },
93
+ { source: 'path', weight: 2, layer: 'route', patterns: [/route/i, /endpoint/i] },
94
+ { source: 'path', weight: 2, layer: 'middleware', patterns: [/middleware/i, /guard/i, /interceptor/i, /filter/i] },
95
+ { source: 'path', weight: 2, layer: 'repository', patterns: [/repo(?:sitory)?/i, /dao/i, /data.?access/i, /dal/i] },
96
+ // ────── DIRECTORY PATTERNS (weight: 2) ──────
97
+ { source: 'directory', weight: 2, layer: 'controller', patterns: [/^controllers?\//i, /^handlers?\//i, /^viewmodels?\//i, /^ViewModels\//] },
98
+ { source: 'directory', weight: 2, layer: 'service', patterns: [/^services?\//i, /^classes\//i, /^business\//i, /^logic\//i, /^managers?\//i] },
99
+ { source: 'directory', weight: 2, layer: 'repository', patterns: [/^repositor(y|ies)\//i, /^data\//i, /^dal\//i, /^dao\//i, /^persistence\//i] },
100
+ { source: 'directory', weight: 2, layer: 'ui_component', patterns: [/^components?\//i, /^views?\//i, /^pages?\//i, /^screens?\//i, /^widgets?\//i, /^windows?\//i, /^controls?\//i, /^styles?\//i, /^layouts?\//i, /^Windows\//] },
101
+ { source: 'directory', weight: 2, layer: 'middleware', patterns: [/^middlewares?\//i, /^guards?\//i, /^interceptors?\//i, /^pipes?\//i] },
102
+ { source: 'directory', weight: 2, layer: 'validator', patterns: [/^validators?\//i, /^schemas?\//i, /^dtos?\//i] },
103
+ { source: 'directory', weight: 2, layer: 'util', patterns: [/^utils?\//i, /^helpers?\//i, /^lib\//i, /^common\//i, /^shared\//i, /^tools?\//i, /^Utils\//] },
104
+ { source: 'directory', weight: 2, layer: 'route', patterns: [/^routes?\//i, /^api\//i, /^endpoints?\//i] },
105
+ { source: 'directory', weight: 2, layer: 'database', patterns: [/^migrations?\//i, /^models?\//i, /^entities?\//i, /^seeds?\//i] },
106
+ { source: 'directory', weight: 2, layer: 'ui_event', patterns: [/^events?\//i, /^listeners?\//i] },
107
+ // ────── IMPORT / INHERITANCE PATTERNS (weight: 4 — strongest signal) ──────
108
+ { source: 'import', weight: 4, layer: 'ui_component', patterns: [
109
+ /:\s*Window\b|:\s*UserControl\b|:\s*Page\b|:\s*ContentPage\b/, // C#/WPF/MAUI inheritance
110
+ /extends\s+(Component|React\.Component|PureComponent)\b/, // React class components
111
+ /extends\s+(StatelessWidget|StatefulWidget|State)\b/, // Flutter/Dart
112
+ /import\s+.*SwiftUI|import\s+.*UIKit/, // Swift UI frameworks
113
+ /import\s+.*\b(react|vue|svelte|angular)\b/i, // JS UI frameworks
114
+ /@Composable\b/, // Jetpack Compose (Kotlin)
115
+ ] },
116
+ { source: 'import', weight: 4, layer: 'controller', patterns: [
117
+ /:\s*ViewModel\b|:\s*ObservableObject\b/, // C# MVVM
118
+ /extends\s+ChangeNotifier\b/, // Flutter
119
+ /@Controller\b|@RestController\b/, // Spring/NestJS
120
+ /class\s+\w+Controller\b/, // *Controller naming convention
121
+ ] },
122
+ { source: 'import', weight: 4, layer: 'service', patterns: [
123
+ /@Injectable\b|@Service\b/, // NestJS/Spring/Angular
124
+ /class\s+\w+(Service|Manager|Engine|Provider)\b/, // Service naming convention
125
+ ] },
126
+ { source: 'import', weight: 4, layer: 'database', patterns: [
127
+ /:\s*DbContext\b|:\s*DbSet\b/, // Entity Framework
128
+ /import\s+.*prisma|import\s+.*typeorm|import\s+.*sequelize/i, // JS ORMs
129
+ /@Entity\b|@Table\b|@Column\b/, // ORM decorators
130
+ /import\s+.*mongoose/i, // MongoDB
131
+ /import\s+.*sqlite|import\s+.*pg\b|import\s+.*mysql/i, // DB drivers
132
+ ] },
133
+ { source: 'import', weight: 4, layer: 'api_call', patterns: [
134
+ /import\s+.*HttpClient|using\s+.*System\.Net\.Http/, // C# / Angular HttpClient
135
+ /import\s+.*axios|import\s+.*node-fetch|import\s+.*got\b/i, // JS HTTP libraries
136
+ /import\s+.*requests\b|from\s+requests\s+import/, // Python requests
137
+ /import\s+.*retrofit|import\s+.*okhttp/i, // Android HTTP
138
+ ] },
139
+ { source: 'import', weight: 4, layer: 'state_update', patterns: [
140
+ /:\s*INotifyPropertyChanged\b/, // C# WPF/MVVM
141
+ /import\s+.*redux|import\s+.*zustand|import\s+.*mobx/i, // JS state libs
142
+ /import\s+.*@ngrx|import\s+.*vuex|import\s+.*pinia/i, // Framework stores
143
+ /import\s+.*bloc\b|import\s+.*riverpod/i, // Flutter state
144
+ ] },
145
+ { source: 'import', weight: 4, layer: 'validator', patterns: [
146
+ /import\s+.*class-validator|import\s+.*joi\b|import\s+.*yup\b|import\s+.*zod\b/i,
147
+ /using\s+.*FluentValidation|using\s+.*DataAnnotations/,
148
+ ] },
149
+ // ────── SYMBOL NAME PATTERNS (weight: 1 — weakest signal) ──────
150
+ { source: 'symbol_name', weight: 1, layer: 'event_handler', patterns: [/^(handle|on)[A-Z]/, /_Click$|_Loaded$|_Changed$|_KeyDown$/] },
151
+ { source: 'symbol_name', weight: 1, layer: 'state_update', patterns: [/^use[A-Z]/, /^set[A-Z].*State$/] },
152
+ { source: 'symbol_name', weight: 1, layer: 'api_call', patterns: [/^fetch[A-Z]|^(get|post|put|delete)[A-Z].*Api$|Async$/] },
153
+ { source: 'symbol_name', weight: 1, layer: 'validator', patterns: [/^validate|^sanitize|^check[A-Z]|^is[A-Z].*Valid$/] },
154
+ { source: 'symbol_name', weight: 1, layer: 'util', patterns: [/^(format|parse|convert|transform|serialize|deserialize|encode|decode)[A-Z]/] },
155
+ { source: 'symbol_name', weight: 1, layer: 'database', patterns: [/^(save|load|persist|query|find|fetch|insert|update|delete)(?:All|By|One|Many)?$/] },
156
+ ];
157
+ // ============================================================
158
+ // Flow Analyzer
159
+ // ============================================================
160
+ export class FlowAnalyzer {
161
+ graph;
162
+ projectRoot;
163
+ constructor(graph, projectRoot) {
164
+ this.graph = graph;
165
+ this.projectRoot = projectRoot;
166
+ }
167
+ // ── Public API ──────────────────────────────────────────────
168
+ /**
169
+ * Build the complete interaction map for the application.
170
+ */
171
+ buildInteractionMap() {
172
+ const allNodes = this.getAllNodes();
173
+ // Classify every file and symbol by layer
174
+ const fileLayerMap = this.classifyFilesByLayer(allNodes);
175
+ const layerSummary = this.buildLayerSummary(allNodes, fileLayerMap);
176
+ // Detect all route definitions
177
+ const routes = this.detectRoutes(allNodes, fileLayerMap);
178
+ // Detect all event handlers in UI components
179
+ const eventHandlers = this.detectEventHandlers(allNodes, fileLayerMap);
180
+ // Build component maps
181
+ const components = this.buildComponentMaps(allNodes, fileLayerMap);
182
+ return {
183
+ routes,
184
+ eventHandlers,
185
+ components,
186
+ layerSummary,
187
+ filesByLayer: fileLayerMap,
188
+ };
189
+ }
190
+ /**
191
+ * Trace a complete flow starting from a specific symbol.
192
+ * "What happens when createNote() is called?"
193
+ */
194
+ traceFlow(startSymbol, maxDepth = 10) {
195
+ // Find the starting node
196
+ const startNodes = this.graph.getNodesByName(startSymbol);
197
+ if (startNodes.length === 0) {
198
+ // Try search
199
+ const searchResults = this.graph.search(startSymbol, 5);
200
+ if (searchResults.length === 0) {
201
+ return this.emptyFlow(`Symbol "${startSymbol}" not found`);
202
+ }
203
+ return this.traceFromNode(searchResults[0], maxDepth);
204
+ }
205
+ return this.traceFromNode(startNodes[0], maxDepth);
206
+ }
207
+ /**
208
+ * Trace a flow starting from a route.
209
+ * "What happens when someone hits POST /api/notes?"
210
+ */
211
+ traceRoute(routePattern) {
212
+ const allNodes = this.getAllNodes();
213
+ const routeNode = allNodes.find((n) => n.type === 'route' && n.name.includes(routePattern));
214
+ if (!routeNode) {
215
+ // Search for the route pattern in signatures
216
+ const searchResults = this.graph.search(routePattern, 10);
217
+ const routeResult = searchResults.find((n) => n.type === 'route' || n.type === 'function');
218
+ if (!routeResult) {
219
+ return this.emptyFlow(`Route "${routePattern}" not found`);
220
+ }
221
+ return this.traceFromNode(routeResult, 10);
222
+ }
223
+ return this.traceFromNode(routeNode, 10);
224
+ }
225
+ /**
226
+ * Classify a single file by its layer using weighted multi-signal scoring.
227
+ */
228
+ classifyFile(filePath) {
229
+ const result = this.scoreFile(filePath);
230
+ return result.layer;
231
+ }
232
+ /**
233
+ * Full classification with confidence details.
234
+ * Returns the winning layer, confidence score, all signal hits, and runner-up layers.
235
+ */
236
+ getFileClassification(filePath) {
237
+ return this.scoreFile(filePath);
238
+ }
239
+ /**
240
+ * Get a summary of how the app is structured by layers.
241
+ */
242
+ getLayerOverview() {
243
+ const allNodes = this.getAllNodes();
244
+ const fileLayerMap = this.classifyFilesByLayer(allNodes);
245
+ // Group by layer
246
+ const layerGroups = new Map();
247
+ for (const node of allNodes) {
248
+ const relPath = relative(this.projectRoot, node.filePath);
249
+ const layer = fileLayerMap[relPath] ?? 'unknown';
250
+ if (!layerGroups.has(layer)) {
251
+ layerGroups.set(layer, { files: new Set(), symbols: [] });
252
+ }
253
+ const group = layerGroups.get(layer);
254
+ group.files.add(relPath);
255
+ if (node.type === 'function' || node.type === 'method' || node.type === 'class') {
256
+ group.symbols.push(node.name);
257
+ }
258
+ }
259
+ return Array.from(layerGroups.entries())
260
+ .map(([layer, data]) => ({
261
+ layer,
262
+ fileCount: data.files.size,
263
+ symbolCount: data.symbols.length,
264
+ files: [...data.files].slice(0, 20),
265
+ keySymbols: data.symbols.slice(0, 15),
266
+ }))
267
+ .sort((a, b) => b.symbolCount - a.symbolCount);
268
+ }
269
+ // ── Private: Tracing ────────────────────────────────────────
270
+ traceFromNode(startNode, maxDepth) {
271
+ const steps = [];
272
+ const visited = new Set();
273
+ const filesInvolved = new Set();
274
+ const sideEffects = [];
275
+ // BFS through the call graph + partial class methods + inline calls
276
+ const queue = [
277
+ { node: startNode, depth: 0 },
278
+ ];
279
+ while (queue.length > 0 && steps.length < 50) {
280
+ const item = queue.shift();
281
+ const { node, depth } = item;
282
+ if (depth > maxDepth || visited.has(node.id))
283
+ continue;
284
+ visited.add(node.id);
285
+ const relPath = relative(this.projectRoot, node.filePath);
286
+ const layer = this.classifySymbol(node, relPath);
287
+ filesInvolved.add(relPath);
288
+ // Detect side effects
289
+ if (layer === 'database') {
290
+ sideEffects.push(`DB: ${node.name} (${relPath})`);
291
+ }
292
+ else if (layer === 'state_update') {
293
+ sideEffects.push(`State: ${node.name} (${relPath})`);
294
+ }
295
+ else if (layer === 'api_call') {
296
+ sideEffects.push(`API: ${node.name} (${relPath})`);
297
+ }
298
+ steps.push({
299
+ order: steps.length,
300
+ action: this.describeAction(node, layer),
301
+ layer,
302
+ filePath: relPath,
303
+ symbolName: node.name,
304
+ signature: node.signature || `${node.name}()`,
305
+ line: node.startLine,
306
+ nodeId: node.id,
307
+ produces: this.inferProduces(node, layer),
308
+ });
309
+ // 1. Follow explicit call edges
310
+ const callees = this.graph.findCallees(node.id);
311
+ for (const callee of callees) {
312
+ if (!visited.has(callee.id)) {
313
+ queue.push({ node: callee, depth: depth + 1 });
314
+ }
315
+ }
316
+ // 2. Follow 'uses' and 'depends_on' edges too
317
+ const outEdges = this.graph.getOutEdges(node.id);
318
+ for (const edge of outEdges) {
319
+ if (edge.type !== 'calls' && !visited.has(edge.targetId)) {
320
+ const target = this.graph.getNode(edge.targetId);
321
+ if (target && target.type !== 'file') {
322
+ queue.push({ node: target, depth: depth + 1 });
323
+ }
324
+ }
325
+ }
326
+ // 3. C# partial class discovery: find sibling methods called in the body
327
+ if (callees.length === 0 && depth < maxDepth) {
328
+ const inlineCalls = this.discoverInlineCalls(node, visited);
329
+ for (const callee of inlineCalls) {
330
+ queue.push({ node: callee, depth: depth + 1 });
331
+ }
332
+ }
333
+ }
334
+ const riskLevel = this.assessRisk(steps, sideEffects);
335
+ const name = this.generateFlowName(startNode, steps);
336
+ return {
337
+ name,
338
+ trigger: `${startNode.name} (${relative(this.projectRoot, startNode.filePath)}:${startNode.startLine})`,
339
+ steps,
340
+ filesInvolved: [...filesInvolved],
341
+ riskLevel,
342
+ sideEffects,
343
+ };
344
+ }
345
+ /**
346
+ * When the graph has no explicit 'calls' edges (common in C# partial classes),
347
+ * read the method body and search for method names that exist in the same class
348
+ * across any partial class file.
349
+ *
350
+ * E.g., NotesToggle_Click() calls OpenNotesPanel() which is in a different
351
+ * partial class file — the static parser may not have created an edge.
352
+ */
353
+ discoverInlineCalls(node, visited) {
354
+ const discovered = [];
355
+ try {
356
+ const content = readFileSync(node.filePath, 'utf-8');
357
+ const lines = content.split('\n');
358
+ const startIdx = Math.max(0, (node.startLine || 1) - 1);
359
+ const endIdx = Math.min(lines.length, (node.endLine || node.startLine || 1));
360
+ const body = lines.slice(startIdx, endIdx).join('\n');
361
+ // Find the class this method belongs to
362
+ const className = node.qualifiedName.includes('.')
363
+ ? node.qualifiedName.split('.')[0]
364
+ : null;
365
+ if (!className)
366
+ return discovered;
367
+ // Get ALL methods in the same class across all partial class files
368
+ const classMethods = this.graph.search(className, 100)
369
+ .filter(n => n.qualifiedName.startsWith(className + '.') &&
370
+ (n.type === 'function' || n.type === 'method') &&
371
+ !visited.has(n.id) &&
372
+ n.id !== node.id);
373
+ // Check which of those method names appear in our method body
374
+ for (const method of classMethods) {
375
+ // Match method name followed by ( — indicates a call
376
+ const callPattern = new RegExp(`\\b${this.escapeRegex(method.name)}\\s*\\(`, 'g');
377
+ if (callPattern.test(body)) {
378
+ discovered.push(method);
379
+ }
380
+ }
381
+ }
382
+ catch {
383
+ // File unreadable, skip
384
+ }
385
+ return discovered.slice(0, 10); // Cap to prevent explosion
386
+ }
387
+ escapeRegex(s) {
388
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
389
+ }
390
+ // ── Private: Detection ──────────────────────────────────────
391
+ detectRoutes(allNodes, fileLayerMap) {
392
+ const flows = [];
393
+ // Find nodes that look like route handlers
394
+ const routeNodes = allNodes.filter((n) => n.type === 'route' ||
395
+ n.type === 'function' &&
396
+ fileLayerMap[relative(this.projectRoot, n.filePath)] === 'route');
397
+ // Also find by signature patterns
398
+ const httpPatterns = /\b(get|post|put|delete|patch)\b/i;
399
+ const handlerNodes = allNodes.filter((n) => (n.type === 'function' || n.type === 'method') &&
400
+ httpPatterns.test(n.name) &&
401
+ !routeNodes.includes(n));
402
+ for (const node of [...routeNodes, ...handlerNodes].slice(0, 30)) {
403
+ flows.push(this.traceFromNode(node, 8));
404
+ }
405
+ return flows;
406
+ }
407
+ detectEventHandlers(allNodes, _fileLayerMap) {
408
+ const flows = [];
409
+ const handlerPattern = /^(handle|on)[A-Z]|_Click$|_Loaded$|_Closing$|_Closed$|_Changed$|_SelectionChanged$|_KeyDown$|_KeyUp$|_MouseDown$|_MouseUp$|_DragEnter$|_Drop$|_Checked$|_Unchecked$|_GotFocus$|_LostFocus$|_TextChanged$/;
410
+ const handlers = allNodes.filter((n) => (n.type === 'function' || n.type === 'method') &&
411
+ handlerPattern.test(n.name));
412
+ for (const handler of handlers.slice(0, 30)) {
413
+ flows.push(this.traceFromNode(handler, 8));
414
+ }
415
+ return flows;
416
+ }
417
+ buildComponentMaps(allNodes, fileLayerMap) {
418
+ const components = [];
419
+ const componentNodes = allNodes.filter((n) => n.type === 'component' ||
420
+ n.type === 'class' &&
421
+ fileLayerMap[relative(this.projectRoot, n.filePath)] === 'ui_component');
422
+ for (const comp of componentNodes.slice(0, 20)) {
423
+ const relPath = relative(this.projectRoot, comp.filePath);
424
+ // Find handlers in same file
425
+ const sameFileNodes = allNodes.filter((n) => n.filePath === comp.filePath);
426
+ const handlers = sameFileNodes.filter((n) => /^(handle|on)[A-Z]/.test(n.name));
427
+ // Determine inputs from parameters
428
+ const inputs = (comp.parameters ?? []).map((p) => ({
429
+ name: p.name,
430
+ type: p.type ?? 'unknown',
431
+ }));
432
+ // Build action flows for each handler
433
+ const actions = handlers.map((h) => ({
434
+ trigger: h.name.replace(/^handle/, 'on').replace(/^on/, 'on'),
435
+ handler: h.name,
436
+ flow: this.traceFromNode(h, 6),
437
+ }));
438
+ // Detect state
439
+ const state = [];
440
+ const statePattern = /useState|useReducer|this\.state/;
441
+ try {
442
+ const content = readFileSync(comp.filePath, 'utf-8');
443
+ const stateMatches = content.match(/(?:const\s+\[(\w+),\s*set\w+\]|this\.state\.(\w+))/g);
444
+ if (stateMatches) {
445
+ for (const m of stateMatches) {
446
+ const varMatch = m.match(/const\s+\[(\w+)|this\.state\.(\w+)/);
447
+ if (varMatch) {
448
+ state.push(varMatch[1] ?? varMatch[2] ?? m);
449
+ }
450
+ }
451
+ }
452
+ }
453
+ catch {
454
+ // Can't read file
455
+ }
456
+ // Find child components (calls to other components)
457
+ const children = [];
458
+ const callees = this.graph.findCallees(comp.id);
459
+ for (const callee of callees) {
460
+ if (callee.type === 'component' ||
461
+ /^[A-Z]/.test(callee.name)) {
462
+ children.push(callee.name);
463
+ }
464
+ }
465
+ // Find data sources
466
+ const dataSources = [];
467
+ for (const node of sameFileNodes) {
468
+ const sig = node.signature || '';
469
+ if (/fetch|axios|useQuery|useSWR|api\./i.test(sig) || /fetch|axios|useQuery|useSWR|api\./i.test(node.name)) {
470
+ dataSources.push(node.name);
471
+ }
472
+ }
473
+ components.push({
474
+ name: comp.name,
475
+ filePath: relPath,
476
+ inputs,
477
+ actions,
478
+ state,
479
+ children,
480
+ dataSources,
481
+ });
482
+ }
483
+ return components;
484
+ }
485
+ // ── Private: Multi-Signal Scoring Engine ────────────────────
486
+ /** Score a file across ALL layers and return the best match */
487
+ scoreFile(filePath) {
488
+ const scores = new Map();
489
+ const signals = [];
490
+ const relPath = relative(this.projectRoot, filePath);
491
+ const fileName = basename(filePath);
492
+ // Read file content once (cached for all content/import checks)
493
+ let content = null;
494
+ try {
495
+ content = readFileSync(filePath, 'utf-8');
496
+ }
497
+ catch {
498
+ // Can't read, rely on path-only signals
499
+ }
500
+ // Get symbol names for this file from the graph
501
+ const fileNodes = this.graph.getFileStructure(filePath);
502
+ const symbolNames = fileNodes
503
+ .filter(n => n.type === 'function' || n.type === 'method' || n.type === 'class')
504
+ .map(n => n.name);
505
+ for (const signal of CLASSIFICATION_SIGNALS) {
506
+ let matched = false;
507
+ switch (signal.source) {
508
+ case 'content':
509
+ if (content) {
510
+ matched = signal.patterns.some(p => p.test(content));
511
+ }
512
+ break;
513
+ case 'path':
514
+ matched = signal.patterns.some(p => p.test(fileName) || p.test(relPath));
515
+ break;
516
+ case 'directory':
517
+ matched = signal.patterns.some(p => p.test(relPath));
518
+ break;
519
+ case 'import':
520
+ if (content) {
521
+ matched = signal.patterns.some(p => p.test(content));
522
+ }
523
+ break;
524
+ case 'symbol_name':
525
+ matched = symbolNames.some(name => signal.patterns.some(p => p.test(name)));
526
+ break;
527
+ }
528
+ if (matched) {
529
+ const current = scores.get(signal.layer) ?? 0;
530
+ scores.set(signal.layer, current + signal.weight);
531
+ signals.push({ source: signal.source, layer: signal.layer, weight: signal.weight });
532
+ }
533
+ }
534
+ // ── Score against AI-learned classification rules ──────────
535
+ try {
536
+ const learnedRules = this.graph.getLearnedClassificationRules();
537
+ for (const rule of learnedRules) {
538
+ if (rule.patterns.length === 0)
539
+ continue;
540
+ let matched = false;
541
+ const compiledPatterns = rule.patterns.map(p => {
542
+ try {
543
+ return new RegExp(p, 'im');
544
+ }
545
+ catch {
546
+ return null;
547
+ }
548
+ }).filter(Boolean);
549
+ const targetLayer = rule.layer;
550
+ switch (rule.source) {
551
+ case 'content':
552
+ case 'import':
553
+ if (content) {
554
+ matched = compiledPatterns.some(p => p.test(content));
555
+ }
556
+ break;
557
+ case 'path':
558
+ matched = compiledPatterns.some(p => p.test(fileName) || p.test(relPath));
559
+ break;
560
+ case 'directory':
561
+ matched = compiledPatterns.some(p => p.test(relPath));
562
+ break;
563
+ case 'symbol_name':
564
+ matched = symbolNames.some(name => compiledPatterns.some(p => p.test(name)));
565
+ break;
566
+ default:
567
+ // Try all: path + content
568
+ matched = compiledPatterns.some(p => p.test(fileName) || p.test(relPath));
569
+ if (!matched && content) {
570
+ matched = compiledPatterns.some(p => p.test(content));
571
+ }
572
+ break;
573
+ }
574
+ if (matched) {
575
+ const w = rule.weight ?? 2;
576
+ const current = scores.get(targetLayer) ?? 0;
577
+ scores.set(targetLayer, current + w);
578
+ signals.push({ source: `learned:${rule.name}`, layer: targetLayer, weight: w });
579
+ // Touch the rule to track usage
580
+ try {
581
+ this.graph.touchLearnedRule(rule.id);
582
+ }
583
+ catch { }
584
+ }
585
+ }
586
+ }
587
+ catch {
588
+ // Learned rules table might not exist yet (first run before migration)
589
+ }
590
+ // Find the winner
591
+ if (scores.size === 0) {
592
+ return { layer: 'unknown', confidence: 0, signals };
593
+ }
594
+ const sorted = [...scores.entries()].sort((a, b) => b[1] - a[1]);
595
+ const [winnerLayer, winnerScore] = sorted[0];
596
+ const totalScore = sorted.reduce((sum, [, s]) => sum + s, 0);
597
+ const confidence = Math.round((winnerScore / totalScore) * 100);
598
+ const runnerUp = sorted.length > 1 ? sorted[1][0] : undefined;
599
+ return { layer: winnerLayer, confidence, signals, runnerUp };
600
+ }
601
+ /** File classification cache to avoid re-reading files */
602
+ fileClassCache = new Map();
603
+ classifyFilesByLayer(allNodes) {
604
+ const result = {};
605
+ const files = new Set(allNodes.map((n) => n.filePath));
606
+ this.fileClassCache.clear();
607
+ for (const filePath of files) {
608
+ const relPath = relative(this.projectRoot, filePath);
609
+ if (result[relPath])
610
+ continue;
611
+ const scored = this.scoreFile(filePath);
612
+ result[relPath] = scored.layer;
613
+ this.fileClassCache.set(filePath, scored.layer);
614
+ }
615
+ return result;
616
+ }
617
+ classifySymbol(node, relPath) {
618
+ // Check node type first (explicit types override scoring)
619
+ if (node.type === 'route')
620
+ return 'route';
621
+ if (node.type === 'component')
622
+ return 'ui_component';
623
+ if (node.type === 'hook')
624
+ return 'state_update';
625
+ // Check symbol name signals
626
+ for (const signal of CLASSIFICATION_SIGNALS) {
627
+ if (signal.source === 'symbol_name') {
628
+ if (signal.patterns.some(p => p.test(node.name))) {
629
+ return signal.layer;
630
+ }
631
+ }
632
+ }
633
+ // Check by signature content
634
+ const sig = (node.signature || '').toLowerCase();
635
+ if (/\b(req\s*,\s*res|request\s*,\s*response|ctx)\b/.test(sig))
636
+ return 'controller';
637
+ if (/\bdb\b|\bprisma\b|\bmodel\b|\brepository\b/i.test(sig))
638
+ return 'repository';
639
+ // Fall back to file-level classification (cached)
640
+ const cached = this.fileClassCache.get(node.filePath);
641
+ if (cached)
642
+ return cached;
643
+ // Score the file if not cached
644
+ const scored = this.scoreFile(node.filePath);
645
+ this.fileClassCache.set(node.filePath, scored.layer);
646
+ return scored.layer;
647
+ }
648
+ buildLayerSummary(allNodes, fileLayerMap) {
649
+ const summary = {};
650
+ for (const node of allNodes) {
651
+ const relPath = relative(this.projectRoot, node.filePath);
652
+ const layer = fileLayerMap[relPath] ?? 'unknown';
653
+ summary[layer] = (summary[layer] ?? 0) + 1;
654
+ }
655
+ return summary;
656
+ }
657
+ // ── Private: Helpers ────────────────────────────────────────
658
+ describeAction(node, layer) {
659
+ const verb = {
660
+ ui_event: 'triggers',
661
+ ui_component: 'renders',
662
+ event_handler: 'handles event via',
663
+ state_update: 'updates state in',
664
+ api_call: 'calls API via',
665
+ route: 'routes to',
666
+ controller: 'handles request in',
667
+ service: 'processes in',
668
+ repository: 'accesses data via',
669
+ database: 'queries database in',
670
+ middleware: 'passes through',
671
+ validator: 'validates in',
672
+ util: 'uses helper',
673
+ unknown: 'executes',
674
+ }[layer] ?? 'executes';
675
+ return `${verb} ${node.name}()`;
676
+ }
677
+ inferProduces(node, layer) {
678
+ if (node.returnType && node.returnType !== 'void') {
679
+ return node.returnType;
680
+ }
681
+ if (layer === 'database')
682
+ return 'DB result';
683
+ if (layer === 'api_call')
684
+ return 'API response';
685
+ if (layer === 'state_update')
686
+ return 'Updated state';
687
+ if (layer === 'ui_component')
688
+ return 'JSX/HTML';
689
+ if (layer === 'validator')
690
+ return 'Validated data';
691
+ return undefined;
692
+ }
693
+ assessRisk(steps, sideEffects) {
694
+ const hasDatabaseWrite = sideEffects.some((e) => e.startsWith('DB:'));
695
+ const hasApiCall = sideEffects.some((e) => e.startsWith('API:'));
696
+ const fileCount = new Set(steps.map((s) => s.filePath)).size;
697
+ if (hasDatabaseWrite && fileCount > 5)
698
+ return 'critical';
699
+ if (hasDatabaseWrite)
700
+ return 'high';
701
+ if (hasApiCall && fileCount > 3)
702
+ return 'high';
703
+ if (hasApiCall || fileCount > 3)
704
+ return 'medium';
705
+ return 'low';
706
+ }
707
+ generateFlowName(startNode, steps) {
708
+ // Try to generate a meaningful name like "Save Note" or "Delete User"
709
+ const name = startNode.name
710
+ .replace(/^handle/, '')
711
+ .replace(/^on/, '')
712
+ .replace(/([A-Z])/g, ' $1')
713
+ .trim();
714
+ const hasDb = steps.some((s) => s.layer === 'database');
715
+ const hasApi = steps.some((s) => s.layer === 'api_call');
716
+ if (hasDb)
717
+ return `${name} (→ DB)`;
718
+ if (hasApi)
719
+ return `${name} (→ API)`;
720
+ return name;
721
+ }
722
+ emptyFlow(reason) {
723
+ return {
724
+ name: reason,
725
+ trigger: 'unknown',
726
+ steps: [],
727
+ filesInvolved: [],
728
+ riskLevel: 'low',
729
+ sideEffects: [],
730
+ };
731
+ }
732
+ getAllNodes() {
733
+ const ids = this.graph.getAllNodeIds();
734
+ return ids
735
+ .map((id) => this.graph.getNode(id))
736
+ .filter((n) => n !== null);
737
+ }
738
+ }
739
+ //# sourceMappingURL=flow-analyzer.js.map