spec-gen-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1078 -0
  3. package/dist/api/analyze.d.ts +17 -0
  4. package/dist/api/analyze.d.ts.map +1 -0
  5. package/dist/api/analyze.js +109 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/drift.d.ts +21 -0
  8. package/dist/api/drift.d.ts.map +1 -0
  9. package/dist/api/drift.js +145 -0
  10. package/dist/api/drift.js.map +1 -0
  11. package/dist/api/generate.d.ts +18 -0
  12. package/dist/api/generate.d.ts.map +1 -0
  13. package/dist/api/generate.js +251 -0
  14. package/dist/api/generate.js.map +1 -0
  15. package/dist/api/index.d.ts +39 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +32 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/init.d.ts +18 -0
  20. package/dist/api/init.d.ts.map +1 -0
  21. package/dist/api/init.js +82 -0
  22. package/dist/api/init.js.map +1 -0
  23. package/dist/api/run.d.ts +19 -0
  24. package/dist/api/run.d.ts.map +1 -0
  25. package/dist/api/run.js +291 -0
  26. package/dist/api/run.js.map +1 -0
  27. package/dist/api/specs.d.ts +49 -0
  28. package/dist/api/specs.d.ts.map +1 -0
  29. package/dist/api/specs.js +136 -0
  30. package/dist/api/specs.js.map +1 -0
  31. package/dist/api/types.d.ts +176 -0
  32. package/dist/api/types.d.ts.map +1 -0
  33. package/dist/api/types.js +9 -0
  34. package/dist/api/types.js.map +1 -0
  35. package/dist/api/verify.d.ts +20 -0
  36. package/dist/api/verify.d.ts.map +1 -0
  37. package/dist/api/verify.js +117 -0
  38. package/dist/api/verify.js.map +1 -0
  39. package/dist/cli/commands/analyze.d.ts +27 -0
  40. package/dist/cli/commands/analyze.d.ts.map +1 -0
  41. package/dist/cli/commands/analyze.js +485 -0
  42. package/dist/cli/commands/analyze.js.map +1 -0
  43. package/dist/cli/commands/drift.d.ts +9 -0
  44. package/dist/cli/commands/drift.d.ts.map +1 -0
  45. package/dist/cli/commands/drift.js +540 -0
  46. package/dist/cli/commands/drift.js.map +1 -0
  47. package/dist/cli/commands/generate.d.ts +9 -0
  48. package/dist/cli/commands/generate.d.ts.map +1 -0
  49. package/dist/cli/commands/generate.js +633 -0
  50. package/dist/cli/commands/generate.js.map +1 -0
  51. package/dist/cli/commands/init.d.ts +9 -0
  52. package/dist/cli/commands/init.d.ts.map +1 -0
  53. package/dist/cli/commands/init.js +171 -0
  54. package/dist/cli/commands/init.js.map +1 -0
  55. package/dist/cli/commands/mcp.d.ts +638 -0
  56. package/dist/cli/commands/mcp.d.ts.map +1 -0
  57. package/dist/cli/commands/mcp.js +574 -0
  58. package/dist/cli/commands/mcp.js.map +1 -0
  59. package/dist/cli/commands/run.d.ts +24 -0
  60. package/dist/cli/commands/run.d.ts.map +1 -0
  61. package/dist/cli/commands/run.js +546 -0
  62. package/dist/cli/commands/run.js.map +1 -0
  63. package/dist/cli/commands/verify.d.ts +9 -0
  64. package/dist/cli/commands/verify.d.ts.map +1 -0
  65. package/dist/cli/commands/verify.js +417 -0
  66. package/dist/cli/commands/verify.js.map +1 -0
  67. package/dist/cli/commands/view.d.ts +9 -0
  68. package/dist/cli/commands/view.d.ts.map +1 -0
  69. package/dist/cli/commands/view.js +511 -0
  70. package/dist/cli/commands/view.js.map +1 -0
  71. package/dist/cli/index.d.ts +9 -0
  72. package/dist/cli/index.d.ts.map +1 -0
  73. package/dist/cli/index.js +83 -0
  74. package/dist/cli/index.js.map +1 -0
  75. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  76. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  77. package/dist/core/analyzer/architecture-writer.js +209 -0
  78. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  79. package/dist/core/analyzer/artifact-generator.d.ts +222 -0
  80. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  81. package/dist/core/analyzer/artifact-generator.js +726 -0
  82. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  83. package/dist/core/analyzer/call-graph.d.ts +83 -0
  84. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  85. package/dist/core/analyzer/call-graph.js +827 -0
  86. package/dist/core/analyzer/call-graph.js.map +1 -0
  87. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  88. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  89. package/dist/core/analyzer/code-shaper.js +149 -0
  90. package/dist/core/analyzer/code-shaper.js.map +1 -0
  91. package/dist/core/analyzer/dependency-graph.d.ts +179 -0
  92. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  93. package/dist/core/analyzer/dependency-graph.js +574 -0
  94. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  95. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  96. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  97. package/dist/core/analyzer/duplicate-detector.js +279 -0
  98. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  99. package/dist/core/analyzer/embedding-service.d.ts +50 -0
  100. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  101. package/dist/core/analyzer/embedding-service.js +104 -0
  102. package/dist/core/analyzer/embedding-service.js.map +1 -0
  103. package/dist/core/analyzer/file-walker.d.ts +78 -0
  104. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  105. package/dist/core/analyzer/file-walker.js +531 -0
  106. package/dist/core/analyzer/file-walker.js.map +1 -0
  107. package/dist/core/analyzer/import-parser.d.ts +91 -0
  108. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  109. package/dist/core/analyzer/import-parser.js +720 -0
  110. package/dist/core/analyzer/import-parser.js.map +1 -0
  111. package/dist/core/analyzer/index.d.ts +10 -0
  112. package/dist/core/analyzer/index.d.ts.map +1 -0
  113. package/dist/core/analyzer/index.js +10 -0
  114. package/dist/core/analyzer/index.js.map +1 -0
  115. package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
  116. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  117. package/dist/core/analyzer/refactor-analyzer.js +339 -0
  118. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  119. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  120. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  121. package/dist/core/analyzer/repository-mapper.js +731 -0
  122. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  123. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  124. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  125. package/dist/core/analyzer/signature-extractor.js +387 -0
  126. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  127. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  128. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  129. package/dist/core/analyzer/significance-scorer.js +407 -0
  130. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  131. package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
  132. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  133. package/dist/core/analyzer/subgraph-extractor.js +129 -0
  134. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  135. package/dist/core/analyzer/vector-index.d.ts +63 -0
  136. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  137. package/dist/core/analyzer/vector-index.js +169 -0
  138. package/dist/core/analyzer/vector-index.js.map +1 -0
  139. package/dist/core/drift/drift-detector.d.ts +102 -0
  140. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  141. package/dist/core/drift/drift-detector.js +597 -0
  142. package/dist/core/drift/drift-detector.js.map +1 -0
  143. package/dist/core/drift/git-diff.d.ts +55 -0
  144. package/dist/core/drift/git-diff.d.ts.map +1 -0
  145. package/dist/core/drift/git-diff.js +356 -0
  146. package/dist/core/drift/git-diff.js.map +1 -0
  147. package/dist/core/drift/index.d.ts +12 -0
  148. package/dist/core/drift/index.d.ts.map +1 -0
  149. package/dist/core/drift/index.js +9 -0
  150. package/dist/core/drift/index.js.map +1 -0
  151. package/dist/core/drift/spec-mapper.d.ts +73 -0
  152. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  153. package/dist/core/drift/spec-mapper.js +353 -0
  154. package/dist/core/drift/spec-mapper.js.map +1 -0
  155. package/dist/core/generator/adr-generator.d.ts +32 -0
  156. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  157. package/dist/core/generator/adr-generator.js +192 -0
  158. package/dist/core/generator/adr-generator.js.map +1 -0
  159. package/dist/core/generator/index.d.ts +9 -0
  160. package/dist/core/generator/index.d.ts.map +1 -0
  161. package/dist/core/generator/index.js +12 -0
  162. package/dist/core/generator/index.js.map +1 -0
  163. package/dist/core/generator/mapping-generator.d.ts +54 -0
  164. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  165. package/dist/core/generator/mapping-generator.js +239 -0
  166. package/dist/core/generator/mapping-generator.js.map +1 -0
  167. package/dist/core/generator/openspec-compat.d.ts +160 -0
  168. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  169. package/dist/core/generator/openspec-compat.js +523 -0
  170. package/dist/core/generator/openspec-compat.js.map +1 -0
  171. package/dist/core/generator/openspec-format-generator.d.ts +111 -0
  172. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  173. package/dist/core/generator/openspec-format-generator.js +817 -0
  174. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  175. package/dist/core/generator/openspec-writer.d.ts +131 -0
  176. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  177. package/dist/core/generator/openspec-writer.js +379 -0
  178. package/dist/core/generator/openspec-writer.js.map +1 -0
  179. package/dist/core/generator/prompts.d.ts +35 -0
  180. package/dist/core/generator/prompts.d.ts.map +1 -0
  181. package/dist/core/generator/prompts.js +212 -0
  182. package/dist/core/generator/prompts.js.map +1 -0
  183. package/dist/core/generator/spec-pipeline.d.ts +94 -0
  184. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  185. package/dist/core/generator/spec-pipeline.js +474 -0
  186. package/dist/core/generator/spec-pipeline.js.map +1 -0
  187. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  188. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  189. package/dist/core/generator/stages/stage1-survey.js +105 -0
  190. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  191. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  192. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  193. package/dist/core/generator/stages/stage2-entities.js +67 -0
  194. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  195. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  196. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  197. package/dist/core/generator/stages/stage3-services.js +75 -0
  198. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  199. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  200. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  201. package/dist/core/generator/stages/stage4-api.js +65 -0
  202. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  203. package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
  204. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  205. package/dist/core/generator/stages/stage5-architecture.js +62 -0
  206. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  207. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  208. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  209. package/dist/core/generator/stages/stage6-adr.js +41 -0
  210. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  211. package/dist/core/services/chat-agent.d.ts +45 -0
  212. package/dist/core/services/chat-agent.d.ts.map +1 -0
  213. package/dist/core/services/chat-agent.js +310 -0
  214. package/dist/core/services/chat-agent.js.map +1 -0
  215. package/dist/core/services/chat-tools.d.ts +32 -0
  216. package/dist/core/services/chat-tools.d.ts.map +1 -0
  217. package/dist/core/services/chat-tools.js +270 -0
  218. package/dist/core/services/chat-tools.js.map +1 -0
  219. package/dist/core/services/config-manager.d.ts +61 -0
  220. package/dist/core/services/config-manager.d.ts.map +1 -0
  221. package/dist/core/services/config-manager.js +143 -0
  222. package/dist/core/services/config-manager.js.map +1 -0
  223. package/dist/core/services/gitignore-manager.d.ts +29 -0
  224. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  225. package/dist/core/services/gitignore-manager.js +106 -0
  226. package/dist/core/services/gitignore-manager.js.map +1 -0
  227. package/dist/core/services/index.d.ts +8 -0
  228. package/dist/core/services/index.d.ts.map +1 -0
  229. package/dist/core/services/index.js +8 -0
  230. package/dist/core/services/index.js.map +1 -0
  231. package/dist/core/services/llm-service.d.ts +336 -0
  232. package/dist/core/services/llm-service.d.ts.map +1 -0
  233. package/dist/core/services/llm-service.js +1155 -0
  234. package/dist/core/services/llm-service.js.map +1 -0
  235. package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
  236. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  237. package/dist/core/services/mcp-handlers/analysis.js +300 -0
  238. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  239. package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
  240. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  241. package/dist/core/services/mcp-handlers/graph.js +509 -0
  242. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  243. package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
  244. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  245. package/dist/core/services/mcp-handlers/semantic.js +172 -0
  246. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  247. package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
  248. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  249. package/dist/core/services/mcp-handlers/utils.js +62 -0
  250. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  251. package/dist/core/services/project-detector.d.ts +32 -0
  252. package/dist/core/services/project-detector.d.ts.map +1 -0
  253. package/dist/core/services/project-detector.js +111 -0
  254. package/dist/core/services/project-detector.js.map +1 -0
  255. package/dist/core/verifier/index.d.ts +5 -0
  256. package/dist/core/verifier/index.d.ts.map +1 -0
  257. package/dist/core/verifier/index.js +5 -0
  258. package/dist/core/verifier/index.js.map +1 -0
  259. package/dist/core/verifier/verification-engine.d.ts +226 -0
  260. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  261. package/dist/core/verifier/verification-engine.js +681 -0
  262. package/dist/core/verifier/verification-engine.js.map +1 -0
  263. package/dist/types/index.d.ts +252 -0
  264. package/dist/types/index.d.ts.map +1 -0
  265. package/dist/types/index.js +5 -0
  266. package/dist/types/index.js.map +1 -0
  267. package/dist/types/pipeline.d.ts +148 -0
  268. package/dist/types/pipeline.d.ts.map +1 -0
  269. package/dist/types/pipeline.js +5 -0
  270. package/dist/types/pipeline.js.map +1 -0
  271. package/dist/utils/errors.d.ts +51 -0
  272. package/dist/utils/errors.d.ts.map +1 -0
  273. package/dist/utils/errors.js +128 -0
  274. package/dist/utils/errors.js.map +1 -0
  275. package/dist/utils/logger.d.ts +149 -0
  276. package/dist/utils/logger.d.ts.map +1 -0
  277. package/dist/utils/logger.js +331 -0
  278. package/dist/utils/logger.js.map +1 -0
  279. package/dist/utils/progress.d.ts +142 -0
  280. package/dist/utils/progress.d.ts.map +1 -0
  281. package/dist/utils/progress.js +280 -0
  282. package/dist/utils/progress.js.map +1 -0
  283. package/dist/utils/prompts.d.ts +53 -0
  284. package/dist/utils/prompts.d.ts.map +1 -0
  285. package/dist/utils/prompts.js +199 -0
  286. package/dist/utils/prompts.js.map +1 -0
  287. package/dist/utils/shutdown.d.ts +89 -0
  288. package/dist/utils/shutdown.d.ts.map +1 -0
  289. package/dist/utils/shutdown.js +237 -0
  290. package/dist/utils/shutdown.js.map +1 -0
  291. package/package.json +114 -0
  292. package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
  293. package/src/viewer/app/index.html +17 -0
  294. package/src/viewer/app/main.jsx +13 -0
  295. package/src/viewer/components/ArchitectureView.jsx +177 -0
  296. package/src/viewer/components/ChatPanel.jsx +448 -0
  297. package/src/viewer/components/ClusterGraph.jsx +441 -0
  298. package/src/viewer/components/FilterBar.jsx +179 -0
  299. package/src/viewer/components/FlatGraph.jsx +275 -0
  300. package/src/viewer/components/MicroComponents.jsx +83 -0
  301. package/src/viewer/hooks/usePanZoom.js +79 -0
  302. package/src/viewer/utils/constants.js +47 -0
  303. package/src/viewer/utils/graph-helpers.js +291 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Spec Verification Engine
