@boyingliu01/xp-gate 0.31.4 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +65 -16
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +65 -16
  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 +101 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +259 -57
  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/pre-commit CHANGED
@@ -710,48 +710,37 @@ if [ "$is_protected" = "true" ]; then
710
710
  esac
711
711
  done
712
712
 
713
- # Doc-only changes: no source code files
713
+ # Doc-only / build-tooling-only changes need no VERSION bump.
714
+ #
715
+ # #624 defect 2: the split used to test ONLY `has_source`, which is true for
716
+ # any .py/.ts/... path INCLUDING `scripts/*`. So a commit that touched
717
+ # nothing but build tooling still fell into the version-check branch and was
718
+ # blocked. `has_real_source` is exactly "production source that needs a bump"
719
+ # — it was computed a few lines up but never consulted here. It must decide.
714
720
  if [ "$has_source" = "false" ]; then
715
721
  echo "✅ PASSED - Gate 0: Version Consistency Check (doc-only changes)"
722
+ elif [ "$has_real_source" = "false" ]; then
723
+ echo "✅ PASSED - Gate 0: Version Consistency Check (build-tooling-only changes)"
716
724
  else
717
- # Check for bypass: commit message prefix (read from COMMIT_EDITMSG during pre-commit hook)
718
- COMMIT_MSG=$(cat "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null | head -1 || echo "")
719
- if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\]'; then
720
- if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\].*(chore:|docs:|release:)'; then
721
- if [ "$has_real_source" = "false" ]; then
722
- echo "✅ PASSED - Gate 0: Version Consistency Check (bypass: chore/docs/release)"
723
- else
724
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
725
- VERSION CONSISTENCY CHECK (Gate 0)
726
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
727
- Branch: $CURRENT_BRANCH
728
- ERROR: [skip-version-check] bypass used but production source files detected.
729
-
730
- Bypass only allows build-tooling changes (adapters/, scripts/, hooks/).
731
- Production source files require VERSION/CHANGELOG update.
732
-
733
- Files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
734
- "
735
- exit 1
736
- fi
737
- else
738
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
739
- VERSION CONSISTENCY CHECK (Gate 0)
740
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
741
- Branch: $CURRENT_BRANCH
742
- ERROR: [skip-version-check] bypass prefix invalid.
743
-
744
- Allowed prefixes: chore:, docs:, release:
745
- Example: [skip-version-check] chore: update dependencies
746
- "
747
- exit 1
748
- fi
749
- else
750
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
725
+ # Production source on a protected branch without VERSION/CHANGELOG.
726
+ #
727
+ # #624 defect 1: this branch used to offer a `[skip-version-check]` message
728
+ # prefix as a bypass, read from `.git/COMMIT_EDITMSG`. That cannot work:
729
+ # git runs pre-commit BEFORE it builds the message, so the file does not
730
+ # exist yet (`-m`, `-F` and the interactive editor path all measured on
731
+ # git 2.54), and the hook gets no arguments. What the old code read was the
732
+ # PREVIOUS commit's message, so the prefix never matched — the documented
733
+ # recipe silently could not clear the gate, and --no-verify was the only
734
+ # way through. Rather than keep a bypass whose input this hook structurally
735
+ # cannot see, the escape hatch is the one that actually works and is
736
+ # already documented in CHANGELOG: the SKIP_VERSION_CHECK=1 environment
737
+ # variable (handled earlier in this Gate 0 block).
738
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
751
739
  VERSION CONSISTENCY CHECK (Gate 0)
752
740
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
753
741
  Branch: $CURRENT_BRANCH
754
- ERROR: Committing to a protected branch without VERSION/CHANGELOG update.
742
+ ERROR: Committing production source to a protected branch without a
743
+ VERSION/CHANGELOG update.
755
744
 
756
745
  This bypasses the ship workflow and causes version drift.
757
746
  Before committing:
@@ -759,18 +748,19 @@ Before committing:
759
748
  2. Add entry to CHANGELOG.md
