@boyingliu01/xp-gate 0.31.1 → 0.31.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/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +254 -10
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +254 -10
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +403 -165
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- 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/delphi-review/SKILL.md +50 -0
- 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__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/adapter-common.sh
CHANGED
|
@@ -155,19 +155,57 @@ require_tool() {
|
|
|
155
155
|
local tool_name="$1"
|
|
156
156
|
local gate_name="${2:-Gate}"
|
|
157
157
|
local install_hint="${3:-}"
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# #593: on Windows the hook runs under git-bash, which does not see the
|
|
160
|
+
# toolchains installed on the Windows side (%APPDATA%\npm, the node dir,
|
|
161
|
+
# pip --user Scripts, ~/bin). Bootstrapping before probing turns "not on this
|
|
162
|
+
# shell's PATH" back into "found", instead of reporting an installed tool as
|
|
163
|
+
# missing. lib/toolchain-path.sh is sourced by pre-commit before this runs; the
|
|
164
|
+
# guard keeps require_tool usable from adapters that load on their own.
|
|
165
|
+
if command -v bootstrap_toolchain_path >/dev/null 2>&1; then
|
|
166
|
+
bootstrap_toolchain_path
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# #580: on WSL (appendWindowsPath=false) and in git-bash, `command -v node`
|
|
170
|
+
# returns 1 while node.exe sits in the same directory and runs. One root cause
|
|
171
|
+
# gave Gate 1 a silent SKIP, Gate 1 a false "Biome check failed" and Gate 8 a
|
|
172
|
+
# false "gitleaks not installed" — an environment fault reported as a code
|
|
173
|
+
# verdict in all three. tool_is_available also counts the .exe suffix and the
|
|
174
|
+
# XP_GATE_<TOOL>_BIN override.
|
|
175
|
+
if command -v tool_is_available >/dev/null 2>&1 && tool_is_available "$tool_name"; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
159
179
|
if command -v "$tool_name" >/dev/null 2>&1; then
|
|
160
180
|
return 0
|
|
161
181
|
fi
|
|
162
|
-
|
|
182
|
+
|
|
183
|
+
if command -v "${tool_name}.exe" >/dev/null 2>&1; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
163
187
|
if command -v npx >/dev/null 2>&1 && npx --no-install "$tool_name" --version >/dev/null 2>&1; then
|
|
164
188
|
return 0
|
|
165
189
|
fi
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
# #593: monorepos keep the tool in a subproject's node_modules/.bin, where
|
|
192
|
+
# neither PATH nor `npx` from the repo root can see it.
|
|
193
|
+
if command -v resolve_tool_path >/dev/null 2>&1 && resolve_tool_path "$tool_name" >/dev/null 2>&1; then
|
|
194
|
+
if command -v ensure_tool_dir_on_path >/dev/null 2>&1; then
|
|
195
|
+
ensure_tool_dir_on_path "$tool_name" || true
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
167
200
|
echo "❌ BLOCKED - Required tool '$tool_name' not available for $gate_name"
|
|
168
201
|
if [ -n "$install_hint" ]; then
|
|
169
202
|
echo " Install: $install_hint"
|
|
170
203
|
fi
|
|
204
|
+
# #593 point 3: "not installed" and "installed but not found" need different
|
|
205
|
+
# fixes, so name every location that was searched.
|
|
206
|
+
if command -v describe_tool_search >/dev/null 2>&1; then
|
|
207
|
+
describe_tool_search "$tool_name"
|
|
208
|
+
fi
|
|
171
209
|
echo " Per QUALITY-GATES-CODE-OF-CONDUCT.md: tool unavailable = BLOCK, not SKIP"
|
|
172
210
|
return 1
|
|
173
211
|
}
|
package/adapters/gate-3.sh
CHANGED
|
@@ -109,7 +109,14 @@ else
|
|
|
109
109
|
GATE_3_STATUS="PASS"
|
|
110
110
|
fi
|
|
111
111
|
else
|
|
112
|
-
|
|
112
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
113
|
+
# cause is an empty CHANGED_FILES. The old wording read like a pass.
|
|
114
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
115
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
116
|
+
else
|
|
117
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
118
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
119
|
+
fi
|
|
113
120
|
GATE_3_STATUS="SKIP"
|
|
114
121
|
GATE_3_REASON="no_source_files_changed"
|
|
115
122
|
fi
|
package/adapters/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
package/adapters/go.sh
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
# Tools: go vet, golangci-lint, arch-go (architecture), go test
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
8
25
|
|
|
9
26
|
run_static_analysis() {
|
|
10
27
|
require_tool go "Go SDK" || return 1
|
package/adapters/iac.sh
CHANGED
|
@@ -5,7 +5,24 @@
|
|
|
5
5
|
# Tools: checkov (primary), hadolint (Docker), kube-score (K8s)
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
|
|
8
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
9
|
+
# the package was installed:
|
|
10
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
11
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
12
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
13
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
14
|
+
# made that indistinguishable from success, leaving every shared function
|
|
15
|
+
# undefined with no warning.
|
|
16
|
+
for _ac_candidate in \
|
|
17
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
18
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
19
|
+
if [ -f "$_ac_candidate" ]; then
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
source "$_ac_candidate"
|
|
22
|
+
break
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
unset _ac_candidate
|
|
9
26
|
|
|
10
27
|
# Detect IaC files in changed files
|
|
11
28
|
detect_iac_files() {
|
package/adapters/powershell.sh
CHANGED
|
@@ -279,12 +279,48 @@ run_static_analysis() {
|
|
|
279
279
|
exit 0
|
|
280
280
|
" 2>&1) || PSSA_RC=$?
|
|
281
281
|
else
|
|
282
|
+
# #595: the unscoped path used to be a bare `-Path . -Recurse`, which walks
|
|
283
|
+
# node_modules. In a repo with Playwright installed that analyses
|
|
284
|
+
# playwright-core/bin/*.ps1 -- vendored files that use Write-Host and WMI
|
|
285
|
+
# cmdlets by design, that `npm ci` overwrites, and that the author cannot
|
|
286
|
+
# edit. Gate 1 then blocked EVERY commit, permanently. A gate that can never
|
|
287
|
+
# pass is not strict; it is one people learn to bypass.
|
|
288
|
+
#
|
|
289
|
+
# The adapter already had the right discovery: _find_powershell_files prunes
|
|
290
|
+
# .git, node_modules, dist, coverage and the rest. The two paths disagreed
|
|
291
|
+
# only because this one never called it. Now both use it, so "what this
|
|
292
|
+
# adapter refuses to analyse" has one definition.
|
|
293
|
+
local legacy_files
|
|
294
|
+
legacy_files=$(_find_powershell_files '*.ps1')
|
|
295
|
+
if [ -z "$legacy_files" ]; then
|
|
296
|
+
# Nothing to analyse is NOT a pass: say so via the reason protocol the
|
|
297
|
+
# module-missing branch uses, so it stays visible in the gate output.
|
|
298
|
+
echo "XP_G1_REASON: no PowerShell files found outside the pruned directories"
|
|
299
|
+
return 3
|
|
300
|
+
fi
|
|
301
|
+
local legacy_array
|
|
302
|
+
legacy_array=$(_powershell_path_array $legacy_files)
|
|
282
303
|
PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
|
|
283
304
|
if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
|
|
284
305
|
Write-Output 'PSSA_MODULE_MISSING'
|
|
285
306
|
exit 0
|
|
286
307
|
}
|
|
287
|
-
\$
|
|
308
|
+
\$ErrorActionPreference = 'Stop'
|
|
309
|
+
try { Import-Module PSScriptAnalyzer -ErrorAction Stop | Out-Null } catch { exit 3 }
|
|
310
|
+
\$settings = Join-Path (Get-Location) 'PSScriptAnalyzerSettings.psd1'
|
|
311
|
+
\$results = @()
|
|
312
|
+
try {
|
|
313
|
+
foreach (\$p in $legacy_array) {
|
|
314
|
+
if (Test-Path -LiteralPath \$settings) {
|
|
315
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -Settings \$settings -ErrorAction Stop)
|
|
316
|
+
} else {
|
|
317
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -ErrorAction Stop)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
Write-Output ('PSSA_RUN_ERROR: ' + \$_.Exception.Message)
|
|
322
|
+
exit 3
|
|
323
|
+
}
|
|
288
324
|
if (\$results) {
|
|
289
325
|
\$results | Format-Table -AutoSize
|
|
290
326
|
exit 1
|
package/adapters/python.sh
CHANGED
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
# Tools: mypy, ruff/flake8, pytest, import-linter (architecture)
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
11
25
|
|
|
12
26
|
# A mypy target must exist AND actually contain .py files: a bare directory
|
|
13
27
|
# check lets through TS repos' src/ (mypy exits 2 with "There are no .py[i]
|
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.
|
|
@@ -233,7 +468,16 @@ else
|
|
|
233
468
|
fi
|
|
234
469
|
fi
|
|
235
470
|
else
|
|
236
|
-
|
|
471
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
472
|
+
# cause is an empty CHANGED_FILES — an empty DIFF, or a `--all` run whose
|
|
473
|
+
# tree genuinely has no sources. The old wording read like a pass, which is
|
|
474
|
+
# why the reporter worked around the gate by running lizard by hand.
|
|
475
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
476
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
477
|
+
else
|
|
478
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
479
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
480
|
+
fi
|
|
237
481
|
GATE_3_STATUS="SKIP"
|
|
238
482
|
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
483
|
# shellcheck disable=SC2034
|