@boyingliu01/xp-gate 0.20.1 → 0.21.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 (84) hide show
  1. package/adapters/java.sh +373 -27
  2. package/adapters/python.sh +73 -6
  3. package/gate-10.sh +107 -64
  4. package/gate-3.sh +51 -3
  5. package/gate-4.sh +32 -1
  6. package/hooks/gate-10.sh +107 -64
  7. package/hooks/gate-3.sh +51 -3
  8. package/hooks/gate-4.sh +32 -1
  9. package/hooks/pre-commit +319 -57
  10. package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
  11. package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
  12. package/lib/doctor-tui.js +21 -3
  13. package/lib/doctor.js +111 -8
  14. package/lib/gate-audit.ts +3 -0
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/mock-policy/AGENTS.md +4 -4
  17. package/mutation/AGENTS.md +4 -4
  18. package/package.json +1 -1
  19. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
  21. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
  22. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
  23. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  24. package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
  25. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
  26. package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
  27. package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
  28. package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
  29. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  30. package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
  31. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  32. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
  33. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
  34. package/plugins/dsh/package.json +1 -1
  35. package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
  36. package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
  37. package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
  38. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  39. package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
  40. package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
  41. package/plugins/dsh/skills/grilling/SKILL.md +1 -0
  42. package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
  43. package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
  44. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  45. package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
  46. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  47. package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
  48. package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
  49. package/plugins/opencode/package.json +1 -1
  50. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
  51. package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
  52. package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
  53. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  54. package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
  55. package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
  56. package/plugins/opencode/skills/grilling/SKILL.md +1 -0
  57. package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
  58. package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
  59. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  60. package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
  61. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  62. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
  63. package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
  64. package/plugins/qoder/plugin.json +1 -1
  65. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  66. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  67. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  68. package/principles/AGENTS.md +4 -4
  69. package/principles/adapters/__tests__/java.test.ts +317 -0
  70. package/principles/adapters/java.ts +325 -13
  71. package/skills/admin-template-guidelines/SKILL.md +1 -0
  72. package/skills/batch-grill-me/SKILL.md +1 -0
  73. package/skills/clipboard-vision/SKILL.md +1 -0
  74. package/skills/delphi-review/AGENTS.md +4 -4
  75. package/skills/domain-modeling/SKILL.md +1 -0
  76. package/skills/grill-with-docs/SKILL.md +1 -0
  77. package/skills/grilling/SKILL.md +1 -0
  78. package/skills/improve-codebase-architecture/SKILL.md +1 -0
  79. package/skills/ralph-loop/SKILL.md +1 -0
  80. package/skills/sprint-flow/AGENTS.md +4 -4
  81. package/skills/test-driven-development/SKILL.md +1 -0
  82. package/skills/test-specification-alignment/AGENTS.md +4 -4
  83. package/skills/test-specification-alignment/SKILL.md +1 -0
  84. package/skills/to-issues/SKILL.md +1 -0
package/hooks/gate-4.sh CHANGED
@@ -1,3 +1,4 @@
1
+ # shellcheck shell=bash
1
2
  # ============================================================================
2
3
  # GATE 4: Principles Checker (Clean Code + SOLID)
3
4
  # Reuses existing principles checker logic
@@ -23,6 +24,31 @@ if ! declare -F run_tsx >/dev/null 2>&1; then
23
24
  }
24
25
  fi
25
26
 
27
+ # Java rule coverage annotation (#507 S4, AC-507-04-02). The normative matrix
28
+ # lives in docs/java-principles-coverage.md; these constants mirror it. Before
29
+ # #507 the Java adapter fed rules only {name,type,line,code}, so the 12 rules
30
+ # reading structured metrics silently saw `undefined` and a Java commit got a
31
+ # green "checked" with zero effective coverage. 12/15 now fire; the three
32
+ # inert ones are rule-side gaps documented in the coverage doc -- they must
33
+ # NOT be announced as covered.
34
+ JAVA_COVERAGE_EFFECTIVE=12
35
+ JAVA_COVERAGE_TOTAL=15
36
+ JAVA_COVERAGE_INERT="lsp, many-exports, code-duplication"
37
+
38
+ # Print the coverage annotation when (and only when) the staged file list
39
+ # contains Java sources. Non-Java commits keep the original output byte for
40
+ # byte. Pure shell builtins: no subprocess per file.
41
+ java_coverage_note() {
42
+ local _f _has_java=""
43
+ for _f in "$@"; do
44
+ case "$_f" in
45
+ *.java|*.Java|*.JAVA) _has_java=1; break ;;
46
+ esac
47
+ done
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)"
50
+ }
51
+
26
52
  2>&1 echo ""
27
53
  2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
28
54
  GATE_4_START=$(gate_start_ms)
@@ -72,7 +98,12 @@ else
72
98
 
73
99
  if [ -n "$PRINCIPLES_DIR" ]; then
