@dewtech/dare-cli 3.11.0 → 3.13.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 (316) hide show
  1. package/README.md +2 -0
  2. package/dist/__tests__/cli-only-invariants.test.d.ts +2 -0
  3. package/dist/__tests__/cli-only-invariants.test.d.ts.map +1 -0
  4. package/dist/__tests__/cli-only-invariants.test.js +100 -0
  5. package/dist/__tests__/cli-only-invariants.test.js.map +1 -0
  6. package/dist/__tests__/cli-only-regression.test.d.ts +2 -0
  7. package/dist/__tests__/cli-only-regression.test.d.ts.map +1 -0
  8. package/dist/__tests__/cli-only-regression.test.js +29 -0
  9. package/dist/__tests__/cli-only-regression.test.js.map +1 -0
  10. package/dist/__tests__/ensure-skills.test.js +5 -0
  11. package/dist/__tests__/ensure-skills.test.js.map +1 -1
  12. package/dist/__tests__/ide-command-parity.test.js +1 -0
  13. package/dist/__tests__/ide-command-parity.test.js.map +1 -1
  14. package/dist/__tests__/project-generator.test.js +17 -0
  15. package/dist/__tests__/project-generator.test.js.map +1 -1
  16. package/dist/__tests__/reverse-facts.test.js +1 -0
  17. package/dist/__tests__/reverse-facts.test.js.map +1 -1
  18. package/dist/__tests__/terminal-parity-regression.test.d.ts +2 -0
  19. package/dist/__tests__/terminal-parity-regression.test.d.ts.map +1 -0
  20. package/dist/__tests__/terminal-parity-regression.test.js +116 -0
  21. package/dist/__tests__/terminal-parity-regression.test.js.map +1 -0
  22. package/dist/__tests__/terminal-parity.test.d.ts +2 -0
  23. package/dist/__tests__/terminal-parity.test.d.ts.map +1 -0
  24. package/dist/__tests__/terminal-parity.test.js +81 -0
  25. package/dist/__tests__/terminal-parity.test.js.map +1 -0
  26. package/dist/agent/__tests__/antigravity-driver.test.d.ts +2 -0
  27. package/dist/agent/__tests__/antigravity-driver.test.d.ts.map +1 -0
  28. package/dist/agent/__tests__/antigravity-driver.test.js +52 -0
  29. package/dist/agent/__tests__/antigravity-driver.test.js.map +1 -0
  30. package/dist/agent/__tests__/codex-driver.test.d.ts +2 -0
  31. package/dist/agent/__tests__/codex-driver.test.d.ts.map +1 -0
  32. package/dist/agent/__tests__/codex-driver.test.js +68 -0
  33. package/dist/agent/__tests__/codex-driver.test.js.map +1 -0
  34. package/dist/agent/__tests__/cursor-driver.test.d.ts +2 -0
  35. package/dist/agent/__tests__/cursor-driver.test.d.ts.map +1 -0
  36. package/dist/agent/__tests__/cursor-driver.test.js +52 -0
  37. package/dist/agent/__tests__/cursor-driver.test.js.map +1 -0
  38. package/dist/agent/driver.d.ts +1 -1
  39. package/dist/agent/driver.d.ts.map +1 -1
  40. package/dist/agent/drivers/antigravity.d.ts +8 -0
  41. package/dist/agent/drivers/antigravity.d.ts.map +1 -0
  42. package/dist/agent/drivers/antigravity.js +99 -0
  43. package/dist/agent/drivers/antigravity.js.map +1 -0
  44. package/dist/agent/drivers/codex.d.ts +12 -0
  45. package/dist/agent/drivers/codex.d.ts.map +1 -0
  46. package/dist/agent/drivers/codex.js +137 -0
  47. package/dist/agent/drivers/codex.js.map +1 -0
  48. package/dist/agent/drivers/cursor.d.ts +8 -0
  49. package/dist/agent/drivers/cursor.d.ts.map +1 -0
  50. package/dist/agent/drivers/cursor.js +99 -0
  51. package/dist/agent/drivers/cursor.js.map +1 -0
  52. package/dist/ai/__tests__/ai-core.test.d.ts +2 -0
  53. package/dist/ai/__tests__/ai-core.test.d.ts.map +1 -0
  54. package/dist/ai/__tests__/ai-core.test.js +41 -0
  55. package/dist/ai/__tests__/ai-core.test.js.map +1 -0
  56. package/dist/ai/__tests__/parity.test.d.ts +2 -0
  57. package/dist/ai/__tests__/parity.test.d.ts.map +1 -0
  58. package/dist/ai/__tests__/parity.test.js +36 -0
  59. package/dist/ai/__tests__/parity.test.js.map +1 -0
  60. package/dist/ai/__tests__/pipeline.test.d.ts +2 -0
  61. package/dist/ai/__tests__/pipeline.test.d.ts.map +1 -0
  62. package/dist/ai/__tests__/pipeline.test.js +147 -0
  63. package/dist/ai/__tests__/pipeline.test.js.map +1 -0
  64. package/dist/ai/__tests__/refine-bridge.test.d.ts +2 -0
  65. package/dist/ai/__tests__/refine-bridge.test.d.ts.map +1 -0
  66. package/dist/ai/__tests__/refine-bridge.test.js +17 -0
  67. package/dist/ai/__tests__/refine-bridge.test.js.map +1 -0
  68. package/dist/ai/__tests__/resolve.test.d.ts +2 -0
  69. package/dist/ai/__tests__/resolve.test.d.ts.map +1 -0
  70. package/dist/ai/__tests__/resolve.test.js +42 -0
  71. package/dist/ai/__tests__/resolve.test.js.map +1 -0
  72. package/dist/ai/capabilities.d.ts +3 -0
  73. package/dist/ai/capabilities.d.ts.map +1 -0
  74. package/dist/ai/capabilities.js +11 -0
  75. package/dist/ai/capabilities.js.map +1 -0
  76. package/dist/ai/command-options.d.ts +10 -0
  77. package/dist/ai/command-options.d.ts.map +1 -0
  78. package/dist/ai/command-options.js +15 -0
  79. package/dist/ai/command-options.js.map +1 -0
  80. package/dist/ai/config.d.ts +27 -0
  81. package/dist/ai/config.d.ts.map +1 -0
  82. package/dist/ai/config.js +89 -0
  83. package/dist/ai/config.js.map +1 -0
  84. package/dist/ai/parity.d.ts +13 -0
  85. package/dist/ai/parity.d.ts.map +1 -0
  86. package/dist/ai/parity.js +87 -0
  87. package/dist/ai/parity.js.map +1 -0
  88. package/dist/ai/parse-json-output.d.ts +5 -0
  89. package/dist/ai/parse-json-output.d.ts.map +1 -0
  90. package/dist/ai/parse-json-output.js +25 -0
  91. package/dist/ai/parse-json-output.js.map +1 -0
  92. package/dist/ai/pipeline.d.ts +20 -0
  93. package/dist/ai/pipeline.d.ts.map +1 -0
  94. package/dist/ai/pipeline.js +303 -0
  95. package/dist/ai/pipeline.js.map +1 -0
  96. package/dist/ai/prompts.d.ts +6 -0
  97. package/dist/ai/prompts.d.ts.map +1 -0
  98. package/dist/ai/prompts.js +49 -0
  99. package/dist/ai/prompts.js.map +1 -0
  100. package/dist/ai/providers.d.ts +63 -0
  101. package/dist/ai/providers.d.ts.map +1 -0
  102. package/dist/ai/providers.js +297 -0
  103. package/dist/ai/providers.js.map +1 -0
  104. package/dist/ai/refine-bridge.d.ts +5 -0
  105. package/dist/ai/refine-bridge.d.ts.map +1 -0
  106. package/dist/ai/refine-bridge.js +14 -0
  107. package/dist/ai/refine-bridge.js.map +1 -0
  108. package/dist/ai/registry.d.ts +12 -0
  109. package/dist/ai/registry.d.ts.map +1 -0
  110. package/dist/ai/registry.js +43 -0
  111. package/dist/ai/registry.js.map +1 -0
  112. package/dist/ai/resolve.d.ts +28 -0
  113. package/dist/ai/resolve.d.ts.map +1 -0
  114. package/dist/ai/resolve.js +83 -0
  115. package/dist/ai/resolve.js.map +1 -0
  116. package/dist/ai/schemas.d.ts +175 -0
  117. package/dist/ai/schemas.d.ts.map +1 -0
  118. package/dist/ai/schemas.js +199 -0
  119. package/dist/ai/schemas.js.map +1 -0
  120. package/dist/ai/types.d.ts +52 -0
  121. package/dist/ai/types.d.ts.map +1 -0
  122. package/dist/ai/types.js +8 -0
  123. package/dist/ai/types.js.map +1 -0
  124. package/dist/bin/dare.js +2 -0
  125. package/dist/bin/dare.js.map +1 -1
  126. package/dist/commands/__tests__/ai-command.test.d.ts +2 -0
  127. package/dist/commands/__tests__/ai-command.test.d.ts.map +1 -0
  128. package/dist/commands/__tests__/ai-command.test.js +68 -0
  129. package/dist/commands/__tests__/ai-command.test.js.map +1 -0
  130. package/dist/commands/__tests__/execute-agent.test.js +82 -0
  131. package/dist/commands/__tests__/execute-agent.test.js.map +1 -1
  132. package/dist/commands/ai.d.ts +3 -0
  133. package/dist/commands/ai.d.ts.map +1 -0
  134. package/dist/commands/ai.js +141 -0
  135. package/dist/commands/ai.js.map +1 -0
  136. package/dist/commands/blueprint.d.ts.map +1 -1
  137. package/dist/commands/blueprint.js +17 -3
  138. package/dist/commands/blueprint.js.map +1 -1
  139. package/dist/commands/design.d.ts.map +1 -1
  140. package/dist/commands/design.js +21 -2
  141. package/dist/commands/design.js.map +1 -1
  142. package/dist/commands/discover.d.ts.map +1 -1
  143. package/dist/commands/discover.js +9 -1
  144. package/dist/commands/discover.js.map +1 -1
  145. package/dist/commands/dna.d.ts.map +1 -1
  146. package/dist/commands/dna.js +23 -3
  147. package/dist/commands/dna.js.map +1 -1
  148. package/dist/commands/execute.d.ts +11 -0
  149. package/dist/commands/execute.d.ts.map +1 -1
  150. package/dist/commands/execute.js +111 -4
  151. package/dist/commands/execute.js.map +1 -1
  152. package/dist/commands/init.d.ts.map +1 -1
  153. package/dist/commands/init.js +1 -0
  154. package/dist/commands/init.js.map +1 -1
  155. package/dist/commands/migrate.d.ts.map +1 -1
  156. package/dist/commands/migrate.js +14 -2
  157. package/dist/commands/migrate.js.map +1 -1
  158. package/dist/commands/patterns.d.ts.map +1 -1
  159. package/dist/commands/patterns.js +14 -2
  160. package/dist/commands/patterns.js.map +1 -1
  161. package/dist/commands/refine.d.ts.map +1 -1
  162. package/dist/commands/refine.js +23 -2
  163. package/dist/commands/refine.js.map +1 -1
  164. package/dist/commands/reverse.d.ts.map +1 -1
  165. package/dist/commands/reverse.js +28 -3
  166. package/dist/commands/reverse.js.map +1 -1
  167. package/dist/commands/review.d.ts.map +1 -1
  168. package/dist/commands/review.js +25 -3
  169. package/dist/commands/review.js.map +1 -1
  170. package/dist/core/types/project.d.ts +1 -1
  171. package/dist/core/types/project.d.ts.map +1 -1
  172. package/dist/dag-runner/run_dag.d.ts +1 -1
  173. package/dist/dag-runner/run_dag.d.ts.map +1 -1
  174. package/dist/exec/safe-spawn.d.ts.map +1 -1
  175. package/dist/exec/safe-spawn.js +6 -1
  176. package/dist/exec/safe-spawn.js.map +1 -1
  177. package/dist/skills/bundled.d.ts +5 -0
  178. package/dist/skills/bundled.d.ts.map +1 -0
  179. package/dist/skills/bundled.js +34 -0
  180. package/dist/skills/bundled.js.map +1 -0
  181. package/dist/skills/commands/add.d.ts +1 -3
  182. package/dist/skills/commands/add.d.ts.map +1 -1
  183. package/dist/skills/commands/add.js +20 -3
  184. package/dist/skills/commands/add.js.map +1 -1
  185. package/dist/skills/tests/bundled.spec.d.ts +2 -0
  186. package/dist/skills/tests/bundled.spec.d.ts.map +1 -0
  187. package/dist/skills/tests/bundled.spec.js +24 -0
  188. package/dist/skills/tests/bundled.spec.js.map +1 -0
  189. package/dist/types/UpdateManifest.types.d.ts +1 -1
  190. package/dist/types/UpdateManifest.types.d.ts.map +1 -1
  191. package/dist/utils/dag-converter.js +1 -1
  192. package/dist/utils/dag-converter.js.map +1 -1
  193. package/dist/utils/project-detector.d.ts +1 -0
  194. package/dist/utils/project-detector.d.ts.map +1 -1
  195. package/dist/utils/project-detector.js +8 -0
  196. package/dist/utils/project-detector.js.map +1 -1
  197. package/dist/utils/project-generator.d.ts +1 -1
  198. package/dist/utils/project-generator.d.ts.map +1 -1
  199. package/dist/utils/project-generator.js +23 -2
  200. package/dist/utils/project-generator.js.map +1 -1
  201. package/dist/utils/templates.d.ts +2 -0
  202. package/dist/utils/templates.d.ts.map +1 -1
  203. package/dist/utils/templates.js +74 -0
  204. package/dist/utils/templates.js.map +1 -1
  205. package/dist/verification/__tests__/safe-spawn.test.js +12 -0
  206. package/dist/verification/__tests__/safe-spawn.test.js.map +1 -1
  207. package/package.json +2 -1
  208. package/skills/dare-ax/generator.ts +325 -0
  209. package/skills/dare-ax/index.ts +19 -0
  210. package/skills/dare-ax/metrics.ts +352 -0
  211. package/skills/dare-ax/package-lock.json +1855 -0
  212. package/skills/dare-ax/package.json +50 -0
  213. package/skills/dare-ax/secret-detector.ts +123 -0
  214. package/skills/dare-ax/skill.yml +19 -0
  215. package/skills/dare-ax/templates/llms.txt.jinja2 +80 -0
  216. package/skills/dare-ax/tests/generator.spec.ts +193 -0
  217. package/skills/dare-ax/tests/metrics.spec.ts +394 -0
  218. package/skills/dare-ax/tests/validator.spec.ts +298 -0
  219. package/skills/dare-ax/tsconfig.json +18 -0
  220. package/skills/dare-ax/types.ts +79 -0
  221. package/skills/dare-ax/validator.ts +238 -0
  222. package/skills/dare-frontend-design/generator.ts +616 -0
  223. package/skills/dare-frontend-design/index.ts +25 -0
  224. package/skills/dare-frontend-design/linter.ts +227 -0
  225. package/skills/dare-frontend-design/metrics.ts +82 -0
  226. package/skills/dare-frontend-design/package-lock.json +1855 -0
  227. package/skills/dare-frontend-design/package.json +43 -0
  228. package/skills/dare-frontend-design/skill.yml +20 -0
  229. package/skills/dare-frontend-design/tests/frontend_design.spec.ts +435 -0
  230. package/skills/dare-frontend-design/tsconfig.json +18 -0
  231. package/skills/dare-frontend-design/types.ts +62 -0
  232. package/skills/dare-layered-design/generator.ts +740 -0
  233. package/skills/dare-layered-design/index.ts +17 -0
  234. package/skills/dare-layered-design/linter.ts +462 -0
  235. package/skills/dare-layered-design/metrics.ts +409 -0
  236. package/skills/dare-layered-design/package-lock.json +1855 -0
  237. package/skills/dare-layered-design/package.json +50 -0
  238. package/skills/dare-layered-design/skill.yml +35 -0
  239. package/skills/dare-layered-design/tests/generator.spec.ts +156 -0
  240. package/skills/dare-layered-design/tests/linter.spec.ts +255 -0
  241. package/skills/dare-layered-design/tests/metrics.spec.ts +286 -0
  242. package/skills/dare-layered-design/tsconfig.json +18 -0
  243. package/skills/dare-layered-design/types.ts +48 -0
  244. package/skills/dare-llm-integration/cache/llm_cache.ts +122 -0
  245. package/skills/dare-llm-integration/index.ts +49 -0
  246. package/skills/dare-llm-integration/metrics.ts +107 -0
  247. package/skills/dare-llm-integration/package-lock.json +1855 -0
  248. package/skills/dare-llm-integration/package.json +49 -0
  249. package/skills/dare-llm-integration/prompts/prompt_loader.ts +258 -0
  250. package/skills/dare-llm-integration/providers/anthropic_provider.ts +159 -0
  251. package/skills/dare-llm-integration/providers/dummy_provider.ts +113 -0
  252. package/skills/dare-llm-integration/providers/llm_provider.ts +6 -0
  253. package/skills/dare-llm-integration/providers/openai_provider.ts +215 -0
  254. package/skills/dare-llm-integration/rate_limit/token_bucket.ts +86 -0
  255. package/skills/dare-llm-integration/skill.yml +23 -0
  256. package/skills/dare-llm-integration/tests/fixtures/greet_v1.jinja2 +1 -0
  257. package/skills/dare-llm-integration/tests/fixtures/summarize_v1.jinja2 +1 -0
  258. package/skills/dare-llm-integration/tests/fixtures/summarize_v2.jinja2 +3 -0
  259. package/skills/dare-llm-integration/tests/llm_integration.spec.ts +657 -0
  260. package/skills/dare-llm-integration/tsconfig.json +23 -0
  261. package/skills/dare-llm-integration/types.ts +91 -0
  262. package/skills/dare-llm-integration/validators/output_validator.ts +200 -0
  263. package/skills/dare-quality-telemetry/collect.ts +134 -0
  264. package/skills/dare-quality-telemetry/collectors/dare_ax_collector.ts +301 -0
  265. package/skills/dare-quality-telemetry/collectors/dare_layered_design_collector.ts +406 -0
  266. package/skills/dare-quality-telemetry/collectors/index.ts +24 -0
  267. package/skills/dare-quality-telemetry/github_actions_template.ts +25 -0
  268. package/skills/dare-quality-telemetry/index.ts +18 -0
  269. package/skills/dare-quality-telemetry/metrics.ts +137 -0
  270. package/skills/dare-quality-telemetry/package-lock.json +1855 -0
  271. package/skills/dare-quality-telemetry/package.json +48 -0
  272. package/skills/dare-quality-telemetry/regression.ts +60 -0
  273. package/skills/dare-quality-telemetry/reporter.ts +132 -0
  274. package/skills/dare-quality-telemetry/skill.yml +18 -0
  275. package/skills/dare-quality-telemetry/tests/quality_telemetry.spec.ts +885 -0
  276. package/skills/dare-quality-telemetry/tsconfig.json +19 -0
  277. package/skills/dare-quality-telemetry/types.ts +41 -0
  278. package/skills/dare-realtime/event_registry.ts +101 -0
  279. package/skills/dare-realtime/index.ts +30 -0
  280. package/skills/dare-realtime/metrics.ts +84 -0
  281. package/skills/dare-realtime/package-lock.json +1855 -0
  282. package/skills/dare-realtime/package.json +43 -0
  283. package/skills/dare-realtime/reconnect_strategy.ts +85 -0
  284. package/skills/dare-realtime/schema_validator.ts +80 -0
  285. package/skills/dare-realtime/skill.yml +21 -0
  286. package/skills/dare-realtime/subscription_manager.ts +106 -0
  287. package/skills/dare-realtime/tests/realtime.spec.ts +482 -0
  288. package/skills/dare-realtime/tsconfig.json +18 -0
  289. package/skills/dare-realtime/types.ts +51 -0
  290. package/templates/ide/antigravity/.agents/skills/dare-ai/SKILL.md +17 -0
  291. package/templates/ide/antigravity/.agents/skills/dare-blueprint/SKILL.md +2 -0
  292. package/templates/ide/antigravity/.agents/skills/dare-design/SKILL.md +2 -0
  293. package/templates/ide/antigravity/.agents/skills/dare-dna/SKILL.md +3 -0
  294. package/templates/ide/antigravity/.agents/skills/dare-migrate/SKILL.md +3 -0
  295. package/templates/ide/antigravity/.agents/skills/dare-patterns/SKILL.md +3 -0
  296. package/templates/ide/antigravity/.agents/skills/dare-refine/SKILL.md +3 -0
  297. package/templates/ide/antigravity/.agents/skills/dare-reverse/SKILL.md +3 -0
  298. package/templates/ide/antigravity/.agents/skills/dare-review/SKILL.md +3 -0
  299. package/templates/ide/claude/.claude/commands/dare-ai.md +17 -0
  300. package/templates/ide/claude/.claude/commands/dare-blueprint.md +2 -0
  301. package/templates/ide/claude/.claude/commands/dare-design.md +2 -0
  302. package/templates/ide/claude/.claude/commands/dare-dna.md +2 -0
  303. package/templates/ide/claude/.claude/commands/dare-migrate.md +2 -0
  304. package/templates/ide/claude/.claude/commands/dare-patterns.md +3 -0
  305. package/templates/ide/claude/.claude/commands/dare-refine.md +3 -0
  306. package/templates/ide/claude/.claude/commands/dare-reverse.md +2 -0
  307. package/templates/ide/claude/.claude/commands/dare-review.md +3 -0
  308. package/templates/ide/cursor/.cursor/commands/dare-ai.md +17 -0
  309. package/templates/ide/cursor/.cursor/commands/dare-blueprint.md +3 -0
  310. package/templates/ide/cursor/.cursor/commands/dare-design.md +3 -0
  311. package/templates/ide/cursor/.cursor/commands/dare-dna.md +2 -0
  312. package/templates/ide/cursor/.cursor/commands/dare-migrate.md +2 -0
  313. package/templates/ide/cursor/.cursor/commands/dare-patterns.md +3 -0
  314. package/templates/ide/cursor/.cursor/commands/dare-refine.md +3 -0
  315. package/templates/ide/cursor/.cursor/commands/dare-reverse.md +2 -0
  316. package/templates/ide/cursor/.cursor/commands/dare-review.md +3 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * dare-layered-design — LayeredDesignMetrics
