@boyingliu01/xp-gate 0.30.1 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/adapters/iac.sh +45 -4
  2. package/adapters/java.sh +82 -16
  3. package/adapters/powershell.sh +70 -9
  4. package/bin/xp-gate.js +8 -0
  5. package/gate-10.sh +32 -2
  6. package/gate-3.sh +47 -6
  7. package/gate-4.sh +12 -0
  8. package/gate-7.sh +4 -0
  9. package/hooks/gate-10.sh +32 -2
  10. package/hooks/gate-3.sh +47 -6
  11. package/hooks/gate-4.sh +12 -0
  12. package/hooks/gate-7.sh +4 -0
  13. package/hooks/hook-module-resolution.sh +14 -0
  14. package/hooks/lib/coverage-total.sh +1 -1
  15. package/hooks/pre-commit +138 -17
  16. package/hooks/pre-push +97 -6
  17. package/hooks/sprint-gate.sh +81 -2
  18. package/lib/__tests__/crap-cli.test.js +76 -0
  19. package/lib/__tests__/crap.test.js +137 -0
  20. package/lib/__tests__/hook-health.test.js +85 -0
  21. package/lib/__tests__/retro-gate-m.test.js +144 -0
  22. package/lib/coverage-total.sh +92 -0
  23. package/lib/crap-cli.js +191 -0
  24. package/lib/crap.js +196 -0
  25. package/lib/doctor.js +44 -0
  26. package/lib/hook-health.js +121 -0
  27. package/lib/mattpocock-license-verdict.json +1 -1
  28. package/lib/retro.js +139 -2
  29. package/mock-policy/AGENTS.md +2 -2
  30. package/mutation/AGENTS.md +2 -2
  31. package/package.json +1 -1
  32. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  33. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  34. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  35. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  36. package/plugins/dsh/package.json +1 -1
  37. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  38. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  39. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  40. package/plugins/opencode/package.json +1 -1
  41. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  42. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  43. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  44. package/plugins/qoder/plugin.json +1 -1
  45. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  46. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  47. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  48. package/principles/AGENTS.md +2 -2
  49. package/skills/delphi-review/AGENTS.md +2 -2
  50. package/skills/sprint-flow/AGENTS.md +2 -2
  51. package/skills/test-specification-alignment/AGENTS.md +2 -2
  52. package/sprint-gate.sh +81 -2
