@boyingliu01/xp-gate 0.35.17 → 0.35.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/gate-3.sh +94 -9
- package/adapters/gate-4.sh +27 -32
- package/adapters/gate-8.sh +32 -3
- package/adapters/gate-9.sh +14 -2
- package/gate-12-file-hygiene.sh +26 -4
- package/gate-13-compiler-strictness.sh +12 -0
- package/gates/gate-3.ts +77 -21
- package/gates/gate-8.ts +39 -26
- package/gates/gate-9.ts +4 -4
- package/hooks/gate-12-file-hygiene.sh +26 -4
- package/hooks/gate-13-compiler-strictness.sh +12 -0
- package/hooks/pre-commit +75 -11
- package/hooks/pre-push +11 -2
- package/lib/__tests__/doctor-delphi.test.js +114 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +74 -0
- package/lib/__tests__/doctor.test.js +9 -0
- package/lib/__tests__/gate-numbering.test.js +52 -0
- package/lib/__tests__/gate-runner.test.js +59 -14
- package/lib/__tests__/shared-utils.test.js +47 -0
- package/lib/__tests__/update-skill.test.js +77 -0
- package/lib/doctor-tui.js +23 -2
- package/lib/doctor.js +28 -7
- package/lib/gate-runner.js +98 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-utils.js +27 -1
- package/lib/update-skill.js +59 -3
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-findings.test.ts +32 -6
- package/plugins/dsh/src/exit-code.test.ts +6 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/sync-package-content.test.js +114 -46
- package/scripts/sync-package-content.js +32 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/gate-3.sh
CHANGED
|
@@ -172,8 +172,19 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
172
172
|
const entries = baseline.functions || baseline.entries || baseline;
|
|
173
173
|
if (Array.isArray(entries)) {
|
|
174
174
|
for (const e of entries) {
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// #632: accept the key the baseline WRITER actually emits.
|
|
176
|
+
// tools/Update-ComplexityBaseline.ps1 (the tool this gate
|
|
177
|
+
// points at in its own fix-it message) writes "function",
|
|
178
|
+
// while this reader only knew "name" -- so every entry was
|
|
179
|
+
// dropped, "known" stayed empty, and every function above
|
|
180
|
+
// the threshold became a brand-new function. The ratchet
|
|
181
|
+
// could never pass. Both spellings are read: this is a
|
|
182
|
+
// reader, and the writer vocabulary is not ours to
|
|
183
|
+
// redefine from here.
|
|
184
|
+
if (!e || !e.file) continue;
|
|
185
|
+
const fnName = e.name || e.function;
|
|
186
|
+
if (!fnName) continue;
|
|
187
|
+
known.set(norm(e.file) + "::" + fnName, Number(e.ccn) || 0);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
// The measurement emits ABSOLUTE paths (pwsh resolved them), while
|
|
@@ -450,6 +461,52 @@ else
|
|
|
450
461
|
GATE_3_DETAIL="mode=ratchet verifier=MISSING"
|
|
451
462
|
exit 1
|
|
452
463
|
fi
|
|
464
|
+
RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
|
|
465
|
+
[ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
|
|
466
|
+
# #617: grade exactly the population the baseline was written from.
|
|
467
|
+
#
|
|
468
|
+
# The comment below has always said so, but only as a comment: a repo whose
|
|
469
|
+
# baseline came from a narrower scan (a Java-only ccn-report, say) had its
|
|
470
|
+
# vendored minified JS graded anyway, and --csv-verified then disagreed with
|
|
471
|
+
# lizard on single-line code and fail-closed with "could not be
|
|
472
|
+
# span-verified". That BLOCK has no repair: the file has no baseline entry
|
|
473
|
+
# to ratchet, so there is nothing the user could add to clear it.
|
|
474
|
+
#
|
|
475
|
+
# The population filter runs BEFORE verification on purpose. Domain
|
|
476
|
+
# restriction is decidable from the path alone, so asking the expensive
|
|
477
|
+
# span re-derivation about a file that will not be graded anyway only
|
|
478
|
+
# manufactures failures for code the ratchet never claims to cover.
|
|
479
|
+
if [ -f "$RATCHET_LIB" ]; then
|
|
480
|
+
# shellcheck source=/dev/null
|
|
481
|
+
. "$RATCHET_LIB" 2>/dev/null || true
|
|
482
|
+
if declare -F ccn_file_in_population >/dev/null 2>&1; then
|
|
483
|
+
_cc_pop_kept=""
|
|
484
|
+
_cc_pop_skipped=""
|
|
485
|
+
while IFS= read -r _cc_row; do
|
|
486
|
+
[ -n "$_cc_row" ] || continue
|
|
487
|
+
_cc_row_path=$(printf '%s' "$_cc_row" | awk -F, '{print $1}')
|
|
488
|
+
if ccn_file_in_population ".ccn-baseline.json" "$_cc_row_path"; then
|
|
489
|
+
_cc_pop_kept="${_cc_pop_kept}${_cc_row}
|
|
490
|
+
"
|
|
491
|
+
else
|
|
492
|
+
_cc_pop_skipped="${_cc_pop_skipped}${_cc_row_path}
|
|
493
|
+
"
|
|
494
|
+
fi
|
|
495
|
+
done <<CC_POP_EOF
|
|
496
|
+
$CC_CSV
|
|
497
|
+
CC_POP_EOF
|
|
498
|
+
CC_CSV=$(printf '%s' "$_cc_pop_kept")
|
|
499
|
+
if [ -n "$_cc_pop_skipped" ]; then
|
|
500
|
+
_cc_pop_skip_n=$(printf '%s' "$_cc_pop_skipped" | grep -c . || true)
|
|
501
|
+
echo "ℹ️ Gate 3: ${_cc_pop_skip_n:-0} file(s) outside the baseline population — not graded, not ratcheted:"
|
|
502
|
+
printf '%s' "$_cc_pop_skipped" | sed 's/^/ /' | sed -n '1,5p'
|
|
503
|
+
if [ "${_cc_pop_skip_n:-0}" -gt 5 ]; then
|
|
504
|
+
echo " ... and $((_cc_pop_skip_n - 5)) more"
|
|
505
|
+
fi
|
|
506
|
+
echo " (declare or widen the population in .ccn-baseline.json to grade them)"
|
|
507
|
+
fi
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
453
510
|
if [ -f "$CC_REPORT" ] && command -v node >/dev/null 2>&1; then
|
|
454
511
|
_cc_verify_failed=0
|
|
455
512
|
CC_CSV_VERIFIED=$(printf '%s\n' "$CC_CSV" | node "$CC_REPORT" --csv-verified batch --root "$(git rev-parse --show-toplevel 2>/dev/null)") || _cc_verify_failed=1
|
|
@@ -460,8 +517,6 @@ else
|
|
|
460
517
|
fi
|
|
461
518
|
CC_CSV="$CC_CSV_VERIFIED"
|
|
462
519
|
fi
|
|
463
|
-
RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
|
|
464
|
-
[ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
|
|
465
520
|
# Fail closed when the ratchet library is missing. Sourcing a nonexistent
|
|
466
521
|
# file only prints "No such file or directory" and leaves
|
|
467
522
|
# evaluate_ccn_ratchet undefined; the verdict then came back empty, every
|
|
@@ -529,13 +584,43 @@ else
|
|
|
529
584
|
# this, a crashing or mis-invoked lizard yields 0 warnings and the gate
|
|
530
585
|
# reports PASSED on zero analysed functions -- the same silent-success
|
|
531
586
|
# failure this issue is about.
|
|
587
|
+
#
|
|
588
|
+
# #666: "no rows" has TWO causes and they must not share a verdict. A
|
|
589
|
+
# module of pure constants legitimately has zero functions, and lizard
|
|
590
|
+
# emits no row for it at all (measured: `lizard pure_data.py --csv` prints
|
|
591
|
+
# nothing and exits 0). That was reported as `lizard_output_unparsable`
|
|
592
|
+
# /WARN, which is the message for a BROKEN lizard -- so a healthy run on a
|
|
593
|
+
# data-only diff was indistinguishable from a tooling fault. The two are
|
|
594
|
+
# separated by a POSITIVE probe: ask lizard to describe a fixture known to
|
|
595
|
+
# contain a function. If it produces rows, lizard works and the input
|
|
596
|
+
# simply had no functions (SKIP, `no_functions_in_input`); if it does not,
|
|
597
|
+
# the tool really is broken (WARN, `lizard_output_unparsable`).
|
|
532
598
|
if ! printf '%s\n' "$CC_CSV_FLAT" | awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { found = 1; exit }
|
|
533
599
|
END { exit(found ? 0 : 1) }'; then
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
600
|
+
_lizard_probe_dir=$(mktemp -d 2>/dev/null || echo "${TMPDIR:-/tmp}")
|
|
601
|
+
_lizard_probe_file="$_lizard_probe_dir/xp-gate-lizard-probe.py"
|
|
602
|
+
printf 'def _xp_gate_probe(x):\n if x:\n return 1\n return 0\n' > "$_lizard_probe_file" 2>/dev/null
|
|
603
|
+
_lizard_probe_rows=0
|
|
604
|
+
if [ -f "$_lizard_probe_file" ]; then
|
|
605
|
+
_lizard_probe_rows=$(printf '%s\n' "$_lizard_probe_file" \
|
|
606
|
+
| lizard_batches --csv 2>/dev/null \
|
|
607
|
+
| awk -F, 'NF > 1 && $2 ~ /^[0-9]+$/ { c++ } END { print c + 0 }')
|
|
608
|
+
fi
|
|
609
|
+
rm -rf "$_lizard_probe_dir" 2>/dev/null || true
|
|
610
|
+
if [ "${_lizard_probe_rows:-0}" -gt 0 ]; then
|
|
611
|
+
echo "⏭️ SKIPPED - Complexity: the changed source files define no functions (pure data module)."
|
|
612
|
+
echo " Verified: lizard parsed a probe fixture normally, so this is an empty result set, not a broken tool."
|
|
613
|
+
GATE_3_STATUS="SKIP"
|
|
614
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
615
|
+
# shellcheck disable=SC2034
|
|
616
|
+
GATE_3_REASON="no_functions_in_input"
|
|
617
|
+
else
|
|
618
|
+
echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
|
|
619
|
+
GATE_3_STATUS="WARN"
|
|
620
|
+
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
621
|
+
# shellcheck disable=SC2034
|
|
622
|
+
GATE_3_REASON="lizard_output_unparsable"
|
|
623
|
+
fi
|
|
539
624
|
elif [ "$CC_WARNINGS" -gt 0 ]; then
|
|
540
625
|
echo "$CC_OUTPUT"
|
|
541
626
|
echo ""
|
package/adapters/gate-4.sh
CHANGED
|
@@ -46,7 +46,14 @@ java_coverage_note() {
|
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
48
|
[ -n "$_has_java" ] || return 0
|
|
49
|
-
|
|
49
|
+
# #628: `docs/` is not in the npm package's `files` list, so pointing a user at
|
|
50
|
+
# `docs/java-principles-coverage.md` was a dead path in every installed project --
|
|
51
|
+
# the note told them to read a file that does not exist on their machine. Cite the
|
|
52
|
+
# upstream URL instead, and keep the local path only when the doc is actually
|
|
53
|
+
# present (i.e. when running inside this repo).
|
|
54
|
+
local _where="https://github.com/boyingliu01/xp-gate/blob/main/docs/java-principles-coverage.md"
|
|
55
|
+
[ -f "docs/java-principles-coverage.md" ] && _where="docs/java-principles-coverage.md"
|
|
56
|
+
echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see $_where)"
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
2>&1 echo ""
|
|
@@ -165,42 +172,30 @@ else
|
|
|
165
172
|
rm -f "$PRINCIPLES_STDERR"
|
|
166
173
|
|
|
167
174
|
if [ "$PRINCIPLES_EXIT" -ge 2 ]; then
|
|
168
|
-
#
|
|
169
|
-
#
|
|
175
|
+
# #661: exit>=2 means the checker did not RUN (crash, module not found,
|
|
176
|
+
# bad invocation). This used to record SKIP, and pre-commit treats SKIP
|
|
177
|
+
# as non-blocking, so a tool failure was absorbed into the green light --
|
|
178
|
+
# the reproduction printed a module-not-found stack trace and then
|
|
179
|
+
# "✅ PASSED - Principles checker (no errors found)".
|
|
170
180
|
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
# The path conversion fixed that one cause; any other way of breaking
|
|
176
|
-
# the launch (missing tsx, corrupt module, OOM) lands here too, and all
|
|
177
|
-
# of them would have been absorbed into a green light.
|
|
181
|
+
# The distinction the gate must keep:
|
|
182
|
+
# - tool NOT INSTALLED -> SKIP is honest; nothing is being claimed.
|
|
183
|
+
# - tool PRESENT, CRASH -> WARN. Not a pass (nothing was checked) and
|
|
184
|
+
# not a silent skip (the user must know their code went unaudited).
|
|
178
185
|
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
echo "
|
|
184
|
-
echo "
|
|
185
|
-
echo "
|
|
186
|
-
|
|
186
|
+
# WARN rather than FAIL on purpose: the commit itself is not necessarily
|
|
187
|
+
# bad, the TOOL is broken, and blocking every commit until node/tsx is
|
|
188
|
+
# repaired would blame the change for an environment fault. What must
|
|
189
|
+
# never happen is the green light.
|
|
190
|
+
echo "⚠️ Principles checker did not run (exit ${PRINCIPLES_EXIT}) — this code is NOT verified."
|
|
191
|
+
echo " Gate 4 cannot claim a pass for a check that never executed."
|
|
192
|
+
echo " Fix the checker invocation above (or run 'npx xp-gate check principles .')."
|
|
193
|
+
echo "⏭️ WARN - Principles check (checker execution error)"
|
|
194
|
+
GATE_4_STATUS="WARN"
|
|
195
|
+
# Consumed by the sourcing parent, not here (see #548).
|
|
187
196
|
# shellcheck disable=SC2034
|
|
188
197
|
GATE_4_REASON="checker_execution_error"
|
|
189
198
|
rm -f "$PRINCIPLES_JSON"
|
|
190
|
-
if [ "${PRINCIPLES_SKIP_TOOL_FAULT:-0}" = "1" ]; then
|
|
191
|
-
echo "⚠️ ESCAPE VALVE: PRINCIPLES_SKIP_TOOL_FAULT=1 — downgrading to SKIP"
|
|
192
|
-
# Same asymmetry as Gate 5a: a downgrade is tolerable on a feature
|
|
193
|
-
# branch, never on main/master, where it would green-light the trunk.
|
|
194
|
-
_g4_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
|
|
195
|
-
if [ "$_g4_branch" = "main" ] || [ "$_g4_branch" = "master" ]; then
|
|
196
|
-
echo "❌ ESCAPE VALVE BLOCKED: PRINCIPLES_SKIP_TOOL_FAULT not allowed on $_g4_branch"
|
|
197
|
-
GATE_4_STATUS="FAIL"
|
|
198
|
-
exit 1
|
|
199
|
-
fi
|
|
200
|
-
GATE_4_STATUS="SKIP"
|
|
201
|
-
else
|
|
202
|
-
exit 1
|
|
203
|
-
fi
|
|
204
199
|
else
|
|
205
200
|
# Check severity levels. The reporter emits JSON.stringify(out, null, 2),
|
|
206
201
|
# i.e. `"severity": "warning"` WITH a space; tolerate any whitespace so a
|
package/adapters/gate-8.sh
CHANGED
|
@@ -59,10 +59,24 @@ fi
|
|
|
59
59
|
# revisions and reports "0 commits scanned" on a pre-commit run -- so a staged
|
|
60
60
|
# secret was never seen and Gate 8 always passed. `git --staged` scans
|
|
61
61
|
# `git diff --cached`, which is exactly the pre-commit payload (#449).
|
|
62
|
+
#
|
|
63
|
+
# #616: `--all` is a different question, and the index is the wrong place to ask
|
|
64
|
+
# it. Under `xp-gate check . --all` the user asked "is the whole tree clean", the
|
|
65
|
+
# index is usually empty, `git --staged` legitimately found nothing, and that
|
|
66
|
+
# nothing was counted as PASSED -- a secret sitting in a tracked file was
|
|
67
|
+
# invisible to the one command meant to find it. The runner already exports
|
|
68
|
+
# GATE_SCOPE (#589); this fragment just has to read it. `dir --no-git` scans the
|
|
69
|
+
# filesystem so tracked-but-unmodified files are covered.
|
|
62
70
|
GITLEAKS_REPORT="${TMPDIR:-/tmp}/gitleaks-report.json"
|
|
63
71
|
rm -f "$GITLEAKS_REPORT"
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
73
|
+
echo " Scope: --all (scanning the working tree, not the index)"
|
|
74
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD dir --no-git --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
75
|
+
GITLEAKS_EXIT=$?
|
|
76
|
+
else
|
|
77
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
78
|
+
GITLEAKS_EXIT=$?
|
|
79
|
+
fi
|
|
66
80
|
|
|
67
81
|
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
68
82
|
# Decide SKIP from the REPORT ARTIFACT, not from log wording. Parsing stdout
|
|
@@ -183,7 +197,22 @@ fi
|
|
|
183
197
|
# if-condition (errexit-exempt); a non-zero hook exit lands in the else branch.
|
|
184
198
|
if command -v detect-secrets-hook >/dev/null 2>&1 && [ -f ".secrets.baseline" ]; then
|
|
185
199
|
DS_TMP="$(dirname "$(mktemp -u)")/ds-output.$$"
|
|
186
|
-
|
|
200
|
+
# #616: same scope split as the gitleaks call above. Under `--all` the index is
|
|
201
|
+
# usually empty, so piping `git diff --cached -z` into xargs fed the hook zero
|
|
202
|
+
# files and it reported success without reading anything.
|
|
203
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
204
|
+
if git ls-files -z | xargs -0 detect-secrets-hook --baseline .secrets.baseline > "$DS_TMP" 2>&1; then
|
|
205
|
+
echo " ✅ detect-secrets PASSED - no new secrets beyond baseline (scope: --all)."
|
|
206
|
+
else
|
|
207
|
+
cat "$DS_TMP"
|
|
208
|
+
echo ""
|
|
209
|
+
echo " ❌ BLOCKED - detect-secrets found secrets not in .secrets.baseline."
|
|
210
|
+
echo " If these are false positives, regenerate the baseline (cross-platform):"
|
|
211
|
+
echo " detect-secrets scan --update .secrets.baseline && git add .secrets.baseline"
|
|
212
|
+
rm -f "$DS_TMP"
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
elif git diff --cached --name-only --diff-filter=ACM -z \
|
|
187
216
|
| xargs -0 detect-secrets-hook --baseline .secrets.baseline > "$DS_TMP" 2>&1; then
|
|
188
217
|
echo " ✅ detect-secrets PASSED - no new secrets beyond baseline."
|
|
189
218
|
else
|
package/adapters/gate-9.sh
CHANGED
|
@@ -32,8 +32,20 @@ if [ -z "$SEMGREP_CMD" ]; then
|
|
|
32
32
|
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
33
33
|
GATE_9_STATUS="WARN"
|
|
34
34
|
else
|
|
35
|
-
# Get staged files filtered to Semgrep-supported languages
|
|
36
|
-
|
|
35
|
+
# Get staged files filtered to Semgrep-supported languages.
|
|
36
|
+
#
|
|
37
|
+
# #616: under `--all` the index is usually empty, so the staged diff yielded no
|
|
38
|
+
# files and the gate reported SKIPPED -- "no supported language files changed"
|
|
39
|
+
# -- for a run whose whole purpose was to scan the tree. Same fail-open shape
|
|
40
|
+
# #589 fixed in Gate 3, one layer down: the CLI says the tree was checked and
|
|
41
|
+
# the fragment checks a diff that does not exist. The runner exports GATE_SCOPE;
|
|
42
|
+
# read it and enumerate tracked files instead.
|
|
43
|
+
if [ "${GATE_SCOPE:-diff}" = "all" ]; then
|
|
44
|
+
echo " Scope: --all (scanning tracked files, not the index)"
|
|
45
|
+
SEMGREP_FILES=$(git ls-files 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
46
|
+
else
|
|
47
|
+
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)
|
|
48
|
+
fi
|
|
37
49
|
|
|
38
50
|
if [ -z "$SEMGREP_FILES" ]; then
|
|
39
51
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
package/gate-12-file-hygiene.sh
CHANGED
|
@@ -36,13 +36,35 @@ fi
|
|
|
36
36
|
HYGIENE_ISSUES=0
|
|
37
37
|
HYGIENE_FILES=""
|
|
38
38
|
|
|
39
|
+
# Is this staged file binary (so line-based checks do not apply)?
|
|
40
|
+
#
|
|
41
|
+
# This replaces `grep -q $'\x00'`, which was a no-op guard: bash strips NUL from
|
|
42
|
+
# `$'\x00'`, so the pattern was the EMPTY string and `grep -q ""` matches any
|
|
43
|
+
# non-empty input. Every staged file was therefore judged binary and `continue`d,
|
|
44
|
+
# silently disabling Check 1 (trailing whitespace), Check 2 (missing EOF newline)
|
|
45
|
+
# and -- worst -- Check 3 (merge conflict markers, a BLOCK-level check). Checks 4
|
|
46
|
+
# and 5 lacked the guard, which is why the gate looked half-working.
|
|
47
|
+
#
|
|
48
|
+
# Ask git instead of guessing: `--numstat` reports a binary entry as `-\t-\t<path>`
|
|
49
|
+
# because git already made that determination. A byte-level NUL test cannot be
|
|
50
|
+
# written with a shell pattern at all (variables and command substitution both drop
|
|
51
|
+
# NUL), and `grep -I` only suppresses output on a binary input -- it does not make
|
|
52
|
+
# `grep -q` fail, which is why that shortcut still reported text.
|
|
53
|
+
is_binary_staged_file() {
|
|
54
|
+
local _file="$1"
|
|
55
|
+
case "$(git diff --cached --numstat -- "$_file" 2>/dev/null)" in
|
|
56
|
+
-[[:space:]]-*) return 0 ;; # binary: git could not count lines
|
|
57
|
+
*) return 1 ;; # text (or empty -- nothing for the line checks)
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
# --- Check 1: Trailing whitespace ---
|
|
40
62
|
TRAILING_WS_FILES=""
|
|
41
63
|
if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
42
64
|
while IFS= read -r file; do
|
|
43
65
|
[ -z "$file" ] && continue
|
|
44
|
-
# Skip binary files
|
|
45
|
-
if
|
|
66
|
+
# Skip binary files : they have no lines to check
|
|
67
|
+
if is_binary_staged_file "$file"; then
|
|
46
68
|
continue
|
|
47
69
|
fi
|
|
48
70
|
# Check staged content for trailing whitespace (spaces/tabs at end of lines)
|
|
@@ -69,7 +91,7 @@ if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
|
69
91
|
while IFS= read -r file; do
|
|
70
92
|
[ -z "$file" ] && continue
|
|
71
93
|
# Skip binary files
|
|
72
|
-
if
|
|
94
|
+
if is_binary_staged_file "$file"; then
|
|
73
95
|
continue
|
|
74
96
|
fi
|
|
75
97
|
# Check if file content ends with newline
|
|
@@ -108,7 +130,7 @@ else
|
|
|
108
130
|
while IFS= read -r file; do
|
|
109
131
|
[ -z "$file" ] && continue
|
|
110
132
|
# Skip binary files
|
|
111
|
-
if
|
|
133
|
+
if is_binary_staged_file "$file"; then
|
|
112
134
|
continue
|
|
113
135
|
fi
|
|
114
136
|
# Check for conflict markers in staged content
|
|
@@ -527,6 +527,18 @@ gate13_count_lint_findings() {
|
|
|
527
527
|
[ -n "$src" ] || src=$(cat)
|
|
528
528
|
printf '%s\n' "$src" | awk '
|
|
529
529
|
BEGIN { n = 0 }
|
|
530
|
+
# (#673) Maven 3.9.7+ reactor summary appends the packaging type per module:
|
|
531
|
+
# [INFO] whaleaudit-core [jar]
|
|
532
|
+
# `[pom]`/`[jar]` match the open-category rule below, so a 3-module reactor
|
|
533
|
+
# reported 4 javac findings on code with ZERO warnings -- and got worse with
|
|
534
|
+
# every module added. The marker set is closed (packaging types are an enum in
|
|
535
|
+
# Maven itself), so excluding it does not reopen the #659 fail-open hole that
|
|
536
|
+
# the open-category rule was introduced to close.
|
|
537
|
+
#
|
|
538
|
+
# Scoped to `[INFO]` lines: a javac diagnostic is never prefixed with the
|
|
539
|
+
# Maven [INFO] banner, so this cannot mask a real finding even when a category
|
|
540
|
+
# name collides with a packaging type.
|
|
541
|
+
/^\[INFO\].*\[(pom|jar|war|ear|rar|ejb|maven-plugin|par|test-jar|java-source|javadoc|zip|tar\.gz|tar\.bz2)\][[:space:]]*$/ { next }
|
|
530
542
|
# Any bracketed lowercase category: covers the whole javac -Xlint enum and any
|
|
531
543
|
# later addition to it. Restricted to lowercase/digit/hyphen so a Maven
|
|
532
544
|
# `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
|
package/gates/gate-3.ts
CHANGED
|
@@ -53,29 +53,85 @@ export function runGate3(input: GateInput): GateOutput {
|
|
|
53
53
|
|
|
54
54
|
messages.push('Checking complexity for source files...');
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
// CSV mode: the verdict comes from the numbers lizard uses, not its table
|
|
57
|
+
// formatting (#674). See countCcnWarnings() for why the old parser never matched.
|
|
58
|
+
const rawOutput = runLizardCsv(lizardCmd, lizardArgs, ccFiles, input.cwd);
|
|
59
|
+
const verdict = buildComplexityVerdict(countCcnWarnings(rawOutput), rawOutput);
|
|
60
|
+
messages.push(...verdict.messages);
|
|
61
|
+
recordAudit('gate-3', 'complexity', verdict.status, verdict.warnings, startMs);
|
|
62
|
+
return { status: verdict.status, messages, exitCode: verdict.exitCode };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run lizard in CSV mode and return its raw output (stdout + stderr). */
|
|
66
|
+
function runLizardCsv(cmd: string, baseArgs: string[], files: string[], cwd: string | undefined): string {
|
|
67
|
+
const result = runTool(cmd, [...baseArgs, '--csv', '-C', String(CCN_THRESHOLD), ...files], { cwd });
|
|
68
|
+
return result.stdout + result.stderr;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Turn a CCN count into the gate's verdict and messages.
|
|
73
|
+
*
|
|
74
|
+
* Split from runGate3 so the three-way outcome (WARN on unparsable output, FAIL
|
|
75
|
+
* on violations, PASS) is visible in one place and testable without driving
|
|
76
|
+
* lizard. Notably `!analysed` must NOT fall through to PASS: a lizard that
|
|
77
|
+
* produced no parsable row would otherwise report success on zero analysed
|
|
78
|
+
* functions -- the same silent-success failure as #674.
|
|
79
|
+
*/
|
|
80
|
+
export function buildComplexityVerdict(
|
|
81
|
+
counts: { warnings: number; analysed: boolean },
|
|
82
|
+
rawOutput: string
|
|
83
|
+
): { status: 'PASS' | 'FAIL' | 'WARN'; messages: string[]; exitCode: number; warnings: number } {
|
|
84
|
+
if (!counts.analysed) {
|
|
85
|
+
return {
|
|
86
|
+
status: 'WARN',
|
|
87
|
+
messages: ['⚠️ WARN - lizard produced no parsable results; complexity not verified'],
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
warnings: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (counts.warnings > 0) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'FAIL',
|
|
95
|
+
messages: [
|
|
96
|
+
rawOutput,
|
|
97
|
+
'',
|
|
98
|
+
`❌ BLOCKED - ${counts.warnings} functions with CCN > ${CCN_THRESHOLD} found.`,
|
|
99
|
+
`Refactor high-complexity functions to keep below ${CCN_THRESHOLD} complexity.`,
|
|
100
|
+
],
|
|
101
|
+
exitCode: 1,
|
|
102
|
+
warnings: counts.warnings,
|
|
103
|
+
};
|
|
74
104
|
}
|
|
105
|
+
return {
|
|
106
|
+
status: 'PASS',
|
|
107
|
+
messages: [`✅ PASSED - All functions within complexity threshold (${CCN_THRESHOLD}).`],
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
warnings: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
75
112
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Count functions whose CCN exceeds the threshold from lizard's --csv output.
|
|
115
|
+
*
|
|
116
|
+
* Split out so the parse can be tested against literal lizard output instead of
|
|
117
|
+
* by driving the tool: a parser bug here is invisible in exactly the same way
|
|
118
|
+
* (#674) -- it returns "clean" and the gate reports PASS.
|
|
119
|
+
*
|
|
120
|
+
* `analysed` distinguishes "lizard found nothing over the threshold" from
|
|
121
|
+
* "lizard produced nothing at all". Only the first is a pass.
|
|
122
|
+
*/
|
|
123
|
+
export function countCcnWarnings(csv: string): { warnings: number; analysed: boolean } {
|
|
124
|
+
let warnings = 0;
|
|
125
|
+
let analysed = false;
|
|
126
|
+
for (const line of csv.split('\n')) {
|
|
127
|
+
const columns = line.split(',');
|
|
128
|
+
if (columns.length < 2) continue;
|
|
129
|
+
// Location column carries ":name" style text, so only trust numeric CCN.
|
|
130
|
+
if (!/^\s*\d+\s*$/.test(columns[1])) continue;
|
|
131
|
+
analysed = true;
|
|
132
|
+
if (parseInt(columns[1], 10) > CCN_THRESHOLD) warnings++;
|
|
133
|
+
}
|
|
134
|
+
return { warnings, analysed };
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
// ── CLI Entry Point ──
|
package/gates/gate-8.ts
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Gate 8: Secret Scanning (gitleaks)
|
|
3
3
|
* Detects secrets (API keys, passwords, tokens) in staged files.
|
|
4
4
|
* TypeScript rewrite of githooks/adapters/gate-8.sh for cross-platform Node.js CLI.
|
|
5
|
+
*
|
|
6
|
+
* Scans the INDEX (`git --staged`), not committed history (#675, mirroring the
|
|
7
|
+
* adapter fix in #449). gitleaks `git --pre-commit` walks committed revisions; on a
|
|
8
|
+
* pre-commit run it reports "0 commits scanned" and exits 0. A staged secret
|
|
9
|
+
* therefore passed the CLI while the hook blocked it -- and the CLI is the path CI
|
|
10
|
+
* and the plugins use. `--staged` scans `git diff --cached`, exactly the
|
|
11
|
+
* pre-commit payload. Same tool, same payload, one word different, opposite verdict.
|
|
5
12
|
*/
|
|
6
13
|
import path from 'path';
|
|
7
14
|
import fs from 'fs';
|
|
@@ -29,20 +36,7 @@ export function runGate8(input: GateInput): GateOutput {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const gitleaksCmd = tool.via === 'npx' ? 'npx' : tool.path;
|
|
32
|
-
const gitleaksArgs = tool.via === 'npx'
|
|
33
|
-
|
|
34
|
-
// Config detection
|
|
35
|
-
const configPath = findGitleaksConfig(cwd);
|
|
36
|
-
if (configPath) {
|
|
37
|
-
gitleaksArgs.push(`--config=${configPath}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Cross-platform temp path (fixes /tmp issue on Windows)
|
|
41
|
-
const reportPath = path.join(getTempDir(), 'gitleaks-report.json');
|
|
42
|
-
|
|
43
|
-
// Run gitleaks on staged changes
|
|
44
|
-
gitleaksArgs.push('git', '--pre-commit', '--redact', '--no-banner',
|
|
45
|
-
'--report-format=json', `--report-path=${reportPath}`);
|
|
39
|
+
const gitleaksArgs = buildGitleaksArgs(tool.via === 'npx', findGitleaksConfig(cwd));
|
|
46
40
|
|
|
47
41
|
const result = runTool(gitleaksCmd, gitleaksArgs, { cwd });
|
|
48
42
|
|
|
@@ -53,33 +47,52 @@ export function runGate8(input: GateInput): GateOutput {
|
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
if (result.exitCode === 1) {
|
|
56
|
-
|
|
57
|
-
messages.push(result.stdout);
|
|
58
|
-
messages.push('');
|
|
59
|
-
messages.push('❌ BLOCKED - Secrets detected in staged files.');
|
|
60
|
-
messages.push('');
|
|
61
|
-
messages.push('Remediation options:');
|
|
62
|
-
messages.push(' 1. Remove the secret and use environment variables instead');
|
|
63
|
-
messages.push(' 2. Add a false positive to .gitleaks.toml allowlist');
|
|
64
|
-
messages.push(' 3. Use git secret or vault for sensitive data');
|
|
65
|
-
messages.push('');
|
|
66
|
-
messages.push('See: https://github.com/gitleaks/gitleaks');
|
|
50
|
+
messages.push(...secretsFoundMessages(result.stdout));
|
|
67
51
|
recordAudit('gate-8', 'secret-scanning', 'FAIL', 1, startMs);
|
|
68
52
|
return { status: 'FAIL', messages, exitCode: 1 };
|
|
69
53
|
}
|
|
70
54
|
|
|
71
|
-
// Runtime error
|
|
72
55
|
messages.push(` ⚠️ gitleaks exited with code ${result.exitCode} - skipping gate`);
|
|
73
56
|
messages.push(' ✅ Secret Scanning (SKIP, gitleaks error)');
|
|
74
57
|
recordAudit('gate-8', 'secret-scanning', 'SKIP', result.exitCode, startMs);
|
|
75
58
|
return { status: 'SKIP', messages, exitCode: 0 };
|
|
76
59
|
}
|
|
77
60
|
|
|
61
|
+
/** The report + remediation shown when gitleaks finds secrets (exit code 1). */
|
|
62
|
+
function secretsFoundMessages(stdout: string): string[] {
|
|
63
|
+
return [
|
|
64
|
+
stdout,
|
|
65
|
+
'',
|
|
66
|
+
'❌ BLOCKED - Secrets detected in staged files.',
|
|
67
|
+
'',
|
|
68
|
+
'Remediation options:',
|
|
69
|
+
' 1. Remove the secret and use environment variables instead',
|
|
70
|
+
' 2. Add a false positive to .gitleaks.toml allowlist',
|
|
71
|
+
' 3. Use git secret or vault for sensitive data',
|
|
72
|
+
'',
|
|
73
|
+
'See: https://github.com/gitleaks/gitleaks',
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
78
77
|
function findGitleaksConfig(cwd: string): string | null {
|
|
79
78
|
const configPath = path.join(cwd, '.gitleaks.toml');
|
|
80
79
|
return fs.existsSync(configPath) ? configPath : null;
|
|
81
80
|
}
|
|
82
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Assemble the gitleaks argument vector.
|
|
84
|
+
*
|
|
85
|
+
* Report goes to a cross-platform temp path (the old hardcoded `/tmp` broke on
|
|
86
|
+
* Windows), and the scan reads the INDEX -- see the file header for why (#675).
|
|
87
|
+
*/
|
|
88
|
+
function buildGitleaksArgs(viaNpx: boolean, configPath: string | null): string[] {
|
|
89
|
+
const args = viaNpx ? ['-y', 'gitleaks'] : [];
|
|
90
|
+
if (configPath) args.push(`--config=${configPath}`);
|
|
91
|
+
args.push('git', '--staged', '--redact', '--no-banner',
|
|
92
|
+
'--report-format=json', `--report-path=${path.join(getTempDir(), 'gitleaks-report.json')}`);
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
// ── CLI Entry Point ──
|
|
84
97
|
if (process.argv.includes('--run')) {
|
|
85
98
|
const cwdIdx = process.argv.indexOf('--cwd');
|
package/gates/gate-9.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate
|
|
2
|
+
* Gate 10: Semgrep SAST Security Scan (file name predates the #677 renumber)
|
|
3
3
|
* Detects security vulnerabilities in staged files.
|
|
4
4
|
* For PowerShell projects: uses PSScriptAnalyzer security rules as alternative.
|
|
5
5
|
* TypeScript rewrite of githooks/adapters/gate-9.sh for cross-platform Node.js CLI.
|
|
@@ -36,7 +36,7 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
36
36
|
const messages: string[] = [];
|
|
37
37
|
|
|
38
38
|
messages.push('');
|
|
39
|
-
messages.push('→ Gate
|
|
39
|
+
messages.push('→ Gate 10: Semgrep SAST Security Scan...');
|
|
40
40
|
|
|
41
41
|
// PowerShell project: use PSScriptAnalyzer security rules
|
|
42
42
|
if (isPowerShellProject(input.projectLang)) {
|
|
@@ -49,7 +49,7 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
49
49
|
messages.push(' ⚠️ WARN - semgrep not installed — SAST scanning unavailable');
|
|
50
50
|
messages.push(' Install: brew install semgrep (macOS) | pip install semgrep (Linux/Windows)');
|
|
51
51
|
messages.push(' Pre-cache rules: semgrep --config=' + SEMGREP_RULESET);
|
|
52
|
-
messages.push(' Gate
|
|
52
|
+
messages.push(' Gate 10: SAST Security (WARN, semgrep not installed)');
|
|
53
53
|
recordAudit('gate-9', 'sast-security', 'WARN', 0, startMs);
|
|
54
54
|
return { status: 'WARN', messages, exitCode: 0 };
|
|
55
55
|
}
|
|
@@ -242,7 +242,7 @@ function runPowerShellSAST(
|
|
|
242
242
|
const ps = detectPowerShell();
|
|
243
243
|
if (!ps.available) {
|
|
244
244
|
messages.push(' ⚠️ WARN - PowerShell not available — SAST scanning unavailable');
|
|
245
|
-
messages.push(' Gate
|
|
245
|
+
messages.push(' Gate 10: SAST Security (WARN, PowerShell not installed)');
|
|
246
246
|
recordAudit('gate-9', 'sast-security', 'WARN', 0, startMs);
|
|
247
247
|
return { status: 'WARN', messages, exitCode: 0 };
|
|
248
248
|
}
|