ai-first-cli 1.1.1 → 1.1.3

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 (193) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.es.md +137 -1
  3. package/README.md +136 -4
  4. package/ai/ai_context.md +2 -2
  5. package/ai/architecture.md +3 -3
  6. package/ai/cache.json +85 -57
  7. package/ai/ccp/jira-123/context.json +7 -0
  8. package/ai/context/repo.json +56 -0
  9. package/ai/context/utils.json +7 -0
  10. package/ai/dependencies.json +51 -1026
  11. package/ai/files.json +195 -3
  12. package/ai/git/commit-activity.json +8646 -0
  13. package/ai/git/recent-features.json +1 -0
  14. package/ai/git/recent-files.json +52 -0
  15. package/ai/git/recent-flows.json +1 -0
  16. package/ai/graph/knowledge-graph.json +43643 -0
  17. package/ai/graph/module-graph.json +4 -0
  18. package/ai/graph/symbol-graph.json +3307 -879
  19. package/ai/graph/symbol-references.json +119 -32
  20. package/ai/index-state.json +843 -188
  21. package/ai/index.db +0 -0
  22. package/ai/modules.json +4 -0
  23. package/ai/repo-map.json +81 -17
  24. package/ai/repo_map.json +81 -17
  25. package/ai/repo_map.md +21 -7
  26. package/ai/summary.md +5 -5
  27. package/ai/symbols.json +1 -20287
  28. package/dist/analyzers/androidResources.d.ts +23 -0
  29. package/dist/analyzers/androidResources.d.ts.map +1 -0
  30. package/dist/analyzers/androidResources.js +93 -0
  31. package/dist/analyzers/androidResources.js.map +1 -0
  32. package/dist/analyzers/dependencies.d.ts.map +1 -1
  33. package/dist/analyzers/dependencies.js +37 -0
  34. package/dist/analyzers/dependencies.js.map +1 -1
  35. package/dist/analyzers/entrypoints.d.ts.map +1 -1
  36. package/dist/analyzers/entrypoints.js +71 -1
  37. package/dist/analyzers/entrypoints.js.map +1 -1
  38. package/dist/analyzers/gradleModules.d.ts +22 -0
  39. package/dist/analyzers/gradleModules.d.ts.map +1 -0
  40. package/dist/analyzers/gradleModules.js +75 -0
  41. package/dist/analyzers/gradleModules.js.map +1 -0
  42. package/dist/analyzers/techStack.d.ts +7 -0
  43. package/dist/analyzers/techStack.d.ts.map +1 -1
  44. package/dist/analyzers/techStack.js +44 -1
  45. package/dist/analyzers/techStack.js.map +1 -1
  46. package/dist/commands/ai-first.d.ts +1 -0
  47. package/dist/commands/ai-first.d.ts.map +1 -1
  48. package/dist/commands/ai-first.js +312 -1
  49. package/dist/commands/ai-first.js.map +1 -1
  50. package/dist/core/adapters/adapterRegistry.d.ts +39 -0
  51. package/dist/core/adapters/adapterRegistry.d.ts.map +1 -0
  52. package/dist/core/adapters/adapterRegistry.js +155 -0
  53. package/dist/core/adapters/adapterRegistry.js.map +1 -0
  54. package/dist/core/adapters/baseAdapter.d.ts +49 -0
  55. package/dist/core/adapters/baseAdapter.d.ts.map +1 -0
  56. package/dist/core/adapters/baseAdapter.js +28 -0
  57. package/dist/core/adapters/baseAdapter.js.map +1 -0
  58. package/dist/core/adapters/community/fastapiAdapter.d.ts +7 -0
  59. package/dist/core/adapters/community/fastapiAdapter.d.ts.map +1 -0
  60. package/dist/core/adapters/community/fastapiAdapter.js +40 -0
  61. package/dist/core/adapters/community/fastapiAdapter.js.map +1 -0
  62. package/dist/core/adapters/community/index.d.ts +11 -0
  63. package/dist/core/adapters/community/index.d.ts.map +1 -0
  64. package/dist/core/adapters/community/index.js +11 -0
  65. package/dist/core/adapters/community/index.js.map +1 -0
  66. package/dist/core/adapters/community/laravelAdapter.d.ts +7 -0
  67. package/dist/core/adapters/community/laravelAdapter.d.ts.map +1 -0
  68. package/dist/core/adapters/community/laravelAdapter.js +47 -0
  69. package/dist/core/adapters/community/laravelAdapter.js.map +1 -0
  70. package/dist/core/adapters/community/nestjsAdapter.d.ts +7 -0
  71. package/dist/core/adapters/community/nestjsAdapter.d.ts.map +1 -0
  72. package/dist/core/adapters/community/nestjsAdapter.js +48 -0
  73. package/dist/core/adapters/community/nestjsAdapter.js.map +1 -0
  74. package/dist/core/adapters/community/phoenixAdapter.d.ts +7 -0
  75. package/dist/core/adapters/community/phoenixAdapter.d.ts.map +1 -0
  76. package/dist/core/adapters/community/phoenixAdapter.js +45 -0
  77. package/dist/core/adapters/community/phoenixAdapter.js.map +1 -0
  78. package/dist/core/adapters/community/springBootAdapter.d.ts +7 -0
  79. package/dist/core/adapters/community/springBootAdapter.d.ts.map +1 -0
  80. package/dist/core/adapters/community/springBootAdapter.js +44 -0
  81. package/dist/core/adapters/community/springBootAdapter.js.map +1 -0
  82. package/dist/core/adapters/dotnetAdapter.d.ts +20 -0
  83. package/dist/core/adapters/dotnetAdapter.d.ts.map +1 -0
  84. package/dist/core/adapters/dotnetAdapter.js +86 -0
  85. package/dist/core/adapters/dotnetAdapter.js.map +1 -0
  86. package/dist/core/adapters/index.d.ts +18 -0
  87. package/dist/core/adapters/index.d.ts.map +1 -0
  88. package/dist/core/adapters/index.js +19 -0
  89. package/dist/core/adapters/index.js.map +1 -0
  90. package/dist/core/adapters/javascriptAdapter.d.ts +11 -0
  91. package/dist/core/adapters/javascriptAdapter.d.ts.map +1 -0
  92. package/dist/core/adapters/javascriptAdapter.js +47 -0
  93. package/dist/core/adapters/javascriptAdapter.js.map +1 -0
  94. package/dist/core/adapters/pythonAdapter.d.ts +20 -0
  95. package/dist/core/adapters/pythonAdapter.d.ts.map +1 -0
  96. package/dist/core/adapters/pythonAdapter.js +99 -0
  97. package/dist/core/adapters/pythonAdapter.js.map +1 -0
  98. package/dist/core/adapters/railsAdapter.d.ts +10 -0
  99. package/dist/core/adapters/railsAdapter.d.ts.map +1 -0
  100. package/dist/core/adapters/railsAdapter.js +52 -0
  101. package/dist/core/adapters/railsAdapter.js.map +1 -0
  102. package/dist/core/adapters/salesforceAdapter.d.ts +16 -0
  103. package/dist/core/adapters/salesforceAdapter.d.ts.map +1 -0
  104. package/dist/core/adapters/salesforceAdapter.js +64 -0
  105. package/dist/core/adapters/salesforceAdapter.js.map +1 -0
  106. package/dist/core/adapters/sdk.d.ts +83 -0
  107. package/dist/core/adapters/sdk.d.ts.map +1 -0
  108. package/dist/core/adapters/sdk.js +114 -0
  109. package/dist/core/adapters/sdk.js.map +1 -0
  110. package/dist/core/ccp.d.ts +37 -0
  111. package/dist/core/ccp.d.ts.map +1 -0
  112. package/dist/core/ccp.js +184 -0
  113. package/dist/core/ccp.js.map +1 -0
  114. package/dist/core/gitAnalyzer.d.ts +74 -0
  115. package/dist/core/gitAnalyzer.d.ts.map +1 -0
  116. package/dist/core/gitAnalyzer.js +298 -0
  117. package/dist/core/gitAnalyzer.js.map +1 -0
  118. package/dist/core/incrementalAnalyzer.d.ts +28 -0
  119. package/dist/core/incrementalAnalyzer.d.ts.map +1 -0
  120. package/dist/core/incrementalAnalyzer.js +343 -0
  121. package/dist/core/incrementalAnalyzer.js.map +1 -0
  122. package/dist/core/knowledgeGraphBuilder.d.ts +31 -0
  123. package/dist/core/knowledgeGraphBuilder.d.ts.map +1 -0
  124. package/dist/core/knowledgeGraphBuilder.js +197 -0
  125. package/dist/core/knowledgeGraphBuilder.js.map +1 -0
  126. package/dist/core/lazyAnalyzer.d.ts +57 -0
  127. package/dist/core/lazyAnalyzer.d.ts.map +1 -0
  128. package/dist/core/lazyAnalyzer.js +204 -0
  129. package/dist/core/lazyAnalyzer.js.map +1 -0
  130. package/dist/core/schema.d.ts +57 -0
  131. package/dist/core/schema.d.ts.map +1 -0
  132. package/dist/core/schema.js +131 -0
  133. package/dist/core/schema.js.map +1 -0
  134. package/dist/core/semanticContexts.d.ts +40 -0
  135. package/dist/core/semanticContexts.d.ts.map +1 -0
  136. package/dist/core/semanticContexts.js +454 -0
  137. package/dist/core/semanticContexts.js.map +1 -0
  138. package/docs/es/guide/adapters.md +143 -0
  139. package/docs/es/guide/ai-repository-schema.md +119 -0
  140. package/docs/es/guide/features.md +67 -0
  141. package/docs/es/guide/flows.md +134 -0
  142. package/docs/es/guide/git-intelligence.md +170 -0
  143. package/docs/es/guide/incremental-analysis.md +131 -0
  144. package/docs/es/guide/knowledge-graph.md +135 -0
  145. package/docs/es/guide/lazy-indexing.md +144 -0
  146. package/docs/es/guide/performance.md +125 -0
  147. package/docs/guide/adapters.md +225 -0
  148. package/docs/guide/ai-repository-schema.md +119 -0
  149. package/docs/guide/architecture.md +69 -1
  150. package/docs/guide/flows.md +134 -0
  151. package/docs/guide/git-intelligence.md +170 -0
  152. package/docs/guide/incremental-analysis.md +131 -0
  153. package/docs/guide/knowledge-graph.md +135 -0
  154. package/docs/guide/lazy-indexing.md +144 -0
  155. package/docs/guide/performance.md +125 -0
  156. package/package.json +5 -2
  157. package/src/analyzers/androidResources.ts +113 -0
  158. package/src/analyzers/dependencies.ts +41 -0
  159. package/src/analyzers/entrypoints.ts +80 -1
  160. package/src/analyzers/gradleModules.ts +100 -0
  161. package/src/analyzers/techStack.ts +56 -0
  162. package/src/commands/ai-first.ts +343 -1
  163. package/src/core/adapters/adapterRegistry.ts +187 -0
  164. package/src/core/adapters/baseAdapter.ts +82 -0
  165. package/src/core/adapters/community/fastapiAdapter.ts +50 -0
  166. package/src/core/adapters/community/index.ts +11 -0
  167. package/src/core/adapters/community/laravelAdapter.ts +56 -0
  168. package/src/core/adapters/community/nestjsAdapter.ts +57 -0
  169. package/src/core/adapters/community/phoenixAdapter.ts +54 -0
  170. package/src/core/adapters/community/springBootAdapter.ts +53 -0
  171. package/src/core/adapters/dotnetAdapter.ts +104 -0
  172. package/src/core/adapters/index.ts +24 -0
  173. package/src/core/adapters/javascriptAdapter.ts +56 -0
  174. package/src/core/adapters/pythonAdapter.ts +118 -0
  175. package/src/core/adapters/railsAdapter.ts +65 -0
  176. package/src/core/adapters/salesforceAdapter.ts +76 -0
  177. package/src/core/adapters/sdk.ts +172 -0
  178. package/src/core/ccp.ts +240 -0
  179. package/src/core/gitAnalyzer.ts +391 -0
  180. package/src/core/incrementalAnalyzer.ts +382 -0
  181. package/src/core/knowledgeGraphBuilder.ts +181 -0
  182. package/src/core/lazyAnalyzer.ts +261 -0
  183. package/src/core/schema.ts +157 -0
  184. package/src/core/semanticContexts.ts +575 -0
  185. package/tests/adapters.test.ts +159 -0
  186. package/tests/gitAnalyzer.test.ts +133 -0
  187. package/tests/incrementalAnalyzer.test.ts +83 -0
  188. package/tests/knowledgeGraph.test.ts +146 -0
  189. package/tests/lazyAnalyzer.test.ts +230 -0
  190. package/tests/schema.test.ts +203 -0
  191. package/tests/semanticContexts.test.ts +435 -0
  192. package/ai/context/analyzers.Symbol.json +0 -19
  193. package/ai/context/analyzers.extractSymbols.json +0 -19
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Incremental Analyzer
3
+ *
4
+ * Performs incremental updates to repository intelligence when files change,
5
+ * without requiring a full re-analysis.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { execSync } from "child_process";
11
+ import { writeFile, readJsonFile } from "../utils/fileUtils.js";
12
+ import { computeFileHash, loadIndexState, saveIndexState, FileState } from "./indexState.js";
13
+ import { extractSymbols } from "../analyzers/symbols.js";
14
+ import { FileInfo } from "./repoScanner.js";
15
+ import { buildKnowledgeGraph } from "./knowledgeGraphBuilder.js";
16
+
17
+ export interface ChangedFile {
18
+ path: string;
19
+ status: "added" | "modified" | "deleted";
20
+ hash?: string;
21
+ }
22
+
23
+ export interface IncrementalUpdateResult {
24
+ changedFiles: ChangedFile[];
25
+ updatedSymbols: number;
26
+ updatedDependencies: number;
27
+ updatedFeatures: string[];
28
+ updatedFlows: string[];
29
+ graphUpdated: boolean;
30
+ errors: string[];
31
+ }
32
+
33
+ // ============================================================
34
+ // File Change Detection
35
+ // ============================================================
36
+
37
+ export function detectChangedFiles(rootDir: string, useGit: boolean = true): ChangedFile[] {
38
+ if (useGit && isGitRepository(rootDir)) {
39
+ return detectChangesWithGit(rootDir);
40
+ }
41
+ return detectChangesWithTimestamps(rootDir);
42
+ }
43
+
44
+ function isGitRepository(rootDir: string): boolean {
45
+ return fs.existsSync(path.join(rootDir, ".git"));
46
+ }
47
+
48
+ function detectChangesWithGit(rootDir: string): ChangedFile[] {
49
+ const changes: ChangedFile[] = [];
50
+
51
+ try {
52
+ const output = execSync("git diff --name-status HEAD", {
53
+ cwd: rootDir,
54
+ encoding: "utf-8"
55
+ });
56
+
57
+ for (const line of output.split("\n")) {
58
+ if (!line.trim()) continue;
59
+ const [status, filePath] = line.split("\t").map(s => s.trim());
60
+ if (!filePath) continue;
61
+
62
+ let changeStatus: "added" | "modified" | "deleted" = "modified";
63
+ if (status.startsWith("A") || status === "??") changeStatus = "added";
64
+ else if (status.startsWith("D")) changeStatus = "deleted";
65
+
66
+ changes.push({ path: filePath, status: changeStatus });
67
+ }
68
+
69
+ const stagedOutput = execSync("git diff --name-status --cached", {
70
+ cwd: rootDir,
71
+ encoding: "utf-8"
72
+ });
73
+
74
+ for (const line of stagedOutput.split("\n")) {
75
+ if (!line.trim()) continue;
76
+ const [status, filePath] = line.split("\t").map(s => s.trim());
77
+ if (!filePath || changes.find(c => c.path === filePath)) continue;
78
+ changes.push({ path: filePath, status: "modified" });
79
+ }
80
+ } catch {
81
+ return detectChangesWithTimestamps(rootDir);
82
+ }
83
+
84
+ return changes;
85
+ }
86
+
87
+ function detectChangesWithTimestamps(rootDir: string): ChangedFile[] {
88
+ const changes: ChangedFile[] = [];
89
+ const aiDir = path.join(rootDir, "ai");
90
+ const state = loadIndexState(aiDir);
91
+
92
+ if (!state) return [];
93
+
94
+ for (const [filePath, fileState] of Object.entries(state.files)) {
95
+ const fullPath = path.join(rootDir, filePath);
96
+
97
+ if (!fs.existsSync(fullPath)) {
98
+ changes.push({ path: filePath, status: "deleted" });
99
+ continue;
100
+ }
101
+
102
+ const currentHash = computeFileHash(fullPath);
103
+ if (currentHash && currentHash.hash !== fileState.hash) {
104
+ changes.push({ path: filePath, status: "modified", hash: currentHash.hash });
105
+ }
106
+ }
107
+
108
+ return changes;
109
+ }
110
+
111
+ // ============================================================
112
+ // Symbol Update
113
+ // ============================================================
114
+
115
+ export function updateSymbols(rootDir: string, changedFiles: ChangedFile[], aiDir: string): number {
116
+ const symbolsPath = path.join(aiDir, "symbols.json");
117
+ let existingSymbols: Array<{name: string; file: string; type?: string}> = [];
118
+
119
+ if (fs.existsSync(symbolsPath)) {
120
+ try {
121
+ const raw = readJsonFile(symbolsPath);
122
+ if (Array.isArray(raw)) existingSymbols = raw as typeof existingSymbols;
123
+ } catch { existingSymbols = []; }
124
+ }
125
+
126
+ const changedPaths = new Set(changedFiles.map(f => f.path));
127
+ existingSymbols = existingSymbols.filter(s => !changedPaths.has(s.file));
128
+
129
+ for (const changed of changedFiles) {
130
+ if (changed.status === "deleted") continue;
131
+ const fullPath = path.join(rootDir, changed.path);
132
+ if (!fs.existsSync(fullPath)) continue;
133
+
134
+ try {
135
+ const fileInfo: FileInfo = {
136
+ path: changed.path,
137
+ relativePath: changed.path,
138
+ extension: path.extname(changed.path),
139
+ name: path.basename(changed.path, path.extname(changed.path))
140
+ };
141
+ const symbols = extractSymbols([fileInfo]);
142
+ for (const symbol of symbols.symbols || []) {
143
+ existingSymbols.push({ name: symbol.name, file: changed.path, type: symbol.type });
144
+ }
145
+ } catch { /* skip */ }
146
+ }
147
+
148
+ writeFile(symbolsPath, JSON.stringify(existingSymbols, null, 2));
149
+ return existingSymbols.length;
150
+ }
151
+
152
+ // ============================================================
153
+ // Dependency Update
154
+ // ============================================================
155
+
156
+ export function updateDependencies(rootDir: string, changedFiles: ChangedFile[], aiDir: string): number {
157
+ const depsPath = path.join(aiDir, "dependencies.json");
158
+ let existingDeps: Record<string, unknown> = {};
159
+
160
+ if (fs.existsSync(depsPath)) {
161
+ try { existingDeps = readJsonFile(depsPath) as typeof existingDeps; } catch { /* ignore */ }
162
+ }
163
+
164
+ const packageFiles = changedFiles.filter(f =>
165
+ f.path.endsWith("package.json") || f.path.endsWith("requirements.txt")
166
+ );
167
+
168
+ for (const pkgFile of packageFiles) {
169
+ const fullPath = path.join(rootDir, pkgFile.path);
170
+ if (!fs.existsSync(fullPath)) continue;
171
+
172
+ try {
173
+ if (pkgFile.path.endsWith("package.json")) {
174
+ const pkg = JSON.parse(fs.readFileSync(fullPath, "utf-8"));
175
+ existingDeps.dependencies = pkg.dependencies || {};
176
+ existingDeps.devDependencies = pkg.devDependencies || {};
177
+ }
178
+ } catch { /* skip */ }
179
+ }
180
+
181
+ writeFile(depsPath, JSON.stringify(existingDeps, null, 2));
182
+ return Object.keys(existingDeps.dependencies as object || {}).length;
183
+ }
184
+
185
+ // ============================================================
186
+ // Feature Update
187
+ // ============================================================
188
+
189
+ export function updateFeatures(rootDir: string, changedFiles: ChangedFile[], aiDir: string): string[] {
190
+ const changedPaths = new Set(changedFiles.map(f => f.path));
191
+ const featuresDir = path.join(aiDir, "context", "features");
192
+ const updatedFeatures: string[] = [];
193
+
194
+ if (!fs.existsSync(featuresDir)) return updatedFeatures;
195
+
196
+ try {
197
+ for (const featureFile of fs.readdirSync(featuresDir)) {
198
+ if (!featureFile.endsWith(".json")) continue;
199
+
200
+ const featurePath = path.join(featuresDir, featureFile);
201
+ const featureData = readJsonFile(featurePath) as {name: string; files?: string[]};
202
+ if (!featureData?.files) continue;
203
+
204
+ const featureFileSet = new Set(featureData.files);
205
+ const affected = [...changedPaths].some(f => featureFileSet.has(f));
206
+
207
+ if (affected || changedFiles.some(c => c.status === "deleted")) {
208
+ const featureFilesList = featureData.files.filter(f =>
209
+ !changedFiles.some(c => c.path === f && c.status === "deleted")
210
+ );
211
+
212
+ if (featureFilesList.length > 0) {
213
+ featureData.files = featureFilesList;
214
+ writeFile(featurePath, JSON.stringify(featureData, null, 2));
215
+ } else {
216
+ fs.unlinkSync(featurePath);
217
+ }
218
+ updatedFeatures.push(featureData.name);
219
+ }
220
+ }
221
+ } catch { /* ignore */ }
222
+
223
+ return updatedFeatures;
224
+ }
225
+
226
+ // ============================================================
227
+ // Flow Update
228
+ // ============================================================
229
+
230
+ export function updateFlows(rootDir: string, changedFiles: ChangedFile[], aiDir: string): string[] {
231
+ const changedPaths = new Set(changedFiles.map(f => f.path));
232
+ const flowsDir = path.join(aiDir, "context", "flows");
233
+ const updatedFlows: string[] = [];
234
+
235
+ if (!fs.existsSync(flowsDir)) return updatedFlows;
236
+
237
+ try {
238
+ for (const flowFile of fs.readdirSync(flowsDir)) {
239
+ if (!flowFile.endsWith(".json")) continue;
240
+
241
+ const flowPath = path.join(flowsDir, flowFile);
242
+ const flowData = readJsonFile(flowPath) as {name: string; files?: string[]};
243
+ if (!flowData?.files) continue;
244
+
245
+ const flowFileSet = new Set(flowData.files);
246
+ const affected = [...changedPaths].some(f => flowFileSet.has(f));
247
+
248
+ if (affected || changedFiles.some(c => c.status === "deleted")) {
249
+ const flowFilesList = flowData.files.filter(f =>
250
+ !changedFiles.some(c => c.path === f && c.status === "deleted")
251
+ );
252
+
253
+ if (flowFilesList.length > 0) {
254
+ flowData.files = flowFilesList;
255
+ writeFile(flowPath, JSON.stringify(flowData, null, 2));
256
+ } else {
257
+ fs.unlinkSync(flowPath);
258
+ }
259
+ updatedFlows.push(flowData.name);
260
+ }
261
+ }
262
+ } catch { /* ignore */ }
263
+
264
+ return updatedFlows;
265
+ }
266
+
267
+ // ============================================================
268
+ // Knowledge Graph Update
269
+ // ============================================================
270
+
271
+ export function updateKnowledgeGraph(rootDir: string, aiDir: string): boolean {
272
+ try {
273
+ buildKnowledgeGraph(rootDir, aiDir);
274
+ return true;
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ // ============================================================
281
+ // Main Incremental Update
282
+ // ============================================================
283
+
284
+ export function runIncrementalUpdate(rootDir: string, aiDir?: string): IncrementalUpdateResult {
285
+ const targetAiDir = aiDir || path.join(rootDir, "ai");
286
+ const errors: string[] = [];
287
+
288
+ if (!fs.existsSync(targetAiDir)) {
289
+ return {
290
+ changedFiles: [],
291
+ updatedSymbols: 0,
292
+ updatedDependencies: 0,
293
+ updatedFeatures: [],
294
+ updatedFlows: [],
295
+ graphUpdated: false,
296
+ errors: ["AI context directory does not exist. Run 'ai-first init' first."]
297
+ };
298
+ }
299
+
300
+ const changedFiles = detectChangedFiles(rootDir);
301
+
302
+ if (changedFiles.length === 0) {
303
+ return {
304
+ changedFiles: [],
305
+ updatedSymbols: 0,
306
+ updatedDependencies: 0,
307
+ updatedFeatures: [],
308
+ updatedFlows: [],
309
+ graphUpdated: false,
310
+ errors: []
311
+ };
312
+ }
313
+
314
+ let updatedSymbols = 0;
315
+ try { updatedSymbols = updateSymbols(rootDir, changedFiles, targetAiDir); }
316
+ catch (e) { errors.push(`Failed to update symbols: ${e}`); }
317
+
318
+ let updatedDependencies = 0;
319
+ try { updatedDependencies = updateDependencies(rootDir, changedFiles, targetAiDir); }
320
+ catch (e) { errors.push(`Failed to update dependencies: ${e}`); }
321
+
322
+ let updatedFeatures: string[] = [];
323
+ try { updatedFeatures = updateFeatures(rootDir, changedFiles, targetAiDir); }
324
+ catch (e) { errors.push(`Failed to update features: ${e}`); }
325
+
326
+ let updatedFlows: string[] = [];
327
+ try { updatedFlows = updateFlows(rootDir, changedFiles, targetAiDir); }
328
+ catch (e) { errors.push(`Failed to update flows: ${e}`); }
329
+
330
+ let graphUpdated = false;
331
+ try { graphUpdated = updateKnowledgeGraph(rootDir, targetAiDir); }
332
+ catch (e) { errors.push(`Failed to update knowledge graph: ${e}`); }
333
+
334
+ try { updateIndexState(rootDir, changedFiles); }
335
+ catch { /* non-critical */ }
336
+
337
+ return {
338
+ changedFiles,
339
+ updatedSymbols,
340
+ updatedDependencies,
341
+ updatedFeatures,
342
+ updatedFlows,
343
+ graphUpdated,
344
+ errors
345
+ };
346
+ }
347
+
348
+ function updateIndexState(rootDir: string, changedFiles: ChangedFile[]): void {
349
+ const aiDir = path.join(rootDir, "ai");
350
+ let state = loadIndexState(aiDir);
351
+
352
+ if (!state) {
353
+ state = { version: "1.0.0", lastIndexed: new Date().toISOString(), totalFiles: 0, files: {} };
354
+ }
355
+
356
+ const filesMap = new Map<string, FileState>(Object.entries(state.files));
357
+
358
+ for (const changed of changedFiles) {
359
+ const fullPath = path.join(rootDir, changed.path);
360
+
361
+ if (changed.status === "deleted") {
362
+ filesMap.delete(changed.path);
363
+ } else if (fs.existsSync(fullPath)) {
364
+ const hashData = computeFileHash(fullPath);
365
+ if (hashData) {
366
+ filesMap.set(changed.path, {
367
+ path: changed.path,
368
+ hash: hashData.hash,
369
+ mtime: hashData.mtime,
370
+ size: hashData.size,
371
+ indexedAt: new Date().toISOString()
372
+ });
373
+ }
374
+ }
375
+ }
376
+
377
+ state.files = Object.fromEntries(filesMap);
378
+ state.totalFiles = filesMap.size;
379
+ state.lastIndexed = new Date().toISOString();
380
+
381
+ saveIndexState(aiDir, filesMap);
382
+ }
@@ -0,0 +1,181 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { ensureDir, writeFile, readJsonFile } from "../utils/fileUtils.js";
4
+
5
+ export type NodeType = "feature" | "flow" | "file" | "symbol" | "commit";
6
+ export type EdgeType = "contains" | "implements" | "declares" | "references" | "modifies";
7
+
8
+ export interface KnowledgeNode {
9
+ id: string;
10
+ type: NodeType;
11
+ label?: string;
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface KnowledgeEdge {
16
+ from: string;
17
+ to: string;
18
+ type: EdgeType;
19
+ }
20
+
21
+ export interface KnowledgeGraph {
22
+ nodes: KnowledgeNode[];
23
+ edges: KnowledgeEdge[];
24
+ metadata: {
25
+ generated: string;
26
+ sources: string[];
27
+ nodeCount: number;
28
+ edgeCount: number;
29
+ };
30
+ }
31
+
32
+ function loadFeatures(aiDir: string) {
33
+ const featuresPath = path.join(aiDir, "context", "features");
34
+ if (!fs.existsSync(featuresPath)) return [];
35
+ const features: Array<{name: string; path: string; files: string[]}> = [];
36
+ try {
37
+ for (const file of fs.readdirSync(featuresPath)) {
38
+ if (!file.endsWith(".json")) continue;
39
+ const data = readJsonFile(path.join(featuresPath, file)) as {name?: unknown; path?: unknown; files?: unknown};
40
+ if (data?.name && Array.isArray(data.files)) {
41
+ features.push({ name: String(data.name), path: String(data.path || ""), files: data.files.map(String) });
42
+ }
43
+ }
44
+ } catch { return []; }
45
+ return features;
46
+ }
47
+
48
+ function loadFlows(aiDir: string) {
49
+ const flowsPath = path.join(aiDir, "context", "flows");
50
+ if (!fs.existsSync(flowsPath)) return [];
51
+ const flows: Array<{name: string; entrypoint: string; files: string[]}> = [];
52
+ try {
53
+ for (const file of fs.readdirSync(flowsPath)) {
54
+ if (!file.endsWith(".json")) continue;
55
+ const data = readJsonFile(path.join(flowsPath, file)) as {name?: unknown; entrypoint?: unknown; files?: unknown};
56
+ if (data?.name && Array.isArray(data.files)) {
57
+ flows.push({ name: String(data.name), entrypoint: String(data.entrypoint || ""), files: data.files.map(String) });
58
+ }
59
+ }
60
+ } catch { return []; }
61
+ return flows;
62
+ }
63
+
64
+ function loadGitActivity(aiDir: string) {
65
+ const gitPath = path.join(aiDir, "git", "commit-activity.json");
66
+ if (!fs.existsSync(gitPath)) return null;
67
+ try {
68
+ const data = readJsonFile(gitPath) as {totalCommits?: unknown; files?: unknown};
69
+ if (typeof data?.totalCommits === "number" && typeof data?.files === "object") {
70
+ return { totalCommits: data.totalCommits, files: data.files as Record<string, number> };
71
+ }
72
+ } catch { return null; }
73
+ return null;
74
+ }
75
+
76
+ function loadSymbols(aiDir: string) {
77
+ const symbolsPath = path.join(aiDir, "symbols.json");
78
+ if (!fs.existsSync(symbolsPath)) return [];
79
+ try {
80
+ const data = readJsonFile(symbolsPath);
81
+ if (Array.isArray(data)) {
82
+ return data.slice(0, 1000).map((s: unknown) => {
83
+ const sym = s as {name?: unknown; file?: unknown; type?: unknown; references?: unknown};
84
+ return { name: String(sym.name || ""), file: String(sym.file || ""), type: sym.type ? String(sym.type) : undefined, references: Array.isArray(sym.references) ? sym.references.map(String) : undefined };
85
+ });
86
+ }
87
+ } catch { return []; }
88
+ return [];
89
+ }
90
+
91
+ function loadFiles(aiDir: string) {
92
+ const filesPath = path.join(aiDir, "files.json");
93
+ if (!fs.existsSync(filesPath)) return [];
94
+ try {
95
+ const data = readJsonFile(filesPath);
96
+ if (Array.isArray(data)) {
97
+ return data.slice(0, 500).map((f: unknown) => typeof f === "string" ? f : (f as {path?: unknown})?.path ? String((f as {path: unknown}).path) : String(f));
98
+ }
99
+ } catch { return []; }
100
+ return [];
101
+ }
102
+
103
+ export function createNodes(aiDir: string): KnowledgeNode[] {
104
+ const nodes: KnowledgeNode[] = [];
105
+ const nodeIds = new Set<string>();
106
+ const addNode = (id: string, type: NodeType, label?: string, metadata?: Record<string, unknown>) => {
107
+ if (nodeIds.has(id)) return;
108
+ nodeIds.add(id);
109
+ nodes.push({ id, type, label: label || id, metadata });
110
+ };
111
+ for (const f of loadFeatures(aiDir)) addNode(f.name, "feature", f.name, { path: f.path, fileCount: f.files.length });
112
+ for (const f of loadFlows(aiDir)) addNode(f.name, "flow", f.name, { entrypoint: f.entrypoint, fileCount: f.files.length });
113
+ for (const f of loadFiles(aiDir)) addNode(f, "file", path.basename(f));
114
+ for (const s of loadSymbols(aiDir)) if (s.file && s.name) addNode(`${s.file}#${s.name}`, "symbol", s.name, { file: s.file, symbolType: s.type });
115
+ const git = loadGitActivity(aiDir);
116
+ if (git) {
117
+ for (const file of Object.entries(git.files).sort((a, b) => b[1] - a[1]).slice(0, 50).map(x => x[0])) {
118
+ addNode(`commit:${file}`, "commit", path.basename(file), { file, commitCount: git.files[file] });
119
+ }
120
+ }
121
+ return nodes;
122
+ }
123
+
124
+ export function createEdges(aiDir: string): KnowledgeEdge[] {
125
+ const edges: KnowledgeEdge[] = [];
126
+ const edgeKeys = new Set<string>();
127
+ const addEdge = (from: string, to: string, type: EdgeType) => {
128
+ const key = `${from}|${to}|${type}`;
129
+ if (edgeKeys.has(key)) return;
130
+ edgeKeys.add(key);
131
+ edges.push({ from, to, type });
132
+ };
133
+ const features = loadFeatures(aiDir);
134
+ const flows = loadFlows(aiDir);
135
+ for (const flow of flows) {
136
+ for (const feature of features) {
137
+ const intersection = flow.files.filter(f => feature.files.includes(f));
138
+ if (intersection.length > 0) addEdge(feature.name, flow.name, "contains");
139
+ }
140
+ }
141
+ for (const flow of flows) for (const file of flow.files) addEdge(flow.name, file, "implements");
142
+ for (const s of loadSymbols(aiDir)) if (s.file && s.name) addEdge(s.file, `${s.file}#${s.name}`, "declares");
143
+ for (const s of loadSymbols(aiDir)) if (s.file && s.references) for (const ref of s.references) addEdge(`${s.file}#${s.name}`, ref, "references");
144
+ const git = loadGitActivity(aiDir);
145
+ if (git) for (const file of Object.keys(git.files)) addEdge(`commit:${file}`, file, "modifies");
146
+ return edges;
147
+ }
148
+
149
+ export function buildKnowledgeGraph(rootDir: string, aiDir?: string): KnowledgeGraph {
150
+ const targetAiDir = aiDir || path.join(rootDir, "ai");
151
+ const graphDir = path.join(targetAiDir, "graph");
152
+ ensureDir(graphDir);
153
+ const nodes = createNodes(targetAiDir);
154
+ const edges = createEdges(targetAiDir);
155
+ const sources: string[] = [];
156
+ if (fs.existsSync(path.join(targetAiDir, "context", "features"))) sources.push("features");
157
+ if (fs.existsSync(path.join(targetAiDir, "context", "flows"))) sources.push("flows");
158
+ if (fs.existsSync(path.join(targetAiDir, "git"))) sources.push("git");
159
+ if (fs.existsSync(path.join(targetAiDir, "symbols.json"))) sources.push("symbols");
160
+ if (fs.existsSync(path.join(targetAiDir, "dependencies.json"))) sources.push("dependencies");
161
+ const graph: KnowledgeGraph = { nodes, edges, metadata: { generated: new Date().toISOString(), sources, nodeCount: nodes.length, edgeCount: edges.length } };
162
+ writeFile(path.join(graphDir, "knowledge-graph.json"), JSON.stringify(graph, null, 2));
163
+ return graph;
164
+ }
165
+
166
+ export function loadKnowledgeGraph(aiDir: string): KnowledgeGraph | null {
167
+ const graphPath = path.join(aiDir, "graph", "knowledge-graph.json");
168
+ if (!fs.existsSync(graphPath)) return null;
169
+ try { return readJsonFile(graphPath) as unknown as KnowledgeGraph; } catch { return null; }
170
+ }
171
+
172
+ export function getNodesByType(graph: KnowledgeGraph, type: NodeType): KnowledgeNode[] { return graph.nodes.filter(n => n.type === type); }
173
+ export function getEdgesByType(graph: KnowledgeGraph, type: EdgeType): KnowledgeEdge[] { return graph.edges.filter(e => e.type === type); }
174
+ export function getNeighbors(graph: KnowledgeGraph, nodeId: string): KnowledgeNode[] {
175
+ const neighborIds = new Set<string>();
176
+ for (const edge of graph.edges) {
177
+ if (edge.from === nodeId) neighborIds.add(edge.to);
178
+ if (edge.to === nodeId) neighborIds.add(edge.from);
179
+ }
180
+ return graph.nodes.filter(n => neighborIds.has(n.id));
181
+ }