@boyingliu01/xp-gate 0.14.31 → 0.15.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/adapters/java.sh +33 -4
- package/gate-9.sh +22 -48
- package/hooks/gate-9.sh +22 -48
- package/hooks/pre-commit +102 -44
- package/hooks/pre-push +1 -33
- package/lib/__tests__/doctor.test.js +34 -31
- package/lib/detect-deps.js +12 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +71 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +71 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/adapters/python.ts +39 -6
- package/scripts/__tests__/sync-package-content.test.js +36 -17
- package/scripts/sync-package-content.js +5 -4
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +1 -0
- package/skills/sprint-flow/references/phase-3-build.md +71 -0
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/java.sh
CHANGED
|
@@ -79,16 +79,45 @@ run_tests() {
|
|
|
79
79
|
fi
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
_detect_jacoco_configured() {
|
|
83
|
+
local build_system="$1"
|
|
84
|
+
if [ "$build_system" = "maven" ]; then
|
|
85
|
+
# Check for jacoco-maven-plugin in pom.xml
|
|
86
|
+
grep -q 'jacoco-maven-plugin\|org.jacoco' pom.xml 2>/dev/null && return 0
|
|
87
|
+
# Check for jacoco.xml report already generated
|
|
88
|
+
[ -f "target/site/jacoco/jacoco.xml" ] && return 0
|
|
89
|
+
return 1
|
|
90
|
+
elif [ "$build_system" = "gradle" ]; then
|
|
91
|
+
# Check for jacoco plugin in build.gradle or build.gradle.kts
|
|
92
|
+
grep -q "id.*jacoco\|apply.*plugin.*jacoco\|jacoco" build.gradle build.gradle.kts 2>/dev/null && return 0
|
|
93
|
+
[ -f "build/reports/jacoco/test/jacocoTestReport.xml" ] && return 0
|
|
94
|
+
return 1
|
|
95
|
+
fi
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
run_coverage() {
|
|
83
100
|
local build_system
|
|
84
101
|
build_system=$(_detect_java_build)
|
|
85
102
|
|
|
86
103
|
if [ "$build_system" = "maven" ]; then
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
if _detect_jacoco_configured "$build_system"; then
|
|
105
|
+
mvn test jacoco:report -q 2>&1 | tail -10
|
|
106
|
+
return "${PIPESTATUS[0]}"
|
|
107
|
+
else
|
|
108
|
+
echo "ℹ️ JaCoCo not configured in pom.xml — SKIP coverage check"
|
|
109
|
+
echo " To enable: add jacoco-maven-plugin to your pom.xml"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
89
112
|
elif [ "$build_system" = "gradle" ]; then
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
if _detect_jacoco_configured "$build_system"; then
|
|
114
|
+
gradle jacocoTestReport --quiet 2>&1 | tail -10
|
|
115
|
+
return "${PIPESTATUS[0]}"
|
|
116
|
+
else
|
|
117
|
+
echo "ℹ️ JaCoCo not configured in Gradle build — SKIP coverage check"
|
|
118
|
+
echo " To enable: add 'id \"jacoco\"' to your build.gradle plugins block"
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
92
121
|
else
|
|
93
122
|
echo "No Maven/Gradle project detected"
|
|
94
123
|
return 1
|
package/gate-9.sh
CHANGED
|
@@ -44,57 +44,31 @@ else
|
|
|
44
44
|
GATE_9_STATUS="PASS"
|
|
45
45
|
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
46
46
|
# Findings detected - parse JSON to categorize
|
|
47
|
-
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" |
|
|
48
|
-
|
|
49
|
-
try
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for r in results:
|
|
54
|
-
extra = r.get('extra', {})
|
|
55
|
-
severity = extra.get('severity', '').upper()
|
|
56
|
-
if severity in ('CRITICAL', 'HIGH'):
|
|
57
|
-
count += 1
|
|
58
|
-
print(count)
|
|
59
|
-
except:
|
|
60
|
-
print('0')
|
|
61
|
-
" 2>/dev/null || echo "0")
|
|
47
|
+
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
48
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
49
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
50
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='CRITICAL'||s==='HIGH';}).length;
|
|
51
|
+
console.log(count);}catch(e){console.log('0');}
|
|
52
|
+
});" 2>/dev/null || echo "0")
|
|
62
53
|
|
|
63
|
-
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" |
|
|
64
|
-
|
|
65
|
-
try
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for r in results:
|
|
70
|
-
extra = r.get('extra', {})
|
|
71
|
-
severity = extra.get('severity', '').upper()
|
|
72
|
-
if severity in ('MEDIUM', 'LOW'):
|
|
73
|
-
count += 1
|
|
74
|
-
print(count)
|
|
75
|
-
except:
|
|
76
|
-
print('0')
|
|
77
|
-
" 2>/dev/null || echo "0")
|
|
54
|
+
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
55
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
56
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
57
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='MEDIUM'||s==='LOW';}).length;
|
|
58
|
+
console.log(count);}catch(e){console.log('0');}
|
|
59
|
+
});" 2>/dev/null || echo "0")
|
|
78
60
|
|
|
79
61
|
# Extract top finding details
|
|
80
|
-
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" |
|
|
81
|
-
|
|
82
|
-
try
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
start_line = r.get('start', {}).get('line', '?')
|
|
91
|
-
message = extra.get('message', '')[:80]
|
|
92
|
-
print(f' [{severity}] {rule_id}')
|
|
93
|
-
print(f' {path}:{start_line} → {message}')
|
|
94
|
-
print()
|
|
95
|
-
except:
|
|
96
|
-
print(' (Failed to parse semgrep output)')
|
|
97
|
-
" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
62
|
+
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
63
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
64
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
65
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};
|
|
66
|
+
const severity=(extra.severity||'UNKNOWN').toUpperCase();
|
|
67
|
+
const ruleId=r.check_id||'unknown';const path=r.path||'unknown';
|
|
68
|
+
const line=(r.start&&r.start.line)||'?';const msg=(extra.message||'').slice(0,80);
|
|
69
|
+
console.log(' ['+severity+'] '+ruleId);console.log(' '+path+':'+line+' → '+msg);console.log();});
|
|
70
|
+
}catch(e){console.log(' (Failed to parse semgrep output)');}
|
|
71
|
+
});" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
98
72
|
|
|
99
73
|
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
100
74
|
echo ""
|
package/hooks/gate-9.sh
CHANGED
|
@@ -44,57 +44,31 @@ else
|
|
|
44
44
|
GATE_9_STATUS="PASS"
|
|
45
45
|
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
46
46
|
# Findings detected - parse JSON to categorize
|
|
47
|
-
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" |
|
|
48
|
-
|
|
49
|
-
try
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for r in results:
|
|
54
|
-
extra = r.get('extra', {})
|
|
55
|
-
severity = extra.get('severity', '').upper()
|
|
56
|
-
if severity in ('CRITICAL', 'HIGH'):
|
|
57
|
-
count += 1
|
|
58
|
-
print(count)
|
|
59
|
-
except:
|
|
60
|
-
print('0')
|
|
61
|
-
" 2>/dev/null || echo "0")
|
|
47
|
+
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
48
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
49
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
50
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='CRITICAL'||s==='HIGH';}).length;
|
|
51
|
+
console.log(count);}catch(e){console.log('0');}
|
|
52
|
+
});" 2>/dev/null || echo "0")
|
|
62
53
|
|
|
63
|
-
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" |
|
|
64
|
-
|
|
65
|
-
try
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for r in results:
|
|
70
|
-
extra = r.get('extra', {})
|
|
71
|
-
severity = extra.get('severity', '').upper()
|
|
72
|
-
if severity in ('MEDIUM', 'LOW'):
|
|
73
|
-
count += 1
|
|
74
|
-
print(count)
|
|
75
|
-
except:
|
|
76
|
-
print('0')
|
|
77
|
-
" 2>/dev/null || echo "0")
|
|
54
|
+
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
55
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
56
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
57
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='MEDIUM'||s==='LOW';}).length;
|
|
58
|
+
console.log(count);}catch(e){console.log('0');}
|
|
59
|
+
});" 2>/dev/null || echo "0")
|
|
78
60
|
|
|
79
61
|
# Extract top finding details
|
|
80
|
-
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" |
|
|
81
|
-
|
|
82
|
-
try
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
start_line = r.get('start', {}).get('line', '?')
|
|
91
|
-
message = extra.get('message', '')[:80]
|
|
92
|
-
print(f' [{severity}] {rule_id}')
|
|
93
|
-
print(f' {path}:{start_line} → {message}')
|
|
94
|
-
print()
|
|
95
|
-
except:
|
|
96
|
-
print(' (Failed to parse semgrep output)')
|
|
97
|
-
" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
62
|
+
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
63
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
64
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
65
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};
|
|
66
|
+
const severity=(extra.severity||'UNKNOWN').toUpperCase();
|
|
67
|
+
const ruleId=r.check_id||'unknown';const path=r.path||'unknown';
|
|
68
|
+
const line=(r.start&&r.start.line)||'?';const msg=(extra.message||'').slice(0,80);
|
|
69
|
+
console.log(' ['+severity+'] '+ruleId);console.log(' '+path+':'+line+' → '+msg);console.log();});
|
|
70
|
+
}catch(e){console.log(' (Failed to parse semgrep output)');}
|
|
71
|
+
});" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
98
72
|
|
|
99
73
|
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
100
74
|
echo ""
|
package/hooks/pre-commit
CHANGED
|
@@ -212,13 +212,20 @@ if [ "$is_protected" = "true" ]; then
|
|
|
212
212
|
|
|
213
213
|
# If either VERSION or CHANGELOG.md is staged, pass
|
|
214
214
|
if [ "$has_version_change" = "true" ] || [ "$has_changelog_change" = "true" ]; then
|
|
215
|
-
# Auto-run sync-version
|
|
216
|
-
# Propagates VERSION → 4 package.json + refreshes
|
|
217
|
-
# then stages only files sync-version
|
|
215
|
+
# Auto-run sync-version when VERSION is staged (issue #206 follow-up).
|
|
216
|
+
# Propagates VERSION → 4 package.json + refreshes AGENTS.md headers,
|
|
217
|
+
# then stages only files sync-version is known to touch (NOT runtime
|
|
218
218
|
# journals or unrelated edits) so the commit captures it atomically.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
# Prefer Node.js (.cjs) for cross-platform; fall back to bash (.sh).
|
|
220
|
+
SYNC_VERSION_CMD=""
|
|
221
|
+
if [ -f "$ROOT_DIR/scripts/sync-version.cjs" ] && command -v node >/dev/null 2>&1; then
|
|
222
|
+
SYNC_VERSION_CMD="node $ROOT_DIR/scripts/sync-version.cjs"
|
|
223
|
+
elif [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
|
|
224
|
+
SYNC_VERSION_CMD="bash $ROOT_DIR/scripts/sync-version.sh"
|
|
225
|
+
fi
|
|
226
|
+
if [ "$has_version_change" = "true" ] && [ -n "$SYNC_VERSION_CMD" ]; then
|
|
227
|
+
echo "🔄 Gate 0: VERSION staged — running sync-version to fan out to package.json + AGENTS.md headers..."
|
|
228
|
+
if $SYNC_VERSION_CMD > /tmp/xp-gate-sync-version.log 2>&1; then
|
|
222
229
|
SYNC_STAGED=0
|
|
223
230
|
# Stage the 4 known package.json targets if modified
|
|
224
231
|
for pkg in package.json src/npm-package/package.json \
|
|
@@ -238,9 +245,9 @@ if [ "$is_protected" = "true" ]; then
|
|
|
238
245
|
SYNC_STAGED=$((SYNC_STAGED + 1))
|
|
239
246
|
fi
|
|
240
247
|
done < <(find "$ROOT_DIR" -name 'AGENTS.md' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0)
|
|
241
|
-
echo " auto-staged $SYNC_STAGED file(s) from sync-version
|
|
248
|
+
echo " auto-staged $SYNC_STAGED file(s) from sync-version (package.json × ≤6 + AGENTS.md headers)"
|
|
242
249
|
else
|
|
243
|
-
echo "⚠️ sync-version
|
|
250
|
+
echo "⚠️ sync-version failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
|
|
244
251
|
fi
|
|
245
252
|
fi
|
|
246
253
|
echo "✅ PASSED - Gate 0: Version Consistency Check (VERSION/CHANGELOG staged)"
|
|
@@ -314,7 +321,7 @@ This bypasses the ship workflow and causes version drift.
|
|
|
314
321
|
Before committing:
|
|
315
322
|
1. Update VERSION file with new version
|
|
316
323
|
2. Add entry to CHANGELOG.md
|
|
317
|
-
3. Run:
|
|
324
|
+
3. Run: node scripts/sync-version.cjs
|
|
318
325
|
|
|
319
326
|
Or use --no-verify (discouraged) or include [skip-version-check] in commit message.
|
|
320
327
|
|
|
@@ -347,13 +354,25 @@ fi
|
|
|
347
354
|
# v0.15.1+; ARCH-03 fix), PROJECT_SUBDIRS (per-language subdir override, empty = root).
|
|
348
355
|
# ============================================================================
|
|
349
356
|
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
# Language override resolution (project-level > git config > env var)
|
|
358
|
+
# Priority: .xp-gate-lang file > git config xp-gate.lang > XP_GATE_LANG env
|
|
359
|
+
_RESOLVED_LANG_OVERRIDE=""
|
|
360
|
+
if [ -f ".xp-gate-lang" ]; then
|
|
361
|
+
_RESOLVED_LANG_OVERRIDE=$(sed -n '1p' .xp-gate-lang 2>/dev/null | tr -d '[:space:]')
|
|
362
|
+
elif git config xp-gate.lang >/dev/null 2>&1; then
|
|
363
|
+
_RESOLVED_LANG_OVERRIDE=$(git config xp-gate.lang 2>/dev/null)
|
|
364
|
+
elif [ -n "${XP_GATE_LANG:-}" ]; then
|
|
365
|
+
_RESOLVED_LANG_OVERRIDE="$XP_GATE_LANG"
|
|
366
|
+
echo "⚠️ XP_GATE_LANG env var is deprecated for multi-project use."
|
|
367
|
+
echo " Prefer: echo '$XP_GATE_LANG' > .xp-gate-lang OR git config xp-gate.lang $XP_GATE_LANG"
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [ -n "$_RESOLVED_LANG_OVERRIDE" ]; then
|
|
371
|
+
PROJECT_LANGS="$_RESOLVED_LANG_OVERRIDE"
|
|
372
|
+
PROJECT_LANG="$_RESOLVED_LANG_OVERRIDE"
|
|
354
373
|
echo ""
|
|
355
374
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
356
|
-
echo " 🔧
|
|
375
|
+
echo " 🔧 LANGUAGE OVERRIDE: $PROJECT_LANG"
|
|
357
376
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
358
377
|
echo ""
|
|
359
378
|
else
|
|
@@ -682,7 +701,7 @@ record_gate_audit() {
|
|
|
682
701
|
local issues_found="$4"
|
|
683
702
|
local start_ms="$5"
|
|
684
703
|
local end_ms
|
|
685
|
-
end_ms=$(date +%s%3N 2>/dev/null ||
|
|
704
|
+
end_ms=$(date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0")
|
|
686
705
|
local duration_ms=$((end_ms - start_ms))
|
|
687
706
|
if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
|
|
688
707
|
duration_ms=0
|
|
@@ -710,7 +729,7 @@ record_gate_audit() {
|
|
|
710
729
|
|
|
711
730
|
# Get milliseconds timestamp (used by gates for duration measurement)
|
|
712
731
|
gate_start_ms() {
|
|
713
|
-
date +%s%3N 2>/dev/null ||
|
|
732
|
+
date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0"
|
|
714
733
|
}
|
|
715
734
|
|
|
716
735
|
# Switch to subdirectory if detected (for single-language subdirectory projects).
|
|
@@ -825,13 +844,13 @@ else
|
|
|
825
844
|
# Compare against lint baseline
|
|
826
845
|
BASELINE_WARNINGS=$(echo "$ESLINT_FILES" | tr ' ' '\n' | node -e "
|
|
827
846
|
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
828
|
-
const stdin=require('fs').readFileSync(
|
|
847
|
+
const stdin=require('fs').readFileSync(0,'utf8').trim();
|
|
829
848
|
const files=stdin?stdin.split('\n'):[];
|
|
830
849
|
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
831
850
|
console.log(total);
|
|
832
851
|
")
|
|
833
852
|
CURRENT_WARNINGS=$(echo "$ESLINT_OUTPUT" | node -e "
|
|
834
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
853
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
835
854
|
console.log(d.reduce((s,f)=>s+(f.warningCount||0)+(f.errorCount||0),0));
|
|
836
855
|
")
|
|
837
856
|
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
@@ -900,6 +919,20 @@ else
|
|
|
900
919
|
fi
|
|
901
920
|
fi
|
|
902
921
|
fi
|
|
922
|
+
|
|
923
|
+
# Debug statement detection (debugger statement in JS/TS)
|
|
924
|
+
TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
|
|
925
|
+
if [ -n "$TS_DEBUG_FILES" ]; then
|
|
926
|
+
DEBUGGER_HITS=$(grep -n '^\s*debugger\s*;\?\s*$' $TS_DEBUG_FILES 2>/dev/null || true)
|
|
927
|
+
if [ -n "$DEBUGGER_HITS" ]; then
|
|
928
|
+
echo ""
|
|
929
|
+
echo "❌ BLOCKED - 'debugger' statements detected:"
|
|
930
|
+
echo "$DEBUGGER_HITS" | head -10
|
|
931
|
+
echo ""
|
|
932
|
+
echo "Remove debugger statements before committing."
|
|
933
|
+
exit 1
|
|
934
|
+
fi
|
|
935
|
+
fi
|
|
903
936
|
fi
|
|
904
937
|
;;
|
|
905
938
|
|
|
@@ -920,19 +953,19 @@ else
|
|
|
920
953
|
if [ -n "$PY_FILES" ]; then
|
|
921
954
|
BASELINE_WARNINGS=$(echo "$PY_FILES" | node -e "
|
|
922
955
|
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
923
|
-
const stdin=require('fs').readFileSync(
|
|
956
|
+
const stdin=require('fs').readFileSync(0,'utf8').trim();
|
|
924
957
|
const files=stdin?stdin.split('\n'):[];
|
|
925
958
|
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
926
959
|
console.log(total);
|
|
927
960
|
")
|
|
928
961
|
CURRENT_WARNINGS=$(echo "$RUFF_OUTPUT" | node -e "
|
|
929
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
962
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
930
963
|
console.log(d.reduce((s,f)=>s+(f.messages?f.messages.length:0),0));
|
|
931
964
|
")
|
|
932
965
|
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
933
966
|
NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
|
|
934
967
|
echo "$RUFF_OUTPUT" | node -e "
|
|
935
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
968
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
936
969
|
for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
|
|
937
970
|
" | head -30
|
|
938
971
|
echo ""
|
|
@@ -952,7 +985,7 @@ else
|
|
|
952
985
|
fi
|
|
953
986
|
else
|
|
954
987
|
echo "$RUFF_OUTPUT" | node -e "
|
|
955
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
988
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
956
989
|
for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
|
|
957
990
|
" | head -30
|
|
958
991
|
echo ""
|
|
@@ -965,6 +998,37 @@ else
|
|
|
965
998
|
else
|
|
966
999
|
echo "✅ PASSED - Ruff linting."
|
|
967
1000
|
fi
|
|
1001
|
+
|
|
1002
|
+
# Ruff format check (formatting gate)
|
|
1003
|
+
if command -v ruff &> /dev/null; then
|
|
1004
|
+
PY_FMT_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
|
|
1005
|
+
if [ -n "$PY_FMT_FILES" ]; then
|
|
1006
|
+
RUFF_FMT_OUTPUT=$(ruff format --check $PY_FMT_FILES 2>&1)
|
|
1007
|
+
RUFF_FMT_EXIT=$?
|
|
1008
|
+
if [ "$RUFF_FMT_EXIT" -ne 0 ]; then
|
|
1009
|
+
echo "$RUFF_FMT_OUTPUT" | head -20
|
|
1010
|
+
echo ""
|
|
1011
|
+
echo "❌ BLOCKED - Ruff format check failed"
|
|
1012
|
+
echo "Fix formatting: run 'ruff format .' to auto-format."
|
|
1013
|
+
exit 1
|
|
1014
|
+
fi
|
|
1015
|
+
echo "✅ PASSED - Ruff format check."
|
|
1016
|
+
fi
|
|
1017
|
+
fi
|
|
1018
|
+
|
|
1019
|
+
# Debug statement detection (breakpoint/pdb/ipdb)
|
|
1020
|
+
PY_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
|
|
1021
|
+
if [ -n "$PY_DEBUG_FILES" ]; then
|
|
1022
|
+
DEBUG_HITS=$(grep -n 'breakpoint()\|pdb\.set_trace()\|ipdb\.set_trace()\|import pdb\|import ipdb' $PY_DEBUG_FILES 2>/dev/null || true)
|
|
1023
|
+
if [ -n "$DEBUG_HITS" ]; then
|
|
1024
|
+
echo ""
|
|
1025
|
+
echo "❌ BLOCKED - Debug statements detected in Python files:"
|
|
1026
|
+
echo "$DEBUG_HITS" | head -10
|
|
1027
|
+
echo ""
|
|
1028
|
+
echo "Remove breakpoint()/pdb.set_trace() before committing."
|
|
1029
|
+
exit 1
|
|
1030
|
+
fi
|
|
1031
|
+
fi
|
|
968
1032
|
|
|
969
1033
|
# Optional: Type checking with mypy
|
|
970
1034
|
if command -v mypy &> /dev/null; then
|
|
@@ -1018,19 +1082,19 @@ else
|
|
|
1018
1082
|
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
1019
1083
|
BASELINE_WARNINGS=$(echo "$SHELL_FILES" | node -e "
|
|
1020
1084
|
const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
|
|
1021
|
-
const stdin=require('fs').readFileSync(
|
|
1085
|
+
const stdin=require('fs').readFileSync(0,'utf8').trim();
|
|
1022
1086
|
const files=stdin?stdin.split('\n'):[];
|
|
1023
1087
|
let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
|
|
1024
1088
|
console.log(total);
|
|
1025
1089
|
")
|
|
1026
1090
|
CURRENT_WARNINGS=$(echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
1027
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
1091
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
1028
1092
|
console.log(d.length);
|
|
1029
1093
|
")
|
|
1030
1094
|
if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
|
|
1031
1095
|
NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
|
|
1032
1096
|
echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
1033
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
1097
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
1034
1098
|
for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
|
|
1035
1099
|
" | head -30
|
|
1036
1100
|
echo ""
|
|
@@ -1047,7 +1111,7 @@ else
|
|
|
1047
1111
|
fi
|
|
1048
1112
|
else
|
|
1049
1113
|
echo "$SHELLCHECK_OUTPUT" | node -e "
|
|
1050
|
-
const d=JSON.parse(require('fs').readFileSync(
|
|
1114
|
+
const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
1051
1115
|
for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
|
|
1052
1116
|
" | head -30
|
|
1053
1117
|
echo ""
|
|
@@ -1476,6 +1540,13 @@ else
|
|
|
1476
1540
|
# ========================================================================
|
|
1477
1541
|
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
|
|
1478
1542
|
|
|
1543
|
+
# Strip PROJECT_SUBDIR prefix from test file paths when running in subdirectory.
|
|
1544
|
+
# git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR" (line 728)
|
|
1545
|
+
# vitest and [ -f ] checks need subdirectory-relative paths. (v0.14.31+ fix)
|
|
1546
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_TEST_FILES" ]; then
|
|
1547
|
+
CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1548
|
+
fi
|
|
1549
|
+
|
|
1479
1550
|
if [ -n "$CHANGED_TEST_FILES" ]; then
|
|
1480
1551
|
echo "Checking mock density in test files..."
|
|
1481
1552
|
for test_file in $CHANGED_TEST_FILES; do
|
|
@@ -1563,6 +1634,10 @@ else
|
|
|
1563
1634
|
|
|
1564
1635
|
# Also get changed source files (non-test) for related-test lookup
|
|
1565
1636
|
CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
|
|
1637
|
+
# Strip PROJECT_SUBDIR prefix (same as CHANGED_TEST_FILES fix above)
|
|
1638
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_SRC_FILES" ]; then
|
|
1639
|
+
CHANGED_SRC_FILES=$(echo "$CHANGED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1640
|
+
fi
|
|
1566
1641
|
CHANGED_SRC_COUNT=$(echo "$CHANGED_SRC_FILES" | grep -c '.' || echo "0")
|
|
1567
1642
|
|
|
1568
1643
|
# Decide strategy
|
|
@@ -2447,24 +2522,7 @@ ENDJSON
|
|
|
2447
2522
|
REPORT_TS=$(date -u +"%Y-%m-%d-%H%M%S")
|
|
2448
2523
|
local PERSISTED_REPORT_FILE="${REPORTS_DIR}/${REPORT_TS}.json"
|
|
2449
2524
|
mkdir -p "$REPORTS_DIR"
|
|
2450
|
-
|
|
2451
|
-
import json
|
|
2452
|
-
from pathlib import Path
|
|
2453
|
-
branch_report = json.loads(Path('$REPORT_FILE').read_text())
|
|
2454
|
-
changed_files = '''${CHANGED_FILES:-}'''.splitlines()
|
|
2455
|
-
report = {
|
|
2456
|
-
'timestamp': '$TIMESTAMP',
|
|
2457
|
-
'trigger': 'pre-commit',
|
|
2458
|
-
'branch': '$BRANCH',
|
|
2459
|
-
'commit': '$COMMIT_HASH',
|
|
2460
|
-
'changed_files': [f for f in changed_files if f],
|
|
2461
|
-
'overall': branch_report.get('overall', {}),
|
|
2462
|
-
'gates': branch_report.get('gates', {}),
|
|
2463
|
-
'warnings': [],
|
|
2464
|
-
'errors': [],
|
|
2465
|
-
}
|
|
2466
|
-
Path('$PERSISTED_REPORT_FILE').write_text(json.dumps(report, indent=2) + '\n')
|
|
2467
|
-
PY
|
|
2525
|
+
_XP_RF="$REPORT_FILE" _XP_PRF="$PERSISTED_REPORT_FILE" _XP_TS="$TIMESTAMP" _XP_BR="$BRANCH" _XP_CH="$COMMIT_HASH" _XP_CF="${CHANGED_FILES:-}" node -e "const fs=require('fs');const e=process.env;const br=JSON.parse(fs.readFileSync(e._XP_RF,'utf8'));const cf=(e._XP_CF||'').split('\n').filter(f=>f);const r={timestamp:e._XP_TS,trigger:'pre-commit',branch:e._XP_BR,commit:e._XP_CH,changed_files:cf,overall:br.overall||{},gates:br.gates||{},warnings:[],errors:[]};fs.writeFileSync(e._XP_PRF,JSON.stringify(r,null,2)+'\n');" 2>/dev/null || cp "$REPORT_FILE" "$PERSISTED_REPORT_FILE"
|
|
2468
2526
|
|
|
2469
2527
|
# ── History append (unchanged — append-only for trend) ────────────────────────────
|
|
2470
2528
|
local GATES_JSON="{"
|
package/hooks/pre-push
CHANGED
|
@@ -27,39 +27,7 @@ write_pre_push_report() {
|
|
|
27
27
|
commit_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
28
28
|
verdict=$([ "$exit_code" -eq 0 ] && echo "PASS" || echo "BLOCKED")
|
|
29
29
|
mkdir -p "$reports_dir"
|
|
30
|
-
|
|
31
|
-
import json
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
pushed_files = '''${PUSHED_FILES:-}'''.splitlines()
|
|
34
|
-
report = {
|
|
35
|
-
'timestamp': '$timestamp',
|
|
36
|
-
'trigger': 'pre-push',
|
|
37
|
-
'branch': '$CURRENT_BRANCH',
|
|
38
|
-
'commit': '$commit_hash',
|
|
39
|
-
'changed_files': [f for f in pushed_files if f],
|
|
40
|
-
'overall': {
|
|
41
|
-
'score': '10.0/10' if '$verdict' == 'PASS' else '0.0/10',
|
|
42
|
-
'gates_passed': 1 if '$verdict' == 'PASS' else 0,
|
|
43
|
-
'gates_total': 1,
|
|
44
|
-
'verdict': '$verdict',
|
|
45
|
-
},
|
|
46
|
-
'gates': [
|
|
47
|
-
{'id': '10', 'name': 'Build Integrity', 'status': '${GATE_10_STATUS:-SKIP}', 'details': {}},
|
|
48
|
-
{'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details': {}},
|
|
49
|
-
{'id': 'M-Python', 'name': 'Mutation Testing (Python)', 'status': '${GATE_M_PYTHON_STATUS:-SKIP}', 'details': {}},
|
|
50
|
-
{'id': 'M-Go', 'name': 'Mutation Testing (Go)', 'status': '${GATE_M_GO_STATUS:-SKIP}', 'details': {}},
|
|
51
|
-
{'id': 'M-Java', 'name': 'Mutation Testing (Java)', 'status': '${GATE_M_JAVA_STATUS:-SKIP}', 'details': {}},
|
|
52
|
-
{'id': 'M-Kotlin', 'name': 'Mutation Testing (Kotlin)', 'status': '${GATE_M_KOTLIN_STATUS:-SKIP}', 'details': {}},
|
|
53
|
-
{'id': 'MD', 'name': 'Mock Density', 'status': '${GATE_M2_STATUS:-SKIP}', 'details': {}},
|
|
54
|
-
{'id': 'ML', 'name': 'Mock Layering', 'status': '${GATE_M3_STATUS:-SKIP}', 'details': {}},
|
|
55
|
-
{'id': 'UI', 'name': 'UI Sprint Gates', 'status': '${GATE_UI_STATUS:-SKIP}', 'details': {}},
|
|
56
|
-
{'id': 'MW', 'name': 'Code Walkthrough', 'status': '${GATE_DELPHI_STATUS:-SKIP}', 'details': {'remote': '$REMOTE', 'url': '$URL'}}
|
|
57
|
-
],
|
|
58
|
-
'warnings': [],
|
|
59
|
-
'errors': [] if '$verdict' == 'PASS' else ['pre-push hook exited with code $exit_code'],
|
|
60
|
-
}
|
|
61
|
-
Path('$report_file').write_text(json.dumps(report, indent=2) + '\n')
|
|
62
|
-
PY
|
|
30
|
+
_XP_RF="$report_file" _XP_TS="$timestamp" _XP_BR="$CURRENT_BRANCH" _XP_CH="$commit_hash" _XP_PF="${PUSHED_FILES:-}" _XP_VD="$verdict" _XP_EC="$exit_code" _XP_G10="${GATE_10_STATUS:-SKIP}" _XP_GM="${GATE_M_STATUS:-SKIP}" _XP_GMP="${GATE_M_PYTHON_STATUS:-SKIP}" _XP_GMGO="${GATE_M_GO_STATUS:-SKIP}" _XP_GMJA="${GATE_M_JAVA_STATUS:-SKIP}" _XP_GMKO="${GATE_M_KOTLIN_STATUS:-SKIP}" _XP_GMD="${GATE_M2_STATUS:-SKIP}" _XP_GML="${GATE_M3_STATUS:-SKIP}" _XP_GUI="${GATE_UI_STATUS:-SKIP}" _XP_GMW="${GATE_DELPHI_STATUS:-SKIP}" _XP_RM="$REMOTE" _XP_URL="$URL" node -e "const fs=require('fs');const e=process.env;const pf=(e._XP_PF||'').split('\n').filter(f=>f);const r={timestamp:e._XP_TS,trigger:'pre-push',branch:e._XP_BR,commit:e._XP_CH,changed_files:pf,overall:{score:e._XP_VD==='PASS'?'10.0/10':'0.0/10',gates_passed:e._XP_VD==='PASS'?1:0,gates_total:1,verdict:e._XP_VD},gates:[{id:'10',name:'Build Integrity',status:e._XP_G10,details:{}},{id:'M',name:'Mutation Testing',status:e._XP_GM,details:{}},{id:'M-Python',name:'Mutation Testing (Python)',status:e._XP_GMP,details:{}},{id:'M-Go',name:'Mutation Testing (Go)',status:e._XP_GMGO,details:{}},{id:'M-Java',name:'Mutation Testing (Java)',status:e._XP_GMJA,details:{}},{id:'M-Kotlin',name:'Mutation Testing (Kotlin)',status:e._XP_GMKO,details:{}},{id:'MD',name:'Mock Density',status:e._XP_GMD,details:{}},{id:'ML',name:'Mock Layering',status:e._XP_GML,details:{}},{id:'UI',name:'UI Sprint Gates',status:e._XP_GUI,details:{}},{id:'MW',name:'Code Walkthrough',status:e._XP_GMW,details:{remote:e._XP_RM,url:e._XP_URL}}],warnings:[],errors:e._XP_VD==='PASS'?[]:['pre-push hook exited with code '+e._XP_EC]};fs.writeFileSync(e._XP_RF,JSON.stringify(r,null,2)+'\n');" 2>/dev/null || true
|
|
63
31
|
}
|
|
64
32
|
|
|
65
33
|
_pre_push_report_on_exit() {
|