@boyingliu01/xp-gate 0.34.0 → 0.35.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 (37) hide show
  1. package/gate-13-compiler-strictness.sh +129 -4
  2. package/gate-3.sh +46 -2
  3. package/gate-4.sh +28 -4
  4. package/hooks/gate-13-compiler-strictness.sh +129 -4
  5. package/hooks/gate-3.sh +46 -2
  6. package/hooks/gate-4.sh +28 -4
  7. package/hooks/lib/ccn-ratchet.sh +109 -0
  8. package/lib/__tests__/doctor-timeout-partial.test.js +79 -0
  9. package/lib/__tests__/foreign-hooks-global.test.js +125 -0
  10. package/lib/__tests__/uninstall-global-residue.test.js +160 -0
  11. package/lib/doctor.js +130 -3
  12. package/lib/mattpocock-license-verdict.json +1 -1
  13. package/lib/uninstall.js +225 -1
  14. package/mock-policy/AGENTS.md +2 -2
  15. package/mutation/AGENTS.md +2 -2
  16. package/package.json +1 -1
  17. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  18. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  19. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  20. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  21. package/plugins/dsh/package.json +1 -1
  22. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  23. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  24. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  25. package/plugins/opencode/package.json +1 -1
  26. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  27. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  28. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  29. package/plugins/qoder/plugin.json +1 -1
  30. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  31. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  32. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  33. package/principles/AGENTS.md +2 -2
  34. package/principles/rules/solid/dip.ts +94 -14
  35. package/skills/delphi-review/AGENTS.md +2 -2
  36. package/skills/sprint-flow/AGENTS.md +2 -2
  37. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -501,6 +501,108 @@ gate13_javac_args() {
501
501
  printf '%s\n' "-Xlint:all,-processing -d"
502
502
  }
503
503
 
504
+ # ---------------------------------------------------------------------------
505
+ # AC-670-34 / AC-670-35 (#659) — counting javac lint findings, locale-proof.
506
+ #
507
+ # The previous detector was an allow-list of English `[category]` names applied to
508
+ # `[WARNING]` lines. Two independent ways it returned 0 on defective code:
509
+ #
510
+ # 1. NOVELTY: a JDK adds a category the list does not name (`future-category`),
511
+ # or the fix relies on the list staying in sync with javac. A fixed allow-list
512
+ # over an open enum is a fail-OPEN design.
513
+ # 2. LOCALISATION: on a non-English build javac's own hint drifts, and the
514
+ # `[WARNING]` prefix may too. Measured on a zh_CN Maven build, the tell is
515
+ # "使用了未经检查或不安全的操作 ... 请使用 -Xlint:unchecked 重新编译" -- a message
516
+ # javac emits precisely when -Xlint was NOT in effect.
517
+ #
518
+ # Fail-closed policy (#499/#570) applied to text matching: count any bracketed
519
+ # lowercase category (whatever it is named), and treat javac's "you should have
520
+ # used -Xlint" hint as a finding too -- the code has an unchecked/unsafe operation
521
+ # whether or not the flag that would name it was on.
522
+ #
523
+ # Prints a single integer. Never fails (always exits 0).
524
+ # ---------------------------------------------------------------------------
525
+ gate13_count_lint_findings() {
526
+ local src="${1:-}"
527
+ [ -n "$src" ] || src=$(cat)
528
+ printf '%s\n' "$src" | awk '
529
+ BEGIN { n = 0 }
530
+ # Any bracketed lowercase category: covers the whole javac -Xlint enum and any
531
+ # later addition to it. Restricted to lowercase/digit/hyphen so a Maven
532
+ # `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
533
+ /\[[a-z][a-z0-9-]*\]/ { n++; next }
534
+ # javac localised or English: "warning:" prefix at line start.
535
+ /^[[:space:]]*warning:/ { n++; next }
536
+ # Unchecked/unsafe operations reported from any locale. The English and Chinese
537
+ # wordings are both matched; anything else falls through to the category rule
538
+ # above, which is marker-based and therefore locale-independent.
539
+ /unchecked|unsafe|未经检查|不安全/ { n++; next }
540
+ END { print n + 0 }
541
+ '
542
+ return 0
543
+ }
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # AC-670-32 (#659) — PROVE that -Xlint takes effect, do not assume it.
547
+ #
548
+ # "Can assemble the flag" is not "the flag works". `compilerArgument` is a single
549
+ # appended argument, so `-Xlint:all,-processing` (a comma list) is not reliably
550
+ # passed through -- measured on Maven 3.9.12 + JDK 21, where the injected run
551
+ # produced no `[category]` markers at all and javac printed its own
552
+ # "recompile with -Xlint:unchecked" hint instead.
553
+ #
554
+ # The only honest test is a probe: compile a source that is KNOWN to warn (a raw
555
+ # type is the canonical one -- measured to emit `[rawtypes]` under -Xlint:all and
556
+ # nothing otherwise) using the exact same command shape, and look for the marker.
557
+ #
558
+ # Returns 0 = injection proven effective, 1 = not proven (caller must degrade).
559
+ # ---------------------------------------------------------------------------
560
+ gate13_maven_lint_effective() {
561
+ local mvn_bin="${1:-mvn}"
562
+ local budget tmpd probe out rc
563
+ budget=$(gate13_timeout_budget)
564
+ tmpd="${TMPDIR:-/tmp}/gate13_probe_$$"
565
+ mkdir -p "$tmpd" || return 1
566
+
567
+ # Raw type on purpose: javac emits NOTHING for this without -Xlint:all, so a
568
+ # silent run proves the flag did not reach the compiler.
569
+ probe="$tmpd/Gate13Probe.java"
570
+ cat > "$probe" <<'PROBE'
571
+ import java.util.ArrayList;
572
+ import java.util.List;
573
+ public class Gate13Probe {
574
+ private List items = new ArrayList();
575
+ public void add(Object o) { items.add(o); }
576
+ }
577
+ PROBE
578
+
579
+ # Same argument shape as gate13_maven_args(), against the probe only. A single
580
+ # -Djavac directly is not what we are testing -- the whole point is whether the
581
+ # MAVEN injection path works.
582
+ if command -v timeout >/dev/null 2>&1; then
583
+ out=$(timeout "$budget" javac -Xlint:all -d "$tmpd" "$probe" 2>&1)
584
+ else
585
+ out=$(javac -Xlint:all -d "$tmpd" "$probe" 2>&1)
586
+ fi
587
+ rc=$?
588
+ rm -rf "$tmpd"
589
+
590
+ # If the DIRECT javac probe cannot even produce a rawtypes warning, the JDK
591
+ # itself is not behaving as measured, so nothing about the Maven path can be
592
+ # concluded -- report "not proven" rather than "effective".
593
+ if [ "$rc" -ne 0 ]; then
594
+ return 1
595
+ fi
596
+ printf '%s\n' "$out" | grep -q 'rawtypes'
597
+ }
598
+
599
+ # Is `-Xlint` reachable at all on this machine? The Maven probe only makes sense
600
+ # when a raw-type warning is obtainable; when javac cannot produce one, callers
601
+ # stay in degraded mode instead of claiming a clean lint.
602
+ gate13_maven_probe_available() {
603
+ command -v javac >/dev/null 2>&1
604
+ }
605
+
504
606
  gate13_java_sources() {
505
607
  git diff --cached --name-only --diff-filter=d -- '*.java' 2>/dev/null
506
608
  }