760
749
  3. Run: node scripts/sync-version.cjs
761
750
 
762
- Or use the bypass prefix — the commit message FIRST LINE must START with:
763
- [skip-version-check] chore: <description>
764
- Allowed types: chore: docs: release:. The bypass only permits build
765
- tooling changes (adapters/, scripts/, hooks/); production source files
766
- still require a VERSION/CHANGELOG update.
751
+ Build-tooling-only changes (adapters/, scripts/, hooks/) need NO version bump
752
+ and pass this gate automatically — no marker required.
753
+
754
+ If you genuinely must land production source without a version bump, the
755
+ supported escape hatch is the env var (a message prefix cannot work here
756
+ because pre-commit runs before git builds the message):
757
+ SKIP_VERSION_CHECK=1 git commit ...
767
758
 
768
759
  Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
769
760
  "
770
- GATE_0_STATUS="BLOCK"
771
- record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "1" "$GATE_0_START"
772
- exit 1
773
- fi
761
+ GATE_0_STATUS="BLOCK"
762
+ record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "1" "$GATE_0_START"
763
+ exit 1
774
764
  fi
775
765
  fi
776
766
  fi # closes SKIP_VERSION_CHECK if/else
@@ -809,7 +799,44 @@ echo ""
809
799
  MIRROR_PATH_FILTER='^src/npm-package/'
810
800
  DASHBOARD_PATH_FILTER='^dashboard/'
811
801
  RAW_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
812
- CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" | grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" || true)
802
+
803
+ # Dependencies and build output are not project code (#649).
804
+ #
805
+ # `git diff --cached` knows nothing about .gitignore: anything that reaches the
806
+ # index is listed. A project that vendors its dependencies, a CI fixture that
807
+ # commits after `npm install`, an accidental `git add -A`, or a monorepo's
808
+ # nested `packages/*/node_modules` therefore hands every downstream scanner a
809
+ # list containing third-party source, and each scanner judges it as the
810
+ # project's own.
811
+ #
812
+ # This has already cost two outages, one language branch at a time: #595 (Gate 1
813
+ # PowerShell arm blocked every commit once Playwright's node_modules was
814
+ # scanned for Write-Host) and #648 (Gate 1 JS/TS arm blocked on the bare
815
+ # `debugger;` in node_modules/typescript/dist/internal/utils.js). Both were
816
+ # fixed at the single site that happened to fail, so the next branch re-broke
817
+ # the same way. The filter belongs HERE, at the one place the list is built, so
818
+ # a scanner added tomorrow is safe by default instead of by memory.
819
+ #
820
+ # The exclusion itself is the shared build-output list from jscpd-run.sh
821
+ # (jscpd_is_build_output), so the directory names have one definition across the
822
+ # gates. Deliberately NOT the full Gate 2 ignore set: that one also drops
823
+ # *.yaml/*.yml/*.txt/*.log because repetition there is data shape rather than a
824
+ # refactoring target, and using it here would make a commit that only adds
825
+ # infra/deployment.yaml look like "No code files changed" -- skipping the IaC
826
+ # and lint gates that exist to scan exactly that file. The XP_GATE_JSCPD_DEFAULT_IGNORE=0
827
+ # escape hatch still applies.
828
+ #
829
+ # RAW_CHANGED_FILES stays unfiltered on purpose: it is the only truthful answer
830
+ # to "did the user stage anything at all", and a future "you accidentally
831
+ # committed dependencies" warning needs to see what this filter removed.
832
+ CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" \
833
+ | grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" \
834
+ | {
835
+ while IFS= read -r _cf; do
836
+ [ -n "$_cf" ] || continue
837
+ jscpd_is_build_output "$_cf" || printf '%s\n' "$_cf"
838
+ done
839
+ } || true)
813
840
 
814
841
  # REQ-F2: one commit-level docs-only verdict, computed once and reused by the
815
842
  # heavy gates (2/5/9/12). Light gates never consult it — a secret in a README
