@boyingliu01/xp-gate 0.34.0 → 0.34.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/gate-13-compiler-strictness.sh +129 -4
- package/gate-4.sh +8 -1
- package/hooks/gate-13-compiler-strictness.sh +129 -4
- package/hooks/gate-4.sh +8 -1
- package/lib/__tests__/foreign-hooks-global.test.js +125 -0
- package/lib/__tests__/uninstall-global-residue.test.js +160 -0
- package/lib/doctor.js +70 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/uninstall.js +225 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/rules/solid/dip.ts +94 -14
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- 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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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 '\[
|
|
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-4.sh
CHANGED
|
@@ -46,7 +46,14 @@ java_coverage_note() {
|
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
48
|
[ -n "$_has_java" ] || return 0
|
|
49
|
-
|
|
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 ""
|
|
@@ -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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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 '\[
|
|
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-4.sh
CHANGED
|
@@ -46,7 +46,14 @@ java_coverage_note() {
|
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
48
|
[ -n "$_has_java" ] || return 0
|
|
49
|
-
|
|
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 ""
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-664
|
|
3
|
+
* @intent `setup-global` points `core.hooksPath` at `~/.config/xp-gate/hooks`, and
|
|
4
|
+
* that setting is GLOBAL — it governs every repository on the machine. So
|
|
5
|
+
* any third-party tool that writes hooks into `core.hooksPath` writes into
|
|
6
|
+
* a directory xp-gate owns. `git lfs install` does exactly this, and the
|
|
7
|
+
* measured result was four hooks xp-gate never installs appearing there:
|
|
8
|
+
*
|
|
9
|
+
* hook-module-resolution.sh lib/ post-checkout post-commit
|
|
10
|
+
* post-merge pre-commit pre-push
|
|
11
|
+
*
|
|
12
|
+
* Two failures followed:
|
|
13
|
+
*
|
|
14
|
+
* 1. `doctor` only checked pre-commit / pre-push / post-merge. The extra
|
|
15
|
+
* files were neither reported nor explained, so the user had no way to
|
|
16
|
+
* learn that a global install had acquired foreign hooks.
|
|
17
|
+
* 2. `uninstall` did `rmSync(GLOBAL_HOOKS_DIR, {recursive: true})` — the
|
|
18
|
+
* whole tree, including the lfs hooks someone else put there. The
|
|
19
|
+
* file already had `verifyFileOwnership()`; the `type: 'dir'` branch
|
|
20
|
+
* simply did not use it.
|
|
21
|
+
*
|
|
22
|
+
* Deleting another tool's files is the more serious of the two: the user
|
|
23
|
+
* loses a working git-lfs setup and has no idea which command did it. So
|
|
24
|
+
* uninstall must classify before it removes, and SAY what it found.
|
|
25
|
+
* @covers AC-670-40, AC-670-41, AC-670-42
|
|
26
|
+
*/
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const os = require('os');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
|
|
33
|
+
/** Hooks xp-gate itself installs; anything else in the dir is foreign. */
|
|
34
|
+
const XP_GATE_HOOKS = ['pre-commit', 'pre-push', 'post-merge'];
|
|
35
|
+
|
|
36
|
+
function freshRequire(relPath) {
|
|
37
|
+
for (const key of Object.keys(require.cache)) {
|
|
38
|
+
if (key.includes(`${path.sep}npm-package${path.sep}lib${path.sep}`)) {
|
|
39
|
+
delete require.cache[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return require(relPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A hooks directory shaped like the measured post-`git lfs install` state. */
|
|
46
|
+
function makeHooksDir() {
|
|
47
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'xpg-hooks-'));
|
|
48
|
+
for (const hook of XP_GATE_HOOKS) {
|
|
49
|
+
fs.writeFileSync(path.join(dir, hook), '#!/bin/sh\n# xp-gate\n');
|
|
50
|
+
}
|
|
51
|
+
// Foreign: two hooks git-lfs installs, plus its support file.
|
|
52
|
+
fs.writeFileSync(path.join(dir, 'post-checkout'), '#!/bin/sh\ngit lfs post-checkout "$@"\n');
|
|
53
|
+
fs.writeFileSync(path.join(dir, 'post-commit'), '#!/bin/sh\ngit lfs post-commit "$@"\n');
|
|
54
|
+
fs.writeFileSync(path.join(dir, 'hook-module-resolution.sh'), '#!/bin/sh\n');
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('foreign hooks in the global hooksPath (#664)', () => {
|
|
59
|
+
it('AC-670-40: the classifier names xp-gate hooks and flags everything else', () => {
|
|
60
|
+
const { classifyHooksDir } = freshRequire('../doctor');
|
|
61
|
+
expect(typeof classifyHooksDir).toBe('function');
|
|
62
|
+
|
|
63
|
+
const dir = makeHooksDir();
|
|
64
|
+
try {
|
|
65
|
+
const result = classifyHooksDir(dir);
|
|
66
|
+
expect(result.owned.sort()).toEqual([...XP_GATE_HOOKS].sort());
|
|
67
|
+
// Every foreign entry is reported, not silently dropped. `foreign` holds
|
|
68
|
+
// { name, source } records, so compare names.
|
|
69
|
+
const foreignNames = result.foreign.map((f) => f.name);
|
|
70
|
+
expect(foreignNames).toContain('post-checkout');
|
|
71
|
+
expect(foreignNames).toContain('post-commit');
|
|
72
|
+
// lib/ is xp-gate's own shared-library directory, not foreign.
|
|
73
|
+
expect(foreignNames).not.toContain('lib');
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('AC-670-41: git-lfs hooks are identified by source, unknown files stay generic', () => {
|
|
80
|
+
const { classifyHooksDir } = freshRequire('../doctor');
|
|
81
|
+
const dir = makeHooksDir();
|
|
82
|
+
try {
|
|
83
|
+
const result = classifyHooksDir(dir);
|
|
84
|
+
// Naming the likely source is what turns a confusing listing into an
|
|
85
|
+
// actionable message: "git lfs" tells the user which tool to look at.
|
|
86
|
+
const lfs = result.foreign.filter((f) => f.source === 'git-lfs').map((f) => f.name);
|
|
87
|
+
expect(lfs).toContain('post-checkout');
|
|
88
|
+
expect(lfs).toContain('post-commit');
|
|
89
|
+
|
|
90
|
+
// A file with no recognizable signature must still be reported, but without
|
|
91
|
+
// guessing a source it cannot prove.
|
|
92
|
+
fs.writeFileSync(path.join(dir, 'post-rebase'), '#!/bin/sh\necho mine\n');
|
|
93
|
+
const again = classifyHooksDir(dir);
|
|
94
|
+
const unknown = again.foreign.find((f) => f.name === 'post-rebase');
|
|
95
|
+
expect(unknown).toBeDefined();
|
|
96
|
+
expect(unknown.source).not.toBe('git-lfs');
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('AC-670-42: uninstall refuses to delete a hooks dir holding foreign files', () => {
|
|
103
|
+
const { planHooksDirRemoval } = freshRequire('../uninstall');
|
|
104
|
+
expect(typeof planHooksDirRemoval).toBe('function');
|
|
105
|
+
|
|
106
|
+
const dir = makeHooksDir();
|
|
107
|
+
try {
|
|
108
|
+
const plan = planHooksDirRemoval(dir);
|
|
109
|
+
// Recursive removal would take git-lfs's files with it, so it must not be
|
|
110
|
+
// the chosen action while foreign content is present.
|
|
111
|
+
expect(plan.action).not.toBe('remove-all');
|
|
112
|
+
expect(plan.foreign.length).toBeGreaterThan(0);
|
|
113
|
+
|
|
114
|
+
// A directory holding ONLY xp-gate files is safe to remove wholesale.
|
|
115
|
+
const clean = fs.mkdtempSync(path.join(os.tmpdir(), 'xpg-hooks-clean-'));
|
|
116
|
+
for (const hook of XP_GATE_HOOKS) {
|
|
117
|
+
fs.writeFileSync(path.join(clean, hook), '#!/bin/sh\n# xp-gate\n');
|
|
118
|
+
}
|
|
119
|
+
expect(planHooksDirRemoval(clean).action).toBe('remove-all');
|
|
120
|
+
fs.rmSync(clean, { recursive: true, force: true });
|
|
121
|
+
} finally {
|
|
122
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-663
|
|
3
|
+
* @intent `xp-gate uninstall` presents itself as the inverse of `init`, but it only
|
|
4
|
+
* removed `hooks/` + `adapters/` + `core.hooksPath`. After
|
|
5
|
+
* `setup-global` then `uninstall --global --force`, the tool printed
|
|
6
|
+
* "Uninstall complete" while leaving behind ~1000 files:
|
|
7
|
+
*
|
|
8
|
+
* ~/.config/xp-gate/modules/ principles/ mutation/ mock-policy/
|
|
9
|
+
* ~/.config/xp-gate/node_modules/ js-yaml / zod / argparse ...
|
|
10
|
+
* ~/.config/xp-gate/package.json
|
|
11
|
+
* ~/.config/xp-gate/package-lock.json
|
|
12
|
+
* ~/.config/xp-gate/xp-gate.json manifest + installedSkills record
|
|
13
|
+
* ~/.xp-gate/version-cache.json
|
|
14
|
+
*
|
|
15
|
+
* And skills were never in scope at all: 12 skills stayed in
|
|
16
|
+
* ~/.qoder/skills/ and ~/.config/opencode/skills/, which the next `init`
|
|
17
|
+
* then treated as already-installed.
|
|
18
|
+
*
|
|
19
|
+
* The residue is not cosmetic. A leftover `node_modules/` makes the NEXT
|
|
20
|
+
* `setup-global` skip its install ("module runtime deps → ... node_modules"
|
|
21
|
+
* reuses whatever is there), so a stale dependency tree survives an
|
|
22
|
+
* upgrade; and a leftover `xp-gate.json` carries the previous version's
|
|
23
|
+
* `manifest` and `lastInit`, which `doctor` then reports as the CURRENT
|
|
24
|
+
* installation.
|
|
25
|
+
*
|
|
26
|
+
* Two rules the fix must respect, both from the issue:
|
|
27
|
+
* - a user config (`.delphi-config.json`) is user DATA, not residue --
|
|
28
|
+
* keep it, but SAY SO instead of leaving it silently;
|
|
29
|
+
* - never delete a skill directory the user has edited. Only remove one
|
|
30
|
+
* whose content still matches the bundled copy.
|
|
31
|
+
* @covers AC-670-37, AC-670-38, AC-670-39
|
|
32
|
+
*/
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
// describe/it/expect/beforeEach/afterEach are vitest globals in this repo's config;
|
|
36
|
+
// requiring them from 'vitest' fails because these tests are CommonJS.
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const os = require('os');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
/** Build a throwaway HOME so the plan is computed against a controlled tree. */
|
|
42
|
+
function makeHome() {
|
|
43
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'xpg-uninst-'));
|
|
44
|
+
return home;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reproduce the on-disk shape `xp-gate setup-global` leaves behind, including the
|
|
49
|
+
* parts that were measured as residue in #663.
|
|
50
|
+
*/
|
|
51
|
+
function seedGlobalInstall() {
|
|
52
|
+
const configDir = path.join(process.env.HOME, '.config', 'xp-gate');
|
|
53
|
+
for (const rel of ['hooks', 'adapters', 'modules/principles', 'node_modules/js-yaml']) {
|
|
54
|
+
fs.mkdirSync(path.join(configDir, rel), { recursive: true });
|
|
55
|
+
fs.writeFileSync(path.join(configDir, rel, 'index.js'), 'module.exports = {};\n');
|
|
56
|
+
}
|
|
57
|
+
for (const file of ['package.json', 'package-lock.json', 'xp-gate.json']) {
|
|
58
|
+
fs.writeFileSync(path.join(configDir, file), '{}\n');
|
|
59
|
+
}
|
|
60
|
+
// User data: must survive, and must be reported.
|
|
61
|
+
fs.writeFileSync(path.join(configDir, '.delphi-config.json'), '{}\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
describe('uninstall --global plans the whole installation, not just hooks (#663)', () => {
|
|
66
|
+
let home;
|
|
67
|
+
const savedEnv = {};
|
|
68
|
+
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
home = makeHome();
|
|
71
|
+
savedEnv.HOME = process.env.HOME;
|
|
72
|
+
savedEnv.USERPROFILE = process.env.USERPROFILE;
|
|
73
|
+
process.env.HOME = home;
|
|
74
|
+
process.env.USERPROFILE = home;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.env.HOME = savedEnv.HOME;
|
|
79
|
+
process.env.USERPROFILE = savedEnv.USERPROFILE;
|
|
80
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load a module fresh so it re-reads HOME. Every lib reads os.homedir() at call
|
|
85
|
+
* time, but a cached module still holds `GLOBAL_DIRS` values captured earlier.
|
|
86
|
+
*/
|
|
87
|
+
function freshRequire(relPath) {
|
|
88
|
+
for (const key of Object.keys(require.cache)) {
|
|
89
|
+
if (key.includes(`${path.sep}npm-package${path.sep}lib${path.sep}`)) {
|
|
90
|
+
delete require.cache[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return require(relPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it('AC-670-37: modules, node_modules and the manifest files are all in scope', () => {
|
|
97
|
+
const { buildPlan } = freshRequire('../uninstall');
|
|
98
|
+
expect(typeof buildPlan).toBe('function');
|
|
99
|
+
|
|
100
|
+
// buildPlan only schedules what actually exists, so the fixture has to look
|
|
101
|
+
// like a real `setup-global` install -- otherwise this asserts nothing.
|
|
102
|
+
seedGlobalInstall();
|
|
103
|
+
|
|
104
|
+
const plan = buildPlan('global');
|
|
105
|
+
const paths = plan.filter((step) => step.path).map((step) => step.path.split(path.sep).join('/'));
|
|
106
|
+
|
|
107
|
+
// Each of these was measured residue. A plan that omits any of them reports
|
|
108
|
+
// "complete" over an installation that is still there.
|
|
109
|
+
for (const required of ['modules', 'node_modules', 'package.json', 'package-lock.json', 'xp-gate.json']) {
|
|
110
|
+
expect(
|
|
111
|
+
paths.some((p) => p.endsWith(required)),
|
|
112
|
+
`${required} is missing from the uninstall plan (measured residue, #663)`,
|
|
113
|
+
).toBe(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('AC-670-38: user config is retained and reported, never silently kept', () => {
|
|
118
|
+
const { buildPlan } = freshRequire('../uninstall');
|
|
119
|
+
seedGlobalInstall();
|
|
120
|
+
const plan = buildPlan('global');
|
|
121
|
+
// Deletion steps only: a RETAIN step names the file precisely because it is
|
|
122
|
+
// being kept, so including it here would assert the opposite of the intent.
|
|
123
|
+
const deletedPaths = plan
|
|
124
|
+
.filter((step) => step.path && step.retain !== true && step.type !== 'retain')
|
|
125
|
+
.map((step) => step.path.split(path.sep).join('/'));
|
|
126
|
+
|
|
127
|
+
// `.delphi-config.json` holds the user's expert/model choice -- it is data, so
|
|
128
|
+
// it must appear in no deletion step. (The --purge branch is a separate,
|
|
129
|
+
// explicitly destructive path and is not what this plan describes.)
|
|
130
|
+
expect(deletedPaths.some((p) => p.endsWith('.delphi-config.json'))).toBe(false);
|
|
131
|
+
// Not deleting it is only half the fix: the summary has to name it.
|
|
132
|
+
const retained = plan.filter((step) => step.retain === true);
|
|
133
|
+
expect(retained.length, 'no step declares a retention, so the summary cannot report one').toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('AC-670-39: an edited skill directory is never scheduled for deletion', () => {
|
|
137
|
+
const { planSkillRemovals } = freshRequire('../uninstall');
|
|
138
|
+
expect(typeof planSkillRemovals).toBe('function');
|
|
139
|
+
|
|
140
|
+
const bundled = path.join(home, 'bundle', 'skills');
|
|
141
|
+
const installedRoot = path.join(home, '.qoder', 'skills');
|
|
142
|
+
for (const [dir, body] of [
|
|
143
|
+
[path.join(bundled, 'clean-skill'), 'bundled body\n'],
|
|
144
|
+
[path.join(installedRoot, 'clean-skill'), 'bundled body\n'],
|
|
145
|
+
[path.join(bundled, 'edited-skill'), 'bundled body\n'],
|
|
146
|
+
[path.join(installedRoot, 'edited-skill'), 'bundled body\n// user edit\n'],
|
|
147
|
+
]) {
|
|
148
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
149
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), body);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const removals = planSkillRemovals(['clean-skill', 'edited-skill'], installedRoot, bundled);
|
|
153
|
+
const names = removals.map((r) => path.basename(r.path));
|
|
154
|
+
|
|
155
|
+
// Untouched -> remove. This is the residue the issue is about.
|
|
156
|
+
expect(names).toContain('clean-skill');
|
|
157
|
+
// Edited -> keep. Deleting a user's modifications is worse than residue.
|
|
158
|
+
expect(names).not.toContain('edited-skill');
|
|
159
|
+
});
|
|
160
|
+
});
|