74
100
  echo "Checking Clean Code + SOLID principles..."
75
-
101
+
102
+ # AC-507-04-02: say what a Java commit is actually covered for, instead
103
+ # of implying a full 15-rule check (#507). Printed before the run so it
104
+ # appears on both the PASS and the FAIL path.
105
+ java_coverage_note ${PRINCIPLES_ARGS[@]+"${PRINCIPLES_ARGS[@]}"}
106
+
76
107
  if command -v npx > /dev/null 2>&1 || [ -f "$PRINCIPLES_BASE/node_modules/tsx/dist/cli.mjs" ]; then
77
108
  # Run principles checker and store results.
78
109
  #
package/hooks/pre-commit CHANGED
@@ -242,6 +242,9 @@ record_gate_audit() {
242
242
  local passed="$3"
243
243
  local issues_found="$4"
244
244
  local start_ms="$5"
245
+ # Optional free-form detail (e.g. gate-3's effective CCN threshold, #507).
246
+ # Additive and omitted when absent, so existing callers stay unchanged.
247
+ local detail="${6:-}"
245
248
  local end_ms
246
249
  end_ms=$(now_ms)
247
250
  local duration_ms=$((end_ms - start_ms))
@@ -271,6 +274,7 @@ record_gate_audit() {
271
274
  --duration-ms "$duration_ms" \
272
275
  $duration_anomaly_flag \
273
276
  --trigger commit \
277
+ ${detail:+--detail "$detail"} \
274
278
  2>/dev/null
275
279
  } &
276
280
  disown 2>/dev/null || true
@@ -474,8 +478,19 @@ echo ""
474
478
  # unconditional and fail-closed (#499), and gating it behind a path-filtered
475
479
  # list would let a mirror-only commit land with zero secret scanning and no
476
480
  # CI backstop.
481
+ # Single source of truth for the mirror-path filter (#510): every consumer
482
+ # that must ignore the byte-identical npm-package mirrors (lint corpus here,
483
+ # Boy Scout's new/modified split further down) reads this one regex, so the
484
+ # two sites cannot drift apart.
485
+ # dashboard/ is a standalone browser-only visualization page (document/Chart.js
486
+ # globals, static HTML output). It is not part of the gate engine, never runs
487
+ # in CI or hooks, and its chart-config object literals trip structural rules
488
+ # (deep-nesting/long-function) that carry no signal for this codebase. Same
489
+ # scope rationale as the mirror exclusion (#507 Delphi round-2 follow-up).
490
+ MIRROR_PATH_FILTER='^src/npm-package/'
491
+ DASHBOARD_PATH_FILTER='^dashboard/'
477
492
  RAW_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
478
- CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" | grep -v '^src/npm-package/' || true)
493
+ CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" | grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" || true)
479
494
 
480
495
  if [ -z "$RAW_CHANGED_FILES" ]; then
481
496
  echo "No files changed. Skipping gates."
@@ -644,21 +659,21 @@ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
644
659
  PROJECT_LANGS="dart"
645
660
  fi
646
661
  elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
647
- KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" 2>/dev/null | wc -l)
662
+ KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | wc -l)
648
663
  if [ "$KOTLIN_FILES" -gt 0 ]; then
649
664
  PROJECT_LANGS="kotlin"
650
665
  else
651
666
  PROJECT_LANGS="java"
652
667
  fi
653
668
  elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
654
- CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
655
- OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
669
+ CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
670
+ OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
656
671
  if [ -n "$CPP_FILES" ]; then
657
672
  PROJECT_LANGS="cpp"
658
673
  elif [ -n "$OBJC_FILES" ]; then
659
674
  PROJECT_LANGS="objectivec"
660
675
  fi
661
- elif [ -n "$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)" ]; then
676
+ elif [ -n "$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)" ]; then
662
677
  PROJECT_LANGS="powershell"
663
678
  fi
664
679
  fi
@@ -695,30 +710,53 @@ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
695
710
  done
696
711
  return 1
697
712
  }
713
+ # #515: when the repo ROOT itself declares the project, a nested marker that
714
+ # is merely a module file (skill_cert/cli/setup.py incident) must not hijack
715
+ # PROJECT_SUBDIR — Gates 1-9 would run from the wrong cwd and false-BLOCK on
716
+ # root-relative paths. The root-marker set below MUST stay aligned with
717
+ # _detect_subdir's marker set above (10 markers): every ecosystem recognized
718
+ # for subproject derivation is also protected at the root. The JS-monorepo
719
+ # check runs FIRST (#515 R2 delphi walkthrough): a root package.json with a
720
+ # "workspaces" key declares a JS monorepo (often together with a shared
721
+ # tsconfig.json), not a single project, so derivation keeps running there.
722
+ # Known limitation (#527): Gradle multi-module, CMake subdirectories and
723
+ # pnpm/Lerna workspaces are not disambiguated from single projects yet —
724
+ # see the pinned BATS case.
725
+ _skip_subdir_derive=0
726
+ if [ -f package.json ] && grep -q '"workspaces":' package.json 2>/dev/null; then
727
+ _skip_subdir_derive=0 # JS monorepo root: subproject derivation stays on
728
+ elif [ -f package.json ] || [ -f tsconfig.json ] || [ -f pyproject.toml ] || \
729
+ [ -f setup.py ] || [ -f go.mod ] || [ -f pubspec.yaml ] || \
730
+ [ -f pom.xml ] || [ -f build.gradle ] || \
731
+ [ -f build.gradle.kts ] || [ -f CMakeLists.txt ]; then
732
+ _skip_subdir_derive=1
733
+ fi
698
734
  _subdir_set=""
