@boyingliu01/xp-gate 0.20.1 → 0.20.2

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 (81) hide show
  1. package/adapters/java.sh +373 -27
  2. package/gate-3.sh +51 -3
  3. package/gate-4.sh +32 -1
  4. package/hooks/gate-3.sh +51 -3
  5. package/hooks/gate-4.sh +32 -1
  6. package/hooks/pre-commit +275 -36
  7. package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
  8. package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
  9. package/lib/doctor-tui.js +21 -3
  10. package/lib/doctor.js +111 -8
  11. package/lib/gate-audit.ts +3 -0
  12. package/lib/mattpocock-license-verdict.json +1 -1
  13. package/mock-policy/AGENTS.md +2 -2
  14. package/mutation/AGENTS.md +2 -2
  15. package/package.json +1 -1
  16. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  17. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
  18. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
  19. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
  20. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  21. package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
  22. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
  23. package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
  24. package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
  25. package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
  26. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  27. package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
  28. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  29. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
  30. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
  31. package/plugins/dsh/package.json +1 -1
  32. package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
  33. package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
  34. package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
  35. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  36. package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
  37. package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
  38. package/plugins/dsh/skills/grilling/SKILL.md +1 -0
  39. package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
  40. package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
  41. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  42. package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
  43. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  44. package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
  45. package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
  46. package/plugins/opencode/package.json +1 -1
  47. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
  48. package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
  49. package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
  50. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  51. package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
  52. package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
  53. package/plugins/opencode/skills/grilling/SKILL.md +1 -0
  54. package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
  55. package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
  56. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  57. package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
  58. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  59. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
  60. package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
  61. package/plugins/qoder/plugin.json +1 -1
  62. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  63. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  64. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  65. package/principles/AGENTS.md +2 -2
  66. package/principles/adapters/__tests__/java.test.ts +317 -0
  67. package/principles/adapters/java.ts +325 -13
  68. package/skills/admin-template-guidelines/SKILL.md +1 -0
  69. package/skills/batch-grill-me/SKILL.md +1 -0
  70. package/skills/clipboard-vision/SKILL.md +1 -0
  71. package/skills/delphi-review/AGENTS.md +2 -2
  72. package/skills/domain-modeling/SKILL.md +1 -0
  73. package/skills/grill-with-docs/SKILL.md +1 -0
  74. package/skills/grilling/SKILL.md +1 -0
  75. package/skills/improve-codebase-architecture/SKILL.md +1 -0
  76. package/skills/ralph-loop/SKILL.md +1 -0
  77. package/skills/sprint-flow/AGENTS.md +2 -2
  78. package/skills/test-driven-development/SKILL.md +1 -0
  79. package/skills/test-specification-alignment/AGENTS.md +2 -2
  80. package/skills/test-specification-alignment/SKILL.md +1 -0
  81. package/skills/to-issues/SKILL.md +1 -0
package/hooks/gate-3.sh CHANGED
@@ -1,3 +1,4 @@
1
+ # shellcheck shell=bash
1
2
  # ============================================================================
2
3
  # GATE 3: Cyclomatic Complexity Check
3
4
  # Uses lizard - checks cyclomatic complexity of changed source files
@@ -17,7 +18,45 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
17
18
  GATE_3_STATUS="SKIP"
18
19
 
19
20
  else