package/adapters/iac.sh CHANGED
@@ -64,10 +64,29 @@ run_static_analysis() {
64
64
  if command -v checkov >/dev/null 2>&1; then
65
65
  echo "Running checkov IaC security scan..."
66
66
 
67
- # checkov 3.x always exits 0 even on failures. We capture output to a
68
- # temp file and parse it for "FAILED for resource" lines instead.
67
+ # checkov 3.x exits 0 on "found failures" (we parse the output for those),
68
+ # so the status cannot be the only signal. It is still NOT discardable:
69
+ # exit 2 means checkov itself crashed -- e.g. AttributeError from a broken
70
+ # detect_secrets dependency -- and a crashed scanner has not judged
71
+ # anything. Counting a traceback as findings invents security issues; a
72
+ # traceback that happens to lack the marker reports a clean pass. Both are
73
+ # wrong, so a crash is tracked separately and answered as "unusable" (#552).
69
74
  local checkov_log=$(mktemp)
70
75
  local checkov_args="--compact"
76
+ local CHECKOV_CRASHED=0
77
+ local checkov_scan_count=0
78
+
79
+ # Record a checkov run's exit status and flag a crash. Codes 0 and 1 are
80
+ # normal (1 = findings); anything else, or any traceback in the log, is the
81
+ # tool failing rather than reporting.
82
+ record_checkov_run() {
83
+ local status=$1 file=$2
84
+ checkov_scan_count=$((checkov_scan_count + 1))
85
+ if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
86
+ CHECKOV_CRASHED=1
87
+ echo " ⚠️ checkov exited $status on $file — the scanner failed, this is not a finding"
88
+ fi
89
+ }
71
90
 
72
91
  # Run checkov on all detected IaC files
73
92
  local tf_files=$(echo "$iac_files" | grep "TERRAFORM:" | sed 's/TERRAFORM://')
@@ -79,6 +98,7 @@ run_static_analysis() {
79
98
  for file in $tf_files; do
80
99
  if [ -f "$file" ]; then
81
100
  checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
101
+ record_checkov_run $? "$file"
82
102
  fi
83
103
  done
84
104
  fi
@@ -99,6 +119,7 @@ run_static_analysis() {
99
119
  for file in $k8s_only; do
100
120
  if [ -f "$file" ]; then
101
121
  checkov --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
122
+ record_checkov_run $? "$file"
102
123
  fi
103
124
  done
104
125
  fi
@@ -108,6 +129,7 @@ run_static_analysis() {
108
129
  for file in $gh_files; do
109
130
  if [ -f "$file" ]; then
110
131
  checkov --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
132
+ record_checkov_run $? "$file"
111
133
  fi
112
134
  done
113
135
  fi
@@ -118,20 +140,39 @@ run_static_analysis() {
118
140
  for file in $docker_files; do
119
141
  if [ -f "$file" ]; then
120
142
  checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
143
+ record_checkov_run $? "$file"
121
144
  fi
122
145
  done
123
146
  fi
124
147
 
125
148
  # Print captured output then check for failures
126
149
  cat "$checkov_log"
150
+
151
+ # A crash is answered before any parsing. The traceback is not evidence of
152
+ # anything: its text decides the verdict if we let grep see it.
153
+ if [ "$CHECKOV_CRASHED" -eq 1 ]; then
154
+ rm -f "$checkov_log"
155
+ echo ""
156
+ echo "❌ BLOCKED - checkov could not run (the scanner crashed, so nothing was scanned)."
157
+ echo " This is NOT a security finding. Fix the environment, usually:"
158
+ echo " pip install -U checkov detect-secrets"
159
+ echo " (A common cause is a detect_secrets version whose FileType enum lacks JSON.)"
160
+ echo " To accept the risk for this commit: XP_GATE_IAC_SOFT=1"
161
+ if [ "${XP_GATE_IAC_SOFT:-}" = "1" ]; then
162
+ echo "⚠️ XP_GATE_IAC_SOFT=1: unusable scanner downgraded to WARN"
163
+ return 3
164
+ fi
165
+ return 2
166
+ fi
167
+
127
168
  local failed_count=$(grep -c "FAILED for resource" "$checkov_log" 2>/dev/null || true)
128
169
  rm -f "$checkov_log"
129
170
 
130
171
  if [ "$failed_count" -gt 0 ]; then
131
- echo "❌ $failed_count security issue(s) found by checkov. Blocking commit."
172
+ echo "❌ $failed_count security issue(s) found by checkov across $checkov_scan_count file(s). Blocking commit."
132
173
  exit_code=1
133
174
  fi
134
- echo "checkov scan completed."
175
+ echo "checkov scan completed ($checkov_scan_count file(s) scanned)."
135
176
  return $exit_code
136
177
  fi
137
178
 
package/adapters/java.sh CHANGED
@@ -106,10 +106,23 @@ java_staged_files() {
106
106
  # counts from the object count -> unusable -> BLOCK;
107
107
  # - a truncated response (no closing bracket) is unusable -> BLOCK.
108
108
  # Prints the violation (object) count on success; returns 1 if unusable.
109
+ #
110
+ # On rejection, a one-line reason goes to STDERR naming the invariant that
111
+ # failed plus the measured counts (#545). Before this, every rejection looked
112
+ # identical to the operator as "not a usable violations JSON", which is why
113
+ # #545 was filed against a code path that was never involved. The number the
114
+ # callers capture comes from stdout, so diagnostics on stderr change nothing
115
+ # for `count=$(wc_java_json_violation_count ...)`.
116
+ wc_java_diag() {
117
+ echo " wc-java JSON rejected: $*" >&2
118
+ }
109
119
  # ---------------------------------------------------------------------------
110
120
  wc_java_json_violation_count() {
111
121
  local json_file="$1"
112
- [ -s "$json_file" ] || return 1
122
+ if [ ! -s "$json_file" ]; then
123
+ wc_java_diag "empty or missing response file ($json_file)"
124
+ return 1
125
+ fi
113
126
 
114
127
  # Two shapes are accepted, each closed by its own bracket:
115
128
  # - bare array `[ ... ]` — the originally pinned contract ([] = clean);
@@ -122,9 +135,18 @@ wc_java_json_violation_count() {
122
135
  head1=$(head -c 1 "$json_file")
123
136
  tail=$(tr -d '[:space:]' < "$json_file" | tail -c 1)
124
137
  case "$head1" in
125
- "[") shape=array; [ "$tail" = "]" ] || return 1 ;;
126
- "{") shape=object; [ "$tail" = "}" ] || return 1 ;;
127
- *) return 1 ;;
138
+ "[") shape=array
139
+ if [ "$tail" != "]" ]; then
140
+ wc_java_diag "array response is unterminated (first char '[', tail '$tail' not ']') — truncated output?"
141
+ return 1
142
+ fi ;;
143
+ "{") shape=object
144
+ if [ "$tail" != "}" ]; then
145
+ wc_java_diag "object response is unterminated (first char '{', tail '$tail' not '}') — truncated output?"
146
+ return 1
147
+ fi ;;
148
+ *) wc_java_diag "response does not start with a JSON bracket (entry char '$head1') — a banner/log line on stdout would cause this"
149
+ return 1 ;;
128
150
  esac
129
151
 
130
152
  local structural obj_count file_count rule_count sev_count
@@ -155,15 +177,36 @@ wc_java_json_violation_count() {
155
177
  file_count=$(grep -o '"file"' <<< "$structural" | wc -l | tr -d '[:space:]')
156
178
  rule_count=$(grep -o '"rule"' <<< "$structural" | wc -l | tr -d '[:space:]')
157
179
  sev_count=$(grep -o '"severity"' <<< "$structural" | wc -l | tr -d '[:space:]')
158
- [ "$file_count" -eq "$rule_count" ] || return 1
159
- [ "$file_count" -eq "$sev_count" ] || return 1
180
+ if [ "$file_count" -ne "$rule_count" ] || [ "$file_count" -ne "$sev_count" ]; then
181
+ wc_java_diag "issue keys are out of sync (file=$file_count rule=$rule_count severity=$sev_count) — an issue record is missing a required key"
182
+ return 1
183
+ fi
160
184
  # Everything from the "issues" member onward (the tool emits it last; if
161
185
  # a future format moves it, keys before it fall outside record parsing ->
162
186
  # sum mismatch -> BLOCK, fail closed). Flatten to one line first: real
163
187
  # output is pretty-printed across lines.
164
188
  flattened=$(tr '\n' ' ' <<< "$structural")
165
189
  tail_from_issues=$(sed -n 's/.*"issues"[[:space:]]*:[[:space:]]*\[/\n/p' <<< "$flattened")
166
- [ -n "$tail_from_issues" ] || return 1
190
+ if [ -z "$tail_from_issues" ]; then
191
+ # `"issues": null` is a legitimate zero-violation report, not a broken one
192
+ # (#545). The Go producer's nil slice marshals to null, and it does so
193
+ # whenever the scan hid at least one info-level finding -- so a file whose
194
+ # only problems are info-level gets null while a fully clean file gets [].
195
+ # Whichever branch produced it, null means "no visible violations".
196
+ if sed -n 's/.*"issues"[[:space:]]*:[[:space:]]*null.*/found/p' <<< "$flattened" | grep -q found; then
197
+ # null is only trustworthy when the summary agrees there is nothing to
198
+ # report. A nonzero total_issues with null is the lying-tool shape.
199
+ null_total=$(sed -n 's/.*"total_issues"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' <<< "$flattened")
200
+ if [ -n "$null_total" ] && [ "$null_total" -gt 0 ]; then
201
+ wc_java_diag "issues is null but total_issues=$null_total — the tool claims violations it did not report"
202
+ return 1
203
+ fi
204
+ echo 0
205
+ return 0
206
+ fi
207
+ wc_java_diag 'no "issues" array found after the summary object — the response is not the {summary, issues} contract'
208
+ return 1
209
+ fi
167
210
  records=$(awk '
168
211
  { rest = $0
169
212
  depth = 0; rec = ""
@@ -182,15 +225,17 @@ wc_java_json_violation_count() {
182
225
  cf=$(grep -o '"file"' <<< "$_rec" | wc -l | tr -d '[:space:]')
183
226
  cr=$(grep -o '"rule"' <<< "$_rec" | wc -l | tr -d '[:space:]')
184
227
  cs=$(grep -o '"severity"' <<< "$_rec" | wc -l | tr -d '[:space:]')
185
- [ "$cf" -eq 1 ] || return 1
186
- [ "$cr" -eq 1 ] || return 1
187
- [ "$cs" -eq 1 ] || return 1
228
+ if [ "$cf" -ne 1 ] || [ "$cr" -ne 1 ] || [ "$cs" -ne 1 ]; then
229
+ wc_java_diag "issue record ${rec_count} has the wrong key multiplicity (file=$cf rule=$cr severity=$cs, each must be 1)"
230
+ return 1
231
+ fi
188
232
  rf=$((rf + cf)); rr=$((rr + cr)); rs=$((rs + cs))
189
233
  rec_count=$((rec_count + 1))
190
234
  done <<< "$records"
191
- [ "$rf" -eq "$file_count" ] || return 1
192
- [ "$rr" -eq "$rule_count" ] || return 1
193
- [ "$rs" -eq "$sev_count" ] || return 1
235
+ if [ "$rf" -ne "$file_count" ] || [ "$rr" -ne "$rule_count" ] || [ "$rs" -ne "$sev_count" ]; then
236
+ wc_java_diag "per-record key totals do not match the document (records rf=$rf rr=$rr rs=$rs vs document file=$file_count rule=$rule_count severity=$sev_count) — a required key appears outside an issue record"
237
+ return 1
238
+ fi
194
239
  echo "$rec_count"
195
240
  return 0
196
241
  fi
@@ -199,9 +244,10 @@ wc_java_json_violation_count() {
199
244
  file_count=$(grep -o '"file"' <<< "$structural" | wc -l | tr -d '[:space:]')
200
245
  rule_count=$(grep -o '"rule"' <<< "$structural" | wc -l | tr -d '[:space:]')
201
246
  sev_count=$(grep -o '"severity"' <<< "$structural" | wc -l | tr -d '[:space:]')
202
- [ "$obj_count" -eq "$file_count" ] || return 1
203
- [ "$obj_count" -eq "$rule_count" ] || return 1
204
- [ "$obj_count" -eq "$sev_count" ] || return 1
247
+ if [ "$obj_count" -ne "$file_count" ] || [ "$obj_count" -ne "$rule_count" ] || [ "$obj_count" -ne "$sev_count" ]; then
248
+ wc_java_diag "array elements and keys are out of sync (objects=$obj_count file=$file_count rule=$rule_count severity=$sev_count)"
249
+ return 1
250
+ fi
205
251
 
206
252
  echo "$obj_count"
207
253
  }
@@ -223,6 +269,26 @@ run_wc_java_lint() {
223
269
  return 9 # not installed -> caller falls back to the legacy chain
224
270
  fi
225
271
 
272
+ # #550: --staged means "only the Java files in the index", and wc-java-lint
273
+ # 0.4.1 honours that for checkstyle, PMD, lizard and simian -- but spotbugs
274
+ # scans the whole target/classes tree regardless. Measured with only
275
+ # docs/_tmp_probe.md staged: three tools reported "skipped: no Java files"
276
+ # while spotbugs reported "checked 1 .class file, found 3 issues" from a class
277
+ # compiled earlier. The verdict then blocks a docs commit over a warning that
278
+ # predates it and that its author never touched.
279
+ #
280
+ # The adapter cannot fix the tool, but it can refuse to relay a verdict the
281
+ # tool had no basis to produce. With no staged Java file there is nothing this
282
+ # gate is entitled to say, so it says nothing -- out loud, not silently.
283
+ local staged_java
284
+ staged_java=$(java_staged_files 2>/dev/null | grep -c '.' || true)
285
+ if [ "${staged_java:-0}" -eq 0 ]; then
286
+ echo "⏭️ SKIPPED - Java static analysis: no staged .java files (wc-java-lint --staged)"
287
+ echo " (spotbugs scans all of target/classes regardless of --staged, so its"
288
+ echo " verdict is withheld here - upstream defect, see xp-gate #550)"
289
+ return 0
290
+ fi
291
+
226
292
  timeout_s=$(resolve_wc_java_lint_timeout)
227
293
  out_file=$(mktemp "${TMPDIR:-/tmp}/wc-java-lint.XXXXXX")
228
294
  err_file="${out_file}.err"
@@ -17,16 +17,46 @@
17
17
  # SKIP for now.
18
18
 
19
19
  # Detect PowerShell executable (prefer pwsh 7+, fallback to powershell.exe 5.1)
20
+ #
21
+ # #547 finding 4: `command -v pwsh` alone is not enough. Under git-bash with a
22
+ # stripped PATH, the standard Windows installs are invisible even though the
23
+ # user can run pwsh from any terminal: pwsh 7 installs to
24
+ # %LOCALAPPDATA%\Microsoft\WindowsApps (a PATH alias that git-bash does not
25
+ # always inherit) and PS 5.1 lives at System32\WindowsPowerShell\v1.0. Probing
26
+ # those absolute paths converts a silent degrade into a working gate.
27
+ #
28
+ # The order is unchanged: whatever is on PATH wins, absolute paths are a
29
+ # fallback, and an empty result still means "not available" -- this widens
30
+ # detection, it does not invent an interpreter.
20
31
  _detect_pwsh() {
21
32
  if command -v pwsh &>/dev/null 2>&1; then
22
33
  echo "pwsh"
23
- elif command -v powershell.exe &>/dev/null 2>&1; then
34
+ return 0
35
+ fi
36
+ if command -v powershell.exe &>/dev/null 2>&1; then
24
37
  echo "powershell.exe"
25
- elif command -v powershell &>/dev/null 2>&1; then
38
+ return 0
39
+ fi
40
+ if command -v powershell &>/dev/null 2>&1; then
26
41
  echo "powershell"
27
- else
28
- echo ""
42
+ return 0
29
43
  fi
44
+ # Windows absolute-path fallbacks.
45
+ local candidate
46
+ for candidate in \
47
+ "${LOCALAPPDATA:-$HOME/AppData/Local}/Microsoft/WindowsApps/pwsh.exe" \
48
+ "/c/Program Files/PowerShell/7/pwsh.exe" \
49
+ "/c/Program Files/PowerShell/6/pwsh.exe" \
50
+ "/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe" \
51
+ "/mnt/c/Program Files/PowerShell/7/pwsh.exe" \
52
+ "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; do
53
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
54
+ echo "$candidate"
55
+ return 0
56
+ fi
57
+ done
58
+ echo ""
59
+ return 1
30
60
  }
31
61
 
32
62
  _find_powershell_files() {
@@ -87,18 +117,49 @@ run_static_analysis() {
87
117
  echo "Running PSScriptAnalyzer static analysis on PowerShell scripts..."
88
118
  # Recursively analyze all .ps1 files from repo root
89
119
  # Exit with non-zero if Error or Warning severity issues found
90
- "$PWSH" "${POWERSHELL_ARGS[@]}" "
120
+ local PSSA_RC=0
121
+ local PSSA_OUT
122
+ PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
123
+ if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
124
+ Write-Output 'PSSA_MODULE_MISSING'
125
+ exit 0
126
+ }
91
127
  \$results = Invoke-ScriptAnalyzer -Path . -Recurse -Severity Error,Warning
92
128
  if (\$results) {
93
129
  \$results | Format-Table -AutoSize
94
130
  exit 1
95
131
  }
96
132
  exit 0
97
- "
98
- return $?
133
+ " 2>&1) || PSSA_RC=$?
134
+ printf '%s\n' "$PSSA_OUT"
135
+ case "$PSSA_RC" in
136
+ 0)
137
+ if printf '%s\n' "$PSSA_OUT" | grep -q 'PSSA_MODULE_MISSING'; then
138
+ # #547 finding 2: the engine exists but its module does not. That is
139
+ # NOT a pass -- nothing was analysed. The XP_G1_REASON protocol (the
140
+ # one the java branch uses) turns this into a WARN that stays in the
141
+ # scoring denominator instead of vanishing.
142
+ echo "XP_G1_REASON: PSScriptAnalyzer module not installed (Install-Module PSScriptAnalyzer)"
143
+ return 3
144
+ fi
145
+ return 0
146
+ ;;
147
+ 1)
148
+ return 1 # real findings -> BLOCK upstream
149
+ ;;
150
+ *)
151
+ echo "XP_G1_REASON: PSScriptAnalyzer run failed (exit $PSSA_RC)"
152
+ return 2 # unusable -> fail closed upstream
153
+ ;;
154
+ esac
99
155
  else
100
- echo "PowerShell not available, skipping PowerShell static analysis"
101
- return 0
156
+ # #547 finding 4: previously `return 0`, i.e. "no PowerShell available"
157
+ # was indistinguishable from "PowerShell is clean" -- a silent false pass on
158
+ # a PowerShell commit. Return the not-available verdict so the caller can
159
+ # WARN and record a reason.
160
+ echo "PowerShell executable not found (probe: pwsh, powershell.exe, and the standard Windows install paths)"
161
+ echo "XP_G1_REASON: pwsh not available (install PowerShell 7: https://aka.ms/powershell)"
162
+ return 3
102
163
  fi
103
164
  }
104
165
 
package/bin/xp-gate.js CHANGED
@@ -350,6 +350,14 @@ const COMMANDS = {
350
350
  },
351
351
  usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
352
352
  },
353
+ 'crap': {
354
+ description: 'Rank functions by CRAP score (complexity x weak coverage) — advisory, never blocks (#404)',
355
+ run: subargs => {
356
+ const { handleCrap } = require('../lib/crap-cli.js');
357
+ handleCrap(subargs).then(code => process.exit(code));
358
+ },
359
+ usage: 'xp-gate crap [--top N] [--threshold N] [--json] [--paths <p...>] [--include-mirrors]'
360
+ },
353
361
  'sprint-init': {
354
362
  description: 'Initialize sprint state (single entry point for sprint creation, resolves #366)',
355
363
  run: subargs => {
package/gate-10.sh CHANGED
@@ -12,7 +12,20 @@ GATE_10_START=$(gate_start_ms)
12
12
  GATE_10_STATUS=""
13
13
  # #536: audit trail for "did this commit actually get scanned?". The report
14
14
  # surfaces reason + scanned_files so a non-scan never reads as a clean PASS.
15
+ #
16
+ # GATE_10_REASON and GATE_10_SCANNED_FILES are consumed by the sourcing parent
17
+ # (pre-commit builds its report JSON from them after `source gate-10.sh`), not
18
+ # inside this file. The linter checks one file at a time and cannot see the
19
+ # consumer, so each assignment is flagged SC2034 "appears unused" -- which
20
+ # blocks Gate 1 for any repo that vendors this adapter, because that repo's own
21
+ # lint pass runs over the vendored copy (#548). Each assignment carries a
22
+ # scoped directive with that reason stated at the site.
23
+ #
24
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
25
+ # shellcheck disable=SC2034
15
26
  GATE_10_REASON=""
27
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
28
+ # shellcheck disable=SC2034
16
29
  GATE_10_SCANNED_FILES=0
17
30
 
18
31
  # Semgrep availability check
@@ -29,14 +42,21 @@ if [ -z "$SEMGREP_CMD" ]; then
29
42
  echo " Pre-cache rules: semgrep --config=p/security-audit"
30
43
  echo " Gate 10: SAST Security (WARN, semgrep not installed)"
31
44
  GATE_10_STATUS="WARN"
45
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
46
+ # shellcheck disable=SC2034
32
47
  GATE_10_REASON="tool_not_installed"
33
48
  else
34
- # Get staged files filtered to Semgrep-supported languages
35
- SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
49
+ # Get staged files filtered to Semgrep-supported languages.
50
+ # #547 finding 3: ps1|psm1 were missing, so a PowerShell-only commit never
51
+ # reached semgrep even when Gate 10 ran -- the SAST gate was simply absent
52
+ # for that language.
53
+ SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift|ps1|psm1)$' || true)
36
54
 
37
55
  if [ -z "$SEMGREP_FILES" ]; then
38
56
  echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
39
57
  GATE_10_STATUS="SKIP"
58
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
59
+ # shellcheck disable=SC2034
40
60
  GATE_10_REASON="no_supported_files"
41
61
  else
42
62
  # One path per line into an array. Quoting the newline-joined list would hand
@@ -91,6 +111,8 @@ EOF
91
111
  echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
92
112
  echo " ⏭️ SKIPPED - SAST (node not installed)"
93
113
  GATE_10_STATUS="SKIP"
114
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
115
+ # shellcheck disable=SC2034
94
116
  GATE_10_REASON="tool_not_installed"
95
117
  else
96
118
 
@@ -164,6 +186,8 @@ console.log("");});
164
186
  fi
165
187
  echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
166
188
  GATE_10_STATUS="SKIP"
189
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
190
+ # shellcheck disable=SC2034
167
191
  GATE_10_REASON="output_unparseable"
168
192
  elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
169
193
  # Nothing was scanned to a verdict: semgrep either failed outright
@@ -178,6 +202,8 @@ console.log("");});
178
202
  fi
