@boyingliu01/xp-gate 0.19.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapters/gate-4.sh +83 -18
- package/adapters/gate-8.sh +49 -5
- package/adapters/iac.sh +1 -1
- package/adapters/typescript.sh +15 -60
- package/bin/xp-gate.js +15 -16
- package/gate-10.sh +35 -2
- package/gate-4.sh +110 -21
- package/gate-8.sh +49 -5
- package/hooks/gate-10.sh +35 -2
- package/hooks/gate-4.sh +110 -21
- package/hooks/gate-8.sh +49 -5
- package/hooks/lib/jscpd-run.sh +63 -0
- package/hooks/lib/sprint-gate-report.sh +58 -0
- package/hooks/lib/test-failure.sh +152 -0
- package/hooks/lib/typecheck.sh +141 -0
- package/hooks/lib/validate-code-walkthrough.cjs +43 -5
- package/hooks/post-merge +100 -0
- package/hooks/pre-commit +389 -162
- package/hooks/pre-push +75 -19
- package/lib/__tests__/bootstrap.test.js +23 -41
- package/lib/__tests__/check-version.test.js +8 -1
- package/lib/__tests__/detect-deps.test.js +16 -0
- package/lib/__tests__/doctor-drift-round2.test.js +212 -0
- package/lib/__tests__/doctor-executed-surface.test.js +209 -0
- package/lib/__tests__/doctor-hook-drift.test.js +329 -0
- package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
- package/lib/__tests__/doctor-module-drift.test.js +206 -0
- package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
- package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
- package/lib/__tests__/doctor.test.js +155 -0
- package/lib/__tests__/gate-runner.test.js +19 -3
- package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
- package/lib/__tests__/init.test.js +44 -17
- package/lib/__tests__/install-skill-full-dir.test.js +151 -0
- package/lib/__tests__/install-skill.test.js +51 -132
- package/lib/__tests__/language-tools-detect.test.js +158 -0
- package/lib/__tests__/phase-transition.test.js +15 -1
- package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
- package/lib/__tests__/shared-paths.test.js +20 -0
- package/lib/__tests__/shared-utils.test.js +69 -0
- package/lib/__tests__/skill-flags.test.js +48 -0
- package/lib/__tests__/ui-detector.test.ts +6 -2
- package/lib/__tests__/uninstall.test.js +20 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/update-skill.test.js +7 -4
- package/lib/detect-deps.js +3 -20
- package/lib/doctor-tui.js +22 -5
- package/lib/doctor.js +1178 -31
- package/lib/init.js +184 -33
- package/lib/install-cmd.js +7 -4
- package/lib/install-skill.js +38 -95
- package/lib/language-tools.js +83 -4
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-paths.js +5 -2
- package/lib/shared-utils.js +47 -3
- package/lib/uninstall.js +12 -0
- package/lib/update-hooks.js +4 -4
- package/lib/update-skill.js +6 -7
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/gate-m.test.ts +66 -14
- package/mutation/__tests__/mutmut-runner.test.ts +91 -42
- package/mutation/__tests__/test-filter-parity.test.ts +37 -0
- package/mutation/gate-m.ts +21 -8
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/README.md +169 -4
- package/plugins/dsh/dsh-overlay.patch.yml +67 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +24 -19
- package/plugins/dsh/setup-dsh.ps1 +160 -0
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
- package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/command.test.ts +66 -2
- package/plugins/dsh/src/command.ts +36 -14
- package/plugins/dsh/src/contract.test.ts +31 -1
- package/plugins/dsh/src/delphi-config.test.ts +190 -0
- package/plugins/dsh/src/delphi-config.ts +223 -0
- package/plugins/dsh/src/delphi-run.test.ts +181 -0
- package/plugins/dsh/src/delphi-run.ts +129 -0
- package/plugins/dsh/src/delphi.test.ts +222 -0
- package/plugins/dsh/src/delphi.ts +247 -0
- package/plugins/dsh/src/dialect.test.ts +59 -0
- package/plugins/dsh/src/dialect.ts +40 -0
- package/plugins/dsh/src/index.ts +70 -4
- package/plugins/dsh/src/skills.test.ts +26 -19
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/agents/delphi-architecture.md +1 -1
- package/plugins/qoder/agents/delphi-feasibility.md +1 -1
- package/plugins/qoder/agents/delphi-technical.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +9 -8
- package/principles/__tests__/adapter-metrics.test.ts +292 -0
- package/principles/__tests__/analyzer.test.ts +29 -6
- package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
- package/principles/__tests__/baseline-prune.test.ts +338 -0
- package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
- package/principles/__tests__/boy-scout-integration.test.ts +97 -6
- package/principles/__tests__/boy-scout.test.ts +253 -7
- package/principles/__tests__/config-enforcement.test.ts +469 -0
- package/principles/__tests__/config-git-timeout.test.ts +66 -0
- package/principles/__tests__/direct-execution.test.ts +187 -0
- package/principles/__tests__/index.test.ts +16 -3
- package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
- package/principles/adapters/typescript.ts +613 -13
- package/principles/analyzer.ts +22 -0
- package/principles/baseline-prune.ts +217 -0
- package/principles/boy-scout.ts +481 -51
- package/principles/config.ts +362 -54
- package/principles/direct-execution.ts +60 -0
- package/principles/index.ts +40 -17
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
- package/principles/rules/clean-code/code-duplication.ts +8 -8
- package/principles/rules/clean-code/deep-nesting.ts +8 -8
- package/principles/rules/clean-code/god-class.ts +8 -8
- package/principles/rules/clean-code/large-file.ts +15 -11
- package/principles/rules/clean-code/long-function.ts +8 -8
- package/principles/rules/clean-code/magic-numbers.ts +10 -7
- package/principles/rules/clean-code/many-exports.ts +7 -7
- package/principles/rules/clean-code/missing-error-handling.ts +5 -5
- package/principles/rules/clean-code/too-many-params.ts +8 -8
- package/principles/rules/clean-code/unused-imports.ts +5 -5
- package/principles/rules/solid/dip.ts +18 -8
- package/principles/rules/solid/isp.ts +9 -8
- package/principles/rules/solid/lsp.ts +6 -6
- package/principles/rules/solid/ocp.ts +6 -5
- package/principles/rules/solid/srp.ts +7 -8
- package/principles/types.ts +15 -0
- package/scripts/__tests__/sync-package-content.test.js +28 -0
- package/scripts/delphi-external-review.cjs +193 -10
- package/scripts/sync-package-content.js +7 -4
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/delphi-review/SKILL.md +98 -14
- package/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/skills/delphi-review/references/requirements.md +6 -6
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/references/phase-2-design.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/lib/__tests__/download-skill.test.js +0 -281
- package/lib/download-skill.js +0 -96
package/hooks/pre-commit
CHANGED
|
@@ -115,6 +115,30 @@ set -o pipefail
|
|
|
115
115
|
# Helper Functions
|
|
116
116
|
# ============================================================================
|
|
117
117
|
|
|
118
|
+
# Run a TypeScript entrypoint under tsx without routing arguments through the
|
|
119
|
+
# npx shim: Windows cmd.exe caps command lines at 8191 chars and long staged
|
|
120
|
+
# file lists (Boy Scout, build integrity) overflow it, turning a healthy run
|
|
121
|
+
# into a false BLOCK. Prefer the repo-local tsx CLI driven by node directly;
|
|
122
|
+
# keep npx as fallback for consumer repos relying on a global install.
|
|
123
|
+
# RUN_TSX_TIMEOUT (e.g. "120s") wraps the invocation in `timeout`.
|
|
124
|
+
if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
125
|
+
run_tsx() {
|
|
126
|
+
local _root="${PROJECT_ROOT:-$(pwd)}" _timeout=()
|
|
127
|
+
if [ -n "${RUN_TSX_TIMEOUT:-}" ]; then
|
|
128
|
+
_timeout=(timeout "$RUN_TSX_TIMEOUT")
|
|
129
|
+
fi
|
|
130
|
+
if [ -f "$_root/node_modules/tsx/dist/cli.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
131
|
+
"${_timeout[@]}" node "$_root/node_modules/tsx/dist/cli.mjs" "$@"
|
|
132
|
+
else
|
|
133
|
+
"${_timeout[@]}" npx tsx "$@"
|
|
134
|
+
fi
|
|
135
|
+
}
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Gate 5's vitest exit-code judgement (is_runner_infrastructure_error,
|
|
139
|
+
# is_partial_threshold_only_exit, handle_test_failure) is sourced from
|
|
140
|
+
# lib/test-failure.sh below, next to the other hook libraries.
|
|
141
|
+
|
|
118
142
|
# Parse LCOV file and return coverage percentage
|
|
119
143
|
# Usage: parse_lcov_coverage <lcov_file>
|
|
120
144
|
# Returns: coverage percentage (e.g., "85.5")
|
|
@@ -190,6 +214,16 @@ PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AU
|
|
|
190
214
|
# Issue #370: source cross-platform now_ms() helper (node-preferred, regex-validated)
|
|
191
215
|
source "${AUDIT_SCRIPT_DIR}/lib/now-ms.sh"
|
|
192
216
|
|
|
217
|
+
# Issue #473 (+ #454): Gate 5's vitest exit-code judgement, sourced as a library
|
|
218
|
+
# so the fail-closed contract is unit-testable without executing this hook.
|
|
219
|
+
source "${AUDIT_SCRIPT_DIR}/lib/test-failure.sh"
|
|
220
|
+
|
|
221
|
+
# Issue #477: Gate 2's jscpd config probe and outcome classification, same reason.
|
|
222
|
+
source "${AUDIT_SCRIPT_DIR}/lib/jscpd-run.sh"
|
|
223
|
+
|
|
224
|
+
# Issue #476: Gate 11's sprint-gate.sh resolution chain and decision translation.
|
|
225
|
+
source "${AUDIT_SCRIPT_DIR}/lib/sprint-gate-report.sh"
|
|
226
|
+
|
|
193
227
|
# Read max_duration_ms from .xp-gate-config.json (default 7200000 = 2 hours)
|
|
194
228
|
_MAX_DURATION_MS=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('${PROJECT_ROOT_FOR_AUDIT}/.xp-gate-config.json','utf8'));console.log((c.audit&&c.audit.max_duration_ms)||7200000);}catch(e){console.log(7200000);}" 2>/dev/null || echo "7200000")
|
|
195
229
|
|
|
@@ -254,7 +288,7 @@ echo ""
|
|
|
254
288
|
GATE_0_START=$(gate_start_ms)
|
|
255
289
|
GATE_0_STATUS="PASS"
|
|
256
290
|
|
|
257
|
-
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
291
|
+
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")
|
|
258
292
|
PROTECTED_BRANCHES="master develop trunk mainline"
|
|
259
293
|
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
|
|
260
294
|
|
|
@@ -402,7 +436,11 @@ Before committing:
|
|
|
402
436
|
2. Add entry to CHANGELOG.md
|
|
403
437
|
3. Run: node scripts/sync-version.cjs
|
|
404
438
|
|
|
405
|
-
Or use
|
|
439
|
+
Or use the bypass prefix — the commit message FIRST LINE must START with:
|
|
440
|
+
[skip-version-check] chore: <description>
|
|
441
|
+
Allowed types: chore: docs: release:. The bypass only permits build
|
|
442
|
+
tooling changes (adapters/, scripts/, hooks/); production source files
|
|
443
|
+
still require a VERSION/CHANGELOG update.
|
|
406
444
|
|
|
407
445
|
Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
408
446
|
"
|
|
@@ -429,6 +467,22 @@ if [ -z "$CHANGED_FILES" ]; then
|
|
|
429
467
|
exit 0
|
|
430
468
|
fi
|
|
431
469
|
|
|
470
|
+
# Gate 5 test selection (#494): vitest.config.ts excludes the bundled mirror
|
|
471
|
+
# subtrees and the skills/plugins source trees, so a path under one of them can
|
|
472
|
+
# never be collected. When such a path is the only target, vitest exits 1 with
|
|
473
|
+
# "No test files found" and Gate 5 reports a failing test run -- a commit that
|
|
474
|
+
# touched nothing but a byte-identical mirror cannot land. Map each mirror path
|
|
475
|
+
# to the canonical twin it copies, drop what has no twin, collapse duplicates.
|
|
476
|
+
select_root_suite_targets() {
|
|
477
|
+
# Deliberately not `s|...|...|`: the alternation below contains `|`, which sed
|
|
478
|
+
# would read as the end of its pattern ("unknown option to `s'").
|
|
479
|
+
printf '%s\n' "$1" \
|
|
480
|
+
| sed -E 's#^src/npm-package/(principles|mutation|mock-policy|build-integrity)/#src/\1/#' \
|
|
481
|
+
| grep -vE '^(src/_wip/|plugins/|skills/|src/npm-package/(plugins|skills|principles|mutation|mock-policy|build-integrity)/)' \
|
|
482
|
+
| sed '/^$/d' \
|
|
483
|
+
| sort -u
|
|
484
|
+
}
|
|
485
|
+
|
|
432
486
|
any_changed_files_match() {
|
|
433
487
|
local pattern
|
|
434
488
|
|
|
@@ -833,72 +887,16 @@ else
|
|
|
833
887
|
echo " ── Checking $CURRENT_LANG ──"
|
|
834
888
|
case "$CURRENT_LANG" in
|
|
835
889
|
typescript)
|
|
836
|
-
# TypeScript: static analysis
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
# Run TypeScript type checking
|
|
847
|
-
echo "Running TypeScript static analysis ($TSC_OUTPUT)..."
|
|
848
|
-
npx tsc --noEmit --skipLibCheck 2>&1 | head -30
|
|
849
|
-
TSC_EXIT=$?
|
|
850
|
-
if [ "$TSC_EXIT" -ne 0 ]; then
|
|
851
|
-
echo ""
|
|
852
|
-
echo "❌ BLOCKED - TYPE ERRORS detected"
|
|
853
|
-
echo "Fix the type errors above before committing."
|
|
854
|
-
exit 1
|
|
855
|
-
fi
|
|
856
|
-
echo "✅ PASSED - TypeScript static analysis."
|
|
857
|
-
|
|
858
|
-
# Also check test files if they exist (Issue #293)
|
|
859
|
-
# Many projects exclude __tests__/ from tsconfig.json, so test file type
|
|
860
|
-
# errors silently accumulate.
|
|
861
|
-
if [ -d "src/__tests__" ] || [ -d "src/tests" ] || [ -d "tests" ] || [ -d "__tests__" ]; then
|
|
862
|
-
HAS_TESTS_IN_TSC=false
|
|
863
|
-
if npx tsc --noEmit --skipLibCheck --listFiles 2>/dev/null | grep -qE '__tests__/|\.test\.ts|\.spec\.ts'; then
|
|
864
|
-
HAS_TESTS_IN_TSC=true
|
|
865
|
-
fi
|
|
866
|
-
if [ "$HAS_TESTS_IN_TSC" = false ]; then
|
|
867
|
-
TSC_TEST_EXIT=0
|
|
868
|
-
if [ -f "tsconfig.tests.json" ]; then
|
|
869
|
-
echo "Checking test files with tsconfig.tests.json..."
|
|
870
|
-
npx tsc --noEmit --project tsconfig.tests.json 2>&1 | head -30
|
|
871
|
-
TSC_TEST_EXIT=$?
|
|
872
|
-
else
|
|
873
|
-
echo "Checking test files for type errors..."
|
|
874
|
-
TEMP_TSCONFIG=".tsconfig.withtests.json"
|
|
875
|
-
node -e "
|
|
876
|
-
const cfg = JSON.parse(require('fs').readFileSync('tsconfig.json','utf8'));
|
|
877
|
-
delete cfg.exclude;
|
|
878
|
-
cfg.include = cfg.include || ['src/**/*'];
|
|
879
|
-
cfg.include.push('src/**/__tests__/**','src/**/*.test.ts','src/**/*.spec.ts');
|
|
880
|
-
require('fs').writeFileSync('$TEMP_TSCONFIG', JSON.stringify(cfg, null, 2));
|
|
881
|
-
" 2>/dev/null
|
|
882
|
-
npx tsc --noEmit --project "$TEMP_TSCONFIG" --skipLibCheck 2>&1 | head -30
|
|
883
|
-
TSC_TEST_EXIT=$?
|
|
884
|
-
rm -f "$TEMP_TSCONFIG"
|
|
885
|
-
fi
|
|
886
|
-
if [ $TSC_TEST_EXIT -ne 0 ]; then
|
|
887
|
-
echo ""
|
|
888
|
-
echo "❌ BLOCKED - TYPE ERRORS in test files"
|
|
889
|
-
echo "Your project's tsconfig.json excludes test files from type checking."
|
|
890
|
-
echo "Add a tsconfig.tests.json to customise test file checking."
|
|
891
|
-
exit 1
|
|
892
|
-
fi
|
|
893
|
-
fi
|
|
894
|
-
fi
|
|
895
|
-
else
|
|
896
|
-
echo "ℹ️ SKIP - tsconfig.json not found (no TypeScript project config)"
|
|
897
|
-
fi
|
|
898
|
-
else
|
|
899
|
-
echo "ℹ️ SKIP - tsc not properly installed ($TSC_OUTPUT)"
|
|
900
|
-
echo "Install typescript (npm install typescript --save-dev) for type checking."
|
|
901
|
-
fi
|
|
890
|
+
# TypeScript: static analysis + linting (eslint). The type-check decision lives
|
|
891
|
+
# in githooks/lib/typecheck.sh so a project's own checker (vue-tsc, svelte-check,
|
|
892
|
+
# a `typecheck` script) is used instead of a hardcoded tsc that cannot resolve
|
|
893
|
+
# single-file components (#436).
|
|
894
|
+
source "${AUDIT_SCRIPT_DIR}/lib/typecheck.sh"
|
|
895
|
+
run_typescript_typecheck
|
|
896
|
+
TS_CHECK_EXIT=$?
|
|
897
|
+
if [ "$TS_CHECK_EXIT" -eq 1 ]; then
|
|
898
|
+
exit 1
|
|
899
|
+
fi
|
|
902
900
|
|
|
903
901
|
# Run ESLint linting if config exists
|
|
904
902
|
if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
|
|
@@ -966,16 +964,30 @@ else
|
|
|
966
964
|
# ── Biome lint/format check ──
|
|
967
965
|
if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
|
|
968
966
|
echo "Running Biome lint/format check..."
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
967
|
+
BIOME_OUT=$(npx biome check --staged . 2>&1)
|
|
968
|
+
BIOME_STATUS=$?
|
|
969
|
+
if [ "$BIOME_STATUS" -ne 0 ]; then
|
|
970
|
+
# Biome exits 1 with `internalError/io - No files were processed` when the
|
|
971
|
+
# staged set yields nothing lintable (docs-only / config-only commit, or
|
|
972
|
+
# everything excluded by biome.json). That is "nothing to do", not a lint
|
|
973
|
+
# violation -- blocking on it makes documentation uncommittable (#458).
|
|
974
|
+
# This matches the hook's stated principle: tool available + check
|
|
975
|
+
# SUCCEEDS = PASS. Real violations still carry diagnostics, so they block.
|
|
976
|
+
if echo "$BIOME_OUT" | grep -qE 'internalError/io|No files were processed'; then
|
|
977
|
+
echo "$BIOME_OUT"
|
|
978
|
+
echo "✅ PASSED - Biome check (no lintable files staged)."
|
|
979
|
+
else
|
|
980
|
+
echo "$BIOME_OUT" | head -50
|
|
981
|
+
echo ""
|
|
982
|
+
echo "❌ BLOCKED - Biome check failed"
|
|
983
|
+
echo "Fix the errors above before committing."
|
|
984
|
+
echo "Tip: Run 'npx biome check --write .' to auto-fix formatting and safe lint issues."
|
|
985
|
+
echo " Run 'npx biome check --write --unsafe .' to also fix unsafe lint issues."
|
|
986
|
+
exit 1
|
|
987
|
+
fi
|
|
988
|
+
else
|
|
989
|
+
echo "✅ PASSED - Biome check."
|
|
977
990
|
fi
|
|
978
|
-
echo "✅ PASSED - Biome check."
|
|
979
991
|
else
|
|
980
992
|
if [ -f "package.json" ] && command -v node &> /dev/null; then
|
|
981
993
|
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
|
|
@@ -1002,7 +1014,6 @@ else
|
|
|
1002
1014
|
exit 1
|
|
1003
1015
|
fi
|
|
1004
1016
|
fi
|
|
1005
|
-
fi
|
|
1006
1017
|
;;
|
|
1007
1018
|
|
|
1008
1019
|
python)
|
|
@@ -1013,14 +1024,20 @@ else
|
|
|
1013
1024
|
else
|
|
1014
1025
|
# Run Ruff (includes syntax check, linting)
|
|
1015
1026
|
echo "Running Ruff linting (syntax + lint)..."
|
|
1016
|
-
# Filter to *.py
|
|
1017
|
-
#
|
|
1018
|
-
|
|
1019
|
-
|
|
1027
|
+
# Filter to *.py/*.pyi into a bash array so each staged path is passed as
|
|
1028
|
+
# ONE argument even when it contains whitespace; skip ruff when no Python
|
|
1029
|
+
# files are staged so yaml/.gitignore are never parsed.
|
|
1030
|
+
RUFF_ARGS=()
|
|
1031
|
+
while IFS= read -r staged_file; do
|
|
1032
|
+
case "$staged_file" in
|
|
1033
|
+
*.py|*.pyi) RUFF_ARGS+=("$staged_file") ;;
|
|
1034
|
+
esac
|
|
1035
|
+
done <<< "$CHANGED_FILES"
|
|
1036
|
+
if [ "${#RUFF_ARGS[@]}" -eq 0 ]; then
|
|
1020
1037
|
RUFF_OUTPUT="[]"
|
|
1021
1038
|
RUFF_EXIT=0
|
|
1022
1039
|
else
|
|
1023
|
-
RUFF_OUTPUT=$(ruff check $
|
|
1040
|
+
RUFF_OUTPUT=$(ruff check "${RUFF_ARGS[@]}" --output-format json 2>&1)
|
|
1024
1041
|
RUFF_EXIT=$?
|
|
1025
1042
|
fi
|
|
1026
1043
|
if [ "$RUFF_EXIT" -ne 0 ]; then
|
|
@@ -1078,9 +1095,14 @@ else
|
|
|
1078
1095
|
|
|
1079
1096
|
# Ruff format check (formatting gate)
|
|
1080
1097
|
if command -v ruff &> /dev/null; then
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1098
|
+
FMT_ARGS=()
|
|
1099
|
+
while IFS= read -r staged_file; do
|
|
1100
|
+
case "$staged_file" in
|
|
1101
|
+
*.py|*.pyi) FMT_ARGS+=("$staged_file") ;;
|
|
1102
|
+
esac
|
|
1103
|
+
done <<< "$CHANGED_FILES"
|
|
1104
|
+
if [ "${#FMT_ARGS[@]}" -gt 0 ]; then
|
|
1105
|
+
RUFF_FMT_OUTPUT=$(ruff format --check "${FMT_ARGS[@]}" 2>&1)
|
|
1084
1106
|
RUFF_FMT_EXIT=$?
|
|
1085
1107
|
if [ "$RUFF_FMT_EXIT" -ne 0 ]; then
|
|
1086
1108
|
echo "$RUFF_FMT_OUTPUT" | head -20
|
|
@@ -1094,9 +1116,14 @@ else
|
|
|
1094
1116
|
fi
|
|
1095
1117
|
|
|
1096
1118
|
# Debug statement detection (breakpoint/pdb/ipdb)
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1119
|
+
DEBUG_ARGS=()
|
|
1120
|
+
while IFS= read -r staged_file; do
|
|
1121
|
+
case "$staged_file" in
|
|
1122
|
+
*.py|*.pyi) DEBUG_ARGS+=("$staged_file") ;;
|
|
1123
|
+
esac
|
|
1124
|
+
done <<< "$CHANGED_FILES"
|
|
1125
|
+
if [ "${#DEBUG_ARGS[@]}" -gt 0 ]; then
|
|
1126
|
+
DEBUG_HITS=$(grep -n 'breakpoint()\|pdb\.set_trace()\|ipdb\.set_trace()\|import pdb\|import ipdb' "${DEBUG_ARGS[@]}" 2>/dev/null || true)
|
|
1100
1127
|
if [ -n "$DEBUG_HITS" ]; then
|
|
1101
1128
|
echo ""
|
|
1102
1129
|
echo "❌ BLOCKED - Debug statements detected in Python files:"
|
|
@@ -1109,12 +1136,17 @@ else
|
|
|
1109
1136
|
|
|
1110
1137
|
# Optional: Type checking with mypy
|
|
1111
1138
|
if command -v mypy &> /dev/null; then
|
|
1112
|
-
|
|
1113
|
-
|
|
1139
|
+
MYPY_ARGS=()
|
|
1140
|
+
while IFS= read -r staged_file; do
|
|
1141
|
+
case "$staged_file" in
|
|
1142
|
+
*.py|*.pyi) MYPY_ARGS+=("$staged_file") ;;
|
|
1143
|
+
esac
|
|
1144
|
+
done <<< "$CHANGED_FILES"
|
|
1145
|
+
if [ "${#MYPY_ARGS[@]}" -eq 0 ]; then
|
|
1114
1146
|
MYPY_EXIT=0
|
|
1115
1147
|
else
|
|
1116
1148
|
echo "Running mypy type checking (optional)..."
|
|
1117
|
-
mypy --ignore-missing-imports $
|
|
1149
|
+
mypy --ignore-missing-imports "${MYPY_ARGS[@]}" 2>&1 | head -20 | tail -10
|
|
1118
1150
|
MYPY_EXIT=$?
|
|
1119
1151
|
fi
|
|
1120
1152
|
if [ "$MYPY_EXIT" -ne 0 ]; then
|
|
@@ -1266,6 +1298,7 @@ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
|
|
|
1266
1298
|
2>&1 echo ""
|
|
1267
1299
|
2>&1 echo "→ Gate 2: Duplicate code detection..."
|
|
1268
1300
|
GATE_2_START=$(gate_start_ms)
|
|
1301
|
+
GATE_2_STATUS="PASS"
|
|
1269
1302
|
|
|
1270
1303
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1271
1304
|
echo "⏭️ SKIPPED - Duplicate code (documentation project)."
|
|
@@ -1280,13 +1313,34 @@ else
|
|
|
1280
1313
|
fi
|
|
1281
1314
|
|
|
1282
1315
|
echo "Running jscpd for duplicate code detection..."
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1316
|
+
# #477: only pass --config when the file actually exists, and let jscpd's report
|
|
1317
|
+
# decide the message. Before this, a missing config produced "os error 2" and a
|
|
1318
|
+
# non-zero exit that the gate translated into a fabricated "found duplicated
|
|
1319
|
+
# code"; a real clone report that exited 0 was announced as a clean pass.
|
|
1320
|
+
JSCPD_CONFIG=$(resolve_jscpd_config)
|
|
1321
|
+
JSCPD_ARGS=()
|
|
1322
|
+
if [ -n "$JSCPD_CONFIG" ]; then
|
|
1323
|
+
JSCPD_ARGS=(--config "$JSCPD_CONFIG")
|
|
1324
|
+
else
|
|
1325
|
+
echo "ℹ️ No jscpd.conf.json found - using jscpd defaults"
|
|
1326
|
+
fi
|
|
1327
|
+
JSCPD_OUTPUT=$(mktemp)
|
|
1328
|
+
JSCPD_EXIT=0
|
|
1329
|
+
# shellcheck disable=SC2086 # pre-existing splitting of the file list; #477 changes the reporting, not the scan set
|
|
1330
|
+
jscpd ${JSCPD_ARGS[@]+"${JSCPD_ARGS[@]}"} $(echo "$CHANGED_FILES" | tr '\n' ' ') > "$JSCPD_OUTPUT" 2>&1 || JSCPD_EXIT=$?
|
|
1331
|
+
head -30 "$JSCPD_OUTPUT"
|
|
1332
|
+
JSCPD_OUTCOME=$(classify_jscpd_run "$JSCPD_EXIT" "$(cat "$JSCPD_OUTPUT")")
|
|
1333
|
+
rm -f "$JSCPD_OUTPUT"
|
|
1334
|
+
if [ "$JSCPD_OUTCOME" = "duplicates" ]; then
|
|
1286
1335
|
echo "ℹ️ jscpd found duplicated code (warning, not blocking by default)"
|
|
1287
1336
|
echo " Consider refactoring duplicate code blocks."
|
|
1337
|
+
elif [ "$JSCPD_OUTCOME" = "error" ]; then
|
|
1338
|
+
echo "⚠️ jscpd exited with code $JSCPD_EXIT without a report — the output above is its real error"
|
|
1339
|
+
echo "⏭️ SKIPPED - Duplicate code (jscpd run error)"
|
|
1340
|
+
GATE_2_STATUS="SKIP"
|
|
1341
|
+
else
|
|
1342
|
+
echo "✅ PASSED - jscpd duplicate code check completed."
|
|
1288
1343
|
fi
|
|
1289
|
-
echo "✅ PASSED - jscpd duplicate code check completed."
|
|
1290
1344
|
;;
|
|
1291
1345
|
|
|
1292
1346
|
python)
|
|
@@ -1344,7 +1398,8 @@ else
|
|
|
1344
1398
|
esac
|
|
1345
1399
|
done
|
|
1346
1400
|
fi
|
|
1347
|
-
|
|
1401
|
+
# Set at the top of this gate; a branch may already have marked it SKIP (#477). The
|
|
1402
|
+
# audit must record what happened, not what the gate defaults to.
|
|
1348
1403
|
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
1349
1404
|
|
|
1350
1405
|
# ============================================================================
|
|
@@ -1405,7 +1460,7 @@ else
|
|
|
1405
1460
|
|
|
1406
1461
|
SKIP_5A_BLOCK=false
|
|
1407
1462
|
if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
|
|
1408
|
-
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
1463
|
+
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "HEAD")
|
|
1409
1464
|
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
|
|
1410
1465
|
REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
|
|
1411
1466
|
mkdir -p .xp-gate/reports
|
|
@@ -1630,7 +1685,15 @@ else
|
|
|
1630
1685
|
# Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
|
|
1631
1686
|
# 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
|
|
1632
1687
|
# ========================================================================
|
|
1633
|
-
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
|
|
1688
|
+
CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs|py|go)$' || true)
|
|
1689
|
+
|
|
1690
|
+
# Nested plugin sub-projects (plugins/<name>/) own their own vitest config and
|
|
1691
|
+
# are excluded from the root suite (`vitest.config.ts` excludes `plugins/**`,
|
|
1692
|
+
# because plugins/*/node_modules contains vendored tests). Feeding their test
|
|
1693
|
+
# paths to the root vitest yields "No test files found" → false BLOCK, so drop
|
|
1694
|
+
# them here and let the per-plugin suite cover them (CI runs `npm test` inside
|
|
1695
|
+
# each plugin directory).
|
|
1696
|
+
CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | grep -vE '^plugins/[^/]+/' || true)
|
|
1634
1697
|
|
|
1635
1698
|
# Strip PROJECT_SUBDIR prefix from test file paths when running in subdirectory.
|
|
1636
1699
|
# git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR" (line 728)
|
|
@@ -1714,7 +1777,7 @@ else
|
|
|
1714
1777
|
echo "⚠️ Gate 5c: req-ids.json is stale (specification.yaml updated since last check-alignment)"
|
|
1715
1778
|
echo " Run 'xp-gate check-alignment' to regenerate before next commit."
|
|
1716
1779
|
else
|
|
1717
|
-
CHANGED_TEST_FILES_ANNOTATED=$(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true)
|
|
1780
|
+
CHANGED_TEST_FILES_ANNOTATED=$(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs)$' || true)
|
|
1718
1781
|
if [ -n "$CHANGED_TEST_FILES_ANNOTATED" ]; then
|
|
1719
1782
|
echo " → Checking @test REQ-XXX annotations..."
|
|
1720
1783
|
ANNOTATION_ERRORS=0
|
|
@@ -1743,7 +1806,7 @@ else
|
|
|
1743
1806
|
# Check reverse: any REQ in spec with no annotated test → visible FAIL (non-blocking)
|
|
1744
1807
|
ALL_SPEC_REQS=$(jq -r '.[]' "$REQ_IDS_FILE" 2>/dev/null)
|
|
1745
1808
|
ANNOTATED_ALL=""
|
|
1746
|
-
for tf in $(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true); do
|
|
1809
|
+
for tf in $(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs)$' || true); do
|
|
1747
1810
|
if [ -f "$tf" ]; then
|
|
1748
1811
|
ANNOTATED_ALL="$ANNOTATED_ALL $(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' || true)"
|
|
1749
1812
|
fi
|
|
@@ -1797,16 +1860,41 @@ else
|
|
|
1797
1860
|
# TypeScript: vitest with smart test selection
|
|
1798
1861
|
if [ "$CURRENT_LANG" = "typescript" ] && { [ -f "package.json" ] || [ -f "$PROJECT_ROOT/package.json" ]; }; then
|
|
1799
1862
|
if run_without_git_context npx vitest --version >/dev/null 2>&1; then
|
|
1800
|
-
# Count changed test files to decide strategy
|
|
1801
|
-
|
|
1863
|
+
# Count changed test files to decide strategy. `grep -c '.'` prints its
|
|
1864
|
+
# own zero AND exits 1 on an empty list, so `|| echo "0"` used to yield
|
|
1865
|
+
# $'0\n0' -- the same two-value bug that false-blocked Gate 8 (#493).
|
|
1866
|
+
# Bundled mirrors under src/npm-package/ are excluded from the root
|
|
1867
|
+
# vitest project (vitest.config.ts, #418), so selecting them makes
|
|
1868
|
+
# vitest exit 1 with "No test files found" and this gate reads that as
|
|
1869
|
+
# failing tests: a false BLOCK for any commit that only touches mirrors
|
|
1870
|
+
# (#494). Route both lists through the selector and announce it.
|
|
1871
|
+
SELECTED_TEST_FILES=$(select_root_suite_targets "$CHANGED_TEST_FILES")
|
|
1872
|
+
if [ "$SELECTED_TEST_FILES" != "$CHANGED_TEST_FILES" ]; then
|
|
1873
|
+
echo " ↪ Selection adjusted for bundled mirrors (#494):"
|
|
1874
|
+
echo " was: $(echo "$CHANGED_TEST_FILES" | tr '\n' ' ')"
|
|
1875
|
+
echo " now: $(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')"
|
|
1876
|
+
fi
|
|
1877
|
+
CHANGED_TEST_FILE_COUNT=$(echo "$SELECTED_TEST_FILES" | grep -c '.' || true)
|
|
1802
1878
|
|
|
1803
1879
|
# Also get changed source files (non-test) for related-test lookup
|
|
1804
1880
|
CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
|
|
1881
|
+
# Nested plugin sub-projects are excluded from the root suite; their
|
|
1882
|
+
# sources must not drive root-vitest related-test lookup either (it
|
|
1883
|
+
# would resolve to plugin-local paths vitest cannot collect).
|
|
1884
|
+
CHANGED_SRC_FILES=$(echo "$CHANGED_SRC_FILES" | grep -vE '^plugins/[^/]+/' || true)
|
|
1885
|
+
# Mirror handling is identical for source files: their test-adjacent
|
|
1886
|
+
# paths must never reach vitest as root-project targets (#494).
|
|
1887
|
+
SELECTED_SRC_FILES=$(select_root_suite_targets "$CHANGED_SRC_FILES")
|
|
1888
|
+
if [ "$SELECTED_SRC_FILES" != "$CHANGED_SRC_FILES" ]; then
|
|
1889
|
+
echo " ↪ Source selection adjusted for bundled mirrors (#494):"
|
|
1890
|
+
echo " was: $(echo "$CHANGED_SRC_FILES" | tr '\n' ' ')"
|
|
1891
|
+
echo " now: $(echo "$SELECTED_SRC_FILES" | tr '\n' ' ')"
|
|
1892
|
+
fi
|
|
1805
1893
|
# Strip PROJECT_SUBDIR prefix (same as CHANGED_TEST_FILES fix above)
|
|
1806
|
-
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$
|
|
1807
|
-
|
|
1894
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$SELECTED_SRC_FILES" ]; then
|
|
1895
|
+
SELECTED_SRC_FILES=$(echo "$SELECTED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1808
1896
|
fi
|
|
1809
|
-
CHANGED_SRC_COUNT=$(echo "$
|
|
1897
|
+
CHANGED_SRC_COUNT=$(echo "$SELECTED_SRC_FILES" | grep -c '.' || true)
|
|
1810
1898
|
|
|
1811
1899
|
# Decide strategy
|
|
1812
1900
|
if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
|
|
@@ -1816,24 +1904,24 @@ else
|
|
|
1816
1904
|
TESTS_EXIT_CODE=$?
|
|
1817
1905
|
echo "$TESTS_OUTPUT" | head -60
|
|
1818
1906
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1819
|
-
|
|
1820
|
-
echo "❌ BLOCKED - Tests FAILED"
|
|
1821
|
-
exit 1
|
|
1907
|
+
handle_test_failure "$TESTS_OUTPUT" "full test suite" || exit 1
|
|
1822
1908
|
else
|
|
1823
1909
|
echo "✅ PASSED - Full test suite passed (with coverage)."
|
|
1824
1910
|
fi
|
|
1825
1911
|
elif [ "$CHANGED_TEST_FILE_COUNT" -gt 0 ] 2>/dev/null; then
|
|
1826
1912
|
# Run only changed test files with coverage
|
|
1827
|
-
|
|
1913
|
+
PARTIAL_TEST_RUN=true # Mark as partial run for coverage check
|
|
1914
|
+
SMART_TARGETS=$(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')
|
|
1828
1915
|
echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
|
|
1829
|
-
echo " Target(s): $(echo "$
|
|
1916
|
+
echo " Target(s): $(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')"
|
|
1830
1917
|
TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage $SMART_TARGETS 2>&1)
|
|
1831
1918
|
TESTS_EXIT_CODE=$?
|
|
1832
1919
|
echo "$TESTS_OUTPUT" | head -60
|
|
1833
1920
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1921
|
+
# Distinguish a real test failure from a runner infrastructure
|
|
1922
|
+
# error -- see #454. handle_test_failure routes both cases and
|
|
1923
|
+
# keeps the message consistent across every Gate 5 branch.
|
|
1924
|
+
handle_test_failure "$TESTS_OUTPUT" "changed test files" || exit 1
|
|
1837
1925
|
else
|
|
1838
1926
|
echo "✅ PASSED - Changed test files passed (with coverage)."
|
|
1839
1927
|
fi
|
|
@@ -1841,7 +1929,7 @@ else
|
|
|
1841
1929
|
# Source files changed but no test files → find related tests or run full
|
|
1842
1930
|
echo "Source file(s) changed, finding related tests..."
|
|
1843
1931
|
RELATED_TESTS=""
|
|
1844
|
-
for src_file in $
|
|
1932
|
+
for src_file in $SELECTED_SRC_FILES; do
|
|
1845
1933
|
base="${src_file%.*}"
|
|
1846
1934
|
filename="${base##*/}"
|
|
1847
1935
|
dir="$(dirname "$src_file")"
|
|
@@ -1875,9 +1963,7 @@ else
|
|
|
1875
1963
|
TESTS_EXIT_CODE=$?
|
|
1876
1964
|
echo "$TESTS_OUTPUT" | head -60
|
|
1877
1965
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1878
|
-
|
|
1879
|
-
echo "❌ BLOCKED - Tests FAILED in related test files"
|
|
1880
|
-
exit 1
|
|
1966
|
+
handle_test_failure "$TESTS_OUTPUT" "related test files" || exit 1
|
|
1881
1967
|
else
|
|
1882
1968
|
echo "✅ PASSED - Related test files passed (with coverage)."
|
|
1883
1969
|
fi
|
|
@@ -1888,9 +1974,7 @@ else
|
|
|
1888
1974
|
TESTS_EXIT_CODE=$?
|
|
1889
1975
|
echo "$TESTS_OUTPUT" | head -60
|
|
1890
1976
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1891
|
-
|
|
1892
|
-
echo "❌ BLOCKED - Tests FAILED"
|
|
1893
|
-
exit 1
|
|
1977
|
+
handle_test_failure "$TESTS_OUTPUT" "full test suite" || exit 1
|
|
1894
1978
|
else
|
|
1895
1979
|
echo "✅ PASSED - Full test suite passed (with coverage)."
|
|
1896
1980
|
fi
|
|
@@ -1907,13 +1991,22 @@ else
|
|
|
1907
1991
|
TESTS_EXIT_CODE=$?
|
|
1908
1992
|
echo "$TESTS_OUTPUT" | head -50
|
|
1909
1993
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1994
|
+
# This branch is reached from the typescript adapter, so run_tests ran
|
|
1995
|
+
# vitest (adapters/typescript.sh) and its output is vitest-shaped: the
|
|
1996
|
+
# same three non-failure exits (#454 runner EPERM, #473 partial
|
|
1997
|
+
# threshold, #498 no test files) apply here as in the four branches
|
|
1998
|
+
# above. #454 left it unguarded because its own comment covers the
|
|
1999
|
+
# NON-typescript fallback below, whose runner is pytest/go/jest; this
|
|
2000
|
+
# one is not that branch.
|
|
2001
|
+
handle_test_failure "$TESTS_OUTPUT" "typescript adapter fallback" || exit 1
|
|
1913
2002
|
fi
|
|
1914
2003
|
fi
|
|
1915
2004
|
else
|
|
1916
|
-
#
|
|
2005
|
+
# Adapter test flow for everything the TypeScript branch above did not
|
|
2006
|
+
# claim. The label this branch carried (#454) was "Non-TypeScript", which
|
|
2007
|
+
# is not what reaches it: a TypeScript project without a package.json also
|
|
2008
|
+
# lands here, and its adapter runs vitest — which is how a test-less TS
|
|
2009
|
+
# repo got false-blocked on "No test files found" (#498).
|
|
1917
2010
|
# Skip for Python — tests already run with coverage in the coverage section below.
|
|
1918
2011
|
# Running them here first without coverage doubles pre-commit time for no benefit.
|
|
1919
2012
|
if [ "$CURRENT_LANG" = "python" ]; then
|
|
@@ -1923,9 +2016,18 @@ else
|
|
|
1923
2016
|
TESTS_EXIT_CODE=$?
|
|
1924
2017
|
echo "$TESTS_OUTPUT" | head -50
|
|
1925
2018
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2019
|
+
if [ "$CURRENT_LANG" = "typescript" ]; then
|
|
2020
|
+
# vitest output, so the same three non-failure exits are judged
|
|
2021
|
+
# here as in the branches above (#454, #473, #498).
|
|
2022
|
+
handle_test_failure "$TESTS_OUTPUT" "typescript adapter (no package.json)" || exit 1
|
|
2023
|
+
else
|
|
2024
|
+
# Genuinely non-vitest runners (pytest/go/jest/...): their output
|
|
2025
|
+
# shape is not vitest's, so the guard could excuse a real failure.
|
|
2026
|
+
# They keep blocking on any non-zero exit (#454).
|
|
2027
|
+
echo ""
|
|
2028
|
+
echo "❌ BLOCKED - Tests FAILED"
|
|
2029
|
+
exit 1
|
|
2030
|
+
fi
|
|
1929
2031
|
else
|
|
1930
2032
|
echo "✅ PASSED - Unit tests passed."
|
|
1931
2033
|
fi
|
|
@@ -2044,8 +2146,9 @@ else
|
|
|
2044
2146
|
" 2>/dev/null)
|
|
2045
2147
|
if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
|
|
2046
2148
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
2047
|
-
#
|
|
2048
|
-
|
|
2149
|
+
# Only a subset test run (smart selection) leaves partial coverage data;
|
|
2150
|
+
# a full suite run must block on <80% even when many test files changed.
|
|
2151
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2049
2152
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2050
2153
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
2051
2154
|
else
|
|
@@ -2085,6 +2188,64 @@ else
|
|
|
2085
2188
|
echo "⚠️ coverage.py not available, cannot enforce Python coverage threshold."
|
|
2086
2189
|
fi
|
|
2087
2190
|
;;
|
|
2191
|
+
"java")
|
|
2192
|
+
COVERAGE_ENFORCED=true
|
|
2193
|
+
# JaCoCo writes target/site/jacoco/ (Maven) or build/reports/jacoco/test/
|
|
2194
|
+
# (Gradle), i.e. neither path probed by the default branch below, so Java
|
|
2195
|
+
# needs its own branch to be measurable at all.
|
|
2196
|
+
jacoco_csv=""
|
|
2197
|
+
for jacoco_candidate in \
|
|
2198
|
+
"target/site/jacoco/jacoco.csv" \
|
|
2199
|
+
"build/reports/jacoco/test/jacocoTestReport.csv"; do
|
|
2200
|
+
[ -f "$jacoco_candidate" ] && { jacoco_csv="$jacoco_candidate"; break; }
|
|
2201
|
+
done
|
|
2202
|
+
|
|
2203
|
+
if [ -n "$jacoco_csv" ]; then
|
|
2204
|
+
# Aggregate INSTRUCTION_MISSED / INSTRUCTION_COVERED over every class row.
|
|
2205
|
+
# Columns are located by header name because their position is not part of
|
|
2206
|
+
# the CSV's stable contract.
|
|
2207
|
+
COVERAGE_PERCENT=$(awk -F, '
|
|
2208
|
+
NR == 1 {
|
|
2209
|
+
for (i = 1; i <= NF; i++) {
|
|
2210
|
+
if ($i == "INSTRUCTION_MISSED") missed_col = i;
|
|
2211
|
+
if ($i == "INSTRUCTION_COVERED") covered_col = i;
|
|
2212
|
+
}
|
|
2213
|
+
next;
|
|
2214
|
+
}
|
|
2215
|
+
missed_col && covered_col {
|
|
2216
|
+
missed += $missed_col;
|
|
2217
|
+
covered += $covered_col;
|
|
2218
|
+
}
|
|
2219
|
+
END {
|
|
2220
|
+
total = missed + covered;
|
|
2221
|
+
if (total > 0) printf "%.2f", (covered / total) * 100;
|
|
2222
|
+
else print "";
|
|
2223
|
+
}
|
|
2224
|
+
' "$jacoco_csv" 2>/dev/null) || COVERAGE_PERCENT=""
|
|
2225
|
+
|
|
2226
|
+
if [ -n "$COVERAGE_PERCENT" ]; then
|
|
2227
|
+
# Compare on the unrounded value so 79.5% cannot pass as 80%.
|
|
2228
|
+
COVERAGE_ROUNDED=${COVERAGE_PERCENT%.*}
|
|
2229
|
+
if awk -v pct="$COVERAGE_PERCENT" 'BEGIN { exit !(pct < 80) }'; then
|
|
2230
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2231
|
+
echo "⚠️ Partial coverage ${COVERAGE_ROUNDED}% (subset test run, not full suite)."
|
|
2232
|
+
echo " Run the full test suite for a complete Java coverage check."
|
|
2233
|
+
else
|
|
2234
|
+
echo "❌ BLOCKED - Java coverage ${COVERAGE_ROUNDED}% below 80% threshold"
|
|
2235
|
+
exit 1
|
|
2236
|
+
fi
|
|
2237
|
+
else
|
|
2238
|
+
echo "Java coverage: ${COVERAGE_ROUNDED}% ✅ (≥ 80%)"
|
|
2239
|
+
fi
|
|
2240
|
+
else
|
|
2241
|
+
echo "⚠️ Could not parse Java coverage from ${jacoco_csv} (no instruction data)."
|
|
2242
|
+
fi
|
|
2243
|
+
else
|
|
2244
|
+
echo "⚠️ No JaCoCo report found. Run 'mvn verify' or 'gradle jacocoTestReport'"
|
|
2245
|
+
echo " so that target/site/jacoco/jacoco.csv exists — otherwise Java coverage"
|
|
2246
|
+
echo " cannot be enforced (see also: JaCoCo detection in adapters/java.sh)."
|
|
2247
|
+
fi
|
|
2248
|
+
;;
|
|
2088
2249
|
"go")
|
|
2089
2250
|
# Go enforcement already handled inline in Stage 1 (lines 933-942)
|
|
2090
2251
|
;;
|
|
@@ -2147,6 +2308,10 @@ else
|
|
|
2147
2308
|
echo "⚠️ coverage.xml not found. Ensure Pester coverage was run."
|
|
2148
2309
|
fi
|
|
2149
2310
|
;;
|
|
2311
|
+
"iac")
|
|
2312
|
+
# IaC has no coverage concept — must not borrow the TypeScript report
|
|
2313
|
+
# that Stage 1 leaves behind (same rule as the PowerShell branch).
|
|
2314
|
+
;;
|
|
2150
2315
|
*)
|
|
2151
2316
|
# Default: attempt to parse coverage from common formats regardless of language
|
|
2152
2317
|
COVERAGE_ENFORCED=true
|
|
@@ -2170,10 +2335,18 @@ else
|
|
|
2170
2335
|
fi
|
|
2171
2336
|
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
|
|
2172
2337
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
2173
|
-
|
|
2174
|
-
|
|
2338
|
+
# Only a subset test run (smart selection) warns — the full suite may
|
|
2339
|
+
# still reach ≥80%; a full suite run must block.
|
|
2340
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
2341
|
+
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2342
|
+
echo " Run the full test suite for a complete ${CURRENT_LANG} coverage check."
|
|
2343
|
+
else
|
|
2344
|
+
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below 80% threshold"
|
|
2345
|
+
exit 1
|
|
2346
|
+
fi
|
|
2347
|
+
else
|
|
2348
|
+
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
2175
2349
|
fi
|
|
2176
|
-
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
2177
2350
|
else
|
|
2178
2351
|
echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage. No standard coverage report files found."
|
|
2179
2352
|
fi
|
|
@@ -2314,10 +2487,20 @@ else
|
|
|
2314
2487
|
fi
|
|
2315
2488
|
;;
|
|
2316
2489
|
"python")
|
|
2317
|
-
|
|
2490
|
+
# detect any import-linter carrier (incl. pyproject.toml [tool.importlinter])
|
|
2491
|
+
if [ -f "$PROJECT_ROOT/.import-linter.yml" ] || [ -f "$PROJECT_ROOT/import_linter_config.yml" ] || [ -f "$PROJECT_ROOT/.importlinter" ] || grep -q '^\[tool\.importlinter\]' "$PROJECT_ROOT/pyproject.toml" 2>/dev/null; then
|
|
2318
2492
|
if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
|
|
2319
2493
|
echo " Running import-linter for Python..."
|
|
2320
|
-
|
|
2494
|
+
# run at PROJECT_ROOT (hook may have cd'd); block on exit code
|
|
2495
|
+
LINT_IMPORTS_OUTPUT=$(cd "$PROJECT_ROOT" && lint-imports 2>&1)
|
|
2496
|
+
LINT_IMPORTS_EXIT=$?
|
|
2497
|
+
echo "$LINT_IMPORTS_OUTPUT" | head -30
|
|
2498
|
+
if [ "$LINT_IMPORTS_EXIT" -ne 0 ]; then
|
|
2499
|
+
echo ""
|
|
2500
|
+
echo "❌ BLOCKED - Python architecture contract violations (import-linter)"
|
|
2501
|
+
echo "Fix the broken contracts above before committing."
|
|
2502
|
+
exit 1
|
|
2503
|
+
fi
|
|
2321
2504
|
echo " ✅ Python architecture validation completed."
|
|
2322
2505
|
else
|
|
2323
2506
|
echo " ❌ BLOCKED - import-linter not installed"
|
|
@@ -2325,7 +2508,7 @@ else
|
|
|
2325
2508
|
exit 1
|
|
2326
2509
|
fi
|
|
2327
2510
|
else
|
|
2328
|
-
echo " ℹ️ No .import-linter.yml
|
|
2511
|
+
echo " ℹ️ No import-linter config found (.import-linter.yml/.importlinter/[tool.importlinter]) - skipping"
|
|
2329
2512
|
echo " ⏭️ SKIPPED - Python architecture validation (no config)"
|
|
2330
2513
|
fi
|
|
2331
2514
|
;;
|
|
@@ -2415,20 +2598,30 @@ else
|
|
|
2415
2598
|
if [ -n "$BOY_SCOUT_SCRIPT" ]; then
|
|
2416
2599
|
echo " Checking Boy Scout Rule compliance..."
|
|
2417
2600
|
|
|
2418
|
-
# Separate new files and modified files
|
|
2419
|
-
|
|
2420
|
-
|
|
2601
|
+
# Separate new files and modified files.
|
|
2602
|
+
#
|
|
2603
|
+
# src/npm-package/** is a byte-identical MIRROR of canonical sources
|
|
2604
|
+
# (plugins/**, src/principles/**, githooks/**) produced by
|
|
2605
|
+
# sync-package-content.js at publish time. Checking it here double-counts
|
|
2606
|
+
# every warning: the mirror path enters the baseline instead of the source
|
|
2607
|
+
# path, so a violation gets recorded against a path nobody edits and the
|
|
2608
|
+
# real source path stays unbudgeted. Exclude it and let the mirroring be
|
|
2609
|
+
# governed by Mirror Parity in CI, which compares content rather than
|
|
2610
|
+
# re-linting it. (See the same exclusion in adapters/powershell.sh.)
|
|
2611
|
+
MIRROR_EXCLUDE='^src/npm-package/'
|
|
2612
|
+
NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | tr '\n' ' ')
|
|
2613
|
+
MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | grep -v "$MIRROR_EXCLUDE" | tr '\n' ' ')
|
|
2421
2614
|
|
|
2422
2615
|
if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
|
|
2423
2616
|
echo " ✅ No new or modified files to check."
|
|
2424
2617
|
else
|
|
2425
2618
|
# Run Boy Scout Rule enforcement
|
|
2426
|
-
if command -v npx &> /dev/null; then
|
|
2619
|
+
if command -v npx &> /dev/null || [ -f "${PROJECT_ROOT:-$(pwd)}/node_modules/tsx/dist/cli.mjs" ]; then
|
|
2427
2620
|
# Skip if no files to check
|
|
2428
2621
|
if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
|
|
2429
2622
|
echo " ✅ No new or modified source files for Boy Scout check."
|
|
2430
2623
|
else
|
|
2431
|
-
BOY_SCOUT_OUTPUT=$(
|
|
2624
|
+
BOY_SCOUT_OUTPUT=$(run_tsx "$BOY_SCOUT_SCRIPT" \
|
|
2432
2625
|
--new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
|
|
2433
2626
|
--modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
|
|
2434
2627
|
--baseline ".warnings-baseline.json" 2>&1)
|
|
@@ -2459,8 +2652,8 @@ else
|
|
|
2459
2652
|
echo " ✅ PASSED - Boy Scout Rule compliance."
|
|
2460
2653
|
fi
|
|
2461
2654
|
else
|
|
2462
|
-
echo " ℹ️ npx not available - skipping Boy Scout Rule"
|
|
2463
|
-
echo " ⏭️ SKIPPED - Boy Scout Rule (Node.js/
|
|
2655
|
+
echo " ℹ️ npx/tsx not available - skipping Boy Scout Rule"
|
|
2656
|
+
echo " ⏭️ SKIPPED - Boy Scout Rule (Node.js/tsx not available)"
|
|
2464
2657
|
fi
|
|
2465
2658
|
fi
|
|
2466
2659
|
else
|
|
@@ -2516,7 +2709,7 @@ if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
|
2516
2709
|
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
2517
2710
|
CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
|
|
2518
2711
|
GATE_9_OUTPUT=$(mktemp)
|
|
2519
|
-
if
|
|
2712
|
+
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
|
|
2520
2713
|
GATE_9_STATUS="PASS"
|
|
2521
2714
|
echo " ✅ Build integrity check passed."
|
|
2522
2715
|
else
|
|
@@ -2544,8 +2737,9 @@ else
|
|
|
2544
2737
|
echo " ⏭️ SKIPPED - Build integrity (not a TypeScript project)"
|
|
2545
2738
|
fi
|
|
2546
2739
|
|
|
2547
|
-
GATE_9_END=$(
|
|
2548
|
-
|
|
2740
|
+
GATE_9_END=$(gate_start_ms)
|
|
2741
|
+
GATE_9_DURATION_MS=$((GATE_9_END - GATE_9_START))
|
|
2742
|
+
GATE_9_DURATION=$((GATE_9_DURATION_MS / 1000))
|
|
2549
2743
|
echo "✅ Gate 9 completed in ${GATE_9_DURATION}s"
|
|
2550
2744
|
record_gate_audit "gate-9" "build-integrity" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
2551
2745
|
echo ""
|
|
@@ -2579,34 +2773,65 @@ GATE_11_START=$(date +%s)
|
|
|
2579
2773
|
GATE_11_STATUS="PASS"
|
|
2580
2774
|
|
|
2581
2775
|
# BEGIN SPRINT_GATE_SCRIPT RESOLUTION
|
|
2582
|
-
#
|
|
2583
|
-
#
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2776
|
+
# Every tier is a *directory*; the resolver returns the first one that actually holds
|
|
2777
|
+
# sprint-gate.sh. The chain used to stop at the gate-script directories, but the flat
|
|
2778
|
+
# global layout ($GATE_DIR -> ~/.config/xp-gate/adapters) carries gate-3/4/7/8/9/10.sh
|
|
2779
|
+
# and never this script: update-hooks installs sprint-gate.sh under
|
|
2780
|
+
# ~/.config/xp-gate/hooks, which nothing probed, so a correctly installed machine was
|
|
2781
|
+
# told the file was missing (#476).
|
|
2782
|
+
SPRINT_GATE_CANDIDATES=(
|
|
2783
|
+
"$GATE_DIR"
|
|
2784
|
+
"$(git rev-parse --show-toplevel 2>/dev/null)/githooks"
|
|
2785
|
+
"$SCRIPT_DIR"
|
|
2786
|
+
"$HOME/.config/xp-gate/hooks"
|
|
2787
|
+
)
|
|
2788
|
+
SPRINT_GATE_SCRIPT=$(resolve_sprint_gate_script ${SPRINT_GATE_CANDIDATES[@]+"${SPRINT_GATE_CANDIDATES[@]}"})
|
|
2592
2789
|
# END SPRINT_GATE_SCRIPT RESOLUTION
|
|
2593
2790
|
|
|
2791
|
+
echo "→ Gate 11: Sprint Flow Enforcement..."
|
|
2792
|
+
|
|
2594
2793
|
if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
2595
|
-
|
|
2794
|
+
# The decision JSON was piped straight to the terminal with no heading, so it landed
|
|
2795
|
+
# under Gate 10's banner and a legitimate "skip: not a sprint project" looked like a
|
|
2796
|
+
# bare machine object. The exit status stays the authority for blocking; the JSON only
|
|
2797
|
+
# decides which words the user reads.
|
|
2798
|
+
SPRINT_GATE_EXIT=0
|
|
2799
|
+
SPRINT_GATE_OUTPUT=$(bash "$SPRINT_GATE_SCRIPT" --pre-commit 2>&1) || SPRINT_GATE_EXIT=$?
|
|
2800
|
+
SPRINT_GATE_DECISION=$(sprint_gate_decision "$SPRINT_GATE_OUTPUT")
|
|
2801
|
+
SPRINT_GATE_DETAIL=$(sprint_gate_detail "$SPRINT_GATE_OUTPUT")
|
|
2802
|
+
|
|
2803
|
+
if [ "$SPRINT_GATE_EXIT" -ne 0 ]; then
|
|
2596
2804
|
GATE_11_STATUS="BLOCK"
|
|
2597
|
-
echo "❌ BLOCKED - Gate 11: Sprint Flow
|
|
2805
|
+
echo "❌ BLOCKED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-sprint gate denied})"
|
|
2806
|
+
printf '%s\n' "$SPRINT_GATE_OUTPUT" | sed 's/^/ /'
|
|
2598
2807
|
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "1" "$GATE_11_START"
|
|
2599
2808
|
# Sprint gate failure is a hard block — exit immediately
|
|
2600
2809
|
exit 1
|
|
2601
2810
|
fi
|
|
2811
|
+
|
|
2812
|
+
case "$SPRINT_GATE_DECISION" in
|
|
2813
|
+
skip)
|
|
2814
|
+
echo "⏭️ SKIPPED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-no reason given})"
|
|
2815
|
+
GATE_11_STATUS="SKIP"
|
|
2816
|
+
;;
|
|
2817
|
+
allow)
|
|
2818
|
+
echo "✅ PASSED - Gate 11: Sprint Flow (${SPRINT_GATE_DETAIL:-sprint state validated})"
|
|
2819
|
+
;;
|
|
2820
|
+
*)
|
|
2821
|
+
# A run that succeeded but said nothing this gate understands. Pass on the exit
|
|
2822
|
+
# status, but keep the raw output visible rather than inventing a verdict.
|
|
2823
|
+
echo "✅ PASSED - Gate 11: Sprint Flow (no recognisable decision in the gate output)"
|
|
2824
|
+
printf '%s\n' "$SPRINT_GATE_OUTPUT" | sed 's/^/ /'
|
|
2825
|
+
;;
|
|
2826
|
+
esac
|
|
2602
2827
|
else
|
|
2603
|
-
echo "⏭️ SKIPPED - Gate 11: Sprint Flow (sprint-gate.sh not found)"
|
|
2828
|
+
echo "⏭️ SKIPPED - Gate 11: Sprint Flow (sprint-gate.sh not found; searched: $(IFS=', '; printf '%s' "${SPRINT_GATE_CANDIDATES[*]}"))"
|
|
2604
2829
|
GATE_11_STATUS="SKIP"
|
|
2605
2830
|
fi
|
|
2606
2831
|
|
|
2607
2832
|
GATE_11_END=$(date +%s)
|
|
2608
2833
|
GATE_11_DURATION=$((GATE_11_END - GATE_11_START))
|
|
2609
|
-
echo "
|
|
2834
|
+
echo " Gate 11 ($GATE_11_STATUS) completed in ${GATE_11_DURATION}s"
|
|
2610
2835
|
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "0" "$GATE_11_START"
|
|
2611
2836
|
echo ""
|
|
2612
2837
|
# ============================================================================
|
|
@@ -2666,8 +2891,10 @@ generate_quality_report() {
|
|
|
2666
2891
|
if [ -n "$TESTS_OUTPUT" ] && echo "$TESTS_OUTPUT" | grep -qi "fail\|error\|FAILED"; then
|
|
2667
2892
|
# Extract failure lines: "FAIL src/foo.test.ts" or " ✗ test name"
|
|
2668
2893
|
local FAIL_LINES
|
|
2669
|
-
|
|
2670
|
-
|
|
2894
|
+
# errexit/pipefail-safe: grep returns 1 on zero matches (see #489) and this
|
|
2895
|
+
# function is also sourced by consumer wrappers that may enable errexit
|
|
2896
|
+
FAIL_LINES=$(echo "$TESTS_OUTPUT" | grep -E "^\s*(FAIL|✗|failed|AssertionError)" | sed -n '1,10p' | \
|
|
2897
|
+
sed 's/"/\\"/g' | awk '{ printf "%s%s%s", sep, "\"", $0; sep="," }') || true
|
|
2671
2898
|
if [ -n "$FAIL_LINES" ]; then
|
|
2672
2899
|
FAILED_TESTS_JSON="[$FAIL_LINES]"
|
|
2673
2900
|
fi
|