@boyingliu01/xp-gate 0.31.2 → 0.31.4
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-3.sh +244 -9
- package/hooks/gate-3.sh +244 -9
- package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
- package/hooks/pre-commit +152 -13
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/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/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/typescript.ts +503 -174
- package/principles/baseline.ts +58 -70
- 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-3.sh
CHANGED
|
@@ -20,15 +20,250 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
20
20
|
GATE_3_REASON="documentation_project"
|
|
21
21
|
|
|
22
22
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
# #537/#598: this arm used to be a hard SKIP with "no PowerShell tool". That
|
|
24
|
+
# was true of lizard (it cannot parse .ps1) but NOT of the platform:
|
|
25
|
+
# PowerShell's own AST gives an exact decision-point count, which is the same
|
|
26
|
+
# quantity lizard measures for other languages. A PowerShell-primary repo
|
|
27
|
+
# therefore had a permanently unmeasured quality dimension while the score
|
|
28
|
+
# implied full coverage.
|
|
29
|
+
#
|
|
30
|
+
# The measurement lives in lib/Measure-PowerShellComplexity.ps1 because the
|
|
31
|
+
# hook cannot know a project's refactor history; the adapter only runs it and
|
|
32
|
+
# translates the verdict.
|
|
33
|
+
#
|
|
34
|
+
# The threshold resolution is duplicated from the lizard arm on purpose: the
|
|
35
|
+
# arms are read independently of each other, and a shared helper here would
|
|
36
|
+
# make the PowerShell path depend on the code path it replaces.
|
|
37
|
+
_gate3_ps_threshold() {
|
|
38
|
+
local default=5 val
|
|
39
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
40
|
+
val=$(sed -n '1s/[[:space:]]*$//p' ".xp-gate/ccn-threshold" 2>/dev/null || true)
|
|
41
|
+
case "$val" in
|
|
42
|
+
''|*[!0-9]*) echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2 ;;
|
|
43
|
+
0) echo "WARN - invalid .xp-gate/ccn-threshold value '0' (expected a positive integer), falling back to ${default}" >&2 ;;
|
|
44
|
+
*) echo "$val"; return 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
fi
|
|
47
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
48
|
+
case "$val" in
|
|
49
|
+
'') echo "$default" ;;
|
|
50
|
+
*[!0-9]*) echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2; echo "$default" ;;
|
|
51
|
+
0) echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '0' (expected a positive integer), falling back to ${default}" >&2; echo "$default" ;;
|
|
52
|
+
*) echo "$val" ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Resolve a PowerShell engine LOCALLY.
|
|
57
|
+
#
|
|
58
|
+
# This arm must NOT call _detect_pwsh, which is defined in
|
|
59
|
+
# adapters/powershell.sh: the hook sources that adapter inside a SUBSHELL for
|
|
60
|
+
# Gate 1 --
|
|
61
|
+
# PS_G1_OUTPUT=$(source "$POWERSHELL_ADAPTER"; run_static_analysis)
|
|
62
|
+
# -- so its functions never reach this gate. Depending on it printed
|
|
63
|
+
# "no PowerShell engine found" on a machine with both pwsh 7 AND PS 5.1
|
|
64
|
+
# installed, i.e. the verdict was about bash scoping, not about the engine.
|
|
65
|
+
# A gate must not depend on another gate having sourced a library first.
|
|
66
|
+
_gate3_detect_pwsh() {
|
|
67
|
+
if command -v pwsh >/dev/null 2>&1; then echo pwsh; return 0; fi
|
|
68
|
+
if command -v powershell.exe >/dev/null 2>&1; then echo powershell.exe; return 0; fi
|
|
69
|
+
local _c
|
|
70
|
+
for _c in \
|
|
71
|
+
"${LOCALAPPDATA:-$HOME/AppData/Local}/Microsoft/WindowsApps/pwsh.exe" \
|
|
72
|
+
"/c/Program Files/PowerShell/7/pwsh.exe" \
|
|
73
|
+
"/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; do
|
|
74
|
+
[ -x "$_c" ] && { echo "$_c"; return 0; }
|
|
75
|
+
done
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
PS_CC_SCRIPT="${AUDIT_SCRIPT_DIR}/lib/Measure-PowerShellComplexity.ps1"
|
|
80
|
+
if [ ! -f "$PS_CC_SCRIPT" ]; then
|
|
81
|
+
echo "ℹ️ No PowerShell complexity tool available"
|
|
82
|
+
echo " (expected at lib/Measure-PowerShellComplexity.ps1)"
|
|
83
|
+
echo "⏭️ SKIPPED - Complexity (no PowerShell tool)"
|
|
84
|
+
GATE_3_STATUS="SKIP"
|
|
85
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
86
|
+
# shellcheck disable=SC2034
|
|
87
|
+
GATE_3_REASON="no_powershell_tool"
|
|
88
|
+
else
|
|
89
|
+
# #581: pwsh.exe is a NATIVE binary and cannot open the POSIX path this hook
|
|
90
|
+
# holds (`/c/Users/...` or `/tmp/...`). Handed one unchanged it resolves the
|
|
91
|
+
# leading `/` to `<current drive>:\`, silently measures zero files, and the
|
|
92
|
+
# gate then reports "no functions" -- an environment fault reported as a
|
|
93
|
+
# verdict about the repository. Convert the script path once, here.
|
|
94
|
+
PS_CC_SCRIPT_NATIVE="$PS_CC_SCRIPT"
|
|
95
|
+
if command -v to_native_path >/dev/null 2>&1; then
|
|
96
|
+
PS_CC_SCRIPT_NATIVE=$(to_native_path "$PS_CC_SCRIPT" 2>/dev/null || echo "$PS_CC_SCRIPT")
|
|
97
|
+
fi
|
|
98
|
+
PS_CC_SCRIPT="$PS_CC_SCRIPT_NATIVE"
|
|
99
|
+
PS_CC_PWSH=$(_gate3_detect_pwsh 2>/dev/null || true)
|
|
100
|
+
if [ -z "$PS_CC_PWSH" ]; then
|
|
101
|
+
echo "⚠️ WARN - no PowerShell engine found; complexity not measured"
|
|
102
|
+
GATE_3_STATUS="WARN"
|
|
103
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
104
|
+
# shellcheck disable=SC2034
|
|
105
|
+
GATE_3_REASON="no_powershell_engine"
|
|
106
|
+
else
|
|
107
|
+
PS_CC_THRESHOLD=$(_gate3_ps_threshold)
|
|
108
|
+
PS_ADDED=$(git diff --cached --name-only --diff-filter=A 2>/dev/null | grep -E '\.ps(m)?1$' || true)
|
|
109
|
+
PS_BASELINE=".xp-gate-powershell-ccn-baseline.json"
|
|
110
|
+
|
|
111
|
+
if [ -z "$PS_ADDED" ]; then
|
|
112
|
+
# No added files, but modified ones can still get MORE complex. That is
|
|
113
|
+
# what the ratchet baseline is for; without a baseline there is nothing
|
|
114
|
+
# to compare, and silently passing would be the #584 failure mode.
|
|
115
|
+
PS_TOUCHED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.ps(m)?1$' || true)
|
|
116
|
+
PS_TOUCHED=$(printf '%s\n' "$PS_TOUCHED" | grep -vE '(^|/)tests/' || true)
|
|
117
|
+
if [ -z "$PS_TOUCHED" ] || [ ! -f "$PS_BASELINE" ]; then
|
|
118
|
+
echo "⏭️ SKIPPED - Complexity (no added PowerShell files to measure)."
|
|
119
|
+
GATE_3_STATUS="SKIP"
|
|
120
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
121
|
+
# shellcheck disable=SC2034
|
|
122
|
+
GATE_3_REASON="no_added_powershell_files"
|
|
123
|
+
else
|
|
124
|
+
PS_CC_ARGS=()
|
|
125
|
+
for _f in $PS_TOUCHED; do PS_CC_ARGS[${#PS_CC_ARGS[@]}]="'$_f'"; done
|
|
126
|
+
|
|
127
|
+
# #598: the ratchet branch must use the SAME threshold as the baseline
|
|
128
|
+
# collector. -Threshold 1 used to make every function with CCN >= 2
|
|
129
|
+
# print OVER, so a brand-new well-factored function (CCN 2-4, below the
|
|
130
|
+
# real threshold) was diffed against a baseline that can never contain
|
|
131
|
+
# it and reported as a "regression": the gate blocked exactly the shape
|
|
132
|
+
# it exists to encourage. -List additionally emits the full population,
|
|
133
|
+
# so the comparison covers functions that are below the threshold on
|
|
134
|
+
# both sides.
|
|
135
|
+
PS_CC_OUT=$("$PS_CC_PWSH" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
|
136
|
+
-Command "& '$PS_CC_SCRIPT' -Path @($(IFS=','; echo "${PS_CC_ARGS[*]}")) -Threshold ${PS_CC_THRESHOLD} -List" 2>&1 || true)
|
|
137
|
+
|
|
138
|
+
PS_CC_MEASURED=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^functions_measured=//p' | tail -1)
|
|
139
|
+
if [ -z "$PS_CC_MEASURED" ] || [ "${PS_CC_MEASURED:-0}" -eq 0 ]; then
|
|
140
|
+
echo "⏭️ SKIPPED - Complexity (no functions in changed PowerShell files)."
|
|
141
|
+
GATE_3_STATUS="SKIP"
|
|
142
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
143
|
+
# shellcheck disable=SC2034
|
|
144
|
+
GATE_3_REASON="no_functions_found"
|
|
145
|
+
else
|
|
146
|
+
PS_CC_NODE=""
|
|
147
|
+
if command -v node >/dev/null 2>&1; then PS_CC_NODE="node"; fi
|
|
148
|
+
if [ -z "$PS_CC_NODE" ]; then
|
|
149
|
+
echo "ℹ️ node unavailable — complexity measured (${PS_CC_MEASURED} function(s)) but ratchet not compared"
|
|
150
|
+
echo "✅ PASSED - PowerShell complexity measured (ratchet unavailable)."
|
|
151
|
+
GATE_3_DETAIL="mode=measured_no_ratchet"
|
|
152
|
+
else
|
|
153
|
+
# Compare every measured function against the baseline. A function
|
|
154
|
+
# absent from the baseline is NEW: it is only a regression if it is
|
|
155
|
+
# above the threshold, which is exactly what the old -Threshold 1
|
|
156
|
+
# comparison got wrong. An unreadable baseline is ERR, not a pass.
|
|
157
|
+
PS_CC_VERDICT=$(printf '%s\n' "$PS_CC_OUT" | "$PS_CC_NODE" -e '
|
|
158
|
+
let raw = "";
|
|
159
|
+
process.stdin.on("data", (c) => { raw += c; });
|
|
160
|
+
process.stdin.on("end", () => {
|
|
161
|
+
const fs = require("fs");
|
|
162
|
+
const fsPath = process.argv[1];
|
|
163
|
+
const threshold = Number(process.argv[2]);
|
|
164
|
+
let baseline;
|
|
165
|
+
try {
|
|
166
|
+
baseline = JSON.parse(fs.readFileSync(fsPath, "utf8"));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log("ERR " + e.message);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const known = new Map();
|
|
172
|
+
const entries = baseline.functions || baseline.entries || baseline;
|
|
173
|
+
if (Array.isArray(entries)) {
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (!e || !e.file || !e.name) continue;
|
|
176
|
+
known.set(norm(e.file) + "::" + e.name, Number(e.ccn) || 0);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function norm(p) { return String(p).replace(/\\\\/g, "/").replace(/^\.\//, ""); }
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const regressions = [];
|
|
182
|
+
const improved = [];
|
|
183
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
184
|
+
const m = line.match(/^(?:OVER|FUNC) (.+):(\d+) (\S+) ccn=(\d+)$/);
|
|
185
|
+
if (!m) continue;
|
|
186
|
+
const key = norm(m[1]) + "::" + m[3];
|
|
187
|
+
const now = Number(m[4]);
|
|
188
|
+
seen.add(key);
|
|
189
|
+
const was = known.has(key) ? known.get(key) : null;
|
|
190
|
+
if (was === null) {
|
|
191
|
+
if (now > threshold) {
|
|
192
|
+
regressions.push(m[1] + ":" + m[2] + " " + m[3] + " new function at ccn=" + now);
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (now > was) {
|
|
197
|
+
regressions.push(m[1] + ":" + m[2] + " " + m[3] + " ccn " + was + " -> " + now);
|
|
198
|
+
} else if (now < was) {
|
|
199
|
+
improved.push(m[1] + ":" + m[2] + " " + m[3] + " ccn " + was + " -> " + now);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const r of regressions) console.log("REGRESSION " + r);
|
|
203
|
+
for (const i of improved) console.log("IMPROVED " + i);
|
|
204
|
+
if (regressions.length > 0) {
|
|
205
|
+
console.log("FAIL " + regressions.length + " function(s) increased cyclomatic complexity vs " + fsPath + ".");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
' "$PS_BASELINE" "$PS_CC_THRESHOLD" 2>&1 || true)
|
|
209
|
+
|
|
210
|
+
if printf '%s\n' "$PS_CC_VERDICT" | grep -q '^FAIL '; then
|
|
211
|
+
PS_CC_REG=$(printf '%s\n' "$PS_CC_VERDICT" | sed -n 's/^FAIL //p' | tail -1)
|
|
212
|
+
printf '%s\n' "$PS_CC_VERDICT" | grep '^REGRESSION ' | sed 's/^REGRESSION / ❌ /'
|
|
213
|
+
echo "❌ BLOCKED - ${PS_CC_REG}"
|
|
214
|
+
GATE_3_STATUS="BLOCK"
|
|
215
|
+
GATE_3_REASON="ccn_ratchet_regression"
|
|
216
|
+
exit 1
|
|
217
|
+
elif printf '%s\n' "$PS_CC_VERDICT" | grep -q '^ERR '; then
|
|
218
|
+
echo "⚠️ WARN - complexity ratchet baseline unreadable: $(printf '%s\n' "$PS_CC_VERDICT" | sed -n 's/^ERR //p' | head -1)"
|
|
219
|
+
GATE_3_STATUS="WARN"
|
|
220
|
+
GATE_3_REASON="baseline_unreadable"
|
|
221
|
+
else
|
|
222
|
+
PS_CC_IMPROVED=$(printf '%s\n' "$PS_CC_VERDICT" | grep -c '^IMPROVED ' || true)
|
|
223
|
+
printf '%s\n' "$PS_CC_VERDICT" | grep '^IMPROVED ' | sed 's/^IMPROVED / 📉 /'
|
|
224
|
+
echo "✅ PASSED - PowerShell complexity within ratchet (${PS_CC_MEASURED} function(s), ${PS_CC_IMPROVED} improved, threshold ${PS_CC_THRESHOLD})."
|
|
225
|
+
GATE_3_STATUS="PASS"
|
|
226
|
+
GATE_3_DETAIL="powerShell_ratchet improved=${PS_CC_IMPROVED}"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
else
|
|
232
|
+
# Freshly added files: this arm has no history to compare, so it judges
|
|
233
|
+
# each file's functions against the threshold. A file that is merely
|
|
234
|
+
# being touched keeps its history (that is what the baseline is for);
|
|
235
|
+
# measuring only ADDED files means the arm can never retroactively block
|
|
236
|
+
# a cleanup commit.
|
|
237
|
+
PS_CC_ARGS=()
|
|
238
|
+
for _f in $PS_ADDED; do PS_CC_ARGS[${#PS_CC_ARGS[@]}]="'$_f'"; done
|
|
239
|
+
PS_CC_OUT=$("$PS_CC_PWSH" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
|
240
|
+
-Command "& '$PS_CC_SCRIPT' -Path @($(IFS=','; echo "${PS_CC_ARGS[*]}")) -Threshold ${PS_CC_THRESHOLD}" 2>&1 || true)
|
|
241
|
+
PS_CC_MEASURED=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^functions_measured=//p' | tail -1)
|
|
242
|
+
PS_CC_OVER=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^over_threshold=//p' | tail -1)
|
|
243
|
+
PS_CC_MAX=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^max_ccn=//p' | tail -1)
|
|
244
|
+
if [ -z "$PS_CC_MEASURED" ] || [ "${PS_CC_MEASURED:-0}" -eq 0 ]; then
|
|
245
|
+
echo "⏭️ SKIPPED - Complexity (no functions in added PowerShell files)."
|
|
246
|
+
GATE_3_STATUS="SKIP"
|
|
247
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
248
|
+
# shellcheck disable=SC2034
|
|
249
|
+
GATE_3_REASON="no_functions_found"
|
|
250
|
+
elif [ "${PS_CC_OVER:-0}" -gt 0 ]; then
|
|
251
|
+
echo " Measured ${PS_CC_MEASURED} function(s) via AST; max CCN ${PS_CC_MAX} (threshold ${PS_CC_THRESHOLD})"
|
|
252
|
+
printf '%s\n' "$PS_CC_OUT" | grep '^OVER ' | sed 's/^/ /'
|
|
253
|
+
echo "❌ BLOCKED - ${PS_CC_OVER} newly added function(s) with CCN > ${PS_CC_THRESHOLD}."
|
|
254
|
+
echo "Refactor high-complexity functions, or raise the threshold deliberately."
|
|
255
|
+
GATE_3_STATUS="BLOCK"
|
|
256
|
+
GATE_3_REASON="ccn_over_threshold"
|
|
257
|
+
exit 1
|
|
258
|
+
else
|
|
259
|
+
echo "✅ PASSED - All added PowerShell functions within complexity threshold (${PS_CC_THRESHOLD})."
|
|
260
|
+
echo " Measured ${PS_CC_MEASURED} function(s) via AST; max CCN ${PS_CC_MAX}."
|
|
261
|
+
GATE_3_STATUS="PASS"
|
|
262
|
+
fi
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
|
|
32
267
|
else
|
|
33
268
|
# S2 (#507): three-level CCN threshold override --
|
|
34
269
|
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|
package/hooks/gate-3.sh
CHANGED
|
@@ -20,15 +20,250 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
20
20
|
GATE_3_REASON="documentation_project"
|
|
21
21
|
|
|
22
22
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
# #537/#598: this arm used to be a hard SKIP with "no PowerShell tool". That
|
|
24
|
+
# was true of lizard (it cannot parse .ps1) but NOT of the platform:
|
|
25
|
+
# PowerShell's own AST gives an exact decision-point count, which is the same
|
|
26
|
+
# quantity lizard measures for other languages. A PowerShell-primary repo
|
|
27
|
+
# therefore had a permanently unmeasured quality dimension while the score
|
|
28
|
+
# implied full coverage.
|
|
29
|
+
#
|
|
30
|
+
# The measurement lives in lib/Measure-PowerShellComplexity.ps1 because the
|
|
31
|
+
# hook cannot know a project's refactor history; the adapter only runs it and
|
|
32
|
+
# translates the verdict.
|
|
33
|
+
#
|
|
34
|
+
# The threshold resolution is duplicated from the lizard arm on purpose: the
|
|
35
|
+
# arms are read independently of each other, and a shared helper here would
|
|
36
|
+
# make the PowerShell path depend on the code path it replaces.
|
|
37
|
+
_gate3_ps_threshold() {
|
|
38
|
+
local default=5 val
|
|
39
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
40
|
+
val=$(sed -n '1s/[[:space:]]*$//p' ".xp-gate/ccn-threshold" 2>/dev/null || true)
|
|
41
|
+
case "$val" in
|
|
42
|
+
''|*[!0-9]*) echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2 ;;
|
|
43
|
+
0) echo "WARN - invalid .xp-gate/ccn-threshold value '0' (expected a positive integer), falling back to ${default}" >&2 ;;
|
|
44
|
+
*) echo "$val"; return 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
fi
|
|
47
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
48
|
+
case "$val" in
|
|
49
|
+
'') echo "$default" ;;
|
|
50
|
+
*[!0-9]*) echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2; echo "$default" ;;
|
|
51
|
+
0) echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '0' (expected a positive integer), falling back to ${default}" >&2; echo "$default" ;;
|
|
52
|
+
*) echo "$val" ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Resolve a PowerShell engine LOCALLY.
|
|
57
|
+
#
|
|
58
|
+
# This arm must NOT call _detect_pwsh, which is defined in
|
|
59
|
+
# adapters/powershell.sh: the hook sources that adapter inside a SUBSHELL for
|
|
60
|
+
# Gate 1 --
|
|
61
|
+
# PS_G1_OUTPUT=$(source "$POWERSHELL_ADAPTER"; run_static_analysis)
|
|
62
|
+
# -- so its functions never reach this gate. Depending on it printed
|
|
63
|
+
# "no PowerShell engine found" on a machine with both pwsh 7 AND PS 5.1
|
|
64
|
+
# installed, i.e. the verdict was about bash scoping, not about the engine.
|
|
65
|
+
# A gate must not depend on another gate having sourced a library first.
|
|
66
|
+
_gate3_detect_pwsh() {
|
|
67
|
+
if command -v pwsh >/dev/null 2>&1; then echo pwsh; return 0; fi
|
|
68
|
+
if command -v powershell.exe >/dev/null 2>&1; then echo powershell.exe; return 0; fi
|
|
69
|
+
local _c
|
|
70
|
+
for _c in \
|
|
71
|
+
"${LOCALAPPDATA:-$HOME/AppData/Local}/Microsoft/WindowsApps/pwsh.exe" \
|
|
72
|
+
"/c/Program Files/PowerShell/7/pwsh.exe" \
|
|
73
|
+
"/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; do
|
|
74
|
+
[ -x "$_c" ] && { echo "$_c"; return 0; }
|
|
75
|
+
done
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
PS_CC_SCRIPT="${AUDIT_SCRIPT_DIR}/lib/Measure-PowerShellComplexity.ps1"
|
|
80
|
+
if [ ! -f "$PS_CC_SCRIPT" ]; then
|
|
81
|
+
echo "ℹ️ No PowerShell complexity tool available"
|
|
82
|
+
echo " (expected at lib/Measure-PowerShellComplexity.ps1)"
|
|
83
|
+
echo "⏭️ SKIPPED - Complexity (no PowerShell tool)"
|
|
84
|
+
GATE_3_STATUS="SKIP"
|
|
85
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
86
|
+
# shellcheck disable=SC2034
|
|
87
|
+
GATE_3_REASON="no_powershell_tool"
|
|
88
|
+
else
|
|
89
|
+
# #581: pwsh.exe is a NATIVE binary and cannot open the POSIX path this hook
|
|
90
|
+
# holds (`/c/Users/...` or `/tmp/...`). Handed one unchanged it resolves the
|
|
91
|
+
# leading `/` to `<current drive>:\`, silently measures zero files, and the
|
|
92
|
+
# gate then reports "no functions" -- an environment fault reported as a
|
|
93
|
+
# verdict about the repository. Convert the script path once, here.
|
|
94
|
+
PS_CC_SCRIPT_NATIVE="$PS_CC_SCRIPT"
|
|
95
|
+
if command -v to_native_path >/dev/null 2>&1; then
|
|
96
|
+
PS_CC_SCRIPT_NATIVE=$(to_native_path "$PS_CC_SCRIPT" 2>/dev/null || echo "$PS_CC_SCRIPT")
|
|
97
|
+
fi
|
|
98
|
+
PS_CC_SCRIPT="$PS_CC_SCRIPT_NATIVE"
|
|
99
|
+
PS_CC_PWSH=$(_gate3_detect_pwsh 2>/dev/null || true)
|
|
100
|
+
if [ -z "$PS_CC_PWSH" ]; then
|
|
101
|
+
echo "⚠️ WARN - no PowerShell engine found; complexity not measured"
|
|
102
|
+
GATE_3_STATUS="WARN"
|
|
103
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
104
|
+
# shellcheck disable=SC2034
|
|
105
|
+
GATE_3_REASON="no_powershell_engine"
|
|
106
|
+
else
|
|
107
|
+
PS_CC_THRESHOLD=$(_gate3_ps_threshold)
|
|
108
|
+
PS_ADDED=$(git diff --cached --name-only --diff-filter=A 2>/dev/null | grep -E '\.ps(m)?1$' || true)
|
|
109
|
+
PS_BASELINE=".xp-gate-powershell-ccn-baseline.json"
|
|
110
|
+
|
|
111
|
+
if [ -z "$PS_ADDED" ]; then
|
|
112
|
+
# No added files, but modified ones can still get MORE complex. That is
|
|
113
|
+
# what the ratchet baseline is for; without a baseline there is nothing
|
|
114
|
+
# to compare, and silently passing would be the #584 failure mode.
|
|
115
|
+
PS_TOUCHED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.ps(m)?1$' || true)
|
|
116
|
+
PS_TOUCHED=$(printf '%s\n' "$PS_TOUCHED" | grep -vE '(^|/)tests/' || true)
|
|
117
|
+
if [ -z "$PS_TOUCHED" ] || [ ! -f "$PS_BASELINE" ]; then
|
|
118
|
+
echo "⏭️ SKIPPED - Complexity (no added PowerShell files to measure)."
|
|
119
|
+
GATE_3_STATUS="SKIP"
|
|
120
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
121
|
+
# shellcheck disable=SC2034
|
|
122
|
+
GATE_3_REASON="no_added_powershell_files"
|
|
123
|
+
else
|
|
124
|
+
PS_CC_ARGS=()
|
|
125
|
+
for _f in $PS_TOUCHED; do PS_CC_ARGS[${#PS_CC_ARGS[@]}]="'$_f'"; done
|
|
126
|
+
|
|
127
|
+
# #598: the ratchet branch must use the SAME threshold as the baseline
|
|
128
|
+
# collector. -Threshold 1 used to make every function with CCN >= 2
|
|
129
|
+
# print OVER, so a brand-new well-factored function (CCN 2-4, below the
|
|
130
|
+
# real threshold) was diffed against a baseline that can never contain
|
|
131
|
+
# it and reported as a "regression": the gate blocked exactly the shape
|
|
132
|
+
# it exists to encourage. -List additionally emits the full population,
|
|
133
|
+
# so the comparison covers functions that are below the threshold on
|
|
134
|
+
# both sides.
|
|
135
|
+
PS_CC_OUT=$("$PS_CC_PWSH" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
|
136
|
+
-Command "& '$PS_CC_SCRIPT' -Path @($(IFS=','; echo "${PS_CC_ARGS[*]}")) -Threshold ${PS_CC_THRESHOLD} -List" 2>&1 || true)
|
|
137
|
+
|
|
138
|
+
PS_CC_MEASURED=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^functions_measured=//p' | tail -1)
|
|
139
|
+
if [ -z "$PS_CC_MEASURED" ] || [ "${PS_CC_MEASURED:-0}" -eq 0 ]; then
|
|
140
|
+
echo "⏭️ SKIPPED - Complexity (no functions in changed PowerShell files)."
|
|
141
|
+
GATE_3_STATUS="SKIP"
|
|
142
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
143
|
+
# shellcheck disable=SC2034
|
|
144
|
+
GATE_3_REASON="no_functions_found"
|
|
145
|
+
else
|
|
146
|
+
PS_CC_NODE=""
|
|
147
|
+
if command -v node >/dev/null 2>&1; then PS_CC_NODE="node"; fi
|
|
148
|
+
if [ -z "$PS_CC_NODE" ]; then
|
|
149
|
+
echo "ℹ️ node unavailable — complexity measured (${PS_CC_MEASURED} function(s)) but ratchet not compared"
|
|
150
|
+
echo "✅ PASSED - PowerShell complexity measured (ratchet unavailable)."
|
|
151
|
+
GATE_3_DETAIL="mode=measured_no_ratchet"
|
|
152
|
+
else
|
|
153
|
+
# Compare every measured function against the baseline. A function
|
|
154
|
+
# absent from the baseline is NEW: it is only a regression if it is
|
|
155
|
+
# above the threshold, which is exactly what the old -Threshold 1
|
|
156
|
+
# comparison got wrong. An unreadable baseline is ERR, not a pass.
|
|
157
|
+
PS_CC_VERDICT=$(printf '%s\n' "$PS_CC_OUT" | "$PS_CC_NODE" -e '
|
|
158
|
+
let raw = "";
|
|
159
|
+
process.stdin.on("data", (c) => { raw += c; });
|
|
160
|
+
process.stdin.on("end", () => {
|
|
161
|
+
const fs = require("fs");
|
|
162
|
+
const fsPath = process.argv[1];
|
|
163
|
+
const threshold = Number(process.argv[2]);
|
|
164
|
+
let baseline;
|
|
165
|
+
try {
|
|
166
|
+
baseline = JSON.parse(fs.readFileSync(fsPath, "utf8"));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log("ERR " + e.message);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const known = new Map();
|
|
172
|
+
const entries = baseline.functions || baseline.entries || baseline;
|
|
173
|
+
if (Array.isArray(entries)) {
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (!e || !e.file || !e.name) continue;
|
|
176
|
+
known.set(norm(e.file) + "::" + e.name, Number(e.ccn) || 0);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function norm(p) { return String(p).replace(/\\\\/g, "/").replace(/^\.\//, ""); }
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const regressions = [];
|
|
182
|
+
const improved = [];
|
|
183
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
184
|
+
const m = line.match(/^(?:OVER|FUNC) (.+):(\d+) (\S+) ccn=(\d+)$/);
|
|
185
|
+
if (!m) continue;
|
|
186
|
+
const key = norm(m[1]) + "::" + m[3];
|
|
187
|
+
const now = Number(m[4]);
|
|
188
|
+
seen.add(key);
|
|
189
|
+
const was = known.has(key) ? known.get(key) : null;
|
|
190
|
+
if (was === null) {
|
|
191
|
+
if (now > threshold) {
|
|
192
|
+
regressions.push(m[1] + ":" + m[2] + " " + m[3] + " new function at ccn=" + now);
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (now > was) {
|
|
197
|
+
regressions.push(m[1] + ":" + m[2] + " " + m[3] + " ccn " + was + " -> " + now);
|
|
198
|
+
} else if (now < was) {
|
|
199
|
+
improved.push(m[1] + ":" + m[2] + " " + m[3] + " ccn " + was + " -> " + now);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const r of regressions) console.log("REGRESSION " + r);
|
|
203
|
+
for (const i of improved) console.log("IMPROVED " + i);
|
|
204
|
+
if (regressions.length > 0) {
|
|
205
|
+
console.log("FAIL " + regressions.length + " function(s) increased cyclomatic complexity vs " + fsPath + ".");
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
' "$PS_BASELINE" "$PS_CC_THRESHOLD" 2>&1 || true)
|
|
209
|
+
|
|
210
|
+
if printf '%s\n' "$PS_CC_VERDICT" | grep -q '^FAIL '; then
|
|
211
|
+
PS_CC_REG=$(printf '%s\n' "$PS_CC_VERDICT" | sed -n 's/^FAIL //p' | tail -1)
|
|
212
|
+
printf '%s\n' "$PS_CC_VERDICT" | grep '^REGRESSION ' | sed 's/^REGRESSION / ❌ /'
|
|
213
|
+
echo "❌ BLOCKED - ${PS_CC_REG}"
|
|
214
|
+
GATE_3_STATUS="BLOCK"
|
|
215
|
+
GATE_3_REASON="ccn_ratchet_regression"
|
|
216
|
+
exit 1
|
|
217
|
+
elif printf '%s\n' "$PS_CC_VERDICT" | grep -q '^ERR '; then
|
|
218
|
+
echo "⚠️ WARN - complexity ratchet baseline unreadable: $(printf '%s\n' "$PS_CC_VERDICT" | sed -n 's/^ERR //p' | head -1)"
|
|
219
|
+
GATE_3_STATUS="WARN"
|
|
220
|
+
GATE_3_REASON="baseline_unreadable"
|
|
221
|
+
else
|
|
222
|
+
PS_CC_IMPROVED=$(printf '%s\n' "$PS_CC_VERDICT" | grep -c '^IMPROVED ' || true)
|
|
223
|
+
printf '%s\n' "$PS_CC_VERDICT" | grep '^IMPROVED ' | sed 's/^IMPROVED / 📉 /'
|
|
224
|
+
echo "✅ PASSED - PowerShell complexity within ratchet (${PS_CC_MEASURED} function(s), ${PS_CC_IMPROVED} improved, threshold ${PS_CC_THRESHOLD})."
|
|
225
|
+
GATE_3_STATUS="PASS"
|
|
226
|
+
GATE_3_DETAIL="powerShell_ratchet improved=${PS_CC_IMPROVED}"
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
else
|
|
232
|
+
# Freshly added files: this arm has no history to compare, so it judges
|
|
233
|
+
# each file's functions against the threshold. A file that is merely
|
|
234
|
+
# being touched keeps its history (that is what the baseline is for);
|
|
235
|
+
# measuring only ADDED files means the arm can never retroactively block
|
|
236
|
+
# a cleanup commit.
|
|
237
|
+
PS_CC_ARGS=()
|
|
238
|
+
for _f in $PS_ADDED; do PS_CC_ARGS[${#PS_CC_ARGS[@]}]="'$_f'"; done
|
|
239
|
+
PS_CC_OUT=$("$PS_CC_PWSH" -NoProfile -NonInteractive -ExecutionPolicy Bypass \
|
|
240
|
+
-Command "& '$PS_CC_SCRIPT' -Path @($(IFS=','; echo "${PS_CC_ARGS[*]}")) -Threshold ${PS_CC_THRESHOLD}" 2>&1 || true)
|
|
241
|
+
PS_CC_MEASURED=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^functions_measured=//p' | tail -1)
|
|
242
|
+
PS_CC_OVER=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^over_threshold=//p' | tail -1)
|
|
243
|
+
PS_CC_MAX=$(printf '%s\n' "$PS_CC_OUT" | sed -n 's/^max_ccn=//p' | tail -1)
|
|
244
|
+
if [ -z "$PS_CC_MEASURED" ] || [ "${PS_CC_MEASURED:-0}" -eq 0 ]; then
|
|
245
|
+
echo "⏭️ SKIPPED - Complexity (no functions in added PowerShell files)."
|
|
246
|
+
GATE_3_STATUS="SKIP"
|
|
247
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
248
|
+
# shellcheck disable=SC2034
|
|
249
|
+
GATE_3_REASON="no_functions_found"
|
|
250
|
+
elif [ "${PS_CC_OVER:-0}" -gt 0 ]; then
|
|
251
|
+
echo " Measured ${PS_CC_MEASURED} function(s) via AST; max CCN ${PS_CC_MAX} (threshold ${PS_CC_THRESHOLD})"
|
|
252
|
+
printf '%s\n' "$PS_CC_OUT" | grep '^OVER ' | sed 's/^/ /'
|
|
253
|
+
echo "❌ BLOCKED - ${PS_CC_OVER} newly added function(s) with CCN > ${PS_CC_THRESHOLD}."
|
|
254
|
+
echo "Refactor high-complexity functions, or raise the threshold deliberately."
|
|
255
|
+
GATE_3_STATUS="BLOCK"
|
|
256
|
+
GATE_3_REASON="ccn_over_threshold"
|
|
257
|
+
exit 1
|
|
258
|
+
else
|
|
259
|
+
echo "✅ PASSED - All added PowerShell functions within complexity threshold (${PS_CC_THRESHOLD})."
|
|
260
|
+
echo " Measured ${PS_CC_MEASURED} function(s) via AST; max CCN ${PS_CC_MAX}."
|
|
261
|
+
GATE_3_STATUS="PASS"
|
|
262
|
+
fi
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
|
|
32
267
|
else
|
|
33
268
|
# S2 (#507): three-level CCN threshold override --
|
|
34
269
|
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|