@@ -574,6 +676,23 @@ gate13_maven_injection_reliable() {
574
676
  return 1
575
677
  fi
576
678
 
679
+ # AC-670-33 (#659): every check above verifies that the arguments can be
680
+ # ASSEMBLED. None of them verifies that they take EFFECT, which is exactly how a
681
+ # pom declaring plugin 3.13.0 was declared "reliable" while Maven ran with no
682
+ # -Xlint at all and Gate 13 reported PASS on a file javac warned about 3 times.
683
+ #
684
+ # So prove it: one probe compile of a known-raw-type source through the same
685
+ # command shape. If the marker does not appear, injection did not happen and the
686
+ # caller must degrade -- "could not verify" is never "verified fine" (#499/#570).
687
+ if ! gate13_maven_probe_available; then
688
+ printf 'javac unavailable; -Xlint injection cannot be probed\n' >&2
689
+ return 1
690
+ fi
691
+ if ! gate13_maven_lint_effective "${MVN_BIN:-mvn}"; then
692
+ printf 'probe compile did not produce a [rawtypes] marker; -Xlint injection is NOT effective\n' >&2
693
+ return 1
694
+ fi
695
+
577
696
  return 0
578
697
  }
579
698
 
@@ -704,14 +823,20 @@ gate13_run_java_analysis() {
704
823
  fi
705
824
 
706
825
  # Count javac's lint output. Maven prefixes it with "[WARNING] ".
707
- mvn_findings=$(printf '%s\n' "$mvn_out" \
708
- | grep -cE '\[WARNING\].*\[(rawtypes|unchecked|deprecation|fallthrough|finally|cast|static|try|overrides|overloads|serial|synchronization|varargs|divzero|empty|exports|module|opens|options|path|processing|removal|requires|text-blocks|this-escape)\]' \
709
- | tr -d '[:space:]')
826
+ #
827
+ # #659: this used to be an English `[WARNING]...[allow-listed-category]`
828
+ # regex. Applied to a localised build -- or to a JDK category the list does
829
+ # not name -- it returned 0 on code javac had warned about, and the gate then
830
+ # printed PASSED. The shared counter below is marker- and locale-proof and
831
+ # fails closed (#499/#570). It lives in one place so the Maven path and the
832
+ # direct-javac path can never disagree about what counts as a finding.
833
+ mvn_findings=$(gate13_count_lint_findings "$mvn_out")
710
834
  mvn_findings=${mvn_findings:-0}
835
+ case "$mvn_findings" in ''|*[!0-9]*) mvn_findings=0 ;; esac
711
836
 
