@boyingliu01/xp-gate 0.10.16 → 0.11.0
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/build-integrity/gate-10.ts +24 -0
- package/hooks/pre-commit +303 -391
- package/hooks/pre-push +429 -28
- 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/delphi-review/SKILL.md +79 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +3 -3
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +79 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/tui-plugin.tsx +306 -0
- 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/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +79 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-push
CHANGED
|
@@ -15,6 +15,60 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
15
15
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
16
16
|
REMOTE="$1"
|
|
17
17
|
URL="$2"
|
|
18
|
+
_PRE_PUSH_REPORT_DONE=0
|
|
19
|
+
|
|
20
|
+
write_pre_push_report() {
|
|
21
|
+
local exit_code="$1"
|
|
22
|
+
local timestamp report_ts reports_dir report_file commit_hash verdict
|
|
23
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
24
|
+
report_ts=$(date -u +"%Y-%m-%d-%H%M%S")
|
|
25
|
+
reports_dir=".xp-gate/reports/pre-push"
|
|
26
|
+
report_file="${reports_dir}/${report_ts}.json"
|
|
27
|
+
commit_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
28
|
+
verdict=$([ "$exit_code" -eq 0 ] && echo "PASS" || echo "BLOCKED")
|
|
29
|
+
mkdir -p "$reports_dir"
|
|
30
|
+
python3 - <<PY 2>/dev/null || true
|
|
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': 'M2', 'name': 'Mock Density', 'status': '${GATE_M2_STATUS:-SKIP}', 'details': {}},
|
|
51
|
+
{'id': 'M3', 'name': 'Mock Layering', 'status': '${GATE_M3_STATUS:-SKIP}', 'details': {}},
|
|
52
|
+
{'id': 'UI', 'name': 'UI Sprint Gates', 'status': '${GATE_UI_STATUS:-SKIP}', 'details': {}},
|
|
53
|
+
{'id': 'Delphi', 'name': 'Code Walkthrough', 'status': '${GATE_DELPHI_STATUS:-SKIP}', 'details': {'remote': '$REMOTE', 'url': '$URL'}}
|
|
54
|
+
],
|
|
55
|
+
'warnings': [],
|
|
56
|
+
'errors': [] if '$verdict' == 'PASS' else ['pre-push hook exited with code $exit_code'],
|
|
57
|
+
}
|
|
58
|
+
Path('$report_file').write_text(json.dumps(report, indent=2) + '\n')
|
|
59
|
+
PY
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_pre_push_report_on_exit() {
|
|
63
|
+
local saved_exit=$?
|
|
64
|
+
if [ "$_PRE_PUSH_REPORT_DONE" = "1" ]; then
|
|
65
|
+
exit "$saved_exit"
|
|
66
|
+
fi
|
|
67
|
+
_PRE_PUSH_REPORT_DONE=1
|
|
68
|
+
write_pre_push_report "$saved_exit"
|
|
69
|
+
exit "$saved_exit"
|
|
70
|
+
}
|
|
71
|
+
trap '_pre_push_report_on_exit' EXIT
|
|
18
72
|
|
|
19
73
|
# Get files being pushed in this push operation
|
|
20
74
|
# pre-push receives stdin with ref information
|
|
@@ -26,9 +80,26 @@ while read local_ref local_sha remote_ref remote_sha; do
|
|
|
26
80
|
echo " ℹ️ Branch deletion detected ($local_ref). Skipping validation."
|
|
27
81
|
continue
|
|
28
82
|
elif [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
29
|
-
# New branch - compare
|
|
30
|
-
|
|
31
|
-
|
|
83
|
+
# New branch - compare cumulative diff vs base branch
|
|
84
|
+
# Use git merge-base HEAD main/master to find the branch point,
|
|
85
|
+
# then diff the full branch range. Falls back to HEAD-only diff if no base found.
|
|
86
|
+
BASE_BRANCH="origin/main"
|
|
87
|
+
if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
88
|
+
BASE_BRANCH="origin/master"
|
|
89
|
+
fi
|
|
90
|
+
if git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
|
|
91
|
+
MERGE_BASE=$(git merge-base HEAD "$BASE_BRANCH" 2>/dev/null)
|
|
92
|
+
if [ -n "$MERGE_BASE" ] && [ "$MERGE_BASE" != "$(git rev-parse HEAD)" ]; then
|
|
93
|
+
PUSHED_FILES=$(git diff --name-only "$MERGE_BASE"...HEAD 2>/dev/null)
|
|
94
|
+
TS_FILES=$(git diff --name-only "$MERGE_BASE"...HEAD 2>/dev/null | grep '\.ts$' || true)
|
|
95
|
+
else
|
|
96
|
+
PUSHED_FILES=$(git diff-tree -r --name-only HEAD)
|
|
97
|
+
TS_FILES=$(git diff-tree -r --name-only HEAD | grep '\.ts$' || true)
|
|
98
|
+
fi
|
|
99
|
+
else
|
|
100
|
+
PUSHED_FILES=$(git diff-tree -r --name-only HEAD)
|
|
101
|
+
TS_FILES=$(git diff-tree -r --name-only HEAD | grep '\.ts$' || true)
|
|
102
|
+
fi
|
|
32
103
|
else
|
|
33
104
|
# Existing branch - compare against what we're pushing from
|
|
34
105
|
PUSHED_FILES=$(git diff-tree -r --name-only "$remote_sha" "$local_sha")
|
|
@@ -72,6 +143,94 @@ echo "Files changed: $FILES_CHANGED"
|
|
|
72
143
|
echo "Lines: +$LINES_ADDED -$LINES_DELETED"
|
|
73
144
|
echo ""
|
|
74
145
|
|
|
146
|
+
# ============================================================================
|
|
147
|
+
# GATE S: SPRINT FLOW ENFORCEMENT
|
|
148
|
+
# Validates sprint state consistency before push
|
|
149
|
+
# Uses sprint-gate.sh for standalone validation logic
|
|
150
|
+
# ============================================================================
|
|
151
|
+
GATE_S_STATUS="PASS"
|
|
152
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
|
153
|
+
SPRINT_GATE_SCRIPT=""
|
|
154
|
+
|
|
155
|
+
if [ -f "$PROJECT_ROOT/githooks/sprint-gate.sh" ]; then
|
|
156
|
+
SPRINT_GATE_SCRIPT="$PROJECT_ROOT/githooks/sprint-gate.sh"
|
|
157
|
+
elif [ -f "$(dirname "$0")/sprint-gate.sh" ]; then
|
|
158
|
+
SPRINT_GATE_SCRIPT="$(dirname "$0")/sprint-gate.sh"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
162
|
+
if ! bash "$SPRINT_GATE_SCRIPT" --pre-push; then
|
|
163
|
+
GATE_S_STATUS="BLOCK"
|
|
164
|
+
echo "❌ BLOCKED - Gate S: Sprint Flow Enforcement"
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
else
|
|
168
|
+
echo "⏭️ SKIPPED - Gate S: Sprint Flow (sprint-gate.sh not found)"
|
|
169
|
+
GATE_S_STATUS="SKIP"
|
|
170
|
+
fi
|
|
171
|
+
echo ""
|
|
172
|
+
|
|
173
|
+
# ============================================================================
|
|
174
|
+
# GATE 10: BUILD INTEGRITY CHECK (TypeScript)
|
|
175
|
+
# ============================================================================
|
|
176
|
+
GATE_10_STATUS="SKIP"
|
|
177
|
+
|
|
178
|
+
if [[ -f "package.json" ]] && [[ -f "tsconfig.json" ]] && [[ -n "$TS_FILES" ]]; then
|
|
179
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
180
|
+
echo " GATE 10: BUILD INTEGRITY CHECK"
|
|
181
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
182
|
+
|
|
183
|
+
GATE_10_SCRIPT=""
|
|
184
|
+
if [[ -f "$PROJECT_ROOT/src/build-integrity/gate-10.ts" ]]; then
|
|
185
|
+
GATE_10_SCRIPT="$PROJECT_ROOT/src/build-integrity/gate-10.ts"
|
|
186
|
+
elif [[ -f ".xp-gate/modules/build-integrity/gate-10.ts" ]]; then
|
|
187
|
+
GATE_10_SCRIPT=".xp-gate/modules/build-integrity/gate-10.ts"
|
|
188
|
+
elif [[ -f "$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts" ]]; then
|
|
189
|
+
GATE_10_SCRIPT="$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [[ -n "$GATE_10_SCRIPT" ]]; then
|
|
193
|
+
# Build comma-separated list of absolute file paths for changed TS files
|
|
194
|
+
GATE_10_FILE_LIST=""
|
|
195
|
+
while IFS= read -r f; do
|
|
196
|
+
if [ -z "$GATE_10_FILE_LIST" ]; then
|
|
197
|
+
GATE_10_FILE_LIST="$f"
|
|
198
|
+
else
|
|
199
|
+
GATE_10_FILE_LIST="$GATE_10_FILE_LIST,$f"
|
|
200
|
+
fi
|
|
201
|
+
done <<< "$TS_FILES"
|
|
202
|
+
|
|
203
|
+
GATE_10_OUTPUT=$(mktemp)
|
|
204
|
+
if timeout 120s npx tsx "$GATE_10_SCRIPT" --changed-files "$GATE_10_FILE_LIST" --project-root "$PROJECT_ROOT" > "$GATE_10_OUTPUT" 2>&1; then
|
|
205
|
+
cat "$GATE_10_OUTPUT"
|
|
206
|
+
GATE_10_STATUS="PASS"
|
|
207
|
+
echo "✅ Gate 10: PASS"
|
|
208
|
+
else
|
|
209
|
+
GATE_10_EXIT=$?
|
|
210
|
+
cat "$GATE_10_OUTPUT"
|
|
211
|
+
if [[ $GATE_10_EXIT -eq 124 ]]; then
|
|
212
|
+
echo "⏱️ Gate 10: TIMEOUT (120s). Allowing push with warning."
|
|
213
|
+
GATE_10_STATUS="TIMEOUT"
|
|
214
|
+
else
|
|
215
|
+
echo ""
|
|
216
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
217
|
+
echo " ❌ GATE 10 FAILED — PUSH BLOCKED"
|
|
218
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
219
|
+
echo "Build integrity check failed. Fix type errors, broken imports,"
|
|
220
|
+
echo "or package manifest issues before pushing."
|
|
221
|
+
rm -f "$GATE_10_OUTPUT"
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
rm -f "$GATE_10_OUTPUT"
|
|
226
|
+
else
|
|
227
|
+
echo "⚠️ Gate 10 script not found. SKIP — Gate 10."
|
|
228
|
+
fi
|
|
229
|
+
else
|
|
230
|
+
echo "📚 Not a TypeScript project or no TS files changed. SKIP — Gate 10."
|
|
231
|
+
fi
|
|
232
|
+
echo ""
|
|
233
|
+
|
|
75
234
|
# ============================================================================
|
|
76
235
|
# GATE M: MUTATION TESTING
|
|
77
236
|
# ============================================================================
|
|
@@ -115,13 +274,22 @@ else
|
|
|
115
274
|
echo "🧬 Running mutation tests on changed files..."
|
|
116
275
|
echo "$CHANGED_SOURCE_FILES"
|
|
117
276
|
|
|
118
|
-
# Check
|
|
119
|
-
|
|
120
|
-
|
|
277
|
+
# Check for mutation gate script in installed modules first, then project src/
|
|
278
|
+
MUTATION_SCRIPT=""
|
|
279
|
+
if [ -f ".xp-gate/modules/mutation/gate-m.ts" ]; then
|
|
280
|
+
MUTATION_SCRIPT=".xp-gate/modules/mutation/gate-m.ts"
|
|
281
|
+
elif [ -f "src/mutation/gate-m.ts" ]; then
|
|
282
|
+
MUTATION_SCRIPT="src/mutation/gate-m.ts"
|
|
283
|
+
elif [ -f "$HOME/.config/xp-gate/modules/mutation/gate-m.ts" ]; then
|
|
284
|
+
MUTATION_SCRIPT="$HOME/.config/xp-gate/modules/mutation/gate-m.ts"
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
if [[ -z "$MUTATION_SCRIPT" ]]; then
|
|
288
|
+
echo "⚠️ Gate M script not found. SKIP — Gate M."
|
|
121
289
|
else
|
|
122
290
|
# Run mutation gate with timeout matching stryker.conf.json (timeoutMS: 600000 = 10min)
|
|
123
291
|
MUTATION_OUTPUT=$(mktemp)
|
|
124
|
-
timeout 600s npx tsx
|
|
292
|
+
timeout 600s npx tsx $MUTATION_SCRIPT --changed-files "$CHANGED_SOURCE_FILES" > "$MUTATION_OUTPUT" 2>&1
|
|
125
293
|
MUTATION_EXIT=$?
|
|
126
294
|
|
|
127
295
|
case $MUTATION_EXIT in
|
|
@@ -162,7 +330,69 @@ else
|
|
|
162
330
|
fi
|
|
163
331
|
|
|
164
332
|
# ============================================================================
|
|
165
|
-
# GATE
|
|
333
|
+
# GATE M (Python): Incremental Mutation Testing via mutmut
|
|
334
|
+
# Runs after TypeScript Gate M; uses the same gate-m.ts orchestrator with
|
|
335
|
+
# MutmutRunner for Python files.
|
|
336
|
+
# ============================================================================
|
|
337
|
+
echo ""
|
|
338
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
339
|
+
echo " GATE M (Python): MUTATION TESTING"
|
|
340
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
341
|
+
|
|
342
|
+
# Collect changed Python source files (exclude tests, venv, cached)
|
|
343
|
+
PY_FILES=$(echo "$PUSHED_FILES" | grep -E '\.py$' | grep -v '__pycache__' | grep -v '\.test\.py' | grep -v '/test_' | grep -v '/tests/' || true)
|
|
344
|
+
PY_SOURCE_FILES=$(echo "$PY_FILES" | grep -v '/venv/' | grep -v '/\.venv/' || true)
|
|
345
|
+
|
|
346
|
+
if [ -z "$PY_SOURCE_FILES" ]; then
|
|
347
|
+
echo "📚 No changed Python source files. SKIP — Gate M (Python)."
|
|
348
|
+
else
|
|
349
|
+
PYTHON_FILE_LIST=$(echo "$PY_SOURCE_FILES" | tr '\n' ',' | sed 's/,$//')
|
|
350
|
+
echo "🐍 Changed Python files: $PYTHON_FILE_LIST"
|
|
351
|
+
|
|
352
|
+
if detect_python_mutation_testable 2>/dev/null; then
|
|
353
|
+
if [ -f "src/mutation/gate-m.ts" ]; then
|
|
354
|
+
MUTATION_OUTPUT=$(mktemp)
|
|
355
|
+
timeout 120s npx tsx src/mutation/gate-m.ts --changed-files "$PYTHON_FILE_LIST" > "$MUTATION_OUTPUT" 2>&1
|
|
356
|
+
MUTATION_EXIT=$?
|
|
357
|
+
|
|
358
|
+
case $MUTATION_EXIT in
|
|
359
|
+
0)
|
|
360
|
+
echo "✅ Gate M (Python): PASS"
|
|
361
|
+
;;
|
|
362
|
+
1)
|
|
363
|
+
echo ""
|
|
364
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
365
|
+
echo " ❌ GATE M (Python) FAILED - PUSH BLOCKED"
|
|
366
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
367
|
+
echo ""
|
|
368
|
+
cat "$MUTATION_OUTPUT"
|
|
369
|
+
echo ""
|
|
370
|
+
rm -f "$MUTATION_OUTPUT"
|
|
371
|
+
exit 1
|
|
372
|
+
;;
|
|
373
|
+
124)
|
|
374
|
+
cat "$MUTATION_OUTPUT"
|
|
375
|
+
echo "⏱ Gate M (Python): TIMEOUT (120s). Allowing push with warning."
|
|
376
|
+
;;
|
|
377
|
+
*)
|
|
378
|
+
cat "$MUTATION_OUTPUT"
|
|
379
|
+
echo "⚠ Gate M (Python): Unexpected exit code $MUTATION_EXIT. Allowing push with warning."
|
|
380
|
+
;;
|
|
381
|
+
esac
|
|
382
|
+
|
|
383
|
+
rm -f "$MUTATION_OUTPUT"
|
|
384
|
+
else
|
|
385
|
+
echo "⚠ Gate M script not found. SKIP — Gate M (Python)."
|
|
386
|
+
fi
|
|
387
|
+
else
|
|
388
|
+
echo "⚠ mutmut not installed. SKIP — Gate M (Python)."
|
|
389
|
+
fi
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# ============================================================================
|
|
393
|
+
# GATE M2: MOCK DENSITY CHECK (BLOCK at 30%, configurable via .mockpolicyrc)
|
|
394
|
+
# Phase 1: WARNING mode (collect false positive data before enabling BLOCK)
|
|
395
|
+
# Per-layer thresholds: .mockpolicyrc layers.*.maxMockDensity
|
|
166
396
|
# ============================================================================
|
|
167
397
|
echo ""
|
|
168
398
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -182,10 +412,20 @@ if [ -n "$PY_TEST_FILES" ]; then
|
|
|
182
412
|
fi
|
|
183
413
|
|
|
184
414
|
if [ -z "$ALL_PUSHED_TEST_FILES" ]; then
|
|
185
|
-
echo "
|
|
415
|
+
echo "⏭️ SKIPPED - Mock density check (no test files in push)"
|
|
186
416
|
else
|
|
187
417
|
MOCK_BLOCKED=false
|
|
188
418
|
|
|
419
|
+
# Read .mockpolicyrc configuration
|
|
420
|
+
MOCK_THRESHOLD=30 # Default threshold
|
|
421
|
+
if [ -f ".mockpolicyrc" ]; then
|
|
422
|
+
# Read global mock-threshold if present
|
|
423
|
+
CUSTOM_THRESHOLD=$(grep -oE '"mock-threshold"\s*:\s*[0-9]+' .mockpolicyrc | grep -oE '[0-9]+$' || true)
|
|
424
|
+
if [ -n "$CUSTOM_THRESHOLD" ]; then
|
|
425
|
+
MOCK_THRESHOLD=$CUSTOM_THRESHOLD
|
|
426
|
+
fi
|
|
427
|
+
fi
|
|
428
|
+
|
|
189
429
|
for test_file in $ALL_PUSHED_TEST_FILES; do
|
|
190
430
|
if [ -f "$test_file" ]; then
|
|
191
431
|
# Count mock keyword references (precise patterns only)
|
|
@@ -196,9 +436,8 @@ else
|
|
|
196
436
|
for kw in 'jest\.mock' 'vi\.mock' 'jest\.spyOn' 'vi\.spyOn' 'jest\.fn' 'vi\.fn' \
|
|
197
437
|
'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
|
|
198
438
|
'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
|
|
199
|
-
'\.patch(' 'gomock' 'mockgen' '
|
|
200
|
-
c=$(grep -o "$kw" "$test_file" 2>/dev/null | wc -l ||
|
|
201
|
-
# Guard against empty/non-numeric values that break bash arithmetic
|
|
439
|
+
'\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
|
|
440
|
+
c=$(grep -o "$kw" "$test_file" 2>/dev/null | wc -l || true)
|
|
202
441
|
c=${c//[^0-9]/}
|
|
203
442
|
c=${c:-0}
|
|
204
443
|
MOCK_COUNT=$((MOCK_COUNT + c))
|
|
@@ -216,32 +455,185 @@ else
|
|
|
216
455
|
MOCK_DENSITY="0"
|
|
217
456
|
fi
|
|
218
457
|
|
|
219
|
-
|
|
220
|
-
THRESHOLD_30=$(awk "BEGIN {print ($MOCK_DENSITY > 30) ? 1 : 0}")
|
|
458
|
+
THRESHOLD_BLOCK=$(awk "BEGIN {print ($MOCK_DENSITY > $MOCK_THRESHOLD) ? 1 : 0}")
|
|
221
459
|
|
|
222
460
|
# Check for @mock-justified annotation with reason text (min 10 chars)
|
|
223
461
|
HAS_JUSTIFIED=$(grep -qE '@mock-justified\s*:\s*.{10,}' "$test_file" 2>/dev/null && echo "true" || echo "false")
|
|
224
462
|
|
|
225
|
-
if [ "$
|
|
463
|
+
if [ "$THRESHOLD_BLOCK" = "1" ]; then
|
|
226
464
|
if [ "$HAS_JUSTIFIED" = "false" ]; then
|
|
227
|
-
|
|
465
|
+
# Phase 1: WARNING mode (will become BLOCK in Phase 2)
|
|
466
|
+
echo "⚠️ WARNING: $test_file — Mock density ${MOCK_DENSITY}% exceeds ${MOCK_THRESHOLD}% threshold"
|
|
228
467
|
echo " Must: Reduce mocks OR add '// @mock-justified: <reason>' (min 10 char explanation)"
|
|
229
|
-
|
|
468
|
+
echo " (Phase 1: WARNING mode — will become BLOCK after baseline analysis)"
|
|
469
|
+
# Note: Do NOT set MOCK_BLOCKED=true in Phase 1
|
|
230
470
|
else
|
|
231
|
-
echo "
|
|
471
|
+
echo "✅ $test_file — Mock density ${MOCK_DENSITY}% (justified by annotation)"
|
|
232
472
|
fi
|
|
233
|
-
elif [ "$THRESHOLD_30" = "1" ]; then
|
|
234
|
-
echo "ℹ️ ADVISORY: $test_file — Mock density ${MOCK_DENSITY}% (consider integration tests)"
|
|
235
473
|
else
|
|
236
|
-
echo "✅ $test_file — Mock density ${MOCK_DENSITY}% (
|
|
474
|
+
echo "✅ $test_file — Mock density ${MOCK_DENSITY}% (within ${MOCK_THRESHOLD}% threshold)"
|
|
237
475
|
fi
|
|
238
476
|
fi
|
|
239
477
|
done
|
|
240
478
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
479
|
+
# Phase 1: WARNING mode only — no blocking
|
|
480
|
+
# Phase 2 (after baseline analysis): enable BLOCK based on false positive rate
|
|
481
|
+
# if [ "$MOCK_BLOCKED" = true ]; then
|
|
482
|
+
# echo ""
|
|
483
|
+
# echo "❌ PUSH BLOCKED — Mock density too high without justification"
|
|
484
|
+
# exit 1
|
|
485
|
+
# fi
|
|
486
|
+
fi
|
|
487
|
+
|
|
488
|
+
# ============================================================================
|
|
489
|
+
# Gate M3: Mock Layering Strategy
|
|
490
|
+
# Validates mock policies based on test layer and dependency scope
|
|
491
|
+
# Runs only in non-main branches, TypeScript projects with the module available
|
|
492
|
+
# ============================================================================
|
|
493
|
+
run_gate_m3() {
|
|
494
|
+
local changed_files="$1"
|
|
495
|
+
local project_root="$2"
|
|
496
|
+
|
|
497
|
+
echo ""
|
|
498
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
499
|
+
echo " GATE M3: MOCK LAYERING STRATEGY"
|
|
500
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
501
|
+
|
|
502
|
+
# Skip on main/master
|
|
503
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
504
|
+
echo " SKIP — pushing to main/master"
|
|
505
|
+
return 0
|
|
506
|
+
fi
|
|
507
|
+
|
|
508
|
+
# Check for mock-policy module in installed modules first, then project src/
|
|
509
|
+
local GATE_M3_SCRIPT=""
|
|
510
|
+
if [ -f ".xp-gate/modules/mock-policy/gate-m3.ts" ]; then
|
|
511
|
+
GATE_M3_SCRIPT=".xp-gate/modules/mock-policy/gate-m3.ts"
|
|
512
|
+
elif [ -f "./src/mock-policy/gate-m3.ts" ]; then
|
|
513
|
+
GATE_M3_SCRIPT="./src/mock-policy/gate-m3.ts"
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
if [[ -z "$GATE_M3_SCRIPT" ]]; then
|
|
517
|
+
echo " SKIP — module not found"
|
|
518
|
+
return 0
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
if [[ ! -f "package.json" ]] || [[ ! -f "tsconfig.json" ]]; then
|
|
522
|
+
echo " SKIP — TypeScript only"
|
|
523
|
+
return 0
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
# Filter to test files only
|
|
527
|
+
local test_files=$(echo "$changed_files" | grep -E '\.(test|spec)\.ts$' || true)
|
|
528
|
+
|
|
529
|
+
if [[ -z "$test_files" ]]; then
|
|
530
|
+
echo " SKIP — no test files changed"
|
|
531
|
+
return 0
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
echo " Changed test files: $(echo "$test_files" | wc -l)"
|
|
535
|
+
|
|
536
|
+
# Convert to array safely (handles spaces in filenames)
|
|
537
|
+
local args=()
|
|
538
|
+
while IFS= read -r f; do
|
|
539
|
+
[[ -n "$f" ]] && args+=("$f")
|
|
540
|
+
done <<< "$test_files"
|
|
541
|
+
|
|
542
|
+
if npx tsx "$GATE_M3_SCRIPT" "${args[@]}"; then
|
|
543
|
+
echo "✅ Gate M3: PASS"
|
|
544
|
+
return 0
|
|
545
|
+
else
|
|
546
|
+
local exit_code=$?
|
|
547
|
+
if [[ "$exit_code" -eq 1 ]]; then
|
|
548
|
+
echo ""
|
|
549
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
550
|
+
echo " ❌ GATE M3 FAILED - PUSH BLOCKED"
|
|
551
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
552
|
+
echo ""
|
|
553
|
+
echo "Mock layering violations found. Review and fix or adjust"
|
|
554
|
+
echo ".mockpolicyrc severity to 'warning' for advisory mode."
|
|
555
|
+
echo ""
|
|
556
|
+
return 1
|
|
557
|
+
fi
|
|
558
|
+
echo "⚠️ Gate M3: Unexpected error — allowing push with warning"
|
|
559
|
+
return 0
|
|
560
|
+
fi
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
run_gate_m3 "$TS_FILES" "$PROJECT_ROOT"
|
|
564
|
+
|
|
565
|
+
# ============================================================================
|
|
566
|
+
# GATE UI: UI SPRINT DETECTION & RESULT VALIDATION
|
|
567
|
+
# ============================================================================
|
|
568
|
+
echo ""
|
|
569
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
570
|
+
echo " GATE UI: UI SPRINT QUALITY GATES"
|
|
571
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
572
|
+
|
|
573
|
+
# Skip on main/master (pre-reviewed via PR)
|
|
574
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
575
|
+
echo "⚠️ Pushing to main/master — UI Gate skipped (pre-reviewed via PR)"
|
|
576
|
+
else
|
|
577
|
+
# Check bypass
|
|
578
|
+
if [[ "$XP_GATE_SKIP_UI_GATES" == "1" ]]; then
|
|
579
|
+
if [[ -z "$XP_GATE_BYPASS_REASON" || ${#XP_GATE_BYPASS_REASON// /} -lt 10 ]]; then
|
|
580
|
+
echo "❌ XP_GATE_BYPASS_REASON required (min 10 non-whitespace chars)"
|
|
581
|
+
exit 1
|
|
582
|
+
fi
|
|
583
|
+
echo "⚠️ UI Gate BYPASSED — reason: $XP_GATE_BYPASS_REASON"
|
|
584
|
+
AUDIT_ENTRY="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"branch\":\"$CURRENT_BRANCH\",\"commit\":\"$(git rev-parse HEAD)\",\"user\":\"$(git config user.name || echo 'unknown')\",\"reason\":\"$XP_GATE_BYPASS_REASON\",\"bypass_type\":\"ui-gates\"}"
|
|
585
|
+
echo "$AUDIT_ENTRY" >> .audit-log.jsonl
|
|
586
|
+
else
|
|
587
|
+
# Detect UI changes in pushed files
|
|
588
|
+
UI_DETECTION=$(echo "$PUSHED_FILES" | npx -y tsx src/npm-package/lib/ui-detector.ts --push-mode --from-stdin 2>/dev/null) || true
|
|
589
|
+
|
|
590
|
+
IS_UI_SPRINT=$(echo "$UI_DETECTION" | jq -r '.isUiSprint' 2>/dev/null || echo "false")
|
|
591
|
+
|
|
592
|
+
if [[ "$IS_UI_SPRINT" == "true" ]]; then
|
|
593
|
+
UI_RESULT_FILE=".ui-gate-result.json"
|
|
594
|
+
MISSING_RESULTS=()
|
|
595
|
+
|
|
596
|
+
if [[ ! -f "$UI_RESULT_FILE" ]]; then
|
|
597
|
+
MISSING_RESULTS+=("$UI_RESULT_FILE")
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
if [[ ${#MISSING_RESULTS[@]} -gt 0 ]]; then
|
|
601
|
+
echo ""
|
|
602
|
+
echo "❌ UI sprint detected — missing result file(s):"
|
|
603
|
+
for f in "${MISSING_RESULTS[@]}"; do echo " - $f"; done
|
|
604
|
+
echo ""
|
|
605
|
+
echo "Before pushing, run: /design-review + /qa-only in your Agent session"
|
|
606
|
+
echo "Or for non-sprint flows: xp-gate ui-review"
|
|
607
|
+
echo ""
|
|
608
|
+
echo "Sprint Flow: Phase 3 automatically triggers these when UI changes detected."
|
|
609
|
+
exit 1
|
|
610
|
+
fi
|
|
611
|
+
|
|
612
|
+
# Validate result file
|
|
613
|
+
RESULT_COMMIT=$(jq -r '.commit' "$UI_RESULT_FILE" 2>/dev/null)
|
|
614
|
+
RESULT_VERDICT=$(jq -r '.verdict' "$UI_RESULT_FILE" 2>/dev/null)
|
|
615
|
+
RESULT_EXPIRES=$(jq -r '.expires' "$UI_RESULT_FILE" 2>/dev/null)
|
|
616
|
+
|
|
617
|
+
if [[ "$RESULT_COMMIT" != "$(git rev-parse HEAD)" ]]; then
|
|
618
|
+
echo "❌ .ui-gate-result.json outdated — expected commit $(git rev-parse HEAD), got $RESULT_COMMIT"
|
|
619
|
+
exit 1
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
if [[ "$RESULT_VERDICT" != "APPROVED" ]]; then
|
|
623
|
+
echo "❌ .ui-gate-result.json verdict: $RESULT_VERDICT (must be APPROVED)"
|
|
624
|
+
exit 1
|
|
625
|
+
fi
|
|
626
|
+
|
|
627
|
+
CURRENT_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
628
|
+
if [[ "$CURRENT_TIME" > "$RESULT_EXPIRES" ]]; then
|
|
629
|
+
echo "❌ .ui-gate-result.json expired ($RESULT_EXPIRES)"
|
|
630
|
+
exit 1
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
echo "✅ Gate UI: PASS — UI changes reviewed and approved"
|
|
634
|
+
else
|
|
635
|
+
echo "ℹ️ No UI changes detected — skipping UI Gate"
|
|
636
|
+
fi
|
|
245
637
|
fi
|
|
246
638
|
fi
|
|
247
639
|
|
|
@@ -292,7 +684,16 @@ if ! command -v jq &> /dev/null; then
|
|
|
292
684
|
fi
|
|
293
685
|
|
|
294
686
|
# Validate result file content
|
|
687
|
+
# SHA self-reference paradox fix: if the result file was committed as part of
|
|
688
|
+
# HEAD, its content references HEAD^ (the commit being reviewed), not HEAD.
|
|
295
689
|
CURRENT_COMMIT=$(git rev-parse HEAD)
|
|
690
|
+
if git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -qx "$RESULT_FILE"; then
|
|
691
|
+
# Result file is part of the latest commit — compare against the reviewed commit
|
|
692
|
+
EXPECTED_COMMIT=$(git rev-parse HEAD^ 2>/dev/null || echo "unknown")
|
|
693
|
+
else
|
|
694
|
+
# Result file is working-tree only — compare against current HEAD
|
|
695
|
+
EXPECTED_COMMIT="$CURRENT_COMMIT"
|
|
696
|
+
fi
|
|
296
697
|
|
|
297
698
|
# Check JSON validity
|
|
298
699
|
if ! jq empty "$RESULT_FILE" 2>/dev/null; then
|
|
@@ -325,15 +726,15 @@ if [ "$RESULT_BRANCH" != "$CURRENT_BRANCH" ]; then
|
|
|
325
726
|
fi
|
|
326
727
|
|
|
327
728
|
# Commit match check
|
|
328
|
-
if [ "$RESULT_COMMIT" != "$
|
|
729
|
+
if [ "$RESULT_COMMIT" != "$EXPECTED_COMMIT" ]; then
|
|
329
730
|
echo ""
|
|
330
731
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
331
732
|
echo " ❌ RESULT FILE OUTDATED - PUSH BLOCKED"
|
|
332
733
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
333
734
|
echo ""
|
|
334
735
|
echo "Result file is for a different commit:"
|
|
335
|
-
|
|
336
|
-
|
|
736
|
+
echo " Expected: $EXPECTED_COMMIT"
|
|
737
|
+
echo " Found: $RESULT_COMMIT"
|
|
337
738
|
echo ""
|
|
338
739
|
echo "Re-run: /delphi-review --mode code-walkthrough"
|
|
339
740
|
echo ""
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-29
|
|
4
|
+
**Commit:** 8efd33c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.11.0.1
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-29
|
|
4
|
+
**Commit:** 8efd33c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.11.0.1
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-29
|
|
4
|
+
**Commit:** 8efd33c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.11.0.1
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|