@boyingliu01/xp-gate 0.31.4 → 0.32.1
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/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +65 -16
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +65 -16
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +101 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +259 -57
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +70 -29
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/gate-3.sh
CHANGED
|
@@ -329,27 +329,62 @@ else
|
|
|
329
329
|
# is reverted at the next publish, so grading it cannot be acted
|
|
330
330
|
# on (doctorDelphi CCN=106 blocked every src/npm-package commit,
|
|
331
331
|
# on HEAD too — no baseline entry could ever exist for it).
|
|
332
|
-
# test
|
|
333
|
-
# in the report's test
|
|
332
|
+
# test test dirs (tests/, test/, __tests__/), *.test.*, test_*.py,
|
|
333
|
+
# *_test.{py,go}, *Test(s).java — recorded in the report's test
|
|
334
|
+
# bucket, never in the baseline.
|
|
334
335
|
# Both prefixes and the test pattern mirror ccn-report.cjs so the gate and
|
|
335
336
|
# the writer cannot drift apart again (pinned by __tests__/gate-3-*.bats).
|
|
337
|
+
#
|
|
338
|
+
# #634: pytest's default is `python_files = test_*.py` under a `tests/`
|
|
339
|
+
# root. The previous pattern only knew the SUFFIX form `_test.{py,go}` and
|
|
340
|
+
# `*.test.*`, so a pytest-laid-out project had every test module graded as
|
|
341
|
+
# production source: test-helper complexity was reported as source debt and
|
|
342
|
+
# the ratchet could block on files the baseline deliberately never records
|
|
343
|
+
# (bucket === 'test').
|
|
344
|
+
# `(^|/)(tests?)/` — a tests/ or test/ directory anywhere in the tree
|
|
345
|
+
# `(^|/)test_[^/]*\.py$` — pytest default filenames at any depth
|
|
346
|
+
# Both are anchored with (^|/) so `latest/`, `contest/`, `attest/` and
|
|
347
|
+
# `my_test_dir/` are NOT caught (a leading `_?test_.*\.py$` would have
|
|
348
|
+
# swallowed `src/my_test_dir/bar.py`).
|
|
349
|
+
# Must stay equivalent to TEST_PATH in scripts/ccn-report.cjs.
|
|
336
350
|
CC_FILES=$(echo "$CC_FILES" | grep -vE \
|
|
337
|
-
'^"?src/npm-package/|^"?plugins/[^/]+/src/|(^|/)
|
|
351
|
+
'^"?src/npm-package/|^"?plugins/[^/]+/src/|(^|/)__tests?__/|(^|/)(tests?)/|\.test\.[cm]?[jt]sx?$|(^|/)test_[^/]*\.py$|_test\.(py|go)$|Test(s)?\.java$' || true)
|
|
338
352
|
|
|
339
353
|
if [ -n "$CC_FILES" ]; then
|
|
340
354
|
echo "Checking complexity for source files..."
|
|
341
355
|
|
|
342
|
-
# Run lizard
|
|
343
|
-
# One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
356
|
+
# Run lizard in chunks. Two reasons, both load-bearing:
|
|
357
|
+
# - One path per line, into an array. Unquoted $CC_FILES relied on word
|
|
358
|
+
# splitting, which also splits any path containing a space -- handing
|
|
359
|
+
# lizard two bogus paths and checking neither (#457 defect class, same
|
|
360
|
+
# fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
|
|
361
|
+
# collapse the list into one argument. An array does both.
|
|
362
|
+
# - Chunked instead of one argv. Windows caps a single command line at
|
|
363
|
+
# 32,767 characters, and under `--all` the population is the whole tree
|
|
364
|
+
# (1,000+ sources on a mid-sized repo). The oversized argv never reached
|
|
365
|
+
# lizard, nothing was printed, and the gate graded an empty CSV as if the
|
|
366
|
+
# tree were clean. XP_GATE_LIZARD_ARGV_BUDGET lets a probe force a split
|
|
367
|
+
# without needing a huge fixture.
|
|
368
|
+
LIZARD_ARGV_BUDGET="${XP_GATE_LIZARD_ARGV_BUDGET:-8000}"
|
|
369
|
+
lizard_batches() {
|
|
370
|
+
# stdin: one path per line. stdout: lizard output, batches concatenated.
|
|
371
|
+
local _batch=() _size=0 _path
|
|
372
|
+
while IFS= read -r _path; do
|
|
373
|
+
[ -n "$_path" ] || continue
|
|
374
|
+
if [ "${#_batch[@]}" -gt 0 ] && [ $((_size + ${#_path} + 1)) -gt "$LIZARD_ARGV_BUDGET" ]; then
|
|
375
|
+
"$LIZARD_PATH" "$@" "${_batch[@]}" 2>/dev/null || true
|
|
376
|
+
_batch=()
|
|
377
|
+
_size=0
|
|
378
|
+
fi
|
|
379
|
+
_batch+=("$_path")
|
|
380
|
+
_size=$((_size + ${#_path} + 1))
|
|
381
|
+
done
|
|
382
|
+
if [ "${#_batch[@]}" -gt 0 ]; then
|
|
383
|
+
"$LIZARD_PATH" "$@" "${_batch[@]}" 2>/dev/null || true
|
|
384
|
+
fi
|
|
385
|
+
return 0
|
|
386
|
+
}
|
|
387
|
+
CC_OUTPUT=$(printf '%s\n' "$CC_FILES" | lizard_batches -C "$CCN_THRESHOLD" 2>&1)
|
|
353
388
|
|
|
354
389
|
# REQ-D (sprint-20261018-quality): baseline ratchet. When
|
|
355
390
|
# .ccn-baseline.json exists it replaces the flat threshold verdict:
|
|
@@ -366,7 +401,7 @@ else
|
|
|
366
401
|
# Route the CSV through the same verifier; on rows it cannot verify the
|
|
367
402
|
# helper exits non-zero, and this gate fails closed instead of passing
|
|
368
403
|
# an unvalidated number.
|
|
369
|
-
CC_CSV=$(
|
|
404
|
+
CC_CSV=$(printf '%s\n' "$CC_FILES" | lizard_batches --csv)
|
|
370
405
|
CC_REPORT="$(git rev-parse --show-toplevel 2>/dev/null)/scripts/ccn-report.cjs"
|
|
371
406
|
# Fail closed when the verifier is unavailable. Previously this fell
|
|
372
407
|
# through and graded the RAW lizard CSV, while CI hard-fails on the same
|
|
@@ -392,6 +427,20 @@ else
|
|
|
392
427
|
fi
|
|
393
428
|
RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
|
|
394
429
|
[ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
|
|
430
|
+
# Fail closed when the ratchet library is missing. Sourcing a nonexistent
|
|
431
|
+
# file only prints "No such file or directory" and leaves
|
|
432
|
+
# evaluate_ccn_ratchet undefined; the verdict then came back empty, every
|
|
433
|
+
# counter read 0 and the gate printed "✅ PASSED" — a CCN regression graded
|
|
434
|
+
# green with nothing compared. The installer never created this path
|
|
435
|
+
# (copyAdapters skipped hooks/lib), so this was the normal state after a
|
|
436
|
+
# global install, not an exotic one.
|
|
437
|
+
if [ ! -f "$RATCHET_LIB" ]; then
|
|
438
|
+
echo "❌ BLOCKED - complexity ratchet library unavailable (ccn-ratchet.sh)."
|
|
439
|
+
echo "Expected at \$GATE_DIR/lib/ccn-ratchet.sh or <repo>/githooks/lib/ccn-ratchet.sh."
|
|
440
|
+
echo "Reinstall with 'xp-gate init --global' (or 'xp-gate update-hooks') to restore it."
|
|
441
|
+
GATE_3_DETAIL="mode=ratchet ratchet=MISSING"
|
|
442
|
+
exit 1
|
|
443
|
+
fi
|
|
395
444
|
# shellcheck source=/dev/null
|
|
396
445
|
. "$RATCHET_LIB"
|
|
397
446
|
# NOTE: the thresholds must be plain assignments BEFORE the pipe. As a
|
|
@@ -436,7 +485,7 @@ else
|
|
|
436
485
|
# file,name,signature,...), so the verdict comes from the same numbers
|
|
437
486
|
# lizard uses, independent of version and table formatting. --csv here is
|
|
438
487
|
# the flat-threshold run; the ratchet branch above builds its own.
|
|
439
|
-
CC_CSV_FLAT=$("$
|
|
488
|
+
CC_CSV_FLAT=$(printf '%s\n' "$CC_FILES" | lizard_batches --csv -C "$CCN_THRESHOLD")
|
|
440
489
|
CC_WARNINGS=$(printf '%s\n' "$CC_CSV_FLAT" | awk -F, -v t="$CCN_THRESHOLD" '
|
|
441
490
|
NF > 1 && $2 + 0 > t + 0 { c++ }
|
|
442
491
|
END { print c + 0 }
|
|
@@ -56,9 +56,25 @@ enforce_python_coverage() {
|
|
|
56
56
|
return 0
|
|
57
57
|
fi
|
|
58
58
|
|
|
59
|
+
# #636: this probe is the SAME fail-open #570 closed, only earlier. Returning 0
|
|
60
|
+
# here means "no coverage.py on PATH" silently disables the Python coverage
|
|
61
|
+
# floor — and unlike a deleted .coverage file, a missing tool is a steady state,
|
|
62
|
+
# not an accident: every consumer repo / CI image / colleague machine without
|
|
63
|
+
# coverage.py had this gate switched off behind a ⚠️ that reads as informational.
|
|
64
|
+
# The #570 suite could not see it because its setup installs a coverage stub
|
|
65
|
+
# precisely so this probe succeeds.
|
|
66
|
+
#
|
|
67
|
+
# A subset run stays the one exception: nothing was measured because only part
|
|
68
|
+
# of the suite ran, so there is no total to compare.
|
|
59
69
|
if ! command -v coverage &> /dev/null; then
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ]; then
|
|
71
|
+
echo "⚠️ coverage.py not available (subset test run) — not blocking."
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
echo "❌ BLOCKED - Python coverage could not be measured: coverage.py is not on PATH,"
|
|
75
|
+
echo " so the ${COVERAGE_THRESHOLD:-80}% floor cannot be verified."
|
|
76
|
+
echo " Install it (pip install coverage) and commit again."
|
|
77
|
+
return 1
|
|
62
78
|
fi
|
|
63
79
|
|
|
64
80
|
COVERAGE_PERCENT=""
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Java smart test selection (#640) — the Java counterpart of the TypeScript
|
|
3
|
+
# selector (#286) and the Python selector (#399).
|
|
4
|
+
#
|
|
5
|
+
# Why: the Java arm of Gate 5 ran the FULL Maven/Gradle suite on every commit.
|
|
6
|
+
# Measured on a 206-file / 1079-test project: `mvn test jacoco:report` takes
|
|
7
|
+
# 77.8s no matter whether one file or a hundred changed. TypeScript and Python
|
|
8
|
+
# have had three-tier selection for a long time; Java had no equivalent — the
|
|
9
|
+
# `PARTIAL_TEST_RUN` capability existed but nothing on the Java path ever set
|
|
10
|
+
# it, so the downgrade channel was dead code for that language.
|
|
11
|
+
#
|
|
12
|
+
# Kept in its own file so the strategy truth table and the related-test lookup
|
|
13
|
+
# are unit-testable without executing the hook
|
|
14
|
+
# (githooks/__tests__/issue-640-java-test-selection.test.bats).
|
|
15
|
+
#
|
|
16
|
+
# Three tiers, matching the TS/Python implementations:
|
|
17
|
+
# 1. changed-tests — only test files changed: run exactly those.
|
|
18
|
+
# 2. related — source files changed, no test files: run the tests the
|
|
19
|
+
# path convention maps to them.
|
|
20
|
+
# 3. full — >20 test files changed, a build manifest changed, or no
|
|
21
|
+
# related tests found: run everything with the real
|
|
22
|
+
# coverage threshold.
|
|
23
|
+
# Tier 1/2 run WITHOUT the global coverage threshold (subset coverage is
|
|
24
|
+
# meaningless globally); the caller sets PARTIAL_TEST_RUN so Gate 5 warns
|
|
25
|
+
# instead of blocking.
|
|
26
|
+
|
|
27
|
+
# Max changed test files before falling back to the full suite. Same value as
|
|
28
|
+
# the TypeScript (#286) and Python (#399) selectors so all three languages
|
|
29
|
+
# behave alike.
|
|
30
|
+
JAVA_MAX_CHANGED_TESTS=20
|
|
31
|
+
|
|
32
|
+
# Build manifests: a change here can break ANY test (dependency bump, surefire
|
|
33
|
+
# config, compiler level), so the selector always escalates to the full suite.
|
|
34
|
+
JAVA_DEPENDENCY_PATTERNS='(^|/)(pom\.xml|build\.gradle(\.kts)?|settings\.gradle(\.kts)?|gradle\.properties|gradle/wrapper/[^/]+|mvnw|mvnw\.cmd|\.mvn/[^/]+)$'
|
|
35
|
+
|
|
36
|
+
# Java test CLASS-name conventions (applied to the basename only).
|
|
37
|
+
#
|
|
38
|
+
# Maven surefire's own defaults are `*Test.java`, `Test*.java`, `*Tests.java`,
|
|
39
|
+
# `*TestCase.java`; Gradle users commonly add `*Spec.java` and `*IT.java`.
|
|
40
|
+
#
|
|
41
|
+
# The `Test[A-Z]\w*` form is deliberately narrowed to `Test[A-Z]` followed by
|
|
42
|
+
# something that looks like a test subject rather than a helper: bare
|
|
43
|
+
# `Test[^/]*` would swallow `TestHelper.java`, `TestUtils.java`,
|
|
44
|
+
# `TestFixtures.java` — production/support classes that happen to start with
|
|
45
|
+
# "Test". Misclassifying a SOURCE as a TEST is the dangerous direction: those
|
|
46
|
+
# helpers are real code whose changing should run `related`, and calling them
|
|
47
|
+
# tests would make the selector run them as if they were suites (Surefire
|
|
48
|
+
# would then find no tests in them) and skip the source-driven lookup.
|
|
49
|
+
#
|
|
50
|
+
# A path under a test source root is caught independently by
|
|
51
|
+
# java_is_test_file, so helpers living in src/test/java are still tests.
|
|
52
|
+
JAVA_TEST_FILE_RE='(^|/)(Test[A-Z]\w*(Test|Tests|Case|Spec|IT)|[A-Z]\w*Test|[A-Z]\w*Tests|[A-Z]\w*TestCase|[A-Z]\w*Spec|[A-Z]\w*IT)\.java$'
|
|
53
|
+
|
|
54
|
+
# Decide the strategy from three counts. Pure function — no filesystem access,
|
|
55
|
+
# so the truth table is exhaustive and cheap to test.
|
|
56
|
+
#
|
|
57
|
+
# $1 test_count changed test files
|
|
58
|
+
# $2 src_count changed non-test Java source files
|
|
59
|
+
# $3 deps_changed 1 when a build manifest changed, else 0
|
|
60
|
+
#
|
|
61
|
+
# Echoes exactly one of: full | changed-tests | related | skip
|
|
62
|
+
select_java_test_strategy() {
|
|
63
|
+
# `${1-0}`, not `${1:-0}`: the `:-` form rewrites an EMPTY argument to "0",
|
|
64
|
+
# which would make the empty check below unreachable — a caller passing ""
|
|
65
|
+
# (e.g. an unset count that reached us as an empty string) would silently
|
|
66
|
+
# mean "0 changed files" and skip the suite. Only a genuinely UNSET argument
|
|
67
|
+
# defaults to 0.
|
|
68
|
+
local test_count="${1-0}"
|
|
69
|
+
local src_count="${2-0}"
|
|
70
|
+
local deps_changed="${3-0}"
|
|
71
|
+
|
|
72
|
+
# Non-numeric OR empty input must not silently become "0" and skip the
|
|
73
|
+
# suite: a caller bug there would run nothing and report PASS. Escalate.
|
|
74
|
+
case "$test_count" in
|
|
75
|
+
''|*[!0-9]*) printf '%s' "full"; return 0 ;;
|
|
76
|
+
esac
|
|
77
|
+
case "$src_count" in
|
|
78
|
+
''|*[!0-9]*) printf '%s' "full"; return 0 ;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
# Dependency/build changes can break anything -> always the full suite.
|
|
82
|
+
if [ "$deps_changed" = "1" ]; then
|
|
83
|
+
printf '%s' "full"
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Large batch: per-file selection loses its point, and the other two
|
|
88
|
+
# language arms use the same cutoff.
|
|
89
|
+
if [ "$test_count" -gt "$JAVA_MAX_CHANGED_TESTS" ]; then
|
|
90
|
+
printf '%s' "full"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ "$test_count" -gt 0 ]; then
|
|
95
|
+
printf '%s' "changed-tests"
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ "$src_count" -gt 0 ]; then
|
|
100
|
+
printf '%s' "related"
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
printf '%s' "skip"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# True (0) when the path looks like a Java test source.
|
|
108
|
+
#
|
|
109
|
+
# $1 path, e.g. src/test/java/com/acme/FooTest.java
|
|
110
|
+
java_is_test_file() {
|
|
111
|
+
local path="$1"
|
|
112
|
+
[ -n "$path" ] || return 1
|
|
113
|
+
# A path under a test source root is a test even when the basename does not
|
|
114
|
+
# end in Test/Tests/Spec — surefire includes are configurable, and a `*IT.java`
|
|
115
|
+
# integration test living in src/test/java must not be mistaken for source.
|
|
116
|
+
#
|
|
117
|
+
# The leading "/" matters: git reports repo-relative paths with no leading
|
|
118
|
+
# slash ("src/test/java/..."), so a bare "*/src/test/*" pattern would never
|
|
119
|
+
# match the most common case of all. Prefixing normalises both forms.
|
|
120
|
+
case "/$path" in
|
|
121
|
+
*/src/test/java/*|*/src/test/*|*/src/androidTest/*) return 0 ;;
|
|
122
|
+
esac
|
|
123
|
+
printf '%s' "$path" | grep -qE "$JAVA_TEST_FILE_RE"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# True (0) when any of the given paths is a Java build manifest.
|
|
127
|
+
java_dependency_changed() {
|
|
128
|
+
local path
|
|
129
|
+
for path in "$@"; do
|
|
130
|
+
[ -n "$path" ] || continue
|
|
131
|
+
if echo "$path" | grep -qE "$JAVA_DEPENDENCY_PATTERNS"; then
|
|
132
|
+
return 0
|
|
133
|
+
fi
|
|
134
|
+
done
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Map a Maven/Gradle source path to the test path(s) the convention implies.
|
|
139
|
+
# Echoes one existing path per line, deduplicated, in discovery order. Echoes
|
|
140
|
+
# nothing when no candidate exists — the caller then falls back to tier 3.
|
|
141
|
+
#
|
|
142
|
+
# $1 source path, e.g. src/main/java/com/acme/service/Foo.java
|
|
143
|
+
#
|
|
144
|
+
# The mapping mirrors the standard layout: src/main/java/<pkg>/X.java ->
|
|
145
|
+
# src/test/java/<pkg>/XTest.java. Multi-module builds are handled because the
|
|
146
|
+
# module prefix is preserved verbatim (only `main`->`test` inside the path is
|
|
147
|
+
# rewritten).
|
|
148
|
+
java_related_tests() {
|
|
149
|
+
local src_file="$1"
|
|
150
|
+
[ -n "$src_file" ] || return 0
|
|
151
|
+
|
|
152
|
+
local filename dir base
|
|
153
|
+
base="${src_file%.java}"
|
|
154
|
+
filename="${base##*/}"
|
|
155
|
+
dir="$(dirname "$src_file")"
|
|
156
|
+
|
|
157
|
+
local candidates=""
|
|
158
|
+
# Primary convention: same package, main -> test.
|
|
159
|
+
# "/$dir" for the same reason as java_is_test_file: git's repo-relative paths
|
|
160
|
+
# have no leading slash, so a bare "*/src/main/java/*" would never match.
|
|
161
|
+
case "/$dir" in
|
|
162
|
+
*/src/main/java/*)
|
|
163
|
+
# Rewrite the source root segment. `#` as the sed delimiter keeps the
|
|
164
|
+
# pattern readable: / would need escaping in both the pattern AND the
|
|
165
|
+
# replacement. The leading "/" is prepended BEFORE the substitution and
|
|
166
|
+
# stripped after: the pattern is anchored on "/src/main/java/", and a
|
|
167
|
+
# repo-relative dir ("src/main/java/com/acme") does not contain that
|
|
168
|
+
# leading slash, so a bare substitution would match nothing and every
|
|
169
|
+
# "related" lookup would return empty — quietly downgrading every commit
|
|
170
|
+
# to the full suite (correct, but pointless).
|
|
171
|
+
local test_dir
|
|
172
|
+
test_dir=$(printf '/%s' "$dir" | sed 's#/src/main/java/#/src/test/java/#')
|
|
173
|
+
test_dir="${test_dir#/}"
|
|
174
|
+
candidates="$candidates
|
|
175
|
+
${test_dir}/${filename}Test.java
|
|
176
|
+
${test_dir}/${filename}Tests.java
|
|
177
|
+
${test_dir}/${filename}Spec.java
|
|
178
|
+
${test_dir}/Test${filename}.java"
|
|
179
|
+
;;
|
|
180
|
+
esac
|
|
181
|
+
# Colocated forms (some projects keep tests beside sources).
|
|
182
|
+
candidates="$candidates
|
|
183
|
+
${dir}/${filename}Test.java
|
|
184
|
+
${dir}/${filename}Tests.java
|
|
185
|
+
${dir}/${filename}Spec.java
|
|
186
|
+
${dir}/__tests__/${filename}Test.java"
|
|
187
|
+
|
|
188
|
+
local seen=""
|
|
189
|
+
local candidate
|
|
190
|
+
# IFS-safe iteration over newline-separated candidates.
|
|
191
|
+
while IFS= read -r candidate; do
|
|
192
|
+
[ -n "$candidate" ] || continue
|
|
193
|
+
[ -f "$candidate" ] || continue
|
|
194
|
+
# Dedupe: several patterns resolve to the same file whenever the source
|
|
195
|
+
# already sits under a test-looking root.
|
|
196
|
+
case "$seen" in
|
|
197
|
+
*"|$candidate|"*) continue ;;
|
|
198
|
+
esac
|
|
199
|
+
seen="$seen|$candidate|"
|
|
200
|
+
printf '%s\n' "$candidate"
|
|
201
|
+
done <<EOF
|
|
202
|
+
$candidates
|
|
203
|
+
EOF
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Turn test file paths into the Maven `-Dtest=` selector list.
|
|
207
|
+
# Echoes one comma-separated argument, or nothing when the input is empty.
|
|
208
|
+
#
|
|
209
|
+
# $1..$N test file paths
|
|
210
|
+
#
|
|
211
|
+
# Maven matches `-Dtest=` against simple class names (not paths), so the
|
|
212
|
+
# directory part is dropped and the `.java` suffix stripped. Duplicates are
|
|
213
|
+
# removed: `-Dtest=A,A` makes Surefire run A twice.
|
|
214
|
+
java_maven_test_selector() {
|
|
215
|
+
local seen=" " out=""
|
|
216
|
+
local path name
|
|
217
|
+
for path in "$@"; do
|
|
218
|
+
[ -n "$path" ] || continue
|
|
219
|
+
name="${path##*/}"
|
|
220
|
+
name="${name%.java}"
|
|
221
|
+
[ -n "$name" ] || continue
|
|
222
|
+
case "$seen" in
|
|
223
|
+
*" $name "*) continue ;;
|
|
224
|
+
esac
|
|
225
|
+
seen="$seen$name "
|
|
226
|
+
if [ -z "$out" ]; then out="$name"; else out="$out,$name"; fi
|
|
227
|
+
done
|
|
228
|
+
[ -n "$out" ] && printf '%s' "$out"
|
|
229
|
+
return 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Turn test file paths into Gradle `--tests` filter arguments.
|
|
233
|
+
# Echoes one argument per line (Gradle wants a repeated flag, not a list).
|
|
234
|
+
#
|
|
235
|
+
# $1..$N test file paths
|
|
236
|
+
java_gradle_test_filters() {
|
|
237
|
+
local seen=""
|
|
238
|
+
local path name
|
|
239
|
+
for path in "$@"; do
|
|
240
|
+
[ -n "$path" ] || continue
|
|
241
|
+
name="${path##*/}"
|
|
242
|
+
name="${name%.java}"
|
|
243
|
+
[ -n "$name" ] || continue
|
|
244
|
+
case "$seen" in
|
|
245
|
+
*"|$name|"*) continue ;;
|
|
246
|
+
esac
|
|
247
|
+
seen="$seen|$name|"
|
|
248
|
+
printf '%s\n' "$name"
|
|
249
|
+
done
|
|
250
|
+
return 0
|
|
251
|
+
}
|
package/hooks/lib/jscpd-run.sh
CHANGED
|
@@ -26,6 +26,55 @@
|
|
|
26
26
|
# than before the fix.
|
|
27
27
|
JSCPD_DEFAULT_IGNORE_PATTERNS='(^|/)(node_modules|dist|build|target|coverage|out|\.next|\.nuxt|vendor)/|\.log$|\.txt$|\.(ya?ml)$|golden|baseline\.json$|evidence'
|
|
28
28
|
|
|
29
|
+
# The BUILD-OUTPUT half of the set above, alone (#649).
|
|
30
|
+
#
|
|
31
|
+
# The combined set answers "is this worth comparing for duplication", which is a
|
|
32
|
+
# question only Gate 2 asks. Filtering file list inputs with the whole set also
|
|
33
|
+
# drops *.yaml, *.yml, *.txt and *.log -- and those are perfectly real project
|
|
34
|
+
# files everywhere else. Doing that broke a live test on the first attempt:
|
|
35
|
+
# `YAML staged files activate lint and IaC but skip SAST` expects a staged
|
|
36
|
+
# infra/deployment.yaml to count as a code change, and whole-set filtering made
|
|
37
|
+
# the hook announce "No code files changed, skipping lint" for a commit that
|
|
38
|
+
# added a deployment manifest.
|
|
39
|
+
#
|
|
40
|
+
# So the two notions are separated here rather than shared:
|
|
41
|
+
#
|
|
42
|
+
# BUILD OUTPUT (this) - a directory tree that is generated or vendored:
|
|
43
|
+
# not our code for ANY gate. Used to filter file lists.
|
|
44
|
+
# NON-REFACTORABLE (the combined set) - files whose duplication is data shape:
|
|
45
|
+
# still project files, just not duplication candidates.
|
|
46
|
+
# Gate 2 only.
|
|
47
|
+
#
|
|
48
|
+
# Same directory names, one definition; a directory added to the build-output
|
|
49
|
+
# list reaches every gate at once, which is the point of #649.
|
|
50
|
+
# Directory names that are generated or vendored in any mainstream toolchain.
|
|
51
|
+
#
|
|
52
|
+
# The second group (caches, virtualenvs, Python bytecode) is here because the
|
|
53
|
+
# first version of this list did not have it and the same class of bug was one
|
|
54
|
+
# `git add -A` away: __pycache__ is written by CPython itself and .pytest_cache
|
|
55
|
+
# by pytest, so a Python project that commits its workspace stages .pyc-adjacent
|
|
56
|
+
# artefacts and every scanner would read them as source. None of these can hold
|
|
57
|
+
# hand-written project code.
|
|
58
|
+
#
|
|
59
|
+
# NOT included on purpose: bin/ and obj/ (real source trees in plenty of
|
|
60
|
+
# projects -- e.g. cmd/bin layout, C# projects where obj/ IS output but bin/ is
|
|
61
|
+
# often committed scripts) and .venv/venv (genuinely ambiguous: some projects
|
|
62
|
+
# keep a small committed venv for reproducibility). Adding a name here removes
|
|
63
|
+
# it from EVERY gate at once, so the list stays the names that are unambiguous.
|
|
64
|
+
JSCPD_BUILD_OUTPUT_PATTERNS='(^|/)(node_modules|dist|build|target|coverage|out|\.next|\.nuxt|vendor|__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.tox|\.turbo|\.cache)/'
|
|
65
|
+
|
|
66
|
+
# Is this path inside a dependency tree or a build-output directory?
|
|
67
|
+
#
|
|
68
|
+
# XP_GATE_JSCPD_DEFAULT_IGNORE=0 still disables it, so the escape hatch keeps
|
|
69
|
+
# covering "some project really does want its vendor/ tree linted".
|
|
70
|
+
jscpd_is_build_output() {
|
|
71
|
+
local _path="$1"
|
|
72
|
+
if [ "${XP_GATE_JSCPD_DEFAULT_IGNORE:-1}" = "0" ]; then
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
printf '%s\n' "$_path" | grep -qE "$JSCPD_BUILD_OUTPUT_PATTERNS"
|
|
76
|
+
}
|
|
77
|
+
|
|
29
78
|
# Human-readable form, printed by the gate so the baseline is auditable (#591
|
|
30
79
|
# point 3). Silence about the default is the same defect as silence about a
|
|
31
80
|
# skipped gate: the user cannot answer "what did it scan".
|
|
@@ -38,6 +87,15 @@ jscpd_default_ignore_patterns() {
|
|
|
38
87
|
# XP_GATE_JSCPD_DEFAULT_IGNORE=0 disables the set entirely: a project that
|
|
39
88
|
# genuinely wants YAML compared must be able to say so, or the default would make
|
|
40
89
|
# some real duplicates unreachable.
|
|
90
|
+
#
|
|
91
|
+
# SCOPE CHANGED IN #648: Gate 1's debugger check now calls this too, so the
|
|
92
|
+
# switch governs "what counts as our code" for BOTH gates — not just jscpd.
|
|
93
|
+
# Setting it to 0 makes Gate 1 scan dependencies and build output for debugger
|
|
94
|
+
# statements as well, which is rarely what someone disabling duplicate detection
|
|
95
|
+
# intends. Kept as one switch deliberately: two gates disagreeing about what a
|
|
96
|
+
# build artifact is would be its own defect, and a project that vendors code
|
|
97
|
+
# wants it treated consistently everywhere. Override per-run rather than
|
|
98
|
+
# permanently if you only meant to widen Gate 2's scan.
|
|
41
99
|
jscpd_is_default_ignored() {
|
|
42
100
|
local _path="$1"
|
|
43
101
|
if [ "${XP_GATE_JSCPD_DEFAULT_IGNORE:-1}" = "0" ]; then
|
|
@@ -46,6 +104,49 @@ jscpd_is_default_ignored() {
|
|
|
46
104
|
printf '%s\n' "$_path" | grep -qE "$JSCPD_DEFAULT_IGNORE_PATTERNS"
|
|
47
105
|
}
|
|
48
106
|
|
|
107
|
+
# Drop dependency and build-output paths from a newline-separated file list on
|
|
108
|
+
# stdin, echoing what remains (#649).
|
|
109
|
+
#
|
|
110
|
+
# Every scanner that builds its own list from `git diff --cached` used to
|
|
111
|
+
# re-derive the exclusions by hand, and they drifted. The test-pairing check
|
|
112
|
+
# excluded node_modules/.next/.nuxt/dist/build/.turbo/.cache but not vendor,
|
|
113
|
+
# target, coverage or out -- so a staged `vendor/x.ts` was reported as a new
|
|
114
|
+
# source file with no test. Six more sites had no exclusions at all. The one
|
|
115
|
+
# place that got a shared ignore set (Gate 2) is the one that stopped having
|
|
116
|
+
# this bug.
|
|
117
|
+
#
|
|
118
|
+
# This is the shared answer: callers pipe their staged list through it and stop
|
|
119
|
+
# maintaining their own list. It reads stdin and writes stdout so it composes
|
|
120
|
+
# with whatever selection the caller already did:
|
|
121
|
+
#
|
|
122
|
+
# NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A \
|
|
123
|
+
# | filter_project_paths | grep -E '\.(ts|tsx)$' || true)
|
|
124
|
+
#
|
|
125
|
+
# Deliberately a filter, not a generator: a caller that needs a different git
|
|
126
|
+
# query (only-added, only-modified) keeps it, and only the exclusion rule is
|
|
127
|
+
# shared. Fail-open on empty input so `|| true` pipelines stay quiet.
|
|
128
|
+
#
|
|
129
|
+
# ON THE COUPLING (Delphi round 1, MC-01/FC-01): yes, this makes Gate 2's
|
|
130
|
+
# ignore set load-bearing for every gate. Widening it now widens what every
|
|
131
|
+
# scanner skips. That risk is accepted deliberately, and the alternative is
|
|
132
|
+
# worse: the bug this fixes WAS a set of per-scanner definitions that drifted
|
|
133
|
+
# apart, so N independent sets would reproduce it N times. The guard against
|
|
134
|
+
# silent widening is that every change to the set lands in one reviewable
|
|
135
|
+
# place, and #649's tests pin the boundary cases (vendor/target/coverage/out
|
|
136
|
+
# ignored; src/lib/docs kept). If a gate ever needs a genuinely different
|
|
137
|
+
# notion of "our code", introduce a second named set HERE rather than letting a
|
|
138
|
+
# caller edit this one.
|
|
139
|
+
filter_project_paths() {
|
|
140
|
+
local _fp
|
|
141
|
+
while IFS= read -r _fp || [ -n "$_fp" ]; do
|
|
142
|
+
[ -n "$_fp" ] || continue
|
|
143
|
+
if ! jscpd_is_build_output "$_fp"; then
|
|
144
|
+
printf '%s\n' "$_fp"
|
|
145
|
+
fi
|
|
146
|
+
done
|
|
147
|
+
return 0
|
|
148
|
+
}
|
|
149
|
+
|
|
49
150
|
# Echo the jscpd config the gate should pass, or nothing when the project has
|
|
50
151
|
# none.
|
|
51
152
|
#
|