forgecraft-mcp 1.2.0 → 1.4.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 (185) hide show
  1. package/README.md +525 -525
  2. package/dist/artifacts/commit-hooks.d.ts +1 -1
  3. package/dist/artifacts/commit-hooks.d.ts.map +1 -1
  4. package/dist/artifacts/commit-hooks.js +2 -0
  5. package/dist/artifacts/commit-hooks.js.map +1 -1
  6. package/dist/cli/commands.d.ts +35 -1
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +109 -2
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/cli/help.d.ts.map +1 -1
  11. package/dist/cli/help.js +51 -44
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +10 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/registry/renderer-skeletons.js +92 -92
  17. package/dist/shared/gs-score-logger.js +6 -6
  18. package/dist/shared/result-utils.d.ts +27 -0
  19. package/dist/shared/result-utils.d.ts.map +1 -0
  20. package/dist/shared/result-utils.js +41 -0
  21. package/dist/shared/result-utils.js.map +1 -0
  22. package/dist/tools/add-module.js +123 -123
  23. package/dist/tools/advice-registry.js +18 -18
  24. package/dist/tools/check-cascade-report.js +64 -64
  25. package/dist/tools/close-cycle-helpers.d.ts +21 -2
  26. package/dist/tools/close-cycle-helpers.d.ts.map +1 -1
  27. package/dist/tools/close-cycle-helpers.js +66 -10
  28. package/dist/tools/close-cycle-helpers.js.map +1 -1
  29. package/dist/tools/close-cycle.d.ts +2 -2
  30. package/dist/tools/close-cycle.d.ts.map +1 -1
  31. package/dist/tools/close-cycle.js +1 -1
  32. package/dist/tools/close-cycle.js.map +1 -1
  33. package/dist/tools/configure-mcp.d.ts +3 -0
  34. package/dist/tools/configure-mcp.d.ts.map +1 -1
  35. package/dist/tools/configure-mcp.js +10 -0
  36. package/dist/tools/configure-mcp.js.map +1 -1
  37. package/dist/tools/consolidate-status.d.ts +81 -0
  38. package/dist/tools/consolidate-status.d.ts.map +1 -0
  39. package/dist/tools/consolidate-status.js +251 -0
  40. package/dist/tools/consolidate-status.js.map +1 -0
  41. package/dist/tools/forgecraft-dispatch.d.ts.map +1 -1
  42. package/dist/tools/forgecraft-dispatch.js +13 -0
  43. package/dist/tools/forgecraft-dispatch.js.map +1 -1
  44. package/dist/tools/forgecraft-router.d.ts +8 -0
  45. package/dist/tools/forgecraft-router.d.ts.map +1 -1
  46. package/dist/tools/forgecraft-router.js +21 -1
  47. package/dist/tools/forgecraft-router.js.map +1 -1
  48. package/dist/tools/forgecraft-schema-params.d.ts +13 -4
  49. package/dist/tools/forgecraft-schema-params.d.ts.map +1 -1
  50. package/dist/tools/forgecraft-schema-params.js +21 -0
  51. package/dist/tools/forgecraft-schema-params.js.map +1 -1
  52. package/dist/tools/forgecraft-schema.d.ts +14 -5
  53. package/dist/tools/forgecraft-schema.d.ts.map +1 -1
  54. package/dist/tools/forgecraft-schema.js +3 -0
  55. package/dist/tools/forgecraft-schema.js.map +1 -1
  56. package/dist/tools/gate-violations.d.ts +59 -0
  57. package/dist/tools/gate-violations.d.ts.map +1 -0
  58. package/dist/tools/gate-violations.js +152 -0
  59. package/dist/tools/gate-violations.js.map +1 -0
  60. package/dist/tools/generate-session-prompt.d.ts +3 -3
  61. package/dist/tools/generate-session-prompt.d.ts.map +1 -1
  62. package/dist/tools/generate-session-prompt.js +57 -15
  63. package/dist/tools/generate-session-prompt.js.map +1 -1
  64. package/dist/tools/refresh-output.js +14 -14
  65. package/dist/tools/roadmap-builder.d.ts.map +1 -1
  66. package/dist/tools/roadmap-builder.js +19 -9
  67. package/dist/tools/roadmap-builder.js.map +1 -1
  68. package/dist/tools/scaffold-spec-stubs.js +115 -115
  69. package/dist/tools/scaffold-templates.js +62 -62
  70. package/dist/tools/session-prompt-builders.d.ts.map +1 -1
  71. package/dist/tools/session-prompt-builders.js +34 -10
  72. package/dist/tools/session-prompt-builders.js.map +1 -1
  73. package/dist/tools/setup-artifact-writers.d.ts +30 -0
  74. package/dist/tools/setup-artifact-writers.d.ts.map +1 -1
  75. package/dist/tools/setup-artifact-writers.js +120 -8
  76. package/dist/tools/setup-artifact-writers.js.map +1 -1
  77. package/dist/tools/setup-phase1.d.ts +3 -0
  78. package/dist/tools/setup-phase1.d.ts.map +1 -1
  79. package/dist/tools/setup-phase1.js +79 -35
  80. package/dist/tools/setup-phase1.js.map +1 -1
  81. package/dist/tools/setup-phase2.d.ts +2 -0
  82. package/dist/tools/setup-phase2.d.ts.map +1 -1
  83. package/dist/tools/setup-phase2.js +10 -1
  84. package/dist/tools/setup-phase2.js.map +1 -1
  85. package/dist/tools/setup-project.d.ts +18 -0
  86. package/dist/tools/setup-project.d.ts.map +1 -1
  87. package/dist/tools/setup-project.js +77 -1
  88. package/dist/tools/setup-project.js.map +1 -1
  89. package/dist/tools/spec-parser-tags.d.ts +9 -0
  90. package/dist/tools/spec-parser-tags.d.ts.map +1 -1
  91. package/dist/tools/spec-parser-tags.js +92 -0
  92. package/dist/tools/spec-parser-tags.js.map +1 -1
  93. package/package.json +89 -86
  94. package/templates/analytics/instructions.yaml +37 -37
  95. package/templates/analytics/mcp-servers.yaml +11 -11
  96. package/templates/analytics/structure.yaml +25 -25
  97. package/templates/api/instructions.yaml +231 -231
  98. package/templates/api/mcp-servers.yaml +22 -13
  99. package/templates/api/nfr.yaml +23 -23
  100. package/templates/api/review.yaml +103 -103
  101. package/templates/api/structure.yaml +34 -34
  102. package/templates/api/verification.yaml +132 -132
  103. package/templates/cli/instructions.yaml +31 -31
  104. package/templates/cli/mcp-servers.yaml +11 -11
  105. package/templates/cli/review.yaml +53 -53
  106. package/templates/cli/structure.yaml +16 -16
  107. package/templates/data-lineage/instructions.yaml +28 -28
  108. package/templates/data-lineage/mcp-servers.yaml +22 -22
  109. package/templates/data-pipeline/instructions.yaml +84 -84
  110. package/templates/data-pipeline/mcp-servers.yaml +13 -13
  111. package/templates/data-pipeline/nfr.yaml +39 -39
  112. package/templates/data-pipeline/structure.yaml +23 -23
  113. package/templates/fintech/hooks.yaml +55 -55
  114. package/templates/fintech/instructions.yaml +112 -112
  115. package/templates/fintech/mcp-servers.yaml +13 -13
  116. package/templates/fintech/nfr.yaml +46 -46
  117. package/templates/fintech/playbook.yaml +210 -210
  118. package/templates/fintech/verification.yaml +239 -239
  119. package/templates/game/instructions.yaml +289 -289
  120. package/templates/game/mcp-servers.yaml +38 -38
  121. package/templates/game/nfr.yaml +64 -64
  122. package/templates/game/playbook.yaml +214 -214
  123. package/templates/game/review.yaml +97 -97
  124. package/templates/game/structure.yaml +67 -67
  125. package/templates/game/verification.yaml +174 -174
  126. package/templates/healthcare/instructions.yaml +42 -42
  127. package/templates/healthcare/mcp-servers.yaml +13 -13
  128. package/templates/healthcare/nfr.yaml +47 -47
  129. package/templates/hipaa/instructions.yaml +41 -41
  130. package/templates/hipaa/mcp-servers.yaml +13 -13
  131. package/templates/infra/instructions.yaml +104 -104
  132. package/templates/infra/mcp-servers.yaml +20 -20
  133. package/templates/infra/nfr.yaml +46 -46
  134. package/templates/infra/review.yaml +65 -65
  135. package/templates/infra/structure.yaml +25 -25
  136. package/templates/library/instructions.yaml +36 -36
  137. package/templates/library/mcp-servers.yaml +20 -20
  138. package/templates/library/review.yaml +56 -56
  139. package/templates/library/structure.yaml +19 -19
  140. package/templates/medallion-architecture/instructions.yaml +41 -41
  141. package/templates/medallion-architecture/mcp-servers.yaml +22 -22
  142. package/templates/ml/instructions.yaml +85 -85
  143. package/templates/ml/mcp-servers.yaml +11 -11
  144. package/templates/ml/nfr.yaml +39 -39
  145. package/templates/ml/structure.yaml +25 -25
  146. package/templates/ml/verification.yaml +156 -156
  147. package/templates/mobile/instructions.yaml +44 -44
  148. package/templates/mobile/mcp-servers.yaml +11 -11
  149. package/templates/mobile/nfr.yaml +49 -49
  150. package/templates/mobile/structure.yaml +27 -27
  151. package/templates/mobile/verification.yaml +121 -121
  152. package/templates/observability-xray/instructions.yaml +40 -40
  153. package/templates/observability-xray/mcp-servers.yaml +15 -15
  154. package/templates/realtime/instructions.yaml +42 -42
  155. package/templates/realtime/mcp-servers.yaml +13 -13
  156. package/templates/soc2/instructions.yaml +41 -41
  157. package/templates/soc2/mcp-servers.yaml +24 -24
  158. package/templates/social/instructions.yaml +43 -43
  159. package/templates/social/mcp-servers.yaml +24 -24
  160. package/templates/state-machine/instructions.yaml +42 -42
  161. package/templates/state-machine/mcp-servers.yaml +11 -11
  162. package/templates/tools-registry.yaml +164 -164
  163. package/templates/universal/hooks.yaml +723 -531
  164. package/templates/universal/instructions.yaml +1692 -1692
  165. package/templates/universal/mcp-servers.yaml +50 -50
  166. package/templates/universal/nfr.yaml +197 -197
  167. package/templates/universal/reference.yaml +326 -326
  168. package/templates/universal/review.yaml +204 -204
  169. package/templates/universal/skills.yaml +262 -262
  170. package/templates/universal/structure.yaml +67 -67
  171. package/templates/universal/verification.yaml +416 -416
  172. package/templates/web-react/hooks.yaml +44 -44
  173. package/templates/web-react/instructions.yaml +207 -207
  174. package/templates/web-react/mcp-servers.yaml +20 -20
  175. package/templates/web-react/nfr.yaml +27 -27
  176. package/templates/web-react/review.yaml +94 -94
  177. package/templates/web-react/structure.yaml +46 -46
  178. package/templates/web-react/verification.yaml +126 -126
  179. package/templates/web-static/instructions.yaml +115 -115
  180. package/templates/web-static/mcp-servers.yaml +20 -20
  181. package/templates/web3/instructions.yaml +44 -44
  182. package/templates/web3/mcp-servers.yaml +11 -11
  183. package/templates/web3/verification.yaml +159 -159
  184. package/templates/zero-trust/instructions.yaml +41 -41
  185. package/templates/zero-trust/mcp-servers.yaml +15 -15