179
203
  echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
180
204
  GATE_10_STATUS="SKIP"
205
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
206
+ # shellcheck disable=SC2034
181
207
  GATE_10_REASON="semgrep_runtime_error"
182
208
  elif [ "$V_BLOCKING" -gt 0 ]; then
183
209
  echo ""
@@ -192,9 +218,13 @@ console.log("");});
192
218
  echo "$FINDING_DETAILS"
193
219
  echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
194
220
  GATE_10_STATUS="FAIL"
221
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
222
+ # shellcheck disable=SC2034
195
223
  GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
196
224
  exit 1
197
225
  else
226
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
227
+ # shellcheck disable=SC2034
198
228
  GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
199
229
  echo ""
200
230
  echo " ✅ PASSED - No blocking vulnerabilities (files scanned: $SEMGREP_FILE_COUNT)"
package/gate-3.sh CHANGED
@@ -8,11 +8,15 @@
8
8
  2>&1 echo "→ Gate 3: Cyclomatic complexity..."
9
9
  GATE_3_START=$(gate_start_ms)
10
10
  # #537: every non-run records WHY so the report can say why a gate is SKIP.
11
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
12
+ # shellcheck disable=SC2034
11
13
  GATE_3_REASON=""
12
14
 
13
15
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
14
16
  echo "⏭️ SKIPPED - Complexity (documentation project)."
