@boyingliu01/xp-gate 0.14.13 → 0.14.15
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/build-integrity/__tests__/gate-10.test.ts +1 -1
- package/hooks/gate-12-file-hygiene.sh +230 -0
- package/hooks/gate-8.sh +1 -1
- package/hooks/pre-commit +467 -279
- package/lib/doctor.js +32 -4
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +2 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/config.test.ts +1 -1
- package/principles/config.ts +1 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-commit
CHANGED
|
@@ -339,240 +339,334 @@ if [ -z "$CHANGED_FILES" ]; then
|
|
|
339
339
|
exit 0
|
|
340
340
|
fi
|
|
341
341
|
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
342
|
+
# ============================================================================
|
|
343
|
+
# Multi-language detection (v0.15.0+): detect all languages from changed file
|
|
344
|
+
# extensions, with fallback to manifest-based detection.
|
|
345
|
+
# Result: PROJECT_LANGS (space-separated string), PROJECT_LANG (first language for
|
|
346
|
+
# compatibility), PROJECT_SUBDIR (derived from changed file paths in monorepos;
|
|
347
|
+
# v0.15.1+; ARCH-03 fix), PROJECT_SUBDIRS (per-language subdir override, empty = root).
|
|
348
|
+
# ============================================================================
|
|
349
|
+
|
|
350
|
+
# XP_GATE_LANG env var override — skip all detection, use single language
|
|
351
|
+
if [ -n "${XP_GATE_LANG:-}" ]; then
|
|
352
|
+
PROJECT_LANGS="$XP_GATE_LANG"
|
|
353
|
+
PROJECT_LANG="$XP_GATE_LANG"
|
|
354
|
+
echo ""
|
|
355
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
356
|
+
echo " 🔧 XP_GATE_LANG OVERRIDE: $PROJECT_LANG"
|
|
357
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
358
|
+
echo ""
|
|
359
|
+
else
|
|
360
|
+
# ---- Extension → language mapping ----
|
|
361
|
+
# Extracts unique languages from changed file extensions
|
|
362
|
+
PROJECT_LANGS=""
|
|
363
|
+
_detect_ext_lang() {
|
|
364
|
+
for ext in "$@"; do
|
|
365
|
+
if echo "$CHANGED_FILES" | grep -qE "\.${ext}$" 2>/dev/null; then
|
|
366
|
+
echo "1"
|
|
367
|
+
return 0
|
|
368
|
+
fi
|
|
369
|
+
done
|
|
370
|
+
echo "0"
|
|
371
|
+
return 1
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Check changed files for each language by extension
|
|
375
|
+
if [ "$(_detect_ext_lang "ts" "tsx" "js" "jsx" "mjs")" = "1" ]; then
|
|
376
|
+
PROJECT_LANGS="$PROJECT_LANGS typescript"
|
|
353
377
|
fi
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
elif [ -f "go.mod" ]; then
|
|
357
|
-
PROJECT_LANG="go"
|
|
358
|
-
elif [ -f "pubspec.yaml" ]; then
|
|
359
|
-
# Check if it's a Flutter project
|
|
360
|
-
if grep -q "flutter:" pubspec.yaml 2>/dev/null || [ -f ".metadata" ]; then
|
|
361
|
-
PROJECT_LANG="flutter"
|
|
362
|
-
else
|
|
363
|
-
PROJECT_LANG="dart"
|
|
378
|
+
if [ "$(_detect_ext_lang "py")" = "1" ]; then
|
|
379
|
+
PROJECT_LANGS="$PROJECT_LANGS python"
|
|
364
380
|
fi
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
|
|
368
|
-
CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
369
|
-
OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
370
|
-
if [ -n "$CPP_FILES" ]; then
|
|
371
|
-
PROJECT_LANG="cpp"
|
|
372
|
-
elif [ -n "$OBJC_FILES" ]; then
|
|
373
|
-
PROJECT_LANG="objectivec"
|
|
381
|
+
if [ "$(_detect_ext_lang "go")" = "1" ]; then
|
|
382
|
+
PROJECT_LANGS="$PROJECT_LANGS go"
|
|
374
383
|
fi
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
378
|
-
if [ -n "$OBJC_FILES" ]; then
|
|
379
|
-
PROJECT_LANG="objectivec"
|
|
384
|
+
if [ "$(_detect_ext_lang "java")" = "1" ]; then
|
|
385
|
+
PROJECT_LANGS="$PROJECT_LANGS java"
|
|
380
386
|
fi
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" 2>/dev/null | wc -l)
|
|
384
|
-
if [ "$KOTLIN_FILES" -gt 0 ]; then
|
|
385
|
-
PROJECT_LANG="kotlin"
|
|
386
|
-
else
|
|
387
|
-
PROJECT_LANG="java"
|
|
387
|
+
if [ "$(_detect_ext_lang "kt" "kts")" = "1" ]; then
|
|
388
|
+
PROJECT_LANGS="$PROJECT_LANGS kotlin"
|
|
388
389
|
fi
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
# If not found at root, search subdirectories (max depth 2) for project manifests
|
|
394
|
-
# This supports mixed/hybrid project structures like:
|
|
395
|
-
# root: Documentation + scripts
|
|
396
|
-
# subdirectory: Flutter/TypeScript/Python/Go project
|
|
397
|
-
# v0.15.1+ (ARCH-03 fix): Also derive PROJECT_SUBDIR from changed file paths
|
|
398
|
-
# when root-level config exists but actual project is in a subdirectory.
|
|
399
|
-
PROJECT_SUBDIR=""
|
|
400
|
-
if [ -z "$PROJECT_LANG" ]; then
|
|
401
|
-
# Search for TypeScript/JavaScript projects (excluding React Native)
|
|
402
|
-
TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
|
|
403
|
-
if [ -n "$TSCONFIG_SUB" ]; then
|
|
404
|
-
PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
|
|
405
|
-
# Check if it's React Native
|
|
406
|
-
if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
|
|
407
|
-
PROJECT_LANG="react-native"
|
|
390
|
+
if [ "$(_detect_ext_lang "dart")" = "1" ]; then
|
|
391
|
+
# Check for Flutter: if pubspec.yaml exists and contains flutter
|
|
392
|
+
if [ -f "pubspec.yaml" ] && grep -q "flutter:" pubspec.yaml 2>/dev/null; then
|
|
393
|
+
PROJECT_LANGS="$PROJECT_LANGS flutter"
|
|
408
394
|
else
|
|
409
|
-
|
|
395
|
+
PROJECT_LANGS="$PROJECT_LANGS dart"
|
|
410
396
|
fi
|
|
411
397
|
fi
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
398
|
+
if [ "$(_detect_ext_lang "swift")" = "1" ]; then
|
|
399
|
+
PROJECT_LANGS="$PROJECT_LANGS swift"
|
|
400
|
+
fi
|
|
401
|
+
if [ "$(_detect_ext_lang "m" "mm")" = "1" ]; then
|
|
402
|
+
PROJECT_LANGS="$PROJECT_LANGS objectivec"
|
|
403
|
+
fi
|
|
404
|
+
if [ "$(_detect_ext_lang "cpp" "cxx" "cc" "c" "hpp" "h")" = "1" ]; then
|
|
405
|
+
PROJECT_LANGS="$PROJECT_LANGS cpp"
|
|
406
|
+
fi
|
|
407
|
+
if [ "$(_detect_ext_lang "sh")" = "1" ]; then
|
|
408
|
+
PROJECT_LANGS="$PROJECT_LANGS shell"
|
|
409
|
+
fi
|
|
410
|
+
if [ "$(_detect_ext_lang "ps1" "psm1")" = "1" ]; then
|
|
411
|
+
PROJECT_LANGS="$PROJECT_LANGS powershell"
|
|
412
|
+
fi
|
|
413
|
+
# IaC: .tf, .tfvars, Dockerfile, docker-compose*, .yaml/.yml in kube context
|
|
414
|
+
if echo "$CHANGED_FILES" | grep -qE '(\.tf$|\.tfvars$|Dockerfile|docker-compose)' 2>/dev/null; then
|
|
415
|
+
PROJECT_LANGS="$PROJECT_LANGS iac"
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# Trim leading space
|
|
419
|
+
PROJECT_LANGS="${PROJECT_LANGS# }"
|
|
420
|
+
|
|
421
|
+
# ---- Fallback: manifest-based detection when no extension matches ----
|
|
422
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
423
|
+
if [ -f "package.json" ]; then
|
|
424
|
+
HAS_RN=$(grep -q "react-native" package.json 2>/dev/null && echo "yes" || echo "no")
|
|
425
|
+
HAS_IOS=$([ -d "ios" ] && echo "yes" || echo "no")
|
|
426
|
+
HAS_ANDROID=$([ -d "android" ] && echo "yes" || echo "no")
|
|
427
|
+
if [ "$HAS_RN" = "yes" ] || { [ "$HAS_IOS" = "yes" ] && [ "$HAS_ANDROID" = "yes" ]; }; then
|
|
428
|
+
PROJECT_LANGS="react-native"
|
|
429
|
+
else
|
|
430
|
+
PROJECT_LANGS="typescript"
|
|
430
431
|
fi
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
432
|
+
elif [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
|
|
433
|
+
PROJECT_LANGS="python"
|
|
434
|
+
elif [ -f "go.mod" ]; then
|
|
435
|
+
PROJECT_LANGS="go"
|
|
436
|
+
elif [ -f "pubspec.yaml" ]; then
|
|
437
|
+
if grep -q "flutter:" pubspec.yaml 2>/dev/null || [ -f ".metadata" ]; then
|
|
438
|
+
PROJECT_LANGS="flutter"
|
|
439
|
+
else
|
|
440
|
+
PROJECT_LANGS="dart"
|
|
441
|
+
fi
|
|
442
|
+
elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
|
|
443
|
+
KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" 2>/dev/null | wc -l)
|
|
444
|
+
if [ "$KOTLIN_FILES" -gt 0 ]; then
|
|
445
|
+
PROJECT_LANGS="kotlin"
|
|
446
|
+
else
|
|
447
|
+
PROJECT_LANGS="java"
|
|
448
|
+
fi
|
|
449
|
+
elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
|
|
450
|
+
CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
451
|
+
OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
|
|
452
|
+
if [ -n "$CPP_FILES" ]; then
|
|
453
|
+
PROJECT_LANGS="cpp"
|
|
454
|
+
elif [ -n "$OBJC_FILES" ]; then
|
|
455
|
+
PROJECT_LANGS="objectivec"
|
|
456
|
+
fi
|
|
457
|
+
elif [ -n "$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)" ]; then
|
|
458
|
+
PROJECT_LANGS="powershell"
|
|
447
459
|
fi
|
|
448
|
-
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# ---- Derive PROJECT_SUBDIR from changed file paths (v0.15.1+; ARCH-03 fix) ----
|
|
463
|
+
# Extension-based detection sets PROJECT_LANGS but leaves PROJECT_SUBDIR empty.
|
|
464
|
+
# In a monorepo where config files (tsconfig.json, pyproject.toml, ...) live in
|
|
465
|
+
# subdirectories (e.g. packages/frontend/), we must derive the subdirectory from
|
|
466
|
+
# the actual changed files so that Gates 1-9 run in the correct context.
|
|
467
|
+
PROJECT_SUBDIR=""
|
|
468
|
+
if [ -n "$PROJECT_LANGS" ]; then
|
|
469
|
+
# Walk up from each changed file to find the nearest project marker.
|
|
470
|
+
_detect_subdir() {
|
|
471
|
+
local fdir
|
|
472
|
+
fdir=$(dirname "$1")
|
|
473
|
+
while [ "$fdir" != "." ] && [ "$fdir" != "/" ]; do
|
|
474
|
+
# Skip node_modules, .git, and other non-project directories
|
|
475
|
+
case "$fdir" in
|
|
476
|
+
*/node_modules/*) fdir="${fdir%/*}" ; continue ;;
|
|
477
|
+
*/.git/*) fdir="${fdir%/*}" ; continue ;;
|
|
478
|
+
esac
|
|
479
|
+
if [ -f "$fdir/tsconfig.json" ] || [ -f "$fdir/package.json" ] || \
|
|
480
|
+
[ -f "$fdir/pyproject.toml" ] || [ -f "$fdir/setup.py" ] || \
|
|
481
|
+
[ -f "$fdir/go.mod" ] || [ -f "$fdir/pubspec.yaml" ] || \
|
|
482
|
+
[ -f "$fdir/pom.xml" ] || [ -f "$fdir/build.gradle" ] || \
|
|
483
|
+
[ -f "$fdir/build.gradle.kts" ] || [ -f "$fdir/CMakeLists.txt" ]; then
|
|
484
|
+
echo "$fdir"
|
|
485
|
+
return 0
|
|
486
|
+
fi
|
|
487
|
+
case "$fdir" in
|
|
488
|
+
*/*) fdir="${fdir%/*}" ;;
|
|
489
|
+
*) break ;;
|
|
490
|
+
esac
|
|
491
|
+
done
|
|
492
|
+
return 1
|
|
493
|
+
}
|
|
494
|
+
_subdir_set=""
|
|
495
|
+
while IFS= read -r _changed; do
|
|
496
|
+
[ -z "$_changed" ] && continue
|
|
497
|
+
_d=$(_detect_subdir "$_changed" 2>/dev/null) || true
|
|
498
|
+
if [ -n "$_d" ] && [ "$_d" != "." ]; then
|
|
499
|
+
case " $_subdir_set " in
|
|
500
|
+
*" $_d "*) ;; # already seen
|
|
501
|
+
*) _subdir_set="${_subdir_set} $_d" ;;
|
|
502
|
+
esac
|
|
503
|
+
fi
|
|
504
|
+
done <<CHANGEDFILELIST
|
|
449
505
|
$CHANGED_FILES
|
|
450
506
|
CHANGEDFILELIST
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
507
|
+
_subdir_count=$(echo "$_subdir_set" | awk '{ n=0; for(i=1;i<=NF;i++) n++; print n }')
|
|
508
|
+
if [ "$_subdir_count" = "1" ]; then
|
|
509
|
+
PROJECT_SUBDIR=$(echo "$_subdir_set" | awk '{print $1}')
|
|
510
|
+
echo ""
|
|
511
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
512
|
+
echo " 📁 MULTILANGUAGE SUBDIR DETECTED FROM CHANGED FILES"
|
|
513
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
514
|
+
echo ""
|
|
515
|
+
echo "Language: $PROJECT_LANGS"
|
|
516
|
+
echo "Location: $PROJECT_SUBDIR/"
|
|
517
|
+
echo ""
|
|
518
|
+
fi
|
|
462
519
|
fi
|
|
463
|
-
fi
|
|
464
520
|
|
|
465
|
-
|
|
466
|
-
#
|
|
467
|
-
if [ -z "$
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
PROJECT_SUBDIR=$(dirname "$
|
|
521
|
+
# ---- Subdirectory fallback (for single-language projects in subdirectories) ----
|
|
522
|
+
# Only used when extension-based detection found nothing
|
|
523
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
524
|
+
# Search for TypeScript/JavaScript projects
|
|
525
|
+
TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
|
|
526
|
+
if [ -n "$TSCONFIG_SUB" ]; then
|
|
527
|
+
PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
|
|
528
|
+
if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
|
|
529
|
+
PROJECT_LANGS="react-native"
|
|
530
|
+
else
|
|
531
|
+
PROJECT_LANGS="typescript"
|
|
532
|
+
fi
|
|
472
533
|
fi
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
534
|
+
|
|
535
|
+
# Search for Python projects
|
|
536
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
537
|
+
PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
538
|
+
if [ -n "$PYPROJECT_SUB" ]; then
|
|
539
|
+
PROJECT_LANGS="python"
|
|
540
|
+
PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
|
|
541
|
+
fi
|
|
481
542
|
fi
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
PROJECT_LANG="flutter"
|
|
490
|
-
else
|
|
491
|
-
PROJECT_LANG="dart"
|
|
543
|
+
|
|
544
|
+
# Search for Go projects
|
|
545
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
546
|
+
GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
547
|
+
if [ -n "$GOMOD_SUB" ]; then
|
|
548
|
+
PROJECT_LANGS="go"
|
|
549
|
+
PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
|
|
492
550
|
fi
|
|
493
|
-
PROJECT_SUBDIR=$(dirname "$PUBSPEC_SUB")
|
|
494
551
|
fi
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if [ "$KOTLIN_COUNT" -gt 0 ]; then
|
|
507
|
-
PROJECT_LANG="kotlin"
|
|
508
|
-
else
|
|
509
|
-
PROJECT_LANG="java"
|
|
552
|
+
|
|
553
|
+
# Search for Flutter/Dart projects
|
|
554
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
555
|
+
PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
556
|
+
if [ -n "$PUBSPEC_SUB" ]; then
|
|
557
|
+
if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
|
|
558
|
+
PROJECT_LANGS="flutter"
|
|
559
|
+
else
|
|
560
|
+
PROJECT_LANGS="dart"
|
|
561
|
+
fi
|
|
562
|
+
PROJECT_SUBDIR=$(dirname "$PUBSPEC_SUB")
|
|
510
563
|
fi
|
|
511
564
|
fi
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
565
|
+
|
|
566
|
+
# Search for Java/Kotlin projects
|
|
567
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
568
|
+
POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
569
|
+
GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
570
|
+
if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
|
|
571
|
+
MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
|
|
572
|
+
PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
|
|
573
|
+
KOTLIN_COUNT=$(find "$PROJECT_SUBDIR" -name "*.kt" -not -path "*/.git/*" 2>/dev/null | wc -l)
|
|
574
|
+
if [ "$KOTLIN_COUNT" -gt 0 ]; then
|
|
575
|
+
PROJECT_LANGS="kotlin"
|
|
576
|
+
else
|
|
577
|
+
PROJECT_LANGS="java"
|
|
578
|
+
fi
|
|
579
|
+
fi
|
|
520
580
|
fi
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
|
|
529
|
-
PROJECT_LANG="react-native"
|
|
581
|
+
|
|
582
|
+
# Search for C++ projects
|
|
583
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
584
|
+
CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
585
|
+
if [ -n "$CMAKE_SUB" ]; then
|
|
586
|
+
PROJECT_LANGS="cpp"
|
|
587
|
+
PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
|
|
530
588
|
fi
|
|
531
589
|
fi
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
590
|
+
|
|
591
|
+
# Search for React Native projects (ios/ + android/ directories)
|
|
592
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
593
|
+
RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
594
|
+
if [ -n "$RN_SUB" ]; then
|
|
595
|
+
PROJECT_SUBDIR=$(dirname "$RN_SUB")
|
|
596
|
+
if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
|
|
597
|
+
PROJECT_LANGS="react-native"
|
|
598
|
+
fi
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
# Search for PowerShell projects
|
|
603
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
604
|
+
PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
605
|
+
if [ -n "$PS_SUB" ]; then
|
|
606
|
+
PROJECT_LANGS="powershell"
|
|
607
|
+
PROJECT_SUBDIR=$(dirname "$PS_SUB")
|
|
608
|
+
fi
|
|
609
|
+
fi
|
|
610
|
+
|
|
611
|
+
# Report detected subdirectory project
|
|
612
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ -n "$PROJECT_LANGS" ]; then
|
|
613
|
+
echo ""
|
|
614
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
615
|
+
echo " 📁 SUBDIRECTORY PROJECT DETECTED"
|
|
616
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
617
|
+
echo ""
|
|
618
|
+
echo "Language: $PROJECT_LANGS"
|
|
619
|
+
echo "Location: $PROJECT_SUBDIR/"
|
|
620
|
+
echo ""
|
|
540
621
|
fi
|
|
541
622
|
fi
|
|
542
|
-
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
623
|
+
|
|
624
|
+
# Set PROJECT_LANG for backward compatibility (first language in list)
|
|
625
|
+
PROJECT_LANG=$(echo "$PROJECT_LANGS" | awk '{print $1}')
|
|
626
|
+
|
|
627
|
+
# ---- Documentation-only fallback ----
|
|
628
|
+
if [ -z "$PROJECT_LANGS" ]; then
|
|
629
|
+
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)
|
|
630
|
+
|
|
631
|
+
if [ "$CODE_FILES" -eq 0 ]; then
|
|
632
|
+
PROJECT_LANGS="documentation-only"
|
|
633
|
+
PROJECT_LANG="documentation-only"
|
|
634
|
+
echo ""
|
|
635
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
636
|
+
echo " 📚 DOCUMENTATION-ONLY PROJECT DETECTED"
|
|
637
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
638
|
+
echo ""
|
|
639
|
+
echo "This project contains no source code files."
|
|
640
|
+
echo "Skipping static analysis and test gates."
|
|
641
|
+
echo ""
|
|
642
|
+
echo "Proceeding with documentation-only checks..."
|
|
643
|
+
fi
|
|
553
644
|
fi
|
|
554
|
-
fi
|
|
555
645
|
|
|
556
|
-
#
|
|
557
|
-
|
|
558
|
-
if [
|
|
559
|
-
# Count source code files (including shell scripts)
|
|
560
|
-
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)
|
|
561
|
-
|
|
562
|
-
if [ "$CODE_FILES" -eq 0 ]; then
|
|
563
|
-
PROJECT_LANG="documentation-only"
|
|
646
|
+
# ---- Multi-language project banner ----
|
|
647
|
+
_lang_count=$(echo "$PROJECT_LANGS" | wc -w)
|
|
648
|
+
if [ "$_lang_count" -gt 1 ]; then
|
|
564
649
|
echo ""
|
|
565
650
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
566
|
-
echo "
|
|
651
|
+
echo " 🌐 MULTI-LANGUAGE PROJECT DETECTED"
|
|
567
652
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
568
653
|
echo ""
|
|
569
|
-
echo "
|
|
570
|
-
|
|
654
|
+
echo "Detected $_lang_count languages from changed files:"
|
|
655
|
+
for _l in $PROJECT_LANGS; do
|
|
656
|
+
echo " • $_l"
|
|
657
|
+
done
|
|
571
658
|
echo ""
|
|
572
|
-
echo "Proceeding with documentation-only checks..."
|
|
573
659
|
fi
|
|
574
660
|
fi
|
|
575
661
|
|
|
662
|
+
# Helper: check if a language is in PROJECT_LANGS
|
|
663
|
+
has_project_lang() {
|
|
664
|
+
for lang in $PROJECT_LANGS; do
|
|
665
|
+
if [ "$lang" = "$1" ]; then return 0; fi
|
|
666
|
+
done
|
|
667
|
+
return 1
|
|
668
|
+
}
|
|
669
|
+
|
|
576
670
|
# ============================================================================
|
|
577
671
|
# Audit helper — records gate execution to .xp-gate/audit.jsonl
|
|
578
672
|
# NEVER blocks commit on failure (try/catch around npx tsx).
|
|
@@ -619,10 +713,11 @@ gate_start_ms() {
|
|
|
619
713
|
date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0"
|
|
620
714
|
}
|
|
621
715
|
|
|
622
|
-
# Switch to subdirectory if detected (for
|
|
623
|
-
#
|
|
716
|
+
# Switch to subdirectory if detected (for single-language subdirectory projects).
|
|
717
|
+
# Multi-language projects stay at root — each adapter handles its own path.
|
|
624
718
|
ORIGINAL_DIR=""
|
|
625
|
-
|
|
719
|
+
_lang_count=$(echo "$PROJECT_LANGS" | wc -w)
|
|
720
|
+
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ "$PROJECT_LANG" != "documentation-only" ] && [ "$_lang_count" -le 1 ]; then
|
|
626
721
|
ORIGINAL_DIR=$(pwd)
|
|
627
722
|
cd "$PROJECT_SUBDIR"
|
|
628
723
|
echo "Working in: $PROJECT_SUBDIR/"
|
|
@@ -632,16 +727,24 @@ fi
|
|
|
632
727
|
# ============================================================================
|
|
633
728
|
# GATE 1: Code Quality (Static Analysis + Linting + Shell Check combined)
|
|
634
729
|
# Combination of: Old Gates 1 (Static), Gate 2 (Linting), Gate 5 (Shell Check)
|
|
730
|
+
# v0.14.3+ (Issue #312): Skip if no code files changed
|
|
635
731
|
# ============================================================================
|
|
636
732
|
2>&1 echo ""
|
|
637
733
|
2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
|
|
734
|
+
if ! any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|mm|dart|sh)$" "\.ya?ml$" "\.json$" "Dockerfile" "\.tf$"; then
|
|
735
|
+
echo "✅ PASSED - No code files changed, skipping lint."
|
|
736
|
+
else
|
|
638
737
|
GATE_1_START=$(gate_start_ms)
|
|
639
738
|
|
|
640
739
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
641
740
|
# Documentation-only project - skip static analysis
|
|
642
741
|
echo "⏭️ SKIPPED - Code quality (documentation project)."
|
|
643
742
|
|
|
644
|
-
|
|
743
|
+
else
|
|
744
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
745
|
+
echo " ── Checking $CURRENT_LANG ──"
|
|
746
|
+
case "$CURRENT_LANG" in
|
|
747
|
+
typescript)
|
|
645
748
|
# TypeScript: static analysis (tsc) + linting (eslint)
|
|
646
749
|
if ! command -v npx &> /dev/null; then
|
|
647
750
|
echo "ℹ️ npx not available — skipping TypeScript checks"
|
|
@@ -798,8 +901,9 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
|
798
901
|
fi
|
|
799
902
|
fi
|
|
800
903
|
fi
|
|
801
|
-
|
|
802
|
-
|
|
904
|
+
;;
|
|
905
|
+
|
|
906
|
+
python)
|
|
803
907
|
# Python: Ruff (includes syntax + linting) + mypy (optional typing)
|
|
804
908
|
if ! command -v ruff &> /dev/null; then
|
|
805
909
|
echo "ℹ️ ruff not available — skipping Python linting"
|
|
@@ -878,8 +982,9 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
878
982
|
echo "ℹ️ Mypy not available - Skipping optional type check"
|
|
879
983
|
fi
|
|
880
984
|
fi
|
|
881
|
-
|
|
882
|
-
|
|
985
|
+
;;
|
|
986
|
+
|
|
987
|
+
go)
|
|
883
988
|
# Go: go vet + golangci-lint
|
|
884
989
|
if ! command -v golangci-lint &> /dev/null; then
|
|
885
990
|
echo "ℹ️ golangci-lint not available — skipping Go linting"
|
|
@@ -896,8 +1001,9 @@ elif [ "$PROJECT_LANG" = "go" ]; then
|
|
|
896
1001
|
fi
|
|
897
1002
|
echo "✅ PASSED - golangci-lint."
|
|
898
1003
|
fi
|
|
899
|
-
|
|
900
|
-
|
|
1004
|
+
;;
|
|
1005
|
+
|
|
1006
|
+
shell)
|
|
901
1007
|
# Shell: check staged shell files only. Installed hook adapters are infrastructure,
|
|
902
1008
|
# not project changes, and should not be linted on unrelated commits.
|
|
903
1009
|
SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || true)
|
|
@@ -967,39 +1073,44 @@ elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$';
|
|
|
967
1073
|
done
|
|
968
1074
|
echo "✅ PASSED - Basic shell script syntax check."
|
|
969
1075
|
fi
|
|
970
|
-
|
|
971
|
-
else
|
|
972
|
-
# Route to appropriate adapter for other languages
|
|
973
|
-
ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
|
|
974
|
-
if [ -n "$ADAPTER_FILE" ] && source "$ADAPTER_FILE" 2>/dev/null; then
|
|
975
|
-
echo "Running static analysis for $PROJECT_LANG..."
|
|
976
|
-
run_static_analysis | head -30 2>/dev/null
|
|
977
|
-
ANALYSIS_EXIT=$?
|
|
978
|
-
if [ "$ANALYSIS_EXIT" -ne 0 ]; then
|
|
979
|
-
echo "⚠️ Static analysis had issues (may be non-blocking)"
|
|
980
|
-
fi
|
|
1076
|
+
;;
|
|
981
1077
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1078
|
+
*)
|
|
1079
|
+
# Route to appropriate adapter for other languages
|
|
1080
|
+
ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
|
|
1081
|
+
if [ -n "$ADAPTER_FILE" ] && source "$ADAPTER_FILE" 2>/dev/null; then
|
|
1082
|
+
echo "Running static analysis for $CURRENT_LANG..."
|
|
1083
|
+
run_static_analysis | head -30 2>/dev/null
|
|
1084
|
+
ANALYSIS_EXIT=$?
|
|
1085
|
+
if [ "$ANALYSIS_EXIT" -ne 0 ]; then
|
|
1086
|
+
echo "⚠️ Static analysis had issues (may be non-blocking)"
|
|
1087
|
+
fi
|
|
988
1088
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1089
|
+
echo "Running lint check for $CURRENT_LANG..."
|
|
1090
|
+
run_lint | head -30 2>/dev/null
|
|
1091
|
+
LINT_EXIT=$?
|
|
1092
|
+
if [ "$LINT_EXIT" -ne 0 ]; then
|
|
1093
|
+
echo "ℹ️ Lint check had issues (may be non-blocking)"
|
|
1094
|
+
fi
|
|
1095
|
+
|
|
1096
|
+
if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
|
|
1097
|
+
echo "✅ PASSED - Language-specific code quality checks."
|
|
1098
|
+
else
|
|
1099
|
+
# Tools unavailable or failed — skip non-blockingly
|
|
1100
|
+
echo "⏭️ SKIPPED - Code quality (tools unavailable or failed)"
|
|
1101
|
+
fi
|
|
1102
|
+
else
|
|
1103
|
+
# If no adapter exists for this language, skip
|
|
1104
|
+
echo "ℹ️ No specific adapter for $CURRENT_LANG — using generic checks if any"
|
|
1105
|
+
echo "⏭️ SKIPPED - Code quality (no specific checks for $CURRENT_LANG)"
|
|
1106
|
+
fi
|
|
1107
|
+
;;
|
|
1108
|
+
esac
|
|
1109
|
+
done
|
|
1000
1110
|
fi
|
|
1111
|
+
fi # End: if ! any_changed_files_match (Issue #312 skip)
|
|
1001
1112
|
GATE_1_STATUS="PASS"
|
|
1002
|
-
GATE_1_TOOL="${
|
|
1113
|
+
GATE_1_TOOL="${PROJECT_LANGS}"
|
|
1003
1114
|
record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
|
|
1004
1115
|
|
|
1005
1116
|
# ============================================================================
|
|
@@ -1013,7 +1124,11 @@ GATE_2_START=$(gate_start_ms)
|
|
|
1013
1124
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1014
1125
|
echo "⏭️ SKIPPED - Duplicate code (documentation project)."
|
|
1015
1126
|
|
|
1016
|
-
|
|
1127
|
+
else
|
|
1128
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
1129
|
+
echo " ── Checking $CURRENT_LANG ──"
|
|
1130
|
+
case "$CURRENT_LANG" in
|
|
1131
|
+
typescript)
|
|
1017
1132
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
1018
1133
|
exit 1
|
|
1019
1134
|
fi
|
|
@@ -1026,8 +1141,9 @@ elif [ "$PROJECT_LANG" = "typescript" ]; then
|
|
|
1026
1141
|
echo " Consider refactoring duplicate code blocks."
|
|
1027
1142
|
fi
|
|
1028
1143
|
echo "✅ PASSED - jscpd duplicate code check completed."
|
|
1029
|
-
|
|
1030
|
-
|
|
1144
|
+
;;
|
|
1145
|
+
|
|
1146
|
+
python)
|
|
1031
1147
|
if command -v pylint &> /dev/null; then
|
|
1032
1148
|
echo "Running pylint duplicate detection..."
|
|
1033
1149
|
pylint --disable=all --enable=duplicate-code $(echo "$CHANGED_FILES" | grep "\.py$")
|
|
@@ -1041,8 +1157,9 @@ elif [ "$PROJECT_LANG" = "python" ]; then
|
|
|
1041
1157
|
echo "ℹ️ pylint not found — skipping Python duplicate code check."
|
|
1042
1158
|
echo " Install pylint: pip install pylint"
|
|
1043
1159
|
fi
|
|
1044
|
-
|
|
1045
|
-
|
|
1160
|
+
;;
|
|
1161
|
+
|
|
1162
|
+
go)
|
|
1046
1163
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
1047
1164
|
exit 1
|
|
1048
1165
|
fi
|
|
@@ -1053,16 +1170,19 @@ elif [ "$PROJECT_LANG" = "go" ]; then
|
|
|
1053
1170
|
jscpd $FILES_TO_CHECK 2>&1 | head -10
|
|
1054
1171
|
fi
|
|
1055
1172
|
echo "✅ PASSED - Go duplicate code check completed."
|
|
1056
|
-
|
|
1057
|
-
|
|
1173
|
+
;;
|
|
1174
|
+
|
|
1175
|
+
powershell)
|
|
1058
1176
|
echo "ℹ️ No PowerShell-native duplicate detector (jscpd does not support .ps1)"
|
|
1059
1177
|
echo "⏭️ SKIPPED - Duplicate code (no tool for PowerShell)"
|
|
1060
|
-
|
|
1061
|
-
|
|
1178
|
+
;;
|
|
1179
|
+
|
|
1180
|
+
shell)
|
|
1062
1181
|
echo "ℹ️ Duplicate detection not typically used for shell scripts"
|
|
1063
1182
|
echo "⏭️ SKIPPED - Duplicate code (no tool for shell scripts)"
|
|
1064
|
-
|
|
1065
|
-
|
|
1183
|
+
;;
|
|
1184
|
+
|
|
1185
|
+
*)
|
|
1066
1186
|
SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
|
|
1067
1187
|
if [ -n "$SOURCE_FILES" ]; then
|
|
1068
1188
|
if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
|
|
@@ -1074,6 +1194,9 @@ else
|
|
|
1074
1194
|
else
|
|
1075
1195
|
echo "✅ PASSED - No source files detected for duplicate checking."
|
|
1076
1196
|
fi
|
|
1197
|
+
;;
|
|
1198
|
+
esac
|
|
1199
|
+
done
|
|
1077
1200
|
fi
|
|
1078
1201
|
GATE_2_STATUS="PASS"
|
|
1079
1202
|
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
|
|
@@ -1093,6 +1216,11 @@ source "$GATE_DIR/gate-4.sh"
|
|
|
1093
1216
|
# ============================================================================
|
|
1094
1217
|
# GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
|
|
1095
1218
|
# Uses adapter system to run tests + coverage for language
|
|
1219
|
+
#
|
|
1220
|
+
# v0.14.3+ (Issue #312): Skip test run when no code files changed.
|
|
1221
|
+
# Code files = src/**/*.ts, plugins/**/*.ts, *.ts, *.tsx, etc. (language-aware).
|
|
1222
|
+
# VERSION, CHANGELOG, docs, config-only changes → SKIP test run entirely.
|
|
1223
|
+
# Gate 5a (new file pairing) and Gate 5b (mock density) still run — they're lightweight.
|
|
1096
1224
|
# ============================================================================
|
|
1097
1225
|
2>&1 echo ""
|
|
1098
1226
|
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
@@ -1397,6 +1525,24 @@ else
|
|
|
1397
1525
|
done
|
|
1398
1526
|
fi
|
|
1399
1527
|
|
|
1528
|
+
# ============================================================================
|
|
1529
|
+
# v0.14.3+ (Issue #312): Early skip when no code files changed.
|
|
1530
|
+
# If the commit only touches VERSION, CHANGELOG, docs, config, .gitignore,
|
|
1531
|
+
# or similar non-code files, skip the test run entirely.
|
|
1532
|
+
# Gate 5a (new file pairing) and 5b (mock density) are cheap and still run.
|
|
1533
|
+
# ============================================================================
|
|
1534
|
+
CODE_EXTENSIONS="\.(ts|tsx|js|jsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|m|mm|dart|sh|ps1)$"
|
|
1535
|
+
CHANGED_CODE_FILES=$(echo "$CHANGED_FILES" | grep -E "$CODE_EXTENSIONS" || true)
|
|
1536
|
+
if [ -z "$CHANGED_CODE_FILES" ]; then
|
|
1537
|
+
echo "✅ PASSED - No code files changed (only docs/config/VERSION), skipping test run."
|
|
1538
|
+
echo " Changed files: $(echo "$CHANGED_FILES" | tr '\n' ' ')"
|
|
1539
|
+
TESTS_EXIT_CODE=0
|
|
1540
|
+
TESTS_SKIPPED=true
|
|
1541
|
+
GATE_5_END=$(gate_start_ms)
|
|
1542
|
+
GATE_5_ELAPSED=$((GATE_5_END - GATE_5_START))
|
|
1543
|
+
echo " ⏱️ Gate 5: ${GATE_5_ELAPSED}ms (skipped — no code changes)"
|
|
1544
|
+
else
|
|
1545
|
+
|
|
1400
1546
|
# ============================================================================
|
|
1401
1547
|
# Smart test selection (Issue #286): only run tests affected by changed files.
|
|
1402
1548
|
# Reuses CHANGED_TEST_FILES already computed in Gate 5b (line 1244).
|
|
@@ -1405,11 +1551,12 @@ else
|
|
|
1405
1551
|
echo "Running tests..."
|
|
1406
1552
|
|
|
1407
1553
|
# Route to appropriate test runner via adapter
|
|
1408
|
-
|
|
1409
|
-
|
|
1554
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
1555
|
+
ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
|
|
1556
|
+
if [ "${CURRENT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
|
|
1410
1557
|
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
1411
1558
|
# TypeScript: vitest with smart test selection
|
|
1412
|
-
if [ "$
|
|
1559
|
+
if [ "$CURRENT_LANG" = "typescript" ] && [ -f "package.json" ]; then
|
|
1413
1560
|
if npx vitest --version >/dev/null 2>&1; then
|
|
1414
1561
|
# Count changed test files to decide strategy
|
|
1415
1562
|
CHANGED_TEST_FILE_COUNT=$(echo "$CHANGED_TEST_FILES" | grep -c '.' || echo "0")
|
|
@@ -1480,6 +1627,7 @@ else
|
|
|
1480
1627
|
done
|
|
1481
1628
|
if [ -n "$RELATED_TESTS" ]; then
|
|
1482
1629
|
echo " Found related test(s): $(echo "$RELATED_TESTS" | tr '\n' ' ')"
|
|
1630
|
+
PARTIAL_TEST_RUN=true # Mark as partial run for coverage check
|
|
1483
1631
|
TESTS_OUTPUT=$(npx vitest run --coverage $RELATED_TESTS 2>&1)
|
|
1484
1632
|
TESTS_EXIT_CODE=$?
|
|
1485
1633
|
echo "$TESTS_OUTPUT" | head -60
|
|
@@ -1530,23 +1678,31 @@ else
|
|
|
1530
1678
|
echo ""
|
|
1531
1679
|
echo "❌ BLOCKED - Tests FAILED"
|
|
1532
1680
|
exit 1
|
|
1533
|
-
|
|
1534
|
-
|
|
1681
|
+
else
|
|
1682
|
+
echo "✅ PASSED - Unit tests passed."
|
|
1535
1683
|
fi
|
|
1536
1684
|
fi
|
|
1537
1685
|
else
|
|
1538
|
-
echo "✅ PASSED - Could not source ${
|
|
1686
|
+
echo "✅ PASSED - Could not source ${CURRENT_LANG} adapter for tests, assuming none to run."
|
|
1539
1687
|
fi
|
|
1540
1688
|
else
|
|
1541
|
-
echo "✅ PASSED - No ${
|
|
1689
|
+
echo "✅ PASSED - No ${CURRENT_LANG} adapter found for tests, assuming none to run."
|
|
1542
1690
|
fi
|
|
1543
|
-
|
|
1544
|
-
|
|
1691
|
+
done # end for CURRENT_LANG
|
|
1692
|
+
|
|
1693
|
+
fi # End: if [ -n "$CHANGED_CODE_FILES" ] (Issue #312 early skip)
|
|
1694
|
+
|
|
1695
|
+
# Next, run coverage check (skip if tests were skipped — Issue #312)
|
|
1696
|
+
if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
|
|
1697
|
+
echo "ℹ️ Test run was skipped — skipping coverage check."
|
|
1698
|
+
else
|
|
1545
1699
|
echo "Running coverage check..."
|
|
1546
1700
|
|
|
1547
|
-
|
|
1701
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
1702
|
+
ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
|
|
1703
|
+
if [ "${CURRENT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
|
|
1548
1704
|
if source "$ADAPTER_FILE" 2>/dev/null; then
|
|
1549
|
-
case "$
|
|
1705
|
+
case "$CURRENT_LANG" in
|
|
1550
1706
|
"typescript")
|
|
1551
1707
|
# Coverage collected during the test section above.
|
|
1552
1708
|
# Smart selection (Issue #286): only runs changed/related tests with --coverage,
|
|
@@ -1601,14 +1757,15 @@ else
|
|
|
1601
1757
|
esac
|
|
1602
1758
|
else
|
|
1603
1759
|
# Adapter source failed — warn and let Stage 2 attempt file-based enforcement
|
|
1604
|
-
echo "⚠️ Could not source ${
|
|
1760
|
+
echo "⚠️ Could not source ${CURRENT_LANG} adapter from ${ADAPTER_FILE}, will check coverage files directly..."
|
|
1605
1761
|
COV_EXIT=0
|
|
1606
1762
|
fi
|
|
1607
1763
|
else
|
|
1608
1764
|
# No adapter available — warn and let Stage 2 attempt file-based enforcement
|
|
1609
|
-
echo "⚠️ No adapter found for ${
|
|
1765
|
+
echo "⚠️ No adapter found for ${CURRENT_LANG} (searched: ${ADAPTER_DIR}/, ${ADAPTER_DIR}/adapters/, ${PROJECT_GITHOOKS}/adapters/, ${SCRIPT_DIR}/adapters/) — will check coverage files directly..."
|
|
1610
1766
|
COV_EXIT=0
|
|
1611
1767
|
fi
|
|
1768
|
+
done # end for CURRENT_LANG (Stage 1 coverage)
|
|
1612
1769
|
|
|
1613
1770
|
# ============================================================================
|
|
1614
1771
|
# Stage 2: UNCONDITIONAL coverage percentage enforcement
|
|
@@ -1622,7 +1779,8 @@ else
|
|
|
1622
1779
|
# ============================================================================
|
|
1623
1780
|
COVERAGE_ENFORCED=false
|
|
1624
1781
|
|
|
1625
|
-
|
|
1782
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
1783
|
+
case "$CURRENT_LANG" in
|
|
1626
1784
|
"typescript")
|
|
1627
1785
|
COVERAGE_ENFORCED=true
|
|
1628
1786
|
if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
|
|
@@ -1637,8 +1795,8 @@ else
|
|
|
1637
1795
|
" 2>/dev/null)
|
|
1638
1796
|
if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
|
|
1639
1797
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
1640
|
-
# If only subset of tests ran, warn instead of block
|
|
1641
|
-
if [ -n "${CHANGED_TEST_FILE_COUNT:-}" ] && [ "${CHANGED_TEST_FILE_COUNT:-0}" -gt 0 ] 2>/dev/null; then
|
|
1798
|
+
# If only subset of tests ran (partial run or related tests), warn instead of block
|
|
1799
|
+
if [ "${PARTIAL_TEST_RUN:-false}" = "true" ] || { [ -n "${CHANGED_TEST_FILE_COUNT:-}" ] && [ "${CHANGED_TEST_FILE_COUNT:-0}" -gt 0 ] 2>/dev/null; }; then
|
|
1642
1800
|
echo "⚠️ Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
|
|
1643
1801
|
echo " Run 'npx vitest run --coverage' for full coverage check."
|
|
1644
1802
|
else
|
|
@@ -1724,12 +1882,12 @@ else
|
|
|
1724
1882
|
fi
|
|
1725
1883
|
if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
|
|
1726
1884
|
if [ "$COVERAGE_PERCENT" -lt 80 ]; then
|
|
1727
|
-
echo "❌ BLOCKED - ${
|
|
1885
|
+
echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below 80% threshold"
|
|
1728
1886
|
exit 1
|
|
1729
1887
|
fi
|
|
1730
|
-
echo "${
|
|
1888
|
+
echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
|
|
1731
1889
|
else
|
|
1732
|
-
echo "⚠️ Could not determine ${
|
|
1890
|
+
echo "⚠️ Could not determine ${CURRENT_LANG} coverage percentage. No standard coverage report files found."
|
|
1733
1891
|
fi
|
|
1734
1892
|
;;
|
|
1735
1893
|
esac
|
|
@@ -1740,15 +1898,18 @@ else
|
|
|
1740
1898
|
exit 1
|
|
1741
1899
|
fi
|
|
1742
1900
|
if [ "$COVERAGE_ENFORCED" = false ]; then
|
|
1743
|
-
echo "ℹ️ Coverage enforcement not applicable for ${
|
|
1901
|
+
echo "ℹ️ Coverage enforcement not applicable for ${CURRENT_LANG}"
|
|
1744
1902
|
fi
|
|
1745
1903
|
echo "✅ PASSED - Coverage check completed."
|
|
1904
|
+
done # end for CURRENT_LANG (Stage 2 coverage)
|
|
1905
|
+
|
|
1906
|
+
fi # End: if [ "${TESTS_SKIPPED:-false}" != "true" ] (Issue #312 coverage skip)
|
|
1746
1907
|
|
|
1747
1908
|
# ============================================================================
|
|
1748
1909
|
# Stage 3: Per-file coverage check for NEW files (Issue #103)
|
|
1749
1910
|
# New files must have ≥80% coverage individually.
|
|
1750
1911
|
# ============================================================================
|
|
1751
|
-
if
|
|
1912
|
+
if has_project_lang "typescript" && [ -f "coverage/coverage-summary.json" ]; then
|
|
1752
1913
|
NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
|
|
1753
1914
|
if [ -n "$NEW_SOURCE_FILES" ]; then
|
|
1754
1915
|
echo ""
|
|
@@ -1802,15 +1963,16 @@ GATE_6_START=$(gate_start_ms)
|
|
|
1802
1963
|
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
1803
1964
|
echo " ✅ Skipped (documentation project)."
|
|
1804
1965
|
|
|
1805
|
-
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
1966
|
+
elif has_project_lang "powershell" 2>/dev/null || [ "$PROJECT_LANG" = "powershell" ]; then
|
|
1806
1967
|
echo " ℹ️ No PowerShell architecture tooling available"
|
|
1807
1968
|
echo " ⏭️ SKIPPED - Architecture validation (no tool for PowerShell)"
|
|
1808
1969
|
|
|
1809
1970
|
else
|
|
1810
|
-
# Check if architecture config exists
|
|
1811
|
-
if [ -f "architecture.yaml" ] || [ -f "
|
|
1971
|
+
# Check if architecture config exists (use $PROJECT_ROOT — hook may have cd'd to a subdir via ARCH-03)
|
|
1972
|
+
if [ -f "$PROJECT_ROOT/architecture.yaml" ] || [ -f "$PROJECT_ROOT/.architecturerc" ] || [ -f "$PROJECT_ROOT/.archlint.yaml" ]; then
|
|
1812
1973
|
# Different architecture tools per language
|
|
1813
|
-
|
|
1974
|
+
for CURRENT_LANG in $PROJECT_LANGS; do
|
|
1975
|
+
case "$CURRENT_LANG" in
|
|
1814
1976
|
"typescript")
|
|
1815
1977
|
# Try local npx install first (devDependencies), fall back to global
|
|
1816
1978
|
if npx --no-install @archlinter/cli --version >/dev/null 2>&1; then
|
|
@@ -1901,13 +2063,14 @@ else
|
|
|
1901
2063
|
fi
|
|
1902
2064
|
;;
|
|
1903
2065
|
*)
|
|
1904
|
-
echo " ℹ️ Architecture validation not configured for $
|
|
1905
|
-
echo " ⏭️ SKIPPED - Architecture validation (not configured for $
|
|
2066
|
+
echo " ℹ️ Architecture validation not configured for $CURRENT_LANG"
|
|
2067
|
+
echo " ⏭️ SKIPPED - Architecture validation (not configured for $CURRENT_LANG)"
|
|
1906
2068
|
;;
|
|
1907
2069
|
esac
|
|
2070
|
+
done # end for CURRENT_LANG (architecture)
|
|
1908
2071
|
else
|
|
1909
2072
|
# Architecture config is REQUIRED — BLOCK if missing
|
|
1910
|
-
if [ -f "
|
|
2073
|
+
if [ -f "$PROJECT_ROOT/.architecture-skip" ]; then
|
|
1911
2074
|
echo " ⚠️ Architecture config missing but .architecture-skip present — allowed to skip"
|
|
1912
2075
|
echo " ⏭️ SKIPPED - Architecture validation (.architecture-skip exemption)"
|
|
1913
2076
|
else
|
|
@@ -2014,8 +2177,13 @@ record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCK
|
|
|
2014
2177
|
# ============================================================================
|
|
2015
2178
|
# GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
|
|
2016
2179
|
# Extracted to gate-7.sh for maintainability
|
|
2180
|
+
# v0.14.3+ (Issue #312): Skip if no IaC files changed
|
|
2017
2181
|
# ============================================================================
|
|
2182
|
+
if any_changed_files_match "\.tf$" "\.tfvars$" "Dockerfile" "\.kube/" "\.ya?ml$"; then
|
|
2018
2183
|
source "$GATE_DIR/gate-7.sh"
|
|
2184
|
+
else
|
|
2185
|
+
echo "✅ PASSED - No IaC files changed, skipping Gate 7."
|
|
2186
|
+
fi
|
|
2019
2187
|
|
|
2020
2188
|
|
|
2021
2189
|
# GATE 8: Secret Scanning (gitleaks)
|
|
@@ -2085,9 +2253,14 @@ echo ""
|
|
|
2085
2253
|
|
|
2086
2254
|
# ============================================================================
|
|
2087
2255
|
# GATE 10: Semgrep SAST Security Scan
|
|
2088
|
-
# Extracted to gate-10.sh for maintainability
|
|
2256
|
+
# Extracted to gate-10.sh for maintainability (sourced as gate-9.sh)
|
|
2257
|
+
# v0.14.3+ (Issue #312): Skip if no code files changed
|
|
2089
2258
|
# ============================================================================
|
|
2259
|
+
if any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|dart|sh)$"; then
|
|
2090
2260
|
source "$GATE_DIR/gate-9.sh"
|
|
2261
|
+
else
|
|
2262
|
+
echo "✅ PASSED - No code files changed, skipping SAST scan."
|
|
2263
|
+
fi
|
|
2091
2264
|
# ============================================================================
|
|
2092
2265
|
|
|
2093
2266
|
# ============================================================================
|
|
@@ -2123,6 +2296,21 @@ echo "✅ Gate 11 completed in ${GATE_11_DURATION}s"
|
|
|
2123
2296
|
echo ""
|
|
2124
2297
|
# ============================================================================
|
|
2125
2298
|
|
|
2299
|
+
# ============================================================================
|
|
2300
|
+
# GATE 12: File Hygiene Check
|
|
2301
|
+
# Detects trailing whitespace, missing EOF newlines, merge conflict markers,
|
|
2302
|
+
# and oversized files in staged changes.
|
|
2303
|
+
# v0.14.15+ (Issue #350, #351)
|
|
2304
|
+
# ============================================================================
|
|
2305
|
+
if [ -f "$GATE_DIR/gate-12-file-hygiene.sh" ]; then
|
|
2306
|
+
source "$GATE_DIR/gate-12-file-hygiene.sh"
|
|
2307
|
+
elif [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-12-file-hygiene.sh" ]; then
|
|
2308
|
+
source "$(git rev-parse --show-toplevel)/githooks/gate-12-file-hygiene.sh"
|
|
2309
|
+
else
|
|
2310
|
+
echo "⏭️ SKIPPED - Gate 12: File Hygiene (gate-12-file-hygiene.sh not found)"
|
|
2311
|
+
fi
|
|
2312
|
+
# ============================================================================
|
|
2313
|
+
|
|
2126
2314
|
generate_quality_report() {
|
|
2127
2315
|
local HISTORY_FILE=".quality-history.jsonl"
|
|
2128
2316
|
local COMMIT_HASH
|
|
@@ -2181,7 +2369,7 @@ generate_quality_report() {
|
|
|
2181
2369
|
"generatedAt": "$TIMESTAMP",
|
|
2182
2370
|
"branch": "$BRANCH",
|
|
2183
2371
|
"commit": "$COMMIT_HASH",
|
|
2184
|
-
"language": "${PROJECT_LANG:-unknown}",
|
|
2372
|
+
"language": "${PROJECT_LANGS:-${PROJECT_LANG:-unknown}}",
|
|
2185
2373
|
"overall": {
|
|
2186
2374
|
"gatesPassed": $PASSED_COUNT,
|
|
2187
2375
|
"gatesTotal": $TOTAL_GATES,
|