@boyingliu01/xp-gate 0.30.3 → 0.30.5
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/gate-10.sh +80 -13
- package/hooks/gate-10.sh +80 -13
- package/hooks/pre-push +53 -3
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gate-m-survivors.test.ts +167 -0
- package/mutation/__tests__/mutation-mirror.test.ts +49 -0
- package/mutation/__tests__/update-baseline.test.ts +7 -3
- package/mutation/gate-m.ts +164 -20
- package/mutation/types.ts +9 -3
- package/mutation/update-baseline.ts +24 -7
- 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/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/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/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/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/gate-10.sh
CHANGED
|
@@ -91,11 +91,27 @@ else
|
|
|
91
91
|
[ -n "$_semgrep_file" ] || continue
|
|
92
92
|
SEMGREP_FILE_COUNT=$((SEMGREP_FILE_COUNT + 1))
|
|
93
93
|
SEMGREP_ARGS+=("--include=${_semgrep_file##*/}")
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
|
|
94
|
+
# A repo-root file has no slash at all, so "%/*" returns it unchanged and
|
|
95
|
+
# the old comment's assumption (`"./x.ts" strips to "./"`) never held:
|
|
96
|
+
# the target became the FILE path dressed as a directory ("./x.ts").
|
|
97
|
+
# Strip only when a slash is present; otherwise the target is the repo
|
|
98
|
+
# root, spelled ".". Scanning "." widens the scan to the whole tree, which
|
|
99
|
+
# is the safe side (see the note above) and is what a root-level staged
|
|
100
|
+
# file requires in order to be analysed at all.
|
|
101
|
+
case "$_semgrep_file" in
|
|
102
|
+
*/*) _semgrep_dir="./${_semgrep_file%/*}" ;;
|
|
103
|
+
*) _semgrep_dir="." ;;
|
|
104
|
+
esac
|
|
105
|
+
# #558: dedup must match WHOLE lines. The previous `case "$SEEN" in
|
|
106
|
+
# *"$_semgrep_dir"*)` was an unanchored substring test: once
|
|
107
|
+
# ./fastdl/tests was recorded, the parent ./fastdl matched it and was
|
|
108
|
+
# never added as a target, so a staged fastdl/a.py contributed an
|
|
109
|
+
# --include= with no directory to scan it in. Nothing errored and nothing
|
|
110
|
+
# was reported -- the file was simply not analysed, which for a security
|
|
111
|
+
# gate is the worst outcome: a silent net loss that looks like a pass.
|
|
112
|
+
# Sibling prefixes (src/api vs src/app) collapsed the same way.
|
|
97
113
|
case "$SEMGREP_DIRS_SEEN" in
|
|
98
|
-
|
|
114
|
+
*$'\n'"$_semgrep_dir"$'\n'*) : ;;
|
|
99
115
|
*) SEMGREP_ARGS+=("$_semgrep_dir"); SEMGREP_DIRS_SEEN="$SEMGREP_DIRS_SEEN$_semgrep_dir"$'\n' ;;
|
|
100
116
|
esac
|
|
101
117
|
done <<EOF
|
|
@@ -131,9 +147,41 @@ EOF
|
|
|
131
147
|
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
132
148
|
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
133
149
|
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
# #559: bound the run. Without a timeout, a wedged semgrep blocks the
|
|
151
|
+
# invocation for as long as its process lives; the fail-closed verdict logic
|
|
152
|
+
# below is correct but unreachable, so the hook hangs instead of judging.
|
|
153
|
+
# A gate that can wedge an interactive `git commit` gets bypassed with
|
|
154
|
+
# --no-verify, which is worse than any verdict it could have produced.
|
|
155
|
+
# `-k` after the primary signal: pysemgrep spinning in a syscall ignores
|
|
156
|
+
# SIGTERM, and the shipped 0.29 code used the same `-k 10` shape.
|
|
157
|
+
SEMGREP_TIMEOUT_S="${XP_GATE_SEMGREP_TIMEOUT:-180}"
|
|
158
|
+
SEMGREP_TIMED_OUT=false
|
|
159
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
160
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
161
|
+
SEMGREP_OUTPUT=$(timeout -k 10 "$SEMGREP_TIMEOUT_S" \
|
|
162
|
+
"$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
163
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
164
|
+
SEMGREP_EXIT=$?
|
|
165
|
+
# 124 = timeout's own "I gave up" code. 137 = 128+SIGKILL, which is what
|
|
166
|
+
# `timeout -k` returns when the child ignored SIGTERM and had to be
|
|
167
|
+
# killed -- measured with a semgrep stand-in that traps TERM. Both mean
|
|
168
|
+
# "bounded, no verdict", so both must be reported as a timeout; checking
|
|
169
|
+
# only 124 told the user their semgrep was unparseable when in fact the
|
|
170
|
+
# scan had been cut off.
|
|
171
|
+
case "$SEMGREP_EXIT" in
|
|
172
|
+
124|137) SEMGREP_TIMED_OUT=true ;;
|
|
173
|
+
esac
|
|
174
|
+
else
|
|
175
|
+
# No `timeout` on this platform (stock macOS). Say so: an unbounded run
|
|
176
|
+
# nobody mentioned is exactly how #559 stayed invisible for a whole
|
|
177
|
+
# release. The scan still runs -- refusing to scan would be a worse
|
|
178
|
+
# trade -- but the unbounded window is now stated.
|
|
179
|
+
echo " ⚠️ 'timeout' command not available — semgrep runs UNBOUNDED (install coreutils or set up gtimeout)"
|
|
180
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
181
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
182
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
183
|
+
SEMGREP_EXIT=$?
|
|
184
|
+
fi
|
|
137
185
|
|
|
138
186
|
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
139
187
|
# finding details. The severity vocabulary is the UNION of the two scales
|
|
@@ -175,7 +223,18 @@ console.log("");});
|
|
|
175
223
|
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
176
224
|
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
177
225
|
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
178
|
-
|
|
226
|
+
#
|
|
227
|
+
# #559: name a timeout as a timeout. The generic "unparseable" message
|
|
228
|
+
# would be true but useless here -- it points at the JSON parser when the
|
|
229
|
+
# real problem is that the scan was cut off. Same verdict (SKIP), but a
|
|
230
|
+
# diagnosis the reader can act on.
|
|
231
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
232
|
+
echo " ⏱️ semgrep exceeded ${SEMGREP_TIMEOUT_S}s and was killed — no verdict is possible"
|
|
233
|
+
echo " This is NOT a finding and NOT a clean scan. Raise XP_GATE_SEMGREP_TIMEOUT"
|
|
234
|
+
echo " if the repo legitimately takes longer; investigate if semgrep itself is wedged."
|
|
235
|
+
else
|
|
236
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
237
|
+
fi
|
|
179
238
|
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
180
239
|
echo " ── what semgrep reported on stderr ──"
|
|
181
240
|
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
@@ -184,11 +243,19 @@ console.log("");});
|
|
|
184
243
|
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
185
244
|
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
186
245
|
fi
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
246
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
247
|
+
echo " ⏭️ SKIPPED - SAST (semgrep timed out after ${SEMGREP_TIMEOUT_S}s)"
|
|
248
|
+
GATE_10_STATUS="SKIP"
|
|
249
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
250
|
+
# shellcheck disable=SC2034
|
|
251
|
+
GATE_10_REASON="semgrep_timeout"
|
|
252
|
+
else
|
|
253
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
254
|
+
GATE_10_STATUS="SKIP"
|
|
255
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
256
|
+
# shellcheck disable=SC2034
|
|
257
|
+
GATE_10_REASON="output_unparseable"
|
|
258
|
+
fi
|
|
192
259
|
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
193
260
|
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
194
261
|
# (nonzero exit) or completed but reported scan errors for every input
|
package/hooks/gate-10.sh
CHANGED
|
@@ -91,11 +91,27 @@ else
|
|
|
91
91
|
[ -n "$_semgrep_file" ] || continue
|
|
92
92
|
SEMGREP_FILE_COUNT=$((SEMGREP_FILE_COUNT + 1))
|
|
93
93
|
SEMGREP_ARGS+=("--include=${_semgrep_file##*/}")
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
|
|
94
|
+
# A repo-root file has no slash at all, so "%/*" returns it unchanged and
|
|
95
|
+
# the old comment's assumption (`"./x.ts" strips to "./"`) never held:
|
|
96
|
+
# the target became the FILE path dressed as a directory ("./x.ts").
|
|
97
|
+
# Strip only when a slash is present; otherwise the target is the repo
|
|
98
|
+
# root, spelled ".". Scanning "." widens the scan to the whole tree, which
|
|
99
|
+
# is the safe side (see the note above) and is what a root-level staged
|
|
100
|
+
# file requires in order to be analysed at all.
|
|
101
|
+
case "$_semgrep_file" in
|
|
102
|
+
*/*) _semgrep_dir="./${_semgrep_file%/*}" ;;
|
|
103
|
+
*) _semgrep_dir="." ;;
|
|
104
|
+
esac
|
|
105
|
+
# #558: dedup must match WHOLE lines. The previous `case "$SEEN" in
|
|
106
|
+
# *"$_semgrep_dir"*)` was an unanchored substring test: once
|
|
107
|
+
# ./fastdl/tests was recorded, the parent ./fastdl matched it and was
|
|
108
|
+
# never added as a target, so a staged fastdl/a.py contributed an
|
|
109
|
+
# --include= with no directory to scan it in. Nothing errored and nothing
|
|
110
|
+
# was reported -- the file was simply not analysed, which for a security
|
|
111
|
+
# gate is the worst outcome: a silent net loss that looks like a pass.
|
|
112
|
+
# Sibling prefixes (src/api vs src/app) collapsed the same way.
|
|
97
113
|
case "$SEMGREP_DIRS_SEEN" in
|
|
98
|
-
|
|
114
|
+
*$'\n'"$_semgrep_dir"$'\n'*) : ;;
|
|
99
115
|
*) SEMGREP_ARGS+=("$_semgrep_dir"); SEMGREP_DIRS_SEEN="$SEMGREP_DIRS_SEEN$_semgrep_dir"$'\n' ;;
|
|
100
116
|
esac
|
|
101
117
|
done <<EOF
|
|
@@ -131,9 +147,41 @@ EOF
|
|
|
131
147
|
# code is demoted to "did the tool itself run". --error stays OFF: it would
|
|
132
148
|
# add nothing the JSON does not already provide, and keeping it off leaves
|
|
133
149
|
# failure exit codes unambiguous for the #475 diagnostics path.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
# #559: bound the run. Without a timeout, a wedged semgrep blocks the
|
|
151
|
+
# invocation for as long as its process lives; the fail-closed verdict logic
|
|
152
|
+
# below is correct but unreachable, so the hook hangs instead of judging.
|
|
153
|
+
# A gate that can wedge an interactive `git commit` gets bypassed with
|
|
154
|
+
# --no-verify, which is worse than any verdict it could have produced.
|
|
155
|
+
# `-k` after the primary signal: pysemgrep spinning in a syscall ignores
|
|
156
|
+
# SIGTERM, and the shipped 0.29 code used the same `-k 10` shape.
|
|
157
|
+
SEMGREP_TIMEOUT_S="${XP_GATE_SEMGREP_TIMEOUT:-180}"
|
|
158
|
+
SEMGREP_TIMED_OUT=false
|
|
159
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
160
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
161
|
+
SEMGREP_OUTPUT=$(timeout -k 10 "$SEMGREP_TIMEOUT_S" \
|
|
162
|
+
"$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
163
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
164
|
+
SEMGREP_EXIT=$?
|
|
165
|
+
# 124 = timeout's own "I gave up" code. 137 = 128+SIGKILL, which is what
|
|
166
|
+
# `timeout -k` returns when the child ignored SIGTERM and had to be
|
|
167
|
+
# killed -- measured with a semgrep stand-in that traps TERM. Both mean
|
|
168
|
+
# "bounded, no verdict", so both must be reported as a timeout; checking
|
|
169
|
+
# only 124 told the user their semgrep was unparseable when in fact the
|
|
170
|
+
# scan had been cut off.
|
|
171
|
+
case "$SEMGREP_EXIT" in
|
|
172
|
+
124|137) SEMGREP_TIMED_OUT=true ;;
|
|
173
|
+
esac
|
|
174
|
+
else
|
|
175
|
+
# No `timeout` on this platform (stock macOS). Say so: an unbounded run
|
|
176
|
+
# nobody mentioned is exactly how #559 stayed invisible for a whole
|
|
177
|
+
# release. The scan still runs -- refusing to scan would be a worse
|
|
178
|
+
# trade -- but the unbounded window is now stated.
|
|
179
|
+
echo " ⚠️ 'timeout' command not available — semgrep runs UNBOUNDED (install coreutils or set up gtimeout)"
|
|
180
|
+
SEMGREP_STDOUT_FILE=$(mktemp)
|
|
181
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
|
|
182
|
+
"${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
|
|
183
|
+
SEMGREP_EXIT=$?
|
|
184
|
+
fi
|
|
137
185
|
|
|
138
186
|
# One node pass over the JSON: first line = verdict counts, remaining lines =
|
|
139
187
|
# finding details. The severity vocabulary is the UNION of the two scales
|
|
@@ -175,7 +223,18 @@ console.log("");});
|
|
|
175
223
|
if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
|
|
176
224
|
# Unparseable output: nothing the exit code claims can be trusted (#518).
|
|
177
225
|
# Fail closed — SKIP with diagnostics, never a PASS.
|
|
178
|
-
|
|
226
|
+
#
|
|
227
|
+
# #559: name a timeout as a timeout. The generic "unparseable" message
|
|
228
|
+
# would be true but useless here -- it points at the JSON parser when the
|
|
229
|
+
# real problem is that the scan was cut off. Same verdict (SKIP), but a
|
|
230
|
+
# diagnosis the reader can act on.
|
|
231
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
232
|
+
echo " ⏱️ semgrep exceeded ${SEMGREP_TIMEOUT_S}s and was killed — no verdict is possible"
|
|
233
|
+
echo " This is NOT a finding and NOT a clean scan. Raise XP_GATE_SEMGREP_TIMEOUT"
|
|
234
|
+
echo " if the repo legitimately takes longer; investigate if semgrep itself is wedged."
|
|
235
|
+
else
|
|
236
|
+
echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
|
|
237
|
+
fi
|
|
179
238
|
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
180
239
|
echo " ── what semgrep reported on stderr ──"
|
|
181
240
|
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
@@ -184,11 +243,19 @@ console.log("");});
|
|
|
184
243
|
echo " ── what semgrep wrote to stdout (tail) ──"
|
|
185
244
|
printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
|
|
186
245
|
fi
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
246
|
+
if [ "$SEMGREP_TIMED_OUT" = true ]; then
|
|
247
|
+
echo " ⏭️ SKIPPED - SAST (semgrep timed out after ${SEMGREP_TIMEOUT_S}s)"
|
|
248
|
+
GATE_10_STATUS="SKIP"
|
|
249
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
250
|
+
# shellcheck disable=SC2034
|
|
251
|
+
GATE_10_REASON="semgrep_timeout"
|
|
252
|
+
else
|
|
253
|
+
echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
|
|
254
|
+
GATE_10_STATUS="SKIP"
|
|
255
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
256
|
+
# shellcheck disable=SC2034
|
|
257
|
+
GATE_10_REASON="output_unparseable"
|
|
258
|
+
fi
|
|
192
259
|
elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
|
|
193
260
|
# Nothing was scanned to a verdict: semgrep either failed outright
|
|
194
261
|
# (nonzero exit) or completed but reported scan errors for every input
|
package/hooks/pre-push
CHANGED
|
@@ -47,11 +47,28 @@ write_pre_push_report() {
|
|
|
47
47
|
commit_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
48
48
|
verdict=$([ "$exit_code" -eq 0 ] && echo "PASS" || echo "BLOCKED")
|
|
49
49
|
mkdir -p "$reports_dir"
|
|
50
|
+
# #544: the survivor detail is a JSON document, so it cannot be interpolated
|
|
51
|
+
# into this heredoc — a single quote in a path would break the Python source.
|
|
52
|
+
# Hand it to the child through the environment and let Python parse it, then
|
|
53
|
+
# store it serialised back into the `details` string domain (every value there
|
|
54
|
+
# is a string; the journal has broken before over mixed types).
|
|
55
|
+
export XP_GATE_M_SURVIVORS_JSON="${M_SURVIVORS_JSON:-}"
|
|
50
56
|
python3 - <<PY 2>/dev/null || true
|
|
51
57
|
import json
|
|
58
|
+
import os
|
|
52
59
|
from pathlib import Path
|
|
53
60
|
pushed_files = '''${PUSHED_FILES:-}'''.splitlines()
|
|
54
61
|
passed = 1 if '$verdict' == 'PASS' else 0
|
|
62
|
+
# Absent stays absent: no key at all when nothing was measured, so a reader can
|
|
63
|
+
# tell "not measured" from "measured: zero survivors" (#544).
|
|
64
|
+
try:
|
|
65
|
+
_survivors_raw = os.environ.get('XP_GATE_M_SURVIVORS_JSON', '').strip()
|
|
66
|
+
survivors_detail = json.loads(_survivors_raw) if _survivors_raw else None
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
survivors_detail = None
|
|
69
|
+
_m_details = {'skip_reason': '${GATE_M_SKIP_REASON:-}', 'duration_ms': '${M_DURATION_MS:-0}', 'mutation_score': '${M_SCORE:-}'}
|
|
70
|
+
if survivors_detail is not None:
|
|
71
|
+
_m_details['survivors'] = json.dumps(survivors_detail, sort_keys=True)
|
|
55
72
|
report = {
|
|
56
73
|
'timestamp': '$timestamp',
|
|
57
74
|
'trigger': 'pre-push',
|
|
@@ -67,15 +84,20 @@ report = {
|
|
|
67
84
|
'gates': [
|
|
68
85
|
{'id': 'MP', 'name': 'Gate-Script Mirror Parity', 'status': '${GATE_MP_STATUS:-SKIP}', 'details': {'duration_ms': '${MP_DURATION_MS:-0}'}},
|
|
69
86
|
{'id': '10', 'name': 'Build Integrity', 'status': '${GATE_10_STATUS:-SKIP}', 'details': {}},
|
|
70
|
-
{'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details':
|
|
71
|
-
|
|
87
|
+
{'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details': _m_details},
|
|
88
|
+
# #557 follow-up: this line read GATE_M_PYTHON_STATUS while the code assigns
|
|
89
|
+
# GATE_M_PY_STATUS, so the field always fell back to its SKIP default -- the
|
|
90
|
+
# "never write the real state" bug #557 set out to fix, still present for
|
|
91
|
+
# this one gate. The skip reason now travels with it, so a retro reader can
|
|
92
|
+
# tell "no Python here" from "timed out" from "mutmut missing".
|
|
93
|
+
{'id': 'M-Python', 'name': 'Mutation Testing (Python)', 'status': '${GATE_M_PY_STATUS:-SKIP}', 'details': {'skip_reason': '${GATE_M_PY_SKIP_REASON:-}'}},
|
|
72
94
|
{'id': 'M-Go', 'name': 'Mutation Testing (Go)', 'status': '${GATE_M_GO_STATUS:-SKIP}', 'details': {}},
|
|
73
95
|
{'id': 'M-Java', 'name': 'Mutation Testing (Java)', 'status': '${GATE_M_JAVA_STATUS:-SKIP}', 'details': {}},
|
|
74
96
|
{'id': 'M-Kotlin', 'name': 'Mutation Testing (Kotlin)', 'status': '${GATE_M_KOTLIN_STATUS:-SKIP}', 'details': {}},
|
|
75
97
|
{'id': 'M2', 'name': 'Mock Density (WARNING only)', 'status': '${GATE_M2_STATUS:-SKIP}', 'details': {}},
|
|
76
98
|
{'id': 'ML', 'name': 'Mock Layering', 'status': '${GATE_M3_STATUS:-SKIP}', 'details': {}},
|
|
77
99
|
{'id': 'UI', 'name': 'UI Sprint Gates', 'status': '${GATE_UI_STATUS:-SKIP}', 'details': {}},
|
|
78
|
-
{'id': 'MW', 'name': 'Code Walkthrough', 'status': '${GATE_DELPHI_STATUS:-SKIP}', 'details': {'remote': '$REMOTE', 'url': '$URL'}},
|
|
100
|
+
{'id': 'MW', 'name': 'Code Walkthrough', 'status': '${GATE_DELPHI_STATUS:-SKIP}', 'details': {'remote': '$REMOTE', 'url': '$URL', 'skip_reason': '${GATE_DELPHI_SKIP_REASON:-}'}},
|
|
79
101
|
{'id': 'S', 'name': 'Sprint Flow Enforcement', 'status': '${GATE_S_STATUS:-SKIP}', 'details': {}}
|
|
80
102
|
],
|
|
81
103
|
'warnings': [],
|
|
@@ -453,6 +475,15 @@ else
|
|
|
453
475
|
# It is printed by gate-m.ts; take the last percentage it reported.
|
|
454
476
|
M_SCORE=$(sed -n 's/.*[Mm]utation score[: ]*\([0-9][0-9]*\(\.[0-9][0-9]*\)\?\)%.*/\1/p' "$MUTATION_OUTPUT" 2>/dev/null | tail -1)
|
|
455
477
|
|
|
478
|
+
# #544: per-file survivor detail, so retro can answer "which module has
|
|
479
|
+
# the most surviving mutants". gate-m.ts emits one
|
|
480
|
+
# GATE_M_SURVIVORS_JSON= line; it is ABSENT when no report was produced,
|
|
481
|
+
# and absent must stay absent. An empty value means "not measured"; a
|
|
482
|
+
# written zero would mean "measured, no survivors". Only the former is
|
|
483
|
+
# ever true when the line is missing -- conflating the two is the whole
|
|
484
|
+
# of #544.
|
|
485
|
+
M_SURVIVORS_JSON=$(printf '%s\n' "$MUTATION_OUTPUT" | sed -n 's/^GATE_M_SURVIVORS_JSON=//p' | tail -1)
|
|
486
|
+
|
|
456
487
|
case $MUTATION_EXIT in
|
|
457
488
|
0)
|
|
458
489
|
echo "✅ Gate M: PASS"
|
|
@@ -1066,6 +1097,15 @@ else
|
|
|
1066
1097
|
# ============================================================================
|
|
1067
1098
|
RESULT_FILE=".code-walkthrough-result.json"
|
|
1068
1099
|
MW_SKIPPED=0
|
|
1100
|
+
# Journal status (#557 follow-up). The journal already read GATE_DELPHI_STATUS
|
|
1101
|
+
# but nothing ever assigned it, so Gate MW reported its SKIP default in every
|
|
1102
|
+
# report -- including pushes where the walkthrough was in fact verified. The
|
|
1103
|
+
# closing banner told the truth while the machine-readable journal did not, which
|
|
1104
|
+
# is the half-fix #557 was about.
|
|
1105
|
+
GATE_DELPHI_STATUS="SKIP"
|
|
1106
|
+
# Consumed by the journal heredoc (sourced/inherited boundary).
|
|
1107
|
+
# shellcheck disable=SC2034
|
|
1108
|
+
GATE_DELPHI_SKIP_REASON="not_run"
|
|
1069
1109
|
|
|
1070
1110
|
# Skipped on main/master by design, exactly like Gate ML and Gate UI above.
|
|
1071
1111
|
# Work is already walkthrough-reviewed on the feature branch before it lands, so
|
|
@@ -1075,6 +1115,9 @@ MW_SKIPPED=0
|
|
|
1075
1115
|
# documented this skip; the code did not implement it (drift fixed here).
|
|
1076
1116
|
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
|
|
1077
1117
|
MW_SKIPPED=1
|
|
1118
|
+
# Consumed by the journal heredoc (sourced/inherited boundary).
|
|
1119
|
+
# shellcheck disable=SC2034
|
|
1120
|
+
GATE_DELPHI_SKIP_REASON="main_branch_push"
|
|
1078
1121
|
echo ""
|
|
1079
1122
|
echo " ℹ️ Gate MW skipped (pushing to ${CURRENT_BRANCH} — pre-reviewed via branch walkthrough)"
|
|
1080
1123
|
echo ""
|
|
@@ -1144,6 +1187,13 @@ if ! VALIDATION_ERROR=$(node "$MW_VALIDATOR" "$RESULT_FILE" "$EXPECTED_COMMIT" "
|
|
|
1144
1187
|
echo ""
|
|
1145
1188
|
exit 1
|
|
1146
1189
|
fi
|
|
1190
|
+
|
|
1191
|
+
# The validator passed: record that the walkthrough was actually verified.
|
|
1192
|
+
# Consumed by the journal heredoc (sourced/inherited boundary).
|
|
1193
|
+
# shellcheck disable=SC2034
|
|
1194
|
+
GATE_DELPHI_STATUS="PASS"
|
|
1195
|
+
# shellcheck disable=SC2034
|
|
1196
|
+
GATE_DELPHI_SKIP_REASON=""
|
|
1147
1197
|
fi
|
|
1148
1198
|
|
|
1149
1199
|
# ============================================================================
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
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.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test #544 - Gate M survivor detail for retro
|
|
3
|
+
* @intent Verify Gate M reports per-file mutant outcome counts, not just a
|
|
4
|
+
* score. #544: the pre-push journal carried
|
|
5
|
+
* {skip_reason, duration_ms, mutation_score} and nothing else, so
|
|
6
|
+
* `xp-gate retro` could not answer "which module has the most surviving
|
|
7
|
+
* mutants". The data was never missing at the source: Stryker's JSON
|
|
8
|
+
* reporter emits files[].nrOfSurvivedMutants, and stryker-types.ts
|
|
9
|
+
* already declares it. The counts were dropped on the way through, then
|
|
10
|
+
* buildMutationScores hardcoded {mutants: 0, killed: 0, survived: 0}.
|
|
11
|
+
* Three zeros written next to a real score read as "measured: no
|
|
12
|
+
* survivors" -- the same class of false success this project keeps
|
|
13
|
+
* fixing, so unknown must stay absent, never zero.
|
|
14
|
+
* @covers REQ-MUT-001 AC-001
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import {
|
|
19
|
+
summarizeSurvivors,
|
|
20
|
+
buildMutationScores,
|
|
21
|
+
type StrykerReportLike,
|
|
22
|
+
type SurvivorSummary,
|
|
23
|
+
} from '../gate-m';
|
|
24
|
+
|
|
25
|
+
/** A report shaped like Stryker's JSON output. */
|
|
26
|
+
function report(files: Record<string, [number, number, number]>): StrykerReportLike {
|
|
27
|
+
const entries = Object.entries(files);
|
|
28
|
+
const totalMutants = entries.reduce((a, [, v]) => a + v[0], 0);
|
|
29
|
+
const totalKilled = entries.reduce((a, [, v]) => a + v[1], 0);
|
|
30
|
+
const totalSurvived = entries.reduce((a, [, v]) => a + v[2], 0);
|
|
31
|
+
return {
|
|
32
|
+
mutationScore: totalMutants > 0 ? parseFloat(((totalKilled / totalMutants) * 100).toFixed(1)) : 100,
|
|
33
|
+
nrOfMutants: totalMutants,
|
|
34
|
+
nrOfKilledMutants: totalKilled,
|
|
35
|
+
nrOfSurvivedMutants: totalSurvived,
|
|
36
|
+
files: Object.fromEntries(
|
|
37
|
+
entries.map(([k, [m, kd, sv]]) => [k, {
|
|
38
|
+
mutationScore: m > 0 ? parseFloat(((kd / m) * 100).toFixed(1)) : 100,
|
|
39
|
+
nrOfMutants: m,
|
|
40
|
+
nrOfKilledMutants: kd,
|
|
41
|
+
nrOfSurvivedMutants: sv,
|
|
42
|
+
}])
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Narrow to a non-null summary, failing the test with a clear message. */
|
|
48
|
+
function expectSummary(s: SurvivorSummary | null): SurvivorSummary {
|
|
49
|
+
expect(s).not.toBeNull();
|
|
50
|
+
if (s === null) throw new Error('expected a survivor summary, got null');
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('AC-544: Gate M survivor detail', () => {
|
|
55
|
+
it('reports per-file survived counts from the Stryker report', () => {
|
|
56
|
+
const summary = expectSummary(summarizeSurvivors(report({
|
|
57
|
+
'src/mutation/gate-m.ts': [20, 8, 12],
|
|
58
|
+
'src/principles/analyzer.ts': [10, 3, 7],
|
|
59
|
+
})));
|
|
60
|
+
|
|
61
|
+
expect(summary.survivedByFile).toEqual({
|
|
62
|
+
'src/mutation/gate-m.ts': 12,
|
|
63
|
+
'src/principles/analyzer.ts': 7,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('totals killed, survived and mutants', () => {
|
|
68
|
+
const summary = expectSummary(summarizeSurvivors(report({
|
|
69
|
+
'a.ts': [20, 8, 12],
|
|
70
|
+
'b.ts': [10, 3, 7],
|
|
71
|
+
})));
|
|
72
|
+
|
|
73
|
+
expect(summary.mutants).toBe(30);
|
|
74
|
+
expect(summary.killed).toBe(11);
|
|
75
|
+
expect(summary.survived).toBe(19);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('orders survivedByFile worst-first so retro can take the top N', () => {
|
|
79
|
+
const summary = expectSummary(summarizeSurvivors(report({
|
|
80
|
+
'small.ts': [10, 9, 1],
|
|
81
|
+
'worst.ts': [10, 0, 10],
|
|
82
|
+
'middle.ts': [10, 5, 5],
|
|
83
|
+
})));
|
|
84
|
+
|
|
85
|
+
expect(Object.keys(summary.survivedByFile)).toEqual(['worst.ts', 'middle.ts', 'small.ts']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('caps the per-file map so one huge repo cannot bloat the journal', () => {
|
|
89
|
+
const many: Record<string, [number, number, number]> = {};
|
|
90
|
+
// Every file must have at least one survivor, otherwise this asserts on a
|
|
91
|
+
// smaller population than intended and the count below drifts.
|
|
92
|
+
for (let i = 0; i < 50; i++) {
|
|
93
|
+
const survived = (i % 9) + 1; // 1..9, never 0
|
|
94
|
+
many[`f${i}.ts`] = [10, 10 - survived, survived];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const summary = expectSummary(summarizeSurvivors(report(many), { limit: 20 }));
|
|
98
|
+
|
|
99
|
+
expect(Object.keys(summary.survivedByFile)).toHaveLength(20);
|
|
100
|
+
// Truncation must be visible, not silent: a capped list that looks complete
|
|
101
|
+
// is how "top 20" gets mistaken for "all files".
|
|
102
|
+
expect(summary.truncated).toBe(true);
|
|
103
|
+
expect(summary.filesWithSurvivors).toBe(50);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not claim truncation when everything fits', () => {
|
|
107
|
+
const summary = expectSummary(summarizeSurvivors(report({ 'a.ts': [10, 5, 5] }), { limit: 20 }));
|
|
108
|
+
expect(summary.truncated).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('omits files with zero survivors', () => {
|
|
112
|
+
const summary = expectSummary(summarizeSurvivors(report({
|
|
113
|
+
'clean.ts': [10, 10, 0],
|
|
114
|
+
'dirty.ts': [10, 4, 6],
|
|
115
|
+
})));
|
|
116
|
+
|
|
117
|
+
expect(summary.survivedByFile).toEqual({ 'dirty.ts': 6 });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('degrades to null instead of inventing zeros when no report is available', () => {
|
|
121
|
+
// A missing/timed-out report must not be reported as "0 survivors" -- that
|
|
122
|
+
// is the #544 failure repeated. null means unknown.
|
|
123
|
+
expect(summarizeSurvivors(null)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('degrades to null when the report has no per-file section', () => {
|
|
127
|
+
expect(summarizeSurvivors({
|
|
128
|
+
mutationScore: 80,
|
|
129
|
+
nrOfMutants: 10,
|
|
130
|
+
nrOfKilledMutants: 8,
|
|
131
|
+
nrOfSurvivedMutants: 2,
|
|
132
|
+
})).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('buildMutationScores carries the real counts (#544)', () => {
|
|
137
|
+
it('reports measured mutant counts instead of hardcoded zeros', () => {
|
|
138
|
+
// The original defect: the score was real and the three counts beside it
|
|
139
|
+
// were literal zeros, which reads as "measured, no survivors". A consumer
|
|
140
|
+
// that trusts `survived: 0` concludes the module is adequately tested.
|
|
141
|
+
const scores = buildMutationScores({ 'a.ts': 78.4 }, {
|
|
142
|
+
'a.ts': { mutants: 30, killed: 25, survived: 5 },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(scores['a.ts']).toEqual({ score: 78.4, mutants: 30, killed: 25, survived: 5 });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('leaves counts absent when they were never measured', () => {
|
|
149
|
+
// Absent must stay absent -- not fabricated as zero.
|
|
150
|
+
const scores = buildMutationScores({ 'a.ts': 50 }, {});
|
|
151
|
+
|
|
152
|
+
expect(scores['a.ts'].score).toBe(50);
|
|
153
|
+
expect(scores['a.ts'].mutants).toBeUndefined();
|
|
154
|
+
expect(scores['a.ts'].survived).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('keeps a genuinely zero-survivor file at zero', () => {
|
|
158
|
+
// The distinction this whole change is about: measured zero is a real,
|
|
159
|
+
// meaningful value and must survive the trip.
|
|
160
|
+
const scores = buildMutationScores({ 'clean.ts': 100 }, {
|
|
161
|
+
'clean.ts': { mutants: 12, killed: 12, survived: 0 },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(scores['clean.ts'].survived).toBe(0);
|
|
165
|
+
expect(scores['clean.ts'].mutants).toBe(12);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test #544 - the shipped mutation module stays a mirror of the source module
|
|
3
|
+
* @intent src/npm-package/mutation/ is published to npm ("mutation/" is in the
|
|
4
|
+
* package `files` list) but has no automated mirror check, unlike the
|
|
5
|
+
* hooks and adapters which check-hook-mirror.sh diffs. It IS maintained
|
|
6
|
+
* as a byte-identical copy today, so a fix applied only to src/mutation/
|
|
7
|
+
* would ship stale code to every consumer -- exactly the v0.27.3 failure
|
|
8
|
+
* (#542) where a mirror-only fix was rolled back at publish. This guard
|
|
9
|
+
* makes that class of drift red instead of silent, and it is what caught
|
|
10
|
+
* the hardcoded zero counts still living in the package copy.
|
|
11
|
+
* @covers REQ-MUT-001 AC-001
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
// This file lives at src/mutation/__tests__/, so the repo root is three levels
|
|
19
|
+
// up (unlike scripts/__tests__/, which is two).
|
|
20
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
21
|
+
const SRC = path.join(ROOT, 'src', 'mutation');
|
|
22
|
+
const PKG = path.join(ROOT, 'src', 'npm-package', 'mutation');
|
|
23
|
+
|
|
24
|
+
/** Files that must be identical in both trees. */
|
|
25
|
+
const MIRRORED = ['gate-m.ts', 'update-baseline.ts', 'types.ts', 'detect-ai-test.ts', 'init-baseline.ts', 'stryker-types.ts'];
|
|
26
|
+
|
|
27
|
+
describe('src/npm-package/mutation mirrors src/mutation', () => {
|
|
28
|
+
it.each(MIRRORED)('%s is byte-identical in both trees', (file) => {
|
|
29
|
+
const a = fs.readFileSync(path.join(SRC, file), 'utf8');
|
|
30
|
+
const b = fs.readFileSync(path.join(PKG, file), 'utf8');
|
|
31
|
+
expect(b, `${file} has drifted: edit src/mutation/${file} and copy it over`).toBe(a);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('the mirror list is non-empty and every entry exists (anti-vacuity)', () => {
|
|
35
|
+
expect(MIRRORED.length).toBeGreaterThan(3);
|
|
36
|
+
for (const f of MIRRORED) {
|
|
37
|
+
expect(fs.existsSync(path.join(SRC, f)), `missing source ${f}`).toBe(true);
|
|
38
|
+
expect(fs.existsSync(path.join(PKG, f)), `missing mirror ${f}`).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('the package copy carries the #544 fix, not the zero-filling original', () => {
|
|
43
|
+
// Anchor on the defect itself so this cannot pass with a stale-but-identical
|
|
44
|
+
// pair of pre-fix files.
|
|
45
|
+
const shipped = fs.readFileSync(path.join(PKG, 'gate-m.ts'), 'utf8');
|
|
46
|
+
expect(shipped).not.toContain('mutants: 0, killed: 0, survived: 0');
|
|
47
|
+
expect(shipped).toContain('MutantCounts');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -225,12 +225,16 @@ describe('update-baseline.ts - Mutation Baseline Update CLI', () => {
|
|
|
225
225
|
const written = JSON.parse(
|
|
226
226
|
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
227
227
|
);
|
|
228
|
+
// #544: a new entry built from a score alone must NOT be seeded with
|
|
229
|
+
// mutants/killed/survived = 0. This assertion previously pinned those
|
|
230
|
+
// zeros as the expected behaviour, which made "nobody measured this file"
|
|
231
|
+
// indistinguishable from "measured, zero survivors" in every baseline
|
|
232
|
+
// written from the CLI. The fields are omitted instead.
|
|
228
233
|
expect(written.scores['src/new.ts']).toEqual({
|
|
229
234
|
score: 72.0,
|
|
230
|
-
mutants: 0,
|
|
231
|
-
killed: 0,
|
|
232
|
-
survived: 0
|
|
233
235
|
});
|
|
236
|
+
expect(written.scores['src/new.ts'].mutants).toBeUndefined();
|
|
237
|
+
expect(written.scores['src/new.ts'].survived).toBeUndefined();
|
|
234
238
|
// Old entry should still be present
|
|
235
239
|
expect(written.scores['src/old.ts'].score).toBe(60.0);
|
|
236
240
|
});
|
package/mutation/gate-m.ts
CHANGED
|
@@ -438,8 +438,9 @@ async function runAllRunners(
|
|
|
438
438
|
groups: Map<string, string[]>,
|
|
439
439
|
timeoutMs: number,
|
|
440
440
|
cwd: string
|
|
441
|
-
): Promise<Record<string, number | null
|
|
441
|
+
): Promise<{ scores: Record<string, number | null>; counts: Record<string, MutantCounts> }> {
|
|
442
442
|
const fileScores: Record<string, number | null> = {};
|
|
443
|
+
const counts: Record<string, MutantCounts> = {};
|
|
443
444
|
|
|
444
445
|
for (const [, files] of Array.from(groups.entries())) {
|
|
445
446
|
const ext = path.extname(files[0]);
|
|
@@ -455,10 +456,10 @@ async function runAllRunners(
|
|
|
455
456
|
}
|
|
456
457
|
|
|
457
458
|
const result = await runner.run({ files, timeoutMs, cwd });
|
|
458
|
-
applyMutationResult(result, files, fileScores);
|
|
459
|
+
applyMutationResult(result, files, fileScores, counts);
|
|
459
460
|
}
|
|
460
461
|
|
|
461
|
-
return fileScores;
|
|
462
|
+
return { scores: fileScores, counts };
|
|
462
463
|
}
|
|
463
464
|
|
|
464
465
|
/**
|
|
@@ -481,6 +482,7 @@ function applyMutationResult(
|
|
|
481
482
|
outcome: MutationRunOutcome,
|
|
482
483
|
files: string[],
|
|
483
484
|
scores: Record<string, number | null>,
|
|
485
|
+
counts: Record<string, MutantCounts>,
|
|
484
486
|
): void {
|
|
485
487
|
if (outcome.timedOut || outcome.error || !outcome.report) {
|
|
486
488
|
for (const f of files) scores[f] = null;
|
|
@@ -490,11 +492,26 @@ function applyMutationResult(
|
|
|
490
492
|
if (outcome.report.files) {
|
|
491
493
|
for (const [fileKey, fileReport] of Object.entries(outcome.report.files)) {
|
|
492
494
|
scores[fileKey] = fileReport.mutationScore;
|
|
495
|
+
// #544: keep the outcome counts the reporter already gave us. Dropping
|
|
496
|
+
// them here is what forced buildMutationScores to invent zeros later.
|
|
497
|
+
counts[fileKey] = {
|
|
498
|
+
mutants: fileReport.nrOfMutants,
|
|
499
|
+
killed: fileReport.nrOfKilledMutants,
|
|
500
|
+
survived: fileReport.nrOfSurvivedMutants,
|
|
501
|
+
};
|
|
493
502
|
}
|
|
494
503
|
return;
|
|
495
504
|
}
|
|
496
505
|
|
|
497
|
-
|
|
506
|
+
const totals: MutantCounts = {
|
|
507
|
+
mutants: outcome.report.nrOfMutants,
|
|
508
|
+
killed: outcome.report.nrOfKilledMutants,
|
|
509
|
+
survived: outcome.report.nrOfSurvivedMutants,
|
|
510
|
+
};
|
|
511
|
+
for (const f of files) {
|
|
512
|
+
scores[f] = outcome.report.mutationScore;
|
|
513
|
+
counts[f] = totals;
|
|
514
|
+
}
|
|
498
515
|
}
|
|
499
516
|
|
|
500
517
|
// ── Main gateway ──
|
|
@@ -514,14 +531,117 @@ function logThresholdDetails(thresholds: FileThreshold[], baseline: MutationBase
|
|
|
514
531
|
}
|
|
515
532
|
}
|
|
516
533
|
|
|
517
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Per-file mutant outcome counts, as reported by the runner. Absent fields mean
|
|
536
|
+
* "the runner did not say", which is different from a measured zero -- the whole
|
|
537
|
+
* point of #544 is that those two must not collapse into each other.
|
|
538
|
+
*/
|
|
539
|
+
export interface MutantCounts {
|
|
540
|
+
mutants?: number;
|
|
541
|
+
killed?: number;
|
|
542
|
+
survived?: number;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function buildMutationScores(
|
|
546
|
+
fileScores: Record<string, number>,
|
|
547
|
+
counts: Record<string, MutantCounts> = {},
|
|
548
|
+
): Record<string, MutationScore> {
|
|
518
549
|
const scores: Record<string, MutationScore> = {};
|
|
519
550
|
for (const [file, score] of Object.entries(fileScores)) {
|
|
520
|
-
|
|
551
|
+
const c = counts[file];
|
|
552
|
+
const entry: MutationScore = { score } as MutationScore;
|
|
553
|
+
// Only attach what the runner actually reported. Writing `survived: 0` for
|
|
554
|
+
// an unmeasured file is what made a skipped run look like a clean one.
|
|
555
|
+
if (c) {
|
|
556
|
+
if (c.mutants !== undefined) entry.mutants = c.mutants;
|
|
557
|
+
if (c.killed !== undefined) entry.killed = c.killed;
|
|
558
|
+
if (c.survived !== undefined) entry.survived = c.survived;
|
|
559
|
+
}
|
|
560
|
+
scores[file] = entry;
|
|
521
561
|
}
|
|
522
562
|
return scores;
|
|
523
563
|
}
|
|
524
564
|
|
|
565
|
+
/**
|
|
566
|
+
* The subset of a runner report that survivor summarisation needs. Declared
|
|
567
|
+
* structurally rather than as a concrete Stryker type because every runner
|
|
568
|
+
* (Stryker, mutmut, gremlins, go-mutant) normalises to `MutationRunResult`,
|
|
569
|
+
* which carries the same field names -- the summary must work for all of them,
|
|
570
|
+
* not just the TS one.
|
|
571
|
+
*/
|
|
572
|
+
export interface StrykerReportLike {
|
|
573
|
+
mutationScore: number;
|
|
574
|
+
nrOfMutants: number;
|
|
575
|
+
nrOfKilledMutants: number;
|
|
576
|
+
nrOfSurvivedMutants: number;
|
|
577
|
+
files?: Record<string, {
|
|
578
|
+
mutationScore: number;
|
|
579
|
+
nrOfMutants: number;
|
|
580
|
+
nrOfKilledMutants: number;
|
|
581
|
+
nrOfSurvivedMutants: number;
|
|
582
|
+
}>;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Per-file mutant outcome detail for the pre-push journal (#544). */
|
|
586
|
+
export interface SurvivorSummary {
|
|
587
|
+
/** Files with at least one surviving mutant, worst first, capped by `limit`. */
|
|
588
|
+
survivedByFile: Record<string, number>;
|
|
589
|
+
/**
|
|
590
|
+
* How many files actually had survivors. Present so a capped map can be told
|
|
591
|
+
* apart from a complete one -- a truncated list that looks exhaustive is how
|
|
592
|
+
* "top 20" gets read as "all files".
|
|
593
|
+
*/
|
|
594
|
+
filesWithSurvivors: number;
|
|
595
|
+
/** True when `survivedByFile` dropped entries to stay under `limit`. */
|
|
596
|
+
truncated: boolean;
|
|
597
|
+
mutants: number;
|
|
598
|
+
killed: number;
|
|
599
|
+
survived: number;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Condense a Stryker report into survivor detail (#544).
|
|
604
|
+
*
|
|
605
|
+
* Returns `null` -- never a summary of zeros -- when the report is missing or
|
|
606
|
+
* carries no per-file section. A timed-out or failed run has no measurements,
|
|
607
|
+
* and writing `survived: 0` next to a real-looking score would report
|
|
608
|
+
* "measured: no survivors" for a run that measured nothing. That is the exact
|
|
609
|
+
* shape of false success this project keeps finding, so unknown stays `null`.
|
|
610
|
+
*/
|
|
611
|
+
export function summarizeSurvivors(
|
|
612
|
+
report: StrykerReportLike | null | undefined,
|
|
613
|
+
options: { limit?: number } = {},
|
|
614
|
+
): SurvivorSummary | null {
|
|
615
|
+
if (!report || !report.files) return null;
|
|
616
|
+
|
|
617
|
+
const limit = options.limit ?? 20;
|
|
618
|
+
const survivors: Array<[string, number]> = [];
|
|
619
|
+
let mutants = 0;
|
|
620
|
+
let killed = 0;
|
|
621
|
+
let survived = 0;
|
|
622
|
+
|
|
623
|
+
for (const [file, entry] of Object.entries(report.files)) {
|
|
624
|
+
const fileSurvived = entry.nrOfSurvivedMutants ?? 0;
|
|
625
|
+
mutants += entry.nrOfMutants ?? 0;
|
|
626
|
+
killed += entry.nrOfKilledMutants ?? 0;
|
|
627
|
+
survived += fileSurvived;
|
|
628
|
+
if (fileSurvived > 0) survivors.push([file, fileSurvived]);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Worst first, then by path so equal counts order deterministically.
|
|
632
|
+
survivors.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
633
|
+
const kept = survivors.slice(0, limit);
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
survivedByFile: Object.fromEntries(kept),
|
|
637
|
+
filesWithSurvivors: survivors.length,
|
|
638
|
+
truncated: survivors.length > kept.length,
|
|
639
|
+
mutants,
|
|
640
|
+
killed,
|
|
641
|
+
survived,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
525
645
|
export async function runGateM(options: GateMOptions): Promise<GateMResult> {
|
|
526
646
|
const warnings: string[] = [];
|
|
527
647
|
const errors: string[] = [];
|
|
@@ -539,7 +659,8 @@ export async function runGateM(options: GateMOptions): Promise<GateMResult> {
|
|
|
539
659
|
logThresholdDetails(thresholds, baseline);
|
|
540
660
|
|
|
541
661
|
const groups = groupByRunner(sourceFiles);
|
|
542
|
-
const fileScoresNullable
|
|
662
|
+
const { scores: fileScoresNullable, counts: mutantCounts } =
|
|
663
|
+
await runAllRunners(groups, options.timeoutMs, process.cwd());
|
|
543
664
|
|
|
544
665
|
const { fileScores, timeoutFiles } = partitionScores(fileScoresNullable);
|
|
545
666
|
if (timeoutFiles.length > 0) {
|
|
@@ -557,12 +678,12 @@ export async function runGateM(options: GateMOptions): Promise<GateMResult> {
|
|
|
557
678
|
}
|
|
558
679
|
|
|
559
680
|
if (evalResult.blocked) {
|
|
560
|
-
return buildResult('block', evaluableThresholds.length, buildMutationScores(fileScores), warnings, errors);
|
|
681
|
+
return buildResult('block', evaluableThresholds.length, buildMutationScores(fileScores, mutantCounts), warnings, errors);
|
|
561
682
|
}
|
|
562
683
|
if (evaluableThresholds.length === 0) {
|
|
563
684
|
return buildResult('skip', 0, {}, warnings, errors);
|
|
564
685
|
}
|
|
565
|
-
return buildResult('pass', evaluableThresholds.length, buildMutationScores(fileScores), warnings, errors);
|
|
686
|
+
return buildResult('pass', evaluableThresholds.length, buildMutationScores(fileScores, mutantCounts), warnings, errors);
|
|
566
687
|
}
|
|
567
688
|
|
|
568
689
|
interface PartitionedScores {
|
|
@@ -595,6 +716,22 @@ async function collectTestIntentWarnings(sourceFiles: string[], warnings: string
|
|
|
595
716
|
}
|
|
596
717
|
}
|
|
597
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Read the mutation report the runner left on disk and summarise it (#544).
|
|
721
|
+
* Returns null when there is no readable report -- the caller then prints
|
|
722
|
+
* nothing, and the journal field stays absent. Every failure here is silent by
|
|
723
|
+
* design: #544 explicitly requires that missing detail must not turn Gate M
|
|
724
|
+
* itself into a failure.
|
|
725
|
+
*/
|
|
726
|
+
async function readSurvivorSummary(): Promise<SurvivorSummary | null> {
|
|
727
|
+
try {
|
|
728
|
+
const raw = await fs.readFile('.stryker-report.json', 'utf-8');
|
|
729
|
+
return summarizeSurvivors(JSON.parse(raw) as StrykerReportLike);
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
598
735
|
export async function main(args: string[]): Promise<number> {
|
|
599
736
|
const options = parseArgs(args);
|
|
600
737
|
|
|
@@ -611,19 +748,14 @@ export async function main(args: string[]): Promise<number> {
|
|
|
611
748
|
|
|
612
749
|
const result = await runGateM(options);
|
|
613
750
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
751
|
+
// #544: survivor detail on a machine-readable line. Printed only when a report
|
|
752
|
+
// exists -- absent means "not measured", never "measured: zero survivors".
|
|
753
|
+
const survivors = await readSurvivorSummary();
|
|
754
|
+
if (survivors) {
|
|
755
|
+
console.log(`GATE_M_SURVIVORS_JSON=${JSON.stringify(survivors)}`);
|
|
619
756
|
}
|
|
620
757
|
|
|
621
|
-
|
|
622
|
-
console.log('\nErrors:');
|
|
623
|
-
for (const error of result.errors) {
|
|
624
|
-
console.error(` ✗ ${error}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
758
|
+
logDiagnostics(result.warnings, result.errors);
|
|
627
759
|
|
|
628
760
|
const statusIcon = result.status === 'pass' ? '✓' :
|
|
629
761
|
result.status === 'skip' ? '⊘' :
|
|
@@ -634,6 +766,18 @@ export async function main(args: string[]): Promise<number> {
|
|
|
634
766
|
return result.exitCode;
|
|
635
767
|
}
|
|
636
768
|
|
|
769
|
+
/** Print Gate M's warnings and errors. Errors go to stderr. */
|
|
770
|
+
function logDiagnostics(warnings: string[], errors: string[]): void {
|
|
771
|
+
if (warnings.length > 0) {
|
|
772
|
+
console.log('\nWarnings:');
|
|
773
|
+
for (const warning of warnings) console.log(` ⚠ ${warning}`);
|
|
774
|
+
}
|
|
775
|
+
if (errors.length > 0) {
|
|
776
|
+
console.log('\nErrors:');
|
|
777
|
+
for (const error of errors) console.error(` ✗ ${error}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
637
781
|
const cliArgs = process.argv.slice(2);
|
|
638
782
|
if (typeof require !== 'undefined' && require.main === module) {
|
|
639
783
|
main(cliArgs)
|
package/mutation/types.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
export interface MutationScore {
|
|
2
2
|
score: number;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Mutant outcome counts. Optional on purpose (#544): a runner that did not
|
|
5
|
+
* report them must leave them absent, so a consumer can tell "not measured"
|
|
6
|
+
* from "measured zero". They used to be hardcoded to 0, which made every
|
|
7
|
+
* unmeasured file read as a clean one.
|
|
8
|
+
*/
|
|
9
|
+
mutants?: number;
|
|
10
|
+
killed?: number;
|
|
11
|
+
survived?: number;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
export interface MutationBaseline {
|
|
@@ -3,9 +3,14 @@ import path from 'path';
|
|
|
3
3
|
|
|
4
4
|
interface MutationScoreEntry {
|
|
5
5
|
score: number;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Optional (#544): a baseline entry built from a score alone has no mutant
|
|
8
|
+
* counts, and inventing zeros would record a clean file that was never
|
|
9
|
+
* measured.
|
|
10
|
+
*/
|
|
11
|
+
mutants?: number;
|
|
12
|
+
killed?: number;
|
|
13
|
+
survived?: number;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
interface MutationBaseline {
|
|
@@ -92,11 +97,13 @@ function updateBaseline(
|
|
|
92
97
|
score: parseFloat(score.toFixed(1))
|
|
93
98
|
};
|
|
94
99
|
} else {
|
|
100
|
+
// #544: do not seed the baseline with zeros. This path only has a score
|
|
101
|
+
// from the hand-edited/CLI score list and no mutant counts at all, so
|
|
102
|
+
// writing mutants/killed/survived = 0 would record "measured, no
|
|
103
|
+
// survivors" for a file nobody measured. Absent means unknown, and the
|
|
104
|
+
// fields are optional on MutationScoreEntry for exactly this reason.
|
|
95
105
|
updatedScores[filePath] = {
|
|
96
106
|
score: parseFloat(score.toFixed(1)),
|
|
97
|
-
mutants: 0,
|
|
98
|
-
killed: 0,
|
|
99
|
-
survived: 0
|
|
100
107
|
};
|
|
101
108
|
}
|
|
102
109
|
}
|
|
@@ -127,7 +134,17 @@ async function removeDeletedFiles(baseline: MutationBaseline): Promise<MutationB
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
async function saveBaseline(baseline: MutationBaseline, baselinePath: string): Promise<void> {
|
|
130
|
-
|
|
137
|
+
// A failed write used to propagate as a bare rejection from deep inside the
|
|
138
|
+
// CLI's top-level await, producing an unhandled-rejection stack with no
|
|
139
|
+
// indication of WHICH file could not be written -- and on a permission or
|
|
140
|
+
// disk-full error the baseline silently stayed stale while the run looked
|
|
141
|
+
// complete. Report the path and the reason, then fail loudly.
|
|
142
|
+
try {
|
|
143
|
+
await fs.writeFile(baselinePath, JSON.stringify(baseline, null, 2));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
146
|
+
throw new Error(`Failed to write mutation baseline to ${baselinePath}: ${reason}`);
|
|
147
|
+
}
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
async function main(): Promise<void> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.5",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.5",
|
|
4
4
|
"description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.5",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-21
|
|
4
|
+
**Commit:** 4e1f8be0
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.30.
|
|
6
|
+
**Version:** 0.30.5.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|