xp-gate 0.5.1

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 (90) hide show
  1. package/adapter-common.sh +192 -0
  2. package/adapters/cpp.sh +76 -0
  3. package/adapters/dart.sh +41 -0
  4. package/adapters/flutter.sh +41 -0
  5. package/adapters/go.sh +59 -0
  6. package/adapters/iac.sh +189 -0
  7. package/adapters/java.sh +191 -0
  8. package/adapters/kotlin.sh +77 -0
  9. package/adapters/objectivec.sh +38 -0
  10. package/adapters/powershell.sh +138 -0
  11. package/adapters/python.sh +104 -0
  12. package/adapters/shell.sh +55 -0
  13. package/adapters/swift.sh +44 -0
  14. package/adapters/typescript.sh +61 -0
  15. package/bin/xp-gate.js +157 -0
  16. package/hooks/adapter-common.sh +192 -0
  17. package/hooks/pre-commit +1667 -0
  18. package/hooks/pre-push +395 -0
  19. package/lib/__tests__/detect-deps.test.js +209 -0
  20. package/lib/__tests__/doctor.test.js +448 -0
  21. package/lib/__tests__/download-skill.test.js +281 -0
  22. package/lib/__tests__/init.test.js +327 -0
  23. package/lib/__tests__/install-skill.test.js +326 -0
  24. package/lib/__tests__/migrate.test.js +212 -0
  25. package/lib/__tests__/rollback.test.js +183 -0
  26. package/lib/__tests__/ui-detector.test.ts +200 -0
  27. package/lib/__tests__/uninstall-skill.test.js +189 -0
  28. package/lib/__tests__/uninstall.test.js +589 -0
  29. package/lib/__tests__/update-skill.test.js +276 -0
  30. package/lib/detect-deps.js +157 -0
  31. package/lib/doctor.js +370 -0
  32. package/lib/download-skill.js +96 -0
  33. package/lib/init.js +367 -0
  34. package/lib/install-skill.js +184 -0
  35. package/lib/migrate.js +120 -0
  36. package/lib/rollback.js +78 -0
  37. package/lib/ui-detector.ts +99 -0
  38. package/lib/uninstall-skill.js +69 -0
  39. package/lib/uninstall.js +401 -0
  40. package/lib/update-skill.js +90 -0
  41. package/package.json +39 -0
  42. package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
  44. package/plugins/claude-code/bin/xp-gate-check +47 -0
  45. package/plugins/claude-code/hooks/hooks.json +37 -0
  46. package/skills/delphi-review/.delphi-config.json.example +45 -0
  47. package/skills/delphi-review/AGENTS.md +54 -0
  48. package/skills/delphi-review/INSTALL.md +152 -0
  49. package/skills/delphi-review/SKILL.md +371 -0
  50. package/skills/delphi-review/evals/evals.json +82 -0
  51. package/skills/delphi-review/opencode.json.delphi.example +56 -0
  52. package/skills/delphi-review/references/code-walkthrough.md +486 -0
  53. package/skills/ralph-loop/SKILL.md +330 -0
  54. package/skills/ralph-loop/evals/evals.json +311 -0
  55. package/skills/ralph-loop/evolution-history.json +59 -0
  56. package/skills/ralph-loop/evolution-log.md +16 -0
  57. package/skills/ralph-loop/references/components/memory.md +55 -0
  58. package/skills/ralph-loop/references/components/middleware.md +54 -0
  59. package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  60. package/skills/ralph-loop/references/components/system-prompt.md +24 -0
  61. package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  62. package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  63. package/skills/ralph-loop/templates/progress-log.md +36 -0
  64. package/skills/sprint-flow/SKILL.md +600 -0
  65. package/skills/sprint-flow/evals/evals.json +78 -0
  66. package/skills/sprint-flow/evolution-history.json +39 -0
  67. package/skills/sprint-flow/evolution-log.md +23 -0
  68. package/skills/sprint-flow/references/components/memory.md +87 -0
  69. package/skills/sprint-flow/references/components/middleware.md +72 -0
  70. package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  71. package/skills/sprint-flow/references/components/system-prompt.md +27 -0
  72. package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  73. package/skills/sprint-flow/references/phase-0-think.md +115 -0
  74. package/skills/sprint-flow/references/phase-1-plan.md +178 -0
  75. package/skills/sprint-flow/references/phase-2-build.md +198 -0
  76. package/skills/sprint-flow/references/phase-3-review.md +213 -0
  77. package/skills/sprint-flow/references/phase-4-uat.md +125 -0
  78. package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  79. package/skills/sprint-flow/references/phase-6-ship.md +193 -0
  80. package/skills/sprint-flow/references/phase-7-land.md +140 -0
  81. package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  82. package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  83. package/skills/sprint-flow/templates/pain-document-template.md +115 -0
  84. package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  85. package/skills/test-specification-alignment/AGENTS.md +59 -0
  86. package/skills/test-specification-alignment/SKILL.md +605 -0
  87. package/skills/test-specification-alignment/evals/evals.json +75 -0
  88. package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  89. package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  90. package/skills/test-specification-alignment/references/specification-format.md +348 -0
