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.
Files changed (192) 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.map +1 -1
  47. package/dist/commands/ai-first.js +311 -1
  48. package/dist/commands/ai-first.js.map +1 -1
  49. package/dist/core/adapters/adapterRegistry.d.ts +39 -0
  50. package/dist/core/adapters/adapterRegistry.d.ts.map +1 -0
  51. package/dist/core/adapters/adapterRegistry.js +155 -0
  52. package/dist/core/adapters/adapterRegistry.js.map +1 -0
  53. package/dist/core/adapters/baseAdapter.d.ts +49 -0
  54. package/dist/core/adapters/baseAdapter.d.ts.map +1 -0
  55. package/dist/core/adapters/baseAdapter.js +28 -0
  56. package/dist/core/adapters/baseAdapter.js.map +1 -0
  57. package/dist/core/adapters/community/fastapiAdapter.d.ts +7 -0
  58. package/dist/core/adapters/community/fastapiAdapter.d.ts.map +1 -0
  59. package/dist/core/adapters/community/fastapiAdapter.js +40 -0
  60. package/dist/core/adapters/community/fastapiAdapter.js.map +1 -0
  61. package/dist/core/adapters/community/index.d.ts +11 -0
  62. package/dist/core/adapters/community/index.d.ts.map +1 -0
  63. package/dist/core/adapters/community/index.js +11 -0
  64. package/dist/core/adapters/community/index.js.map +1 -0
  65. package/dist/core/adapters/community/laravelAdapter.d.ts +7 -0
  66. package/dist/core/adapters/community/laravelAdapter.d.ts.map +1 -0
  67. package/dist/core/adapters/community/laravelAdapter.js +47 -0
  68. package/dist/core/adapters/community/laravelAdapter.js.map +1 -0
  69. package/dist/core/adapters/community/nestjsAdapter.d.ts +7 -0
  70. package/dist/core/adapters/community/nestjsAdapter.d.ts.map +1 -0
  71. package/dist/core/adapters/community/nestjsAdapter.js +48 -0
  72. package/dist/core/adapters/community/nestjsAdapter.js.map +1 -0
  73. package/dist/core/adapters/community/phoenixAdapter.d.ts +7 -0
  74. package/dist/core/adapters/community/phoenixAdapter.d.ts.map +1 -0
  75. package/dist/core/adapters/community/phoenixAdapter.js +45 -0
  76. package/dist/core/adapters/community/phoenixAdapter.js.map +1 -0
  77. package/dist/core/adapters/community/springBootAdapter.d.ts +7 -0
  78. package/dist/core/adapters/community/springBootAdapter.d.ts.map +1 -0
  79. package/dist/core/adapters/community/springBootAdapter.js +44 -0
  80. package/dist/core/adapters/community/springBootAdapter.js.map +1 -0
  81. package/dist/core/adapters/dotnetAdapter.d.ts +20 -0
  82. package/dist/core/adapters/dotnetAdapter.d.ts.map +1 -0
  83. package/dist/core/adapters/dotnetAdapter.js +86 -0
  84. package/dist/core/adapters/dotnetAdapter.js.map +1 -0
  85. package/dist/core/adapters/index.d.ts +18 -0
  86. package/dist/core/adapters/index.d.ts.map +1 -0
  87. package/dist/core/adapters/index.js +19 -0
  88. package/dist/core/adapters/index.js.map +1 -0
  89. package/dist/core/adapters/javascriptAdapter.d.ts +11 -0
  90. package/dist/core/adapters/javascriptAdapter.d.ts.map +1 -0
  91. package/dist/core/adapters/javascriptAdapter.js +47 -0
  92. package/dist/core/adapters/javascriptAdapter.js.map +1 -0
  93. package/dist/core/adapters/pythonAdapter.d.ts +20 -0
  94. package/dist/core/adapters/pythonAdapter.d.ts.map +1 -0
  95. package/dist/core/adapters/pythonAdapter.js +99 -0
  96. package/dist/core/adapters/pythonAdapter.js.map +1 -0
  97. package/dist/core/adapters/railsAdapter.d.ts +10 -0
  98. package/dist/core/adapters/railsAdapter.d.ts.map +1 -0
  99. package/dist/core/adapters/railsAdapter.js +52 -0
  100. package/dist/core/adapters/railsAdapter.js.map +1 -0
  101. package/dist/core/adapters/salesforceAdapter.d.ts +16 -0
  102. package/dist/core/adapters/salesforceAdapter.d.ts.map +1 -0
  103. package/dist/core/adapters/salesforceAdapter.js +64 -0
  104. package/dist/core/adapters/salesforceAdapter.js.map +1 -0
  105. package/dist/core/adapters/sdk.d.ts +83 -0
  106. package/dist/core/adapters/sdk.d.ts.map +1 -0
  107. package/dist/core/adapters/sdk.js +114 -0
  108. package/dist/core/adapters/sdk.js.map +1 -0
  109. package/dist/core/ccp.d.ts +37 -0
  110. package/dist/core/ccp.d.ts.map +1 -0
  111. package/dist/core/ccp.js +184 -0
  112. package/dist/core/ccp.js.map +1 -0
  113. package/dist/core/gitAnalyzer.d.ts +74 -0
  114. package/dist/core/gitAnalyzer.d.ts.map +1 -0
  115. package/dist/core/gitAnalyzer.js +298 -0
  116. package/dist/core/gitAnalyzer.js.map +1 -0
  117. package/dist/core/incrementalAnalyzer.d.ts +28 -0
  118. package/dist/core/incrementalAnalyzer.d.ts.map +1 -0
  119. package/dist/core/incrementalAnalyzer.js +343 -0
  120. package/dist/core/incrementalAnalyzer.js.map +1 -0
  121. package/dist/core/knowledgeGraphBuilder.d.ts +31 -0
  122. package/dist/core/knowledgeGraphBuilder.d.ts.map +1 -0
  123. package/dist/core/knowledgeGraphBuilder.js +197 -0
  124. package/dist/core/knowledgeGraphBuilder.js.map +1 -0
  125. package/dist/core/lazyAnalyzer.d.ts +57 -0
  126. package/dist/core/lazyAnalyzer.d.ts.map +1 -0
  127. package/dist/core/lazyAnalyzer.js +204 -0
  128. package/dist/core/lazyAnalyzer.js.map +1 -0
  129. package/dist/core/schema.d.ts +57 -0
  130. package/dist/core/schema.d.ts.map +1 -0
  131. package/dist/core/schema.js +131 -0
  132. package/dist/core/schema.js.map +1 -0
  133. package/dist/core/semanticContexts.d.ts +40 -0
  134. package/dist/core/semanticContexts.d.ts.map +1 -0
  135. package/dist/core/semanticContexts.js +454 -0
  136. package/dist/core/semanticContexts.js.map +1 -0
  137. package/docs/es/guide/adapters.md +143 -0
  138. package/docs/es/guide/ai-repository-schema.md +119 -0
  139. package/docs/es/guide/features.md +67 -0
  140. package/docs/es/guide/flows.md +134 -0
  141. package/docs/es/guide/git-intelligence.md +170 -0
  142. package/docs/es/guide/incremental-analysis.md +131 -0
  143. package/docs/es/guide/knowledge-graph.md +135 -0
  144. package/docs/es/guide/lazy-indexing.md +144 -0
  145. package/docs/es/guide/performance.md +125 -0
  146. package/docs/guide/adapters.md +225 -0
  147. package/docs/guide/ai-repository-schema.md +119 -0
  148. package/docs/guide/architecture.md +69 -1
  149. package/docs/guide/flows.md +134 -0
  150. package/docs/guide/git-intelligence.md +170 -0
  151. package/docs/guide/incremental-analysis.md +131 -0
  152. package/docs/guide/knowledge-graph.md +135 -0
  153. package/docs/guide/lazy-indexing.md +144 -0
  154. package/docs/guide/performance.md +125 -0
  155. package/package.json +5 -2
  156. package/src/analyzers/androidResources.ts +113 -0
  157. package/src/analyzers/dependencies.ts +41 -0
  158. package/src/analyzers/entrypoints.ts +80 -1
  159. package/src/analyzers/gradleModules.ts +100 -0
  160. package/src/analyzers/techStack.ts +56 -0
  161. package/src/commands/ai-first.ts +342 -1
  162. package/src/core/adapters/adapterRegistry.ts +187 -0
  163. package/src/core/adapters/baseAdapter.ts +82 -0
  164. package/src/core/adapters/community/fastapiAdapter.ts +50 -0
  165. package/src/core/adapters/community/index.ts +11 -0
  166. package/src/core/adapters/community/laravelAdapter.ts +56 -0
  167. package/src/core/adapters/community/nestjsAdapter.ts +57 -0
  168. package/src/core/adapters/community/phoenixAdapter.ts +54 -0
  169. package/src/core/adapters/community/springBootAdapter.ts +53 -0
  170. package/src/core/adapters/dotnetAdapter.ts +104 -0
  171. package/src/core/adapters/index.ts +24 -0
  172. package/src/core/adapters/javascriptAdapter.ts +56 -0
  173. package/src/core/adapters/pythonAdapter.ts +118 -0
  174. package/src/core/adapters/railsAdapter.ts +65 -0
  175. package/src/core/adapters/salesforceAdapter.ts +76 -0
  176. package/src/core/adapters/sdk.ts +172 -0
  177. package/src/core/ccp.ts +240 -0
  178. package/src/core/gitAnalyzer.ts +391 -0
  179. package/src/core/incrementalAnalyzer.ts +382 -0
  180. package/src/core/knowledgeGraphBuilder.ts +181 -0
  181. package/src/core/lazyAnalyzer.ts +261 -0
  182. package/src/core/schema.ts +157 -0
  183. package/src/core/semanticContexts.ts +575 -0
  184. package/tests/adapters.test.ts +159 -0
  185. package/tests/gitAnalyzer.test.ts +133 -0
  186. package/tests/incrementalAnalyzer.test.ts +83 -0
  187. package/tests/knowledgeGraph.test.ts +146 -0
  188. package/tests/lazyAnalyzer.test.ts +230 -0
  189. package/tests/schema.test.ts +203 -0
  190. package/tests/semanticContexts.test.ts +435 -0
  191. package/ai/context/analyzers.Symbol.json +0 -19
  192. package/ai/context/analyzers.extractSymbols.json +0 -19
