@boyingliu01/xp-gate 0.31.4 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/adapters/java.sh +256 -4
  2. package/adapters/powershell.sh +45 -6
  3. package/bin/xp-gate.js +8 -2
  4. package/build-integrity/__tests__/gate-10.test.ts +88 -1
  5. package/build-integrity/gate-10.ts +51 -16
  6. package/dsh.bundle.patch.yml +15 -0
  7. package/gate-13-compiler-strictness.sh +1031 -0
  8. package/gate-3.sh +48 -13
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +48 -13
  11. package/hooks/lib/coverage-total.sh +18 -2
  12. package/hooks/lib/java-test-selection.sh +251 -0
  13. package/hooks/lib/jscpd-run.sh +9 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +149 -2
  16. package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
  17. package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
  18. package/lib/__tests__/gate-runner.test.js +29 -2
  19. package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
  20. package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
  21. package/lib/__tests__/shared-utils.test.js +21 -0
  22. package/lib/check.js +6 -2
  23. package/lib/detect-deps.js +42 -10
  24. package/lib/doctor-tui.js +111 -21
  25. package/lib/gate-runner.js +111 -37
  26. package/lib/mattpocock-license-verdict.json +1 -1
  27. package/lib/principles.js +48 -6
  28. package/lib/shared-utils.js +8 -0
  29. package/mock-policy/AGENTS.md +3 -3
  30. package/mock-policy/gate-m3.ts +57 -23
  31. package/mutation/AGENTS.md +3 -3
  32. package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
  33. package/mutation/__tests__/test-filter-parity.test.ts +5 -2
  34. package/mutation/gate-m.ts +30 -24
  35. package/mutation/runners/mutmut-runner.ts +80 -30
  36. package/mutation/runners/pitest-runner.ts +60 -42
  37. package/package.json +14 -2
  38. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  39. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/dsh/lib/command.d.ts +38 -0
  43. package/plugins/dsh/lib/command.d.ts.map +1 -0
  44. package/plugins/dsh/lib/command.js +87 -0
  45. package/plugins/dsh/lib/credentials.d.ts +39 -0
  46. package/plugins/dsh/lib/credentials.d.ts.map +1 -0
  47. package/plugins/dsh/lib/credentials.js +57 -0
  48. package/plugins/dsh/lib/delphi-config.d.ts +129 -0
  49. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  50. package/plugins/dsh/lib/delphi-config.js +369 -0
  51. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  52. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  53. package/plugins/dsh/lib/delphi-run.js +113 -0
  54. package/plugins/dsh/lib/delphi.d.ts +94 -0
  55. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  56. package/plugins/dsh/lib/delphi.js +207 -0
  57. package/plugins/dsh/lib/dialect.d.ts +24 -0
  58. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  59. package/plugins/dsh/lib/dialect.js +36 -0
  60. package/plugins/dsh/lib/evidence.d.ts +70 -0
  61. package/plugins/dsh/lib/evidence.d.ts.map +1 -0
  62. package/plugins/dsh/lib/evidence.js +92 -0
  63. package/plugins/dsh/lib/gate-runner.d.ts +49 -0
  64. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  65. package/plugins/dsh/lib/gate-runner.js +95 -0
  66. package/plugins/dsh/lib/git-context.d.ts +26 -0
  67. package/plugins/dsh/lib/git-context.d.ts.map +1 -0
  68. package/plugins/dsh/lib/git-context.js +30 -0
  69. package/plugins/dsh/lib/hooks.d.ts +42 -0
  70. package/plugins/dsh/lib/hooks.d.ts.map +1 -0
  71. package/plugins/dsh/lib/hooks.js +113 -0
  72. package/plugins/dsh/lib/index.d.ts +20 -0
  73. package/plugins/dsh/lib/index.d.ts.map +1 -0
  74. package/plugins/dsh/lib/index.js +430 -0
  75. package/plugins/dsh/lib/skill-provider.d.ts +15 -0
  76. package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
  77. package/plugins/dsh/lib/skill-provider.js +205 -0
  78. package/plugins/dsh/lib/sprint.d.ts +60 -0
  79. package/plugins/dsh/lib/sprint.d.ts.map +1 -0
  80. package/plugins/dsh/lib/sprint.js +92 -0
  81. package/plugins/dsh/package.json +14 -8
  82. package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/plugins/dsh/src/command-single-form.test.ts +67 -0
  87. package/plugins/dsh/src/command.test.ts +21 -17
  88. package/plugins/dsh/src/command.ts +19 -21
  89. package/plugins/dsh/src/compat.test.ts +200 -0
  90. package/plugins/dsh/src/contract.test.ts +7 -1
  91. package/plugins/dsh/src/credentials.test.ts +93 -0
  92. package/plugins/dsh/src/credentials.ts +71 -0
  93. package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
  94. package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
  95. package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
  96. package/plugins/dsh/src/delphi-config.ts +146 -53
  97. package/plugins/dsh/src/delphi-run.ts +27 -4
  98. package/plugins/dsh/src/delphi.ts +133 -65
  99. package/plugins/dsh/src/evidence.test.ts +74 -0
  100. package/plugins/dsh/src/evidence.ts +141 -0
  101. package/plugins/dsh/src/gate-runner.test.ts +14 -6
  102. package/plugins/dsh/src/gate-runner.ts +68 -5
  103. package/plugins/dsh/src/git-context.test.ts +39 -0
  104. package/plugins/dsh/src/git-context.ts +38 -0
  105. package/plugins/dsh/src/hooks.test.ts +87 -0
  106. package/plugins/dsh/src/hooks.ts +126 -0
  107. package/plugins/dsh/src/index.test.ts +63 -6
  108. package/plugins/dsh/src/index.ts +340 -13
  109. package/plugins/dsh/src/skill-provider.test.ts +194 -0
  110. package/plugins/dsh/src/skill-provider.ts +231 -0
  111. package/plugins/dsh/src/sprint.test.ts +64 -0
  112. package/plugins/dsh/src/sprint.ts +122 -0
  113. package/plugins/dsh/src/tri-state.test.ts +123 -0
  114. package/plugins/opencode/index.ts +192 -58
  115. package/plugins/opencode/package.json +1 -1
  116. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  118. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  119. package/plugins/qoder/plugin.json +1 -1
  120. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  122. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  123. package/principles/AGENTS.md +3 -3
  124. package/principles/__tests__/analyze-read-failure.test.ts +70 -0
  125. package/principles/__tests__/baseline-prune.test.ts +61 -0
  126. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  127. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  128. package/principles/adapters/java.ts +22 -3
  129. package/principles/adapters/typescript.ts +70 -29
  130. package/principles/analyzer.ts +36 -3
  131. package/principles/baseline-prune.ts +19 -7
  132. package/principles/config.ts +56 -35
  133. package/principles/index.ts +122 -40
  134. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  135. package/principles/rules/clean-code/unused-imports.ts +30 -19
  136. package/principles/rules/solid/dip.ts +30 -20
  137. package/principles/rules/solid/lsp.ts +17 -2
  138. package/principles/rules/solid/ocp.ts +33 -23
  139. package/principles/rules/solid/srp.ts +65 -23
  140. package/scripts/delphi-external-review.cjs +25 -2
  141. package/scripts/sync-package-content.js +28 -1
  142. package/skills/delphi-review/AGENTS.md +3 -3
  143. package/skills/sprint-flow/AGENTS.md +3 -3
  144. package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/gate-3.sh CHANGED