699
- while IFS= read -r _changed; do
700
- [ -z "$_changed" ] && continue
701
- _d=$(_detect_subdir "$_changed" 2>/dev/null) || true
702
- if [ -n "$_d" ] && [ "$_d" != "." ]; then
703
- case " $_subdir_set " in
704
- *" $_d "*) ;; # already seen
705
- *) _subdir_set="${_subdir_set} $_d" ;;
706
- esac
707
- fi
708
- done <<CHANGEDFILELIST
735
+ if [ "$_skip_subdir_derive" -eq 0 ]; then
736
+ while IFS= read -r _changed; do
737
+ [ -z "$_changed" ] && continue
738
+ _d=$(_detect_subdir "$_changed" 2>/dev/null) || true
739
+ if [ -n "$_d" ] && [ "$_d" != "." ]; then
740
+ case " $_subdir_set " in
741
+ *" $_d "*) ;; # already seen
742
+ *) _subdir_set="${_subdir_set} $_d" ;;
743
+ esac
744
+ fi
745
+ done <<CHANGEDFILELIST
709
746
  $CHANGED_FILES
710
747
  CHANGEDFILELIST
711
- _subdir_count=$(echo "$_subdir_set" | awk '{ n=0; for(i=1;i<=NF;i++) n++; print n }')
712
- if [ "$_subdir_count" = "1" ]; then
713
- PROJECT_SUBDIR=$(echo "$_subdir_set" | awk '{print $1}')
714
- echo ""
715
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
716
- echo " 📁 MULTILANGUAGE SUBDIR DETECTED FROM CHANGED FILES"
717
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
718
- echo ""
719
- echo "Language: $PROJECT_LANGS"
720
- echo "Location: $PROJECT_SUBDIR/"
721
- echo ""
748
+ _subdir_count=$(echo "$_subdir_set" | awk '{ n=0; for(i=1;i<=NF;i++) n++; print n }')
749
+ if [ "$_subdir_count" = "1" ]; then
750
+ PROJECT_SUBDIR=$(echo "$_subdir_set" | awk '{print $1}')
751
+ echo ""
752
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
753
+ echo " 📁 MULTILANGUAGE SUBDIR DETECTED FROM CHANGED FILES"
754
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
755
+ echo ""
756
+ echo "Language: $PROJECT_LANGS"
757
+ echo "Location: $PROJECT_SUBDIR/"
758
+ echo ""
759
+ fi
722
760
  fi
723
761
  fi
724
762
 
@@ -726,7 +764,7 @@ CHANGEDFILELIST
726
764
  # Only used when extension-based detection found nothing
727
765
  if [ -z "$PROJECT_LANGS" ]; then
728
766
  # Search for TypeScript/JavaScript projects
729
- TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
767
+ TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "*/node_modules/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
730
768
  if [ -n "$TSCONFIG_SUB" ]; then
731
769
  PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
732
770
  if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
@@ -738,7 +776,7 @@ CHANGEDFILELIST
738
776
 
739
777
  # Search for Python projects
740
778
  if [ -z "$PROJECT_LANGS" ]; then
741
- PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" 2>/dev/null | head -1)
779
+ PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
742
780
  if [ -n "$PYPROJECT_SUB" ]; then
743
781
  PROJECT_LANGS="python"
744
782
  PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
@@ -747,7 +785,7 @@ CHANGEDFILELIST
747
785
 
748
786
  # Search for Go projects
749
787
  if [ -z "$PROJECT_LANGS" ]; then
750
- GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" 2>/dev/null | head -1)
788
+ GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
751
789
  if [ -n "$GOMOD_SUB" ]; then
752
790
  PROJECT_LANGS="go"
753
791
  PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
@@ -756,7 +794,7 @@ CHANGEDFILELIST
756
794
 
757
795
  # Search for Flutter/Dart projects
758
796
  if [ -z "$PROJECT_LANGS" ]; then
759
- PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" 2>/dev/null | head -1)
797
+ PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
760
798
  if [ -n "$PUBSPEC_SUB" ]; then
761
799
  if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
762
800
  PROJECT_LANGS="flutter"
@@ -769,8 +807,8 @@ CHANGEDFILELIST
769
807
 
770
808
  # Search for Java/Kotlin projects