3
+ * Collects M-01 to M-04 metrics for a project.
4
+ * License: MIT
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { MetricResult } from './types.js';
10
+ import { LayeredDesignLinter } from './linter.js';
11
+
12
+ export class LayeredDesignMetrics {
13
+ private readonly linter: LayeredDesignLinter;
14
+
15
+ constructor() {
16
+ this.linter = new LayeredDesignLinter();
17
+ }
18
+
19
+ /**
20
+ * Collects all four layered-design metrics.
21
+ */
22
+ collect(projectPath: string): MetricResult[] {
23
+ return [
24
+ this.collectM01(projectPath),
25
+ this.collectM02(projectPath),
26
+ this.collectM03(projectPath),
27
+ this.collectM04(projectPath),
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * M-01: Services have unit tests (test files exist for services).
33
+ * Checks that every service file has a corresponding test file.
34
+ */
35
+ collectM01(projectPath: string): MetricResult {
36
+ const serviceFiles = findServiceFiles(projectPath);
37
+
38
+ if (serviceFiles.length === 0) {
39
+ return {
40
+ id: 'M-01',
41
+ pass: false,
42
+ description: '100% of Services have unit tests',
43
+ detail: 'No service files found in services/ directories. Create services first.',
44
+ };
45
+ }
46
+
47
+ const servicesWithoutTests: string[] = [];
48
+
49
+ for (const serviceFile of serviceFiles) {
50
+ if (!hasTestFile(serviceFile, projectPath)) {
51
+ servicesWithoutTests.push(path.relative(projectPath, serviceFile));
52
+ }
53
+ }
54
+
55
+ if (servicesWithoutTests.length > 0) {
56
+ return {
57
+ id: 'M-01',
58
+ pass: false,
59
+ description: '100% of Services have unit tests',
60
+ detail:
61
+ `${servicesWithoutTests.length} service(s) have no corresponding test file: ` +
62
+ servicesWithoutTests.slice(0, 5).join(', ') +
63
+ (servicesWithoutTests.length > 5 ? ` ... (${servicesWithoutTests.length - 5} more)` : ''),
64
+ };
65
+ }
66
+
67
+ return {
68
+ id: 'M-01',
69
+ pass: true,
70
+ description: '100% of Services have unit tests',
71
+ detail: `All ${serviceFiles.length} service file(s) have corresponding test files.`,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * M-02: 0% Handler→Repository direct calls.
77
+ * Uses the LayeredDesignLinter to detect violations.
78
+ */
79
+ collectM02(projectPath: string): MetricResult {
80
+ const result = this.linter.lint(projectPath);
81
+
82
+ if (result.filesScanned === 0) {
83
+ return {
84
+ id: 'M-02',
85
+ pass: true,
86
+ description: '0% Handler→Repository direct calls',
87
+ detail: 'No handler files found to scan.',
88
+ };
89
+ }
90
+
91
+ if (result.violations.length > 0) {
92
+ const summary = result.violations
93
+ .slice(0, 3)
94
+ .map(
95
+ (v) =>
96
+ `${path.relative(projectPath, v.file)}:${v.line} — ${v.message}`
97
+ )
98
+ .join('; ');
99
+
100
+ return {
101
+ id: 'M-02',
102
+ pass: false,
103
+ description: '0% Handler→Repository direct calls',
104
+ detail:
105
+ `Found ${result.violations.length} violation(s) in ${result.filesScanned} handler files scanned. ` +
106
+ `First violations: ${summary}`,
107
+ };
108
+ }
109
+
110
+ return {
111
+ id: 'M-02',
112
+ pass: true,
113
+ description: '0% Handler→Repository direct calls',
114
+ detail: `No violations found across ${result.filesScanned} handler file(s) scanned.`,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * M-03: 100% of Handlers use dependency injection (no `new Service()` inside handlers).
120
+ * Checks handler files for direct service instantiation.
121
+ */
122
+ collectM03(projectPath: string): MetricResult {
123
+ const handlerDirs = findLayerDirs(projectPath, ['handlers', 'controllers']);
124
+
125
+ if (handlerDirs.length === 0) {
126
+ return {
127
+ id: 'M-03',
128
+ pass: true,
129
+ description: '100% of Handlers use dependency injection',
130
+ detail: 'No handler directories found.',
131
+ };
132
+ }
133
+
134
+ const violations: Array<{ file: string; line: number; content: string }> = [];
135
+
136
+ for (const dir of handlerDirs) {
137
+ const files = collectFilesRecursive(dir, ['.ts', '.js', '.rb', '.rs', '.py', '.go', '.php'], 4);
138
+ for (const file of files) {
139
+ findDIViolations(file, violations);
140
+ }
141
+ }
142
+
143
+ if (violations.length > 0) {
144
+ const summary = violations
145
+ .slice(0, 3)
146
+ .map((v) => `${path.relative(projectPath, v.file)}:${v.line}`)
147
+ .join(', ');
148
+ return {
149
+ id: 'M-03',
150
+ pass: false,
151
+ description: '100% of Handlers use dependency injection',
152
+ detail: `Found ${violations.length} handler(s) that instantiate services directly (e.g., new XService()). Violations: ${summary}. Use constructor injection instead.`,
153
+ };
154
+ }
155
+
156
+ const totalFiles = handlerDirs.reduce(
157
+ (sum, d) => sum + collectFilesRecursive(d, ['.ts', '.js', '.rb', '.rs', '.py', '.go'], 4).length,
158
+ 0
159
+ );
160
+
161
+ return {
162
+ id: 'M-03',
163
+ pass: true,
164
+ description: '100% of Handlers use dependency injection',
165
+ detail: `No direct service instantiation found in ${totalFiles} handler file(s).`,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * M-04: 100% of Repositories are agnostic to upper layers.
171
+ * Checks repository files for HTTP-specific code (status codes, response objects).
172
+ */
173
+ collectM04(projectPath: string): MetricResult {
174
+ const repoDirs = findLayerDirs(projectPath, ['repositories', 'repos', 'data_access']);
175
+
176
+ if (repoDirs.length === 0) {
177
+ return {
178
+ id: 'M-04',
179
+ pass: true,
180
+ description: '100% of Repositories are agnostic to upper layers',
181
+ detail: 'No repository directories found.',
182
+ };
183
+ }
184
+
185
+ const violations: Array<{ file: string; line: number; content: string }> = [];
186
+
187
+ for (const dir of repoDirs) {
188
+ const files = collectFilesRecursive(dir, ['.ts', '.js', '.rb', '.rs', '.py', '.go', '.php'], 4);
189
+ for (const file of files) {
190
+ findHTTPInRepoViolations(file, violations);
191
+ }
192
+ }
193
+
194
+ if (violations.length > 0) {
195
+ const summary = violations
196
+ .slice(0, 3)
197
+ .map((v) => `${path.relative(projectPath, v.file)}:${v.line} — "${v.content.slice(0, 60)}"`)
198
+ .join('; ');
199
+ return {
200
+ id: 'M-04',
201
+ pass: false,
202
+ description: '100% of Repositories are agnostic to upper layers',
203
+ detail: `Found ${violations.length} HTTP concern(s) in repository files. Repositories must not return HTTP status codes or DTOs. Violations: ${summary}`,
204
+ };
205
+ }
206
+
207
+ return {
208
+ id: 'M-04',
209
+ pass: true,
210
+ description: '100% of Repositories are agnostic to upper layers',
211
+ detail: 'No HTTP concerns found in repository files.',
212
+ };
213
+ }
214
+ }
215
+
216
+ // ── Helpers ──────────────────────────────────────────────────────────────────
217
+
218
+ const SKIP_DIRS = new Set(['node_modules', 'target', 'dist', '.git', 'vendor', '__pycache__', 'coverage']);
219
+
220
+ /** Patterns that indicate direct service instantiation in a handler (DI violation) */
221
+ const DI_VIOLATION_PATTERNS: RegExp[] = [
222
+ /new\s+\w+Service\s*\(/,
223
+ /new\s+\w+UseCase\s*\(/,
224
+ /new\s+Create\w+\s*\(/,
225
+ /new\s+Update\w+\s*\(/,
226
+ /new\s+Delete\w+\s*\(/,
227
+ /\w+Service\.new\b/, // Ruby style
228
+ /\w+UseCase\.new\b/, // Ruby style
229
+ ];
230
+
231
+ /** Patterns that indicate HTTP knowledge inside a repository (layer violation) */
232
+ const HTTP_IN_REPO_PATTERNS: RegExp[] = [
233
+ /status\s*[:=]\s*[45]\d\d/, // status: 404, status = 500
234
+ /http_status|httpStatus|HttpStatus/i,
235
+ /res\.status\s*\(/, // res.status(404)
236
+ /raise\s+.*NotFound.*Error.*HTTP/i, // raise HTTP::NotFoundError
237
+ /NotFoundException.*\(\s*[45]\d\d/, // NotFoundException(404)
238
+ /render\s+json.*status:/i, // Rails: render json: ..., status:
239
+ /Response\.\w+\(\s*[45]\d\d/, // Response.notFound(404)
240
+ /throw new HttpException/i, // NestJS
241
+ ];
242
+
243
+ function findServiceFiles(projectPath: string): string[] {
244
+ const dirs = findLayerDirs(projectPath, ['services', 'use_cases', 'interactors', 'commands']);
245
+ const results: string[] = [];
246
+ const extensions = ['.ts', '.js', '.rb', '.rs', '.py', '.go', '.php'];
247
+
248
+ for (const dir of dirs) {
249
+ collectFilesRecursive(dir, extensions, 4).forEach((f) => {
250
+ // Exclude test files
251
+ if (!isTestFile(f)) results.push(f);
252
+ });
253
+ }
254
+
255
+ return results;
256
+ }
257
+
258
+ function findLayerDirs(projectPath: string, layerNames: string[]): string[] {
259
+ const results: string[] = [];
260
+ const searchRoots = [projectPath, ...['src', 'app', 'lib'].map((r) => path.join(projectPath, r))];
261
+
262
+ for (const root of searchRoots) {
263
+ if (!fs.existsSync(root)) continue;
264
+ for (const name of layerNames) {
265
+ const candidate = path.join(root, name);
266
+ if (fs.existsSync(candidate)) results.push(candidate);
267
+ }
268
+ }
269
+
270
+ return results;
271
+ }
272
+
273
+ function collectFilesRecursive(dir: string, extensions: string[], maxDepth: number, depth = 0): string[] {
274
+ if (depth > maxDepth) return [];
275
+
276
+ let entries: fs.Dirent[];
277
+ try {
278
+ entries = fs.readdirSync(dir, { withFileTypes: true });
279
+ } catch {
280
+ return [];
281
+ }
282
+
283
+ const results: string[] = [];
284
+ for (const entry of entries) {
285
+ if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
286
+ const fp = path.join(dir, entry.name);
287
+ if (entry.isDirectory()) {
288
+ results.push(...collectFilesRecursive(fp, extensions, maxDepth, depth + 1));
289
+ } else if (entry.isFile() && extensions.includes(path.extname(entry.name))) {
290
+ results.push(fp);
291
+ }
292
+ }
293
+ return results;
294
+ }
295
+
296
+ function hasTestFile(serviceFile: string, projectPath: string): boolean {
297
+ const basename = path.basename(serviceFile, path.extname(serviceFile));
298
+ const ext = path.extname(serviceFile);
299
+
300
+ // Possible test file patterns
301
+ const testPatterns = [
302
+ `${basename}.spec${ext}`,
303
+ `${basename}.test${ext}`,
304
+ `${basename}_spec${ext}`,
305
+ `${basename}_test${ext}`,
306
+ `test_${basename}${ext}`,
307
+ ];
308
+
309
+ // Search in common test directories
310
+ const testRoots = [
311
+ path.join(projectPath, 'tests'),
312
+ path.join(projectPath, 'test'),
313
+ path.join(projectPath, 'spec'),
314
+ path.join(projectPath, '__tests__'),
315
+ path.join(projectPath, 'src', '__tests__'),
316
+ path.dirname(serviceFile), // Co-located tests
317
+ ];
318
+
319
+ for (const testRoot of testRoots) {
320
+ if (!fs.existsSync(testRoot)) continue;
321
+ for (const pattern of testPatterns) {
322
+ const candidate = path.join(testRoot, pattern);
323
+ if (fs.existsSync(candidate)) return true;
324
+ }
325
+ // Also search recursively for the test file
326
+ const found = findFileByName(testRoot, testPatterns, 4);
327
+ if (found) return true;
328
+ }
329
+
330
+ return false;
331
+ }
332
+
333
+ function findFileByName(dir: string, names: string[], maxDepth: number, depth = 0): string | null {
334
+ if (depth > maxDepth) return null;
335
+
336
+ let entries: fs.Dirent[];
337
+ try {
338
+ entries = fs.readdirSync(dir, { withFileTypes: true });
339
+ } catch {
340
+ return null;
341
+ }
342
+
343
+ for (const entry of entries) {
344
+ if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
345
+ const fp = path.join(dir, entry.name);
346
+ if (entry.isFile() && names.includes(entry.name)) return fp;
347
+ if (entry.isDirectory()) {
348
+ const found = findFileByName(fp, names, maxDepth, depth + 1);
349
+ if (found) return found;
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+
355
+ function isTestFile(file: string): boolean {
356
+ return /\.(spec|test)\.[a-z]+$/.test(file) || /_spec\.[a-z]+$/.test(file) || /_test\.[a-z]+$/.test(file);
357
+ }
358
+
359
+ function findDIViolations(
360
+ file: string,
361
+ out: Array<{ file: string; line: number; content: string }>
362
+ ): void {
363
+ let content: string;
364
+ try {
365
+ content = fs.readFileSync(file, 'utf-8');
366
+ } catch {
367
+ return;
368
+ }
369
+
370
+ const lines = content.split('\n');
371
+ for (let i = 0; i < lines.length; i++) {
372
+ const line = lines[i];
373
+ const trimmed = line.trim();
374
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) continue;
375
+
376
+ for (const pattern of DI_VIOLATION_PATTERNS) {
377
+ if (pattern.test(line)) {
378
+ out.push({ file, line: i + 1, content: trimmed });
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ function findHTTPInRepoViolations(
386
+ file: string,
387
+ out: Array<{ file: string; line: number; content: string }>
388
+ ): void {
389
+ let content: string;
390
+ try {
391
+ content = fs.readFileSync(file, 'utf-8');
392
+ } catch {
393
+ return;
394
+ }
395
+
396
+ const lines = content.split('\n');
397
+ for (let i = 0; i < lines.length; i++) {
398
+ const line = lines[i];
399
+ const trimmed = line.trim();
400
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) continue;
401
+
402
+ for (const pattern of HTTP_IN_REPO_PATTERNS) {
403
+ if (pattern.test(line)) {
404
+ out.push({ file, line: i + 1, content: trimmed });
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ }