@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.
Files changed (86) hide show
  1. package/adapter-common.sh +41 -3
  2. package/adapters/gate-3.sh +8 -1
  3. package/adapters/gate-8.sh +67 -6
  4. package/adapters/go.sh +18 -1
  5. package/adapters/iac.sh +18 -1
  6. package/adapters/powershell.sh +37 -1
  7. package/adapters/python.sh +18 -4
  8. package/gate-3.sh +254 -10
  9. package/gate-8.sh +67 -6
  10. package/hooks/adapter-common.sh +41 -3
  11. package/hooks/gate-3.sh +254 -10
  12. package/hooks/gate-8.sh +67 -6
  13. package/hooks/lib/Measure-PowerShellComplexity.ps1 +186 -0
  14. package/hooks/lib/coverage-total.sh +20 -5
  15. package/hooks/lib/gate-5c-alignment.sh +145 -0
  16. package/hooks/lib/jscpd-run.sh +40 -1
  17. package/hooks/lib/native-path.sh +106 -0
  18. package/hooks/lib/prepush-score.py +143 -0
  19. package/hooks/lib/sprint-gate-report.sh +23 -0
  20. package/hooks/lib/toolchain-path.sh +370 -0
  21. package/hooks/lib/validate-code-walkthrough.cjs +224 -23
  22. package/hooks/pre-commit +403 -165
  23. package/hooks/pre-push +192 -74
  24. package/hooks/sprint-gate.sh +14 -3
  25. package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
  26. package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
  27. package/lib/__tests__/doctor-delphi.test.js +46 -0
  28. package/lib/__tests__/doctor-exec-retry.test.js +20 -10
  29. package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
  30. package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
  31. package/lib/__tests__/doctor.test.js +7 -1
  32. package/lib/__tests__/hook-health-path.test.js +9 -1
  33. package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
  34. package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
  35. package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
  36. package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
  37. package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
  38. package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
  39. package/lib/arch.js +261 -44
  40. package/lib/check-alignment-runner.cjs +23 -0
  41. package/lib/check.js +4 -1
  42. package/lib/doctor.js +194 -49
  43. package/lib/gate-runner.js +265 -58
  44. package/lib/mattpocock-license-verdict.json +1 -1
  45. package/lib/principles.js +228 -6
  46. package/lib/shared-paths.js +53 -19
  47. package/mock-policy/AGENTS.md +3 -3
  48. package/mutation/AGENTS.md +3 -3
  49. package/mutation/__tests__/gremlins-runner.test.ts +3 -3
  50. package/package.json +1 -1
  51. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  52. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  53. package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
  54. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  55. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  56. package/plugins/dsh/package.json +1 -1
  57. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  58. package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
  59. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  60. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  61. package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
  62. package/plugins/dsh/src/delphi-config.ts +151 -9
  63. package/plugins/opencode/package.json +1 -1
  64. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  65. package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
  66. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  67. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  68. package/plugins/qoder/plugin.json +1 -1
  69. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  70. package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
  71. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  72. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  73. package/principles/AGENTS.md +3 -3
  74. package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
  75. package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
  76. package/principles/adapters/java.ts +127 -11
  77. package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
  78. package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
  79. package/principles/rules/solid/lsp.ts +348 -23
  80. package/scripts/__tests__/sync-package-content.test.js +32 -1
  81. package/scripts/sync-package-content.js +6 -1
  82. package/skills/delphi-review/AGENTS.md +3 -3
  83. package/skills/delphi-review/SKILL.md +50 -0
  84. package/skills/sprint-flow/AGENTS.md +3 -3
  85. package/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/sprint-gate.sh +14 -3
package/hooks/pre-commit CHANGED
@@ -1,20 +1,29 @@
1
1
  #!/bin/bash