@@ -844,7 +871,22 @@ any_changed_files_match() {
844
871
  local pattern
845
872
 
846
873
  for pattern in "$@"; do
847
- if printf '%s\n' "$CHANGED_FILES" | grep -qE -- "$pattern"; then
874
+ # Here-string, not `printf | grep -q`. Under the `set -o pipefail` above,
875
+ # `grep -q` exits the moment it matches, the writer takes SIGPIPE, and the
876
+ # pipeline reports 141 -- so the `if` reads "no match" even though grep DID
877
+ # match. Whether that happens depends on whether the list fits in the pipe
878
+ # buffer, which is why it showed up as a size-dependent, flaky SKIP.
879
+ #
880
+ # It was not theoretical: in the Cross-Platform CI first-commit fixture the
881
+ # staged list is long enough to trip it, and Gate 1 announced "No code files
882
+ # changed, skipping lint" for a commit that had staged 19 shell scripts --
883
+ # while Gate 2, reading the same variable, happily scanned them and reported
884
+ # duplicates. The hook log carried the fingerprint the whole time:
885
+ # `line 847: printf: Broken pipe`.
886
+ #
887
+ # A here-string has no writer process to be killed, so the match cannot be
888
+ # lost. Same reason `run_tsx` and the coverage probes avoid `| head`.
889
+ if grep -qE -- "$pattern" <<< "$CHANGED_FILES"; then
848
890
  return 0
849
891
  fi
850
892
  done
@@ -1506,9 +1548,32 @@ else
1506
1548
  fi
1507
1549
 
1508
1550
  # Debug statement detection (debugger statement in JS/TS)
1509
- TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
1551
+ #
1552
+ # Dependencies and build output are filtered out first (#648). The list
1553
+ # comes from `git diff --cached`, so a project (or a CI fixture) that
1554
+ # vendors or commits its dependencies gets the dependency's OWN source
1555
+ # scanned as if it were project code. TypeScript 7.x ships a bare
1556
+ # `debugger;` in dist/internal/utils.js, which blocked commits that changed
1557
+ # nothing of their own:
1558
+ # ❌ BLOCKED - 'debugger' statements detected:
1559
+ # node_modules/typescript/dist/internal/utils.js:18: debugger;
1560
+ # Pinning the dependency's version only postpones that: any package may
1561
+ # carry a debugger statement, and which one lands changes with every
1562
+ # install. The rule the gate actually wants is "no debugger in OUR code",
1563
+ # so the ignore set Gate 2 already uses decides what counts as ours — one
1564
+ # notion of build output across the gates, overridable through the same
1565
+ # XP_GATE_JSCPD_DEFAULT_IGNORE switch.
1566
+ TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' | {
1567
+ while IFS= read -r _df; do
1568
+ [ -n "$_df" ] || continue
1569
+ if ! jscpd_is_build_output "$_df"; then
1570
+ printf '%s\n' "$_df"
1571
+ fi
1572
+ done
1573
+ } || true)
1510
1574
  if [ -n "$TS_DEBUG_FILES" ]; then
1511
- DEBUGGER_HITS=$(grep -n '^\s*debugger\s*;\?\s*$' $TS_DEBUG_FILES 2>/dev/null || true)
1575
+ # NUL-separated so paths with spaces survive; a bare $VAR would word-split.
1576
+ DEBUGGER_HITS=$(printf '%s\n' "$TS_DEBUG_FILES" | tr '\n' '\0' | xargs -0 grep -n '^\s*debugger\s*;\?\s*$' 2>/dev/null || true)
1512
1577
  if [ -n "$DEBUGGER_HITS" ]; then
1513
1578
  echo ""
1514
1579
  echo "❌ BLOCKED - 'debugger' statements detected:"
@@ -2189,7 +2254,10 @@ else
2189
2254
  fi
2190
2255
 
2191
2256
  # --- BLOCK path: New .ts/.tsx files without corresponding tests ---
