forgecraft-mcp 1.2.0 → 1.3.2

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 (136) hide show
  1. package/README.md +525 -525
  2. package/dist/cli/help.js +44 -44
  3. package/dist/registry/renderer-skeletons.js +92 -92
  4. package/dist/shared/gs-score-logger.js +6 -6
  5. package/dist/tools/add-module.js +123 -123
  6. package/dist/tools/advice-registry.js +18 -18
  7. package/dist/tools/check-cascade-report.js +64 -64
  8. package/dist/tools/configure-mcp.d.ts +3 -0
  9. package/dist/tools/configure-mcp.d.ts.map +1 -1
  10. package/dist/tools/configure-mcp.js +10 -0
  11. package/dist/tools/configure-mcp.js.map +1 -1
  12. package/dist/tools/forgecraft-dispatch.d.ts.map +1 -1
  13. package/dist/tools/forgecraft-dispatch.js +3 -0
  14. package/dist/tools/forgecraft-dispatch.js.map +1 -1
  15. package/dist/tools/forgecraft-schema-params.d.ts +9 -0
  16. package/dist/tools/forgecraft-schema-params.d.ts.map +1 -1
  17. package/dist/tools/forgecraft-schema-params.js +21 -0
  18. package/dist/tools/forgecraft-schema-params.js.map +1 -1
  19. package/dist/tools/forgecraft-schema.d.ts +9 -0
  20. package/dist/tools/forgecraft-schema.d.ts.map +1 -1
  21. package/dist/tools/refresh-output.js +14 -14
  22. package/dist/tools/scaffold-spec-stubs.js +115 -115
  23. package/dist/tools/scaffold-templates.js +62 -62
  24. package/dist/tools/setup-artifact-writers.d.ts +30 -0
  25. package/dist/tools/setup-artifact-writers.d.ts.map +1 -1
  26. package/dist/tools/setup-artifact-writers.js +120 -8
  27. package/dist/tools/setup-artifact-writers.js.map +1 -1
  28. package/dist/tools/setup-phase1.d.ts +3 -0
  29. package/dist/tools/setup-phase1.d.ts.map +1 -1
  30. package/dist/tools/setup-phase1.js +79 -35
  31. package/dist/tools/setup-phase1.js.map +1 -1
  32. package/dist/tools/setup-phase2.d.ts +2 -0
  33. package/dist/tools/setup-phase2.d.ts.map +1 -1
  34. package/dist/tools/setup-phase2.js +10 -1
  35. package/dist/tools/setup-phase2.js.map +1 -1
  36. package/dist/tools/setup-project.d.ts +18 -0
  37. package/dist/tools/setup-project.d.ts.map +1 -1
  38. package/dist/tools/setup-project.js +77 -1
  39. package/dist/tools/setup-project.js.map +1 -1
  40. package/dist/tools/spec-parser-tags.d.ts +9 -0
  41. package/dist/tools/spec-parser-tags.d.ts.map +1 -1
  42. package/dist/tools/spec-parser-tags.js +92 -0
  43. package/dist/tools/spec-parser-tags.js.map +1 -1
  44. package/package.json +89 -86
  45. package/templates/analytics/instructions.yaml +37 -37
  46. package/templates/analytics/mcp-servers.yaml +11 -11
  47. package/templates/analytics/structure.yaml +25 -25
  48. package/templates/api/instructions.yaml +231 -231
  49. package/templates/api/mcp-servers.yaml +22 -13
  50. package/templates/api/nfr.yaml +23 -23
  51. package/templates/api/review.yaml +103 -103
  52. package/templates/api/structure.yaml +34 -34
  53. package/templates/api/verification.yaml +132 -132
  54. package/templates/cli/instructions.yaml +31 -31
  55. package/templates/cli/mcp-servers.yaml +11 -11
  56. package/templates/cli/review.yaml +53 -53
  57. package/templates/cli/structure.yaml +16 -16
  58. package/templates/data-lineage/instructions.yaml +28 -28
  59. package/templates/data-lineage/mcp-servers.yaml +22 -22
  60. package/templates/data-pipeline/instructions.yaml +84 -84
  61. package/templates/data-pipeline/mcp-servers.yaml +13 -13
  62. package/templates/data-pipeline/nfr.yaml +39 -39
  63. package/templates/data-pipeline/structure.yaml +23 -23
  64. package/templates/fintech/hooks.yaml +55 -55
  65. package/templates/fintech/instructions.yaml +112 -112
  66. package/templates/fintech/mcp-servers.yaml +13 -13
  67. package/templates/fintech/nfr.yaml +46 -46
  68. package/templates/fintech/playbook.yaml +210 -210
  69. package/templates/fintech/verification.yaml +239 -239
  70. package/templates/game/instructions.yaml +289 -289
  71. package/templates/game/mcp-servers.yaml +38 -38
  72. package/templates/game/nfr.yaml +64 -64
  73. package/templates/game/playbook.yaml +214 -214
  74. package/templates/game/review.yaml +97 -97
  75. package/templates/game/structure.yaml +67 -67
  76. package/templates/game/verification.yaml +174 -174
  77. package/templates/healthcare/instructions.yaml +42 -42
  78. package/templates/healthcare/mcp-servers.yaml +13 -13
  79. package/templates/healthcare/nfr.yaml +47 -47
  80. package/templates/hipaa/instructions.yaml +41 -41
  81. package/templates/hipaa/mcp-servers.yaml +13 -13
  82. package/templates/infra/instructions.yaml +104 -104
  83. package/templates/infra/mcp-servers.yaml +20 -20
  84. package/templates/infra/nfr.yaml +46 -46
  85. package/templates/infra/review.yaml +65 -65
  86. package/templates/infra/structure.yaml +25 -25
  87. package/templates/library/instructions.yaml +36 -36
  88. package/templates/library/mcp-servers.yaml +20 -20
  89. package/templates/library/review.yaml +56 -56
  90. package/templates/library/structure.yaml +19 -19
  91. package/templates/medallion-architecture/instructions.yaml +41 -41
  92. package/templates/medallion-architecture/mcp-servers.yaml +22 -22
  93. package/templates/ml/instructions.yaml +85 -85
  94. package/templates/ml/mcp-servers.yaml +11 -11
  95. package/templates/ml/nfr.yaml +39 -39
  96. package/templates/ml/structure.yaml +25 -25
  97. package/templates/ml/verification.yaml +156 -156
  98. package/templates/mobile/instructions.yaml +44 -44
  99. package/templates/mobile/mcp-servers.yaml +11 -11
  100. package/templates/mobile/nfr.yaml +49 -49
  101. package/templates/mobile/structure.yaml +27 -27
  102. package/templates/mobile/verification.yaml +121 -121
  103. package/templates/observability-xray/instructions.yaml +40 -40
  104. package/templates/observability-xray/mcp-servers.yaml +15 -15
  105. package/templates/realtime/instructions.yaml +42 -42
  106. package/templates/realtime/mcp-servers.yaml +13 -13
  107. package/templates/soc2/instructions.yaml +41 -41
  108. package/templates/soc2/mcp-servers.yaml +24 -24
  109. package/templates/social/instructions.yaml +43 -43
  110. package/templates/social/mcp-servers.yaml +24 -24
  111. package/templates/state-machine/instructions.yaml +42 -42
  112. package/templates/state-machine/mcp-servers.yaml +11 -11
  113. package/templates/tools-registry.yaml +164 -164
  114. package/templates/universal/hooks.yaml +531 -531
  115. package/templates/universal/instructions.yaml +1692 -1692
  116. package/templates/universal/mcp-servers.yaml +50 -50
  117. package/templates/universal/nfr.yaml +197 -197
  118. package/templates/universal/reference.yaml +326 -326
  119. package/templates/universal/review.yaml +204 -204
  120. package/templates/universal/skills.yaml +262 -262
  121. package/templates/universal/structure.yaml +67 -67
  122. package/templates/universal/verification.yaml +416 -416
  123. package/templates/web-react/hooks.yaml +44 -44
  124. package/templates/web-react/instructions.yaml +207 -207
  125. package/templates/web-react/mcp-servers.yaml +20 -20
  126. package/templates/web-react/nfr.yaml +27 -27
  127. package/templates/web-react/review.yaml +94 -94
  128. package/templates/web-react/structure.yaml +46 -46
  129. package/templates/web-react/verification.yaml +126 -126
  130. package/templates/web-static/instructions.yaml +115 -115
  131. package/templates/web-static/mcp-servers.yaml +20 -20
  132. package/templates/web3/instructions.yaml +44 -44
  133. package/templates/web3/mcp-servers.yaml +11 -11
  134. package/templates/web3/verification.yaml +159 -159
  135. package/templates/zero-trust/instructions.yaml +41 -41
  136. package/templates/zero-trust/mcp-servers.yaml +15 -15
@@ -1,531 +1,531 @@
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
+ # 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