712
837
  if [ "$mvn_findings" -gt 0 ]; then
713
838
  printf '%s\n' "$mvn_out" \
714
- | grep -E '\[WARNING\].*\[[a-z-]+\]' | head -10 | sed 's/^/ /'
839
+ | grep -E '\[[a-z][a-z0-9-]*\]|warning:' | head -10 | sed 's/^/ /'
715
840
  echo " ❌ ${mvn_findings} javac -Xlint finding(s)"
716
841
  return "$GATE13_FAIL"
717
842
  fi
package/gate-3.sh CHANGED
@@ -461,6 +461,52 @@ else
461
461
  GATE_3_DETAIL="mode=ratchet verifier=MISSING"
462
462
  exit 1
463
463
  fi
464
+ RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
465
+ [ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
466
+ # #617: grade exactly the population the baseline was written from.
467
+ #
468
+ # The comment below has always said so, but only as a comment: a repo whose
469
+ # baseline came from a narrower scan (a Java-only ccn-report, say) had its
470
+ # vendored minified JS graded anyway, and --csv-verified then disagreed with
471
+ # lizard on single-line code and fail-closed with "could not be
472
+ # span-verified". That BLOCK has no repair: the file has no baseline entry
473
+ # to ratchet, so there is nothing the user could add to clear it.
474
+ #
475
+ # The population filter runs BEFORE verification on purpose. Domain
476
+ # restriction is decidable from the path alone, so asking the expensive
477
+ # span re-derivation about a file that will not be graded anyway only
478
+ # manufactures failures for code the ratchet never claims to cover.
479
+ if [ -f "$RATCHET_LIB" ]; then
480
+ # shellcheck source=/dev/null
481
+ . "$RATCHET_LIB" 2>/dev/null || true
482
+ if declare -F ccn_file_in_population >/dev/null 2>&1; then
483
+ _cc_pop_kept=""
484
+ _cc_pop_skipped=""
485
+ while IFS= read -r _cc_row; do
486
+ [ -n "$_cc_row" ] || continue
487
+ _cc_row_path=$(printf '%s' "$_cc_row" | awk -F, '{print $1}')
488
+ if ccn_file_in_population ".ccn-baseline.json" "$_cc_row_path"; then
489
+ _cc_pop_kept="${_cc_pop_kept}${_cc_row}
490
+ "
491
+ else
492
+ _cc_pop_skipped="${_cc_pop_skipped}${_cc_row_path}
493
+ "
494
+ fi
495
+ done <<CC_POP_EOF
496
+ $CC_CSV
497
+ CC_POP_EOF
498
+ CC_CSV=$(printf '%s' "$_cc_pop_kept")
499
+ if [ -n "$_cc_pop_skipped" ]; then
500
+ _cc_pop_skip_n=$(printf '%s' "$_cc_pop_skipped" | grep -c . || true)
501
+ echo "ℹ️ Gate 3: ${_cc_pop_skip_n:-0} file(s) outside the baseline population — not graded, not ratcheted:"
502
+ printf '%s' "$_cc_pop_skipped" | sed 's/^/ /' | sed -n '1,5p'
503
+ if [ "${_cc_pop_skip_n:-0}" -gt 5 ]; then
504
+ echo " ... and $((_cc_pop_skip_n - 5)) more"
505
+ fi
506
+ echo " (declare or widen the population in .ccn-baseline.json to grade them)"
507
+ fi
508
+ fi
509
+ fi
464
510
  if [ -f "$CC_REPORT" ] && command -v node >/dev/null 2>&1; then
465
511
  _cc_verify_failed=0
466
512
  CC_CSV_VERIFIED=$(printf '%s\n' "$CC_CSV" | node "$CC_REPORT" --csv-verified batch --root "$(git rev-parse --show-toplevel 2>/dev/null)") || _cc_verify_failed=1
@@ -471,8 +517,6 @@ else
471
517
  fi
472
518
  CC_CSV="$CC_CSV_VERIFIED"
473
519
  fi
474
- RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
475
- [ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
476
520
  # Fail closed when the ratchet library is missing. Sourcing a nonexistent
477
521
  # file only prints "No such file or directory" and leaves
478
522
  # evaluate_ccn_ratchet undefined; the verdict then came back empty, every
package/gate-4.sh CHANGED
@@ -46,7 +46,14 @@ java_coverage_note() {
46
46
  esac
47
47
  done
48
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)"
49
+ # #628: `docs/` is not in the npm package's `files` list, so pointing a user at
50
+ # `docs/java-principles-coverage.md` was a dead path in every installed project --
51
+ # the note told them to read a file that does not exist on their machine. Cite the
52
+ # upstream URL instead, and keep the local path only when the doc is actually
53
+ # present (i.e. when running inside this repo).
54
+ local _where="https://github.com/boyingliu01/xp-gate/blob/main/docs/java-principles-coverage.md"
55
+ [ -f "docs/java-principles-coverage.md" ] && _where="docs/java-principles-coverage.md"
56
+ echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see $_where)"
50
57
  }
