@boyingliu01/xp-gate 0.8.8 → 0.8.10
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/bin/xp-gate.js +18 -0
- package/hooks/pre-commit +565 -120
- package/lib/arch.js +49 -0
- package/lib/check.js +50 -0
- package/lib/init.js +36 -0
- package/lib/principles.js +48 -0
- package/mock-policy/AGENTS.md +94 -0
- package/mock-policy/__tests__/config.test.ts +292 -0
- package/mock-policy/__tests__/integration.test.ts +600 -0
- package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
- package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
- package/mock-policy/config.ts +106 -0
- package/mock-policy/gate-m3.ts +233 -0
- package/mock-policy/mock-decision-engine.ts +155 -0
- package/mock-policy/schema.ts +22 -0
- package/mock-policy/scope-scanner.ts +288 -0
- package/mock-policy/types.ts +65 -0
- package/mutation/AGENTS.md +73 -0
- package/mutation/__tests__/detect-ai-test.test.ts +152 -0
- package/mutation/__tests__/gate-m.test.ts +194 -0
- package/mutation/detect-ai-test.ts +128 -0
- package/mutation/gate-m.ts +619 -0
- package/mutation/init-baseline.ts +192 -0
- package/mutation/stryker-types.ts +14 -0
- package/mutation/types.ts +70 -0
- package/mutation/update-baseline.ts +160 -0
- package/package.json +5 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +23 -7
- package/plugins/opencode/index.ts +36 -21
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +20 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/qoder/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/qoder/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +111 -0
- package/principles/__tests__/analyzer.test.ts +343 -0
- package/principles/__tests__/baseline-storage.test.ts +187 -0
- package/principles/__tests__/baseline.test.ts +328 -0
- package/principles/__tests__/boy-scout-integration.test.ts +264 -0
- package/principles/__tests__/boy-scout.test.ts +646 -0
- package/principles/__tests__/config.test.ts +78 -0
- package/principles/__tests__/index.test.ts +81 -0
- package/principles/__tests__/lint-baseline.test.ts +314 -0
- package/principles/__tests__/reporter.test.ts +832 -0
- package/principles/__tests__/types.test.ts +101 -0
- package/principles/adapters/__tests__/base.test.ts +95 -0
- package/principles/adapters/__tests__/cpp.test.ts +290 -0
- package/principles/adapters/__tests__/dart.test.ts +84 -0
- package/principles/adapters/__tests__/go.test.ts +68 -0
- package/principles/adapters/__tests__/java.test.ts +68 -0
- package/principles/adapters/__tests__/kotlin.test.ts +76 -0
- package/principles/adapters/__tests__/objectivec.test.ts +453 -0
- package/principles/adapters/__tests__/python.test.ts +84 -0
- package/principles/adapters/__tests__/swift.test.ts +68 -0
- package/principles/adapters/__tests__/typescript.test.ts +116 -0
- package/principles/adapters/base.ts +124 -0
- package/principles/adapters/cpp.ts +115 -0
- package/principles/adapters/dart.ts +44 -0
- package/principles/adapters/go.ts +55 -0
- package/principles/adapters/java.ts +56 -0
- package/principles/adapters/kotlin.ts +54 -0
- package/principles/adapters/objectivec.ts +82 -0
- package/principles/adapters/python.ts +83 -0
- package/principles/adapters/swift.ts +40 -0
- package/principles/adapters/typescript.ts +66 -0
- package/principles/analyzer.ts +190 -0
- package/principles/baseline.ts +273 -0
- package/principles/boy-scout.ts +416 -0
- package/principles/config.ts +118 -0
- package/principles/index.ts +87 -0
- package/principles/lint-baseline.ts +385 -0
- package/principles/reporter.ts +188 -0
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
- package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
- package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
- package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
- package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
- package/principles/rules/__tests__/index.test.ts +49 -0
- package/principles/rules/__tests__/solid/dip.test.ts +121 -0
- package/principles/rules/__tests__/solid/isp.test.ts +60 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
- package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
- package/principles/rules/__tests__/solid/srp.test.ts +89 -0
- package/principles/rules/clean-code/code-duplication.ts +31 -0
- package/principles/rules/clean-code/deep-nesting.ts +38 -0
- package/principles/rules/clean-code/god-class.ts +54 -0
- package/principles/rules/clean-code/large-file.ts +34 -0
- package/principles/rules/clean-code/long-function.ts +47 -0
- package/principles/rules/clean-code/magic-numbers.ts +59 -0
- package/principles/rules/clean-code/many-exports.ts +40 -0
- package/principles/rules/clean-code/missing-error-handling.ts +33 -0
- package/principles/rules/clean-code/too-many-params.ts +35 -0
- package/principles/rules/clean-code/unused-imports.ts +33 -0
- package/principles/rules/index.ts +61 -0
- package/principles/rules/solid/dip.ts +47 -0
- package/principles/rules/solid/isp.ts +37 -0
- package/principles/rules/solid/lsp.ts +79 -0
- package/principles/rules/solid/ocp.ts +40 -0
- package/principles/rules/solid/srp.ts +44 -0
- package/principles/types.ts +27 -0
- package/skills/delphi-review/AGENTS.md +7 -7
- package/skills/delphi-review/INSTALL.md +1 -1
- package/skills/delphi-review/SKILL.md +12 -12
- package/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/skills/sprint-flow/AGENTS.md +83 -36
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +1 -1
- package/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-commit
CHANGED
|
@@ -26,13 +26,14 @@ fi
|
|
|
26
26
|
source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
|
|
27
27
|
echo "Error: Cannot source adapter-common.sh from $ADAPTER_DIR"
|
|
28
28
|
echo "Run: xp-gate init (per-project) or xp-gate setup-global (all projects)"
|
|
29
|
-
exit
|
|
29
|
+
exit 1
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
# Trap: ensure quality report generated on ANY exit (pass or fail).
|
|
33
33
|
# Using a guard prevents recursion when 'exit' is called from inside the trap.
|
|
34
|
+
# IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
|
|
34
35
|
_QUALITY_REPORT_DONE=0
|
|
35
|
-
|
|
36
|
+
_quality_report_on_exit() {
|
|
36
37
|
local _saved_exit=$?
|
|
37
38
|
if [ "$_QUALITY_REPORT_DONE" = "1" ]; then
|
|
38
39
|
exit $_saved_exit
|
|
@@ -49,6 +50,9 @@ if [[ -d "$PROJECT_ROOT/node_modules/.bin" ]]; then
|
|
|
49
50
|
export PATH="$PROJECT_ROOT/node_modules/.bin:$PATH"
|
|
50
51
|
fi
|
|
51
52
|
|
|
53
|
+
# Ensure pipe failures propagate exit codes through | head/tail/grep
|
|
54
|
+
set -o pipefail
|
|
55
|
+
|
|
52
56
|
# ============================================================================
|
|
53
57
|
# Helper Functions
|
|
54
58
|
# ============================================================================
|
|
@@ -123,6 +127,166 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
123
127
|
echo "Refactored Structure: 6 Type-Based Gates"
|
|
124
128
|
echo ""
|
|
125
129
|
|
|
130
|
+
# ============================================================================
|
|
131
|
+
# Gate 0: Version Consistency Check (Protected Branches)
|
|
132
|
+
# ============================================================================
|
|
133
|
+
|
|
134
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
135
|
+
PROTECTED_BRANCHES="main master develop trunk mainline"
|
|
136
|
+
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
|
|
137
|
+
|
|
138
|
+
# Optional: project-level .protected-branches config file
|
|
139
|
+
if [ -f "$ROOT_DIR/.protected-branches" ]; then
|
|
140
|
+
PROTECTED_BRANCHES="$PROTECTED_BRANCHES $(cat "$ROOT_DIR/.protected-branches" | tr '\n' ' ')"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
is_protected=false
|
|
144
|
+
for branch in $PROTECTED_BRANCHES; do
|
|
145
|
+
if [ "$CURRENT_BRANCH" = "$branch" ]; then
|
|
146
|
+
is_protected=true
|
|
147
|
+
break
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
has_version_change=false
|
|
152
|
+
has_changelog_change=false
|
|
153
|
+
|
|
154
|
+
if [ "$is_protected" = "true" ]; then
|
|
155
|
+
# Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
|
|
156
|
+
# Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
|
|
157
|
+
if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
|
|
158
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (SKIP_VERSION_CHECK=1 env var)"
|
|
159
|
+
else
|
|
160
|
+
# Check if staged changes include VERSION or CHANGELOG.md
|
|
161
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
162
|
+
for file in $STAGED_FILES; do
|
|
163
|
+
case "$file" in
|
|
164
|
+
VERSION) has_version_change=true ;;
|
|
165
|
+
CHANGELOG.md) has_changelog_change=true ;;
|
|
166
|
+
esac
|
|
167
|
+
done
|
|
168
|
+
|
|
169
|
+
# If either VERSION or CHANGELOG.md is staged, pass
|
|
170
|
+
if [ "$has_version_change" = "true" ] || [ "$has_changelog_change" = "true" ]; then
|
|
171
|
+
# Auto-run sync-version.sh when VERSION is staged (issue #206 follow-up).
|
|
172
|
+
# Propagates VERSION → 4 package.json + refreshes 32 AGENTS.md headers,
|
|
173
|
+
# then stages only files sync-version.sh is known to touch (NOT runtime
|
|
174
|
+
# journals or unrelated edits) so the commit captures it atomically.
|
|
175
|
+
if [ "$has_version_change" = "true" ] && [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
|
|
176
|
+
echo "🔄 Gate 0: VERSION staged — running sync-version.sh to fan out to package.json + AGENTS.md headers..."
|
|
177
|
+
if bash "$ROOT_DIR/scripts/sync-version.sh" > /tmp/xp-gate-sync-version.log 2>&1; then
|
|
178
|
+
SYNC_STAGED=0
|
|
179
|
+
# Stage the 4 known package.json targets if modified
|
|
180
|
+
for pkg in package.json src/npm-package/package.json \
|
|
181
|
+
plugins/claude-code/.claude-plugin/plugin.json \
|
|
182
|
+
plugins/opencode/package.json \
|
|
183
|
+
src/npm-package/plugins/claude-code/.claude-plugin/plugin.json \
|
|
184
|
+
src/npm-package/plugins/opencode/package.json; do
|
|
185
|
+
if [ -f "$ROOT_DIR/$pkg" ] && ! git diff --quiet -- "$ROOT_DIR/$pkg" 2>/dev/null; then
|
|
186
|
+
git add "$ROOT_DIR/$pkg"
|
|
187
|
+
SYNC_STAGED=$((SYNC_STAGED + 1))
|
|
188
|
+
fi
|
|
189
|
+
done
|
|
190
|
+
# Stage AGENTS.md files with header-only modifications (header refresh is the only thing sync-version touches in AGENTS.md)
|
|
191
|
+
while IFS= read -r -d '' agents_file; do
|
|
192
|
+
if ! git diff --quiet -- "$agents_file" 2>/dev/null; then
|
|
193
|
+
git add "$agents_file"
|
|
194
|
+
SYNC_STAGED=$((SYNC_STAGED + 1))
|
|
195
|
+
fi
|
|
196
|
+
done < <(find "$ROOT_DIR" -name 'AGENTS.md' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0)
|
|
197
|
+
echo " auto-staged $SYNC_STAGED file(s) from sync-version.sh (package.json × ≤6 + AGENTS.md headers)"
|
|
198
|
+
else
|
|
199
|
+
echo "⚠️ sync-version.sh failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (VERSION/CHANGELOG staged)"
|
|
203
|
+
else
|
|
204
|
+
# Check for doc-only changes (no source code files)
|
|
205
|
+
has_source=false
|
|
206
|
+
for file in $STAGED_FILES; do
|
|
207
|
+
case "$file" in
|
|
208
|
+
*.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
|
|
209
|
+
has_source=true
|
|
210
|
+
;;
|
|
211
|
+
esac
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
# Check for build-tooling-only changes (adapters, scripts, hooks)
|
|
215
|
+
has_real_source=false
|
|
216
|
+
for file in $STAGED_FILES; do
|
|
217
|
+
case "$file" in
|
|
218
|
+
src/npm-package/adapters/*|src/npm-package/scripts/*|src/npm-package/hooks/*|scripts/*|githooks/adapters/*)
|
|
219
|
+
continue ;;
|
|
220
|
+
*.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
|
|
221
|
+
has_real_source=true
|
|
222
|
+
;;
|
|
223
|
+
esac
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
# Doc-only changes: no source code files
|
|
227
|
+
if [ "$has_source" = "false" ]; then
|
|
228
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (doc-only changes)"
|
|
229
|
+
else
|
|
230
|
+
# Check for bypass: commit message prefix (read from COMMIT_EDITMSG during pre-commit hook)
|
|
231
|
+
COMMIT_MSG=$(cat "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null | head -1 || echo "")
|
|
232
|
+
if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\]'; then
|
|
233
|
+
if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\].*(chore:|docs:|release:)'; then
|
|
234
|
+
if [ "$has_real_source" = "false" ]; then
|
|
235
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (bypass: chore/docs/release)"
|
|
236
|
+
else
|
|
237
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
238
|
+
VERSION CONSISTENCY CHECK (Gate 0)
|
|
239
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
240
|
+
Branch: $CURRENT_BRANCH
|
|
241
|
+
ERROR: [skip-version-check] bypass used but production source files detected.
|
|
242
|
+
|
|
243
|
+
Bypass only allows build-tooling changes (adapters/, scripts/, hooks/).
|
|
244
|
+
Production source files require VERSION/CHANGELOG update.
|
|
245
|
+
|
|
246
|
+
Files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
247
|
+
"
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
250
|
+
else
|
|
251
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
252
|
+
VERSION CONSISTENCY CHECK (Gate 0)
|
|
253
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
254
|
+
Branch: $CURRENT_BRANCH
|
|
255
|
+
ERROR: [skip-version-check] bypass prefix invalid.
|
|
256
|
+
|
|
257
|
+
Allowed prefixes: chore:, docs:, release:
|
|
258
|
+
Example: [skip-version-check] chore: update dependencies
|
|
259
|
+
"
|
|
260
|
+
exit 1
|
|
261
|
+
fi
|
|
262
|
+
else
|
|
263
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
264
|
+
VERSION CONSISTENCY CHECK (Gate 0)
|
|
265
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
266
|
+
Branch: $CURRENT_BRANCH
|
|
267
|
+
ERROR: Committing to a protected branch without VERSION/CHANGELOG update.
|
|
268
|
+
|
|
269
|
+
This bypasses the ship workflow and causes version drift.
|
|
270
|
+
Before committing:
|
|
271
|
+
1. Update VERSION file with new version
|
|
272
|
+
2. Add entry to CHANGELOG.md
|
|
273
|
+
3. Run: bash scripts/sync-version.sh
|
|
274
|
+
|
|
275
|
+
Or use --no-verify (discouraged) or include [skip-version-check] in commit message.
|
|
276
|
+
|
|
277
|
+
Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
278
|
+
"
|
|
279
|
+
exit 1
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
282
|
+
fi
|
|
283
|
+
fi # closes SKIP_VERSION_CHECK if/else
|
|
284
|
+
else
|
|
285
|
+
echo "✅ PASSED - Gate 0: Version Consistency Check (non-protected branch: $CURRENT_BRANCH)"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
echo ""
|
|
289
|
+
|
|
126
290
|
# Get list of changed files
|
|
127
291
|
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
128
292
|
|
|
@@ -157,8 +321,8 @@ elif [ -f "pubspec.yaml" ]; then
|
|
|
157
321
|
elif [ -n "$(find . -name '*.ps1' -not -path './.git/*' -maxdepth 2 | head -1)" ]; then
|
|
158
322
|
PROJECT_LANG="powershell"
|
|
159
323
|
elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
|
|
160
|
-
CPP_FILES=$(find . -maxdepth 2 -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
161
|
-
OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
324
|
+
CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
325
|
+
OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
162
326
|
if [ -n "$CPP_FILES" ]; then
|
|
163
327
|
PROJECT_LANG="cpp"
|
|
164
328
|
elif [ -n "$OBJC_FILES" ]; then
|
|
@@ -166,7 +330,7 @@ elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
|
|
|
166
330
|
fi
|
|
167
331
|
elif [ -f "*.xcodeproj" ] || [ -d "*.xcworkspace" ]; then
|
|
168
332
|
# Xcode project - Objective-C or Swift
|
|
169
|
-
OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
333
|
+
OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
170
334
|
if [ -n "$OBJC_FILES" ]; then
|
|
171
335
|
PROJECT_LANG="objectivec"
|
|
172
336
|
fi
|
|
@@ -310,6 +474,52 @@ if [ -z "$PROJECT_LANG" ]; then
|
|
|
310
474
|
fi
|
|
311
475
|
fi
|
|
312
476
|
|
|
477
|
+
# ============================================================================
|
|
478
|
+
# Audit helper — records gate execution to .xp-gate/audit.jsonl
|
|
479
|
+
# NEVER blocks commit on failure (try/catch around npx tsx).
|
|
480
|
+
# Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
|
|
481
|
+
# ============================================================================
|
|
482
|
+
AUDIT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
483
|
+
PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AUDIT_SCRIPT_DIR/../..")"
|
|
484
|
+
|
|
485
|
+
record_gate_audit() {
|
|
486
|
+
local gate_id="$1"
|
|
487
|
+
local gate_name="$2"
|
|
488
|
+
local passed="$3"
|
|
489
|
+
local issues_found="$4"
|
|
490
|
+
local start_ms="$5"
|
|
491
|
+
local end_ms
|
|
492
|
+
end_ms=$(date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0")
|
|
493
|
+
local duration_ms=$((end_ms - start_ms))
|
|
494
|
+
if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
|
|
495
|
+
duration_ms=0
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# Determine gate result: PASS or FAIL based on status variable
|
|
499
|
+
local gate_result="true"
|
|
500
|
+
if [ "$passed" != "PASS" ] && [ "$passed" != "SKIP" ]; then
|
|
501
|
+
gate_result="false"
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
# Fire-and-forget: audit must never block the commit
|
|
505
|
+
{
|
|
506
|
+
npx tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
|
|
507
|
+
--gate-id "$gate_id" \
|
|
508
|
+
--gate-name "$gate_name" \
|
|
509
|
+
--passed "$gate_result" \
|
|
510
|
+
--issues-found "$issues_found" \
|
|
511
|
+
--duration-ms "$duration_ms" \
|
|
512
|
+
--trigger commit \
|
|
513
|
+
2>/dev/null
|
|
514
|
+
} &
|
|
515
|
+
disown 2>/dev/null || true
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
# Get milliseconds timestamp (used by gates for duration measurement)
|
|
519
|
+
gate_start_ms() {
|
|
520
|
+
date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0"
|
|
521
|
+
}
|
|
522
|
+
|
|
313
523
|
# Switch to subdirectory if detected (for mixed/hybrid projects)
|
|
314
524
|
# All subsequent gates will run in the subdirectory context
|
|
315
525
|
ORIGINAL_DIR=""
|
|
@@ -324,43 +534,95 @@ fi
|
|
|
324
534
|
# GATE 1: Code Quality (Static Analysis + Linting + Shell Check combined)
|
|
325
535
|
# Combination of: Old Gates 1 (Static), Gate 2 (Linting), Gate 5 (Shell Check)
|
|
326
536
|
# ============================================================================
|
|
327
|
-
2>&1 echo ""
|
|
328
|
-
2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
|
|
537
|
+
2>&1 echo ""
|
|
538
|
+
2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
|
|
539
|
+
GATE_1_START=$(gate_start_ms)
|
|
329
540
|
|
|
330
541
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
331
542
|
# Documentation-only project - skip static analysis
|
|
332
543
|
echo "✅ PASSED - Skipped (no source code to analyze)."
|
|
333
544
|
|
|
334
|
-
elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
335
|
-
# TypeScript: static analysis (tsc) + linting (eslint)
|
|
545
|
+
elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
546
|
+
# TypeScript: static analysis (tsc) + linting (eslint)
|
|
336
547
|
if ! command -v npx &> /dev/null; then
|
|
337
548
|
echo "ℹ️ SKIP - npx not available (non-blocking for TypeScript)"
|
|
338
549
|
echo "✅ PASSED - Code Quality Gate (SKIP)"
|
|
339
|
-
else
|
|
340
|
-
#
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
550
|
+
else
|
|
551
|
+
# Verify tsc is real TypeScript compiler (npx may fetch deprecated tsc@2.0.4 placeholder)
|
|
552
|
+
TSC_OUTPUT=$(npx tsc --version 2>&1)
|
|
553
|
+
if echo "$TSC_OUTPUT" | grep -q "Version"; then
|
|
554
|
+
# Only run tsc if tsconfig.json exists (without it tsc prints help and exits non-zero)
|
|
555
|
+
if [ -f "tsconfig.json" ]; then
|
|
556
|
+
# Run TypeScript type checking
|
|
557
|
+
echo "Running TypeScript static analysis ($TSC_OUTPUT)..."
|
|
558
|
+
npx tsc --noEmit --skipLibCheck 2>&1 | head -30
|
|
559
|
+
TSC_EXIT=$?
|
|
560
|
+
if [ "$TSC_EXIT" -ne 0 ]; then
|
|
561
|
+
echo ""
|
|
562
|
+
echo "❌ BLOCKED - TYPE ERRORS detected"
|
|
563
|
+
echo "Fix the type errors above before committing."
|
|
564
|
+
exit 1
|
|
565
|
+
fi
|
|
566
|
+
echo "✅ PASSED - TypeScript static analysis."
|
|
567
|
+
else
|
|
568
|
+
echo "ℹ️ SKIP - tsconfig.json not found (no TypeScript project config)"
|
|
569
|
+
fi
|
|
570
|
+
else
|
|
571
|
+
echo "ℹ️ SKIP - tsc not properly installed ($TSC_OUTPUT)"
|
|
572
|
+
echo "Install typescript (npm install typescript --save-dev) for type checking."
|
|
349
573
|
fi
|
|
350
|
-
echo "✅ PASSED - TypeScript static analysis."
|
|
351
574
|
|
|
352
575
|
# Run ESLint linting if config exists
|
|
353
576
|
if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
|
|
354
577
|
echo "Running ESLint linting..."
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if [ "$
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
578
|
+
# Filter to only TS/JS files - ESLint config ignores non-matching files
|
|
579
|
+
ESLINT_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
|
|
580
|
+
if [ -n "$ESLINT_FILES" ]; then
|
|
581
|
+
ESLINT_OUTPUT=$(npx eslint $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
582
|
+
ESLINT_EXIT=$?
|
|
583
|
+
if [ "$ESLINT_EXIT" -ne 0 ]; then
|
|
584
|
+
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
585
|
+
# Compare against lint baseline
|
|
586
|
+
BASELINE_WARNINGS=$(echo "$ESLINT_FILES" | tr ' ' '\n' | node -e "
|
|
587
|
+
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
588
|
+
const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
|
|
589
|
+
const files=stdin?stdin.split('\n'):[];
|
|
590
|
+
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
591
|
+
console.log(total);
|
|
592
|
+
")
|
|
593
|
+
CURRENT_WARNINGS=$(echo "$ESLINT_OUTPUT" | node -e "
|
|
594
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
595
|
+
console.log(d.reduce((s,f)=>s+(f.warningCount||0)+(f.errorCount||0),0));
|
|
596
|
+
")
|
|
597
|
+
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
598
|
+
NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
|
|
599
|
+
echo "$ESLINT_OUTPUT" | head -30
|
|
600
|
+
echo ""
|
|
601
|
+
echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
|
|
602
|
+
echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
|
|
603
|
+
exit 1
|
|
604
|
+
else
|
|
605
|
+
REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
|
|
606
|
+
if [ "$REDUCED" -gt 0 ]; then
|
|
607
|
+
echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
|
|
608
|
+
else
|
|
609
|
+
echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
|
|
610
|
+
fi
|
|
611
|
+
fi
|
|
612
|
+
else
|
|
613
|
+
echo "$ESLINT_OUTPUT" | head -30
|
|
614
|
+
echo ""
|
|
615
|
+
echo "❌ BLOCKED - LINT ERRORS detected"
|
|
616
|
+
echo "Fix the lint errors above before committing."
|
|
617
|
+
echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
|
|
618
|
+
exit 1
|
|
619
|
+
fi
|
|
620
|
+
else
|
|
621
|
+
echo "✅ PASSED - ESLint linting."
|
|
622
|
+
fi
|
|
623
|
+
else
|
|
624
|
+
echo "✅ PASSED - No staged TS/JS files to lint."
|
|
362
625
|
fi
|
|
363
|
-
echo "✅ PASSED - ESLint linting."
|
|
364
626
|
else
|
|
365
627
|
echo "ℹ️ No ESLint configuration found - Skipping"
|
|
366
628
|
fi
|
|
@@ -374,16 +636,60 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
374
636
|
else
|
|
375
637
|
# Run Ruff (includes syntax check, linting)
|
|
376
638
|
echo "Running Ruff linting (syntax + lint)..."
|
|
377
|
-
ruff check "$CHANGED_FILES" 2>&1
|
|
639
|
+
RUFF_OUTPUT=$(ruff check "$CHANGED_FILES" --output-format json 2>&1)
|
|
378
640
|
RUFF_EXIT=$?
|
|
379
641
|
if [ "$RUFF_EXIT" -ne 0 ]; then
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
642
|
+
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
643
|
+
# Get baseline warnings for changed Python files
|
|
644
|
+
PY_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
|
|
645
|
+
if [ -n "$PY_FILES" ]; then
|
|
646
|
+
BASELINE_WARNINGS=$(echo "$PY_FILES" | node -e "
|
|
647
|
+
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
648
|
+
const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
|
|
649
|
+
const files=stdin?stdin.split('\n'):[];
|
|
650
|
+
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
651
|
+
console.log(total);
|
|
652
|
+
")
|
|
653
|
+
CURRENT_WARNINGS=$(echo "$RUFF_OUTPUT" | node -e "
|
|
654
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
655
|
+
console.log(d.reduce((s,f)=>s+(f.messages?f.messages.length:0),0));
|
|
656
|
+
")
|
|
657
|
+
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
658
|
+
NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
|
|
659
|
+
echo "$RUFF_OUTPUT" | node -e "
|
|
660
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
661
|
+
for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
|
|
662
|
+
" | head -30
|
|
663
|
+
echo ""
|
|
664
|
+
echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
|
|
665
|
+
echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
|
|
666
|
+
exit 1
|
|
667
|
+
else
|
|
668
|
+
REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
|
|
669
|
+
if [ "$REDUCED" -gt 0 ]; then
|
|
670
|
+
echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
|
|
671
|
+
else
|
|
672
|
+
echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
|
|
673
|
+
fi
|
|
674
|
+
fi
|
|
675
|
+
else
|
|
676
|
+
echo "✅ PASSED - Ruff linting (no Python files to check)."
|
|
677
|
+
fi
|
|
678
|
+
else
|
|
679
|
+
echo "$RUFF_OUTPUT" | node -e "
|
|
680
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
681
|
+
for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
|
|
682
|
+
" | head -30
|
|
683
|
+
echo ""
|
|
684
|
+
echo "❌ BLOCKED - RUFF ERRORS detected"
|
|
685
|
+
echo "Fix the lint errors above before committing."
|
|
686
|
+
echo "Tip: Run 'ruff check --fix' to auto-fix some issues."
|
|
687
|
+
echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
|
|
688
|
+
exit 1
|
|
689
|
+
fi
|
|
690
|
+
else
|
|
691
|
+
echo "✅ PASSED - Ruff linting."
|
|
385
692
|
fi
|
|
386
|
-
echo "✅ PASSED - Ruff linting."
|
|
387
693
|
|
|
388
694
|
# Optional: Type checking with mypy
|
|
389
695
|
if command -v mypy &> /dev/null; then
|
|
@@ -394,7 +700,7 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
394
700
|
echo ""
|
|
395
701
|
echo "❌ BLOCKED - MYPI TYPE ERRORS detected"
|
|
396
702
|
echo "Fix the type errors above before committing."
|
|
397
|
-
exit
|
|
703
|
+
exit 1
|
|
398
704
|
fi
|
|
399
705
|
echo "✅ PASSED - mypy type checking."
|
|
400
706
|
else
|
|
@@ -415,42 +721,80 @@ elif [ "$PROJECT_LANG" = "go" ]; then
|
|
|
415
721
|
echo ""
|
|
416
722
|
echo "❌ BLOCKED - GOLANGCI-LINT ERRORS detected"
|
|
417
723
|
echo "Fix the lint errors above before committing."
|
|
418
|
-
exit
|
|
724
|
+
exit 1
|
|
419
725
|
fi
|
|
420
726
|
echo "✅ PASSED - golangci-lint."
|
|
421
727
|
fi
|
|
422
728
|
|
|
423
|
-
elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$'
|
|
424
|
-
# Shell:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
729
|
+
elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$'; then
|
|
730
|
+
# Shell: check staged shell files only. Installed hook adapters are infrastructure,
|
|
731
|
+
# not project changes, and should not be linted on unrelated commits.
|
|
732
|
+
SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || true)
|
|
733
|
+
|
|
734
|
+
if [ -z "$SHELL_FILES" ]; then
|
|
735
|
+
echo "✅ PASSED - No staged shell scripts to check."
|
|
736
|
+
elif command -v shellcheck &> /dev/null; then
|
|
737
|
+
echo "Running shellcheck on staged shell scripts..."
|
|
738
|
+
SHELLCHECK_OUTPUT=$(shellcheck -f json $SHELL_FILES 2>&1)
|
|
428
739
|
SHELLCHECK_EXIT=$?
|
|
429
740
|
if [ "$SHELLCHECK_EXIT" -ne 0 ]; then
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if [ -
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
741
|
+
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
742
|
+
BASELINE_WARNINGS=$(echo "$SHELL_FILES" | node -e "
|
|
743
|
+
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
744
|
+
const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
|
|
745
|
+
const files=stdin?stdin.split('\n'):[];
|
|
746
|
+
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
747
|
+
console.log(total);
|
|
748
|
+
")
|
|
749
|
+
CURRENT_WARNINGS=$(echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
750
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
751
|
+
console.log(d.length);
|
|
752
|
+
")
|
|
753
|
+
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
754
|
+
NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
|
|
755
|
+
echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
756
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
757
|
+
for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
|
|
758
|
+
" | head -30
|
|
759
|
+
echo ""
|
|
760
|
+
echo "❌ BLOCKED - ${NEW_WARNINGS} NEW shellcheck issue(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
|
|
761
|
+
echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
|
|
762
|
+
exit 1
|
|
763
|
+
else
|
|
764
|
+
REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
|
|
765
|
+
if [ "$REDUCED" -gt 0 ]; then
|
|
766
|
+
echo "✅ PASSED - Shellcheck debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
|
|
767
|
+
else
|
|
768
|
+
echo "✅ PASSED - No new shellcheck issues (baseline: ${BASELINE_WARNINGS})"
|
|
447
769
|
fi
|
|
448
770
|
fi
|
|
449
|
-
|
|
450
|
-
|
|
771
|
+
else
|
|
772
|
+
echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
773
|
+
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
|
|
774
|
+
for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
|
|
775
|
+
" | head -30
|
|
776
|
+
echo ""
|
|
777
|
+
echo "❌ BLOCKED - SHELLCHECK ERRORS detected"
|
|
778
|
+
echo "Fix the shell script errors above."
|
|
779
|
+
echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
|
|
780
|
+
exit 1
|
|
781
|
+
fi
|
|
451
782
|
else
|
|
452
|
-
echo "✅ PASSED -
|
|
783
|
+
echo "✅ PASSED - Shellcheck completed."
|
|
453
784
|
fi
|
|
785
|
+
else
|
|
786
|
+
echo "ℹ️ shellcheck not available - Attempting basic syntax check"
|
|
787
|
+
# Basic check for staged shell script files
|
|
788
|
+
for sh_file in $SHELL_FILES; do
|
|
789
|
+
if [ -f "$sh_file" ]; then
|
|
790
|
+
bash -n "$sh_file"
|
|
791
|
+
if [ $? -ne 0 ]; then
|
|
792
|
+
echo "❌ BLOCKED - SYNTAX ERROR in $sh_file"
|
|
793
|
+
exit 1
|
|
794
|
+
fi
|
|
795
|
+
fi
|
|
796
|
+
done
|
|
797
|
+
echo "✅ PASSED - Basic shell script syntax check."
|
|
454
798
|
fi
|
|
455
799
|
|
|
456
800
|
else
|
|
@@ -484,24 +828,26 @@ else
|
|
|
484
828
|
fi
|
|
485
829
|
GATE_1_STATUS="PASS"
|
|
486
830
|
GATE_1_TOOL="${PROJECT_LANG}"
|
|
831
|
+
record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
|
|
487
832
|
|
|
488
833
|
# ============================================================================
|
|
489
834
|
# GATE 2: Duplicate Code Detection (NEW gate)
|
|
490
835
|
# Uses jscpd, or similar tools, specific to language
|
|
491
836
|
# ============================================================================
|
|
492
|
-
2>&1 echo ""
|
|
493
|
-
2>&1 echo "→ Gate 2: Duplicate code detection..."
|
|
837
|
+
2>&1 echo ""
|
|
838
|
+
2>&1 echo "→ Gate 2: Duplicate code detection..."
|
|
839
|
+
GATE_2_START=$(gate_start_ms)
|
|
494
840
|
|
|
495
841
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
496
842
|
echo "✅ PASSED - Skipped (no source code to analyze)."
|
|
497
843
|
|
|
498
844
|
elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
499
845
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
500
|
-
exit
|
|
846
|
+
exit 1
|
|
501
847
|
fi
|
|
502
848
|
|
|
503
849
|
echo "Running jscpd for duplicate code detection..."
|
|
504
|
-
jscpd --config jscpd.conf.json "$CHANGED_FILES" 2>&1 | head -30
|
|
850
|
+
jscpd --config jscpd.conf.json $(echo "$CHANGED_FILES" | tr '\n' ' ') 2>&1 | head -30
|
|
505
851
|
JSCPD_EXIT=$?
|
|
506
852
|
if [ "$JSCPD_EXIT" -ne 0 ]; then
|
|
507
853
|
echo "ℹ️ jscpd found duplicated code (warning, not blocking by default)"
|
|
@@ -523,12 +869,12 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
523
869
|
ruff check --select=DUP $(echo "$CHANGED_FILES" | grep "\.py$") 2>&1 | head -30
|
|
524
870
|
echo "✅ PASSED - ruff duplicate code check completed."
|
|
525
871
|
else
|
|
526
|
-
exit
|
|
872
|
+
exit 1
|
|
527
873
|
fi
|
|
528
874
|
|
|
529
875
|
elif [ "$PROJECT_LANG" = "go" ]; then
|
|
530
876
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
531
|
-
exit
|
|
877
|
+
exit 1
|
|
532
878
|
fi
|
|
533
879
|
|
|
534
880
|
echo "Running jscpd for Go duplicate code detection..."
|
|
@@ -550,7 +896,7 @@ else
|
|
|
550
896
|
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
|
|
551
897
|
if [ -n "$SOURCE_FILES" ]; then
|
|
552
898
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
553
|
-
exit
|
|
899
|
+
exit 1
|
|
554
900
|
fi
|
|
555
901
|
echo "Running jscpd for duplicate code detection..."
|
|
556
902
|
jscpd $SOURCE_FILES 2>&1 | head -15
|
|
@@ -560,13 +906,15 @@ else
|
|
|
560
906
|
fi
|
|
561
907
|
fi
|
|
562
908
|
GATE_2_STATUS="PASS"
|
|
909
|
+
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
563
910
|
|
|
564
911
|
# ============================================================================
|
|
565
912
|
# GATE 3: Cyclomatic Complexity Check (Old Gate 7)
|
|
566
913
|
# Uses lizard - keeps existing logic for consistency
|
|
567
914
|
# ============================================================================
|
|
568
|
-
2>&1 echo ""
|
|
569
|
-
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
915
|
+
2>&1 echo ""
|
|
916
|
+
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
917
|
+
GATE_3_START=$(gate_start_ms)
|
|
570
918
|
|
|
571
919
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
572
920
|
echo "✅ PASSED - Skipped (documentation project)."
|
|
@@ -616,19 +964,22 @@ else
|
|
|
616
964
|
echo "✅ PASSED - No source files to check for complexity."
|
|
617
965
|
fi
|
|
618
966
|
else
|
|
619
|
-
echo "
|
|
620
|
-
echo "
|
|
621
|
-
echo "
|
|
967
|
+
echo "⚠️ WARN - lizard not installed, complexity check not performed"
|
|
968
|
+
echo " Install with: pip install --user lizard"
|
|
969
|
+
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
970
|
+
GATE_3_STATUS="WARN"
|
|
622
971
|
fi
|
|
623
972
|
fi
|
|
624
973
|
GATE_3_STATUS="PASS"
|
|
974
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
625
975
|
|
|
626
976
|
# ============================================================================
|
|
627
977
|
# GATE 4: Principles Checker (Old Gate 6 - Clean Code + SOLID)
|
|
628
978
|
# Reuses existing principles checker logic
|
|
629
979
|
# ============================================================================
|
|
630
|
-
2>&1 echo ""
|
|
631
|
-
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
980
|
+
2>&1 echo ""
|
|
981
|
+
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
982
|
+
GATE_4_START=$(gate_start_ms)
|
|
632
983
|
|
|
633
984
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
634
985
|
echo "✅ PASSED - Skipped (documentation project)."
|
|
@@ -659,7 +1010,7 @@ else
|
|
|
659
1010
|
echo " - SOLID principle violations"
|
|
660
1011
|
echo " - architectural violations"
|
|
661
1012
|
npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format console
|
|
662
|
-
exit
|
|
1013
|
+
exit 1
|
|
663
1014
|
fi
|
|
664
1015
|
|
|
665
1016
|
echo "✅ PASSED - Principles checker (no errors found)."
|
|
@@ -683,13 +1034,15 @@ else
|
|
|
683
1034
|
fi
|
|
684
1035
|
fi
|
|
685
1036
|
GATE_4_STATUS="PASS"
|
|
1037
|
+
record_gate_audit "gate-4" "principles" "$GATE_4_STATUS" "${WARNING_COUNT:-0}" "$GATE_4_START"
|
|
686
1038
|
|
|
687
1039
|
# ============================================================================
|
|
688
1040
|
# GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
|
|
689
1041
|
# Uses adapter system to run tests + coverage for language
|
|
690
1042
|
# ============================================================================
|
|
691
|
-
2>&1 echo ""
|
|
692
|
-
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
1043
|
+
2>&1 echo ""
|
|
1044
|
+
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
1045
|
+
GATE_5_START=$(gate_start_ms)
|
|
693
1046
|
|
|
694
1047
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
695
1048
|
echo "✅ PASSED - Skipped (documentation project)."
|
|
@@ -815,7 +1168,7 @@ else
|
|
|
815
1168
|
# Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
|
|
816
1169
|
# 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
|
|
817
1170
|
# ========================================================================
|
|
818
|
-
CHANGED_TEST_FILES=$(git diff --cached --name-only | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
|
|
1171
|
+
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
|
|
819
1172
|
|
|
820
1173
|
if [ -n "$CHANGED_TEST_FILES" ]; then
|
|
821
1174
|
echo "Checking mock density in test files..."
|
|
@@ -827,7 +1180,8 @@ else
|
|
|
827
1180
|
'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
|
|
828
1181
|
'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
|
|
829
1182
|
'\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
|
|
830
|
-
c=$(grep -
|
|
1183
|
+
c=$(grep -c "$kw" "$test_file" 2>/dev/null)
|
|
1184
|
+
c=${c:-0}
|
|
831
1185
|
MOCK_COUNT=$((MOCK_COUNT + c))
|
|
832
1186
|
done
|
|
833
1187
|
|
|
@@ -873,7 +1227,7 @@ else
|
|
|
873
1227
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
874
1228
|
echo ""
|
|
875
1229
|
echo "❌ BLOCKED - Tests FAILED"
|
|
876
|
-
exit
|
|
1230
|
+
exit 1
|
|
877
1231
|
else
|
|
878
1232
|
echo "✅ PASSED - Unit tests passed."
|
|
879
1233
|
fi
|
|
@@ -1073,14 +1427,58 @@ else
|
|
|
1073
1427
|
echo "ℹ️ Coverage enforcement not applicable for ${PROJECT_LANG}"
|
|
1074
1428
|
fi
|
|
1075
1429
|
echo "✅ PASSED - Coverage check completed."
|
|
1430
|
+
|
|
1431
|
+
# ============================================================================
|
|
1432
|
+
# Stage 3: Per-file coverage check for NEW files (Issue #103)
|
|
1433
|
+
# New files must have ≥80% coverage individually.
|
|
1434
|
+
# ============================================================================
|
|
1435
|
+
if [ "$PROJECT_LANG" = "typescript" ] && [ -f "coverage/coverage-summary.json" ]; then
|
|
1436
|
+
NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
|
|
1437
|
+
if [ -n "$NEW_SOURCE_FILES" ]; then
|
|
1438
|
+
echo ""
|
|
1439
|
+
echo " └─ Per-file coverage check for new files:"
|
|
1440
|
+
for new_file in $NEW_SOURCE_FILES; do
|
|
1441
|
+
# Normalize path for coverage-summary.json lookup
|
|
1442
|
+
NORMALIZED_PATH=$(echo "$new_file" | sed 's|^\./||')
|
|
1443
|
+
FILE_COV=$(node -e "
|
|
1444
|
+
try {
|
|
1445
|
+
const fs = require('fs');
|
|
1446
|
+
const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
|
|
1447
|
+
const entry = data['$NORMALIZED_PATH'];
|
|
1448
|
+
if (entry && entry.lines) {
|
|
1449
|
+
console.log(Math.round(entry.lines.pct));
|
|
1450
|
+
} else {
|
|
1451
|
+
console.log('no_data');
|
|
1452
|
+
}
|
|
1453
|
+
} catch(e) { console.log('parse_error'); }
|
|
1454
|
+
" 2>/dev/null)
|
|
1455
|
+
if [ "$FILE_COV" = "no_data" ] || [ "$FILE_COV" = "parse_error" ]; then
|
|
1456
|
+
echo " ⚠️ $new_file — no coverage data (file may be excluded or not exercised)"
|
|
1457
|
+
elif [ "$FILE_COV" -lt 80 ]; then
|
|
1458
|
+
echo " ❌ $new_file — ${FILE_COV}% coverage (below 80% threshold)"
|
|
1459
|
+
NEW_FILE_COV_FAIL=true
|
|
1460
|
+
else
|
|
1461
|
+
echo " ✅ $new_file — ${FILE_COV}% coverage"
|
|
1462
|
+
fi
|
|
1463
|
+
done
|
|
1464
|
+
if [ "${NEW_FILE_COV_FAIL:-false}" = "true" ]; then
|
|
1465
|
+
echo ""
|
|
1466
|
+
echo "❌ BLOCKED - New file(s) below 80% coverage threshold"
|
|
1467
|
+
echo "Add tests to cover the new files before committing."
|
|
1468
|
+
exit 1
|
|
1469
|
+
fi
|
|
1470
|
+
fi
|
|
1471
|
+
fi
|
|
1076
1472
|
fi
|
|
1077
1473
|
GATE_5_STATUS="PASS"
|
|
1474
|
+
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START"
|
|
1078
1475
|
|
|
1079
1476
|
# ============================================================================
|
|
1080
1477
|
# GATE 6: Architecture & Tech Debt (Combines Old Gate 8 - Boy Scout and Gate 9 - Architecture)
|
|
1081
1478
|
# ============================================================================
|
|
1082
|
-
2>&1 echo ""
|
|
1083
|
-
2>&1 echo "→ Gate 6: Architecture & tech debt..."
|
|
1479
|
+
2>&1 echo ""
|
|
1480
|
+
2>&1 echo "→ Gate 6: Architecture & tech debt..."
|
|
1481
|
+
GATE_6_START=$(gate_start_ms)
|
|
1084
1482
|
|
|
1085
1483
|
# First Part: Architecture Validation (previously Gate 9)
|
|
1086
1484
|
2>&1 echo " └─ Architecture validation:"
|
|
@@ -1110,18 +1508,34 @@ else
|
|
|
1110
1508
|
echo " Install with: npm install --save-dev @archlinter/cli"
|
|
1111
1509
|
exit 1
|
|
1112
1510
|
fi
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1511
|
+
echo " Running archlint for TypeScript..."
|
|
1512
|
+
# Use diff mode with baseline for ratchet (block only new violations)
|
|
1513
|
+
# Fall back to raw scan if no baseline exists (new project, zero-tolerance)
|
|
1514
|
+
if [ -f ".architecture-baseline.json" ]; then
|
|
1515
|
+
ARCHLINT_OUTPUT=$($ARCHLINT_CMD diff .architecture-baseline.json --fail-on high 2>&1)
|
|
1516
|
+
else
|
|
1517
|
+
ARCHLINT_OUTPUT=$($ARCHLINT_CMD scan . -f table --quiet 2>&1)
|
|
1518
|
+
fi
|
|
1519
|
+
ARCHLINT_EXIT=$?
|
|
1520
|
+
echo "$ARCHLINT_OUTPUT" | tail -30
|
|
1521
|
+
if [ "$ARCHLINT_EXIT" -ne 0 ]; then
|
|
1522
|
+
echo ""
|
|
1523
|
+
echo "❌ BLOCKED - Architecture violations detected"
|
|
1524
|
+
if [ -f ".architecture-baseline.json" ]; then
|
|
1525
|
+
echo "New architecture violations found (baseline ratchet mode)."
|
|
1526
|
+
echo "Fix the new violations above, or update the baseline with: npx @archlinter/cli snapshot -o .architecture-baseline.json"
|
|
1527
|
+
else
|
|
1528
|
+
echo "Fix the architecture violations above before committing."
|
|
1529
|
+
echo "Or generate a baseline for gradual adoption: npx @archlinter/cli snapshot -o .architecture-baseline.json"
|
|
1530
|
+
fi
|
|
1531
|
+
exit 1
|
|
1532
|
+
fi
|
|
1533
|
+
if [ -f ".architecture-baseline.json" ]; then
|
|
1534
|
+
echo " ✅ TypeScript architecture validation completed (baseline ratchet mode)."
|
|
1535
|
+
else
|
|
1536
|
+
echo " ✅ TypeScript architecture validation completed (zero-tolerance mode). no baseline found."
|
|
1537
|
+
fi
|
|
1538
|
+
;;
|
|
1125
1539
|
"python")
|
|
1126
1540
|
if [ -f ".import-linter.yml" ] || [ -f "import_linter_config.yml" ]; then
|
|
1127
1541
|
if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
|
|
@@ -1131,7 +1545,7 @@ else
|
|
|
1131
1545
|
else
|
|
1132
1546
|
echo " ❌ BLOCKED - import-linter not installed"
|
|
1133
1547
|
echo " Install with: pip install import-linter"
|
|
1134
|
-
exit
|
|
1548
|
+
exit 1
|
|
1135
1549
|
fi
|
|
1136
1550
|
else
|
|
1137
1551
|
echo " ℹ️ No .import-linter.yml found - skipping Python architecture"
|
|
@@ -1147,7 +1561,7 @@ else
|
|
|
1147
1561
|
else
|
|
1148
1562
|
echo " ❌ BLOCKED - arch-go not installed"
|
|
1149
1563
|
echo " Install with: go install github.com/arch-go/arch-go@latest"
|
|
1150
|
-
exit
|
|
1564
|
+
exit 1
|
|
1151
1565
|
fi
|
|
1152
1566
|
else
|
|
1153
1567
|
echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
|
|
@@ -1183,10 +1597,11 @@ else
|
|
|
1183
1597
|
else
|
|
1184
1598
|
echo ""
|
|
1185
1599
|
echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
|
|
1186
|
-
echo " Required configuration file
|
|
1600
|
+
echo " Required configuration file (.archlint.yaml / architecture.yaml) is NOT found."
|
|
1187
1601
|
echo " Architecture Quality Gate requires explicit architecture constraints."
|
|
1188
1602
|
echo ""
|
|
1189
|
-
echo " Create
|
|
1603
|
+
echo " Create .archlint.yaml with: npx archlint init --no-interactive"
|
|
1604
|
+
echo " Or architecture.yaml for custom Clean Architecture layer definitions:"
|
|
1190
1605
|
echo " layers:"
|
|
1191
1606
|
echo " - name: api"
|
|
1192
1607
|
echo " paths: [\"src/api/**\"]"
|
|
@@ -1199,7 +1614,7 @@ else
|
|
|
1199
1614
|
echo ""
|
|
1200
1615
|
echo " After creating architecture.yaml, retry the commit."
|
|
1201
1616
|
echo " (To skip with warning, create .architecture-skip file)"
|
|
1202
|
-
exit
|
|
1617
|
+
exit 1
|
|
1203
1618
|
fi
|
|
1204
1619
|
fi
|
|
1205
1620
|
fi
|
|
@@ -1243,7 +1658,7 @@ else
|
|
|
1243
1658
|
echo " - NEW files: must have zero warnings"
|
|
1244
1659
|
echo " - MODIFIED files: cannot increase warnings from baseline"
|
|
1245
1660
|
echo " - Files with ≤5 warnings: must clear to zero"
|
|
1246
|
-
exit
|
|
1661
|
+
exit 1
|
|
1247
1662
|
fi
|
|
1248
1663
|
|
|
1249
1664
|
# Check output for violations
|
|
@@ -1253,7 +1668,7 @@ else
|
|
|
1253
1668
|
echo "$BOY_SCOUT_OUTPUT"
|
|
1254
1669
|
echo ""
|
|
1255
1670
|
echo "❌ BLOCKED - Boy Scout Rule violations detected ($VIOLATION_COUNT)"
|
|
1256
|
-
exit
|
|
1671
|
+
exit 1
|
|
1257
1672
|
fi
|
|
1258
1673
|
|
|
1259
1674
|
echo " ✅ PASSED - Boy Scout Rule compliance."
|
|
@@ -1269,6 +1684,7 @@ else
|
|
|
1269
1684
|
fi
|
|
1270
1685
|
fi
|
|
1271
1686
|
GATE_6_STATUS="PASS"
|
|
1687
|
+
record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCKED:-0}" "$GATE_6_START"
|
|
1272
1688
|
|
|
1273
1689
|
# ============================================================================
|
|
1274
1690
|
|
|
@@ -1277,8 +1693,9 @@ GATE_6_STATUS="PASS"
|
|
|
1277
1693
|
# Detects security issues in Infrastructure as Code files
|
|
1278
1694
|
# Tools: checkov (recommended), hadolint, kube-score, tflint
|
|
1279
1695
|
# ============================================================================
|
|
1280
|
-
2>&1 echo ""
|
|
1281
|
-
2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
|
|
1696
|
+
2>&1 echo ""
|
|
1697
|
+
2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
|
|
1698
|
+
GATE_7_START=$(gate_start_ms)
|
|
1282
1699
|
|
|
1283
1700
|
# Check if any IaC files are changed
|
|
1284
1701
|
IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
|
|
@@ -1306,7 +1723,7 @@ else
|
|
|
1306
1723
|
echo "Fix the security issues above before committing."
|
|
1307
1724
|
echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
|
|
1308
1725
|
GATE_7_STATUS="FAIL"
|
|
1309
|
-
exit
|
|
1726
|
+
exit 1
|
|
1310
1727
|
fi
|
|
1311
1728
|
else
|
|
1312
1729
|
echo "ℹ️ SKIP - IaC adapter not found"
|
|
@@ -1314,6 +1731,7 @@ else
|
|
|
1314
1731
|
GATE_7_STATUS="SKIP"
|
|
1315
1732
|
fi
|
|
1316
1733
|
fi
|
|
1734
|
+
record_gate_audit "gate-7" "iac-security" "$GATE_7_STATUS" "0" "$GATE_7_START"
|
|
1317
1735
|
|
|
1318
1736
|
|
|
1319
1737
|
# GATE 8: Secret Scanning (gitleaks)
|
|
@@ -1321,8 +1739,9 @@ fi
|
|
|
1321
1739
|
# Tool: gitleaks -- https://github.com/gitleaks/gitleaks
|
|
1322
1740
|
# ============================================================================
|
|
1323
1741
|
|
|
1324
|
-
2>&1 echo ""
|
|
1325
|
-
2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
|
|
1742
|
+
2>&1 echo ""
|
|
1743
|
+
2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
|
|
1744
|
+
GATE_8_START=$(gate_start_ms)
|
|
1326
1745
|
|
|
1327
1746
|
# Gitleaks availability check
|
|
1328
1747
|
GITLEAKS_CMD=""
|
|
@@ -1357,7 +1776,7 @@ if [ -n "$GITLEAKS_CMD" ]; then
|
|
|
1357
1776
|
echo " 3. Use git secret or vault for sensitive data"
|
|
1358
1777
|
echo ""
|
|
1359
1778
|
echo "See: https://github.com/gitleaks/gitleaks"
|
|
1360
|
-
exit
|
|
1779
|
+
exit 1
|
|
1361
1780
|
else
|
|
1362
1781
|
echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
|
|
1363
1782
|
echo " ✅ Secret Scanning (SKIP, gitleaks error)"
|
|
@@ -1369,6 +1788,7 @@ else
|
|
|
1369
1788
|
echo " ✅ Secret Scanning (SKIP, gitleaks not installed)"
|
|
1370
1789
|
GATE_8_STATUS="SKIP"
|
|
1371
1790
|
fi
|
|
1791
|
+
record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
|
|
1372
1792
|
|
|
1373
1793
|
# Switch back to original directory if we were in a subdirectory
|
|
1374
1794
|
if [ -n "$ORIGINAL_DIR" ]; then
|
|
@@ -1381,8 +1801,9 @@ fi
|
|
|
1381
1801
|
# Tool: semgrep -- https://semgrep.dev
|
|
1382
1802
|
# ============================================================================
|
|
1383
1803
|
|
|
1384
|
-
2>&1 echo ""
|
|
1385
|
-
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
1804
|
+
2>&1 echo ""
|
|
1805
|
+
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
1806
|
+
GATE_9_START=$(gate_start_ms)
|
|
1386
1807
|
|
|
1387
1808
|
GATE_9_STATUS="PASS"
|
|
1388
1809
|
|
|
@@ -1395,11 +1816,11 @@ elif [ -f "$HOME/.local/bin/semgrep" ]; then
|
|
|
1395
1816
|
fi
|
|
1396
1817
|
|
|
1397
1818
|
if [ -z "$SEMGREP_CMD" ]; then
|
|
1398
|
-
echo "
|
|
1819
|
+
echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
|
|
1399
1820
|
echo " Install: brew install semgrep | pip install semgrep"
|
|
1400
1821
|
echo " Pre-cache rules: semgrep --config=p/security-audit"
|
|
1401
|
-
echo "
|
|
1402
|
-
GATE_9_STATUS="
|
|
1822
|
+
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
1823
|
+
GATE_9_STATUS="WARN"
|
|
1403
1824
|
else
|
|
1404
1825
|
# Get staged files filtered to Semgrep-supported languages
|
|
1405
1826
|
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)
|
|
@@ -1485,7 +1906,7 @@ except:
|
|
|
1485
1906
|
echo "$FINDING_DETAILS"
|
|
1486
1907
|
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
1487
1908
|
GATE_9_STATUS="FAIL"
|
|
1488
|
-
exit
|
|
1909
|
+
exit 1
|
|
1489
1910
|
else
|
|
1490
1911
|
echo ""
|
|
1491
1912
|
echo " ✅ PASSED - No critical/high vulnerabilities"
|
|
@@ -1503,9 +1924,7 @@ except:
|
|
|
1503
1924
|
fi
|
|
1504
1925
|
fi
|
|
1505
1926
|
fi
|
|
1506
|
-
|
|
1507
|
-
# ============================================================================
|
|
1508
|
-
# QUALITY REPORT GENERATION
|
|
1927
|
+
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
1509
1928
|
# ============================================================================
|
|
1510
1929
|
|
|
1511
1930
|
generate_quality_report() {
|
|
@@ -1522,7 +1941,7 @@ generate_quality_report() {
|
|
|
1522
1941
|
|
|
1523
1942
|
for gate in 1 2 3 4 5 6 7 8 9; do
|
|
1524
1943
|
local status_var="GATE_${gate}_STATUS"
|
|
1525
|
-
if [ "${!status_var}" = "PASS" ]
|
|
1944
|
+
if [ "${!status_var}" = "PASS" ]; then
|
|
1526
1945
|
PASSED_COUNT=$((PASSED_COUNT + 1))
|
|
1527
1946
|
fi
|
|
1528
1947
|
done
|
|
@@ -1628,6 +2047,31 @@ generate_quality_report() {
|
|
|
1628
2047
|
}
|
|
1629
2048
|
ENDJSON
|
|
1630
2049
|
|
|
2050
|
+
# ── Persistent per-run report (Issue #101) ────────────────────────────────────────
|
|
2051
|
+
local REPORTS_DIR=".xp-gate/reports/pre-commit"
|
|
2052
|
+
local REPORT_TS
|
|
2053
|
+
REPORT_TS=$(date -u +"%Y-%m-%d-%H%M%S")
|
|
2054
|
+
local PERSISTED_REPORT_FILE="${REPORTS_DIR}/${REPORT_TS}.json"
|
|
2055
|
+
mkdir -p "$REPORTS_DIR"
|
|
2056
|
+
python3 - <<PY 2>/dev/null || cp "$REPORT_FILE" "$PERSISTED_REPORT_FILE"
|
|
2057
|
+
import json
|
|
2058
|
+
from pathlib import Path
|
|
2059
|
+
branch_report = json.loads(Path('$REPORT_FILE').read_text())
|
|
2060
|
+
changed_files = '''${CHANGED_FILES:-}'''.splitlines()
|
|
2061
|
+
report = {
|
|
2062
|
+
'timestamp': '$TIMESTAMP',
|
|
2063
|
+
'trigger': 'pre-commit',
|
|
2064
|
+
'branch': '$BRANCH',
|
|
2065
|
+
'commit': '$COMMIT_HASH',
|
|
2066
|
+
'changed_files': [f for f in changed_files if f],
|
|
2067
|
+
'overall': branch_report.get('overall', {}),
|
|
2068
|
+
'gates': branch_report.get('gates', {}),
|
|
2069
|
+
'warnings': [],
|
|
2070
|
+
'errors': [],
|
|
2071
|
+
}
|
|
2072
|
+
Path('$PERSISTED_REPORT_FILE').write_text(json.dumps(report, indent=2) + '\n')
|
|
2073
|
+
PY
|
|
2074
|
+
|
|
1631
2075
|
# ── History append (unchanged — append-only for trend) ────────────────────────────
|
|
1632
2076
|
local GATES_JSON="{"
|
|
1633
2077
|
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-PASS}\",\"name\":\"Code Quality\"},"
|
|
@@ -1661,6 +2105,7 @@ ENDJSON
|
|
|
1661
2105
|
echo ""
|
|
1662
2106
|
echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
|
|
1663
2107
|
echo " Branch status: $REPORT_FILE"
|
|
2108
|
+
echo " Run report: $PERSISTED_REPORT_FILE"
|
|
1664
2109
|
echo " History saved: $HISTORY_FILE"
|
|
1665
2110
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1666
2111
|
echo ""
|
|
@@ -1668,4 +2113,4 @@ ENDJSON
|
|
|
1668
2113
|
|
|
1669
2114
|
generate_quality_report
|
|
1670
2115
|
|
|
1671
|
-
exit
|
|
2116
|
+
exit 0
|