ai-first-cli 1.1.1 → 1.1.2
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/CHANGELOG.md +78 -0
- package/README.es.md +137 -1
- package/README.md +136 -4
- package/ai/ai_context.md +2 -2
- package/ai/architecture.md +3 -3
- package/ai/cache.json +85 -57
- package/ai/ccp/jira-123/context.json +7 -0
- package/ai/context/repo.json +56 -0
- package/ai/context/utils.json +7 -0
- package/ai/dependencies.json +51 -1026
- package/ai/files.json +195 -3
- package/ai/git/commit-activity.json +8646 -0
- package/ai/git/recent-features.json +1 -0
- package/ai/git/recent-files.json +52 -0
- package/ai/git/recent-flows.json +1 -0
- package/ai/graph/knowledge-graph.json +43643 -0
- package/ai/graph/module-graph.json +4 -0
- package/ai/graph/symbol-graph.json +3307 -879
- package/ai/graph/symbol-references.json +119 -32
- package/ai/index-state.json +843 -188
- package/ai/index.db +0 -0
- package/ai/modules.json +4 -0
- package/ai/repo-map.json +81 -17
- package/ai/repo_map.json +81 -17
- package/ai/repo_map.md +21 -7
- package/ai/summary.md +5 -5
- package/ai/symbols.json +1 -20287
- package/dist/analyzers/androidResources.d.ts +23 -0
- package/dist/analyzers/androidResources.d.ts.map +1 -0
- package/dist/analyzers/androidResources.js +93 -0
- package/dist/analyzers/androidResources.js.map +1 -0
- package/dist/analyzers/dependencies.d.ts.map +1 -1
- package/dist/analyzers/dependencies.js +37 -0
- package/dist/analyzers/dependencies.js.map +1 -1
- package/dist/analyzers/entrypoints.d.ts.map +1 -1
- package/dist/analyzers/entrypoints.js +71 -1
- package/dist/analyzers/entrypoints.js.map +1 -1
- package/dist/analyzers/gradleModules.d.ts +22 -0
- package/dist/analyzers/gradleModules.d.ts.map +1 -0
- package/dist/analyzers/gradleModules.js +75 -0
- package/dist/analyzers/gradleModules.js.map +1 -0
- package/dist/analyzers/techStack.d.ts +7 -0
- package/dist/analyzers/techStack.d.ts.map +1 -1
- package/dist/analyzers/techStack.js +44 -1
- package/dist/analyzers/techStack.js.map +1 -1
- package/dist/commands/ai-first.d.ts.map +1 -1
- package/dist/commands/ai-first.js +311 -1
- package/dist/commands/ai-first.js.map +1 -1
- package/dist/core/adapters/adapterRegistry.d.ts +39 -0
- package/dist/core/adapters/adapterRegistry.d.ts.map +1 -0
- package/dist/core/adapters/adapterRegistry.js +155 -0
- package/dist/core/adapters/adapterRegistry.js.map +1 -0
- package/dist/core/adapters/baseAdapter.d.ts +49 -0
- package/dist/core/adapters/baseAdapter.d.ts.map +1 -0
- package/dist/core/adapters/baseAdapter.js +28 -0
- package/dist/core/adapters/baseAdapter.js.map +1 -0
- package/dist/core/adapters/community/fastapiAdapter.d.ts +7 -0
- package/dist/core/adapters/community/fastapiAdapter.d.ts.map +1 -0
- package/dist/core/adapters/community/fastapiAdapter.js +40 -0
- package/dist/core/adapters/community/fastapiAdapter.js.map +1 -0
- package/dist/core/adapters/community/index.d.ts +11 -0
- package/dist/core/adapters/community/index.d.ts.map +1 -0
- package/dist/core/adapters/community/index.js +11 -0
- package/dist/core/adapters/community/index.js.map +1 -0
- package/dist/core/adapters/community/laravelAdapter.d.ts +7 -0
- package/dist/core/adapters/community/laravelAdapter.d.ts.map +1 -0
- package/dist/core/adapters/community/laravelAdapter.js +47 -0
- package/dist/core/adapters/community/laravelAdapter.js.map +1 -0
- package/dist/core/adapters/community/nestjsAdapter.d.ts +7 -0
- package/dist/core/adapters/community/nestjsAdapter.d.ts.map +1 -0
- package/dist/core/adapters/community/nestjsAdapter.js +48 -0
- package/dist/core/adapters/community/nestjsAdapter.js.map +1 -0
- package/dist/core/adapters/community/phoenixAdapter.d.ts +7 -0
- package/dist/core/adapters/community/phoenixAdapter.d.ts.map +1 -0
- package/dist/core/adapters/community/phoenixAdapter.js +45 -0
- package/dist/core/adapters/community/phoenixAdapter.js.map +1 -0
- package/dist/core/adapters/community/springBootAdapter.d.ts +7 -0
- package/dist/core/adapters/community/springBootAdapter.d.ts.map +1 -0
- package/dist/core/adapters/community/springBootAdapter.js +44 -0
- package/dist/core/adapters/community/springBootAdapter.js.map +1 -0
- package/dist/core/adapters/dotnetAdapter.d.ts +20 -0
- package/dist/core/adapters/dotnetAdapter.d.ts.map +1 -0
- package/dist/core/adapters/dotnetAdapter.js +86 -0
- package/dist/core/adapters/dotnetAdapter.js.map +1 -0
- package/dist/core/adapters/index.d.ts +18 -0
- package/dist/core/adapters/index.d.ts.map +1 -0
- package/dist/core/adapters/index.js +19 -0
- package/dist/core/adapters/index.js.map +1 -0
- package/dist/core/adapters/javascriptAdapter.d.ts +11 -0
- package/dist/core/adapters/javascriptAdapter.d.ts.map +1 -0
- package/dist/core/adapters/javascriptAdapter.js +47 -0
- package/dist/core/adapters/javascriptAdapter.js.map +1 -0
- package/dist/core/adapters/pythonAdapter.d.ts +20 -0
- package/dist/core/adapters/pythonAdapter.d.ts.map +1 -0
- package/dist/core/adapters/pythonAdapter.js +99 -0
- package/dist/core/adapters/pythonAdapter.js.map +1 -0
- package/dist/core/adapters/railsAdapter.d.ts +10 -0
- package/dist/core/adapters/railsAdapter.d.ts.map +1 -0
- package/dist/core/adapters/railsAdapter.js +52 -0
- package/dist/core/adapters/railsAdapter.js.map +1 -0
- package/dist/core/adapters/salesforceAdapter.d.ts +16 -0
- package/dist/core/adapters/salesforceAdapter.d.ts.map +1 -0
- package/dist/core/adapters/salesforceAdapter.js +64 -0
- package/dist/core/adapters/salesforceAdapter.js.map +1 -0
- package/dist/core/adapters/sdk.d.ts +83 -0
- package/dist/core/adapters/sdk.d.ts.map +1 -0
- package/dist/core/adapters/sdk.js +114 -0
- package/dist/core/adapters/sdk.js.map +1 -0
- package/dist/core/ccp.d.ts +37 -0
- package/dist/core/ccp.d.ts.map +1 -0
- package/dist/core/ccp.js +184 -0
- package/dist/core/ccp.js.map +1 -0
- package/dist/core/gitAnalyzer.d.ts +74 -0
- package/dist/core/gitAnalyzer.d.ts.map +1 -0
- package/dist/core/gitAnalyzer.js +298 -0
- package/dist/core/gitAnalyzer.js.map +1 -0
- package/dist/core/incrementalAnalyzer.d.ts +28 -0
- package/dist/core/incrementalAnalyzer.d.ts.map +1 -0
- package/dist/core/incrementalAnalyzer.js +343 -0
- package/dist/core/incrementalAnalyzer.js.map +1 -0
- package/dist/core/knowledgeGraphBuilder.d.ts +31 -0
- package/dist/core/knowledgeGraphBuilder.d.ts.map +1 -0
- package/dist/core/knowledgeGraphBuilder.js +197 -0
- package/dist/core/knowledgeGraphBuilder.js.map +1 -0
- package/dist/core/lazyAnalyzer.d.ts +57 -0
- package/dist/core/lazyAnalyzer.d.ts.map +1 -0
- package/dist/core/lazyAnalyzer.js +204 -0
- package/dist/core/lazyAnalyzer.js.map +1 -0
- package/dist/core/schema.d.ts +57 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +131 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/semanticContexts.d.ts +40 -0
- package/dist/core/semanticContexts.d.ts.map +1 -0
- package/dist/core/semanticContexts.js +454 -0
- package/dist/core/semanticContexts.js.map +1 -0
- package/docs/es/guide/adapters.md +143 -0
- package/docs/es/guide/ai-repository-schema.md +119 -0
- package/docs/es/guide/features.md +67 -0
- package/docs/es/guide/flows.md +134 -0
- package/docs/es/guide/git-intelligence.md +170 -0
- package/docs/es/guide/incremental-analysis.md +131 -0
- package/docs/es/guide/knowledge-graph.md +135 -0
- package/docs/es/guide/lazy-indexing.md +144 -0
- package/docs/es/guide/performance.md +125 -0
- package/docs/guide/adapters.md +225 -0
- package/docs/guide/ai-repository-schema.md +119 -0
- package/docs/guide/architecture.md +69 -1
- package/docs/guide/flows.md +134 -0
- package/docs/guide/git-intelligence.md +170 -0
- package/docs/guide/incremental-analysis.md +131 -0
- package/docs/guide/knowledge-graph.md +135 -0
- package/docs/guide/lazy-indexing.md +144 -0
- package/docs/guide/performance.md +125 -0
- package/package.json +5 -2
- package/src/analyzers/androidResources.ts +113 -0
- package/src/analyzers/dependencies.ts +41 -0
- package/src/analyzers/entrypoints.ts +80 -1
- package/src/analyzers/gradleModules.ts +100 -0
- package/src/analyzers/techStack.ts +56 -0
- package/src/commands/ai-first.ts +342 -1
- package/src/core/adapters/adapterRegistry.ts +187 -0
- package/src/core/adapters/baseAdapter.ts +82 -0
- package/src/core/adapters/community/fastapiAdapter.ts +50 -0
- package/src/core/adapters/community/index.ts +11 -0
- package/src/core/adapters/community/laravelAdapter.ts +56 -0
- package/src/core/adapters/community/nestjsAdapter.ts +57 -0
- package/src/core/adapters/community/phoenixAdapter.ts +54 -0
- package/src/core/adapters/community/springBootAdapter.ts +53 -0
- package/src/core/adapters/dotnetAdapter.ts +104 -0
- package/src/core/adapters/index.ts +24 -0
- package/src/core/adapters/javascriptAdapter.ts +56 -0
- package/src/core/adapters/pythonAdapter.ts +118 -0
- package/src/core/adapters/railsAdapter.ts +65 -0
- package/src/core/adapters/salesforceAdapter.ts +76 -0
- package/src/core/adapters/sdk.ts +172 -0
- package/src/core/ccp.ts +240 -0
- package/src/core/gitAnalyzer.ts +391 -0
- package/src/core/incrementalAnalyzer.ts +382 -0
- package/src/core/knowledgeGraphBuilder.ts +181 -0
- package/src/core/lazyAnalyzer.ts +261 -0
- package/src/core/schema.ts +157 -0
- package/src/core/semanticContexts.ts +575 -0
- package/tests/adapters.test.ts +159 -0
- package/tests/gitAnalyzer.test.ts +133 -0
- package/tests/incrementalAnalyzer.test.ts +83 -0
- package/tests/knowledgeGraph.test.ts +146 -0
- package/tests/lazyAnalyzer.test.ts +230 -0
- package/tests/schema.test.ts +203 -0
- package/tests/semanticContexts.test.ts +435 -0
- package/ai/context/analyzers.Symbol.json +0 -19
- package/ai/context/analyzers.extractSymbols.json +0 -19
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { ensureDir, writeFile, readJsonFile } from "../utils/fileUtils.js";
|
|
4
|
+
|
|
5
|
+
export interface Feature {
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
files: string[];
|
|
9
|
+
entrypoints: string[];
|
|
10
|
+
dependencies: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Flow {
|
|
14
|
+
name: string;
|
|
15
|
+
entrypoint: string;
|
|
16
|
+
files: string[];
|
|
17
|
+
depth: number;
|
|
18
|
+
layers: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SemanticContexts {
|
|
22
|
+
features: Feature[];
|
|
23
|
+
flows: Flow[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// CONFIGURATION - Feature Detection Rules
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
// 1. Candidate roots - scan inside these directories
|
|
31
|
+
const CANDIDATE_ROOTS = ["src", "app", "packages", "services", "modules", "features"];
|
|
32
|
+
|
|
33
|
+
// 2. Ignore folders - these are technical, not business features
|
|
34
|
+
const IGNORED_FOLDERS = new Set([
|
|
35
|
+
"utils",
|
|
36
|
+
"helpers",
|
|
37
|
+
"types",
|
|
38
|
+
"interfaces",
|
|
39
|
+
"constants",
|
|
40
|
+
"config",
|
|
41
|
+
"dto",
|
|
42
|
+
"models",
|
|
43
|
+
"common",
|
|
44
|
+
"shared"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// 3. Entrypoint patterns - files that indicate a business feature
|
|
48
|
+
const ENTRYPOINT_PATTERNS = [
|
|
49
|
+
"controller",
|
|
50
|
+
"route",
|
|
51
|
+
"handler",
|
|
52
|
+
"command",
|
|
53
|
+
"service"
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// 4. Flow-specific patterns
|
|
57
|
+
const FLOW_ENTRY_PATTERNS = ["controller", "route", "handler", "command"];
|
|
58
|
+
|
|
59
|
+
// 5. Flow exclusion patterns
|
|
60
|
+
const FLOW_EXCLUDE = new Set([
|
|
61
|
+
"repository", "repo", "utils", "helper", "model", "entity",
|
|
62
|
+
"dto", "type", "interface", "constant", "config"
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// 6. Layer detection
|
|
66
|
+
const LAYER_PATTERNS: Record<string, string[]> = {
|
|
67
|
+
api: ["controller", "handler", "route", "router", "api", "endpoint"],
|
|
68
|
+
service: ["service", "services", "usecase", "interactor"],
|
|
69
|
+
data: ["repository", "repo", "dal", "dao", "data", "persistence"],
|
|
70
|
+
domain: ["model", "entity", "schema", "domain"],
|
|
71
|
+
util: ["util", "helper", "lib", "common"]
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const LAYER_PRIORITY: Record<string, number> = {
|
|
75
|
+
api: 1, controller: 1, handler: 1, route: 1, router: 1,
|
|
76
|
+
service: 2, usecase: 2, interactor: 2,
|
|
77
|
+
data: 3, repository: 3, repo: 3, dal: 3, dao: 3, persistence: 3,
|
|
78
|
+
model: 4, entity: 4, domain: 4,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const MAX_FLOW_DEPTH = 5;
|
|
82
|
+
const MAX_FLOW_FILES = 30;
|
|
83
|
+
|
|
84
|
+
// Supported source file extensions
|
|
85
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
86
|
+
".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".kt", ".go", ".rs", ".rb", ".php", ".cs", ".vue", ".svelte"
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// HELPER FUNCTIONS
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
function isSourceFile(filePath: string): boolean {
|
|
94
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
95
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isEntrypoint(filePath: string): boolean {
|
|
99
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
100
|
+
return ENTRYPOINT_PATTERNS.some(pattern => basename.includes(pattern));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isFlowEntrypoint(filePath: string): boolean {
|
|
104
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
105
|
+
return FLOW_ENTRY_PATTERNS.some(pattern => basename.includes(pattern));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isIgnoredFolder(folderName: string): boolean {
|
|
109
|
+
return IGNORED_FOLDERS.has(folderName.toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isFlowExcluded(filePath: string): boolean {
|
|
113
|
+
const lower = filePath.toLowerCase();
|
|
114
|
+
return Array.from(FLOW_EXCLUDE).some(p => lower.includes("/" + p) || lower.includes("\\" + p));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getLayer(filePath: string): string {
|
|
118
|
+
const parts = filePath.split(/[/\\]/).map(s =>
|
|
119
|
+
s.toLowerCase().replace(/\.(ts|js|tsx|jsx)$/, "")
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
for (const [layer, patterns] of Object.entries(LAYER_PATTERNS)) {
|
|
123
|
+
if (parts.some(s => patterns.includes(s))) {
|
|
124
|
+
return layer;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return "unknown";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getLayerPriority(filePath: string): number {
|
|
131
|
+
const parts = [...filePath.split(/[/\\]/)].reverse();
|
|
132
|
+
for (const p of parts) {
|
|
133
|
+
const name = p.replace(/\.(ts|js|tsx|jsx)$/, "").toLowerCase();
|
|
134
|
+
if (LAYER_PRIORITY[name] !== undefined) {
|
|
135
|
+
return LAYER_PRIORITY[name];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return 99;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================
|
|
142
|
+
// FEATURE DETECTION ALGORITHM
|
|
143
|
+
// ============================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Find feature candidates by scanning file paths
|
|
147
|
+
*
|
|
148
|
+
* Rules:
|
|
149
|
+
* - Scan inside: src/*, app/*, packages/*, services/*, modules/*, features/*
|
|
150
|
+
* - Ignore: utils, helpers, types, interfaces, constants, config, dto, models, common, shared
|
|
151
|
+
* - Support depth 1 and 2: src/auth, src/modules/auth
|
|
152
|
+
* - Must have ≥3 source files and ≥1 entrypoint
|
|
153
|
+
*/
|
|
154
|
+
function findFeatureCandidates(files: string[]): Map<string, string[]> {
|
|
155
|
+
const candidates = new Map<string, string[]>();
|
|
156
|
+
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
const parts = file.split("/");
|
|
159
|
+
|
|
160
|
+
// Find candidate root index
|
|
161
|
+
const rootIdx = parts.findIndex(p =>
|
|
162
|
+
CANDIDATE_ROOTS.includes(p.toLowerCase())
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (rootIdx === -1) continue;
|
|
166
|
+
|
|
167
|
+
// Check depth 1: src/auth
|
|
168
|
+
for (let depth = 1; depth <= 2; depth++) {
|
|
169
|
+
const featureIdx = rootIdx + depth;
|
|
170
|
+
|
|
171
|
+
// Don't go beyond array bounds
|
|
172
|
+
if (featureIdx >= parts.length - 1) continue;
|
|
173
|
+
|
|
174
|
+
const featureName = parts[featureIdx];
|
|
175
|
+
|
|
176
|
+
// Skip ignored folders
|
|
177
|
+
if (isIgnoredFolder(featureName)) continue;
|
|
178
|
+
|
|
179
|
+
// Skip if feature name is a file (depth too deep)
|
|
180
|
+
if (featureName.includes(".")) continue;
|
|
181
|
+
|
|
182
|
+
// Build feature path
|
|
183
|
+
const featurePath = parts.slice(0, featureIdx + 1).join("/");
|
|
184
|
+
|
|
185
|
+
if (!candidates.has(featurePath)) {
|
|
186
|
+
candidates.set(featurePath, []);
|
|
187
|
+
}
|
|
188
|
+
candidates.get(featurePath)!.push(file);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return candidates;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract dependencies from modules.json for a feature
|
|
197
|
+
*/
|
|
198
|
+
function getFeatureDependencies(
|
|
199
|
+
featurePath: string,
|
|
200
|
+
modules: Record<string, { path: string; files: string[] }>
|
|
201
|
+
): string[] {
|
|
202
|
+
const deps: string[] = [];
|
|
203
|
+
const featureFiles = Object.values(modules).find(m => m.path === featurePath)?.files || [];
|
|
204
|
+
|
|
205
|
+
// Get all other modules and check if any of their files import from feature files
|
|
206
|
+
for (const [modName, mod] of Object.entries(modules)) {
|
|
207
|
+
if (mod.path === featurePath) continue;
|
|
208
|
+
|
|
209
|
+
for (const file of mod.files || []) {
|
|
210
|
+
// Simple heuristic: if file path contains feature name, it's related
|
|
211
|
+
const featureName = featurePath.split("/").pop() || "";
|
|
212
|
+
if (file.toLowerCase().includes(featureName.toLowerCase())) {
|
|
213
|
+
if (!deps.includes(modName)) {
|
|
214
|
+
deps.push(modName);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return deps;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generate features from modules.json
|
|
225
|
+
*
|
|
226
|
+
* Output format:
|
|
227
|
+
* {
|
|
228
|
+
* "name": "auth",
|
|
229
|
+
* "path": "src/auth",
|
|
230
|
+
* "files": [],
|
|
231
|
+
* "entrypoints": [],
|
|
232
|
+
* "dependencies": []
|
|
233
|
+
* }
|
|
234
|
+
*/
|
|
235
|
+
export function generateFeatures(
|
|
236
|
+
modulesJsonPath: string,
|
|
237
|
+
_symbolsJsonPath: string
|
|
238
|
+
): Feature[] {
|
|
239
|
+
const features: Feature[] = [];
|
|
240
|
+
|
|
241
|
+
// Load modules
|
|
242
|
+
let modules: Record<string, { path: string; files: string[] }> = {};
|
|
243
|
+
try {
|
|
244
|
+
if (fs.existsSync(modulesJsonPath)) {
|
|
245
|
+
const data = readJsonFile(modulesJsonPath);
|
|
246
|
+
modules = (data as any).modules || {};
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {
|
|
249
|
+
// Ignore errors, return empty
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get all files from modules
|
|
253
|
+
const allFiles = Object.values(modules).flatMap(m => m.files || []);
|
|
254
|
+
|
|
255
|
+
// Find candidate features
|
|
256
|
+
const candidates = findFeatureCandidates(allFiles);
|
|
257
|
+
|
|
258
|
+
// Filter and validate candidates
|
|
259
|
+
for (const [featurePath, files] of candidates) {
|
|
260
|
+
// Filter to source files only
|
|
261
|
+
const sourceFiles = files.filter(isSourceFile);
|
|
262
|
+
|
|
263
|
+
// Must have at least 3 source files
|
|
264
|
+
if (sourceFiles.length < 3) continue;
|
|
265
|
+
|
|
266
|
+
// Must have at least one entrypoint
|
|
267
|
+
const entrypoints = sourceFiles.filter(isEntrypoint);
|
|
268
|
+
if (entrypoints.length === 0) continue;
|
|
269
|
+
|
|
270
|
+
// Extract feature name from path
|
|
271
|
+
const featureName = featurePath.split("/").pop() || featurePath;
|
|
272
|
+
|
|
273
|
+
// Get dependencies
|
|
274
|
+
const dependencies = getFeatureDependencies(featurePath, modules);
|
|
275
|
+
|
|
276
|
+
features.push({
|
|
277
|
+
name: featureName,
|
|
278
|
+
path: featurePath,
|
|
279
|
+
files: sourceFiles.slice(0, 50),
|
|
280
|
+
entrypoints: entrypoints.slice(0, 10),
|
|
281
|
+
dependencies
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return features;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// FLOW DETECTION ALGORITHM
|
|
290
|
+
// ============================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Generate flows from symbol graph
|
|
294
|
+
*/
|
|
295
|
+
function flowsFromGraph(graphData: any): Flow[] {
|
|
296
|
+
const flows: Flow[] = [];
|
|
297
|
+
const { symbols = [], relationships = [] } = graphData;
|
|
298
|
+
|
|
299
|
+
// Build symbol lookup
|
|
300
|
+
const bySymbol: Record<string, any[]> = {};
|
|
301
|
+
for (const r of relationships) {
|
|
302
|
+
if (!bySymbol[r.symbolId]) bySymbol[r.symbolId] = [];
|
|
303
|
+
bySymbol[r.symbolId].push(r);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find entrypoint symbols
|
|
307
|
+
const entrypoints = symbols.filter((s: any) =>
|
|
308
|
+
s.file && isFlowEntrypoint(s.file)
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
for (const ep of entrypoints) {
|
|
312
|
+
const visited = new Set<string>();
|
|
313
|
+
const fileSet = new Set<string>();
|
|
314
|
+
const layerSet = new Set<string>();
|
|
315
|
+
|
|
316
|
+
const traverse = (symbolId: string, depth: number) => {
|
|
317
|
+
if (depth > MAX_FLOW_DEPTH || visited.has(symbolId) || fileSet.size >= MAX_FLOW_FILES) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
visited.add(symbolId);
|
|
322
|
+
|
|
323
|
+
const symbol = symbols.find((s: any) => s.id === symbolId);
|
|
324
|
+
if (!symbol?.file || isFlowExcluded(symbol.file)) return;
|
|
325
|
+
|
|
326
|
+
fileSet.add(symbol.file);
|
|
327
|
+
layerSet.add(getLayer(symbol.file));
|
|
328
|
+
|
|
329
|
+
// Follow relationships
|
|
330
|
+
for (const r of bySymbol[symbolId] || []) {
|
|
331
|
+
if (["calls", "imports", "references"].includes(r.type)) {
|
|
332
|
+
traverse(r.targetId, depth + 1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
if (ep.file) {
|
|
338
|
+
fileSet.add(ep.file);
|
|
339
|
+
layerSet.add(getLayer(ep.file));
|
|
340
|
+
|
|
341
|
+
for (const r of bySymbol[ep.id] || []) {
|
|
342
|
+
if (["calls", "imports", "references"].includes(r.type)) {
|
|
343
|
+
traverse(r.targetId, 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Must have at least 3 files and 2 layers
|
|
349
|
+
if (fileSet.size >= 3 && layerSet.size >= 2) {
|
|
350
|
+
flows.push({
|
|
351
|
+
name: path.basename(ep.file || ""),
|
|
352
|
+
entrypoint: ep.file || "",
|
|
353
|
+
files: Array.from(fileSet).slice(0, MAX_FLOW_FILES),
|
|
354
|
+
depth: Math.min(MAX_FLOW_DEPTH, [...visited].length),
|
|
355
|
+
layers: Array.from(layerSet)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return flows;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Generate flows from folder structure (fallback)
|
|
365
|
+
*/
|
|
366
|
+
function flowsFromFolders(files: string[]): Flow[] {
|
|
367
|
+
const flows: Flow[] = [];
|
|
368
|
+
|
|
369
|
+
// Group by feature prefix (e.g., authController -> auth)
|
|
370
|
+
const byFeature = new Map<string, string[]>();
|
|
371
|
+
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
if (!isSourceFile(file)) continue;
|
|
374
|
+
|
|
375
|
+
const baseName = path.basename(file).replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
376
|
+
// Extract feature: authController -> auth, userService -> user
|
|
377
|
+
const feature = baseName.replace(/(Controller|Service|Repository|Handler|Route|Model|Entity)$/i, "");
|
|
378
|
+
const key = feature.toLowerCase();
|
|
379
|
+
|
|
380
|
+
if (!byFeature.has(key)) {
|
|
381
|
+
byFeature.set(key, []);
|
|
382
|
+
}
|
|
383
|
+
byFeature.get(key)!.push(file);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const [feature, featureFiles] of byFeature) {
|
|
387
|
+
if (featureFiles.length < 3) continue;
|
|
388
|
+
|
|
389
|
+
const layers = new Set(featureFiles.map(getLayer).filter(l => l !== "unknown"));
|
|
390
|
+
if (layers.size < 2) continue;
|
|
391
|
+
|
|
392
|
+
const sorted = [...featureFiles].sort((a, b) =>
|
|
393
|
+
getLayerPriority(a) - getLayerPriority(b)
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const entrypoint = sorted.find(isFlowEntrypoint) || sorted[0];
|
|
397
|
+
|
|
398
|
+
flows.push({
|
|
399
|
+
name: feature,
|
|
400
|
+
entrypoint,
|
|
401
|
+
files: sorted.slice(0, MAX_FLOW_FILES),
|
|
402
|
+
depth: layers.size,
|
|
403
|
+
layers: Array.from(layers)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return flows;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Generate flows from dependencies (fallback)
|
|
412
|
+
*/
|
|
413
|
+
function flowsFromImports(
|
|
414
|
+
dependenciesPath: string,
|
|
415
|
+
files: string[]
|
|
416
|
+
): Flow[] {
|
|
417
|
+
const flows: Flow[] = [];
|
|
418
|
+
|
|
419
|
+
let deps: any = { byFile: {} };
|
|
420
|
+
try {
|
|
421
|
+
if (fs.existsSync(dependenciesPath)) {
|
|
422
|
+
deps = readJsonFile(dependenciesPath);
|
|
423
|
+
}
|
|
424
|
+
} catch {
|
|
425
|
+
return flows;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Build import graph
|
|
429
|
+
const importsTo = new Map<string, Set<string>>();
|
|
430
|
+
for (const [file, imports] of Object.entries(deps.byFile || {})) {
|
|
431
|
+
if (!importsTo.has(file)) {
|
|
432
|
+
importsTo.set(file, new Set());
|
|
433
|
+
}
|
|
434
|
+
for (const imp of imports as string[]) {
|
|
435
|
+
importsTo.get(file)!.add(imp);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Find entrypoints
|
|
440
|
+
const entrypoints = files.filter(isFlowEntrypoint);
|
|
441
|
+
|
|
442
|
+
for (const ep of entrypoints) {
|
|
443
|
+
const visited = new Set<string>([ep]);
|
|
444
|
+
const fileSet = new Set<string>([ep]);
|
|
445
|
+
const layerSet = new Set<string>();
|
|
446
|
+
|
|
447
|
+
const traverse = (file: string, depth: number) => {
|
|
448
|
+
if (depth > MAX_FLOW_DEPTH || fileSet.size >= MAX_FLOW_FILES) return;
|
|
449
|
+
|
|
450
|
+
for (const imp of importsTo.get(file) || []) {
|
|
451
|
+
if (visited.has(imp) || isFlowExcluded(imp)) continue;
|
|
452
|
+
|
|
453
|
+
visited.add(imp);
|
|
454
|
+
fileSet.add(imp);
|
|
455
|
+
layerSet.add(getLayer(imp));
|
|
456
|
+
traverse(imp, depth + 1);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
traverse(ep, 1);
|
|
461
|
+
|
|
462
|
+
if (fileSet.size >= 3 && layerSet.size >= 2) {
|
|
463
|
+
flows.push({
|
|
464
|
+
name: path.basename(ep, path.extname(ep)),
|
|
465
|
+
entrypoint: ep,
|
|
466
|
+
files: Array.from(fileSet),
|
|
467
|
+
depth: Math.min(MAX_FLOW_DEPTH, [...visited].length),
|
|
468
|
+
layers: Array.from(layerSet)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return flows;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Generate flows using multiple fallback methods
|
|
478
|
+
*/
|
|
479
|
+
export function generateFlows(
|
|
480
|
+
graphPath: string,
|
|
481
|
+
modulesPath: string,
|
|
482
|
+
dependenciesPath?: string
|
|
483
|
+
): Flow[] {
|
|
484
|
+
// Load graph data
|
|
485
|
+
let graphData: any = { symbols: [], relationships: [] };
|
|
486
|
+
try {
|
|
487
|
+
if (fs.existsSync(graphPath)) {
|
|
488
|
+
graphData = readJsonFile(graphPath);
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// Ignore
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Load modules
|
|
495
|
+
let modules: Record<string, { path: string; files: string[] }> = {};
|
|
496
|
+
try {
|
|
497
|
+
if (fs.existsSync(modulesPath)) {
|
|
498
|
+
modules = (readJsonFile(modulesPath) as any).modules || {};
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// Ignore
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const allFiles = Object.values(modules).flatMap(m => m.files || []);
|
|
505
|
+
|
|
506
|
+
// Determine if graph is weak
|
|
507
|
+
const relationshipCount = graphData.relationships?.length || 0;
|
|
508
|
+
const symbolCount = graphData.symbols?.length || 1;
|
|
509
|
+
const density = relationshipCount / symbolCount;
|
|
510
|
+
const isWeakGraph = density < 0.5 || relationshipCount < 10;
|
|
511
|
+
|
|
512
|
+
const flows: Flow[] = [];
|
|
513
|
+
|
|
514
|
+
// Try graph-based flow detection first (if graph is strong)
|
|
515
|
+
if (!isWeakGraph && relationshipCount > 0) {
|
|
516
|
+
flows.push(...flowsFromGraph(graphData));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Fallback to folder-based detection
|
|
520
|
+
if (flows.length === 0 || isWeakGraph) {
|
|
521
|
+
flows.push(...flowsFromFolders(allFiles));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Fallback to import-based detection
|
|
525
|
+
if (flows.length === 0 || isWeakGraph) {
|
|
526
|
+
flows.push(...flowsFromImports(dependenciesPath || "", allFiles));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Deduplicate by name
|
|
530
|
+
const seen = new Set<string>();
|
|
531
|
+
return flows
|
|
532
|
+
.filter(f => {
|
|
533
|
+
if (seen.has(f.name)) return false;
|
|
534
|
+
seen.add(f.name);
|
|
535
|
+
return true;
|
|
536
|
+
})
|
|
537
|
+
.slice(0, 20);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ============================================================
|
|
541
|
+
// MAIN ENTRY POINT
|
|
542
|
+
// ============================================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Generate all semantic contexts (features and flows)
|
|
546
|
+
*/
|
|
547
|
+
export function generateSemanticContexts(aiDir: string): SemanticContexts {
|
|
548
|
+
const featuresDir = path.join(aiDir, "context", "features");
|
|
549
|
+
const flowsDir = path.join(aiDir, "context", "flows");
|
|
550
|
+
|
|
551
|
+
const modulesPath = path.join(aiDir, "modules.json");
|
|
552
|
+
const symbolsPath = path.join(aiDir, "symbols.json");
|
|
553
|
+
const graphPath = path.join(aiDir, "graph", "symbol-graph.json");
|
|
554
|
+
const dependenciesPath = path.join(aiDir, "dependencies.json");
|
|
555
|
+
|
|
556
|
+
// Generate features and flows
|
|
557
|
+
const features = generateFeatures(modulesPath, symbolsPath);
|
|
558
|
+
const flows = generateFlows(graphPath, modulesPath, dependenciesPath);
|
|
559
|
+
|
|
560
|
+
// Write features
|
|
561
|
+
ensureDir(featuresDir);
|
|
562
|
+
for (const feature of features) {
|
|
563
|
+
const filePath = path.join(featuresDir, `${feature.name}.json`);
|
|
564
|
+
writeFile(filePath, JSON.stringify(feature, null, 2));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Write flows
|
|
568
|
+
ensureDir(flowsDir);
|
|
569
|
+
for (const flow of flows) {
|
|
570
|
+
const filePath = path.join(flowsDir, `${flow.name}.json`);
|
|
571
|
+
writeFile(filePath, JSON.stringify(flow, null, 2));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return { features, flows };
|
|
575
|
+
}
|