@@ -1,531 +1,723 @@
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
- # Re-stage formatted files
94
- echo "$STAGED" | xargs -r git add
95
-
96
- - name: secrets-scanner
97
- trigger: pre-commit
98
- description: "Scan for accidentally committed secrets (AWS keys, API keys, passwords)"
99
- filename: pre-commit-secrets.sh
100
- script: |
101
- #!/bin/bash
102
- PATTERNS=(
103
- 'AKIA[0-9A-Z]{16}'
104
- 'password\s*=\s*["\x27][^"\x27]+'
105
- 'BEGIN RSA PRIVATE KEY'
106
- 'sk-[a-zA-Z0-9]{48}'
107
- 'ghp_[a-zA-Z0-9]{36}'
108
- )
109
- STAGED=$(git diff --cached --name-only)
110
- for file in $STAGED; do
111
- for pattern in "${PATTERNS[@]}"; do
112
- if grep -qE "$pattern" "$file" 2>/dev/null; then
113
- echo " Potential secret found in $file matching pattern"
114
- exit 1
115
- fi
116
- done
117
- done
118
-
119
- - name: compile-check
120
- trigger: pre-commit
121
- description: "Detect project type and run appropriate compile/build check"
122
- filename: pre-commit-compile.sh
123
- script: |
124
- #!/bin/bash
125
- echo "🔨 Running build check..."
126
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
127
- STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
128
- if [ -n "$STAGED_PY" ]; then
129
- for file in $STAGED_PY; do
130
- python -m py_compile "$file" 2>&1
131
- if [ $? -ne 0 ]; then
132
- echo " Syntax error in $file"
133
- exit 1
134
- fi
135
- done
136
- echo " ✅ Python syntax OK"
137
- fi
138
- fi
139
- if [ -f "tsconfig.json" ]; then
140
- npx tsc --noEmit 2>&1
141
- if [ $? -ne 0 ]; then
142
- echo "❌ TypeScript compilation failed."
143
- exit 1
144
- fi
145
- echo " TypeScript compilation OK"
146
- fi
147
- echo "🔨 Build check passed"
148
-
149
- - name: tdd-phase-gate
150
- trigger: pre-commit
151
- description: "RED gate: test-only commits must have failing tests; warn on implementation without tests"
152
- filename: pre-commit-tdd-check.sh
153
- script: |
154
- #!/bin/bash
155
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
156
- if [ -z "$STAGED" ]; then exit 0; fi
157
- SRC_PATTERNS='^(src|lib|app|server|client|pkg|internal|cmd)/'
158
- TEST_PATTERNS='^(tests?|spec|__tests__)/'
159
- TEST_FILE_PATTERNS='\.(test|spec)\.(ts|tsx|js|jsx|mjs|py|go|rb|java|kt)$'
160
- SRC_FILES=()
161
- TEST_FILES=()
162
- while IFS= read -r file; do
163
- if echo "$file" | grep -qE "$SRC_PATTERNS"; then
164
- SRC_FILES+=("$file")
165
- elif echo "$file" | grep -qE "$TEST_PATTERNS"; then
166
- TEST_FILES+=("$file")
167
- elif echo "$file" | grep -qE "$TEST_FILE_PATTERNS"; then
168
- TEST_FILES+=("$file")
169
- fi
170
- done <<< "$STAGED"
171
- SRC_COUNT=${#SRC_FILES[@]}
172
- TEST_COUNT=${#TEST_FILES[@]}
173
- # RED gate: test-only commit must have at least one failing test
174
- if [ "$TEST_COUNT" -gt 0 ] && [ "$SRC_COUNT" -eq 0 ]; then
175
- echo "🔴 TDD gate: test-only commit running staged tests..."
176
- RUN_CMD=""
177
- if [ -f "package.json" ]; then
178
- if grep -q '"vitest"' package.json 2>/dev/null; then
179
- RUN_CMD="npx vitest run"
180
- elif grep -q '"jest"' package.json 2>/dev/null; then
181
- RUN_CMD="npx jest --passWithNoTests"
182
- fi
183
- elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
184
- RUN_CMD="python -m pytest"
185
- fi
186
- if [ -n "$RUN_CMD" ]; then
187
- $RUN_CMD "${TEST_FILES[@]}" > /tmp/tdd-check-output.txt 2>&1
188
- if [ $? -eq 0 ]; then
189
- echo "❌ TDD RED gate violation: all staged tests PASS"
190
- echo " A test-only commit must contain at least one failing test."
191
- echo " Either tests were written after implementation, or assertions are vacuous."
192
- cat /tmp/tdd-check-output.txt | head -30
193
- exit 1
194
- fi
195
- echo " ✅ RED gate satisfied — staged tests fail as expected"
196
- else
197
- echo " ⚠️ Cannot detect test runner — skipping RED gate check"
198
- fi
199
- fi
200
- # Source gate: warn on implementation without tests
201
- if [ "$SRC_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
202
- echo "⚠️ TDD warning: implementation committed without test changes"
203
- echo " Verify a preceding test(scope): [RED] commit exists in this branch."
204
- fi
205
- exit 0
206
-
207
- - name: test-coverage
208
- trigger: pre-commit
209
- description: "Run tests for test-only commits; skip for docs/config-only commits; defer to coverage-gate when src/ staged"
210
- filename: pre-commit-test.sh
211
- script: |
212
- #!/bin/bash
213
- COVERAGE_MIN={{coverage_minimum | default: 80}}
214
-
215
- # Collect staged files once.
216
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
217
-
218
- # Determine what kinds of files are staged.
219
- SRC_STAGED=0
220
- CODE_STAGED=0
221
- while IFS= read -r file; do
222
- if echo "$file" | grep -qE '^src/'; then
223
- SRC_STAGED=1
224
- CODE_STAGED=1
225
- elif echo "$file" | grep -qE '^tests?/'; then
226
- CODE_STAGED=1
227
- fi
228
- done <<< "$STAGED"
229
-
230
- # If src/ is staged the coverage-gate hook runs the full test + coverage pass.
231
- # Skip the bare run here to avoid running the full suite twice.
232
- if [ "$SRC_STAGED" -eq 1 ]; then
233
- echo "🧪 src/ files staged — tests will run via coverage gate, skipping bare run."
234
- exit 0
235
- fi
236
-
237
- # If no code files at all are staged (docs-only, config-only, etc.) skip the run.
238
- if [ "$CODE_STAGED" -eq 0 ]; then
239
- echo "🧪 No code files staged — skipping test run."
240
- exit 0
241
- fi
242
-
243
- echo "🧪 Running tests..."
244
- if [ -f "package.json" ]; then
245
- if grep -q '"vitest"' package.json 2>/dev/null; then
246
- npx vitest run --reporter=verbose 2>&1
247
- if [ $? -ne 0 ]; then
248
- echo "❌ Tests failed."
249
- exit 1
250
- fi
251
- echo " Tests passed"
252
- elif grep -q '"jest"' package.json 2>/dev/null; then
253
- npx jest --passWithNoTests --coverage \
254
- --coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN}}" \
255
- --silent 2>&1
256
- if [ $? -ne 0 ]; then
257
- echo "❌ Jest tests failed or coverage below ${COVERAGE_MIN}%."
258
- exit 1
259
- fi
260
- echo " ✅ Jest tests passed"
261
- fi
262
- fi
263
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
264
- if command -v pytest &> /dev/null; then
265
- pytest --tb=short --quiet --cov=src --cov-fail-under=$COVERAGE_MIN 2>&1
266
- if [ $? -ne 0 ]; then
267
- echo "❌ Tests failed or coverage below ${COVERAGE_MIN}%."
268
- exit 1
269
- fi
270
- echo " Python tests passed"
271
- fi
272
- fi
273
- echo "🧪 All tests passed"
274
-
275
- - name: coverage-gate
276
- trigger: pre-commit
277
- description: "Enforce minimum coverage thresholds when src/ files are staged"
278
- filename: pre-commit-coverage.sh
279
- script: |
280
- #!/bin/bash
281
- # ──────────────────────────────────────────────────────────────────────
282
- # Pre-Commit Hook: Coverage Gate
283
- #
284
- # Enforces minimum coverage thresholds before a commit lands.
285
- # Thresholds are read from vitest.config.ts (via --coverage).
286
- # Only runs when files under src/ are staged — skips for
287
- # docs-only, config-only, or test-only commits.
288
- #
289
- # Current thresholds (vitest.config.ts):
290
- # Lines / Statements / Functions: {{coverage_minimum | default: 80}}%
291
- # Branches: 70%
292
- #
293
- # Trigger: git pre-commit (via scripts/setup-hooks.sh)
294
- # Exit: 1 blocks commit, 0 allows
295
- # ──────────────────────────────────────────────────────────────────────
296
-
297
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
298
-
299
- if [ -z "$STAGED" ]; then
300
- exit 0
301
- fi
302
-
303
- # ── Check if any src/ file is staged ─────────────────────────────────
304
- SRC_STAGED=0
305
- while IFS= read -r file; do
306
- if echo "$file" | grep -qE '^src/'; then
307
- SRC_STAGED=1
308
- break
309
- fi
310
- done <<< "$STAGED"
311
-
312
- if [ "$SRC_STAGED" -eq 0 ]; then
313
- echo "📊 Coverage gate: no src/ files staged, skipping."
314
- exit 0
315
- fi
316
-
317
- # ── Run coverage ───────────────────────────────────────────────────────
318
- echo "📊 Running coverage gate (src/ files staged)..."
319
-
320
- if [ ! -f "package.json" ]; then
321
- echo " ⚠️ No package.json found — skipping coverage check."
322
- exit 0
323
- fi
324
-
325
- if grep -q '"vitest"' package.json 2>/dev/null; then
326
- OUTPUT=$(npx vitest run --coverage --reporter=verbose 2>&1)
327
- EXIT_CODE=$?
328
-
329
- if [ $EXIT_CODE -ne 0 ]; then
330
- echo "$OUTPUT" | grep -E "ERROR|Coverage|does not meet|%|FAIL|passed|failed" | head -40
331
- echo ""
332
- echo "❌ Coverage gate failed — thresholds not met."
333
- echo " Run 'npx vitest run --coverage' locally to see the full report."
334
- echo " Add tests until coverage meets the configured minimums."
335
- exit 1
336
- fi
337
- echo " ✅ Coverage gate passed"
338
- exit 0
339
- fi
340
-
341
- if grep -q '"jest"' package.json 2>/dev/null; then
342
- COVERAGE_MIN={{coverage_minimum | default: 80}}
343
- npx jest --passWithNoTests --coverage \
344
- --coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN,\"statements\":$COVERAGE_MIN,\"functions\":$COVERAGE_MIN,\"branches\":70}}" \
345
- --silent 2>&1
346
- if [ $? -ne 0 ]; then
347
- echo "❌ Coverage gate failed — thresholds not met."
348
- exit 1
349
- fi
350
- echo " ✅ Coverage gate passed"
351
- exit 0
352
- fi
353
-
354
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
355
- if command -v pytest &> /dev/null; then
356
- pytest --tb=no --quiet --cov=src --cov-fail-under={{coverage_minimum | default: 80}} 2>&1
357
- if [ $? -ne 0 ]; then
358
- echo "❌ Coverage gate failed — below {{coverage_minimum | default: 80}}%."
359
- exit 1
360
- fi
361
- echo " ✅ Coverage gate passed"
362
- fi
363
- fi
364
-
365
- exit 0
366
-
367
- - name: anti-pattern-detector
368
- trigger: pre-commit
369
- description: "Scan source files for production code anti-patterns. Respects .forgecraft/exceptions.json for recorded false positives."
370
- filename: pre-commit-prod-quality.sh
371
- checks:
372
- - Hardcoded URLs/hosts (localhost, 127.0.0.1)
373
- - Mock/stub data in production code
374
- - Direct DB calls in route/controller layer (layer violation)
375
- - Bare Error throws (custom hierarchy recommended)
376
- - .env vs .env.example drift (warns on missing variables)
377
- script: |
378
- #!/bin/bash
379
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
380
- SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(py|ts|tsx|js|jsx)$' | grep -vE '(test_|\.test\.|\.spec\.|__tests__|tests/|fixtures/|mock|conftest)')
381
- if [ -z "$SOURCE_FILES" ]; then exit 0; fi
382
- VIOLATIONS=0
383
- WARNINGS=0
384
- # Check if a file is covered by a hook exception in .forgecraft/exceptions.json
385
- # Usage: is_excepted "layer-boundary" "src/migrations/001.ts"
386
- # Add entries to .forgecraft/exceptions.json to record known false positives.
387
- is_excepted() {
388
- local hook_name="$1"
389
- local file_path="$2"
390
- if [ ! -f ".forgecraft/exceptions.json" ]; then return 1; fi
391
- node -e "
392
- const fs = require('fs');
393
- const data = JSON.parse(fs.readFileSync('.forgecraft/exceptions.json', 'utf-8'));
394
- const exc = (data.exceptions || []).find(e => {
395
- if (e.hook !== '$hook_name') return false;
396
- const pat = e.pattern.replace(/\\/g, '/').replace(/\./g, '\\\\.').replace(/\*\*/g, '<<<D>>>').replace(/\*/g, '[^/]*').replace(/<<<D>>>/g, '.*');
397
- return new RegExp('^' + pat + '$').test('$file_path'.replace(/\\\\/g, '/'));
398
- });
399
- if (exc) { console.log('EXCEPTED: ' + exc.reason); process.exit(0); }
400
- process.exit(1);
401
- " 2>/dev/null
402
- }
403
- echo "🔍 Scanning for production code anti-patterns..."
404
- for file in $SOURCE_FILES; do
405
- if echo "$file" | grep -vqE '(config|settings|\.env)'; then
406
- if grep -nE '(localhost|127\.0\.0\.1|0\.0\.0\.0)' "$file" | grep -vE '(#|//|""")' > /tmp/violations 2>/dev/null; then
407
- if [ -s /tmp/violations ]; then
408
- echo " ❌ $file — hardcoded URL/host"
409
- VIOLATIONS=$((VIOLATIONS + 1))
410
- fi
411
- fi
412
- fi
413
- if ! is_excepted "anti-pattern/mock-data" "$file"; then
414
- if grep -nEi '\b(mock_data|fake_data|dummy_data|stub_response)' "$file" > /tmp/violations 2>/dev/null; then
415
- if [ -s /tmp/violations ]; then
416
- echo " ❌ $file — mock/stub data in production code"
417
- VIOLATIONS=$((VIOLATIONS + 1))
418
- fi
419
- fi
420
- fi
421
- # Layer boundary: no direct DB/ORM imports from route handlers / controllers
422
- if echo "$file" | grep -qE '(routes|controllers|handlers|endpoints)'; then
423
- if ! is_excepted "layer-boundary" "$file"; then
424
- if grep -nE '\b(prisma\.|knex\(|mongoose\.|sequelize\.|db\.query|pool\.query)' "$file" > /tmp/violations 2>/dev/null; then
425
- if [ -s /tmp/violations ]; then
426
- echo " $file direct DB call in route/controller (layer violation)"
427
- VIOLATIONS=$((VIOLATIONS + 1))
428
- fi
429
- fi
430
- fi
431
- fi
432
- # Bare Error throws in business logic (not test files)
433
- if ! is_excepted "error-hierarchy" "$file"; then
434
- if grep -nE 'throw new Error\(' "$file" > /tmp/violations 2>/dev/null; then
435
- if [ -s /tmp/violations ]; then
436
- echo " ⚠️ $file bare 'throw new Error()' found use custom error hierarchy"
437
- WARNINGS=$((WARNINGS + 1))
438
- fi
439
- fi
440
- fi
441
- LINE_COUNT=$(wc -l < "$file")
442
- if [ "$LINE_COUNT" -gt {{max_file_length | default: 300}} ]; then
443
- echo " ⚠️ $file $LINE_COUNT lines (max {{max_file_length | default: 300}})"
444
- WARNINGS=$((WARNINGS + 1))
445
- fi
446
- done
447
- rm -f /tmp/violations
448
- if [ $VIOLATIONS -gt 0 ]; then
449
- echo "❌ $VIOLATIONS violation(s) found — commit blocked."
450
- exit 1
451
- fi
452
- if [ $WARNINGS -gt 0 ]; then
453
- echo "⚠️ $WARNINGS warning(s) found — review recommended."
454
- fi
455
- echo "🔍 Production quality scan passed"
456
-
457
- - name: function-length
458
- trigger: pre-commit
459
- description: "Warn when staged functions exceed the configured maximum line count"
460
- filename: pre-commit-function-length.sh
461
- script: |
462
- #!/bin/bash
463
- MAX_LENGTH={{max_function_length | default: 50}}
464
- STAGED=$(git diff --cached --name-only --diff-filter=ACM)
465
- SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(ts|tsx|js|jsx)$' | grep -vE '(\.test\.|\.spec\.|__tests__|tests/)')
466
- if [ -z "$SOURCE_FILES" ]; then exit 0; fi
467
- WARNINGS=0
468
- for file in $SOURCE_FILES; do
469
- # Heuristic: find function/method declarations and count lines to next declaration or closing brace
470
- awk -v max="$MAX_LENGTH" -v fname="$file" '
471
- /^[[:space:]]*(export )?(async )?(function |const [a-zA-Z]+ = (async )?\(|[a-zA-Z]+\(.*\) \{|[a-zA-Z]+\(.*\): )/ {
472
- if (start > 0 && NR - start > max) {
473
- printf " ⚠️ %s:%d function starting here is %d lines (max %d)\n", fname, start, NR - start, max
474
- warnings++
475
- }
476
- start = NR
477
- }
478
- END {
479
- if (start > 0 && NR - start > max) {
480
- printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
481
- warnings++
482
- }
483
- }
484
- ' "$file"
485
- WARNINGS=$((WARNINGS + $?))
486
- done
487
- # Warning only does not block commit since bash heuristics aren't perfect
488
- exit 0
489
-
490
- - name: import-cycle-detector
491
- trigger: pre-commit
492
- description: "Detect circular import dependencies in TypeScript and Python projects"
493
- filename: pre-commit-import-cycles.sh
494
- script: |
495
- #!/bin/bash
496
- echo "🔄 Checking for circular imports..."
497
- if [ -f "tsconfig.json" ]; then
498
- if command -v npx &> /dev/null; then
499
- RESULT=$(npx --yes madge --circular --extensions ts src/ 2>&1)
500
- if echo "$RESULT" | grep -q "Found.*circular"; then
501
- echo "❌ Circular imports detected in TypeScript:"
502
- echo "$RESULT"
503
- exit 1
504
- fi
505
- echo " ✅ No circular imports (TypeScript)"
506
- fi
507
- fi
508
- if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
509
- if command -v lint-imports &> /dev/null; then
510
- lint-imports 2>&1
511
- if [ $? -ne 0 ]; then
512
- echo "❌ Circular imports detected in Python"
513
- exit 1
514
- fi
515
- echo " No circular imports (Python)"
516
- fi
517
- fi
518
- echo "🔄 Import cycle check passed"
519
-
520
- - name: code-review
521
- trigger: pre-commit
522
- description: "AI assistant reviews diff against project standards"
523
- filename: pre-commit-review.sh
524
- script: |
525
- #!/bin/bash
526
- DIFF=$(git diff --cached)
527
- if [ -z "$DIFF" ]; then exit 0; fi
528
- echo "📝 Staged changes ready for review"
529
- # Full auto-review requires claude CLI integration
530
- # This hook validates the diff is non-empty and staged
531
- 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
+ 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