51
58
 
52
59
  2>&1 echo ""
@@ -165,9 +172,26 @@ else
165
172
  rm -f "$PRINCIPLES_STDERR"
166
173
 
167
174
  if [ "$PRINCIPLES_EXIT" -ge 2 ]; then
168
- echo "⚠️ Warning: Principles checker execution failed"
169
- echo "⏭️ SKIPPED - Principles check (execution issue)"
170
- GATE_4_STATUS="SKIP"
175
+ # #661: exit>=2 means the checker did not RUN (crash, module not found,
176
+ # bad invocation). This used to record SKIP, and pre-commit treats SKIP
177
+ # as non-blocking, so a tool failure was absorbed into the green light --
178
+ # the reproduction printed a module-not-found stack trace and then
179
+ # "✅ PASSED - Principles checker (no errors found)".
180
+ #
181
+ # The distinction the gate must keep:
182
+ # - tool NOT INSTALLED -> SKIP is honest; nothing is being claimed.
183
+ # - tool PRESENT, CRASH -> WARN. Not a pass (nothing was checked) and
184
+ # not a silent skip (the user must know their code went unaudited).
185
+ #
186
+ # WARN rather than FAIL on purpose: the commit itself is not necessarily
187
+ # bad, the TOOL is broken, and blocking every commit until node/tsx is
188
+ # repaired would blame the change for an environment fault. What must
189
+ # never happen is the green light.
190
+ echo "⚠️ Principles checker did not run (exit ${PRINCIPLES_EXIT}) — this code is NOT verified."
191
+ echo " Gate 4 cannot claim a pass for a check that never executed."
192
+ echo " Fix the checker invocation above (or run 'npx xp-gate check principles .')."
193
+ echo "⏭️ WARN - Principles check (checker execution error)"
194
+ GATE_4_STATUS="WARN"
171
195
  # Consumed by the sourcing parent, not here (see #548).
172
196
  # shellcheck disable=SC2034
173
197
  GATE_4_REASON="checker_execution_error"
@@ -501,6 +501,108 @@ gate13_javac_args() {
501
501
  printf '%s\n' "-Xlint:all,-processing -d"
502
502
  }
503
503
 