771
809
  if [ -z "$PROJECT_LANGS" ]; then
772
- POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" 2>/dev/null | head -1)
773
- GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" 2>/dev/null | head -1)
810
+ POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
811
+ GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
774
812
  if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
775
813
  MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
776
814
  PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
@@ -785,7 +823,7 @@ CHANGEDFILELIST
785
823
 
786
824
  # Search for C++ projects
787
825
  if [ -z "$PROJECT_LANGS" ]; then
788
- CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" 2>/dev/null | head -1)
826
+ CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
789
827
  if [ -n "$CMAKE_SUB" ]; then
790
828
  PROJECT_LANGS="cpp"
791
829
  PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
@@ -794,7 +832,7 @@ CHANGEDFILELIST
794
832
 
795
833
  # Search for React Native projects (ios/ + android/ directories)
796
834
  if [ -z "$PROJECT_LANGS" ]; then
797
- RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" 2>/dev/null | head -1)
835
+ RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
798
836
  if [ -n "$RN_SUB" ]; then
799
837
  PROJECT_SUBDIR=$(dirname "$RN_SUB")
800
838
  if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
@@ -805,7 +843,7 @@ CHANGEDFILELIST
805
843
 
806
844
  # Search for PowerShell projects
807
845
  if [ -z "$PROJECT_LANGS" ]; then
808
- PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)
846
+ PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
809
847
  if [ -n "$PS_SUB" ]; then
810
848
  PROJECT_LANGS="powershell"
811
849
  PROJECT_SUBDIR=$(dirname "$PS_SUB")
@@ -830,7 +868,7 @@ CHANGEDFILELIST
830
868
 
831
869
  # ---- Documentation-only fallback ----
832
870
  if [ -z "$PROJECT_LANGS" ]; then
