forgecraft-mcp 1.4.0 → 1.7.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 (418) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +527 -525
  3. package/dist/analyzers/anchors/anchor-loader.d.ts +47 -0
  4. package/dist/analyzers/anchors/anchor-loader.d.ts.map +1 -0
  5. package/dist/analyzers/anchors/anchor-loader.js +113 -0
  6. package/dist/analyzers/anchors/anchor-loader.js.map +1 -0
  7. package/dist/analyzers/anti-pattern.d.ts.map +1 -1
  8. package/dist/analyzers/anti-pattern.js +38 -26
  9. package/dist/analyzers/anti-pattern.js.map +1 -1
  10. package/dist/analyzers/completeness-helpers.d.ts +5 -0
  11. package/dist/analyzers/completeness-helpers.d.ts.map +1 -1
  12. package/dist/analyzers/completeness-helpers.js +17 -0
  13. package/dist/analyzers/completeness-helpers.js.map +1 -1
  14. package/dist/analyzers/completeness.d.ts.map +1 -1
  15. package/dist/analyzers/completeness.js +4 -4
  16. package/dist/analyzers/completeness.js.map +1 -1
  17. package/dist/analyzers/gs-scorer.d.ts +3 -1
  18. package/dist/analyzers/gs-scorer.d.ts.map +1 -1
  19. package/dist/analyzers/gs-scorer.js +5 -2
  20. package/dist/analyzers/gs-scorer.js.map +1 -1
  21. package/dist/analyzers/package-json.d.ts.map +1 -1
  22. package/dist/analyzers/package-json.js +194 -34
  23. package/dist/analyzers/package-json.js.map +1 -1
  24. package/dist/analyzers/scorers/composable-scorer.d.ts +4 -2
  25. package/dist/analyzers/scorers/composable-scorer.d.ts.map +1 -1
  26. package/dist/analyzers/scorers/composable-scorer.js +50 -2
  27. package/dist/analyzers/scorers/composable-scorer.js.map +1 -1
  28. package/dist/analyzers/scorers/executable-scorer.d.ts +3 -2
  29. package/dist/analyzers/scorers/executable-scorer.d.ts.map +1 -1
  30. package/dist/analyzers/scorers/executable-scorer.js +64 -4
  31. package/dist/analyzers/scorers/executable-scorer.js.map +1 -1
  32. package/dist/analyzers/scorers/scorer-utils.d.ts +5 -3
  33. package/dist/analyzers/scorers/scorer-utils.d.ts.map +1 -1
  34. package/dist/analyzers/scorers/scorer-utils.js +34 -9
  35. package/dist/analyzers/scorers/scorer-utils.js.map +1 -1
  36. package/dist/analyzers/scorers/self-describing-scorer.d.ts +7 -4
  37. package/dist/analyzers/scorers/self-describing-scorer.d.ts.map +1 -1
  38. package/dist/analyzers/scorers/self-describing-scorer.js +17 -18
  39. package/dist/analyzers/scorers/self-describing-scorer.js.map +1 -1
  40. package/dist/cli/help.js +51 -51
  41. package/dist/disciplines/catalog.d.ts +16 -0
  42. package/dist/disciplines/catalog.d.ts.map +1 -0
  43. package/dist/disciplines/catalog.js +196 -0
  44. package/dist/disciplines/catalog.js.map +1 -0
  45. package/dist/disciplines/runner.d.ts +13 -0
  46. package/dist/disciplines/runner.d.ts.map +1 -0
  47. package/dist/disciplines/runner.js +35 -0
  48. package/dist/disciplines/runner.js.map +1 -0
  49. package/dist/registry/composer.d.ts.map +1 -1
  50. package/dist/registry/composer.js +9 -4
  51. package/dist/registry/composer.js.map +1 -1
  52. package/dist/registry/loader-tag.d.ts.map +1 -1
  53. package/dist/registry/loader-tag.js +1 -0
  54. package/dist/registry/loader-tag.js.map +1 -1
  55. package/dist/registry/remote-gates.js +1 -1
  56. package/dist/registry/remote-gates.js.map +1 -1
  57. package/dist/registry/renderer-skeletons.js +92 -92
  58. package/dist/registry/sentinel-renderer.js +299 -20
  59. package/dist/registry/sentinel-renderer.js.map +1 -1
  60. package/dist/sentinel/detect.d.ts +41 -0
  61. package/dist/sentinel/detect.d.ts.map +1 -0
  62. package/dist/sentinel/detect.js +122 -0
  63. package/dist/sentinel/detect.js.map +1 -0
  64. package/dist/sentinel/write.d.ts +54 -0
  65. package/dist/sentinel/write.d.ts.map +1 -0
  66. package/dist/sentinel/write.js +75 -0
  67. package/dist/sentinel/write.js.map +1 -0
  68. package/dist/shared/cnt-health.d.ts +16 -0
  69. package/dist/shared/cnt-health.d.ts.map +1 -1
  70. package/dist/shared/cnt-health.js +55 -8
  71. package/dist/shared/cnt-health.js.map +1 -1
  72. package/dist/shared/config.d.ts +14 -0
  73. package/dist/shared/config.d.ts.map +1 -1
  74. package/dist/shared/config.js +45 -0
  75. package/dist/shared/config.js.map +1 -1
  76. package/dist/shared/gs-score-logger.js +6 -6
  77. package/dist/shared/hook-installer.d.ts +58 -0
  78. package/dist/shared/hook-installer.d.ts.map +1 -0
  79. package/dist/shared/hook-installer.js +316 -0
  80. package/dist/shared/hook-installer.js.map +1 -0
  81. package/dist/shared/project-gates-helpers.d.ts +9 -0
  82. package/dist/shared/project-gates-helpers.d.ts.map +1 -1
  83. package/dist/shared/project-gates-helpers.js +35 -0
  84. package/dist/shared/project-gates-helpers.js.map +1 -1
  85. package/dist/shared/types/config.d.ts +7 -1
  86. package/dist/shared/types/config.d.ts.map +1 -1
  87. package/dist/shared/types/gates.d.ts +34 -0
  88. package/dist/shared/types/gates.d.ts.map +1 -1
  89. package/dist/shared/types/project.d.ts +68 -2
  90. package/dist/shared/types/project.d.ts.map +1 -1
  91. package/dist/shared/types/project.js +1 -0
  92. package/dist/shared/types/project.js.map +1 -1
  93. package/dist/shared/types/templates.d.ts +8 -1
  94. package/dist/shared/types/templates.d.ts.map +1 -1
  95. package/dist/shared/types/verify.d.ts +51 -1
  96. package/dist/shared/types/verify.d.ts.map +1 -1
  97. package/dist/shared/types/verify.js +37 -1
  98. package/dist/shared/types/verify.js.map +1 -1
  99. package/dist/tools/add-hook.d.ts.map +1 -1
  100. package/dist/tools/add-hook.js +8 -1
  101. package/dist/tools/add-hook.js.map +1 -1
  102. package/dist/tools/add-module.js +123 -123
  103. package/dist/tools/advice-registry.d.ts.map +1 -1
  104. package/dist/tools/advice-registry.js +108 -18
  105. package/dist/tools/advice-registry.js.map +1 -1
  106. package/dist/tools/advise-session-advisor.d.ts +16 -0
  107. package/dist/tools/advise-session-advisor.d.ts.map +1 -0
  108. package/dist/tools/advise-session-advisor.js +89 -0
  109. package/dist/tools/advise-session-advisor.js.map +1 -0
  110. package/dist/tools/advise-session-signals.d.ts +21 -0
  111. package/dist/tools/advise-session-signals.d.ts.map +1 -0
  112. package/dist/tools/advise-session-signals.js +113 -0
  113. package/dist/tools/advise-session-signals.js.map +1 -0
  114. package/dist/tools/advise-session.d.ts +22 -0
  115. package/dist/tools/advise-session.d.ts.map +1 -0
  116. package/dist/tools/advise-session.js +31 -0
  117. package/dist/tools/advise-session.js.map +1 -0
  118. package/dist/tools/analyze-harness.d.ts +18 -0
  119. package/dist/tools/analyze-harness.d.ts.map +1 -0
  120. package/dist/tools/analyze-harness.js +298 -0
  121. package/dist/tools/analyze-harness.js.map +1 -0
  122. package/dist/tools/audit.d.ts.map +1 -1
  123. package/dist/tools/audit.js +19 -0
  124. package/dist/tools/audit.js.map +1 -1
  125. package/dist/tools/change-request.d.ts +53 -0
  126. package/dist/tools/change-request.d.ts.map +1 -0
  127. package/dist/tools/change-request.js +395 -0
  128. package/dist/tools/change-request.js.map +1 -0
  129. package/dist/tools/check-cascade-contracts.d.ts +13 -0
  130. package/dist/tools/check-cascade-contracts.d.ts.map +1 -1
  131. package/dist/tools/check-cascade-contracts.js +73 -2
  132. package/dist/tools/check-cascade-contracts.js.map +1 -1
  133. package/dist/tools/check-cascade-report.js +64 -64
  134. package/dist/tools/check-cascade-steps.d.ts +3 -0
  135. package/dist/tools/check-cascade-steps.d.ts.map +1 -1
  136. package/dist/tools/check-cascade-steps.js +104 -15
  137. package/dist/tools/check-cascade-steps.js.map +1 -1
  138. package/dist/tools/check-cascade.d.ts +4 -3
  139. package/dist/tools/check-cascade.d.ts.map +1 -1
  140. package/dist/tools/check-cascade.js +30 -12
  141. package/dist/tools/check-cascade.js.map +1 -1
  142. package/dist/tools/check-derivation-chain.d.ts +37 -0
  143. package/dist/tools/check-derivation-chain.d.ts.map +1 -0
  144. package/dist/tools/check-derivation-chain.js +418 -0
  145. package/dist/tools/check-derivation-chain.js.map +1 -0
  146. package/dist/tools/check-spec-consistency.d.ts +25 -0
  147. package/dist/tools/check-spec-consistency.d.ts.map +1 -0
  148. package/dist/tools/check-spec-consistency.js +339 -0
  149. package/dist/tools/check-spec-consistency.js.map +1 -0
  150. package/dist/tools/check-t4.d.ts +54 -0
  151. package/dist/tools/check-t4.d.ts.map +1 -0
  152. package/dist/tools/check-t4.js +305 -0
  153. package/dist/tools/check-t4.js.map +1 -0
  154. package/dist/tools/close-cycle.d.ts +11 -0
  155. package/dist/tools/close-cycle.d.ts.map +1 -1
  156. package/dist/tools/close-cycle.js +364 -4
  157. package/dist/tools/close-cycle.js.map +1 -1
  158. package/dist/tools/cnt-add-routing.d.ts +31 -0
  159. package/dist/tools/cnt-add-routing.d.ts.map +1 -0
  160. package/dist/tools/cnt-add-routing.js +99 -0
  161. package/dist/tools/cnt-add-routing.js.map +1 -0
  162. package/dist/tools/configure-mcp.d.ts.map +1 -1
  163. package/dist/tools/configure-mcp.js +52 -2
  164. package/dist/tools/configure-mcp.js.map +1 -1
  165. package/dist/tools/consolidate-status.d.ts +31 -0
  166. package/dist/tools/consolidate-status.d.ts.map +1 -1
  167. package/dist/tools/consolidate-status.js +105 -0
  168. package/dist/tools/consolidate-status.js.map +1 -1
  169. package/dist/tools/executable-gates.d.ts +52 -0
  170. package/dist/tools/executable-gates.d.ts.map +1 -0
  171. package/dist/tools/executable-gates.js +333 -0
  172. package/dist/tools/executable-gates.js.map +1 -0
  173. package/dist/tools/extract-adrs-from-spec.d.ts +33 -0
  174. package/dist/tools/extract-adrs-from-spec.d.ts.map +1 -0
  175. package/dist/tools/extract-adrs-from-spec.js +410 -0
  176. package/dist/tools/extract-adrs-from-spec.js.map +1 -0
  177. package/dist/tools/extract-adrs-history.d.ts +47 -0
  178. package/dist/tools/extract-adrs-history.d.ts.map +1 -0
  179. package/dist/tools/extract-adrs-history.js +265 -0
  180. package/dist/tools/extract-adrs-history.js.map +1 -0
  181. package/dist/tools/forgecraft-dispatch-extended.d.ts.map +1 -1
  182. package/dist/tools/forgecraft-dispatch-extended.js +137 -0
  183. package/dist/tools/forgecraft-dispatch-extended.js.map +1 -1
  184. package/dist/tools/forgecraft-dispatch.d.ts.map +1 -1
  185. package/dist/tools/forgecraft-dispatch.js +16 -0
  186. package/dist/tools/forgecraft-dispatch.js.map +1 -1
  187. package/dist/tools/forgecraft-schema-params.d.ts +174 -2
  188. package/dist/tools/forgecraft-schema-params.d.ts.map +1 -1
  189. package/dist/tools/forgecraft-schema-params.js +197 -0
  190. package/dist/tools/forgecraft-schema-params.js.map +1 -1
  191. package/dist/tools/forgecraft-schema.d.ts +179 -7
  192. package/dist/tools/forgecraft-schema.d.ts.map +1 -1
  193. package/dist/tools/forgecraft-schema.js +37 -0
  194. package/dist/tools/forgecraft-schema.js.map +1 -1
  195. package/dist/tools/generate-adr.js +6 -6
  196. package/dist/tools/generate-adr.js.map +1 -1
  197. package/dist/tools/generate-decision.d.ts +77 -0
  198. package/dist/tools/generate-decision.d.ts.map +1 -0
  199. package/dist/tools/generate-decision.js +162 -0
  200. package/dist/tools/generate-decision.js.map +1 -0
  201. package/dist/tools/generate-env-probe.d.ts +49 -0
  202. package/dist/tools/generate-env-probe.d.ts.map +1 -0
  203. package/dist/tools/generate-env-probe.js +365 -0
  204. package/dist/tools/generate-env-probe.js.map +1 -0
  205. package/dist/tools/generate-harness.d.ts +53 -0
  206. package/dist/tools/generate-harness.d.ts.map +1 -0
  207. package/dist/tools/generate-harness.js +395 -0
  208. package/dist/tools/generate-harness.js.map +1 -0
  209. package/dist/tools/generate-roadmap.d.ts +1 -1
  210. package/dist/tools/generate-roadmap.d.ts.map +1 -1
  211. package/dist/tools/generate-roadmap.js +38 -4
  212. package/dist/tools/generate-roadmap.js.map +1 -1
  213. package/dist/tools/generate-session-prompt.d.ts +3 -3
  214. package/dist/tools/generate-session-prompt.d.ts.map +1 -1
  215. package/dist/tools/generate-session-prompt.js +9 -1
  216. package/dist/tools/generate-session-prompt.js.map +1 -1
  217. package/dist/tools/generate-slo-probe.d.ts +53 -0
  218. package/dist/tools/generate-slo-probe.d.ts.map +1 -0
  219. package/dist/tools/generate-slo-probe.js +366 -0
  220. package/dist/tools/generate-slo-probe.js.map +1 -0
  221. package/dist/tools/layer-status-gates.d.ts +24 -0
  222. package/dist/tools/layer-status-gates.d.ts.map +1 -0
  223. package/dist/tools/layer-status-gates.js +151 -0
  224. package/dist/tools/layer-status-gates.js.map +1 -0
  225. package/dist/tools/layer-status.d.ts +126 -0
  226. package/dist/tools/layer-status.d.ts.map +1 -0
  227. package/dist/tools/layer-status.js +647 -0
  228. package/dist/tools/layer-status.js.map +1 -0
  229. package/dist/tools/list.d.ts.map +1 -1
  230. package/dist/tools/list.js +9 -5
  231. package/dist/tools/list.js.map +1 -1
  232. package/dist/tools/postcondition-coverage.d.ts +57 -0
  233. package/dist/tools/postcondition-coverage.d.ts.map +1 -0
  234. package/dist/tools/postcondition-coverage.js +256 -0
  235. package/dist/tools/postcondition-coverage.js.map +1 -0
  236. package/dist/tools/probe-runners.d.ts +21 -0
  237. package/dist/tools/probe-runners.d.ts.map +1 -0
  238. package/dist/tools/probe-runners.js +246 -0
  239. package/dist/tools/probe-runners.js.map +1 -0
  240. package/dist/tools/probe-templates.d.ts +27 -0
  241. package/dist/tools/probe-templates.d.ts.map +1 -0
  242. package/dist/tools/probe-templates.js +279 -0
  243. package/dist/tools/probe-templates.js.map +1 -0
  244. package/dist/tools/propose-session.d.ts +28 -0
  245. package/dist/tools/propose-session.d.ts.map +1 -0
  246. package/dist/tools/propose-session.js +333 -0
  247. package/dist/tools/propose-session.js.map +1 -0
  248. package/dist/tools/refresh-output.js +14 -14
  249. package/dist/tools/review-stubs.d.ts +29 -0
  250. package/dist/tools/review-stubs.d.ts.map +1 -0
  251. package/dist/tools/review-stubs.js +173 -0
  252. package/dist/tools/review-stubs.js.map +1 -0
  253. package/dist/tools/roadmap-builder.d.ts +49 -1
  254. package/dist/tools/roadmap-builder.d.ts.map +1 -1
  255. package/dist/tools/roadmap-builder.js +210 -5
  256. package/dist/tools/roadmap-builder.js.map +1 -1
  257. package/dist/tools/run-env-probe.d.ts +57 -0
  258. package/dist/tools/run-env-probe.d.ts.map +1 -0
  259. package/dist/tools/run-env-probe.js +270 -0
  260. package/dist/tools/run-env-probe.js.map +1 -0
  261. package/dist/tools/run-harness.d.ts +52 -0
  262. package/dist/tools/run-harness.d.ts.map +1 -0
  263. package/dist/tools/run-harness.js +279 -0
  264. package/dist/tools/run-harness.js.map +1 -0
  265. package/dist/tools/run-slo-probe.d.ts +50 -0
  266. package/dist/tools/run-slo-probe.d.ts.map +1 -0
  267. package/dist/tools/run-slo-probe.js +281 -0
  268. package/dist/tools/run-slo-probe.js.map +1 -0
  269. package/dist/tools/scaffold-spec-stubs.js +115 -115
  270. package/dist/tools/scaffold-templates.js +62 -62
  271. package/dist/tools/scaffold-writer.d.ts.map +1 -1
  272. package/dist/tools/scaffold-writer.js +9 -0
  273. package/dist/tools/scaffold-writer.js.map +1 -1
  274. package/dist/tools/score-rubric.d.ts +19 -0
  275. package/dist/tools/score-rubric.d.ts.map +1 -0
  276. package/dist/tools/score-rubric.js +411 -0
  277. package/dist/tools/score-rubric.js.map +1 -0
  278. package/dist/tools/session-prompt-builders.d.ts +20 -0
  279. package/dist/tools/session-prompt-builders.d.ts.map +1 -1
  280. package/dist/tools/session-prompt-builders.js +78 -5
  281. package/dist/tools/session-prompt-builders.js.map +1 -1
  282. package/dist/tools/session-prompt-sections.d.ts +4 -2
  283. package/dist/tools/session-prompt-sections.d.ts.map +1 -1
  284. package/dist/tools/session-prompt-sections.js +22 -10
  285. package/dist/tools/session-prompt-sections.js.map +1 -1
  286. package/dist/tools/setup-artifact-writers.d.ts +69 -4
  287. package/dist/tools/setup-artifact-writers.d.ts.map +1 -1
  288. package/dist/tools/setup-artifact-writers.js +681 -5
  289. package/dist/tools/setup-artifact-writers.js.map +1 -1
  290. package/dist/tools/setup-cnt-builders.d.ts.map +1 -1
  291. package/dist/tools/setup-cnt-builders.js +162 -34
  292. package/dist/tools/setup-cnt-builders.js.map +1 -1
  293. package/dist/tools/setup-monitoring.d.ts +41 -0
  294. package/dist/tools/setup-monitoring.d.ts.map +1 -0
  295. package/dist/tools/setup-monitoring.js +364 -0
  296. package/dist/tools/setup-monitoring.js.map +1 -0
  297. package/dist/tools/setup-phase1.d.ts.map +1 -1
  298. package/dist/tools/setup-phase1.js +14 -1
  299. package/dist/tools/setup-phase1.js.map +1 -1
  300. package/dist/tools/setup-phase2.d.ts +14 -0
  301. package/dist/tools/setup-phase2.d.ts.map +1 -1
  302. package/dist/tools/setup-phase2.js +130 -3
  303. package/dist/tools/setup-phase2.js.map +1 -1
  304. package/dist/tools/setup-project.d.ts +8 -0
  305. package/dist/tools/setup-project.d.ts.map +1 -1
  306. package/dist/tools/setup-project.js +52 -2
  307. package/dist/tools/setup-project.js.map +1 -1
  308. package/dist/tools/spec-parser-tags.d.ts.map +1 -1
  309. package/dist/tools/spec-parser-tags.js +1 -0
  310. package/dist/tools/spec-parser-tags.js.map +1 -1
  311. package/dist/tools/verify-formatter.d.ts.map +1 -1
  312. package/dist/tools/verify-formatter.js +15 -1
  313. package/dist/tools/verify-formatter.js.map +1 -1
  314. package/dist/tools/verify.d.ts.map +1 -1
  315. package/dist/tools/verify.js +3 -0
  316. package/dist/tools/verify.js.map +1 -1
  317. package/package.json +98 -89
  318. package/templates/analytics/instructions.yaml +37 -37
  319. package/templates/analytics/mcp-servers.yaml +11 -11
  320. package/templates/analytics/structure.yaml +25 -25
  321. package/templates/api/harness/uc-template.hurl +20 -0
  322. package/templates/api/instructions.yaml +231 -231
  323. package/templates/api/mcp-servers.yaml +22 -22
  324. package/templates/api/nfr.yaml +23 -23
  325. package/templates/api/review.yaml +103 -103
  326. package/templates/api/structure.yaml +34 -34
  327. package/templates/api/verification.yaml +132 -132
  328. package/templates/cli/instructions.yaml +31 -31
  329. package/templates/cli/mcp-servers.yaml +11 -11
  330. package/templates/cli/review.yaml +53 -53
  331. package/templates/cli/structure.yaml +16 -16
  332. package/templates/data-lineage/instructions.yaml +28 -28
  333. package/templates/data-lineage/mcp-servers.yaml +22 -22
  334. package/templates/data-pipeline/instructions.yaml +84 -84
  335. package/templates/data-pipeline/mcp-servers.yaml +13 -13
  336. package/templates/data-pipeline/nfr.yaml +39 -39
  337. package/templates/data-pipeline/structure.yaml +23 -23
  338. package/templates/docs-manifest.yaml +227 -0
  339. package/templates/fintech/hooks.yaml +55 -55
  340. package/templates/fintech/instructions.yaml +112 -112
  341. package/templates/fintech/mcp-servers.yaml +13 -13
  342. package/templates/fintech/nfr.yaml +46 -46
  343. package/templates/fintech/playbook.yaml +210 -210
  344. package/templates/fintech/verification.yaml +239 -239
  345. package/templates/game/harness/uc-template.sim.ts +29 -0
  346. package/templates/game/instructions.yaml +289 -289
  347. package/templates/game/mcp-servers.yaml +38 -38
  348. package/templates/game/nfr.yaml +64 -64
  349. package/templates/game/playbook.yaml +214 -214
  350. package/templates/game/review.yaml +97 -97
  351. package/templates/game/structure.yaml +67 -67
  352. package/templates/game/verification.yaml +174 -174
  353. package/templates/healthcare/instructions.yaml +42 -42
  354. package/templates/healthcare/mcp-servers.yaml +13 -13
  355. package/templates/healthcare/nfr.yaml +47 -47
  356. package/templates/hipaa/instructions.yaml +41 -41
  357. package/templates/hipaa/mcp-servers.yaml +13 -13
  358. package/templates/infra/instructions.yaml +104 -104
  359. package/templates/infra/mcp-servers.yaml +20 -20
  360. package/templates/infra/nfr.yaml +46 -46
  361. package/templates/infra/review.yaml +65 -65
  362. package/templates/infra/structure.yaml +25 -25
  363. package/templates/library/instructions.yaml +36 -36
  364. package/templates/library/mcp-servers.yaml +20 -20
  365. package/templates/library/review.yaml +56 -56
  366. package/templates/library/structure.yaml +19 -19
  367. package/templates/medallion-architecture/instructions.yaml +41 -41
  368. package/templates/medallion-architecture/mcp-servers.yaml +22 -22
  369. package/templates/ml/instructions.yaml +85 -85
  370. package/templates/ml/mcp-servers.yaml +11 -11
  371. package/templates/ml/nfr.yaml +39 -39
  372. package/templates/ml/structure.yaml +25 -25
  373. package/templates/ml/verification.yaml +156 -156
  374. package/templates/mobile/instructions.yaml +44 -44
  375. package/templates/mobile/mcp-servers.yaml +11 -11
  376. package/templates/mobile/nfr.yaml +49 -49
  377. package/templates/mobile/structure.yaml +27 -27
  378. package/templates/mobile/verification.yaml +121 -121
  379. package/templates/observability-xray/instructions.yaml +40 -40
  380. package/templates/observability-xray/mcp-servers.yaml +15 -15
  381. package/templates/realtime/instructions.yaml +42 -42
  382. package/templates/realtime/mcp-servers.yaml +13 -13
  383. package/templates/soc2/instructions.yaml +41 -41
  384. package/templates/soc2/mcp-servers.yaml +24 -24
  385. package/templates/social/instructions.yaml +43 -43
  386. package/templates/social/mcp-servers.yaml +24 -24
  387. package/templates/state-machine/instructions.yaml +42 -42
  388. package/templates/state-machine/mcp-servers.yaml +11 -11
  389. package/templates/tools-registry.yaml +164 -164
  390. package/templates/universal/claude-md-blocks/layer-navigation.md +20 -0
  391. package/templates/universal/claude-md-blocks/nfr-contracts.md +22 -0
  392. package/templates/universal/hooks.yaml +879 -723
  393. package/templates/universal/instructions.yaml +1692 -1692
  394. package/templates/universal/mcp-servers.yaml +50 -50
  395. package/templates/universal/nfr.yaml +197 -197
  396. package/templates/universal/reference.yaml +326 -326
  397. package/templates/universal/review.yaml +204 -204
  398. package/templates/universal/skills.yaml +262 -262
  399. package/templates/universal/structure.yaml +67 -67
  400. package/templates/universal/verification.yaml +416 -416
  401. package/templates/web-next/hooks.yaml +114 -0
  402. package/templates/web-next/instructions.yaml +106 -0
  403. package/templates/web-react/harness/uc-template.spec.ts +35 -0
  404. package/templates/web-react/hooks.yaml +156 -44
  405. package/templates/web-react/instructions.yaml +296 -207
  406. package/templates/web-react/mcp-servers.yaml +20 -20
  407. package/templates/web-react/nfr.yaml +27 -27
  408. package/templates/web-react/review.yaml +94 -94
  409. package/templates/web-react/structure.yaml +46 -46
  410. package/templates/web-react/verification.yaml +126 -126
  411. package/templates/web-static/hooks.yaml +85 -0
  412. package/templates/web-static/instructions.yaml +204 -115
  413. package/templates/web-static/mcp-servers.yaml +20 -20
  414. package/templates/web3/instructions.yaml +44 -44
  415. package/templates/web3/mcp-servers.yaml +11 -11
  416. package/templates/web3/verification.yaml +159 -159
  417. package/templates/zero-trust/instructions.yaml +41 -41
  418. package/templates/zero-trust/mcp-servers.yaml +15 -15
