@yasserkhanorg/impact-gate 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (587) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +520 -0
  3. package/dist/adapters/cypress.d.ts +10 -0
  4. package/dist/adapters/cypress.d.ts.map +1 -0
  5. package/dist/adapters/cypress.js +86 -0
  6. package/dist/adapters/framework_adapter.d.ts +41 -0
  7. package/dist/adapters/framework_adapter.d.ts.map +1 -0
  8. package/dist/adapters/framework_adapter.js +152 -0
  9. package/dist/adapters/playwright.d.ts +10 -0
  10. package/dist/adapters/playwright.d.ts.map +1 -0
  11. package/dist/adapters/playwright.js +86 -0
  12. package/dist/adapters/pytest.d.ts +10 -0
  13. package/dist/adapters/pytest.d.ts.map +1 -0
  14. package/dist/adapters/pytest.js +96 -0
  15. package/dist/adapters/supertest.d.ts +12 -0
  16. package/dist/adapters/supertest.d.ts.map +1 -0
  17. package/dist/adapters/supertest.js +85 -0
  18. package/dist/agent/api_catalog.d.ts +11 -0
  19. package/dist/agent/api_catalog.d.ts.map +1 -0
  20. package/dist/agent/api_catalog.js +210 -0
  21. package/dist/agent/config.d.ts +193 -0
  22. package/dist/agent/config.d.ts.map +1 -0
  23. package/dist/agent/config.js +875 -0
  24. package/dist/agent/feedback.d.ts +91 -0
  25. package/dist/agent/feedback.d.ts.map +1 -0
  26. package/dist/agent/feedback.js +323 -0
  27. package/dist/agent/git.d.ts +19 -0
  28. package/dist/agent/git.d.ts.map +1 -0
  29. package/dist/agent/git.js +257 -0
  30. package/dist/agent/handoff.d.ts +22 -0
  31. package/dist/agent/handoff.d.ts.map +1 -0
  32. package/dist/agent/handoff.js +180 -0
  33. package/dist/agent/llm_agents_flow.d.ts +15 -0
  34. package/dist/agent/llm_agents_flow.d.ts.map +1 -0
  35. package/dist/agent/llm_agents_flow.js +434 -0
  36. package/dist/agent/native_flow.d.ts +6 -0
  37. package/dist/agent/native_flow.d.ts.map +1 -0
  38. package/dist/agent/native_flow.js +179 -0
  39. package/dist/agent/pipeline.d.ts +7 -0
  40. package/dist/agent/pipeline.d.ts.map +1 -0
  41. package/dist/agent/pipeline.js +260 -0
  42. package/dist/agent/pipeline_types.d.ts +54 -0
  43. package/dist/agent/pipeline_types.d.ts.map +1 -0
  44. package/dist/agent/pipeline_types.js +4 -0
  45. package/dist/agent/pipeline_utils.d.ts +12 -0
  46. package/dist/agent/pipeline_utils.d.ts.map +1 -0
  47. package/dist/agent/pipeline_utils.js +156 -0
  48. package/dist/agent/plan.d.ts +170 -0
  49. package/dist/agent/plan.d.ts.map +1 -0
  50. package/dist/agent/plan.js +86 -0
  51. package/dist/agent/playwright_report.d.ts +8 -0
  52. package/dist/agent/playwright_report.d.ts.map +1 -0
  53. package/dist/agent/playwright_report.js +126 -0
  54. package/dist/agent/process_runner.d.ts +10 -0
  55. package/dist/agent/process_runner.d.ts.map +1 -0
  56. package/dist/agent/process_runner.js +92 -0
  57. package/dist/agent/spec_generator.d.ts +5 -0
  58. package/dist/agent/spec_generator.d.ts.map +1 -0
  59. package/dist/agent/spec_generator.js +253 -0
  60. package/dist/agent/test_path.d.ts +2 -0
  61. package/dist/agent/test_path.d.ts.map +1 -0
  62. package/dist/agent/test_path.js +23 -0
  63. package/dist/agent/traceability_capture.d.ts +18 -0
  64. package/dist/agent/traceability_capture.d.ts.map +1 -0
  65. package/dist/agent/traceability_capture.js +313 -0
  66. package/dist/agent/traceability_ingest.d.ts +21 -0
  67. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  68. package/dist/agent/traceability_ingest.js +237 -0
  69. package/dist/agent/types.d.ts +42 -0
  70. package/dist/agent/types.d.ts.map +1 -0
  71. package/dist/agent/types.js +4 -0
  72. package/dist/agent/utils.d.ts +13 -0
  73. package/dist/agent/utils.d.ts.map +1 -0
  74. package/dist/agent/utils.js +152 -0
  75. package/dist/agent/validation_runner.d.ts +5 -0
  76. package/dist/agent/validation_runner.d.ts.map +1 -0
  77. package/dist/agent/validation_runner.js +77 -0
  78. package/dist/agentic/fix_loop.d.ts +26 -0
  79. package/dist/agentic/fix_loop.d.ts.map +1 -0
  80. package/dist/agentic/fix_loop.js +96 -0
  81. package/dist/agentic/playwright_runner.d.ts +43 -0
  82. package/dist/agentic/playwright_runner.d.ts.map +1 -0
  83. package/dist/agentic/playwright_runner.js +165 -0
  84. package/dist/agentic/runner.d.ts +27 -0
  85. package/dist/agentic/runner.d.ts.map +1 -0
  86. package/dist/agentic/runner.js +210 -0
  87. package/dist/agentic/types.d.ts +62 -0
  88. package/dist/agentic/types.d.ts.map +1 -0
  89. package/dist/agentic/types.js +4 -0
  90. package/dist/agents/coverage-evaluator.d.ts +8 -0
  91. package/dist/agents/coverage-evaluator.d.ts.map +1 -0
  92. package/dist/agents/coverage-evaluator.js +41 -0
  93. package/dist/agents/cross-impact.d.ts +13 -0
  94. package/dist/agents/cross-impact.d.ts.map +1 -0
  95. package/dist/agents/cross-impact.js +140 -0
  96. package/dist/agents/executor.d.ts +8 -0
  97. package/dist/agents/executor.d.ts.map +1 -0
  98. package/dist/agents/executor.js +75 -0
  99. package/dist/agents/explorer.d.ts +12 -0
  100. package/dist/agents/explorer.d.ts.map +1 -0
  101. package/dist/agents/explorer.js +43 -0
  102. package/dist/agents/generator.d.ts +8 -0
  103. package/dist/agents/generator.d.ts.map +1 -0
  104. package/dist/agents/generator.js +77 -0
  105. package/dist/agents/healer.d.ts +8 -0
  106. package/dist/agents/healer.d.ts.map +1 -0
  107. package/dist/agents/healer.js +31 -0
  108. package/dist/agents/impact-analyst.d.ts +8 -0
  109. package/dist/agents/impact-analyst.d.ts.map +1 -0
  110. package/dist/agents/impact-analyst.js +38 -0
  111. package/dist/agents/regression-advisor.d.ts +8 -0
  112. package/dist/agents/regression-advisor.d.ts.map +1 -0
  113. package/dist/agents/regression-advisor.js +116 -0
  114. package/dist/agents/strategist.d.ts +9 -0
  115. package/dist/agents/strategist.d.ts.map +1 -0
  116. package/dist/agents/strategist.js +92 -0
  117. package/dist/agents/test-designer.d.ts +8 -0
  118. package/dist/agents/test-designer.d.ts.map +1 -0
  119. package/dist/agents/test-designer.js +111 -0
  120. package/dist/anthropic_provider.d.ts +65 -0
  121. package/dist/anthropic_provider.d.ts.map +1 -0
  122. package/dist/anthropic_provider.js +334 -0
  123. package/dist/api.d.ts +48 -0
  124. package/dist/api.d.ts.map +1 -0
  125. package/dist/api.js +151 -0
  126. package/dist/base_provider.d.ts +109 -0
  127. package/dist/base_provider.d.ts.map +1 -0
  128. package/dist/base_provider.js +203 -0
  129. package/dist/budget_ledger.d.ts +28 -0
  130. package/dist/budget_ledger.d.ts.map +1 -0
  131. package/dist/budget_ledger.js +62 -0
  132. package/dist/cache/cached_provider.d.ts +49 -0
  133. package/dist/cache/cached_provider.d.ts.map +1 -0
  134. package/dist/cache/cached_provider.js +91 -0
  135. package/dist/cache/response_cache.d.ts +79 -0
  136. package/dist/cache/response_cache.d.ts.map +1 -0
  137. package/dist/cache/response_cache.js +177 -0
  138. package/dist/cli/commands/analyze.d.ts +3 -0
  139. package/dist/cli/commands/analyze.d.ts.map +1 -0
  140. package/dist/cli/commands/analyze.js +77 -0
  141. package/dist/cli/commands/bootstrap.d.ts +3 -0
  142. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  143. package/dist/cli/commands/bootstrap.js +109 -0
  144. package/dist/cli/commands/cost_report.d.ts +3 -0
  145. package/dist/cli/commands/cost_report.d.ts.map +1 -0
  146. package/dist/cli/commands/cost_report.js +115 -0
  147. package/dist/cli/commands/crew.d.ts +3 -0
  148. package/dist/cli/commands/crew.d.ts.map +1 -0
  149. package/dist/cli/commands/crew.js +255 -0
  150. package/dist/cli/commands/feedback.d.ts +3 -0
  151. package/dist/cli/commands/feedback.d.ts.map +1 -0
  152. package/dist/cli/commands/feedback.js +39 -0
  153. package/dist/cli/commands/finalize.d.ts +3 -0
  154. package/dist/cli/commands/finalize.d.ts.map +1 -0
  155. package/dist/cli/commands/finalize.js +41 -0
  156. package/dist/cli/commands/gate.d.ts +3 -0
  157. package/dist/cli/commands/gate.d.ts.map +1 -0
  158. package/dist/cli/commands/gate.js +89 -0
  159. package/dist/cli/commands/generate.d.ts +4 -0
  160. package/dist/cli/commands/generate.d.ts.map +1 -0
  161. package/dist/cli/commands/generate.js +108 -0
  162. package/dist/cli/commands/heal.d.ts +3 -0
  163. package/dist/cli/commands/heal.d.ts.map +1 -0
  164. package/dist/cli/commands/heal.js +60 -0
  165. package/dist/cli/commands/impact.d.ts +4 -0
  166. package/dist/cli/commands/impact.d.ts.map +1 -0
  167. package/dist/cli/commands/impact.js +33 -0
  168. package/dist/cli/commands/init.d.ts +2 -0
  169. package/dist/cli/commands/init.d.ts.map +1 -0
  170. package/dist/cli/commands/init.js +169 -0
  171. package/dist/cli/commands/llm_health.d.ts +2 -0
  172. package/dist/cli/commands/llm_health.d.ts.map +1 -0
  173. package/dist/cli/commands/llm_health.js +22 -0
  174. package/dist/cli/commands/plan.d.ts +4 -0
  175. package/dist/cli/commands/plan.d.ts.map +1 -0
  176. package/dist/cli/commands/plan.js +120 -0
  177. package/dist/cli/commands/plan_crew.d.ts +17 -0
  178. package/dist/cli/commands/plan_crew.d.ts.map +1 -0
  179. package/dist/cli/commands/plan_crew.js +316 -0
  180. package/dist/cli/commands/traceability.d.ts +4 -0
  181. package/dist/cli/commands/traceability.d.ts.map +1 -0
  182. package/dist/cli/commands/traceability.js +77 -0
  183. package/dist/cli/commands/train.d.ts +3 -0
  184. package/dist/cli/commands/train.d.ts.map +1 -0
  185. package/dist/cli/commands/train.js +391 -0
  186. package/dist/cli/defaults.d.ts +35 -0
  187. package/dist/cli/defaults.d.ts.map +1 -0
  188. package/dist/cli/defaults.js +172 -0
  189. package/dist/cli/errors.d.ts +27 -0
  190. package/dist/cli/errors.d.ts.map +1 -0
  191. package/dist/cli/errors.js +57 -0
  192. package/dist/cli/parse_args.d.ts +6 -0
  193. package/dist/cli/parse_args.d.ts.map +1 -0
  194. package/dist/cli/parse_args.js +257 -0
  195. package/dist/cli/types.d.ts +87 -0
  196. package/dist/cli/types.d.ts.map +1 -0
  197. package/dist/cli/types.js +4 -0
  198. package/dist/cli/usage.d.ts +2 -0
  199. package/dist/cli/usage.d.ts.map +1 -0
  200. package/dist/cli/usage.js +109 -0
  201. package/dist/cli.d.ts +3 -0
  202. package/dist/cli.d.ts.map +1 -0
  203. package/dist/cli.js +194 -0
  204. package/dist/crew/context.d.ts +55 -0
  205. package/dist/crew/context.d.ts.map +1 -0
  206. package/dist/crew/context.js +36 -0
  207. package/dist/crew/orchestrator.d.ts +50 -0
  208. package/dist/crew/orchestrator.d.ts.map +1 -0
  209. package/dist/crew/orchestrator.js +329 -0
  210. package/dist/crew/protocol.d.ts +46 -0
  211. package/dist/crew/protocol.d.ts.map +1 -0
  212. package/dist/crew/protocol.js +4 -0
  213. package/dist/crew/provider.d.ts +17 -0
  214. package/dist/crew/provider.d.ts.map +1 -0
  215. package/dist/crew/provider.js +36 -0
  216. package/dist/crew/sanitize.d.ts +3 -0
  217. package/dist/crew/sanitize.d.ts.map +1 -0
  218. package/dist/crew/sanitize.js +31 -0
  219. package/dist/crew/types.d.ts +52 -0
  220. package/dist/crew/types.d.ts.map +1 -0
  221. package/dist/crew/types.js +4 -0
  222. package/dist/crew/workflows.d.ts +52 -0
  223. package/dist/crew/workflows.d.ts.map +1 -0
  224. package/dist/crew/workflows.js +36 -0
  225. package/dist/custom_provider.d.ts +20 -0
  226. package/dist/custom_provider.d.ts.map +1 -0
  227. package/dist/custom_provider.js +277 -0
  228. package/dist/engine/ai_enrichment.d.ts +44 -0
  229. package/dist/engine/ai_enrichment.d.ts.map +1 -0
  230. package/dist/engine/ai_enrichment.js +267 -0
  231. package/dist/engine/diff_loader.d.ts +11 -0
  232. package/dist/engine/diff_loader.d.ts.map +1 -0
  233. package/dist/engine/diff_loader.js +63 -0
  234. package/dist/engine/impact_engine.d.ts +72 -0
  235. package/dist/engine/impact_engine.d.ts.map +1 -0
  236. package/dist/engine/impact_engine.js +298 -0
  237. package/dist/engine/plan_builder.d.ts +11 -0
  238. package/dist/engine/plan_builder.d.ts.map +1 -0
  239. package/dist/engine/plan_builder.js +599 -0
  240. package/dist/esm/adapters/cypress.js +49 -0
  241. package/dist/esm/adapters/framework_adapter.js +114 -0
  242. package/dist/esm/adapters/playwright.js +49 -0
  243. package/dist/esm/adapters/pytest.js +59 -0
  244. package/dist/esm/adapters/supertest.js +48 -0
  245. package/dist/esm/agent/api_catalog.js +199 -0
  246. package/dist/esm/agent/config.js +872 -0
  247. package/dist/esm/agent/feedback.js +317 -0
  248. package/dist/esm/agent/git.js +252 -0
  249. package/dist/esm/agent/handoff.js +177 -0
  250. package/dist/esm/agent/llm_agents_flow.js +421 -0
  251. package/dist/esm/agent/native_flow.js +175 -0
  252. package/dist/esm/agent/pipeline.js +256 -0
  253. package/dist/esm/agent/pipeline_types.js +3 -0
  254. package/dist/esm/agent/pipeline_utils.js +146 -0
  255. package/dist/esm/agent/plan.js +83 -0
  256. package/dist/esm/agent/playwright_report.js +123 -0
  257. package/dist/esm/agent/process_runner.js +83 -0
  258. package/dist/esm/agent/spec_generator.js +249 -0
  259. package/dist/esm/agent/test_path.js +20 -0
  260. package/dist/esm/agent/traceability_capture.js +310 -0
  261. package/dist/esm/agent/traceability_ingest.js +234 -0
  262. package/dist/esm/agent/types.js +3 -0
  263. package/dist/esm/agent/utils.js +138 -0
  264. package/dist/esm/agent/validation_runner.js +73 -0
  265. package/dist/esm/agentic/fix_loop.js +91 -0
  266. package/dist/esm/agentic/playwright_runner.js +161 -0
  267. package/dist/esm/agentic/runner.js +207 -0
  268. package/dist/esm/agentic/types.js +3 -0
  269. package/dist/esm/agents/coverage-evaluator.js +37 -0
  270. package/dist/esm/agents/cross-impact.js +136 -0
  271. package/dist/esm/agents/executor.js +71 -0
  272. package/dist/esm/agents/explorer.js +39 -0
  273. package/dist/esm/agents/generator.js +73 -0
  274. package/dist/esm/agents/healer.js +27 -0
  275. package/dist/esm/agents/impact-analyst.js +34 -0
  276. package/dist/esm/agents/regression-advisor.js +112 -0
  277. package/dist/esm/agents/strategist.js +88 -0
  278. package/dist/esm/agents/test-designer.js +107 -0
  279. package/dist/esm/anthropic_provider.js +326 -0
  280. package/dist/esm/api.js +143 -0
  281. package/dist/esm/base_provider.js +198 -0
  282. package/dist/esm/budget_ledger.js +58 -0
  283. package/dist/esm/cache/cached_provider.js +85 -0
  284. package/dist/esm/cache/response_cache.js +140 -0
  285. package/dist/esm/cli/commands/analyze.js +74 -0
  286. package/dist/esm/cli/commands/bootstrap.js +106 -0
  287. package/dist/esm/cli/commands/cost_report.js +112 -0
  288. package/dist/esm/cli/commands/crew.js +252 -0
  289. package/dist/esm/cli/commands/feedback.js +36 -0
  290. package/dist/esm/cli/commands/finalize.js +38 -0
  291. package/dist/esm/cli/commands/gate.js +86 -0
  292. package/dist/esm/cli/commands/generate.js +105 -0
  293. package/dist/esm/cli/commands/heal.js +57 -0
  294. package/dist/esm/cli/commands/impact.js +30 -0
  295. package/dist/esm/cli/commands/init.js +133 -0
  296. package/dist/esm/cli/commands/llm_health.js +19 -0
  297. package/dist/esm/cli/commands/plan.js +117 -0
  298. package/dist/esm/cli/commands/plan_crew.js +309 -0
  299. package/dist/esm/cli/commands/traceability.js +73 -0
  300. package/dist/esm/cli/commands/train.js +355 -0
  301. package/dist/esm/cli/defaults.js +165 -0
  302. package/dist/esm/cli/errors.js +52 -0
  303. package/dist/esm/cli/parse_args.js +251 -0
  304. package/dist/esm/cli/types.js +3 -0
  305. package/dist/esm/cli/usage.js +106 -0
  306. package/dist/esm/cli.js +192 -0
  307. package/dist/esm/crew/context.js +32 -0
  308. package/dist/esm/crew/orchestrator.js +325 -0
  309. package/dist/esm/crew/protocol.js +3 -0
  310. package/dist/esm/crew/provider.js +33 -0
  311. package/dist/esm/crew/sanitize.js +27 -0
  312. package/dist/esm/crew/types.js +3 -0
  313. package/dist/esm/crew/workflows.js +33 -0
  314. package/dist/esm/custom_provider.js +273 -0
  315. package/dist/esm/engine/ai_enrichment.js +264 -0
  316. package/dist/esm/engine/diff_loader.js +59 -0
  317. package/dist/esm/engine/impact_engine.js +291 -0
  318. package/dist/esm/engine/plan_builder.js +593 -0
  319. package/dist/esm/index.js +72 -0
  320. package/dist/esm/knowledge/api_surface.js +408 -0
  321. package/dist/esm/knowledge/cluster_utils.js +60 -0
  322. package/dist/esm/knowledge/context_loader.js +85 -0
  323. package/dist/esm/knowledge/failure_history.js +121 -0
  324. package/dist/esm/knowledge/kg_bridge.js +381 -0
  325. package/dist/esm/knowledge/kg_types.js +3 -0
  326. package/dist/esm/knowledge/route_families.js +393 -0
  327. package/dist/esm/knowledge/spec_index.js +122 -0
  328. package/dist/esm/logger.js +115 -0
  329. package/dist/esm/mcp-server.js +621 -0
  330. package/dist/esm/metrics/prometheus.js +149 -0
  331. package/dist/esm/model_router.js +59 -0
  332. package/dist/esm/ollama_provider.js +301 -0
  333. package/dist/esm/openai_provider.js +243 -0
  334. package/dist/esm/package.json +3 -0
  335. package/dist/esm/pipeline/orchestrator.js +228 -0
  336. package/dist/esm/pipeline/spec_verifier.js +75 -0
  337. package/dist/esm/pipeline/stage0_preprocess.js +102 -0
  338. package/dist/esm/pipeline/stage1_impact.js +140 -0
  339. package/dist/esm/pipeline/stage2_coverage.js +153 -0
  340. package/dist/esm/pipeline/stage3_generation.js +284 -0
  341. package/dist/esm/pipeline/stage4_heal.js +288 -0
  342. package/dist/esm/progress.js +112 -0
  343. package/dist/esm/prompts/coverage.js +57 -0
  344. package/dist/esm/prompts/cross-impact.js +53 -0
  345. package/dist/esm/prompts/generation.js +297 -0
  346. package/dist/esm/prompts/generation_profile.js +147 -0
  347. package/dist/esm/prompts/heal.js +91 -0
  348. package/dist/esm/prompts/impact.js +63 -0
  349. package/dist/esm/prompts/json_extract.js +36 -0
  350. package/dist/esm/prompts/strategist.js +61 -0
  351. package/dist/esm/prompts/test-designer.js +92 -0
  352. package/dist/esm/provider_factory.js +366 -0
  353. package/dist/esm/provider_interface.js +23 -0
  354. package/dist/esm/provider_utils.js +96 -0
  355. package/dist/esm/qa-agent/cli.js +205 -0
  356. package/dist/esm/qa-agent/orchestrator.js +120 -0
  357. package/dist/esm/qa-agent/phase1/runner.js +139 -0
  358. package/dist/esm/qa-agent/phase1/scope.js +126 -0
  359. package/dist/esm/qa-agent/phase2/agent_browser.js +95 -0
  360. package/dist/esm/qa-agent/phase2/agent_loop.js +351 -0
  361. package/dist/esm/qa-agent/phase2/exploration_state.js +97 -0
  362. package/dist/esm/qa-agent/phase2/tools.js +386 -0
  363. package/dist/esm/qa-agent/phase2/vision.js +75 -0
  364. package/dist/esm/qa-agent/phase3/feedback.js +34 -0
  365. package/dist/esm/qa-agent/phase3/reporter.js +145 -0
  366. package/dist/esm/qa-agent/phase3/spec_generator.js +62 -0
  367. package/dist/esm/qa-agent/phase3/verdict.js +66 -0
  368. package/dist/esm/qa-agent/safe_env.js +23 -0
  369. package/dist/esm/qa-agent/types.js +3 -0
  370. package/dist/esm/reporters/junit.js +86 -0
  371. package/dist/esm/reporters/reporter.js +3 -0
  372. package/dist/esm/reporters/sarif.js +132 -0
  373. package/dist/esm/resilience/circuit_breaker.js +78 -0
  374. package/dist/esm/resilience/retry.js +56 -0
  375. package/dist/esm/sanitize.js +66 -0
  376. package/dist/esm/training/enricher.js +345 -0
  377. package/dist/esm/training/kg_scanner.js +115 -0
  378. package/dist/esm/training/merger.js +204 -0
  379. package/dist/esm/training/scanner.js +923 -0
  380. package/dist/esm/training/types.js +6 -0
  381. package/dist/esm/training/validator.js +254 -0
  382. package/dist/esm/validation/guardrails.js +101 -0
  383. package/dist/esm/validation/output_schema.js +80 -0
  384. package/dist/esm/version.js +33 -0
  385. package/dist/index.d.ts +99 -0
  386. package/dist/index.d.ts.map +1 -0
  387. package/dist/index.js +169 -0
  388. package/dist/knowledge/api_surface.d.ts +37 -0
  389. package/dist/knowledge/api_surface.d.ts.map +1 -0
  390. package/dist/knowledge/api_surface.js +418 -0
  391. package/dist/knowledge/cluster_utils.d.ts +28 -0
  392. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  393. package/dist/knowledge/cluster_utils.js +67 -0
  394. package/dist/knowledge/context_loader.d.ts +13 -0
  395. package/dist/knowledge/context_loader.d.ts.map +1 -0
  396. package/dist/knowledge/context_loader.js +90 -0
  397. package/dist/knowledge/failure_history.d.ts +39 -0
  398. package/dist/knowledge/failure_history.d.ts.map +1 -0
  399. package/dist/knowledge/failure_history.js +128 -0
  400. package/dist/knowledge/kg_bridge.d.ts +31 -0
  401. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  402. package/dist/knowledge/kg_bridge.js +388 -0
  403. package/dist/knowledge/kg_types.d.ts +75 -0
  404. package/dist/knowledge/kg_types.d.ts.map +1 -0
  405. package/dist/knowledge/kg_types.js +4 -0
  406. package/dist/knowledge/route_families.d.ts +98 -0
  407. package/dist/knowledge/route_families.d.ts.map +1 -0
  408. package/dist/knowledge/route_families.js +410 -0
  409. package/dist/knowledge/spec_index.d.ts +18 -0
  410. package/dist/knowledge/spec_index.d.ts.map +1 -0
  411. package/dist/knowledge/spec_index.js +128 -0
  412. package/dist/logger.d.ts +31 -0
  413. package/dist/logger.d.ts.map +1 -0
  414. package/dist/logger.js +119 -0
  415. package/dist/mcp-server.d.ts +68 -0
  416. package/dist/mcp-server.d.ts.map +1 -0
  417. package/dist/mcp-server.js +629 -0
  418. package/dist/metrics/prometheus.d.ts +37 -0
  419. package/dist/metrics/prometheus.d.ts.map +1 -0
  420. package/dist/metrics/prometheus.js +153 -0
  421. package/dist/model_router.d.ts +28 -0
  422. package/dist/model_router.d.ts.map +1 -0
  423. package/dist/model_router.js +63 -0
  424. package/dist/ollama_provider.d.ts +65 -0
  425. package/dist/ollama_provider.d.ts.map +1 -0
  426. package/dist/ollama_provider.js +309 -0
  427. package/dist/openai_provider.d.ts +23 -0
  428. package/dist/openai_provider.d.ts.map +1 -0
  429. package/dist/openai_provider.js +251 -0
  430. package/dist/pipeline/orchestrator.d.ts +33 -0
  431. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  432. package/dist/pipeline/orchestrator.js +231 -0
  433. package/dist/pipeline/spec_verifier.d.ts +20 -0
  434. package/dist/pipeline/spec_verifier.d.ts.map +1 -0
  435. package/dist/pipeline/spec_verifier.js +79 -0
  436. package/dist/pipeline/stage0_preprocess.d.ts +31 -0
  437. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -0
  438. package/dist/pipeline/stage0_preprocess.js +105 -0
  439. package/dist/pipeline/stage1_impact.d.ts +19 -0
  440. package/dist/pipeline/stage1_impact.d.ts.map +1 -0
  441. package/dist/pipeline/stage1_impact.js +143 -0
  442. package/dist/pipeline/stage2_coverage.d.ts +19 -0
  443. package/dist/pipeline/stage2_coverage.d.ts.map +1 -0
  444. package/dist/pipeline/stage2_coverage.js +156 -0
  445. package/dist/pipeline/stage3_generation.d.ts +43 -0
  446. package/dist/pipeline/stage3_generation.d.ts.map +1 -0
  447. package/dist/pipeline/stage3_generation.js +287 -0
  448. package/dist/pipeline/stage4_heal.d.ts +62 -0
  449. package/dist/pipeline/stage4_heal.d.ts.map +1 -0
  450. package/dist/pipeline/stage4_heal.js +294 -0
  451. package/dist/progress.d.ts +22 -0
  452. package/dist/progress.d.ts.map +1 -0
  453. package/dist/progress.js +116 -0
  454. package/dist/prompts/coverage.d.ts +39 -0
  455. package/dist/prompts/coverage.d.ts.map +1 -0
  456. package/dist/prompts/coverage.js +61 -0
  457. package/dist/prompts/cross-impact.d.ts +23 -0
  458. package/dist/prompts/cross-impact.d.ts.map +1 -0
  459. package/dist/prompts/cross-impact.js +57 -0
  460. package/dist/prompts/generation.d.ts +25 -0
  461. package/dist/prompts/generation.d.ts.map +1 -0
  462. package/dist/prompts/generation.js +302 -0
  463. package/dist/prompts/generation_profile.d.ts +29 -0
  464. package/dist/prompts/generation_profile.d.ts.map +1 -0
  465. package/dist/prompts/generation_profile.js +151 -0
  466. package/dist/prompts/heal.d.ts +23 -0
  467. package/dist/prompts/heal.d.ts.map +1 -0
  468. package/dist/prompts/heal.js +95 -0
  469. package/dist/prompts/impact.d.ts +31 -0
  470. package/dist/prompts/impact.d.ts.map +1 -0
  471. package/dist/prompts/impact.js +67 -0
  472. package/dist/prompts/json_extract.d.ts +14 -0
  473. package/dist/prompts/json_extract.d.ts.map +1 -0
  474. package/dist/prompts/json_extract.js +39 -0
  475. package/dist/prompts/strategist.d.ts +25 -0
  476. package/dist/prompts/strategist.d.ts.map +1 -0
  477. package/dist/prompts/strategist.js +65 -0
  478. package/dist/prompts/test-designer.d.ts +35 -0
  479. package/dist/prompts/test-designer.d.ts.map +1 -0
  480. package/dist/prompts/test-designer.js +96 -0
  481. package/dist/provider_factory.d.ts +104 -0
  482. package/dist/provider_factory.d.ts.map +1 -0
  483. package/dist/provider_factory.js +371 -0
  484. package/dist/provider_interface.d.ts +365 -0
  485. package/dist/provider_interface.d.ts.map +1 -0
  486. package/dist/provider_interface.js +28 -0
  487. package/dist/provider_utils.d.ts +39 -0
  488. package/dist/provider_utils.d.ts.map +1 -0
  489. package/dist/provider_utils.js +103 -0
  490. package/dist/qa-agent/cli.d.ts +3 -0
  491. package/dist/qa-agent/cli.d.ts.map +1 -0
  492. package/dist/qa-agent/cli.js +207 -0
  493. package/dist/qa-agent/orchestrator.d.ts +3 -0
  494. package/dist/qa-agent/orchestrator.d.ts.map +1 -0
  495. package/dist/qa-agent/orchestrator.js +123 -0
  496. package/dist/qa-agent/phase1/runner.d.ts +3 -0
  497. package/dist/qa-agent/phase1/runner.d.ts.map +1 -0
  498. package/dist/qa-agent/phase1/runner.js +142 -0
  499. package/dist/qa-agent/phase1/scope.d.ts +6 -0
  500. package/dist/qa-agent/phase1/scope.d.ts.map +1 -0
  501. package/dist/qa-agent/phase1/scope.js +129 -0
  502. package/dist/qa-agent/phase2/agent_browser.d.ts +35 -0
  503. package/dist/qa-agent/phase2/agent_browser.d.ts.map +1 -0
  504. package/dist/qa-agent/phase2/agent_browser.js +99 -0
  505. package/dist/qa-agent/phase2/agent_loop.d.ts +3 -0
  506. package/dist/qa-agent/phase2/agent_loop.d.ts.map +1 -0
  507. package/dist/qa-agent/phase2/agent_loop.js +357 -0
  508. package/dist/qa-agent/phase2/exploration_state.d.ts +12 -0
  509. package/dist/qa-agent/phase2/exploration_state.d.ts.map +1 -0
  510. package/dist/qa-agent/phase2/exploration_state.js +109 -0
  511. package/dist/qa-agent/phase2/tools.d.ts +28 -0
  512. package/dist/qa-agent/phase2/tools.d.ts.map +1 -0
  513. package/dist/qa-agent/phase2/tools.js +390 -0
  514. package/dist/qa-agent/phase2/vision.d.ts +3 -0
  515. package/dist/qa-agent/phase2/vision.d.ts.map +1 -0
  516. package/dist/qa-agent/phase2/vision.js +78 -0
  517. package/dist/qa-agent/phase3/feedback.d.ts +3 -0
  518. package/dist/qa-agent/phase3/feedback.d.ts.map +1 -0
  519. package/dist/qa-agent/phase3/feedback.js +37 -0
  520. package/dist/qa-agent/phase3/reporter.d.ts +3 -0
  521. package/dist/qa-agent/phase3/reporter.d.ts.map +1 -0
  522. package/dist/qa-agent/phase3/reporter.js +148 -0
  523. package/dist/qa-agent/phase3/spec_generator.d.ts +3 -0
  524. package/dist/qa-agent/phase3/spec_generator.d.ts.map +1 -0
  525. package/dist/qa-agent/phase3/spec_generator.js +65 -0
  526. package/dist/qa-agent/phase3/verdict.d.ts +3 -0
  527. package/dist/qa-agent/phase3/verdict.d.ts.map +1 -0
  528. package/dist/qa-agent/phase3/verdict.js +69 -0
  529. package/dist/qa-agent/safe_env.d.ts +3 -0
  530. package/dist/qa-agent/safe_env.d.ts.map +1 -0
  531. package/dist/qa-agent/safe_env.js +26 -0
  532. package/dist/qa-agent/types.d.ts +130 -0
  533. package/dist/qa-agent/types.d.ts.map +1 -0
  534. package/dist/qa-agent/types.js +4 -0
  535. package/dist/reporters/junit.d.ts +6 -0
  536. package/dist/reporters/junit.d.ts.map +1 -0
  537. package/dist/reporters/junit.js +89 -0
  538. package/dist/reporters/reporter.d.ts +42 -0
  539. package/dist/reporters/reporter.d.ts.map +1 -0
  540. package/dist/reporters/reporter.js +4 -0
  541. package/dist/reporters/sarif.d.ts +7 -0
  542. package/dist/reporters/sarif.d.ts.map +1 -0
  543. package/dist/reporters/sarif.js +135 -0
  544. package/dist/resilience/circuit_breaker.d.ts +36 -0
  545. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  546. package/dist/resilience/circuit_breaker.js +82 -0
  547. package/dist/resilience/retry.d.ts +11 -0
  548. package/dist/resilience/retry.d.ts.map +1 -0
  549. package/dist/resilience/retry.js +59 -0
  550. package/dist/sanitize.d.ts +15 -0
  551. package/dist/sanitize.d.ts.map +1 -0
  552. package/dist/sanitize.js +71 -0
  553. package/dist/training/enricher.d.ts +17 -0
  554. package/dist/training/enricher.d.ts.map +1 -0
  555. package/dist/training/enricher.js +350 -0
  556. package/dist/training/kg_scanner.d.ts +13 -0
  557. package/dist/training/kg_scanner.d.ts.map +1 -0
  558. package/dist/training/kg_scanner.js +118 -0
  559. package/dist/training/merger.d.ts +15 -0
  560. package/dist/training/merger.d.ts.map +1 -0
  561. package/dist/training/merger.js +208 -0
  562. package/dist/training/scanner.d.ts +36 -0
  563. package/dist/training/scanner.d.ts.map +1 -0
  564. package/dist/training/scanner.js +932 -0
  565. package/dist/training/types.d.ts +117 -0
  566. package/dist/training/types.d.ts.map +1 -0
  567. package/dist/training/types.js +9 -0
  568. package/dist/training/validator.d.ts +21 -0
  569. package/dist/training/validator.d.ts.map +1 -0
  570. package/dist/training/validator.js +262 -0
  571. package/dist/validation/guardrails.d.ts +31 -0
  572. package/dist/validation/guardrails.d.ts.map +1 -0
  573. package/dist/validation/guardrails.js +112 -0
  574. package/dist/validation/output_schema.d.ts +67 -0
  575. package/dist/validation/output_schema.d.ts.map +1 -0
  576. package/dist/validation/output_schema.js +84 -0
  577. package/dist/version.d.ts +6 -0
  578. package/dist/version.d.ts.map +1 -0
  579. package/dist/version.js +36 -0
  580. package/package.json +126 -0
  581. package/schemas/flow-decision.schema.json +83 -0
  582. package/schemas/gap.schema.json +18 -0
  583. package/schemas/impact.schema.json +455 -0
  584. package/schemas/plan.schema.json +491 -0
  585. package/schemas/route-families.schema.json +137 -0
  586. package/schemas/subsystem-risk-map.schema.json +62 -0
  587. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,923 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ import { readdirSync, readFileSync, lstatSync, existsSync } from 'fs';