3
+ *
4
+ * Tests whether generated specs accurately describe the codebase by using
5
+ * the specs to predict code behavior and comparing against actual files.
6
+ */
7
+ import { readFile, writeFile, mkdir, access, readdir } from 'node:fs/promises';
8
+ import { join, basename, relative } from 'node:path';
9
+ import logger from '../../utils/logger.js';
10
+ import { ImportExportParser } from '../analyzer/import-parser.js';
11
+ // ============================================================================
12
+ // SYSTEM PROMPTS
13
+ // ============================================================================
14
+ const PREDICTION_SYSTEM_PROMPT = `You are testing the accuracy of OpenSpec specifications.
15
+
16
+ You will be given:
17
+ 1. A set of specifications describing a software system (in OpenSpec format)
18
+ 2. A file path within that system
19
+
20
+ Your task: Based ONLY on the specifications, predict:
21
+ - What this file likely does (purpose)
22
+ - What modules/files it probably imports
23
+ - What it probably exports (functions, classes, etc.)
24
+ - Key logic patterns you'd expect to see based on the spec requirements
25
+
26
+ Be specific. If the specs don't provide enough info, say so.
27
+
28
+ Respond with valid JSON only.`;
29
+ // ============================================================================
30
+ // VERIFICATION ENGINE
31
+ // ============================================================================
32
+ /**
33
+ * Spec Verification Engine
34
+ */
35
+ export class SpecVerificationEngine {
36
+ llm;
37
+ options;
38
+ specs = [];
39
+ parser;
40
+ constructor(llm, options) {
41
+ this.llm = llm;
42
+ this.parser = new ImportExportParser();
43
+ this.options = {
44
+ rootPath: options.rootPath,
45
+ openspecPath: options.openspecPath,
46
+ outputDir: options.outputDir,
47
+ minComplexity: options.minComplexity ?? 50,
48
+ maxComplexity: options.maxComplexity ?? 500,
49
+ filesPerDomain: options.filesPerDomain ?? 3,
50
+ passThreshold: options.passThreshold ?? 0.6,
51
+ generationContext: options.generationContext ?? [],
52
+ };
53
+ }
54
+ /**
55
+ * Run full verification
56
+ */
57
+ async verify(depGraph, specVersion) {
58
+ const startTime = Date.now();
59
+ // Load all specs
60
+ await this.loadSpecs();
61
+ if (this.specs.length === 0) {
62
+ throw new Error('No specs found to verify against');
63
+ }
64
+ logger.analysis(`Loaded ${this.specs.length} spec(s) for verification`);
65
+ // Select verification candidates
66
+ const candidates = this.selectCandidates(depGraph);
67
+ logger.discovery(`Selected ${candidates.length} candidate file(s) for verification`);
68
+ if (candidates.length === 0) {
69
+ throw new Error('No suitable verification candidates found');
70
+ }
71
+ // Run verification for each candidate
72
+ const results = [];
73
+ for (let i = 0; i < candidates.length; i++) {
74
+ const candidate = candidates[i];
75
+ logger.analysis(`Verifying ${i + 1}/${candidates.length}: ${candidate.path}`);
76
+ try {
77
+ const result = await this.verifyFile(candidate);
78
+ results.push(result);
79
+ }
80
+ catch (error) {
81
+ logger.warning(`Failed to verify ${candidate.path}: ${error.message}`);
82
+ }
83
+ }
84
+ // Generate report
85
+ const report = this.generateReport(results, specVersion);
86
+ // Save report
87
+ await this.saveReport(report);
88
+ const duration = Date.now() - startTime;
89
+ logger.success(`Verification complete in ${(duration / 1000).toFixed(1)}s`);
90
+ return report;
91
+ }
92
+ /**
93
+ * Load all specs from openspec directory
94
+ */
95
+ async loadSpecs() {
96
+ this.specs = [];
97
+ const specsDir = join(this.options.openspecPath, 'specs');
98
+ try {
99
+ await access(specsDir);
100
+ }
101
+ catch {
102
+ return;
103
+ }
104
+ const entries = await readdir(specsDir, { withFileTypes: true });
105
+ for (const entry of entries) {
106
+ if (!entry.isDirectory())
107
+ continue;
108
+ const specPath = join(specsDir, entry.name, 'spec.md');
109
+ try {
110
+ const content = await readFile(specPath, 'utf-8');
111
+ this.specs.push({
112
+ domain: entry.name,
113
+ path: relative(this.options.rootPath, specPath),
114
+ content,
115
+ });
116
+ }
117
+ catch {
118
+ // Spec doesn't exist for this domain
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Select verification candidate files
124
+ */
125
+ selectCandidates(depGraph) {
126
+ const candidates = [];
127
+ const usedPaths = new Set(this.options.generationContext);
128
+ // Group files by domain
129
+ const filesByDomain = new Map();
130
+ for (const node of depGraph.nodes) {
131
+ // Skip files used in generation
132
+ if (usedPaths.has(node.file.path))
133
+ continue;
134
+ // Skip test files
135
+ if (node.file.isTest)
136
+ continue;
137
+ // Skip generated files
138
+ if (node.file.isGenerated)
139
+ continue;
140
+ // Skip files outside complexity range
141
+ if (node.file.lines < this.options.minComplexity)
142
+ continue;
143
+ if (node.file.lines > this.options.maxComplexity)
144
+ continue;
145
+ // Determine domain from path
146
+ const domain = this.inferDomain(node.file.path);
147
+ if (!filesByDomain.has(domain)) {
148
+ filesByDomain.set(domain, []);
149
+ }
150
+ filesByDomain.get(domain).push(node);
151
+ }
152
+ // Select files from each domain
153
+ for (const [domain, nodes] of filesByDomain) {
154
+ // Prefer leaf nodes (low connectivity)
155
+ const sorted = nodes.sort((a, b) => {
156
+ const aConnectivity = a.metrics.inDegree + a.metrics.outDegree;
157
+ const bConnectivity = b.metrics.inDegree + b.metrics.outDegree;
158
+ return aConnectivity - bConnectivity;
159
+ });
160
+ // Take up to filesPerDomain
161
+ const selected = sorted.slice(0, this.options.filesPerDomain);
162
+ for (const node of selected) {
163
+ candidates.push({
164
+ path: node.file.path,
165
+ absolutePath: node.file.absolutePath,
166
+ domain,
167
+ usedInGeneration: false,
168
+ complexity: node.file.lines,
169
+ lines: node.file.lines,
170
+ imports: node.metrics.outDegree,
171
+ exports: node.exports.length,
172
+ });
173
+ }
174
+ }
175
+ return candidates;
176
+ }
177
+ /**
178
+ * Infer domain from file path
179
+ */
180
+ inferDomain(filePath) {
181
+ const parts = filePath.split('/');
182
+ // Look for known domain indicators
183
+ for (const part of parts) {
184
+ const lower = part.toLowerCase();
185
+ // Skip common non-domain directories
186
+ if (['src', 'lib', 'app', 'core', 'utils', 'helpers', 'common', 'shared'].includes(lower)) {
187
+ continue;
188
+ }
189
+ // Return first meaningful directory
190
+ if (part.length > 1 && !part.startsWith('.')) {
191
+ return lower;
192
+ }
193
+ }
194
+ return 'misc';
195
+ }
196
+ /**
197
+ * Verify a single file
198
+ */
199
+ async verifyFile(candidate) {
200
+ // Get prediction from LLM
201
+ const prediction = await this.getPrediction(candidate);
202
+ // Analyze actual file
203
+ const fileContent = await readFile(candidate.absolutePath, 'utf-8');
204
+ const fileAnalysis = await this.parser.parseFile(candidate.absolutePath);
205
+ // Compare prediction to actual
206
+ const purposeMatch = this.comparePurpose(prediction.predictedPurpose, fileContent);
207
+ const importMatch = this.compareImports(prediction.predictedImports, fileAnalysis.imports.map(i => i.source));
208
+ const exportMatch = this.compareExports(prediction.predictedExports, fileAnalysis.exports.map(e => e.name));
209
+ const requirementCoverage = this.analyzeRequirementCoverage(prediction.relatedRequirements, fileContent);
210
+ // Calculate overall score
211
+ const overallScore = this.calculateOverallScore(purposeMatch, importMatch, exportMatch, requirementCoverage);
212
+ // Generate feedback
213
+ const feedback = this.generateFeedback(candidate, prediction, purposeMatch, importMatch, exportMatch, requirementCoverage);
214
+ return {
215
+ filePath: candidate.path,
216
+ domain: candidate.domain,
217
+ purposeMatch,
218
+ importMatch,
219
+ exportMatch,
220
+ requirementCoverage,
221
+ overallScore,
222
+ llmConfidence: prediction.confidence,
223
+ feedback,
224
+ };
225
+ }
226
+ /**
227
+ * Get prediction from LLM
228
+ */
229
+ async getPrediction(candidate) {
230
+ // Build specs context
231
+ const specsContent = this.specs
232
+ .map(s => `=== ${s.domain} (${s.path}) ===\n${s.content}`)
233
+ .join('\n\n');
234
+ const userPrompt = `Here are the specifications:
235
+
236
+ ${specsContent}
237
+
238
+ Predict the contents of: ${candidate.path}
239
+
240
+ Respond in JSON:
241
+ {
242
+ "predictedPurpose": "...",
243
+ "predictedImports": ["...", "..."],
244
+ "predictedExports": ["...", "..."],
245
+ "predictedLogic": ["...", "..."],
246
+ "relatedRequirements": ["RequirementName1", "RequirementName2"],
247
+ "confidence": 0.0-1.0,
248
+ "reasoning": "..."
249
+ }`;
250
+ try {
251
+ const prediction = await this.llm.completeJSON({
252
+ systemPrompt: PREDICTION_SYSTEM_PROMPT,
253
+ userPrompt,
254
+ temperature: 0.3,
255
+ maxTokens: 1000,
256
+ });
257
+ return {
258
+ predictedPurpose: prediction.predictedPurpose ?? '',
259
+ predictedImports: prediction.predictedImports ?? [],
260
+ predictedExports: prediction.predictedExports ?? [],
261
+ predictedLogic: prediction.predictedLogic ?? [],
262
+ relatedRequirements: prediction.relatedRequirements ?? [],
263
+ confidence: prediction.confidence ?? 0.5,
264
+ reasoning: prediction.reasoning ?? '',
265
+ };
266
+ }
267
+ catch (error) {
268
+ logger.warning(`Prediction failed for ${candidate.path}: ${error.message}`);
269
+ return {
270
+ predictedPurpose: '',
271
+ predictedImports: [],
272
+ predictedExports: [],
273
+ predictedLogic: [],
274
+ relatedRequirements: [],
275
+ confidence: 0,
276
+ reasoning: 'Prediction failed',
277
+ };
278
+ }
279
+ }
280
+ /**
281
+ * Compare predicted purpose to actual file content
282
+ */
283
+ comparePurpose(predicted, fileContent) {
284
+ // Extract actual purpose from file comments
285
+ const actual = this.extractPurpose(fileContent);
286
+ // Calculate similarity using keyword overlap
287
+ const similarity = this.calculateSimilarity(predicted, actual);
288
+ return { predicted, actual, similarity };
289
+ }
290
+ /**
291
+ * Extract purpose from file content (comments, docstrings)
292
+ */
293
+ extractPurpose(content) {
294
+ const lines = content.split('\n');
295
+ const purposeLines = [];
296
+ // Look for JSDoc/TSDoc comment at top of file
297
+ let inBlockComment = false;
298
+ for (const line of lines.slice(0, 30)) {
299
+ const trimmed = line.trim();
300
+ if (trimmed.startsWith('/**')) {
301
+ inBlockComment = true;
302
+ continue;
303
+ }
304
+ if (trimmed.startsWith('*/') || trimmed.endsWith('*/')) {
305
+ inBlockComment = false;
306
+ break;
307
+ }
308
+ if (inBlockComment) {
309
+ const comment = trimmed.replace(/^\*\s*/, '').trim();
310
+ if (comment && !comment.startsWith('@')) {
311
+ purposeLines.push(comment);
312
+ }
313
+ }
314
+ // Single line comments at top
315
+ if (trimmed.startsWith('//') && !inBlockComment && purposeLines.length < 3) {
316
+ purposeLines.push(trimmed.replace(/^\/\/\s*/, ''));
317
+ }
318
+ }
319
+ return purposeLines.join(' ').slice(0, 500);
320
+ }
321
+ /**
322
+ * Calculate text similarity using keyword overlap
323
+ */
324
+ calculateSimilarity(text1, text2) {
325
+ if (!text1 || !text2)
326
+ return 0;
327
+ const words1 = this.extractKeywords(text1);
328
+ const words2 = this.extractKeywords(text2);
329
+ if (words1.size === 0 || words2.size === 0)
330
+ return 0;
331
+ let matches = 0;
332
+ for (const word of words1) {
333
+ if (words2.has(word))
334
+ matches++;
335
+ }
336
+ // Jaccard similarity
337
+ const union = new Set([...words1, ...words2]);
338
+ return matches / union.size;
339
+ }
340
+ /**
341
+ * Extract keywords from text
342
+ */
343
+ extractKeywords(text) {
344
+ const words = text
345
+ .toLowerCase()
346
+ .replace(/[^a-z0-9\s]/g, ' ')
347
+ .split(/\s+/)
348
+ .filter(w => w.length > 2);
349
+ // Filter out common words
350
+ const stopwords = new Set(['the', 'and', 'for', 'this', 'that', 'with', 'are', 'from', 'has', 'have', 'will', 'can', 'all', 'each', 'which', 'when', 'there', 'been', 'being', 'their', 'would', 'could', 'should']);
351
+ return new Set(words.filter(w => !stopwords.has(w)));
352
+ }
353
+ /**
354
+ * Compare predicted imports to actual
355
+ */
356
+ compareImports(predicted, actual) {
357
+ return this.calculateSetMatch(predicted.map(p => this.normalizeImport(p)), actual.map(a => this.normalizeImport(a)));
358
+ }
359
+ /**
360
+ * Normalize import path for comparison
361
+ */
362
+ normalizeImport(importPath) {
363
+ // Remove extensions
364
+ const normalized = importPath
365
+ .replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, '')
366
+ .replace(/^\.\//, '')
367
+ .replace(/^\.\.\//, '');
368
+ // Extract module name from path
369
+ const parts = normalized.split('/');
370
+ return parts[parts.length - 1].toLowerCase();
371
+ }
372
+ /**
373
+ * Compare predicted exports to actual
374
+ */
375
+ compareExports(predicted, actual) {
376
+ return this.calculateSetMatch(predicted.map(p => p.toLowerCase()), actual.map(a => a.toLowerCase()));
377
+ }
378
+ /**
379
+ * Calculate precision, recall, F1 for set comparison
380
+ */
381
+ calculateSetMatch(predicted, actual) {
382
+ const predictedSet = new Set(predicted);
383
+ const actualSet = new Set(actual);
384
+ let truePositives = 0;
385
+ for (const p of predictedSet) {
386
+ if (actualSet.has(p))
387
+ truePositives++;
388
+ }
389
+ const precision = predictedSet.size > 0 ? truePositives / predictedSet.size : 0;
390
+ const recall = actualSet.size > 0 ? truePositives / actualSet.size : 0;
391
+ const f1Score = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
392
+ return {
393
+ predicted,
394
+ actual,
395
+ precision,
396
+ recall,
397
+ f1Score,
398
+ };
399
+ }
400
+ /**
401
+ * Analyze requirement coverage
402
+ */
403
+ analyzeRequirementCoverage(relatedRequirements, fileContent) {
404
+ const actuallyImplements = [];
405
+ const contentLower = fileContent.toLowerCase();
406
+ for (const req of relatedRequirements) {
407
+ // Check if requirement keywords appear in the file
408
+ const reqWords = req.toLowerCase().split(/[\s-_]+/);
409
+ const matches = reqWords.filter(w => w.length > 3 && contentLower.includes(w));
410
+ if (matches.length >= Math.min(2, reqWords.length)) {
411
+ actuallyImplements.push(req);
412
+ }
413
+ }
414
+ const coverage = relatedRequirements.length > 0
415
+ ? actuallyImplements.length / relatedRequirements.length
416
+ : 0;
417
+ return {
418
+ relatedRequirements,
419
+ actuallyImplements,
420
+ coverage,
421
+ };
422
+ }
423
+ /**
424
+ * Calculate overall score (weighted combination)
425
+ */
426
+ calculateOverallScore(purposeMatch, importMatch, exportMatch, requirementCoverage) {
427
+ // Weights from spec:
428
+ // - Purpose: 25%
429
+ // - Imports: 30%
430
+ // - Exports: 30%
431
+ // - Requirements: 15%
432
+ return (purposeMatch.similarity * 0.25 +
433
+ importMatch.f1Score * 0.30 +
434
+ exportMatch.f1Score * 0.30 +
435
+ requirementCoverage.coverage * 0.15);
436
+ }
437
+ /**
438
+ * Generate feedback for gaps
439
+ */
440
+ generateFeedback(candidate, prediction, purposeMatch, importMatch, exportMatch, requirementCoverage) {
441
+ const feedback = [];
442
+ // Low purpose match
443
+ if (purposeMatch.similarity < 0.3) {
444
+ feedback.push(`Purpose mismatch: specs don't clearly describe what ${basename(candidate.path)} does`);
445
+ }
446
+ // Missing imports
447
+ const missingImports = importMatch.actual.filter(a => !importMatch.predicted.includes(a));
448
+ if (missingImports.length > 0) {
449
+ feedback.push(`Missing dependencies: specs don't mention ${missingImports.slice(0, 3).join(', ')}`);
450
+ }
451
+ // Missing exports
452
+ const missingExports = exportMatch.actual.filter(a => !exportMatch.predicted.includes(a));
453
+ if (missingExports.length > 0) {
454
+ feedback.push(`Undocumented exports: ${missingExports.slice(0, 3).join(', ')} not described in specs`);
455
+ }
456
+ // Low requirement coverage
457
+ if (requirementCoverage.coverage < 0.5 && prediction.relatedRequirements.length > 0) {
458
+ const missing = prediction.relatedRequirements.filter(r => !requirementCoverage.actuallyImplements.includes(r));
459
+ if (missing.length > 0) {
460
+ feedback.push(`Requirements ${missing.slice(0, 2).join(', ')} don't appear to be implemented in this file`);
461
+ }
462
+ }
463
+ // Low confidence from LLM
464
+ if (prediction.confidence < 0.5) {
465
+ feedback.push(`LLM had low confidence: "${prediction.reasoning}"`);
466
+ }
467
+ return feedback;
468
+ }
469
+ /**
470
+ * Generate verification report
471
+ */
472
+ generateReport(results, specVersion) {
473
+ const passedFiles = results.filter(r => r.overallScore >= this.options.passThreshold).length;
474
+ const overallConfidence = results.length > 0
475
+ ? results.reduce((sum, r) => sum + r.overallScore, 0) / results.length
476
+ : 0;
477
+ // Domain breakdown
478
+ const domainResults = new Map();
479
+ for (const result of results) {
480
+ if (!domainResults.has(result.domain)) {
481
+ domainResults.set(result.domain, []);
482
+ }
483
+ domainResults.get(result.domain).push(result);
484
+ }
485
+ const domainBreakdown = [];
486
+ for (const [domain, domainRes] of domainResults) {
487
+ const avgScore = domainRes.reduce((sum, r) => sum + r.overallScore, 0) / domainRes.length;
488
+ // Find weakest area
489
+ const avgPurpose = domainRes.reduce((sum, r) => sum + r.purposeMatch.similarity, 0) / domainRes.length;
490
+ const avgImport = domainRes.reduce((sum, r) => sum + r.importMatch.f1Score, 0) / domainRes.length;
491
+ const avgExport = domainRes.reduce((sum, r) => sum + r.exportMatch.f1Score, 0) / domainRes.length;
492
+ const avgReq = domainRes.reduce((sum, r) => sum + r.requirementCoverage.coverage, 0) / domainRes.length;
493
+ const areas = [
494
+ { name: 'purpose', score: avgPurpose },
495
+ { name: 'imports', score: avgImport },
496
+ { name: 'exports', score: avgExport },
497
+ { name: 'requirements', score: avgReq },
498
+ ];
499
+ const weakest = areas.sort((a, b) => a.score - b.score)[0];
500
+ domainBreakdown.push({
501
+ domain,
502
+ specPath: `openspec/specs/${domain}/spec.md`,
503
+ filesVerified: domainRes.length,
504
+ averageScore: avgScore,
505
+ weakestArea: weakest.name,
506
+ });
507
+ }
508
+ // Common gaps
509
+ const allFeedback = results.flatMap(r => r.feedback);
510
+ const feedbackCounts = new Map();
511
+ for (const fb of allFeedback) {
512
+ // Normalize feedback for grouping
513
+ const key = fb.split(':')[0];
514
+ feedbackCounts.set(key, (feedbackCounts.get(key) ?? 0) + 1);
515
+ }
516
+ const commonGaps = Array.from(feedbackCounts.entries())
517
+ .filter(([_, count]) => count >= 2)
518
+ .sort((a, b) => b[1] - a[1])
519
+ .slice(0, 5)
520
+ .map(([gap, _]) => gap);
521
+ // Suggested improvements
522
+ const suggestedImprovements = [];
523
+ for (const breakdown of domainBreakdown) {
524
+ if (breakdown.averageScore < 0.7) {
525
+ suggestedImprovements.push({
526
+ domain: breakdown.domain,
527
+ issue: `Low verification score (${(breakdown.averageScore * 100).toFixed(0)}%)`,
528
+ suggestion: `Review and enhance ${breakdown.specPath}, especially ${breakdown.weakestArea} descriptions`,
529
+ });
530
+ }
531
+ }
532
+ // Recommendation
533
+ let recommendation;
534
+ if (overallConfidence >= 0.75) {
535
+ recommendation = 'ready';
536
+ }
537
+ else if (overallConfidence >= 0.5) {
538
+ recommendation = 'needs-review';
539
+ }
540
+ else {
541
+ recommendation = 'regenerate';
542
+ }
543
+ return {
544
+ timestamp: new Date().toISOString(),
545
+ specVersion,
546
+ sampledFiles: results.length,
547
+ passedFiles,
548
+ overallConfidence,
549
+ domainBreakdown,
550
+ commonGaps,
551
+ recommendation,
552
+ suggestedImprovements,
553
+ results,
554
+ };
555
+ }
556
+ /**
557
+ * Save verification report
558
+ */
559
+ async saveReport(report) {
560
+ await mkdir(this.options.outputDir, { recursive: true });
561
+ // Save JSON report
562
+ const jsonPath = join(this.options.outputDir, 'report.json');
563
+ await writeFile(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
564
+ logger.discovery(`Saved JSON report to ${relative(this.options.rootPath, jsonPath)}`);
565
+ // Save Markdown report
566
+ const mdPath = join(this.options.outputDir, 'REPORT.md');
567
+ const markdown = this.generateMarkdownReport(report);
568
+ await writeFile(mdPath, markdown, 'utf-8');
569
+ logger.discovery(`Saved Markdown report to ${relative(this.options.rootPath, mdPath)}`);
570
+ }
571
+ /**
572
+ * Generate markdown report
573
+ */
574
+ generateMarkdownReport(report) {
575
+ const lines = [];
576
+ lines.push('# Spec Verification Report');
577
+ lines.push('');
578
+ lines.push(`Generated: ${report.timestamp}`);
579
+ lines.push(`Spec Version: ${report.specVersion}`);
580
+ lines.push('');
581
+ // Summary
582
+ lines.push('## Summary');
583
+ lines.push('');
584
+ lines.push(`| Metric | Value |`);
585
+ lines.push(`|--------|-------|`);
586
+ lines.push(`| Files Verified | ${report.sampledFiles} |`);
587
+ lines.push(`| Files Passed | ${report.passedFiles} (${((report.passedFiles / report.sampledFiles) * 100).toFixed(0)}%) |`);
588
+ lines.push(`| Overall Confidence | ${(report.overallConfidence * 100).toFixed(1)}% |`);
589
+ lines.push(`| Recommendation | **${report.recommendation}** |`);
590
+ lines.push('');
591
+ // Recommendation explanation
592
+ lines.push('### Recommendation');
593
+ if (report.recommendation === 'ready') {
594
+ lines.push('✅ Specs accurately describe the codebase and are ready for use.');
595
+ }
596
+ else if (report.recommendation === 'needs-review') {
597
+ lines.push('⚠️ Specs need review. Some gaps were identified that should be addressed.');
598
+ }
599
+ else {
600
+ lines.push('❌ Specs have significant gaps. Consider regenerating with improved context.');
601
+ }
602
+ lines.push('');
603
+ // Domain breakdown
604
+ lines.push('## Domain Breakdown');
605
+ lines.push('');
606
+ lines.push('| Domain | Spec Path | Files | Avg Score | Weakest Area |');
607
+ lines.push('|--------|-----------|-------|-----------|--------------|');
608
+ for (const domain of report.domainBreakdown) {
609
+ const scorePercent = (domain.averageScore * 100).toFixed(0);
610
+ lines.push(`| ${domain.domain} | ${domain.specPath} | ${domain.filesVerified} | ${scorePercent}% | ${domain.weakestArea} |`);
611
+ }
612
+ lines.push('');
613
+ // Common gaps
614
+ if (report.commonGaps.length > 0) {
615
+ lines.push('## Common Gaps');
616
+ lines.push('');
617
+ for (const gap of report.commonGaps) {
618
+ lines.push(`- ${gap}`);
619
+ }
620
+ lines.push('');
621
+ }
622
+ // Suggested improvements
623
+ if (report.suggestedImprovements.length > 0) {
624
+ lines.push('## Suggested Improvements');
625
+ lines.push('');
626
+ for (const improvement of report.suggestedImprovements) {
627
+ lines.push(`### ${improvement.domain}`);
628
+ lines.push(`- **Issue**: ${improvement.issue}`);
629
+ lines.push(`- **Suggestion**: ${improvement.suggestion}`);
630
+ lines.push('');
631
+ }
632
+ }
633
+ // Detailed results
634
+ lines.push('## Detailed Results');
635
+ lines.push('');
636
+ for (const result of report.results) {
637
+ const scorePercent = (result.overallScore * 100).toFixed(0);
638
+ const status = result.overallScore >= 0.6 ? '✅' : '❌';
639
+ lines.push(`### ${status} ${result.filePath}`);
640
+ lines.push('');
641
+ lines.push(`- **Domain**: ${result.domain}`);
642
+ lines.push(`- **Overall Score**: ${scorePercent}%`);
643
+ lines.push(`- **LLM Confidence**: ${(result.llmConfidence * 100).toFixed(0)}%`);
644
+ lines.push('');
645
+ lines.push('| Category | Score |');
646
+ lines.push('|----------|-------|');
647
+ lines.push(`| Purpose Match | ${(result.purposeMatch.similarity * 100).toFixed(0)}% |`);
648
+ lines.push(`| Import Match (F1) | ${(result.importMatch.f1Score * 100).toFixed(0)}% |`);
649
+ lines.push(`| Export Match (F1) | ${(result.exportMatch.f1Score * 100).toFixed(0)}% |`);
650
+ lines.push(`| Requirement Coverage | ${(result.requirementCoverage.coverage * 100).toFixed(0)}% |`);
651
+ lines.push('');
652
+ if (result.feedback.length > 0) {
653
+ lines.push('**Feedback:**');
654
+ for (const fb of result.feedback) {
655
+ lines.push(`- ${fb}`);
656
+ }
657
+ lines.push('');
658
+ }
659
+ }
660
+ lines.push('---');
661
+ lines.push('*Generated by spec-gen verify*');
662
+ return lines.join('\n');
663
+ }
664
+ /**
665
+ * Get list of domains
666
+ */
667
+ getDomains() {
668
+ return this.specs.map(s => s.domain);
669
+ }
670
+ }
671
+ // ============================================================================
672
+ // CONVENIENCE FUNCTIONS
673
+ // ============================================================================
674
+ /**
675
+ * Run verification on a project
676
+ */
677
+ export async function verifySpecs(llm, depGraph, options, specVersion) {
678
+ const engine = new SpecVerificationEngine(llm, options);
679
+ return engine.verify(depGraph, specVersion);
680
+ }
681
+ //# sourceMappingURL=verification-engine.js.map