@boyingliu01/xp-gate 0.31.1 → 0.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +37 -1
- package/adapters/python.sh +18 -4
- package/gate-3.sh +254 -10
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +254 -10
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +403 -165
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/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."
|
|
@@ -2060,6 +2130,13 @@ source "$GATE_DIR/gate-4.sh"
|
|
|
2060
2130
|
2>&1 echo ""
|
|
2061
2131
|
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
2062
2132
|
GATE_5_START=$(gate_start_ms)
|
|
2133
|
+
# #554 follow-up: explicit "nothing decided yet" sentinel. A branch inside the
|
|
2134
|
+
# else-block may record its own verdict (the no-code early skip records SKIP);
|
|
2135
|
+
# the epilogue must not overwrite it with PASS. Using "" rather than leaving the
|
|
2136
|
+
# variable unset keeps the epilogue's decision readable, and keeps every render
|
|
2137
|
+
# site's own default (`${GATE_5_STATUS:-SKIP}`) honest for the paths that never
|
|
2138
|
+
# assign at all.
|
|
2139
|
+
GATE_5_STATUS=""
|
|
2063
2140
|
|
|
2064
2141
|
if [ "$PROJECT_LANG" = "documentation-only" ] || [ "$DOCS_ONLY_COMMIT" = true ]; then
|
|
2065
2142
|
echo "⏭️ SKIPPED - Tests & coverage (documentation-only commit — no code changed to test)."
|
|
@@ -2393,80 +2470,24 @@ else
|
|
|
2393
2470
|
fi
|
|
2394
2471
|
|
|
2395
2472
|
# ============================================================================
|
|
2396
|
-
#
|
|
2397
|
-
#
|
|
2473
|
+
# ============================================================================
|
|
2474
|
+
# Gate 5c: Semantic Annotation Check (#585)
|
|
2475
|
+
#
|
|
2476
|
+
# The check itself lives in lib/gate-5c-alignment.sh so its contract is
|
|
2477
|
+
# unit-testable without executing the hook. Two defects it fixes, both of which
|
|
2478
|
+
# made this gate a decoration:
|
|
2479
|
+
# 1. The whole body sat inside `if [ -f req-ids.json ] && command -v jq`.
|
|
2480
|
+
# .sprint-state/ is gitignored, so on a fresh clone the gate printed
|
|
2481
|
+
# NOTHING — not PASS, not FAIL, not SKIP — and the report had no slot for
|
|
2482
|
+
# it. Silence read as success, the same defect as #449/#499/#570.
|
|
2483
|
+
# 2. Test discovery matched only JS/TS, so Pester's *.Tests.ps1, pytest's
|
|
2484
|
+
# test_*.py, Go's *_test.go, Java's *Test.java and Kotlin's *Tests.kt found
|
|
2485
|
+
# nothing and the gate reported a vacuous "0 annotated, 0 uncovered".
|
|
2398
2486
|
#
|
|
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.
|
|
2487
|
+
# GATE_5C_STATUS is now always PASS, SKIP or BLOCK — never empty.
|
|
2405
2488
|
# ============================================================================
|
|
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
|
|
2489
|
+
GATE_5C_STATUS=""
|
|
2490
|
+
gate_5c_alignment || exit 1
|
|
2470
2491
|
|
|
2471
2492
|
# ============================================================================
|
|
2472
2493
|
# v0.14.3+ (Issue #312): Early skip when no code files changed.
|
|
@@ -2477,10 +2498,19 @@ else
|
|
|
2477
2498
|
CODE_EXTENSIONS="\.(ts|tsx|js|jsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|m|mm|dart|sh|ps1)$"
|
|
2478
2499
|
CHANGED_CODE_FILES=$(echo "$CHANGED_FILES" | grep -E "$CODE_EXTENSIONS" || true)
|
|
2479
2500
|
if [ -z "$CHANGED_CODE_FILES" ]; then
|
|
2480
|
-
|
|
2501
|
+
# #554: this branch DID stand down deliberately, but "PASS" is a claim that
|
|
2502
|
+
# a check ran and found nothing. Nothing ran here: no test executed, no
|
|
2503
|
+
# coverage number exists. Recording PASS inflated the pass rate and made an
|
|
2504
|
+
# unmeasured gate indistinguishable from a measured clean one -- the same
|
|
2505
|
+
# defect #554 fixed on the docs-only branch, on the branch that fix did not
|
|
2506
|
+
# cover (measured on the v0.31.2.0 release commit: journal said
|
|
2507
|
+
# gate5_tests.status=PASS while the summary printed "coverage: N/A%").
|
|
2508
|
+
echo "⏭️ SKIPPED - No code files changed (only docs/config/VERSION) — test run not needed."
|
|
2481
2509
|
echo " Changed files: $(echo "$CHANGED_FILES" | tr '\n' ' ')"
|
|
2482
2510
|
TESTS_EXIT_CODE=0
|
|
2483
2511
|
TESTS_SKIPPED=true
|
|
2512
|
+
GATE_5_STATUS="SKIP"
|
|
2513
|
+
GATE_5_DETAIL="no_code_files_changed"
|
|
2484
2514
|
GATE_5_END=$(gate_start_ms)
|
|
2485
2515
|
GATE_5_ELAPSED=$((GATE_5_END - GATE_5_START))
|
|
2486
2516
|
echo " ⏱️ Gate 5: ${GATE_5_ELAPSED}ms (skipped — no code changes)"
|
|
@@ -2841,7 +2871,7 @@ else
|
|
|
2841
2871
|
go test -coverprofile=coverage.out ./... 2>/dev/null
|
|
2842
2872
|
TOTAL_COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total:" | awk '{print substr($3, 1, length($3)-1)}')
|
|
2843
2873
|
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"
|
|
2874
|
+
echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2845
2875
|
exit 1
|
|
2846
2876
|
fi
|
|
2847
2877
|
echo "Go coverage: $TOTAL_COVERAGE%"
|
|
@@ -2885,38 +2915,84 @@ else
|
|
|
2885
2915
|
COVERAGE_ENFORCED=false
|
|
2886
2916
|
|
|
2887
2917
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
2918
|
+
# #570: every comparison below reads $COVERAGE_THRESHOLD, and the messages
|
|
2919
|
+
# quote it. If it ever arrives empty (a helper called outside the hook, a test
|
|
2920
|
+
# harness that sources only the case block), `[ "$pct" -lt "" ]` is false and
|
|
2921
|
+
# `awk 'pct < thr'` with an empty thr is false too — the gate would PASS every
|
|
2922
|
+
# value, i.e. exactly the fail-open this issue is about. Fall back to the
|
|
2923
|
+
# historical default so an unset variable can never widen the gate.
|
|
2924
|
+
#
|
|
2925
|
+
# Kept INSIDE the loop (immediately before the case) so a harness that extracts
|
|
2926
|
+
# only `case ... esac` still gets the guarantee it depends on.
|
|
2927
|
+
if ! [[ "${COVERAGE_THRESHOLD:-}" =~ ^[0-9]+$ ]]; then
|
|
2928
|
+
COVERAGE_THRESHOLD=80
|
|
2929
|
+
COVERAGE_THRESHOLD_SOURCE="default (threshold was unset at Stage 2)"
|
|
2930
|
+
fi
|
|
2888
2931
|
case "$CURRENT_LANG" in
|
|
2889
2932
|
"typescript")
|
|
2890
2933
|
COVERAGE_ENFORCED=true
|
|
2891
2934
|
if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
|
|
2892
2935
|
echo "✅ PASSED - Coverage check skipped (no test/source files changed)."
|
|
2893
2936
|
elif [ -f "coverage/coverage-summary.json" ]; then
|
|
2937
|
+
# #594: reject NaN at the SOURCE, not after the fact. Math.round() does
|
|
2938
|
+
# not fail on a non-numeric input -- it returns NaN and console.log prints
|
|
2939
|
+
# "NaN" -- so istanbul's "Unknown" (a file with no executable lines) sailed
|
|
2940
|
+
# past both the `!= "parse_error"` and `-n` guards. Emit `no_data`, which
|
|
2941
|
+
# the guard below already treats as "could not measure".
|
|
2894
2942
|
COVERAGE_PERCENT=$(node -e "
|
|
2895
2943
|
try {
|
|
2896
2944
|
const fs = require('fs');
|
|
2897
2945
|
const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
|
|
2898
|
-
|
|
2946
|
+
const pct = data && data.total && data.total.lines && data.total.lines.pct;
|
|
2947
|
+
console.log(Number.isFinite(pct) ? Math.round(pct) : 'no_data');
|
|
2899
2948
|
} catch(e) { console.log('parse_error'); }
|
|
2900
2949
|
" 2>/dev/null)
|
|
2901
|
-
|
|
2902
|
-
|
|
2950
|
+
# #594: `-n` was not enough. `[ "NaN" -lt 80 ]` is not false in bash -- it
|
|
2951
|
+
# is an ERROR (exit 2, "integer expression expected"), and an `if` only
|
|
2952
|
+
# tells zero from non-zero, so the error took the else branch: the PASS
|
|
2953
|
+
# branch. Require an actual integer before comparing anything.
|
|
2954
|
+
if [[ "$COVERAGE_PERCENT" =~ ^[0-9]+$ ]]; then
|
|
2955
|
+
# #570: default the threshold at the comparison site too — an empty
|
|
2956
|
+
# value makes test(1) fail, the branch is skipped, and low coverage is
|
|
2957
|
+
# reported as a pass.
|
|
2958
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
2903
2959
|
# Only a subset test run (smart selection) leaves partial coverage data;
|
|
2904
2960
|
# a full suite run must block on <80% even when many test files changed.
|
|
2905
2961
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2906
2962
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2907
2963
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
2908
2964
|
else
|
|
2909
|
-
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
2965
|
+
echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2910
2966
|
exit 1
|
|
2911
2967
|
fi
|
|
2912
2968
|
else
|
|
2913
|
-
echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
2969
|
+
echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
2914
2970
|
fi
|
|
2915
2971
|
else
|
|
2916
|
-
|
|
2972
|
+
# #570: an unparseable report is not a pass. Silently continuing here
|
|
2973
|
+
# meant a corrupt or unexpected-shape coverage-summary.json disabled the
|
|
2974
|
+
# TypeScript coverage gate while the run still looked clean.
|
|
2975
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2976
|
+
echo "⚠️ Could not parse TypeScript coverage from coverage/coverage-summary.json (subset test run) — not blocking."
|
|
2977
|
+
else
|
|
2978
|
+
echo "❌ BLOCKED - Could not parse TypeScript coverage from coverage/coverage-summary.json."
|
|
2979
|
+
echo " The report exists but carries no usable total.lines.pct, so the"
|
|
2980
|
+
echo " ${COVERAGE_THRESHOLD:-80}% floor could not be checked."
|
|
2981
|
+
exit 1
|
|
2982
|
+
fi
|
|
2917
2983
|
fi
|
|
2918
2984
|
else
|
|
2919
|
-
|
|
2985
|
+
# #570: the report file is absent, so nothing was measured. Warning and
|
|
2986
|
+
# continuing made "delete coverage/coverage-summary.json" an effective way
|
|
2987
|
+
# to switch this gate off.
|
|
2988
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2989
|
+
echo "⚠️ coverage/coverage-summary.json not found (subset test run) — not blocking."
|
|
2990
|
+
else
|
|
2991
|
+
echo "❌ BLOCKED - coverage/coverage-summary.json not found."
|
|
2992
|
+
echo " No coverage could be measured, so the ${COVERAGE_THRESHOLD:-80}% floor could not be"
|
|
2993
|
+
echo " checked. Run 'npx vitest run --coverage' before committing."
|
|
2994
|
+
exit 1
|
|
2995
|
+
fi
|
|
2920
2996
|
fi
|
|
2921
2997
|
;;
|
|
2922
2998
|
"python")
|
|
@@ -2965,24 +3041,39 @@ else
|
|
|
2965
3041
|
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
2966
3042
|
# Compare on the unrounded value so 79.5% cannot pass as 80%.
|
|
2967
3043
|
COVERAGE_ROUNDED=${COVERAGE_PERCENT%.*}
|
|
2968
|
-
if awk -v pct="$COVERAGE_PERCENT" 'BEGIN { exit !(pct <
|
|
3044
|
+
if awk -v pct="$COVERAGE_PERCENT" -v thr="${COVERAGE_THRESHOLD:-80}" 'BEGIN { exit !(pct < thr) }'; then
|
|
2969
3045
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2970
3046
|
echo "⚠️ Partial coverage ${COVERAGE_ROUNDED}% (subset test run, not full suite)."
|
|
2971
3047
|
echo " Run the full test suite for a complete Java coverage check."
|
|
2972
3048
|
else
|
|
2973
|
-
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
3049
|
+
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
2974
3050
|
exit 1
|
|
2975
3051
|
fi
|
|
2976
3052
|
else
|
|
2977
|
-
echo "Java coverage: ${COVERAGE_ROUNDED}% ✅ (≥ 80%)"
|
|
3053
|
+
echo "Java coverage: ${COVERAGE_ROUNDED}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
2978
3054
|
fi
|
|
2979
3055
|
else
|
|
2980
|
-
|
|
3056
|
+
# #570: a JaCoCo report that yields no instruction data measured
|
|
3057
|
+
# nothing. Warning and continuing let the gate pass on no evidence.
|
|
3058
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3059
|
+
echo "⚠️ Could not parse Java coverage from ${jacoco_csv} (subset test run) — not blocking."
|
|
3060
|
+
else
|
|
3061
|
+
echo "❌ BLOCKED - Could not parse Java coverage from ${jacoco_csv} (no instruction data),"
|
|
3062
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3063
|
+
exit 1
|
|
3064
|
+
fi
|
|
2981
3065
|
fi
|
|
2982
3066
|
else
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3067
|
+
# #570: no report at all is the fail-open path this issue was filed for.
|
|
3068
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3069
|
+
echo "⚠️ No JaCoCo report found (subset test run) — not blocking."
|
|
3070
|
+
else
|
|
3071
|
+
echo "❌ BLOCKED - No JaCoCo report found, so the ${COVERAGE_THRESHOLD:-80}% Java coverage"
|
|
3072
|
+
echo " floor cannot be verified. Run 'mvn verify' or 'gradle jacocoTestReport'"
|
|
3073
|
+
echo " so that target/site/jacoco/jacoco.csv exists (see also: JaCoCo detection"
|
|
3074
|
+
echo " in adapters/java.sh)."
|
|
3075
|
+
exit 1
|
|
3076
|
+
fi
|
|
2986
3077
|
fi
|
|
2987
3078
|
;;
|
|
2988
3079
|
"go")
|
|
@@ -2995,23 +3086,52 @@ else
|
|
|
2995
3086
|
COVERAGE_ENFORCED=true
|
|
2996
3087
|
if [ -f "coverage/lcov.info" ]; then
|
|
2997
3088
|
COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT="0"
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3089
|
+
# #570: `-ge 0`, not `!= "0"`. A measured 0% is DATA — the tool ran and
|
|
3090
|
+
# covered nothing — so it must reach the threshold comparison instead of
|
|
3091
|
+
# being folded into "could not determine".
|
|
3092
|
+
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" -ge 0 ] 2>/dev/null; then
|
|
3093
|
+
# #570: default at the comparison site; an empty threshold makes
|
|
3094
|
+
# test(1) fail and silently skips the BLOCK.
|
|
3095
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
3096
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3097
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3098
|
+
echo " Run the full test suite for a complete Flutter/Dart coverage check."
|
|
3099
|
+
else
|
|
3100
|
+
echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3101
|
+
exit 1
|
|
3102
|
+
fi
|
|
3103
|
+
else
|
|
3104
|
+
echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3002
3105
|
fi
|
|
3003
|
-
echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3004
3106
|
else
|
|
3005
|
-
|
|
3107
|
+
# #570: lcov.info exists but carries no usable hit/found pair.
|
|
3108
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3109
|
+
echo "⚠️ Could not determine Flutter/Dart coverage (subset test run) — not blocking."
|
|
3110
|
+
else
|
|
3111
|
+
echo "❌ BLOCKED - Could not determine Flutter/Dart coverage from coverage/lcov.info,"
|
|
3112
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3113
|
+
exit 1
|
|
3114
|
+
fi
|
|
3006
3115
|
fi
|
|
3007
3116
|
else
|
|
3008
|
-
|
|
3117
|
+
# #570: the fail-open path. `dart test --coverage` not run means the gate
|
|
3118
|
+
# has no evidence, and no evidence is not a pass.
|
|
3119
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3120
|
+
echo "⚠️ coverage/lcov.info not found (subset test run) — not blocking."
|
|
3121
|
+
else
|
|
3122
|
+
echo "❌ BLOCKED - coverage/lcov.info not found, so the ${COVERAGE_THRESHOLD:-80}% Flutter/Dart"
|
|
3123
|
+
echo " coverage floor cannot be verified. Ensure 'dart test --coverage' was run."
|
|
3124
|
+
exit 1
|
|
3125
|
+
fi
|
|
3009
3126
|
fi
|
|
3010
3127
|
;;
|
|
3011
3128
|
"powershell")
|
|
3012
3129
|
COVERAGE_ENFORCED=true
|
|
3013
3130
|
if [ -f "coverage.xml" ]; then
|
|
3014
|
-
|
|
3131
|
+
# The threshold is passed INTO node: the verdict used to be computed
|
|
3132
|
+
# against a hard-coded 80 inside the script, so a project that configured
|
|
3133
|
+
# a different floor got a verdict about a number it never set (#570).
|
|
3134
|
+
COVERAGE_RESULT=$(THRESHOLD="${COVERAGE_THRESHOLD:-80}" node -e "
|
|
3015
3135
|
try {
|
|
3016
3136
|
const fs = require('fs');
|
|
3017
3137
|
const xml = fs.readFileSync('coverage.xml', 'utf8');
|
|
@@ -3028,23 +3148,49 @@ else
|
|
|
3028
3148
|
if (!Number.isFinite(missed) || !Number.isFinite(covered) || missed < 0 || covered < 0 || total <= 0) {
|
|
3029
3149
|
throw new Error('invalid LINE counter');
|
|
3030
3150
|
}
|
|
3151
|
+
const threshold = Number(process.env.THRESHOLD);
|
|
3152
|
+
// Number("") is 0, which would make every percentage "pass" — the
|
|
3153
|
+
// exact fail-open shape this fix is about. Refuse an unusable floor.
|
|
3154
|
+
if (!Number.isFinite(threshold) || threshold <= 0) {
|
|
3155
|
+
throw new Error('unusable THRESHOLD');
|
|
3156
|
+
}
|
|
3031
3157
|
const percentage = (covered / total) * 100;
|
|
3032
|
-
console.log((percentage <
|
|
3158
|
+
console.log((percentage < threshold ? 'below' : 'pass') + '|' + Math.round(percentage));
|
|
3033
3159
|
} catch (error) { console.log('parse_error'); }
|
|
3034
3160
|
" 2>/dev/null) || COVERAGE_RESULT="parse_error"
|
|
3035
3161
|
if [ "$COVERAGE_RESULT" != "parse_error" ] && [ -n "$COVERAGE_RESULT" ]; then
|
|
3036
3162
|
COVERAGE_STATUS=${COVERAGE_RESULT%%|*}
|
|
3037
3163
|
COVERAGE_PERCENT=${COVERAGE_RESULT#*|}
|
|
3038
3164
|
if [ "$COVERAGE_STATUS" = "below" ]; then
|
|
3039
|
-
|
|
3040
|
-
|
|
3165
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3166
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3167
|
+
echo " Run the full Pester suite for a complete PowerShell coverage check."
|
|
3168
|
+
else
|
|
3169
|
+
echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3170
|
+
exit 1
|
|
3171
|
+
fi
|
|
3172
|
+
else
|
|
3173
|
+
echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3041
3174
|
fi
|
|
3042
|
-
echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3043
3175
|
else
|
|
3044
|
-
|
|
3176
|
+
# #570: an unparseable coverage.xml is no evidence, not a pass.
|
|
3177
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3178
|
+
echo "⚠️ Could not parse PowerShell coverage from coverage.xml (subset test run) - not blocking."
|
|
3179
|
+
else
|
|
3180
|
+
echo "❌ BLOCKED - Could not parse PowerShell coverage from coverage.xml,"
|
|
3181
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
3182
|
+
exit 1
|
|
3183
|
+
fi
|
|
3045
3184
|
fi
|
|
3046
3185
|
else
|
|
3047
|
-
|
|
3186
|
+
# #570: Pester coverage not run means the gate has nothing to answer with.
|
|
3187
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3188
|
+
echo "⚠️ coverage.xml not found (subset test run) - not blocking."
|
|
3189
|
+
else
|
|
3190
|
+
echo "❌ BLOCKED - coverage.xml not found, so the ${COVERAGE_THRESHOLD:-80}% PowerShell"
|
|
3191
|
+
echo " coverage floor cannot be verified. Ensure Pester coverage was run."
|
|
3192
|
+
exit 1
|
|
3193
|
+
fi
|
|
3048
3194
|
fi
|
|
3049
3195
|
;;
|
|
3050
3196
|
"iac")
|
|
@@ -3072,22 +3218,48 @@ else
|
|
|
3072
3218
|
COVERAGE_PERCENT=$(grep -oP 'coverage: \K[0-9]+%' coverage/lcov.info 2>/dev/null | head -1 | tr -d '%') || COVERAGE_PERCENT=""
|
|
3073
3219
|
fi
|
|
3074
3220
|
fi
|
|
3075
|
-
|
|
3076
|
-
|
|
3221
|
+
# #579: `-ge 0`, not `-gt 0`. A measured 0% is DATA -- coverage.py ran and
|
|
3222
|
+
# covered nothing -- not a missing value. Folding it into the
|
|
3223
|
+
# "could not determine" branch let a suite that tested nothing pass the
|
|
3224
|
+
# coverage gate with a WARNING. The two claims are different: 0% is
|
|
3225
|
+
# "executed, proved nothing"; undetermined is "no evidence produced".
|
|
3226
|
+
# #570: the threshold is defaulted HERE as well as quoted in the message.
|
|
3227
|
+
# A bare `[ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]` with an empty
|
|
3228
|
+
# threshold makes test(1) fail with "integer expected", the condition is
|
|
3229
|
+
# false, and 16% coverage was reported as ✅ (the same fail-open shape this
|
|
3230
|
+
# issue is about, one line further down). `2>/dev/null` on the numeric test
|
|
3231
|
+
# keeps a non-numeric report on the "cannot determine" path instead of
|
|
3232
|
+
# dying.
|
|
3233
|
+
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] \
|
|
3234
|
+
&& [ "$COVERAGE_PERCENT" -ge 0 ] 2>/dev/null; then
|
|
3235
|
+
if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
|
|
3077
3236
|
# Only a subset test run (smart selection) warns — the full suite may
|
|
3078
3237
|
# still reach ≥80%; a full suite run must block.
|
|
3079
3238
|
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3080
3239
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
3081
3240
|
echo " Run the full test suite for a complete ${CURRENT_LANG} coverage check."
|
|
3082
3241
|
else
|
|
3083
|
-
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
|
|
3242
|
+
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD:-80}% threshold"
|
|
3084
3243
|
exit 1
|
|
3085
3244
|
fi
|
|
3086
3245
|
else
|
|
3087
|
-
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
3246
|
+
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ ${COVERAGE_THRESHOLD:-80}%)"
|
|
3088
3247
|
fi
|
|
3089
3248
|
else
|
|
3090
|
-
|
|
3249
|
+
# #570: "could not determine" is a FAILED answer to the gate's question
|
|
3250
|
+
# ("is coverage at or above the floor?"), not a lenient one. Warning and
|
|
3251
|
+
# falling through made the generic branch fail-open: a language that
|
|
3252
|
+
# produced no standard report file passed the coverage gate while the
|
|
3253
|
+
# summary still looked clean. The ONE exception is a subset run, where
|
|
3254
|
+
# the global percentage is meaningless by construction.
|
|
3255
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
3256
|
+
echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage (subset test run) — not blocking."
|
|
3257
|
+
else
|
|
3258
|
+
echo "❌ BLOCKED - Could not determine ${CURRENT_LANG} coverage percentage."
|
|
3259
|
+
echo " No standard coverage report file was found, so the ${COVERAGE_THRESHOLD:-80}% floor"
|
|
3260
|
+
echo " could not be checked. Produce coverage data or run a subset test run."
|
|
3261
|
+
exit 1
|
|
3262
|
+
fi
|
|
3091
3263
|
fi
|
|
3092
3264
|
;;
|
|
3093
3265
|
esac
|
|
@@ -3156,7 +3328,20 @@ else
|
|
|
3156
3328
|
# "Tests + Coverage: PASS (coverage: N/A)" for a commit where no test ran
|
|
3157
3329
|
# (#554) -- a SKIP promoted to PASS inflates the pass rate and hides that the
|
|
3158
3330
|
# gate never executed.
|
|
3159
|
-
|
|
3331
|
+
#
|
|
3332
|
+
# #554 follow-up: the same promotion was still happening ONE LEVEL IN. The
|
|
3333
|
+
# early-skip branch above (no source extension among the changed files, e.g. a
|
|
3334
|
+
# version bump) sets GATE_5_STATUS="SKIP", and this unguarded assignment
|
|
3335
|
+
# overwrote it on the way out of the same else-block. A branch that records
|
|
3336
|
+
# its own verdict must not be re-decided by the block epilogue: default to
|
|
3337
|
+
# PASS only when the sentinel is still untouched.
|
|
3338
|
+
#
|
|
3339
|
+
# Not written as a `:-PASS` expansion: AC-599-03 forbids a PASS default in
|
|
3340
|
+
# any `GATE_N_STATUS:-DEFAULT` expansion, because that is how an unassigned
|
|
3341
|
+
# gate came to render as "passed". An explicit comparison keeps both rules.
|
|
3342
|
+
if [ -z "$GATE_5_STATUS" ]; then
|
|
3343
|
+
GATE_5_STATUS="PASS"
|
|
3344
|
+
fi
|
|
3160
3345
|
fi
|
|
3161
3346
|
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START" "${GATE_5_DETAIL:-}"
|
|
3162
3347
|
|
|
@@ -3611,10 +3796,20 @@ elif [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
|
3611
3796
|
GATE_9_SCRIPT="$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts"
|
|
3612
3797
|
fi
|
|
3613
3798
|
|
|
3799
|
+
# #581: the script path goes to node.exe. On WSL $HOME is /home/<user>, which node.exe
|
|
3800
|
+
# resolves to <current drive>:\home\<user>\... -- module not found -- and Gate 9 then
|
|
3801
|
+
# reported "Build integrity check failed" with advice to fix type errors in the USER'S
|
|
3802
|
+
# code. Convert the path first so the failure, if any, is a real one.
|
|
3803
|
+
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
3804
|
+
GATE_9_SCRIPT=$(to_native_path "$GATE_9_SCRIPT")
|
|
3805
|
+
fi
|
|
3806
|
+
# Same for the project root, which reaches both node and tsx as an absolute path.
|
|
3807
|
+
GATE_9_PROJECT_ROOT=$(to_native_path "$PROJECT_ROOT")
|
|
3808
|
+
|
|
3614
3809
|
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
3615
3810
|
CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
|
|
3616
3811
|
GATE_9_OUTPUT=$(mktemp)
|
|
3617
|
-
if RUN_TSX_TIMEOUT=120s run_tsx "$GATE_9_SCRIPT" --changed-files "$CHANGED_FILES_CSV" --project-root "$
|
|
3812
|
+
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
3813
|
GATE_9_TS_STATUS="PASS"
|
|
3619
3814
|
echo " ✅ Build integrity check passed."
|
|
3620
3815
|
else
|
|
@@ -3770,7 +3965,17 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
3770
3965
|
GATE_11_STATUS="SKIP"
|
|
3771
3966
|
;;
|
|
3772
3967
|
allow)
|
|
3773
|
-
|
|
3968
|
+
# #584: `allow` covered three different outcomes and all of them became
|
|
3969
|
+
# PASS, so a sprint that was never examined raised PASSED_COUNT and the
|
|
3970
|
+
# score. Only a genuinely validated sprint certifies this gate; the rest
|
|
3971
|
+
# declined to evaluate and must read as SKIP with the real reason shown.
|
|
3972
|
+
SPRINT_GATE_ALLOW_REASON=$(sprint_gate_allow_reason "$SPRINT_GATE_OUTPUT")
|
|
3973
|
+
if [ "$SPRINT_GATE_ALLOW_REASON" = "checks-passed" ]; then
|
|
3974
|
+
echo "✅ PASSED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-sprint state validated})"
|
|
3975
|
+
else
|
|
3976
|
+
echo "⏭️ SKIPPED - Gate 11: Sprint Flow (${SPRINT_GATE_ALLOW_REASON}: ${SPRINT_GATE_DETAIL:-not evaluated})"
|
|
3977
|
+
GATE_11_STATUS="SKIP"
|
|
3978
|
+
fi
|
|
3774
3979
|
;;
|
|
3775
3980
|
*)
|
|
3776
3981
|
# A run that succeeded but said nothing this gate understands. Pass on the exit
|
|
@@ -3841,7 +4046,14 @@ generate_quality_report() {
|
|
|
3841
4046
|
# -- degraded coverage, kept in the denominator but flagged separately so a
|
|
3842
4047
|
# degraded gate is never confused with a real PASS.
|
|
3843
4048
|
local DEGRADED_COUNT=0
|
|
4049
|
+
# The report's gatesDegraded is a superset: DEGRADED plus WARN (#536), because
|
|
4050
|
+
# both mean "this gate could not fully verify what it was asked to". Kept
|
|
4051
|
+
# separate from the scoring loop's DEGRADED_COUNT above so the two statuses stay
|
|
4052
|
+
# distinct where that distinction is load-bearing (#599-10).
|
|
4053
|
+
local REPORT_DEGRADED_COUNT=0
|
|
3844
4054
|
local DEGRADED_JSON=""
|
|
4055
|
+
local FAIL_COUNT=0
|
|
4056
|
+
local FAILED_JSON=""
|
|
3845
4057
|
for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
3846
4058
|
local status_var="GATE_${gate}_STATUS"
|
|
3847
4059
|
# Default mirrors the JSON report rendering below EXACTLY (gates 1-11
|
|
@@ -3857,8 +4069,22 @@ generate_quality_report() {
|
|
|
3857
4069
|
case "$gate_status" in
|
|
3858
4070
|
PASS) PASSED_COUNT=$((PASSED_COUNT + 1)) ;;
|
|
3859
4071
|
SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
|
|
3860
|
-
WARN
|
|
3861
|
-
|
|
4072
|
+
# #536: a WARN gate ran but could not fully do its job, so the REPORT's
|
|
4073
|
+
# gatesDegraded must include it -- that field answers "how much of this run
|
|
4074
|
+
# could actually verify what it was asked to". The scoring loop's
|
|
4075
|
+
# DEGRADED_COUNT deliberately does NOT include it: a plain WARN must stay
|
|
4076
|
+
# distinguishable from a gate that reported DEGRADED (#599-10), or the two
|
|
4077
|
+
# statuses become one and neither is worth anything.
|
|
4078
|
+
WARN) REPORT_DEGRADED_COUNT=$((REPORT_DEGRADED_COUNT + 1)); WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
|
|
4079
|
+
# #574: DEGRADED is a status a gate really produces -- Gate 5 reports it
|
|
4080
|
+
# when coverage could not be measured honestly instead of claiming PASS.
|
|
4081
|
+
# It had no arm here, so it fell into the catch-all below and counted as a
|
|
4082
|
+
# HARD FAILURE: the gate was punished for telling the truth about itself.
|
|
4083
|
+
# It must also feed WARN_COUNT, because the FAIL predicate is
|
|
4084
|
+
# (NONPASS_COUNT - WARN_COUNT) > 0; adding only NONPASS_COUNT would turn
|
|
4085
|
+
# every honest DEGRADED run into FAIL.
|
|
4086
|
+
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}\"" ;;
|
|
4087
|
+
*) FAIL_COUNT=$((FAIL_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)); FAILED_JSON="${FAILED_JSON:+$FAILED_JSON, }\"gate${gate}\"" ;;
|
|
3862
4088
|
esac
|
|
3863
4089
|
done
|
|
3864
4090
|
|
|
@@ -3872,6 +4098,14 @@ generate_quality_report() {
|
|
|
3872
4098
|
else
|
|
3873
4099
|
VERDICT="WARN" # all effective gates passed or warned; warns forbid PASS
|
|
3874
4100
|
fi
|
|
4101
|
+
elif [ "$SKIP_COUNT" -gt 0 ]; then
|
|
4102
|
+
# #575: PARTIAL. Something passed, but not everything ran, and "PASS" reads
|
|
4103
|
+
# as a statement about the commit — not about the fraction of the pipeline
|
|
4104
|
+
# that happened to execute. A docs-only commit runs 3 of 12 gates and used
|
|
4105
|
+
# to report `verdict: PASS`, which is a claim the run did not support.
|
|
4106
|
+
# This is deliberately NOT FAIL: nothing failed. It is also not SKIP-ALL,
|
|
4107
|
+
# which means no gate ran at all. Naming the gap is the whole point.
|
|
4108
|
+
VERDICT="PARTIAL"
|
|
3875
4109
|
fi
|
|
3876
4110
|
|
|
3877
4111
|
local SCORE=0
|
|
@@ -3966,8 +4200,8 @@ generate_quality_report() {
|
|
|
3966
4200
|
COVERAGE_WARNINGS_JSON="${COVERAGE_WARNINGS_JSON:+$COVERAGE_WARNINGS_JSON, }\"unsupported_language_name\""
|
|
3967
4201
|
continue
|
|
3968
4202
|
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:-
|
|
4203
|
+
_cm_g3=$(classify_coverage_reason "${GATE_3_REASON:-}" "${GATE_3_STATUS:-SKIP}")
|
|
4204
|
+
_cm_g4=$(classify_coverage_reason "${GATE_4_REASON:-}" "${GATE_4_STATUS:-SKIP}")
|
|
3971
4205
|
# Gate 6 reason for this language — extracted so the rule is testable
|
|
3972
4206
|
# rather than inlined (see coverage_g6_reason_for below).
|
|
3973
4207
|
_cm_g6_reason=$(coverage_g6_reason_for "$_cm_lang")
|
|
@@ -3989,7 +4223,10 @@ generate_quality_report() {
|
|
|
3989
4223
|
"gatesPassed": $PASSED_COUNT,
|
|
3990
4224
|
"gatesTotal": $TOTAL_GATES,
|
|
3991
4225
|
"gatesSkipped": $SKIP_COUNT,
|
|
3992
|
-
"
|
|
4226
|
+
"gatesWarned": $WARN_COUNT,
|
|
4227
|
+
"gatesFailed": $FAIL_COUNT,
|
|
4228
|
+
"failedGates": [$FAILED_JSON],
|
|
4229
|
+
"gatesDegraded": $REPORT_DEGRADED_COUNT,
|
|
3993
4230
|
"degradedGates": [$DEGRADED_JSON],
|
|
3994
4231
|
"gatesEffective": $EFFECTIVE_TOTAL,
|
|
3995
4232
|
"effectivePassRate": $EFFECTIVE_PASS_RATE_JSON,
|
|
@@ -4004,7 +4241,7 @@ generate_quality_report() {
|
|
|
4004
4241
|
"gates": {
|
|
4005
4242
|
"gate1_static_analysis": {
|
|
4006
4243
|
"name": "Code Quality (Static + Lint + Shell)",
|
|
4007
|
-
"status": "${GATE_1_STATUS:-
|
|
4244
|
+
"status": "${GATE_1_STATUS:-SKIP}",
|
|
4008
4245
|
"tool": "${GATE_1_TOOL:-auto}"
|
|
4009
4246
|
},
|
|
4010
4247
|
"gate2_dup_code": {
|
|
@@ -4014,7 +4251,7 @@ generate_quality_report() {
|
|
|
4014
4251
|
},
|
|
4015
4252
|
"gate3_complexity": {
|
|
4016
4253
|
"name": "Cyclomatic Complexity",
|
|
4017
|
-
"status": "${GATE_3_STATUS:-
|
|
4254
|
+
"status": "${GATE_3_STATUS:-SKIP}",
|
|
4018
4255
|
"reason": "${GATE_3_REASON:-}",
|
|
4019
4256
|
"threshold": "${CCN_THRESHOLD:-5}",
|
|
4020
4257
|
"blockThreshold": "${CCN_THRESHOLD:-5}",
|
|
@@ -4022,13 +4259,14 @@ generate_quality_report() {
|
|
|
4022
4259
|
},
|
|
4023
4260
|
"gate4_principles": {
|
|
4024
4261
|
"name": "Clean Code + SOLID",
|
|
4025
|
-
"status": "${GATE_4_STATUS:-
|
|
4262
|
+
"status": "${GATE_4_STATUS:-SKIP}",
|
|
4026
4263
|
"reason": "${GATE_4_REASON:-}",
|
|
4027
4264
|
"warnings": ${WARNING_COUNT:-0}
|
|
4028
4265
|
},
|
|
4029
4266
|
"gate5_tests": {
|
|
4030
4267
|
"name": "Tests + Coverage",
|
|
4031
|
-
"status": "${GATE_5_STATUS:-
|
|
4268
|
+
"status": "${GATE_5_STATUS:-SKIP}",
|
|
4269
|
+
"reason": "${GATE_5_DETAIL:-}",
|
|
4032
4270
|
"thresholds": { "lines": ${COVERAGE_THRESHOLD}, "functions": null, "branches": null, "statements": null },
|
|
4033
4271
|
"threshold_source": "${COVERAGE_THRESHOLD_SOURCE}",
|
|
4034
4272
|
"actual": { "coverage": "${COV_PCT}" },
|
|
@@ -4043,30 +4281,30 @@ generate_quality_report() {
|
|
|
4043
4281
|
},
|
|
4044
4282
|
"gate7_iac_security": {
|
|
4045
4283
|
"name": "IaC Security Scanning",
|
|
4046
|
-
"status": "${GATE_7_STATUS:-
|
|
4284
|
+
"status": "${GATE_7_STATUS:-SKIP}",
|
|
4047
4285
|
"scanned_files": ${GATE_7_SCANNED_FILES:-0},
|
|
4048
4286
|
"tools": "checkov, hadolint, kube-score, tflint"
|
|
4049
4287
|
},
|
|
4050
4288
|
"gate8_secret_scanning": {
|
|
4051
4289
|
"name": "Secret Scanning",
|
|
4052
|
-
"status": "${GATE_8_STATUS:-
|
|
4290
|
+
"status": "${GATE_8_STATUS:-SKIP}",
|
|
4053
4291
|
"tool": "gitleaks"
|
|
4054
4292
|
},
|
|
4055
4293
|
"gate9_build_integrity": {
|
|
4056
4294
|
"name": "Build Integrity",
|
|
4057
|
-
"status": "${GATE_9_STATUS:-
|
|
4295
|
+
"status": "${GATE_9_STATUS:-SKIP}",
|
|
4058
4296
|
"tool": "tsc + npm pack + import check"
|
|
4059
4297
|
},
|
|
4060
4298
|
"gate10_sast": {
|
|
4061
4299
|
"name": "SAST Security Scan",
|
|
4062
|
-
"status": "${GATE_10_STATUS:-
|
|
4300
|
+
"status": "${GATE_10_STATUS:-SKIP}",
|
|
4063
4301
|
"reason": "${GATE_10_REASON:-}",
|
|
4064
4302
|
"scanned_files": ${GATE_10_SCANNED_FILES:-0},
|
|
4065
4303
|
"tool": "semgrep"
|
|
4066
4304
|
},
|
|
4067
4305
|
"gate11_sprint_flow": {
|
|
4068
4306
|
"name": "Sprint Flow Enforcement",
|
|
4069
|
-
"status": "${GATE_11_STATUS:-
|
|
4307
|
+
"status": "${GATE_11_STATUS:-SKIP}",
|
|
4070
4308
|
"tool": "sprint-gate.sh"
|
|
4071
4309
|
},
|
|
4072
4310
|
"gate12_file_hygiene": {
|
|
@@ -4088,17 +4326,17 @@ ENDJSON
|
|
|
4088
4326
|
|
|
4089
4327
|
# ── History append (unchanged — append-only for trend) ────────────────────────────
|
|
4090
4328
|
local GATES_JSON="{"
|
|
4091
|
-
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-
|
|
4329
|
+
GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-SKIP}\",\"name\":\"Code Quality\"},"
|
|
4092
4330
|
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:-
|
|
4331
|
+
GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-SKIP}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
|
|
4332
|
+
GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-SKIP}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
|
|
4333
|
+
GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-SKIP}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
|
|
4096
4334
|
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:-
|
|
4335
|
+
GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-SKIP}\",\"name\":\"IaC Security\"},"
|
|
4336
|
+
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-SKIP}\",\"name\":\"Secret Scanning\"},"
|
|
4337
|
+
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-SKIP}\",\"name\":\"Build Integrity\"},"
|
|
4338
|
+
GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-SKIP}\",\"name\":\"SAST Security\"},"
|
|
4339
|
+
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-SKIP}\",\"name\":\"Sprint Flow\"},"
|
|
4102
4340
|
GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
|
|
4103
4341
|
|
|
4104
4342
|
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 +4347,17 @@ ENDJSON
|
|
|
4109
4347
|
echo " 📊 Quality Report — $TIMESTAMP"
|
|
4110
4348
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4111
4349
|
echo ""
|
|
4112
|
-
printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-
|
|
4350
|
+
printf " %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-SKIP}"
|
|
4113
4351
|
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:-
|
|
4352
|
+
printf " %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-SKIP}"
|
|
4353
|
+
printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-SKIP}"
|
|
4354
|
+
printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-SKIP}"
|
|
4117
4355
|
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:-
|
|
4356
|
+
printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-SKIP}"
|
|
4357
|
+
printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-SKIP}"
|
|
4358
|
+
printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-SKIP}"
|
|
4359
|
+
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-SKIP}"
|
|
4360
|
+
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-SKIP}"
|
|
4123
4361
|
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
4124
4362
|
echo ""
|
|
4125
4363
|
# #564: both views in one line. The effective score answers "of the gates that
|