2
- # Line-ending self-check (#546). Must stay on the first executable line: if this
3
- # file is ever rewritten with CRLF, bash needs to reach this check before the
4
- # parser chokes somewhere far away and blames an innocent line. `tr` also
5
- # strips the \r from this very line, so the check runs even when everything
6
- # around it is broken.
7
- if tr -d '\r' < "$0" | sed -n '2p' | tr -d '\r' | grep -q 'Line-ending self-check'; then
8
- if [ "$(tr -cd '\r' < "$0" | wc -c | tr -d '[:space:]')" -gt 0 ]; then
9
- echo "======================================================================" >&2
10
- echo "FATAL: hook has CRLF line endings — bash cannot parse it (#546)." >&2
11
- echo " file: $0" >&2
12
- echo " fix : tr -d '\\r' < \"$0\" > \"$0.tmp\" && mv \"$0.tmp\" \"$0\" && chmod +x \"$0\"" >&2
13
- echo "Or re-sync the installed copy: xp-gate doctor --sync-hooks" >&2
14
- echo "======================================================================" >&2
15
- exit 1
16
- fi
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:-PASS}"
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
- echo "ℹ️ No jscpd.conf.json found - using jscpd defaults"
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 changes the reporting, not the scan set
1953
- jscpd ${JSCPD_ARGS[@]+"${JSCPD_ARGS[@]}"} $(echo "$CHANGED_FILES" | tr '\n' ' ') > "$JSCPD_OUTPUT" 2>&1 || JSCPD_EXIT=$?
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
- jscpd $SOURCE_FILES 2>&1 | head -15
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
- # Gate 5c: Semantic Annotation Check (v0.19.0+)
2397
- # Validates @test REQ-XXX annotations against specification.yaml.
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
- # Strategy: Reads pre-compiled req-ids.json (produced by xp-gate check-alignment).
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
- REQ_IDS_FILE=".sprint-state/phase-outputs/req-ids.json"
2407
- if [ -f "$REQ_IDS_FILE" ] && command -v jq >/dev/null 2>&1; then
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
- echo "✅ PASSED - No code files changed (only docs/config/VERSION), skipping test run."
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
- console.log(Math.round(data.total.lines.pct));
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
- if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
2902
- if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
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
- echo "⚠️ Could not parse TypeScript coverage from coverage/coverage-summary.json"
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
- echo "⚠️ coverage/coverage-summary.json not found. If vitest --coverage was used, check vitest.config.ts coverageDirectory."
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 < 80) }'; then
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
- echo "⚠️ Could not parse Java coverage from ${jacoco_csv} (no instruction data)."
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
- echo "⚠️ No JaCoCo report found. Run 'mvn verify' or 'gradle jacocoTestReport'"
2984
- echo " so that target/site/jacoco/jacoco.csv exists — otherwise Java coverage"
2985
- echo " cannot be enforced (see also: JaCoCo detection in adapters/java.sh)."
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
- if [ "$COVERAGE_PERCENT" != "0" ] && [ -n "$COVERAGE_PERCENT" ]; then
2999
- if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
3000
- echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
3001
- exit 1
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
- echo "⚠️ Could not determine Flutter/Dart coverage percentage from lcov.info."
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
- echo "⚠️ coverage/lcov.info not found. Ensure dart test --coverage was run."
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
- COVERAGE_RESULT=$(node -e "
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 < 80 ? 'below' : 'pass') + '|' + Math.round(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
- echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below ${COVERAGE_THRESHOLD}% threshold"
3040
- exit 1
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
- echo "⚠️ Could not parse PowerShell coverage from coverage.xml"
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
- echo "⚠️ coverage.xml not found. Ensure Pester coverage was run."
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
- if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
3076
- if [ "$COVERAGE_PERCENT" -lt "$COVERAGE_THRESHOLD" ]; then
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
- echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage. No standard coverage report files found."
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
- GATE_5_STATUS="PASS"
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 "$PROJECT_ROOT" --timeout 115000 > "$GATE_9_OUTPUT" 2>&1; then
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
- echo "✅ PASSED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-sprint state validated})"
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) WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)); DEGRADED_COUNT=$((DEGRADED_COUNT + 1)); DEGRADED_JSON="${DEGRADED_JSON:+$DEGRADED_JSON, }\"gate${gate}\"";;
3861
- *) NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
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:-PASS}")
3970
- _cm_g4=$(classify_coverage_reason "${GATE_4_REASON:-}" "${GATE_4_STATUS:-PASS}")
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
- "gatesDegraded": $DEGRADED_COUNT,
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}",
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:-PASS}\",\"name\":\"Code Quality\"},"
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:-PASS}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
4094
- GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-PASS}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
4095
- GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-PASS}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
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:-PASS}\",\"name\":\"IaC Security\"},"
4098
- GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
4099
- GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"Build Integrity\"},"
4100
- GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-PASS}\",\"name\":\"SAST Security\"},"
4101
- GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
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:-PASS}"
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:-PASS}"
4115
- printf " %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-PASS}"
4116
- printf " %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-PASS}"
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:-PASS}"
4119
- printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
4120
- printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-PASS}"
4121
- printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-PASS}"
4122
- printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
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