@boyingliu01/xp-gate 0.31.1 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +10 -1
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +10 -1
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +371 -163
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/hooks/pre-commit
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
fi
|
|
2
|
+
_LE_CR=$(tr -cd '\r' < "$0" | wc -c | tr -d '[:space:]'); test "${_LE_CR:-0}" -eq 0 || { printf '%s\n' "======================================================================" "FATAL: hook has CRLF line endings -- bash cannot parse it (#546)." " file: $0" " fix : tr -d '\\r' < \"$0\" > \"$0.tmp\" && mv \"$0.tmp\" \"$0\" && chmod +x \"$0\"" "Or re-sync the installed copy: xp-gate doctor --sync-hooks" "======================================================================" >&2; exit 1; }
|
|
3
|
+
#
|
|
4
|
+
# The line above is the #546 line-ending self-check. It MUST stay on the first
|
|
5
|
+
# executable line, as a SINGLE physical line, using no construct whose keyword
|
|
6
|
+
# needs a line boundary. All three are load-bearing:
|
|
7
|
+
#
|
|
8
|
+
# * A CRLF rewrite puts \r at the end of EVERY line. Any construct whose
|
|
9
|
+
# keywords must match across lines -- if/then/fi, case/esac, for/do/done --
|
|
10
|
+
# then fails to PARSE and none of it runs. The original #546 implementation
|
|
11
|
+
# was a nested if/fi spanning 11 lines: it could not survive the very
|
|
12
|
+
# condition it detects. On a CRLF copy bash reported
|
|
13
|
+
# line 76: syntax error near unexpected token `$'{\r''
|
|
14
|
+
# blaming an innocent function far below -- which is exactly the failure #546
|
|
15
|
+
# set out to prevent. It looked correct locally only because that bash build
|
|
16
|
+
# deferred the error past the first command.
|
|
17
|
+
# * Even on one line, `if ... then ... fi` ends in `fi\r`, and `then\r` / `fi\r`
|
|
18
|
+
# are ordinary words to the tokenizer. So the check uses only
|
|
19
|
+
# `test ... || { ...; exit 1; }`: the `{` already follows `; `, so a trailing
|
|
20
|
+
# \r on the line cannot change how it tokenizes.
|
|
21
|
+
# * The CR count is byte-wise (`tr -cd`). `grep -c $'\r'` reports 0 matches
|
|
22
|
+
# through the Git-Bash/MSYS layer even on files that carry CR bytes, which is
|
|
23
|
+
# what made earlier attempts at this fixture look green while CI stayed red.
|
|
24
|
+
#
|
|
25
|
+
# Verified: an LF copy runs through (0 CR bytes); a CRLF copy prints the FATAL
|
|
26
|
+
# diagnosis and exits 1 with no syntax error.
|
|
18
27
|
# OpenCode Quality Gates - Pre-Commit Hook - 12 numbered gates (Gate 0–11)
|
|
19
28
|
#
|
|
20
29
|
# DESIGN PRINCIPLE: Tool unavailable = SKIP (graceful degradation), tool available + check fails = BLOCK
|
|
@@ -386,6 +395,28 @@ source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/sprint-gate-report.sh"
|
|
|
386
395
|
# unit-testable without executing this hook.
|
|
387
396
|
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/coverage-total.sh"
|
|
388
397
|
|
|
398
|
+
# Issue #585: Gate 5c's spec/test alignment check. It used to sit inside
|
|
399
|
+
# `if [ -f req-ids.json ] && command -v jq`, so a fresh clone (where .sprint-state/
|
|
400
|
+
# does not exist) printed nothing at all; and its test discovery only matched
|
|
401
|
+
# JS/TS. Sourced as a library so both contracts are unit-testable.
|
|
402
|
+
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/gate-5c-alignment.sh"
|
|
403
|
+
|
|
404
|
+
# Issue #593: on Windows git-bash does not see the toolchains installed on the
|
|
405
|
+
# Windows side (%APPDATA%\npm, the node install dir, pip --user's Scripts, ~/bin),
|
|
406
|
+
# so four gates reported installed tools as missing and two of those reports were
|
|
407
|
+
# false GREENS. One bootstrap, appended (never prepended) so the caller's PATH
|
|
408
|
+
# order stays authoritative.
|
|
409
|
+
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/toolchain-path.sh"
|
|
410
|
+
bootstrap_toolchain_path
|
|
411
|
+
|
|
412
|
+
# Issue #581: POSIX -> native path conversion for the absolute paths handed to node.exe
|
|
413
|
+
# and tsx. On WSL a leading `/` resolves to `<current drive>:\` inside node, so
|
|
414
|
+
# `$HOME/.config/...` became `D:\home\...` and Gate 9 reported the module-not-found as
|
|
415
|
+
# "Build integrity check failed" -- advice to fix the user's type errors. Kept in a
|
|
416
|
+
# library because Gate 8 needed the same conversion for gitleaks (#582) and both were
|
|
417
|
+
# solved inline otherwise.
|
|
418
|
+
source_gate_lib "${AUDIT_SCRIPT_DIR}/lib/native-path.sh"
|
|
419
|
+
|
|
389
420
|
# Issue #399: Python smart test selection — strategy decision + related-test
|
|
390
421
|
# lookup, kept in a library so the truth table and the path conventions are
|
|
391
422
|
# unit-testable without executing this hook.
|
|
@@ -1901,7 +1932,7 @@ else
|
|
|
1901
1932
|
done
|
|
1902
1933
|
fi
|
|
1903
1934
|
fi # End: if ! any_changed_files_match (Issue #312 skip)
|
|
1904
|
-
GATE_1_STATUS="${GATE_1_STATUS:-
|
|
1935
|
+
GATE_1_STATUS="${GATE_1_STATUS:-SKIP}"
|
|
1905
1936
|
GATE_1_TOOL="${PROJECT_LANGS}"
|
|
1906
1937
|
# The java branch may set GATE_1_STATUS=WARN + a GATE_1_DETAIL warn reason
|
|
1907
1938
|
# (engine unavailable / soft or report downgrade); route it into the audit
|
|
@@ -1945,13 +1976,30 @@ else
|
|
|
1945
1976
|
if [ -n "$JSCPD_CONFIG" ]; then
|
|
1946
1977
|
JSCPD_ARGS=(--config "$JSCPD_CONFIG")
|
|
1947
1978
|
else
|
|
1948
|
-
|
|
1979
|
+
# #591: with no project config, jscpd's own defaults treat .yaml/.txt/.log as
|
|
1980
|
+
# scannable source, so an exemptions manifest, a golden snapshot and two build
|
|
1981
|
+
# logs were reported as "duplicated code" (measured: 48.72% on a fixture with
|
|
1982
|
+
# no source in it). Print the baseline that is actually in force, or the user
|
|
1983
|
+
# cannot tell what was scanned.
|
|
1984
|
+
echo "ℹ️ No jscpd.conf.json found - using the xp-gate default ignore set"
|
|
1985
|
+
echo " $(jscpd_default_ignore_patterns)"
|
|
1949
1986
|
fi
|
|
1987
|
+
# #591: drop data/evidence/build-output paths before the scan. Real source
|
|
1988
|
+
# extensions are untouched, so this narrows the noise, not the gate.
|
|
1989
|
+
JSCPD_SCAN_FILES=""
|
|
1990
|
+
while IFS= read -r _jf; do
|
|
1991
|
+
[ -n "$_jf" ] || continue
|
|
1992
|
+
if ! jscpd_is_default_ignored "$_jf"; then
|
|
1993
|
+
JSCPD_SCAN_FILES="$JSCPD_SCAN_FILES $_jf"
|
|
1994
|
+
fi
|
|
1995
|
+
done <<EOF_CHANGED
|
|
1996
|
+
$CHANGED_FILES
|
|
1997
|
+
EOF_CHANGED
|
|
1950
1998
|
JSCPD_OUTPUT=$(mktemp)
|
|
1951
1999
|
JSCPD_EXIT=0
|
|
1952
|
-
# shellcheck disable=SC2086 # pre-existing splitting of the file list; #477
|
|
1953
|
-
jscpd ${JSCPD_ARGS[@]+"${JSCPD_ARGS[@]}"} $
|
|
1954
|
-
head -30 "$JSCPD_OUTPUT"
|
|
2000
|
+
# shellcheck disable=SC2086 # pre-existing splitting of the file list; #477/#591 change the scan set, not the splitting
|
|
2001
|
+
jscpd ${JSCPD_ARGS[@]+"${JSCPD_ARGS[@]}"} $JSCPD_SCAN_FILES > "$JSCPD_OUTPUT" 2>&1 || JSCPD_EXIT=$?
|
|
2002
|
+
head -30 "$JSCPD_OUTPUT" || true
|
|
1955
2003
|
JSCPD_OUTCOME=$(classify_jscpd_run "$JSCPD_EXIT" "$(cat "$JSCPD_OUTPUT")")
|
|
1956
2004
|
rm -f "$JSCPD_OUTPUT"
|
|
1957
2005
|
if [ "$JSCPD_OUTCOME" = "duplicates" ]; then
|
|
@@ -2015,13 +2063,35 @@ else
|
|
|
2015
2063
|
;;
|
|
2016
2064
|
|
|
2017
2065
|
*)
|
|
2018
|
-
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
|
|
2066
|
+
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$' || true)
|
|
2019
2067
|
if [ -n "$SOURCE_FILES" ]; then
|
|
2020
2068
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
2021
2069
|
exit 1
|
|
2022
2070
|
fi
|
|
2023
2071
|
echo "Running jscpd for duplicate code detection..."
|
|
2024
|
-
|
|
2072
|
+
# #591 point 4: this branch piped straight into `head`, so jscpd's exit status
|
|
2073
|
+
# was replaced by head's (and SIGPIPE under pipefail) — inconsistent with the
|
|
2074
|
+
# temp-file treatment the typescript branch already had from #477/#392. The
|
|
2075
|
+
# #591 default ignore set applies here too.
|
|
2076
|
+
GENERIC_JSCPD_FILES=""
|
|
2077
|
+
while IFS= read -r _gf; do
|
|
2078
|
+
[ -n "$_gf" ] || continue
|
|
2079
|
+
if ! jscpd_is_default_ignored "$_gf"; then
|
|
2080
|
+
GENERIC_JSCPD_FILES="$GENERIC_JSCPD_FILES $_gf"
|
|
2081
|
+
fi
|
|
2082
|
+
done <<EOF_GENERIC
|
|
2083
|
+
$SOURCE_FILES
|
|
2084
|
+
EOF_GENERIC
|
|
2085
|
+
if [ -n "$GENERIC_JSCPD_FILES" ]; then
|
|
2086
|
+
GENERIC_JSCPD_TMP=$(mktemp)
|
|
2087
|
+
# shellcheck disable=SC2086
|
|
2088
|
+
jscpd $GENERIC_JSCPD_FILES > "$GENERIC_JSCPD_TMP" 2>&1 || GENERIC_JSCPD_EXIT=$?
|
|
2089
|
+
head -15 "$GENERIC_JSCPD_TMP" || true
|
|
2090
|
+
rm -f "$GENERIC_JSCPD_TMP"
|
|
2091
|
+
if [ "${GENERIC_JSCPD_EXIT:-0}" -ne 0 ]; then
|
|
2092
|
+
echo "⚠️ jscpd exited with code $GENERIC_JSCPD_EXIT (see output above)"
|
|
2093
|
+
fi
|
|
2094
|
+
fi
|
|
2025
2095
|
echo "✅ PASSED - jscpd check completed."
|
|
2026
2096
|
else
|
|
2027
2097
|
echo "✅ PASSED - No source files detected for duplicate checking."
|
|
@@ -2393,80 +2463,24 @@ else
|
|
|
2393
2463
|
fi
|
|
2394
2464
|
|
|
2395
2465
|
# ============================================================================
|
|
2396
|
-
#
|
|
2397
|
-
#
|
|
2466
|
+
# ============================================================================
|
|
2467
|
+
# Gate 5c: Semantic Annotation Check (#585)
|
|
2468
|
+
#
|
|
2469
|
+
# The check itself lives in lib/gate-5c-alignment.sh so its contract is
|
|
2470
|
+
# unit-testable without executing the hook. Two defects it fixes, both of which
|
|
2471
|
+
# made this gate a decoration:
|
|
2472
|
+
# 1. The whole body sat inside `if [ -f req-ids.json ] && command -v jq`.
|
|
2473
|
+
# .sprint-state/ is gitignored, so on a fresh clone the gate printed
|
|
2474
|
+
# NOTHING — not PASS, not FAIL, not SKIP — and the report had no slot for
|
|
2475
|
+
# it. Silence read as success, the same defect as #449/#499/#570.
|
|
2476
|
+
# 2. Test discovery matched only JS/TS, so Pester's *.Tests.ps1, pytest's
|
|
2477
|
+
# test_*.py, Go's *_test.go, Java's *Test.java and Kotlin's *Tests.kt found
|
|
2478
|
+
# nothing and the gate reported a vacuous "0 annotated, 0 uncovered".
|
|
2398
2479
|
#
|
|
2399
|
-
#
|
|
2400
|
-
# If req-ids.json is absent or stale, degrades to SKIP.
|
|
2401
|
-
# If present: greps staged tests for @test REQ-XXX, cross-references against
|
|
2402
|
-
# the pre-compiled REQ ID list. BLOCKs if any REQ ID doesn't exist in spec.
|
|
2403
|
-
# If any REQ in spec has no annotated test, emits visible [FAIL] message
|
|
2404
|
-
# (exit 0, appears in hook summary) — full alignment check runs at CLI time.
|
|
2480
|
+
# GATE_5C_STATUS is now always PASS, SKIP or BLOCK — never empty.
|
|
2405
2481
|
# ============================================================================
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
# Check staleness: req-ids.json must be newer than specification.yaml
|
|
2409
|
-
SPEC_FILE="specification.yaml"
|
|
2410
|
-
REQ_IDS_STALE=false
|
|
2411
|
-
if [ -f "$SPEC_FILE" ]; then
|
|
2412
|
-
if [ "$REQ_IDS_FILE" -ot "$SPEC_FILE" ]; then
|
|
2413
|
-
REQ_IDS_STALE=true
|
|
2414
|
-
fi
|
|
2415
|
-
fi
|
|
2416
|
-
|
|
2417
|
-
if [ "$REQ_IDS_STALE" = true ]; then
|
|
2418
|
-
echo "⚠️ Gate 5c: req-ids.json is stale (specification.yaml updated since last check-alignment)"
|
|
2419
|
-
echo " Run 'xp-gate check-alignment' to regenerate before next commit."
|
|
2420
|
-
else
|
|
2421
|
-
CHANGED_TEST_FILES_ANNOTATED=$(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs)$' || true)
|
|
2422
|
-
if [ -n "$CHANGED_TEST_FILES_ANNOTATED" ]; then
|
|
2423
|
-
echo " → Checking @test REQ-XXX annotations..."
|
|
2424
|
-
ANNOTATION_ERRORS=0
|
|
2425
|
-
for tf in $CHANGED_TEST_FILES_ANNOTATED; do
|
|
2426
|
-
if [ -f "$tf" ]; then
|
|
2427
|
-
# Extract @test REQ-XXX annotations from staged test file (word-bounded)
|
|
2428
|
-
ANNOTATED_REQS=$(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' | sort -u || true)
|
|
2429
|
-
for req_id in $ANNOTATED_REQS; do
|
|
2430
|
-
# Cross-reference against pre-compiled REQ ID list
|
|
2431
|
-
FOUND=$(jq -r --arg rid "$req_id" '.[] | select(. == $rid)' "$REQ_IDS_FILE" 2>/dev/null || true)
|
|
2432
|
-
if [ -z "$FOUND" ]; then
|
|
2433
|
-
echo "❌ Gate 5c: $tf — @test $req_id: REQ ID not found in specification.yaml"
|
|
2434
|
-
ANNOTATION_ERRORS=$((ANNOTATION_ERRORS + 1))
|
|
2435
|
-
fi
|
|
2436
|
-
done
|
|
2437
|
-
fi
|
|
2438
|
-
done
|
|
2439
|
-
|
|
2440
|
-
if [ "$ANNOTATION_ERRORS" -gt 0 ]; then
|
|
2441
|
-
echo ""
|
|
2442
|
-
echo "❌ $ANNOTATION_ERRORS test annotation(s) reference non-existent REQ IDs — commit BLOCKED"
|
|
2443
|
-
echo " Fix: Verify REQ IDs against specification.yaml, or run 'xp-gate check-alignment'"
|
|
2444
|
-
exit 1
|
|
2445
|
-
fi
|
|
2446
|
-
|
|
2447
|
-
# Check reverse: any REQ in spec with no annotated test → visible FAIL (non-blocking)
|
|
2448
|
-
ALL_SPEC_REQS=$(jq -r '.[]' "$REQ_IDS_FILE" 2>/dev/null)
|
|
2449
|
-
ANNOTATED_ALL=""
|
|
2450
|
-
for tf in $(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs)$' || true); do
|
|
2451
|
-
if [ -f "$tf" ]; then
|
|
2452
|
-
ANNOTATED_ALL="$ANNOTATED_ALL $(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' || true)"
|
|
2453
|
-
fi
|
|
2454
|
-
done
|
|
2455
|
-
UNTESTED_COUNT=0
|
|
2456
|
-
for spec_req in $ALL_SPEC_REQS; do
|
|
2457
|
-
if ! echo "$ANNOTATED_ALL" | grep -qF "$spec_req"; then
|
|
2458
|
-
UNTESTED_COUNT=$((UNTESTED_COUNT + 1))
|
|
2459
|
-
fi
|
|
2460
|
-
done
|
|
2461
|
-
if [ "$UNTESTED_COUNT" -gt 0 ]; then
|
|
2462
|
-
echo "⚠️ [FAIL] Gate 5c: $UNTESTED_COUNT REQ(s) in specification.yaml have no annotated tests"
|
|
2463
|
-
echo " This is non-blocking. Run 'xp-gate check-alignment' for full alignment report."
|
|
2464
|
-
fi
|
|
2465
|
-
|
|
2466
|
-
echo " ✅ @test annotation check complete."
|
|
2467
|
-
fi
|
|
2468
|
-
fi
|
|
2469
|
-
fi
|
|
2482
|
+
GATE_5C_STATUS=""
|
|
2483
|
+
gate_5c_alignment || exit 1
|
|
2470
2484
|
|
|
2471
2485
|
# ============================================================================
|
|
2472
2486
|
# v0.14.3+ (Issue #312): Early skip when no code files changed.
|
|
@@ -2841,7 +2855,7 @@ else
|
|
|
2841
2855
|
go test -coverprofile=coverage.out ./... 2>/dev/null
|
|
2842
2856
|
TOTAL_COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total:" | awk '{print substr($3, 1, length($3)-1)}')
|
|
2843
2857
|
if [ -n "$TOTAL_COVERAGE" ] && (( $(echo "$TOTAL_COVERAGE < 80" | bc -l 2>/dev/null || echo "0") )); then
|
|
2844
|
-
echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2858
|
+
echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2845
2859
|
exit 1
|
|
2846
2860
|
fi
|
|
2847
2861
|
echo "Go coverage: $TOTAL_COVERAGE%"
|
|
@@ -2885,38 +2899,84 @@ else
|
|
|
2885
2899
|
COVERAGE_ENFORCED=false
|
|
2886
2900
|
|
|
2887
2901
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
2902
|
+
# #570: every comparison below reads $COVERAGE_THRESHOLD, and the messages
|
|
2903
|
+
# quote it. If it ever arrives empty (a helper called outside the hook, a test
|
|
2904
|
+
# harness that sources only the case block), `[ "$pct" -lt "" ]` is false and
|
|
2905
|
+
# `awk 'pct < thr'` with an empty thr is false too — the gate would PASS every
|
|
2906
|
+
# value, i.e. exactly the fail-open this issue is about. Fall back to the
|
|
2907
|
+
# historical default so an unset variable can never widen the gate.
|
|
2908
|
+
#
|
|
2909
|
+
# Kept INSIDE the loop (immediately before the case) so a harness that extracts
|
|
2910
|
+
# only `case ... esac` still gets the guarantee it depends on.
|
|
2911
|
+
if ! [[ "${COVERAGE_THRESHOLD:-}" =~ ^[0-9]+$ ]]; then
|
|
2912
|
+
COVERAGE_THRESHOLD=80
|
|
2913
|
+
COVERAGE_THRESHOLD_SOURCE="default (threshold was unset at Stage 2)"
|
|
2914
|
+
fi
|
|
2888
2915
|
case "$CURRENT_LANG" in
|
|
2889
2916
|
"typescript")
|
|
2890
2917
|
COVERAGE_ENFORCED=true
|
|
2891
2918
|
if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
|
|
2892
2919
|
echo "✅ PASSED - Coverage check skipped (no test/source files changed)."
|
|
2893
2920
|
elif [ -f "coverage/coverage-summary.json" ]; then
|
|
2921
|
+
# #594: reject NaN at the SOURCE, not after the fact. Math.round() does
|
|
2922
|
+
# not fail on a non-numeric input -- it returns NaN and console.log prints
|
|
2923
|
+
# "NaN" -- so istanbul's "Unknown" (a file with no executable lines) sailed
|
|
2924
|
+
# past both the `!= "parse_error"` and `-n` guards. Emit `no_data`, which
|
|
2925
|
+
# the guard below already treats as "could not measure".
|
|
2894
2926
|
COVERAGE_PERCENT=$(node -e "
|
|
2895
2927
|
try {
|
|
2896
2928
|
const fs = require('fs');
|
|
2897
2929
|
const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
|
|
2898
|
-
|
|
2930
|
+
const pct = data && data.total && data.total.lines && data.total.lines.pct;
|
|
2931
|
+
console.log(Number.isFinite(pct) ? Math.round(pct) : 'no_data');
|
|
2899
2932
|
} catch(e) { console.log('parse_error'); }
|
|
2900
2933
|
" 2>/dev/null)
|
|
2901
|
-
|
|
2902
|
-
|
|
2934
|
+
# #594: `-n` was not enough. `[ "NaN" -lt 80 ]` is not false in bash -- it
|
|
2935
|
+
# is an ERROR (exit 2, "integer expression expected"), and an `if` only
|
|
2936
|
+
# tells zero from non-zero, so the error took the else branch: the PASS
|
|
2937
|
+
# branch. Require an actual integer before comparing anything.
|
|
2938
|
+
if [[ "$COVERAGE_PERCENT" =~ ^[0-9]+$ ]]; then
|
|
2939
|
+
# #570: default the threshold at the comparison site too — an empty
|
|
2940
|
+
# value makes test(1) fail, the branch is skipped, and low coverage is
|
|
2941
|
+
# reported as a pass.
|
|
2942
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
2903
2943
|
# Only a subset test run (smart selection) leaves partial coverage data;
|
|
2904
2944
|
# a full suite run must block on <80% even when many test files changed.
|
|
2905
2945
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2906
2946
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2907
2947
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
2908
2948
|
else
|
|
2909
|
-
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2949
|
+
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2910
2950
|
exit 1
|
|
2911
2951
|
fi
|
|
2912
2952
|
else
|
|
2913
|
-
echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
2953
|
+
echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
2914
2954
|
fi
|
|
2915
2955
|
else
|
|
2916
|
-
|
|
2956
|
+
# #570: an unparseable report is not a pass. Silently continuing here
|
|
2957
|
+
# meant a corrupt or unexpected-shape coverage-summary.json disabled the
|
|
2958
|
+
# TypeScript coverage gate while the run still looked clean.
|
|
2959
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2960
|
+
echo "⚠️ Could not parse TypeScript coverage from coverage/coverage-summary.json (subset test run) — not blocking."
|
|
2961
|
+
else
|
|
2962
|
+
echo "❌ BLOCKED - Could not parse TypeScript coverage from coverage/coverage-summary.json."
|
|
2963
|
+
echo " The report exists but carries no usable total.lines.pct, so the"
|
|
2964
|
+
echo " ${COVERAGE_THRESHOLD:-80}% floor could not be checked."
|
|
2965
|
+
exit 1
|
|
2966
|
+
fi
|
|
2917
2967
|
fi
|
|
2918
2968
|
else
|
|
2919
|
-
|
|
2969
|
+
# #570: the report file is absent, so nothing was measured. Warning and
|
|
2970
|
+
# continuing made "delete coverage/coverage-summary.json" an effective way
|
|
2971
|
+
# to switch this gate off.
|
|
2972
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2973
|
+
echo "⚠️ coverage/coverage-summary.json not found (subset test run) — not blocking."
|
|
2974
|
+
else
|
|
2975
|
+
echo "❌ BLOCKED - coverage/coverage-summary.json not found."
|
|
2976
|
+
echo " No coverage could be measured, so the ${COVERAGE_THRESHOLD:-80}% floor could not be"
|
|
2977
|
+
echo " checked. Run 'npx vitest run --coverage' before committing."
|
|
2978
|
+
exit 1
|
|
2979
|
+
fi
|
|
2920
2980
|
fi
|
|
2921
2981
|
;;
|
|
2922
2982
|
"python")
|
|
@@ -2965,24 +3025,39 @@ else
|
|
|
2965
3025
|
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
2966
3026
|
# Compare on the unrounded value so 79.5% cannot pass as 80%.
|
|
2967
3027
|
COVERAGE_ROUNDED=${COVERAGE_PERCENT%.*}
|
|
2968
|
-
if awk -v pct="$COVERAGE_PERCENT" 'BEGIN { exit !(pct <
|
|
3028
|
+
if awk -v pct="$COVERAGE_PERCENT" -v thr="${COVERAGE_THRESHOLD:-80}" 'BEGIN { exit !(pct < thr) }'; then
|
|
2969
3029
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2970
3030
|
echo "⚠️ Partial coverage ${COVERAGE_ROUNDED}% (subset test run, not full suite)."
|
|
2971
3031
|
echo " Run the full test suite for a complete Java coverage check."
|
|
2972
3032
|
else
|
|
2973
|
-
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
3033
|
+
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2974
3034
|
exit 1
|
|
2975
3035
|
fi
|
|
2976
3036
|
else
|
|
2977
|
-
echo "Java coverage: ${COVERAGE_ROUNDED}% ✅ (≥ 80%)"
|
|
3037
|
+
echo "Java coverage: ${COVERAGE_ROUNDED}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
2978
3038
|
fi
|
|
2979
3039
|
else
|
|
2980
|
-
|
|
3040
|
+
# #570: a JaCoCo report that yields no instruction data measured
|
|
3041
|
+
# nothing. Warning and continuing let the gate pass on no evidence.
|
|
3042
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3043
|
+
echo "⚠️ Could not parse Java coverage from ${jacoco_csv} (subset test run) — not blocking."
|
|
3044
|
+
else
|
|
3045
|
+
echo "❌ BLOCKED - Could not parse Java coverage from ${jacoco_csv} (no instruction data),"
|
|
3046
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3047
|
+
exit 1
|
|
3048
|
+
fi
|
|
2981
3049
|
fi
|
|
2982
3050
|
else
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3051
|
+
# #570: no report at all is the fail-open path this issue was filed for.
|
|
3052
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3053
|
+
echo "⚠️ No JaCoCo report found (subset test run) — not blocking."
|
|
3054
|
+
else
|
|
3055
|
+
echo "❌ BLOCKED - No JaCoCo report found, so the ${COVERAGE_THRESHOLD:-80}% Java coverage"
|
|
3056
|
+
echo " floor cannot be verified. Run 'mvn verify' or 'gradle jacocoTestReport'"
|
|
3057
|
+
echo " so that target/site/jacoco/jacoco.csv exists (see also: JaCoCo detection"
|
|
3058
|
+
echo " in adapters/java.sh)."
|
|
3059
|
+
exit 1
|
|
3060
|
+
fi
|
|
2986
3061
|
fi
|
|
2987
3062
|
;;
|
|
2988
3063
|
"go")
|
|
@@ -2995,23 +3070,52 @@ else
|
|
|
2995
3070
|
COVERAGE_ENFORCED=true
|
|
2996
3071
|
if [ -f "coverage/lcov.info" ]; then
|
|
2997
3072
|
COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT="0"
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3073
|
+
# #570: `-ge 0`, not `!= "0"`. A measured 0% is DATA — the tool ran and
|
|
3074
|
+
# covered nothing — so it must reach the threshold comparison instead of
|
|
3075
|
+
# being folded into "could not determine".
|
|
3076
|
+
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" -ge 0 ] 2>/dev/null; then
|
|
3077
|
+
# #570: default at the comparison site; an empty threshold makes
|
|
3078
|
+
# test(1) fail and silently skips the BLOCK.
|
|
3079
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
3080
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3081
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3082
|
+
echo " Run the full test suite for a complete Flutter/Dart coverage check."
|
|
3083
|
+
else
|
|
3084
|
+
echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3085
|
+
exit 1
|
|
3086
|
+
fi
|
|
3087
|
+
else
|
|
3088
|
+
echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3002
3089
|
fi
|
|
3003
|
-
echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3004
3090
|
else
|
|
3005
|
-
|
|
3091
|
+
# #570: lcov.info exists but carries no usable hit/found pair.
|
|
3092
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3093
|
+
echo "⚠️ Could not determine Flutter/Dart coverage (subset test run) — not blocking."
|
|
3094
|
+
else
|
|
3095
|
+
echo "❌ BLOCKED - Could not determine Flutter/Dart coverage from coverage/lcov.info,"
|
|
3096
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3097
|
+
exit 1
|
|
3098
|
+
fi
|
|
3006
3099
|
fi
|
|
3007
3100
|
else
|
|
3008
|
-
|
|
3101
|
+
# #570: the fail-open path. `dart test --coverage` not run means the gate
|
|
3102
|
+
# has no evidence, and no evidence is not a pass.
|
|
3103
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3104
|
+
echo "⚠️ coverage/lcov.info not found (subset test run) — not blocking."
|
|
3105
|
+
else
|
|
3106
|
+
echo "❌ BLOCKED - coverage/lcov.info not found, so the ${COVERAGE_THRESHOLD:-80}% Flutter/Dart"
|
|
3107
|
+
echo " coverage floor cannot be verified. Ensure 'dart test --coverage' was run."
|
|
3108
|
+
exit 1
|
|
3109
|
+
fi
|
|
3009
3110
|
fi
|
|
3010
3111
|
;;
|
|
3011
3112
|
"powershell")
|
|
3012
3113
|
COVERAGE_ENFORCED=true
|
|
3013
3114
|
if [ -f "coverage.xml" ]; then
|
|
3014
|
-
|
|
3115
|
+
# The threshold is passed INTO node: the verdict used to be computed
|
|
3116
|
+
# against a hard-coded 80 inside the script, so a project that configured
|
|
3117
|
+
# a different floor got a verdict about a number it never set (#570).
|
|
3118
|
+
COVERAGE_RESULT=$(THRESHOLD="${COVERAGE_THRESHOLD:-80}" node -e "
|
|
3015
3119
|
try {
|
|
3016
3120
|
const fs = require('fs');
|
|
3017
3121
|
const xml = fs.readFileSync('coverage.xml', 'utf8');
|
|
@@ -3028,23 +3132,49 @@ else
|
|
|
3028
3132
|
if (!Number.isFinite(missed) || !Number.isFinite(covered) || missed < 0 || covered < 0 || total <= 0) {
|
|
3029
3133
|
throw new Error('invalid LINE counter');
|
|
3030
3134
|
}
|
|
3135
|
+
const threshold = Number(process.env.THRESHOLD);
|
|
3136
|
+
// Number("") is 0, which would make every percentage "pass" — the
|
|
3137
|
+
// exact fail-open shape this fix is about. Refuse an unusable floor.
|
|
3138
|
+
if (!Number.isFinite(threshold) || threshold <= 0) {
|
|
3139
|
+
throw new Error('unusable THRESHOLD');
|
|
3140
|
+
}
|
|
3031
3141
|
const percentage = (covered / total) * 100;
|
|
3032
|
-
console.log((percentage <
|
|
3142
|
+
console.log((percentage < threshold ? 'below' : 'pass') + '|' + Math.round(percentage));
|
|
3033
3143
|
} catch (error) { console.log('parse_error'); }
|
|
3034
3144
|
" 2>/dev/null) || COVERAGE_RESULT="parse_error"
|
|
3035
3145
|
if [ "$COVERAGE_RESULT" != "parse_error" ] && [ -n "$COVERAGE_RESULT" ]; then
|
|
3036
3146
|
COVERAGE_STATUS=${COVERAGE_RESULT%%|*}
|
|
3037
3147
|
COVERAGE_PERCENT=${COVERAGE_RESULT#*|}
|
|
3038
3148
|
if [ "$COVERAGE_STATUS" = "below" ]; then
|
|
3039
|
-
|
|
3040
|
-
|
|
3149
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3150
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3151
|
+
echo " Run the full Pester suite for a complete PowerShell coverage check."
|
|
3152
|
+
else
|
|
3153
|
+
echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3154
|
+
exit 1
|
|
3155
|
+
fi
|
|
3156
|
+
else
|
|
3157
|
+
echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3041
3158
|
fi
|
|
3042
|
-
echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3043
3159
|
else
|
|
3044
|
-
|
|
3160
|
+
# #570: an unparseable coverage.xml is no evidence, not a pass.
|
|
3161
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3162
|
+
echo "⚠️ Could not parse PowerShell coverage from coverage.xml (subset test run) - not blocking."
|
|
3163
|
+
else
|
|
3164
|
+
echo "❌ BLOCKED - Could not parse PowerShell coverage from coverage.xml,"
|
|
3165
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3166
|
+
exit 1
|
|
3167
|
+
fi
|
|
3045
3168
|
fi
|
|
3046
3169
|
else
|
|
3047
|
-
|
|
3170
|
+
# #570: Pester coverage not run means the gate has nothing to answer with.
|
|
3171
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3172
|
+
echo "⚠️ coverage.xml not found (subset test run) - not blocking."
|
|
3173
|
+
else
|
|
3174
|
+
echo "❌ BLOCKED - coverage.xml not found, so the ${COVERAGE_THRESHOLD:-80}% PowerShell"
|
|
3175
|
+
echo " coverage floor cannot be verified. Ensure Pester coverage was run."
|
|
3176
|
+
exit 1
|
|
3177
|
+
fi
|
|
3048
3178
|
fi
|
|
3049
3179
|
;;
|
|
3050
3180
|
"iac")
|
|
@@ -3072,22 +3202,48 @@ else
|
|
|
3072
3202
|
COVERAGE_PERCENT=$(grep -oP 'coverage: \K[0-9]+%' coverage/lcov.info 2>/dev/null | head -1 | tr -d '%') || COVERAGE_PERCENT=""
|
|
3073
3203
|
fi
|
|
3074
3204
|
fi
|
|
3075
|
-
|
|
3076
|
-
|
|
3205
|
+
# #579: `-ge 0`, not `-gt 0`. A measured 0% is DATA -- coverage.py ran and
|
|
3206
|
+
# covered nothing -- not a missing value. Folding it into the
|
|
3207
|
+
# "could not determine" branch let a suite that tested nothing pass the
|
|
3208
|
+
# coverage gate with a WARNING. The two claims are different: 0% is
|
|
3209
|
+
# "executed, proved nothing"; undetermined is "no evidence produced".
|
|
3210
|
+
# #570: the threshold is defaulted HERE as well as quoted in the message.
|
|
3211
|
+
# A bare `[ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]` with an empty
|
|
3212
|
+
# threshold makes test(1) fail with "integer expected", the condition is
|
|
3213
|
+
# false, and 16% coverage was reported as ✅ (the same fail-open shape this
|
|
3214
|
+
# issue is about, one line further down). `2>/dev/null` on the numeric test
|
|
3215
|
+
# keeps a non-numeric report on the "cannot determine" path instead of
|
|
3216
|
+
# dying.
|
|
3217
|
+
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] \
|
|
3218
|
+
&& [ "$COVERAGE_PERCENT" -ge 0 ] 2>/dev/null; then
|
|
3219
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
3077
3220
|
# Only a subset test run (smart selection) warns — the full suite may
|
|
3078
3221
|
# still reach ≥80%; a full suite run must block.
|
|
3079
3222
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3080
3223
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3081
3224
|
echo " Run the full test suite for a complete ${CURRENT_LANG} coverage check."
|
|
3082
3225
|
else
|
|
3083
|
-
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
3226
|
+
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3084
3227
|
exit 1
|
|
3085
3228
|
fi
|
|
3086
3229
|
else
|
|
3087
|
-
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3230
|
+
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3088
3231
|
fi
|
|
3089
3232
|
else
|
|
3090
|
-
|
|
3233
|
+
# #570: "could not determine" is a FAILED answer to the gate's question
|
|
3234
|
+
# ("is coverage at or above the floor?"), not a lenient one. Warning and
|
|
3235
|
+
# falling through made the generic branch fail-open: a language that
|
|
3236
|
+
# produced no standard report file passed the coverage gate while the
|
|
3237
|
+
# summary still looked clean. The ONE exception is a subset run, where
|
|
3238
|
+
# the global percentage is meaningless by construction.
|
|
3239
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3240
|
+
echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage (subset test run) — not blocking."
|
|
3241
|
+
else
|
|
3242
|
+
echo "❌ BLOCKED - Could not determine ${CURRENT_LANG} coverage percentage."
|
|
3243
|
+
echo " No standard coverage report file was found, so the ${COVERAGE_THRESHOLD:-80}% floor"
|
|
3244
|
+
echo " could not be checked. Produce coverage data or run a subset test run."
|
|
3245
|
+
exit 1
|
|
3246
|
+
fi
|
|
3091
3247
|
fi
|
|
3092
3248
|
;;
|
|
3093
3249
|
esac
|
|
@@ -3611,10 +3767,20 @@ elif [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
|
3611
3767
|
GATE_9_SCRIPT="$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts"
|
|
3612
3768
|
fi
|
|
3613
3769
|
|
|
3770
|
+
# #581: the script path goes to node.exe. On WSL $HOME is /home/<user>, which node.exe
|
|
3771
|
+
# resolves to <current drive>:\home\<user>\... -- module not found -- and Gate 9 then
|
|
3772
|
+
# reported "Build integrity check failed" with advice to fix type errors in the USER'S
|
|
3773
|
+
# code. Convert the path first so the failure, if any, is a real one.
|
|
3774
|
+
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
3775
|
+
GATE_9_SCRIPT=$(to_native_path "$GATE_9_SCRIPT")
|
|
3776
|
+
fi
|
|
3777
|
+
# Same for the project root, which reaches both node and tsx as an absolute path.
|
|
3778
|
+
GATE_9_PROJECT_ROOT=$(to_native_path "$PROJECT_ROOT")
|
|
3779
|
+
|
|
3614
3780
|
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
3615
3781
|
CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
|
|
3616
3782
|
GATE_9_OUTPUT=$(mktemp)
|
|
3617
|
-
if RUN_TSX_TIMEOUT=120s run_tsx "$GATE_9_SCRIPT" --changed-files "$CHANGED_FILES_CSV" --project-root "$
|
|
3783
|
+
if RUN_TSX_TIMEOUT=120s run_tsx "$GATE_9_SCRIPT" --changed-files "$CHANGED_FILES_CSV" --project-root "$GATE_9_PROJECT_ROOT" --timeout 115000 > "$GATE_9_OUTPUT" 2>&1; then
|
|
3618
3784
|
GATE_9_TS_STATUS="PASS"
|
|
3619
3785
|
echo " ✅ Build integrity check passed."
|
|
3620
3786
|
else
|
|
@@ -3770,7 +3936,17 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
3770
3936
|
GATE_11_STATUS="SKIP"
|
|
3771
3937
|
;;
|
|
3772
3938
|
allow)
|
|
3773
|
-
|
|
3939
|
+
# #584: `allow` covered three different outcomes and all of them became
|
|
3940
|
+
# PASS, so a sprint that was never examined raised PASSED_COUNT and the
|
|
3941
|
+
# score. Only a genuinely validated sprint certifies this gate; the rest
|
|
3942
|
+
# declined to evaluate and must read as SKIP with the real reason shown.
|
|
3943
|
+
SPRINT_GATE_ALLOW_REASON=$(sprint_gate_allow_reason "$SPRINT_GATE_OUTPUT")
|
|
3944
|
+
if [ "$SPRINT_GATE_ALLOW_REASON" = "checks-passed" ]; then
|
|
3945
|
+
echo "✅ PASSED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-sprint state validated})"
|
|
3946
|
+
else
|
|
3947
|
+
echo "⏭️ SKIPPED - Gate 11: Sprint Flow (${SPRINT_GATE_ALLOW_REASON}: ${SPRINT_GATE_DETAIL:-not evaluated})"
|
|
3948
|
+
GATE_11_STATUS="SKIP"
|
|
3949
|
+
fi
|
|
3774
3950
|
;;
|
|
3775
3951
|
*)
|
|
3776
3952
|
# A run that succeeded but said nothing this gate understands. Pass on the exit
|
|
@@ -3841,7 +4017,14 @@ generate_quality_report() {
|
|
|
3841
4017
|
# -- degraded coverage, kept in the denominator but flagged separately so a
|
|
3842
4018
|
# degraded gate is never confused with a real PASS.
|
|
3843
4019
|
local DEGRADED_COUNT=0
|
|
4020
|
+
# The report's gatesDegraded is a superset: DEGRADED plus WARN (#536), because
|
|
4021
|
+
# both mean "this gate could not fully verify what it was asked to". Kept
|
|
4022
|
+
# separate from the scoring loop's DEGRADED_COUNT above so the two statuses stay
|
|
4023
|
+
# distinct where that distinction is load-bearing (#599-10).
|
|
4024
|
+
local REPORT_DEGRADED_COUNT=0
|
|
3844
4025
|
local DEGRADED_JSON=""
|
|
4026
|
+
local FAIL_COUNT=0
|
|
4027
|
+
local FAILED_JSON=""
|
|
3845
4028
|
for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
3846
4029
|
local status_var="GATE_${gate}_STATUS"
|
|
3847
4030
|
# Default mirrors the JSON report rendering below EXACTLY (gates 1-11
|
|
@@ -3857,8 +4040,22 @@ generate_quality_report() {
|
|
|
3857
4040
|
case "$gate_status" in
|
|
3858
4041
|
PASS) PASSED_COUNT=$((PASSED_COUNT + 1)) ;;
|
|
3859
4042
|
SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
|
|
3860
|
-
WARN
|
|
3861
|
-
|
|
4043
|
+
# #536: a WARN gate ran but could not fully do its job, so the REPORT's
|
|
4044
|
+
# gatesDegraded must include it -- that field answers "how much of this run
|
|
4045
|
+
# could actually verify what it was asked to". The scoring loop's
|
|
4046
|
+
# DEGRADED_COUNT deliberately does NOT include it: a plain WARN must stay
|
|
4047
|
+
# distinguishable from a gate that reported DEGRADED (#599-10), or the two
|
|
4048
|
+
# statuses become one and neither is worth anything.
|
|
4049
|
+
WARN) REPORT_DEGRADED_COUNT=$((REPORT_DEGRADED_COUNT + 1)); WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
|
|
4050
|
+
# #574: DEGRADED is a status a gate really produces -- Gate 5 reports it
|
|
4051
|
+
# when coverage could not be measured honestly instead of claiming PASS.
|
|
4052
|
+
# It had no arm here, so it fell into the catch-all below and counted as a
|
|
4053
|
+
# HARD FAILURE: the gate was punished for telling the truth about itself.
|
|
4054
|
+
# It must also feed WARN_COUNT, because the FAIL predicate is
|
|
4055
|
+
# (NONPASS_COUNT - WARN_COUNT) > 0; adding only NONPASS_COUNT would turn
|
|
4056
|
+
# every honest DEGRADED run into FAIL.
|
|
4057
|
+
DEGRADED) DEGRADED_COUNT=$((DEGRADED_COUNT + 1)); REPORT_DEGRADED_COUNT=$((REPORT_DEGRADED_COUNT + 1)); WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)); DEGRADED_JSON="${DEGRADED_JSON:+$DEGRADED_JSON, }\"gate${gate}\"" ;;
|
|
4058
|
+
*) FAIL_COUNT=$((FAIL_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)); FAILED_JSON="${FAILED_JSON:+$FAILED_JSON, }\"gate${gate}\"" ;;
|
|
3862
4059
|
esac
|
|
3863
4060
|
done
|
|
3864
4061
|
|
|
@@ -3872,6 +4069,14 @@ generate_quality_report() {
|
|
|
3872
4069
|
else
|
|
3873
4070
|
VERDICT="WARN" # all effective gates passed or warned; warns forbid PASS
|
|
3874
4071
|
fi
|
|
4072
|
+
elif [ "$SKIP_COUNT" -gt 0 ]; then
|
|
4073
|
+
# #575: PARTIAL. Something passed, but not everything ran, and "PASS" reads
|
|
4074
|
+
# as a statement about the commit — not about the fraction of the pipeline
|
|
4075
|
+
# that happened to execute. A docs-only commit runs 3 of 12 gates and used
|
|
4076
|
+
# to report `verdict: PASS`, which is a claim the run did not support.
|
|
4077
|
+
# This is deliberately NOT FAIL: nothing failed. It is also not SKIP-ALL,
|
|
4078
|
+
# which means no gate ran at all. Naming the gap is the whole point.
|
|
4079
|
+
VERDICT="PARTIAL"
|
|
3875
4080
|
fi
|
|
3876
4081
|
|
|
3877
4082
|
local SCORE=0
|
|
@@ -3966,8 +4171,8 @@ generate_quality_report() {
|
|
|
3966
4171
|
COVERAGE_WARNINGS_JSON="${COVERAGE_WARNINGS_JSON:+$COVERAGE_WARNINGS_JSON, }\"unsupported_language_name\""
|
|
3967
4172
|
continue
|
|
3968
4173
|
fi
|
|
3969
|
-
_cm_g3=$(classify_coverage_reason "${GATE_3_REASON:-}" "${GATE_3_STATUS:-
|
|
3970
|
-
_cm_g4=$(classify_coverage_reason "${GATE_4_REASON:-}" "${GATE_4_STATUS:-
|
|
4174
|
+
_cm_g3=$(classify_coverage_reason "${GATE_3_REASON:-}" "${GATE_3_STATUS:-SKIP}")
|
|
4175
|
+
_cm_g4=$(classify_coverage_reason "${GATE_4_REASON:-}" "${GATE_4_STATUS:-SKIP}")
|
|
3971
4176
|
# Gate 6 reason for this language — extracted so the rule is testable
|
|
3972
4177
|
# rather than inlined (see coverage_g6_reason_for below).
|
|
3973
4178
|
_cm_g6_reason=$(coverage_g6_reason_for "$_cm_lang")
|
|
@@ -3989,7 +4194,10 @@ generate_quality_report() {
|
|
|
3989
4194
|
"gatesPassed": $PASSED_COUNT,
|
|
3990
4195
|
"gatesTotal": $TOTAL_GATES,
|
|
3991
4196
|
"gatesSkipped": $SKIP_COUNT,
|
|
3992
|
-
"
|
|
4197
|
+
"gatesWarned": $WARN_COUNT,
|
|
4198
|
+
"gatesFailed": $FAIL_COUNT,
|
|
4199
|
+
"failedGates": [$FAILED_JSON],
|
|
4200
|
+
"gatesDegraded": $REPORT_DEGRADED_COUNT,
|
|
3993
4201
|
"degradedGates": [$DEGRADED_JSON],
|
|
3994
4202
|
"gatesEffective": $EFFECTIVE_TOTAL,
|
|
3995
4203
|
"effectivePassRate": $EFFECTIVE_PASS_RATE_JSON,
|
|
@@ -4004,7 +4212,7 @@ generate_quality_report() {
|
|
|
4004
4212
|
"gates": {
|
|
4005
4213
|
"gate1_static_analysis": {
|
|
4006
4214
|
"name": "Code Quality (Static + Lint + Shell)",
|
|
4007
|
-
"status": "${GATE_1_STATUS:-
|
|
4215
|
+
"status": "${GATE_1_STATUS:-SKIP}",
|
|
4008
4216
|
"tool": "${GATE_1_TOOL:-auto}"
|
|
4009
4217
|
},
|
|
4010
4218
|
"gate2_dup_code": {
|
|
@@ -4014,7 +4222,7 @@ generate_quality_report() {
|
|
|
4014
4222
|
},
|
|
4015
4223
|
"gate3_complexity": {
|
|
4016
4224
|
"name": "Cyclomatic Complexity",
|
|
4017
|
-
"status": "${GATE_3_STATUS:-
|
|
4225
|
+
"status": "${GATE_3_STATUS:-SKIP}",
|
|
4018
4226
|
"reason": "${GATE_3_REASON:-}",
|
|
4019
4227
|
"threshold": "${CCN_THRESHOLD:-5}",
|
|
4020
4228
|
"blockThreshold": "${CCN_THRESHOLD:-5}",
|
|
@@ -4022,13 +4230,13 @@ generate_quality_report() {
|
|
|
4022
4230
|
},
|
|
4023
4231
|
"gate4_principles": {
|
|
4024
4232
|
"name": "Clean Code + SOLID",
|
|
4025
|
-
"status": "${GATE_4_STATUS:-
|
|
4233
|
+
"status": "${GATE_4_STATUS:-SKIP}",
|
|
4026
4234
|
"reason": "${GATE_4_REASON:-}",
|
|
4027
4235
|
"warnings": ${WARNING_COUNT:-0}
|
|
4028
4236
|
},
|
|
4029
4237
|
"gate5_tests": {
|
|
4030
4238
|
"name": "Tests + Coverage",
|
|
4031
|
-
"status": "${GATE_5_STATUS:-
|
|
4239
|
+
"status": "${GATE_5_STATUS:-SKIP}",
|
|
4032
4240
|
"thresholds": { "lines": ${COVERAGE_THRESHOLD}, "functions": null, "branches": null, "statements": null },
|
|
4033
4241
|
"threshold_source": "${COVERAGE_THRESHOLD_SOURCE}",
|
|
4034
4242
|
"actual": { "coverage": "${COV_PCT}" },
|
|
@@ -4043,30 +4251,30 @@ generate_quality_report() {
|
|
|
4043
4251
|
},
|
|
4044
4252
|
"gate7_iac_security": {
|
|
4045
4253
|
"name": "IaC Security Scanning",
|
|
4046
|
-
"status": "${GATE_7_STATUS:-
|
|
4254
|
+
"status": "${GATE_7_STATUS:-SKIP}",
|
|
4047
4255
|
"scanned_files": ${GATE_7_SCANNED_FILES:-0},
|
|
4048
4256
|
"tools": "checkov, hadolint, kube-score, tflint"
|
|
4049
4257
|
},
|
|
4050
4258
|
"gate8_secret_scanning": {
|
|
4051
4259
|
"name": "Secret Scanning",
|
|
4052
|
-
"status": "${GATE_8_STATUS:-
|
|
4260
|
+
"status": "${GATE_8_STATUS:-SKIP}",
|
|
4053
4261
|
"tool": "gitleaks"
|
|
4054
4262
|
},
|
|
4055
4263
|
"gate9_build_integrity": {
|
|
4056
4264
|
"name": "Build Integrity",
|
|
4057
|
-
"status": "${GATE_9_STATUS:-
|
|
4265
|
+
"status": "${GATE_9_STATUS:-SKIP}",
|
|
4058
4266
|
"tool": "tsc + npm pack + import check"
|
|
4059
4267
|
},
|
|
4060
4268
|
"gate10_sast": {
|
|
4061
4269
|
"name": "SAST Security Scan",
|
|
4062
|
-
"status": "${GATE_10_STATUS:-
|
|
4270
|
+
"status": "${GATE_10_STATUS:-SKIP}",
|
|
4063
4271
|
"reason": "${GATE_10_REASON:-}",
|
|
4064
4272
|
"scanned_files": ${GATE_10_SCANNED_FILES:-0},
|
|
4065
4273
|
"tool": "semgrep"
|
|
4066
4274
|
},
|
|
4067
4275
|
"gate11_sprint_flow": {
|
|
4068
4276
|
"name": "Sprint Flow Enforcement",
|
|
4069
|
-
"status": "${GATE_11_STATUS:-
|
|
4277
|
+
"status": "${GATE_11_STATUS:-SKIP}",
|
|
4070
4278
|
"tool": "sprint-gate.sh"
|
|
4071
4279
|
},
|
|
4072
4280
|
"gate12_file_hygiene": {
|
|
@@ -4088,17 +4296,17 @@ ENDJSON
|
|
|
4088
4296
|
|
|
4089
4297
|
# ── History append (unchanged — append-only for trend) ────────────────────────────
|
|
4090
4298
|
local GATES_JSON="{"
|
|
4091
|
-
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-
|
|
4299
|
+
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-SKIP}\",\"name\":\"Code Quality\"},"
|
|
4092
4300
|
GATES_JSON="${GATES_JSON}\"gate2\":{\"status\":\"${GATE_2_STATUS:-SKIP}\",\"name\":\"Duplicate Code\"},"
|
|
4093
|
-
GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-
|
|
4094
|
-
GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-
|
|
4095
|
-
GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-
|
|
4301
|
+
GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-SKIP}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
|
|
4302
|
+
GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-SKIP}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
|
|
4303
|
+
GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-SKIP}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
|
|
4096
4304
|
GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-SKIP}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
|
|
4097
|
-
GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-
|
|
4098
|
-
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-
|
|
4099
|
-
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-
|
|
4100
|
-
GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-
|
|
4101
|
-
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-
|
|
4305
|
+
GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-SKIP}\",\"name\":\"IaC Security\"},"
|
|
4306
|
+
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-SKIP}\",\"name\":\"Secret Scanning\"},"
|
|
4307
|
+
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-SKIP}\",\"name\":\"Build Integrity\"},"
|
|
4308
|
+
GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-SKIP}\",\"name\":\"SAST Security\"},"
|
|
4309
|
+
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-SKIP}\",\"name\":\"Sprint Flow\"},"
|
|
4102
4310
|
GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
|
|
4103
4311
|
|
|
4104
4312
|
echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE_JSON,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"effectiveTotal\":$EFFECTIVE_TOTAL,\"effectivePassRate\":$EFFECTIVE_PASS_RATE_JSON,\"skipCount\":$SKIP_COUNT,\"verdict\":\"$VERDICT\",\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
|
|
@@ -4109,17 +4317,17 @@ ENDJSON
|
|
|
4109
4317
|
echo " 📊 Quality Report — $TIMESTAMP"
|
|
4110
4318
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4111
4319
|
echo ""
|
|
4112
|
-
printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-
|
|
4320
|
+
printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-SKIP}"
|
|
4113
4321
|
printf " %-45s %s\n" "Gate 2: Duplicate Code" "${GATE_2_STATUS:-SKIP}"
|
|
4114
|
-
printf " %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-
|
|
4115
|
-
printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-
|
|
4116
|
-
printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-
|
|
4322
|
+
printf " %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-SKIP}"
|
|
4323
|
+
printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-SKIP}"
|
|
4324
|
+
printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-SKIP}"
|
|
4117
4325
|
printf " %-45s %s\n" "Gate 6: Architecture + Boy Scout" "${GATE_6_STATUS:-SKIP}"
|
|
4118
|
-
printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-
|
|
4119
|
-
printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-
|
|
4120
|
-
printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-
|
|
4121
|
-
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-
|
|
4122
|
-
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-
|
|
4326
|
+
printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-SKIP}"
|
|
4327
|
+
printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-SKIP}"
|
|
4328
|
+
printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-SKIP}"
|
|
4329
|
+
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-SKIP}"
|
|
4330
|
+
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-SKIP}"
|
|
4123
4331
|
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
4124
4332
|
echo ""
|
|
4125
4333
|
# #564: both views in one line. The effective score answers "of the gates that
|