@@ -1,723 +1,879 @@
1
- tag: UNIVERSAL
2
- section: hooks
3
- hooks:
4
- - name: commit-msg
5
- trigger: commit-msg
6
- description: "Enforce conventional commit format: <type>(<scope>): <description>"
7
- filename: commit-msg.sh
8
- script: |
9
- #!/usr/bin/env bash
10
- # commit-msg: enforce conventional commit format
11
- # ForgeCraft — generated hook
12
-
13
- COMMIT_MSG_FILE="$1"
14
- COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
15
-
16
- # Skip merge commits and fixup commits
17
- if echo "$COMMIT_MSG" | grep -qE "^(Merge|Revert|fixup!|squash!)"; then
18
- exit 0
19
- fi
20
-
21
- # Skip empty messages and comments-only
22
- STRIPPED=$(echo "$COMMIT_MSG" | sed '/^#/d' | sed '/^$/d')
23
- if [ -z "$STRIPPED" ]; then
24
- exit 0
25
- fi
26
-
27
- PATTERN="^(feat|fix|refactor|docs|test|chore|perf|ci|build|revert)(\([a-z0-9/_-]+\))?(!)?: .{1,72}"
28
-
29
- if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
30
- echo ""
31
- echo " ✗ Commit message does not follow conventional commit format."
32
- echo ""
33
- echo " Required format: <type>(<scope>): <description>"
34
- echo " Types: feat | fix | refactor | docs | test | chore | perf | ci | build | revert"
35
- echo " Examples:"
36
- echo " feat(auth): add JWT refresh token support"
37
- echo " fix(api): handle null response from payment gateway"
38
- echo " docs: update README with setup instructions"
39
- echo ""
40
- echo " Your message: $COMMIT_MSG"
41
- echo ""
42
- exit 1
43
- fi
44
-
45
- exit 0
46
-
47
- - name: branch-protection
48
- trigger: pre-commit
49
- description: "Block direct commits to main/master branches"
50
- filename: pre-commit-branch-check.sh
51
- script: |
52
- #!/bin/bash
53
- BRANCH=$(git rev-parse --abbrev-ref HEAD)
54
- if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
55
- echo "❌ Direct commits to $BRANCH are blocked. Create a feature branch."
56
- exit 1
57
- fi
58
-
59
- - name: dangerous-commands
60
- trigger: pre-exec
61
- description: "Block destructive commands (rm -rf /, DROP DATABASE, force push)"
62
- filename: pre-exec-safety.sh
63
- script: |
64
- #!/bin/bash
65
- DANGEROUS_PATTERNS=(
66
- "rm -rf /"
67
- "DROP DATABASE"
68
- "DROP TABLE"
69
- "TRUNCATE"
70
- "force push"
71
- "git push.*--force"
72
- "kubectl delete namespace"
73
- )
74
- for pattern in "${DANGEROUS_PATTERNS[@]}"; do
75
- if echo "$1" | grep -iqE "$pattern"; then
76
- echo "❌ Blocked dangerous command matching: $pattern"
77
- exit 1
78
- fi
79
- done
80
-
81
- - name: auto-format
82
- trigger: pre-commit
83
- description: "Run language-appropriate formatter on staged files"
84
- filename: pre-commit-format.sh
85
- script: |
86
- #!/bin/bash
87
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
88
- # Python
89
- echo "$STAGED" | grep '\.py$' | xargs -r black --quiet 2>/dev/null
90
- echo "$STAGED" | grep '\.py$' | xargs -r isort --quiet 2>/dev/null
91
- # TypeScript/JavaScript
92
- echo "$STAGED" | grep '\.\(ts\|tsx\|js\|jsx\)$' | xargs -r npx prettier --write 2>/dev/null
93
- # Rust
94
- if echo "$STAGED" | grep -q '\.rs$' && [ -f "Cargo.toml" ]; then
95
- cargo fmt 2>/dev/null
96
- fi
97
- # Re-stage formatted files
98
- echo "$STAGED" | xargs -r git add
99
-
100
- - name: secrets-scanner
101
- trigger: pre-commit
102
- description: "Scan for accidentally committed secrets (AWS keys, API keys, passwords)"
103
- filename: pre-commit-secrets.sh
104
- script: |
105
- #!/bin/bash
106
- _fc_write_violation() {
107
- local hook_name="$1" severity="${2:-error}" message="$3"
108
- local repo_root
109
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
110
- local dir="$repo_root/.forgecraft"
111
- mkdir -p "$dir" 2>/dev/null || return 0
112
- local ts
113
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
114
- local esc_msg
115
- esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
116
- printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
117
- "$hook_name" "$severity" "$esc_msg" "$ts" \
118
- >> "$dir/gate-violations.jsonl" 2>/dev/null || true
119
- }
120
- PATTERNS=(
121
- 'AKIA[0-9A-Z]{16}'
122
- 'password\s*=\s*["\x27][^"\x27]+'
123
- 'BEGIN RSA PRIVATE KEY'
124
- 'sk-[a-zA-Z0-9]{48}'
125
- 'ghp_[a-zA-Z0-9]{36}'
126
- )
127
- STAGED=$(git diff --cached --name-only)
128
- for file in $STAGED; do
129
- for pattern in "${PATTERNS[@]}"; do
130
- if grep -qE "$pattern" "$file" 2>/dev/null; then
131
- echo " Potential secret found in $file matching pattern"
132
- _fc_write_violation "pre-commit-secrets" "error" "Potential secrets detected in staged files — review output above"
133
- exit 1
134
- fi
135
- done
136
- done
137
-
138
- - name: compile-check
139
- trigger: pre-commit
140
- description: "Detect project type and run appropriate compile/build check"
141
- filename: pre-commit-compile.sh
142
- script: |
143
- #!/bin/bash
144
- _fc_write_violation() {
145
- local hook_name="$1" severity="${2:-error}" message="$3"
146
- local repo_root
147
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
148
- local dir="$repo_root/.forgecraft"
149
- mkdir -p "$dir" 2>/dev/null || return 0
150
- local ts
151
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
152
- local esc_msg
153
- esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
154
- printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
155
- "$hook_name" "$severity" "$esc_msg" "$ts" \
156
- >> "$dir/gate-violations.jsonl" 2>/dev/null || true
157
- }
158
- echo "🔨 Running build check..."
159
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
160
- STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
161
- if [ -n "$STAGED_PY" ]; then
162
- for file in $STAGED_PY; do
163
- python -m py_compile "$file" 2>&1
164
- if [ $? -ne 0 ]; then
165
- echo "❌ Syntax error in $file"
166
- _fc_write_violation "pre-commit-compile" "error" "TypeScript compilation failed — run tsc --noEmit to see errors"
167
- exit 1
168
- fi
169
- done
170
- echo " ✅ Python syntax OK"
171
- fi
172
- fi
173
- if [ -f "tsconfig.json" ]; then
174
- npx tsc --noEmit 2>&1
175
- if [ $? -ne 0 ]; then
176
- echo "❌ TypeScript compilation failed."
177
- _fc_write_violation "pre-commit-compile" "error" "TypeScript compilation failed — run tsc --noEmit to see errors"
178
- exit 1
179
- fi
180
- echo " ✅ TypeScript compilation OK"
181
- fi
182
- if [ -f "Cargo.toml" ]; then
183
- STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rs$')
184
- if [ -n "$STAGED_RS" ]; then
185
- cargo check --quiet 2>&1
186
- if [ $? -ne 0 ]; then
187
- echo "❌ Rust cargo check failed."
188
- _fc_write_violation "pre-commit-compile" "error" "Rust cargo check failed — run cargo check to see errors"
189
- exit 1
190
- fi
191
- echo " Rust cargo check OK"
192
- fi
193
- fi
194
- echo "🔨 Build check passed"
195
-
196
- - name: cargo-clippy
197
- trigger: pre-commit
198
- description: "Run cargo clippy on Rust projects — catches bugs, style issues, and common mistakes beyond cargo check"
199
- filename: pre-commit-clippy.sh
200
- script: |
201
- #!/bin/bash
202
- _fc_write_violation() {
203
- local hook_name="$1" severity="${2:-error}" message="$3"
204
- local repo_root
205
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
206
- local dir="$repo_root/.forgecraft"
207
- mkdir -p "$dir" 2>/dev/null || return 0
208
- local ts
209
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
210
- local esc_msg
211
- esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
212
- printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
213
- "$hook_name" "$severity" "$esc_msg" "$ts" \
214
- >> "$dir/gate-violations.jsonl" 2>/dev/null || true
215
- }
216
- if [ ! -f "Cargo.toml" ]; then
217
- exit 0
218
- fi
219
- STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rs$')
220
- if [ -z "$STAGED_RS" ]; then
221
- exit 0
222
- fi
223
- echo "🦀 Running cargo clippy..."
224
- cargo clippy --all-targets --all-features 2>&1
225
- if [ $? -ne 0 ]; then
226
- echo "❌ cargo clippy failed — fix lint errors before committing."
227
- echo " Run: cargo clippy --all-targets --all-features"
228
- _fc_write_violation "pre-commit-clippy" "error" "Cargo clippy violations found — run cargo clippy to see details"
229
- exit 1
230
- fi
231
- echo " cargo clippy passed"
232
-
233
- - name: tdd-phase-gate
234
- trigger: pre-commit
235
- description: "RED gate: test-only commits must have failing tests; warn on implementation without tests"
236
- filename: pre-commit-tdd-check.sh
237
- script: |
238
- #!/bin/bash
239
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
240
- if [ -z "$STAGED" ]; then exit 0; fi
241
- SRC_PATTERNS='^(src|lib|app|server|client|pkg|internal|cmd)/'
242
- TEST_PATTERNS='^(tests?|spec|__tests__)/'
243
- TEST_FILE_PATTERNS='\.(test|spec)\.(ts|tsx|js|jsx|mjs|py|go|rb|java|kt)$'
244
- SRC_FILES=()
245
- TEST_FILES=()
246
- while IFS= read -r file; do
247
- if echo "$file" | grep -qE "$SRC_PATTERNS"; then
248
- SRC_FILES+=("$file")
249
- elif echo "$file" | grep -qE "$TEST_PATTERNS"; then
250
- TEST_FILES+=("$file")
251
- elif echo "$file" | grep -qE "$TEST_FILE_PATTERNS"; then
252
- TEST_FILES+=("$file")
253
- fi
254
- done <<< "$STAGED"
255
- SRC_COUNT=${#SRC_FILES[@]}
256
- TEST_COUNT=${#TEST_FILES[@]}
257
- # RED gate: test-only commit must have at least one failing test
258
- if [ "$TEST_COUNT" -gt 0 ] && [ "$SRC_COUNT" -eq 0 ]; then
259
- echo "🔴 TDD gate: test-only commit — running staged tests..."
260
- RUN_CMD=""
261
- if [ -f "package.json" ]; then
262
- if grep -q '"vitest"' package.json 2>/dev/null; then
263
- RUN_CMD="npx vitest run"
264
- elif grep -q '"jest"' package.json 2>/dev/null; then
265
- RUN_CMD="npx jest --passWithNoTests"
266
- fi
267
- elif [ -f "Cargo.toml" ]; then
268
- RUN_CMD="cargo test"
269
- elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
270
- RUN_CMD="python -m pytest"
271
- fi
272
- if [ -n "$RUN_CMD" ]; then
273
- $RUN_CMD "${TEST_FILES[@]}" > /tmp/tdd-check-output.txt 2>&1
274
- if [ $? -eq 0 ]; then
275
- echo "❌ TDD RED gate violation: all staged tests PASS"
276
- echo " A test-only commit must contain at least one failing test."
277
- echo " Either tests were written after implementation, or assertions are vacuous."
278
- cat /tmp/tdd-check-output.txt | head -30
279
- exit 1
280
- fi
281
- echo " ✅ RED gate satisfied — staged tests fail as expected"
282
- else
283
- echo " ⚠️ Cannot detect test runner — skipping RED gate check"
284
- fi
285
- fi
286
- # Source gate: warn on implementation without tests
287
- if [ "$SRC_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
288
- echo "⚠️ TDD warning: implementation committed without test changes"
289
- echo " Verify a preceding test(scope): [RED] commit exists in this branch."
290
- fi
291
- exit 0
292
-
293
- - name: test-coverage
294
- trigger: pre-commit
295
- description: "Run tests for test-only commits; skip for docs/config-only commits; defer to coverage-gate when src/ staged"
296
- filename: pre-commit-test.sh
297
- script: |
298
- #!/bin/bash
299
- COVERAGE_MIN={{coverage_minimum | default: 80}}
300
-
301
- # Collect staged files once.
302
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
303
-
304
- # Determine what kinds of files are staged.
305
- SRC_STAGED=0
306
- CODE_STAGED=0
307
- while IFS= read -r file; do
308
- if echo "$file" | grep -qE '^src/'; then
309
- SRC_STAGED=1
310
- CODE_STAGED=1
311
- elif echo "$file" | grep -qE '^tests?/'; then
312
- CODE_STAGED=1
313
- fi
314
- done <<< "$STAGED"
315
-
316
- # If src/ is staged the coverage-gate hook runs the full test + coverage pass.
317
- # Skip the bare run here to avoid running the full suite twice.
318
- if [ "$SRC_STAGED" -eq 1 ]; then
319
- echo "🧪 src/ files staged — tests will run via coverage gate, skipping bare run."
320
- exit 0
321
- fi
322
-
323
- # If no code files at all are staged (docs-only, config-only, etc.) skip the run.
324
- if [ "$CODE_STAGED" -eq 0 ]; then
325
- echo "🧪 No code files staged skipping test run."
326
- exit 0
327
- fi
328
-
329
- echo "🧪 Running tests..."
330
- if [ -f "package.json" ]; then
331
- if grep -q '"vitest"' package.json 2>/dev/null; then
332
- npx vitest run --reporter=verbose 2>&1
333
- if [ $? -ne 0 ]; then
334
- echo "❌ Tests failed."
335
- exit 1
336
- fi
337
- echo " ✅ Tests passed"
338
- elif grep -q '"jest"' package.json 2>/dev/null; then
339
- npx jest --passWithNoTests --coverage \
340
- --coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN}}" \
341
- --silent 2>&1
342
- if [ $? -ne 0 ]; then
343
- echo "❌ Jest tests failed or coverage below ${COVERAGE_MIN}%."
344
- exit 1
345
- fi
346
- echo " ✅ Jest tests passed"
347
- fi
348
- fi
349
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
350
- if command -v pytest &> /dev/null; then
351
- pytest --tb=short --quiet --cov=src --cov-fail-under=$COVERAGE_MIN 2>&1
352
- if [ $? -ne 0 ]; then
353
- echo "❌ Tests failed or coverage below ${COVERAGE_MIN}%."
354
- exit 1
355
- fi
356
- echo " ✅ Python tests passed"
357
- fi
358
- fi
359
- if [ -f "Cargo.toml" ]; then
360
- cargo test --quiet 2>&1
361
- if [ $? -ne 0 ]; then
362
- echo " Rust tests failed."
363
- exit 1
364
- fi
365
- echo " ✅ Rust tests passed"
366
- fi
367
- echo "🧪 All tests passed"
368
-
369
- - name: coverage-gate
370
- trigger: pre-commit
371
- description: "Enforce minimum coverage thresholds when src/ files are staged"
372
- filename: pre-commit-coverage.sh
373
- script: |
374
- #!/bin/bash
375
- _fc_write_violation() {
376
- local hook_name="$1" severity="${2:-error}" message="$3"
377
- local repo_root
378
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
379
- local dir="$repo_root/.forgecraft"
380
- mkdir -p "$dir" 2>/dev/null || return 0
381
- local ts
382
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
383
- local esc_msg
384
- esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
385
- printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
386
- "$hook_name" "$severity" "$esc_msg" "$ts" \
387
- >> "$dir/gate-violations.jsonl" 2>/dev/null || true
388
- }
389
- # ──────────────────────────────────────────────────────────────────────
390
- # Pre-Commit Hook: Coverage Gate
391
- #
392
- # Enforces minimum coverage thresholds before a commit lands.
393
- # Thresholds are read from vitest.config.ts (via --coverage).
394
- # Only runs when files under src/ are staged — skips for
395
- # docs-only, config-only, or test-only commits.
396
- #
397
- # Current thresholds (vitest.config.ts):
398
- # Lines / Statements / Functions: {{coverage_minimum | default: 80}}%
399
- # Branches: 70%
400
- #
401
- # Trigger: git pre-commit (via scripts/setup-hooks.sh)
402
- # Exit: 1 blocks commit, 0 allows
403
- # ──────────────────────────────────────────────────────────────────────
404
-
405
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
406
-
407
- if [ -z "$STAGED" ]; then
408
- exit 0
409
- fi
410
-
411
- # ── Check if any src/ file is staged ─────────────────────────────────
412
- SRC_STAGED=0
413
- while IFS= read -r file; do
414
- if echo "$file" | grep -qE '^src/'; then
415
- SRC_STAGED=1
416
- break
417
- fi
418
- done <<< "$STAGED"
419
-
420
- if [ "$SRC_STAGED" -eq 0 ]; then
421
- echo "📊 Coverage gate: no src/ files staged, skipping."
422
- exit 0
423
- fi
424
-
425
- # ── Run coverage ───────────────────────────────────────────────────────
426
- echo "📊 Running coverage gate (src/ files staged)..."
427
-
428
- if [ ! -f "package.json" ] && [ ! -f "pyproject.toml" ] && [ ! -f "setup.py" ]; then
429
- if [ -f "Cargo.toml" ]; then
430
- # Rust: run tests; use cargo-tarpaulin for coverage if available
431
- if command -v cargo-tarpaulin &> /dev/null; then
432
- cargo tarpaulin --out Stdout --fail-under {{coverage_minimum | default: 80}} 2>&1
433
- if [ $? -ne 0 ]; then
434
- echo "❌ Coverage gate failed — below {{coverage_minimum | default: 80}}%."
435
- echo " Run: cargo tarpaulin --out Html"
436
- _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed run npm test --coverage to see report"
437
- exit 1
438
- fi
439
- echo " ✅ Rust coverage gate passed"
440
- else
441
- cargo test --quiet 2>&1
442
- if [ $? -ne 0 ]; then
443
- echo "❌ Rust tests failed."
444
- _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
445
- exit 1
446
- fi
447
- echo " ✅ Rust tests passed (install cargo-tarpaulin for coverage enforcement)"
448
- fi
449
- else
450
- echo " ⚠️ No supported build system found — skipping coverage check."
451
- fi
452
- exit 0
453
- fi
454
-
455
- if grep -q '"vitest"' package.json 2>/dev/null; then
456
- OUTPUT=$(npx vitest run --coverage --reporter=verbose 2>&1)
457
- EXIT_CODE=$?
458
-
459
- if [ $EXIT_CODE -ne 0 ]; then
460
- echo "$OUTPUT" | grep -E "ERROR|Coverage|does not meet|%|FAIL|passed|failed" | head -40
461
- echo ""
462
- echo "❌ Coverage gate failed — thresholds not met."
463
- echo " Run 'npx vitest run --coverage' locally to see the full report."
464
- echo " Add tests until coverage meets the configured minimums."
465
- _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
466
- exit 1
467
- fi
468
- echo " ✅ Coverage gate passed"
469
- exit 0
470
- fi
471
-
472
- if grep -q '"jest"' package.json 2>/dev/null; then
473
- COVERAGE_MIN={{coverage_minimum | default: 80}}
474
- npx jest --passWithNoTests --coverage \
475
- --coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN,\"statements\":$COVERAGE_MIN,\"functions\":$COVERAGE_MIN,\"branches\":70}}" \
476
- --silent 2>&1
477
- if [ $? -ne 0 ]; then
478
- echo " Coverage gate failed — thresholds not met."
479
- _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
480
- exit 1
481
- fi
482
- echo " ✅ Coverage gate passed"
483
- exit 0
484
- fi
485
-
486
- if [ -f "pyproject.toml" ]|| [ -f "setup.py" ]; then
487
- if command -v pytest &> /dev/null; then
488
- pytest --tb=no --quiet --cov=src --cov-fail-under={{coverage_minimum | default: 80}} 2>&1
489
- if [ $? -ne 0 ]; then
490
- echo " Coverage gate failed below {{coverage_minimum | default: 80}}%."
491
- _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
492
- exit 1
493
- fi
494
- echo " ✅ Coverage gate passed"
495
- fi
496
- fi
497
-
498
- exit 0
499
-
500
- - name: anti-pattern-detector
501
- trigger: pre-commit
502
- description: "Scan source files for production code anti-patterns. Respects .forgecraft/exceptions.json for recorded false positives."
503
- filename: pre-commit-prod-quality.sh
504
- checks:
505
- - Hardcoded URLs/hosts (localhost, 127.0.0.1)
506
- - Mock/stub data in production code
507
- - Direct DB calls in route/controller layer (layer violation)
508
- - Bare Error throws (custom hierarchy recommended)
509
- - .env vs .env.example drift (warns on missing variables)
510
- script: |
511
- #!/bin/bash
512
- _fc_write_violation() {
513
- local hook_name="$1" severity="${2:-error}" message="$3"
514
- local repo_root
515
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
516
- local dir="$repo_root/.forgecraft"
517
- mkdir -p "$dir" 2>/dev/null || return 0
518
- local ts
519
- ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
520
- local esc_msg
521
- esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
522
- printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
523
- "$hook_name" "$severity" "$esc_msg" "$ts" \
524
- >> "$dir/gate-violations.jsonl" 2>/dev/null || true
525
- }
526
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
527
- SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(py|ts|tsx|js|jsx|rs)$' | grep -vE '(test_|\.test\.|\.spec\.|__tests__|tests/|fixtures/|mock|conftest|_test\.rs)')
528
- if [ -z "$SOURCE_FILES" ]; then exit 0; fi
529
- VIOLATIONS=0
530
- WARNINGS=0
531
- # Check if a file is covered by a hook exception in .forgecraft/exceptions.json
532
- # Usage: is_excepted "layer-boundary" "src/migrations/001.ts"
533
- # Add entries to .forgecraft/exceptions.json to record known false positives.
534
- is_excepted() {
535
- local hook_name="$1"
536
- local file_path="$2"
537
- if [ ! -f ".forgecraft/exceptions.json" ]; then return 1; fi
538
- node -e "
539
- const fs = require('fs');
540
- const data = JSON.parse(fs.readFileSync('.forgecraft/exceptions.json', 'utf-8'));
541
- const exc = (data.exceptions || []).find(e => {
542
- if (e.hook !== '$hook_name') return false;
543
- const pat = e.pattern.replace(/\\/g, '/').replace(/\./g, '\\\\.').replace(/\*\*/g, '<<<D>>>').replace(/\*/g, '[^/]*').replace(/<<<D>>>/g, '.*');
544
- return new RegExp('^' + pat + '$').test('$file_path'.replace(/\\\\/g, '/'));
545
- });
546
- if (exc) { console.log('EXCEPTED: ' + exc.reason); process.exit(0); }
547
- process.exit(1);
548
- " 2>/dev/null
549
- }
550
- echo "🔍 Scanning for production code anti-patterns..."
551
- for file in $SOURCE_FILES; do
552
- if echo "$file" | grep -vqE '(config|settings|\.env)'; then
553
- if grep -nE '(localhost|127\.0\.0\.1|0\.0\.0\.0)' "$file" | grep -vE '(#|//|""")' > /tmp/violations 2>/dev/null; then
554
- if [ -s /tmp/violations ]; then
555
- echo " ❌ $file — hardcoded URL/host"
556
- VIOLATIONS=$((VIOLATIONS + 1))
557
- fi
558
- fi
559
- fi
560
- if ! is_excepted "anti-pattern/mock-data" "$file"; then
561
- if grep -nEi '\b(mock_data|fake_data|dummy_data|stub_response)' "$file" > /tmp/violations 2>/dev/null; then
562
- if [ -s /tmp/violations ]; then
563
- echo " ❌ $file — mock/stub data in production code"
564
- VIOLATIONS=$((VIOLATIONS + 1))
565
- fi
566
- fi
567
- fi
568
- # Layer boundary: no direct DB/ORM imports from route handlers / controllers
569
- if echo "$file" | grep -qE '(routes|controllers|handlers|endpoints)'; then
570
- if ! is_excepted "layer-boundary" "$file"; then
571
- if grep -nE '\b(prisma\.|knex\(|mongoose\.|sequelize\.|db\.query|pool\.query)' "$file" > /tmp/violations 2>/dev/null; then
572
- if [ -s /tmp/violations ]; then
573
- echo " ❌ $file — direct DB call in route/controller (layer violation)"
574
- VIOLATIONS=$((VIOLATIONS + 1))
575
- fi
576
- fi
577
- fi
578
- fi
579
- # Bare Error throws in business logic (not test files)
580
- if ! is_excepted "error-hierarchy" "$file"; then
581
- if grep -nE 'throw new Error\(' "$file" > /tmp/violations 2>/dev/null; then
582
- if [ -s /tmp/violations ]; then
583
- echo " ⚠️ $file bare 'throw new Error()' found — use custom error hierarchy"
584
- WARNINGS=$((WARNINGS + 1))
585
- fi
586
- fi
587
- fi
588
- LINE_COUNT=$(wc -l < "$file")
589
- if [ "$LINE_COUNT" -gt {{max_file_length | default: 300}} ]; then
590
- echo " ⚠️ $file — $LINE_COUNT lines (max {{max_file_length | default: 300}})"
591
- WARNINGS=$((WARNINGS + 1))
592
- fi
593
- # Rust-specific anti-patterns
594
- if echo "$file" | grep -q '\.rs$'; then
595
- if ! is_excepted "rust/unwrap" "$file"; then
596
- if grep -nE '\.unwrap\(\)' "$file" > /tmp/violations 2>/dev/null; then
597
- if [ -s /tmp/violations ]; then
598
- echo " ⚠️ $file — .unwrap() in production code — use ? or explicit error handling"
599
- WARNINGS=$((WARNINGS + 1))
600
- fi
601
- fi
602
- fi
603
- if grep -nE '\btodo!\(|\bunimplemented!\(' "$file" > /tmp/violations 2>/dev/null; then
604
- if [ -s /tmp/violations ]; then
605
- echo " ❌ $file todo!/unimplemented! in production code"
606
- VIOLATIONS=$((VIOLATIONS + 1))
607
- fi
608
- fi
609
- if grep -nE '^[[:space:]]*#\[allow\(dead_code\)\]' "$file" > /tmp/violations 2>/dev/null; then
610
- if [ -s /tmp/violations ]; then
611
- echo " ⚠️ $file #[allow(dead_code)] suppression delete orphaned code instead"
612
- WARNINGS=$((WARNINGS + 1))
613
- fi
614
- fi
615
- if grep -nE '^[[:space:]]*unsafe[[:space:]]*\{' "$file" > /tmp/violations 2>/dev/null; then
616
- if [ -s /tmp/violations ]; then
617
- echo " ⚠️ $file — unsafe block present — requires explicit justification comment"
618
- WARNINGS=$((WARNINGS + 1))
619
- fi
620
- fi
621
- fi
622
- rm -f /tmp/violations
623
- if [ $VIOLATIONS -gt 0 ]; then
624
- echo "❌ $VIOLATIONS violation(s) found commit blocked."
625
- _fc_write_violation "pre-commit-anti-patterns" "error" "Anti-patterns detected in staged files — see output above"
626
- exit 1
627
- fi
628
- if [ $WARNINGS -gt 0 ]; then
629
- echo "⚠️ $WARNINGS warning(s) found — review recommended."
630
- fi
631
- echo "🔍 Production quality scan passed"
632
-
633
- - name: function-length
634
- trigger: pre-commit
635
- description: "Warn when staged functions exceed the configured maximum line count"
636
- filename: pre-commit-function-length.sh
637
- script: |
638
- #!/bin/bash
639
- MAX_LENGTH={{max_function_length | default: 50}}
640
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
641
- SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(ts|tsx|js|jsx|rs)$' | grep -vE '(\.test\.|\.spec\.|__tests__|tests/|_test\.rs)')
642
- if [ -z "$SOURCE_FILES" ]; then exit 0; fi
643
- WARNINGS=0
644
- for file in $SOURCE_FILES; do
645
- # Heuristic: find function/method declarations and count lines to next declaration
646
- if echo "$file" | grep -q '\.rs$'; then
647
- awk -v max="$MAX_LENGTH" -v fname="$file" '
648
- /^[[:space:]]*(pub )?(async )?fn [a-zA-Z_]/ {
649
- if (start > 0 && NR - start > max) {
650
- printf " ⚠️ %s:%d — fn starting here is %d lines (max %d)\n", fname, start, NR - start, max
651
- }
652
- start = NR
653
- }
654
- END {
655
- if (start > 0 && NR - start > max) {
656
- printf " ⚠️ %s:%d — fn starting here is %d lines (max %d)\n", fname, start, NR - start, max
657
- }
658
- }
659
- ' "$file"
660
- else
661
- awk -v max="$MAX_LENGTH" -v fname="$file" '
662
- /^[[:space:]]*(export )?(async )?(function |const [a-zA-Z]+ = (async )?\(|[a-zA-Z]+\(.*\) \{|[a-zA-Z]+\(.*\): )/ {
663
- if (start > 0 && NR - start > max) {
664
- printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
665
- warnings++
666
- }
667
- start = NR
668
- }
669
- END {
670
- if (start > 0 && NR - start > max) {
671
- printf " ⚠️ %s:%d function starting here is %d lines (max %d)\n", fname, start, NR - start, max
672
- warnings++
673
- }
674
- }
675
- ' "$file"
676
- fi
677
- WARNINGS=$((WARNINGS + $?))
678
- done
679
- # Warning only — does not block commit since bash heuristics aren't perfect
680
- exit 0
681
-
682
- - name: import-cycle-detector
683
- trigger: pre-commit
684
- description: "Detect circular import dependencies in TypeScript and Python projects"
685
- filename: pre-commit-import-cycles.sh
686
- script: |
687
- #!/bin/bash
688
- echo "🔄 Checking for circular imports..."
689
- if [ -f "tsconfig.json" ]; then
690
- if command -v npx &> /dev/null; then
691
- RESULT=$(npx --yes madge --circular --extensions ts src/ 2>&1)
692
- if echo "$RESULT" | grep -q "Found.*circular"; then
693
- echo " Circular imports detected in TypeScript:"
694
- echo "$RESULT"
695
- exit 1
696
- fi
697
- echo " ✅ No circular imports (TypeScript)"
698
- fi
699
- fi
700
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
701
- if command -v lint-imports &> /dev/null; then
702
- lint-imports 2>&1
703
- if [ $? -ne 0 ]; then
704
- echo " Circular imports detected in Python"
705
- exit 1
706
- fi
707
- echo " No circular imports (Python)"
708
- fi
709
- fi
710
- echo "🔄 Import cycle check passed"
711
-
712
- - name: code-review
713
- trigger: pre-commit
714
- description: "AI assistant reviews diff against project standards"
715
- filename: pre-commit-review.sh
716
- script: |
717
- #!/bin/bash
718
- DIFF=$(git diff --cached)
719
- if [ -z "$DIFF" ]; then exit 0; fi
720
- echo "📝 Staged changes ready for review"
721
- # Full auto-review requires claude CLI integration
722
- # This hook validates the diff is non-empty and staged
723
- exit 0
1
+ tag: UNIVERSAL
2
+ section: hooks
3
+ hooks:
4
+ - name: commit-msg
5
+ trigger: commit-msg
6
+ description: "Enforce conventional commit format: <type>(<scope>): <description>"
7
+ filename: commit-msg.sh
8
+ script: |
9
+ #!/usr/bin/env bash
10
+ # commit-msg: enforce conventional commit format
11
+ # ForgeCraft — generated hook
12
+
13
+ COMMIT_MSG_FILE="$1"
14
+ COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
15
+
16
+ # Skip merge commits and fixup commits
17
+ if echo "$COMMIT_MSG" | grep -qE "^(Merge|Revert|fixup!|squash!)"; then
18
+ exit 0
19
+ fi
20
+
21
+ # Skip empty messages and comments-only
22
+ STRIPPED=$(echo "$COMMIT_MSG" | sed '/^#/d' | sed '/^$/d')
23
+ if [ -z "$STRIPPED" ]; then
24
+ exit 0
25
+ fi
26
+
27
+ PATTERN="^(feat|fix|refactor|docs|test|chore|perf|ci|build|revert)(\([a-z0-9/_-]+\))?(!)?: .{1,72}"
28
+
29
+ if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
30
+ echo ""
31
+ echo " ✗ Commit message does not follow conventional commit format."
32
+ echo ""
33
+ echo " Required format: <type>(<scope>): <description>"
34
+ echo " Types: feat | fix | refactor | docs | test | chore | perf | ci | build | revert"
35
+ echo " Examples:"
36
+ echo " feat(auth): add JWT refresh token support"
37
+ echo " fix(api): handle null response from payment gateway"
38
+ echo " docs: update README with setup instructions"
39
+ echo ""
40
+ echo " Your message: $COMMIT_MSG"
41
+ echo ""
42
+ exit 1
43
+ fi
44
+
45
+ # ── TDD [RED]/[GREEN] phase enforcement ────────────────────────────────
46
+ SUBJECT=$(echo "$COMMIT_MSG" | head -1)
47
+ BODY=$(echo "$COMMIT_MSG" | tail -n +3)
48
+ TYPE=$(echo "$SUBJECT" | sed -E 's/^([a-z]+).*/\1/')
49
+ SCOPE=$(echo "$SUBJECT" | sed -nE 's/^[a-z]+\(([^)]+)\).*/\1/p')
50
+
51
+ # If subject contains [GREEN] and type is feat|fix, verify preceding [RED] commit
52
+ if echo "$SUBJECT" | grep -q '\[GREEN\]'; then
53
+ if echo "$TYPE" | grep -qE '^(feat|fix)$'; then
54
+ # Allow explicit tdd-skip annotation in body
55
+ if echo "$BODY" | grep -q '\[tdd-skip:'; then
56
+ echo " ⚠️ TDD skip recorded: $(echo "$BODY" | grep '\[tdd-skip:' | head -1)"
57
+ else
58
+ # Look for a test(scope): [RED] commit in recent history (7 days)
59
+ SEARCH_SCOPE="${SCOPE:-}"
60
+ if [ -n "$SEARCH_SCOPE" ]; then
61
+ RED_FOUND=$(git log --since="7 days ago" --pretty="%s" 2>/dev/null | grep -E "^test\(${SEARCH_SCOPE}\):.*\[RED\]" | head -1)
62
+ else
63
+ RED_FOUND=$(git log --since="7 days ago" --pretty="%s" 2>/dev/null | grep -E "^test[:(].*\[RED\]" | head -1)
64
+ fi
65
+ if [ -z "$RED_FOUND" ]; then
66
+ echo ""
67
+ echo " TDD [GREEN] gate: no preceding test(${SCOPE:-*}): [RED] commit found in the last 7 days."
68
+ echo ""
69
+ echo " A [GREEN] commit must follow a [RED] test commit for the same scope."
70
+ echo " Expected: test(${SCOPE:-scope}): [RED] <description>"
71
+ echo ""
72
+ echo " To bypass (document the reason): add [tdd-skip: reason] in the commit body."
73
+ echo " Example body line: [tdd-skip: brownfield module without test harness]"
74
+ echo ""
75
+ exit 1
76
+ fi
77
+ echo " ✅ TDD gate: found preceding RED commit — $RED_FOUND"
78
+ fi
79
+ fi
80
+ fi
81
+
82
+ exit 0
83
+
84
+ - name: branch-protection
85
+ trigger: pre-commit
86
+ description: "Block direct commits to main/master branches"
87
+ filename: pre-commit-branch-check.sh
88
+ script: |
89
+ #!/bin/bash
90
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
91
+ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
92
+ echo " Direct commits to $BRANCH are blocked. Create a feature branch."
93
+ exit 1
94
+ fi
95
+
96
+ - name: dangerous-commands
97
+ trigger: pre-exec
98
+ description: "Block destructive commands (rm -rf /, DROP DATABASE, force push)"
99
+ filename: pre-exec-safety.sh
100
+ script: |
101
+ #!/bin/bash
102
+ DANGEROUS_PATTERNS=(
103
+ "rm -rf /"
104
+ "DROP DATABASE"
105
+ "DROP TABLE"
106
+ "TRUNCATE"
107
+ "force push"
108
+ "git push.*--force"
109
+ "kubectl delete namespace"
110
+ )
111
+ for pattern in "${DANGEROUS_PATTERNS[@]}"; do
112
+ if echo "$1" | grep -iqE "$pattern"; then
113
+ echo " Blocked dangerous command matching: $pattern"
114
+ exit 1
115
+ fi
116
+ done
117
+
118
+ - name: auto-format
119
+ trigger: pre-commit
120
+ description: "Run language-appropriate formatter on staged files"
121
+ filename: pre-commit-format.sh
122
+ script: |
123
+ #!/bin/bash
124
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
125
+ # Python
126
+ echo "$STAGED" | grep '\.py$' | xargs -r black --quiet 2>/dev/null
127
+ echo "$STAGED" | grep '\.py$' | xargs -r isort --quiet 2>/dev/null
128
+ # TypeScript/JavaScript
129
+ echo "$STAGED" | grep '\.\(ts\|tsx\|js\|jsx\)$' | xargs -r npx prettier --write 2>/dev/null
130
+ # Rust
131
+ if echo "$STAGED" | grep -q '\.rs$' && [ -f "Cargo.toml" ]; then
132
+ cargo fmt 2>/dev/null
133
+ fi
134
+ # Re-stage formatted files
135
+ echo "$STAGED" | xargs -r git add
136
+
137
+ - name: secrets-scanner
138
+ trigger: pre-commit
139
+ description: "Scan for accidentally committed secrets (AWS keys, API keys, passwords)"
140
+ filename: pre-commit-secrets.sh
141
+ script: |
142
+ #!/bin/bash
143
+ _fc_write_violation() {
144
+ local hook_name="$1" severity="${2:-error}" message="$3"
145
+ local repo_root
146
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
147
+ local dir="$repo_root/.forgecraft"
148
+ mkdir -p "$dir" 2>/dev/null || return 0
149
+ local ts
150
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
151
+ local esc_msg
152
+ esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
153
+ printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
154
+ "$hook_name" "$severity" "$esc_msg" "$ts" \
155
+ >> "$dir/gate-violations.jsonl" 2>/dev/null || true
156
+ }
157
+ PATTERNS=(
158
+ 'AKIA[0-9A-Z]{16}'
159
+ 'password\s*=\s*["\x27][^"\x27]+'
160
+ 'BEGIN RSA PRIVATE KEY'
161
+ 'sk-[a-zA-Z0-9]{48}'
162
+ 'ghp_[a-zA-Z0-9]{36}'
163
+ )
164
+ STAGED=$(git diff --cached --name-only)
165
+ for file in $STAGED; do
166
+ for pattern in "${PATTERNS[@]}"; do
167
+ if grep -qE "$pattern" "$file" 2>/dev/null; then
168
+ echo "❌ Potential secret found in $file matching pattern"
169
+ _fc_write_violation "pre-commit-secrets" "error" "Potential secrets detected in staged files — review output above"
170
+ exit 1
171
+ fi
172
+ done
173
+ done
174
+
175
+ - name: compile-check
176
+ trigger: pre-commit
177
+ description: "Detect project type and run appropriate compile/build check"
178
+ filename: pre-commit-compile.sh
179
+ script: |
180
+ #!/bin/bash
181
+ _fc_write_violation() {
182
+ local hook_name="$1" severity="${2:-error}" message="$3"
183
+ local repo_root
184
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
185
+ local dir="$repo_root/.forgecraft"
186
+ mkdir -p "$dir" 2>/dev/null || return 0
187
+ local ts
188
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
189
+ local esc_msg
190
+ esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
191
+ printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
192
+ "$hook_name" "$severity" "$esc_msg" "$ts" \
193
+ >> "$dir/gate-violations.jsonl" 2>/dev/null || true
194
+ }
195
+ echo "🔨 Running build check..."
196
+ if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
197
+ STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
198
+ if [ -n "$STAGED_PY" ]; then
199
+ for file in $STAGED_PY; do
200
+ python -m py_compile "$file" 2>&1
201
+ if [ $? -ne 0 ]; then
202
+ echo "❌ Syntax error in $file"
203
+ _fc_write_violation "pre-commit-compile" "error" "TypeScript compilation failed — run tsc --noEmit to see errors"
204
+ exit 1
205
+ fi
206
+ done
207
+ echo " Python syntax OK"
208
+ fi
209
+ fi
210
+ if [ -f "tsconfig.json" ]; then
211
+ npx tsc --noEmit 2>&1
212
+ if [ $? -ne 0 ]; then
213
+ echo " TypeScript compilation failed."
214
+ _fc_write_violation "pre-commit-compile" "error" "TypeScript compilation failed — run tsc --noEmit to see errors"
215
+ exit 1
216
+ fi
217
+ echo " ✅ TypeScript compilation OK"
218
+ fi
219
+ if [ -f "Cargo.toml" ]; then
220
+ STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rs$')
221
+ if [ -n "$STAGED_RS" ]; then
222
+ cargo check --quiet 2>&1
223
+ if [ $? -ne 0 ]; then
224
+ echo "❌ Rust cargo check failed."
225
+ _fc_write_violation "pre-commit-compile" "error" "Rust cargo check failed — run cargo check to see errors"
226
+ exit 1
227
+ fi
228
+ echo " Rust cargo check OK"
229
+ fi
230
+ fi
231
+ echo "🔨 Build check passed"
232
+
233
+ - name: cargo-clippy
234
+ trigger: pre-commit
235
+ stack: [RUST]
236
+ description: "Run cargo clippy on Rust projects — catches bugs, style issues, and common mistakes beyond cargo check"
237
+ filename: pre-commit-clippy.sh
238
+ script: |
239
+ #!/bin/bash
240
+ _fc_write_violation() {
241
+ local hook_name="$1" severity="${2:-error}" message="$3"
242
+ local repo_root
243
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
244
+ local dir="$repo_root/.forgecraft"
245
+ mkdir -p "$dir" 2>/dev/null || return 0
246
+ local ts
247
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
248
+ local esc_msg
249
+ esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
250
+ printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
251
+ "$hook_name" "$severity" "$esc_msg" "$ts" \
252
+ >> "$dir/gate-violations.jsonl" 2>/dev/null || true
253
+ }
254
+ if [ ! -f "Cargo.toml" ]; then
255
+ exit 0
256
+ fi
257
+ STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rs$')
258
+ if [ -z "$STAGED_RS" ]; then
259
+ exit 0
260
+ fi
261
+ echo "🦀 Running cargo clippy..."
262
+ cargo clippy --all-targets --all-features 2>&1
263
+ if [ $? -ne 0 ]; then
264
+ echo "❌ cargo clippy failed fix lint errors before committing."
265
+ echo " Run: cargo clippy --all-targets --all-features"
266
+ _fc_write_violation "pre-commit-clippy" "error" "Cargo clippy violations found — run cargo clippy to see details"
267
+ exit 1
268
+ fi
269
+ echo " cargo clippy passed"
270
+
271
+ - name: tdd-phase-gate
272
+ trigger: pre-commit
273
+ description: "RED gate: test-only commits must have failing tests; warn on implementation without tests"
274
+ filename: pre-commit-tdd-check.sh
275
+ script: |
276
+ #!/bin/bash
277
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
278
+ if [ -z "$STAGED" ]; then exit 0; fi
279
+ SRC_PATTERNS='^(src|lib|app|server|client|pkg|internal|cmd)/'
280
+ TEST_PATTERNS='^(tests?|spec|__tests__)/'
281
+ TEST_FILE_PATTERNS='\.(test|spec)\.(ts|tsx|js|jsx|mjs|py|go|rb|java|kt)$'
282
+ SRC_FILES=()
283
+ TEST_FILES=()
284
+ while IFS= read -r file; do
285
+ if echo "$file" | grep -qE "$SRC_PATTERNS"; then
286
+ SRC_FILES+=("$file")
287
+ elif echo "$file" | grep -qE "$TEST_PATTERNS"; then
288
+ TEST_FILES+=("$file")
289
+ elif echo "$file" | grep -qE "$TEST_FILE_PATTERNS"; then
290
+ TEST_FILES+=("$file")
291
+ fi
292
+ done <<< "$STAGED"
293
+ SRC_COUNT=${#SRC_FILES[@]}
294
+ TEST_COUNT=${#TEST_FILES[@]}
295
+ # RED gate: test-only commit must have at least one failing test
296
+ if [ "$TEST_COUNT" -gt 0 ] && [ "$SRC_COUNT" -eq 0 ]; then
297
+ echo "🔴 TDD gate: test-only commit — running staged tests..."
298
+ RUN_CMD=""
299
+ if [ -f "package.json" ]; then
300
+ if grep -q '"vitest"' package.json 2>/dev/null; then
301
+ RUN_CMD="npx vitest run"
302
+ elif grep -q '"jest"' package.json 2>/dev/null; then
303
+ RUN_CMD="npx jest --passWithNoTests"
304
+ fi
305
+ elif [ -f "Cargo.toml" ]; then
306
+ RUN_CMD="cargo test"
307
+ elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
308
+ RUN_CMD="python -m pytest"
309
+ fi
310
+ if [ -n "$RUN_CMD" ]; then
311
+ $RUN_CMD "${TEST_FILES[@]}" > /tmp/tdd-check-output.txt 2>&1
312
+ if [ $? -eq 0 ]; then
313
+ echo "❌ TDD RED gate violation: all staged tests PASS"
314
+ echo " A test-only commit must contain at least one failing test."
315
+ echo " Either tests were written after implementation, or assertions are vacuous."
316
+ cat /tmp/tdd-check-output.txt | head -30
317
+ exit 1
318
+ fi
319
+ echo " RED gate satisfiedstaged tests fail as expected"
320
+ else
321
+ echo " ⚠️ Cannot detect test runner — skipping RED gate check"
322
+ fi
323
+ fi
324
+ # Source gate: warn on implementation without tests
325
+ if [ "$SRC_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
326
+ echo "⚠️ TDD warning: implementation committed without test changes"
327
+ echo " Verify a preceding test(scope): [RED] commit exists in this branch."
328
+ fi
329
+ exit 0
330
+
331
+ - name: test-coverage
332
+ trigger: pre-commit
333
+ description: "Run affected tests only (--changed) for pre-commit; skip for docs/config-only commits"
334
+ filename: pre-commit-test.sh
335
+ script: |
336
+ #!/bin/bash
337
+ # pre-commit: run only affected tests using --changed flag.
338
+ # Full suite runs pre-push. TDD_RED=1 bypasses for [RED] commits.
339
+
340
+ # TDD RED bypass: skip test run when committing a failing test (RED phase)
341
+ if [ "${TDD_RED:-0}" = "1" ]; then
342
+ echo "🔴 TDD_RED=1: bypassing test run for RED commit."
343
+ exit 0
344
+ fi
345
+
346
+ # Collect staged files once.
347
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
348
+
349
+ # Determine what kinds of files are staged.
350
+ CODE_STAGED=0
351
+ while IFS= read -r file; do
352
+ if echo "$file" | grep -qE '^(src|tests?|lib|app)/'; then
353
+ CODE_STAGED=1
354
+ break
355
+ elif echo "$file" | grep -qE '\.(ts|tsx|js|jsx|py|rs)$'; then
356
+ CODE_STAGED=1
357
+ break
358
+ fi
359
+ done <<< "$STAGED"
360
+
361
+ # If no code files at all are staged (docs-only, config-only, etc.) skip the run.
362
+ if [ "$CODE_STAGED" -eq 0 ]; then
363
+ echo "🧪 No code files staged — skipping test run."
364
+ exit 0
365
+ fi
366
+
367
+ echo "🧪 Running affected tests (--changed)..."
368
+ if [ -f "package.json" ]; then
369
+ if grep -q '"vitest"' package.json 2>/dev/null; then
370
+ npx vitest run --changed --passWithNoTests 2>&1
371
+ if [ $? -ne 0 ]; then
372
+ echo "❌ Affected tests failed."
373
+ echo " To bypass for a RED commit: TDD_RED=1 git commit ..."
374
+ exit 1
375
+ fi
376
+ echo " Affected tests passed"
377
+ elif grep -q '"jest"' package.json 2>/dev/null; then
378
+ npx jest --passWithNoTests --onlyChanged --silent 2>&1
379
+ if [ $? -ne 0 ]; then
380
+ echo " Jest affected tests failed."
381
+ exit 1
382
+ fi
383
+ echo " ✅ Jest affected tests passed"
384
+ fi
385
+ fi
386
+ if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
387
+ if command -v pytest &> /dev/null; then
388
+ # pytest doesn't have --changed; run only staged test files as best-effort
389
+ STAGED_TESTS=$(echo "$STAGED" | grep -E '(test_.*\.py|.*_test\.py)$')
390
+ if [ -n "$STAGED_TESTS" ]; then
391
+ pytest --tb=short --quiet $STAGED_TESTS 2>&1
392
+ if [ $? -ne 0 ]; then
393
+ echo "❌ Python tests failed."
394
+ exit 1
395
+ fi
396
+ echo " ✅ Python staged tests passed"
397
+ else
398
+ echo "🧪 No Python test files staged skipping pytest."
399
+ fi
400
+ fi
401
+ fi
402
+ if [ -f "Cargo.toml" ]; then
403
+ cargo test --quiet 2>&1
404
+ if [ $? -ne 0 ]; then
405
+ echo "❌ Rust tests failed."
406
+ exit 1
407
+ fi
408
+ echo " ✅ Rust tests passed"
409
+ fi
410
+ echo "🧪 Affected tests passed"
411
+
412
+ - name: coverage-gate
413
+ trigger: pre-commit
414
+ description: "Enforce minimum coverage thresholds when src/ files are staged"
415
+ filename: pre-commit-coverage.sh
416
+ script: |
417
+ #!/bin/bash
418
+ _fc_write_violation() {
419
+ local hook_name="$1" severity="${2:-error}" message="$3"
420
+ local repo_root
421
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
422
+ local dir="$repo_root/.forgecraft"
423
+ mkdir -p "$dir" 2>/dev/null || return 0
424
+ local ts
425
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
426
+ local esc_msg
427
+ esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
428
+ printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
429
+ "$hook_name" "$severity" "$esc_msg" "$ts" \
430
+ >> "$dir/gate-violations.jsonl" 2>/dev/null || true
431
+ }
432
+ # ──────────────────────────────────────────────────────────────────────
433
+ # Pre-Commit Hook: Coverage Gate
434
+ #
435
+ # Enforces minimum coverage thresholds before a commit lands.
436
+ # Thresholds are read from vitest.config.ts (via --coverage).
437
+ # Only runs when files under src/ are staged — skips for
438
+ # docs-only, config-only, or test-only commits.
439
+ #
440
+ # Current thresholds (vitest.config.ts):
441
+ # Lines / Statements / Functions: {{coverage_minimum | default: 80}}%
442
+ # Branches: 70%
443
+ #
444
+ # Trigger: git pre-commit (via scripts/setup-hooks.sh)
445
+ # Exit: 1 blocks commit, 0 allows
446
+ # ──────────────────────────────────────────────────────────────────────
447
+
448
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
449
+
450
+ if [ -z "$STAGED" ]; then
451
+ exit 0
452
+ fi
453
+
454
+ # ── Check if any src/ file is staged ─────────────────────────────────
455
+ SRC_STAGED=0
456
+ while IFS= read -r file; do
457
+ if echo "$file" | grep -qE '^src/'; then
458
+ SRC_STAGED=1
459
+ break
460
+ fi
461
+ done <<< "$STAGED"
462
+
463
+ if [ "$SRC_STAGED" -eq 0 ]; then
464
+ echo "📊 Coverage gate: no src/ files staged, skipping."
465
+ exit 0
466
+ fi
467
+
468
+ # ── Run coverage ───────────────────────────────────────────────────────
469
+ echo "📊 Running coverage gate (src/ files staged)..."
470
+
471
+ if [ ! -f "package.json" ] && [ ! -f "pyproject.toml" ] && [ ! -f "setup.py" ]; then
472
+ if [ -f "Cargo.toml" ]; then
473
+ # Rust: run tests; use cargo-tarpaulin for coverage if available
474
+ if command -v cargo-tarpaulin &> /dev/null; then
475
+ cargo tarpaulin --out Stdout --fail-under {{coverage_minimum | default: 80}} 2>&1
476
+ if [ $? -ne 0 ]; then
477
+ echo "❌ Coverage gate failed below {{coverage_minimum | default: 80}}%."
478
+ echo " Run: cargo tarpaulin --out Html"
479
+ _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
480
+ exit 1
481
+ fi
482
+ echo " ✅ Rust coverage gate passed"
483
+ else
484
+ cargo test --quiet 2>&1
485
+ if [ $? -ne 0 ]; then
486
+ echo " Rust tests failed."
487
+ _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
488
+ exit 1
489
+ fi
490
+ echo " Rust tests passed (install cargo-tarpaulin for coverage enforcement)"
491
+ fi
492
+ else
493
+ echo " ⚠️ No supported build system found — skipping coverage check."
494
+ fi
495
+ exit 0
496
+ fi
497
+
498
+ if grep -q '"vitest"' package.json 2>/dev/null; then
499
+ OUTPUT=$(npx vitest run --coverage --reporter=verbose 2>&1)
500
+ EXIT_CODE=$?
501
+
502
+ if [ $EXIT_CODE -ne 0 ]; then
503
+ echo "$OUTPUT" | grep -E "ERROR|Coverage|does not meet|%|FAIL|passed|failed" | head -40
504
+ echo ""
505
+ echo "❌ Coverage gate failed — thresholds not met."
506
+ echo " Run 'npx vitest run --coverage' locally to see the full report."
507
+ echo " Add tests until coverage meets the configured minimums."
508
+ _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed run npm test --coverage to see report"
509
+ exit 1
510
+ fi
511
+ echo " ✅ Coverage gate passed"
512
+ exit 0
513
+ fi
514
+
515
+ if grep -q '"jest"' package.json 2>/dev/null; then
516
+ COVERAGE_MIN={{coverage_minimum | default: 80}}
517
+ npx jest --passWithNoTests --coverage \
518
+ --coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN,\"statements\":$COVERAGE_MIN,\"functions\":$COVERAGE_MIN,\"branches\":70}}" \
519
+ --silent 2>&1
520
+ if [ $? -ne 0 ]; then
521
+ echo " Coverage gate failed thresholds not met."
522
+ _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
523
+ exit 1
524
+ fi
525
+ echo " ✅ Coverage gate passed"
526
+ exit 0
527
+ fi
528
+
529
+ if [ -f "pyproject.toml" ]|| [ -f "setup.py" ]; then
530
+ if command -v pytest &> /dev/null; then
531
+ pytest --tb=no --quiet --cov=src --cov-fail-under={{coverage_minimum | default: 80}} 2>&1
532
+ if [ $? -ne 0 ]; then
533
+ echo "❌ Coverage gate failed below {{coverage_minimum | default: 80}}%."
534
+ _fc_write_violation "pre-commit-coverage" "error" "Coverage gate failed — run npm test --coverage to see report"
535
+ exit 1
536
+ fi
537
+ echo " ✅ Coverage gate passed"
538
+ fi
539
+ fi
540
+
541
+ exit 0
542
+
543
+ - name: anti-pattern-detector
544
+ trigger: pre-commit
545
+ description: "Scan source files for production code anti-patterns. Respects .forgecraft/exceptions.json for recorded false positives."
546
+ filename: pre-commit-prod-quality.sh
547
+ checks:
548
+ - Hardcoded URLs/hosts (localhost, 127.0.0.1)
549
+ - Mock/stub data in production code
550
+ - Direct DB calls in route/controller layer (layer violation)
551
+ - Bare Error throws (custom hierarchy recommended)
552
+ - .env vs .env.example drift (warns on missing variables)
553
+ script: |
554
+ #!/bin/bash
555
+ _fc_write_violation() {
556
+ local hook_name="$1" severity="${2:-error}" message="$3"
557
+ local repo_root
558
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
559
+ local dir="$repo_root/.forgecraft"
560
+ mkdir -p "$dir" 2>/dev/null || return 0
561
+ local ts
562
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || printf "unknown")"
563
+ local esc_msg
564
+ esc_msg="$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g')"
565
+ printf '{"hook":"%s","severity":"%s","message":"%s","timestamp":"%s"}\n' \
566
+ "$hook_name" "$severity" "$esc_msg" "$ts" \
567
+ >> "$dir/gate-violations.jsonl" 2>/dev/null || true
568
+ }
569
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
570
+ SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(py|ts|tsx|js|jsx|rs)$' | grep -vE '(test_|\.test\.|\.spec\.|__tests__|tests/|fixtures/|mock|conftest|_test\.rs)')
571
+ if [ -z "$SOURCE_FILES" ]; then exit 0; fi
572
+ VIOLATIONS=0
573
+ WARNINGS=0
574
+ # Check if a file is covered by a hook exception in .forgecraft/exceptions.json
575
+ # Usage: is_excepted "layer-boundary" "src/migrations/001.ts"
576
+ # Add entries to .forgecraft/exceptions.json to record known false positives.
577
+ is_excepted() {
578
+ local hook_name="$1"
579
+ local file_path="$2"
580
+ if [ ! -f ".forgecraft/exceptions.json" ]; then return 1; fi
581
+ node -e "
582
+ const fs = require('fs');
583
+ const data = JSON.parse(fs.readFileSync('.forgecraft/exceptions.json', 'utf-8'));
584
+ const exc = (data.exceptions || []).find(e => {
585
+ if (e.hook !== '$hook_name') return false;
586
+ const pat = e.pattern.replace(/\\/g, '/').replace(/\./g, '\\\\.').replace(/\*\*/g, '<<<D>>>').replace(/\*/g, '[^/]*').replace(/<<<D>>>/g, '.*');
587
+ return new RegExp('^' + pat + '$').test('$file_path'.replace(/\\\\/g, '/'));
588
+ });
589
+ if (exc) { console.log('EXCEPTED: ' + exc.reason); process.exit(0); }
590
+ process.exit(1);
591
+ " 2>/dev/null
592
+ }
593
+ echo "🔍 Scanning for production code anti-patterns..."
594
+ for file in $SOURCE_FILES; do
595
+ if echo "$file" | grep -vqE '(config|settings|\.env)'; then
596
+ if grep -nE '(localhost|127\.0\.0\.1|0\.0\.0\.0)' "$file" | grep -vE '(#|//|""")' > /tmp/violations 2>/dev/null; then
597
+ if [ -s /tmp/violations ]; then
598
+ echo " $file — hardcoded URL/host"
599
+ VIOLATIONS=$((VIOLATIONS + 1))
600
+ fi
601
+ fi
602
+ fi
603
+ if ! is_excepted "anti-pattern/mock-data" "$file"; then
604
+ if grep -nEi '\b(mock_data|fake_data|dummy_data|stub_response)' "$file" > /tmp/violations 2>/dev/null; then
605
+ if [ -s /tmp/violations ]; then
606
+ echo " ❌ $file — mock/stub data in production code"
607
+ VIOLATIONS=$((VIOLATIONS + 1))
608
+ fi
609
+ fi
610
+ fi
611
+ # Layer boundary: no direct DB/ORM imports from route handlers / controllers
612
+ if echo "$file" | grep -qE '(routes|controllers|handlers|endpoints)'; then
613
+ if ! is_excepted "layer-boundary" "$file"; then
614
+ if grep -nE '\b(prisma\.|knex\(|mongoose\.|sequelize\.|db\.query|pool\.query)' "$file" > /tmp/violations 2>/dev/null; then
615
+ if [ -s /tmp/violations ]; then
616
+ echo " ❌ $file — direct DB call in route/controller (layer violation)"
617
+ VIOLATIONS=$((VIOLATIONS + 1))
618
+ fi
619
+ fi
620
+ fi
621
+ fi
622
+ # Bare Error throws in business logic (not test files)
623
+ if ! is_excepted "error-hierarchy" "$file"; then
624
+ if grep -nE 'throw new Error\(' "$file" > /tmp/violations 2>/dev/null; then
625
+ if [ -s /tmp/violations ]; then
626
+ echo " ⚠️ $file — bare 'throw new Error()' found — use custom error hierarchy"
627
+ WARNINGS=$((WARNINGS + 1))
628
+ fi
629
+ fi
630
+ fi
631
+ LINE_COUNT=$(wc -l < "$file")
632
+ if [ "$LINE_COUNT" -gt {{max_file_length | default: 300}} ]; then
633
+ echo " ⚠️ $file — $LINE_COUNT lines (max {{max_file_length | default: 300}})"
634
+ WARNINGS=$((WARNINGS + 1))
635
+ fi
636
+ # Rust-specific anti-patterns
637
+ if echo "$file" | grep -q '\.rs$'; then
638
+ if ! is_excepted "rust/unwrap" "$file"; then
639
+ if grep -nE '\.unwrap\(\)' "$file" > /tmp/violations 2>/dev/null; then
640
+ if [ -s /tmp/violations ]; then
641
+ echo " ⚠️ $file .unwrap() in production code — use ? or explicit error handling"
642
+ WARNINGS=$((WARNINGS + 1))
643
+ fi
644
+ fi
645
+ fi
646
+ if grep -nE '\btodo!\(|\bunimplemented!\(' "$file" > /tmp/violations 2>/dev/null; then
647
+ if [ -s /tmp/violations ]; then
648
+ echo " ❌ $file todo!/unimplemented! in production code"
649
+ VIOLATIONS=$((VIOLATIONS + 1))
650
+ fi
651
+ fi
652
+ if grep -nE '^[[:space:]]*#\[allow\(dead_code\)\]' "$file" > /tmp/violations 2>/dev/null; then
653
+ if [ -s /tmp/violations ]; then
654
+ echo " ⚠️ $file — #[allow(dead_code)] suppression — delete orphaned code instead"
655
+ WARNINGS=$((WARNINGS + 1))
656
+ fi
657
+ fi
658
+ if grep -nE '^[[:space:]]*unsafe[[:space:]]*\{' "$file" > /tmp/violations 2>/dev/null; then
659
+ if [ -s /tmp/violations ]; then
660
+ echo " ⚠️ $file — unsafe block present — requires explicit justification comment"
661
+ WARNINGS=$((WARNINGS + 1))
662
+ fi
663
+ fi
664
+ fi
665
+ rm -f /tmp/violations
666
+ if [ $VIOLATIONS -gt 0 ]; then
667
+ echo "❌ $VIOLATIONS violation(s) found — commit blocked."
668
+ _fc_write_violation "pre-commit-anti-patterns" "error" "Anti-patterns detected in staged files — see output above"
669
+ exit 1
670
+ fi
671
+ if [ $WARNINGS -gt 0 ]; then
672
+ echo "⚠️ $WARNINGS warning(s) found — review recommended."
673
+ fi
674
+ echo "🔍 Production quality scan passed"
675
+
676
+ - name: function-length
677
+ trigger: pre-commit
678
+ description: "Warn when staged functions exceed the configured maximum line count"
679
+ filename: pre-commit-function-length.sh
680
+ script: |
681
+ #!/bin/bash
682
+ MAX_LENGTH={{max_function_length | default: 50}}
683
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
684
+ SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(ts|tsx|js|jsx|rs)$' | grep -vE '(\.test\.|\.spec\.|__tests__|tests/|_test\.rs)')
685
+ if [ -z "$SOURCE_FILES" ]; then exit 0; fi
686
+ WARNINGS=0
687
+ for file in $SOURCE_FILES; do
688
+ # Heuristic: find function/method declarations and count lines to next declaration
689
+ if echo "$file" | grep -q '\.rs$'; then
690
+ awk -v max="$MAX_LENGTH" -v fname="$file" '
691
+ /^[[:space:]]*(pub )?(async )?fn [a-zA-Z_]/ {
692
+ if (start > 0 && NR - start > max) {
693
+ printf " ⚠️ %s:%d fn starting here is %d lines (max %d)\n", fname, start, NR - start, max
694
+ }
695
+ start = NR
696
+ }
697
+ END {
698
+ if (start > 0 && NR - start > max) {
699
+ printf " ⚠️ %s:%d — fn starting here is %d lines (max %d)\n", fname, start, NR - start, max
700
+ }
701
+ }
702
+ ' "$file"
703
+ else
704
+ awk -v max="$MAX_LENGTH" -v fname="$file" '
705
+ /^[[:space:]]*(export )?(async )?(function |const [a-zA-Z]+ = (async )?\(|[a-zA-Z]+\(.*\) \{|[a-zA-Z]+\(.*\): )/ {
706
+ if (start > 0 && NR - start > max) {
707
+ printf " ⚠️ %s:%d function starting here is %d lines (max %d)\n", fname, start, NR - start, max
708
+ warnings++
709
+ }
710
+ start = NR
711
+ }
712
+ END {
713
+ if (start > 0 && NR - start > max) {
714
+ printf " ⚠️ %s:%d function starting here is %d lines (max %d)\n", fname, start, NR - start, max
715
+ warnings++
716
+ }
717
+ }
718
+ ' "$file"
719
+ fi
720
+ WARNINGS=$((WARNINGS + $?))
721
+ done
722
+ # Warning only does not block commit since bash heuristics aren't perfect
723
+ exit 0
724
+
725
+ - name: import-cycle-detector
726
+ trigger: pre-commit
727
+ description: "Detect circular import dependencies in TypeScript and Python projects"
728
+ filename: pre-commit-import-cycles.sh
729
+ script: |
730
+ #!/bin/bash
731
+ echo "🔄 Checking for circular imports..."
732
+ if [ -f "tsconfig.json" ]; then
733
+ if command -v npx &> /dev/null; then
734
+ RESULT=$(npx --yes madge --circular --extensions ts src/ 2>&1)
735
+ if echo "$RESULT" | grep -q "Found.*circular"; then
736
+ echo "❌ Circular imports detected in TypeScript:"
737
+ echo "$RESULT"
738
+ exit 1
739
+ fi
740
+ echo " ✅ No circular imports (TypeScript)"
741
+ fi
742
+ fi
743
+ if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
744
+ if command -v lint-imports &> /dev/null; then
745
+ lint-imports 2>&1
746
+ if [ $? -ne 0 ]; then
747
+ echo "❌ Circular imports detected in Python"
748
+ exit 1
749
+ fi
750
+ echo " ✅ No circular imports (Python)"
751
+ fi
752
+ fi
753
+ echo "🔄 Import cycle check passed"
754
+
755
+ - name: code-review
756
+ trigger: pre-commit
757
+ description: "AI assistant reviews diff against project standards"
758
+ filename: pre-commit-review.sh
759
+ script: |
760
+ #!/bin/bash
761
+ DIFF=$(git diff --cached)
762
+ if [ -z "$DIFF" ]; then exit 0; fi
763
+ echo "📝 Staged changes ready for review"
764
+ # Full auto-review requires claude CLI integration
765
+ # This hook validates the diff is non-empty and staged
766
+ exit 0
767
+
768
+ - name: pre-tool-use
769
+ trigger: pre-tool-use
770
+ description: "Block Tier 3 (irreversible) operations during Claude Code sessions"
771
+ filename: pre-tool-use.sh
772
+ script: |
773
+ #!/usr/bin/env bash
774
+ # pre-tool-use: block Tier 3 destructive operations.
775
+ # Invoked by Claude Code PreToolUse hook before Bash tool executes.
776
+ set -u
777
+ input=$(cat)
778
+ if command -v jq >/dev/null 2>&1; then
779
+ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
780
+ cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
781
+ else
782
+ tool_name=$(echo "$input" | grep -o '"tool_name":"[^"]*"' | cut -d'"' -f4)
783
+ cmd=$(echo "$input" | grep -o '"command":"[^"]*"' | cut -d'"' -f4)
784
+ fi
785
+ [[ "$tool_name" != "Bash" ]] && exit 0
786
+ [[ "${FORGECRAFT_ALLOW_DESTRUCTIVE:-0}" == "1" ]] && exit 0
787
+ block() {
788
+ echo "🚫 BLOCKED by pre-tool-use hook: $1" >&2
789
+ echo " Command: $cmd" >&2
790
+ echo " See docs/operation-classification.md" >&2
791
+ echo " To override (use with care): FORGECRAFT_ALLOW_DESTRUCTIVE=1 <command>" >&2
792
+ exit 2
793
+ }
794
+ [[ "$cmd" =~ DROP[[:space:]]+TABLE ]] && block "DROP TABLE — Tier 3 irreversible"
795
+ [[ "$cmd" =~ TRUNCATE[[:space:]] ]] && block "TRUNCATE — Tier 3 irreversible"
796
+ if [[ "$cmd" =~ git[[:space:]]+push ]] && [[ "$cmd" =~ (-f|--force) ]] && [[ "$cmd" =~ (main|master) ]]; then
797
+ block "force push to main — Tier 3 irreversible"
798
+ fi
799
+ if [[ "$cmd" =~ rm[[:space:]]+-rf ]] && [[ "$cmd" =~ (src|docs|migrations|\.git)[/[:space:]] ]]; then
800
+ block "rm -rf on protected path — Tier 3"
801
+ fi
802
+ if [[ "$cmd" =~ git[[:space:]]+push[[:space:]]+origin[[:space:]]+(main|master) ]]; then
803
+ echo "⚠️ Direct push to main detected. Use a PR instead." >&2
804
+ fi
805
+ exit 0
806
+
807
+ - name: post-edit
808
+ trigger: post-edit
809
+ description: "After Write/Edit: check Bounded property (file size, console.log, any)"
810
+ filename: post-edit.sh
811
+ script: |
812
+ #!/usr/bin/env bash
813
+ # post-edit: enforce Bounded property after every file write/edit.
814
+ set -u
815
+ input=$(cat)
816
+ if command -v jq >/dev/null 2>&1; then
817
+ file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
818
+ else
819
+ file_path=$(echo "$input" | grep -o '"file_path":"[^"]*"' | cut -d'"' -f4 | head -1)
820
+ fi
821
+ [[ -z "$file_path" ]] && exit 0
822
+ [[ ! -f "$file_path" ]] && exit 0
823
+ echo "$file_path" | grep -qE '(node_modules|pnpm-lock|package-lock|\.d\.ts$|\.min\.|/generated/)' && exit 0
824
+ line_count=$(wc -l < "$file_path")
825
+ if [[ "$line_count" -gt 300 ]]; then
826
+ echo "⚠️ BOUNDED: $file_path has $line_count lines (max 300). Extract to sub-modules before commit." >&2
827
+ fi
828
+ if [[ "$file_path" =~ \.(ts|tsx|js|jsx)$ ]] && [[ ! "$file_path" =~ \.(test|spec)\. ]] && [[ ! "$file_path" =~ /scripts/ ]]; then
829
+ grep -q 'console\.\(log\|debug\)' "$file_path" && echo "⚠️ console.log in $file_path — use structured logger" >&2
830
+ grep -nE '^[^/]*:\s*any\b|\bas\s+any\b' "$file_path" >/dev/null 2>&1 && echo "⚠️ 'any' type in $file_path — prefer 'unknown' + narrowing" >&2
831
+ fi
832
+ exit 0
833
+
834
+ - name: prompt-guard
835
+ trigger: user-prompt-submit
836
+ description: "Intercept prompts with destructive or structural signals; inject sentinel reminder"
837
+ filename: prompt-guard.sh
838
+ script: |
839
+ #!/usr/bin/env bash
840
+ # prompt-guard: runs on every user prompt (UserPromptSubmit).
841
+ # Two jobs:
842
+ # 1. Detect Tier 2/3 operation signals -> remind about operation-classification.md.
843
+ # 2. Detect structural task signals -> remind to read the sentinel first.
844
+ set -u
845
+ input=$(cat)
846
+ if command -v jq >/dev/null 2>&1; then
847
+ user_prompt=$(echo "$input" | jq -r '.prompt // empty')
848
+ else
849
+ user_prompt=$(echo "$input" | grep -o '"prompt":"[^"]*"' | cut -d'"' -f4 | head -1)
850
+ fi
851
+ lower=$(echo "$user_prompt" | tr '[:upper:]' '[:lower:]')
852
+ sensitive_signals=("drop table" "truncate" "delete from" "force push" "full resync"
853
+ "backfill" "reset db" "disable rls" "service role" "bypass auth"
854
+ "rm -rf" "drop database" "wipe data")
855
+ for signal in "${sensitive_signals[@]}"; do
856
+ if [[ "$lower" == *"$signal"* ]]; then
857
+ printf "🧭 prompt-guard: destructive signal detected (\"%s\").\n" "$signal"
858
+ printf " Before executing:\n"
859
+ printf " 1. Check docs/operation-classification.md -- classify the operation (Tier 0-3).\n"
860
+ printf " 2. If Tier 2+: confirm with the user before proceeding.\n"
861
+ printf " 3. If Tier 3: require FORGECRAFT_ALLOW_DESTRUCTIVE=1 override + ADR.\n"
862
+ break
863
+ fi
864
+ done
865
+ structural_signals=("create a new" "add a new" "refactor" "migrate" "new feature"
866
+ "new endpoint" "new module" "change architecture" "add table" "add column"
867
+ "new component" "new service" "new repository")
868
+ for signal in "${structural_signals[@]}"; do
869
+ if [[ "$lower" == *"$signal"* ]]; then
870
+ printf "🧭 prompt-guard: structural task detected (\"%s\").\n" "$signal"
871
+ printf " Before coding:\n"
872
+ printf " 1. Read CLAUDE.md (sentinel) -- architecture rules and prohibited ops.\n"
873
+ printf " 2. Read the relevant section of docs/PRD.md or use-cases.md.\n"
874
+ printf " 3. If decision is non-trivial -> propose an ADR before implementing.\n"
875
+ printf " 4. If the change spans > 3 files -> propose a brief plan first.\n"
876
+ break
877
+ fi
878
+ done
879
+ exit 0