20
- CCN_THRESHOLD=5
21
+ # S2 (#507): three-level CCN threshold override --
22
+ # .xp-gate/ccn-threshold (positive integer) > XP_GATE_CCN_THRESHOLD > 5.
23
+ # Invalid values WARN on stderr and fall back to the default; the resolved
24
+ # value reaches the gate-3 audit record via the --detail argument below.
25
+ # Implemented with bash builtins only (no tr/grep spawns): every process
26
+ # spawn is expensive on Windows under AV filter drivers.
27
+ resolve_ccn_threshold() {
28
+ local default=5 val
29
+ if [ -f ".xp-gate/ccn-threshold" ]; then
30
+ val=$(<".xp-gate/ccn-threshold")
31
+ val="${val//[[:space:]]/}"
32
+ case "$val" in
33
+ ''|*[!0-9]*|0)
34
+ echo "WARN - invalid .xp-gate/ccn-threshold value '${val}' (expected a positive integer), falling back to ${default}" >&2
35
+ ;;
36
+ *)
37
+ echo "$val"
38
+ return 0
39
+ ;;
40
+ esac
41
+ echo "$default"
42
+ return 0
43
+ fi
44
+ val=${XP_GATE_CCN_THRESHOLD:-}
45
+ if [ -n "$val" ]; then
46
+ case "$val" in
47
+ ''|*[!0-9]*|0)
48
+ echo "WARN - invalid XP_GATE_CCN_THRESHOLD value '${val}' (expected a positive integer), falling back to ${default}" >&2
49
+ ;;
50
+ *)
51
+ echo "$val"
52
+ return 0
53
+ ;;
54
+ esac
55
+ fi
56
+ echo "$default"
57
+ }
58
+
59
+ CCN_THRESHOLD=$(resolve_ccn_threshold)
21
60
 
22
61
  # Check lizard availability
23
62
  LIZARD_CMD=""
@@ -37,7 +76,16 @@ else
37
76
  echo "Checking complexity for source files..."
38
77
 
39
78
  # Run lizard with CCN threshold
40
- CC_OUTPUT=$($LIZARD_PATH -C $CCN_THRESHOLD $CC_FILES 2>&1 || true)
79
+ # One path per line, into an array. Unquoted $CC_FILES relied on word
80
+ # splitting, which also splits any path containing a space -- handing
81
+ # lizard two bogus paths and checking neither (#457 defect class, same
82
+ # fix as gate-4's PRINCIPLES_ARGS). Quoting the scalar would instead
83
+ # collapse the list into one argument. An array does both.
84
+ CC_FILES_ARGS=()
85
+ while IFS= read -r _cc_file; do
86
+ [ -n "$_cc_file" ] && CC_FILES_ARGS+=("$_cc_file")
87
+ done <<< "$CC_FILES"
88
+ CC_OUTPUT=$("$LIZARD_PATH" -C "$CCN_THRESHOLD" ${CC_FILES_ARGS[@]+"${CC_FILES_ARGS[@]}"} 2>&1 || true)
41
89
 
42
90
  # Parse warning count from the summary table: "Warning cnt 8"
43
91
  # Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
@@ -65,4 +113,4 @@ else
65
113
  GATE_3_STATUS="WARN"
66
114
  fi
67
115
  fi
68
- record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
116
+ record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START" "ccn_threshold=${CCN_THRESHOLD:-5}"
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
@@ -726,7 +741,7 @@ CHANGEDFILELIST
726
741
  # Only used when extension-based detection found nothing
727
742
  if [ -z "$PROJECT_LANGS" ]; then
728
743
  # 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)
744
+ 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
745
  if [ -n "$TSCONFIG_SUB" ]; then
731
746
  PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
732
747
  if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
@@ -738,7 +753,7 @@ CHANGEDFILELIST
738
753
 
739
754
  # Search for Python projects
740
755
  if [ -z "$PROJECT_LANGS" ]; then
741
- PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" 2>/dev/null | head -1)
756
+ PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
742
757
  if [ -n "$PYPROJECT_SUB" ]; then
743
758
  PROJECT_LANGS="python"
744
759
  PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
@@ -747,7 +762,7 @@ CHANGEDFILELIST
747
762
 
748
763
  # Search for Go projects
749
764
  if [ -z "$PROJECT_LANGS" ]; then
750
- GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" 2>/dev/null | head -1)
765
+ GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
751
766
  if [ -n "$GOMOD_SUB" ]; then
752
767
  PROJECT_LANGS="go"
753
768
  PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
@@ -756,7 +771,7 @@ CHANGEDFILELIST
756
771
 
