@boyingliu01/xp-gate 0.10.17 → 0.11.1
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/build-integrity/gate-10.ts +24 -0
- package/hooks/pre-commit +303 -391
- package/hooks/pre-push +429 -28
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +105 -1
- 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/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +105 -1
- 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/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +105 -1
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-commit
CHANGED
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
# 1. Global: ~/.config/xp-gate/adapters (for core.hooksPath global setup)
|
|
12
12
|
# 2. Project-local: <repo>/githooks/ (for per-project init)
|
|
13
13
|
# 3. Script dir: hooks directory containing this script (fallback)
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
15
|
ADAPTER_DIR=""
|
|
15
16
|
GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"
|
|
16
17
|
PROJECT_GITHOOKS="$(git rev-parse --show-toplevel 2>/dev/null)/githooks"
|
|
18
|
+
GATES_DIR="$SCRIPT_DIR/gates"
|
|
17
19
|
|
|
18
20
|
if [ -f "$GLOBAL_ADAPTER_DIR/adapter-common.sh" ]; then
|
|
19
21
|
ADAPTER_DIR="$GLOBAL_ADAPTER_DIR"
|
|
20
22
|
elif [ -f "$PROJECT_GITHOOKS/adapter-common.sh" ]; then
|
|
21
23
|
ADAPTER_DIR="$PROJECT_GITHOOKS"
|
|
22
24
|
else
|
|
23
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
25
|
ADAPTER_DIR="$SCRIPT_DIR"
|
|
25
26
|
fi
|
|
26
27
|
source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
|
|
@@ -29,6 +30,49 @@ source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
|
|
|
29
30
|
exit 1
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
# Resolve adapter file path — handles both layouts:
|
|
34
|
+
# Global: ~/.config/xp-gate/adapters/typescript.sh (flat)
|
|
35
|
+
# Project: <repo>/githooks/adapters/typescript.sh (nested)
|
|
36
|
+
# Script dir: <hook-dir>/adapters/typescript.sh (nested)
|
|
37
|
+
# ADAPTER_DIR points to the base directory that contains either
|
|
38
|
+
# adapter-common.sh directly (global) or adapter-common.sh in a flat layout.
|
|
39
|
+
# Gate scripts live alongside adapter-common.sh.
|
|
40
|
+
resolve_adapter_path() {
|
|
41
|
+
local lang="$1"
|
|
42
|
+
# Try flat layout first (global: typescript.sh next to adapter-common.sh)
|
|
43
|
+
if [ -f "$ADAPTER_DIR/${lang}.sh" ]; then
|
|
44
|
+
echo "$ADAPTER_DIR/${lang}.sh"
|
|
45
|
+
return 0
|
|
46
|
+
fi
|
|
47
|
+
# Try nested layout (project: adapters/typescript.sh)
|
|
48
|
+
if [ -f "$ADAPTER_DIR/adapters/${lang}.sh" ]; then
|
|
49
|
+
echo "$ADAPTER_DIR/adapters/${lang}.sh"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
# Try project githooks as fallback
|
|
53
|
+
if [ -f "$PROJECT_GITHOOKS/adapters/${lang}.sh" ]; then
|
|
54
|
+
echo "$PROJECT_GITHOOKS/adapters/${lang}.sh"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
# Try script dir as last resort
|
|
58
|
+
if [ -f "$SCRIPT_DIR/adapters/${lang}.sh" ]; then
|
|
59
|
+
echo "$SCRIPT_DIR/adapters/${lang}.sh"
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Gate scripts directory — falls back to project githooks if ADAPTER_DIR doesn't have them
|
|
66
|
+
if [ -f "$ADAPTER_DIR/gate-3.sh" ]; then
|
|
67
|
+
GATE_DIR="$ADAPTER_DIR"
|
|
68
|
+
elif [ -f "$PROJECT_GITHOOKS/gate-3.sh" ]; then
|
|
69
|
+
GATE_DIR="$PROJECT_GITHOOKS"
|
|
70
|
+
elif [ -f "$SCRIPT_DIR/gate-3.sh" ]; then
|
|
71
|
+
GATE_DIR="$SCRIPT_DIR"
|
|
72
|
+
else
|
|
73
|
+
GATE_DIR="$ADAPTER_DIR"
|
|
74
|
+
fi
|
|
75
|
+
|
|
32
76
|
# Trap: ensure quality report generated on ANY exit (pass or fail).
|
|
33
77
|
# Using a guard prevents recursion when 'exit' is called from inside the trap.
|
|
34
78
|
# IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
|
|
@@ -132,7 +176,7 @@ echo ""
|
|
|
132
176
|
# ============================================================================
|
|
133
177
|
|
|
134
178
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
135
|
-
PROTECTED_BRANCHES="
|
|
179
|
+
PROTECTED_BRANCHES="master develop trunk mainline"
|
|
136
180
|
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
|
|
137
181
|
|
|
138
182
|
# Optional: project-level .protected-branches config file
|
|
@@ -155,7 +199,7 @@ if [ "$is_protected" = "true" ]; then
|
|
|
155
199
|
# Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
|
|
156
200
|
# Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
|
|
157
201
|
if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
|
|
158
|
-
echo "
|
|
202
|
+
echo "⏭️ SKIPPED - Gate 0: Version consistency (SKIP_VERSION_CHECK=1 env var)"
|
|
159
203
|
else
|
|
160
204
|
# Check if staged changes include VERSION or CHANGELOG.md
|
|
161
205
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
@@ -540,13 +584,13 @@ GATE_1_START=$(gate_start_ms)
|
|
|
540
584
|
|
|
541
585
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
542
586
|
# Documentation-only project - skip static analysis
|
|
543
|
-
echo "
|
|
544
|
-
|
|
587
|
+
echo "⏭️ SKIPPED - Code quality (documentation project)."
|
|
588
|
+
|
|
545
589
|
elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
546
590
|
# TypeScript: static analysis (tsc) + linting (eslint)
|
|
547
591
|
if ! command -v npx &> /dev/null; then
|
|
548
|
-
echo "ℹ️
|
|
549
|
-
echo "
|
|
592
|
+
echo "ℹ️ npx not available — skipping TypeScript checks"
|
|
593
|
+
echo "⏭️ SKIPPED - Code quality (npx not available)"
|
|
550
594
|
else
|
|
551
595
|
# Verify tsc is real TypeScript compiler (npx may fetch deprecated tsc@2.0.4 placeholder)
|
|
552
596
|
TSC_OUTPUT=$(npx tsc --version 2>&1)
|
|
@@ -625,14 +669,36 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
625
669
|
fi
|
|
626
670
|
else
|
|
627
671
|
echo "ℹ️ No ESLint configuration found - Skipping"
|
|
672
|
+
if [ -f "package.json" ] && command -v node &> /dev/null; then
|
|
673
|
+
HAS_ESLINT=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d.eslint||deps.eslint)?'yes':'no')}catch{console.log('no')}") && true
|
|
674
|
+
if [ "$HAS_ESLINT" = "yes" ]; then
|
|
675
|
+
echo "⚠️ WARNING: eslint is installed in package.json but no eslint config found"
|
|
676
|
+
echo " Either create .eslintrc.* / eslint.config.* or remove eslint from dependencies"
|
|
677
|
+
echo " See: https://eslint.org/docs/latest/use/configure/configuration-files"
|
|
678
|
+
fi
|
|
679
|
+
fi
|
|
680
|
+
fi
|
|
681
|
+
|
|
682
|
+
# ── Biome config detection ──
|
|
683
|
+
if ! [ -f "biome.json" ] && ! [ -f "biome.jsonc" ]; then
|
|
684
|
+
if [ -f "package.json" ] && command -v node &> /dev/null; then
|
|
685
|
+
HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
|
|
686
|
+
if [ "$HAS_BIOME" = "yes" ]; then
|
|
687
|
+
echo ""
|
|
688
|
+
echo "⚠️ WARNING: @biomejs/biome is installed in package.json but no biome.json found"
|
|
689
|
+
echo " Create a biome.json configuration file to enable Biome linting."
|
|
690
|
+
echo " NOTE: useLiteralKeys conflicts with TypeScript TS4111 — see docs/biome-configuration.md"
|
|
691
|
+
echo " For quick setup: add '\"linter\":{\"rules\":{\"complexity\":{\"useLiteralKeys\":\"off\"}}}' to biome.json"
|
|
692
|
+
fi
|
|
693
|
+
fi
|
|
628
694
|
fi
|
|
629
695
|
fi
|
|
630
696
|
|
|
631
697
|
elif [ "$PROJECT_LANG" = "python" ]; then
|
|
632
698
|
# Python: Ruff (includes syntax + linting) + mypy (optional typing)
|
|
633
699
|
if ! command -v ruff &> /dev/null; then
|
|
634
|
-
echo "ℹ️
|
|
635
|
-
echo "
|
|
700
|
+
echo "ℹ️ ruff not available — skipping Python linting"
|
|
701
|
+
echo "⏭️ SKIPPED - Code quality (ruff not available, run: pip install ruff)"
|
|
636
702
|
else
|
|
637
703
|
# Run Ruff (includes syntax check, linting)
|
|
638
704
|
echo "Running Ruff linting (syntax + lint)..."
|
|
@@ -711,8 +777,8 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
711
777
|
elif [ "$PROJECT_LANG" = "go" ]; then
|
|
712
778
|
# Go: go vet + golangci-lint
|
|
713
779
|
if ! command -v golangci-lint &> /dev/null; then
|
|
714
|
-
echo "ℹ️
|
|
715
|
-
echo "
|
|
780
|
+
echo "ℹ️ golangci-lint not available — skipping Go linting"
|
|
781
|
+
echo "⏭️ SKIPPED - Code quality (golangci-lint not available)"
|
|
716
782
|
else
|
|
717
783
|
echo "Running golangci-lint (comprehensive static analysis)..."
|
|
718
784
|
golangci-lint run 2>&1 | head -30
|
|
@@ -799,7 +865,8 @@ elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$';
|
|
|
799
865
|
|
|
800
866
|
else
|
|
801
867
|
# Route to appropriate adapter for other languages
|
|
802
|
-
|
|
868
|
+
ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
|
|
869
|
+
if [ -n "$ADAPTER_FILE" ] && source "$ADAPTER_FILE" 2>/dev/null; then
|
|
803
870
|
echo "Running static analysis for $PROJECT_LANG..."
|
|
804
871
|
run_static_analysis | head -30 2>/dev/null
|
|
805
872
|
ANALYSIS_EXIT=$?
|
|
@@ -817,13 +884,13 @@ else
|
|
|
817
884
|
if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
|
|
818
885
|
echo "✅ PASSED - Language-specific code quality checks."
|
|
819
886
|
else
|
|
820
|
-
#
|
|
821
|
-
echo "
|
|
887
|
+
# Tools unavailable or failed — skip non-blockingly
|
|
888
|
+
echo "⏭️ SKIPPED - Code quality (tools unavailable or failed)"
|
|
822
889
|
fi
|
|
823
890
|
else
|
|
824
891
|
# If no adapter exists for this language, skip
|
|
825
|
-
echo "ℹ️ No specific adapter for $PROJECT_LANG
|
|
826
|
-
echo "
|
|
892
|
+
echo "ℹ️ No specific adapter for $PROJECT_LANG — using generic checks if any"
|
|
893
|
+
echo "⏭️ SKIPPED - Code quality (no specific checks for $PROJECT_LANG)"
|
|
827
894
|
fi
|
|
828
895
|
fi
|
|
829
896
|
GATE_1_STATUS="PASS"
|
|
@@ -839,8 +906,8 @@ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
|
|
|
839
906
|
GATE_2_START=$(gate_start_ms)
|
|
840
907
|
|
|
841
908
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
842
|
-
|
|
843
|
-
|
|
909
|
+
echo "⏭️ SKIPPED - Duplicate code (documentation project)."
|
|
910
|
+
|
|
844
911
|
elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
845
912
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
846
913
|
exit 1
|
|
@@ -886,11 +953,11 @@ elif [ "$PROJECT_LANG" = "go" ]; then
|
|
|
886
953
|
|
|
887
954
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
888
955
|
echo "ℹ️ No PowerShell-native duplicate detector (jscpd does not support .ps1)"
|
|
889
|
-
echo "
|
|
956
|
+
echo "⏭️ SKIPPED - Duplicate code (no tool for PowerShell)"
|
|
890
957
|
|
|
891
958
|
elif [ "$PROJECT_LANG" = "shell" ]; then
|
|
892
959
|
echo "ℹ️ Duplicate detection not typically used for shell scripts"
|
|
893
|
-
echo "
|
|
960
|
+
echo "⏭️ SKIPPED - Duplicate code (no tool for shell scripts)"
|
|
894
961
|
|
|
895
962
|
else
|
|
896
963
|
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
|
|
@@ -909,132 +976,16 @@ GATE_2_STATUS="PASS"
|
|
|
909
976
|
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
910
977
|
|
|
911
978
|
# ============================================================================
|
|
912
|
-
# GATE 3: Cyclomatic Complexity Check
|
|
913
|
-
#
|
|
979
|
+
# GATE 3: Cyclomatic Complexity Check
|
|
980
|
+
# Extracted to gate-3.sh for maintainability
|
|
914
981
|
# ============================================================================
|
|
915
|
-
|
|
916
|
-
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
917
|
-
GATE_3_START=$(gate_start_ms)
|
|
918
|
-
|
|
919
|
-
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
920
|
-
echo "✅ PASSED - Skipped (documentation project)."
|
|
921
|
-
|
|
922
|
-
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
923
|
-
echo "ℹ️ No PowerShell principles checker available"
|
|
924
|
-
echo "✅ PASSED - Skipped (no PowerShell Clean Code / SOLID tool)"
|
|
925
|
-
|
|
926
|
-
else
|
|
927
|
-
CCN_THRESHOLD=5
|
|
928
|
-
|
|
929
|
-
# Check lizard availability
|
|
930
|
-
LIZARD_CMD=""
|
|
931
|
-
if command -v lizard > /dev/null 2>&1; then
|
|
932
|
-
LIZARD_CMD=lizard
|
|
933
|
-
elif [ -f ~/.local/bin/lizard ]; then
|
|
934
|
-
LIZARD_CMD=~/.local/bin/lizard
|
|
935
|
-
fi
|
|
936
|
-
|
|
937
|
-
if [ -n "$LIZARD_CMD" ]; then
|
|
938
|
-
LIZARD_PATH=$(eval echo "$LIZARD_CMD")
|
|
939
|
-
|
|
940
|
-
# Get changed source files for complexity check
|
|
941
|
-
CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
|
|
942
|
-
|
|
943
|
-
if [ -n "$CC_FILES" ]; then
|
|
944
|
-
echo "Checking complexity for source files..."
|
|
945
|
-
|
|
946
|
-
# Run lizard with CCN threshold
|
|
947
|
-
CC_OUTPUT=$($LIZARD_PATH -C $CCN_THRESHOLD $CC_FILES 2>&1 || true)
|
|
948
|
-
|
|
949
|
-
# Parse warning count from the summary table: "Warning cnt 8"
|
|
950
|
-
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
951
|
-
CC_WARNINGS=$(echo "$CC_OUTPUT" | grep "^Warning cnt" | awk '{print $NF}' | tr -d '[:space:]' | sed 's/[^0-9]//g' || true)
|
|
952
|
-
CC_WARNINGS=${CC_WARNINGS:-0}
|
|
953
|
-
|
|
954
|
-
if [ "$CC_WARNINGS" -gt 0 ]; then
|
|
955
|
-
echo "$CC_OUTPUT"
|
|
956
|
-
echo ""
|
|
957
|
-
echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
|
|
958
|
-
echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
|
|
959
|
-
exit 1
|
|
960
|
-
else
|
|
961
|
-
echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
|
|
962
|
-
fi
|
|
963
|
-
else
|
|
964
|
-
echo "✅ PASSED - No source files to check for complexity."
|
|
965
|
-
fi
|
|
966
|
-
else
|
|
967
|
-
echo "⚠️ WARN - lizard not installed, complexity check not performed"
|
|
968
|
-
echo " Install with: pip install --user lizard"
|
|
969
|
-
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
970
|
-
GATE_3_STATUS="WARN"
|
|
971
|
-
fi
|
|
972
|
-
fi
|
|
973
|
-
GATE_3_STATUS="PASS"
|
|
974
|
-
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
982
|
+
source "$GATE_DIR/gate-3.sh"
|
|
975
983
|
|
|
976
984
|
# ============================================================================
|
|
977
|
-
# GATE 4: Principles Checker (
|
|
978
|
-
#
|
|
985
|
+
# GATE 4: Principles Checker (Clean Code + SOLID)
|
|
986
|
+
# Extracted to gate-4.sh for maintainability
|
|
979
987
|
# ============================================================================
|
|
980
|
-
|
|
981
|
-
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
982
|
-
GATE_4_START=$(gate_start_ms)
|
|
983
|
-
|
|
984
|
-
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
985
|
-
echo "✅ PASSED - Skipped (documentation project)."
|
|
986
|
-
|
|
987
|
-
else
|
|
988
|
-
# Get source files to check against principles
|
|
989
|
-
PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
|
|
990
|
-
|
|
991
|
-
if [ -n "$PRINCIPLES_FILES" ]; then
|
|
992
|
-
# Check if principles checker exists in project
|
|
993
|
-
if [ -f "src/principles/index.ts" ]; then
|
|
994
|
-
echo "Checking Clean Code + SOLID principles..."
|
|
995
|
-
|
|
996
|
-
if command -v npx > /dev/null 2>&1; then
|
|
997
|
-
# Run principles checker and store results
|
|
998
|
-
if npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format json > /tmp/principles-output.json 2>/dev/null; then
|
|
999
|
-
# Check severity levels
|
|
1000
|
-
ERROR_COUNT=$(grep -c '"severity":"error"' /tmp/principles-output.json 2>/dev/null || true)
|
|
1001
|
-
ERROR_COUNT=${ERROR_COUNT:-0}
|
|
1002
|
-
WARNING_COUNT=$(grep -c '"severity":"warning"' /tmp/principles-output.json 2>/dev/null || true)
|
|
1003
|
-
WARNING_COUNT=${WARNING_COUNT:-0}
|
|
1004
|
-
|
|
1005
|
-
if [ "$ERROR_COUNT" -gt 0 ]; then
|
|
1006
|
-
echo ""
|
|
1007
|
-
echo "❌ BLOCKED - $ERROR_COUNT principle ERROR(S) found"
|
|
1008
|
-
echo "Critical violations must be fixed before commit:"
|
|
1009
|
-
echo " - error-handling violations"
|
|
1010
|
-
echo " - SOLID principle violations"
|
|
1011
|
-
echo " - architectural violations"
|
|
1012
|
-
npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format console
|
|
1013
|
-
exit 1
|
|
1014
|
-
fi
|
|
1015
|
-
|
|
1016
|
-
echo "✅ PASSED - Principles checker (no errors found)."
|
|
1017
|
-
if [ "$WARNING_COUNT" -gt 0 ]; then
|
|
1018
|
-
echo "ℹ️ $WARNING_COUNT warnings found (will be handled by Boy Scout Rule)."
|
|
1019
|
-
fi
|
|
1020
|
-
else
|
|
1021
|
-
echo "⚠️ Warning: Principles checker execution failed"
|
|
1022
|
-
echo "✅ PASSED - Principles check (SKIP, execution issue)"
|
|
1023
|
-
fi
|
|
1024
|
-
else
|
|
1025
|
-
echo "ℹ️ npx not available - skipping principles check"
|
|
1026
|
-
echo "✅ PASSED - Principles check (SKIP, no Node.js)"
|
|
1027
|
-
fi
|
|
1028
|
-
else
|
|
1029
|
-
echo "ℹ️ Principles checker not found in project - skipping"
|
|
1030
|
-
echo "✅ PASSED - Principles check (SKIP, not available in project)"
|
|
1031
|
-
fi
|
|
1032
|
-
else
|
|
1033
|
-
echo "✅ PASSED - No source files changed (principles check skipped)."
|
|
1034
|
-
fi
|
|
1035
|
-
fi
|
|
1036
|
-
GATE_4_STATUS="PASS"
|
|
1037
|
-
record_gate_audit "gate-4" "principles" "$GATE_4_STATUS" "${WARNING_COUNT:-0}" "$GATE_4_START"
|
|
988
|
+
source "$GATE_DIR/gate-4.sh"
|
|
1038
989
|
|
|
1039
990
|
# ============================================================================
|
|
1040
991
|
# GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
|
|
@@ -1049,15 +1000,133 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
1049
1000
|
|
|
1050
1001
|
else
|
|
1051
1002
|
# ========================================================================
|
|
1052
|
-
# Gate 5a: Test-Source File Pairing Check
|
|
1003
|
+
# Gate 5a: Test-Source File Pairing Check
|
|
1004
|
+
# New .ts/.tsx files: BLOCK (no corresponding test → commit rejected)
|
|
1005
|
+
# Modified files (any lang): WARNING (backward compatible)
|
|
1006
|
+
# New non-TS files: WARNING (unchanged)
|
|
1007
|
+
# Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only, audit logged)
|
|
1008
|
+
# Grace period: .tdd-adoption.yaml gracePeriod: N (first N commits = WARNING only)
|
|
1053
1009
|
# Checks coexistence, not temporal order. True "test-first" enforced by
|
|
1054
1010
|
# Agent skills (Layer 1 + Layer 4).
|
|
1055
1011
|
# ========================================================================
|
|
1056
|
-
NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' || true)
|
|
1057
1012
|
|
|
1058
|
-
|
|
1013
|
+
# --- Pre-checks: grace period + escape valve ---
|
|
1014
|
+
TDD_GRACE=0
|
|
1015
|
+
if [ -f ".tdd-adoption.yaml" ]; then
|
|
1016
|
+
TDD_ENABLED=$(grep -E '^\s*enabled:' .tdd-adoption.yaml | awk '{print $2}' || echo "true")
|
|
1017
|
+
if [ "$TDD_ENABLED" != "false" ]; then
|
|
1018
|
+
TDD_GRACE=$(grep -E '^\s*gracePeriod:' .tdd-adoption.yaml | awk '{print $2}' || echo "0")
|
|
1019
|
+
fi
|
|
1020
|
+
fi
|
|
1021
|
+
|
|
1022
|
+
TDD_BLOCK_DOWNGRADE=false
|
|
1023
|
+
if [ "$TDD_GRACE" -gt 0 ] 2>/dev/null; then
|
|
1024
|
+
TDD_BLOCK_DOWNGRADE=true
|
|
1025
|
+
fi
|
|
1026
|
+
|
|
1027
|
+
SKIP_5A_BLOCK=false
|
|
1028
|
+
if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
|
|
1029
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
1030
|
+
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
|
|
1031
|
+
REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
|
|
1032
|
+
mkdir -p .xp-gate/reports
|
|
1033
|
+
# Escape special characters in REASON for valid JSON
|
|
1034
|
+
ESCAPED_REASON=$(printf '%s' "$REASON" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
1035
|
+
ESCAPED_USER=$(printf '%s' "$(git config user.name)" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
1036
|
+
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"branch\":\"$CURRENT_BRANCH\",\"user\":\"$ESCAPED_USER\",\"reason\":\"$ESCAPED_REASON\",\"gate\":\"5a-block\"}" >> .xp-gate/reports/escape-valve-log.json
|
|
1037
|
+
echo "⚠️ ESCAPE VALVE: SKIP_GATE_5A_BLOCK=1 on branch $CURRENT_BRANCH (reason: $REASON)"
|
|
1038
|
+
echo " ⚠️ Post-mortem issue required within 24h"
|
|
1039
|
+
SKIP_5A_BLOCK=true
|
|
1040
|
+
else
|
|
1041
|
+
echo "❌ ESCAPE VALVE BLOCKED: SKIP_GATE_5A_BLOCK not allowed on main/master branch"
|
|
1042
|
+
exit 1
|
|
1043
|
+
fi
|
|
1044
|
+
fi
|
|
1045
|
+
|
|
1046
|
+
# --- BLOCK path: New .ts/.tsx files without corresponding tests ---
|
|
1047
|
+
NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v 'node_modules/' | grep -v '\.next/' | grep -v '\.nuxt/' | grep -v 'dist/' | grep -v 'build/' | grep -v '.turbo/' | grep -v '.cache/' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
|
|
1048
|
+
|
|
1049
|
+
if [ -n "$NEW_TS_FILES" ]; then
|
|
1050
|
+
BLOCKED_TS_FILES=0
|
|
1051
|
+
for src_file in $NEW_TS_FILES; do
|
|
1052
|
+
base="${src_file%.*}"
|
|
1053
|
+
ext="${src_file##*.}"
|
|
1054
|
+
filename="${base##*/}"
|
|
1055
|
+
dir="$(dirname "$src_file")"
|
|
1056
|
+
|
|
1057
|
+
# Skip excluded files: index/barrel, types, interfaces, constants, declarations
|
|
1058
|
+
case "$filename" in
|
|
1059
|
+
index|types|interfaces|constants|__init__) continue ;;
|
|
1060
|
+
esac
|
|
1061
|
+
case "$ext" in
|
|
1062
|
+
d.ts|pyi) continue ;;
|
|
1063
|
+
esac
|
|
1064
|
+
# Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
|
|
1065
|
+
# Order matters: @no-test-required must be checked first — @no-test is a substring match
|
|
1066
|
+
if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
|
|
1067
|
+
if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
|
|
1068
|
+
continue
|
|
1069
|
+
fi
|
|
1070
|
+
# @no-test-required with short reason (< 10 chars) — fall through to BLOCK
|
|
1071
|
+
elif grep -q '@no-test' "$src_file" 2>/dev/null; then
|
|
1072
|
+
continue
|
|
1073
|
+
fi
|
|
1074
|
+
|
|
1075
|
+
# Check common test file patterns for TypeScript
|
|
1076
|
+
TEST_FOUND=false
|
|
1077
|
+
patterns=(
|
|
1078
|
+
"${base}.test.${ext}"
|
|
1079
|
+
"${base}.spec.${ext}"
|
|
1080
|
+
"${dir}/__tests__/${filename}.test.${ext}"
|
|
1081
|
+
"${dir}/__tests__/${filename}.spec.${ext}"
|
|
1082
|
+
"tests/${filename}.test.${ext}"
|
|
1083
|
+
"tests/${filename}.spec.${ext}"
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
for pattern in "${patterns[@]}"; do
|
|
1087
|
+
if [[ "$pattern" == *'*'* ]]; then
|
|
1088
|
+
found=$(find . -path "*/${pattern}" -type f 2>/dev/null | head -1)
|
|
1089
|
+
if [ -n "$found" ]; then TEST_FOUND=true; break; fi
|
|
1090
|
+
elif [ -f "$pattern" ]; then
|
|
1091
|
+
TEST_FOUND=true
|
|
1092
|
+
break
|
|
1093
|
+
fi
|
|
1094
|
+
done
|
|
1095
|
+
|
|
1096
|
+
if [ "$TEST_FOUND" = false ]; then
|
|
1097
|
+
if [ "$SKIP_5A_BLOCK" = true ] || [ "$TDD_BLOCK_DOWNGRADE" = true ]; then
|
|
1098
|
+
echo "⚠️ TEST PAIRING WARNING (downgraded): New TypeScript file without test: $src_file"
|
|
1099
|
+
echo " Expected: ${patterns[0]}, ${patterns[1]:-...}"
|
|
1100
|
+
echo " Or: Add '// @no-test-required: <reason>' if this file doesn't need tests"
|
|
1101
|
+
else
|
|
1102
|
+
echo "❌ BLOCKED: $src_file — New TypeScript source file without corresponding test"
|
|
1103
|
+
echo " Expected: ${patterns[0]}, ${patterns[1]:-...}"
|
|
1104
|
+
echo " Add '// @no-test-required: <reason>' if this file doesn't need tests"
|
|
1105
|
+
BLOCKED_TS_FILES=$((BLOCKED_TS_FILES + 1))
|
|
1106
|
+
fi
|
|
1107
|
+
fi
|
|
1108
|
+
done
|
|
1109
|
+
|
|
1110
|
+
if [ "$BLOCKED_TS_FILES" -gt 0 ]; then
|
|
1111
|
+
echo ""
|
|
1112
|
+
echo "❌ $BLOCKED_TS_FILES new TypeScript file(s) without corresponding tests — commit BLOCKED"
|
|
1113
|
+
echo " Add test files or '// @no-test-required: <reason>' annotation."
|
|
1114
|
+
echo " Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only)"
|
|
1115
|
+
exit 1
|
|
1116
|
+
fi
|
|
1117
|
+
fi
|
|
1118
|
+
|
|
1119
|
+
# --- WARNING path: Modified files (any lang) + New non-TS files ---
|
|
1120
|
+
WARNING_SOURCE_FILES=$(
|
|
1121
|
+
{
|
|
1122
|
+
git diff --cached --name-only --diff-filter=M | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
1123
|
+
git diff --cached --name-only --diff-filter=A | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
|
|
1124
|
+
} | sort -u || true
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
if [ -n "$WARNING_SOURCE_FILES" ]; then
|
|
1059
1128
|
PAIRING_WARNINGS=0
|
|
1060
|
-
for src_file in $
|
|
1129
|
+
for src_file in $WARNING_SOURCE_FILES; do
|
|
1061
1130
|
base="${src_file%.*}"
|
|
1062
1131
|
ext="${src_file##*.}"
|
|
1063
1132
|
filename="${base##*/}"
|
|
@@ -1070,8 +1139,14 @@ else
|
|
|
1070
1139
|
case "$ext" in
|
|
1071
1140
|
d.ts|pyi) continue ;;
|
|
1072
1141
|
esac
|
|
1073
|
-
# Skip files with
|
|
1074
|
-
|
|
1142
|
+
# Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
|
|
1143
|
+
# Order matters: @no-test-required must be checked first — @no-test is a substring match
|
|
1144
|
+
if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
|
|
1145
|
+
if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
|
|
1146
|
+
continue
|
|
1147
|
+
fi
|
|
1148
|
+
# @no-test-required with short reason (< 10 chars) — fall through to BLOCK
|
|
1149
|
+
elif grep -q '@no-test' "$src_file" 2>/dev/null; then
|
|
1075
1150
|
continue
|
|
1076
1151
|
fi
|
|
1077
1152
|
|
|
@@ -1149,18 +1224,18 @@ else
|
|
|
1149
1224
|
done
|
|
1150
1225
|
|
|
1151
1226
|
if [ "$TEST_FOUND" = false ]; then
|
|
1152
|
-
echo "⚠️ TEST PAIRING WARNING:
|
|
1227
|
+
echo "⚠️ TEST PAIRING WARNING: Source file without corresponding test: $src_file"
|
|
1153
1228
|
echo " Expected patterns for .${ext}: ${patterns[0]}, ${patterns[1]:-...}"
|
|
1154
|
-
echo " Or: Add '// @no-test' annotation if this file doesn't need tests"
|
|
1229
|
+
echo " Or: Add '// @no-test-required: <reason>' annotation if this file doesn't need tests"
|
|
1155
1230
|
PAIRING_WARNINGS=$((PAIRING_WARNINGS + 1))
|
|
1156
1231
|
fi
|
|
1157
1232
|
done
|
|
1158
1233
|
|
|
1159
1234
|
if [ "$PAIRING_WARNINGS" -gt 0 ]; then
|
|
1160
1235
|
echo ""
|
|
1161
|
-
echo "⚠️ $PAIRING_WARNINGS
|
|
1236
|
+
echo "⚠️ $PAIRING_WARNINGS source file(s) without corresponding tests"
|
|
1162
1237
|
echo " This is a WARNING — commit proceeds. TDD order enforced by Agent skills."
|
|
1163
|
-
echo " To suppress for specific files, add '// @no-test' at the top."
|
|
1238
|
+
echo " To suppress for specific files, add '// @no-test-required: <reason>' at the top."
|
|
1164
1239
|
fi
|
|
1165
1240
|
fi
|
|
1166
1241
|
|
|
@@ -1180,7 +1255,11 @@ else
|
|
|
1180
1255
|
'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
|
|
1181
1256
|
'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
|
|
1182
1257
|
'\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
|
|
1183
|
-
|
|
1258
|
+
# Use grep -o piped to wc -l for reliable single-number output (grep -c can emit
|
|
1259
|
+
# multi-line output with filenames under some grep versions, causing bash arithmetic
|
|
1260
|
+
# syntax errors). grep -o extracts each match on its own line, wc -l counts them.
|
|
1261
|
+
c=$(grep -o "$kw" "$test_file" 2>/dev/null | wc -l || true)
|
|
1262
|
+
c=${c//[^0-9]/}
|
|
1184
1263
|
c=${c:-0}
|
|
1185
1264
|
MOCK_COUNT=$((MOCK_COUNT + c))
|
|
1186
1265
|
done
|
|
@@ -1219,8 +1298,9 @@ else
|
|
|
1219
1298
|
echo "Running tests..."
|
|
1220
1299
|
|
|
1221
1300
|
# Route to appropriate test runner via adapter
|
|
1222
|
-
|
|
1223
|
-
|
|
1301
|
+
ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
|
|
1302
|
+
if [ "${PROJECT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
|
|
1303
|
+
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
1224
1304
|
TESTS_OUTPUT=$(run_tests 2>&1)
|
|
1225
1305
|
TESTS_EXIT_CODE=$?
|
|
1226
1306
|
echo "$TESTS_OUTPUT" | head -50
|
|
@@ -1232,17 +1312,17 @@ else
|
|
|
1232
1312
|
echo "✅ PASSED - Unit tests passed."
|
|
1233
1313
|
fi
|
|
1234
1314
|
else
|
|
1235
|
-
echo "✅ PASSED -
|
|
1315
|
+
echo "✅ PASSED - Could not source ${PROJECT_LANG} adapter for tests, assuming none to run."
|
|
1236
1316
|
fi
|
|
1237
1317
|
else
|
|
1238
|
-
echo "✅ PASSED - No
|
|
1318
|
+
echo "✅ PASSED - No ${PROJECT_LANG} adapter found for tests, assuming none to run."
|
|
1239
1319
|
fi
|
|
1240
1320
|
|
|
1241
1321
|
# Next, run coverage check
|
|
1242
1322
|
echo "Running coverage check..."
|
|
1243
1323
|
|
|
1244
|
-
if [ "${PROJECT_LANG}" != "unknown" ] && [ -
|
|
1245
|
-
if source "$
|
|
1324
|
+
if [ "${PROJECT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
|
|
1325
|
+
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
1246
1326
|
case "$PROJECT_LANG" in
|
|
1247
1327
|
"typescript")
|
|
1248
1328
|
if [ -f "package.json" ] && grep -q '"test:coverage"' package.json 2>/dev/null; then
|
|
@@ -1299,12 +1379,12 @@ else
|
|
|
1299
1379
|
esac
|
|
1300
1380
|
else
|
|
1301
1381
|
# Adapter source failed — warn and let Stage 2 attempt file-based enforcement
|
|
1302
|
-
echo "⚠️ Could not source ${PROJECT_LANG} adapter, will check coverage files directly..."
|
|
1382
|
+
echo "⚠️ Could not source ${PROJECT_LANG} adapter from ${ADAPTER_FILE}, will check coverage files directly..."
|
|
1303
1383
|
COV_EXIT=0
|
|
1304
1384
|
fi
|
|
1305
1385
|
else
|
|
1306
1386
|
# No adapter available — warn and let Stage 2 attempt file-based enforcement
|
|
1307
|
-
echo "⚠️ No adapter for ${PROJECT_LANG}
|
|
1387
|
+
echo "⚠️ No adapter found for ${PROJECT_LANG} (searched: ${ADAPTER_DIR}/, ${ADAPTER_DIR}/adapters/, ${PROJECT_GITHOOKS}/adapters/, ${SCRIPT_DIR}/adapters/) — will check coverage files directly..."
|
|
1308
1388
|
COV_EXIT=0
|
|
1309
1389
|
fi
|
|
1310
1390
|
|
|
@@ -1488,7 +1568,7 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
1488
1568
|
|
|
1489
1569
|
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
1490
1570
|
echo " ℹ️ No PowerShell architecture tooling available"
|
|
1491
|
-
|
|
1571
|
+
echo " ⏭️ SKIPPED - Architecture validation (no tool for PowerShell)"
|
|
1492
1572
|
|
|
1493
1573
|
else
|
|
1494
1574
|
# Check if architecture config exists
|
|
@@ -1549,7 +1629,7 @@ else
|
|
|
1549
1629
|
fi
|
|
1550
1630
|
else
|
|
1551
1631
|
echo " ℹ️ No .import-linter.yml found - skipping Python architecture"
|
|
1552
|
-
echo "
|
|
1632
|
+
echo " ⏭️ SKIPPED - Python architecture validation (no config)"
|
|
1553
1633
|
fi
|
|
1554
1634
|
;;
|
|
1555
1635
|
"go")
|
|
@@ -1565,7 +1645,7 @@ else
|
|
|
1565
1645
|
fi
|
|
1566
1646
|
else
|
|
1567
1647
|
echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
|
|
1568
|
-
echo "
|
|
1648
|
+
echo " ⏭️ SKIPPED - Go architecture validation (no config)"
|
|
1569
1649
|
fi
|
|
1570
1650
|
;;
|
|
1571
1651
|
"java")
|
|
@@ -1581,19 +1661,19 @@ else
|
|
|
1581
1661
|
echo " ✅ Java architecture validation completed."
|
|
1582
1662
|
else
|
|
1583
1663
|
echo " ℹ️ No architecture test files found - skipping"
|
|
1584
|
-
echo "
|
|
1664
|
+
echo " ⏭️ SKIPPED - Architecture validation (no tests)"
|
|
1585
1665
|
fi
|
|
1586
1666
|
;;
|
|
1587
1667
|
*)
|
|
1588
1668
|
echo " ℹ️ Architecture validation not configured for $PROJECT_LANG"
|
|
1589
|
-
echo "
|
|
1669
|
+
echo " ⏭️ SKIPPED - Architecture validation (not configured for $PROJECT_LANG)"
|
|
1590
1670
|
;;
|
|
1591
1671
|
esac
|
|
1592
1672
|
else
|
|
1593
1673
|
# Architecture config is REQUIRED — BLOCK if missing
|
|
1594
1674
|
if [ -f ".architecture-skip" ]; then
|
|
1595
1675
|
echo " ⚠️ Architecture config missing but .architecture-skip present — allowed to skip"
|
|
1596
|
-
echo "
|
|
1676
|
+
echo " ⏭️ SKIPPED - Architecture validation (.architecture-skip exemption)"
|
|
1597
1677
|
else
|
|
1598
1678
|
echo ""
|
|
1599
1679
|
echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
|
|
@@ -1626,8 +1706,15 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
1626
1706
|
echo " ✅ Skipped (documentation project)."
|
|
1627
1707
|
|
|
1628
1708
|
else
|
|
1629
|
-
# Check
|
|
1630
|
-
|
|
1709
|
+
# Check for Boy Scout script in installed modules first, then project src/
|
|
1710
|
+
BOY_SCOUT_SCRIPT=""
|
|
1711
|
+
if [ -f ".xp-gate/modules/principles/boy-scout.ts" ]; then
|
|
1712
|
+
BOY_SCOUT_SCRIPT=".xp-gate/modules/principles/boy-scout.ts"
|
|
1713
|
+
elif [ -f "src/principles/boy-scout.ts" ]; then
|
|
1714
|
+
BOY_SCOUT_SCRIPT="src/principles/boy-scout.ts"
|
|
1715
|
+
fi
|
|
1716
|
+
|
|
1717
|
+
if [ -n "$BOY_SCOUT_SCRIPT" ]; then
|
|
1631
1718
|
echo " Checking Boy Scout Rule compliance..."
|
|
1632
1719
|
|
|
1633
1720
|
# Separate new files and modified files
|
|
@@ -1643,7 +1730,7 @@ else
|
|
|
1643
1730
|
if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
|
|
1644
1731
|
echo " ✅ No new or modified source files for Boy Scout check."
|
|
1645
1732
|
else
|
|
1646
|
-
BOY_SCOUT_OUTPUT=$(npx tsx
|
|
1733
|
+
BOY_SCOUT_OUTPUT=$(npx tsx $BOY_SCOUT_SCRIPT \
|
|
1647
1734
|
--new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
|
|
1648
1735
|
--modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
|
|
1649
1736
|
--baseline ".warnings-baseline.json" 2>&1)
|
|
@@ -1675,12 +1762,12 @@ else
|
|
|
1675
1762
|
fi
|
|
1676
1763
|
else
|
|
1677
1764
|
echo " ℹ️ npx not available - skipping Boy Scout Rule"
|
|
1678
|
-
echo "
|
|
1765
|
+
echo " ⏭️ SKIPPED - Boy Scout Rule (Node.js/npx not available)"
|
|
1679
1766
|
fi
|
|
1680
1767
|
fi
|
|
1681
1768
|
else
|
|
1682
1769
|
echo " ℹ️ Boy scout rule not available in project - skipping"
|
|
1683
|
-
echo "
|
|
1770
|
+
echo " ⏭️ SKIPPED - Boy Scout Rule (not available in project)"
|
|
1684
1771
|
fi
|
|
1685
1772
|
fi
|
|
1686
1773
|
GATE_6_STATUS="PASS"
|
|
@@ -1690,105 +1777,15 @@ record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCK
|
|
|
1690
1777
|
|
|
1691
1778
|
# ============================================================================
|
|
1692
1779
|
# GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
|
|
1693
|
-
#
|
|
1694
|
-
# Tools: checkov (recommended), hadolint, kube-score, tflint
|
|
1780
|
+
# Extracted to gate-7.sh for maintainability
|
|
1695
1781
|
# ============================================================================
|
|
1696
|
-
|
|
1697
|
-
2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
|
|
1698
|
-
GATE_7_START=$(gate_start_ms)
|
|
1699
|
-
|
|
1700
|
-
# Check if any IaC files are changed
|
|
1701
|
-
IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
|
|
1702
|
-
if [ -z "$IAC_FILES" ]; then
|
|
1703
|
-
echo "✅ PASSED - No IaC files detected in changes."
|
|
1704
|
-
GATE_7_STATUS="PASS"
|
|
1705
|
-
else
|
|
1706
|
-
# Run IaC adapter
|
|
1707
|
-
if [ -f "githooks/adapters/iac.sh" ]; then
|
|
1708
|
-
# shellcheck source=githooks/adapters/iac.sh
|
|
1709
|
-
source "githooks/adapters/iac.sh"
|
|
1710
|
-
|
|
1711
|
-
# Run static analysis for IaC files
|
|
1712
|
-
IAC_OUTPUT=$(run_static_analysis "$IAC_FILES" 2>&1)
|
|
1713
|
-
IAC_EXIT=$?
|
|
1714
|
-
|
|
1715
|
-
echo "$IAC_OUTPUT"
|
|
1716
|
-
|
|
1717
|
-
if [ $IAC_EXIT -eq 0 ]; then
|
|
1718
|
-
echo "✅ PASSED - IaC security scan."
|
|
1719
|
-
GATE_7_STATUS="PASS"
|
|
1720
|
-
else
|
|
1721
|
-
echo ""
|
|
1722
|
-
echo "❌ BLOCKED - IaC security issues detected"
|
|
1723
|
-
echo "Fix the security issues above before committing."
|
|
1724
|
-
echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
|
|
1725
|
-
GATE_7_STATUS="FAIL"
|
|
1726
|
-
exit 1
|
|
1727
|
-
fi
|
|
1728
|
-
else
|
|
1729
|
-
echo "ℹ️ SKIP - IaC adapter not found"
|
|
1730
|
-
echo "✅ PASSED - IaC Security (SKIP)"
|
|
1731
|
-
GATE_7_STATUS="SKIP"
|
|
1732
|
-
fi
|
|
1733
|
-
fi
|
|
1734
|
-
record_gate_audit "gate-7" "iac-security" "$GATE_7_STATUS" "0" "$GATE_7_START"
|
|
1782
|
+
source "$GATE_DIR/gate-7.sh"
|
|
1735
1783
|
|
|
1736
1784
|
|
|
1737
1785
|
# GATE 8: Secret Scanning (gitleaks)
|
|
1738
|
-
#
|
|
1739
|
-
# Tool: gitleaks -- https://github.com/gitleaks/gitleaks
|
|
1786
|
+
# Extracted to gate-8.sh for maintainability
|
|
1740
1787
|
# ============================================================================
|
|
1741
|
-
|
|
1742
|
-
2>&1 echo ""
|
|
1743
|
-
2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
|
|
1744
|
-
GATE_8_START=$(gate_start_ms)
|
|
1745
|
-
|
|
1746
|
-
# Gitleaks availability check
|
|
1747
|
-
GITLEAKS_CMD=""
|
|
1748
|
-
if command -v gitleaks >/dev/null 2>&1; then
|
|
1749
|
-
GITLEAKS_CMD="gitleaks"
|
|
1750
|
-
elif [ -f "$HOME/.local/bin/gitleaks" ]; then
|
|
1751
|
-
GIBLEAKS_CMD="$HOME/.local/bin/gitleaks"
|
|
1752
|
-
fi
|
|
1753
|
-
|
|
1754
|
-
if [ -n "$GITLEAKS_CMD" ]; then
|
|
1755
|
-
GITLEAKS_CONFIG=""
|
|
1756
|
-
if [ -f ".gitleaks.toml" ]; then
|
|
1757
|
-
GITLEAKS_CONFIG="--config=.gitleaks.toml"
|
|
1758
|
-
fi
|
|
1759
|
-
|
|
1760
|
-
# Run gitleaks on staged changes only (pre-commit mode for speed)
|
|
1761
|
-
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --pre-commit --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path=/tmp/gitleaks-report.json 2>&1)
|
|
1762
|
-
GITLEAKS_EXIT=$?
|
|
1763
|
-
|
|
1764
|
-
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
1765
|
-
echo " ✅ PASSED - No secrets detected."
|
|
1766
|
-
GATE_8_STATUS="PASS"
|
|
1767
|
-
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
1768
|
-
# Secrets found — output details
|
|
1769
|
-
echo "$GITLEAKS_OUTPUT"
|
|
1770
|
-
echo ""
|
|
1771
|
-
echo "❌ BLOCKED - Secrets detected in staged files."
|
|
1772
|
-
echo ""
|
|
1773
|
-
echo "Remediation options:"
|
|
1774
|
-
echo " 1. Remove the secret and use environment variables instead"
|
|
1775
|
-
echo " 2. Add a false positive to .gitleaks.toml allowlist"
|
|
1776
|
-
echo " 3. Use git secret or vault for sensitive data"
|
|
1777
|
-
echo ""
|
|
1778
|
-
echo "See: https://github.com/gitleaks/gitleaks"
|
|
1779
|
-
exit 1
|
|
1780
|
-
else
|
|
1781
|
-
echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
|
|
1782
|
-
echo " ✅ Secret Scanning (SKIP, gitleaks error)"
|
|
1783
|
-
GATE_8_STATUS="SKIP"
|
|
1784
|
-
fi
|
|
1785
|
-
else
|
|
1786
|
-
echo " ℹ️ gitleaks not installed — secret scanning unavailable"
|
|
1787
|
-
echo " Install: brew install gitleaks (macOS) | scripts/install-gitleaks.sh (Linux)"
|
|
1788
|
-
echo " ✅ Secret Scanning (SKIP, gitleaks not installed)"
|
|
1789
|
-
GATE_8_STATUS="SKIP"
|
|
1790
|
-
fi
|
|
1791
|
-
record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
|
|
1788
|
+
source "$GATE_DIR/gate-8.sh"
|
|
1792
1789
|
|
|
1793
1790
|
# Switch back to original directory if we were in a subdirectory
|
|
1794
1791
|
if [ -n "$ORIGINAL_DIR" ]; then
|
|
@@ -1797,134 +1794,42 @@ fi
|
|
|
1797
1794
|
|
|
1798
1795
|
# ============================================================================
|
|
1799
1796
|
# GATE 9: Semgrep SAST Security Scan
|
|
1800
|
-
#
|
|
1801
|
-
#
|
|
1797
|
+
# Extracted to gate-9.sh for maintainability
|
|
1798
|
+
# ============================================================================
|
|
1799
|
+
source "$GATE_DIR/gate-9.sh"
|
|
1802
1800
|
# ============================================================================
|
|
1803
1801
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1802
|
+
# ============================================================================
|
|
1803
|
+
# GATE 10: Sprint Flow Enforcement
|
|
1804
|
+
# Validates sprint state consistency (delphi-review APPROVED before BUILD)
|
|
1805
|
+
# Uses sprint-gate.sh for standalone validation logic
|
|
1806
|
+
# ============================================================================
|
|
1807
|
+
GATE_10_START=$(date +%s)
|
|
1808
|
+
GATE_10_STATUS="PASS"
|
|
1809
|
+
|
|
1810
|
+
SPRINT_GATE_SCRIPT=""
|
|
1811
|
+
if [ -f "$GATE_DIR/sprint-gate.sh" ]; then
|
|
1812
|
+
SPRINT_GATE_SCRIPT="$GATE_DIR/sprint-gate.sh"
|
|
1813
|
+
elif [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/sprint-gate.sh" ]; then
|
|
1814
|
+
SPRINT_GATE_SCRIPT="$(git rev-parse --show-toplevel)/githooks/sprint-gate.sh"
|
|
1816
1815
|
fi
|
|
1817
1816
|
|
|
1818
|
-
if [ -
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
else
|
|
1825
|
-
# Get staged files filtered to Semgrep-supported languages
|
|
1826
|
-
SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
1827
|
-
|
|
1828
|
-
if [ -z "$SEMGREP_FILES" ]; then
|
|
1829
|
-
echo " ✅ PASSED - No supported language files in staged changes."
|
|
1830
|
-
GATE_9_STATUS="PASS"
|
|
1831
|
-
else
|
|
1832
|
-
# Run semgrep with JSON output
|
|
1833
|
-
# --config=p/security-audit: explicit security ruleset
|
|
1834
|
-
# --json: machine-readable output
|
|
1835
|
-
# --disable-version-check: skip network call
|
|
1836
|
-
SEMGREP_OUTPUT=$($SEMGREP_CMD scan --config=p/security-audit --json --disable-version-check $SEMGREP_FILES 2>&1)
|
|
1837
|
-
SEMGREP_EXIT=$?
|
|
1838
|
-
|
|
1839
|
-
if [ "$SEMGREP_EXIT" -eq 0 ]; then
|
|
1840
|
-
echo " ✅ PASSED - No security vulnerabilities found."
|
|
1841
|
-
GATE_9_STATUS="PASS"
|
|
1842
|
-
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
1843
|
-
# Findings detected - parse JSON to categorize
|
|
1844
|
-
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
|
|
1845
|
-
import sys, json
|
|
1846
|
-
try:
|
|
1847
|
-
data = json.load(sys.stdin)
|
|
1848
|
-
results = data.get('results', [])
|
|
1849
|
-
count = 0
|
|
1850
|
-
for r in results:
|
|
1851
|
-
extra = r.get('extra', {})
|
|
1852
|
-
severity = extra.get('severity', '').upper()
|
|
1853
|
-
if severity in ('CRITICAL', 'HIGH'):
|
|
1854
|
-
count += 1
|
|
1855
|
-
print(count)
|
|
1856
|
-
except:
|
|
1857
|
-
print('0')
|
|
1858
|
-
" 2>/dev/null || echo "0")
|
|
1859
|
-
|
|
1860
|
-
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | python3 -c "
|
|
1861
|
-
import sys, json
|
|
1862
|
-
try:
|
|
1863
|
-
data = json.load(sys.stdin)
|
|
1864
|
-
results = data.get('results', [])
|
|
1865
|
-
count = 0
|
|
1866
|
-
for r in results:
|
|
1867
|
-
extra = r.get('extra', {})
|
|
1868
|
-
severity = extra.get('severity', '').upper()
|
|
1869
|
-
if severity in ('MEDIUM', 'LOW'):
|
|
1870
|
-
count += 1
|
|
1871
|
-
print(count)
|
|
1872
|
-
except:
|
|
1873
|
-
print('0')
|
|
1874
|
-
" 2>/dev/null || echo "0")
|
|
1875
|
-
|
|
1876
|
-
# Extract top finding details
|
|
1877
|
-
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | python3 -c "
|
|
1878
|
-
import sys, json
|
|
1879
|
-
try:
|
|
1880
|
-
data = json.load(sys.stdin)
|
|
1881
|
-
results = data.get('results', [])
|
|
1882
|
-
for r in results[:5]: # Show top 5
|
|
1883
|
-
extra = r.get('extra', {})
|
|
1884
|
-
severity = extra.get('severity', 'UNKNOWN').upper()
|
|
1885
|
-
rule_id = r.get('check_id', 'unknown')
|
|
1886
|
-
path = r.get('path', 'unknown')
|
|
1887
|
-
start_line = r.get('start', {}).get('line', '?')
|
|
1888
|
-
message = extra.get('message', '')[:80]
|
|
1889
|
-
print(f' [{severity}] {rule_id}')
|
|
1890
|
-
print(f' {path}:{start_line} → {message}')
|
|
1891
|
-
print()
|
|
1892
|
-
except:
|
|
1893
|
-
print(' (Failed to parse semgrep output)')
|
|
1894
|
-
" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
1895
|
-
|
|
1896
|
-
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
1897
|
-
echo ""
|
|
1898
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1899
|
-
echo " GATE 9: Semgrep Security Gate"
|
|
1900
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1901
|
-
echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
|
|
1902
|
-
echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
|
|
1903
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1904
|
-
echo " ❌ BLOCKED — Critical/High vulnerability found"
|
|
1905
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
1906
|
-
echo "$FINDING_DETAILS"
|
|
1907
|
-
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
1908
|
-
GATE_9_STATUS="FAIL"
|
|
1909
|
-
exit 1
|
|
1910
|
-
else
|
|
1911
|
-
echo ""
|
|
1912
|
-
echo " ✅ PASSED - No critical/high vulnerabilities"
|
|
1913
|
-
if [ "$MEDIUM_LOW" -gt 0 ]; then
|
|
1914
|
-
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
1915
|
-
echo "$FINDING_DETAILS"
|
|
1916
|
-
fi
|
|
1917
|
-
GATE_9_STATUS="PASS"
|
|
1918
|
-
fi
|
|
1919
|
-
else
|
|
1920
|
-
# semgrep runtime error (timeout, config error, etc.)
|
|
1921
|
-
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
1922
|
-
echo " ✅ Semgrep SAST (SKIP, semgrep error)"
|
|
1923
|
-
GATE_9_STATUS="SKIP"
|
|
1924
|
-
fi
|
|
1817
|
+
if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
1818
|
+
if ! bash "$SPRINT_GATE_SCRIPT" --pre-commit; then
|
|
1819
|
+
GATE_10_STATUS="BLOCK"
|
|
1820
|
+
echo "❌ BLOCKED - Gate 10: Sprint Flow Enforcement"
|
|
1821
|
+
# Sprint gate failure is a hard block — exit immediately
|
|
1822
|
+
exit 1
|
|
1925
1823
|
fi
|
|
1824
|
+
else
|
|
1825
|
+
echo "⏭️ SKIPPED - Gate 10: Sprint Flow (sprint-gate.sh not found)"
|
|
1826
|
+
GATE_10_STATUS="SKIP"
|
|
1926
1827
|
fi
|
|
1927
|
-
|
|
1828
|
+
|
|
1829
|
+
GATE_10_END=$(date +%s)
|
|
1830
|
+
GATE_10_DURATION=$((GATE_10_END - GATE_10_START))
|
|
1831
|
+
echo "✅ Gate 10 completed in ${GATE_10_DURATION}s"
|
|
1832
|
+
echo ""
|
|
1928
1833
|
# ============================================================================
|
|
1929
1834
|
|
|
1930
1835
|
generate_quality_report() {
|
|
@@ -1933,13 +1838,13 @@ generate_quality_report() {
|
|
|
1933
1838
|
local TIMESTAMP
|
|
1934
1839
|
local BRANCH
|
|
1935
1840
|
local PASSED_COUNT=0
|
|
1936
|
-
local TOTAL_GATES=
|
|
1841
|
+
local TOTAL_GATES=10
|
|
1937
1842
|
|
|
1938
1843
|
COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
1939
1844
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
1940
1845
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1941
1846
|
|
|
1942
|
-
for gate in 1 2 3 4 5 6 7 8 9; do
|
|
1847
|
+
for gate in 1 2 3 4 5 6 7 8 9 10; do
|
|
1943
1848
|
local status_var="GATE_${gate}_STATUS"
|
|
1944
1849
|
if [ "${!status_var}" = "PASS" ]; then
|
|
1945
1850
|
PASSED_COUNT=$((PASSED_COUNT + 1))
|
|
@@ -2042,6 +1947,11 @@ generate_quality_report() {
|
|
|
2042
1947
|
"name": "SAST Security Scan",
|
|
2043
1948
|
"status": "${GATE_9_STATUS:-PASS}",
|
|
2044
1949
|
"tool": "semgrep"
|
|
1950
|
+
},
|
|
1951
|
+
"gate10_sprint_flow": {
|
|
1952
|
+
"name": "Sprint Flow Enforcement",
|
|
1953
|
+
"status": "${GATE_10_STATUS:-PASS}",
|
|
1954
|
+
"tool": "sprint-gate.sh"
|
|
2045
1955
|
}
|
|
2046
1956
|
}
|
|
2047
1957
|
}
|
|
@@ -2082,7 +1992,8 @@ PY
|
|
|
2082
1992
|
GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-PASS}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
|
|
2083
1993
|
GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-PASS}\",\"name\":\"IaC Security\"},"
|
|
2084
1994
|
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
|
|
2085
|
-
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"SAST Security\"}
|
|
1995
|
+
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"SAST Security\"},"
|
|
1996
|
+
GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-PASS}\",\"name\":\"Sprint Flow\"}}"
|
|
2086
1997
|
GATES_JSON="${GATES_JSON}}"
|
|
2087
1998
|
|
|
2088
1999
|
echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
|
|
@@ -2102,6 +2013,7 @@ PY
|
|
|
2102
2013
|
printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-PASS}"
|
|
2103
2014
|
printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
|
|
2104
2015
|
printf " %-45s %s\n" "Gate 9: SAST Security" "${GATE_9_STATUS:-PASS}"
|
|
2016
|
+
printf " %-45s %s\n" "Gate 10: Sprint Flow" "${GATE_10_STATUS:-PASS}"
|
|
2105
2017
|
echo ""
|
|
2106
2018
|
echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
|
|
2107
2019
|
echo " Branch status: $REPORT_FILE"
|