833
- CODE_FILES=$(find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.java" -o -name "*.go" -o -name "*.rs" -o -name "*.cpp" -o -name "*.c" -o -name "*.swift" -o -name "*.kt" -o -name "*.sh" -o -name "*.dart" -o -name "*.ps1" \) -not -path "./.git/*" 2>/dev/null | wc -l)
871
+ CODE_FILES=$(find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.java" -o -name "*.go" -o -name "*.rs" -o -name "*.cpp" -o -name "*.c" -o -name "*.swift" -o -name "*.kt" -o -name "*.sh" -o -name "*.dart" -o -name "*.ps1" \) -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | wc -l)
834
872
 
835
873
  if [ "$CODE_FILES" -eq 0 ]; then
836
874
  PROJECT_LANGS="documentation-only"
@@ -1332,7 +1370,66 @@ else
1332
1370
  echo "✅ PASSED - Basic shell script syntax check."
1333
1371
  fi
1334
1372
  ;;
1335
-
1373
+
1374
+ java)
1375
+ # GATE 1 Java: wc-java-lint with fail-closed verdicts (#507 S1, DR-001).
1376
+ # java.sh run_static_analysis verdict codes:
1377
+ # 0=PASS 1=FAIL(violations) 2=BLOCK(execution error) 3=WARN(non-blocking).
1378
+ # Every non-0/1/2 outcome is scored as WARN -- IN the denominator, so it
1379
+ # forbids an overall PASS. Delphi round-2 A-MAJOR-1: these paths used to
1380
+ # map to SKIP, which sits OUT of the denominator, letting a repo with no
1381
+ # Java engine (or findings suppressed in report mode) report PASS/10.0
1382
+ # while its Java quality was never verified.
1383
+ # The adapter applies the XP_GATE_WC_JAVA_LINT=soft escape valve itself, so
1384
+ # a 2 reaching this branch is hard: BLOCK the commit. Captured to a var
1385
+ # (not piped) so the verdict code survives regardless of output length --
1386
+ # `cmd | head` makes a long report die with SIGPIPE, which would turn a
1387
+ # real verdict into an accident of line count.
1388
+ ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
1389
+ if [ -z "$ADAPTER_FILE" ] || ! source "$ADAPTER_FILE" 2>/dev/null; then
1390
+ echo "⚠️ WARN - Java adapter not found (code quality NOT verified)"
1391
+ GATE_1_STATUS="WARN"
1392
+ GATE_1_DETAIL="java: adapter missing"
1393
+ else
1394
+ JAVA_G1_EXIT=0
1395
+ # `|| JAVA_G1_EXIT=$?` (not a bare assignment): under a set -e consumer a
1396
+ # non-zero verdict would abort the host hook before the mapping below.
1397
+ JAVA_G1_OUTPUT=$(run_static_analysis 2>&1) || JAVA_G1_EXIT=$?
1398
+ # SKIP/WARN reasons cross the command-substitution subshell as an
1399
+ # `XP_G1_REASON: ...` stdout line -- an `export` inside the adapter can
1400
+ # NEVER reach this process (#507 Delphi round-1 B-MAJOR-1: verified the
1401
+ # subshell discards the child env). Parse, then strip from display.
1402
+ JAVA_G1_REASON=$(printf '%s\n' "$JAVA_G1_OUTPUT" | sed -n 's/^XP_G1_REASON: //p' | head -1)
1403
+ if [ -n "$JAVA_G1_REASON" ]; then
1404
+ JAVA_G1_OUTPUT=$(printf '%s\n' "$JAVA_G1_OUTPUT" | grep -v '^XP_G1_REASON: ')
1405
+ fi
1406
+ [ -n "$JAVA_G1_OUTPUT" ] && echo "$JAVA_G1_OUTPUT" | head -40
1407
+ case "$JAVA_G1_EXIT" in
1408
+ 0)
1409
+ echo "✅ PASSED - Java code quality (wc-java-lint)."
1410
+ ;;
1411
+ 1)
1412
+ echo "❌ BLOCKED - Java static analysis violations (fix or add suppressions)"
1413
+ exit 1
1414
+ ;;
1415
+ 2)
1416
+ echo "❌ BLOCKED - wc-java-lint unusable (fail-closed; unset XP_GATE_WC_JAVA_LINT=soft only if you accept the risk)"
1417
+ exit 1
1418
+ ;;
1419
+ 3)
1420
+ echo "⚠️ WARN - Java code quality NOT verified (engine unavailable or downgraded)"
1421
+ GATE_1_STATUS="WARN"
1422
+ GATE_1_DETAIL="java: ${JAVA_G1_REASON:-unavailable (warn)}"
1423
+ ;;
1424
+ *)
1425
+ echo "⚠️ WARN - Java code quality NOT verified (unexpected verdict $JAVA_G1_EXIT)"
1426
+ GATE_1_STATUS="WARN"
1427
+ GATE_1_DETAIL="java: unexpected verdict $JAVA_G1_EXIT"
1428
+ ;;
1429
+ esac
1430
+ fi
1431
+ ;;
1432
+
1336
1433
  *)
1337
1434
  # Route to appropriate adapter for other languages
1338
1435
  ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
@@ -1367,9 +1464,13 @@ else
1367
1464
  done
1368
1465
  fi
1369
1466
  fi # End: if ! any_changed_files_match (Issue #312 skip)
1370
- GATE_1_STATUS="PASS"
1467
+ GATE_1_STATUS="${GATE_1_STATUS:-PASS}"
1371
1468
  GATE_1_TOOL="${PROJECT_LANGS}"
1372
- record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
1469
+ # The java branch may set GATE_1_STATUS=WARN + a GATE_1_DETAIL warn reason
1470
+ # (engine unavailable / soft or report downgrade); route it into the audit
1471
+ # trail via the S2 detail plumbing so a WARN verdict is diagnosable, not
1472
+ # silent. WARN stays IN the scoring denominator (Delphi round-2 A-MAJOR-1).
1473
+ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START" "${GATE_1_DETAIL:-}"
1373
1474
 
1374
1475
  # ============================================================================
1375
1476
  # GATE 2: Duplicate Code Detection (NEW gate)
@@ -2701,9 +2802,9 @@ else
2701
2802
  # real source path stays unbudgeted. Exclude it and let the mirroring be
2702
2803
  # governed by Mirror Parity in CI, which compares content rather than
2703
2804
  # re-linting it. (See the same exclusion in adapters/powershell.sh.)
2704
- MIRROR_EXCLUDE='^src/npm-package/'
2705
- NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | tr '\n' ' ')
2706
- MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | grep -v "$MIRROR_EXCLUDE" | tr '\n' ' ')
2805
+ MIRROR_EXCLUDE="$MIRROR_PATH_FILTER"
2806
+ NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
2807
+ MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
2707
2808
 
2708
2809
  if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
2709
2810
  echo " ✅ No new or modified files to check."
@@ -2794,9 +2895,73 @@ fi
2794
2895
  # ============================================================================
2795
2896
  # GATE 9: Build Integrity Check (TypeScript compilation + package + imports)
2796
2897
  # ============================================================================
2898
+
2899
+ # Java build-integrity dispatch (#507 S5, DR-003). The old gate only knew
2900
+ # TypeScript: a Maven project got a permanent SKIP -- "not a TypeScript
2901
+ # project" -- which is exactly the silent-no-coverage path this sprint
2902
+ # removes. Verdict codes (mirror of the java.sh G1 contract):
2903
+ # 0=PASS 2=BLOCK(compile failed) 3=SKIP+WARN(reason printed)
2904
+ resolve_build_integrity_timeout() {
2905
+ local file=".xp-gate/build-integrity-timeout" val=""
2906
+ if [ -f "$file" ]; then
2907
+ val=$(tr -d '[:space:]' < "$file")
2908
+ fi
2909
+ if [ -z "$val" ]; then
2910
+ echo "180"
2911
+ return 0
2912
+ fi
2913
+ case "$val" in
2914
+ ''|*[!0-9]*|0)
2915
+ echo "WARN - invalid $file value '${val}' (expected a positive integer), falling back to 180s" >&2
2916
+ echo "180"
2917
+ ;;
2918
+ *) echo "$val" ;;
2919
+ esac
2920
+ }
2921
+
2922
+ run_java_build_integrity() {
2923
+ local timeout_s build_bin
2924
+ # Wrapper first: mvnw pins the project's own Maven version.
2925
+ if [ -x "./mvnw" ]; then
2926
+ build_bin="./mvnw"
2927
+ elif command -v mvn >/dev/null 2>&1; then
2928
+ build_bin="mvn"
2929
+ else
2930
+ echo " ⚠️ Java changes staged but no Maven build tool found (mvnw/mvn)"
2931
+ echo " ⏭️ SKIPPED - Build integrity Java (no build tool)"
2932
+ return 3
2933
+ fi
2934
+
2935
+ timeout_s=$(resolve_build_integrity_timeout)
2936
+ if command -v timeout >/dev/null 2>&1; then
2937
+ timeout "$timeout_s" "$build_bin" -q -DskipTests compile 2>&1 | tail -20
2938
+ else
2939
+ "$build_bin" -q -DskipTests compile 2>&1 | tail -20
2940
+ fi
2941
+ local rc="${PIPESTATUS[0]}"
2942
+ if [ "$rc" -eq 0 ]; then
2943
+ echo " ✅ Java build integrity verified (mvn -q -DskipTests compile)."
2944
+ return 0
2945
+ fi
2946
+ if [ "$rc" -eq 124 ]; then
2947
+ echo " ⏱️ Java build timed out after ${timeout_s}s."
2948
+ echo " ⏭️ SKIPPED - Build integrity Java (timeout)"
2949
+ return 3
2950
+ fi
2951
+ echo " ❌ Java build failed (exit $rc)"
2952
+ return 2
2953
+ }
2954
+
2797
2955
  2>&1 echo ""
2798
2956
  2>&1 echo "→ Gate 9: Build integrity... "
2799
2957
  GATE_9_START=$(gate_start_ms)
2958
+ # Sub-status aggregation (#507 Delphi round-1 B-MAJOR-4): each half records
2959
+ # its own outcome; GATE_9_STATUS is aggregated after both. The previous
2960
+ # single-variable design made a Maven-only repo (TS half never runs, Java
2961
+ # half passes) record an ETERNAL SKIP -- a real compile verification that
2962
+ # never appeared as PASS, contradicting S5's own motivation.
2963
+ GATE_9_TS_STATUS="SKIP"
2964
+ GATE_9_JAVA_STATUS="SKIP"
2800
2965
  GATE_9_STATUS="SKIP"
2801
2966
 
2802
2967
  if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
@@ -2813,15 +2978,15 @@ if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
2813
2978
  CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
2814
2979
  GATE_9_OUTPUT=$(mktemp)
2815
2980
  if RUN_TSX_TIMEOUT=120s run_tsx "$GATE_9_SCRIPT" --changed-files "$CHANGED_FILES_CSV" --project-root "$PROJECT_ROOT" --timeout 115000 > "$GATE_9_OUTPUT" 2>&1; then
2816
- GATE_9_STATUS="PASS"
2981
+ GATE_9_TS_STATUS="PASS"
2817
2982
  echo " ✅ Build integrity check passed."
2818
2983
  else
2819
2984
  GATE_9_EXIT=$?
2820
2985
  if [ $GATE_9_EXIT -eq 124 ]; then
2821
- GATE_9_STATUS="SKIP"
2986
+ GATE_9_TS_STATUS="SKIP"
2822
2987
  echo " ⏱️ Build integrity check timed out (120s). SKIPPED."
2823
2988
  else
2824
- GATE_9_STATUS="BLOCK"
2989
+ GATE_9_TS_STATUS="BLOCK"
2825
2990
  cat "$GATE_9_OUTPUT"
2826
2991
  echo ""
2827
2992
  echo "❌ BLOCKED - Gate 9: Build integrity check failed"
@@ -2840,6 +3005,52 @@ else
2840
3005
  echo " ⏭️ SKIPPED - Build integrity (not a TypeScript project)"
2841
3006
  fi
2842
3007
 
3008
+ # Java dispatch (#507 S5): runs when .java files are staged, regardless of
3009
+ # whether the TypeScript half ran. Gradle-only repos SKIP+WARN (gradle
3010
+ # support is backlog; DR-003).
3011
+ if echo "$CHANGED_FILES" | grep -q '\.java$'; then
3012
+ if [ -f "pom.xml" ]; then
3013
+ JAVA_BI_EXIT=0
3014
+ run_java_build_integrity || JAVA_BI_EXIT=$?
3015
+ case "$JAVA_BI_EXIT" in
3016
+ 0)
3017
+ GATE_9_JAVA_STATUS="PASS"
3018
+ ;;
3019
+ 2)
3020
+ GATE_9_JAVA_STATUS="BLOCK"
3021
+ echo ""
3022
+ echo "❌ BLOCKED - Gate 9: Java build integrity check failed"
3023
+ echo "Fix the compile errors before committing."
3024
+ exit 1
3025
+ ;;
3026
+ *)
3027
+ GATE_9_JAVA_STATUS="SKIP"
3028
+ echo " ⏭️ SKIPPED - Build integrity Java (see reason above)"
3029
+ ;;
3030
+ esac
3031
+ elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
3032
+ echo " ⚠️ Java changes staged but this is a Gradle project - build integrity for Gradle is not implemented yet"
3033
+ echo " ⏭️ SKIPPED - Build integrity Java (Gradle backlog, #507 DR-003)"
3034
+ else
3035
+ echo " ℹ️ Java changes staged but no Maven/Gradle build file - skipping Java build integrity"
3036
+ fi
3037
+ fi
3038
+
3039
+ # Aggregate the two halves (#507 Delphi round-1 B-MAJOR-4): PASS when every
3040
+ # half that RAN passed (TS PASS + Java SKIP, or vice versa, both count),
3041
+ # SKIP only when NOTHING ran, BLOCK on any failure (normally unreachable --
3042
+ # failing halves exit 1 inline). This replaces the blind-review MINOR-1
3043
+ # single-variable guard: with explicit sub-statuses a Java PASS no longer
3044
+ # "resurrects" a TS SKIP -- each half keeps its own record and the gate
3045
+ # reflects the union of what was actually verified.
3046
+ GATE_9_STATUS="SKIP"
3047
+ if [ "$GATE_9_TS_STATUS" = "PASS" ] || [ "$GATE_9_JAVA_STATUS" = "PASS" ]; then
3048
+ GATE_9_STATUS="PASS"
3049
+ fi
3050
+ if [ "$GATE_9_TS_STATUS" = "BLOCK" ] || [ "$GATE_9_JAVA_STATUS" = "BLOCK" ]; then
3051
+ GATE_9_STATUS="BLOCK"
3052
+ fi
3053
+
2843
3054
  GATE_9_END=$(gate_start_ms)
2844
3055
  GATE_9_DURATION_MS=$((GATE_9_END - GATE_9_START))
2845
3056
  GATE_9_DURATION=$((GATE_9_DURATION_MS / 1000))
@@ -2872,7 +3083,7 @@ fi
2872
3083
  # Validates sprint state consistency (delphi-review APPROVED before BUILD)
2873
3084
  # Uses sprint-gate.sh for standalone validation logic
2874
3085
  # ============================================================================
2875
- GATE_11_START=$(date +%s)
3086
+ GATE_11_START=$(gate_start_ms)
2876
3087
  GATE_11_STATUS="PASS"
2877
3088
 
2878
3089
  # BEGIN SPRINT_GATE_SCRIPT RESOLUTION
@@ -2961,24 +3172,72 @@ generate_quality_report() {
2961
3172
  local BRANCH
2962
3173
  local PASSED_COUNT=0
2963
3174
  local TOTAL_GATES=12
3175
+ local SKIP_COUNT=0
3176
+ local WARN_COUNT=0
3177
+ local NONPASS_COUNT=0
2964
3178
 
2965
3179
  COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
2966
3180
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
2967
3181
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2968
3182
 
3183
+ # Scoring per #507 S3: SKIP removes a gate from the denominator (a missing
3184
+ # tool must not drag the score down -- that was the "honest but punishing"
3185
+ # old behavior), WARN stays in the denominator but never counts as passed,
3186
+ # and FAIL/BLOCK count as non-passing items like before. NONPASS tracks
3187
+ # non-passing gates INSIDE the effective denominator only -- a skipped gate
3188
+ # is out of scope, not a failure.
3189
+ local gate_status gate_default
2969
3190
  for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
2970
3191
  local status_var="GATE_${gate}_STATUS"
2971
- if [ "${!status_var}" = "PASS" ]; then
2972
- PASSED_COUNT=$((PASSED_COUNT + 1))
2973
- fi
3192
+ # Default mirrors the JSON report rendering below EXACTLY (gates 1-11
3193
+ # PASS, gate 12 WARN): a gate that never assigned a status is "not
3194
+ # applicable" and must render and score identically. A previous scoring
3195
+ # default of FAIL produced self-contradictory reports -- the gate showed
3196
+ # PASS in JSON but scored as FAIL (#507 Delphi round-1 A-MAJOR-2).
3197
+ if [ "$gate" -eq 12 ]; then gate_default="WARN"; else gate_default="PASS"; fi
3198
+ gate_status="${!status_var:-$gate_default}"
3199
+ case "$gate_status" in
3200
+ PASS) PASSED_COUNT=$((PASSED_COUNT + 1)) ;;
3201
+ SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
3202
+ WARN) WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
3203
+ *) NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
3204
+ esac
2974
3205
  done
2975
3206
 
3207
+ local EFFECTIVE_TOTAL=$((TOTAL_GATES - SKIP_COUNT))
3208
+ local VERDICT="PASS"
3209
+ if [ "$EFFECTIVE_TOTAL" -eq 0 ]; then
3210
+ VERDICT="SKIP-ALL" # every gate skipped: never report a fake 10/10
3211
+ elif [ "$NONPASS_COUNT" -gt 0 ]; then
3212
+ if [ "$((NONPASS_COUNT - WARN_COUNT))" -gt 0 ]; then
3213
+ VERDICT="FAIL" # at least one FAIL/BLOCK among the effective gates
3214
+ else
3215
+ VERDICT="WARN" # all effective gates passed or warned; warns forbid PASS
3216
+ fi
3217
+ fi
3218
+
2976
3219
  local SCORE=0
2977
- if command -v bc >/dev/null 2>&1; then
2978
- SCORE=$(echo "scale=1; ($PASSED_COUNT / $TOTAL_GATES) * 10" | bc)
3220
+ if [ "$EFFECTIVE_TOTAL" -eq 0 ]; then
3221
+ SCORE="N/A"
2979
3222
  else
2980
- SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $TOTAL_GATES) * 10}")
2981
- fi
3223
+ # Single awk path (#507 Delphi round-1 B-MAJOR-2): bc's scale=1 truncates
3224
+ # the DIVISION before multiplying (8/9 -> 0.8, *10 -> 8.0) while awk
3225
+ # rounds the final result (8.9) -- the same commit scored differently on
3226
+ # machines with and without bc. awk(1) is POSIX-guaranteed: one code path,
3227
+ # one score.
3228
+ SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $EFFECTIVE_TOTAL) * 10}")
3229
+ fi
3230
+ local EFFECTIVE_PASS_RATE="N/A"
3231
+ if [ "$EFFECTIVE_TOTAL" -gt 0 ]; then
3232
+ EFFECTIVE_PASS_RATE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $EFFECTIVE_TOTAL) * 100}")
3233
+ fi
3234
+ # JSON bodies need a number or null -- a literal N/A breaks every consumer.
3235
+ local SCORE_JSON="$SCORE"
3236
+ [ "$SCORE_JSON" = "N/A" ] && SCORE_JSON="null"
3237
+ # Same for the pass rate: numeric like `score`, not a quoted string --
3238
+ # mixed types in one object broke consumers (#507 Delphi round-1 B-minor-6).
3239
+ local EFFECTIVE_PASS_RATE_JSON="$EFFECTIVE_PASS_RATE"
3240
+ [ "$EFFECTIVE_PASS_RATE_JSON" = "N/A" ] && EFFECTIVE_PASS_RATE_JSON="null"
2982
3241
 
2983
3242
  local BS_PASSED=0
2984
3243
  local BS_BLOCKED=0
@@ -3018,8 +3277,11 @@ generate_quality_report() {
3018
3277
  "overall": {
3019
3278
  "gatesPassed": $PASSED_COUNT,
3020
3279
  "gatesTotal": $TOTAL_GATES,
3021
- "score": $SCORE,
3022
- "verdict": "$([ "$PASSED_COUNT" -eq "$TOTAL_GATES" ] && echo "PASS" || echo "PARTIAL")"
3280
+ "gatesSkipped": $SKIP_COUNT,
3281
+ "gatesEffective": $EFFECTIVE_TOTAL,
3282
+ "effectivePassRate": $EFFECTIVE_PASS_RATE_JSON,
3283
+ "score": $SCORE_JSON,
3284
+ "verdict": "$VERDICT"
3023
3285
  },
3024
3286
  "gates": {
3025
3287
  "gate1_static_analysis": {
@@ -3114,7 +3376,7 @@ ENDJSON
3114
3376
  GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
3115
3377
  GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
3116
3378
 
3117
- echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
3379
+ echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE_JSON,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"effectiveTotal\":$EFFECTIVE_TOTAL,\"effectivePassRate\":$EFFECTIVE_PASS_RATE_JSON,\"skipCount\":$SKIP_COUNT,\"verdict\":\"$VERDICT\",\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
3118
3380
 
3119
3381
  # ── Console output ────────────────────────────────────────────────────────
3120
3382
  echo ""
@@ -3135,7 +3397,7 @@ ENDJSON
3135
3397
  printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
3136
3398
  printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
3137
3399
  echo ""
3138
- echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
3400
+ echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed (effective: $PASSED_COUNT/$EFFECTIVE_TOTAL, skipped: $SKIP_COUNT, verdict: $VERDICT)"
3139
3401
  echo " Branch status: $REPORT_FILE"
3140
3402
  echo " Run report: $PERSISTED_REPORT_FILE"
3141
3403
  echo " History saved: $HISTORY_FILE"