@boyingliu01/xp-gate 0.30.2 → 0.30.3
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/iac.sh +45 -4
- package/adapters/java.sh +82 -16
- package/adapters/powershell.sh +70 -9
- package/gate-10.sh +32 -2
- package/gate-3.sh +17 -0
- package/gate-4.sh +12 -0
- package/gate-7.sh +4 -0
- package/hooks/gate-10.sh +32 -2
- package/hooks/gate-3.sh +17 -0
- package/hooks/gate-4.sh +12 -0
- package/hooks/gate-7.sh +4 -0
- package/hooks/hook-module-resolution.sh +14 -0
- package/hooks/lib/coverage-total.sh +1 -1
- package/hooks/pre-commit +138 -17
- package/hooks/pre-push +97 -6
- package/hooks/sprint-gate.sh +81 -2
- package/lib/__tests__/hook-health.test.js +85 -0
- package/lib/coverage-total.sh +92 -0
- package/lib/doctor.js +44 -0
- package/lib/hook-health.js +121 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/sprint-gate.sh +81 -2
package/hooks/gate-3.sh
CHANGED
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
9
9
|
GATE_3_START=$(gate_start_ms)
|
|
10
10
|
# #537: every non-run records WHY so the report can say why a gate is SKIP.
|
|
11
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
12
|
+
# shellcheck disable=SC2034
|
|
11
13
|
GATE_3_REASON=""
|
|
12
14
|
|
|
13
15
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
14
16
|
echo "⏭️ SKIPPED - Complexity (documentation project)."
|
|
15
17
|
GATE_3_STATUS="SKIP"
|
|
18
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
19
|
+
# shellcheck disable=SC2034
|
|
16
20
|
GATE_3_REASON="documentation_project"
|
|
17
21
|
|
|
18
22
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
@@ -21,6 +25,8 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
21
25
|
GATE_3_STATUS="SKIP"
|
|
22
26
|
# #537: a PowerShell-primary repo was invisible to this dimension while the
|
|
23
27
|
# score implied full coverage — say exactly why nothing ran.
|
|
28
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
29
|
+
# shellcheck disable=SC2034
|
|
24
30
|
GATE_3_REASON="no_powershell_tool"
|
|
25
31
|
|
|
26
32
|
else
|
|
@@ -159,7 +165,10 @@ else
|
|
|
159
165
|
# empty stdin — the verdict comes back empty and Gate 3 passes on zero
|
|
160
166
|
# rows (measured while wiring #544: same input passed or blocked
|
|
161
167
|
# depending only on where the prefix sat).
|
|
168
|
+
# Consumed by evaluate_ccn_ratchet in the sourced lib, not here.
|
|
169
|
+
# shellcheck disable=SC2034
|
|
162
170
|
CCN_WARN_THRESHOLD="$CCN_THRESHOLD"
|
|
171
|
+
# shellcheck disable=SC2034
|
|
163
172
|
CCN_BLOCK_THRESHOLD=$((CCN_THRESHOLD * 2))
|
|
164
173
|
RATCHET_VERDICT=$(printf '%s\n' "$CC_CSV" | evaluate_ccn_ratchet ".ccn-baseline.json")
|
|
165
174
|
CC_VIOLATIONS=$(printf '%s\n' "$RATCHET_VERDICT" | grep -c '^BLOCK ' || true)
|
|
@@ -205,6 +214,8 @@ else
|
|
|
205
214
|
END { exit(found ? 0 : 1) }'; then
|
|
206
215
|
echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
|
|
207
216
|
GATE_3_STATUS="WARN"
|
|
217
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
218
|
+
# shellcheck disable=SC2034
|
|
208
219
|
GATE_3_REASON="lizard_output_unparsable"
|
|
209
220
|
elif [ "$CC_WARNINGS" -gt 0 ]; then
|
|
210
221
|
echo "$CC_OUTPUT"
|
|
@@ -212,6 +223,8 @@ else
|
|
|
212
223
|
echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
|
|
213
224
|
echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
|
|
214
225
|
GATE_3_STATUS="BLOCK"
|
|
226
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
227
|
+
# shellcheck disable=SC2034
|
|
215
228
|
GATE_3_REASON="ccn_over_threshold"
|
|
216
229
|
exit 1
|
|
217
230
|
else
|
|
@@ -222,6 +235,8 @@ else
|
|
|
222
235
|
else
|
|
223
236
|
echo "⏭️ SKIPPED - Complexity (no source files to check)."
|
|
224
237
|
GATE_3_STATUS="SKIP"
|
|
238
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
|
+
# shellcheck disable=SC2034
|
|
225
240
|
GATE_3_REASON="no_source_files_changed"
|
|
226
241
|
fi
|
|
227
242
|
else
|
|
@@ -229,6 +244,8 @@ else
|
|
|
229
244
|
echo " Install with: pip install --user lizard"
|
|
230
245
|
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
231
246
|
GATE_3_STATUS="WARN"
|
|
247
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
248
|
+
# shellcheck disable=SC2034
|
|
232
249
|
GATE_3_REASON="tool_not_installed"
|
|
233
250
|
fi
|
|
234
251
|
fi
|
package/hooks/gate-4.sh
CHANGED
|
@@ -56,11 +56,15 @@ GATE_4_START=$(gate_start_ms)
|
|
|
56
56
|
GATE_4_STATUS=""
|
|
57
57
|
# #537: every non-run records WHY (no files / tool missing / unsupported lang)
|
|
58
58
|
# so the quality report can distinguish "not applicable" from "not covered".
|
|
59
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
60
|
+
# shellcheck disable=SC2034
|
|
59
61
|
GATE_4_REASON=""
|
|
60
62
|
|
|
61
63
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
62
64
|
echo "⏭️ SKIPPED - Principles check (documentation project)."
|
|
63
65
|
GATE_4_STATUS="SKIP"
|
|
66
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
67
|
+
# shellcheck disable=SC2034
|
|
64
68
|
GATE_4_REASON="documentation_project"
|
|
65
69
|
|
|
66
70
|
else
|
|
@@ -156,6 +160,8 @@ else
|
|
|
156
160
|
echo "⚠️ Warning: Principles checker execution failed"
|
|
157
161
|
echo "⏭️ SKIPPED - Principles check (execution issue)"
|
|
158
162
|
GATE_4_STATUS="SKIP"
|
|
163
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
164
|
+
# shellcheck disable=SC2034
|
|
159
165
|
GATE_4_REASON="checker_execution_error"
|
|
160
166
|
rm -f "$PRINCIPLES_JSON"
|
|
161
167
|
else
|
|
@@ -191,17 +197,23 @@ else
|
|
|
191
197
|
echo "ℹ️ npx/tsx not available - skipping principles check"
|
|
192
198
|
echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
|
|
193
199
|
GATE_4_STATUS="SKIP"
|
|
200
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
201
|
+
# shellcheck disable=SC2034
|
|
194
202
|
GATE_4_REASON="tool_not_installed"
|
|
195
203
|
fi
|
|
196
204
|
else
|
|
197
205
|
echo "ℹ️ Principles checker not found in project - skipping"
|
|
198
206
|
echo "⏭️ SKIPPED - Principles check (checker not in project)"
|
|
199
207
|
GATE_4_STATUS="SKIP"
|
|
208
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
209
|
+
# shellcheck disable=SC2034
|
|
200
210
|
GATE_4_REASON="checker_not_found"
|
|
201
211
|
fi
|
|
202
212
|
else
|
|
203
213
|
echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
|
|
204
214
|
GATE_4_STATUS="SKIP"
|
|
215
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
216
|
+
# shellcheck disable=SC2034
|
|
205
217
|
GATE_4_REASON="no_matching_files_changed"
|
|
206
218
|
fi
|
|
207
219
|
fi
|
package/hooks/gate-7.sh
CHANGED
|
@@ -12,11 +12,15 @@ GATE_7_START=$(gate_start_ms)
|
|
|
12
12
|
IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
|
|
13
13
|
# #536: scanned_files makes a trivial PASS (nothing to scan) auditable in the
|
|
14
14
|
# quality report instead of reading like a covered security check.
|
|
15
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
16
|
+
# shellcheck disable=SC2034
|
|
15
17
|
GATE_7_SCANNED_FILES=0
|
|
16
18
|
if [ -z "$IAC_FILES" ]; then
|
|
17
19
|
echo "✅ PASSED - No IaC files detected in changes."
|
|
18
20
|
GATE_7_STATUS="PASS"
|
|
19
21
|
else
|
|
22
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
23
|
+
# shellcheck disable=SC2034
|
|
20
24
|
GATE_7_SCANNED_FILES=$(printf '%s\n' "$IAC_FILES" | sed -n '/./p' | wc -l | tr -d '[:space:]')
|
|
21
25
|
# Run IaC adapter. Resolution follows the documented 3-tier convention
|
|
22
26
|
# (adapter-common.sh): global ~/.config/xp-gate/adapters -> project
|
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
# is its shell projection. Editing it by hand makes the hook disagree with
|
|
8
8
|
# doctor.js, which reads the JS module directly — the exact divergence
|
|
9
9
|
# #495/#500 were about.
|
|
10
|
+
#
|
|
11
|
+
# Every HOOK_* constant below is read by the sourcing parent, not by this
|
|
12
|
+
# file. A linter checking this file alone cannot see that consumer, so each
|
|
13
|
+
# declaration carries a scoped directive with the reason stated at the site
|
|
14
|
+
# (#548 -- the same false SC2034 used to block consumer repos that vendor
|
|
15
|
+
# these hook modules). The directives are generated, not hand-added.
|
|
10
16
|
|
|
11
17
|
# Probe file used to pick the gate tier.
|
|
18
|
+
# Consumed by the sourcing parent, not here.
|
|
19
|
+
# shellcheck disable=SC2034
|
|
12
20
|
HOOK_GATE_PROBE="gate-3.sh"
|
|
13
21
|
|
|
14
22
|
# ADAPTER_DIR cascade, highest priority first.
|
|
15
23
|
# Each entry is "tier_id|base_var|probe_file".
|
|
24
|
+
# Consumed by the sourcing parent, not here.
|
|
25
|
+
# shellcheck disable=SC2034
|
|
16
26
|
HOOK_ADAPTER_DIR_TIERS=(
|
|
17
27
|
'global|GLOBAL_ADAPTER_DIR|adapter-common.sh'
|
|
18
28
|
'project|PROJECT_GITHOOKS|adapter-common.sh'
|
|
@@ -21,6 +31,8 @@ HOOK_ADAPTER_DIR_TIERS=(
|
|
|
21
31
|
|
|
22
32
|
# Adapter resolution tiers, highest priority first.
|
|
23
33
|
# Each entry is "tier_id|base_var|template" with {name} as the basename.
|
|
34
|
+
# Consumed by the sourcing parent, not here.
|
|
35
|
+
# shellcheck disable=SC2034
|
|
24
36
|
HOOK_ADAPTER_TIERS=(
|
|
25
37
|
'adapter_flat|ADAPTER_DIR|{name}.sh'
|
|
26
38
|
'adapter_nested|ADAPTER_DIR|adapters/{name}.sh'
|
|
@@ -32,6 +44,8 @@ HOOK_ADAPTER_TIERS=(
|
|
|
32
44
|
# Semantics differ from adapter tiers: each entry names the DIRECTORY that
|
|
33
45
|
# would hold the gate probe, so a match yields the directory (GATE_DIR), not
|
|
34
46
|
# the probe file itself. Entry format: "tier_id|base_var|(empty suffix)".
|
|
47
|
+
# Consumed by the sourcing parent, not here.
|
|
48
|
+
# shellcheck disable=SC2034
|
|
35
49
|
HOOK_GATE_TIERS=(
|
|
36
50
|
'gate_adapter_dir|ADAPTER_DIR|'
|
|
37
51
|
'gate_project|PROJECT_GITHOOKS|'
|
|
@@ -70,7 +70,7 @@ enforce_python_coverage() {
|
|
|
70
70
|
fi
|
|
71
71
|
|
|
72
72
|
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
73
|
-
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
73
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
74
74
|
# #399: a subset run (smart test selection) measures only the tested
|
|
75
75
|
# files, so its global total is meaningless as a verdict — the full
|
|
76
76
|
# suite may well be >=80% even when the subset is not. Warn exactly as
|
package/hooks/pre-commit
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# Line-ending self-check (#546). Must stay on the first executable line: if this
|
|
3
|
+
# file is ever rewritten with CRLF, bash needs to reach this check before the
|
|
4
|
+
# parser chokes somewhere far away and blames an innocent line. `tr` also
|
|
5
|
+
# strips the \r from this very line, so the check runs even when everything
|
|
6
|
+
# around it is broken.
|
|
7
|
+
if tr -d '\r' < "$0" | sed -n '2p' | tr -d '\r' | grep -q 'Line-ending self-check'; then
|
|
8
|
+
if [ "$(tr -cd '\r' < "$0" | wc -c | tr -d '[:space:]')" -gt 0 ]; then
|
|
9
|
+
echo "======================================================================" >&2
|
|
10
|
+
echo "FATAL: hook has CRLF line endings — bash cannot parse it (#546)." >&2
|
|
11
|
+
echo " file: $0" >&2
|
|
12
|
+
echo " fix : tr -d '\\r' < \"$0\" > \"$0.tmp\" && mv \"$0.tmp\" \"$0\" && chmod +x \"$0\"" >&2
|
|
13
|
+
echo "Or re-sync the installed copy: xp-gate doctor --sync-hooks" >&2
|
|
14
|
+
echo "======================================================================" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
2
18
|
# OpenCode Quality Gates - Pre-Commit Hook - 12 numbered gates (Gate 0–11)
|
|
3
19
|
#
|
|
4
20
|
# DESIGN PRINCIPLE: Tool unavailable = SKIP (graceful degradation), tool available + check fails = BLOCK
|
|
@@ -41,6 +57,41 @@ fi
|
|
|
41
57
|
# shellcheck source=/dev/null
|
|
42
58
|
. "$HOOK_RESOLUTION_SH"
|
|
43
59
|
|
|
60
|
+
# --- Coverage threshold: ONE resolver, one number (#555) --------------------
|
|
61
|
+
# The threshold used to exist three times with two different values: the report
|
|
62
|
+
# printed a literal 80, coverage-total.sh compared against a literal 80, and the
|
|
63
|
+
# generic block branch echoed "below 80% threshold" — while the repo's own
|
|
64
|
+
# pyproject declared fail_under. CI honours the declared value, so a commit at
|
|
65
|
+
# 81% passed the hook (judged against 80) and failed CI (82). The report was the
|
|
66
|
+
# worst of the three: it quoted a threshold as though it were the enforced one,
|
|
67
|
+
# and a plausible wrong number is harder to notice than a visible gap.
|
|
68
|
+
#
|
|
69
|
+
# Resolve from the repo's own declaration, in the order the tools themselves
|
|
70
|
+
# use: pyproject [tool.coverage.report] fail_under, then .coveragerc. A repo
|
|
71
|
+
# declaring neither keeps the historical 80.
|
|
72
|
+
_resolve_coverage_threshold() {
|
|
73
|
+
_rct_value=""
|
|
74
|
+
_rct_src="default"
|
|
75
|
+
if [ -f pyproject.toml ]; then
|
|
76
|
+
_rct_value=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['tool']['coverage']['report']['fail_under'])" 2>/dev/null || true)
|
|
77
|
+
[ -n "$_rct_value" ] && _rct_src="pyproject.toml [tool.coverage.report] fail_under"
|
|
78
|
+
if [ -z "$_rct_value" ]; then
|
|
79
|
+
_rct_value=$(grep -oE '^[[:space:]]*fail_under[[:space:]]*=[[:space:]]*[0-9]+' pyproject.toml 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)
|
|
80
|
+
[ -n "$_rct_value" ] && _rct_src="pyproject.toml fail_under (text scan)"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
if [ -z "$_rct_value" ] && [ -f .coveragerc ]; then
|
|
84
|
+
_rct_value=$(grep -oE '^[[:space:]]*fail_under[[:space:]]*=[[:space:]]*[0-9]+' .coveragerc 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)
|
|
85
|
+
[ -n "$_rct_value" ] && _rct_src=".coveragerc fail_under"
|
|
86
|
+
fi
|
|
87
|
+
case "$_rct_value" in
|
|
88
|
+
''|*[!0-9]*) _rct_value="80"; _rct_src="default (no fail_under declared)" ;;
|
|
89
|
+
esac
|
|
90
|
+
COVERAGE_THRESHOLD_SOURCE="$_rct_src"
|
|
91
|
+
printf '%s' "$_rct_value"
|
|
92
|
+
}
|
|
93
|
+
COVERAGE_THRESHOLD="$(_resolve_coverage_threshold)"
|
|
94
|
+
|
|
44
95
|
# Look up one tier's directory variable by name without eval-ing arbitrary input:
|
|
45
96
|
# the base names are a closed set baked into the generated file.
|
|
46
97
|
_hook_tier_base() {
|
|
@@ -1238,7 +1289,7 @@ fi
|
|
|
1238
1289
|
GATE_1_START=$(gate_start_ms)
|
|
1239
1290
|
GATE_1_STATUS="PASS"
|
|
1240
1291
|
GATE_1_DETAIL=""
|
|
1241
|
-
if ! any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|mm|dart|sh)$" "\.ya?ml$" "\.json$" "Dockerfile" "\.tf$"; then
|
|
1292
|
+
if ! any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|mm|dart|sh|ps1|psm1)$" "\.ya?ml$" "\.json$" "Dockerfile" "\.tf$"; then
|
|
1242
1293
|
echo "✅ PASSED - No code files changed, skipping lint."
|
|
1243
1294
|
GATE_1_STATUS="SKIP"
|
|
1244
1295
|
GATE_1_DETAIL="no_matching_files"
|
|
@@ -1764,6 +1815,58 @@ else
|
|
|
1764
1815
|
fi
|
|
1765
1816
|
;;
|
|
1766
1817
|
|
|
1818
|
+
powershell)
|
|
1819
|
+
# #547 finding 2: PowerShell had no arm here, so it fell into the generic
|
|
1820
|
+
# `*)` branch, where a non-zero analysis verdict printed a non-blocking
|
|
1821
|
+
# line and GATE_1_STATUS stayed PASS. The java arm's XP_G1_REASON
|
|
1822
|
+
# protocol applies verbatim: 0 = pass, 1 = block, 2 = unusable
|
|
1823
|
+
# (fail-closed), 3 = WARN with a reason that STAYS in the scoring
|
|
1824
|
+
# denominator so a skip is visible in retro instead of vanishing.
|
|
1825
|
+
POWERSHELL_ADAPTER=$(resolve_adapter_path "powershell")
|
|
1826
|
+
if [ -z "$POWERSHELL_ADAPTER" ]; then
|
|
1827
|
+
echo "⚠️ WARN - PowerShell adapter not found (code quality NOT verified)"
|
|
1828
|
+
GATE_1_STATUS="WARN"
|
|
1829
|
+
GATE_1_DETAIL="powershell: adapter missing"
|
|
1830
|
+
else
|
|
1831
|
+
PS_G1_EXIT=0
|
|
1832
|
+
# Subshell: reasons cross as `XP_G1_REASON: ...` stdout lines, exactly
|
|
1833
|
+
# as in the java arm — an export inside the adapter cannot reach us.
|
|
1834
|
+
PS_G1_OUTPUT=$(source "$POWERSHELL_ADAPTER" 2>/dev/null; run_static_analysis 2>&1) || PS_G1_EXIT=$?
|
|
1835
|
+
PS_G1_REASON=$(printf '%s\n' "$PS_G1_OUTPUT" | sed -n 's/^XP_G1_REASON: //p' | head -1)
|
|
1836
|
+
if [ -n "$PS_G1_REASON" ]; then
|
|
1837
|
+
PS_G1_OUTPUT=$(printf '%s\n' "$PS_G1_OUTPUT" | grep -v '^XP_G1_REASON: ')
|
|
1838
|
+
fi
|
|
1839
|
+
[ -n "$PS_G1_OUTPUT" ] && printf '%s\n' "$PS_G1_OUTPUT" | head -40
|
|
1840
|
+
case "$PS_G1_EXIT" in
|
|
1841
|
+
0)
|
|
1842
|
+
echo "✅ PASSED - PowerShell code quality (PSScriptAnalyzer)."
|
|
1843
|
+
;;
|
|
1844
|
+
1)
|
|
1845
|
+
echo "❌ BLOCKED - PSScriptAnalyzer violations (fix or add suppressions)"
|
|
1846
|
+
GATE_1_STATUS="BLOCK"
|
|
1847
|
+
GATE_1_DETAIL="powershell: PSScriptAnalyzer findings"
|
|
1848
|
+
exit 1
|
|
1849
|
+
;;
|
|
1850
|
+
2)
|
|
1851
|
+
echo "❌ BLOCKED - PowerShell static analysis unusable (fail-closed): ${PS_G1_REASON:-unknown}"
|
|
1852
|
+
GATE_1_STATUS="BLOCK"
|
|
1853
|
+
GATE_1_DETAIL="powershell: ${PS_G1_REASON:-unusable}"
|
|
1854
|
+
exit 1
|
|
1855
|
+
;;
|
|
1856
|
+
3)
|
|
1857
|
+
echo "⚠️ WARN - PowerShell code quality NOT verified: ${PS_G1_REASON:-engine unavailable}"
|
|
1858
|
+
GATE_1_STATUS="WARN"
|
|
1859
|
+
GATE_1_DETAIL="powershell: ${PS_G1_REASON:-engine unavailable}"
|
|
1860
|
+
;;
|
|
1861
|
+
*)
|
|
1862
|
+
echo "⚠️ WARN - PowerShell code quality NOT verified (unexpected verdict $PS_G1_EXIT)"
|
|
1863
|
+
GATE_1_STATUS="WARN"
|
|
1864
|
+
GATE_1_DETAIL="powershell: unexpected verdict $PS_G1_EXIT"
|
|
1865
|
+
;;
|
|
1866
|
+
esac
|
|
1867
|
+
fi
|
|
1868
|
+
;;
|
|
1869
|
+
|
|
1767
1870
|
*)
|
|
1768
1871
|
# Route to appropriate adapter for other languages
|
|
1769
1872
|
ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
|
|
@@ -2711,11 +2814,18 @@ else
|
|
|
2711
2814
|
PY_TARGET_COUNT=$(printf '%s\n' "$PY_TARGETS" | grep -c '.' || true)
|
|
2712
2815
|
echo "ℹ️ Python: selective run ($PY_STRATEGY) — $PY_TARGET_COUNT target(s):"
|
|
2713
2816
|
printf '%s\n' "$PY_TARGETS" | sed 's/^/ /'
|
|
2714
|
-
# Subset run: keep --cov so data is collected, but
|
|
2715
|
-
#
|
|
2817
|
+
# Subset run: keep --cov so data is collected, but the global
|
|
2818
|
+
# threshold must be actively DISABLED, not merely omitted.
|
|
2819
|
+
# Omitting --cov-fail-under does not work (#556): pytest-cov
|
|
2820
|
+
# reads fail_under from pyproject on its own, so the floor
|
|
2821
|
+
# stayed in force, a selective run at 23% exited 1, and the
|
|
2822
|
+
# exit was escalated to a hard BLOCK -- printing "Partial
|
|
2823
|
+
# coverage ... not full suite" immediately above "BLOCKED -
|
|
2824
|
+
# Coverage check FAILED". --cov-fail-under=0 is the documented
|
|
2825
|
+
# way to switch it off for one invocation.
|
|
2716
2826
|
PARTIAL_TEST_RUN=true
|
|
2717
2827
|
# shellcheck disable=SC2086
|
|
2718
|
-
pytest $PY_XDIST_ARGS $PY_COV_SOURCE --tb=short $PY_TARGETS
|
|
2828
|
+
pytest $PY_XDIST_ARGS $PY_COV_SOURCE --cov-fail-under=0 --tb=short $PY_TARGETS
|
|
2719
2829
|
COV_EXIT=$((COV_EXIT || $?))
|
|
2720
2830
|
fi
|
|
2721
2831
|
fi
|
|
@@ -2731,7 +2841,7 @@ else
|
|
|
2731
2841
|
go test -coverprofile=coverage.out ./... 2>/dev/null
|
|
2732
2842
|
TOTAL_COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total:" | awk '{print substr($3, 1, length($3)-1)}')
|
|
2733
2843
|
if [ -n "$TOTAL_COVERAGE" ] && (( $(echo "$TOTAL_COVERAGE < 80" | bc -l 2>/dev/null || echo "0") )); then
|
|
2734
|
-
echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below
|
|
2844
|
+
echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2735
2845
|
exit 1
|
|
2736
2846
|
fi
|
|
2737
2847
|
echo "Go coverage: $TOTAL_COVERAGE%"
|
|
@@ -2789,14 +2899,14 @@ else
|
|
|
2789
2899
|
} catch(e) { console.log('parse_error'); }
|
|
2790
2900
|
" 2>/dev/null)
|
|
2791
2901
|
if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
|
|
2792
|
-
if [ "$COVERAGE_PERCENT" -lt
|
|
2902
|
+
if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
|
|
2793
2903
|
# Only a subset test run (smart selection) leaves partial coverage data;
|
|
2794
2904
|
# a full suite run must block on <80% even when many test files changed.
|
|
2795
2905
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2796
2906
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2797
2907
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
2798
2908
|
else
|
|
2799
|
-
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below
|
|
2909
|
+
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2800
2910
|
exit 1
|
|
2801
2911
|
fi
|
|
2802
2912
|
else
|
|
@@ -2860,7 +2970,7 @@ else
|
|
|
2860
2970
|
echo "⚠️ Partial coverage ${COVERAGE_ROUNDED}% (subset test run, not full suite)."
|
|
2861
2971
|
echo " Run the full test suite for a complete Java coverage check."
|
|
2862
2972
|
else
|
|
2863
|
-
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below
|
|
2973
|
+
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2864
2974
|
exit 1
|
|
2865
2975
|
fi
|
|
2866
2976
|
else
|
|
@@ -2886,8 +2996,8 @@ else
|
|
|
2886
2996
|
if [ -f "coverage/lcov.info" ]; then
|
|
2887
2997
|
COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT="0"
|
|
2888
2998
|
if [ "$COVERAGE_PERCENT" != "0" ] && [ -n "$COVERAGE_PERCENT" ]; then
|
|
2889
|
-
if [ "$COVERAGE_PERCENT" -lt
|
|
2890
|
-
echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below
|
|
2999
|
+
if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
|
|
3000
|
+
echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2891
3001
|
exit 1
|
|
2892
3002
|
fi
|
|
2893
3003
|
echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
@@ -2926,7 +3036,7 @@ else
|
|
|
2926
3036
|
COVERAGE_STATUS=${COVERAGE_RESULT%%|*}
|
|
2927
3037
|
COVERAGE_PERCENT=${COVERAGE_RESULT#*|}
|
|
2928
3038
|
if [ "$COVERAGE_STATUS" = "below" ]; then
|
|
2929
|
-
echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below
|
|
3039
|
+
echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2930
3040
|
exit 1
|
|
2931
3041
|
fi
|
|
2932
3042
|
echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
@@ -2963,14 +3073,14 @@ else
|
|
|
2963
3073
|
fi
|
|
2964
3074
|
fi
|
|
2965
3075
|
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
|
|
2966
|
-
if [ "$COVERAGE_PERCENT" -lt
|
|
3076
|
+
if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
|
|
2967
3077
|
# Only a subset test run (smart selection) warns — the full suite may
|
|
2968
3078
|
# still reach ≥80%; a full suite run must block.
|
|
2969
3079
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2970
3080
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2971
3081
|
echo " Run the full test suite for a complete ${CURRENT_LANG} coverage check."
|
|
2972
3082
|
else
|
|
2973
|
-
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below
|
|
3083
|
+
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2974
3084
|
exit 1
|
|
2975
3085
|
fi
|
|
2976
3086
|
else
|
|
@@ -3019,10 +3129,15 @@ else
|
|
|
3019
3129
|
}
|
|
3020
3130
|
} catch(e) { console.log('parse_error'); }
|
|
3021
3131
|
" 2>/dev/null)
|
|
3132
|
+
# NOTE (#555): the 80 below is deliberately NOT $COVERAGE_THRESHOLD.
|
|
3133
|
+
# That one is the project-level floor a repo declares in fail_under;
|
|
3134
|
+
# this is the bar a single NEW file must clear to justify existing at
|
|
3135
|
+
# all. Tying them would mean a repo with fail_under=82 could not add a
|
|
3136
|
+
# file at 81% even while the project sits well above its floor.
|
|
3022
3137
|
if [ "$FILE_COV" = "no_data" ] || [ "$FILE_COV" = "parse_error" ]; then
|
|
3023
3138
|
echo " ⚠️ $new_file — no coverage data (file may be excluded or not exercised)"
|
|
3024
3139
|
elif [ "$FILE_COV" -lt 80 ]; then
|
|
3025
|
-
echo " ❌ $new_file — ${FILE_COV}% coverage (below 80% threshold)"
|
|
3140
|
+
echo " ❌ $new_file — ${FILE_COV}% coverage (below 80% per-new-file threshold)"
|
|
3026
3141
|
NEW_FILE_COV_FAIL=true
|
|
3027
3142
|
else
|
|
3028
3143
|
echo " ✅ $new_file — ${FILE_COV}% coverage"
|
|
@@ -3036,8 +3151,13 @@ else
|
|
|
3036
3151
|
fi
|
|
3037
3152
|
fi
|
|
3038
3153
|
fi
|
|
3154
|
+
# Only reached on the non-docs-only path. Assigning PASS outside the if/else
|
|
3155
|
+
# overwrote the docs-only SKIP a few hundred lines above and reported
|
|
3156
|
+
# "Tests + Coverage: PASS (coverage: N/A)" for a commit where no test ran
|
|
3157
|
+
# (#554) -- a SKIP promoted to PASS inflates the pass rate and hides that the
|
|
3158
|
+
# gate never executed.
|
|
3159
|
+
GATE_5_STATUS="PASS"
|
|
3039
3160
|
fi
|
|
3040
|
-
GATE_5_STATUS="PASS"
|
|
3041
3161
|
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START" "${GATE_5_DETAIL:-}"
|
|
3042
3162
|
|
|
3043
3163
|
# ============================================================================
|
|
@@ -3582,7 +3702,7 @@ echo ""
|
|
|
3582
3702
|
# v0.14.3+ (Issue #312): Skip if no code files changed
|
|
3583
3703
|
# v0.18.5 (Issue #397): Fix gate status variable collision — SAST now sets GATE_10_STATUS
|
|
3584
3704
|
# ============================================================================
|
|
3585
|
-
if any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|dart|sh)$"; then
|
|
3705
|
+
if any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|dart|sh|ps1|psm1)$"; then
|
|
3586
3706
|
if [ -f "$GATE_DIR/gate-10.sh" ]; then
|
|
3587
3707
|
source "$GATE_DIR/gate-10.sh"
|
|
3588
3708
|
else
|
|
@@ -3885,7 +4005,8 @@ generate_quality_report() {
|
|
|
3885
4005
|
"gate5_tests": {
|
|
3886
4006
|
"name": "Tests + Coverage",
|
|
3887
4007
|
"status": "${GATE_5_STATUS:-PASS}",
|
|
3888
|
-
"thresholds": { "lines":
|
|
4008
|
+
"thresholds": { "lines": ${COVERAGE_THRESHOLD}, "functions": null, "branches": null, "statements": null },
|
|
4009
|
+
"threshold_source": "${COVERAGE_THRESHOLD_SOURCE}",
|
|
3889
4010
|
"actual": { "coverage": "${COV_PCT}" },
|
|
3890
4011
|
"failedTests": $FAILED_TESTS_JSON
|
|
3891
4012
|
},
|
package/hooks/pre-push
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# Line-ending self-check (#546). Must stay on the first executable line: if this
|
|
3
|
+
# file is ever rewritten with CRLF, bash needs to reach this check before the
|
|
4
|
+
# parser chokes somewhere far away and blames an innocent line. `tr` also
|
|
5
|
+
# strips the \r from this very line, so the check runs even when everything
|
|
6
|
+
# around it is broken.
|
|
7
|
+
if tr -d '\r' < "$0" | sed -n '2p' | tr -d '\r' | grep -q 'Line-ending self-check'; then
|
|
8
|
+
if [ "$(tr -cd '\r' < "$0" | wc -c | tr -d '[:space:]')" -gt 0 ]; then
|
|
9
|
+
echo "======================================================================" >&2
|
|
10
|
+
echo "FATAL: hook has CRLF line endings — bash cannot parse it (#546)." >&2
|
|
11
|
+
echo " file: $0" >&2
|
|
12
|
+
echo " fix : tr -d '\\r' < \"$0\" > \"$0.tmp\" && mv \"$0.tmp\" \"$0\" && chmod +x \"$0\"" >&2
|
|
13
|
+
echo "Or re-sync the installed copy: xp-gate doctor --sync-hooks" >&2
|
|
14
|
+
echo "======================================================================" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
2
18
|
# Pre-push Hook — 8 gates: Gate 10 (Build Integrity) + M/M-Python/M-Go/M-Java/M-Kotlin (Mutation)
|
|
3
19
|
# + M2 (Mock Density, WARNING) + ML (Mock Layering) + UI (UI Sprint Gates) + MW (Code Walkthrough) + S (Sprint Flow)
|
|
4
20
|
#
|
|
@@ -220,6 +236,10 @@ if [ -f "$MP_ROOT/scripts/check-hook-mirror.sh" ]; then
|
|
|
220
236
|
fi
|
|
221
237
|
else
|
|
222
238
|
echo "⚠️ Gate MP: check-hook-mirror.sh not found. SKIP — Gate MP."
|
|
239
|
+
echo " Gate MP did not run; this push was not checked for mirror drift."
|
|
240
|
+
GATE_MP_STATUS="SKIP"
|
|
241
|
+
GATE_MP_SKIP_REASON="mirror_script_not_found"
|
|
242
|
+
GATES_SKIPPED="${GATES_SKIPPED:-}Gate MP (mirror parity: script not found)|"
|
|
223
243
|
MP_DURATION_MS=0
|
|
224
244
|
fi
|
|
225
245
|
echo ""
|
|
@@ -491,6 +511,13 @@ PY_SOURCE_FILES=$(echo "$PY_FILES" | grep -v '/venv/' | grep -v '/\.venv/' || tr
|
|
|
491
511
|
|
|
492
512
|
if [ -z "$PY_SOURCE_FILES" ]; then
|
|
493
513
|
echo "📚 No changed Python source files. SKIP — Gate M (Python)."
|
|
514
|
+
# #557: this outermost branch was the only Gate M (Python) path that assigned
|
|
515
|
+
# no status, while Gate M (TypeScript) records a reason on every path. Without
|
|
516
|
+
# it, "no Python in this push" and "the tool is missing" look identical in the
|
|
517
|
+
# journals, which is the gap #404 P0 spent effort closing for the TS gate.
|
|
518
|
+
GATE_M_PY_STATUS="SKIP"
|
|
519
|
+
GATE_M_PY_SKIP_REASON="no_python_source_files"
|
|
520
|
+
GATES_SKIPPED="${GATES_SKIPPED:-}Gate M Python (no changed Python source files)|"
|
|
494
521
|
else
|
|
495
522
|
PYTHON_FILE_LIST=$(echo "$PY_SOURCE_FILES" | tr '\n' ',' | sed 's/,$//')
|
|
496
523
|
echo "🐍 Changed Python files: $PYTHON_FILE_LIST"
|
|
@@ -504,6 +531,7 @@ else
|
|
|
504
531
|
case $MUTATION_EXIT in
|
|
505
532
|
0)
|
|
506
533
|
echo "✅ Gate M (Python): PASS"
|
|
534
|
+
GATE_M_PY_STATUS="PASS"
|
|
507
535
|
;;
|
|
508
536
|
1)
|
|
509
537
|
echo ""
|
|
@@ -519,16 +547,28 @@ else
|
|
|
519
547
|
124)
|
|
520
548
|
cat "$MUTATION_OUTPUT"
|
|
521
549
|
echo "⏱ Gate M (Python): TIMEOUT (120s). Allowing push with warning."
|
|
550
|
+
# A timeout means the mutation score was never measured. Saying
|
|
551
|
+
# nothing here let the closing banner claim success for a gate that
|
|
552
|
+
# started work and could not finish (#557).
|
|
553
|
+
GATE_M_PY_STATUS="TIMEOUT"
|
|
554
|
+
GATE_M_PY_SKIP_REASON="timeout_120s"
|
|
555
|
+
GATES_SKIPPED="${GATES_SKIPPED:-}Gate M Python (TIMEOUT — score never measured)|"
|
|
522
556
|
;;
|
|
523
557
|
*)
|
|
524
558
|
cat "$MUTATION_OUTPUT"
|
|
525
559
|
echo "⚠ Gate M (Python): Unexpected exit code $MUTATION_EXIT. Allowing push with warning."
|
|
560
|
+
GATE_M_PY_STATUS="ERROR"
|
|
561
|
+
GATE_M_PY_SKIP_REASON="unexpected_exit_${MUTATION_EXIT}"
|
|
562
|
+
GATES_SKIPPED="${GATES_SKIPPED:-}Gate M Python (error, exit ${MUTATION_EXIT} — score never measured)|"
|
|
526
563
|
;;
|
|
527
564
|
esac
|
|
528
565
|
|
|
529
566
|
rm -f "$MUTATION_OUTPUT"
|
|
530
567
|
else
|
|
531
568
|
echo "⚠ Gate M script not found. SKIP — Gate M (Python)."
|
|
569
|
+
GATE_M_PY_STATUS="SKIP"
|
|
570
|
+
GATE_M_PY_SKIP_REASON="gate_m_script_not_found"
|
|
571
|
+
GATES_SKIPPED="${GATES_SKIPPED:-}Gate M Python (gate-m.ts not found)|"
|
|
532
572
|
fi
|
|
533
573
|
else
|
|
534
574
|
echo "⚠ mutmut not installed. SKIP — Gate M (Python)."
|
|
@@ -929,11 +969,51 @@ if [[ "$XP_GATE_SKIP_UI_GATES" == "1" ]]; then
|
|
|
929
969
|
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\"}"
|
|
930
970
|
echo "$AUDIT_ENTRY" >> .audit-log.jsonl
|
|
931
971
|
else
|
|
932
|
-
# Detect UI changes in pushed files
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
972
|
+
# Detect UI changes in pushed files.
|
|
973
|
+
#
|
|
974
|
+
# stderr is kept out of the captured value but its ABSENCE is no longer
|
|
975
|
+
# treated as a clean "no UI here" answer (#553). jq -r prints the empty
|
|
976
|
+
# string and exits 0 on empty input, so the old `|| echo "false"` fallback
|
|
977
|
+
# was unreachable; an empty verdict became "", which is not "true", which
|
|
978
|
+
# silently took the "No UI changes detected — skipping" branch. A push whose
|
|
979
|
+
# only change was src/ui/streamlit_app.py was therefore told there was
|
|
980
|
+
# nothing to review — the failure Gate UI exists to prevent, inverted.
|
|
981
|
+
UI_DETECTION=$(echo "$PUSHED_FILES" | npx -y tsx src/npm-package/lib/ui-detector.ts --push-mode --from-stdin 2>/dev/null)
|
|
982
|
+
UI_DETECT_EXIT=$?
|
|
983
|
+
|
|
984
|
+
if [ "$UI_DETECT_EXIT" -eq 0 ] && [ -n "$UI_DETECTION" ]; then
|
|
985
|
+
IS_UI_SPRINT=$(echo "$UI_DETECTION" | jq -r 'if type == "object" then (.isUiSprint | tostring) else "unknown" end' 2>/dev/null)
|
|
986
|
+
else
|
|
987
|
+
IS_UI_SPRINT=""
|
|
988
|
+
fi
|
|
989
|
+
# Three answers, not two. "" (detector missing, crashed, or emitted nothing)
|
|
990
|
+
# is "unknown", and unknown must never read as "no UI".
|
|
991
|
+
if [ -z "$IS_UI_SPRINT" ] || [ "$IS_UI_SPRINT" = "null" ]; then
|
|
992
|
+
IS_UI_SPRINT="unknown"
|
|
993
|
+
fi
|
|
994
|
+
|
|
995
|
+
if [ "$IS_UI_SPRINT" = "unknown" ]; then
|
|
996
|
+
# A missing detector must not disable UI review. Fall back to a path
|
|
997
|
+
# check over the same file list: a cheap, conservative substitute for a
|
|
998
|
+
# verdict we do not have.
|
|
999
|
+
UI_PATH_HITS=$(echo "$PUSHED_FILES" | grep -Ei '(^|/)(src/ui|ui|components|pages|views|screens)/|[.](tsx|jsx|vue|svelte|css|scss|sass|less)$' || true)
|
|
1000
|
+
if [ -n "$UI_PATH_HITS" ]; then
|
|
1001
|
+
echo ""
|
|
1002
|
+
echo "❌ Gate UI: could not evaluate the UI verdict (detector unavailable or produced no verdict, exit ${UI_DETECT_EXIT})."
|
|
1003
|
+
echo " This push touches UI-looking paths:"
|
|
1004
|
+
echo "$UI_PATH_HITS" | sed 's/^/ /'
|
|
1005
|
+
echo ""
|
|
1006
|
+
echo " The gate cannot claim there is nothing to review when it never got an answer."
|
|
1007
|
+
echo " Fix: install the detector (npx -y tsx must be able to load src/npm-package/lib/ui-detector.ts),"
|
|
1008
|
+
echo " or run the review explicitly: xp-gate ui-review"
|
|
1009
|
+
echo " To accept the risk for this push: XP_GATE_SKIP_UI_GATES=1 XP_GATE_BYPASS_REASON=\"<why>\""
|
|
1010
|
+
echo ""
|
|
1011
|
+
exit 1
|
|
1012
|
+
fi
|
|
1013
|
+
echo "ℹ️ Gate UI: detector unavailable (exit ${UI_DETECT_EXIT}) and no UI-looking paths in this push — nothing to review."
|
|
1014
|
+
IS_UI_SPRINT="false"
|
|
1015
|
+
fi
|
|
1016
|
+
|
|
937
1017
|
if [[ "$IS_UI_SPRINT" == "true" ]]; then
|
|
938
1018
|
UI_RESULT_FILE=".ui-gate-result.json"
|
|
939
1019
|
MISSING_RESULTS=()
|
|
@@ -1067,10 +1147,21 @@ fi
|
|
|
1067
1147
|
fi
|
|
1068
1148
|
|
|
1069
1149
|
# ============================================================================
|
|
1070
|
-
#
|
|
1150
|
+
# Closing banner. States what actually happened (#557): a gate that skipped is
|
|
1151
|
+
# named. The heading used to read "ALL CHECKS PASSED" unconditionally while Gate
|
|
1152
|
+
# MP and Gate M (Python) skipped on a typical push without recording why, so a
|
|
1153
|
+
# reviewer skimming the log had no signal that mutation testing never ran.
|
|
1071
1154
|
# ============================================================================
|
|
1072
1155
|
echo ""
|
|
1073
1156
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1157
|
+
# Trim the trailing separator and turn it into a displayable list.
|
|
1158
|
+
SKIPPED_DISPLAY=$(printf '%s' "${GATES_SKIPPED:-}" | sed 's/|$//' | sed 's/|/ , /g')
|
|
1159
|
+
if [ -n "$SKIPPED_DISPLAY" ]; then
|
|
1160
|
+
echo " ⚠️ NO BLOCKING FAILURES — but not every gate ran:"
|
|
1161
|
+
echo "$SKIPPED_DISPLAY" | tr ',' '\n' | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*/ - /'
|
|
1162
|
+
else
|
|
1163
|
+
echo " ✅ ALL CHECKS PASSED"
|
|
1164
|
+
fi
|
|
1074
1165
|
# Say what actually happened. Printing "VERIFIED" on a skipped gate would be a
|
|
1075
1166
|
# claim the hook cannot support — the same dishonesty (evidence asserting an
|
|
1076
1167
|
# unperformed check) that Gate MW exists to prevent.
|