@@ -0,0 +1,1667 @@
1
+ #!/bin/bash
2
+ # OpenCode Quality Gates - Pre-Commit Hook - Refactored 6-Type-Based Gates
3
+ #
4
+ # DESIGN PRINCIPLE: Zero tolerance - tool unavailable = BLOCK (per CODE-OF-CONDUCT.md)
5
+ # Each gate requires its tools to be installed; missing tools block the commit.
6
+ #
7
+ # Converts 9 sequential gates into 6 type-based comprehensive gates
8
+ # Sourced from adapter-common.sh and language-specific adapters
9
+
10
+ # Source the common adapter functions — 3-tier resolution:
11
+ # 1. Global: ~/.config/xp-gate/adapters (for core.hooksPath global setup)
12
+ # 2. Project-local: <repo>/githooks/ (for per-project init)
13
+ # 3. Script dir: hooks directory containing this script (fallback)
14
+ ADAPTER_DIR=""
15
+ GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"
16
+ PROJECT_GITHOOKS="$(git rev-parse --show-toplevel 2>/dev/null)/githooks"
17
+
18
+ if [ -f "$GLOBAL_ADAPTER_DIR/adapter-common.sh" ]; then
19
+ ADAPTER_DIR="$GLOBAL_ADAPTER_DIR"
20
+ elif [ -f "$PROJECT_GITHOOKS/adapter-common.sh" ]; then
21
+ ADAPTER_DIR="$PROJECT_GITHOOKS"
22
+ else
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ ADAPTER_DIR="$SCRIPT_DIR"
25
+ fi
26
+ source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
27
+ echo "Error: Cannot source adapter-common.sh from $ADAPTER_DIR"
28
+ echo "Run: xp-gate init (per-project) or xp-gate setup-global (all projects)"
29
+ exit
30
+ }
31
+
32
+ # Trap: ensure quality report generated on ANY exit (pass or fail).
33
+ # Using a guard prevents recursion when 'exit' is called from inside the trap.
34
+ _QUALITY_REPORT_DONE=0
35
+ _quality_report_on_exit() {
36
+ if [ "$_QUALITY_REPORT_DONE" = "1" ]; then
37
+ exit
38
+ return
39
+ fi
40
+ _QUALITY_REPORT_DONE=1
41
+ generate_quality_report
42
+ exit
43
+ }
44
+ trap '_quality_report_on_exit' EXIT
45
+
46
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$(cd "$SCRIPT_DIR/../.." 2>/dev/null || echo "$SCRIPT_DIR/..")" )"
47
+ if [[ -d "$PROJECT_ROOT/node_modules/.bin" ]]; then
48
+ export PATH="$PROJECT_ROOT/node_modules/.bin:$PATH"
49
+ fi
50
+
51
+ # ============================================================================
52
+ # Helper Functions
53
+ # ============================================================================
54
+
55
+ # Parse LCOV file and return coverage percentage
56
+ # Usage: parse_lcov_coverage <lcov_file>
57
+ # Returns: coverage percentage (e.g., "85.5")
58
+ parse_lcov_coverage() {
59
+ local lcov_file="$1"
60
+ local total_lines=0
61
+ local covered_lines=0
62
+
63
+ if [ ! -f "$lcov_file" ]; then
64
+ echo "0"
65
+ return
66
+ fi
67
+
68
+ # Extract LF (Lines Found) and LH (Lines Hit) from lcov.info
69
+ while IFS= read -r line; do
70
+ case "$line" in
71
+ LF:*)
72
+ total_lines=$(echo "$line" | sed 's/LF://')
73
+ ;;
74
+ LH:*)
75
+ covered_lines=$(echo "$line" | sed 's/LH://')
76
+ ;;
77
+ esac
78
+ done < "$lcov_file"
79
+
80
+ if [ "$total_lines" -gt 0 ]; then
81
+ # Calculate percentage using awk for floating point
82
+ awk "BEGIN {printf \"%.1f\", ($covered_lines / $total_lines) * 100}"
83
+ else
84
+ echo "0"
85
+ fi
86
+ }
87
+
88
+ # Run command in subdirectory if PROJECT_SUBDIR is set
89
+ # Usage: run_in_subdir <command>
90
+ run_in_subdir() {
91
+ local cmd="$1"
92
+
93
+ if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ]; then
94
+ # Run command in subdirectory, then return to original directory
95
+ pushd "$PROJECT_SUBDIR" > /dev/null 2>&1
96
+ eval "$cmd"
97
+ local exit_code=$?
98
+ popd > /dev/null 2>&1
99
+ return $exit_code
100
+ else
101
+ # Run in current directory
102
+ eval "$cmd"
103
+ return $?
104
+ fi
105
+ }
106
+
107
+ # Check if file exists in subdirectory
108
+ # Usage: file_exists_in_subdir <filename>
109
+ file_exists_in_subdir() {
110
+ local filename="$1"
111
+
112
+ if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ]; then
113
+ [ -f "$PROJECT_SUBDIR/$filename" ]
114
+ else
115
+ [ -f "$filename" ]
116
+ fi
117
+ }
118
+
119
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
120
+ echo " QUALITY GATES - PRE-COMMIT CHECK"
121
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
122
+ echo "Refactored Structure: 6 Type-Based Gates"
123
+ echo ""
124
+
125
+ # Get list of changed files
126
+ CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
127
+
128
+ if [ -z "$CHANGED_FILES" ]; then
129
+ echo "No files changed. Skipping gates."
130
+ exit 0
131
+ fi
132
+
133
+ # Determine project language stack
134
+ PROJECT_LANG=""
135
+ if [ -f "package.json" ]; then
136
+ # Check if it's a React Native project
137
+ HAS_RN=$(grep -q "react-native" package.json 2>/dev/null && echo "yes" || echo "no")
138
+ HAS_IOS=$([ -d "ios" ] && echo "yes" || echo "no")
139
+ HAS_ANDROID=$([ -d "android" ] && echo "yes" || echo "no")
140
+ if [ "$HAS_RN" = "yes" ] || { [ "$HAS_IOS" = "yes" ] && [ "$HAS_ANDROID" = "yes" ]; }; then
141
+ PROJECT_LANG="react-native"
142
+ else
143
+ PROJECT_LANG="typescript"
144
+ fi
145
+ elif [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
146
+ PROJECT_LANG="python"
147
+ elif [ -f "go.mod" ]; then
148
+ PROJECT_LANG="go"
149
+ elif [ -f "pubspec.yaml" ]; then
150
+ # Check if it's a Flutter project
151
+ if grep -q "flutter:" pubspec.yaml 2>/dev/null || [ -f ".metadata" ]; then
152
+ PROJECT_LANG="flutter"
153
+ else
154
+ PROJECT_LANG="dart"
155
+ fi
156
+ elif [ -n "$(find . -name '*.ps1' -not -path './.git/*' -maxdepth 2 | head -1)" ]; then
157
+ PROJECT_LANG="powershell"
158
+ elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
159
+ CPP_FILES=$(find . -maxdepth 2 -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" -not -path "./.git/*" 2>/dev/null | head -1)
160
+ OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
161
+ if [ -n "$CPP_FILES" ]; then
162
+ PROJECT_LANG="cpp"
163
+ elif [ -n "$OBJC_FILES" ]; then
164
+ PROJECT_LANG="objectivec"
165
+ fi
166
+ elif [ -f "*.xcodeproj" ] || [ -d "*.xcworkspace" ]; then
167
+ # Xcode project - Objective-C or Swift
168
+ OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
169
+ if [ -n "$OBJC_FILES" ]; then
170
+ PROJECT_LANG="objectivec"
171
+ fi
172
+ elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
173
+ # Check if it's Kotlin or Java
174
+ KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" 2>/dev/null | wc -l)
175
+ if [ "$KOTLIN_FILES" -gt 0 ]; then
176
+ PROJECT_LANG="kotlin"
177
+ else
178
+ PROJECT_LANG="java"
179
+ fi
180
+ elif [ -n "$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)" ] || [ -f "*.ps1" ]; then
181
+ PROJECT_LANG="powershell"
182
+ fi
183
+
184
+ # If not found at root, search subdirectories (max depth 2) for project manifests
185
+ # This supports mixed/hybrid project structures like:
186
+ # root: Documentation + scripts
187
+ # subdirectory: Flutter/TypeScript/Python/Go project
188
+ PROJECT_SUBDIR=""
189
+ if [ -z "$PROJECT_LANG" ]; then
190
+ # Search for TypeScript/JavaScript projects (excluding React Native)
191
+ TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
192
+ if [ -n "$TSCONFIG_SUB" ]; then
193
+ PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
194
+ # Check if it's React Native
195
+ if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
196
+ PROJECT_LANG="react-native"
197
+ else
198
+ PROJECT_LANG="typescript"
199
+ fi
200
+ fi
201
+
202
+ # Search for Python projects
203
+ if [ -z "$PROJECT_LANG" ]; then
204
+ PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" 2>/dev/null | head -1)
205
+ if [ -n "$PYPROJECT_SUB" ]; then
206
+ PROJECT_LANG="python"
207
+ PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
208
+ fi
209
+ fi
210
+
211
+ # Search for Go projects
212
+ if [ -z "$PROJECT_LANG" ]; then
213
+ GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" 2>/dev/null | head -1)
214
+ if [ -n "$GOMOD_SUB" ]; then
215
+ PROJECT_LANG="go"
216
+ PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
217
+ fi
218
+ fi
219
+
220
+ # Search for Flutter/Dart projects
221
+ if [ -z "$PROJECT_LANG" ]; then
222
+ PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" 2>/dev/null | head -1)
223
+ if [ -n "$PUBSPEC_SUB" ]; then
224
+ if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
225
+ PROJECT_LANG="flutter"
226
+ else
227
+ PROJECT_LANG="dart"
228
+ fi
229
+ PROJECT_SUBDIR=$(dirname "$PUBSPEC_SUB")
230
+ fi
231
+ fi
232
+
233
+ # Search for Java/Kotlin projects
234
+ if [ -z "$PROJECT_LANG" ]; then
235
+ POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" 2>/dev/null | head -1)
236
+ GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" 2>/dev/null | head -1)
237
+ if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
238
+ MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
239
+ PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
240
+ # Check for Kotlin files in subdirectory
241
+ KOTLIN_COUNT=$(find "$PROJECT_SUBDIR" -name "*.kt" -not -path "*/.git/*" 2>/dev/null | wc -l)
242
+ if [ "$KOTLIN_COUNT" -gt 0 ]; then
243
+ PROJECT_LANG="kotlin"
244
+ else
245
+ PROJECT_LANG="java"
246
+ fi
247
+ fi
248
+ fi
249
+
250
+ # Search for C++ projects
251
+ if [ -z "$PROJECT_LANG" ]; then
252
+ CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" 2>/dev/null | head -1)
253
+ if [ -n "$CMAKE_SUB" ]; then
254
+ PROJECT_LANG="cpp"
255
+ PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
256
+ fi
257
+ fi
258
+
259
+ # Search for React Native projects (ios/ + android/ directories)
260
+ if [ -z "$PROJECT_LANG" ]; then
261
+ RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" 2>/dev/null | head -1)
262
+ if [ -n "$RN_SUB" ]; then
263
+ PROJECT_SUBDIR=$(dirname "$RN_SUB")
264
+ if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
265
+ PROJECT_LANG="react-native"
266
+ fi
267
+ fi
268
+ fi
269
+
270
+ # Search for PowerShell projects
271
+ if [ -z "$PROJECT_LANG" ]; then
272
+ PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)
273
+ if [ -n "$PS_SUB" ]; then
274
+ PROJECT_LANG="powershell"
275
+ PROJECT_SUBDIR=$(dirname "$PS_SUB")
276
+ fi
277
+ fi
278
+
279
+ # Report detected subdirectory project
280
+ if [ -n "$PROJECT_SUBDIR" ] && [ -n "$PROJECT_LANG" ]; then
281
+ echo ""
282
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
283
+ echo " 📁 SUBDIRECTORY PROJECT DETECTED"
284
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
285
+ echo ""
286
+ echo "Language: $PROJECT_LANG"
287
+ echo "Location: $PROJECT_SUBDIR/"
288
+ echo ""
289
+ fi
290
+ fi
291
+
292
+ # Check if this is a documentation-only project (no source code)
293
+ # Such projects have only markdown, yaml, json, skills, docs - no actual code
294
+ if [ -z "$PROJECT_LANG" ]; then
295
+ # Count source code files (including shell scripts)
296
+ CODE_FILES=$(find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.java" -o -name "*.go" -o -name "*.rs" -o -name "*.cpp" -o -name "*.c" -o -name "*.swift" -o -name "*.kt" -o -name "*.sh" -o -name "*.dart" -o -name "*.ps1" \) -not -path "./.git/*" 2>/dev/null | wc -l)
297
+
298
+ if [ "$CODE_FILES" -eq 0 ]; then
299
+ PROJECT_LANG="documentation-only"
300
+ echo ""
301
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
302
+ echo " 📚 DOCUMENTATION-ONLY PROJECT DETECTED"
303
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
304
+ echo ""
305
+ echo "This project contains no source code files."
306
+ echo "Skipping static analysis and test gates."
307
+ echo ""
308
+ echo "Proceeding with documentation-only checks..."
309
+ fi
310
+ fi
311
+
312
+ # Switch to subdirectory if detected (for mixed/hybrid projects)
313
+ # All subsequent gates will run in the subdirectory context
314
+ ORIGINAL_DIR=""
315
+ if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ "$PROJECT_LANG" != "documentation-only" ]; then
316
+ ORIGINAL_DIR=$(pwd)
317
+ cd "$PROJECT_SUBDIR"
318
+ echo "Working in: $PROJECT_SUBDIR/"
319
+ echo ""
320
+ fi
321
+
322
+ # ============================================================================
323
+ # GATE 1: Code Quality (Static Analysis + Linting + Shell Check combined)
324
+ # Combination of: Old Gates 1 (Static), Gate 2 (Linting), Gate 5 (Shell Check)
325
+ # ============================================================================
326
+ 2>&1 echo ""
327
+ 2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
328
+
329
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
330
+ # Documentation-only project - skip static analysis
331
+ echo "✅ PASSED - Skipped (no source code to analyze)."
332
+
333
+ elif [ "$PROJECT_LANG" = "typescript" ]; then
334
+ # TypeScript: static analysis (tsc) + linting (eslint)
335
+ if ! command -v npx &> /dev/null; then
336
+ echo "ℹ️ SKIP - npx not available (non-blocking for TypeScript)"
337
+ echo "✅ PASSED - Code Quality Gate (SKIP)"
338
+ else
339
+ # Run TypeScript type checking
340
+ echo "Running TypeScript static analysis (tsc)..."
341
+ npx tsc --noEmit --skipLibCheck 2>&1 | head -30
342
+ TSC_EXIT=$?
343
+ if [ "$TSC_EXIT" -ne 0 ]; then
344
+ echo ""
345
+ echo "❌ BLOCKED - TYPE ERRORS detected"
346
+ echo "Fix the type errors above before committing."
347
+ exit
348
+ fi
349
+ echo "✅ PASSED - TypeScript static analysis."
350
+
351
+ # Run ESLint linting if config exists
352
+ if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
353
+ echo "Running ESLint linting..."
354
+ npx eslint "$CHANGED_FILES" --ext .js,.jsx,.ts,.tsx --max-warnings 0 2>&1 | head -30
355
+ ESLINT_EXIT=$?
356
+ if [ "$ESLINT_EXIT" -ne 0 ]; then
357
+ echo ""
358
+ echo "❌ BLOCKED - LINT ERRORS detected"
359
+ echo "Fix the lint errors above before committing."
360
+ exit
361
+ fi
362
+ echo "✅ PASSED - ESLint linting."
363
+ else
364
+ echo "ℹ️ No ESLint configuration found - Skipping"
365
+ fi
366
+ fi
367
+
368
+ elif [ "$PROJECT_LANG" = "python" ]; then
369
+ # Python: Ruff (includes syntax + linting) + mypy (optional typing)
370
+ if ! command -v ruff &> /dev/null; then
371
+ echo "ℹ️ SKIP - ruff not available (non-blocking for Python)"
372
+ echo "✅ PASSED - Code Quality Gate (SKIP)"
373
+ else
374
+ # Run Ruff (includes syntax check, linting)
375
+ echo "Running Ruff linting (syntax + lint)..."
376
+ ruff check "$CHANGED_FILES" 2>&1 | head -30
377
+ RUFF_EXIT=$?
378
+ if [ "$RUFF_EXIT" -ne 0 ]; then
379
+ echo ""
380
+ echo "❌ BLOCKED - RUFF ERRORS detected"
381
+ echo "Fix the lint errors above before committing."
382
+ echo "Tip: Run 'ruff check --fix' to auto-fix some issues."
383
+ exit
384
+ fi
385
+ echo "✅ PASSED - Ruff linting."
386
+
387
+ # Optional: Type checking with mypy
388
+ if command -v mypy &> /dev/null; then
389
+ echo "Running mypy type checking (optional)..."
390
+ mypy --ignore-missing-imports $(echo "$CHANGED_FILES" | grep "\.py$" || echo ".") 2>&1 | head -20 | tail -10
391
+ MYPY_EXIT=$?
392
+ if [ "$MYPY_EXIT" -ne 0 ]; then
393
+ echo ""
394
+ echo "❌ BLOCKED - MYPI TYPE ERRORS detected"
395
+ echo "Fix the type errors above before committing."
396
+ exit
397
+ fi
398
+ echo "✅ PASSED - mypy type checking."
399
+ else
400
+ echo "ℹ️ Mypy not available - Skipping optional type check"
401
+ fi
402
+ fi
403
+
404
+ elif [ "$PROJECT_LANG" = "go" ]; then
405
+ # Go: go vet + golangci-lint
406
+ if ! command -v golangci-lint &> /dev/null; then
407
+ echo "ℹ️ SKIP - golangci-lint not available (non-blocking for Go)"
408
+ echo "✅ PASSED - Code Quality Gate (SKIP)"
409
+ else
410
+ echo "Running golangci-lint (comprehensive static analysis)..."
411
+ golangci-lint run 2>&1 | head -30
412
+ GOLANGCI_EXIT=$?
413
+ if [ "$GOLANGCI_EXIT" -ne 0 ]; then
414
+ echo ""
415
+ echo "❌ BLOCKED - GOLANGCI-LINT ERRORS detected"
416
+ echo "Fix the lint errors above before committing."
417
+ exit
418
+ fi
419
+ echo "✅ PASSED - golangci-lint."
420
+ fi
421
+
422
+ elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$' || find . -name "*.sh" -type f | head -n 1; then
423
+ # Shell: For shell files, use shellcheck directly
424
+ if command -v shellcheck &> /dev/null; then
425
+ echo "Running shellcheck on shell scripts..."
426
+ shellcheck $(echo "$CHANGED_FILES" | grep '\.sh$' || echo "."/*.sh ./**/**/*.sh 2>/dev/null) 2>&1 | head -30
427
+ SHELLCHECK_EXIT=$?
428
+ if [ "$SHELLCHECK_EXIT" -ne 0 ]; then
429
+ echo ""
430
+ echo "❌ BLOCKED - SHELLCHECK ERRORS detected"
431
+ echo "Fix the shell script errors above."
432
+ exit
433
+ fi
434
+ echo "✅ PASSED - Shellcheck completed."
435
+ else
436
+ echo "ℹ️ shellcheck not available - Attempting basic syntax check"
437
+ # Basic check for shell script files
438
+ SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || find . -name "*.sh" -type f 2>/dev/null | head -10)
439
+ if [ -n "$SHELL_FILES" ]; then
440
+ for sh_file in $SHELL_FILES; do
441
+ if [ -f "$sh_file" ]; then
442
+ bash -n "$sh_file"
443
+ if [ $? -ne 0 ]; then
444
+ echo "❌ BLOCKED - SYNTAX ERROR in $sh_file"
445
+ exit
446
+ fi
447
+ fi
448
+ done
449
+ echo "✅ PASSED - Basic shell script syntax check."
450
+ else
451
+ echo "✅ PASSED - No shell scripts to check."
452
+ fi
453
+ fi
454
+
455
+ else
456
+ # Route to appropriate adapter for other languages
457
+ if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
458
+ echo "Running static analysis for $PROJECT_LANG..."
459
+ run_static_analysis | head -30 2>/dev/null
460
+ ANALYSIS_EXIT=$?
461
+ if [ "$ANALYSIS_EXIT" -ne 0 ]; then
462
+ echo "⚠️ Static analysis had issues (may be non-blocking)"
463
+ fi
464
+
465
+ echo "Running lint check for $PROJECT_LANG..."
466
+ run_lint | head -30 2>/dev/null
467
+ LINT_EXIT=$?
468
+ if [ "$LINT_EXIT" -ne 0 ]; then
469
+ echo "ℹ️ Lint check had issues (may be non-blocking)"
470
+ fi
471
+
472
+ if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
473
+ echo "✅ PASSED - Language-specific code quality checks."
474
+ else
475
+ # If tools are not available or failed, skip non-blockingly
476
+ echo "✅ PASSED - Code Quality Gate (SKIP for unavailable tools)"
477
+ fi
478
+ else
479
+ # If no adapter exists for this language, skip
480
+ echo "ℹ️ No specific adapter for $PROJECT_LANG - using generic checks if any"
481
+ echo "✅ PASSED - Code Quality Gate (no specific checks available)."
482
+ fi
483
+ fi
484
+ GATE_1_STATUS="PASS"
485
+ GATE_1_TOOL="${PROJECT_LANG}"
486
+
487
+ # ============================================================================
488
+ # GATE 2: Duplicate Code Detection (NEW gate)
489
+ # Uses jscpd, or similar tools, specific to language
490
+ # ============================================================================
491
+ 2>&1 echo ""
492
+ 2>&1 echo "→ Gate 2: Duplicate code detection..."
493
+
494
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
495
+ echo "✅ PASSED - Skipped (no source code to analyze)."
496
+
497
+ elif [ "$PROJECT_LANG" = "typescript" ]; then
498
+ if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
499
+ exit
500
+ fi
501
+
502
+ echo "Running jscpd for duplicate code detection..."
503
+ jscpd --config jscpd.conf.json "$CHANGED_FILES" 2>&1 | head -30
504
+ JSCPD_EXIT=$?
505
+ if [ "$JSCPD_EXIT" -ne 0 ]; then
506
+ echo "ℹ️ jscpd found duplicated code (warning, not blocking by default)"
507
+ echo " Consider refactoring duplicate code blocks."
508
+ fi
509
+ echo "✅ PASSED - jscpd duplicate code check completed."
510
+
511
+ elif [ "$PROJECT_LANG" = "python" ]; then
512
+ if command -v pylint &> /dev/null; then
513
+ echo "Running pylint duplicate detection..."
514
+ pylint --disable=all --enable=duplicate-code $(echo "$CHANGED_FILES" | grep "\.py$")
515
+ DUPLICATE_EXIT=$?
516
+ if [ "$DUPLICATE_EXIT" -ne 0 ]; then
517
+ echo "ℹ️ pylint found duplicate code (warning, not typically blocking)"
518
+ else
519
+ echo "✅ PASSED - pylint found no duplicates."
520
+ fi
521
+ elif require_tool "ruff" "Gate 2" "pip install ruff"; then
522
+ ruff check --select=DUP $(echo "$CHANGED_FILES" | grep "\.py$") 2>&1 | head -30
523
+ echo "✅ PASSED - ruff duplicate code check completed."
524
+ else
525
+ exit
526
+ fi
527
+
528
+ elif [ "$PROJECT_LANG" = "go" ]; then
529
+ if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
530
+ exit
531
+ fi
532
+
533
+ echo "Running jscpd for Go duplicate code detection..."
534
+ FILES_TO_CHECK=$(echo "$CHANGED_FILES" | grep "\.go$" | head -20)
535
+ if [ -n "$FILES_TO_CHECK" ]; then
536
+ jscpd $FILES_TO_CHECK 2>&1 | head -10
537
+ fi
538
+ echo "✅ PASSED - Go duplicate code check completed."
539
+
540
+ elif [ "$PROJECT_LANG" = "powershell" ]; then
541
+ echo "ℹ️ No PowerShell-native duplicate detector (jscpd does not support .ps1)"
542
+ echo "✅ PASSED - Skipped (no standardized tool for PowerShell duplicate detection)"
543
+
544
+ elif [ "$PROJECT_LANG" = "shell" ]; then
545
+ echo "ℹ️ Duplicate detection not typically used for shell scripts"
546
+ echo "✅ PASSED - Skipped (shell scripts, no standardized dup tool)"
547
+
548
+ else
549
+ SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
550
+ if [ -n "$SOURCE_FILES" ]; then
551
+ if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
552
+ exit
553
+ fi
554
+ echo "Running jscpd for duplicate code detection..."
555
+ jscpd $SOURCE_FILES 2>&1 | head -15
556
+ echo "✅ PASSED - jscpd check completed."
557
+ else
558
+ echo "✅ PASSED - No source files detected for duplicate checking."
559
+ fi
560
+ fi
561
+ GATE_2_STATUS="PASS"
562
+
563
+ # ============================================================================
564
+ # GATE 3: Cyclomatic Complexity Check (Old Gate 7)
565
+ # Uses lizard - keeps existing logic for consistency
566
+ # ============================================================================
567
+ 2>&1 echo ""
568
+ 2>&1 echo "→ Gate 3: Cyclomatic complexity..."
569
+
570
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
571
+ echo "✅ PASSED - Skipped (documentation project)."
572
+
573
+ elif [ "$PROJECT_LANG" = "powershell" ]; then
574
+ echo "ℹ️ No PowerShell principles checker available"
575
+ echo "✅ PASSED - Skipped (no PowerShell Clean Code / SOLID tool)"
576
+
577
+ else
578
+ CCN_THRESHOLD=5
579
+ CCN_ERROR_THRESHOLD=10 # Changed from 15 to 10 as per standard practice
580
+
581
+ # Check lizard availability
582
+ LIZARD_CMD=""
583
+ if command -v lizard > /dev/null 2>&1; then
584
+ LIZARD_CMD=lizard
585
+ elif [ -f ~/.local/bin/lizard ]; then
586
+ LIZARD_CMD=~/.local/bin/lizard
587
+ fi
588
+
589
+ if [ -n "$LIZARD_CMD" ]; then
590
+ LIZARD_PATH=$(eval echo "$LIZARD_CMD")
591
+
592
+ # Get changed source files for complexity check
593
+ CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
594
+
595
+ if [ -n "$CC_FILES" ]; then
596
+ echo "Checking complexity for source files..."
597
+
598
+ # Run lizard with CCN threshold
599
+ CC_OUTPUT=$($LIZARD_PATH -C $CCN_THRESHOLD $CC_FILES 2>&1 || true)
600
+
601
+ # Parse warning count from the summary table: "Warning cnt 8"
602
+ # Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
603
+ CC_WARNINGS=$(echo "$CC_OUTPUT" | grep "^Warning cnt" | awk '{print $NF}' | tr -d '[:space:]' | sed 's/[^0-9]//g' || true)
604
+ CC_WARNINGS=${CC_WARNINGS:-0}
605
+
606
+ # Count functions with CCN > CCN_ERROR_THRESHOLD (these block)
607
+ CC_ERRORS=$(echo "$CC_OUTPUT" | awk 'NF>=6 && $2 ~ /^[0-9]+$/ && $NF ~ /@/ && $2+0 > '"$CCN_ERROR_THRESHOLD"'' | sort -u -k5,5 | wc -l)
608
+
609
+ if [ "$CC_ERRORS" -gt 0 ]; then
610
+ echo "$CC_OUTPUT"
611
+ echo ""
612
+ echo "❌ BLOCKED - Functions with CCN > $CCN_ERROR_THRESHOLD found."
613
+ echo "Refactor high-complexity functions to keep below $CCN_ERROR_THRESHOLD complexity."
614
+ exit
615
+ fi
616
+
617
+ if [ "$CC_WARNINGS" -gt 0 ]; then
618
+ echo "$CC_OUTPUT"
619
+ echo ""
620
+ echo "⚠️ Found $CC_WARNINGS functions with CCN > $CCN_THRESHOLD (acceptable warning level)"
621
+ echo "✅ PASSED - Within error threshold (warnings allowed)."
622
+ else
623
+ echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
624
+ fi
625
+ else
626
+ echo "✅ PASSED - No source files to check for complexity."
627
+ fi
628
+ else
629
+ echo "ℹ️ lizard not installed - Skipping complexity check (non-blocking)"
630
+ echo "ℹ️ Install with: pip3 install --user lizard"
631
+ echo "✅ PASSED - Complexity check (SKIP, tool not available)"
632
+ fi
633
+ fi
634
+ GATE_3_STATUS="PASS"
635
+
636
+ # ============================================================================
637
+ # GATE 4: Principles Checker (Old Gate 6 - Clean Code + SOLID)
638
+ # Reuses existing principles checker logic
639
+ # ============================================================================
640
+ 2>&1 echo ""
641
+ 2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
642
+
643
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
644
+ echo "✅ PASSED - Skipped (documentation project)."
645
+
646
+ else
647
+ # Get source files to check against principles
648
+ PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
649
+
650
+ if [ -n "$PRINCIPLES_FILES" ]; then
651
+ # Check if principles checker exists in project
652
+ if [ -f "src/principles/index.ts" ]; then
653
+ echo "Checking Clean Code + SOLID principles..."
654
+
655
+ if command -v npx > /dev/null 2>&1; then
656
+ # Run principles checker and store results
657
+ if npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format json > /tmp/principles-output.json 2>/dev/null; then
658
+ # Check severity levels
659
+ ERROR_COUNT=$(grep -c '"severity":"error"' /tmp/principles-output.json 2>/dev/null || true)
660
+ ERROR_COUNT=${ERROR_COUNT:-0}
661
+ WARNING_COUNT=$(grep -c '"severity":"warning"' /tmp/principles-output.json 2>/dev/null || true)
662
+ WARNING_COUNT=${WARNING_COUNT:-0}
663
+
664
+ if [ "$ERROR_COUNT" -gt 0 ]; then
665
+ echo ""
666
+ echo "❌ BLOCKED - $ERROR_COUNT principle ERROR(S) found"
667
+ echo "Critical violations must be fixed before commit:"
668
+ echo " - error-handling violations"
669
+ echo " - SOLID principle violations"
670
+ echo " - architectural violations"
671
+ npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format console
672
+ exit
673
+ fi
674
+
675
+ echo "✅ PASSED - Principles checker (no errors found)."
676
+ if [ "$WARNING_COUNT" -gt 0 ]; then
677
+ echo "ℹ️ $WARNING_COUNT warnings found (will be handled by Boy Scout Rule)."
678
+ fi
679
+ else
680
+ echo "⚠️ Warning: Principles checker execution failed"
681
+ echo "✅ PASSED - Principles check (SKIP, execution issue)"
682
+ fi
683
+ else
684
+ echo "ℹ️ npx not available - skipping principles check"
685
+ echo "✅ PASSED - Principles check (SKIP, no Node.js)"
686
+ fi
687
+ else
688
+ echo "ℹ️ Principles checker not found in project - skipping"
689
+ echo "✅ PASSED - Principles check (SKIP, not available in project)"
690
+ fi
691
+ else
692
+ echo "✅ PASSED - No source files changed (principles check skipped)."
693
+ fi
694
+ fi
695
+ GATE_4_STATUS="PASS"
696
+
697
+ # ============================================================================
698
+ # GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
699
+ # Uses adapter system to run tests + coverage for language
700
+ # ============================================================================
701
+ 2>&1 echo ""
702
+ 2>&1 echo "→ Gate 5: Tests & coverage..."
703
+
704
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
705
+ echo "✅ PASSED - Skipped (documentation project)."
706
+
707
+ else
708
+ # ========================================================================
709
+ # Gate 5a: Test-Source File Pairing Check (WARNING only)
710
+ # Checks coexistence, not temporal order. True "test-first" enforced by
711
+ # Agent skills (Layer 1 + Layer 4).
712
+ # ========================================================================
713
+ NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' || true)
714
+
715
+ if [ -n "$NEW_SOURCE_FILES" ]; then
716
+ PAIRING_WARNINGS=0
717
+ for src_file in $NEW_SOURCE_FILES; do
718
+ base="${src_file%.*}"
719
+ ext="${src_file##*.}"
720
+ filename="${base##*/}"
721
+ dir="$(dirname "$src_file")"
722
+
723
+ # Skip excluded files: index/barrel, types, interfaces, constants, declarations
724
+ case "$filename" in
725
+ index|types|interfaces|constants|__init__) continue ;;
726
+ esac
727
+ case "$ext" in
728
+ d.ts|pyi) continue ;;
729
+ esac
730
+ # Skip files with // @no-test annotation
731
+ if grep -q '@no-test' "$src_file" 2>/dev/null; then
732
+ continue
733
+ fi
734
+
735
+ # Check common test file patterns (language-specific)
736
+ TEST_FOUND=false
737
+ case "$ext" in
738
+ ts|tsx|js|jsx)
739
+ patterns=(
740
+ "${base}.test.${ext}"
741
+ "${base}.spec.${ext}"
742
+ "${dir}/__tests__/${filename}.test.${ext}"
743
+ "${dir}/__tests__/${filename}.spec.${ext}"
744
+ "tests/${filename}.test.${ext}"
745
+ "tests/${filename}.spec.${ext}"
746
+ )
747
+ ;;
748
+ py)
749
+ patterns=(
750
+ "tests/${filename}_test.py"
751
+ "tests/test_${filename}.py"
752
+ "${dir}/tests/test_${filename}.py"
753
+ "${base}_test.py"
754
+ )
755
+ ;;
756
+ go)
757
+ patterns=(
758
+ "${base}_test.go"
759
+ "${dir}/${filename}_test.go"
760
+ )
761
+ ;;
762
+ java|kt|kts)
763
+ patterns=(
764
+ "${base}Test.${ext}"
765
+ "src/test/java/**/${filename}Test.${ext}"
766
+ "src/test/kotlin/**/${filename}Test.${ext}"
767
+ )
768
+ ;;
769
+ cpp|cc|cxx|c)
770
+ patterns=(
771
+ "${base}_test.${ext}"
772
+ "${base}Test.${ext}"
773
+ "${dir}/test_${filename}.${ext}"
774
+ )
775
+ ;;
776
+ swift)
777
+ patterns=(
778
+ "${base}Tests.swift"
779
+ "${dir}/${filename}Tests.swift"
780
+ )
781
+ ;;
782
+ dart)
783
+ patterns=("${base}_test.${ext}")
784
+ ;;
785
+ rb)
786
+ patterns=(
787
+ "${dir}/test_${filename}.rb"
788
+ "${base}_test.rb"
789
+ )
790
+ ;;
791
+ rs)
792
+ # Rust tests are typically inline with #[cfg(test)]
793
+ continue
794
+ ;;
795
+ *) continue ;;
796
+ esac
797
+
798
+ for pattern in "${patterns[@]}"; do
799
+ if [[ "$pattern" == *'*'* ]]; then
800
+ found=$(find . -path "*/${pattern}" -type f 2>/dev/null | head -1)
801
+ if [ -n "$found" ]; then TEST_FOUND=true; break; fi
802
+ elif [ -f "$pattern" ]; then
803
+ TEST_FOUND=true
804
+ break
805
+ fi
806
+ done
807
+
808
+ if [ "$TEST_FOUND" = false ]; then
809
+ echo "⚠️ TEST PAIRING WARNING: New source file without corresponding test: $src_file"
810
+ echo " Expected patterns for .${ext}: ${patterns[0]}, ${patterns[1]:-...}"
811
+ echo " Or: Add '// @no-test' annotation if this file doesn't need tests"
812
+ PAIRING_WARNINGS=$((PAIRING_WARNINGS + 1))
813
+ fi
814
+ done
815
+
816
+ if [ "$PAIRING_WARNINGS" -gt 0 ]; then
817
+ echo ""
818
+ echo "⚠️ $PAIRING_WARNINGS new source file(s) without corresponding tests"
819
+ echo " This is a WARNING — commit proceeds. TDD order enforced by Agent skills."
820
+ echo " To suppress for specific files, add '// @no-test' at the top."
821
+ fi
822
+ fi
823
+
824
+ # ========================================================================
825
+ # Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
826
+ # 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
827
+ # ========================================================================
828
+ CHANGED_TEST_FILES=$(git diff --cached --name-only | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
829
+
830
+ if [ -n "$CHANGED_TEST_FILES" ]; then
831
+ echo "Checking mock density in test files..."
832
+ for test_file in $CHANGED_TEST_FILES; do
833
+ if [ -f "$test_file" ]; then
834
+ # Count mock keyword references (precise patterns only)
835
+ MOCK_COUNT=0
836
+ for kw in 'jest\.mock' 'vi\.mock' 'jest\.spyOn' 'vi\.spyOn' 'jest\.fn' 'vi\.fn' \
837
+ 'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
838
+ 'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
839
+ '\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
840
+ c=$(grep -o -c "$kw" "$test_file" 2>/dev/null || echo "0")
841
+ MOCK_COUNT=$((MOCK_COUNT + c))
842
+ done
843
+
844
+ # Count total non-empty, non-comment lines for density denominator
845
+ TOTAL_LINES=$(grep -v '^\s*$' "$test_file" | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '^\s*#' | wc -l | awk '{print $1}')
846
+
847
+ if [ "$TOTAL_LINES" -gt 0 ] 2>/dev/null; then
848
+ MOCK_DENSITY=$(awk "BEGIN {printf \"%.1f\", ($MOCK_COUNT / $TOTAL_LINES) * 100}")
849
+ else
850
+ MOCK_DENSITY="0"
851
+ fi
852
+
853
+ THRESHOLD_30=$(awk "BEGIN {print ($MOCK_DENSITY > 30) ? 1 : 0}")
854
+ THRESHOLD_50=$(awk "BEGIN {print ($MOCK_DENSITY > 50) ? 1 : 0}")
855
+
856
+ # Check for @mock-justified annotation with reason text (min 10 chars)
857
+ HAS_JUSTIFIED=$(grep -qE '@mock-justified\s*:\s*.{10,}' "$test_file" 2>/dev/null && echo "true" || echo "false")
858
+
859
+ if [ "$THRESHOLD_50" = "1" ]; then
860
+ if [ "$HAS_JUSTIFIED" = "false" ]; then
861
+ echo "⚠️ MOCK DENSITY: $test_file — ${MOCK_DENSITY}% (exceeds 50%)"
862
+ echo " Consider: integration test with real collaborators"
863
+ echo " Or: Add '// @mock-justified: <reason>' (min 10 char explanation)"
864
+ else
865
+ echo "📝 MOCK DENSITY: $test_file — ${MOCK_DENSITY}% (justified)"
866
+ fi
867
+ elif [ "$THRESHOLD_30" = "1" ]; then
868
+ echo "ℹ️ MOCK DENSITY ADVISORY: $test_file — ${MOCK_DENSITY}% (consider reducing mocks)"
869
+ fi
870
+ fi
871
+ done
872
+ fi
873
+
874
+ # First, run tests using adapter system
875
+ echo "Running tests..."
876
+
877
+ # Route to appropriate test runner via adapter
878
+ if [ "${PROJECT_LANG}" != "unknown" ] && [ -f "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" ]; then
879
+ if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
880
+ TESTS_OUTPUT=$(run_tests 2>&1)
881
+ TESTS_EXIT_CODE=$?
882
+ echo "$TESTS_OUTPUT" | head -50
883
+ if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
884
+ echo ""
885
+ echo "❌ BLOCKED - Tests FAILED"
886
+ exit
887
+ else
888
+ echo "✅ PASSED - Unit tests passed."
889
+ fi
890
+ else
891
+ echo "✅ PASSED - No adapter for tests, assuming none to run."
892
+ fi
893
+ else
894
+ echo "✅ PASSED - No specific adapter, no tests to run."
895
+ fi
896
+
897
+ # Next, run coverage check
898
+ echo "Running coverage check..."
899
+
900
+ if [ "${PROJECT_LANG}" != "unknown" ] && [ -f "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" ]; then
901
+ if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
902
+ case "$PROJECT_LANG" in
903
+ "typescript")
904
+ if [ -f "package.json" ] && grep -q '"test:coverage"' package.json 2>/dev/null; then
905
+ echo "Running TypeScript coverage with minimum 80% requirement..."
906
+ npx vitest run --coverage 2>&1
907
+ COV_EXIT=$?
908
+ if [ "$COV_EXIT" -ne 0 ]; then
909
+ echo ""
910
+ echo "❌ BLOCKED - Coverage check FAILED (exit code: $COV_EXIT)"
911
+ echo "Coverage must meet the thresholds defined in vitest.config.ts"
912
+ exit 1
913
+ fi
914
+ else
915
+ echo "No 'test:coverage' script in package.json, running coverage via adapter..."
916
+ run_coverage_output=$(run_coverage 2>&1)
917
+ COV_EXIT=$?
918
+ echo "$run_coverage_output" | head -30
919
+ fi
920
+ ;;
921
+ "python")
922
+ if command -v pytest &> /dev/null && command -v coverage &> /dev/null; then
923
+ pytest --cov=. --cov-fail-under=80 --tb=short
924
+ COV_EXIT=$?
925
+ else
926
+ echo "pytest/coverage not available, running coverage via adapter..."
927
+ run_coverage_output=$(run_coverage 2>&1)
928
+ COV_EXIT=$?
929
+ echo "$run_coverage_output" | head -30
930
+ fi
931
+ ;;
932
+ "go")
933
+ if command -v go &> /dev/null; then
934
+ go test -coverprofile=coverage.out ./... 2>/dev/null
935
+ TOTAL_COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total:" | awk '{print substr($3, 1, length($3)-1)}')
936
+ if [ -n "$TOTAL_COVERAGE" ] && (( $(echo "$TOTAL_COVERAGE < 80" | bc -l 2>/dev/null || echo "0") )); then
937
+ echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below 80% threshold"
938
+ exit 1
939
+ fi
940
+ echo "Go coverage: $TOTAL_COVERAGE%"
941
+ COV_EXIT=0
942
+ fi
943
+ ;;
944
+ "shell")
945
+ shell_cov_output=$(run_coverage 2>&1)
946
+ COV_EXIT=$?
947
+ echo "$shell_cov_output" | head -10
948
+ echo "ℹ️ Shell coverage not typically measured"
949
+ ;;
950
+ *)
951
+ default_cov_output=$(run_coverage 2>&1)
952
+ COV_EXIT=$?
953
+ echo "$default_cov_output" | head -30
954
+ ;;
955
+ esac
956
+ else
957
+ # Adapter source failed — warn and let Stage 2 attempt file-based enforcement
958
+ echo "⚠️ Could not source ${PROJECT_LANG} adapter, will check coverage files directly..."
959
+ COV_EXIT=0
960
+ fi
961
+ else
962
+ # No adapter available — warn and let Stage 2 attempt file-based enforcement
963
+ echo "⚠️ No adapter for ${PROJECT_LANG}, will check coverage files directly..."
964
+ COV_EXIT=0
965
+ fi
966
+
967
+ # ============================================================================
968
+ # Stage 2: UNCONDITIONAL coverage percentage enforcement
969
+ # Always attempt to parse coverage from available sources. If percentage
970
+ # can be determined and is < 80% → BLOCK. If no data found → warn.
971
+ # ============================================================================
972
+ COVERAGE_ENFORCED=false
973
+
974
+ case "$PROJECT_LANG" in
975
+ "typescript")
976
+ COVERAGE_ENFORCED=true
977
+ if [ -f "coverage/coverage-summary.json" ]; then
978
+ COVERAGE_PERCENT=$(node -e "
979
+ try {
980
+ const fs = require('fs');
981
+ const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
982
+ console.log(Math.round(data.total.lines.pct));
983
+ } catch(e) { console.log('parse_error'); }
984
+ " 2>/dev/null)
985
+ if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
986
+ if [ "$COVERAGE_PERCENT" -lt 80 ]; then
987
+ echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below 80% threshold"
988
+ exit 1
989
+ fi
990
+ echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
991
+ else
992
+ echo "⚠️ Could not parse TypeScript coverage from coverage/coverage-summary.json"
993
+ fi
994
+ else
995
+ echo "⚠️ coverage/coverage-summary.json not found. If vitest --coverage was used, check vitest.config.ts coverageDirectory."
996
+ fi
997
+ ;;
998
+ "python")
999
+ COVERAGE_ENFORCED=true
1000
+ if command -v coverage &> /dev/null; then
1001
+ COVERAGE_PERCENT=""
1002
+ if [ -f ".coverage" ]; then
1003
+ COVERAGE_PERCENT=$(coverage report --format=total 2>/dev/null | grep -o '[0-9]*%' | head -1 | tr -d '%') || true
1004
+ fi
1005
+ if [ -z "$COVERAGE_PERCENT" ] && [ -f "coverage/.coverage" ]; then
1006
+ COVERAGE_PERCENT=$(coverage report --format=total -i coverage 2>/dev/null | grep -o '[0-9]*%' | head -1 | tr -d '%') || true
1007
+ fi
1008
+ if [ -n "$COVERAGE_PERCENT" ]; then
1009
+ if [ "$COVERAGE_PERCENT" -lt 80 ]; then
1010
+ echo "❌ BLOCKED - Python coverage ${COVERAGE_PERCENT}% below 80% threshold"
1011
+ exit 1
1012
+ fi
1013
+ echo "Python coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
1014
+ else
1015
+ echo "⚠️ Could not determine Python coverage percentage. Ensure pytest-cov or coverage.py data exists."
1016
+ fi
1017
+ else
1018
+ echo "⚠️ coverage.py not available, cannot enforce Python coverage threshold."
1019
+ fi
1020
+ ;;
1021
+ "go")
1022
+ # Go enforcement already handled inline in Stage 1 (lines 933-942)
1023
+ ;;
1024
+ "shell")
1025
+ # Shell coverage not typically measured
1026
+ ;;
1027
+ "dart"|"flutter")
1028
+ COVERAGE_ENFORCED=true
1029
+ if [ -f "coverage/lcov.info" ]; then
1030
+ COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT="0"
1031
+ if [ "$COVERAGE_PERCENT" != "0" ] && [ -n "$COVERAGE_PERCENT" ]; then
1032
+ if [ "$COVERAGE_PERCENT" -lt 80 ]; then
1033
+ echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below 80% threshold"
1034
+ exit 1
1035
+ fi
1036
+ echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
1037
+ else
1038
+ echo "⚠️ Could not determine Flutter/Dart coverage percentage from lcov.info."
1039
+ fi
1040
+ else
1041
+ echo "⚠️ coverage/lcov.info not found. Ensure dart test --coverage was run."
1042
+ fi
1043
+ ;;
1044
+ *)
1045
+ # Default: attempt to parse coverage from common formats regardless of language
1046
+ COVERAGE_ENFORCED=true
1047
+ COVERAGE_PERCENT=""
1048
+ if [ -f "coverage/coverage-summary.json" ]; then
1049
+ COVERAGE_PERCENT=$(node -e "
1050
+ try {
1051
+ const fs = require('fs');
1052
+ const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
1053
+ console.log(Math.round(data.total.lines.pct));
1054
+ } catch(e) { console.log('parse_error'); }
1055
+ " 2>/dev/null) || COVERAGE_PERCENT=""
1056
+ elif [ -f "coverage/lcov.info" ]; then
1057
+ if command -v parse_lcov_coverage &> /dev/null; then
1058
+ COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT=""
1059
+ else
1060
+ COVERAGE_PERCENT=$(grep -oP 'SF:.*' coverage/lcov.info 2>/dev/null | wc -l || echo "0")
1061
+ # Simple fallback: check lcov for coverage percentage
1062
+ COVERAGE_PERCENT=$(grep -oP 'coverage: \K[0-9]+%' coverage/lcov.info 2>/dev/null | head -1 | tr -d '%') || COVERAGE_PERCENT=""
1063
+ fi
1064
+ fi
1065
+ if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
1066
+ if [ "$COVERAGE_PERCENT" -lt 80 ]; then
1067
+ echo "❌ BLOCKED - ${PROJECT_LANG} coverage ${COVERAGE_PERCENT}% below 80% threshold"
1068
+ exit 1
1069
+ fi
1070
+ echo "${PROJECT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
1071
+ else
1072
+ echo "⚠️ Could not determine ${PROJECT_LANG} coverage percentage. No standard coverage report files found."
1073
+ fi
1074
+ ;;
1075
+ esac
1076
+
1077
+ if [ "$COV_EXIT" -ne 0 ]; then
1078
+ echo ""
1079
+ echo "❌ BLOCKED - Coverage check FAILED"
1080
+ exit 1
1081
+ fi
1082
+ if [ "$COVERAGE_ENFORCED" = false ]; then
1083
+ echo "ℹ️ Coverage enforcement not applicable for ${PROJECT_LANG}"
1084
+ fi
1085
+ echo "✅ PASSED - Coverage check completed."
1086
+ fi
1087
+ GATE_5_STATUS="PASS"
1088
+
1089
+ # ============================================================================
1090
+ # GATE 6: Architecture & Tech Debt (Combines Old Gate 8 - Boy Scout and Gate 9 - Architecture)
1091
+ # ============================================================================
1092
+ 2>&1 echo ""
1093
+ 2>&1 echo "→ Gate 6: Architecture & tech debt..."
1094
+
1095
+ # First Part: Architecture Validation (previously Gate 9)
1096
+ 2>&1 echo " └─ Architecture validation:"
1097
+
1098
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
1099
+ echo " ✅ Skipped (documentation project)."
1100
+
1101
+ elif [ "$PROJECT_LANG" = "powershell" ]; then
1102
+ echo " ℹ️ No PowerShell architecture tooling available"
1103
+ echo " ✅ Architecture validation (SKIP, no tool for PowerShell)"
1104
+
1105
+ else
1106
+ # Check if architecture config exists
1107
+ if [ -f "architecture.yaml" ] || [ -f ".architecturerc" ]; then
1108
+ # Different architecture tools per language
1109
+ case "$PROJECT_LANG" in
1110
+ "typescript")
1111
+ if ! require_tool "archlint" "Gate 6" "npm install -g @archlinter/cli"; then
1112
+ echo " ❌ BLOCKED - archlint not installed"
1113
+ echo " Install with: npm install -g @archlinter/cli"
1114
+ exit
1115
+ else
1116
+ echo " Running archlint for TypeScript..."
1117
+ archlint scan . -f table --quiet 2>&1 | tail -30 || echo "⚠️ Architectural smells detected (pre-existing, continuing)"
1118
+ echo " ✅ TypeScript architecture validation completed."
1119
+ fi
1120
+ ;;
1121
+ "python")
1122
+ if [ -f ".import-linter.yml" ] || [ -f "import_linter_config.yml" ]; then
1123
+ if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
1124
+ echo " Running import-linter for Python..."
1125
+ lint-imports 2>&1 | head -20
1126
+ echo " ✅ Python architecture validation completed."
1127
+ else
1128
+ echo " ❌ BLOCKED - import-linter not installed"
1129
+ echo " Install with: pip install import-linter"
1130
+ exit
1131
+ fi
1132
+ else
1133
+ echo " ℹ️ No .import-linter.yml found - skipping Python architecture"
1134
+ echo " ✅ Python architecture validation (SKIP, no config)"
1135
+ fi
1136
+ ;;
1137
+ "go")
1138
+ if [ -f "arch-go.yaml" ] || [ -f "arch-go.yml" ]; then
1139
+ if require_tool "arch-go" "Gate 6" "go install github.com/arch-go/arch-go@latest"; then
1140
+ echo " Running arch-go for Go..."
1141
+ arch-go check 2>&1 | head -20
1142
+ echo " ✅ Go architecture validation completed."
1143
+ else
1144
+ echo " ❌ BLOCKED - arch-go not installed"
1145
+ echo " Install with: go install github.com/arch-go/arch-go@latest"
1146
+ exit
1147
+ fi
1148
+ else
1149
+ echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
1150
+ echo " ✅ Go architecture validation (SKIP, no config)"
1151
+ fi
1152
+ ;;
1153
+ "java")
1154
+ if [ -f "src/test/java/architecture" ] || [ -d "src/test/java/architecture" ]; then
1155
+ echo " Running Java architecture tests..."
1156
+ if [ -f "pom.xml" ]; then
1157
+ mvn test -Dtest=architecture.* 2>&1 | head -20 || mvn test -Dtest=**Architecture** 2>&1 | head -20 || echo "⚠️ Java architecture tests completed"
1158
+ elif [ -f "build.gradle" ]; then
1159
+ if [ -f "./gradlew" ]; then
1160
+ ./gradlew test --tests "*Architecture*" 2>&1 | head -20 || echo "⚠️ Java architecture tests completed"
1161
+ fi
1162
+ fi
1163
+ echo " ✅ Java architecture validation completed."
1164
+ else
1165
+ echo " ℹ️ No architecture test files found - skipping"
1166
+ echo " ✅ Architecture validation (SKIP, no tests)"
1167
+ fi
1168
+ ;;
1169
+ *)
1170
+ echo " ℹ️ Architecture validation not configured for $PROJECT_LANG"
1171
+ echo " ✅ Architecture validation (SKIP, not configured)"
1172
+ ;;
1173
+ esac
1174
+ else
1175
+ # Architecture config is REQUIRED — BLOCK if missing
1176
+ if [ -f ".architecture-skip" ]; then
1177
+ echo " ⚠️ Architecture config missing but .architecture-skip present — allowed to skip"
1178
+ echo " ✅ Architecture validation (SKIP, .architecture-skip exemption)"
1179
+ else
1180
+ echo ""
1181
+ echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
1182
+ echo " Required configuration file 'architecture.yaml' is NOT found."
1183
+ echo " Architecture Quality Gate requires explicit architecture constraints."
1184
+ echo ""
1185
+ echo " Create architecture.yaml with:"
1186
+ echo " layers:"
1187
+ echo " - name: api"
1188
+ echo " paths: [\"src/api/**\"]"
1189
+ echo " - name: domain"
1190
+ echo " paths: [\"src/domain/**\"]"
1191
+ echo " allowed_imports: [domain]"
1192
+ echo " - name: infrastructure"
1193
+ echo " paths: [\"src/infrastructure/**\"]"
1194
+ echo " allowed_imports: [domain, infrastructure]"
1195
+ echo ""
1196
+ echo " After creating architecture.yaml, retry the commit."
1197
+ echo " (To skip with warning, create .architecture-skip file)"
1198
+ exit
1199
+ fi
1200
+ fi
1201
+ fi
1202
+
1203
+ # Second Part: Boy Scout Rule (previously Gate 8) - Unified Enforcement
1204
+ 2>&1 echo " └─ Boy Scout Rule enforcement:"
1205
+
1206
+ if [ "$PROJECT_LANG" = "documentation-only" ]; then
1207
+ echo " ✅ Skipped (documentation project)."
1208
+
1209
+ else
1210
+ # Check if principles directory exists for Boy Scout
1211
+ if [ -f "src/principles/boy-scout.ts" ]; then
1212
+ echo " Checking Boy Scout Rule compliance..."
1213
+
1214
+ # Separate new files and modified files
1215
+ NEW_FILES=$(git diff --cached --name-only --diff-filter=A | tr '\n' ' ')
1216
+ MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | tr '\n' ' ')
1217
+
1218
+ if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
1219
+ echo " ✅ No new or modified files to check."
1220
+ else
1221
+ # Run Boy Scout Rule enforcement
1222
+ if command -v npx &> /dev/null; then
1223
+ # Skip if no files to check
1224
+ if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
1225
+ echo " ✅ No new or modified source files for Boy Scout check."
1226
+ else
1227
+ BOY_SCOUT_OUTPUT=$(npx tsx src/principles/boy-scout.ts \
1228
+ --new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
1229
+ --modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
1230
+ --baseline ".warnings-baseline.json" 2>&1)
1231
+
1232
+ BOY_SCOUT_EXIT=$?
1233
+
1234
+ if [ "$BOY_SCOUT_EXIT" -ne 0 ]; then
1235
+ echo "$BOY_SCOUT_OUTPUT"
1236
+ echo ""
1237
+ echo "❌ BLOCKED - Boy Scout Rule violation"
1238
+ echo "Requirements:"
1239
+ echo " - NEW files: must have zero warnings"
1240
+ echo " - MODIFIED files: cannot increase warnings from baseline"
1241
+ echo " - Files with ≤5 warnings: must clear to zero"
1242
+ exit
1243
+ fi
1244
+
1245
+ # Check output for violations
1246
+ VIOLATION_COUNT=$(echo "$BOY_SCOUT_OUTPUT" | grep -c '"enforcement": "BLOCK"' 2>/dev/null || true)
1247
+ VIOLATION_COUNT=${VIOLATION_COUNT:-0}
1248
+ if [ "$VIOLATION_COUNT" -gt 0 ]; then
1249
+ echo "$BOY_SCOUT_OUTPUT"
1250
+ echo ""
1251
+ echo "❌ BLOCKED - Boy Scout Rule violations detected ($VIOLATION_COUNT)"
1252
+ exit
1253
+ fi
1254
+
1255
+ echo " ✅ PASSED - Boy Scout Rule compliance."
1256
+ fi
1257
+ else
1258
+ echo " ℹ️ npx not available - skipping Boy Scout Rule"
1259
+ echo " ✅ Boy Scout Rule (SKIP, Node.js not available)"
1260
+ fi
1261
+ fi
1262
+ else
1263
+ echo " ℹ️ Boy scout rule not available in project - skipping"
1264
+ echo " ✅ Boy Scout Rule (SKIP, not available in project)"
1265
+ fi
1266
+ fi
1267
+ GATE_6_STATUS="PASS"
1268
+
1269
+ # ============================================================================
1270
+
1271
+ # ============================================================================
1272
+ # GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
1273
+ # Detects security issues in Infrastructure as Code files
1274
+ # Tools: checkov (recommended), hadolint, kube-score, tflint
1275
+ # ============================================================================
1276
+ 2>&1 echo ""
1277
+ 2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
1278
+
1279
+ # Check if any IaC files are changed
1280
+ IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
1281
+ if [ -z "$IAC_FILES" ]; then
1282
+ echo "✅ PASSED - No IaC files detected in changes."
1283
+ GATE_7_STATUS="PASS"
1284
+ else
1285
+ # Run IaC adapter
1286
+ if [ -f "githooks/adapters/iac.sh" ]; then
1287
+ # shellcheck source=githooks/adapters/iac.sh
1288
+ source "githooks/adapters/iac.sh"
1289
+
1290
+ # Run static analysis for IaC files
1291
+ IAC_OUTPUT=$(run_static_analysis "$IAC_FILES" 2>&1)
1292
+ IAC_EXIT=$?
1293
+
1294
+ echo "$IAC_OUTPUT"
1295
+
1296
+ if [ $IAC_EXIT -eq 0 ]; then
1297
+ echo "✅ PASSED - IaC security scan."
1298
+ GATE_7_STATUS="PASS"
1299
+ else
1300
+ echo ""
1301
+ echo "❌ BLOCKED - IaC security issues detected"
1302
+ echo "Fix the security issues above before committing."
1303
+ echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
1304
+ GATE_7_STATUS="FAIL"
1305
+ exit
1306
+ fi
1307
+ else
1308
+ echo "ℹ️ SKIP - IaC adapter not found"
1309
+ echo "✅ PASSED - IaC Security (SKIP)"
1310
+ GATE_7_STATUS="SKIP"
1311
+ fi
1312
+ fi
1313
+
1314
+
1315
+ # GATE 8: Secret Scanning (gitleaks)
1316
+ # Detects secrets (API keys, passwords, tokens) in staged files
1317
+ # Tool: gitleaks -- https://github.com/gitleaks/gitleaks
1318
+ # ============================================================================
1319
+
1320
+ 2>&1 echo ""
1321
+ 2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
1322
+
1323
+ # Gitleaks availability check
1324
+ GITLEAKS_CMD=""
1325
+ if command -v gitleaks >/dev/null 2>&1; then
1326
+ GITLEAKS_CMD="gitleaks"
1327
+ elif [ -f "$HOME/.local/bin/gitleaks" ]; then
1328
+ GIBLEAKS_CMD="$HOME/.local/bin/gitleaks"
1329
+ fi
1330
+
1331
+ if [ -n "$GITLEAKS_CMD" ]; then
1332
+ GITLEAKS_CONFIG=""
1333
+ if [ -f ".gitleaks.toml" ]; then
1334
+ GITLEAKS_CONFIG="--config=.gitleaks.toml"
1335
+ fi
1336
+
1337
+ # Run gitleaks on staged changes only (pre-commit mode for speed)
1338
+ GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --pre-commit --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path=/tmp/gitleaks-report.json 2>&1)
1339
+ GITLEAKS_EXIT=$?
1340
+
1341
+ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
1342
+ echo " ✅ PASSED - No secrets detected."
1343
+ GATE_8_STATUS="PASS"
1344
+ elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
1345
+ # Secrets found — output details
1346
+ echo "$GITLEAKS_OUTPUT"
1347
+ echo ""
1348
+ echo "❌ BLOCKED - Secrets detected in staged files."
1349
+ echo ""
1350
+ echo "Remediation options:"
1351
+ echo " 1. Remove the secret and use environment variables instead"
1352
+ echo " 2. Add a false positive to .gitleaks.toml allowlist"
1353
+ echo " 3. Use git secret or vault for sensitive data"
1354
+ echo ""
1355
+ echo "See: https://github.com/gitleaks/gitleaks"
1356
+ exit
1357
+ else
1358
+ echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
1359
+ echo " ✅ Secret Scanning (SKIP, gitleaks error)"
1360
+ GATE_8_STATUS="SKIP"
1361
+ fi
1362
+ else
1363
+ echo " ℹ️ gitleaks not installed — secret scanning unavailable"
1364
+ echo " Install: brew install gitleaks (macOS) | scripts/install-gitleaks.sh (Linux)"
1365
+ echo " ✅ Secret Scanning (SKIP, gitleaks not installed)"
1366
+ GATE_8_STATUS="SKIP"
1367
+ fi
1368
+
1369
+ # Switch back to original directory if we were in a subdirectory
1370
+ if [ -n "$ORIGINAL_DIR" ]; then
1371
+ cd "$ORIGINAL_DIR"
1372
+ fi
1373
+
1374
+ # ============================================================================
1375
+ # GATE 9: Semgrep SAST Security Scan
1376
+ # Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
1377
+ # Tool: semgrep -- https://semgrep.dev
1378
+ # ============================================================================
1379
+
1380
+ 2>&1 echo ""
1381
+ 2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
1382
+
1383
+ GATE_9_STATUS="PASS"
1384
+
1385
+ # Semgrep availability check
1386
+ SEMGREP_CMD=""
1387
+ if command -v semgrep >/dev/null 2>&1; then
1388
+ SEMGREP_CMD="semgrep"
1389
+ elif [ -f "$HOME/.local/bin/semgrep" ]; then
1390
+ SEMGREP_CMD="$HOME/.local/bin/semgrep"
1391
+ fi
1392
+
1393
+ if [ -z "$SEMGREP_CMD" ]; then
1394
+ echo " ℹ️ semgrep not installed — SAST scanning unavailable"
1395
+ echo " Install: brew install semgrep | pip install semgrep"
1396
+ echo " Pre-cache rules: semgrep --config=p/security-audit"
1397
+ echo " ✅ Semgrep SAST (SKIP, semgrep not installed)"
1398
+ GATE_9_STATUS="SKIP"
1399
+ else
1400
+ # Get staged files filtered to Semgrep-supported languages
1401
+ SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
1402
+
1403
+ if [ -z "$SEMGREP_FILES" ]; then
1404
+ echo " ✅ PASSED - No supported language files in staged changes."
1405
+ GATE_9_STATUS="PASS"
1406
+ else
1407
+ # Run semgrep with JSON output
1408
+ # --config=p/security-audit: explicit security ruleset
1409
+ # --json: machine-readable output
1410
+ # --disable-version-check: skip network call
1411
+ SEMGREP_OUTPUT=$($SEMGREP_CMD scan --config=p/security-audit --json --disable-version-check $SEMGREP_FILES 2>&1)
1412
+ SEMGREP_EXIT=$?
1413
+
1414
+ if [ "$SEMGREP_EXIT" -eq 0 ]; then
1415
+ echo " ✅ PASSED - No security vulnerabilities found."
1416
+ GATE_9_STATUS="PASS"
1417
+ elif [ "$SEMGREP_EXIT" -eq 1 ]; then
1418
+ # Findings detected - parse JSON to categorize
1419
+ CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1420
+ import sys, json
1421
+ try:
1422
+ data = json.load(sys.stdin)
1423
+ results = data.get('results', [])
1424
+ count = 0
1425
+ for r in results:
1426
+ extra = r.get('extra', {})
1427
+ severity = extra.get('severity', '').upper()
1428
+ if severity in ('CRITICAL', 'HIGH'):
1429
+ count += 1
1430
+ print(count)
1431
+ except:
1432
+ print('0')
1433
+ " 2>/dev/null || echo "0")
1434
+
1435
+ MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1436
+ import sys, json
1437
+ try:
1438
+ data = json.load(sys.stdin)
1439
+ results = data.get('results', [])
1440
+ count = 0
1441
+ for r in results:
1442
+ extra = r.get('extra', {})
1443
+ severity = extra.get('severity', '').upper()
1444
+ if severity in ('MEDIUM', 'LOW'):
1445
+ count += 1
1446
+ print(count)
1447
+ except:
1448
+ print('0')
1449
+ " 2>/dev/null || echo "0")
1450
+
1451
+ # Extract top finding details
1452
+ FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1453
+ import sys, json
1454
+ try:
1455
+ data = json.load(sys.stdin)
1456
+ results = data.get('results', [])
1457
+ for r in results[:5]: # Show top 5
1458
+ extra = r.get('extra', {})
1459
+ severity = extra.get('severity', 'UNKNOWN').upper()
1460
+ rule_id = r.get('check_id', 'unknown')
1461
+ path = r.get('path', 'unknown')
1462
+ start_line = r.get('start', {}).get('line', '?')
1463
+ message = extra.get('message', '')[:80]
1464
+ print(f' [{severity}] {rule_id}')
1465
+ print(f' {path}:{start_line} → {message}')
1466
+ print()
1467
+ except:
1468
+ print(' (Failed to parse semgrep output)')
1469
+ " 2>/dev/null || echo " (Failed to parse semgrep output)")
1470
+
1471
+ if [ "$CRITICAL_HIGH" -gt 0 ]; then
1472
+ echo ""
1473
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1474
+ echo " GATE 9: Semgrep Security Gate"
1475
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1476
+ echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
1477
+ echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
1478
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1479
+ echo " ❌ BLOCKED — Critical/High vulnerability found"
1480
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1481
+ echo "$FINDING_DETAILS"
1482
+ echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
1483
+ GATE_9_STATUS="FAIL"
1484
+ exit
1485
+ else
1486
+ echo ""
1487
+ echo " ✅ PASSED - No critical/high vulnerabilities"
1488
+ if [ "$MEDIUM_LOW" -gt 0 ]; then
1489
+ echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
1490
+ echo "$FINDING_DETAILS"
1491
+ fi
1492
+ GATE_9_STATUS="PASS"
1493
+ fi
1494
+ else
1495
+ # semgrep runtime error (timeout, config error, etc.)
1496
+ echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
1497
+ echo " ✅ Semgrep SAST (SKIP, semgrep error)"
1498
+ GATE_9_STATUS="SKIP"
1499
+ fi
1500
+ fi
1501
+ fi
1502
+
1503
+ # ============================================================================
1504
+ # QUALITY REPORT GENERATION
1505
+ # ============================================================================
1506
+
1507
+ generate_quality_report() {
1508
+ local HISTORY_FILE=".quality-history.jsonl"
1509
+ local COMMIT_HASH
1510
+ local TIMESTAMP
1511
+ local BRANCH
1512
+ local PASSED_COUNT=0
1513
+ local TOTAL_GATES=9
1514
+
1515
+ COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
1516
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
1517
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1518
+
1519
+ for gate in 1 2 3 4 5 6 7 8 9; do
1520
+ local status_var="GATE_${gate}_STATUS"
1521
+ if [ "${!status_var}" = "PASS" ] || [ "${!status_var}" = "SKIP" ]; then
1522
+ PASSED_COUNT=$((PASSED_COUNT + 1))
1523
+ fi
1524
+ done
1525
+
1526
+ local SCORE=0
1527
+ if command -v bc >/dev/null 2>&1; then
1528
+ SCORE=$(echo "scale=1; ($PASSED_COUNT / $TOTAL_GATES) * 10" | bc)
1529
+ else
1530
+ SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $TOTAL_GATES) * 10}")
1531
+ fi
1532
+
1533
+ local BS_PASSED=0
1534
+ local BS_BLOCKED=0
1535
+ if [ -n "$BOY_SCOUT_OUTPUT" ]; then
1536
+ BS_PASSED=$(echo "$BOY_SCOUT_OUTPUT" | grep -o '"passedFiles": [0-9]*' | grep -o '[0-9]*' || echo "0")
1537
+ BS_BLOCKED=$(echo "$BOY_SCOUT_OUTPUT" | grep -o '"blockedFiles": [0-9]*' | grep -o '[0-9]*' || echo "0")
1538
+ fi
1539
+
1540
+ local COV_PCT="${COVERAGE_PERCENT:-N/A}"
1541
+
1542
+ # ── Build failed-tests list from TESTS_OUTPUT (Gate 5) ──────────────────────────────
1543
+ local FAILED_TESTS_JSON="[]"
1544
+ if [ -n "$TESTS_OUTPUT" ] && echo "$TESTS_OUTPUT" | grep -qi "fail\|error\|FAILED"; then
1545
+ # Extract failure lines: "FAIL src/foo.test.ts" or " ✗ test name"
1546
+ local FAIL_LINES
1547
+ FAIL_LINES=$(echo "$TESTS_OUTPUT" | grep -E "^\s*(FAIL|✗|failed|AssertionError)" | head -10 | \
1548
+ sed 's/"/\\"/g' | awk '{ printf "%s%s%s", sep, "\"", $0; sep="," }')
1549
+ if [ -n "$FAIL_LINES" ]; then
1550
+ FAILED_TESTS_JSON="[$FAIL_LINES]"
1551
+ fi
1552
+ fi
1553
+
1554
+ # ── Branch-level quality status file ─────────────────────────────────────────────
1555
+ local STATUS_DIR=".xp-gate/quality-status"
1556
+ local REPORT_FILE="${STATUS_DIR}/${BRANCH}.json"
1557
+ mkdir -p "$STATUS_DIR"
1558
+
1559
+ cat > "$REPORT_FILE" << ENDJSON
1560
+ {
1561
+ "reportVersion": "1.1",
1562
+ "generatedAt": "$TIMESTAMP",
1563
+ "branch": "$BRANCH",
1564
+ "commit": "$COMMIT_HASH",
1565
+ "language": "${PROJECT_LANG:-unknown}",
1566
+ "overall": {
1567
+ "gatesPassed": $PASSED_COUNT,
1568
+ "gatesTotal": $TOTAL_GATES,
1569
+ "score": $SCORE,
1570
+ "verdict": "$([ "$PASSED_COUNT" -eq "$TOTAL_GATES" ] && echo "PASS" || echo "PARTIAL")"
1571
+ },
1572
+ "gates": {
1573
+ "gate1_static_analysis": {
1574
+ "name": "Code Quality (Static + Lint + Shell)",
1575
+ "status": "${GATE_1_STATUS:-PASS}",
1576
+ "tool": "${GATE_1_TOOL:-auto}"
1577
+ },
1578
+ "gate2_dup_code": {
1579
+ "name": "Duplicate Code",
1580
+ "status": "${GATE_2_STATUS:-PASS}",
1581
+ "metric": "similarity <= 5%"
1582
+ },
1583
+ "gate3_complexity": {
1584
+ "name": "Cyclomatic Complexity",
1585
+ "status": "${GATE_3_STATUS:-PASS}",
1586
+ "threshold": "${CCN_THRESHOLD:-5}",
1587
+ "errorThreshold": "${CCN_ERROR_THRESHOLD:-10}",
1588
+ "warnings": ${CC_WARNINGS:-0}
1589
+ },
1590
+ "gate4_principles": {
1591
+ "name": "Clean Code + SOLID",
1592
+ "status": "${GATE_4_STATUS:-PASS}",
1593
+ "warnings": ${WARNING_COUNT:-0}
1594
+ },
1595
+ "gate5_tests": {
1596
+ "name": "Tests + Coverage",
1597
+ "status": "${GATE_5_STATUS:-PASS}",
1598
+ "thresholds": { "lines": 80, "functions": 80, "branches": 70, "statements": 80 },
1599
+ "actual": { "coverage": "${COV_PCT}" },
1600
+ "failedTests": $FAILED_TESTS_JSON
1601
+ },
1602
+ "gate6_arch_boyscout": {
1603
+ "name": "Architecture + Boy Scout Rule",
1604
+ "status": "${GATE_6_STATUS:-PASS}",
1605
+ "boyScoutPassed": $BS_PASSED,
1606
+ "boyScoutBlocked": $BS_BLOCKED
1607
+ },
1608
+ "gate7_iac_security": {
1609
+ "name": "IaC Security Scanning",
1610
+ "status": "${GATE_7_STATUS:-PASS}",
1611
+ "tools": "checkov, hadolint, kube-score, tflint"
1612
+ },
1613
+ "gate8_secret_scanning": {
1614
+ "name": "Secret Scanning",
1615
+ "status": "${GATE_8_STATUS:-PASS}",
1616
+ "tool": "gitleaks"
1617
+ },
1618
+ "gate9_sast": {
1619
+ "name": "SAST Security Scan",
1620
+ "status": "${GATE_9_STATUS:-PASS}",
1621
+ "tool": "semgrep"
1622
+ }
1623
+ }
1624
+ }
1625
+ ENDJSON
1626
+
1627
+ # ── History append (unchanged — append-only for trend) ────────────────────────────
1628
+ local GATES_JSON="{"
1629
+ GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-PASS}\",\"name\":\"Code Quality\"},"
1630
+ GATES_JSON="${GATES_JSON}\"gate2\":{\"status\":\"${GATE_2_STATUS:-PASS}\",\"name\":\"Duplicate Code\"},"
1631
+ GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-PASS}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
1632
+ GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-PASS}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
1633
+ GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-PASS}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
1634
+ GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-PASS}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
1635
+ GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-PASS}\",\"name\":\"IaC Security\"},"
1636
+ GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
1637
+ GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"SAST Security\"}}"
1638
+ GATES_JSON="${GATES_JSON}}"
1639
+
1640
+ echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
1641
+
1642
+ # ── Console output ────────────────────────────────────────────────────────
1643
+ echo ""
1644
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1645
+ echo " 📊 Quality Report — $TIMESTAMP"
1646
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1647
+ echo ""
1648
+ printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-PASS}"
1649
+ printf " %-45s %s\n" "Gate 2: Duplicate Code" "${GATE_2_STATUS:-PASS}"
1650
+ printf " %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-PASS}"
1651
+ printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-PASS}"
1652
+ printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-PASS}"
1653
+ printf " %-45s %s\n" "Gate 6: Architecture + Boy Scout" "${GATE_6_STATUS:-PASS}"
1654
+ printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-PASS}"
1655
+ printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
1656
+ printf " %-45s %s\n" "Gate 9: SAST Security" "${GATE_9_STATUS:-PASS}"
1657
+ echo ""
1658
+ echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
1659
+ echo " Branch status: $REPORT_FILE"
1660
+ echo " History saved: $HISTORY_FILE"
1661
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1662
+ echo ""
1663
+ }
1664
+
1665
+ generate_quality_report
1666
+
1667
+ exit