@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.
Files changed (36) hide show
  1. package/gate-3.sh +244 -9
  2. package/hooks/gate-3.sh +244 -9
  3. package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
  4. package/hooks/pre-commit +152 -13
  5. package/lib/mattpocock-license-verdict.json +1 -1
  6. package/mock-policy/AGENTS.md +3 -3
  7. package/mutation/AGENTS.md +3 -3
  8. package/package.json +1 -1
  9. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  11. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  12. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  13. package/plugins/dsh/package.json +1 -1
  14. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  15. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  16. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  17. package/plugins/opencode/package.json +1 -1
  18. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  19. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  20. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  21. package/plugins/qoder/plugin.json +1 -1
  22. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  23. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  24. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  25. package/principles/AGENTS.md +3 -3
  26. package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
  27. package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
  28. package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
  29. package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
  30. package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
  31. package/principles/adapters/cpp.ts +102 -50
  32. package/principles/adapters/typescript.ts +503 -174
  33. package/principles/baseline.ts +58 -70
  34. package/skills/delphi-review/AGENTS.md +3 -3
  35. package/skills/sprint-flow/AGENTS.md +3 -3
  36. 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
- echo "ℹ️ No PowerShell Clean Code / SOLID tool available"
24
- echo "⏭️ SKIPPED - Complexity (no PowerShell tool)"
25
- GATE_3_STATUS="SKIP"
26
- # #537: a PowerShell-primary repo was invisible to this dimension while the
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
30
- GATE_3_REASON="no_powershell_tool"
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
- echo "ℹ️ No PowerShell Clean Code / SOLID tool available"
24
- echo "⏭️ SKIPPED - Complexity (no PowerShell tool)"
25
- GATE_3_STATUS="SKIP"
26
- # #537: a PowerShell-primary repo was invisible to this dimension while the
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
30
- GATE_3_REASON="no_powershell_tool"
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.