@boyingliu01/xp-gate 0.19.2 → 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 +401 -155
- 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 +2 -2
- 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 +2 -2
- package/plugins/qoder/skills/admin-template-guidelines/SKILL.md +6 -797
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-1.md +91 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-2.md +155 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-3.md +70 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-4.md +154 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-5.md +126 -0
- package/plugins/qoder/skills/admin-template-guidelines/references/rule-6.md +180 -0
- package/plugins/qoder/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +161 -0
- package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +61 -28
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/delphi-review/INSTALL.md +97 -98
- package/plugins/qoder/skills/delphi-review/SKILL.md +170 -63
- package/plugins/qoder/skills/delphi-review/hooks.md +19 -0
- package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +6 -10
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
- package/plugins/qoder/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
- package/plugins/qoder/skills/delphi-review/references/requirements.md +402 -0
- package/plugins/qoder/skills/delphi-review/references/round-templates.md +134 -0
- package/plugins/qoder/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/qoder/skills/delphi-review/tools.md +29 -0
- package/plugins/qoder/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/qoder/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/qoder/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/qoder/skills/grilling/SKILL.md +12 -0
- package/plugins/qoder/skills/ralph-loop/SKILL.md +87 -428
- package/plugins/qoder/skills/ralph-loop/references/components.md +113 -0
- package/plugins/qoder/skills/ralph-loop/references/workflow-details.md +99 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +5 -5
- package/plugins/qoder/skills/sprint-flow/SKILL.md +255 -454
- package/plugins/qoder/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
- package/plugins/qoder/skills/sprint-flow/hooks.md +18 -0
- package/plugins/qoder/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/qoder/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +0 -1
- package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +34 -27
- package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +160 -123
- package/plugins/qoder/skills/sprint-flow/references/phase-3-build.md +203 -2
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +107 -34
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +194 -12
- package/plugins/qoder/skills/sprint-flow/references/phase-6-close.md +139 -7
- package/plugins/qoder/skills/sprint-flow/references/phase-overview.md +486 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/qoder/skills/sprint-flow/tools.md +29 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/SKILL.md +104 -601
- package/plugins/qoder/skills/to-issues/SKILL.md +1 -1
- 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/plugins/qoder/skills/ralph-loop/references/components/memory.md +0 -55
- package/plugins/qoder/skills/ralph-loop/references/components/middleware.md +0 -54
- package/plugins/qoder/skills/ralph-loop/references/components/skill-invocations.md +0 -39
- package/plugins/qoder/skills/ralph-loop/references/components/system-prompt.md +0 -24
- package/plugins/qoder/skills/ralph-loop/references/components/tool-descriptions.md +0 -32
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,8 +1024,22 @@ else
|
|
|
1013
1024
|
else
|
|
1014
1025
|
# Run Ruff (includes syntax check, linting)
|
|
1015
1026
|
echo "Running Ruff linting (syntax + lint)..."
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1037
|
+
RUFF_OUTPUT="[]"
|
|
1038
|
+
RUFF_EXIT=0
|
|
1039
|
+
else
|
|
1040
|
+
RUFF_OUTPUT=$(ruff check "${RUFF_ARGS[@]}" --output-format json 2>&1)
|
|
1041
|
+
RUFF_EXIT=$?
|
|
1042
|
+
fi
|
|
1018
1043
|
if [ "$RUFF_EXIT" -ne 0 ]; then
|
|
1019
1044
|
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
1020
1045
|
# Get baseline warnings for changed Python files
|
|
@@ -1070,9 +1095,14 @@ else
|
|
|
1070
1095
|
|
|
1071
1096
|
# Ruff format check (formatting gate)
|
|
1072
1097
|
if command -v ruff &> /dev/null; then
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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)
|
|
1076
1106
|
RUFF_FMT_EXIT=$?
|
|
1077
1107
|
if [ "$RUFF_FMT_EXIT" -ne 0 ]; then
|
|
1078
1108
|
echo "$RUFF_FMT_OUTPUT" | head -20
|
|
@@ -1086,9 +1116,14 @@ else
|
|
|
1086
1116
|
fi
|
|
1087
1117
|
|
|
1088
1118
|
# Debug statement detection (breakpoint/pdb/ipdb)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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)
|
|
1092
1127
|
if [ -n "$DEBUG_HITS" ]; then
|
|
1093
1128
|
echo ""
|
|
1094
1129
|
echo "❌ BLOCKED - Debug statements detected in Python files:"
|
|
@@ -1101,9 +1136,19 @@ else
|
|
|
1101
1136
|
|
|
1102
1137
|
# Optional: Type checking with mypy
|
|
1103
1138
|
if command -v mypy &> /dev/null; then
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
|
1146
|
+
MYPY_EXIT=0
|
|
1147
|
+
else
|
|
1148
|
+
echo "Running mypy type checking (optional)..."
|
|
1149
|
+
mypy --ignore-missing-imports "${MYPY_ARGS[@]}" 2>&1 | head -20 | tail -10
|
|
1150
|
+
MYPY_EXIT=$?
|
|
1151
|
+
fi
|
|
1107
1152
|
if [ "$MYPY_EXIT" -ne 0 ]; then
|
|
1108
1153
|
echo ""
|
|
1109
1154
|
echo "❌ BLOCKED - MYPI TYPE ERRORS detected"
|
|
@@ -1253,6 +1298,7 @@ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
|
|
|
1253
1298
|
2>&1 echo ""
|
|
1254
1299
|
2>&1 echo "→ Gate 2: Duplicate code detection..."
|
|
1255
1300
|
GATE_2_START=$(gate_start_ms)
|
|
1301
|
+
GATE_2_STATUS="PASS"
|
|
1256
1302
|
|
|
1257
1303
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1258
1304
|
echo "⏭️ SKIPPED - Duplicate code (documentation project)."
|
|
@@ -1267,13 +1313,34 @@ else
|
|
|
1267
1313
|
fi
|
|
1268
1314
|
|
|
1269
1315
|
echo "Running jscpd for duplicate code detection..."
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
|
1273
1335
|
echo "ℹ️ jscpd found duplicated code (warning, not blocking by default)"
|
|
1274
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."
|
|
1275
1343
|
fi
|
|
1276
|
-
echo "✅ PASSED - jscpd duplicate code check completed."
|
|
1277
1344
|
;;
|
|
1278
1345
|
|
|
1279
1346
|
python)
|
|
@@ -1331,7 +1398,8 @@ else
|
|
|
1331
1398
|
esac
|
|
1332
1399
|
done
|
|
1333
1400
|
fi
|
|
1334
|
-
|
|
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.
|
|
1335
1403
|
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
1336
1404
|
|
|
1337
1405
|
# ============================================================================
|
|
@@ -1392,7 +1460,7 @@ else
|
|
|
1392
1460
|
|
|
1393
1461
|
SKIP_5A_BLOCK=false
|
|
1394
1462
|
if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
|
|
1395
|
-
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")
|
|
1396
1464
|
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
|
|
1397
1465
|
REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
|
|
1398
1466
|
mkdir -p .xp-gate/reports
|
|
@@ -1617,7 +1685,15 @@ else
|
|
|
1617
1685
|
# Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
|
|
1618
1686
|
# 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
|
|
1619
1687
|
# ========================================================================
|
|
1620
|
-
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)
|
|
1621
1697
|
|
|
1622
1698
|
# Strip PROJECT_SUBDIR prefix from test file paths when running in subdirectory.
|
|
1623
1699
|
# git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR" (line 728)
|
|
@@ -1701,7 +1777,7 @@ else
|
|
|
1701
1777
|
echo "⚠️ Gate 5c: req-ids.json is stale (specification.yaml updated since last check-alignment)"
|
|
1702
1778
|
echo " Run 'xp-gate check-alignment' to regenerate before next commit."
|
|
1703
1779
|
else
|
|
1704
|
-
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)
|
|
1705
1781
|
if [ -n "$CHANGED_TEST_FILES_ANNOTATED" ]; then
|
|
1706
1782
|
echo " → Checking @test REQ-XXX annotations..."
|
|
1707
1783
|
ANNOTATION_ERRORS=0
|
|
@@ -1730,7 +1806,7 @@ else
|
|
|
1730
1806
|
# Check reverse: any REQ in spec with no annotated test → visible FAIL (non-blocking)
|
|
1731
1807
|
ALL_SPEC_REQS=$(jq -r '.[]' "$REQ_IDS_FILE" 2>/dev/null)
|
|
1732
1808
|
ANNOTATED_ALL=""
|
|
1733
|
-
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
|
|
1734
1810
|
if [ -f "$tf" ]; then
|
|
1735
1811
|
ANNOTATED_ALL="$ANNOTATED_ALL $(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' || true)"
|
|
1736
1812
|
fi
|
|
@@ -1784,16 +1860,41 @@ else
|
|
|
1784
1860
|
# TypeScript: vitest with smart test selection
|
|
1785
1861
|
if [ "$CURRENT_LANG" = "typescript" ] && { [ -f "package.json" ] || [ -f "$PROJECT_ROOT/package.json" ]; }; then
|
|
1786
1862
|
if run_without_git_context npx vitest --version >/dev/null 2>&1; then
|
|
1787
|
-
# Count changed test files to decide strategy
|
|
1788
|
-
|
|
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)
|
|
1789
1878
|
|
|
1790
1879
|
# Also get changed source files (non-test) for related-test lookup
|
|
1791
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
|
|
1792
1893
|
# Strip PROJECT_SUBDIR prefix (same as CHANGED_TEST_FILES fix above)
|
|
1793
|
-
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$
|
|
1794
|
-
|
|
1894
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$SELECTED_SRC_FILES" ]; then
|
|
1895
|
+
SELECTED_SRC_FILES=$(echo "$SELECTED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1795
1896
|
fi
|
|
1796
|
-
CHANGED_SRC_COUNT=$(echo "$
|
|
1897
|
+
CHANGED_SRC_COUNT=$(echo "$SELECTED_SRC_FILES" | grep -c '.' || true)
|
|
1797
1898
|
|
|
1798
1899
|
# Decide strategy
|
|
1799
1900
|
if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
|
|
@@ -1803,24 +1904,24 @@ else
|
|
|
1803
1904
|
TESTS_EXIT_CODE=$?
|
|
1804
1905
|
echo "$TESTS_OUTPUT" | head -60
|
|
1805
1906
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1806
|
-
|
|
1807
|
-
echo "❌ BLOCKED - Tests FAILED"
|
|
1808
|
-
exit 1
|
|
1907
|
+
handle_test_failure "$TESTS_OUTPUT" "full test suite" || exit 1
|
|
1809
1908
|
else
|
|
1810
1909
|
echo "✅ PASSED - Full test suite passed (with coverage)."
|
|
1811
1910
|
fi
|
|
1812
1911
|
elif [ "$CHANGED_TEST_FILE_COUNT" -gt 0 ] 2>/dev/null; then
|
|
1813
1912
|
# Run only changed test files with coverage
|
|
1814
|
-
|
|
1913
|
+
PARTIAL_TEST_RUN=true # Mark as partial run for coverage check
|
|
1914
|
+
SMART_TARGETS=$(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')
|
|
1815
1915
|
echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
|
|
1816
|
-
echo " Target(s): $(echo "$
|
|
1916
|
+
echo " Target(s): $(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')"
|
|
1817
1917
|
TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage $SMART_TARGETS 2>&1)
|
|
1818
1918
|
TESTS_EXIT_CODE=$?
|
|
1819
1919
|
echo "$TESTS_OUTPUT" | head -60
|
|
1820
1920
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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
|
|
1824
1925
|
else
|
|
1825
1926
|
echo "✅ PASSED - Changed test files passed (with coverage)."
|
|
1826
1927
|
fi
|
|
@@ -1828,7 +1929,7 @@ else
|
|
|
1828
1929
|
# Source files changed but no test files → find related tests or run full
|
|
1829
1930
|
echo "Source file(s) changed, finding related tests..."
|
|
1830
1931
|
RELATED_TESTS=""
|
|
1831
|
-
for src_file in $
|
|
1932
|
+
for src_file in $SELECTED_SRC_FILES; do
|
|
1832
1933
|
base="${src_file%.*}"
|
|
1833
1934
|
filename="${base##*/}"
|
|
1834
1935
|
dir="$(dirname "$src_file")"
|
|
@@ -1862,9 +1963,7 @@ else
|
|
|
1862
1963
|
TESTS_EXIT_CODE=$?
|
|
1863
1964
|
echo "$TESTS_OUTPUT" | head -60
|
|
1864
1965
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1865
|
-
|
|
1866
|
-
echo "❌ BLOCKED - Tests FAILED in related test files"
|
|
1867
|
-
exit 1
|
|
1966
|
+
handle_test_failure "$TESTS_OUTPUT" "related test files" || exit 1
|
|
1868
1967
|
else
|
|
1869
1968
|
echo "✅ PASSED - Related test files passed (with coverage)."
|
|
1870
1969
|
fi
|
|
@@ -1875,9 +1974,7 @@ else
|
|
|
1875
1974
|
TESTS_EXIT_CODE=$?
|
|
1876
1975
|
echo "$TESTS_OUTPUT" | head -60
|
|
1877
1976
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1878
|
-
|
|
1879
|
-
echo "❌ BLOCKED - Tests FAILED"
|
|
1880
|
-
exit 1
|
|
1977
|
+
handle_test_failure "$TESTS_OUTPUT" "full test suite" || exit 1
|
|
1881
1978
|
else
|
|
1882
1979
|
echo "✅ PASSED - Full test suite passed (with coverage)."
|
|
1883
1980
|
fi
|
|
@@ -1894,13 +1991,22 @@ else
|
|
|
1894
1991
|
TESTS_EXIT_CODE=$?
|
|
1895
1992
|
echo "$TESTS_OUTPUT" | head -50
|
|
1896
1993
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
|
1900
2002
|
fi
|
|
1901
2003
|
fi
|
|
1902
2004
|
else
|
|
1903
|
-
#
|
|
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).
|
|
1904
2010
|
# Skip for Python — tests already run with coverage in the coverage section below.
|
|
1905
2011
|
# Running them here first without coverage doubles pre-commit time for no benefit.
|
|
1906
2012
|
if [ "$CURRENT_LANG" = "python" ]; then
|
|
@@ -1910,9 +2016,18 @@ else
|
|
|
1910
2016
|
TESTS_EXIT_CODE=$?
|
|
1911
2017
|
echo "$TESTS_OUTPUT" | head -50
|
|
1912
2018
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
|
1916
2031
|
else
|
|
1917
2032
|
echo "✅ PASSED - Unit tests passed."
|
|
1918
2033
|
fi
|
|
@@ -2031,8 +2146,9 @@ else
|
|
|
2031
2146
|
" 2>/dev/null)
|
|
2032
2147
|
if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
|
|
2033
2148
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
2034
|
-
#
|
|
2035
|
-
|
|
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
|
|
2036
2152
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
2037
2153
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
2038
2154
|
else
|
|
@@ -2072,6 +2188,64 @@ else
|
|
|
2072
2188
|
echo "⚠️ coverage.py not available, cannot enforce Python coverage threshold."
|
|
2073
2189
|
fi
|
|
2074
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
|
+
;;
|
|
2075
2249
|
"go")
|
|
2076
2250
|
# Go enforcement already handled inline in Stage 1 (lines 933-942)
|
|
2077
2251
|
;;
|
|
@@ -2134,6 +2308,10 @@ else
|
|
|
2134
2308
|
echo "⚠️ coverage.xml not found. Ensure Pester coverage was run."
|
|
2135
2309
|
fi
|
|
2136
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
|
+
;;
|
|
2137
2315
|
*)
|
|
2138
2316
|
# Default: attempt to parse coverage from common formats regardless of language
|
|
2139
2317
|
COVERAGE_ENFORCED=true
|
|
@@ -2157,10 +2335,18 @@ else
|
|
|
2157
2335
|
fi
|
|
2158
2336
|
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
|
|
2159
2337
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
2160
|
-
|
|
2161
|
-
|
|
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%)"
|
|
2162
2349
|
fi
|
|
2163
|
-
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
2164
2350
|
else
|
|
2165
2351
|
echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage. No standard coverage report files found."
|
|
2166
2352
|
fi
|
|
@@ -2301,10 +2487,20 @@ else
|
|
|
2301
2487
|
fi
|
|
2302
2488
|
;;
|
|
2303
2489
|
"python")
|
|
2304
|
-
|
|
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
|
|
2305
2492
|
if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
|
|
2306
2493
|
echo " Running import-linter for Python..."
|
|
2307
|
-
|
|
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
|
|
2308
2504
|
echo " ✅ Python architecture validation completed."
|
|
2309
2505
|
else
|
|
2310
2506
|
echo " ❌ BLOCKED - import-linter not installed"
|
|
@@ -2312,7 +2508,7 @@ else
|
|
|
2312
2508
|
exit 1
|
|
2313
2509
|
fi
|
|
2314
2510
|
else
|
|
2315
|
-
echo " ℹ️ No .import-linter.yml
|
|
2511
|
+
echo " ℹ️ No import-linter config found (.import-linter.yml/.importlinter/[tool.importlinter]) - skipping"
|
|
2316
2512
|
echo " ⏭️ SKIPPED - Python architecture validation (no config)"
|
|
2317
2513
|
fi
|
|
2318
2514
|
;;
|
|
@@ -2402,20 +2598,30 @@ else
|
|
|
2402
2598
|
if [ -n "$BOY_SCOUT_SCRIPT" ]; then
|
|
2403
2599
|
echo " Checking Boy Scout Rule compliance..."
|
|
2404
2600
|
|
|
2405
|
-
# Separate new files and modified files
|
|
2406
|
-
|
|
2407
|
-
|
|
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' ' ')
|
|
2408
2614
|
|
|
2409
2615
|
if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
|
|
2410
2616
|
echo " ✅ No new or modified files to check."
|
|
2411
2617
|
else
|
|
2412
2618
|
# Run Boy Scout Rule enforcement
|
|
2413
|
-
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
|
|
2414
2620
|
# Skip if no files to check
|
|
2415
2621
|
if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
|
|
2416
2622
|
echo " ✅ No new or modified source files for Boy Scout check."
|
|
2417
2623
|
else
|
|
2418
|
-
BOY_SCOUT_OUTPUT=$(
|
|
2624
|
+
BOY_SCOUT_OUTPUT=$(run_tsx "$BOY_SCOUT_SCRIPT" \
|
|
2419
2625
|
--new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
|
|
2420
2626
|
--modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
|
|
2421
2627
|
--baseline ".warnings-baseline.json" 2>&1)
|
|
@@ -2446,8 +2652,8 @@ else
|
|
|
2446
2652
|
echo " ✅ PASSED - Boy Scout Rule compliance."
|
|
2447
2653
|
fi
|
|
2448
2654
|
else
|
|
2449
|
-
echo " ℹ️ npx not available - skipping Boy Scout Rule"
|
|
2450
|
-
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)"
|
|
2451
2657
|
fi
|
|
2452
2658
|
fi
|
|
2453
2659
|
else
|
|
@@ -2503,7 +2709,7 @@ if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
|
2503
2709
|
if [ -n "$GATE_9_SCRIPT" ]; then
|
|
2504
2710
|
CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
|
|
2505
2711
|
GATE_9_OUTPUT=$(mktemp)
|
|
2506
|
-
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
|
|
2507
2713
|
GATE_9_STATUS="PASS"
|
|
2508
2714
|
echo " ✅ Build integrity check passed."
|
|
2509
2715
|
else
|
|
@@ -2531,8 +2737,9 @@ else
|
|
|
2531
2737
|
echo " ⏭️ SKIPPED - Build integrity (not a TypeScript project)"
|
|
2532
2738
|
fi
|
|
2533
2739
|
|
|
2534
|
-
GATE_9_END=$(
|
|
2535
|
-
|
|
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))
|
|
2536
2743
|
echo "✅ Gate 9 completed in ${GATE_9_DURATION}s"
|
|
2537
2744
|
record_gate_audit "gate-9" "build-integrity" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
2538
2745
|
echo ""
|
|
@@ -2565,29 +2772,66 @@ fi
|
|
|
2565
2772
|
GATE_11_START=$(date +%s)
|
|
2566
2773
|
GATE_11_STATUS="PASS"
|
|
2567
2774
|
|
|
2568
|
-
SPRINT_GATE_SCRIPT
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2775
|
+
# BEGIN SPRINT_GATE_SCRIPT RESOLUTION
|
|
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[@]}"})
|
|
2789
|
+
# END SPRINT_GATE_SCRIPT RESOLUTION
|
|
2790
|
+
|
|
2791
|
+
echo "→ Gate 11: Sprint Flow Enforcement..."
|
|
2574
2792
|
|
|
2575
2793
|
if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
2576
|
-
|
|
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
|
|
2577
2804
|
GATE_11_STATUS="BLOCK"
|
|
2578
|
-
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/^/ /'
|
|
2579
2807
|
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "1" "$GATE_11_START"
|
|
2580
2808
|
# Sprint gate failure is a hard block — exit immediately
|
|
2581
2809
|
exit 1
|
|
2582
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
|
|
2583
2827
|
else
|
|
2584
|
-
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[*]}"))"
|
|
2585
2829
|
GATE_11_STATUS="SKIP"
|
|
2586
2830
|
fi
|
|
2587
2831
|
|
|
2588
2832
|
GATE_11_END=$(date +%s)
|
|
2589
2833
|
GATE_11_DURATION=$((GATE_11_END - GATE_11_START))
|
|
2590
|
-
echo "
|
|
2834
|
+
echo " Gate 11 ($GATE_11_STATUS) completed in ${GATE_11_DURATION}s"
|
|
2591
2835
|
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "0" "$GATE_11_START"
|
|
2592
2836
|
echo ""
|
|
2593
2837
|
# ============================================================================
|
|
@@ -2647,8 +2891,10 @@ generate_quality_report() {
|
|
|
2647
2891
|
if [ -n "$TESTS_OUTPUT" ] && echo "$TESTS_OUTPUT" | grep -qi "fail\|error\|FAILED"; then
|
|
2648
2892
|
# Extract failure lines: "FAIL src/foo.test.ts" or " ✗ test name"
|
|
2649
2893
|
local FAIL_LINES
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
|
2652
2898
|
if [ -n "$FAIL_LINES" ]; then
|
|
2653
2899
|
FAILED_TESTS_JSON="[$FAIL_LINES]"
|
|
2654
2900
|
fi
|