757
772
  # Search for Flutter/Dart projects
758
773
  if [ -z "$PROJECT_LANGS" ]; then
759
- PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" 2>/dev/null | head -1)
774
+ PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
760
775
  if [ -n "$PUBSPEC_SUB" ]; then
761
776
  if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
762
777
  PROJECT_LANGS="flutter"
@@ -769,8 +784,8 @@ CHANGEDFILELIST
769
784
 
770
785
  # Search for Java/Kotlin projects
771
786
  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)
787
+ POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
788
+ GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
774
789
  if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
775
790
  MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
776
791
  PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
@@ -785,7 +800,7 @@ CHANGEDFILELIST
785
800
 
786
801
  # Search for C++ projects
787
802
  if [ -z "$PROJECT_LANGS" ]; then
788
- CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" 2>/dev/null | head -1)
803
+ CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
789
804
  if [ -n "$CMAKE_SUB" ]; then
790
805
  PROJECT_LANGS="cpp"
791
806
  PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
@@ -794,7 +809,7 @@ CHANGEDFILELIST
794
809
 
795
810
  # Search for React Native projects (ios/ + android/ directories)
796
811
  if [ -z "$PROJECT_LANGS" ]; then
797
- RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" 2>/dev/null | head -1)
812
+ RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
798
813
  if [ -n "$RN_SUB" ]; then
799
814
  PROJECT_SUBDIR=$(dirname "$RN_SUB")
800
815
  if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
@@ -805,7 +820,7 @@ CHANGEDFILELIST
805
820
 
806
821
  # Search for PowerShell projects
807
822
  if [ -z "$PROJECT_LANGS" ]; then