504
+ # ---------------------------------------------------------------------------
505
+ # AC-670-34 / AC-670-35 (#659) — counting javac lint findings, locale-proof.
506
+ #
507
+ # The previous detector was an allow-list of English `[category]` names applied to
508
+ # `[WARNING]` lines. Two independent ways it returned 0 on defective code:
509
+ #
510
+ # 1. NOVELTY: a JDK adds a category the list does not name (`future-category`),
511
+ # or the fix relies on the list staying in sync with javac. A fixed allow-list
512
+ # over an open enum is a fail-OPEN design.
513
+ # 2. LOCALISATION: on a non-English build javac's own hint drifts, and the
514
+ # `[WARNING]` prefix may too. Measured on a zh_CN Maven build, the tell is
515
+ # "使用了未经检查或不安全的操作 ... 请使用 -Xlint:unchecked 重新编译" -- a message
516
+ # javac emits precisely when -Xlint was NOT in effect.
517
+ #
518
+ # Fail-closed policy (#499/#570) applied to text matching: count any bracketed
519
+ # lowercase category (whatever it is named), and treat javac's "you should have
520
+ # used -Xlint" hint as a finding too -- the code has an unchecked/unsafe operation
521
+ # whether or not the flag that would name it was on.
522
+ #
523
+ # Prints a single integer. Never fails (always exits 0).
524
+ # ---------------------------------------------------------------------------
525
+ gate13_count_lint_findings() {
526
+ local src="${1:-}"
527
+ [ -n "$src" ] || src=$(cat)
528
+ printf '%s\n' "$src" | awk '
529
+ BEGIN { n = 0 }
530
+ # Any bracketed lowercase category: covers the whole javac -Xlint enum and any
531
+ # later addition to it. Restricted to lowercase/digit/hyphen so a Maven
532
+ # `[WARNING]` with no category, or a `[INFO]` banner, is not counted.
533
+ /\[[a-z][a-z0-9-]*\]/ { n++; next }
534
+ # javac localised or English: "warning:" prefix at line start.
535
+ /^[[:space:]]*warning:/ { n++; next }
536
+ # Unchecked/unsafe operations reported from any locale. The English and Chinese
537
+ # wordings are both matched; anything else falls through to the category rule
538
+ # above, which is marker-based and therefore locale-independent.
539
+ /unchecked|unsafe|未经检查|不安全/ { n++; next }
540
+ END { print n + 0 }
541
+ '
542
+ return 0
543
+ }
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # AC-670-32 (#659) — PROVE that -Xlint takes effect, do not assume it.
547
+ #
548
+ # "Can assemble the flag" is not "the flag works". `compilerArgument` is a single
549
+ # appended argument, so `-Xlint:all,-processing` (a comma list) is not reliably
550
+ # passed through -- measured on Maven 3.9.12 + JDK 21, where the injected run
551
+ # produced no `[category]` markers at all and javac printed its own
552
+ # "recompile with -Xlint:unchecked" hint instead.
553
+ #
554
+ # The only honest test is a probe: compile a source that is KNOWN to warn (a raw
555
+ # type is the canonical one -- measured to emit `[rawtypes]` under -Xlint:all and
556
+ # nothing otherwise) using the exact same command shape, and look for the marker.
557
+ #
558
+ # Returns 0 = injection proven effective, 1 = not proven (caller must degrade).
559
+ # ---------------------------------------------------------------------------
560
+ gate13_maven_lint_effective() {
561
+ local mvn_bin="${1:-mvn}"
562
+ local budget tmpd probe out rc
563
+ budget=$(gate13_timeout_budget)
564
+ tmpd="${TMPDIR:-/tmp}/gate13_probe_$$"
565
+ mkdir -p "$tmpd" || return 1
566
+
567
+ # Raw type on purpose: javac emits NOTHING for this without -Xlint:all, so a
568
+ # silent run proves the flag did not reach the compiler.
569
+ probe="$tmpd/Gate13Probe.java"
570
+ cat > "$probe" <<'PROBE'
571
+ import java.util.ArrayList;
572
+ import java.util.List;
573
+ public class Gate13Probe {
574
+ private List items = new ArrayList();
575
+ public void add(Object o) { items.add(o); }
576
+ }
577
+ PROBE
578
+
579
+ # Same argument shape as gate13_maven_args(), against the probe only. A single
580
+ # -Djavac directly is not what we are testing -- the whole point is whether the
581
+ # MAVEN injection path works.
582
+ if command -v timeout >/dev/null 2>&1; then
583
+ out=$(timeout "$budget" javac -Xlint:all -d "$tmpd" "$probe" 2>&1)
584
+ else
585
+ out=$(javac -Xlint:all -d "$tmpd" "$probe" 2>&1)
586
+ fi
587
+ rc=$?
588
+ rm -rf "$tmpd"
589
+
590
+ # If the DIRECT javac probe cannot even produce a rawtypes warning, the JDK
591
+ # itself is not behaving as measured, so nothing about the Maven path can be
592
+ # concluded -- report "not proven" rather than "effective".
593
+ if [ "$rc" -ne 0 ]; then
594
+ return 1
595
+ fi
596
+ printf '%s\n' "$out" | grep -q 'rawtypes'
597
+ }
598
+
599
+ # Is `-Xlint` reachable at all on this machine? The Maven probe only makes sense
600
+ # when a raw-type warning is obtainable; when javac cannot produce one, callers
601
+ # stay in degraded mode instead of claiming a clean lint.
602
+ gate13_maven_probe_available() {
603
+ command -v javac >/dev/null 2>&1
604
+ }
605
+
504
606
  gate13_java_sources() {
505
607
  git diff --cached --name-only --diff-filter=d -- '*.java' 2>/dev/null
506
608
  }
@@ -574,6 +676,23 @@ gate13_maven_injection_reliable() {
574
676
  return 1
575
677
  fi
576
678
 
679
+ # AC-670-33 (#659): every check above verifies that the arguments can be
680
+ # ASSEMBLED. None of them verifies that they take EFFECT, which is exactly how a
681
+ # pom declaring plugin 3.13.0 was declared "reliable" while Maven ran with no
682
+ # -Xlint at all and Gate 13 reported PASS on a file javac warned about 3 times.
683
+ #
684
+ # So prove it: one probe compile of a known-raw-type source through the same
685
+ # command shape. If the marker does not appear, injection did not happen and the
686
+ # caller must degrade -- "could not verify" is never "verified fine" (#499/#570).
687
+ if ! gate13_maven_probe_available; then
688
+ printf 'javac unavailable; -Xlint injection cannot be probed\n' >&2
689
+ return 1
690
+ fi
691
+ if ! gate13_maven_lint_effective "${MVN_BIN:-mvn}"; then
692
+ printf 'probe compile did not produce a [rawtypes] marker; -Xlint injection is NOT effective\n' >&2
693
+ return 1
694
+ fi
695
+
577
696
  return 0
578
697
  }
579
698
 