15
17
  GATE_3_STATUS="SKIP"
18
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
19
+ # shellcheck disable=SC2034
16
20
  GATE_3_REASON="documentation_project"
17
21
 
18
22
  elif [ "$PROJECT_LANG" = "powershell" ]; then
@@ -21,6 +25,8 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
21
25
  GATE_3_STATUS="SKIP"
22
26
  # #537: a PowerShell-primary repo was invisible to this dimension while the
23
27
  # score implied full coverage — say exactly why nothing ran.
28
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
29
+ # shellcheck disable=SC2034
24
30
  GATE_3_REASON="no_powershell_tool"
25
31
 
26
32
  else
@@ -159,7 +165,10 @@ else
159
165
  # empty stdin — the verdict comes back empty and Gate 3 passes on zero
160
166
  # rows (measured while wiring #544: same input passed or blocked
161
167
  # depending only on where the prefix sat).
168
+ # Consumed by evaluate_ccn_ratchet in the sourced lib, not here.
169
+ # shellcheck disable=SC2034
162
170
  CCN_WARN_THRESHOLD="$CCN_THRESHOLD"
171
+ # shellcheck disable=SC2034
163
172
  CCN_BLOCK_THRESHOLD=$((CCN_THRESHOLD * 2))
164
173
  RATCHET_VERDICT=$(printf '%s\n' "$CC_CSV" | evaluate_ccn_ratchet ".ccn-baseline.json")
