@planu/cli 0.89.0 → 0.90.1

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 (446) hide show
  1. package/dist/cli/commands/activate.d.ts +14 -0
  2. package/dist/cli/commands/activate.d.ts.map +1 -0
  3. package/dist/cli/commands/activate.js +174 -0
  4. package/dist/cli/commands/activate.js.map +1 -0
  5. package/dist/cli/commands/doctor.d.ts +16 -0
  6. package/dist/cli/commands/doctor.d.ts.map +1 -0
  7. package/dist/cli/commands/doctor.js +162 -0
  8. package/dist/cli/commands/doctor.js.map +1 -0
  9. package/dist/cli/commands/install.d.ts +48 -0
  10. package/dist/cli/commands/install.d.ts.map +1 -0
  11. package/dist/cli/commands/install.js +348 -0
  12. package/dist/cli/commands/install.js.map +1 -0
  13. package/dist/cli/commands/uninstall.d.ts +10 -0
  14. package/dist/cli/commands/uninstall.d.ts.map +1 -0
  15. package/dist/cli/commands/uninstall.js +133 -0
  16. package/dist/cli/commands/uninstall.js.map +1 -0
  17. package/dist/cli/router.d.ts.map +1 -1
  18. package/dist/cli/router.js +9 -1
  19. package/dist/cli/router.js.map +1 -1
  20. package/dist/config/license-plans.json +2 -1
  21. package/dist/engine/agent-generator.test.d.ts +2 -0
  22. package/dist/engine/agent-generator.test.d.ts.map +1 -0
  23. package/dist/engine/agent-generator.test.js +556 -0
  24. package/dist/engine/agent-generator.test.js.map +1 -0
  25. package/dist/engine/analyzer.test.d.ts +2 -0
  26. package/dist/engine/analyzer.test.d.ts.map +1 -0
  27. package/dist/engine/analyzer.test.js +1461 -0
  28. package/dist/engine/analyzer.test.js.map +1 -0
  29. package/dist/engine/auditor.test.d.ts +2 -0
  30. package/dist/engine/auditor.test.d.ts.map +1 -0
  31. package/dist/engine/auditor.test.js +2075 -0
  32. package/dist/engine/auditor.test.js.map +1 -0
  33. package/dist/engine/doc-generator.test.d.ts +2 -0
  34. package/dist/engine/doc-generator.test.d.ts.map +1 -0
  35. package/dist/engine/doc-generator.test.js +961 -0
  36. package/dist/engine/doc-generator.test.js.map +1 -0
  37. package/dist/engine/estimator.test.d.ts +2 -0
  38. package/dist/engine/estimator.test.d.ts.map +1 -0
  39. package/dist/engine/estimator.test.js +334 -0
  40. package/dist/engine/estimator.test.js.map +1 -0
  41. package/dist/engine/skill-generator.test.d.ts +2 -0
  42. package/dist/engine/skill-generator.test.d.ts.map +1 -0
  43. package/dist/engine/skill-generator.test.js +742 -0
  44. package/dist/engine/skill-generator.test.js.map +1 -0
  45. package/dist/engine/spec-migrator/filesystem-import.d.ts +14 -0
  46. package/dist/engine/spec-migrator/filesystem-import.d.ts.map +1 -0
  47. package/dist/engine/spec-migrator/filesystem-import.js +96 -0
  48. package/dist/engine/spec-migrator/filesystem-import.js.map +1 -0
  49. package/dist/engine/spec-migrator/flatten-specs.d.ts +12 -0
  50. package/dist/engine/spec-migrator/flatten-specs.d.ts.map +1 -0
  51. package/dist/engine/spec-migrator/flatten-specs.js +111 -0
  52. package/dist/engine/spec-migrator/flatten-specs.js.map +1 -0
  53. package/dist/engine/spec-migrator/folder-operations.d.ts +9 -0
  54. package/dist/engine/spec-migrator/folder-operations.d.ts.map +1 -0
  55. package/dist/engine/spec-migrator/folder-operations.js +109 -0
  56. package/dist/engine/spec-migrator/folder-operations.js.map +1 -0
  57. package/dist/engine/spec-migrator/frontmatter-parser.d.ts +11 -0
  58. package/dist/engine/spec-migrator/frontmatter-parser.d.ts.map +1 -0
  59. package/dist/engine/spec-migrator/frontmatter-parser.js +92 -0
  60. package/dist/engine/spec-migrator/frontmatter-parser.js.map +1 -0
  61. package/dist/engine/spec-migrator/index.d.ts +9 -0
  62. package/dist/engine/spec-migrator/index.d.ts.map +1 -0
  63. package/dist/engine/spec-migrator/index.js +18 -0
  64. package/dist/engine/spec-migrator/index.js.map +1 -0
  65. package/dist/engine/spec-migrator/legacy-migration.d.ts +13 -0
  66. package/dist/engine/spec-migrator/legacy-migration.d.ts.map +1 -0
  67. package/dist/engine/spec-migrator/legacy-migration.js +75 -0
  68. package/dist/engine/spec-migrator/legacy-migration.js.map +1 -0
  69. package/dist/engine/spec-migrator/migration-validator.d.ts +20 -0
  70. package/dist/engine/spec-migrator/migration-validator.d.ts.map +1 -0
  71. package/dist/engine/spec-migrator/migration-validator.js +35 -0
  72. package/dist/engine/spec-migrator/migration-validator.js.map +1 -0
  73. package/dist/engine/spec-migrator/path-utils.d.ts +13 -0
  74. package/dist/engine/spec-migrator/path-utils.d.ts.map +1 -0
  75. package/dist/engine/spec-migrator/path-utils.js +40 -0
  76. package/dist/engine/spec-migrator/path-utils.js.map +1 -0
  77. package/dist/engine/spec-migrator/prefix-migration.d.ts +11 -0
  78. package/dist/engine/spec-migrator/prefix-migration.d.ts.map +1 -0
  79. package/dist/engine/spec-migrator/prefix-migration.js +73 -0
  80. package/dist/engine/spec-migrator/prefix-migration.js.map +1 -0
  81. package/dist/engine/spec-migrator/reconcile-paths.d.ts +12 -0
  82. package/dist/engine/spec-migrator/reconcile-paths.d.ts.map +1 -0
  83. package/dist/engine/spec-migrator/reconcile-paths.js +77 -0
  84. package/dist/engine/spec-migrator/reconcile-paths.js.map +1 -0
  85. package/dist/engine/spec-migrator/version-detection.d.ts +5 -0
  86. package/dist/engine/spec-migrator/version-detection.d.ts.map +1 -0
  87. package/dist/engine/spec-migrator/version-detection.js +19 -0
  88. package/dist/engine/spec-migrator/version-detection.js.map +1 -0
  89. package/dist/engine/spec-migrator.d.ts +1 -58
  90. package/dist/engine/spec-migrator.d.ts.map +1 -1
  91. package/dist/engine/spec-migrator.js +2 -658
  92. package/dist/engine/spec-migrator.js.map +1 -1
  93. package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +6 -0
  94. package/dist/engine/spec-summary-html/dashboard-renderer.d.ts.map +1 -0
  95. package/dist/engine/spec-summary-html/dashboard-renderer.js +333 -0
  96. package/dist/engine/spec-summary-html/dashboard-renderer.js.map +1 -0
  97. package/dist/engine/spec-summary-html/hash-utils.d.ts +11 -0
  98. package/dist/engine/spec-summary-html/hash-utils.d.ts.map +1 -0
  99. package/dist/engine/spec-summary-html/hash-utils.js +39 -0
  100. package/dist/engine/spec-summary-html/hash-utils.js.map +1 -0
  101. package/dist/engine/spec-summary-html/index.d.ts +4 -0
  102. package/dist/engine/spec-summary-html/index.d.ts.map +1 -0
  103. package/dist/engine/spec-summary-html/index.js +6 -0
  104. package/dist/engine/spec-summary-html/index.js.map +1 -0
  105. package/dist/engine/spec-summary-html/report-renderer.d.ts +9 -0
  106. package/dist/engine/spec-summary-html/report-renderer.d.ts.map +1 -0
  107. package/dist/engine/spec-summary-html/report-renderer.js +139 -0
  108. package/dist/engine/spec-summary-html/report-renderer.js.map +1 -0
  109. package/dist/engine/spec-summary-html.d.ts +1 -0
  110. package/dist/engine/spec-summary-html.d.ts.map +1 -1
  111. package/dist/engine/spec-summary-html.js +19 -473
  112. package/dist/engine/spec-summary-html.js.map +1 -1
  113. package/dist/engine/update-notifier.d.ts +8 -0
  114. package/dist/engine/update-notifier.d.ts.map +1 -0
  115. package/dist/engine/update-notifier.js +130 -0
  116. package/dist/engine/update-notifier.js.map +1 -0
  117. package/dist/engine/validator/dor-dod.d.ts.map +1 -1
  118. package/dist/engine/validator/dor-dod.js +8 -5
  119. package/dist/engine/validator/dor-dod.js.map +1 -1
  120. package/dist/engine/validator.d.ts.map +1 -1
  121. package/dist/engine/validator.js +4 -3
  122. package/dist/engine/validator.js.map +1 -1
  123. package/dist/engine/validator.test.d.ts +2 -0
  124. package/dist/engine/validator.test.d.ts.map +1 -0
  125. package/dist/engine/validator.test.js +2371 -0
  126. package/dist/engine/validator.test.js.map +1 -0
  127. package/dist/engine/web-fetcher.test.d.ts +2 -0
  128. package/dist/engine/web-fetcher.test.d.ts.map +1 -0
  129. package/dist/engine/web-fetcher.test.js +360 -0
  130. package/dist/engine/web-fetcher.test.js.map +1 -0
  131. package/dist/i18n/index.test.d.ts +2 -0
  132. package/dist/i18n/index.test.d.ts.map +1 -0
  133. package/dist/i18n/index.test.js +375 -0
  134. package/dist/i18n/index.test.js.map +1 -0
  135. package/dist/index.js +8 -0
  136. package/dist/index.js.map +1 -1
  137. package/dist/index.test.d.ts +2 -0
  138. package/dist/index.test.d.ts.map +1 -0
  139. package/dist/index.test.js +124 -0
  140. package/dist/index.test.js.map +1 -0
  141. package/dist/resources/patterns.test.d.ts +2 -0
  142. package/dist/resources/patterns.test.d.ts.map +1 -0
  143. package/dist/resources/patterns.test.js +142 -0
  144. package/dist/resources/patterns.test.js.map +1 -0
  145. package/dist/resources/process.test.d.ts +2 -0
  146. package/dist/resources/process.test.d.ts.map +1 -0
  147. package/dist/resources/process.test.js +48 -0
  148. package/dist/resources/process.test.js.map +1 -0
  149. package/dist/resources/registry.test.d.ts +2 -0
  150. package/dist/resources/registry.test.d.ts.map +1 -0
  151. package/dist/resources/registry.test.js +138 -0
  152. package/dist/resources/registry.test.js.map +1 -0
  153. package/dist/resources/specs.test.d.ts +2 -0
  154. package/dist/resources/specs.test.d.ts.map +1 -0
  155. package/dist/resources/specs.test.js +130 -0
  156. package/dist/resources/specs.test.js.map +1 -0
  157. package/dist/resources/templates.test.d.ts +2 -0
  158. package/dist/resources/templates.test.d.ts.map +1 -0
  159. package/dist/resources/templates.test.js +119 -0
  160. package/dist/resources/templates.test.js.map +1 -0
  161. package/dist/smoke.test.d.ts +2 -0
  162. package/dist/smoke.test.d.ts.map +1 -0
  163. package/dist/smoke.test.js +229 -0
  164. package/dist/smoke.test.js.map +1 -0
  165. package/dist/storage/base-store.test.d.ts +2 -0
  166. package/dist/storage/base-store.test.d.ts.map +1 -0
  167. package/dist/storage/base-store.test.js +180 -0
  168. package/dist/storage/base-store.test.js.map +1 -0
  169. package/dist/storage/global-store.test.d.ts +2 -0
  170. package/dist/storage/global-store.test.d.ts.map +1 -0
  171. package/dist/storage/global-store.test.js +327 -0
  172. package/dist/storage/global-store.test.js.map +1 -0
  173. package/dist/storage/index.test.d.ts +2 -0
  174. package/dist/storage/index.test.d.ts.map +1 -0
  175. package/dist/storage/index.test.js +56 -0
  176. package/dist/storage/index.test.js.map +1 -0
  177. package/dist/storage/knowledge-store.test.d.ts +2 -0
  178. package/dist/storage/knowledge-store.test.d.ts.map +1 -0
  179. package/dist/storage/knowledge-store.test.js +368 -0
  180. package/dist/storage/knowledge-store.test.js.map +1 -0
  181. package/dist/storage/metrics-store.test.d.ts +2 -0
  182. package/dist/storage/metrics-store.test.d.ts.map +1 -0
  183. package/dist/storage/metrics-store.test.js +212 -0
  184. package/dist/storage/metrics-store.test.js.map +1 -0
  185. package/dist/storage/pattern-store.test.d.ts +2 -0
  186. package/dist/storage/pattern-store.test.d.ts.map +1 -0
  187. package/dist/storage/pattern-store.test.js +224 -0
  188. package/dist/storage/pattern-store.test.js.map +1 -0
  189. package/dist/storage/spec-store.test.d.ts +2 -0
  190. package/dist/storage/spec-store.test.d.ts.map +1 -0
  191. package/dist/storage/spec-store.test.js +227 -0
  192. package/dist/storage/spec-store.test.js.map +1 -0
  193. package/dist/tools/audit.test.d.ts +2 -0
  194. package/dist/tools/audit.test.d.ts.map +1 -0
  195. package/dist/tools/audit.test.js +169 -0
  196. package/dist/tools/audit.test.js.map +1 -0
  197. package/dist/tools/challenge-spec.test.d.ts +2 -0
  198. package/dist/tools/challenge-spec.test.d.ts.map +1 -0
  199. package/dist/tools/challenge-spec.test.js +782 -0
  200. package/dist/tools/challenge-spec.test.js.map +1 -0
  201. package/dist/tools/check-versions.test.d.ts +2 -0
  202. package/dist/tools/check-versions.test.d.ts.map +1 -0
  203. package/dist/tools/check-versions.test.js +214 -0
  204. package/dist/tools/check-versions.test.js.map +1 -0
  205. package/dist/tools/clarify-requirements.test.d.ts +2 -0
  206. package/dist/tools/clarify-requirements.test.d.ts.map +1 -0
  207. package/dist/tools/clarify-requirements.test.js +161 -0
  208. package/dist/tools/clarify-requirements.test.js.map +1 -0
  209. package/dist/tools/consult-docs.test.d.ts +2 -0
  210. package/dist/tools/consult-docs.test.d.ts.map +1 -0
  211. package/dist/tools/consult-docs.test.js +140 -0
  212. package/dist/tools/consult-docs.test.js.map +1 -0
  213. package/dist/tools/create-spec.test.d.ts +2 -0
  214. package/dist/tools/create-spec.test.d.ts.map +1 -0
  215. package/dist/tools/create-spec.test.js +233 -0
  216. package/dist/tools/create-spec.test.js.map +1 -0
  217. package/dist/tools/define-ui-contract.test.d.ts +2 -0
  218. package/dist/tools/define-ui-contract.test.d.ts.map +1 -0
  219. package/dist/tools/define-ui-contract.test.js +479 -0
  220. package/dist/tools/define-ui-contract.test.js.map +1 -0
  221. package/dist/tools/design-schema.test.d.ts +2 -0
  222. package/dist/tools/design-schema.test.d.ts.map +1 -0
  223. package/dist/tools/design-schema.test.js +301 -0
  224. package/dist/tools/design-schema.test.js.map +1 -0
  225. package/dist/tools/detect-agent.test.d.ts +2 -0
  226. package/dist/tools/detect-agent.test.d.ts.map +1 -0
  227. package/dist/tools/detect-agent.test.js +133 -0
  228. package/dist/tools/detect-agent.test.js.map +1 -0
  229. package/dist/tools/detect-drift.test.d.ts +2 -0
  230. package/dist/tools/detect-drift.test.d.ts.map +1 -0
  231. package/dist/tools/detect-drift.test.js +312 -0
  232. package/dist/tools/detect-drift.test.js.map +1 -0
  233. package/dist/tools/discover-mcps.test.d.ts +2 -0
  234. package/dist/tools/discover-mcps.test.d.ts.map +1 -0
  235. package/dist/tools/discover-mcps.test.js +345 -0
  236. package/dist/tools/discover-mcps.test.js.map +1 -0
  237. package/dist/tools/estimate.test.d.ts +2 -0
  238. package/dist/tools/estimate.test.d.ts.map +1 -0
  239. package/dist/tools/estimate.test.js +137 -0
  240. package/dist/tools/estimate.test.js.map +1 -0
  241. package/dist/tools/generate-adr.test.d.ts +2 -0
  242. package/dist/tools/generate-adr.test.d.ts.map +1 -0
  243. package/dist/tools/generate-adr.test.js +206 -0
  244. package/dist/tools/generate-adr.test.js.map +1 -0
  245. package/dist/tools/generate-checklist.test.d.ts +2 -0
  246. package/dist/tools/generate-checklist.test.d.ts.map +1 -0
  247. package/dist/tools/generate-checklist.test.js +201 -0
  248. package/dist/tools/generate-checklist.test.js.map +1 -0
  249. package/dist/tools/generate-docs.test.d.ts +2 -0
  250. package/dist/tools/generate-docs.test.d.ts.map +1 -0
  251. package/dist/tools/generate-docs.test.js +183 -0
  252. package/dist/tools/generate-docs.test.js.map +1 -0
  253. package/dist/tools/generate-execution-plan.test.d.ts +2 -0
  254. package/dist/tools/generate-execution-plan.test.d.ts.map +1 -0
  255. package/dist/tools/generate-execution-plan.test.js +643 -0
  256. package/dist/tools/generate-execution-plan.test.js.map +1 -0
  257. package/dist/tools/generate-rules.test.d.ts +2 -0
  258. package/dist/tools/generate-rules.test.d.ts.map +1 -0
  259. package/dist/tools/generate-rules.test.js +148 -0
  260. package/dist/tools/generate-rules.test.js.map +1 -0
  261. package/dist/tools/generate-skill.test.d.ts +2 -0
  262. package/dist/tools/generate-skill.test.d.ts.map +1 -0
  263. package/dist/tools/generate-skill.test.js +138 -0
  264. package/dist/tools/generate-skill.test.js.map +1 -0
  265. package/dist/tools/generate-sub-agent.test.d.ts +2 -0
  266. package/dist/tools/generate-sub-agent.test.d.ts.map +1 -0
  267. package/dist/tools/generate-sub-agent.test.js +162 -0
  268. package/dist/tools/generate-sub-agent.test.js.map +1 -0
  269. package/dist/tools/generate-tests.test.d.ts +2 -0
  270. package/dist/tools/generate-tests.test.d.ts.map +1 -0
  271. package/dist/tools/generate-tests.test.js +222 -0
  272. package/dist/tools/generate-tests.test.js.map +1 -0
  273. package/dist/tools/init-constitution.test.d.ts +2 -0
  274. package/dist/tools/init-constitution.test.d.ts.map +1 -0
  275. package/dist/tools/init-constitution.test.js +398 -0
  276. package/dist/tools/init-constitution.test.js.map +1 -0
  277. package/dist/tools/init-project/config-builder.d.ts +12 -0
  278. package/dist/tools/init-project/config-builder.d.ts.map +1 -0
  279. package/dist/tools/init-project/config-builder.js +31 -0
  280. package/dist/tools/init-project/config-builder.js.map +1 -0
  281. package/dist/tools/init-project/git-setup.d.ts +8 -0
  282. package/dist/tools/init-project/git-setup.d.ts.map +1 -0
  283. package/dist/tools/init-project/git-setup.js +70 -0
  284. package/dist/tools/init-project/git-setup.js.map +1 -0
  285. package/dist/tools/init-project/handler.d.ts.map +1 -1
  286. package/dist/tools/init-project/handler.js +25 -371
  287. package/dist/tools/init-project/handler.js.map +1 -1
  288. package/dist/tools/init-project/lifecycle-helpers.d.ts +32 -0
  289. package/dist/tools/init-project/lifecycle-helpers.d.ts.map +1 -0
  290. package/dist/tools/init-project/lifecycle-helpers.js +153 -0
  291. package/dist/tools/init-project/lifecycle-helpers.js.map +1 -0
  292. package/dist/tools/init-project/migration-runner.d.ts +28 -0
  293. package/dist/tools/init-project/migration-runner.d.ts.map +1 -0
  294. package/dist/tools/init-project/migration-runner.js +57 -0
  295. package/dist/tools/init-project/migration-runner.js.map +1 -0
  296. package/dist/tools/init-project/rules-writer.d.ts +14 -0
  297. package/dist/tools/init-project/rules-writer.d.ts.map +1 -0
  298. package/dist/tools/init-project/rules-writer.js +43 -0
  299. package/dist/tools/init-project/rules-writer.js.map +1 -0
  300. package/dist/tools/init-project/scaffold-writer.d.ts +29 -0
  301. package/dist/tools/init-project/scaffold-writer.d.ts.map +1 -0
  302. package/dist/tools/init-project/scaffold-writer.js +76 -0
  303. package/dist/tools/init-project/scaffold-writer.js.map +1 -0
  304. package/dist/tools/init-project/stack-detector.d.ts +16 -0
  305. package/dist/tools/init-project/stack-detector.d.ts.map +1 -0
  306. package/dist/tools/init-project/stack-detector.js +19 -0
  307. package/dist/tools/init-project/stack-detector.js.map +1 -0
  308. package/dist/tools/init-project.test.d.ts +2 -0
  309. package/dist/tools/init-project.test.d.ts.map +1 -0
  310. package/dist/tools/init-project.test.js +158 -0
  311. package/dist/tools/init-project.test.js.map +1 -0
  312. package/dist/tools/integrate-pm.test.d.ts +2 -0
  313. package/dist/tools/integrate-pm.test.d.ts.map +1 -0
  314. package/dist/tools/integrate-pm.test.js +558 -0
  315. package/dist/tools/integrate-pm.test.js.map +1 -0
  316. package/dist/tools/learn.test.d.ts +2 -0
  317. package/dist/tools/learn.test.d.ts.map +1 -0
  318. package/dist/tools/learn.test.js +123 -0
  319. package/dist/tools/learn.test.js.map +1 -0
  320. package/dist/tools/list-specs.js +1 -1
  321. package/dist/tools/list-specs.js.map +1 -1
  322. package/dist/tools/list-specs.test.d.ts +2 -0
  323. package/dist/tools/list-specs.test.d.ts.map +1 -0
  324. package/dist/tools/list-specs.test.js +110 -0
  325. package/dist/tools/list-specs.test.js.map +1 -0
  326. package/dist/tools/manage-context.test.d.ts +2 -0
  327. package/dist/tools/manage-context.test.d.ts.map +1 -0
  328. package/dist/tools/manage-context.test.js +359 -0
  329. package/dist/tools/manage-context.test.js.map +1 -0
  330. package/dist/tools/manage-git.test.d.ts +2 -0
  331. package/dist/tools/manage-git.test.d.ts.map +1 -0
  332. package/dist/tools/manage-git.test.js +882 -0
  333. package/dist/tools/manage-git.test.js.map +1 -0
  334. package/dist/tools/orchestrate.test.d.ts +2 -0
  335. package/dist/tools/orchestrate.test.d.ts.map +1 -0
  336. package/dist/tools/orchestrate.test.js +1117 -0
  337. package/dist/tools/orchestrate.test.js.map +1 -0
  338. package/dist/tools/reconcile-spec.test.d.ts +2 -0
  339. package/dist/tools/reconcile-spec.test.d.ts.map +1 -0
  340. package/dist/tools/reconcile-spec.test.js +259 -0
  341. package/dist/tools/reconcile-spec.test.js.map +1 -0
  342. package/dist/tools/red-team.d.ts +3 -0
  343. package/dist/tools/red-team.d.ts.map +1 -0
  344. package/dist/tools/red-team.js +302 -0
  345. package/dist/tools/red-team.js.map +1 -0
  346. package/dist/tools/register-platform-tools/design-stack-tools.d.ts.map +1 -1
  347. package/dist/tools/register-platform-tools/design-stack-tools.js +14 -0
  348. package/dist/tools/register-platform-tools/design-stack-tools.js.map +1 -1
  349. package/dist/tools/register-platform-tools.test.d.ts +2 -0
  350. package/dist/tools/register-platform-tools.test.d.ts.map +1 -0
  351. package/dist/tools/register-platform-tools.test.js +404 -0
  352. package/dist/tools/register-platform-tools.test.js.map +1 -0
  353. package/dist/tools/register-spec-tools.test.d.ts +2 -0
  354. package/dist/tools/register-spec-tools.test.d.ts.map +1 -0
  355. package/dist/tools/register-spec-tools.test.js +407 -0
  356. package/dist/tools/register-spec-tools.test.js.map +1 -0
  357. package/dist/tools/reverse-engineer.test.d.ts +2 -0
  358. package/dist/tools/reverse-engineer.test.d.ts.map +1 -0
  359. package/dist/tools/reverse-engineer.test.js +206 -0
  360. package/dist/tools/reverse-engineer.test.js.map +1 -0
  361. package/dist/tools/schemas.d.ts +20 -0
  362. package/dist/tools/schemas.d.ts.map +1 -0
  363. package/dist/tools/schemas.js +133 -0
  364. package/dist/tools/schemas.js.map +1 -0
  365. package/dist/tools/schemas.test.d.ts +2 -0
  366. package/dist/tools/schemas.test.d.ts.map +1 -0
  367. package/dist/tools/schemas.test.js +245 -0
  368. package/dist/tools/schemas.test.js.map +1 -0
  369. package/dist/tools/set-locale.test.d.ts +2 -0
  370. package/dist/tools/set-locale.test.d.ts.map +1 -0
  371. package/dist/tools/set-locale.test.js +74 -0
  372. package/dist/tools/set-locale.test.js.map +1 -0
  373. package/dist/tools/suggest-mcps.test.d.ts +2 -0
  374. package/dist/tools/suggest-mcps.test.d.ts.map +1 -0
  375. package/dist/tools/suggest-mcps.test.js +198 -0
  376. package/dist/tools/suggest-mcps.test.js.map +1 -0
  377. package/dist/tools/suggest-stack.test.d.ts +2 -0
  378. package/dist/tools/suggest-stack.test.d.ts.map +1 -0
  379. package/dist/tools/suggest-stack.test.js +181 -0
  380. package/dist/tools/suggest-stack.test.js.map +1 -0
  381. package/dist/tools/suggest-tooling.test.d.ts +2 -0
  382. package/dist/tools/suggest-tooling.test.d.ts.map +1 -0
  383. package/dist/tools/suggest-tooling.test.js +213 -0
  384. package/dist/tools/suggest-tooling.test.js.map +1 -0
  385. package/dist/tools/summarize-spec.test.d.ts +2 -0
  386. package/dist/tools/summarize-spec.test.d.ts.map +1 -0
  387. package/dist/tools/summarize-spec.test.js +180 -0
  388. package/dist/tools/summarize-spec.test.js.map +1 -0
  389. package/dist/tools/update-status/dod-gates.d.ts +16 -0
  390. package/dist/tools/update-status/dod-gates.d.ts.map +1 -0
  391. package/dist/tools/update-status/dod-gates.js +117 -0
  392. package/dist/tools/update-status/dod-gates.js.map +1 -0
  393. package/dist/tools/update-status/file-sync.d.ts +6 -0
  394. package/dist/tools/update-status/file-sync.d.ts.map +1 -0
  395. package/dist/tools/update-status/file-sync.js +112 -0
  396. package/dist/tools/update-status/file-sync.js.map +1 -0
  397. package/dist/tools/update-status/index.d.ts +3 -0
  398. package/dist/tools/update-status/index.d.ts.map +1 -0
  399. package/dist/tools/update-status/index.js +181 -0
  400. package/dist/tools/update-status/index.js.map +1 -0
  401. package/dist/tools/update-status/response-builder.d.ts +4 -0
  402. package/dist/tools/update-status/response-builder.d.ts.map +1 -0
  403. package/dist/tools/update-status/response-builder.js +69 -0
  404. package/dist/tools/update-status/response-builder.js.map +1 -0
  405. package/dist/tools/update-status/side-effects.d.ts +15 -0
  406. package/dist/tools/update-status/side-effects.d.ts.map +1 -0
  407. package/dist/tools/update-status/side-effects.js +64 -0
  408. package/dist/tools/update-status/side-effects.js.map +1 -0
  409. package/dist/tools/update-status/transition-guard.d.ts +20 -0
  410. package/dist/tools/update-status/transition-guard.d.ts.map +1 -0
  411. package/dist/tools/update-status/transition-guard.js +75 -0
  412. package/dist/tools/update-status/transition-guard.js.map +1 -0
  413. package/dist/tools/update-status.d.ts +1 -2
  414. package/dist/tools/update-status.d.ts.map +1 -1
  415. package/dist/tools/update-status.js +2 -481
  416. package/dist/tools/update-status.js.map +1 -1
  417. package/dist/tools/update-status.test.d.ts +2 -0
  418. package/dist/tools/update-status.test.d.ts.map +1 -0
  419. package/dist/tools/update-status.test.js +142 -0
  420. package/dist/tools/update-status.test.js.map +1 -0
  421. package/dist/tools/validate.d.ts.map +1 -1
  422. package/dist/tools/validate.js +6 -4
  423. package/dist/tools/validate.js.map +1 -1
  424. package/dist/tools/validate.test.d.ts +2 -0
  425. package/dist/tools/validate.test.d.ts.map +1 -0
  426. package/dist/tools/validate.test.js +137 -0
  427. package/dist/tools/validate.test.js.map +1 -0
  428. package/dist/types/analysis.d.ts +2 -1
  429. package/dist/types/analysis.d.ts.map +1 -1
  430. package/dist/types/index.d.ts +2 -0
  431. package/dist/types/index.d.ts.map +1 -1
  432. package/dist/types/index.js +2 -0
  433. package/dist/types/index.js.map +1 -1
  434. package/dist/types/red-team.d.ts +29 -0
  435. package/dist/types/red-team.d.ts.map +1 -0
  436. package/dist/types/red-team.js +3 -0
  437. package/dist/types/red-team.js.map +1 -0
  438. package/dist/types/update-notifier.d.ts +5 -0
  439. package/dist/types/update-notifier.d.ts.map +1 -0
  440. package/dist/types/update-notifier.js +3 -0
  441. package/dist/types/update-notifier.js.map +1 -0
  442. package/package.json +9 -2
  443. package/src/config/license-plans.json +2 -1
  444. package/src/i18n/messages/en.json +5 -0
  445. package/src/i18n/messages/es.json +5 -0
  446. package/src/i18n/messages/pt.json +5 -0