@@ -704,14 +823,20 @@ gate13_run_java_analysis() {
704
823
  fi
705
824
 
706
825
  # Count javac's lint output. Maven prefixes it with "[WARNING] ".
707
- mvn_findings=$(printf '%s\n' "$mvn_out" \
708
- | grep -cE '\[WARNING\].*\[(rawtypes|unchecked|deprecation|fallthrough|finally|cast|static|try|overrides|overloads|serial|synchronization|varargs|divzero|empty|exports|module|opens|options|path|processing|removal|requires|text-blocks|this-escape)\]' \
709
- | tr -d '[:space:]')
826
+ #
827
+ # #659: this used to be an English `[WARNING]...[allow-listed-category]`
828
+ # regex. Applied to a localised build -- or to a JDK category the list does
829
+ # not name -- it returned 0 on code javac had warned about, and the gate then
830
+ # printed PASSED. The shared counter below is marker- and locale-proof and
831
+ # fails closed (#499/#570). It lives in one place so the Maven path and the
832
+ # direct-javac path can never disagree about what counts as a finding.
833
+ mvn_findings=$(gate13_count_lint_findings "$mvn_out")
710
834
  mvn_findings=${mvn_findings:-0}
835
+ case "$mvn_findings" in ''|*[!0-9]*) mvn_findings=0 ;; esac
711
836
 
712
837
  if [ "$mvn_findings" -gt 0 ]; then
713
838
  printf '%s\n' "$mvn_out" \
714
- | grep -E '\[WARNING\].*\[[a-z-]+\]' | head -10 | sed 's/^/ /'
839
+ | grep -E '\[[a-z][a-z0-9-]*\]|warning:' | head -10 | sed 's/^/ /'
715
840
  echo " ❌ ${mvn_findings} javac -Xlint finding(s)"
716
841
  return "$GATE13_FAIL"
717
842
  fi
package/hooks/gate-3.sh CHANGED
@@ -461,6 +461,52 @@ else
461
461
  GATE_3_DETAIL="mode=ratchet verifier=MISSING"
462
462
  exit 1
463
463
  fi
464
+ RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
465
+ [ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
466
+ # #617: grade exactly the population the baseline was written from.
467
+ #
468
+ # The comment below has always said so, but only as a comment: a repo whose
469
+ # baseline came from a narrower scan (a Java-only ccn-report, say) had its
470
+ # vendored minified JS graded anyway, and --csv-verified then disagreed with
471
+ # lizard on single-line code and fail-closed with "could not be
472
+ # span-verified". That BLOCK has no repair: the file has no baseline entry
473
+ # to ratchet, so there is nothing the user could add to clear it.
474
+ #
475
+ # The population filter runs BEFORE verification on purpose. Domain
476
+ # restriction is decidable from the path alone, so asking the expensive
477
+ # span re-derivation about a file that will not be graded anyway only
478
+ # manufactures failures for code the ratchet never claims to cover.
479
+ if [ -f "$RATCHET_LIB" ]; then
480
+ # shellcheck source=/dev/null
481
+ . "$RATCHET_LIB" 2>/dev/null || true
482
+ if declare -F ccn_file_in_population >/dev/null 2>&1; then
483
+ _cc_pop_kept=""
484
+ _cc_pop_skipped=""
485
+ while IFS= read -r _cc_row; do
486
+ [ -n "$_cc_row" ] || continue
487
+ _cc_row_path=$(printf '%s' "$_cc_row" | awk -F, '{print $1}')
488
+ if ccn_file_in_population ".ccn-baseline.json" "$_cc_row_path"; then
489
+ _cc_pop_kept="${_cc_pop_kept}${_cc_row}
490
+ "
491
+ else
492
+ _cc_pop_skipped="${_cc_pop_skipped}${_cc_row_path}
493
+ "
494
+ fi
495
+ done <<CC_POP_EOF
496
+ $CC_CSV
497
+ CC_POP_EOF
498
+ CC_CSV=$(printf '%s' "$_cc_pop_kept")
499
+ if [ -n "$_cc_pop_skipped" ]; then
500
+ _cc_pop_skip_n=$(printf '%s' "$_cc_pop_skipped" | grep -c . || true)
501
+ echo "ℹ️ Gate 3: ${_cc_pop_skip_n:-0} file(s) outside the baseline population — not graded, not ratcheted:"
502
+ printf '%s' "$_cc_pop_skipped" | sed 's/^/ /' | sed -n '1,5p'
503
+ if [ "${_cc_pop_skip_n:-0}" -gt 5 ]; then
504
+ echo " ... and $((_cc_pop_skip_n - 5)) more"
505
+ fi
506
+ echo " (declare or widen the population in .ccn-baseline.json to grade them)"
507
+ fi
508
+ fi
509
+ fi
464
510
  if [ -f "$CC_REPORT" ] && command -v node >/dev/null 2>&1; then
465
511
  _cc_verify_failed=0
466
512
  CC_CSV_VERIFIED=$(printf '%s\n' "$CC_CSV" | node "$CC_REPORT" --csv-verified batch --root "$(git rev-parse --show-toplevel 2>/dev/null)") || _cc_verify_failed=1
@@ -471,8 +517,6 @@ else
471
517
  fi
472
518
  CC_CSV="$CC_CSV_VERIFIED"
473
519
  fi
474
- RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"
475
- [ -f "$RATCHET_LIB" ] || RATCHET_LIB="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/lib/ccn-ratchet.sh"
476
520
  # Fail closed when the ratchet library is missing. Sourcing a nonexistent
477
521
  # file only prints "No such file or directory" and leaves
478
522
  # evaluate_ccn_ratchet undefined; the verdict then came back empty, every
package/hooks/gate-4.sh CHANGED
@@ -46,7 +46,14 @@ java_coverage_note() {
46
46
  esac
47
47
  done
48
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)"
49
+ # #628: `docs/` is not in the npm package's `files` list, so pointing a user at
50
+ # `docs/java-principles-coverage.md` was a dead path in every installed project --
51
+ # the note told them to read a file that does not exist on their machine. Cite the
52
+ # upstream URL instead, and keep the local path only when the doc is actually
53
+ # present (i.e. when running inside this repo).
54
+ local _where="https://github.com/boyingliu01/xp-gate/blob/main/docs/java-principles-coverage.md"
55
+ [ -f "docs/java-principles-coverage.md" ] && _where="docs/java-principles-coverage.md"
56
+ echo "ℹ️ Java rule coverage: $JAVA_COVERAGE_EFFECTIVE/$JAVA_COVERAGE_TOTAL rules effective (inert on Java: $JAVA_COVERAGE_INERT -- see $_where)"
50
57
  }