808
- PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)
823
+ PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" -not -path "*/node_modules/*" 2>/dev/null | head -1)
809
824
  if [ -n "$PS_SUB" ]; then
810
825
  PROJECT_LANGS="powershell"
811
826
  PROJECT_SUBDIR=$(dirname "$PS_SUB")
@@ -830,7 +845,7 @@ CHANGEDFILELIST
830
845
 
831
846
  # ---- Documentation-only fallback ----
832
847
  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)
848
+ 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
849
 
835
850
  if [ "$CODE_FILES" -eq 0 ]; then
836
851
  PROJECT_LANGS="documentation-only"
@@ -1332,7 +1347,66 @@ else
1332
1347
  echo "✅ PASSED - Basic shell script syntax check."
1333
1348
  fi
1334
1349
  ;;
1335
-
1350
+
1351
+ java)
1352
+ # GATE 1 Java: wc-java-lint with fail-closed verdicts (#507 S1, DR-001).
1353
+ # java.sh run_static_analysis verdict codes:
1354
+ # 0=PASS 1=FAIL(violations) 2=BLOCK(execution error) 3=WARN(non-blocking).
1355
+ # Every non-0/1/2 outcome is scored as WARN -- IN the denominator, so it
1356
+ # forbids an overall PASS. Delphi round-2 A-MAJOR-1: these paths used to
1357
+ # map to SKIP, which sits OUT of the denominator, letting a repo with no
1358
+ # Java engine (or findings suppressed in report mode) report PASS/10.0
1359
+ # while its Java quality was never verified.
1360
+ # The adapter applies the XP_GATE_WC_JAVA_LINT=soft escape valve itself, so
1361
+ # a 2 reaching this branch is hard: BLOCK the commit. Captured to a var
1362
+ # (not piped) so the verdict code survives regardless of output length --
1363
+ # `cmd | head` makes a long report die with SIGPIPE, which would turn a
1364
+ # real verdict into an accident of line count.
1365
+ ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
1366
+ if [ -z "$ADAPTER_FILE" ] || ! source "$ADAPTER_FILE" 2>/dev/null; then
1367
+ echo "⚠️ WARN - Java adapter not found (code quality NOT verified)"
1368
+ GATE_1_STATUS="WARN"
1369
+ GATE_1_DETAIL="java: adapter missing"
1370
+ else
1371
+ JAVA_G1_EXIT=0
1372
+ # `|| JAVA_G1_EXIT=$?` (not a bare assignment): under a set -e consumer a
1373
+ # non-zero verdict would abort the host hook before the mapping below.
1374
+ JAVA_G1_OUTPUT=$(run_static_analysis 2>&1) || JAVA_G1_EXIT=$?
1375
+ # SKIP/WARN reasons cross the command-substitution subshell as an
1376
+ # `XP_G1_REASON: ...` stdout line -- an `export` inside the adapter can
1377
+ # NEVER reach this process (#507 Delphi round-1 B-MAJOR-1: verified the
1378
+ # subshell discards the child env). Parse, then strip from display.
1379
+ JAVA_G1_REASON=$(printf '%s\n' "$JAVA_G1_OUTPUT" | sed -n 's/^XP_G1_REASON: //p' | head -1)
1380
+ if [ -n "$JAVA_G1_REASON" ]; then
1381
+ JAVA_G1_OUTPUT=$(printf '%s\n' "$JAVA_G1_OUTPUT" | grep -v '^XP_G1_REASON: ')
1382
+ fi
1383
+ [ -n "$JAVA_G1_OUTPUT" ] && echo "$JAVA_G1_OUTPUT" | head -40
1384
+ case "$JAVA_G1_EXIT" in
1385
+ 0)
1386
+ echo "✅ PASSED - Java code quality (wc-java-lint)."
1387
+ ;;
1388
+ 1)
1389
+ echo "❌ BLOCKED - Java static analysis violations (fix or add suppressions)"
1390
+ exit 1
1391
+ ;;
1392
+ 2)
1393
+ echo "❌ BLOCKED - wc-java-lint unusable (fail-closed; unset XP_GATE_WC_JAVA_LINT=soft only if you accept the risk)"
1394
+ exit 1
1395
+ ;;
1396
+ 3)
1397
+ echo "⚠️ WARN - Java code quality NOT verified (engine unavailable or downgraded)"
1398
+ GATE_1_STATUS="WARN"
1399
+ GATE_1_DETAIL="java: ${JAVA_G1_REASON:-unavailable (warn)}"
1400
+ ;;
1401
+ *)
1402
+ echo "⚠️ WARN - Java code quality NOT verified (unexpected verdict $JAVA_G1_EXIT)"
1403
+ GATE_1_STATUS="WARN"
1404
+ GATE_1_DETAIL="java: unexpected verdict $JAVA_G1_EXIT"
1405
+ ;;
1406
+ esac
1407
+ fi
1408
+ ;;
1409
+
1336
1410
  *)
1337
1411
  # Route to appropriate adapter for other languages
1338
1412
  ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
@@ -1367,9 +1441,13 @@ else
1367
1441
  done
1368
1442
  fi
1369
1443
  fi # End: if ! any_changed_files_match (Issue #312 skip)
1370
- GATE_1_STATUS="PASS"
1444
+ GATE_1_STATUS="${GATE_1_STATUS:-PASS}"
1371
1445
  GATE_1_TOOL="${PROJECT_LANGS}"
1372
- record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
1446
+ # The java branch may set GATE_1_STATUS=WARN + a GATE_1_DETAIL warn reason
1447
+ # (engine unavailable / soft or report downgrade); route it into the audit
1448
+ # trail via the S2 detail plumbing so a WARN verdict is diagnosable, not
1449
+ # silent. WARN stays IN the scoring denominator (Delphi round-2 A-MAJOR-1).
1450
+ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START" "${GATE_1_DETAIL:-}"
1373
1451
 
1374
1452
  # ============================================================================
1375
1453
  # GATE 2: Duplicate Code Detection (NEW gate)
@@ -2701,9 +2779,9 @@ else
2701
2779
  # real source path stays unbudgeted. Exclude it and let the mirroring be
2702
2780
  # governed by Mirror Parity in CI, which compares content rather than
2703
2781
  # 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' ' ')
2782
+ MIRROR_EXCLUDE="$MIRROR_PATH_FILTER"
2783
+ NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
2784
+ MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | grep -v "$MIRROR_EXCLUDE" | grep -v "$DASHBOARD_PATH_FILTER" | tr '\n' ' ')
2707
2785
 
2708
2786
  if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
2709
2787
  echo " ✅ No new or modified files to check."
@@ -2794,9 +2872,73 @@ fi
2794
2872
  # ============================================================================
2795
2873
  # GATE 9: Build Integrity Check (TypeScript compilation + package + imports)
2796
2874
  # ============================================================================
2875
+
2876
+ # Java build-integrity dispatch (#507 S5, DR-003). The old gate only knew
2877
+ # TypeScript: a Maven project got a permanent SKIP -- "not a TypeScript
2878
+ # project" -- which is exactly the silent-no-coverage path this sprint
2879
+ # removes. Verdict codes (mirror of the java.sh G1 contract):
2880
+ # 0=PASS 2=BLOCK(compile failed) 3=SKIP+WARN(reason printed)
2881
+ resolve_build_integrity_timeout() {
2882
+ local file=".xp-gate/build-integrity-timeout" val=""
2883
+ if [ -f "$file" ]; then
2884
+ val=$(tr -d '[:space:]' < "$file")
2885
+ fi
2886
+ if [ -z "$val" ]; then
2887
+ echo "180"
2888
+ return 0
2889
+ fi
2890
+ case "$val" in
2891
+ ''|*[!0-9]*|0)
2892
+ echo "WARN - invalid $file value '${val}' (expected a positive integer), falling back to 180s" >&2
2893
+ echo "180"
2894
+ ;;
2895
+ *) echo "$val" ;;
2896
+ esac
2897
+ }
2898
+
2899
+ run_java_build_integrity() {
2900
+ local timeout_s build_bin
2901
+ # Wrapper first: mvnw pins the project's own Maven version.
2902
+ if [ -x "./mvnw" ]; then
2903
+ build_bin="./mvnw"
2904
+ elif command -v mvn >/dev/null 2>&1; then
2905
+ build_bin="mvn"
2906
+ else
2907
+ echo " ⚠️ Java changes staged but no Maven build tool found (mvnw/mvn)"
2908
+ echo " ⏭️ SKIPPED - Build integrity Java (no build tool)"
2909
+ return 3
2910
+ fi
2911
+
2912
+ timeout_s=$(resolve_build_integrity_timeout)
2913
+ if command -v timeout >/dev/null 2>&1; then
2914
+ timeout "$timeout_s" "$build_bin" -q -DskipTests compile 2>&1 | tail -20
2915
+ else
2916
+ "$build_bin" -q -DskipTests compile 2>&1 | tail -20
2917
+ fi
2918
+ local rc="${PIPESTATUS[0]}"
2919
+ if [ "$rc" -eq 0 ]; then
2920
+ echo " ✅ Java build integrity verified (mvn -q -DskipTests compile)."
2921
+ return 0
2922
+ fi
2923
+ if [ "$rc" -eq 124 ]; then
2924
+ echo " ⏱️ Java build timed out after ${timeout_s}s."
2925
+ echo " ⏭️ SKIPPED - Build integrity Java (timeout)"
2926
+ return 3
2927
+ fi
2928
+ echo " ❌ Java build failed (exit $rc)"
2929
+ return 2
2930
+ }
2931
+
2797
2932
  2>&1 echo ""
2798
2933
  2>&1 echo "→ Gate 9: Build integrity... "
2799
2934
  GATE_9_START=$(gate_start_ms)
2935
+ # Sub-status aggregation (#507 Delphi round-1 B-MAJOR-4): each half records
2936
+ # its own outcome; GATE_9_STATUS is aggregated after both. The previous
2937
+ # single-variable design made a Maven-only repo (TS half never runs, Java
2938
+ # half passes) record an ETERNAL SKIP -- a real compile verification that
2939
+ # never appeared as PASS, contradicting S5's own motivation.
2940
+ GATE_9_TS_STATUS="SKIP"
2941
+ GATE_9_JAVA_STATUS="SKIP"
2800
2942
  GATE_9_STATUS="SKIP"
2801
2943
 
2802
2944
  if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
@@ -2813,15 +2955,15 @@ if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
2813
2955
  CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
2814
2956
  GATE_9_OUTPUT=$(mktemp)
2815
2957
  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"
2958
+ GATE_9_TS_STATUS="PASS"
2817
2959
  echo " ✅ Build integrity check passed."
2818
2960
  else
2819
2961
  GATE_9_EXIT=$?
2820
2962
  if [ $GATE_9_EXIT -eq 124 ]; then
2821
- GATE_9_STATUS="SKIP"
2963
+ GATE_9_TS_STATUS="SKIP"
2822
2964
  echo " ⏱️ Build integrity check timed out (120s). SKIPPED."
2823
2965
  else
2824
- GATE_9_STATUS="BLOCK"
2966
+ GATE_9_TS_STATUS="BLOCK"
2825
2967
  cat "$GATE_9_OUTPUT"
2826
2968
  echo ""
2827
2969
  echo "❌ BLOCKED - Gate 9: Build integrity check failed"
@@ -2840,6 +2982,52 @@ else
2840
2982
  echo " ⏭️ SKIPPED - Build integrity (not a TypeScript project)"
2841
2983
  fi
2842
2984
 
2985
+ # Java dispatch (#507 S5): runs when .java files are staged, regardless of
2986
+ # whether the TypeScript half ran. Gradle-only repos SKIP+WARN (gradle
2987
+ # support is backlog; DR-003).
2988
+ if echo "$CHANGED_FILES" | grep -q '\.java$'; then
2989
+ if [ -f "pom.xml" ]; then
2990
+ JAVA_BI_EXIT=0
2991
+ run_java_build_integrity || JAVA_BI_EXIT=$?
2992
+ case "$JAVA_BI_EXIT" in
2993
+ 0)
2994
+ GATE_9_JAVA_STATUS="PASS"
2995
+ ;;
2996
+ 2)
2997
+ GATE_9_JAVA_STATUS="BLOCK"
2998
+ echo ""
2999
+ echo "❌ BLOCKED - Gate 9: Java build integrity check failed"
3000
+ echo "Fix the compile errors before committing."
3001
+ exit 1
3002
+ ;;
3003
+ *)
3004
+ GATE_9_JAVA_STATUS="SKIP"
3005
+ echo " ⏭️ SKIPPED - Build integrity Java (see reason above)"
3006
+ ;;
3007
+ esac
3008
+ elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
3009
+ echo " ⚠️ Java changes staged but this is a Gradle project - build integrity for Gradle is not implemented yet"
3010
+ echo " ⏭️ SKIPPED - Build integrity Java (Gradle backlog, #507 DR-003)"
3011
+ else
3012
+ echo " ℹ️ Java changes staged but no Maven/Gradle build file - skipping Java build integrity"
3013
+ fi
3014
+ fi
3015
+
3016
+ # Aggregate the two halves (#507 Delphi round-1 B-MAJOR-4): PASS when every
3017
+ # half that RAN passed (TS PASS + Java SKIP, or vice versa, both count),
3018
+ # SKIP only when NOTHING ran, BLOCK on any failure (normally unreachable --
3019
+ # failing halves exit 1 inline). This replaces the blind-review MINOR-1
3020
+ # single-variable guard: with explicit sub-statuses a Java PASS no longer
3021
+ # "resurrects" a TS SKIP -- each half keeps its own record and the gate
3022
+ # reflects the union of what was actually verified.
3023
+ GATE_9_STATUS="SKIP"
3024
+ if [ "$GATE_9_TS_STATUS" = "PASS" ] || [ "$GATE_9_JAVA_STATUS" = "PASS" ]; then
3025
+ GATE_9_STATUS="PASS"
3026
+ fi
3027
+ if [ "$GATE_9_TS_STATUS" = "BLOCK" ] || [ "$GATE_9_JAVA_STATUS" = "BLOCK" ]; then
3028
+ GATE_9_STATUS="BLOCK"
3029
+ fi
3030
+
2843
3031
  GATE_9_END=$(gate_start_ms)
2844
3032
  GATE_9_DURATION_MS=$((GATE_9_END - GATE_9_START))
2845
3033
  GATE_9_DURATION=$((GATE_9_DURATION_MS / 1000))
@@ -2872,7 +3060,7 @@ fi
2872
3060
  # Validates sprint state consistency (delphi-review APPROVED before BUILD)
2873
3061
  # Uses sprint-gate.sh for standalone validation logic
2874
3062
  # ============================================================================
2875
- GATE_11_START=$(date +%s)
3063
+ GATE_11_START=$(gate_start_ms)
2876
3064
  GATE_11_STATUS="PASS"
2877
3065
 
2878
3066
  # BEGIN SPRINT_GATE_SCRIPT RESOLUTION
@@ -2961,24 +3149,72 @@ generate_quality_report() {
2961
3149
  local BRANCH
2962
3150
  local PASSED_COUNT=0
2963
3151
  local TOTAL_GATES=12
3152
+ local SKIP_COUNT=0
3153
+ local WARN_COUNT=0
3154
+ local NONPASS_COUNT=0
2964
3155
 
2965
3156
  COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
2966
3157
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
2967
3158
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
2968
3159
 
3160
+ # Scoring per #507 S3: SKIP removes a gate from the denominator (a missing
3161
+ # tool must not drag the score down -- that was the "honest but punishing"
3162
+ # old behavior), WARN stays in the denominator but never counts as passed,
3163
+ # and FAIL/BLOCK count as non-passing items like before. NONPASS tracks
3164
+ # non-passing gates INSIDE the effective denominator only -- a skipped gate
3165
+ # is out of scope, not a failure.
3166
+ local gate_status gate_default
2969
3167
  for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
2970
3168
  local status_var="GATE_${gate}_STATUS"
2971
- if [ "${!status_var}" = "PASS" ]; then
2972
- PASSED_COUNT=$((PASSED_COUNT + 1))
2973
- fi
3169
+ # Default mirrors the JSON report rendering below EXACTLY (gates 1-11
3170
+ # PASS, gate 12 WARN): a gate that never assigned a status is "not
3171
+ # applicable" and must render and score identically. A previous scoring
3172
+ # default of FAIL produced self-contradictory reports -- the gate showed
3173
+ # PASS in JSON but scored as FAIL (#507 Delphi round-1 A-MAJOR-2).
3174
+ if [ "$gate" -eq 12 ]; then gate_default="WARN"; else gate_default="PASS"; fi
3175
+ gate_status="${!status_var:-$gate_default}"
3176
+ case "$gate_status" in
3177
+ PASS) PASSED_COUNT=$((PASSED_COUNT + 1)) ;;
3178
+ SKIP) SKIP_COUNT=$((SKIP_COUNT + 1)) ;;
3179
+ WARN) WARN_COUNT=$((WARN_COUNT + 1)); NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
3180
+ *) NONPASS_COUNT=$((NONPASS_COUNT + 1)) ;;
3181
+ esac
2974
3182
  done
2975
3183
 
3184
+ local EFFECTIVE_TOTAL=$((TOTAL_GATES - SKIP_COUNT))
3185
+ local VERDICT="PASS"
3186
+ if [ "$EFFECTIVE_TOTAL" -eq 0 ]; then
3187
+ VERDICT="SKIP-ALL" # every gate skipped: never report a fake 10/10
3188
+ elif [ "$NONPASS_COUNT" -gt 0 ]; then
3189
+ if [ "$((NONPASS_COUNT - WARN_COUNT))" -gt 0 ]; then
3190
+ VERDICT="FAIL" # at least one FAIL/BLOCK among the effective gates
3191
+ else
3192
+ VERDICT="WARN" # all effective gates passed or warned; warns forbid PASS
3193
+ fi
3194
+ fi
3195
+
2976
3196
  local SCORE=0
2977
- if command -v bc >/dev/null 2>&1; then
2978
- SCORE=$(echo "scale=1; ($PASSED_COUNT / $TOTAL_GATES) * 10" | bc)
3197
+ if [ "$EFFECTIVE_TOTAL" -eq 0 ]; then
3198
+ SCORE="N/A"
2979
3199
  else
2980
- SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $TOTAL_GATES) * 10}")
2981
- fi
3200
+ # Single awk path (#507 Delphi round-1 B-MAJOR-2): bc's scale=1 truncates
3201
+ # the DIVISION before multiplying (8/9 -> 0.8, *10 -> 8.0) while awk
3202
+ # rounds the final result (8.9) -- the same commit scored differently on
3203
+ # machines with and without bc. awk(1) is POSIX-guaranteed: one code path,
3204
+ # one score.
3205
+ SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $EFFECTIVE_TOTAL) * 10}")
3206
+ fi
3207
+ local EFFECTIVE_PASS_RATE="N/A"
3208
+ if [ "$EFFECTIVE_TOTAL" -gt 0 ]; then
3209
+ EFFECTIVE_PASS_RATE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $EFFECTIVE_TOTAL) * 100}")
3210
+ fi
3211
+ # JSON bodies need a number or null -- a literal N/A breaks every consumer.
3212
+ local SCORE_JSON="$SCORE"
3213
+ [ "$SCORE_JSON" = "N/A" ] && SCORE_JSON="null"
3214
+ # Same for the pass rate: numeric like `score`, not a quoted string --
3215
+ # mixed types in one object broke consumers (#507 Delphi round-1 B-minor-6).
3216
+ local EFFECTIVE_PASS_RATE_JSON="$EFFECTIVE_PASS_RATE"
3217
+ [ "$EFFECTIVE_PASS_RATE_JSON" = "N/A" ] && EFFECTIVE_PASS_RATE_JSON="null"
2982
3218
 
2983
3219
  local BS_PASSED=0
2984
3220
  local BS_BLOCKED=0
@@ -3018,8 +3254,11 @@ generate_quality_report() {
3018
3254
  "overall": {
3019
3255
  "gatesPassed": $PASSED_COUNT,
3020
3256
  "gatesTotal": $TOTAL_GATES,
3021
- "score": $SCORE,
3022
- "verdict": "$([ "$PASSED_COUNT" -eq "$TOTAL_GATES" ] && echo "PASS" || echo "PARTIAL")"
3257
+ "gatesSkipped": $SKIP_COUNT,
3258
+ "gatesEffective": $EFFECTIVE_TOTAL,
3259
+ "effectivePassRate": $EFFECTIVE_PASS_RATE_JSON,
3260
+ "score": $SCORE_JSON,
3261
+ "verdict": "$VERDICT"
3023
3262
  },
3024
3263
  "gates": {
3025
3264
  "gate1_static_analysis": {
@@ -3114,7 +3353,7 @@ ENDJSON
3114
3353
  GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
3115
3354
  GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
3116
3355
 
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"
3356
+ 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
3357
 
3119
3358
  # ── Console output ────────────────────────────────────────────────────────
3120
3359
  echo ""
@@ -3135,7 +3374,7 @@ ENDJSON
3135
3374
  printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
3136
3375
  printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
3137
3376
  echo ""
3138
- echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
3377
+ echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed (effective: $PASSED_COUNT/$EFFECTIVE_TOTAL, skipped: $SKIP_COUNT, verdict: $VERDICT)"
3139
3378
  echo " Branch status: $REPORT_FILE"
3140
3379
  echo " Run report: $PERSISTED_REPORT_FILE"
3141
3380
  echo " History saved: $HISTORY_FILE"