2192
- NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v 'node_modules/' | grep -v '\.next/' | grep -v '\.nuxt/' | grep -v 'dist/' | grep -v 'build/' | grep -v '.turbo/' | grep -v '.cache/' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
2257
+ # Exclusions come from filter_project_paths (#649). The hand-maintained list
2258
+ # this replaced omitted vendor/target/coverage/out, so a staged vendor/x.ts
2259
+ # was reported as new source with no test.
2260
+ NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
2193
2261
 
2194
2262
  # Strip PROJECT_SUBDIR prefix from new-TS-file paths ONLY when the hook
2195
2263
  # actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1; single-language
@@ -2274,10 +2342,12 @@ else
2274
2342
  fi
2275
2343
 
2276
2344
  # --- WARNING path: Modified files (any lang) + New non-TS files ---
2345
+ # filter_project_paths strips dependencies and build output (#649); without
2346
+ # it a staged vendor/ or target/ module is reported as source needing a test.
2277
2347
  WARNING_SOURCE_FILES=$(
2278
2348
  {
2279
- git diff --cached --name-only --diff-filter=M | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
2280
- git diff --cached --name-only --diff-filter=A | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
2349
+ git diff --cached --name-only --diff-filter=M | filter_project_paths | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
2350
+ git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
2281
2351
  } | sort -u || true
2282
2352
  )
2283
2353
 
@@ -2400,7 +2470,7 @@ else
2400
2470
  # Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
2401
2471
  # 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
2402
2472
  # ========================================================================
2403
- CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs|py|go)$' || true)
2473
+ CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | filter_project_paths | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|cjs|mjs|py|go)$' || true)
2404
2474
 
2405
2475
  # Nested plugin sub-projects (plugins/<name>/) own their own vitest config and
2406
2476
  # are excluded from the root suite (`vitest.config.ts` excludes `plugins/**`,
@@ -2548,7 +2618,7 @@ else
2548
2618
  CHANGED_TEST_FILE_COUNT=$(echo "$SELECTED_TEST_FILES" | grep -c '.' || true)
2549
2619
 
2550
2620
  # Also get changed source files (non-test) for related-test lookup
2551
- CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
2621
+ CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | filter_project_paths | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
2552
2622
  # Nested plugin sub-projects are excluded from the root suite; their
2553
2623
  # sources must not drive root-vitest related-test lookup either (it
2554
2624
  # would resolve to plugin-local paths vitest cannot collect).
@@ -2784,9 +2854,14 @@ else
2784
2854
  # full -> unchanged behavior: whole suite + real threshold.
2785
2855
  # skip -> nothing test-relevant changed; no pytest at all.
2786
2856
  PY_CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null \
2857
+ | filter_project_paths \
2787
2858
  | grep -E '(^|/)test_[^/]*\.py$|_test\.py$' || true)
2788
2859
  PY_CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null \
2860
+ | filter_project_paths \
2789
2861
  | grep -E '\.py$' | grep -vE '(^|/)test_[^/]*\.py$|_test\.py$' || true)
2862
+ # NOT filtered on purpose: this list answers "did a requirements
2863
+ # file change", which is a property of the whole commit including
2864
+ # paths under a vendor/ tree that filter_project_paths removes.
2790
2865
  PY_CHANGED_ALL=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
2791
2866
  # `grep -c '.'` prints its own zero AND exits 1 on an empty list
2792
2867
  # (#493), so the count is derived with `|| true` + wc -l instead.
@@ -2885,9 +2960,33 @@ else
2885
2960
  echo "ℹ️ Shell coverage not typically measured"
2886
2961
  ;;
2887
2962
  *)
