@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/gate-8.sh
CHANGED
|
@@ -19,16 +19,60 @@ fi
|
|
|
19
19
|
if [ -n "$GITLEAKS_CMD" ]; then
|
|
20
20
|
GITLEAKS_CONFIG=""
|
|
21
21
|
if [ -f ".gitleaks.toml" ]; then
|
|
22
|
-
|
|
22
|
+
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
23
|
+
# absolute path in the host's own format when we can derive one (#449).
|
|
24
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
25
|
+
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
26
|
+
else
|
|
27
|
+
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
28
|
+
fi
|
|
23
29
|
fi
|
|
24
30
|
|
|
25
|
-
#
|
|
26
|
-
|
|
31
|
+
# Scan the INDEX, not history. gitleaks 8.x `git --pre-commit` walks committed
|
|
32
|
+
# revisions and reports "0 commits scanned" on a pre-commit run -- so a staged
|
|
33
|
+
# secret was never seen and Gate 8 always passed. `git --staged` scans
|
|
34
|
+
# `git diff --cached`, which is exactly the pre-commit payload (#449).
|
|
35
|
+
GITLEAKS_REPORT="${TMPDIR:-/tmp}/gitleaks-report.json"
|
|
36
|
+
rm -f "$GITLEAKS_REPORT"
|
|
37
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
27
38
|
GITLEAKS_EXIT=$?
|
|
28
39
|
|
|
29
40
|
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
# Decide SKIP from the REPORT ARTIFACT, not from log wording. Parsing stdout
|
|
42
|
+
# meant a gitleaks message change would silently turn a no-evidence run into
|
|
43
|
+
# a PASS -- the same fail-open class this gate just fixed (#449).
|
|
44
|
+
# The report exists and is a JSON array of findings; `[]` == clean scan.
|
|
45
|
+
GITLEAKS_FINDINGS="?"
|
|
46
|
+
if [ -f "$GITLEAKS_REPORT" ]; then
|
|
47
|
+
# A report that is not shaped like a JSON array is no evidence: leave the
|
|
48
|
+
# '?' sentinel so the gate reports SKIP, never a PASS it cannot support
|
|
49
|
+
# (the fail-closed-on-the-claim rule #449 established).
|
|
50
|
+
if grep -q '^\[' "$GITLEAKS_REPORT" 2>/dev/null; then
|
|
51
|
+
# Count occurrences, not matching lines: `grep -c` prints its own zero AND
|
|
52
|
+
# exits 1 on a clean report, so the old `|| echo 0` handed the verdict table
|
|
53
|
+
# $'0\n0' -- no branch matched and a clean scan was BLOCKED as if it leaked
|
|
54
|
+
# (#493). wc -l emits exactly one value, and `|| true` keeps the assignment
|
|
55
|
+
# errexit/pipefail-safe (#489).
|
|
56
|
+
GITLEAKS_FINDINGS=$(grep -o '"RuleID"' "$GITLEAKS_REPORT" 2>/dev/null | wc -l | tr -d '[:space:]') || true
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ "$GITLEAKS_FINDINGS" = "0" ]; then
|
|
61
|
+
echo " ✅ PASSED - No secrets detected."
|
|
62
|
+
GATE_8_STATUS="PASS"
|
|
63
|
+
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
64
|
+
# Exit 0 but no readable report: we have no evidence the scan happened.
|
|
65
|
+
# Fail closed on the *claim*: report SKIP, never PASS.
|
|
66
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report"
|
|
67
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
68
|
+
GATE_8_STATUS="SKIP"
|
|
69
|
+
else
|
|
70
|
+
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
71
|
+
echo "$GITLEAKS_OUTPUT"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Remediation: remove the secret, or allowlist a false positive in .gitleaks.toml."
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
32
76
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
33
77
|
# Secrets found — output details
|
|
34
78
|
echo "$GITLEAKS_OUTPUT"
|
package/hooks/gate-10.sh
CHANGED
|
@@ -33,15 +33,39 @@ else
|
|
|
33
33
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
34
34
|
GATE_10_STATUS="SKIP"
|
|
35
35
|
else
|
|
36
|
+
# One path per line into an array. Quoting the newline-joined list would hand
|
|
37
|
+
# semgrep ONE argument containing newlines -> "Invalid scanning root" once
|
|
38
|
+
# >=2 files are staged (#490). Same defect class as the Gate 4 --files fix
|
|
39
|
+
# (#457): an array is the only form that keeps spaces intact AND splits files.
|
|
40
|
+
#
|
|
41
|
+
# Each element gets a ./ prefix: a tracked path may legitimately start with
|
|
42
|
+
# '-' (`git add -- ./-o.ts`), and bare in argv that element reads as an option
|
|
43
|
+
# and rewrites the scan it was meant to constrain. ./ is a path to every
|
|
44
|
+
# argument parser.
|
|
45
|
+
#
|
|
46
|
+
# The unquoted heredoc below expands $SEMGREP_FILES once, then feeds literal
|
|
47
|
+
# lines to `read`; bash never rescans the result for separators. Its correct
|
|
48
|
+
# reading rests on one property of the producer above, not of the loop:
|
|
49
|
+
# `git diff --cached --name-only` terminates paths with LF and quotes control
|
|
50
|
+
# characters (a CR in a name arrives as the four bytes \215, never as a bare
|
|
51
|
+
# CR), so one line is exactly one path. Asserted for spaces and a leading '-'
|
|
52
|
+
# by AC-490-04.
|
|
53
|
+
SEMGREP_ARGS=()
|
|
54
|
+
while IFS= read -r _semgrep_file; do
|
|
55
|
+
[ -n "$_semgrep_file" ] && SEMGREP_ARGS+=("./$_semgrep_file")
|
|
56
|
+
done <<EOF
|
|
57
|
+
$SEMGREP_FILES
|
|
58
|
+
EOF
|
|
59
|
+
|
|
36
60
|
# Run semgrep with JSON output
|
|
37
61
|
# --config=p/security-audit: explicit security ruleset
|
|
38
62
|
# --json: machine-readable output
|
|
39
63
|
# --disable-version-check: skip network call
|
|
40
|
-
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "$
|
|
64
|
+
SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1)
|
|
41
65
|
SEMGREP_EXIT=$?
|
|
42
66
|
|
|
43
67
|
if [ "$SEMGREP_EXIT" -eq 0 ]; then
|
|
44
|
-
echo " ✅ PASSED - No security vulnerabilities found."
|
|
68
|
+
echo " ✅ PASSED - No security vulnerabilities found (files scanned: ${#SEMGREP_ARGS[@]})."
|
|
45
69
|
GATE_10_STATUS="PASS"
|
|
46
70
|
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
47
71
|
# Findings detected - parse JSON to categorize
|
|
@@ -96,7 +120,16 @@ console.log(' ['+severity+'] '+ruleId);console.log(' '+path+':'+line+' → '+m
|
|
|
96
120
|
fi
|
|
97
121
|
else
|
|
98
122
|
# semgrep runtime error (timeout, config error, etc.)
|
|
123
|
+
#
|
|
124
|
+
# SEMGREP_OUTPUT is captured above and was never shown here, so an intermittent
|
|
125
|
+
# ruleset-fetch failure looked identical to a broken install: the gate printed
|
|
126
|
+
# "runtime error" and kept the reason to itself (#475). The verdict stays SKIP --
|
|
127
|
+
# an operational failure must not block the commit, and must not be called a pass.
|
|
99
128
|
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
129
|
+
if [ -n "$SEMGREP_OUTPUT" ]; then
|
|
130
|
+
echo " ── what semgrep reported ──"
|
|
131
|
+
printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
|
|
132
|
+
fi
|
|
100
133
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
101
134
|
GATE_10_STATUS="SKIP"
|
|
102
135
|
fi
|
package/hooks/gate-4.sh
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
# Reuses existing principles checker logic
|
|
4
4
|
# ============================================================================
|
|
5
5
|
|
|
6
|
+
# Run a TypeScript entrypoint under tsx without routing arguments through the
|
|
7
|
+
# npx shim: Windows cmd.exe caps command lines at 8191 chars and long staged
|
|
8
|
+
# file lists overflow it, turning a healthy run into a false BLOCK. Prefer the
|
|
9
|
+
# repo-local tsx CLI driven by node directly; keep npx as fallback for consumer
|
|
10
|
+
# repos relying on a global install. Defined here as well so this module also
|
|
11
|
+
# works when sourced outside pre-commit.
|
|
12
|
+
if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
13
|
+
run_tsx() {
|
|
14
|
+
local _root="${PROJECT_ROOT:-$(pwd)}" _timeout=()
|
|
15
|
+
if [ -n "${RUN_TSX_TIMEOUT:-}" ]; then
|
|
16
|
+
_timeout=(timeout "$RUN_TSX_TIMEOUT")
|
|
17
|
+
fi
|
|
18
|
+
if [ -f "$_root/node_modules/tsx/dist/cli.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
19
|
+
"${_timeout[@]}" node "$_root/node_modules/tsx/dist/cli.mjs" "$@"
|
|
20
|
+
else
|
|
21
|
+
"${_timeout[@]}" npx tsx "$@"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
fi
|
|
25
|
+
|
|
6
26
|
2>&1 echo ""
|
|
7
27
|
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
8
28
|
GATE_4_START=$(gate_start_ms)
|
|
@@ -16,29 +36,101 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
16
36
|
else
|
|
17
37
|
# Get source files to check against principles
|
|
18
38
|
PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
|
|
39
|
+
|
|
40
|
+
# One path per line, into an array. Passing $PRINCIPLES_FILES unquoted relies on
|
|
41
|
+
# word splitting to turn the list into separate argv entries, which also splits
|
|
42
|
+
# any path containing a space -- handing the checker two bogus paths and checking
|
|
43
|
+
# neither. Quoting it would instead collapse the list into one argument. An array
|
|
44
|
+
# is the only form that does both. Same defect class as the --config fix (#457).
|
|
45
|
+
PRINCIPLES_ARGS=()
|
|
46
|
+
while IFS= read -r _principles_file; do
|
|
47
|
+
[ -n "$_principles_file" ] && PRINCIPLES_ARGS+=("$_principles_file")
|
|
48
|
+
done <<< "$PRINCIPLES_FILES"
|
|
19
49
|
|
|
20
50
|
if [ -n "$PRINCIPLES_FILES" ]; then
|
|
21
|
-
#
|
|
51
|
+
# The checker resolves every path it is given -- and every path in CHANGED_FILES
|
|
52
|
+
# comes from `git diff`, so it is repo-root relative -- against its own working
|
|
53
|
+
# directory. Resolving against the hook's CWD broke in single-language subdir
|
|
54
|
+
# mode, where pre-commit has already cd'd into $PROJECT_SUBDIR: web/src/a.ts was
|
|
55
|
+
# looked up as web/web/src/a.ts, the probes below missed the checker that was
|
|
56
|
+
# installed one level up, and a working gate degraded to SKIPPED (#478).
|
|
57
|
+
# Everything this gate reads is therefore anchored at the repo root.
|
|
58
|
+
PRINCIPLES_BASE="${PROJECT_ROOT:-$(pwd)}"
|
|
59
|
+
|
|
22
60
|
PRINCIPLES_DIR=""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
for _principles_candidate in \
|
|
62
|
+
"$PRINCIPLES_BASE/.xp-gate/modules/principles" \
|
|
63
|
+
"$PRINCIPLES_BASE/src/principles" \
|
|
64
|
+
"$(pwd)/.xp-gate/modules/principles" \
|
|
65
|
+
"$(pwd)/src/principles" \
|
|
66
|
+
"$HOME/.config/xp-gate/modules/principles"; do
|
|
67
|
+
if [ -f "$_principles_candidate/index.ts" ]; then
|
|
68
|
+
PRINCIPLES_DIR="$_principles_candidate"
|
|
69
|
+
break
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
30
72
|
|
|
31
73
|
if [ -n "$PRINCIPLES_DIR" ]; then
|
|
32
74
|
echo "Checking Clean Code + SOLID principles..."
|
|
33
75
|
|
|
34
|
-
if command -v npx > /dev/null 2>&1; then
|
|
35
|
-
# Run principles checker and store results
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
if command -v npx > /dev/null 2>&1 || [ -f "$PRINCIPLES_BASE/node_modules/tsx/dist/cli.mjs" ]; then
|
|
77
|
+
# Run principles checker and store results.
|
|
78
|
+
#
|
|
79
|
+
# Do NOT wrap this in `if ...; then`: the checker's exit codes are
|
|
80
|
+
# deliberately distinct (0 clean, 1 ERROR-severity findings, >=2 tool
|
|
81
|
+
# failure), and an `if` collapses 1 and 2 into "failed". That sent real
|
|
82
|
+
# error-severity findings down the crash branch, which SKIPs and prints
|
|
83
|
+
# PASSED -- releasing exactly the most serious violations.
|
|
84
|
+
# The trailing `|| PRINCIPLES_EXIT=$?` captures the exit status without
|
|
85
|
+
# letting a non-zero return abort the hook under `set -e`. pre-commit itself
|
|
86
|
+
# runs with only `set -o pipefail` (no `set -e`, pinned by
|
|
87
|
+
# scripts/__tests__/gate-5-runner-error.test.ts AC-454-08), but this module is
|
|
88
|
+
# also sourced by consumer hooks that may set it, and a bare call followed by
|
|
89
|
+
# `PRINCIPLES_EXIT=$?` never reaches the assignment under `set -e`. `|| true`
|
|
90
|
+
# would erase the status -- which is exactly the distinction this gate depends
|
|
91
|
+
# on.
|
|
92
|
+
PRINCIPLES_EXIT=0
|
|
93
|
+
# Keep stderr: it carries the checker's configuration warnings -- a rejected
|
|
94
|
+
# threshold or an out-of-vocabulary severity says so here and nowhere else.
|
|
95
|
+
# Sending it to /dev/null made an exit-2 SKIP undiagnosable and hid every
|
|
96
|
+
# .principlesrc typo (Delphi walkthrough MC-03). Captured to a file so a clean
|
|
97
|
+
# run stays quiet, then replayed only when the checker had a problem.
|
|
98
|
+
#
|
|
99
|
+
# `cd || exit 2`: a base we cannot enter means the checker never ran, which is
|
|
100
|
+
# this module's tool-failure code. Falling through would return the exit status
|
|
101
|
+
# of the failed cd (1), and 1 asserts "I examined files and found errors" -- an
|
|
102
|
+
# empty report at 1 reads as a clean pass.
|
|
103
|
+
#
|
|
104
|
+
# The report is a PER-INVOCATION mktemp, not a shared `/tmp/principles-output.json`:
|
|
105
|
+
# two commits racing on one machine (two worktrees, a hook and a CI job) overwrote
|
|
106
|
+
# each other's report before it was counted, so a run with ERROR findings could
|
|
107
|
+
# read a clean report and PASS (#457). PRINCIPLES_JSON is exported so a caller can
|
|
108
|
+
# point a harness at the same file.
|
|
109
|
+
PRINCIPLES_STDERR=$(mktemp)
|
|
110
|
+
PRINCIPLES_JSON=$(mktemp)
|
|
111
|
+
export PRINCIPLES_JSON
|
|
112
|
+
( cd "$PRINCIPLES_BASE" 2>/dev/null || exit 2
|
|
113
|
+
run_tsx "$PRINCIPLES_DIR/index.ts" --files ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"} --format json ) > "$PRINCIPLES_JSON" 2>"$PRINCIPLES_STDERR" || PRINCIPLES_EXIT=$?
|
|
114
|
+
|
|
115
|
+
if [ -s "$PRINCIPLES_STDERR" ]; then
|
|
116
|
+
sed 's/^/ /' "$PRINCIPLES_STDERR"
|
|
117
|
+
fi
|
|
118
|
+
rm -f "$PRINCIPLES_STDERR"
|
|
119
|
+
|
|
120
|
+
if [ "$PRINCIPLES_EXIT" -ge 2 ]; then
|
|
121
|
+
echo "⚠️ Warning: Principles checker execution failed"
|
|
122
|
+
echo "⏭️ SKIPPED - Principles check (execution issue)"
|
|
123
|
+
GATE_4_STATUS="SKIP"
|
|
124
|
+
rm -f "$PRINCIPLES_JSON"
|
|
125
|
+
else
|
|
126
|
+
# Check severity levels. The reporter emits JSON.stringify(out, null, 2),
|
|
127
|
+
# i.e. `"severity": "warning"` WITH a space; tolerate any whitespace so a
|
|
128
|
+
# future minified format cannot silently zero these counts again (#444).
|
|
129
|
+
ERROR_COUNT=$(grep -cE '"severity"[[:space:]]*:[[:space:]]*"error"' "$PRINCIPLES_JSON" 2>/dev/null || true)
|
|
39
130
|
ERROR_COUNT=${ERROR_COUNT:-0}
|
|
40
|
-
WARNING_COUNT=$(grep -
|
|
131
|
+
WARNING_COUNT=$(grep -cE '"severity"[[:space:]]*:[[:space:]]*"warning"' "$PRINCIPLES_JSON" 2>/dev/null || true)
|
|
41
132
|
WARNING_COUNT=${WARNING_COUNT:-0}
|
|
133
|
+
rm -f "$PRINCIPLES_JSON"
|
|
42
134
|
|
|
43
135
|
if [ "$ERROR_COUNT" -gt 0 ]; then
|
|
44
136
|
echo ""
|
|
@@ -47,7 +139,8 @@ else
|
|
|
47
139
|
echo " - error-handling violations"
|
|
48
140
|
echo " - SOLID principle violations"
|
|
49
141
|
echo " - architectural violations"
|
|
50
|
-
|
|
142
|
+
( cd "$PRINCIPLES_BASE" 2>/dev/null || exit 2
|
|
143
|
+
run_tsx "$PRINCIPLES_DIR/index.ts" --files ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"} --format console )
|
|
51
144
|
GATE_4_STATUS="FAIL"
|
|
52
145
|
exit 1
|
|
53
146
|
fi
|
|
@@ -57,14 +150,10 @@ else
|
|
|
57
150
|
if [ "$WARNING_COUNT" -gt 0 ]; then
|
|
58
151
|
echo "ℹ️ $WARNING_COUNT warnings found (will be handled by Boy Scout Rule)."
|
|
59
152
|
fi
|
|
60
|
-
else
|
|
61
|
-
echo "⚠️ Warning: Principles checker execution failed"
|
|
62
|
-
echo "⏭️ SKIPPED - Principles check (execution issue)"
|
|
63
|
-
GATE_4_STATUS="SKIP"
|
|
64
153
|
fi
|
|
65
154
|
else
|
|
66
|
-
echo "ℹ️ npx not available - skipping principles check"
|
|
67
|
-
echo "⏭️ SKIPPED - Principles check (no Node.js/
|
|
155
|
+
echo "ℹ️ npx/tsx not available - skipping principles check"
|
|
156
|
+
echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
|
|
68
157
|
GATE_4_STATUS="SKIP"
|
|
69
158
|
fi
|
|
70
159
|
else
|
package/hooks/gate-8.sh
CHANGED
|
@@ -19,16 +19,60 @@ fi
|
|
|
19
19
|
if [ -n "$GITLEAKS_CMD" ]; then
|
|
20
20
|
GITLEAKS_CONFIG=""
|
|
21
21
|
if [ -f ".gitleaks.toml" ]; then
|
|
22
|
-
|
|
22
|
+
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
23
|
+
# absolute path in the host's own format when we can derive one (#449).
|
|
24
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
25
|
+
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
26
|
+
else
|
|
27
|
+
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
28
|
+
fi
|
|
23
29
|
fi
|
|
24
30
|
|
|
25
|
-
#
|
|
26
|
-
|
|
31
|
+
# Scan the INDEX, not history. gitleaks 8.x `git --pre-commit` walks committed
|
|
32
|
+
# revisions and reports "0 commits scanned" on a pre-commit run -- so a staged
|
|
33
|
+
# secret was never seen and Gate 8 always passed. `git --staged` scans
|
|
34
|
+
# `git diff --cached`, which is exactly the pre-commit payload (#449).
|
|
35
|
+
GITLEAKS_REPORT="${TMPDIR:-/tmp}/gitleaks-report.json"
|
|
36
|
+
rm -f "$GITLEAKS_REPORT"
|
|
37
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
27
38
|
GITLEAKS_EXIT=$?
|
|
28
39
|
|
|
29
40
|
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
# Decide SKIP from the REPORT ARTIFACT, not from log wording. Parsing stdout
|
|
42
|
+
# meant a gitleaks message change would silently turn a no-evidence run into
|
|
43
|
+
# a PASS -- the same fail-open class this gate just fixed (#449).
|
|
44
|
+
# The report exists and is a JSON array of findings; `[]` == clean scan.
|
|
45
|
+
GITLEAKS_FINDINGS="?"
|
|
46
|
+
if [ -f "$GITLEAKS_REPORT" ]; then
|
|
47
|
+
# A report that is not shaped like a JSON array is no evidence: leave the
|
|
48
|
+
# '?' sentinel so the gate reports SKIP, never a PASS it cannot support
|
|
49
|
+
# (the fail-closed-on-the-claim rule #449 established).
|
|
50
|
+
if grep -q '^\[' "$GITLEAKS_REPORT" 2>/dev/null; then
|
|
51
|
+
# Count occurrences, not matching lines: `grep -c` prints its own zero AND
|
|
52
|
+
# exits 1 on a clean report, so the old `|| echo 0` handed the verdict table
|
|
53
|
+
# $'0\n0' -- no branch matched and a clean scan was BLOCKED as if it leaked
|
|
54
|
+
# (#493). wc -l emits exactly one value, and `|| true` keeps the assignment
|
|
55
|
+
# errexit/pipefail-safe (#489).
|
|
56
|
+
GITLEAKS_FINDINGS=$(grep -o '"RuleID"' "$GITLEAKS_REPORT" 2>/dev/null | wc -l | tr -d '[:space:]') || true
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ "$GITLEAKS_FINDINGS" = "0" ]; then
|
|
61
|
+
echo " ✅ PASSED - No secrets detected."
|
|
62
|
+
GATE_8_STATUS="PASS"
|
|
63
|
+
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
64
|
+
# Exit 0 but no readable report: we have no evidence the scan happened.
|
|
65
|
+
# Fail closed on the *claim*: report SKIP, never PASS.
|
|
66
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report"
|
|
67
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
68
|
+
GATE_8_STATUS="SKIP"
|
|
69
|
+
else
|
|
70
|
+
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
71
|
+
echo "$GITLEAKS_OUTPUT"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "Remediation: remove the secret, or allowlist a false positive in .gitleaks.toml."
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
32
76
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
33
77
|
# Secrets found — output details
|
|
34
78
|
echo "$GITLEAKS_OUTPUT"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# Gate 2 duplicate-code helpers (issue #477)
|
|
4
|
+
#
|
|
5
|
+
# Extracted from pre-commit so the outcome contract is unit-testable without
|
|
6
|
+
# executing the hook, and so an installed hook can source it the same way it
|
|
7
|
+
# sources the other libraries in this directory.
|
|
8
|
+
# ============================================================================
|
|
9
|
+
|
|
10
|
+
# Echo the jscpd config the gate should pass, or nothing when the project has
|
|
11
|
+
# none.
|
|
12
|
+
#
|
|
13
|
+
# pre-commit used to hand jscpd `--config jscpd.conf.json` unconditionally. When
|
|
14
|
+
# that file was not in the working directory jscpd printed
|
|
15
|
+
# "config file jscpd.conf.json: ... (os error 2)" and the gate then read the
|
|
16
|
+
# non-zero exit as "duplicates found" -- a fabricated finding on top of a
|
|
17
|
+
# fabricated error (#477). The probe is anchored at PROJECT_ROOT first because
|
|
18
|
+
# git hands the gate repo-root-relative state while the hook may have cd'd into
|
|
19
|
+
# a language subdirectory.
|
|
20
|
+
resolve_jscpd_config() {
|
|
21
|
+
local _base="${PROJECT_ROOT:-$(pwd)}" _candidate
|
|
22
|
+
for _candidate in "$_base/jscpd.conf.json" "$(pwd)/jscpd.conf.json"; do
|
|
23
|
+
if [ -f "$_candidate" ]; then
|
|
24
|
+
printf '%s\n' "$_candidate"
|
|
25
|
+
return 0
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
return 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Echo exactly one of `clean`, `duplicates`, `error` for a finished jscpd run.
|
|
32
|
+
#
|
|
33
|
+
# $1 -- the tool's exit status
|
|
34
|
+
# $2 -- everything it printed (stdout and stderr together)
|
|
35
|
+
#
|
|
36
|
+
# jscpd's exit status cannot carry this distinction: the default CLI exits 0
|
|
37
|
+
# after a report that contains clones, and exits non-zero for a config or IO
|
|
38
|
+
# failure that produced no report at all. The report is the evidence; the exit
|
|
39
|
+
# status only explains a run that left none.
|
|
40
|
+
#
|
|
41
|
+
# Nothing here blocks or unblocks: Gate 2's severity is unchanged (duplicates
|
|
42
|
+
# stay a warning). This only stops the gate from naming the wrong reason.
|
|
43
|
+
classify_jscpd_run() {
|
|
44
|
+
local _exit="$1" _out="$2" _report _count
|
|
45
|
+
_report=$(printf '%s' "$_out" | grep -oE 'Found [0-9]+ (clones?|duplicates?)' | tail -1)
|
|
46
|
+
if [ -n "$_report" ]; then
|
|
47
|
+
_count=$(printf '%s' "$_report" | sed -E 's/^Found ([0-9]+).*/\1/')
|
|
48
|
+
if [ "$_count" -gt 0 ] 2>/dev/null; then
|
|
49
|
+
printf 'duplicates\n'
|
|
50
|
+
else
|
|
51
|
+
printf 'clean\n'
|
|
52
|
+
fi
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
if [ "$_exit" -ne 0 ] 2>/dev/null; then
|
|
56
|
+
printf 'error\n'
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
# Exit 0 with no report: the run said nothing we can stand behind, but calling
|
|
60
|
+
# it an error would turn every unfamiliar jscpd format into a SKIP. Leave the
|
|
61
|
+
# gate's verdict as it was.
|
|
62
|
+
printf 'clean\n'
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# Gate 11 Sprint Flow reporting helpers (issue #476)
|
|
4
|
+
#
|
|
5
|
+
# Extracted from pre-commit so the resolution chain and the decision translation
|
|
6
|
+
# are unit-testable without executing the hook, and reachable from an installed
|
|
7
|
+
# hook through the lib/ directory install.sh copies beside it.
|
|
8
|
+
# ============================================================================
|
|
9
|
+
|
|
10
|
+
# Echo the first <dir>/sprint-gate.sh that exists among the given directories.
|
|
11
|
+
#
|
|
12
|
+
# The chain used to stop after three tiers that all point at *gate script*
|
|
13
|
+
# directories. The flat global layout ($HOME/.config/xp-gate/adapters) holds
|
|
14
|
+
# gate-3/4/7/8/9/10.sh but not sprint-gate.sh, which update-hooks installs under
|
|
15
|
+
# $HOME/.config/xp-gate/hooks instead -- so a machine with the canonical install
|
|
16
|
+
# reported "sprint-gate.sh not found" about a file that was there (#476).
|
|
17
|
+
#
|
|
18
|
+
# Returns non-zero when nothing matched; the caller must then name every place it
|
|
19
|
+
# looked.
|
|
20
|
+
resolve_sprint_gate_script() {
|
|
21
|
+
local _dir
|
|
22
|
+
for _dir in "$@"; do
|
|
23
|
+
[ -n "$_dir" ] || continue
|
|
24
|
+
if [ -f "$_dir/sprint-gate.sh" ]; then
|
|
25
|
+
printf '%s\n' "$_dir/sprint-gate.sh"
|
|
26
|
+
return 0
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Echo the decision verb from a sprint-gate JSON line: allow, deny, skip, or
|
|
33
|
+
# unknown when the text carries no recognisable decision at all.
|
|
34
|
+
#
|
|
35
|
+
# jq is not a dependency of the hooks, so this reads the field the way it is
|
|
36
|
+
# actually written by sprint-gate.sh: one flat object per line.
|
|
37
|
+
sprint_gate_decision() {
|
|
38
|
+
local _decision
|
|
39
|
+
_decision=$(printf '%s' "${1:-}" | sed -n 's/.*"decision"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | tail -1)
|
|
40
|
+
case "$_decision" in
|
|
41
|
+
allow|deny|skip) printf '%s\n' "$_decision" ;;
|
|
42
|
+
*) printf 'unknown\n' ;;
|
|
43
|
+
esac
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Echo the human-readable detail of a decision (reason, message, or warning --
|
|
47
|
+
# in that order of preference), or nothing when the gate gave none.
|
|
48
|
+
sprint_gate_detail() {
|
|
49
|
+
local _out="${1:-}" _field _detail
|
|
50
|
+
for _field in reason message warning; do
|
|
51
|
+
_detail=$(printf '%s' "$_out" | sed -n "s/.*\"$_field\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | tail -1)
|
|
52
|
+
if [ -n "$_detail" ]; then
|
|
53
|
+
printf '%s\n' "$_detail"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Gate 5 exit-code judgement for vitest runs, shared by every pre-commit branch
|
|
3
|
+
# that invokes vitest. Sourced by githooks/pre-commit; kept in its own file so
|
|
4
|
+
# the contract is unit-testable without executing the hook
|
|
5
|
+
# (githooks/__tests__/gate-5-runner-error.test.bats, gate-5-partial-threshold.test.bats).
|
|
6
|
+
|
|
7
|
+
# The one condition every excuse below shares: a real failure always wins.
|
|
8
|
+
#
|
|
9
|
+
# Kept as a single expression because a marker added for one exit class and
|
|
10
|
+
# forgotten in another re-opens the false BLOCK the excuse exists to close.
|
|
11
|
+
has_real_failure_marker() {
|
|
12
|
+
printf '%s' "$1" | grep -qE 'Tests +[0-9]+ failed|FAIL |AssertionError'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Detect a vitest *runner infrastructure* failure as opposed to a real test
|
|
16
|
+
# failure. On Windows, vitest 1.6.x can exit non-zero while reporting zero failed
|
|
17
|
+
# tests, because its temp/ssr module cache hits EPERM (#454). Treating that as
|
|
18
|
+
# "Tests FAILED" sends developers chasing a bug that does not exist.
|
|
19
|
+
#
|
|
20
|
+
# The two patterns below are vitest's DEFAULT-REPORTER shape for the assumed major
|
|
21
|
+
# (declared as `vitest: ^1.6.1` in package.json). They are not universal: a reporter
|
|
22
|
+
# change or a vitest 2 upgrade could stop emitting "Unhandled Error", and the
|
|
23
|
+
# detection would silently revert to false BLOCKs. That assumption is therefore
|
|
24
|
+
# asserted, not assumed -- scripts/__tests__/gate-5-runner-error.test.ts
|
|
25
|
+
# (AC-454-07) fails the moment the declared version leaves the pinned range, which
|
|
26
|
+
# is the signal to re-verify these patterns against the new output shape.
|
|
27
|
+
#
|
|
28
|
+
# Returns 0 (true) only when both hold:
|
|
29
|
+
# - the output mentions "Unhandled Error" (vitest's runner-level failure), AND
|
|
30
|
+
# - no real test failure marker is present.
|
|
31
|
+
is_runner_infrastructure_error() {
|
|
32
|
+
local _out="$1"
|
|
33
|
+
[ -n "$_out" ] || return 1
|
|
34
|
+
printf '%s' "$_out" | grep -qE 'Unhandled Error' || return 1
|
|
35
|
+
has_real_failure_marker "$_out" && return 1
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Detect the partial-run coverage-threshold exit (#473).
|
|
40
|
+
#
|
|
41
|
+
# Gate 5's "changed test files" and "related test files" branches run
|
|
42
|
+
# `npx vitest run --coverage <a few files>`. vitest applies the project's GLOBAL
|
|
43
|
+
# coverage thresholds to whatever subset it ran, so a subset in which every test
|
|
44
|
+
# passed still exits 1 with
|
|
45
|
+
# ERROR: Coverage for lines (10.87%) does not meet global threshold (80%)
|
|
46
|
+
# Reading that as "Tests FAILED" false-blocked every partial run on any machine
|
|
47
|
+
# with node installed.
|
|
48
|
+
#
|
|
49
|
+
# Scoped to PARTIAL_TEST_RUN=true on purpose: a full-suite threshold miss is a
|
|
50
|
+
# genuine coverage failure and must still block.
|
|
51
|
+
#
|
|
52
|
+
# Returns 0 only when all three hold:
|
|
53
|
+
# - this was a partial run,
|
|
54
|
+
# - the output carries the threshold complaint, AND
|
|
55
|
+
# - no real test failure marker is present.
|
|
56
|
+
is_partial_threshold_only_exit() {
|
|
57
|
+
local _out="$1"
|
|
58
|
+
[ "${PARTIAL_TEST_RUN:-false}" = "true" ] || return 1
|
|
59
|
+
[ -n "$_out" ] || return 1
|
|
60
|
+
printf '%s' "$_out" | grep -qE 'does not meet global threshold' || return 1
|
|
61
|
+
# A real failure must still block.
|
|
62
|
+
has_real_failure_marker "$_out" && return 1
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Detect the third non-failure exit: vitest found nothing to run at all (#498).
|
|
67
|
+
#
|
|
68
|
+
# A TypeScript project with no test files, and a partial run whose every target
|
|
69
|
+
# falls outside the suite it is being run from, both make vitest print
|
|
70
|
+
# No test files found, exiting with code 1
|
|
71
|
+
# and exit non-zero. No test failed; none ran. Reporting that as "Tests FAILED"
|
|
72
|
+
# blocked every commit in such a repo, including the fixtures that were written
|
|
73
|
+
# to prove Gate 5a blocks a *missing test* (githooks/__tests__/gate-5a-block.test.bats
|
|
74
|
+
# died in setup, before any assertion of its own was reached).
|
|
75
|
+
#
|
|
76
|
+
# Matched as vitest's whole sentence including its exit-code clause rather than
|
|
77
|
+
# as the bare phrase: a test that logs the phrase from its own title describes a
|
|
78
|
+
# run that did collect files, and must not be excused.
|
|
79
|
+
#
|
|
80
|
+
# Returns 0 only when both hold:
|
|
81
|
+
# - the output carries vitest's no-test-files sentence, AND
|
|
82
|
+
# - no real test failure marker is present.
|
|
83
|
+
is_no_test_files_found() {
|
|
84
|
+
local _out="$1"
|
|
85
|
+
[ -n "$_out" ] || return 1
|
|
86
|
+
printf '%s' "$_out" | grep -qE 'No test files found, exiting with code [0-9]+' || return 1
|
|
87
|
+
has_real_failure_marker "$_out" && return 1
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Judge a non-zero vitest exit and report the outcome (Issues #454, #473, #498).
|
|
92
|
+
#
|
|
93
|
+
# Before this helper existed, each Gate 5 branch carried its own
|
|
94
|
+
# `echo "❌ BLOCKED - Tests FAILED"`, and only ONE of them consulted
|
|
95
|
+
# is_runner_infrastructure_error. The others therefore reported a Windows
|
|
96
|
+
# EPERM-on-temp-cache runner error as a genuine test failure, sending
|
|
97
|
+
# developers hunting for a bug that did not exist. Routing every vitest branch
|
|
98
|
+
# through one helper makes that class of drift structurally impossible.
|
|
99
|
+
#
|
|
100
|
+
# Usage: handle_test_failure <output> <site-context>
|
|
101
|
+
# $1 = captured test output (passed EXPLICITLY -- never read a global, so a
|
|
102
|
+
# branch that forgets to set one fails loudly instead of silently
|
|
103
|
+
# treating empty output as "no runner error")
|
|
104
|
+
# $2 = short context label used in the BLOCK message; may be empty
|
|
105
|
+
# Returns 0 when the run should be treated as SKIPPED (a runner infrastructure
|
|
106
|
+
# error, a partial run that only missed the global coverage threshold, or a run
|
|
107
|
+
# that matched no test files at all), 1 when it must BLOCK. Callers use
|
|
108
|
+
# `|| exit 1`.
|
|
109
|
+
#
|
|
110
|
+
# NOTE: only vitest branches may call this. The generic `run_tests` fallbacks
|
|
111
|
+
# (non-TS adapters) are deliberately left unguarded: their output format is not
|
|
112
|
+
# guaranteed to look like vitest's, so guarding them could let a real failure
|
|
113
|
+
# through.
|
|
114
|
+
handle_test_failure() {
|
|
115
|
+
local _out="$1"
|
|
116
|
+
local _ctx="$2"
|
|
117
|
+
if is_runner_infrastructure_error "$_out"; then
|
|
118
|
+
echo ""
|
|
119
|
+
echo "⏭️ SKIPPED - vitest runner infrastructure error (no test failed)"
|
|
120
|
+
echo " Detected: Unhandled Errors with 0 failed tests."
|
|
121
|
+
echo " Known Windows issue: EPERM on vitest temp/ssr cache (#454)."
|
|
122
|
+
echo " Workaround: npx vitest run --no-file-parallelism <targets>"
|
|
123
|
+
TESTS_SKIPPED=true
|
|
124
|
+
TESTS_EXIT_CODE=0
|
|
125
|
+
return 0
|
|
126
|
+
fi
|
|
127
|
+
if is_partial_threshold_only_exit "$_out"; then
|
|
128
|
+
echo ""
|
|
129
|
+
echo "⏭️ SKIPPED - coverage threshold judgement for this partial run (no test failed)"
|
|
130
|
+
echo " Detected: 'does not meet global threshold' with 0 failed tests."
|
|
131
|
+
echo " vitest scores the whole project against a subset of tests (#473)."
|
|
132
|
+
echo " Full-suite coverage is still enforced by the full run and by CI."
|
|
133
|
+
TESTS_EXIT_CODE=0
|
|
134
|
+
# TESTS_SKIPPED is deliberately NOT set here, unlike the runner-error path:
|
|
135
|
+
# this run produced real coverage data, and the coverage section downstream
|
|
136
|
+
# carries the new-file coverage block, which must stay live.
|
|
137
|
+
return 0
|
|
138
|
+
fi
|
|
139
|
+
if is_no_test_files_found "$_out"; then
|
|
140
|
+
echo ""
|
|
141
|
+
echo "⏭️ SKIPPED - no test files matched, so Gate 5 ran zero tests"
|
|
142
|
+
echo " Detected: 'No test files found, exiting with code N' with 0 failed tests."
|
|
143
|
+
echo " This is not a pass: nothing executed (#498)."
|
|
144
|
+
echo " Test presence is enforced by Gate 5a and by CI, not by this exit."
|
|
145
|
+
TESTS_SKIPPED=true
|
|
146
|
+
TESTS_EXIT_CODE=0
|
|
147
|
+
return 0
|
|
148
|
+
fi
|
|
149
|
+
echo ""
|
|
150
|
+
echo "❌ BLOCKED - Tests FAILED${_ctx:+ in ${_ctx}}"
|
|
151
|
+
return 1
|
|
152
|
+
}
|