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.
- package/adapter-common.sh +192 -0
- package/adapters/cpp.sh +76 -0
- package/adapters/dart.sh +41 -0
- package/adapters/flutter.sh +41 -0
- package/adapters/go.sh +59 -0
- package/adapters/iac.sh +189 -0
- package/adapters/java.sh +191 -0
- package/adapters/kotlin.sh +77 -0
- package/adapters/objectivec.sh +38 -0
- package/adapters/powershell.sh +138 -0
- package/adapters/python.sh +104 -0
- package/adapters/shell.sh +55 -0
- package/adapters/swift.sh +44 -0
- package/adapters/typescript.sh +61 -0
- package/bin/xp-gate.js +157 -0
- package/hooks/adapter-common.sh +192 -0
- package/hooks/pre-commit +1667 -0
- package/hooks/pre-push +395 -0
- package/lib/__tests__/detect-deps.test.js +209 -0
- package/lib/__tests__/doctor.test.js +448 -0
- package/lib/__tests__/download-skill.test.js +281 -0
- package/lib/__tests__/init.test.js +327 -0
- package/lib/__tests__/install-skill.test.js +326 -0
- package/lib/__tests__/migrate.test.js +212 -0
- package/lib/__tests__/rollback.test.js +183 -0
- package/lib/__tests__/ui-detector.test.ts +200 -0
- package/lib/__tests__/uninstall-skill.test.js +189 -0
- package/lib/__tests__/uninstall.test.js +589 -0
- package/lib/__tests__/update-skill.test.js +276 -0
- package/lib/detect-deps.js +157 -0
- package/lib/doctor.js +370 -0
- package/lib/download-skill.js +96 -0
- package/lib/init.js +367 -0
- package/lib/install-skill.js +184 -0
- package/lib/migrate.js +120 -0
- package/lib/rollback.js +78 -0
- package/lib/ui-detector.ts +99 -0
- package/lib/uninstall-skill.js +69 -0
- package/lib/uninstall.js +401 -0
- package/lib/update-skill.js +90 -0
- package/package.json +39 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
- package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
- package/plugins/claude-code/bin/xp-gate-check +47 -0
- package/plugins/claude-code/hooks/hooks.json +37 -0
- package/skills/delphi-review/.delphi-config.json.example +45 -0
- package/skills/delphi-review/AGENTS.md +54 -0
- package/skills/delphi-review/INSTALL.md +152 -0
- package/skills/delphi-review/SKILL.md +371 -0
- package/skills/delphi-review/evals/evals.json +82 -0
- package/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/skills/ralph-loop/SKILL.md +330 -0
- package/skills/ralph-loop/evals/evals.json +311 -0
- package/skills/ralph-loop/evolution-history.json +59 -0
- package/skills/ralph-loop/evolution-log.md +16 -0
- package/skills/ralph-loop/references/components/memory.md +55 -0
- package/skills/ralph-loop/references/components/middleware.md +54 -0
- package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/skills/ralph-loop/templates/progress-log.md +36 -0
- package/skills/sprint-flow/SKILL.md +600 -0
- package/skills/sprint-flow/evals/evals.json +78 -0
- package/skills/sprint-flow/evolution-history.json +39 -0
- package/skills/sprint-flow/evolution-log.md +23 -0
- package/skills/sprint-flow/references/components/memory.md +87 -0
- package/skills/sprint-flow/references/components/middleware.md +72 -0
- package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/skills/test-specification-alignment/AGENTS.md +59 -0
- package/skills/test-specification-alignment/SKILL.md +605 -0
- package/skills/test-specification-alignment/evals/evals.json +75 -0
- package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/skills/test-specification-alignment/references/specification-format.md +348 -0
package/hooks/pre-commit
ADDED
|
@@ -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
|