2963
+ # #640: hand the staged set to the adapter. Java's run_coverage uses it
2964
+ # for three-tier smart selection (changed-tests -> related -> full);
2965
+ # without it the adapter deliberately runs the full suite rather than
2966
+ # guessing, so this export failing closed is safe.
2967
+ # Colon-separated: newlines do not survive every adapter's word
2968
+ # splitting, and paths with spaces stay intact inside the value.
2969
+ GATE_CHANGED_FILES=$(printf '%s' "$CHANGED_FILES" | tr '\n' ':')
2970
+ export GATE_CHANGED_FILES
2971
+ # The adapter runs inside $(...), so it cannot set PARTIAL_TEST_RUN
2972
+ # for us. It writes the chosen strategy to this file and we raise the
2973
+ # flag here, in the shell that later reads it. Without this a Java
2974
+ # subset run under 80% coverage would take the hard-BLOCK branch
2975
+ # instead of the WARN branch, exactly the bug #556 fixed for Python.
2976
+ _java_partial_file=$(mktemp 2>/dev/null || echo "")
2977
+ if [ -n "$_java_partial_file" ]; then
2978
+ export XP_GATE_PARTIAL_RUN_FILE="$_java_partial_file"
2979
+ fi
2888
2980
  default_cov_output=$(run_coverage 2>&1)
2889
2981
  COV_EXIT=$((COV_EXIT || $?))
2890
2982
  echo "$default_cov_output" | head -30
2983
+ unset XP_GATE_PARTIAL_RUN_FILE GATE_CHANGED_FILES
2984
+ if [ -n "$_java_partial_file" ]; then
2985
+ if [ -s "$_java_partial_file" ]; then
2986
+ PARTIAL_TEST_RUN=true
2987
+ fi
2988
+ rm -f "$_java_partial_file" 2>/dev/null || true
2989
+ fi
2891
2990
  ;;
2892
2991
  esac
2893
2992
  else
@@ -3316,7 +3415,7 @@ else
3316
3415
  # New files must have ≥80% coverage individually.
3317
3416
  # ============================================================================
3318
3417
  if has_project_lang "typescript" && [ -f "coverage/coverage-summary.json" ]; then
3319
- NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
3418
+ NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
3320
3419
  if [ -n "$NEW_SOURCE_FILES" ]; then
3321
3420
  echo ""
3322
3421
  echo " └─ Per-file coverage check for new files:"
@@ -3717,8 +3816,11 @@ else
3717
3816
  # governed by Mirror Parity in CI, which compares content rather than
3718
3817
  # re-linting it. (See the same exclusion in adapters/powershell.sh.)
3719
3818
  MIRROR_EXCLUDE="$MIRROR_PATH_FILTER"