@@ -0,0 +1,1461 @@
1
+ // analyzer.test.ts — Unit tests for the project analysis engine
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ // Mock all filesystem and glob dependencies
4
+ vi.mock('node:fs/promises', () => ({
5
+ readFile: vi.fn(),
6
+ readdir: vi.fn(),
7
+ stat: vi.fn(),
8
+ access: vi.fn(),
9
+ }));
10
+ vi.mock('glob', () => ({
11
+ glob: vi.fn(),
12
+ }));
13
+ import { readFile, readdir, stat, access } from 'node:fs/promises';
14
+ import { glob } from 'glob';
15
+ const mockedReadFile = vi.mocked(readFile);
16
+ const mockedReaddir = vi.mocked(readdir);
17
+ const mockedStat = vi.mocked(stat);
18
+ const mockedAccess = vi.mocked(access);
19
+ const mockedGlob = vi.mocked(glob);
20
+ // Import after mocks are set up
21
+ const { analyzeProject, quickAnalyze, checkProjectCompleteness } = await import('./analyzer.js');
22
+ // === Helpers ===
23
+ const STACK_SIGNATURES = {
24
+ languages: {
25
+ typescript: {
26
+ files: ['tsconfig.json'],
27
+ extensions: ['.ts', '.tsx'],
28
+ },
29
+ python: {
30
+ files: ['requirements.txt', 'pyproject.toml'],
31
+ extensions: ['.py'],
32
+ },
33
+ go: {
34
+ files: ['go.mod'],
35
+ extensions: ['.go'],
36
+ },
37
+ },
38
+ frameworks: {
39
+ 'next.js': {
40
+ indicators: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
41
+ dependencies: ['next'],
42
+ },
43
+ express: {
44
+ indicators: [],
45
+ dependencies: ['express'],
46
+ },
47
+ react: {
48
+ indicators: [],
49
+ dependencies: ['react'],
50
+ },
51
+ django: {
52
+ indicators: ['manage.py'],
53
+ dependencies: ['django'],
54
+ },
55
+ expo: {
56
+ indicators: ['app.json'],
57
+ dependencies: ['expo'],
58
+ },
59
+ hono: {
60
+ indicators: [],
61
+ dependencies: ['hono'],
62
+ },
63
+ },
64
+ packageManagers: {
65
+ npm: { lockfile: 'package-lock.json' },
66
+ pnpm: { lockfile: 'pnpm-lock.yaml' },
67
+ yarn: { lockfile: 'yarn.lock' },
68
+ },
69
+ databases: {
70
+ postgresql: {
71
+ engine: 'postgresql',
72
+ indicators: ['prisma/schema.prisma'],
73
+ dependencies: ['pg', 'prisma', '@prisma/client'],
74
+ },
75
+ mongodb: {
76
+ engine: 'mongodb',
77
+ indicators: [],
78
+ dependencies: ['mongoose', 'mongodb'],
79
+ },
80
+ sqlite: {
81
+ engine: 'sqlite',
82
+ indicators: [],
83
+ dependencies: ['better-sqlite3', 'sqlite3'],
84
+ },
85
+ supabase: {
86
+ engine: 'supabase',
87
+ indicators: [],
88
+ dependencies: ['@supabase/supabase-js'],
89
+ },
90
+ firebase: {
91
+ engine: 'firebase',
92
+ indicators: ['firebase.json'],
93
+ dependencies: ['firebase'],
94
+ },
95
+ mysql: {
96
+ engine: 'mysql',
97
+ indicators: [],
98
+ dependencies: ['mysql2'],
99
+ },
100
+ },
101
+ };
102
+ const ARCH_PATTERNS = {
103
+ patterns: {
104
+ monolith: {
105
+ name: 'Monolith',
106
+ indicators: {
107
+ directories: ['src/', 'app/'],
108
+ },
109
+ },
110
+ 'modular-monolith': {
111
+ name: 'Modular Monolith',
112
+ indicators: {
113
+ directories: ['src/modules/', 'src/features/'],
114
+ },
115
+ },
116
+ layered: {
117
+ name: 'Layered',
118
+ indicators: {
119
+ directories: ['controllers/', 'services/', 'repositories/'],
120
+ },
121
+ },
122
+ microservices: {
123
+ name: 'Microservices',
124
+ indicators: {
125
+ files: ['docker-compose.yml'],
126
+ dependencies: ['@nestjs/microservices'],
127
+ },
128
+ },
129
+ },
130
+ };
131
+ /** Helper: make access() succeed for specific files */
132
+ function setupFileExists(existingFiles) {
133
+ mockedAccess.mockImplementation(((p) => {
134
+ const path = p;
135
+ if (existingFiles.some((f) => path.endsWith(f))) {
136
+ return Promise.resolve(undefined);
137
+ }
138
+ const err = new Error('ENOENT');
139
+ err.code = 'ENOENT';
140
+ return Promise.reject(err);
141
+ }));
142
+ }
143
+ /** Helper: make stat return isDirectory for specific paths */
144
+ function setupStat(directories) {
145
+ mockedStat.mockImplementation(((p) => {
146
+ const path = p;
147
+ if (directories.some((d) => path.includes(d))) {
148
+ return Promise.resolve({ isDirectory: () => true });
149
+ }
150
+ const err = new Error('ENOENT');
151
+ err.code = 'ENOENT';
152
+ return Promise.reject(err);
153
+ }));
154
+ }
155
+ beforeEach(() => {
156
+ vi.resetAllMocks();
157
+ // Default: nothing exists
158
+ mockedAccess.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
159
+ mockedReadFile.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
160
+ mockedReaddir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
161
+ mockedStat.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
162
+ mockedGlob.mockResolvedValue([]);
163
+ });
164
+ // =============================================================================
165
+ // analyzeProject — full analysis
166
+ // =============================================================================
167
+ describe('analyzeProject', () => {
168
+ it('should return a complete ProjectKnowledge object for a minimal project', async () => {
169
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
170
+ expect(result.projectId).toBe('proj-1');
171
+ expect(result.projectPath).toBe('/tmp/project');
172
+ expect(result.locale).toBe('en');
173
+ expect(result.experienceLevel).toBe('intermediate');
174
+ expect(result.language).toBeDefined();
175
+ expect(result.stack).toBeDefined();
176
+ expect(result.architecture).toBeDefined();
177
+ expect(result.conventions).toBeDefined();
178
+ expect(result.environments).toBeDefined();
179
+ expect(result.specLocation).toBe('docs/sdd/specs');
180
+ expect(result.lastAnalyzed).toBeDefined();
181
+ });
182
+ it('should detect TypeScript language from tsconfig.json', async () => {
183
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
184
+ mockedGlob.mockImplementation(((pattern) => {
185
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
186
+ if (p === 'tsconfig.json' || p === 'tsconfig.*.json') {
187
+ return ['tsconfig.json'];
188
+ }
189
+ return [];
190
+ }));
191
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
192
+ expect(result.language).toBe('typescript');
193
+ });
194
+ it('should detect Next.js framework from next.config.js', async () => {
195
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
196
+ mockedGlob.mockImplementation(((pattern) => {
197
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
198
+ if (p === 'tsconfig.json') {
199
+ return ['tsconfig.json'];
200
+ }
201
+ if (p === 'next.config.js' || p === 'next.config.mjs' || p === 'next.config.ts') {
202
+ return ['next.config.js'];
203
+ }
204
+ return [];
205
+ }));
206
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
207
+ expect(result.framework).toBe('next.js');
208
+ });
209
+ it('should detect npm as package manager from package-lock.json', async () => {
210
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
211
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
212
+ expect(result.packageManager).toBe('npm');
213
+ });
214
+ it('should detect pnpm from pnpm-lock.yaml', async () => {
215
+ setupFileExists(['tsconfig.json', 'pnpm-lock.yaml']);
216
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
217
+ expect(result.packageManager).toBe('pnpm');
218
+ });
219
+ it('should detect yarn from yarn.lock', async () => {
220
+ setupFileExists(['tsconfig.json', 'yarn.lock']);
221
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
222
+ expect(result.packageManager).toBe('yarn');
223
+ });
224
+ it('should build stack list with language, framework, database, package manager', async () => {
225
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
226
+ mockedGlob.mockImplementation(((pattern) => {
227
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
228
+ if (p === 'tsconfig.json') {
229
+ return ['tsconfig.json'];
230
+ }
231
+ if (p === 'next.config.js') {
232
+ return ['next.config.js'];
233
+ }
234
+ return [];
235
+ }));
236
+ mockedReadFile.mockImplementation(((path) => {
237
+ const p = path;
238
+ if (p.endsWith('package.json')) {
239
+ return JSON.stringify({
240
+ dependencies: { next: '14.0.0', pg: '8.0.0' },
241
+ scripts: { build: 'next build', test: 'vitest' },
242
+ });
243
+ }
244
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
245
+ }));
246
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
247
+ expect(result.stack).toContain('typescript');
248
+ expect(result.packageManager).toBe('npm');
249
+ });
250
+ it('should detect framework from dependencies in package.json', async () => {
251
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
252
+ mockedReadFile.mockImplementation(((path) => {
253
+ const p = path;
254
+ if (p.endsWith('package.json')) {
255
+ return JSON.stringify({ dependencies: { express: '4.18.0' } });
256
+ }
257
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
258
+ }));
259
+ mockedGlob.mockImplementation(((pattern) => {
260
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
261
+ if (p === 'tsconfig.json') {
262
+ return ['tsconfig.json'];
263
+ }
264
+ return [];
265
+ }));
266
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
267
+ expect(result.framework).toBe('express');
268
+ });
269
+ it('should detect database from dependencies', async () => {
270
+ mockedReadFile.mockImplementation(((path) => {
271
+ const p = path;
272
+ if (p.endsWith('package.json')) {
273
+ return JSON.stringify({ dependencies: { mongoose: '7.0.0' } });
274
+ }
275
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
276
+ }));
277
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
278
+ expect(result.database).toBe('mongodb');
279
+ });
280
+ it('should detect database from indicator files', async () => {
281
+ setupFileExists(['tsconfig.json']);
282
+ mockedGlob.mockImplementation(((pattern) => {
283
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
284
+ if (p === 'tsconfig.json') {
285
+ return ['tsconfig.json'];
286
+ }
287
+ if (p === 'prisma/schema.prisma') {
288
+ return ['prisma/schema.prisma'];
289
+ }
290
+ return [];
291
+ }));
292
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
293
+ expect(result.database).toBe('postgresql');
294
+ });
295
+ it('should return "unknown" database when none detected', async () => {
296
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
297
+ expect(result.database).toBe('unknown');
298
+ });
299
+ it('should detect monorepo apps from turbo.json', async () => {
300
+ setupFileExists(['tsconfig.json', 'turbo.json']);
301
+ mockedReaddir.mockImplementation(((path) => {
302
+ const p = path;
303
+ if (p.endsWith('/apps') || p.endsWith('/packages')) {
304
+ return [
305
+ { name: 'web', isDirectory: () => true },
306
+ { name: 'api', isDirectory: () => true },
307
+ ];
308
+ }
309
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
310
+ }));
311
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
312
+ expect(result.apps.length).toBeGreaterThan(0);
313
+ });
314
+ it('should infer frontend app type from directory name containing "web"', async () => {
315
+ setupFileExists(['tsconfig.json', 'turbo.json']);
316
+ mockedReaddir.mockImplementation(((path) => {
317
+ const p = path;
318
+ if (p.endsWith('/apps')) {
319
+ return [
320
+ { name: 'web-client', isDirectory: () => true },
321
+ ];
322
+ }
323
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
324
+ }));
325
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
326
+ const webApp = result.apps.find((a) => a.name === 'web-client');
327
+ expect(webApp?.type).toBe('frontend');
328
+ });
329
+ it('should infer backend type from directory name with "api"', async () => {
330
+ setupFileExists(['tsconfig.json', 'turbo.json']);
331
+ mockedReaddir.mockImplementation(((path) => {
332
+ const p = path;
333
+ if (p.endsWith('/apps')) {
334
+ return [
335
+ { name: 'api-server', isDirectory: () => true },
336
+ ];
337
+ }
338
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
339
+ }));
340
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
341
+ const apiApp = result.apps.find((a) => a.name === 'api-server');
342
+ expect(apiApp?.type).toBe('backend');
343
+ });
344
+ it('should infer mobile type from directory name', async () => {
345
+ setupFileExists(['tsconfig.json', 'turbo.json']);
346
+ mockedReaddir.mockImplementation(((path) => {
347
+ const p = path;
348
+ if (p.endsWith('/apps')) {
349
+ return [
350
+ { name: 'mobile-app', isDirectory: () => true },
351
+ ];
352
+ }
353
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
354
+ }));
355
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
356
+ const mApp = result.apps.find((a) => a.name === 'mobile-app');
357
+ expect(mApp?.type).toBe('mobile');
358
+ });
359
+ it('should infer shared type from directory name containing "shared"', async () => {
360
+ setupFileExists(['tsconfig.json', 'turbo.json']);
361
+ mockedReaddir.mockImplementation(((path) => {
362
+ const p = path;
363
+ if (p.endsWith('/packages')) {
364
+ return [
365
+ { name: 'shared-utils', isDirectory: () => true },
366
+ ];
367
+ }
368
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
369
+ }));
370
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
371
+ const sharedApp = result.apps.find((a) => a.name === 'shared-utils');
372
+ expect(sharedApp?.type).toBe('shared');
373
+ });
374
+ it('should infer docs type from directory name', async () => {
375
+ setupFileExists(['tsconfig.json', 'turbo.json']);
376
+ mockedReaddir.mockImplementation(((path) => {
377
+ const p = path;
378
+ if (p.endsWith('/apps')) {
379
+ return [
380
+ { name: 'docs', isDirectory: () => true },
381
+ ];
382
+ }
383
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
384
+ }));
385
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
386
+ const docsApp = result.apps.find((a) => a.name === 'docs');
387
+ expect(docsApp?.type).toBe('docs');
388
+ });
389
+ it('should infer worker type from directory name', async () => {
390
+ setupFileExists(['tsconfig.json', 'turbo.json']);
391
+ mockedReaddir.mockImplementation(((path) => {
392
+ const p = path;
393
+ if (p.endsWith('/apps')) {
394
+ return [
395
+ { name: 'worker-queue', isDirectory: () => true },
396
+ ];
397
+ }
398
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
399
+ }));
400
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
401
+ const workerApp = result.apps.find((a) => a.name === 'worker-queue');
402
+ expect(workerApp?.type).toBe('worker');
403
+ });
404
+ it('should treat root as single app when no monorepo detected', async () => {
405
+ const result = await analyzeProject('/tmp/my-project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
406
+ expect(result.apps.length).toBeGreaterThanOrEqual(0);
407
+ });
408
+ it('should detect architecture patterns scoring directories', async () => {
409
+ setupStat(['src/', 'app/']);
410
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
411
+ expect(result.architecture).toBeDefined();
412
+ expect(result.architecture.primary).toBeDefined();
413
+ });
414
+ it('should detect linting tools from config files', async () => {
415
+ mockedGlob.mockImplementation(((pattern) => {
416
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
417
+ if (p === '.eslintrc*' || p === 'eslint.config.*') {
418
+ return ['.eslintrc.js'];
419
+ }
420
+ if (p === '.prettierrc*' || p === 'prettier.config.*') {
421
+ return ['.prettierrc'];
422
+ }
423
+ return [];
424
+ }));
425
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
426
+ expect(result.linting).toBeDefined();
427
+ expect(result.linting.detectedLinters.length).toBeGreaterThanOrEqual(0);
428
+ });
429
+ it('should detect linter conflicts between eslint and biome', async () => {
430
+ mockedGlob.mockImplementation(((pattern) => {
431
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
432
+ if (p === '.eslintrc*' || p === 'eslint.config.*') {
433
+ return ['.eslintrc.js'];
434
+ }
435
+ if (p === 'biome.json' || p === 'biome.jsonc') {
436
+ return ['biome.json'];
437
+ }
438
+ return [];
439
+ }));
440
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
441
+ const conflicts = result.linting.rulesConflicts;
442
+ expect(conflicts.some((c) => c.linterA === 'eslint' && c.linterB === 'biome')).toBe(true);
443
+ });
444
+ it('should detect prettier vs biome conflict', async () => {
445
+ mockedGlob.mockImplementation(((pattern) => {
446
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
447
+ if (p === '.prettierrc*' || p === 'prettier.config.*') {
448
+ return ['.prettierrc'];
449
+ }
450
+ if (p === 'biome.json' || p === 'biome.jsonc') {
451
+ return ['biome.json'];
452
+ }
453
+ return [];
454
+ }));
455
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
456
+ const conflicts = result.linting.rulesConflicts;
457
+ expect(conflicts.some((c) => c.linterA === 'prettier' && c.linterB === 'biome')).toBe(true);
458
+ });
459
+ it('should detect linter as installed-not-configured from npm deps', async () => {
460
+ mockedReadFile.mockImplementation(((path) => {
461
+ const p = path;
462
+ if (p.endsWith('package.json')) {
463
+ return JSON.stringify({
464
+ devDependencies: { eslint: '8.0.0' },
465
+ });
466
+ }
467
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
468
+ }));
469
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
470
+ const eslintDetection = result.linting.detectedLinters.find((l) => l.tool === 'eslint');
471
+ if (eslintDetection) {
472
+ expect(eslintDetection.status).toBe('installed-not-configured');
473
+ }
474
+ });
475
+ it('should parse .env.example and detect env variables', async () => {
476
+ mockedReadFile.mockImplementation(((path) => {
477
+ const p = path;
478
+ if (p.endsWith('.env.example')) {
479
+ return 'DATABASE_URL=postgres://localhost\nAPI_SECRET_KEY=\nPORT=3000\n# comment\n\n';
480
+ }
481
+ if (p.endsWith('.gitignore')) {
482
+ return '.env\nnode_modules';
483
+ }
484
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
485
+ }));
486
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
487
+ expect(result.envSetup.variables.length).toBeGreaterThan(0);
488
+ const dbVar = result.envSetup.variables.find((v) => v.name === 'DATABASE_URL');
489
+ expect(dbVar).toBeDefined();
490
+ expect(dbVar.service).toBe('database');
491
+ const secretVar = result.envSetup.variables.find((v) => v.name === 'API_SECRET_KEY');
492
+ expect(secretVar).toBeDefined();
493
+ expect(secretVar.sensitive).toBe(true);
494
+ });
495
+ it('should detect env security issue when .gitignore does not have .env', async () => {
496
+ mockedReadFile.mockImplementation(((path) => {
497
+ const p = path;
498
+ if (p.endsWith('.gitignore')) {
499
+ return 'node_modules\ndist';
500
+ }
501
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
502
+ }));
503
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
504
+ expect(result.envSetup.security.gitignoreHasEnv).toBe(false);
505
+ expect(result.envSetup.security.issues.length).toBeGreaterThan(0);
506
+ expect(result.envSetup.security.issues[0].severity).toBe('critical');
507
+ });
508
+ it('should detect .env in gitignore as secure', async () => {
509
+ mockedReadFile.mockImplementation(((path) => {
510
+ const p = path;
511
+ if (p.endsWith('.gitignore')) {
512
+ return '.env\nnode_modules';
513
+ }
514
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
515
+ }));
516
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
517
+ expect(result.envSetup.security.gitignoreHasEnv).toBe(true);
518
+ expect(result.envSetup.security.issues).toHaveLength(0);
519
+ });
520
+ it('should detect OpenAPI contracts', async () => {
521
+ setupFileExists(['openapi.yaml']);
522
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
523
+ expect(result.apiContracts.some((c) => c.type === 'openapi')).toBe(true);
524
+ });
525
+ it('should detect GraphQL contracts', async () => {
526
+ mockedGlob.mockImplementation(((pattern) => {
527
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
528
+ if (p === 'schema.graphql' || p === 'schema.gql' || p === '**/*.graphql') {
529
+ return ['schema.graphql'];
530
+ }
531
+ return [];
532
+ }));
533
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
534
+ expect(result.apiContracts.some((c) => c.type === 'graphql')).toBe(true);
535
+ });
536
+ it('should detect tRPC contracts from dependencies', async () => {
537
+ mockedReadFile.mockImplementation(((path) => {
538
+ const p = path;
539
+ if (p.endsWith('package.json')) {
540
+ return JSON.stringify({ dependencies: { '@trpc/server': '10.0.0' } });
541
+ }
542
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
543
+ }));
544
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
545
+ expect(result.apiContracts.some((c) => c.type === 'trpc')).toBe(true);
546
+ });
547
+ it('should detect gRPC contracts from .proto files', async () => {
548
+ mockedGlob.mockImplementation(((pattern) => {
549
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
550
+ if (p === '**/*.proto') {
551
+ return ['api/service.proto'];
552
+ }
553
+ return [];
554
+ }));
555
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
556
+ expect(result.apiContracts.some((c) => c.type === 'grpc')).toBe(true);
557
+ });
558
+ it('should discover MCP servers from .claude/settings.json', async () => {
559
+ mockedReadFile.mockImplementation(((path) => {
560
+ const p = path;
561
+ if (p.endsWith('.claude/settings.json')) {
562
+ return JSON.stringify({
563
+ mcpServers: {
564
+ supabase: { command: 'npx supabase-mcp', tools: ['query', 'migrate'] },
565
+ },
566
+ });
567
+ }
568
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
569
+ }));
570
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
571
+ expect(result.availableMcps.length).toBeGreaterThan(0);
572
+ expect(result.availableMcps[0].name).toBe('supabase');
573
+ expect(result.availableMcps[0].command).toBe('npx supabase-mcp');
574
+ expect(result.availableMcps[0].tools).toEqual(['query', 'migrate']);
575
+ });
576
+ it('should handle MCP config with non-string command and non-array tools', async () => {
577
+ mockedReadFile.mockImplementation(((path) => {
578
+ const p = path;
579
+ if (p.endsWith('.claude/settings.json')) {
580
+ return JSON.stringify({
581
+ mcpServers: {
582
+ test: { command: 123, tools: 'not-array' },
583
+ },
584
+ });
585
+ }
586
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
587
+ }));
588
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
589
+ const testMcp = result.availableMcps.find((m) => m.name === 'test');
590
+ if (testMcp) {
591
+ expect(testMcp.command).toBe('');
592
+ expect(testMcp.tools).toEqual([]);
593
+ }
594
+ });
595
+ it('should detect conventions: CLAUDE.md, .cursorrules, .editorconfig', async () => {
596
+ setupFileExists(['CLAUDE.md', '.cursorrules', '.editorconfig']);
597
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
598
+ expect(result.conventions['ai-rules']).toBe('CLAUDE.md');
599
+ expect(result.conventions['cursor-rules']).toBe('.cursorrules');
600
+ expect(result.conventions['editor-config']).toBe('.editorconfig');
601
+ });
602
+ it('should detect commit conventions from commitlint config', async () => {
603
+ mockedGlob.mockImplementation(((pattern) => {
604
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
605
+ if (p === 'commitlint.config.*' || p === '.commitlintrc*') {
606
+ return ['commitlint.config.js'];
607
+ }
608
+ return [];
609
+ }));
610
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
611
+ expect(result.conventions['commit-convention']).toBe('conventional-commits');
612
+ });
613
+ it('should detect husky git hooks', async () => {
614
+ setupFileExists(['.husky']);
615
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
616
+ expect(result.conventions['git-hooks']).toBe('husky');
617
+ });
618
+ it('should detect environments from env files', async () => {
619
+ setupFileExists(['.env.staging', '.env.production']);
620
+ mockedGlob.mockImplementation(((pattern) => {
621
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
622
+ if (p === '.env.staging' || p === '.env.test' || p === '.env.production') {
623
+ return ['.env.staging'];
624
+ }
625
+ return [];
626
+ }));
627
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
628
+ expect(result.environments).toContain('development');
629
+ });
630
+ it('should default environments to development + staging + production when no env files found', async () => {
631
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
632
+ expect(result.environments).toContain('development');
633
+ expect(result.environments).toContain('staging');
634
+ expect(result.environments).toContain('production');
635
+ });
636
+ it('should detect build command from package.json scripts', async () => {
637
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
638
+ mockedReadFile.mockImplementation(((path) => {
639
+ const p = path;
640
+ if (p.endsWith('package.json')) {
641
+ return JSON.stringify({ scripts: { build: 'tsc', test: 'vitest' } });
642
+ }
643
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
644
+ }));
645
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
646
+ expect(result.buildCommand).toBe('npm run build');
647
+ });
648
+ it('should detect pnpm run build for pnpm package manager', async () => {
649
+ setupFileExists(['tsconfig.json', 'pnpm-lock.yaml']);
650
+ mockedReadFile.mockImplementation(((path) => {
651
+ const p = path;
652
+ if (p.endsWith('package.json')) {
653
+ return JSON.stringify({ scripts: { build: 'tsc', test: 'vitest' } });
654
+ }
655
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
656
+ }));
657
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
658
+ expect(result.buildCommand).toBe('pnpm run build');
659
+ });
660
+ it('should detect yarn run build for yarn package manager', async () => {
661
+ setupFileExists(['tsconfig.json', 'yarn.lock']);
662
+ mockedReadFile.mockImplementation(((path) => {
663
+ const p = path;
664
+ if (p.endsWith('package.json')) {
665
+ return JSON.stringify({ scripts: { build: 'tsc', test: 'vitest' } });
666
+ }
667
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
668
+ }));
669
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
670
+ expect(result.buildCommand).toBe('yarn run build');
671
+ });
672
+ it('should detect Makefile build command', async () => {
673
+ setupFileExists(['Makefile']);
674
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
675
+ expect(result.buildCommand).toBe('make build');
676
+ });
677
+ it('should detect cargo build for Rust projects', async () => {
678
+ setupFileExists(['Cargo.toml']);
679
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
680
+ expect(result.buildCommand).toBe('cargo build');
681
+ });
682
+ it('should detect go build for Go projects', async () => {
683
+ setupFileExists(['go.mod']);
684
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
685
+ expect(result.buildCommand).toBe('go build ./...');
686
+ });
687
+ it('should detect npm test command', async () => {
688
+ setupFileExists(['package-lock.json']);
689
+ mockedReadFile.mockImplementation(((path) => {
690
+ const p = path;
691
+ if (p.endsWith('package.json')) {
692
+ return JSON.stringify({ scripts: { test: 'vitest' } });
693
+ }
694
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
695
+ }));
696
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
697
+ expect(result.testCommand).toBe('npm test');
698
+ });
699
+ it('should detect cargo test for Rust projects', async () => {
700
+ setupFileExists(['Cargo.toml']);
701
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
702
+ expect(result.testCommand).toBe('cargo test');
703
+ });
704
+ it('should detect go test for Go projects', async () => {
705
+ setupFileExists(['go.mod']);
706
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
707
+ expect(result.testCommand).toBe('go test ./...');
708
+ });
709
+ it('should detect pytest from pytest.ini', async () => {
710
+ setupFileExists(['pytest.ini']);
711
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
712
+ expect(result.testCommand).toBe('pytest');
713
+ });
714
+ it('should detect pytest from pyproject.toml', async () => {
715
+ setupFileExists(['pyproject.toml']);
716
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
717
+ expect(result.testCommand).toBe('pytest');
718
+ });
719
+ it('should detect Docker deployment unit', async () => {
720
+ setupFileExists(['Dockerfile']);
721
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
722
+ expect(result.architecture.deploymentUnits).toContain('docker');
723
+ });
724
+ it('should detect docker-compose deployment unit', async () => {
725
+ setupFileExists(['docker-compose.yml']);
726
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
727
+ expect(result.architecture.deploymentUnits).toContain('docker-compose');
728
+ });
729
+ it('should detect vercel deployment unit', async () => {
730
+ setupFileExists(['vercel.json']);
731
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
732
+ expect(result.architecture.deploymentUnits).toContain('vercel');
733
+ });
734
+ it('should detect netlify deployment unit', async () => {
735
+ setupFileExists(['netlify.toml']);
736
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
737
+ expect(result.architecture.deploymentUnits).toContain('netlify');
738
+ });
739
+ it('should detect railway deployment unit from railway.json', async () => {
740
+ setupFileExists(['railway.json']);
741
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
742
+ expect(result.architecture.deploymentUnits).toContain('railway');
743
+ });
744
+ it('should detect fly.io deployment unit', async () => {
745
+ setupFileExists(['fly.toml']);
746
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
747
+ expect(result.architecture.deploymentUnits).toContain('fly.io');
748
+ });
749
+ it('should detect render deployment unit', async () => {
750
+ setupFileExists(['render.yaml']);
751
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
752
+ expect(result.architecture.deploymentUnits).toContain('render');
753
+ });
754
+ it('should detect terraform deployment unit', async () => {
755
+ mockedGlob.mockImplementation(((pattern) => {
756
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
757
+ if (p === '*.tf') {
758
+ return ['main.tf'];
759
+ }
760
+ return [];
761
+ }));
762
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
763
+ expect(result.architecture.deploymentUnits).toContain('terraform');
764
+ });
765
+ it('should default to "manual" when no deployment unit found', async () => {
766
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
767
+ expect(result.architecture.deploymentUnits).toContain('manual');
768
+ });
769
+ it('should detect layers from directory structure', async () => {
770
+ mockedStat.mockImplementation(((p) => {
771
+ const path = p;
772
+ if (path.includes('/src/services') ||
773
+ path.includes('/src/components') ||
774
+ path.includes('/src/lib')) {
775
+ return { isDirectory: () => true };
776
+ }
777
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
778
+ }));
779
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
780
+ expect(result.architecture.layers.length).toBeGreaterThanOrEqual(0);
781
+ });
782
+ it('should detect module boundaries from src/modules', async () => {
783
+ mockedReaddir.mockImplementation(((path) => {
784
+ const p = path;
785
+ if (p.endsWith('/src/modules')) {
786
+ return [
787
+ { name: 'auth', isDirectory: () => true },
788
+ { name: 'billing', isDirectory: () => true },
789
+ ];
790
+ }
791
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
792
+ }));
793
+ // Make index.ts exist for auth module
794
+ setupFileExists(['src/modules/auth/index.ts']);
795
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
796
+ expect(result.architecture.boundaries.length).toBeGreaterThan(0);
797
+ });
798
+ it('should detect communication patterns based on architecture', async () => {
799
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
800
+ expect(result.architecture.communicationPatterns).toBeDefined();
801
+ expect(result.architecture.communicationPatterns.length).toBeGreaterThan(0);
802
+ });
803
+ it('should include default quality profile', async () => {
804
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
805
+ expect(result.qualityProfile).toBeDefined();
806
+ expect(result.qualityProfile.enabledCategories).toContain('solid');
807
+ expect(result.qualityProfile.principles).toContain('SOLID');
808
+ expect(result.qualityProfile.strictness).toBe('standard');
809
+ });
810
+ it('should collect dependencies from requirements.txt', async () => {
811
+ mockedReadFile.mockImplementation(((path) => {
812
+ const p = path;
813
+ if (p.endsWith('requirements.txt')) {
814
+ return 'django>=4.0\ncelery~=5.3\n# comment\nredis';
815
+ }
816
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
817
+ }));
818
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
819
+ expect(result.framework).toBe('django');
820
+ });
821
+ it('should collect dependencies from pyproject.toml', async () => {
822
+ mockedReadFile.mockImplementation(((path) => {
823
+ const p = path;
824
+ if (p.endsWith('pyproject.toml')) {
825
+ return '[project]\ndependencies = [\n"django>=4.0"\n"celery>=5.3"\n]';
826
+ }
827
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
828
+ }));
829
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
830
+ expect(result.framework).toBe('django');
831
+ });
832
+ it('should collect dependencies from go.mod', async () => {
833
+ mockedReadFile.mockImplementation(((path) => {
834
+ const p = path;
835
+ if (p.endsWith('go.mod')) {
836
+ return 'module myapp\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)';
837
+ }
838
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
839
+ }));
840
+ // gin framework detects via dependency
841
+ const goSignatures = {
842
+ ...STACK_SIGNATURES,
843
+ frameworks: {
844
+ ...STACK_SIGNATURES.frameworks,
845
+ gin: { indicators: [], dependencies: ['github.com/gin-gonic/gin'] },
846
+ },
847
+ };
848
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', goSignatures, ARCH_PATTERNS);
849
+ expect(result.framework).toBe('gin');
850
+ });
851
+ it('should collect dependencies from Gemfile', async () => {
852
+ mockedReadFile.mockImplementation(((path) => {
853
+ const p = path;
854
+ if (p.endsWith('Gemfile')) {
855
+ return "source 'https://rubygems.org'\ngem 'rails', '~> 7.0'\ngem 'pg'\n";
856
+ }
857
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
858
+ }));
859
+ const railsSigs = {
860
+ ...STACK_SIGNATURES,
861
+ frameworks: {
862
+ ...STACK_SIGNATURES.frameworks,
863
+ rails: { indicators: [], dependencies: ['rails'] },
864
+ },
865
+ };
866
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', railsSigs, ARCH_PATTERNS);
867
+ expect(result.framework).toBe('rails');
868
+ });
869
+ it('should collect dependencies from Cargo.toml', async () => {
870
+ mockedReadFile.mockImplementation(((path) => {
871
+ const p = path;
872
+ if (p.endsWith('Cargo.toml')) {
873
+ return '[package]\nname = "myapp"\n\n[dependencies]\nactix-web = "4"\nserde = { version = "1" }';
874
+ }
875
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
876
+ }));
877
+ const rustSigs = {
878
+ ...STACK_SIGNATURES,
879
+ frameworks: {
880
+ ...STACK_SIGNATURES.frameworks,
881
+ actix: { indicators: [], dependencies: ['actix-web'] },
882
+ },
883
+ };
884
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', rustSigs, ARCH_PATTERNS);
885
+ expect(result.framework).toBe('actix');
886
+ });
887
+ it('should collect dependencies from composer.json', async () => {
888
+ mockedReadFile.mockImplementation(((path) => {
889
+ const p = path;
890
+ if (p.endsWith('composer.json')) {
891
+ return JSON.stringify({
892
+ require: { 'laravel/framework': '^10.0' },
893
+ 'require-dev': { 'phpunit/phpunit': '^10.0' },
894
+ });
895
+ }
896
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
897
+ }));
898
+ const phpSigs = {
899
+ ...STACK_SIGNATURES,
900
+ frameworks: {
901
+ ...STACK_SIGNATURES.frameworks,
902
+ laravel: { indicators: [], dependencies: ['laravel/framework'] },
903
+ },
904
+ };
905
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', phpSigs, ARCH_PATTERNS);
906
+ expect(result.framework).toBe('laravel');
907
+ });
908
+ it('should infer env variable services correctly', async () => {
909
+ mockedReadFile.mockImplementation(((path) => {
910
+ const p = path;
911
+ if (p.endsWith('.env.example')) {
912
+ return [
913
+ 'SUPABASE_URL=',
914
+ 'STRIPE_KEY=',
915
+ 'FIREBASE_API_KEY=',
916
+ 'AWS_ACCESS_KEY=',
917
+ 'DATABASE_URL=',
918
+ 'REDIS_URL=',
919
+ 'SENTRY_DSN=',
920
+ 'GITHUB_TOKEN=',
921
+ 'OPENAI_API_KEY=',
922
+ 'ANTHROPIC_KEY=',
923
+ 'APP_PORT=',
924
+ 'DB_HOST=',
925
+ ].join('\n');
926
+ }
927
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
928
+ }));
929
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
930
+ const vars = result.envSetup.variables;
931
+ expect(vars.find((v) => v.name === 'SUPABASE_URL')?.service).toBe('supabase');
932
+ expect(vars.find((v) => v.name === 'STRIPE_KEY')?.service).toBe('stripe');
933
+ expect(vars.find((v) => v.name === 'FIREBASE_API_KEY')?.service).toBe('firebase');
934
+ expect(vars.find((v) => v.name === 'AWS_ACCESS_KEY')?.service).toBe('aws');
935
+ expect(vars.find((v) => v.name === 'DATABASE_URL')?.service).toBe('database');
936
+ expect(vars.find((v) => v.name === 'REDIS_URL')?.service).toBe('redis');
937
+ expect(vars.find((v) => v.name === 'SENTRY_DSN')?.service).toBe('sentry');
938
+ expect(vars.find((v) => v.name === 'GITHUB_TOKEN')?.service).toBe('github');
939
+ expect(vars.find((v) => v.name === 'OPENAI_API_KEY')?.service).toBe('ai');
940
+ expect(vars.find((v) => v.name === 'ANTHROPIC_KEY')?.service).toBe('ai');
941
+ expect(vars.find((v) => v.name === 'APP_PORT')?.service).toBe('app');
942
+ expect(vars.find((v) => v.name === 'DB_HOST')?.service).toBe('database');
943
+ });
944
+ it('should parse .env.local.example as fallback', async () => {
945
+ mockedReadFile.mockImplementation(((path) => {
946
+ const p = path;
947
+ if (p.endsWith('.env.example')) {
948
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
949
+ }
950
+ if (p.endsWith('.env.local.example')) {
951
+ return 'SECRET_KEY=abc';
952
+ }
953
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
954
+ }));
955
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
956
+ expect(result.envSetup.variables.length).toBeGreaterThan(0);
957
+ });
958
+ it('should fallback detect language by file extensions when no signature files match', async () => {
959
+ mockedGlob.mockImplementation(((pattern, opts) => {
960
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
961
+ if (p === '**/*' && opts && typeof opts === 'object') {
962
+ return ['main.py', 'utils.py', 'test.py'];
963
+ }
964
+ return [];
965
+ }));
966
+ const minimalSigs = {
967
+ languages: {
968
+ python: { files: [], extensions: ['.py'] },
969
+ },
970
+ frameworks: {},
971
+ packageManagers: {},
972
+ databases: {},
973
+ };
974
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', minimalSigs, ARCH_PATTERNS);
975
+ expect(result.language).toBe('python');
976
+ });
977
+ it('should return "unknown" language when nothing matches', async () => {
978
+ const emptySigs = {
979
+ languages: {},
980
+ frameworks: {},
981
+ packageManagers: {},
982
+ databases: {},
983
+ };
984
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', emptySigs, ARCH_PATTERNS);
985
+ expect(result.language).toBe('unknown');
986
+ });
987
+ it('should return null framework when none detected', async () => {
988
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
989
+ expect(result.framework).toBeNull();
990
+ });
991
+ it('should return null package manager when none detected', async () => {
992
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
993
+ expect(result.packageManager).toBeNull();
994
+ });
995
+ it('should score architecture patterns from file indicators', async () => {
996
+ mockedGlob.mockImplementation(((pattern) => {
997
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
998
+ if (p === 'docker-compose.yml') {
999
+ return ['docker-compose.yml'];
1000
+ }
1001
+ return [];
1002
+ }));
1003
+ mockedReadFile.mockImplementation(((path) => {
1004
+ const p = path;
1005
+ if (p.endsWith('package.json')) {
1006
+ return JSON.stringify({ dependencies: { '@nestjs/microservices': '10.0.0' } });
1007
+ }
1008
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
1009
+ }));
1010
+ const result = await analyzeProject('/tmp/project', 'proj-1', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
1011
+ // microservices pattern should score higher
1012
+ expect(result.architecture.primary).toBeDefined();
1013
+ });
1014
+ });
1015
+ // =============================================================================
1016
+ // quickAnalyze
1017
+ // =============================================================================
1018
+ describe('quickAnalyze', () => {
1019
+ it('should return language, framework, packageManager, database', async () => {
1020
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
1021
+ mockedGlob.mockImplementation(((pattern) => {
1022
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1023
+ if (p === 'tsconfig.json') {
1024
+ return ['tsconfig.json'];
1025
+ }
1026
+ return [];
1027
+ }));
1028
+ const result = await quickAnalyze('/tmp/project');
1029
+ expect(result).toHaveProperty('language');
1030
+ expect(result).toHaveProperty('framework');
1031
+ expect(result).toHaveProperty('packageManager');
1032
+ expect(result).toHaveProperty('database');
1033
+ });
1034
+ it('should return unknown values for empty project', async () => {
1035
+ const result = await quickAnalyze('/tmp/empty-project');
1036
+ expect(result.language).toBe('unknown');
1037
+ expect(result.framework).toBeNull();
1038
+ expect(result.packageManager).toBeNull();
1039
+ expect(result.database).toBe('unknown');
1040
+ });
1041
+ });
1042
+ // =============================================================================
1043
+ // checkProjectCompleteness
1044
+ // =============================================================================
1045
+ describe('checkProjectCompleteness', () => {
1046
+ it('should return completeness score of 0 for empty project', async () => {
1047
+ const result = await checkProjectCompleteness('/tmp/empty-project');
1048
+ expect(result.completenessScore).toBe(0);
1049
+ expect(result.hasReadme).toBe(false);
1050
+ expect(result.hasGitignore).toBe(false);
1051
+ expect(result.hasLicense).toBe(false);
1052
+ });
1053
+ it('should return higher score when files exist', async () => {
1054
+ setupFileExists(['README.md', '.gitignore', 'LICENSE']);
1055
+ mockedGlob.mockImplementation(((pattern) => {
1056
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1057
+ if (p === '.eslintrc*' || p === 'eslint.config.*') {
1058
+ return ['.eslintrc.js'];
1059
+ }
1060
+ if (p === '.prettierrc*' || p === 'biome.json' || p === '.editorconfig') {
1061
+ return ['.prettierrc'];
1062
+ }
1063
+ if (p === 'vitest.config.*' || p === 'jest.config.*') {
1064
+ return ['vitest.config.ts'];
1065
+ }
1066
+ return [];
1067
+ }));
1068
+ const result = await checkProjectCompleteness('/tmp/project');
1069
+ expect(result.hasReadme).toBe(true);
1070
+ expect(result.hasGitignore).toBe(true);
1071
+ expect(result.hasLicense).toBe(true);
1072
+ expect(result.hasLinting).toBe(true);
1073
+ expect(result.hasFormatting).toBe(true);
1074
+ expect(result.hasTesting).toBe(true);
1075
+ expect(result.completenessScore).toBeGreaterThan(0);
1076
+ });
1077
+ it('should detect .env.example', async () => {
1078
+ setupFileExists(['.env.example']);
1079
+ const result = await checkProjectCompleteness('/tmp/project');
1080
+ expect(result.hasEnvExample).toBe(true);
1081
+ });
1082
+ it('should detect .env.local.example as fallback for env example', async () => {
1083
+ setupFileExists(['.env.local.example']);
1084
+ const result = await checkProjectCompleteness('/tmp/project');
1085
+ expect(result.hasEnvExample).toBe(true);
1086
+ });
1087
+ it('should detect CI/CD from github workflows', async () => {
1088
+ mockedGlob.mockImplementation(((pattern) => {
1089
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1090
+ if (p === '.github/workflows/*') {
1091
+ return ['.github/workflows/ci.yml'];
1092
+ }
1093
+ return [];
1094
+ }));
1095
+ const result = await checkProjectCompleteness('/tmp/project');
1096
+ expect(result.hasCiCd).toBe(true);
1097
+ });
1098
+ it('should detect type checking from tsconfig.json', async () => {
1099
+ mockedGlob.mockImplementation(((pattern) => {
1100
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1101
+ if (p === 'tsconfig.json') {
1102
+ return ['tsconfig.json'];
1103
+ }
1104
+ return [];
1105
+ }));
1106
+ const result = await checkProjectCompleteness('/tmp/project');
1107
+ expect(result.hasTypeChecking).toBe(true);
1108
+ });
1109
+ it('should detect editorconfig', async () => {
1110
+ setupFileExists(['.editorconfig']);
1111
+ const result = await checkProjectCompleteness('/tmp/project');
1112
+ expect(result.hasEditorConfig).toBe(true);
1113
+ });
1114
+ it('should detect contributing guide', async () => {
1115
+ setupFileExists(['CONTRIBUTING.md']);
1116
+ const result = await checkProjectCompleteness('/tmp/project');
1117
+ expect(result.hasContributing).toBe(true);
1118
+ });
1119
+ it('should return all 20 completeness fields', async () => {
1120
+ const result = await checkProjectCompleteness('/tmp/project');
1121
+ expect(result).toHaveProperty('hasReadme');
1122
+ expect(result).toHaveProperty('hasGitignore');
1123
+ expect(result).toHaveProperty('hasEnvExample');
1124
+ expect(result).toHaveProperty('hasLicense');
1125
+ expect(result).toHaveProperty('hasEditorConfig');
1126
+ expect(result).toHaveProperty('hasLinting');
1127
+ expect(result).toHaveProperty('hasFormatting');
1128
+ expect(result).toHaveProperty('hasTesting');
1129
+ expect(result).toHaveProperty('hasCiCd');
1130
+ expect(result).toHaveProperty('hasErrorTracking');
1131
+ expect(result).toHaveProperty('hasTypeChecking');
1132
+ expect(result).toHaveProperty('hasSecurityScanning');
1133
+ expect(result).toHaveProperty('hasDocumentation');
1134
+ expect(result).toHaveProperty('hasContributing');
1135
+ expect(result).toHaveProperty('hasChangelogStrategy');
1136
+ expect(result).toHaveProperty('hasBackupStrategy');
1137
+ expect(result).toHaveProperty('hasAccessibility');
1138
+ expect(result).toHaveProperty('hasSeo');
1139
+ expect(result).toHaveProperty('hasPerformanceBudget');
1140
+ expect(result).toHaveProperty('hasSeedData');
1141
+ expect(result).toHaveProperty('completenessScore');
1142
+ });
1143
+ it('should calculate 100% completeness score when all checks pass', async () => {
1144
+ // Make everything exist
1145
+ setupFileExists([
1146
+ 'README.md',
1147
+ '.gitignore',
1148
+ '.env.example',
1149
+ 'LICENSE',
1150
+ '.editorconfig',
1151
+ 'CONTRIBUTING.md',
1152
+ ]);
1153
+ mockedGlob.mockResolvedValue(['match']);
1154
+ const result = await checkProjectCompleteness('/tmp/project');
1155
+ expect(result.completenessScore).toBe(100);
1156
+ });
1157
+ });
1158
+ // =============================================================================
1159
+ // hasAnyFile — positive path (glob finds matches → returns true)
1160
+ // =============================================================================
1161
+ describe('hasAnyFile positive path', () => {
1162
+ it('should return true for completeness checks when glob matches files', async () => {
1163
+ // Mock glob to return matches for specific patterns used by checkProjectCompleteness
1164
+ mockedGlob.mockImplementation(((pattern) => {
1165
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1166
+ if (p === '.eslintrc*' || p === 'eslint.config.*') {
1167
+ return ['.eslintrc.js'];
1168
+ }
1169
+ if (p === '.prettierrc*') {
1170
+ return ['.prettierrc'];
1171
+ }
1172
+ if (p === 'vitest.config.*' || p === 'jest.config.*') {
1173
+ return ['vitest.config.ts'];
1174
+ }
1175
+ if (p === '.github/workflows/*') {
1176
+ return ['.github/workflows/ci.yml'];
1177
+ }
1178
+ if (p === 'tsconfig.json') {
1179
+ return ['tsconfig.json'];
1180
+ }
1181
+ if (p === 'docs/**/*.md') {
1182
+ return ['docs/guide.md'];
1183
+ }
1184
+ if (p === 'CHANGELOG.md') {
1185
+ return ['CHANGELOG.md'];
1186
+ }
1187
+ if (p === 'robots.txt') {
1188
+ return ['robots.txt'];
1189
+ }
1190
+ if (p === 'lighthouse*') {
1191
+ return ['lighthouse.config.js'];
1192
+ }
1193
+ if (p === 'seed.*' || p === 'seeds/*') {
1194
+ return ['seed.ts'];
1195
+ }
1196
+ return [];
1197
+ }));
1198
+ setupFileExists([
1199
+ 'README.md',
1200
+ '.gitignore',
1201
+ '.env.example',
1202
+ 'LICENSE',
1203
+ '.editorconfig',
1204
+ 'CONTRIBUTING.md',
1205
+ ]);
1206
+ const result = await checkProjectCompleteness('/tmp/project');
1207
+ expect(result.hasLinting).toBe(true);
1208
+ expect(result.hasFormatting).toBe(true);
1209
+ expect(result.hasTesting).toBe(true);
1210
+ expect(result.hasCiCd).toBe(true);
1211
+ expect(result.hasTypeChecking).toBe(true);
1212
+ expect(result.hasDocumentation).toBe(true);
1213
+ expect(result.completenessScore).toBeGreaterThan(50);
1214
+ });
1215
+ it('should return false for completeness checks when glob returns empty arrays', async () => {
1216
+ mockedGlob.mockResolvedValue([]);
1217
+ const result = await checkProjectCompleteness('/tmp/empty-project');
1218
+ expect(result.hasLinting).toBe(false);
1219
+ expect(result.hasFormatting).toBe(false);
1220
+ expect(result.hasTesting).toBe(false);
1221
+ expect(result.hasCiCd).toBe(false);
1222
+ });
1223
+ it('should handle glob errors in hasAnyFile gracefully (catch block)', async () => {
1224
+ mockedGlob.mockRejectedValue(new Error('glob failed'));
1225
+ const result = await checkProjectCompleteness('/tmp/broken-project');
1226
+ expect(result.hasLinting).toBe(false);
1227
+ expect(result.hasFormatting).toBe(false);
1228
+ expect(result.completenessScore).toBe(0);
1229
+ });
1230
+ });
1231
+ // =============================================================================
1232
+ // loadStackSignatures / loadArchPatterns — catch blocks (fallback paths)
1233
+ // =============================================================================
1234
+ describe('loadStackSignatures and loadArchPatterns fallback', () => {
1235
+ it('quickAnalyze falls back to empty signatures when config file fails to load', async () => {
1236
+ // The default beforeEach mocks readFile to reject with ENOENT for ALL paths,
1237
+ // so loadStackSignatures() will hit its catch block and return the fallback.
1238
+ // With empty signatures, detection returns "unknown" / null.
1239
+ const result = await quickAnalyze('/tmp/no-config-project');
1240
+ expect(result.language).toBe('unknown');
1241
+ expect(result.framework).toBeNull();
1242
+ expect(result.packageManager).toBeNull();
1243
+ expect(result.database).toBe('unknown');
1244
+ });
1245
+ it('analyzeProject falls back when called without explicit signatures', async () => {
1246
+ // Call analyzeProject WITHOUT passing stackSignatures and archPatterns
1247
+ // so it invokes loadStackSignatures() and loadArchPatterns() internally.
1248
+ // Both will fail (readFile rejects) and use fallback values.
1249
+ const result = await analyzeProject('/tmp/fallback-project', 'proj-fallback', 'en');
1250
+ expect(result.projectId).toBe('proj-fallback');
1251
+ expect(result.language).toBe('unknown');
1252
+ expect(result.framework).toBeNull();
1253
+ expect(result.architecture).toBeDefined();
1254
+ expect(result.architecture.primary).toBeDefined();
1255
+ });
1256
+ it('loadStackSignatures succeeds when readFile returns valid JSON', async () => {
1257
+ // Make readFile succeed for the config path with valid JSON
1258
+ mockedReadFile.mockImplementation(((path) => {
1259
+ const p = String(path);
1260
+ if (p.includes('stack-signatures.json')) {
1261
+ return Promise.resolve(JSON.stringify({
1262
+ languages: { ruby: { files: ['Gemfile'], extensions: ['.rb'] } },
1263
+ frameworks: {},
1264
+ packageManagers: {},
1265
+ databases: {},
1266
+ }));
1267
+ }
1268
+ return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
1269
+ }));
1270
+ // hasAnyFile uses glob to check for files; return match for Gemfile
1271
+ mockedGlob.mockImplementation(((pattern) => {
1272
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1273
+ if (p === 'Gemfile') {
1274
+ return ['Gemfile'];
1275
+ }
1276
+ return [];
1277
+ }));
1278
+ const result = await quickAnalyze('/tmp/ruby-project');
1279
+ expect(result.language).toBe('ruby');
1280
+ });
1281
+ it('loadArchPatterns succeeds when readFile returns valid JSON', async () => {
1282
+ mockedReadFile.mockImplementation(((path) => {
1283
+ const p = String(path);
1284
+ if (p.includes('stack-signatures.json')) {
1285
+ return Promise.resolve(JSON.stringify({
1286
+ languages: {},
1287
+ frameworks: {},
1288
+ packageManagers: {},
1289
+ databases: {},
1290
+ }));
1291
+ }
1292
+ if (p.includes('architecture-patterns.json')) {
1293
+ return Promise.resolve(JSON.stringify({
1294
+ patterns: {
1295
+ 'custom-arch': {
1296
+ name: 'Custom Arch',
1297
+ indicators: { directories: ['src/custom/'] },
1298
+ },
1299
+ },
1300
+ }));
1301
+ }
1302
+ return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
1303
+ }));
1304
+ mockedGlob.mockResolvedValue([]);
1305
+ // Call analyzeProject without explicit archPatterns to trigger loadArchPatterns
1306
+ const result = await analyzeProject('/tmp/arch-project', 'proj-arch', 'en');
1307
+ expect(result.architecture).toBeDefined();
1308
+ });
1309
+ it('loadStackSignatures returns fallback when JSON is malformed', async () => {
1310
+ mockedReadFile.mockImplementation(((path) => {
1311
+ const p = String(path);
1312
+ if (p.includes('stack-signatures.json')) {
1313
+ return Promise.resolve('not-valid-json{{{');
1314
+ }
1315
+ if (p.includes('architecture-patterns.json')) {
1316
+ return Promise.resolve('also-not-valid{');
1317
+ }
1318
+ return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
1319
+ }));
1320
+ const result = await quickAnalyze('/tmp/malformed-config');
1321
+ // With fallback (empty) signatures, nothing is detected
1322
+ expect(result.language).toBe('unknown');
1323
+ expect(result.framework).toBeNull();
1324
+ });
1325
+ it('loadArchPatterns returns fallback when JSON is malformed', async () => {
1326
+ mockedReadFile.mockImplementation(((path) => {
1327
+ const p = String(path);
1328
+ if (p.includes('stack-signatures.json')) {
1329
+ return Promise.resolve(JSON.stringify({
1330
+ languages: {},
1331
+ frameworks: {},
1332
+ packageManagers: {},
1333
+ databases: {},
1334
+ }));
1335
+ }
1336
+ if (p.includes('architecture-patterns.json')) {
1337
+ return Promise.resolve('{invalid json!!}');
1338
+ }
1339
+ return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
1340
+ }));
1341
+ // Call without explicit archPatterns to trigger loadArchPatterns
1342
+ const result = await analyzeProject('/tmp/bad-arch-config', 'proj-bad-arch', 'en');
1343
+ // Architecture should still work with fallback patterns
1344
+ expect(result.architecture).toBeDefined();
1345
+ expect(result.architecture.primary).toBeDefined();
1346
+ });
1347
+ });
1348
+ // =============================================================================
1349
+ // detectProjectType — mobile framework branch
1350
+ // =============================================================================
1351
+ describe('detectProjectType — mobile framework', () => {
1352
+ it('should return "mobile" when framework is react-native', async () => {
1353
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
1354
+ mockedReadFile.mockImplementation(((path) => {
1355
+ const p = path;
1356
+ if (p.endsWith('package.json')) {
1357
+ return JSON.stringify({ dependencies: { 'react-native': '0.72.0' } });
1358
+ }
1359
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
1360
+ }));
1361
+ const sigs = {
1362
+ ...STACK_SIGNATURES,
1363
+ frameworks: {
1364
+ ...STACK_SIGNATURES.frameworks,
1365
+ 'react-native': { indicators: [], dependencies: ['react-native'] },
1366
+ },
1367
+ };
1368
+ const result = await analyzeProject('/tmp/mobile-project', 'proj-mobile', 'en', 'intermediate', sigs, ARCH_PATTERNS);
1369
+ expect(result.framework).toBe('react-native');
1370
+ });
1371
+ it('should return "mobile" when framework is expo', async () => {
1372
+ setupFileExists(['tsconfig.json', 'package-lock.json']);
1373
+ mockedReadFile.mockImplementation(((path) => {
1374
+ const p = path;
1375
+ if (p.endsWith('package.json')) {
1376
+ return JSON.stringify({ dependencies: { expo: '49.0.0' } });
1377
+ }
1378
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
1379
+ }));
1380
+ const result = await analyzeProject('/tmp/expo-project', 'proj-expo', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
1381
+ expect(result.framework).toBe('expo');
1382
+ });
1383
+ it('should return "mobile" when framework is flutter', async () => {
1384
+ const flutterSigs = {
1385
+ ...STACK_SIGNATURES,
1386
+ languages: {
1387
+ ...STACK_SIGNATURES.languages,
1388
+ dart: { files: ['pubspec.yaml'], extensions: ['.dart'] },
1389
+ },
1390
+ frameworks: {
1391
+ ...STACK_SIGNATURES.frameworks,
1392
+ flutter: { indicators: ['pubspec.yaml'], dependencies: ['flutter'] },
1393
+ },
1394
+ };
1395
+ setupFileExists(['pubspec.yaml']);
1396
+ mockedGlob.mockImplementation(((pattern) => {
1397
+ const p = Array.isArray(pattern) ? pattern[0] : pattern;
1398
+ if (p === 'pubspec.yaml') {
1399
+ return ['pubspec.yaml'];
1400
+ }
1401
+ return [];
1402
+ }));
1403
+ const result = await analyzeProject('/tmp/flutter-project', 'proj-flutter', 'en', 'intermediate', flutterSigs, ARCH_PATTERNS);
1404
+ expect(result.framework).toBe('flutter');
1405
+ });
1406
+ });
1407
+ // =============================================================================
1408
+ // parseEnvFile — line with no = sign
1409
+ // =============================================================================
1410
+ describe('parseEnvFile — skip lines without equals sign', () => {
1411
+ it('should skip .env lines that have no = sign', async () => {
1412
+ mockedReadFile.mockImplementation(((path) => {
1413
+ const p = path;
1414
+ if (p.endsWith('.env.example')) {
1415
+ return 'VALID_KEY=some_value\nINVALID_LINE_NO_EQUALS\nANOTHER_KEY=value2\n';
1416
+ }
1417
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
1418
+ }));
1419
+ const result = await analyzeProject('/tmp/env-test', 'proj-env', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
1420
+ // Only 2 vars should be parsed (the one without = is skipped)
1421
+ expect(result.envSetup.variables).toHaveLength(2);
1422
+ expect(result.envSetup.variables.map((v) => v.name)).toContain('VALID_KEY');
1423
+ expect(result.envSetup.variables.map((v) => v.name)).toContain('ANOTHER_KEY');
1424
+ });
1425
+ });
1426
+ // =============================================================================
1427
+ // detectEnvironments — .env.test file exists
1428
+ // =============================================================================
1429
+ describe('detectEnvironments — test environment', () => {
1430
+ it('should detect test environment when .env.test exists', async () => {
1431
+ setupFileExists(['.env.staging', '.env.test', '.env.production']);
1432
+ mockedGlob.mockImplementation(((pattern) => {
1433
+ const p = Array.isArray(pattern) ? pattern[0] ?? '' : pattern;
1434
+ if (p === '.env.staging' || p === '.env.test' || p === '.env.production') {
1435
+ return Promise.resolve([p]);
1436
+ }
1437
+ return Promise.resolve([]);
1438
+ }));
1439
+ const result = await analyzeProject('/tmp/env-envs', 'proj-envs', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
1440
+ expect(result.environments).toContain('development');
1441
+ expect(result.environments).toContain('test');
1442
+ expect(result.environments).toContain('staging');
1443
+ expect(result.environments).toContain('production');
1444
+ });
1445
+ it('should detect only test environment when only .env.test exists', async () => {
1446
+ setupFileExists(['.env.test']);
1447
+ mockedGlob.mockImplementation(((pattern) => {
1448
+ const p = Array.isArray(pattern) ? pattern[0] ?? '' : pattern;
1449
+ if (p === '.env.test') {
1450
+ return Promise.resolve([p]);
1451
+ }
1452
+ return Promise.resolve([]);
1453
+ }));
1454
+ const result = await analyzeProject('/tmp/env-test-only', 'proj-envs2', 'en', 'intermediate', STACK_SIGNATURES, ARCH_PATTERNS);
1455
+ expect(result.environments).toContain('development');
1456
+ expect(result.environments).toContain('test');
1457
+ expect(result.environments).not.toContain('staging');
1458
+ expect(result.environments).not.toContain('production');
1459
+ });
1460
+ });
1461
+ //# sourceMappingURL=analyzer.test.js.map