51
58
 
52
59
  2>&1 echo ""
@@ -165,9 +172,26 @@ else
165
172
  rm -f "$PRINCIPLES_STDERR"
166
173
 
167
174
  if [ "$PRINCIPLES_EXIT" -ge 2 ]; then
168
- echo "⚠️ Warning: Principles checker execution failed"
169
- echo "⏭️ SKIPPED - Principles check (execution issue)"
170
- GATE_4_STATUS="SKIP"
175
+ # #661: exit>=2 means the checker did not RUN (crash, module not found,
176
+ # bad invocation). This used to record SKIP, and pre-commit treats SKIP
177
+ # as non-blocking, so a tool failure was absorbed into the green light --
178
+ # the reproduction printed a module-not-found stack trace and then
179
+ # "✅ PASSED - Principles checker (no errors found)".
180
+ #
181
+ # The distinction the gate must keep:
182
+ # - tool NOT INSTALLED -> SKIP is honest; nothing is being claimed.
183
+ # - tool PRESENT, CRASH -> WARN. Not a pass (nothing was checked) and
184
+ # not a silent skip (the user must know their code went unaudited).
185
+ #
186
+ # WARN rather than FAIL on purpose: the commit itself is not necessarily
187
+ # bad, the TOOL is broken, and blocking every commit until node/tsx is
188
+ # repaired would blame the change for an environment fault. What must
189
+ # never happen is the green light.
190
+ echo "⚠️ Principles checker did not run (exit ${PRINCIPLES_EXIT}) — this code is NOT verified."
191
+ echo " Gate 4 cannot claim a pass for a check that never executed."
192
+ echo " Fix the checker invocation above (or run 'npx xp-gate check principles .')."
193
+ echo "⏭️ WARN - Principles check (checker execution error)"
194
+ GATE_4_STATUS="WARN"
171
195
  # Consumed by the sourcing parent, not here (see #548).
172
196
  # shellcheck disable=SC2034
173
197
  GATE_4_REASON="checker_execution_error"
@@ -11,6 +11,23 @@
11
11
  # improves we say so and suggest refreshing the baseline, because a stale
12
12
  # ceiling invites nobody to finish the job.
13
13
  #
14
+ # ── Population (#617) ───────────────────────────────────────────────────────
15
+ # A ratchet is only meaningful over the population it was recorded from. Gate 3
16
+ # grades every extension it recognises; a repo whose baseline came from a
17
+ # narrower scan then reports pre-existing code as "new", which is a false BLOCK
18
+ # no repair can clear — the file has no baseline entry to fix.
19
+ #
20
+ # The baseline may therefore DECLARE its population:
21
+ #
22
+ # { "population": { "roots": ["src/main/java"], "extensions": [".java"] },
23
+ # "entries": { "src/main/java/A.java::f": 4 } }
24
+ #
25
+ # A baseline without that key is LEGACY and is treated as unrestricted, which is
26
+ # exactly the historical behaviour: every baseline already in the field has the
27
+ # flat `{ "path::fn": ccn }` shape, and refusing to load it would break every
28
+ # install on upgrade. Absent metadata must never be read as an empty population
29
+ # -- that would silently grade nothing at all, the opposite failure.
30
+ #
14
31
  # Key = "<relative/path>::<functionName>". Rename or move = new key = treated
