@boyingliu01/xp-gate 0.20.0 → 0.20.2
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-8.sh +115 -73
- package/adapters/java.sh +373 -27
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-3.sh +51 -3
- package/gate-4.sh +32 -1
- package/gate-8.sh +115 -73
- package/hooks/gate-3.sh +51 -3
- package/hooks/gate-4.sh +32 -1
- package/hooks/gate-8.sh +115 -73
- package/hooks/pre-commit +404 -62
- package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
- package/lib/doctor-tui.js +21 -3
- package/lib/doctor.js +111 -8
- package/lib/gate-audit.ts +3 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/__tests__/java.test.ts +317 -0
- package/principles/adapters/java.ts +325 -13
- package/principles/boy-scout.ts +36 -6
- package/scripts/sync-package-content.js +13 -6
- package/skills/admin-template-guidelines/SKILL.md +1 -0
- package/skills/batch-grill-me/SKILL.md +1 -0
- package/skills/clipboard-vision/SKILL.md +1 -0
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/skills/ralph-loop/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-driven-development/SKILL.md +1 -0
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/skills/test-specification-alignment/SKILL.md +1 -0
- package/skills/to-issues/SKILL.md +1 -0
|
@@ -12,6 +12,10 @@ import path from 'path';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import { extractImports, resolveImportPath, runImportCheck } from '../gate-10';
|
|
14
14
|
|
|
15
|
+
// Named timeout for the runImportCheck calls that assert verdicts; keeps the
|
|
16
|
+
// Boy Scout magic-number count from growing with each new test case.
|
|
17
|
+
const IMPORT_CHECK_TIMEOUT_MS = 30000;
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* ─── extractImports ────────────────────────────────────────────────────────────
|
|
17
21
|
*/
|
|
@@ -321,6 +325,61 @@ describe('runImportCheck', () => {
|
|
|
321
325
|
expect(result.violations[0].reason).toMatch(/not found|does not exist|missing/i);
|
|
322
326
|
});
|
|
323
327
|
|
|
328
|
+
it('passes when projectRoot spelling differs in drive case/slashes (Windows, #508)', async () => {
|
|
329
|
+
// git rev-parse --show-toplevel reports "D:/repo" while path.resolve of a
|
|
330
|
+
// repo-relative changed file inherits process.cwd()'s "d:\repo" drive case.
|
|
331
|
+
// The case-sensitive startsWith judged every import as boundary-escaping,
|
|
332
|
+
// blocking any TS-bearing push. The same root spelled the other way must
|
|
333
|
+
// pass on Windows; drive letters do not exist on POSIX so skip there.
|
|
334
|
+
if (process.platform !== 'win32') return;
|
|
335
|
+
|
|
336
|
+
let fooFile: string;
|
|
337
|
+
try {
|
|
338
|
+
const srcDir = path.join(tmpDir, 'src');
|
|
339
|
+
await fs.mkdir(srcDir);
|
|
340
|
+
|
|
341
|
+
const barFile = path.join(srcDir, 'bar.ts');
|
|
342
|
+
await fs.writeFile(barFile, 'export const x = 1;');
|
|
343
|
+
|
|
344
|
+
fooFile = path.join(srcDir, 'foo.ts');
|
|
345
|
+
await fs.writeFile(fooFile, `import { x } from './bar';\nconsole.log(x);\n`);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw new Error(`Fixture setup failed: ${error}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const altRoot = tmpDir.replace(/\\/g, '/').replace(/^[a-zA-Z]:/, (m) => m.toLowerCase());
|
|
351
|
+
expect(altRoot).not.toBe(tmpDir);
|
|
352
|
+
|
|
353
|
+
const result = await runImportCheck([fooFile], altRoot, IMPORT_CHECK_TIMEOUT_MS);
|
|
354
|
+
expect(result.status).toBe('pass');
|
|
355
|
+
expect(result.violations).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('still fails a true boundary escape after case normalization (#508 follow-up)', async () => {
|
|
359
|
+
// Normalization must only absorb case/spelling differences, never widen the
|
|
360
|
+
// boundary: an import that genuinely leaves the project root keeps failing
|
|
361
|
+
// even when the root is spelled with a different drive-letter case.
|
|
362
|
+
if (process.platform !== 'win32') return;
|
|
363
|
+
|
|
364
|
+
let badFile: string;
|
|
365
|
+
try {
|
|
366
|
+
const nestedDir = path.join(tmpDir, 'pkg', 'dist', 'sub');
|
|
367
|
+
await fs.mkdir(nestedDir, { recursive: true });
|
|
368
|
+
|
|
369
|
+
badFile = path.join(nestedDir, 'file.ts');
|
|
370
|
+
await fs.writeFile(badFile, `import { x } from '../../../../outside';\n`);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new Error(`Fixture setup failed: ${error}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const altRoot = tmpDir.replace(/\\/g, '/').replace(/^[a-zA-Z]:/, (m) => m.toLowerCase());
|
|
376
|
+
|
|
377
|
+
const result = await runImportCheck([badFile], altRoot, IMPORT_CHECK_TIMEOUT_MS);
|
|
378
|
+
expect(result.status).toBe('fail');
|
|
379
|
+
expect(result.violations.length).toBeGreaterThan(0);
|
|
380
|
+
expect(result.violations[0].reason).toMatch(/escapes|outside|boundary/i);
|
|
381
|
+
});
|
|
382
|
+
|
|
324
383
|
it('skips YAML files containing JavaScript-like require text', async () => {
|
|
325
384
|
const workflowDir = path.join(tmpDir, '.github', 'workflows');
|
|
326
385
|
await fs.mkdir(workflowDir, { recursive: true });
|
|
@@ -307,6 +307,8 @@ export async function runImportCheck(
|
|
|
307
307
|
|
|
308
308
|
const fsSync = fsNative;
|
|
309
309
|
|
|
310
|
+
const normalizedRoot = path.resolve(projectRoot);
|
|
311
|
+
|
|
310
312
|
for (const file of changedFiles) {
|
|
311
313
|
if (!isSupportedModuleFile(file)) continue;
|
|
312
314
|
|
|
@@ -327,14 +329,27 @@ export async function runImportCheck(
|
|
|
327
329
|
continue;
|
|
328
330
|
}
|
|
329
331
|
|
|
332
|
+
// Windows paths are case-insensitive but the two sides of the boundary
|
|
333
|
+
// comparison get their case from different sources: path.resolve of a
|
|
334
|
+
// repo-relative file inherits process.cwd()'s drive-letter case (often
|
|
335
|
+
// lowercase), while projectRoot comes from `git rev-parse --show-toplevel`
|
|
336
|
+
// (uppercase drive). A case-sensitive startsWith then judged EVERY import
|
|
337
|
+
// as escaping the boundary — pre-push Gate 10 blocked any TS-bearing push
|
|
338
|
+
// from a shell whose drive-letter case disagreed with git's (#508).
|
|
339
|
+
const normalizeForBoundary = (p: string): string => {
|
|
340
|
+
const resolved = path.resolve(p);
|
|
341
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
342
|
+
};
|
|
343
|
+
const boundaryRoot = normalizeForBoundary(normalizedRoot);
|
|
344
|
+
|
|
330
345
|
const imports = extractImports(content);
|
|
331
346
|
|
|
332
347
|
for (const imp of imports) {
|
|
333
348
|
const rawResolved = path.resolve(path.dirname(file), imp.path);
|
|
334
|
-
const
|
|
349
|
+
const boundaryTarget = normalizeForBoundary(rawResolved);
|
|
335
350
|
|
|
336
351
|
const escapesBoundary =
|
|
337
|
-
!
|
|
352
|
+
!boundaryTarget.startsWith(boundaryRoot + path.sep) && boundaryTarget !== boundaryRoot;
|
|
338
353
|
|
|
339
354
|
if (escapesBoundary) {
|
|
340
355
|
violations.push({
|
package/gate-3.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 3: Cyclomatic Complexity Check
|
|
3
4
|
# Uses lizard - checks cyclomatic complexity of changed source files
|
|
@@ -17,7 +18,45 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
17
18
|
GATE_3_STATUS="SKIP"
|
|
18
19
|
|
|
19
20
|
else
|
|
20
|
-
|
|
21
|
+
# S2 (#507): three-level CCN threshold override --
|
|
22
|
+
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|
|
23
|
+
# Invalid values WARN on stderr and fall back to the default; the resolved
|
|
24
|
+
# value reaches the gate-3 audit record via the --detail argument below.
|
|
25
|
+
# Implemented with bash builtins only (no tr/grep spawns): every process
|
|
26
|
+
# spawn is expensive on Windows under AV filter drivers.
|
|
27
|
+
resolve_ccn_threshold() {
|
|
28
|
+
local default=5 val
|
|
29
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
30
|
+
val=$(<".xp-gate/ccn-threshold")
|
|
31
|
+
val="${val//[[:space:]]/}"
|
|
32
|
+
case "$val" in
|
|
33
|
+
''|*[!0-9]*|0)
|
|
34
|
+
echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
echo "$val"
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
echo "$default"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
45
|
+
if [ -n "$val" ]; then
|
|
46
|
+
case "$val" in
|
|
47
|
+
''|*[!0-9]*|0)
|
|
48
|
+
echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "$val"
|
|
52
|
+
return 0
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
56
|
+
echo "$default"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CCN_THRESHOLD=$(resolve_ccn_threshold)
|
|
21
60
|
|
|
22
61
|
# Check lizard availability
|
|
23
62
|
LIZARD_CMD=""
|
|
@@ -37,7 +76,16 @@ else
|
|
|
37
76
|
echo "Checking complexity for source files..."
|
|
38
77
|
|
|
39
78
|
# Run lizard with CCN threshold
|
|
40
|
-
|
|
79
|
+
# One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
80
|
+
# splitting, which also splits any path containing a space -- handing
|
|
81
|
+
# lizard two bogus paths and checking neither (#457 defect class, same
|
|
82
|
+
# fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
|
|
83
|
+
# collapse the list into one argument. An array does both.
|
|
84
|
+
CC_FILES_ARGS=()
|
|
85
|
+
while IFS= read -r _cc_file; do
|
|
86
|
+
[ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
|
|
87
|
+
done <<< "$CC_FILES"
|
|
88
|
+
CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
|
|
41
89
|
|
|
42
90
|
# Parse warning count from the summary table: "Warning cnt 8"
|
|
43
91
|
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
@@ -65,4 +113,4 @@ else
|
|
|
65
113
|
GATE_3_STATUS="WARN"
|
|
66
114
|
fi
|
|
67
115
|
fi
|
|
68
|
-
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
116
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START" "ccn_threshold=${CCN_THRESHOLD:-5}"
|
package/gate-4.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 4: Principles Checker (Clean Code + SOLID)
|
|
3
4
|
# Reuses existing principles checker logic
|
|
@@ -23,6 +24,31 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
|
23
24
|
}
|
|
24
25
|
fi
|
|
25
26
|
|
|
27
|
+
# Java rule coverage annotation (#507 S4, AC-507-04-02). The normative matrix
|
|
28
|
+
# lives in docs/java-principles-coverage.md; these constants mirror it. Before
|
|
29
|
+
# #507 the Java adapter fed rules only {name,type,line,code}, so the 12 rules
|
|
30
|
+
# reading structured metrics silently saw `undefined` and a Java commit got a
|
|
31
|
+
# green "checked" with zero effective coverage. 12/15 now fire; the three
|
|
32
|
+
# inert ones are rule-side gaps documented in the coverage doc -- they must
|
|
33
|
+
# NOT be announced as covered.
|
|
34
|
+
JAVA_COVERAGE_EFFECTIVE=12
|
|
35
|
+
JAVA_COVERAGE_TOTAL=15
|
|
36
|
+
JAVA_COVERAGE_INERT="lsp, many-exports, code-duplication"
|
|
37
|
+
|
|
38
|
+
# Print the coverage annotation when (and only when) the staged file list
|
|
39
|
+
# contains Java sources. Non-Java commits keep the original output byte for
|
|
40
|
+
# byte. Pure shell builtins: no subprocess per file.
|
|
41
|
+
java_coverage_note() {
|
|
42
|
+
local _f _has_java=""
|
|
43
|
+
for _f in "$@"; do
|
|
44
|
+
case "$_f" in
|
|
45
|
+
*.java|*.Java|*.JAVA) _has_java=1; break ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
[ -n "$_has_java" ] || return 0
|
|
49
|
+
echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see docs/java-principles-coverage.md)"
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
2>&1 echo ""
|
|
27
53
|
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
28
54
|
GATE_4_START=$(gate_start_ms)
|
|
@@ -72,7 +98,12 @@ else
|
|
|
72
98
|
|
|
73
99
|
if [ -n "$PRINCIPLES_DIR" ]; then
|
|
74
100
|
echo "Checking Clean Code + SOLID principles..."
|
|
75
|
-
|
|
101
|
+
|
|
102
|
+
# AC-507-04-02: say what a Java commit is actually covered for, instead
|
|
103
|
+
# of implying a full 15-rule check (#507). Printed before the run so it
|
|
104
|
+
# appears on both the PASS and the FAIL path.
|
|
105
|
+
java_coverage_note ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"}
|
|
106
|
+
|
|
76
107
|
if command -v npx > /dev/null 2>&1 || [ -f "$PRINCIPLES_BASE/node_modules/tsx/dist/cli.mjs" ]; then
|
|
77
108
|
# Run principles checker and store results.
|
|
78
109
|
#
|
package/gate-8.sh
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# ============================================================================
|
|
2
|
-
# GATE 8: Secret Scanning (gitleaks)
|
|
3
|
-
# Detects secrets (API keys, passwords, tokens) in staged files
|
|
4
|
-
#
|
|
2
|
+
# GATE 8: Secret Scanning (gitleaks + detect-secrets)
|
|
3
|
+
# Detects secrets (API keys, passwords, tokens) in staged files.
|
|
4
|
+
# Tools:
|
|
5
|
+
# gitleaks -- primary, fail-closed (missing or non-0/1 exit => BLOCK, #499)
|
|
6
|
+
# detect-secrets -- secondary, baseline-diff: only blocks on secrets NOT in
|
|
7
|
+
# .secrets.baseline; silently does nothing when unavailable
|
|
5
8
|
# ============================================================================
|
|
6
9
|
|
|
7
10
|
2>&1 echo ""
|
|
8
|
-
2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
|
|
11
|
+
2>&1 echo "→ Gate 8: Secret scanning (gitleaks + detect-secrets)..."
|
|
9
12
|
GATE_8_START=$(gate_start_ms)
|
|
10
13
|
|
|
11
14
|
# Gitleaks availability check
|
|
@@ -16,85 +19,124 @@ elif [ -f "$HOME/.local/bin/gitleaks" ]; then
|
|
|
16
19
|
GITLEAKS_CMD="$HOME/.local/bin/gitleaks"
|
|
17
20
|
fi
|
|
18
21
|
|
|
19
|
-
if [ -
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
if [ -z "$GITLEAKS_CMD" ]; then
|
|
23
|
+
# fail-closed (#499): an uninstalled gitleaks used to SKIP the gate, so any
|
|
24
|
+
# contributor without it shipped commits with zero secret protection and no
|
|
25
|
+
# signal. A security gate that cannot run must not report success.
|
|
26
|
+
echo " ❌ BLOCKED - gitleaks not installed (fail-closed, #499)."
|
|
27
|
+
echo " Secret scanning is mandatory before commit. Install gitleaks then retry:"
|
|
28
|
+
echo " macOS: brew install gitleaks"
|
|
29
|
+
echo " Windows: winget install gitleaks"
|
|
30
|
+
echo " Linux: scripts/install-gitleaks.sh (or https://github.com/gitleaks/gitleaks)"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
GITLEAKS_CONFIG=""
|
|
35
|
+
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
37
|
+
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
39
|
+
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
|
+
else
|
|
41
|
+
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
29
42
|
fi
|
|
43
|
+
fi
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
# Scan the INDEX, not history. gitleaks 8.x `git --pre-commit` walks committed
|
|
46
|
+
# revisions and reports "0 commits scanned" on a pre-commit run -- so a staged
|
|
47
|
+
# secret was never seen and Gate 8 always passed. `git --staged` scans
|
|
48
|
+
# `git diff --cached`, which is exactly the pre-commit payload (#449).
|
|
49
|
+
GITLEAKS_REPORT="${TMPDIR:-/tmp}/gitleaks-report.json"
|
|
50
|
+
rm -f "$GITLEAKS_REPORT"
|
|
51
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --staged --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path="$GITLEAKS_REPORT" 2>&1)
|
|
52
|
+
GITLEAKS_EXIT=$?
|
|
39
53
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fi
|
|
54
|
+
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
55
|
+
# Decide SKIP from the REPORT ARTIFACT, not from log wording. Parsing stdout
|
|
56
|
+
# meant a gitleaks message change would silently turn a no-evidence run into
|
|
57
|
+
# a PASS -- the same fail-open class this gate just fixed (#449).
|
|
58
|
+
# The report exists and is a JSON array of findings; `[]` == clean scan.
|
|
59
|
+
GITLEAKS_FINDINGS="?"
|
|
60
|
+
if [ -f "$GITLEAKS_REPORT" ]; then
|
|
61
|
+
# A report that is not shaped like a JSON array is no evidence: leave the
|
|
62
|
+
# '?' sentinel so the gate reports SKIP, never a PASS it cannot support
|
|
63
|
+
# (the fail-closed-on-the-claim rule #449 established).
|
|
64
|
+
if grep -q '^\[' "$GITLEAKS_REPORT" 2>/dev/null; then
|
|
65
|
+
# Count occurrences, not matching lines: `grep -c` prints its own zero AND
|
|
66
|
+
# exits 1 on a clean report, so the old `|| echo 0` handed the verdict table
|
|
67
|
+
# $'0\n0' -- no branch matched and a clean scan was BLOCKED as if it leaked
|
|
68
|
+
# (#493). wc -l emits exactly one value, and `|| true` keeps the assignment
|
|
69
|
+
# errexit/pipefail-safe (#489).
|
|
70
|
+
GITLEAKS_FINDINGS=$(grep -o '"RuleID"' "$GITLEAKS_REPORT" 2>/dev/null | wc -l | tr -d '[:space:]') || true
|
|
58
71
|
fi
|
|
72
|
+
fi
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
76
|
-
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
77
|
-
# Secrets found — output details
|
|
74
|
+
if [ "$GITLEAKS_FINDINGS" = "0" ]; then
|
|
75
|
+
echo " ✅ PASSED - No secrets detected."
|
|
76
|
+
GATE_8_STATUS="PASS"
|
|
77
|
+
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
+
# Exit 0 but no readable report: we have no evidence the scan happened.
|
|
79
|
+
# Fail closed on the *claim*: report SKIP, never PASS.
|
|
80
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report"
|
|
81
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
82
|
+
GATE_8_STATUS="SKIP"
|
|
83
|
+
else
|
|
84
|
+
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
78
85
|
echo "$GITLEAKS_OUTPUT"
|
|
79
86
|
echo ""
|
|
80
|
-
echo "
|
|
81
|
-
echo ""
|
|
82
|
-
echo "Remediation options:"
|
|
83
|
-
echo " 1. Remove the secret and use environment variables instead"
|
|
84
|
-
echo " 2. Add a false positive to .gitleaks.toml allowlist"
|
|
85
|
-
echo " 3. Use git secret or vault for sensitive data"
|
|
86
|
-
echo ""
|
|
87
|
-
echo "See: https://github.com/gitleaks/gitleaks"
|
|
87
|
+
echo "Remediation: remove the secret, or allowlist a false positive in .gitleaks.toml."
|
|
88
88
|
exit 1
|
|
89
|
+
fi
|
|
90
|
+
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
91
|
+
# Secrets found — output details
|
|
92
|
+
echo "$GITLEAKS_OUTPUT"
|
|
93
|
+
echo ""
|
|
94
|
+
echo " ❌ BLOCKED - Secrets detected in staged files."
|
|
95
|
+
echo ""
|
|
96
|
+
echo "Remediation options:"
|
|
97
|
+
echo " 1. Remove the secret and use environment variables instead"
|
|
98
|
+
echo " 2. Add a false positive to .gitleaks.toml allowlist"
|
|
99
|
+
echo " 3. Use git secret or vault for sensitive data"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "See: https://github.com/gitleaks/gitleaks"
|
|
102
|
+
exit 1
|
|
103
|
+
else
|
|
104
|
+
# fail-closed (#499): a non-0/1 exit (config error, crash) used to SKIP, which
|
|
105
|
+
# let a broken scanner wave every commit through without any scan.
|
|
106
|
+
echo " ❌ BLOCKED - gitleaks exited with code $GITLEAKS_EXIT (fail-closed, #499)."
|
|
107
|
+
echo "$GITLEAKS_OUTPUT"
|
|
108
|
+
echo " Secret scanning could not complete; fix the gitleaks error above and retry."
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ---- detect-secrets(次要防线,基线比对)----
|
|
113
|
+
# detect-secrets-hook comes from the project's Python dev extra; it blocks only
|
|
114
|
+
# on secrets NOT yet registered in .secrets.baseline, so test-fixture dummy keys
|
|
115
|
+
# recorded in the baseline never trip it.
|
|
116
|
+
#
|
|
117
|
+
# The file list is piped NUL-delimited STRAIGHT into xargs -0 — never through a
|
|
118
|
+
# command substitution. `var=$(cmd -z)` strips every NUL byte (bash cannot hold
|
|
119
|
+
# NULs in a variable), which concatenated all filenames into one giant
|
|
120
|
+
# nonexistent path and would have BLOCKED every commit once a project shipped
|
|
121
|
+
# both detect-secrets-hook and .secrets.baseline. The pipeline lives inside an
|
|
122
|
+
# if-condition (errexit-exempt); a non-zero hook exit lands in the else branch.
|
|
123
|
+
if command -v detect-secrets-hook >/dev/null 2>&1 && [ -f ".secrets.baseline" ]; then
|
|
124
|
+
DS_TMP="$(dirname "$(mktemp -u)")/ds-output.$$"
|
|
125
|
+
if git diff --cached --name-only --diff-filter=ACM -z \
|
|
126
|
+
| xargs -0 detect-secrets-hook --baseline .secrets.baseline > "$DS_TMP" 2>&1; then
|
|
127
|
+
echo " ✅ detect-secrets PASSED - no new secrets beyond baseline."
|
|
89
128
|
else
|
|
90
|
-
|
|
91
|
-
echo "
|
|
92
|
-
|
|
129
|
+
cat "$DS_TMP"
|
|
130
|
+
echo ""
|
|
131
|
+
echo " ❌ BLOCKED - detect-secrets found secrets not in .secrets.baseline."
|
|
132
|
+
echo " If these are false positives, regenerate the baseline (cross-platform):"
|
|
133
|
+
echo " detect-secrets scan --update .secrets.baseline && git add .secrets.baseline"
|
|
134
|
+
rm -f "$DS_TMP"
|
|
135
|
+
exit 1
|
|
93
136
|
fi
|
|
137
|
+
rm -f "$DS_TMP"
|
|
94
138
|
else
|
|
95
|
-
echo "
|
|
96
|
-
echo " Install: brew install gitleaks (macOS) | winget install gitleaks (Windows) | scripts/install-gitleaks.sh (Linux)"
|
|
97
|
-
echo " ⏭️ SKIPPED - Secret scanning (gitleaks not installed)"
|
|
98
|
-
GATE_8_STATUS="SKIP"
|
|
139
|
+
echo " ⚠️ detect-secrets-hook unavailable (install: pip install -e \".[dev]\") — gitleaks only."
|
|
99
140
|
fi
|
|
141
|
+
|
|
100
142
|
record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
|
package/hooks/gate-3.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 3: Cyclomatic Complexity Check
|
|
3
4
|
# Uses lizard - checks cyclomatic complexity of changed source files
|
|
@@ -17,7 +18,45 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
|
17
18
|
GATE_3_STATUS="SKIP"
|
|
18
19
|
|
|
19
20
|
else
|
|
20
|
-
|
|
21
|
+
# S2 (#507): three-level CCN threshold override --
|
|
22
|
+
# .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
|
|
23
|
+
# Invalid values WARN on stderr and fall back to the default; the resolved
|
|
24
|
+
# value reaches the gate-3 audit record via the --detail argument below.
|
|
25
|
+
# Implemented with bash builtins only (no tr/grep spawns): every process
|
|
26
|
+
# spawn is expensive on Windows under AV filter drivers.
|
|
27
|
+
resolve_ccn_threshold() {
|
|
28
|
+
local default=5 val
|
|
29
|
+
if [ -f ".xp-gate/ccn-threshold" ]; then
|
|
30
|
+
val=$(<".xp-gate/ccn-threshold")
|
|
31
|
+
val="${val//[[:space:]]/}"
|
|
32
|
+
case "$val" in
|
|
33
|
+
''|*[!0-9]*|0)
|
|
34
|
+
echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
echo "$val"
|
|
38
|
+
return 0
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
echo "$default"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
val=${XP_GATE_CCN_THRESHOLD:-}
|
|
45
|
+
if [ -n "$val" ]; then
|
|
46
|
+
case "$val" in
|
|
47
|
+
''|*[!0-9]*|0)
|
|
48
|
+
echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "$val"
|
|
52
|
+
return 0
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
56
|
+
echo "$default"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CCN_THRESHOLD=$(resolve_ccn_threshold)
|
|
21
60
|
|
|
22
61
|
# Check lizard availability
|
|
23
62
|
LIZARD_CMD=""
|
|
@@ -37,7 +76,16 @@ else
|
|
|
37
76
|
echo "Checking complexity for source files..."
|
|
38
77
|
|
|
39
78
|
# Run lizard with CCN threshold
|
|
40
|
-
|
|
79
|
+
# One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
80
|
+
# splitting, which also splits any path containing a space -- handing
|
|
81
|
+
# lizard two bogus paths and checking neither (#457 defect class, same
|
|
82
|
+
# fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
|
|
83
|
+
# collapse the list into one argument. An array does both.
|
|
84
|
+
CC_FILES_ARGS=()
|
|
85
|
+
while IFS= read -r _cc_file; do
|
|
86
|
+
[ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
|
|
87
|
+
done <<< "$CC_FILES"
|
|
88
|
+
CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
|
|
41
89
|
|
|
42
90
|
# Parse warning count from the summary table: "Warning cnt 8"
|
|
43
91
|
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
@@ -65,4 +113,4 @@ else
|
|
|
65
113
|
GATE_3_STATUS="WARN"
|
|
66
114
|
fi
|
|
67
115
|
fi
|
|
68
|
-
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
116
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START" "ccn_threshold=${CCN_THRESHOLD:-5}"
|
package/hooks/gate-4.sh
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
1
2
|
# ============================================================================
|
|
2
3
|
# GATE 4: Principles Checker (Clean Code + SOLID)
|
|
3
4
|
# Reuses existing principles checker logic
|
|
@@ -23,6 +24,31 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
|
|
|
23
24
|
}
|
|
24
25
|
fi
|
|
25
26
|
|
|
27
|
+
# Java rule coverage annotation (#507 S4, AC-507-04-02). The normative matrix
|
|
28
|
+
# lives in docs/java-principles-coverage.md; these constants mirror it. Before
|
|
29
|
+
# #507 the Java adapter fed rules only {name,type,line,code}, so the 12 rules
|
|
30
|
+
# reading structured metrics silently saw `undefined` and a Java commit got a
|
|
31
|
+
# green "checked" with zero effective coverage. 12/15 now fire; the three
|
|
32
|
+
# inert ones are rule-side gaps documented in the coverage doc -- they must
|
|
33
|
+
# NOT be announced as covered.
|
|
34
|
+
JAVA_COVERAGE_EFFECTIVE=12
|
|
35
|
+
JAVA_COVERAGE_TOTAL=15
|
|
36
|
+
JAVA_COVERAGE_INERT="lsp, many-exports, code-duplication"
|
|
37
|
+
|
|
38
|
+
# Print the coverage annotation when (and only when) the staged file list
|
|
39
|
+
# contains Java sources. Non-Java commits keep the original output byte for
|
|
40
|
+
# byte. Pure shell builtins: no subprocess per file.
|
|
41
|
+
java_coverage_note() {
|
|
42
|
+
local _f _has_java=""
|
|
43
|
+
for _f in "$@"; do
|
|
44
|
+
case "$_f" in
|
|
45
|
+
*.java|*.Java|*.JAVA) _has_java=1; break ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
[ -n "$_has_java" ] || return 0
|
|
49
|
+
echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see docs/java-principles-coverage.md)"
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
2>&1 echo ""
|
|
27
53
|
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
28
54
|
GATE_4_START=$(gate_start_ms)
|
|
@@ -72,7 +98,12 @@ else
|
|
|
72
98
|
|
|
73
99
|
if [ -n "$PRINCIPLES_DIR" ]; then
|
|
74
100
|
echo "Checking Clean Code + SOLID principles..."
|
|
75
|
-
|
|
101
|
+
|
|
102
|
+
# AC-507-04-02: say what a Java commit is actually covered for, instead
|
|
103
|
+
# of implying a full 15-rule check (#507). Printed before the run so it
|
|
104
|
+
# appears on both the PASS and the FAIL path.
|
|
105
|
+
java_coverage_note ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"}
|
|
106
|
+
|
|
76
107
|
if command -v npx > /dev/null 2>&1 || [ -f "$PRINCIPLES_BASE/node_modules/tsx/dist/cli.mjs" ]; then
|
|
77
108
|
# Run principles checker and store results.
|
|
78
109
|
#
|