@@ -0,0 +1,230 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ buildMinimalIndex,
4
+ expandFeatureContext,
5
+ expandFlowContext,
6
+ expandFullContext,
7
+ getLazyIndexState,
8
+ hasMinimalIndex,
9
+ loadMinimalIndex,
10
+ MinimalIndex,
11
+ LazyIndexState
12
+ } from "../src/core/lazyAnalyzer.js";
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import os from "os";
16
+
17
+ function createTempProjectDir(files: Record<string, string>): string {
18
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ai-first-lazy-test-"));
19
+ for (const [filePath, content] of Object.entries(files)) {
20
+ const fullPath = path.join(tempDir, filePath);
21
+ const dir = path.dirname(fullPath);
22
+ if (!fs.existsSync(dir)) {
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ }
25
+ fs.writeFileSync(fullPath, content);
26
+ }
27
+ return tempDir;
28
+ }
29
+
30
+ describe("Lazy Analyzer", () => {
31
+ describe("buildMinimalIndex", () => {
32
+ it("should build minimal index with Stage 1 data", () => {
33
+ const projectDir = createTempProjectDir({
34
+ "package.json": '{"name": "test"}',
35
+ "src/index.js": "export const hello = () => {}",
36
+ "src/utils.js": "export const util = () => {}"
37
+ });
38
+
39
+ const aiDir = path.join(projectDir, "ai");
40
+ fs.mkdirSync(aiDir, { recursive: true });
41
+
42
+ const result = buildMinimalIndex(projectDir, aiDir);
43
+
44
+ expect(result.languages).toBeDefined();
45
+ expect(result.frameworks).toBeDefined();
46
+ expect(result.entrypoints).toBeDefined();
47
+ expect(result.repoMap).toBeDefined();
48
+ expect(result.generatedAt).toBeDefined();
49
+
50
+ // Check state file
51
+ const state = getLazyIndexState(aiDir);
52
+ expect(state?.stage1Complete).toBe(true);
53
+ expect(state?.stage2Complete).toBe(false);
54
+
55
+ // Check minimal index file
56
+ expect(hasMinimalIndex(aiDir)).toBe(true);
57
+ const minimal = loadMinimalIndex(aiDir);
58
+ expect(minimal?.languages).toBeDefined();
59
+ });
60
+
61
+ it("should create minimal-index.json file", () => {
62
+ const projectDir = createTempProjectDir({
63
+ "package.json": '{"name": "test"}',
64
+ "src/app.js": "const app = {}"
65
+ });
66
+
67
+ const aiDir = path.join(projectDir, "ai");
68
+ fs.mkdirSync(aiDir, { recursive: true });
69
+
70
+ buildMinimalIndex(projectDir, aiDir);
71
+
72
+ const indexPath = path.join(aiDir, "minimal-index.json");
73
+ expect(fs.existsSync(indexPath)).toBe(true);
74
+
75
+ const content = JSON.parse(fs.readFileSync(indexPath, "utf-8"));
76
+ expect(content.languages).toBeDefined();
77
+ });
78
+ });
79
+
80
+ describe("expandFeatureContext", () => {
81
+ it("should expand context for a specific feature", () => {
82
+ const projectDir = createTempProjectDir({
83
+ "package.json": '{"name": "test"}',
84
+ "src/auth/login.js": "export const login = () => {}",
85
+ "src/auth/logout.js": "export const logout = () => {}"
86
+ });
87
+
88
+ const aiDir = path.join(projectDir, "ai");
89
+ fs.mkdirSync(aiDir, { recursive: true });
90
+
91
+ // Build minimal first
92
+ buildMinimalIndex(projectDir, aiDir);
93
+
94
+ // Expand feature
95
+ const result = expandFeatureContext(projectDir, aiDir, "auth");
96
+
97
+ expect(result.success).toBe(true);
98
+ expect(result.files).toBeDefined();
99
+
100
+ // Check state was updated
101
+ const state = getLazyIndexState(aiDir);
102
+ expect(state?.featuresExpanded).toContain("auth");
103
+ });
104
+ });
105
+
106
+ describe("expandFlowContext", () => {
107
+ it("should expand context for a specific flow", () => {
108
+ const projectDir = createTempProjectDir({
109
+ "package.json": '{"name": "test"}',
110
+ "src/user.js": "import { auth } from './auth'",
111
+ "src/auth.js": "export const auth = {}"
112
+ });
113
+
114
+ const aiDir = path.join(projectDir, "ai");
115
+ fs.mkdirSync(aiDir, { recursive: true });
116
+
117
+ // Build minimal first
118
+ buildMinimalIndex(projectDir, aiDir);
119
+
120
+ // Expand flow
121
+ const result = expandFlowContext(projectDir, aiDir, "user");
122
+
123
+ expect(result.success).toBe(true);
124
+
125
+ // Check state was updated
126
+ const state = getLazyIndexState(aiDir);
127
+ expect(state?.flowsExpanded).toContain("user");
128
+ });
129
+ });
130
+
131
+ describe("expandFullContext", () => {
132
+ it("should expand all context (Stage 2)", () => {
133
+ const projectDir = createTempProjectDir({
134
+ "package.json": '{"name": "test"}',
135
+ "src/index.js": "export const index = () => {}"
136
+ });
137
+
138
+ const aiDir = path.join(projectDir, "ai");
139
+ fs.mkdirSync(aiDir, { recursive: true });
140
+
141
+ buildMinimalIndex(projectDir, aiDir);
142
+
143
+ const result = expandFullContext(projectDir, aiDir);
144
+
145
+ expect(result.symbols).toBeDefined();
146
+ expect(result.dependencies).toBeDefined();
147
+ expect(result.features).toBeDefined();
148
+ expect(result.flows).toBeDefined();
149
+
150
+ // Check stage 2 is complete
151
+ const state = getLazyIndexState(aiDir);
152
+ expect(state?.stage2Complete).toBe(true);
153
+ });
154
+ });
155
+
156
+ describe("getLazyIndexState", () => {
157
+ it("should return null when no state exists", () => {
158
+ const projectDir = createTempProjectDir({
159
+ "package.json": "{}"
160
+ });
161
+ const aiDir = path.join(projectDir, "ai");
162
+
163
+ const state = getLazyIndexState(aiDir);
164
+ expect(state).toBeNull();
165
+ });
166
+
167
+ it("should return state when it exists", () => {
168
+ const projectDir = createTempProjectDir({
169
+ "package.json": "{}"
170
+ });
171
+ const aiDir = path.join(projectDir, "ai");
172
+ fs.mkdirSync(aiDir, { recursive: true });
173
+
174
+ buildMinimalIndex(projectDir, aiDir);
175
+
176
+ const state = getLazyIndexState(aiDir);
177
+ expect(state).not.toBeNull();
178
+ expect(state?.stage1Complete).toBe(true);
179
+ });
180
+ });
181
+
182
+ describe("hasMinimalIndex", () => {
183
+ it("should return false when no minimal index exists", () => {
184
+ const projectDir = createTempProjectDir({
185
+ "package.json": "{}"
186
+ });
187
+ const aiDir = path.join(projectDir, "ai");
188
+
189
+ expect(hasMinimalIndex(aiDir)).toBe(false);
190
+ });
191
+
192
+ it("should return true when minimal index exists", () => {
193
+ const projectDir = createTempProjectDir({
194
+ "package.json": "{}"
195
+ });
196
+ const aiDir = path.join(projectDir, "ai");
197
+ fs.mkdirSync(aiDir, { recursive: true });
198
+
199
+ buildMinimalIndex(projectDir, aiDir);
200
+
201
+ expect(hasMinimalIndex(aiDir)).toBe(true);
202
+ });
203
+ });
204
+
205
+ describe("loadMinimalIndex", () => {
206
+ it("should return null when no minimal index exists", () => {
207
+ const projectDir = createTempProjectDir({
208
+ "package.json": "{}"
209
+ });
210
+ const aiDir = path.join(projectDir, "ai");
211
+
212
+ const index = loadMinimalIndex(aiDir);
213
+ expect(index).toBeNull();
214
+ });
215
+
216
+ it("should load minimal index when it exists", () => {
217
+ const projectDir = createTempProjectDir({
218
+ "package.json": "{}"
219
+ });
220
+ const aiDir = path.join(projectDir, "ai");
221
+ fs.mkdirSync(aiDir, { recursive: true });
222
+
223
+ buildMinimalIndex(projectDir, aiDir);
224
+
225
+ const index = loadMinimalIndex(aiDir);
226
+ expect(index).not.toBeNull();
227
+ expect(index?.repoMap).toBeDefined();
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ generateSchema,
4
+ generateProject,
5
+ generateTools,
6
+ generateAllSchema,
7
+ loadSchema,
8
+ loadProject,
9
+ loadTools,
10
+ loadFullSchema,
11
+ isCompatible,
12
+ validateSchema,
13
+ SCHEMA_VERSION,
14
+ GENERATED_BY
15
+ } from "../src/core/schema.js";
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import os from "os";
19
+
20
+ function createTempAiDir(): string {
21
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ai-first-schema-test-"));
22
+ const aiDir = path.join(tempDir, "ai");
23
+ fs.mkdirSync(aiDir, { recursive: true });
24
+ return aiDir;
25
+ }
26
+
27
+ describe("Schema System", () => {
28
+ describe("generateSchema", () => {
29
+ it("should generate schema.json with correct version", () => {
30
+ const aiDir = createTempAiDir();
31
+ const schema = generateSchema(aiDir);
32
+
33
+ expect(schema.schemaVersion).toBe(SCHEMA_VERSION);
34
+ expect(schema.generatedBy).toBe(GENERATED_BY);
35
+ expect(schema.generatedAt).toBeDefined();
36
+
37
+ // Verify file was created
38
+ const schemaPath = path.join(aiDir, "schema.json");
39
+ expect(fs.existsSync(schemaPath)).toBe(true);
40
+
41
+ const loaded = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
42
+ expect(loaded.schemaVersion).toBe(SCHEMA_VERSION);
43
+ });
44
+ });
45
+
46
+ describe("generateProject", () => {
47
+ it("should generate project.json with project info", () => {
48
+ const aiDir = createTempAiDir();
49
+ const rootDir = "/test/project";
50
+
51
+ const project = generateProject(rootDir, aiDir, {
52
+ name: "test-project",
53
+ features: ["auth", "users"],
54
+ flows: ["login", "register"],
55
+ languages: ["TypeScript"],
56
+ frameworks: ["Express"]
57
+ });
58
+
59
+ expect(project.name).toBe("test-project");
60
+ expect(project.rootDir).toBe(rootDir);
61
+ expect(project.features).toEqual(["auth", "users"]);
62
+ expect(project.flows).toEqual(["login", "register"]);
63
+ expect(project.languages).toEqual(["TypeScript"]);
64
+ expect(project.frameworks).toEqual(["Express"]);
65
+ expect(project.generatedAt).toBeDefined();
66
+
67
+ const projectPath = path.join(aiDir, "project.json");
68
+ expect(fs.existsSync(projectPath)).toBe(true);
69
+ });
70
+
71
+ it("should auto-detect features from context directory", () => {
72
+ const aiDir = createTempAiDir();
73
+ const rootDir = "/test/project";
74
+
75
+ // Create features directory with files
76
+ const featuresDir = path.join(aiDir, "context", "features");
77
+ fs.mkdirSync(featuresDir, { recursive: true });
78
+ fs.writeFileSync(path.join(featuresDir, "auth.json"), "{}");
79
+ fs.writeFileSync(path.join(featuresDir, "users.json"), "{}");
80
+
81
+ const project = generateProject(rootDir, aiDir);
82
+ expect(project.features).toContain("auth");
83
+ expect(project.features).toContain("users");
84
+ });
85
+ });
86
+
87
+ describe("generateTools", () => {
88
+ it("should generate tools.json with compatible agents", () => {
89
+ const aiDir = createTempAiDir();
90
+ const tools = generateTools(aiDir);
91
+
92
+ expect(tools.compatibleAgents).toContain("opencode");
93
+ expect(tools.compatibleAgents).toContain("cursor");
94
+ expect(tools.schemaVersion).toBe(SCHEMA_VERSION);
95
+
96
+ const toolsPath = path.join(aiDir, "tools.json");
97
+ expect(fs.existsSync(toolsPath)).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe("generateAllSchema", () => {
102
+ it("should generate all schema files at once", () => {
103
+ const aiDir = createTempAiDir();
104
+ const rootDir = "/test/project";
105
+
106
+ const result = generateAllSchema(rootDir, aiDir, {
107
+ projectName: "full-test"
108
+ });
109
+
110
+ expect(result.schema).toBeDefined();
111
+ expect(result.project).toBeDefined();
112
+ expect(result.tools).toBeDefined();
113
+
114
+ expect(fs.existsSync(path.join(aiDir, "schema.json"))).toBe(true);
115
+ expect(fs.existsSync(path.join(aiDir, "project.json"))).toBe(true);
116
+ expect(fs.existsSync(path.join(aiDir, "tools.json"))).toBe(true);
117
+ });
118
+ });
119
+
120
+ describe("loadSchema", () => {
121
+ it("should load schema from existing file", () => {
122
+ const aiDir = createTempAiDir();
123
+ generateSchema(aiDir);
124
+
125
+ const loaded = loadSchema(aiDir);
126
+ expect(loaded).not.toBeNull();
127
+ expect(loaded?.schemaVersion).toBe(SCHEMA_VERSION);
128
+ });
129
+
130
+ it("should return null for missing schema", () => {
131
+ const aiDir = createTempAiDir();
132
+ const loaded = loadSchema(aiDir);
133
+ expect(loaded).toBeNull();
134
+ });
135
+ });
136
+
137
+ describe("loadFullSchema", () => {
138
+ it("should load all schema files", () => {
139
+ const aiDir = createTempAiDir();
140
+ generateAllSchema("/test", aiDir);
141
+
142
+ const full = loadFullSchema(aiDir);
143
+ expect(full).not.toBeNull();
144
+ expect(full?.schema).toBeDefined();
145
+ expect(full?.project).toBeDefined();
146
+ expect(full?.tools).toBeDefined();
147
+ });
148
+
149
+ it("should return null if any file is missing", () => {
150
+ const aiDir = createTempAiDir();
151
+ generateSchema(aiDir);
152
+
153
+ const full = loadFullSchema(aiDir);
154
+ expect(full).toBeNull();
155
+ });
156
+ });
157
+
158
+ describe("isCompatible", () => {
159
+ it("should return true for same major version", () => {
160
+ expect(isCompatible("1.0")).toBe(true);
161
+ expect(isCompatible("1.5")).toBe(true);
162
+ });
163
+
164
+ it("should return false for different major version", () => {
165
+ expect(isCompatible("2.0")).toBe(false);
166
+ expect(isCompatible("0.9")).toBe(false);
167
+ });
168
+ });
169
+
170
+ describe("validateSchema", () => {
171
+ it("should validate complete schema", () => {
172
+ const aiDir = createTempAiDir();
173
+ generateAllSchema("/test", aiDir);
174
+
175
+ const result = validateSchema(aiDir);
176
+ expect(result.valid).toBe(true);
177
+ expect(result.errors).toHaveLength(0);
178
+ });
179
+
180
+ it("should detect missing files", () => {
181
+ const aiDir = createTempAiDir();
182
+
183
+ const result = validateSchema(aiDir);
184
+ expect(result.valid).toBe(false);
185
+ expect(result.errors.length).toBeGreaterThan(0);
186
+ });
187
+
188
+ it("should detect incompatible version", () => {
189
+ const aiDir = createTempAiDir();
190
+ generateAllSchema("/test", aiDir);
191
+
192
+ // Manually write incompatible version
193
+ const schemaPath = path.join(aiDir, "schema.json");
194
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
195
+ schema.schemaVersion = "99.0.0";
196
+ fs.writeFileSync(schemaPath, JSON.stringify(schema));
197
+
198
+ const result = validateSchema(aiDir);
199
+ expect(result.valid).toBe(false);
200
+ expect(result.errors.some(e => e.includes("Incompatible"))).toBe(true);
201
+ });
202
+ });
203
+ });