165
174
  CC_VIOLATIONS=$(printf '%s\n' "$RATCHET_VERDICT" | grep -c '^BLOCK ' || true)
@@ -179,16 +188,44 @@ else
179
188
  GATE_3_STATUS="PASS"
180
189
  fi
181
190
  else
182
- # Parse warning count from the summary table: "Warning cnt 8"
183
- # Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
184
- CC_WARNINGS=$(echo "$CC_OUTPUT" | grep "^Warning cnt" | awk '{print $NF}' | tr -d '[:space:]' | sed 's/[^0-9]//g' || true)
185
- CC_WARNINGS=${CC_WARNINGS:-0}
186
-
187
- if [ "$CC_WARNINGS" -gt 0 ]; then
191
+ # #543: count from lizard's machine-readable CSV, never its tables.
192
+ #
193
+ # The previous parser grepped '^Warning cnt' for a summary row. Two things
194
+ # made that never match: "Warning cnt" is a column header in the MIDDLE of
195
+ # a line ("Total nloc Avg.NLOC ... Warning cnt Fun Rt nloc Rt"), and
196
+ # lizard omits the summary block entirely when given explicit file
197
+ # arguments -- which is how this gate always invokes it. CC_WARNINGS was
198
+ # therefore always 0 and the flat-threshold path could not block anything.
199
+ #
200
+ # --csv has a stable column order (NLOC,CCN,token,PARAM,length,location,
201
+ # file,name,signature,...), so the verdict comes from the same numbers
202
+ # lizard uses, independent of version and table formatting. --csv here is
203
+ # the flat-threshold run; the ratchet branch above builds its own.
204
+ CC_CSV_FLAT=$("$LIZARD_PATH" --csv -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>/dev/null || true)
205
+ CC_WARNINGS=$(printf '%s\n' "$CC_CSV_FLAT" | awk -F, -v t="$CCN_THRESHOLD" '
206
+ NF > 1 && $2 + 0 > t + 0 { c++ }
207
+ END { print c + 0 }
208
+ ')
209
+ # A lizard that produced no parsable row must not read as "clean". Without
210
+ # this, a crashing or mis-invoked lizard yields 0 warnings and the gate
211
+ # reports PASSED on zero analysed functions -- the same silent-success
212
+ # failure this issue is about.
213
+ if ! printf '%s\n' "$CC_CSV_FLAT" | awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { found = 1; exit }
214
+ END { exit(found ? 0 : 1) }'; then
215
+ echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
216
+ GATE_3_STATUS="WARN"
217
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
218
+ # shellcheck disable=SC2034
219
+ GATE_3_REASON="lizard_output_unparsable"
220
+ elif [ "$CC_WARNINGS" -gt 0 ]; then
188
221
  echo "$CC_OUTPUT"
189
222
  echo ""
190
223
  echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
191
224
  echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
225
+ GATE_3_STATUS="BLOCK"
226
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
227
+ # shellcheck disable=SC2034
228
+ GATE_3_REASON="ccn_over_threshold"
192
229
  exit 1
193
230
  else
194
231
  echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
@@ -198,6 +235,8 @@ else
198
235
  else
199
236
  echo "⏭️ SKIPPED - Complexity (no source files to check)."
200
237
  GATE_3_STATUS="SKIP"
238
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
239
+ # shellcheck disable=SC2034
201
240
  GATE_3_REASON="no_source_files_changed"
202
241
  fi
203
242
  else
@@ -205,6 +244,8 @@ else
205
244
  echo " Install with: pip install --user lizard"
206
245
  echo " Gate 3: Complexity check (WARN, tool not available)"
207
246
  GATE_3_STATUS="WARN"
247
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
248
+ # shellcheck disable=SC2034
208
249
  GATE_3_REASON="tool_not_installed"
209
250
  fi
210
251
  fi
package/gate-4.sh CHANGED
@@ -56,11 +56,15 @@ GATE_4_START=$(gate_start_ms)
56
56
  GATE_4_STATUS=""
57
57
  # #537: every non-run records WHY (no files / tool missing / unsupported lang)
58
58
  # so the quality report can distinguish "not applicable" from "not covered".
59
+ # Consumed by the sourcing parent, not here (see #548).
60
+ # shellcheck disable=SC2034
59
61
  GATE_4_REASON=""
60
62
 
61
63
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
62
64
  echo "⏭️ SKIPPED - Principles check (documentation project)."
63
65
  GATE_4_STATUS="SKIP"
66
+ # Consumed by the sourcing parent, not here (see #548).
67
+ # shellcheck disable=SC2034
64
68
  GATE_4_REASON="documentation_project"
65
69
 
66
70
  else
@@ -156,6 +160,8 @@ else
156
160
  echo "⚠️ Warning: Principles checker execution failed"
157
161
  echo "⏭️ SKIPPED - Principles check (execution issue)"
158
162
  GATE_4_STATUS="SKIP"
163
+ # Consumed by the sourcing parent, not here (see #548).
164
+ # shellcheck disable=SC2034
159
165
  GATE_4_REASON="checker_execution_error"
160
166
  rm -f "$PRINCIPLES_JSON"
161
167
  else
@@ -191,17 +197,23 @@ else
191
197
  echo "ℹ️ npx/tsx not available - skipping principles check"
192
198
  echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
193
199
  GATE_4_STATUS="SKIP"
200
+ # Consumed by the sourcing parent, not here (see #548).
201
+ # shellcheck disable=SC2034
194
202
  GATE_4_REASON="tool_not_installed"
195
203
  fi
196
204
  else
197
205
  echo "ℹ️ Principles checker not found in project - skipping"
198
206
  echo "⏭️ SKIPPED - Principles check (checker not in project)"
199
207
  GATE_4_STATUS="SKIP"
208
+ # Consumed by the sourcing parent, not here (see #548).
209
+ # shellcheck disable=SC2034
200
210
  GATE_4_REASON="checker_not_found"
201
211
  fi
202
212
  else
203
213
  echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
204
214
  GATE_4_STATUS="SKIP"
215
+ # Consumed by the sourcing parent, not here (see #548).
216
+ # shellcheck disable=SC2034
205
217
  GATE_4_REASON="no_matching_files_changed"
206
218
  fi
207
219
  fi
package/gate-7.sh CHANGED
@@ -12,11 +12,15 @@ GATE_7_START=$(gate_start_ms)
12
12
  IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
13
13
  # #536: scanned_files makes a trivial PASS (nothing to scan) auditable in the
14
14
  # quality report instead of reading like a covered security check.
15
+ # Consumed by the sourcing parent, not here (see #548).
16
+ # shellcheck disable=SC2034
15
17
  GATE_7_SCANNED_FILES=0
16
18
  if [ -z "$IAC_FILES" ]; then
17
19
  echo "✅ PASSED - No IaC files detected in changes."
18
20
  GATE_7_STATUS="PASS"
19
21
  else
22
+ # Consumed by the sourcing parent, not here (see #548).
23
+ # shellcheck disable=SC2034
20
24
  GATE_7_SCANNED_FILES=$(printf '%s\n' "$IAC_FILES" | sed -n '/./p' | wc -l | tr -d '[:space:]')
21
25
  # Run IaC adapter. Resolution follows the documented 3-tier convention
22
26
  # (adapter-common.sh): global ~/.config/xp-gate/adapters -> project