@@ -339,17 +339,38 @@ else
339
339
  if [ -n "$CC_FILES" ]; then
340
340
  echo "Checking complexity for source files..."
341
341
 
342
- # Run lizard with CCN threshold
343
- # One path per line, into an array. Unquoted $CC_FILES relied on word
344
- # splitting, which also splits any path containing a space -- handing
345
- # lizard two bogus paths and checking neither (#457 defect class, same
346
- # fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
347
- # collapse the list into one argument. An array does both.
348
- CC_FILES_ARGS=()
349
- while IFS= read -r _cc_file; do
350
- [ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
351
- done <<< "$CC_FILES"
352
- CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
342
+ # Run lizard in chunks. Two reasons, both load-bearing:
343
+ # - One path per line, into an array. Unquoted $CC_FILES relied on word
344
+ # splitting, which also splits any path containing a space -- handing
345
+ # lizard two bogus paths and checking neither (#457 defect class, same
346
+ # fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
347
+ # collapse the list into one argument. An array does both.
348
+ # - Chunked instead of one argv. Windows caps a single command line at
349
+ # 32,767 characters, and under `--all` the population is the whole tree
350
+ # (1,000+ sources on a mid-sized repo). The oversized argv never reached
351
+ # lizard, nothing was printed, and the gate graded an empty CSV as if the
352
+ # tree were clean. XP_GATE_LIZARD_ARGV_BUDGET lets a probe force a split
353
+ # without needing a huge fixture.
354
+ LIZARD_ARGV_BUDGET="${XP_GATE_LIZARD_ARGV_BUDGET:-8000}"
355
+ lizard_batches() {
356
+ # stdin: one path per line. stdout: lizard output, batches concatenated.
357
+ local _batch=() _size=0 _path
358
+ while IFS= read -r _path; do
359
+ [ -n "$_path" ] || continue
360
+ if [ "${#_batch[@]}" -gt 0 ] && [ $((_size + ${#_path} + 1)) -gt "$LIZARD_ARGV_BUDGET" ]; then
361
+ "$LIZARD_PATH" "$@" "${_batch[@]}" 2>/dev/null || true
362
+ _batch=()
363
+ _size=0
364
+ fi
365
+ _batch+=("$_path")
366
+ _size=$((_size + ${#_path} + 1))
367
+ done
368
+ if [ "${#_batch[@]}" -gt 0 ]; then
369
+ "$LIZARD_PATH" "$@" "${_batch[@]}" 2>/dev/null || true
370
+ fi
371
+ return 0
372
+ }
373
+ CC_OUTPUT=$(printf '%s\n' "$CC_FILES" | lizard_batches -C "$CCN_THRESHOLD" 2>&1)
353
374
 
354
375
  # REQ-D (sprint-20261018-quality): baseline ratchet. When
355
376
  # .ccn-baseline.json exists it replaces the flat threshold verdict:
@@ -366,7 +387,7 @@ else
366
387
  # Route the CSV through the same verifier; on rows it cannot verify the
367
388
  # helper exits non-zero, and this gate fails closed instead of passing
368
389
  # an unvalidated number.
369
- CC_CSV=$("$LIZARD_PATH" --csv ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>/dev/null || true)
390
+ CC_CSV=$(printf '%s\n' "$CC_FILES" | lizard_batches --csv)
370
391
  CC_REPORT="$(git rev-parse --show-toplevel 2>/dev/null)/scripts/ccn-report.cjs"
371
392
  # Fail closed when the verifier is unavailable. Previously this fell
372
393
  # through and graded the RAW lizard CSV, while CI hard-fails on the same
@@ -392,6 +413,20 @@ else
392
413
  fi
393
414
  RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
394
415
  [ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
416
+ # Fail closed when the ratchet library is missing. Sourcing a nonexistent
417
+ # file only prints "No such file or directory" and leaves
418
+ # evaluate_ccn_ratchet undefined; the verdict then came back empty, every
419
+ # counter read 0 and the gate printed "✅ PASSED" — a CCN regression graded
420
+ # green with nothing compared. The installer never created this path
421
+ # (copyAdapters skipped hooks/lib), so this was the normal state after a
422
+ # global install, not an exotic one.
423
+ if [ ! -f "$RATCHET_LIB" ]; then
424
+ echo "❌ BLOCKED - complexity ratchet library unavailable (ccn-ratchet.sh)."
425
+ echo "Expected at \$GATE_DIR/lib/ccn-ratchet.sh or <repo>/githooks/lib/ccn-ratchet.sh."
426
+ echo "Reinstall with 'xp-gate init --global' (or 'xp-gate update-hooks') to restore it."
427
+ GATE_3_DETAIL="mode=ratchet ratchet=MISSING"
428
+ exit 1
429
+ fi
395
430
  # shellcheck source=/dev/null
396
431
  . "$RATCHET_LIB"
397
432
  # NOTE: the thresholds must be plain assignments BEFORE the pipe. As a
@@ -436,7 +471,7 @@ else
436
471
  # file,name,signature,...), so the verdict comes from the same numbers
437
472
  # lizard uses, independent of version and table formatting. --csv here is
438
473
  # the flat-threshold run; the ratchet branch above builds its own.
439
- CC_CSV_FLAT=$("$LIZARD_PATH" --csv -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>/dev/null || true)
474
+ CC_CSV_FLAT=$(printf '%s\n' "$CC_FILES" | lizard_batches --csv -C "$CCN_THRESHOLD")
440
475
  CC_WARNINGS=$(printf '%s\n' "$CC_CSV_FLAT" | awk -F, -v t="$CCN_THRESHOLD" '
441
476
  NF > 1 && $2 + 0 > t + 0 { c++ }
442
477
  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
- echo "⚠️ coverage.py not available, cannot enforce Python coverage threshold."
61
- return 0
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
+ }
@@ -38,6 +38,15 @@ jscpd_default_ignore_patterns() {
38
38
  # XP_GATE_JSCPD_DEFAULT_IGNORE=0 disables the set entirely: a project that
39
39
  # genuinely wants YAML compared must be able to say so, or the default would make
40
40
  # some real duplicates unreachable.
41
+ #
42
+ # SCOPE CHANGED IN #648: Gate 1's debugger check now calls this too, so the
43
+ # switch governs "what counts as our code" for BOTH gates — not just jscpd.
44
+ # Setting it to 0 makes Gate 1 scan dependencies and build output for debugger
45
+ # statements as well, which is rarely what someone disabling duplicate detection
46
+ # intends. Kept as one switch deliberately: two gates disagreeing about what a
47
+ # build artifact is would be its own defect, and a project that vendors code
48
+ # wants it treated consistently everywhere. Override per-run rather than
49
+ # permanently if you only meant to widen Gate 2's scan.
41
50
  jscpd_is_default_ignored() {
42
51
  local _path="$1"
43
52
  if [ "${XP_GATE_JSCPD_DEFAULT_IGNORE:-1}" = "0" ]; then