@boyingliu01/xp-gate 0.20.0 → 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.
- package/adapters/gate-8.sh +115 -73
- package/adapters/java.sh +373 -27
- package/adapters/powershell.sh +60 -5
- package/adapters/python.sh +38 -4
- package/build-integrity/__tests__/import-resolver.test.ts +59 -0
- package/build-integrity/gate-10.ts +17 -2
- package/gate-3.sh +51 -3
- package/gate-4.sh +32 -1
- package/gate-8.sh +115 -73
- package/hooks/gate-3.sh +51 -3
- package/hooks/gate-4.sh +32 -1
- package/hooks/gate-8.sh +115 -73
- package/hooks/pre-commit +404 -62
- package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
- package/lib/doctor-tui.js +21 -3
- package/lib/doctor.js +111 -8
- package/lib/gate-audit.ts +3 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/__tests__/java.test.ts +317 -0
- package/principles/adapters/java.ts +325 -13
- package/principles/boy-scout.ts +36 -6
- package/scripts/sync-package-content.js +13 -6
- package/skills/admin-template-guidelines/SKILL.md +1 -0
- package/skills/batch-grill-me/SKILL.md +1 -0
- package/skills/clipboard-vision/SKILL.md +1 -0
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/skills/ralph-loop/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-driven-development/SKILL.md +1 -0
- package/skills/test-specification-alignment/AGENTS.md +4 -4
- package/skills/test-specification-alignment/SKILL.md +1 -0
- package/skills/to-issues/SKILL.md +1 -0
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
|
|
@@ -459,10 +463,36 @@ record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "0" "$GATE_0_S
|
|
|
459
463
|
|
|
460
464
|
echo ""
|
|
461
465
|
|
|
462
|
-
# Get list of changed files
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
+
# Get list of changed files.
|
|
467
|
+
#
|
|
468
|
+
# Byte-identical mirrors (src/npm-package/**) are governed by CI Mirror Parity,
|
|
469
|
+
# not by re-linting. Leaving them in this scan set double-counted every
|
|
470
|
+
# lint/complexity signal and made jscpd report the source-vs-mirror pair as a
|
|
471
|
+
# 100% whole-file clone, which then tripped Gate 6's "<=5 warnings must clear
|
|
472
|
+
# to zero" rule on ANY edit to a mirrored file -- a warning that is
|
|
473
|
+
# structurally impossible to clear. (Gate 6's MIRROR_EXCLUDE and #494's test
|
|
474
|
+
# selection already exclude mirrors for the same reason.)
|
|
475
|
+
#
|
|
476
|
+
# IMPORTANT: the exclusion applies to the LINT SCAN CORPUS only. The early
|
|
477
|
+
# exit below must key on the RAW staged list -- Gate 8 (secret scanning) is
|
|
478
|
+
# unconditional and fail-closed (#499), and gating it behind a path-filtered
|
|
479
|
+
# list would let a mirror-only commit land with zero secret scanning and no
|
|
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/'
|
|
492
|
+
RAW_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
493
|
+
CHANGED_FILES=$(printf '%s\n' "$RAW_CHANGED_FILES" | grep -v "$MIRROR_PATH_FILTER" | grep -v "$DASHBOARD_PATH_FILTER" || true)
|
|
494
|
+
|
|
495
|
+
if [ -z "$RAW_CHANGED_FILES" ]; then
|
|
466
496
|
echo "No files changed. Skipping gates."
|
|
467
497
|
exit 0
|
|
468
498
|
fi
|
|
@@ -629,21 +659,21 @@ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
|
|
|
629
659
|
PROJECT_LANGS="dart"
|
|
630
660
|
fi
|
|
631
661
|
elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
|
|
632
|
-
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)
|
|
633
663
|
if [ "$KOTLIN_FILES" -gt 0 ]; then
|
|
634
664
|
PROJECT_LANGS="kotlin"
|
|
635
665
|
else
|
|
636
666
|
PROJECT_LANGS="java"
|
|
637
667
|
fi
|
|
638
668
|
elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
|
|
639
|
-
CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
640
|
-
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)
|
|
641
671
|
if [ -n "$CPP_FILES" ]; then
|
|
642
672
|
PROJECT_LANGS="cpp"
|
|
643
673
|
elif [ -n "$OBJC_FILES" ]; then
|
|
644
674
|
PROJECT_LANGS="objectivec"
|
|
645
675
|
fi
|
|
646
|
-
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
|
|
647
677
|
PROJECT_LANGS="powershell"
|
|
648
678
|
fi
|
|
649
679
|
fi
|
|
@@ -711,7 +741,7 @@ CHANGEDFILELIST
|
|
|
711
741
|
# Only used when extension-based detection found nothing
|
|
712
742
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
713
743
|
# Search for TypeScript/JavaScript projects
|
|
714
|
-
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)
|
|
715
745
|
if [ -n "$TSCONFIG_SUB" ]; then
|
|
716
746
|
PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
|
|
717
747
|
if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
|
|
@@ -723,7 +753,7 @@ CHANGEDFILELIST
|
|
|
723
753
|
|
|
724
754
|
# Search for Python projects
|
|
725
755
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
726
|
-
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)
|
|
727
757
|
if [ -n "$PYPROJECT_SUB" ]; then
|
|
728
758
|
PROJECT_LANGS="python"
|
|
729
759
|
PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
|
|
@@ -732,7 +762,7 @@ CHANGEDFILELIST
|
|
|
732
762
|
|
|
733
763
|
# Search for Go projects
|
|
734
764
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
735
|
-
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)
|
|
736
766
|
if [ -n "$GOMOD_SUB" ]; then
|
|
737
767
|
PROJECT_LANGS="go"
|
|
738
768
|
PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
|
|
@@ -741,7 +771,7 @@ CHANGEDFILELIST
|
|
|
741
771
|
|
|
742
772
|
# Search for Flutter/Dart projects
|
|
743
773
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
744
|
-
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)
|
|
745
775
|
if [ -n "$PUBSPEC_SUB" ]; then
|
|
746
776
|
if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
|
|
747
777
|
PROJECT_LANGS="flutter"
|
|
@@ -754,8 +784,8 @@ CHANGEDFILELIST
|
|
|
754
784
|
|
|
755
785
|
# Search for Java/Kotlin projects
|
|
756
786
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
757
|
-
POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
758
|
-
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)
|
|
759
789
|
if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
|
|
760
790
|
MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
|
|
761
791
|
PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
|
|
@@ -770,7 +800,7 @@ CHANGEDFILELIST
|
|
|
770
800
|
|
|
771
801
|
# Search for C++ projects
|
|
772
802
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
773
|
-
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)
|
|
774
804
|
if [ -n "$CMAKE_SUB" ]; then
|
|
775
805
|
PROJECT_LANGS="cpp"
|
|
776
806
|
PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
|
|
@@ -779,7 +809,7 @@ CHANGEDFILELIST
|
|
|
779
809
|
|
|
780
810
|
# Search for React Native projects (ios/ + android/ directories)
|
|
781
811
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
782
|
-
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)
|
|
783
813
|
if [ -n "$RN_SUB" ]; then
|
|
784
814
|
PROJECT_SUBDIR=$(dirname "$RN_SUB")
|
|
785
815
|
if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
|
|
@@ -790,7 +820,7 @@ CHANGEDFILELIST
|
|
|
790
820
|
|
|
791
821
|
# Search for PowerShell projects
|
|
792
822
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
793
|
-
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)
|
|
794
824
|
if [ -n "$PS_SUB" ]; then
|
|
795
825
|
PROJECT_LANGS="powershell"
|
|
796
826
|
PROJECT_SUBDIR=$(dirname "$PS_SUB")
|
|
@@ -815,7 +845,7 @@ CHANGEDFILELIST
|
|
|
815
845
|
|
|
816
846
|
# ---- Documentation-only fallback ----
|
|
817
847
|
if [ -z "$PROJECT_LANGS" ]; then
|
|
818
|
-
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)
|
|
819
849
|
|
|
820
850
|
if [ "$CODE_FILES" -eq 0 ]; then
|
|
821
851
|
PROJECT_LANGS="documentation-only"
|
|
@@ -857,11 +887,21 @@ has_project_lang() {
|
|
|
857
887
|
}
|
|
858
888
|
# Switch to subdirectory if detected (for single-language subdirectory projects).
|
|
859
889
|
# Multi-language projects stay at root — each adapter handles its own path.
|
|
890
|
+
#
|
|
891
|
+
# IN_PROJECT_SUBDIR records whether this cd actually happened. Git reports
|
|
892
|
+
# staged paths relative to the repo root; after cd "$PROJECT_SUBDIR" the [ -f ]
|
|
893
|
+
# probes below need subdirectory-relative paths (hence the PROJECT_SUBDIR
|
|
894
|
+
# prefix strips in Gates 5a/5b/5c), but when the hook stays at the root
|
|
895
|
+
# (multi-language projects) stripping the prefix makes every path-based check
|
|
896
|
+
# resolve against the wrong directory — false BLOCKs in Gate 5a, silent no-ops
|
|
897
|
+
# in 5b/5c. Only strip when we really are inside the subdirectory.
|
|
860
898
|
ORIGINAL_DIR=""
|
|
899
|
+
IN_PROJECT_SUBDIR=0
|
|
861
900
|
_lang_count=$(echo "$PROJECT_LANGS" | wc -w)
|
|
862
901
|
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ "$PROJECT_LANG" != "documentation-only" ] && [ "$_lang_count" -le 1 ]; then
|
|
863
902
|
ORIGINAL_DIR=$(pwd)
|
|
864
903
|
cd "$PROJECT_SUBDIR"
|
|
904
|
+
IN_PROJECT_SUBDIR=1
|
|
865
905
|
echo "Working in: $PROJECT_SUBDIR/"
|
|
866
906
|
echo ""
|
|
867
907
|
fi
|
|
@@ -891,20 +931,67 @@ else
|
|
|
891
931
|
# in githooks/lib/typecheck.sh so a project's own checker (vue-tsc, svelte-check,
|
|
892
932
|
# a `typecheck` script) is used instead of a hardcoded tsc that cannot resolve
|
|
893
933
|
# single-file components (#436).
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
934
|
+
#
|
|
935
|
+
# Resolve the TS project directory first (#470). The probes below used to run
|
|
936
|
+
# against the repo root only; in multi-language monorepos (e.g. web/ +
|
|
937
|
+
# backend/, where the hook stays at the root — see the cd block above)
|
|
938
|
+
# tsconfig.json / biome.json live in the language subdirectory, so the whole
|
|
939
|
+
# TS half of Gate 1 silently SKIPped ("tsconfig.json not found") while a
|
|
940
|
+
# project config existed. Order: repo root first (unchanged behaviour for
|
|
941
|
+
# plain projects), then the detected language subdirectory, then a shallow
|
|
942
|
+
# search.
|
|
943
|
+
TS_PROJECT_DIR=""
|
|
944
|
+
for _ts_dir in "." "${PROJECT_SUBDIR:-}"; do
|
|
945
|
+
[ -z "$_ts_dir" ] && continue
|
|
946
|
+
if [ -f "$_ts_dir/tsconfig.json" ] || [ -f "$_ts_dir/biome.json" ] || [ -f "$_ts_dir/biome.jsonc" ] \
|
|
947
|
+
|| [ -f "$_ts_dir/eslint.config.js" ] || [ -f "$_ts_dir/.eslintrc.json" ] \
|
|
948
|
+
|| [ -f "$_ts_dir/.eslintrc.js" ] || [ -f "$_ts_dir/.eslintrc.cjs" ]; then
|
|
949
|
+
TS_PROJECT_DIR="$_ts_dir"
|
|
950
|
+
break
|
|
951
|
+
fi
|
|
952
|
+
done
|
|
953
|
+
if [ -z "$TS_PROJECT_DIR" ]; then
|
|
954
|
+
_TSCONFIG_CAND=$(find . -maxdepth 2 \( -name tsconfig.json -o -name biome.json -o -name biome.jsonc -o -name eslint.config.js -o -name .eslintrc.json -o -name .eslintrc.js -o -name .eslintrc.cjs \) -not -path './.git/*' -not -path './node_modules/*' 2>/dev/null | head -1)
|
|
955
|
+
if [ -n "$_TSCONFIG_CAND" ]; then
|
|
956
|
+
TS_PROJECT_DIR=$(dirname "$_TSCONFIG_CAND")
|
|
957
|
+
fi
|
|
958
|
+
fi
|
|
959
|
+
[ -z "$TS_PROJECT_DIR" ] && TS_PROJECT_DIR="."
|
|
960
|
+
if [ "$TS_PROJECT_DIR" != "." ]; then
|
|
961
|
+
echo " TypeScript project directory: $TS_PROJECT_DIR/"
|
|
962
|
+
# typecheck.sh is cwd-relative: run it from the TS project subdirectory in a
|
|
963
|
+
# subshell so the language loop's next iteration still starts at the root.
|
|
964
|
+
# Exit contract: 0 clean, 1 findings (block), 2 checker unavailable (skip).
|
|
965
|
+
# A cd/source infrastructure failure must NOT read as "type errors": map it
|
|
966
|
+
# to 2 (skip) so 1 stays reserved for real findings (#508 walkthrough).
|
|
967
|
+
( cd "$TS_PROJECT_DIR" 2>/dev/null || exit 2 \
|
|
968
|
+
; [ -r "${AUDIT_SCRIPT_DIR}/lib/typecheck.sh" ] || exit 2 \
|
|
969
|
+
; source "${AUDIT_SCRIPT_DIR}/lib/typecheck.sh" && run_typescript_typecheck )
|
|
970
|
+
TS_CHECK_EXIT=$?
|
|
971
|
+
else
|
|
972
|
+
source "${AUDIT_SCRIPT_DIR}/lib/typecheck.sh"
|
|
973
|
+
run_typescript_typecheck
|
|
974
|
+
TS_CHECK_EXIT=$?
|
|
975
|
+
fi
|
|
897
976
|
if [ "$TS_CHECK_EXIT" -eq 1 ]; then
|
|
898
977
|
exit 1
|
|
899
978
|
fi
|
|
900
979
|
|
|
901
|
-
# Run ESLint linting if config exists
|
|
902
|
-
if [ -f "
|
|
980
|
+
# Run ESLint linting if config exists (root or TS project subdirectory)
|
|
981
|
+
if [ -f "$TS_PROJECT_DIR/.eslintrc.json" ] || [ -f "$TS_PROJECT_DIR/.eslintrc.js" ] || [ -f "$TS_PROJECT_DIR/.eslintrc.cjs" ] || [ -f "$TS_PROJECT_DIR/eslint.config.js" ]; then
|
|
903
982
|
echo "Running ESLint linting..."
|
|
904
983
|
# Filter to only TS/JS files - ESLint config ignores non-matching files
|
|
905
984
|
ESLINT_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
|
|
906
985
|
if [ -n "$ESLINT_FILES" ]; then
|
|
907
|
-
|
|
986
|
+
# File paths from git are repo-root relative, so they are correct from
|
|
987
|
+
# the root. Legacy .eslintrc.* configs are discovered by walking up from
|
|
988
|
+
# each linted file (found in the subdirectory automatically); flat
|
|
989
|
+
# configs are discovered from the CWD, so point at them explicitly.
|
|
990
|
+
if [ "$TS_PROJECT_DIR" != "." ] && [ -f "$TS_PROJECT_DIR/eslint.config.js" ]; then
|
|
991
|
+
ESLINT_OUTPUT=$(npx eslint --config "$TS_PROJECT_DIR/eslint.config.js" $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
992
|
+
else
|
|
993
|
+
ESLINT_OUTPUT=$(npx eslint $ESLINT_FILES -f json --no-warn-ignored 2>&1)
|
|
994
|
+
fi
|
|
908
995
|
ESLINT_EXIT=$?
|
|
909
996
|
if [ "$ESLINT_EXIT" -ne 0 ]; then
|
|
910
997
|
if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
|
|
@@ -951,8 +1038,8 @@ else
|
|
|
951
1038
|
fi
|
|
952
1039
|
else
|
|
953
1040
|
echo "ℹ️ No ESLint configuration found - Skipping"
|
|
954
|
-
if [ -f "package.json" ] && command -v node &> /dev/null; then
|
|
955
|
-
HAS_ESLINT=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d.eslint||deps.eslint)?'yes':'no')}catch{console.log('no')}") && true
|
|
1041
|
+
if [ -f "$TS_PROJECT_DIR/package.json" ] && command -v node &> /dev/null; then
|
|
1042
|
+
HAS_ESLINT=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('$TS_PROJECT_DIR/package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d.eslint||deps.eslint)?'yes':'no')}catch{console.log('no')}") && true
|
|
956
1043
|
if [ "$HAS_ESLINT" = "yes" ]; then
|
|
957
1044
|
echo "⚠️ WARNING: eslint is installed in package.json but no eslint config found"
|
|
958
1045
|
echo " Either create .eslintrc.* / eslint.config.* or remove eslint from dependencies"
|
|
@@ -962,9 +1049,17 @@ else
|
|
|
962
1049
|
fi
|
|
963
1050
|
|
|
964
1051
|
# ── Biome lint/format check ──
|
|
965
|
-
|
|
1052
|
+
# Default the project dir so this branch stays runnable standalone
|
|
1053
|
+
# (the gate-1-biome-noop suite extracts it verbatim); a no-op inside the
|
|
1054
|
+
# hook, where TS_PROJECT_DIR is resolved above.
|
|
1055
|
+
TS_PROJECT_DIR="${TS_PROJECT_DIR:-.}"
|
|
1056
|
+
if [ -f "$TS_PROJECT_DIR/biome.json" ] || [ -f "$TS_PROJECT_DIR/biome.jsonc" ]; then
|
|
966
1057
|
echo "Running Biome lint/format check..."
|
|
967
|
-
|
|
1058
|
+
# Run from the TS project directory so Biome auto-discovers its config and
|
|
1059
|
+
# scopes --staged to that subdirectory (mirrors the single-language subdir
|
|
1060
|
+
# mode, which executes after cd "$PROJECT_SUBDIR"). Empirically verified:
|
|
1061
|
+
# `biome check --staged .` from the subdir checks the staged files under it.
|
|
1062
|
+
BIOME_OUT=$(cd "$TS_PROJECT_DIR" && npx biome check --staged . 2>&1)
|
|
968
1063
|
BIOME_STATUS=$?
|
|
969
1064
|
if [ "$BIOME_STATUS" -ne 0 ]; then
|
|
970
1065
|
# Biome exits 1 with `internalError/io - No files were processed` when the
|
|
@@ -989,8 +1084,8 @@ else
|
|
|
989
1084
|
echo "✅ PASSED - Biome check."
|
|
990
1085
|
fi
|
|
991
1086
|
else
|
|
992
|
-
if [ -f "package.json" ] && command -v node &> /dev/null; then
|
|
993
|
-
HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
|
|
1087
|
+
if [ -f "$TS_PROJECT_DIR/package.json" ] && command -v node &> /dev/null; then
|
|
1088
|
+
HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('$TS_PROJECT_DIR/package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
|
|
994
1089
|
if [ "$HAS_BIOME" = "yes" ]; then
|
|
995
1090
|
echo ""
|
|
996
1091
|
echo "⚠️ WARNING: @biomejs/biome is installed in package.json but no biome.json found"
|
|
@@ -1252,7 +1347,66 @@ else
|
|
|
1252
1347
|
echo "✅ PASSED - Basic shell script syntax check."
|
|
1253
1348
|
fi
|
|
1254
1349
|
;;
|
|
1255
|
-
|
|
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
|
+
|
|
1256
1410
|
*)
|
|
1257
1411
|
# Route to appropriate adapter for other languages
|
|
1258
1412
|
ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
|
|
@@ -1287,9 +1441,13 @@ else
|
|
|
1287
1441
|
done
|
|
1288
1442
|
fi
|
|
1289
1443
|
fi # End: if ! any_changed_files_match (Issue #312 skip)
|
|
1290
|
-
GATE_1_STATUS="PASS"
|
|
1444
|
+
GATE_1_STATUS="${GATE_1_STATUS:-PASS}"
|
|
1291
1445
|
GATE_1_TOOL="${PROJECT_LANGS}"
|
|
1292
|
-
|
|
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:-}"
|
|
1293
1451
|
|
|
1294
1452
|
# ============================================================================
|
|
1295
1453
|
# GATE 2: Duplicate Code Detection (NEW gate)
|
|
@@ -1480,11 +1638,15 @@ else
|
|
|
1480
1638
|
# --- BLOCK path: New .ts/.tsx files without corresponding tests ---
|
|
1481
1639
|
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)
|
|
1482
1640
|
|
|
1483
|
-
# Strip PROJECT_SUBDIR prefix from new-TS-file paths when
|
|
1484
|
-
#
|
|
1485
|
-
#
|
|
1641
|
+
# Strip PROJECT_SUBDIR prefix from new-TS-file paths ONLY when the hook
|
|
1642
|
+
# actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1; single-language
|
|
1643
|
+
# subdirectory projects). git diff returns repo-root relative paths; after
|
|
1644
|
+
# cd "$PROJECT_SUBDIR" the test-pairing [ -f ] checks below need
|
|
1645
|
+
# subdirectory-relative paths. When the hook stays at the repo root
|
|
1646
|
+
# (multi-language projects) the paths must KEEP their prefix or every
|
|
1647
|
+
# [ -f ] lookup misses and paired files are falsely BLOCKED.
|
|
1486
1648
|
# (Mirrors the v0.14.31+ Gate 5b fix at lines 1603-1608; Gate 5a was missing it.)
|
|
1487
|
-
if [
|
|
1649
|
+
if [ "$IN_PROJECT_SUBDIR" = "1" ] && [ -n "$NEW_TS_FILES" ]; then
|
|
1488
1650
|
NEW_TS_FILES=$(echo "$NEW_TS_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1489
1651
|
fi
|
|
1490
1652
|
|
|
@@ -1695,10 +1857,13 @@ else
|
|
|
1695
1857
|
# each plugin directory).
|
|
1696
1858
|
CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | grep -vE '^plugins/[^/]+/' || true)
|
|
1697
1859
|
|
|
1698
|
-
# Strip PROJECT_SUBDIR prefix from test file paths when
|
|
1699
|
-
#
|
|
1700
|
-
# vitest and [ -f ]
|
|
1701
|
-
|
|
1860
|
+
# Strip PROJECT_SUBDIR prefix from test file paths ONLY when the hook
|
|
1861
|
+
# actually cd'd into the subdirectory (IN_PROJECT_SUBDIR=1). git diff returns
|
|
1862
|
+
# repo-root relative paths; after cd "$PROJECT_SUBDIR" vitest and [ -f ]
|
|
1863
|
+
# checks need subdirectory-relative paths. At the repo root (multi-language
|
|
1864
|
+
# projects) stripping would silently skip the mock-density scan and feed
|
|
1865
|
+
# vitest paths that do not exist. (v0.14.31+ fix, gated on the cd in v0.19.x)
|
|
1866
|
+
if [ "$IN_PROJECT_SUBDIR" = "1" ] && [ -n "$CHANGED_TEST_FILES" ]; then
|
|
1702
1867
|
CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1703
1868
|
fi
|
|
1704
1869
|
|
|
@@ -1884,14 +2049,20 @@ else
|
|
|
1884
2049
|
CHANGED_SRC_FILES=$(echo "$CHANGED_SRC_FILES" | grep -vE '^plugins/[^/]+/' || true)
|
|
1885
2050
|
# Mirror handling is identical for source files: their test-adjacent
|
|
1886
2051
|
# paths must never reach vitest as root-project targets (#494).
|
|
2052
|
+
# Selection runs on repo-root relative paths (git diff output), before
|
|
2053
|
+
# any PROJECT_SUBDIR stripping, so the bundled-mirror prefixes match.
|
|
1887
2054
|
SELECTED_SRC_FILES=$(select_root_suite_targets "$CHANGED_SRC_FILES")
|
|
1888
2055
|
if [ "$SELECTED_SRC_FILES" != "$CHANGED_SRC_FILES" ]; then
|
|
1889
2056
|
echo " ↪ Source selection adjusted for bundled mirrors (#494):"
|
|
1890
2057
|
echo " was: $(echo "$CHANGED_SRC_FILES" | tr '\n' ' ')"
|
|
1891
2058
|
echo " now: $(echo "$SELECTED_SRC_FILES" | tr '\n' ' ')"
|
|
1892
2059
|
fi
|
|
1893
|
-
# Strip PROJECT_SUBDIR prefix
|
|
1894
|
-
|
|
2060
|
+
# Strip PROJECT_SUBDIR prefix ONLY when the hook cd'd into the
|
|
2061
|
+
# subdirectory (IN_PROJECT_SUBDIR=1; same gate as the CHANGED_TEST_FILES
|
|
2062
|
+
# fix above — at the repo root the paths must stay root-relative or the
|
|
2063
|
+
# related-test lookup and vitest targets resolve against the wrong dir,
|
|
2064
|
+
# #470)
|
|
2065
|
+
if [ "$IN_PROJECT_SUBDIR" = "1" ] && [ -n "$SELECTED_SRC_FILES" ]; then
|
|
1895
2066
|
SELECTED_SRC_FILES=$(echo "$SELECTED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
|
|
1896
2067
|
fi
|
|
1897
2068
|
CHANGED_SRC_COUNT=$(echo "$SELECTED_SRC_FILES" | grep -c '.' || true)
|
|
@@ -2608,9 +2779,9 @@ else
|
|
|
2608
2779
|
# real source path stays unbudgeted. Exclude it and let the mirroring be
|
|
2609
2780
|
# governed by Mirror Parity in CI, which compares content rather than
|
|
2610
2781
|
# re-linting it. (See the same exclusion in adapters/powershell.sh.)
|
|
2611
|
-
MIRROR_EXCLUDE=
|
|
2612
|
-
NEW_FILES=$(git diff --cached --name-only --diff-filter=A | grep -v "$MIRROR_EXCLUDE" | tr '\n' ' ')
|
|
2613
|
-
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' ' ')
|
|
2614
2785
|
|
|
2615
2786
|
if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
|
|
2616
2787
|
echo " ✅ No new or modified files to check."
|
|
@@ -2648,7 +2819,17 @@ else
|
|
|
2648
2819
|
echo "❌ BLOCKED - Boy Scout Rule violations detected ($VIOLATION_COUNT)"
|
|
2649
2820
|
exit 1
|
|
2650
2821
|
fi
|
|
2651
|
-
|
|
2822
|
+
|
|
2823
|
+
# REQ-4 (#452): the auto-init rewrites a committed file, which used to
|
|
2824
|
+
# surface as an unexplained dirty ` M .warnings-baseline.json` after
|
|
2825
|
+
# every commit that touched a first-time-modified file. Stage it so the
|
|
2826
|
+
# budget update travels with the commit that caused it; when the
|
|
2827
|
+
# baseline is unchanged (the common case) this is a no-op.
|
|
2828
|
+
if [ -n "$(git status --porcelain -- .warnings-baseline.json 2>/dev/null)" ]; then
|
|
2829
|
+
git add .warnings-baseline.json 2>/dev/null || true
|
|
2830
|
+
echo " ℹ️ .warnings-baseline.json auto-updated and staged with this commit."
|
|
2831
|
+
fi
|
|
2832
|
+
|
|
2652
2833
|
echo " ✅ PASSED - Boy Scout Rule compliance."
|
|
2653
2834
|
fi
|
|
2654
2835
|
else
|
|
@@ -2691,9 +2872,73 @@ fi
|
|
|
2691
2872
|
# ============================================================================
|
|
2692
2873
|
# GATE 9: Build Integrity Check (TypeScript compilation + package + imports)
|
|
2693
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
|
+
|
|
2694
2932
|
2>&1 echo ""
|
|
2695
2933
|
2>&1 echo "→ Gate 9: Build integrity... "
|
|
2696
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"
|
|
2697
2942
|
GATE_9_STATUS="SKIP"
|
|
2698
2943
|
|
|
2699
2944
|
if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
@@ -2710,15 +2955,15 @@ if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
|
|
|
2710
2955
|
CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
|
|
2711
2956
|
GATE_9_OUTPUT=$(mktemp)
|
|
2712
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
|
|
2713
|
-
|
|
2958
|
+
GATE_9_TS_STATUS="PASS"
|
|
2714
2959
|
echo " ✅ Build integrity check passed."
|
|
2715
2960
|
else
|
|
2716
2961
|
GATE_9_EXIT=$?
|
|
2717
2962
|
if [ $GATE_9_EXIT -eq 124 ]; then
|
|
2718
|
-
|
|
2963
|
+
GATE_9_TS_STATUS="SKIP"
|
|
2719
2964
|
echo " ⏱️ Build integrity check timed out (120s). SKIPPED."
|
|
2720
2965
|
else
|
|
2721
|
-
|
|
2966
|
+
GATE_9_TS_STATUS="BLOCK"
|
|
2722
2967
|
cat "$GATE_9_OUTPUT"
|
|
2723
2968
|
echo ""
|
|
2724
2969
|
echo "❌ BLOCKED - Gate 9: Build integrity check failed"
|
|
@@ -2737,6 +2982,52 @@ else
|
|
|
2737
2982
|
echo " ⏭️ SKIPPED - Build integrity (not a TypeScript project)"
|
|
2738
2983
|
fi
|
|
2739
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
|
+
|
|
2740
3031
|
GATE_9_END=$(gate_start_ms)
|
|
2741
3032
|
GATE_9_DURATION_MS=$((GATE_9_END - GATE_9_START))
|
|
2742
3033
|
GATE_9_DURATION=$((GATE_9_DURATION_MS / 1000))
|
|
@@ -2769,7 +3060,7 @@ fi
|
|
|
2769
3060
|
# Validates sprint state consistency (delphi-review APPROVED before BUILD)
|
|
2770
3061
|
# Uses sprint-gate.sh for standalone validation logic
|
|
2771
3062
|
# ============================================================================
|
|
2772
|
-
GATE_11_START=$(
|
|
3063
|
+
GATE_11_START=$(gate_start_ms)
|
|
2773
3064
|
GATE_11_STATUS="PASS"
|
|
2774
3065
|
|
|
2775
3066
|
# BEGIN SPRINT_GATE_SCRIPT RESOLUTION
|
|
@@ -2858,24 +3149,72 @@ generate_quality_report() {
|
|
|
2858
3149
|
local BRANCH
|
|
2859
3150
|
local PASSED_COUNT=0
|
|
2860
3151
|
local TOTAL_GATES=12
|
|
3152
|
+
local SKIP_COUNT=0
|
|
3153
|
+
local WARN_COUNT=0
|
|
3154
|
+
local NONPASS_COUNT=0
|
|
2861
3155
|
|
|
2862
3156
|
COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
2863
3157
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
2864
3158
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2865
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
|
|
2866
3167
|
for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
2867
3168
|
local status_var="GATE_${gate}_STATUS"
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
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
|
|
2871
3182
|
done
|
|
2872
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
|
+
|
|
2873
3196
|
local SCORE=0
|
|
2874
|
-
if
|
|
2875
|
-
SCORE
|
|
3197
|
+
if [ "$EFFECTIVE_TOTAL" -eq 0 ]; then
|
|
3198
|
+
SCORE="N/A"
|
|
2876
3199
|
else
|
|
2877
|
-
|
|
2878
|
-
|
|
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"
|
|
2879
3218
|
|
|
2880
3219
|
local BS_PASSED=0
|
|
2881
3220
|
local BS_BLOCKED=0
|
|
@@ -2915,8 +3254,11 @@ generate_quality_report() {
|
|
|
2915
3254
|
"overall": {
|
|
2916
3255
|
"gatesPassed": $PASSED_COUNT,
|
|
2917
3256
|
"gatesTotal": $TOTAL_GATES,
|
|
2918
|
-
"
|
|
2919
|
-
"
|
|
3257
|
+
"gatesSkipped": $SKIP_COUNT,
|
|
3258
|
+
"gatesEffective": $EFFECTIVE_TOTAL,
|
|
3259
|
+
"effectivePassRate": $EFFECTIVE_PASS_RATE_JSON,
|
|
3260
|
+
"score": $SCORE_JSON,
|
|
3261
|
+
"verdict": "$VERDICT"
|
|
2920
3262
|
},
|
|
2921
3263
|
"gates": {
|
|
2922
3264
|
"gate1_static_analysis": {
|
|
@@ -3011,7 +3353,7 @@ ENDJSON
|
|
|
3011
3353
|
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
|
|
3012
3354
|
GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
|
|
3013
3355
|
|
|
3014
|
-
echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$
|
|
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"
|
|
3015
3357
|
|
|
3016
3358
|
# ── Console output ────────────────────────────────────────────────────────
|
|
3017
3359
|
echo ""
|
|
@@ -3032,7 +3374,7 @@ ENDJSON
|
|
|
3032
3374
|
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
|
|
3033
3375
|
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
3034
3376
|
echo ""
|
|
3035
|
-
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)"
|
|
3036
3378
|
echo " Branch status: $REPORT_FILE"
|
|
3037
3379
|
echo " Run report: $PERSISTED_REPORT_FILE"
|
|
3038
3380
|
echo " History saved: $HISTORY_FILE"
|