3720
- NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
3721
- MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
3819
+ # filter_project_paths also removes dependencies and build output (#649):
3820
+ # a staged node_modules/ or dist/ file is not budgetable project code, and
3821
+ # entering it in the Boy Scout baseline would budget a path nobody edits.
3822
+ NEW_FILES=$(git diff --cached --name-only --diff-filter=A | filter_project_paths | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
3823
+ MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | filter_project_paths | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
3722
3824
 
3723
3825
  if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
3724
3826
  echo " ✅ No new or modified files to check."
@@ -4129,6 +4231,90 @@ else
4129
4231
  fi
4130
4232
  # ============================================================================
4131
4233
 
4234
+ # ============================================================================
4235
+ # GATE 13: Compiler Strictness Check
4236
+ # The compiler is the first and cheapest static analyser for a compiled language
4237
+ # and XP-Gate never used it as one: cpp.sh only runs clang-tidy/cppcheck over the
4238
+ # first 20 files, and Gate 9's build-integrity half is TypeScript-and-Maven only.
4239
+ # A measured `g++ -Wall -Wextra` pass reports -Wnon-virtual-dtor, -Wreorder,
4240
+ # -Wparentheses and -Wclass-memaccess on an innocuous-looking file; bare `javac`
4241
+ # reports NOTHING for a raw type where `-Xlint:all` reports [rawtypes].
4242
+ #
4243
+ # Verdicts: 0 PASS, 1 FAIL (new findings -> blocked), 2 BLOCK (ran but output
4244
+ # untrustworthy), 3 WARN (tool absent / cross-compiling / timeout / warn-only).
4245
+ # A missing compiler is a SKIP-class WARN per the project convention, NOT a block.
4246
+ #
4247
+ # Escape hatches: XP_GATE_DISABLE_13=1 (kill switch),
4248
+ # XP_GATE_COMPILER_STRICTNESS=strict|balanced|warn-only,
4249
+ # XP_GATE_COMPILER_ANALYZER=1 (gcc -fanalyzer, C only, opt-in).
4250
+ # Design: docs/plans/2026-10-26-gate-13-compiler-strictness-design.md
4251
+ #
4252
+ # NOTE: this block is a TAIL APPEND after Gate 12 and intentionally touches no
4253
+ # line of the gates above it.
4254
+ # ============================================================================
4255
+ GATE_13_STATUS="SKIP"
4256
+ if [ "${XP_GATE_DISABLE_13:-}" = "1" ]; then
4257
+ echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (XP_GATE_DISABLE_13=1)"
4258
+ GATE_13_STATUS="SKIP"
4259
+ elif [ -f "$GATE_DIR/gate-13-compiler-strictness.sh" ] || [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-13-compiler-strictness.sh" ]; then
4260
+ echo "⚙️ Gate 13: Compiler Strictness Check..."
4261
+ GATE13_START=$(gate_start_ms)
4262
+ (
4263
+ # Each stage runs inside the subshell so its shell state cannot leak back
4264
+ # into pre-commit. Verdicts are reduced to the WORST code seen:
4265
+ # FAIL/BLOCK (1/2) always win; WARN (3) only beats PASS (0).
4266
+ _g13_script="$GATE_DIR/gate-13-compiler-strictness.sh"
4267
+ [ -f "$_g13_script" ] || _g13_script="$(git rev-parse --show-toplevel)/githooks/gate-13-compiler-strictness.sh"
4268
+ # shellcheck source=/dev/null
4269
+ . "$_g13_script"
4270
+
4271
+ _g13_worst=0
4272
+ _g13_rc=0
4273
+ _g13_note() {
4274
+ case "$1" in
4275
+ 1|2) _g13_worst=$1 ;;
4276
+ 3) [ "$_g13_worst" -eq 0 ] && _g13_worst=3 ;;
4277
+ esac
4278
+ return 0
4279
+ }
4280
+
4281
+ gate13_run_cpp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
4282
+ gate13_run_java_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
4283
+ gate13_run_analyzer; _g13_rc=$?; _g13_note "$_g13_rc"
4284
+
4285
+ exit "$_g13_worst"
4286
+ )
4287
+ GATE_13_RC=$?
4288
+ GATE13_END=$(now_ms)
4289
+ GATE_13_DURATION=$(( (GATE13_END - GATE13_START) / 1000 ))
4290
+ case "$GATE_13_RC" in
4291
+ 0) GATE_13_STATUS="PASS" ;;
4292
+ 1) GATE_13_STATUS="FAIL" ;;
4293
+ 2) GATE_13_STATUS="BLOCK" ;;
4294
+ *) GATE_13_STATUS="WARN" ;;
4295
+ esac
4296
+ echo " Gate 13 ($GATE_13_STATUS) completed in ${GATE_13_DURATION}s"
4297
+ record_gate_audit "gate-13" "compiler-strictness" "$GATE_13_STATUS" "0" "$GATE13_START"
4298
+ # Every other gate blocks by exiting immediately (e.g. Gate 1's
4299
+ # "❌ BLOCKED ... exit 1"); there is no aggregate-then-exit shared with
4300
+ # gates 0-12, so Gate 13 must do the same or its FAIL would only be cosmetic
4301
+ # in the report while the commit sails through.
4302
+ if [ "$GATE_13_STATUS" = "FAIL" ] || [ "$GATE_13_STATUS" = "BLOCK" ]; then
4303
+ echo ""
4304
+ echo "❌ BLOCKED - Gate 13: Compiler Strictness (${GATE_13_STATUS})"
4305
+ echo " Fix the compiler findings above. To record pre-existing warnings:"
4306
+ echo " xp-gate compiler-baseline create"
4307
+ echo " To relax strictness: XP_GATE_COMPILER_STRICTNESS=warn-only"
4308
+ echo " Emergency only: XP_GATE_DISABLE_13=1"
4309
+ exit 1
4310
+ fi
4311
+ else
4312
+ echo "⏭️ SKIPPED - Gate 13: Compiler Strictness (gate-13-compiler-strictness.sh not found)"
4313
+ GATE_13_STATUS="SKIP"
4314
+ fi
4315
+ echo ""
4316
+ # ============================================================================
4317
+
4132
4318
  generate_quality_report() {
4133
4319
  local HISTORY_FILE=".quality-history.jsonl"
4134
4320
  local COMMIT_HASH
@@ -4425,6 +4611,22 @@ generate_quality_report() {
4425
4611
  }
4426
4612
  ENDJSON
4427
4613
 
4614
+ # ── Gate 13 injection (tail append; no existing line above is modified) ────
4615
+ # The heredoc is closed deliberately before Gate 13 so this sprint ADDS lines
4616
+ # instead of editing gates 0-12's report block, keeping the two branches'
4617
+ # hunks disjoint. Scores are recomputed over 13 gates afterwards.
4618
+ if [ -n "${GATE_13_STATUS:-}" ] && [ -f "$REPORT_FILE" ]; then
4619
+ _XP_G13="$REPORT_FILE" _XP_G13S="$GATE_13_STATUS" node -e "
4620
+ const fs=require('fs'),p=process.env._XP_G13;
4621
+ try{
4622
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
4623
+ j.gates=j.gates||{};
4624
+ j.gates.gate13_compiler_strictness={name:'Compiler Strictness',status:process.env._XP_G13S,tool:'gate-13-compiler-strictness.sh'};
4625
+ fs.writeFileSync(p,JSON.stringify(j,null,2)+'\n');
4626
+ }catch(e){}
4627
+ " 2>/dev/null || true
4628
+ fi
4629
+
4428
4630
  # ── Persistent per-run report (Issue #101) ────────────────────────────────────────
4429
4631
  local REPORTS_DIR=".xp-gate/reports/pre-commit"
4430
4632
  local REPORT_TS
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @test REQ-635
3
+ * @intent `xp-gate init` 会向 ~/.dsh/skills 部署技能、npm 包也带 plugins/dsh,
4
+ * 但 PLATFORM_PROFILES 里没有 dsh 条目,getSkillsDirs('dsh') 静默回落到
5
+ * opencode 的路径,导致 DSH 机器的技能从不被解析(#635)
6
+ * @covers AC-635-04, AC-635-05
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ describe('PLATFORM_PROFILES covers dsh (#635)', () => {
13
+ let tmpHome;
14
+ let originalHome;
15
+
16
+ beforeEach(() => {
17
+ originalHome = process.env.HOME;
18
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh-'));
19
+ process.env.HOME = tmpHome;
20
+ vi.resetModules();
21
+ delete require.cache[require.resolve('../detect-deps')];
22
+ delete require.cache[require.resolve('../shared-paths')];
23
+ });
24
+
25
+ afterEach(() => {
26
+ process.env.HOME = originalHome;
27
+ fs.rmSync(tmpHome, { recursive: true, force: true });
28
+ });
29
+
30
+ // AC-635-04: asking for dsh skills must return the dsh directory, not a
31
+ // silent fallback to opencode's.
32
+ it('getSkillsDirs("dsh") resolves ~/.dsh, not the opencode fallback', () => {
33
+ const { getSkillsDirs } = require('../detect-deps');
34
+ const dirs = getSkillsDirs('dsh');
35
+
36
+ expect(dirs.length).toBeGreaterThan(0);
37
+ expect(dirs[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
38
+ // The pre-fix behaviour: falls through to PLATFORM_PROFILES.opencode.
39
+ expect(dirs[0]).not.toContain(path.join('.config', 'opencode'));
40
+ });
41
+
42
+ // AC-635-05: the profile is a real entry, so a caller enumerating platforms
43
+ // sees dsh alongside the three that were already there.
44
+ it('dsh is an enumerable platform profile with skillsDirs and requiredDeps', () => {
45
+ const { PLATFORM_PROFILES, getSkillsDirs } = require('../detect-deps');
46
+
47
+ expect(Object.keys(PLATFORM_PROFILES)).toContain('dsh');
48
+ expect(Object.keys(PLATFORM_PROFILES)).toEqual(
49
+ expect.arrayContaining(['opencode', 'claude-code', 'qoder', 'dsh'])
50
+ );
51
+
52
+ const profile = PLATFORM_PROFILES.dsh;
53
+ expect(Array.isArray(profile.skillsDirs)).toBe(true);
54
+ expect(profile.requiredDeps).toBeDefined();
55
+ // Profiles store home-relative segments; getSkillsDirs resolves them.
56
+ expect(profile.skillsDirs[0]).toEqual(['.dsh', 'skills']);
57
+ expect(getSkillsDirs('dsh')[0]).toBe(path.join(tmpHome, '.dsh', 'skills'));
58
+ });
59
+
60
+ // Every profile must be plain data — no getters mixed in with values, which
61
+ // would break Object.keys()/JSON round-trips and made the object's contract
62
+ // depend on which key you touched (Delphi R1 CI-01).
63
+ it('every profile is plain enumerable data, with no accessor properties', () => {
64
+ const { PLATFORM_PROFILES } = require('../detect-deps');
65
+
66
+ for (const name of Object.keys(PLATFORM_PROFILES)) {
67
+ const descriptor = Object.getOwnPropertyDescriptor(PLATFORM_PROFILES, name);
68
+ expect(descriptor.get, `${name} must not be an accessor`).toBeUndefined();
69
+ // A plain value also survives a JSON round-trip unchanged.
70
+ expect(JSON.parse(JSON.stringify(PLATFORM_PROFILES[name]))).toEqual(
71
+ PLATFORM_PROFILES[name]
72
+ );
73
+ }
74
+ });
75
+
76
+ // Paths must follow process.env.HOME rather than pinning the home directory
77
+ // at require() time — the trap doctor.js documents at its top.
78
+ it('resolved paths follow a HOME change after require', () => {
79
+ const { getSkillsDirs } = require('../detect-deps');
80
+ // Loaded above with the first tmpHome; move HOME and resolve again.
81
+ const secondHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-dsh2-'));
82
+ try {
83
+ process.env.HOME = secondHome;
84
+ expect(getSkillsDirs('dsh')[0]).toBe(path.join(secondHome, '.dsh', 'skills'));
85
+ // The pre-existing profiles must follow too — they used to be frozen at
86
+ // require() time from a destructured HOME_DIR.
87
+ expect(getSkillsDirs('opencode')[0]).toBe(
88
+ path.join(secondHome, '.config', 'opencode', 'skills')
89
+ );
90
+ } finally {
91
+ fs.rmSync(secondHome, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ // Anti-vacuity: the three pre-existing profiles must keep their own paths,
96
+ // proving the getter did not disturb them.
97
+ it('leaves the pre-existing profiles unchanged', () => {
98
+ const { getSkillsDirs } = require('../detect-deps');
99
+
100
+ expect(getSkillsDirs('opencode')[0]).toBe(
101
+ path.join(tmpHome, '.config', 'opencode', 'skills')
102
+ );
103
+ expect(getSkillsDirs('qoder')[0]).toBe(path.join(tmpHome, '.qoder', 'skills'));
104
+ expect(getSkillsDirs('claude-code')[0]).toBe(path.join(tmpHome, '.claude', 'skills'));
105
+ });
106
+ });