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.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/change-tracker/change-log.d.ts +160 -0
- package/dist/change-tracker/change-log.d.ts.map +1 -0
- package/dist/change-tracker/change-log.js +507 -0
- package/dist/change-tracker/change-log.js.map +1 -0
- package/dist/change-tracker/diff-engine.d.ts +149 -0
- package/dist/change-tracker/diff-engine.d.ts.map +1 -0
- package/dist/change-tracker/diff-engine.js +530 -0
- package/dist/change-tracker/diff-engine.js.map +1 -0
- package/dist/change-tracker/watcher.d.ts +137 -0
- package/dist/change-tracker/watcher.d.ts.map +1 -0
- package/dist/change-tracker/watcher.js +300 -0
- package/dist/change-tracker/watcher.js.map +1 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +937 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +222 -0
- package/dist/config.js.map +1 -0
- package/dist/context/compressor.d.ts +49 -0
- package/dist/context/compressor.d.ts.map +1 -0
- package/dist/context/compressor.js +769 -0
- package/dist/context/compressor.js.map +1 -0
- package/dist/context/progressive-disclosure.d.ts +71 -0
- package/dist/context/progressive-disclosure.d.ts.map +1 -0
- package/dist/context/progressive-disclosure.js +470 -0
- package/dist/context/progressive-disclosure.js.map +1 -0
- package/dist/context/token-budget.d.ts +121 -0
- package/dist/context/token-budget.d.ts.map +1 -0
- package/dist/context/token-budget.js +282 -0
- package/dist/context/token-budget.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +944 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +66 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +946 -0
- package/dist/install.js.map +1 -0
- package/dist/knowledge-graph/architecture.d.ts +213 -0
- package/dist/knowledge-graph/architecture.d.ts.map +1 -0
- package/dist/knowledge-graph/architecture.js +585 -0
- package/dist/knowledge-graph/architecture.js.map +1 -0
- package/dist/knowledge-graph/cypher.d.ts +113 -0
- package/dist/knowledge-graph/cypher.d.ts.map +1 -0
- package/dist/knowledge-graph/cypher.js +1051 -0
- package/dist/knowledge-graph/cypher.js.map +1 -0
- package/dist/knowledge-graph/dead-code.d.ts +121 -0
- package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
- package/dist/knowledge-graph/dead-code.js +331 -0
- package/dist/knowledge-graph/dead-code.js.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.js +739 -0
- package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
- package/dist/knowledge-graph/graph.d.ts +291 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -0
- package/dist/knowledge-graph/graph.js +978 -0
- package/dist/knowledge-graph/graph.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +17 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +14 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/indexer.d.ts +112 -0
- package/dist/knowledge-graph/indexer.d.ts.map +1 -0
- package/dist/knowledge-graph/indexer.js +506 -0
- package/dist/knowledge-graph/indexer.js.map +1 -0
- package/dist/knowledge-graph/pagerank.d.ts +141 -0
- package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
- package/dist/knowledge-graph/pagerank.js +493 -0
- package/dist/knowledge-graph/pagerank.js.map +1 -0
- package/dist/knowledge-graph/parser.d.ts +55 -0
- package/dist/knowledge-graph/parser.d.ts.map +1 -0
- package/dist/knowledge-graph/parser.js +1090 -0
- package/dist/knowledge-graph/parser.js.map +1 -0
- package/dist/knowledge-graph/snapshot.d.ts +107 -0
- package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
- package/dist/knowledge-graph/snapshot.js +435 -0
- package/dist/knowledge-graph/snapshot.js.map +1 -0
- package/dist/memory/decision-log.d.ts +151 -0
- package/dist/memory/decision-log.d.ts.map +1 -0
- package/dist/memory/decision-log.js +482 -0
- package/dist/memory/decision-log.js.map +1 -0
- package/dist/memory/persistent-memory.d.ts +182 -0
- package/dist/memory/persistent-memory.d.ts.map +1 -0
- package/dist/memory/persistent-memory.js +579 -0
- package/dist/memory/persistent-memory.js.map +1 -0
- package/dist/memory/session-memory.d.ts +165 -0
- package/dist/memory/session-memory.d.ts.map +1 -0
- package/dist/memory/session-memory.js +382 -0
- package/dist/memory/session-memory.js.map +1 -0
- package/dist/stress-test.d.ts +10 -0
- package/dist/stress-test.d.ts.map +1 -0
- package/dist/stress-test.js +258 -0
- package/dist/stress-test.js.map +1 -0
- package/dist/tools/advanced-tools.d.ts +32 -0
- package/dist/tools/advanced-tools.d.ts.map +1 -0
- package/dist/tools/advanced-tools.js +480 -0
- package/dist/tools/advanced-tools.js.map +1 -0
- package/dist/tools/change-tools.d.ts +76 -0
- package/dist/tools/change-tools.d.ts.map +1 -0
- package/dist/tools/change-tools.js +93 -0
- package/dist/tools/change-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +68 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +141 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/debug-tools.d.ts +25 -0
- package/dist/tools/debug-tools.d.ts.map +1 -0
- package/dist/tools/debug-tools.js +286 -0
- package/dist/tools/debug-tools.js.map +1 -0
- package/dist/tools/evolving-tools.d.ts +23 -0
- package/dist/tools/evolving-tools.d.ts.map +1 -0
- package/dist/tools/evolving-tools.js +207 -0
- package/dist/tools/evolving-tools.js.map +1 -0
- package/dist/tools/flow-tools.d.ts +24 -0
- package/dist/tools/flow-tools.d.ts.map +1 -0
- package/dist/tools/flow-tools.js +265 -0
- package/dist/tools/flow-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +71 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +165 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +62 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +195 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/smart-tools.d.ts +23 -0
- package/dist/tools/smart-tools.d.ts.map +1 -0
- package/dist/tools/smart-tools.js +482 -0
- package/dist/tools/smart-tools.js.map +1 -0
- package/dist/tools/snapshot-tools.d.ts +19 -0
- package/dist/tools/snapshot-tools.d.ts.map +1 -0
- package/dist/tools/snapshot-tools.js +149 -0
- package/dist/tools/snapshot-tools.js.map +1 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +45 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +142 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/token-counter.d.ts +51 -0
- package/dist/utils/token-counter.d.ts.map +1 -0
- package/dist/utils/token-counter.js +181 -0
- package/dist/utils/token-counter.js.map +1 -0
- package/install.ps1 +321 -0
- package/install.sh +345 -0
- package/package.json +94 -0
- 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
|