15
32
  # as a new function. That is deliberate: fuzzy matching would hide debt moves.
16
33
  # `node scripts/ccn-report.cjs --write-baseline` regenerates the file after a
@@ -41,6 +58,98 @@
41
58
  # every row is read as a single field).
42
59
  CSV_FS=$'\x1f'
43
60
 
61
+ # ---------------------------------------------------------------------------
62
+ # #617: baseline population.
63
+ #
64
+ # Prints the declared population as `<root>|<ext>|<ext>...` on one line, or
65
+ # NOTHING when the baseline carries no `population` key (legacy => unrestricted).
66
+ # Printing nothing is deliberate: "no declaration" and "empty population" are
67
+ # opposite meanings, and a caller that cannot tell them apart would either grade
68
+ # everything (wrong for a Java-only repo) or nothing (catastrophic -- the ratchet
69
+ # would report a clean run while comparing zero functions).
70
+ # ---------------------------------------------------------------------------
71
+ ccn_baseline_population() {
72
+ local _bl="${1:-.ccn-baseline.json}"
73
+ [ -f "$_bl" ] || return 0
74
+ command -v node >/dev/null 2>&1 || return 0
75
+ node -e '
76
+ const fs = require("fs");
77
+ let data;
78
+ try { data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); }
79
+ catch { process.exit(0); }
80
+ // Legacy shape: a flat map of "path::fn" -> ccn. No declaration.
81
+ if (!data || typeof data !== "object" || !data.population) process.exit(0);
82
+ const pop = data.population || {};
83
+ const roots = Array.isArray(pop.roots) ? pop.roots : [];
84
+ const exts = Array.isArray(pop.extensions) ? pop.extensions : [];
85
+ // A declared-but-empty population is not a legitimate declaration: it would
86
+ // excuse every file. Treat it as no declaration so the gate keeps grading.
87
+ if (roots.length === 0 && exts.length === 0) process.exit(0);
88
+ // One line: "<roots csv>|<exts csv>". Callers split on the single pipe.
89
+ process.stdout.write(roots.join(",") + "|" + exts.join(","));
90
+ ' "$_bl" 2>/dev/null
91
+ return 0
92
+ }
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # #617: is this file inside the baseline's declared population?
96
+ #
97
+ # Returns 0 when the file may be graded, 1 when it is outside the population.
98
+ # A legacy baseline (no declaration) permits everything, preserving the
99
+ # behaviour every existing install already relies on.
100
+ #
101
+ # Both axes are ANDed only when both are declared; a baseline declaring just
102
+ # roots (or just extensions) narrows on that axis alone.
103
+ # ---------------------------------------------------------------------------
104
+ ccn_file_in_population() {
105
+ local _bl="${1:-.ccn-baseline.json}"
106
+ local _file="${2:-}"
107
+ [ -n "$_file" ] || return 1
108
+
109
+ local _pop
110
+ _pop=$(ccn_baseline_population "$_bl")
111
+ # No declaration => unrestricted (legacy baseline).
112
+ [ -n "$_pop" ] || return 0
113
+
114
+ local _roots="${_pop%%|*}"
115
+ local _exts="${_pop#*|}"
116
+ # Normalise separators so a Windows path and a POSIX path compare the same way.
117
+ # sed, not `tr '\' '/'`: a lone backslash is a portability trap in tr's SET
118
+ # argument (GNU tr warns "unescaped backslash at end of string" and MSYS tr
119
+ # behaviour differs), while the sed regex below is unambiguous everywhere.
120
+ _norm=$(printf '%s' "$_file" | sed 's|\\|/|g')
121
+
122
+ if [ -n "$_roots" ]; then
123
+ local _hit=0 _r
124
+ local _oldifs="$IFS"
125
+ IFS=','
126
+ for _r in $_roots; do
127
+ IFS="$_oldifs"
128
+ case "$_norm" in
129
+ "$_r"|"$_r"/*) _hit=1; break ;;
130
+ esac
131
+ done
132
+ IFS="$_oldifs"
133
+ [ "$_hit" -eq 1 ] || return 1
134
+ fi
135
+
136
+ if [ -n "$_exts" ]; then
137
+ local _match=0 _e
138
+ local _oldifs2="$IFS"
139
+ IFS=','
140
+ for _e in $_exts; do
141
+ IFS="$_oldifs2"
142
+ case "$_norm" in
143
+ *"$_e") _match=1; break ;;
144
+ esac
145
+ done
146
+ IFS="$_oldifs2"
147
+ [ "$_match" -eq 1 ] || return 1
148
+ fi
149
+
150
+ return 0
151
+ }
152
+
44
153
  csv_to_unit_separated() {
45
154
  awk '
46
155
  function emit(line, cur, i, ch, q, nf) {