@boyingliu01/xp-gate 0.35.16 → 0.35.18
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/gate-12-file-hygiene.sh +26 -4
- package/gate-13-compiler-strictness.sh +12 -0
- package/gates/gate-3.ts +77 -21
- package/gates/gate-8.ts +39 -26
- package/gates/gate-9.ts +4 -4
- package/hooks/gate-12-file-hygiene.sh +26 -4
- package/hooks/gate-13-compiler-strictness.sh +12 -0
- package/hooks/pre-commit +82 -12
- package/hooks/pre-push +11 -2
- package/lib/__tests__/doctor.test.js +9 -0
- package/lib/__tests__/gate-numbering.test.js +52 -0
- package/lib/__tests__/gate-runner.test.js +59 -14
- package/lib/__tests__/shared-utils.test.js +47 -0
- package/lib/gate-runner.js +98 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-utils.js +27 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/mutation/runners/mutmut-runner.ts +165 -70
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/src/delphi-findings.test.ts +32 -6
- package/plugins/dsh/src/exit-code.test.ts +6 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/adapters/import-clause.ts +57 -0
- package/principles/adapters/member-scan.ts +73 -0
- package/principles/adapters/position-analysis.ts +43 -0
- package/principles/adapters/quote-state.ts +80 -0
- package/principles/adapters/typescript.ts +57 -116
- package/principles/rules/clean-code/god-class.ts +37 -14
- package/principles/rules/solid/dip.ts +15 -2
- package/scripts/__tests__/sync-package-content.test.js +114 -46
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/skills/test-specification-alignment/AGENTS.md +2 -2
package/gate-12-file-hygiene.sh
CHANGED
|
@@ -36,13 +36,35 @@ fi
|
|
|
36
36
|
HYGIENE_ISSUES=0
|
|
37
37
|
HYGIENE_FILES=""
|
|
38
38
|
|
|
39
|
+
# Is this staged file binary (so line-based checks do not apply)?
|
|
40
|
+
#
|
|
41
|
+
# This replaces `grep -q $'\x00'`, which was a no-op guard: bash strips NUL from
|
|
42
|
+
# `$'\x00'`, so the pattern was the EMPTY string and `grep -q ""` matches any
|
|
43
|
+
# non-empty input. Every staged file was therefore judged binary and `continue`d,
|
|
44
|
+
# silently disabling Check 1 (trailing whitespace), Check 2 (missing EOF newline)
|
|
45
|
+
# and -- worst -- Check 3 (merge conflict markers, a BLOCK-level check). Checks 4
|
|
46
|
+
# and 5 lacked the guard, which is why the gate looked half-working.
|
|
47
|
+
#
|
|
48
|
+
# Ask git instead of guessing: `--numstat` reports a binary entry as `-\t-\t<path>`
|
|
49
|
+
# because git already made that determination. A byte-level NUL test cannot be
|
|
50
|
+
# written with a shell pattern at all (variables and command substitution both drop
|
|
51
|
+
# NUL), and `grep -I` only suppresses output on a binary input -- it does not make
|
|
52
|
+
# `grep -q` fail, which is why that shortcut still reported text.
|
|
53
|
+
is_binary_staged_file() {
|
|
54
|
+
local _file="$1"
|
|
55
|
+
case "$(git diff --cached --numstat -- "$_file" 2>/dev/null)" in
|
|
56
|
+
-[[:space:]]-*) return 0 ;; # binary: git could not count lines
|
|
57
|
+
*) return 1 ;; # text (or empty -- nothing for the line checks)
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
# --- Check 1: Trailing whitespace ---
|
|
40
62
|
TRAILING_WS_FILES=""
|
|
41
63
|
if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
42
64
|
while IFS= read -r file; do
|
|
43
65
|
[ -z "$file" ] && continue
|
|
44
|
-
# Skip binary files
|
|
45
|
-
if
|
|
66
|
+
# Skip binary files : they have no lines to check
|
|
67
|
+
if is_binary_staged_file "$file"; then
|
|
46
68
|
continue
|
|
47
69
|
fi
|
|
48
70
|
# Check staged content for trailing whitespace (spaces/tabs at end of lines)
|
|
@@ -69,7 +91,7 @@ if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
|
69
91
|
while IFS= read -r file; do
|
|
70
92
|
[ -z "$file" ] && continue
|
|
71
93
|
# Skip binary files
|
|
72
|
-
if
|
|
94
|
+
if is_binary_staged_file "$file"; then
|
|
73
95
|
continue
|
|
74
96
|
fi
|
|
75
97
|
# Check if file content ends with newline
|
|
@@ -108,7 +130,7 @@ else
|
|
|
108
130
|
while IFS= read -r file; do
|
|
109
131
|
[ -z "$file" ] && continue
|
|
110
132
|
# Skip binary files
|
|
111
|
-
if
|
|
133
|
+
if is_binary_staged_file "$file"; then
|
|
112
134
|
continue
|
|
113
135
|
fi
|
|
114
136
|
# Check for conflict markers in staged content
|
|
@@ -527,6 +527,18 @@ gate13_count_lint_findings() {
|
|
|
527
527
|
[ -n "$src" ] || src=$(cat)
|
|
528
528
|
printf '%s\n' "$src" | awk '
|
|
529
529
|
BEGIN { n = 0 }
|
|
530
|
+
# (#673) Maven 3.9.7+ reactor summary appends the packaging type per module:
|
|
531
|
+
# [INFO] whaleaudit-core [jar]
|
|
532
|
+
# `[pom]`/`[jar]` match the open-category rule below, so a 3-module reactor
|
|
533
|
+
# reported 4 javac findings on code with ZERO warnings -- and got worse with
|
|
534
|
+
# every module added. The marker set is closed (packaging types are an enum in
|
|
535
|
+
# Maven itself), so excluding it does not reopen the #659 fail-open hole that
|
|
536
|
+
# the open-category rule was introduced to close.
|
|
537
|
+
#
|
|
538
|
+
# Scoped to `[INFO]` lines: a javac diagnostic is never prefixed with the
|
|
539
|
+
# Maven [INFO] banner, so this cannot mask a real finding even when a category
|
|
540
|
+
# name collides with a packaging type.
|
|
541
|
+
/^\[INFO\].*\[(pom|jar|war|ear|rar|ejb|maven-plugin|par|test-jar|java-source|javadoc|zip|tar\.gz|tar\.bz2)\][[:space:]]*$/ { next }
|
|
530
542
|
# Any bracketed lowercase category: covers the whole javac -Xlint enum and any
|
|
531
543
|
# later addition to it. Restricted to lowercase/digit/hyphen so a Maven
|
|
532
544
|
# `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
|
package/gates/gate-3.ts
CHANGED
|
@@ -53,29 +53,85 @@ export function runGate3(input: GateInput): GateOutput {
|
|
|
53
53
|
|
|
54
54
|
messages.push('Checking complexity for source files...');
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
// CSV mode: the verdict comes from the numbers lizard uses, not its table
|
|
57
|
+
// formatting (#674). See countCcnWarnings() for why the old parser never matched.
|
|
58
|
+
const rawOutput = runLizardCsv(lizardCmd, lizardArgs, ccFiles, input.cwd);
|
|
59
|
+
const verdict = buildComplexityVerdict(countCcnWarnings(rawOutput), rawOutput);
|
|
60
|
+
messages.push(...verdict.messages);
|
|
61
|
+
recordAudit('gate-3', 'complexity', verdict.status, verdict.warnings, startMs);
|
|
62
|
+
return { status: verdict.status, messages, exitCode: verdict.exitCode };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run lizard in CSV mode and return its raw output (stdout + stderr). */
|
|
66
|
+
function runLizardCsv(cmd: string, baseArgs: string[], files: string[], cwd: string | undefined): string {
|
|
67
|
+
const result = runTool(cmd, [...baseArgs, '--csv', '-C', String(CCN_THRESHOLD), ...files], { cwd });
|
|
68
|
+
return result.stdout + result.stderr;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Turn a CCN count into the gate's verdict and messages.
|
|
73
|
+
*
|
|
74
|
+
* Split from runGate3 so the three-way outcome (WARN on unparsable output, FAIL
|
|
75
|
+
* on violations, PASS) is visible in one place and testable without driving
|
|
76
|
+
* lizard. Notably `!analysed` must NOT fall through to PASS: a lizard that
|
|
77
|
+
* produced no parsable row would otherwise report success on zero analysed
|
|
78
|
+
* functions -- the same silent-success failure as #674.
|
|
79
|
+
*/
|
|
80
|
+
export function buildComplexityVerdict(
|
|
81
|
+
counts: { warnings: number; analysed: boolean },
|
|
82
|
+
rawOutput: string
|
|
83
|
+
): { status: 'PASS' | 'FAIL' | 'WARN'; messages: string[]; exitCode: number; warnings: number } {
|
|
84
|
+
if (!counts.analysed) {
|
|
85
|
+
return {
|
|
86
|
+
status: 'WARN',
|
|
87
|
+
messages: ['⚠️ WARN - lizard produced no parsable results; complexity not verified'],
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
warnings: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (counts.warnings > 0) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'FAIL',
|
|
95
|
+
messages: [
|
|
96
|
+
rawOutput,
|
|
97
|
+
'',
|
|
98
|
+
`❌ BLOCKED - ${counts.warnings} functions with CCN > ${CCN_THRESHOLD} found.`,
|
|
99
|
+
`Refactor high-complexity functions to keep below ${CCN_THRESHOLD} complexity.`,
|
|
100
|
+
],
|
|
101
|
+
exitCode: 1,
|
|
102
|
+
warnings: counts.warnings,
|
|
103
|
+
};
|
|
74
104
|
}
|
|
105
|
+
return {
|
|
106
|
+
status: 'PASS',
|
|
107
|
+
messages: [`✅ PASSED - All functions within complexity threshold (${CCN_THRESHOLD}).`],
|
|
108
|
+
exitCode: 0,
|
|
109
|
+
warnings: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
75
112
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Count functions whose CCN exceeds the threshold from lizard's --csv output.
|
|
115
|
+
*
|
|
116
|
+
* Split out so the parse can be tested against literal lizard output instead of
|
|
117
|
+
* by driving the tool: a parser bug here is invisible in exactly the same way
|
|
118
|
+
* (#674) -- it returns "clean" and the gate reports PASS.
|
|
119
|
+
*
|
|
120
|
+
* `analysed` distinguishes "lizard found nothing over the threshold" from
|
|
121
|
+
* "lizard produced nothing at all". Only the first is a pass.
|
|
122
|
+
*/
|
|
123
|
+
export function countCcnWarnings(csv: string): { warnings: number; analysed: boolean } {
|
|
124
|
+
let warnings = 0;
|
|
125
|
+
let analysed = false;
|
|
126
|
+
for (const line of csv.split('\n')) {
|
|
127
|
+
const columns = line.split(',');
|
|
128
|
+
if (columns.length < 2) continue;
|
|
129
|
+
// Location column carries ":name" style text, so only trust numeric CCN.
|
|
130
|
+
if (!/^\s*\d+\s*$/.test(columns[1])) continue;
|
|
131
|
+
analysed = true;
|
|
132
|
+
if (parseInt(columns[1], 10) > CCN_THRESHOLD) warnings++;
|
|
133
|
+
}
|
|
134
|
+
return { warnings, analysed };
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
// ── CLI Entry Point ──
|
package/gates/gate-8.ts
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Gate 8: Secret Scanning (gitleaks)
|
|
3
3
|
* Detects secrets (API keys, passwords, tokens) in staged files.
|
|
4
4
|
* TypeScript rewrite of githooks/adapters/gate-8.sh for cross-platform Node.js CLI.
|
|
5
|
+
*
|
|
6
|
+
* Scans the INDEX (`git --staged`), not committed history (#675, mirroring the
|
|
7
|
+
* adapter fix in #449). gitleaks `git --pre-commit` walks committed revisions; on a
|
|
8
|
+
* pre-commit run it reports "0 commits scanned" and exits 0. A staged secret
|
|
9
|
+
* therefore passed the CLI while the hook blocked it -- and the CLI is the path CI
|
|
10
|
+
* and the plugins use. `--staged` scans `git diff --cached`, exactly the
|
|
11
|
+
* pre-commit payload. Same tool, same payload, one word different, opposite verdict.
|
|
5
12
|
*/
|
|
6
13
|
import path from 'path';
|
|
7
14
|
import fs from 'fs';
|
|
@@ -29,20 +36,7 @@ export function runGate8(input: GateInput): GateOutput {
|
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
const gitleaksCmd = tool.via === 'npx' ? 'npx' : tool.path;
|
|
32
|
-
const gitleaksArgs = tool.via === 'npx'
|
|
33
|
-
|
|
34
|
-
// Config detection
|
|
35
|
-
const configPath = findGitleaksConfig(cwd);
|
|
36
|
-
if (configPath) {
|
|
37
|
-
gitleaksArgs.push(`--config=${configPath}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Cross-platform temp path (fixes /tmp issue on Windows)
|
|
41
|
-
const reportPath = path.join(getTempDir(), 'gitleaks-report.json');
|
|
42
|
-
|
|
43
|
-
// Run gitleaks on staged changes
|
|
44
|
-
gitleaksArgs.push('git', '--pre-commit', '--redact', '--no-banner',
|
|
45
|
-
'--report-format=json', `--report-path=${reportPath}`);
|
|
39
|
+
const gitleaksArgs = buildGitleaksArgs(tool.via === 'npx', findGitleaksConfig(cwd));
|
|
46
40
|
|
|
47
41
|
const result = runTool(gitleaksCmd, gitleaksArgs, { cwd });
|
|
48
42
|
|
|
@@ -53,33 +47,52 @@ export function runGate8(input: GateInput): GateOutput {
|
|
|
53
47
|
}
|
|
54
48
|
|
|
55
49
|
if (result.exitCode === 1) {
|
|
56
|
-
|
|
57
|
-
messages.push(result.stdout);
|
|
58
|
-
messages.push('');
|
|
59
|
-
messages.push('❌ BLOCKED - Secrets detected in staged files.');
|
|
60
|
-
messages.push('');
|
|
61
|
-
messages.push('Remediation options:');
|
|
62
|
-
messages.push(' 1. Remove the secret and use environment variables instead');
|
|
63
|
-
messages.push(' 2. Add a false positive to .gitleaks.toml allowlist');
|
|
64
|
-
messages.push(' 3. Use git secret or vault for sensitive data');
|
|
65
|
-
messages.push('');
|
|
66
|
-
messages.push('See: https://github.com/gitleaks/gitleaks');
|
|
50
|
+
messages.push(...secretsFoundMessages(result.stdout));
|
|
67
51
|
recordAudit('gate-8', 'secret-scanning', 'FAIL', 1, startMs);
|
|
68
52
|
return { status: 'FAIL', messages, exitCode: 1 };
|
|
69
53
|
}
|
|
70
54
|
|
|
71
|
-
// Runtime error
|
|
72
55
|
messages.push(` ⚠️ gitleaks exited with code ${result.exitCode} - skipping gate`);
|
|
73
56
|
messages.push(' ✅ Secret Scanning (SKIP, gitleaks error)');
|
|
74
57
|
recordAudit('gate-8', 'secret-scanning', 'SKIP', result.exitCode, startMs);
|
|
75
58
|
return { status: 'SKIP', messages, exitCode: 0 };
|
|
76
59
|
}
|
|
77
60
|
|
|
61
|
+
/** The report + remediation shown when gitleaks finds secrets (exit code 1). */
|
|
62
|
+
function secretsFoundMessages(stdout: string): string[] {
|
|
63
|
+
return [
|
|
64
|
+
stdout,
|
|
65
|
+
'',
|
|
66
|
+
'❌ BLOCKED - Secrets detected in staged files.',
|
|
67
|
+
'',
|
|
68
|
+
'Remediation options:',
|
|
69
|
+
' 1. Remove the secret and use environment variables instead',
|
|
70
|
+
' 2. Add a false positive to .gitleaks.toml allowlist',
|
|
71
|
+
' 3. Use git secret or vault for sensitive data',
|
|
72
|
+
'',
|
|
73
|
+
'See: https://github.com/gitleaks/gitleaks',
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
78
77
|
function findGitleaksConfig(cwd: string): string | null {
|
|
79
78
|
const configPath = path.join(cwd, '.gitleaks.toml');
|
|
80
79
|
return fs.existsSync(configPath) ? configPath : null;
|
|
81
80
|
}
|
|
82
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Assemble the gitleaks argument vector.
|
|
84
|
+
*
|
|
85
|
+
* Report goes to a cross-platform temp path (the old hardcoded `/tmp` broke on
|
|
86
|
+
* Windows), and the scan reads the INDEX -- see the file header for why (#675).
|
|
87
|
+
*/
|
|
88
|
+
function buildGitleaksArgs(viaNpx: boolean, configPath: string | null): string[] {
|
|
89
|
+
const args = viaNpx ? ['-y', 'gitleaks'] : [];
|
|
90
|
+
if (configPath) args.push(`--config=${configPath}`);
|
|
91
|
+
args.push('git', '--staged', '--redact', '--no-banner',
|
|
92
|
+
'--report-format=json', `--report-path=${path.join(getTempDir(), 'gitleaks-report.json')}`);
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
|
|
83
96
|
// ── CLI Entry Point ──
|
|
84
97
|
if (process.argv.includes('--run')) {
|
|
85
98
|
const cwdIdx = process.argv.indexOf('--cwd');
|
package/gates/gate-9.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate
|
|
2
|
+
* Gate 10: Semgrep SAST Security Scan (file name predates the #677 renumber)
|
|
3
3
|
* Detects security vulnerabilities in staged files.
|
|
4
4
|
* For PowerShell projects: uses PSScriptAnalyzer security rules as alternative.
|
|
5
5
|
* TypeScript rewrite of githooks/adapters/gate-9.sh for cross-platform Node.js CLI.
|
|
@@ -36,7 +36,7 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
36
36
|
const messages: string[] = [];
|
|
37
37
|
|
|
38
38
|
messages.push('');
|
|
39
|
-
messages.push('→ Gate
|
|
39
|
+
messages.push('→ Gate 10: Semgrep SAST Security Scan...');
|
|
40
40
|
|
|
41
41
|
// PowerShell project: use PSScriptAnalyzer security rules
|
|
42
42
|
if (isPowerShellProject(input.projectLang)) {
|
|
@@ -49,7 +49,7 @@ export function runGate9(input: GateInput): GateOutput {
|
|
|
49
49
|
messages.push(' ⚠️ WARN - semgrep not installed — SAST scanning unavailable');
|
|
50
50
|
messages.push(' Install: brew install semgrep (macOS) | pip install semgrep (Linux/Windows)');
|
|
51
51
|
messages.push(' Pre-cache rules: semgrep --config=' + SEMGREP_RULESET);
|
|
52
|
-
messages.push(' Gate
|
|
52
|
+
messages.push(' Gate 10: SAST Security (WARN, semgrep not installed)');
|
|
53
53
|
recordAudit('gate-9', 'sast-security', 'WARN', 0, startMs);
|
|
54
54
|
return { status: 'WARN', messages, exitCode: 0 };
|
|
55
55
|
}
|
|
@@ -242,7 +242,7 @@ function runPowerShellSAST(
|
|
|
242
242
|
const ps = detectPowerShell();
|
|
243
243
|
if (!ps.available) {
|
|
244
244
|
messages.push(' ⚠️ WARN - PowerShell not available — SAST scanning unavailable');
|
|
245
|
-
messages.push(' Gate
|
|
245
|
+
messages.push(' Gate 10: SAST Security (WARN, PowerShell not installed)');
|
|
246
246
|
recordAudit('gate-9', 'sast-security', 'WARN', 0, startMs);
|
|
247
247
|
return { status: 'WARN', messages, exitCode: 0 };
|
|
248
248
|
}
|
|
@@ -36,13 +36,35 @@ fi
|
|
|
36
36
|
HYGIENE_ISSUES=0
|
|
37
37
|
HYGIENE_FILES=""
|
|
38
38
|
|
|
39
|
+
# Is this staged file binary (so line-based checks do not apply)?
|
|
40
|
+
#
|
|
41
|
+
# This replaces `grep -q $'\x00'`, which was a no-op guard: bash strips NUL from
|
|
42
|
+
# `$'\x00'`, so the pattern was the EMPTY string and `grep -q ""` matches any
|
|
43
|
+
# non-empty input. Every staged file was therefore judged binary and `continue`d,
|
|
44
|
+
# silently disabling Check 1 (trailing whitespace), Check 2 (missing EOF newline)
|
|
45
|
+
# and -- worst -- Check 3 (merge conflict markers, a BLOCK-level check). Checks 4
|
|
46
|
+
# and 5 lacked the guard, which is why the gate looked half-working.
|
|
47
|
+
#
|
|
48
|
+
# Ask git instead of guessing: `--numstat` reports a binary entry as `-\t-\t<path>`
|
|
49
|
+
# because git already made that determination. A byte-level NUL test cannot be
|
|
50
|
+
# written with a shell pattern at all (variables and command substitution both drop
|
|
51
|
+
# NUL), and `grep -I` only suppresses output on a binary input -- it does not make
|
|
52
|
+
# `grep -q` fail, which is why that shortcut still reported text.
|
|
53
|
+
is_binary_staged_file() {
|
|
54
|
+
local _file="$1"
|
|
55
|
+
case "$(git diff --cached --numstat -- "$_file" 2>/dev/null)" in
|
|
56
|
+
-[[:space:]]-*) return 0 ;; # binary: git could not count lines
|
|
57
|
+
*) return 1 ;; # text (or empty -- nothing for the line checks)
|
|
58
|
+
esac
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
# --- Check 1: Trailing whitespace ---
|
|
40
62
|
TRAILING_WS_FILES=""
|
|
41
63
|
if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
42
64
|
while IFS= read -r file; do
|
|
43
65
|
[ -z "$file" ] && continue
|
|
44
|
-
# Skip binary files
|
|
45
|
-
if
|
|
66
|
+
# Skip binary files : they have no lines to check
|
|
67
|
+
if is_binary_staged_file "$file"; then
|
|
46
68
|
continue
|
|
47
69
|
fi
|
|
48
70
|
# Check staged content for trailing whitespace (spaces/tabs at end of lines)
|
|
@@ -69,7 +91,7 @@ if [ "${GATE_12_DOCS_ONLY:-false}" != true ]; then
|
|
|
69
91
|
while IFS= read -r file; do
|
|
70
92
|
[ -z "$file" ] && continue
|
|
71
93
|
# Skip binary files
|
|
72
|
-
if
|
|
94
|
+
if is_binary_staged_file "$file"; then
|
|
73
95
|
continue
|
|
74
96
|
fi
|
|
75
97
|
# Check if file content ends with newline
|
|
@@ -108,7 +130,7 @@ else
|
|
|
108
130
|
while IFS= read -r file; do
|
|
109
131
|
[ -z "$file" ] && continue
|
|
110
132
|
# Skip binary files
|
|
111
|
-
if
|
|
133
|
+
if is_binary_staged_file "$file"; then
|
|
112
134
|
continue
|
|
113
135
|
fi
|
|
114
136
|
# Check for conflict markers in staged content
|
|
@@ -527,6 +527,18 @@ gate13_count_lint_findings() {
|
|
|
527
527
|
[ -n "$src" ] || src=$(cat)
|
|
528
528
|
printf '%s\n' "$src" | awk '
|
|
529
529
|
BEGIN { n = 0 }
|
|
530
|
+
# (#673) Maven 3.9.7+ reactor summary appends the packaging type per module:
|
|
531
|
+
# [INFO] whaleaudit-core [jar]
|
|
532
|
+
# `[pom]`/`[jar]` match the open-category rule below, so a 3-module reactor
|
|
533
|
+
# reported 4 javac findings on code with ZERO warnings -- and got worse with
|
|
534
|
+
# every module added. The marker set is closed (packaging types are an enum in
|
|
535
|
+
# Maven itself), so excluding it does not reopen the #659 fail-open hole that
|
|
536
|
+
# the open-category rule was introduced to close.
|
|
537
|
+
#
|
|
538
|
+
# Scoped to `[INFO]` lines: a javac diagnostic is never prefixed with the
|
|
539
|
+
# Maven [INFO] banner, so this cannot mask a real finding even when a category
|
|
540
|
+
# name collides with a packaging type.
|
|
541
|
+
/^\[INFO\].*\[(pom|jar|war|ear|rar|ejb|maven-plugin|par|test-jar|java-source|javadoc|zip|tar\.gz|tar\.bz2)\][[:space:]]*$/ { next }
|
|
530
542
|
# Any bracketed lowercase category: covers the whole javac -Xlint enum and any
|
|
531
543
|
# later addition to it. Restricted to lowercase/digit/hyphen so a Maven
|
|
532
544
|
# `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
|
package/hooks/pre-commit
CHANGED
|
@@ -319,6 +319,32 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
|
319
319
|
}
|
|
320
320
|
fi
|
|
321
321
|
|
|
322
|
+
# Run a repo-local Node CLI (eslint, vitest, biome, ...) without npx when the
|
|
323
|
+
# binary is already in this repo's node_modules.
|
|
324
|
+
#
|
|
325
|
+
# `npx <tool>` costs ~1.4-1.5 s more than the tool itself on this machine
|
|
326
|
+
# (measured: eslint --version 1654 ms via npx vs 348 ms direct; vitest 1799 ms vs
|
|
327
|
+
# 250 ms). That is npm resolving the package before it ever runs: on a cold
|
|
328
|
+
# cache it may reach the network, which a quality gate must never depend on.
|
|
329
|
+
# pre-commit invokes these tools a handful of times per commit, so the wrapper
|
|
330
|
+
# was paying several seconds to start processes that were already installed.
|
|
331
|
+
#
|
|
332
|
+
# `run_node_bin <tool> <node-entry> [args...]`. Falls back to npx when the local
|
|
333
|
+
# entry is absent (a globally-installed toolchain is still a valid setup).
|
|
334
|
+
if ! declare -F run_node_bin >/dev/null 2>&1; then
|
|
335
|
+
run_node_bin() {
|
|
336
|
+
local _tool="$1" _entry="$2"
|
|
337
|
+
shift 2
|
|
338
|
+
local _root="${PROJECT_ROOT:-$(pwd)}"
|
|
339
|
+
local _local="$_root/node_modules/$_entry"
|
|
340
|
+
if [ -f "$_local" ] && command -v node >/dev/null 2>&1; then
|
|
341
|
+
node "$_local" "$@"
|
|
342
|
+
else
|
|
343
|
+
npx "$_tool" "$@"
|
|
344
|
+
fi
|
|
345
|
+
}
|
|
346
|
+
fi
|
|
347
|
+
|
|
322
348
|
# Gate 5's vitest exit-code judgement (is_runner_infrastructure_error,
|
|
323
349
|
# is_partial_threshold_only_exit, handle_test_failure) is sourced from
|
|
324
350
|
# lib/test-failure.sh below, next to the other hook libraries.
|
|
@@ -1677,9 +1703,9 @@ else
|
|
|
1677
1703
|
# each linted file (found in the subdirectory automatically); flat
|
|
1678
1704
|
# configs are discovered from the CWD, so point at them explicitly.
|
|
1679
1705
|
if [ "$TS_PROJECT_DIR" != "." ] && [ -f "$TS_PROJECT_DIR/eslint.config.js" ]; then
|
|
1680
|
-
ESLINT_OUTPUT=$(
|
|
1706
|
+
ESLINT_OUTPUT=$(run_node_bin eslint eslint/bin/eslint.js --config "$TS_PROJECT_DIR/eslint.config.js" $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
1681
1707
|
else
|
|
1682
|
-
ESLINT_OUTPUT=$(
|
|
1708
|
+
ESLINT_OUTPUT=$(run_node_bin eslint eslint/bin/eslint.js $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
1683
1709
|
fi
|
|
1684
1710
|
ESLINT_EXIT=$?
|
|
1685
1711
|
if [ "$ESLINT_EXIT" -ne 0 ]; then
|
|
@@ -2659,7 +2685,13 @@ else
|
|
|
2659
2685
|
# Exclusions come from filter_project_paths (#649). The hand-maintained list
|
|
2660
2686
|
# this replaced omitted vendor/target/coverage/out, so a staged vendor/x.ts
|
|
2661
2687
|
# was reported as new source with no test.
|
|
2662
|
-
|
|
2688
|
+
#
|
|
2689
|
+
# Byte-identical mirrors under src/npm-package/ are excluded the same way
|
|
2690
|
+
# lint (#494) and Boy Scout (#649) already exclude them. A mirror cannot
|
|
2691
|
+
# carry its own test: the next resync overwrites the directory, so the
|
|
2692
|
+
# requirement is structurally unsatisfiable and blocked every commit that
|
|
2693
|
+
# added a file to a mirrored module. The canonical source keeps its test.
|
|
2694
|
+
NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -Ev "$MIRROR_PATH_FILTER" | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
|
|
2663
2695
|
|
|
2664
2696
|
# Strip PROJECT_SUBDIR prefix from new-TS-file paths ONLY when the hook
|
|
2665
2697
|
# actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1; single-language
|
|
@@ -3002,7 +3034,7 @@ else
|
|
|
3002
3034
|
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
3003
3035
|
# TypeScript: vitest with smart test selection
|
|
3004
3036
|
if [ "$CURRENT_LANG" = "typescript" ] && { [ -f "package.json" ] || [ -f "$PROJECT_ROOT/package.json" ]; }; then
|
|
3005
|
-
if run_without_git_context
|
|
3037
|
+
if run_without_git_context run_node_bin vitest vitest/vitest.mjs --version >/dev/null 2>&1; then
|
|
3006
3038
|
# Count changed test files to decide strategy. `grep -c '.'` prints its
|
|
3007
3039
|
# own zero AND exits 1 on an empty list, so `|| echo "0"` used to yield
|
|
3008
3040
|
# $'0\n0' -- the same two-value bug that false-blocked Gate 8 (#493).
|
|
@@ -3049,7 +3081,7 @@ else
|
|
|
3049
3081
|
if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
|
|
3050
3082
|
# Too many test files changed → full run
|
|
3051
3083
|
echo "Too many test files changed ($CHANGED_TEST_FILE_COUNT), running full test suite..."
|
|
3052
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3084
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage 2>&1)
|
|
3053
3085
|
TESTS_EXIT_CODE=$?
|
|
3054
3086
|
echo "$TESTS_OUTPUT" | head -60
|
|
3055
3087
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3063,7 +3095,7 @@ else
|
|
|
3063
3095
|
SMART_TARGETS=$(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')
|
|
3064
3096
|
echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
|
|
3065
3097
|
echo " Target(s): $(echo "$SELECTED_TEST_FILES" | tr '\n' ' ')"
|
|
3066
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3098
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage $SMART_TARGETS 2>&1)
|
|
3067
3099
|
TESTS_EXIT_CODE=$?
|
|
3068
3100
|
echo "$TESTS_OUTPUT" | head -60
|
|
3069
3101
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3108,7 +3140,7 @@ else
|
|
|
3108
3140
|
if [ -n "$RELATED_TESTS" ]; then
|
|
3109
3141
|
echo " Found related test(s): $(echo "$RELATED_TESTS" | tr '\n' ' ')"
|
|
3110
3142
|
PARTIAL_TEST_RUN=true # Mark as partial run for coverage check
|
|
3111
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3143
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage $RELATED_TESTS 2>&1)
|
|
3112
3144
|
TESTS_EXIT_CODE=$?
|
|
3113
3145
|
echo "$TESTS_OUTPUT" | head -60
|
|
3114
3146
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3119,7 +3151,7 @@ else
|
|
|
3119
3151
|
else
|
|
3120
3152
|
# No related tests found → full run is safest
|
|
3121
3153
|
echo " No related test files found, running full test suite..."
|
|
3122
|
-
TESTS_OUTPUT=$(run_without_git_context
|
|
3154
|
+
TESTS_OUTPUT=$(run_without_git_context run_node_bin vitest vitest/vitest.mjs run --coverage 2>&1)
|
|
3123
3155
|
TESTS_EXIT_CODE=$?
|
|
3124
3156
|
echo "$TESTS_OUTPUT" | head -60
|
|
3125
3157
|
if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
|
|
@@ -3976,6 +4008,8 @@ else
|
|
|
3976
4008
|
cd "$PROJECT_ROOT" || exit 1
|
|
3977
4009
|
if [ "$ARCHLINT_KIND" = "npx" ]; then
|
|
3978
4010
|
npx @archlinter/cli "$@"
|
|
4011
|
+
elif [ "${ARCHLINT_NODE:-0}" = "1" ]; then
|
|
4012
|
+
node "$ARCHLINT_BIN" "$@"
|
|
3979
4013
|
else
|
|
3980
4014
|
"$ARCHLINT_BIN" "$@"
|
|
3981
4015
|
fi
|
|
@@ -3996,8 +4030,17 @@ else
|
|
|
3996
4030
|
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
3997
4031
|
case "$CURRENT_LANG" in
|
|
3998
4032
|
"typescript")
|
|
3999
|
-
#
|
|
4000
|
-
|
|
4033
|
+
# Prefer node on the package's own entry over the .bin shim: the shim's
|
|
4034
|
+
# exec bit and extension vary by platform and package manager (npm writes
|
|
4035
|
+
# archlint, archlint.cmd and archlint.ps1 on Windows), so `-x .bin/...`
|
|
4036
|
+
# is true here while the no-extension shim is what Git Bash would run.
|
|
4037
|
+
# `node <entry>` sidesteps all of that and also avoids npx's ~1.6 s
|
|
4038
|
+
# package resolution (measured: 1909 ms via npx vs 319 ms direct).
|
|
4039
|
+
if [ -f "$PROJECT_ROOT/node_modules/@archlinter/cli/bin/archlint.js" ] && command -v node >/dev/null 2>&1; then
|
|
4040
|
+
ARCHLINT_KIND="binary"
|
|
4041
|
+
ARCHLINT_BIN="$PROJECT_ROOT/node_modules/@archlinter/cli/bin/archlint.js"
|
|
4042
|
+
ARCHLINT_NODE=1
|
|
4043
|
+
elif [ -x "$PROJECT_ROOT/node_modules/.bin/archlint" ]; then
|
|
4001
4044
|
ARCHLINT_KIND="binary"
|
|
4002
4045
|
ARCHLINT_BIN="$PROJECT_ROOT/node_modules/.bin/archlint"
|
|
4003
4046
|
elif command -v archlint >/dev/null 2>&1; then
|
|
@@ -4758,7 +4801,12 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
|
|
|
4758
4801
|
MIRROR_FAIL=0
|
|
4759
4802
|
while IFS= read -r _mf; do
|
|
4760
4803
|
[ -n "$_mf" ] || continue
|
|
4804
|
+
# `__tests__/` has no mirror BY DESIGN: sync-package-content.js lists it in
|
|
4805
|
+
# SKIP_DIRS so test suites never ship to consumers. Demanding a mirror for
|
|
4806
|
+
# them makes every new BATS file un-committable -- the two rules contradicted
|
|
4807
|
+
# each other, and the only way past was to edit a generated file by hand.
|
|
4761
4808
|
case "$_mf" in
|
|
4809
|
+
*/__tests__/*) continue ;;
|
|
4762
4810
|
githooks/*) _mm="src/npm-package/hooks/${_mf#githooks/}" ;;
|
|
4763
4811
|
plugins/*/src/*) _mm=$(printf '%s' "$_mf" | sed 's|^plugins/\([^/]*\)/src/|src/npm-package/plugins/\1/src/|') ;;
|
|
4764
4812
|
*) continue ;;
|
|
@@ -4874,7 +4922,22 @@ generate_quality_report() {
|
|
|
4874
4922
|
local TIMESTAMP
|
|
4875
4923
|
local BRANCH
|
|
4876
4924
|
local PASSED_COUNT=0
|
|
4877
|
-
|
|
4925
|
+
# #680: the gate list is the single source of truth. TOTAL_GATES, the scoring
|
|
4926
|
+
# loop, the console table and the JSON `overall` block all derive from it.
|
|
4927
|
+
#
|
|
4928
|
+
# Gate 13 was added in v0.35.x with only its execution and JSON write -- these
|
|
4929
|
+
# three sites kept saying "12". A machine with no C/C++ compiler therefore saw
|
|
4930
|
+
# `10.0/10 effective`, `gatesWarned: 0` and a table that stopped at Gate 12,
|
|
4931
|
+
# while the JSON `gates` object held 13 keys. "Compiler strictness was never
|
|
4932
|
+
# checked" and "compiler reported zero warnings" rendered identically, which is
|
|
4933
|
+
# the same silent-success pattern as #561/#570/#585.
|
|
4934
|
+
#
|
|
4935
|
+
# Adding a gate now means editing ALL_GATE_NUMBERS + ONE table row. Nothing
|
|
4936
|
+
# else carries a count.
|
|
4937
|
+
local ALL_GATE_NUMBERS="1 2 3 4 5 6 7 8 9 10 11 12 13"
|
|
4938
|
+
local TOTAL_GATES=0
|
|
4939
|
+
local _g
|
|
4940
|
+
for _g in $ALL_GATE_NUMBERS; do TOTAL_GATES=$((TOTAL_GATES + 1)); done
|
|
4878
4941
|
local SKIP_COUNT=0
|
|
4879
4942
|
local WARN_COUNT=0
|
|
4880
4943
|
local NONPASS_COUNT=0
|
|
@@ -4902,7 +4965,7 @@ generate_quality_report() {
|
|
|
4902
4965
|
local DEGRADED_JSON=""
|
|
4903
4966
|
local FAIL_COUNT=0
|
|
4904
4967
|
local FAILED_JSON=""
|
|
4905
|
-
for gate in
|
|
4968
|
+
for gate in $ALL_GATE_NUMBERS; do
|
|
4906
4969
|
local status_var="GATE_${gate}_STATUS"
|
|
4907
4970
|
# Default mirrors the JSON report rendering below EXACTLY (gates 1-11
|
|
4908
4971
|
# PASS, gate 12 WARN): a gate that never assigned a status is "not
|
|
@@ -4912,6 +4975,10 @@ generate_quality_report() {
|
|
|
4912
4975
|
# Default direction is "did not run", never "passed": an unassigned status
|
|
4913
4976
|
# is a gate that never reached a verdict, and scoring it PASS both claims a
|
|
4914
4977
|
# success that did not happen and hides the gap in the effective rate.
|
|
4978
|
+
#
|
|
4979
|
+
# Gate 13 defaults to SKIP like the rest: it is only WARN when the toolchain
|
|
4980
|
+
# is missing and the gate explicitly says so, and Gate 12 is the one gate
|
|
4981
|
+
# whose "no verdict" is still a warning rather than "not applicable".
|
|
4915
4982
|
if [ "$gate" -eq 12 ]; then gate_default="WARN"; else gate_default="SKIP"; fi
|
|
4916
4983
|
gate_status="${!status_var:-$gate_default}"
|
|
4917
4984
|
case "$gate_status" in
|
|
@@ -5258,6 +5325,9 @@ ENDJSON
|
|
|
5258
5325
|
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-SKIP}"
|
|
5259
5326
|
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-SKIP}"
|
|
5260
5327
|
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
5328
|
+
# #680: Gate 13 ran and could block, but had no row here -- the table is what a
|
|
5329
|
+
# human reads, so a gate missing from it reads as a gate that does not exist.
|
|
5330
|
+
printf " %-45s %s\n" "Gate 13: Compiler Strictness" "${GATE_13_STATUS:-SKIP}"
|
|
5261
5331
|
echo ""
|
|
5262
5332
|
# #564: both views in one line. The effective score answers "of the gates that
|
|
5263
5333
|
# could run, how many passed"; the all-gates score counts a skip against the
|
package/hooks/pre-push
CHANGED
|
@@ -426,7 +426,16 @@ SPRINT_GATE_SCRIPT=""
|
|
|
426
426
|
# ~/.config/xp-gate/hooks -- a correctly installed machine was told the file was
|
|
427
427
|
# missing here too. Nothing is sourced for this: pre-push resolves its libraries
|
|
428
428
|
# further down (ADAPTER_COMMON), and this gate must decide before that.
|
|
429
|
-
|
|
429
|
+
#
|
|
430
|
+
# `$HOME/.config/xp-gate/adapters` is the tier #678 found missing: `xp-gate
|
|
431
|
+
# setup-global` runs copyAdapters(), which ships every `gate-*.sh` AND
|
|
432
|
+
# `sprint-gate.sh` into the ADAPTERS directory -- never into hooks/. So a globally
|
|
433
|
+
# installed machine had sprint-gate.sh present under adapters/ while this loop
|
|
434
|
+
# searched only hooks/, and Gate MS skipped on every push without complaint. Both
|
|
435
|
+
# tiers are searched now: which directory `init` chose is an install detail this
|
|
436
|
+
# gate should not depend on.
|
|
437
|
+
for _sprint_gate_dir in "$PROJECT_ROOT/githooks" "$(dirname "$0")" \
|
|
438
|
+
"$HOME/.config/xp-gate/hooks" "$HOME/.config/xp-gate/adapters"; do
|
|
430
439
|
if [ -n "$_sprint_gate_dir" ] && [ -f "$_sprint_gate_dir/sprint-gate.sh" ]; then
|
|
431
440
|
SPRINT_GATE_SCRIPT="$_sprint_gate_dir/sprint-gate.sh"
|
|
432
441
|
break
|
|
@@ -440,7 +449,7 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
440
449
|
exit 1
|
|
441
450
|
fi
|
|
442
451
|
else
|
|
443
|
-
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found; searched: $PROJECT_ROOT/githooks, $(dirname "$0"), $HOME/.config/xp-gate/hooks)"
|
|
452
|
+
echo "⏭️ SKIPPED - Gate MS: Sprint Flow (sprint-gate.sh not found; searched: $PROJECT_ROOT/githooks, $(dirname "$0"), $HOME/.config/xp-gate/hooks, $HOME/.config/xp-gate/adapters)"
|
|
444
453
|
GATE_S_STATUS="SKIP"
|
|
445
454
|
fi
|
|
446
455
|
echo ""
|
|
@@ -13,7 +13,16 @@ const util = require('util');
|
|
|
13
13
|
// tests run in parallel across worker threads.
|
|
14
14
|
const DESCRIBE_ID = String(Math.random().toString(36).slice(2, 8));
|
|
15
15
|
|
|
16
|
+
// Budget for every case in this file. `doctor --fix` walks the whole install
|
|
17
|
+
// surface (hooks, adapters, config, version cache) and the default 5 s budget is
|
|
18
|
+
// a measurement of an idle machine, not of one running a full suite: under
|
|
19
|
+
// concurrent load the same passing case times out, which reads as a doctor
|
|
20
|
+
// defect. 30 s keeps real hangs bounded while leaving the CPU contention room.
|
|
21
|
+
const DOCTOR_TIMEOUT_MS = 30000;
|
|
22
|
+
|
|
16
23
|
describe('doctor', () => {
|
|
24
|
+
// Applies to every `it` below (the per-test arg is only needed when overriding).
|
|
25
|
+
vi.setConfig({ testTimeout: DOCTOR_TIMEOUT_MS });
|
|
17
26
|
let tmpHome;
|
|
18
27
|
let tmpProject;
|
|
19
28
|
let originalHome;
|