4
+ import { join, relative, basename, resolve } from 'path';
5
+ import { normalizeToClusterId } from '../knowledge/cluster_utils.js';
6
+ const SOURCE_MAX_DEPTH = 3;
7
+ // One deeper than source to account for test framework wrapper dirs (e2e/, integration/)
8
+ const TEST_MAX_DEPTH = 5;
9
+ const SPEC_FILES_MAX_DEPTH = 10;
10
+ const SOURCE_ROOTS = ['src', 'app', 'pages', 'components', 'features', 'modules'];
11
+ const SERVER_ROOTS = ['server', 'api', 'cmd', 'model', 'services'];
12
+ const SKIP_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', '.nuxt', 'dist', 'build',
14
+ 'coverage', '__pycache__', '.e2e-ai-agents', '.cache',
15
+ 'vendor', 'third_party',
16
+ ]);
17
+ const TEST_EXTENSIONS = ['.spec.ts', '.test.ts', '.spec.js', '.test.js', '.spec.tsx', '.test.tsx'];
18
+ const GO_TEST_SUFFIX = '_test.go';
19
+ /**
20
+ * Test category directories that organize tests but aren't feature families.
21
+ * Test-only families matching these names are excluded.
22
+ */
23
+ const TEST_CATEGORY_DIRS = new Set([
24
+ 'specs', 'spec', 'accessibility', 'visual', 'smoke', 'regression',
25
+ 'integration', 'functional', 'unit', 'e2e', 'performance', 'load',
26
+ ]);
27
+ /**
28
+ * Structural directories that are code-organization concerns, not feature families.
29
+ * Discovered source dirs matching these names are excluded from family creation.
30
+ */
31
+ const STRUCTURAL_DIRS = new Set([
32
+ 'actions', 'client', 'components', 'hooks', 'i18n', 'packages',
33
+ 'reducers', 'selectors', 'store', 'stores', 'tests', 'types',
34
+ 'utils', 'helpers', 'lib', 'common', 'shared', 'constants',
35
+ 'config', 'styles', 'sass', 'css', 'assets', 'images', 'fonts',
36
+ 'middleware', 'contexts', 'providers', 'layouts', 'templates',
37
+ ]);
38
+ /**
39
+ * Default server Go files that are infrastructure / cross-cutting concerns,
40
+ * not feature-specific domains. Matched after stripping _local/_store suffixes.
41
+ * Override via ScannerConfig.serverInfraFiles.
42
+ */
43
+ const DEFAULT_SERVER_INFRA_FILES = new Set([
44
+ 'api', 'apitestlib', 'context', 'helpers', 'params', 'swagger',
45
+ 'app', 'server', 'enterprise', 'product_service', 'security_update_check',
46
+ 'store', 'adapters', 'errors', 'integrity', 'migrate', 'doc',
47
+ 'main', 'init', 'cluster_discovery', 'web_conn', 'web_broadcast_hooks',
48
+ 'manualtesting', 'testlib', 'router', 'handler', 'opentracing',
49
+ 'platform', 'focalboard', 'playbooks', 'client4', 'model',
50
+ 'manifest', 'permission', 'log', 'utils',
51
+ ]);
52
+ /**
53
+ * Default server tier directories to scan for Go domain files.
54
+ * Each tier represents a layer of the backend architecture.
55
+ * Override via ScannerConfig.serverTiers.
56
+ */
57
+ const DEFAULT_SERVER_TIERS = [
58
+ 'channels/api4',
59
+ 'channels/app',
60
+ 'channels/store/sqlstore',
61
+ 'channels/web',
62
+ 'channels/wsapi',
63
+ 'public/model',
64
+ ];
65
+ /** Type-safe includes check for readonly arrays */
66
+ const includes = (arr, v) => arr.includes(v);
67
+ function isSkipped(name) {
68
+ return name.startsWith('.') || SKIP_DIRS.has(name);
69
+ }
70
+ const normalizeId = normalizeToClusterId;
71
+ function extractFamilyHint(dirPath, projectRoot) {
72
+ const rel = relative(projectRoot, dirPath).replace(/\\/g, '/');
73
+ const parts = rel.split('/').filter(Boolean);
74
+ // Skip the root category dir (src/, server/, tests/, etc.)
75
+ // Return the first meaningful subdirectory name
76
+ for (let i = 1; i < parts.length; i++) {
77
+ const part = parts[i];
78
+ if (!isSkipped(part) && part !== 'e2e' && part !== 'integration' && part !== 'functional') {
79
+ return normalizeId(part);
80
+ }
81
+ }
82
+ return normalizeId(parts[parts.length - 1] || basename(dirPath));
83
+ }
84
+ function walkDirs(root, projectRoot, category, maxDepth, results, depth = 0) {
85
+ if (depth > maxDepth || !existsSync(root)) {
86
+ return;
87
+ }
88
+ let dirents;
89
+ try {
90
+ dirents = readdirSync(root, { withFileTypes: true });
91
+ }
92
+ catch {
93
+ // ENOENT or EACCES — skip inaccessible entries
94
+ return;
95
+ }
96
+ const hasSourceFiles = dirents.some((d) => {
97
+ if (!d.isFile())
98
+ return false;
99
+ const ext = d.name.slice(d.name.lastIndexOf('.'));
100
+ return ['.ts', '.tsx', '.js', '.jsx', '.go', '.py', '.rs'].includes(ext);
101
+ });
102
+ const subdirs = dirents.filter((d) => {
103
+ if (!d.isDirectory() || d.isSymbolicLink())
104
+ return false;
105
+ return !isSkipped(d.name);
106
+ }).map((d) => d.name);
107
+ if (hasSourceFiles && depth >= 1) {
108
+ results.push({
109
+ path: resolve(root),
110
+ relativePath: relative(projectRoot, root).replace(/\\/g, '/'),
111
+ category,
112
+ familyHint: extractFamilyHint(root, projectRoot),
113
+ });
114
+ }
115
+ for (const sub of subdirs) {
116
+ walkDirs(join(root, sub), projectRoot, category, maxDepth, results, depth + 1);
117
+ }
118
+ }
119
+ export function discoverSourceDirs(projectRoot) {
120
+ const results = [];
121
+ const resolved = resolve(projectRoot);
122
+ let entries;
123
+ try {
124
+ entries = readdirSync(resolved);
125
+ }
126
+ catch {
127
+ // ENOENT or EACCES — skip inaccessible entries
128
+ return results;
129
+ }
130
+ for (const entry of entries) {
131
+ if (isSkipped(entry))
132
+ continue;
133
+ const fullPath = join(resolved, entry);
134
+ try {
135
+ const stat = lstatSync(fullPath);
136
+ if (stat.isSymbolicLink() || !stat.isDirectory())
137
+ continue;
138
+ }
139
+ catch {
140
+ // ENOENT or EACCES — skip inaccessible entries
141
+ continue;
142
+ }
143
+ if (includes(SOURCE_ROOTS, entry)) {
144
+ walkDirs(fullPath, resolved, 'webapp', SOURCE_MAX_DEPTH, results);
145
+ }
146
+ else if (includes(SERVER_ROOTS, entry)) {
147
+ walkDirs(fullPath, resolved, 'server', SOURCE_MAX_DEPTH, results);
148
+ }
149
+ }
150
+ return results;
151
+ }
152
+ export function discoverTestDirs(projectRoot) {
153
+ const results = [];
154
+ const resolved = resolve(projectRoot);
155
+ function walk(dir, category, depth) {
156
+ if (depth > TEST_MAX_DEPTH || !existsSync(dir))
157
+ return;
158
+ let entries;
159
+ try {
160
+ entries = readdirSync(dir);
161
+ }
162
+ catch {
163
+ // ENOENT or EACCES — skip inaccessible entries
164
+ return;
165
+ }
166
+ const hasTests = entries.some((e) => {
167
+ return TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX);
168
+ });
169
+ if (hasTests) {
170
+ results.push({
171
+ path: resolve(dir),
172
+ relativePath: relative(resolved, dir).replace(/\\/g, '/'),
173
+ category,
174
+ familyHint: extractFamilyHint(dir, resolved),
175
+ });
176
+ }
177
+ for (const entry of entries) {
178
+ if (isSkipped(entry))
179
+ continue;
180
+ const full = join(dir, entry);
181
+ try {
182
+ const stat = lstatSync(full);
183
+ if (stat.isSymbolicLink())
184
+ continue;
185
+ if (stat.isDirectory()) {
186
+ walk(full, category, depth + 1);
187
+ }
188
+ }
189
+ catch {
190
+ // ENOENT or EACCES — skip inaccessible entries
191
+ }
192
+ }
193
+ }
194
+ const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
195
+ const cypressRoots = ['cypress/e2e', 'cypress/integration'];
196
+ for (const root of testRoots) {
197
+ walk(join(resolved, root), 'test', 0);
198
+ }
199
+ for (const root of cypressRoots) {
200
+ walk(join(resolved, root), 'cypress', 0);
201
+ }
202
+ // Also scan server dirs for Go test files
203
+ for (const root of SERVER_ROOTS) {
204
+ const serverPath = join(resolved, root);
205
+ if (existsSync(serverPath)) {
206
+ walk(serverPath, 'test', 0);
207
+ }
208
+ }
209
+ return results;
210
+ }
211
+ function extractTags(specFiles) {
212
+ const tags = new Set();
213
+ for (const file of specFiles) {
214
+ try {
215
+ const content = readFileSync(file, 'utf-8');
216
+ const matches = content.match(/@[a-zA-Z][a-zA-Z0-9_-]*/g);
217
+ if (matches) {
218
+ for (const m of matches) {
219
+ if (!m.startsWith('@playwright') && !m.startsWith('@param') && !m.startsWith('@returns')) {
220
+ tags.add(m);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ catch {
226
+ // ENOENT or EACCES — skip unreadable files
227
+ }
228
+ }
229
+ return Array.from(tags);
230
+ }
231
+ function getSpecFiles(dir, depth = 0) {
232
+ if (depth > SPEC_FILES_MAX_DEPTH)
233
+ return [];
234
+ const files = [];
235
+ try {
236
+ for (const entry of readdirSync(dir)) {
237
+ const full = join(dir, entry);
238
+ try {
239
+ const stat = lstatSync(full);
240
+ if (stat.isSymbolicLink())
241
+ continue;
242
+ if (stat.isDirectory()) {
243
+ files.push(...getSpecFiles(full, depth + 1));
244
+ }
245
+ else if (TEST_EXTENSIONS.some((ext) => entry.endsWith(ext))) {
246
+ files.push(full);
247
+ }
248
+ }
249
+ catch {
250
+ // ENOENT or EACCES — skip inaccessible entries
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // ENOENT or EACCES — skip inaccessible directories
256
+ }
257
+ return files;
258
+ }
259
+ function buildGlobPattern(relativePath) {
260
+ const normalized = relativePath.replace(/\\/g, '/');
261
+ return `${normalized}/*`;
262
+ }
263
+ function groupByFamily(dirs) {
264
+ const groups = new Map();
265
+ for (const dir of dirs) {
266
+ const key = normalizeId(dir.familyHint);
267
+ if (!groups.has(key)) {
268
+ groups.set(key, { webapp: [], server: [], test: [], cypress: [] });
269
+ }
270
+ const group = groups.get(key);
271
+ if (dir.category === 'webapp')
272
+ group.webapp.push(dir);
273
+ else if (dir.category === 'server')
274
+ group.server.push(dir);
275
+ else if (dir.category === 'cypress')
276
+ group.cypress.push(dir);
277
+ else
278
+ group.test.push(dir);
279
+ }
280
+ return groups;
281
+ }
282
+ function detectFeatures(familyId, group, projectRoot) {
283
+ const features = [];
284
+ const webappSubdirs = new Map();
285
+ for (const dir of group.webapp) {
286
+ try {
287
+ for (const entry of readdirSync(dir.path)) {
288
+ if (isSkipped(entry))
289
+ continue;
290
+ const full = join(dir.path, entry);
291
+ try {
292
+ const stat = lstatSync(full);
293
+ if (stat.isSymbolicLink())
294
+ continue;
295
+ if (stat.isDirectory()) {
296
+ const hint = normalizeId(entry);
297
+ if (!webappSubdirs.has(hint))
298
+ webappSubdirs.set(hint, []);
299
+ webappSubdirs.get(hint).push({
300
+ path: full,
301
+ relativePath: relative(projectRoot, full).replace(/\\/g, '/'),
302
+ category: 'webapp',
303
+ familyHint: entry,
304
+ });
305
+ }
306
+ }
307
+ catch {
308
+ // ENOENT or EACCES — skip inaccessible entries
309
+ }
310
+ }
311
+ }
312
+ catch {
313
+ // ENOENT or EACCES — skip inaccessible directories
314
+ }
315
+ }
316
+ for (const testDir of group.test) {
317
+ try {
318
+ for (const entry of readdirSync(testDir.path)) {
319
+ if (isSkipped(entry))
320
+ continue;
321
+ const full = join(testDir.path, entry);
322
+ try {
323
+ const stat = lstatSync(full);
324
+ if (stat.isSymbolicLink())
325
+ continue;
326
+ if (!stat.isDirectory())
327
+ continue;
328
+ }
329
+ catch {
330
+ // ENOENT or EACCES — skip inaccessible entries
331
+ continue;
332
+ }
333
+ const hint = normalizeId(entry);
334
+ if (webappSubdirs.has(hint)) {
335
+ const webDirs = webappSubdirs.get(hint);
336
+ features.push({
337
+ id: `${familyId}/${hint}`,
338
+ webappPaths: webDirs.map((d) => buildGlobPattern(d.relativePath)),
339
+ serverPaths: [],
340
+ specDirs: [relative(projectRoot, full).replace(/\\/g, '/') + '/'],
341
+ });
342
+ }
343
+ }
344
+ }
345
+ catch {
346
+ // ENOENT or EACCES — skip inaccessible directories
347
+ }
348
+ }
349
+ return features;
350
+ }
351
+ /**
352
+ * Discover families by walking the test directory tree at depth ≥ 2.
353
+ *
354
+ * This is the primary family discovery mechanism for projects where source
355
+ * code is organized by code type (components/, actions/) but tests are
356
+ * organized by feature (channels/drafts/, channels/search/).
357
+ *
358
+ * Each leaf test directory (containing spec files) at meaningful depth ≥ 2
359
+ * becomes a candidate family. Top-level feature dirs (depth 1) are already
360
+ * discovered by the standard `discoverTestDirs` + `groupByFamily` pipeline.
361
+ */
362
+ /**
363
+ * Normalize a Go filename into a family domain identifier.
364
+ * Strips _local, _store, trailing 's' (plurals), and normalizes casing.
365
+ */
366
+ function normalizeServerDomain(baseName) {
367
+ let name = baseName;
368
+ // Strip common suffixes
369
+ name = name.replace(/_local$/, '');
370
+ name = name.replace(/_store$/, '');
371
+ // Skip very short names (e.g., single-letter files)
372
+ if (name.length < 3)
373
+ return null;
374
+ return normalizeId(name);
375
+ }
376
+ /**
377
+ * Given a domain name like "channel_bookmark", find its parent domain
378
+ * if a shorter prefix exists in the set (e.g., "channel").
379
+ * This groups related server files under a single family.
380
+ */
381
+ function findParentDomain(name, allDomains) {
382
+ const parts = name.split('_');
383
+ // Try progressively shorter prefixes
384
+ for (let i = parts.length - 1; i >= 1; i--) {
385
+ const candidate = parts.slice(0, i).join('_');
386
+ if (allDomains.has(candidate) && candidate !== name) {
387
+ return candidate;
388
+ }
389
+ }
390
+ return name;
391
+ }
392
+ /**
393
+ * Discover families by scanning server Go source files.
394
+ *
395
+ * The backend follows a three-tier pattern:
396
+ * api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
397
+ *
398
+ * Related files are grouped under parent domains:
399
+ * channel.go, channel_bookmark.go, channel_category.go → "channel" family
400
+ *
401
+ * Each domain becomes a candidate family with precise serverPaths.
402
+ */
403
+ export function discoverServerDerivedFamilies(serverRoot, infraFiles = DEFAULT_SERVER_INFRA_FILES, tiers = DEFAULT_SERVER_TIERS) {
404
+ const resolved = resolve(serverRoot);
405
+ // First pass: collect all raw domain names across tiers
406
+ const allRawDomains = new Set();
407
+ // domain → tier → Set<file basenames>
408
+ const domainTierFiles = new Map();
409
+ function collectGoFile(entry, tierRelPath) {
410
+ if (!entry.endsWith('.go') || entry.endsWith('_test.go') || entry.startsWith('.'))
411
+ return;
412
+ const baseName = entry.replace('.go', '');
413
+ const domain = normalizeServerDomain(baseName);
414
+ if (!domain || infraFiles.has(domain))
415
+ return;
416
+ allRawDomains.add(domain);
417
+ if (!domainTierFiles.has(domain))
418
+ domainTierFiles.set(domain, new Map());
419
+ const tierMap = domainTierFiles.get(domain);
420
+ if (!tierMap.has(tierRelPath))
421
+ tierMap.set(tierRelPath, new Set());
422
+ tierMap.get(tierRelPath).add(baseName);
423
+ }
424
+ for (const tier of tiers) {
425
+ const tierPath = join(resolved, tier);
426
+ if (!existsSync(tierPath))
427
+ continue;
428
+ let entries;
429
+ try {
430
+ entries = readdirSync(tierPath);
431
+ }
432
+ catch {
433
+ continue;
434
+ }
435
+ for (const entry of entries) {
436
+ collectGoFile(entry, tier);
437
+ // Also check subdirectories (e.g., app/slashcommands/, app/users/)
438
+ const subPath = join(tierPath, entry);
439
+ try {
440
+ const stat = lstatSync(subPath);
441
+ if (stat.isDirectory() && !isSkipped(entry)) {
442
+ const subEntries = readdirSync(subPath);
443
+ for (const subEntry of subEntries) {
444
+ collectGoFile(subEntry, `${tier}/${entry}`);
445
+ }
446
+ }
447
+ }
448
+ catch { /* skip */ }
449
+ }
450
+ }
451
+ // Scan job directories — each subdirectory is a job type
452
+ const jobsPath = join(resolved, 'channels/jobs');
453
+ if (existsSync(jobsPath)) {
454
+ try {
455
+ for (const entry of readdirSync(jobsPath)) {
456
+ const jobPath = join(jobsPath, entry);
457
+ try {
458
+ if (!lstatSync(jobPath).isDirectory() || isSkipped(entry))
459
+ continue;
460
+ const domain = normalizeId(entry);
461
+ if (infraFiles.has(domain))
462
+ continue;
463
+ allRawDomains.add(domain);
464
+ const jobFiles = readdirSync(jobPath);
465
+ for (const jf of jobFiles) {
466
+ if (jf.endsWith('.go') && !jf.endsWith('_test.go')) {
467
+ if (!domainTierFiles.has(domain))
468
+ domainTierFiles.set(domain, new Map());
469
+ const tierMap = domainTierFiles.get(domain);
470
+ const tierKey = `channels/jobs/${entry}`;
471
+ if (!tierMap.has(tierKey))
472
+ tierMap.set(tierKey, new Set());
473
+ tierMap.get(tierKey).add(jf.replace('.go', ''));
474
+ }
475
+ }
476
+ }
477
+ catch { /* skip */ }
478
+ }
479
+ }
480
+ catch { /* skip */ }
481
+ }
482
+ // Second pass: group child domains under parents
483
+ // e.g., channel_bookmark → channel, post_priority → post
484
+ // Track which top-level tiers each family touches for significance filtering.
485
+ const familyPaths = new Map();
486
+ const familyTiers = new Map();
487
+ for (const [domain, tierMap] of domainTierFiles) {
488
+ const parentDomain = findParentDomain(domain, allRawDomains);
489
+ if (!familyPaths.has(parentDomain))
490
+ familyPaths.set(parentDomain, new Set());
491
+ if (!familyTiers.has(parentDomain))
492
+ familyTiers.set(parentDomain, new Set());
493
+ const paths = familyPaths.get(parentDomain);
494
+ const tiers = familyTiers.get(parentDomain);
495
+ for (const [tierRelPath, fileNames] of tierMap) {
496
+ // Track the top-level tier (e.g., "channels/api4" from "channels/api4/slashcommands")
497
+ const topTier = tierRelPath.split('/').slice(0, 2).join('/');
498
+ tiers.add(topTier);
499
+ for (const baseName of fileNames) {
500
+ // Use directory-level glob to capture the file and related variants
501
+ paths.add(`server/${tierRelPath}/${baseName}*.go`);
502
+ }
503
+ }
504
+ }
505
+ // Build families from grouped domains.
506
+ // Multi-tier families (≥2 tiers) can be new families.
507
+ // Single-tier families can only merge into existing families.
508
+ const multiTierFamilies = [];
509
+ const singleTierFamilies = [];
510
+ for (const [domain, paths] of familyPaths) {
511
+ if (paths.size === 0)
512
+ continue;
513
+ const tierCount = familyTiers.get(domain)?.size ?? 0;
514
+ const family = {
515
+ id: domain,
516
+ routes: [`/${domain.replace(/_/g, '-')}`],
517
+ webappPaths: [],
518
+ serverPaths: Array.from(paths),
519
+ specDirs: [],
520
+ cypressSpecDirs: [],
521
+ tags: [],
522
+ features: [],
523
+ routesGuessed: true,
524
+ };
525
+ if (tierCount >= 2) {
526
+ multiTierFamilies.push(family);
527
+ }
528
+ else {
529
+ singleTierFamilies.push(family);
530
+ }
531
+ }
532
+ return { multiTierFamilies, singleTierFamilies };
533
+ }
534
+ export function discoverTestDerivedFamilies(testsRoot) {
535
+ const resolved = resolve(testsRoot);
536
+ const candidates = [];
537
+ function walk(dir, depth) {
538
+ if (depth > 8)
539
+ return;
540
+ let entries;
541
+ try {
542
+ entries = readdirSync(dir);
543
+ }
544
+ catch {
545
+ return;
546
+ }
547
+ const hasSpecs = entries.some((e) => TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX));
548
+ const subdirs = entries.filter((e) => {
549
+ if (isSkipped(e))
550
+ return false;
551
+ try {
552
+ const stat = lstatSync(join(dir, e));
553
+ return !stat.isSymbolicLink() && stat.isDirectory();
554
+ }
555
+ catch {
556
+ return false;
557
+ }
558
+ });
559
+ const relPath = relative(resolved, dir).replace(/\\/g, '/');
560
+ const parts = relPath.split('/').filter(Boolean);
561
+ const meaningful = parts.filter((p) => !TEST_CATEGORY_DIRS.has(normalizeId(p)) && !isSkipped(p));
562
+ // Depth-2+ meaningful dirs with spec files → candidate families
563
+ if (meaningful.length >= 2 && hasSpecs) {
564
+ const leafId = normalizeId(meaningful[meaningful.length - 1]);
565
+ const parentId = normalizeId(meaningful[meaningful.length - 2]);
566
+ if (!STRUCTURAL_DIRS.has(leafId) && !TEST_CATEGORY_DIRS.has(leafId)) {
567
+ candidates.push({ dir, relPath, leafId, parentId });
568
+ }
569
+ }
570
+ for (const sub of subdirs) {
571
+ walk(join(dir, sub), depth + 1);
572
+ }
573
+ }
574
+ // Walk from standard test roots
575
+ const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
576
+ for (const root of testRoots) {
577
+ const rootPath = join(resolved, root);
578
+ if (existsSync(rootPath)) {
579
+ walk(rootPath, 0);
580
+ }
581
+ }
582
+ // Detect leaf-name collisions across parents
583
+ const idCount = new Map();
584
+ for (const c of candidates) {
585
+ idCount.set(c.leafId, (idCount.get(c.leafId) || 0) + 1);
586
+ }
587
+ // Build families — prefix with parent when names collide
588
+ const familyMap = new Map();
589
+ for (const c of candidates) {
590
+ let familyId = c.leafId;
591
+ if ((idCount.get(c.leafId) || 0) > 1 && c.parentId) {
592
+ familyId = `${c.parentId}_${c.leafId}`;
593
+ }
594
+ if (!familyMap.has(familyId)) {
595
+ const specFiles = getSpecFiles(c.dir);
596
+ familyMap.set(familyId, {
597
+ id: familyId,
598
+ routes: [`/${familyId.replace(/_/g, '-')}`],
599
+ webappPaths: [],
600
+ serverPaths: [],
601
+ specDirs: [c.relPath + '/'],
602
+ cypressSpecDirs: [],
603
+ tags: extractTags(specFiles),
604
+ features: [],
605
+ routesGuessed: true,
606
+ });
607
+ }
608
+ else {
609
+ const existing = familyMap.get(familyId);
610
+ const specDir = c.relPath + '/';
611
+ if (!existing.specDirs.includes(specDir)) {
612
+ existing.specDirs.push(specDir);
613
+ existing.tags = [...new Set([...existing.tags, ...extractTags(getSpecFiles(c.dir))])];
614
+ }
615
+ }
616
+ }
617
+ return Array.from(familyMap.values());
618
+ }
619
+ /**
620
+ * Discover test library paths (page objects, helpers) organized by feature.
621
+ * Walks well-known test lib directories and maps subdirectories and files to family IDs.
622
+ */
623
+ export function discoverTestLibPaths(testsRoot) {
624
+ const resolved = resolve(testsRoot);
625
+ const result = new Map();
626
+ const libDirs = [
627
+ 'lib/src/ui/components',
628
+ 'lib/src/ui/pages',
629
+ 'lib/src/server',
630
+ ];
631
+ for (const libDir of libDirs) {
632
+ const fullDir = join(resolved, libDir);
633
+ if (!existsSync(fullDir))
634
+ continue;
635
+ let entries;
636
+ try {
637
+ entries = readdirSync(fullDir);
638
+ }
639
+ catch {
640
+ continue;
641
+ }
642
+ for (const entry of entries) {
643
+ if (isSkipped(entry))
644
+ continue;
645
+ const fullPath = join(fullDir, entry);
646
+ try {
647
+ const stat = lstatSync(fullPath);
648
+ if (stat.isSymbolicLink())
649
+ continue;
650
+ if (stat.isDirectory()) {
651
+ // Subdirectory → family ID from dir name
652
+ const familyId = normalizeId(entry);
653
+ const relPath = relative(resolved, fullPath).replace(/\\/g, '/');
654
+ if (!result.has(familyId))
655
+ result.set(familyId, []);
656
+ result.get(familyId).push(`${relPath}/*`);
657
+ }
658
+ else if (stat.isFile()) {
659
+ // File → family ID from basename (e.g., channel.ts → channel)
660
+ const ext = entry.slice(entry.lastIndexOf('.'));
661
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
662
+ continue;
663
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
664
+ const familyId = normalizeId(baseName);
665
+ if (familyId.length < 3)
666
+ continue;
667
+ const relPath = relative(resolved, fullPath).replace(/\\/g, '/');
668
+ if (!result.has(familyId))
669
+ result.set(familyId, []);
670
+ result.get(familyId).push(relPath);
671
+ }
672
+ }
673
+ catch {
674
+ continue;
675
+ }
676
+ }
677
+ }
678
+ return result;
679
+ }
680
+ /**
681
+ * Discover files in well-known directories (types, utils) whose basename
682
+ * maps directly to a family ID.
683
+ */
684
+ export function discoverNameMatchedPaths(appPath, gitRepoRoot) {
685
+ const result = new Map();
686
+ const resolvedApp = resolve(appPath);
687
+ const scanRoots = [
688
+ { root: join(resolvedApp, 'src/utils'), base: resolvedApp },
689
+ { root: join(resolvedApp, 'src/types'), base: resolvedApp },
690
+ ];
691
+ // Monorepo-aware: scan platform types and server model directories
692
+ if (gitRepoRoot) {
693
+ const resolvedGitRoot = resolve(gitRepoRoot);
694
+ const platformTypes = join(resolvedGitRoot, 'webapp/platform/types/src');
695
+ if (existsSync(platformTypes)) {
696
+ scanRoots.push({ root: platformTypes, base: resolvedGitRoot });
697
+ }
698
+ const platformClient = join(resolvedGitRoot, 'webapp/platform/client/src');
699
+ if (existsSync(platformClient)) {
700
+ scanRoots.push({ root: platformClient, base: resolvedGitRoot });
701
+ }
702
+ const serverModel = join(resolvedGitRoot, 'server/public/model');
703
+ if (existsSync(serverModel)) {
704
+ scanRoots.push({ root: serverModel, base: resolvedGitRoot });
705
+ }
706
+ }
707
+ for (const { root, base } of scanRoots) {
708
+ if (!existsSync(root))
709
+ continue;
710
+ let entries;
711
+ try {
712
+ entries = readdirSync(root);
713
+ }
714
+ catch {
715
+ continue;
716
+ }
717
+ for (const entry of entries) {
718
+ if (entry.startsWith('.'))
719
+ continue;
720
+ const ext = entry.slice(entry.lastIndexOf('.'));
721
+ if (!['.ts', '.tsx', '.js', '.jsx', '.go'].includes(ext))
722
+ continue;
723
+ // Skip Go test files
724
+ if (entry.endsWith('_test.go'))
725
+ continue;
726
+ const fullPath = join(root, entry);
727
+ try {
728
+ const stat = lstatSync(fullPath);
729
+ if (!stat.isFile() || stat.isSymbolicLink())
730
+ continue;
731
+ }
732
+ catch {
733
+ continue;
734
+ }
735
+ // Strip extension and normalize
736
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
737
+ const familyId = normalizeId(baseName);
738
+ if (familyId.length < 3)
739
+ continue;
740
+ const relPath = relative(base, fullPath).replace(/\\/g, '/');
741
+ if (!result.has(familyId))
742
+ result.set(familyId, []);
743
+ result.get(familyId).push(relPath);
744
+ }
745
+ }
746
+ return result;
747
+ }
748
+ export function scanProject(projectRoot, testsRoot, serverRoot, gitRepoRoot, config) {
749
+ // Resolve scanner config for this run (passed to functions, no module-level mutation)
750
+ const resolvedInfraFiles = config?.serverInfraFiles || DEFAULT_SERVER_INFRA_FILES;
751
+ const resolvedTiers = config?.serverTiers || DEFAULT_SERVER_TIERS;
752
+ const resolved = resolve(projectRoot);
753
+ const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolved;
754
+ const sourceDirs = discoverSourceDirs(resolved);
755
+ const testDirs = discoverTestDirs(resolvedTestsRoot);
756
+ const allDirs = [...sourceDirs, ...testDirs];
757
+ const groups = groupByFamily(allDirs);
758
+ const families = [];
759
+ for (const [familyId, group] of groups) {
760
+ const hasSrc = group.webapp.length > 0 || group.server.length > 0;
761
+ const hasTests = group.test.length > 0 || group.cypress.length > 0;
762
+ if (!hasSrc && !hasTests)
763
+ continue;
764
+ // Skip structural directories that are code-organization, not features.
765
+ // Only skip if they have source dirs but no corresponding test dirs.
766
+ if (STRUCTURAL_DIRS.has(familyId) && !hasTests)
767
+ continue;
768
+ // Skip test-only families that match broad test categories (not feature families).
769
+ if (!hasSrc && hasTests && TEST_CATEGORY_DIRS.has(familyId))
770
+ continue;
771
+ const allSpecFiles = [];
772
+ for (const td of [...group.test, ...group.cypress]) {
773
+ allSpecFiles.push(...getSpecFiles(td.path));
774
+ }
775
+ const features = detectFeatures(familyId, group, resolved);
776
+ families.push({
777
+ id: familyId,
778
+ routes: [`/${familyId}`],
779
+ webappPaths: group.webapp.map((d) => buildGlobPattern(d.relativePath)),
780
+ serverPaths: group.server.map((d) => buildGlobPattern(d.relativePath)),
781
+ specDirs: group.test.map((d) => d.relativePath + '/'),
782
+ cypressSpecDirs: group.cypress.map((d) => d.relativePath + '/'),
783
+ tags: extractTags(allSpecFiles),
784
+ features,
785
+ routesGuessed: true,
786
+ });
787
+ }
788
+ // When a separate testsRoot is provided, discover families from test
789
+ // directory structure. Projects with feature-organized tests but
790
+ // code-type-organized source benefit from this.
791
+ if (testsRoot) {
792
+ const testFamilies = discoverTestDerivedFamilies(resolvedTestsRoot);
793
+ const existingIds = new Set(families.map((f) => f.id));
794
+ for (const tf of testFamilies) {
795
+ if (existingIds.has(tf.id)) {
796
+ // Merge specDirs into existing family
797
+ const existing = families.find((f) => f.id === tf.id);
798
+ for (const sd of tf.specDirs) {
799
+ if (!existing.specDirs.includes(sd)) {
800
+ existing.specDirs.push(sd);
801
+ }
802
+ }
803
+ existing.tags = [...new Set([...existing.tags, ...tf.tags])];
804
+ }
805
+ else {
806
+ families.push(tf);
807
+ existingIds.add(tf.id);
808
+ }
809
+ }
810
+ }
811
+ // When a separate serverRoot is provided, discover families from Go source
812
+ // filenames across the three-tier backend (api4, app, store).
813
+ if (serverRoot) {
814
+ const { multiTierFamilies: serverMulti, singleTierFamilies: serverSingle } = discoverServerDerivedFamilies(resolve(serverRoot), resolvedInfraFiles, resolvedTiers);
815
+ const existingIds = new Set(families.map((f) => f.id));
816
+ // Merge ALL server families (multi + single tier) into existing families,
817
+ // but only add NEW families if they span ≥2 tiers.
818
+ const allServerFamilies = [...serverMulti, ...serverSingle];
819
+ for (const sf of allServerFamilies) {
820
+ // Try exact match, then singular/plural variants
821
+ let target = families.find((f) => f.id === sf.id);
822
+ if (!target && !sf.id.endsWith('s')) {
823
+ target = families.find((f) => f.id === sf.id + 's');
824
+ }
825
+ if (!target && sf.id.endsWith('s')) {
826
+ target = families.find((f) => f.id === sf.id.slice(0, -1));
827
+ }
828
+ if (target) {
829
+ // Merge serverPaths into existing family
830
+ for (const sp of sf.serverPaths) {
831
+ if (!target.serverPaths.includes(sp)) {
832
+ target.serverPaths.push(sp);
833
+ }
834
+ }
835
+ }
836
+ else if (serverMulti.includes(sf)) {
837
+ // Only add new families if they span ≥2 tiers
838
+ families.push(sf);
839
+ existingIds.add(sf.id);
840
+ }
841
+ }
842
+ }
843
+ // Merge test library paths (page objects, helpers) into existing families
844
+ if (testsRoot) {
845
+ const testLibPaths = discoverTestLibPaths(resolvedTestsRoot);
846
+ for (const [libFamilyId, patterns] of testLibPaths) {
847
+ let target = families.find((f) => f.id === libFamilyId);
848
+ if (!target && !libFamilyId.endsWith('s')) {
849
+ target = families.find((f) => f.id === libFamilyId + 's');
850
+ }
851
+ if (!target && libFamilyId.endsWith('s')) {
852
+ target = families.find((f) => f.id === libFamilyId.slice(0, -1));
853
+ }
854
+ if (target) {
855
+ for (const p of patterns) {
856
+ if (!target.webappPaths.includes(p)) {
857
+ target.webappPaths.push(p);
858
+ }
859
+ }
860
+ }
861
+ }
862
+ }
863
+ // Merge name-matched type/util files into existing families
864
+ {
865
+ const nameMatchedPaths = discoverNameMatchedPaths(resolved, gitRepoRoot);
866
+ for (const [nmFamilyId, paths] of nameMatchedPaths) {
867
+ let target = families.find((f) => f.id === nmFamilyId);
868
+ if (!target && !nmFamilyId.endsWith('s')) {
869
+ target = families.find((f) => f.id === nmFamilyId + 's');
870
+ }
871
+ if (!target && nmFamilyId.endsWith('s')) {
872
+ target = families.find((f) => f.id === nmFamilyId.slice(0, -1));
873
+ }
874
+ if (target) {
875
+ for (const p of paths) {
876
+ if (!target.webappPaths.includes(p)) {
877
+ target.webappPaths.push(p);
878
+ }
879
+ }
880
+ }
881
+ }
882
+ }
883
+ const familyIds = new Set(families.map((f) => f.id));
884
+ const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
885
+ const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
886
+ let totalSourceFiles = 0;
887
+ let totalTestFiles = 0;
888
+ for (const dir of sourceDirs) {
889
+ try {
890
+ totalSourceFiles += readdirSync(dir.path).filter((e) => {
891
+ try {
892
+ const stat = lstatSync(join(dir.path, e));
893
+ return !stat.isSymbolicLink() && !stat.isDirectory();
894
+ }
895
+ catch {
896
+ // ENOENT or EACCES — skip inaccessible entries
897
+ return false;
898
+ }
899
+ }).length;
900
+ }
901
+ catch {
902
+ // ENOENT or EACCES — skip inaccessible directories
903
+ }
904
+ }
905
+ for (const dir of testDirs) {
906
+ try {
907
+ totalTestFiles += getSpecFiles(dir.path).length;
908
+ }
909
+ catch {
910
+ // ENOENT or EACCES — skip inaccessible directories
911
+ }
912
+ }
913
+ return {
914
+ families,
915
+ unmatchedSourceDirs,
916
+ unmatchedTestDirs,
917
+ stats: {
918
+ totalSourceFiles,
919
+ totalTestFiles,
920
+ familyCount: families.length,
921
+ },
922
+ };
923
+ }