@boyingliu01/xp-gate 0.8.8 → 0.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/bin/xp-gate.js +18 -0
  2. package/hooks/pre-commit +565 -120
  3. package/lib/arch.js +49 -0
  4. package/lib/check.js +50 -0
  5. package/lib/init.js +36 -0
  6. package/lib/principles.js +48 -0
  7. package/mock-policy/AGENTS.md +94 -0
  8. package/mock-policy/__tests__/config.test.ts +292 -0
  9. package/mock-policy/__tests__/integration.test.ts +600 -0
  10. package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
  11. package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
  12. package/mock-policy/config.ts +106 -0
  13. package/mock-policy/gate-m3.ts +233 -0
  14. package/mock-policy/mock-decision-engine.ts +155 -0
  15. package/mock-policy/schema.ts +22 -0
  16. package/mock-policy/scope-scanner.ts +288 -0
  17. package/mock-policy/types.ts +65 -0
  18. package/mutation/AGENTS.md +73 -0
  19. package/mutation/__tests__/detect-ai-test.test.ts +152 -0
  20. package/mutation/__tests__/gate-m.test.ts +194 -0
  21. package/mutation/detect-ai-test.ts +128 -0
  22. package/mutation/gate-m.ts +619 -0
  23. package/mutation/init-baseline.ts +192 -0
  24. package/mutation/stryker-types.ts +14 -0
  25. package/mutation/types.ts +70 -0
  26. package/mutation/update-baseline.ts +160 -0
  27. package/package.json +5 -2
  28. package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
  29. package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
  30. package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
  31. package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
  32. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
  33. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +83 -36
  34. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  35. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
  36. package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
  37. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  38. package/plugins/opencode/README.md +23 -7
  39. package/plugins/opencode/index.ts +36 -21
  40. package/plugins/opencode/package.json +1 -1
  41. package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
  42. package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
  43. package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
  44. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
  45. package/plugins/opencode/skills/sprint-flow/AGENTS.md +83 -36
  46. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  47. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
  48. package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
  49. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  50. package/plugins/qoder/plugin.json +20 -0
  51. package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
  52. package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
  53. package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
  54. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
  55. package/plugins/qoder/skills/sprint-flow/AGENTS.md +83 -36
  56. package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
  57. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
  58. package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
  59. package/plugins/qoder/skills/test-driven-development/SKILL.md +371 -0
  60. package/plugins/qoder/skills/test-driven-development/testing-anti-patterns.md +299 -0
  61. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  62. package/principles/AGENTS.md +111 -0
  63. package/principles/__tests__/analyzer.test.ts +343 -0
  64. package/principles/__tests__/baseline-storage.test.ts +187 -0
  65. package/principles/__tests__/baseline.test.ts +328 -0
  66. package/principles/__tests__/boy-scout-integration.test.ts +264 -0
  67. package/principles/__tests__/boy-scout.test.ts +646 -0
  68. package/principles/__tests__/config.test.ts +78 -0
  69. package/principles/__tests__/index.test.ts +81 -0
  70. package/principles/__tests__/lint-baseline.test.ts +314 -0
  71. package/principles/__tests__/reporter.test.ts +832 -0
  72. package/principles/__tests__/types.test.ts +101 -0
  73. package/principles/adapters/__tests__/base.test.ts +95 -0
  74. package/principles/adapters/__tests__/cpp.test.ts +290 -0
  75. package/principles/adapters/__tests__/dart.test.ts +84 -0
  76. package/principles/adapters/__tests__/go.test.ts +68 -0
  77. package/principles/adapters/__tests__/java.test.ts +68 -0
  78. package/principles/adapters/__tests__/kotlin.test.ts +76 -0
  79. package/principles/adapters/__tests__/objectivec.test.ts +453 -0
  80. package/principles/adapters/__tests__/python.test.ts +84 -0
  81. package/principles/adapters/__tests__/swift.test.ts +68 -0
  82. package/principles/adapters/__tests__/typescript.test.ts +116 -0
  83. package/principles/adapters/base.ts +124 -0
  84. package/principles/adapters/cpp.ts +115 -0
  85. package/principles/adapters/dart.ts +44 -0
  86. package/principles/adapters/go.ts +55 -0
  87. package/principles/adapters/java.ts +56 -0
  88. package/principles/adapters/kotlin.ts +54 -0
  89. package/principles/adapters/objectivec.ts +82 -0
  90. package/principles/adapters/python.ts +83 -0
  91. package/principles/adapters/swift.ts +40 -0
  92. package/principles/adapters/typescript.ts +66 -0
  93. package/principles/analyzer.ts +190 -0
  94. package/principles/baseline.ts +273 -0
  95. package/principles/boy-scout.ts +416 -0
  96. package/principles/config.ts +118 -0
  97. package/principles/index.ts +87 -0
  98. package/principles/lint-baseline.ts +385 -0
  99. package/principles/reporter.ts +188 -0
  100. package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
  101. package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
  102. package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
  103. package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
  104. package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
  105. package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
  106. package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
  107. package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
  108. package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
  109. package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
  110. package/principles/rules/__tests__/index.test.ts +49 -0
  111. package/principles/rules/__tests__/solid/dip.test.ts +121 -0
  112. package/principles/rules/__tests__/solid/isp.test.ts +60 -0
  113. package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
  114. package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
  115. package/principles/rules/__tests__/solid/srp.test.ts +89 -0
  116. package/principles/rules/clean-code/code-duplication.ts +31 -0
  117. package/principles/rules/clean-code/deep-nesting.ts +38 -0
  118. package/principles/rules/clean-code/god-class.ts +54 -0
  119. package/principles/rules/clean-code/large-file.ts +34 -0
  120. package/principles/rules/clean-code/long-function.ts +47 -0
  121. package/principles/rules/clean-code/magic-numbers.ts +59 -0
  122. package/principles/rules/clean-code/many-exports.ts +40 -0
  123. package/principles/rules/clean-code/missing-error-handling.ts +33 -0
  124. package/principles/rules/clean-code/too-many-params.ts +35 -0
  125. package/principles/rules/clean-code/unused-imports.ts +33 -0
  126. package/principles/rules/index.ts +61 -0
  127. package/principles/rules/solid/dip.ts +47 -0
  128. package/principles/rules/solid/isp.ts +37 -0
  129. package/principles/rules/solid/lsp.ts +79 -0
  130. package/principles/rules/solid/ocp.ts +40 -0
  131. package/principles/rules/solid/srp.ts +44 -0
  132. package/principles/types.ts +27 -0
  133. package/skills/delphi-review/AGENTS.md +7 -7
  134. package/skills/delphi-review/INSTALL.md +1 -1
  135. package/skills/delphi-review/SKILL.md +12 -12
  136. package/skills/delphi-review/references/code-walkthrough.md +1 -1
  137. package/skills/sprint-flow/AGENTS.md +83 -36
  138. package/skills/sprint-flow/SKILL.md +1 -1
  139. package/skills/sprint-flow/references/force-levels.md +1 -1
  140. package/skills/sprint-flow/references/phase-3-review.md +2 -2
  141. package/skills/test-specification-alignment/AGENTS.md +3 -3
package/hooks/pre-commit CHANGED
@@ -26,13 +26,14 @@ fi
26
26
  source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
27
27
  echo "Error: Cannot source adapter-common.sh from $ADAPTER_DIR"
28
28
  echo "Run: xp-gate init (per-project) or xp-gate setup-global (all projects)"
29
- exit
29
+ exit 1
30
30
  }
31
31
 
32
32
  # Trap: ensure quality report generated on ANY exit (pass or fail).
33
33
  # Using a guard prevents recursion when 'exit' is called from inside the trap.
34
+ # IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
34
35
  _QUALITY_REPORT_DONE=0
35
- _quality_report_on_exit() {
36
+ _quality_report_on_exit() {
36
37
  local _saved_exit=$?
37
38
  if [ "$_QUALITY_REPORT_DONE" = "1" ]; then
38
39
  exit $_saved_exit
@@ -49,6 +50,9 @@ if [[ -d "$PROJECT_ROOT/node_modules/.bin" ]]; then
49
50
  export PATH="$PROJECT_ROOT/node_modules/.bin:$PATH"
50
51
  fi
51
52
 
53
+ # Ensure pipe failures propagate exit codes through | head/tail/grep
54
+ set -o pipefail
55
+
52
56
  # ============================================================================
53
57
  # Helper Functions
54
58
  # ============================================================================
@@ -123,6 +127,166 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
123
127
  echo "Refactored Structure: 6 Type-Based Gates"
124
128
  echo ""
125
129
 
130
+ # ============================================================================
131
+ # Gate 0: Version Consistency Check (Protected Branches)
132
+ # ============================================================================
133
+
134
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
135
+ PROTECTED_BRANCHES="main master develop trunk mainline"
136
+ ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
137
+
138
+ # Optional: project-level .protected-branches config file
139
+ if [ -f "$ROOT_DIR/.protected-branches" ]; then
140
+ PROTECTED_BRANCHES="$PROTECTED_BRANCHES $(cat "$ROOT_DIR/.protected-branches" | tr '\n' ' ')"
141
+ fi
142
+
143
+ is_protected=false
144
+ for branch in $PROTECTED_BRANCHES; do
145
+ if [ "$CURRENT_BRANCH" = "$branch" ]; then
146
+ is_protected=true
147
+ break
148
+ fi
149
+ done
150
+
151
+ has_version_change=false
152
+ has_changelog_change=false
153
+
154
+ if [ "$is_protected" = "true" ]; then
155
+ # Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
156
+ # Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
157
+ if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
158
+ echo "✅ PASSED - Gate 0: Version Consistency Check (SKIP_VERSION_CHECK=1 env var)"
159
+ else
160
+ # Check if staged changes include VERSION or CHANGELOG.md
161
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
162
+ for file in $STAGED_FILES; do
163
+ case "$file" in
164
+ VERSION) has_version_change=true ;;
165
+ CHANGELOG.md) has_changelog_change=true ;;
166
+ esac
167
+ done
168
+
169
+ # If either VERSION or CHANGELOG.md is staged, pass
170
+ if [ "$has_version_change" = "true" ] || [ "$has_changelog_change" = "true" ]; then
171
+ # Auto-run sync-version.sh when VERSION is staged (issue #206 follow-up).
172
+ # Propagates VERSION → 4 package.json + refreshes 32 AGENTS.md headers,
173
+ # then stages only files sync-version.sh is known to touch (NOT runtime
174
+ # journals or unrelated edits) so the commit captures it atomically.
175
+ if [ "$has_version_change" = "true" ] && [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
176
+ echo "🔄 Gate 0: VERSION staged — running sync-version.sh to fan out to package.json + AGENTS.md headers..."
177
+ if bash "$ROOT_DIR/scripts/sync-version.sh" > /tmp/xp-gate-sync-version.log 2>&1; then
178
+ SYNC_STAGED=0
179
+ # Stage the 4 known package.json targets if modified
180
+ for pkg in package.json src/npm-package/package.json \
181
+ plugins/claude-code/.claude-plugin/plugin.json \
182
+ plugins/opencode/package.json \
183
+ src/npm-package/plugins/claude-code/.claude-plugin/plugin.json \
184
+ src/npm-package/plugins/opencode/package.json; do
185
+ if [ -f "$ROOT_DIR/$pkg" ] && ! git diff --quiet -- "$ROOT_DIR/$pkg" 2>/dev/null; then
186
+ git add "$ROOT_DIR/$pkg"
187
+ SYNC_STAGED=$((SYNC_STAGED + 1))
188
+ fi
189
+ done
190
+ # Stage AGENTS.md files with header-only modifications (header refresh is the only thing sync-version touches in AGENTS.md)
191
+ while IFS= read -r -d '' agents_file; do
192
+ if ! git diff --quiet -- "$agents_file" 2>/dev/null; then
193
+ git add "$agents_file"
194
+ SYNC_STAGED=$((SYNC_STAGED + 1))
195
+ fi
196
+ done < <(find "$ROOT_DIR" -name 'AGENTS.md' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0)
197
+ echo " auto-staged $SYNC_STAGED file(s) from sync-version.sh (package.json × ≤6 + AGENTS.md headers)"
198
+ else
199
+ echo "⚠️ sync-version.sh failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
200
+ fi
201
+ fi
202
+ echo "✅ PASSED - Gate 0: Version Consistency Check (VERSION/CHANGELOG staged)"
203
+ else
204
+ # Check for doc-only changes (no source code files)
205
+ has_source=false
206
+ for file in $STAGED_FILES; do
207
+ case "$file" in
208
+ *.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
209
+ has_source=true
210
+ ;;
211
+ esac
212
+ done
213
+
214
+ # Check for build-tooling-only changes (adapters, scripts, hooks)
215
+ has_real_source=false
216
+ for file in $STAGED_FILES; do
217
+ case "$file" in
218
+ src/npm-package/adapters/*|src/npm-package/scripts/*|src/npm-package/hooks/*|scripts/*|githooks/adapters/*)
219
+ continue ;;
220
+ *.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
221
+ has_real_source=true
222
+ ;;
223
+ esac
224
+ done
225
+
226
+ # Doc-only changes: no source code files
227
+ if [ "$has_source" = "false" ]; then
228
+ echo "✅ PASSED - Gate 0: Version Consistency Check (doc-only changes)"
229
+ else
230
+ # Check for bypass: commit message prefix (read from COMMIT_EDITMSG during pre-commit hook)
231
+ COMMIT_MSG=$(cat "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null | head -1 || echo "")
232
+ if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\]'; then
233
+ if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\].*(chore:|docs:|release:)'; then
234
+ if [ "$has_real_source" = "false" ]; then
235
+ echo "✅ PASSED - Gate 0: Version Consistency Check (bypass: chore/docs/release)"
236
+ else
237
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
238
+ VERSION CONSISTENCY CHECK (Gate 0)
239
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240
+ Branch: $CURRENT_BRANCH
241
+ ERROR: [skip-version-check] bypass used but production source files detected.
242
+
243
+ Bypass only allows build-tooling changes (adapters/, scripts/, hooks/).
244
+ Production source files require VERSION/CHANGELOG update.
245
+
246
+ Files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
247
+ "
248
+ exit 1
249
+ fi
250
+ else
251
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
252
+ VERSION CONSISTENCY CHECK (Gate 0)
253
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
254
+ Branch: $CURRENT_BRANCH
255
+ ERROR: [skip-version-check] bypass prefix invalid.
256
+
257
+ Allowed prefixes: chore:, docs:, release:
258
+ Example: [skip-version-check] chore: update dependencies
259
+ "
260
+ exit 1
261
+ fi
262
+ else
263
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
264
+ VERSION CONSISTENCY CHECK (Gate 0)
265
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
266
+ Branch: $CURRENT_BRANCH
267
+ ERROR: Committing to a protected branch without VERSION/CHANGELOG update.
268
+
269
+ This bypasses the ship workflow and causes version drift.
270
+ Before committing:
271
+ 1. Update VERSION file with new version
272
+ 2. Add entry to CHANGELOG.md
273
+ 3. Run: bash scripts/sync-version.sh
274
+
275
+ Or use --no-verify (discouraged) or include [skip-version-check] in commit message.
276
+
277
+ Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
278
+ "
279
+ exit 1
280
+ fi
281
+ fi
282
+ fi
283
+ fi # closes SKIP_VERSION_CHECK if/else
284
+ else
285
+ echo "✅ PASSED - Gate 0: Version Consistency Check (non-protected branch: $CURRENT_BRANCH)"
286
+ fi
287
+
288
+ echo ""
289
+
126
290
  # Get list of changed files
127
291
  CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
128
292
 
@@ -157,8 +321,8 @@ elif [ -f "pubspec.yaml" ]; then
157
321
  elif [ -n "$(find . -name '*.ps1' -not -path './.git/*' -maxdepth 2 | head -1)" ]; then
158
322
  PROJECT_LANG="powershell"
159
323
  elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
160
- CPP_FILES=$(find . -maxdepth 2 -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" -not -path "./.git/*" 2>/dev/null | head -1)
161
- OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
324
+ CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
325
+ OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
162
326
  if [ -n "$CPP_FILES" ]; then
163
327
  PROJECT_LANG="cpp"
164
328
  elif [ -n "$OBJC_FILES" ]; then
@@ -166,7 +330,7 @@ elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
166
330
  fi
167
331
  elif [ -f "*.xcodeproj" ] || [ -d "*.xcworkspace" ]; then
168
332
  # Xcode project - Objective-C or Swift
169
- OBJC_FILES=$(find . -maxdepth 2 -name "*.m" -o -name "*.mm" -not -path "./.git/*" 2>/dev/null | head -1)
333
+ OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
170
334
  if [ -n "$OBJC_FILES" ]; then
171
335
  PROJECT_LANG="objectivec"
172
336
  fi
@@ -310,6 +474,52 @@ if [ -z "$PROJECT_LANG" ]; then
310
474
  fi
311
475
  fi
312
476
 
477
+ # ============================================================================
478
+ # Audit helper — records gate execution to .xp-gate/audit.jsonl
479
+ # NEVER blocks commit on failure (try/catch around npx tsx).
480
+ # Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
481
+ # ============================================================================
482
+ AUDIT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
483
+ PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AUDIT_SCRIPT_DIR/../..")"
484
+
485
+ record_gate_audit() {
486
+ local gate_id="$1"
487
+ local gate_name="$2"
488
+ local passed="$3"
489
+ local issues_found="$4"
490
+ local start_ms="$5"
491
+ local end_ms
492
+ end_ms=$(date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0")
493
+ local duration_ms=$((end_ms - start_ms))
494
+ if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
495
+ duration_ms=0
496
+ fi
497
+
498
+ # Determine gate result: PASS or FAIL based on status variable
499
+ local gate_result="true"
500
+ if [ "$passed" != "PASS" ] && [ "$passed" != "SKIP" ]; then
501
+ gate_result="false"
502
+ fi
503
+
504
+ # Fire-and-forget: audit must never block the commit
505
+ {
506
+ npx tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
507
+ --gate-id "$gate_id" \
508
+ --gate-name "$gate_name" \
509
+ --passed "$gate_result" \
510
+ --issues-found "$issues_found" \
511
+ --duration-ms "$duration_ms" \
512
+ --trigger commit \
513
+ 2>/dev/null
514
+ } &
515
+ disown 2>/dev/null || true
516
+ }
517
+
518
+ # Get milliseconds timestamp (used by gates for duration measurement)
519
+ gate_start_ms() {
520
+ date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0"
521
+ }
522
+
313
523
  # Switch to subdirectory if detected (for mixed/hybrid projects)
314
524
  # All subsequent gates will run in the subdirectory context
315
525
  ORIGINAL_DIR=""
@@ -324,43 +534,95 @@ fi
324
534
  # GATE 1: Code Quality (Static Analysis + Linting + Shell Check combined)
325
535
  # Combination of: Old Gates 1 (Static), Gate 2 (Linting), Gate 5 (Shell Check)
326
536
  # ============================================================================
327
- 2>&1 echo ""
328
- 2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
537
+ 2>&1 echo ""
538
+ 2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
539
+ GATE_1_START=$(gate_start_ms)
329
540
 
330
541
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
331
542
  # Documentation-only project - skip static analysis
332
543
  echo "✅ PASSED - Skipped (no source code to analyze)."
333
544
 
334
- elif [ "$PROJECT_LANG" = "typescript" ]; then
335
- # TypeScript: static analysis (tsc) + linting (eslint)
545
+ elif [ "$PROJECT_LANG" = "typescript" ]; then
546
+ # TypeScript: static analysis (tsc) + linting (eslint)
336
547
  if ! command -v npx &> /dev/null; then
337
548
  echo "ℹ️ SKIP - npx not available (non-blocking for TypeScript)"
338
549
  echo "✅ PASSED - Code Quality Gate (SKIP)"
339
- else
340
- # Run TypeScript type checking
341
- echo "Running TypeScript static analysis (tsc)..."
342
- npx tsc --noEmit --skipLibCheck 2>&1 | head -30
343
- TSC_EXIT=$?
344
- if [ "$TSC_EXIT" -ne 0 ]; then
345
- echo ""
346
- echo " BLOCKED - TYPE ERRORS detected"
347
- echo "Fix the type errors above before committing."
348
- exit
550
+ else
551
+ # Verify tsc is real TypeScript compiler (npx may fetch deprecated tsc@2.0.4 placeholder)
552
+ TSC_OUTPUT=$(npx tsc --version 2>&1)
553
+ if echo "$TSC_OUTPUT" | grep -q "Version"; then
554
+ # Only run tsc if tsconfig.json exists (without it tsc prints help and exits non-zero)
555
+ if [ -f "tsconfig.json" ]; then
556
+ # Run TypeScript type checking
557
+ echo "Running TypeScript static analysis ($TSC_OUTPUT)..."
558
+ npx tsc --noEmit --skipLibCheck 2>&1 | head -30
559
+ TSC_EXIT=$?
560
+ if [ "$TSC_EXIT" -ne 0 ]; then
561
+ echo ""
562
+ echo "❌ BLOCKED - TYPE ERRORS detected"
563
+ echo "Fix the type errors above before committing."
564
+ exit 1
565
+ fi
566
+ echo "✅ PASSED - TypeScript static analysis."
567
+ else
568
+ echo "ℹ️ SKIP - tsconfig.json not found (no TypeScript project config)"
569
+ fi
570
+ else
571
+ echo "ℹ️ SKIP - tsc not properly installed ($TSC_OUTPUT)"
572
+ echo "Install typescript (npm install typescript --save-dev) for type checking."
349
573
  fi
350
- echo "✅ PASSED - TypeScript static analysis."
351
574
 
352
575
  # Run ESLint linting if config exists
353
576
  if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
354
577
  echo "Running ESLint linting..."
355
- npx eslint "$CHANGED_FILES" --ext .js,.jsx,.ts,.tsx --max-warnings 0 2>&1 | head -30
356
- ESLINT_EXIT=$?
357
- if [ "$ESLINT_EXIT" -ne 0 ]; then
358
- echo ""
359
- echo "❌ BLOCKED - LINT ERRORS detected"
360
- echo "Fix the lint errors above before committing."
361
- exit
578
+ # Filter to only TS/JS files - ESLint config ignores non-matching files
579
+ ESLINT_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
580
+ if [ -n "$ESLINT_FILES" ]; then
581
+ ESLINT_OUTPUT=$(npx eslint $ESLINT_FILES -f json --no-warn-ignored 2>&1)
582
+ ESLINT_EXIT=$?
583
+ if [ "$ESLINT_EXIT" -ne 0 ]; then
584
+ if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
585
+ # Compare against lint baseline
586
+ BASELINE_WARNINGS=$(echo "$ESLINT_FILES" | tr ' ' '\n' | node -e "
587
+ const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
588
+ const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
589
+ const files=stdin?stdin.split('\n'):[];
590
+ let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
591
+ console.log(total);
592
+ ")
593
+ CURRENT_WARNINGS=$(echo "$ESLINT_OUTPUT" | node -e "
594
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
595
+ console.log(d.reduce((s,f)=>s+(f.warningCount||0)+(f.errorCount||0),0));
596
+ ")
597
+ if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
598
+ NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
599
+ echo "$ESLINT_OUTPUT" | head -30
600
+ echo ""
601
+ echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
602
+ echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
603
+ exit 1
604
+ else
605
+ REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
606
+ if [ "$REDUCED" -gt 0 ]; then
607
+ echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
608
+ else
609
+ echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
610
+ fi
611
+ fi
612
+ else
613
+ echo "$ESLINT_OUTPUT" | head -30
614
+ echo ""
615
+ echo "❌ BLOCKED - LINT ERRORS detected"
616
+ echo "Fix the lint errors above before committing."
617
+ echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
618
+ exit 1
619
+ fi
620
+ else
621
+ echo "✅ PASSED - ESLint linting."
622
+ fi
623
+ else
624
+ echo "✅ PASSED - No staged TS/JS files to lint."
362
625
  fi
363
- echo "✅ PASSED - ESLint linting."
364
626
  else
365
627
  echo "ℹ️ No ESLint configuration found - Skipping"
366
628
  fi
@@ -374,16 +636,60 @@ elif [ "$PROJECT_LANG" = "python" ]; then
374
636
  else
375
637
  # Run Ruff (includes syntax check, linting)
376
638
  echo "Running Ruff linting (syntax + lint)..."
377
- ruff check "$CHANGED_FILES" 2>&1 | head -30
639
+ RUFF_OUTPUT=$(ruff check "$CHANGED_FILES" --output-format json 2>&1)
378
640
  RUFF_EXIT=$?
379
641
  if [ "$RUFF_EXIT" -ne 0 ]; then
380
- echo ""
381
- echo "❌ BLOCKED - RUFF ERRORS detected"
382
- echo "Fix the lint errors above before committing."
383
- echo "Tip: Run 'ruff check --fix' to auto-fix some issues."
384
- exit
642
+ if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
643
+ # Get baseline warnings for changed Python files
644
+ PY_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
645
+ if [ -n "$PY_FILES" ]; then
646
+ BASELINE_WARNINGS=$(echo "$PY_FILES" | node -e "
647
+ const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
648
+ const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
649
+ const files=stdin?stdin.split('\n'):[];
650
+ let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
651
+ console.log(total);
652
+ ")
653
+ CURRENT_WARNINGS=$(echo "$RUFF_OUTPUT" | node -e "
654
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
655
+ console.log(d.reduce((s,f)=>s+(f.messages?f.messages.length:0),0));
656
+ ")
657
+ if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
658
+ NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
659
+ echo "$RUFF_OUTPUT" | node -e "
660
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
661
+ for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
662
+ " | head -30
663
+ echo ""
664
+ echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
665
+ echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
666
+ exit 1
667
+ else
668
+ REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
669
+ if [ "$REDUCED" -gt 0 ]; then
670
+ echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
671
+ else
672
+ echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
673
+ fi
674
+ fi
675
+ else
676
+ echo "✅ PASSED - Ruff linting (no Python files to check)."
677
+ fi
678
+ else
679
+ echo "$RUFF_OUTPUT" | node -e "
680
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
681
+ for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
682
+ " | head -30
683
+ echo ""
684
+ echo "❌ BLOCKED - RUFF ERRORS detected"
685
+ echo "Fix the lint errors above before committing."
686
+ echo "Tip: Run 'ruff check --fix' to auto-fix some issues."
687
+ echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
688
+ exit 1
689
+ fi
690
+ else
691
+ echo "✅ PASSED - Ruff linting."
385
692
  fi
386
- echo "✅ PASSED - Ruff linting."
387
693
 
388
694
  # Optional: Type checking with mypy
389
695
  if command -v mypy &> /dev/null; then
@@ -394,7 +700,7 @@ elif [ "$PROJECT_LANG" = "python" ]; then
394
700
  echo ""
395
701
  echo "❌ BLOCKED - MYPI TYPE ERRORS detected"
396
702
  echo "Fix the type errors above before committing."
397
- exit
703
+ exit 1
398
704
  fi
399
705
  echo "✅ PASSED - mypy type checking."
400
706
  else
@@ -415,42 +721,80 @@ elif [ "$PROJECT_LANG" = "go" ]; then
415
721
  echo ""
416
722
  echo "❌ BLOCKED - GOLANGCI-LINT ERRORS detected"
417
723
  echo "Fix the lint errors above before committing."
418
- exit
724
+ exit 1
419
725
  fi
420
726
  echo "✅ PASSED - golangci-lint."
421
727
  fi
422
728
 
423
- elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$' || find . -name "*.sh" -type f | head -n 1; then
424
- # Shell: For shell files, use shellcheck directly
425
- if command -v shellcheck &> /dev/null; then
426
- echo "Running shellcheck on shell scripts..."
427
- shellcheck $(echo "$CHANGED_FILES" | grep '\.sh$' || echo "."/*.sh ./**/**/*.sh 2>/dev/null) 2>&1 | head -30
729
+ elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$'; then
730
+ # Shell: check staged shell files only. Installed hook adapters are infrastructure,
731
+ # not project changes, and should not be linted on unrelated commits.
732
+ SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || true)
733
+
734
+ if [ -z "$SHELL_FILES" ]; then
735
+ echo "✅ PASSED - No staged shell scripts to check."
736
+ elif command -v shellcheck &> /dev/null; then
737
+ echo "Running shellcheck on staged shell scripts..."
738
+ SHELLCHECK_OUTPUT=$(shellcheck -f json $SHELL_FILES 2>&1)
428
739
  SHELLCHECK_EXIT=$?
429
740
  if [ "$SHELLCHECK_EXIT" -ne 0 ]; then
430
- echo ""
431
- echo " BLOCKED - SHELLCHECK ERRORS detected"
432
- echo "Fix the shell script errors above."
433
- exit
434
- fi
435
- echo "✅ PASSED - Shellcheck completed."
436
- else
437
- echo "ℹ️ shellcheck not available - Attempting basic syntax check"
438
- # Basic check for shell script files
439
- SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || find . -name "*.sh" -type f 2>/dev/null | head -10)
440
- if [ -n "$SHELL_FILES" ]; then
441
- for sh_file in $SHELL_FILES; do
442
- if [ -f "$sh_file" ]; then
443
- bash -n "$sh_file"
444
- if [ $? -ne 0 ]; then
445
- echo "❌ BLOCKED - SYNTAX ERROR in $sh_file"
446
- exit
741
+ if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
742
+ BASELINE_WARNINGS=$(echo "$SHELL_FILES" | node -e "
743
+ const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
744
+ const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
745
+ const files=stdin?stdin.split('\n'):[];
746
+ let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
747
+ console.log(total);
748
+ ")
749
+ CURRENT_WARNINGS=$(echo "$SHELLCHECK_OUTPUT" | node -e "
750
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
751
+ console.log(d.length);
752
+ ")
753
+ if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
754
+ NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
755
+ echo "$SHELLCHECK_OUTPUT" | node -e "
756
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
757
+ for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
758
+ " | head -30
759
+ echo ""
760
+ echo "❌ BLOCKED - ${NEW_WARNINGS} NEW shellcheck issue(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
761
+ echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
762
+ exit 1
763
+ else
764
+ REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
765
+ if [ "$REDUCED" -gt 0 ]; then
766
+ echo "✅ PASSED - Shellcheck debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
767
+ else
768
+ echo "✅ PASSED - No new shellcheck issues (baseline: ${BASELINE_WARNINGS})"
447
769
  fi
448
770
  fi
449
- done
450
- echo " PASSED - Basic shell script syntax check."
771
+ else
772
+ echo "$SHELLCHECK_OUTPUT" | node -e "
773
+ const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
774
+ for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
775
+ " | head -30
776
+ echo ""
777
+ echo "❌ BLOCKED - SHELLCHECK ERRORS detected"
778
+ echo "Fix the shell script errors above."
779
+ echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
780
+ exit 1
781
+ fi
451
782
  else
452
- echo "✅ PASSED - No shell scripts to check."
783
+ echo "✅ PASSED - Shellcheck completed."
453
784
  fi
785
+ else
786
+ echo "ℹ️ shellcheck not available - Attempting basic syntax check"
787
+ # Basic check for staged shell script files
788
+ for sh_file in $SHELL_FILES; do
789
+ if [ -f "$sh_file" ]; then
790
+ bash -n "$sh_file"
791
+ if [ $? -ne 0 ]; then
792
+ echo "❌ BLOCKED - SYNTAX ERROR in $sh_file"
793
+ exit 1
794
+ fi
795
+ fi
796
+ done
797
+ echo "✅ PASSED - Basic shell script syntax check."
454
798
  fi
455
799
 
456
800
  else
@@ -484,24 +828,26 @@ else
484
828
  fi
485
829
  GATE_1_STATUS="PASS"
486
830
  GATE_1_TOOL="${PROJECT_LANG}"
831
+ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
487
832
 
488
833
  # ============================================================================
489
834
  # GATE 2: Duplicate Code Detection (NEW gate)
490
835
  # Uses jscpd, or similar tools, specific to language
491
836
  # ============================================================================
492
- 2>&1 echo ""
493
- 2>&1 echo "→ Gate 2: Duplicate code detection..."
837
+ 2>&1 echo ""
838
+ 2>&1 echo "→ Gate 2: Duplicate code detection..."
839
+ GATE_2_START=$(gate_start_ms)
494
840
 
495
841
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
496
842
  echo "✅ PASSED - Skipped (no source code to analyze)."
497
843
 
498
844
  elif [ "$PROJECT_LANG" = "typescript" ]; then
499
845
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
500
- exit
846
+ exit 1
501
847
  fi
502
848
 
503
849
  echo "Running jscpd for duplicate code detection..."
504
- jscpd --config jscpd.conf.json "$CHANGED_FILES" 2>&1 | head -30
850
+ jscpd --config jscpd.conf.json $(echo "$CHANGED_FILES" | tr '\n' ' ') 2>&1 | head -30
505
851
  JSCPD_EXIT=$?
506
852
  if [ "$JSCPD_EXIT" -ne 0 ]; then
507
853
  echo "ℹ️ jscpd found duplicated code (warning, not blocking by default)"
@@ -523,12 +869,12 @@ elif [ "$PROJECT_LANG" = "python" ]; then
523
869
  ruff check --select=DUP $(echo "$CHANGED_FILES" | grep "\.py$") 2>&1 | head -30
524
870
  echo "✅ PASSED - ruff duplicate code check completed."
525
871
  else
526
- exit
872
+ exit 1
527
873
  fi
528
874
 
529
875
  elif [ "$PROJECT_LANG" = "go" ]; then
530
876
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
531
- exit
877
+ exit 1
532
878
  fi
533
879
 
534
880
  echo "Running jscpd for Go duplicate code detection..."
@@ -550,7 +896,7 @@ else
550
896
  SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
551
897
  if [ -n "$SOURCE_FILES" ]; then
552
898
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
553
- exit
899
+ exit 1
554
900
  fi
555
901
  echo "Running jscpd for duplicate code detection..."
556
902
  jscpd $SOURCE_FILES 2>&1 | head -15
@@ -560,13 +906,15 @@ else
560
906
  fi
561
907
  fi
562
908
  GATE_2_STATUS="PASS"
909
+ record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
563
910
 
564
911
  # ============================================================================
565
912
  # GATE 3: Cyclomatic Complexity Check (Old Gate 7)
566
913
  # Uses lizard - keeps existing logic for consistency
567
914
  # ============================================================================
568
- 2>&1 echo ""
569
- 2>&1 echo "→ Gate 3: Cyclomatic complexity..."
915
+ 2>&1 echo ""
916
+ 2>&1 echo "→ Gate 3: Cyclomatic complexity..."
917
+ GATE_3_START=$(gate_start_ms)
570
918
 
571
919
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
572
920
  echo "✅ PASSED - Skipped (documentation project)."
@@ -616,19 +964,22 @@ else
616
964
  echo "✅ PASSED - No source files to check for complexity."
617
965
  fi
618
966
  else
619
- echo "ℹ️ lizard not installed - Skipping complexity check (non-blocking)"
620
- echo "ℹ️ Install with: pip3 install --user lizard"
621
- echo " PASSED - Complexity check (SKIP, tool not available)"
967
+ echo "⚠️ WARN - lizard not installed, complexity check not performed"
968
+ echo " Install with: pip install --user lizard"
969
+ echo " Gate 3: Complexity check (WARN, tool not available)"
970
+ GATE_3_STATUS="WARN"
622
971
  fi
623
972
  fi
624
973
  GATE_3_STATUS="PASS"
974
+ record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
625
975
 
626
976
  # ============================================================================
627
977
  # GATE 4: Principles Checker (Old Gate 6 - Clean Code + SOLID)
628
978
  # Reuses existing principles checker logic
629
979
  # ============================================================================
630
- 2>&1 echo ""
631
- 2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
980
+ 2>&1 echo ""
981
+ 2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
982
+ GATE_4_START=$(gate_start_ms)
632
983
 
633
984
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
634
985
  echo "✅ PASSED - Skipped (documentation project)."
@@ -659,7 +1010,7 @@ else
659
1010
  echo " - SOLID principle violations"
660
1011
  echo " - architectural violations"
661
1012
  npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format console
662
- exit
1013
+ exit 1
663
1014
  fi
664
1015
 
665
1016
  echo "✅ PASSED - Principles checker (no errors found)."
@@ -683,13 +1034,15 @@ else
683
1034
  fi
684
1035
  fi
685
1036
  GATE_4_STATUS="PASS"
1037
+ record_gate_audit "gate-4" "principles" "$GATE_4_STATUS" "${WARNING_COUNT:-0}" "$GATE_4_START"
686
1038
 
687
1039
  # ============================================================================
688
1040
  # GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
689
1041
  # Uses adapter system to run tests + coverage for language
690
1042
  # ============================================================================
691
- 2>&1 echo ""
692
- 2>&1 echo "→ Gate 5: Tests & coverage..."
1043
+ 2>&1 echo ""
1044
+ 2>&1 echo "→ Gate 5: Tests & coverage..."
1045
+ GATE_5_START=$(gate_start_ms)
693
1046
 
694
1047
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
695
1048
  echo "✅ PASSED - Skipped (documentation project)."
@@ -815,7 +1168,7 @@ else
815
1168
  # Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
816
1169
  # 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
817
1170
  # ========================================================================
818
- CHANGED_TEST_FILES=$(git diff --cached --name-only | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
1171
+ CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
819
1172
 
820
1173
  if [ -n "$CHANGED_TEST_FILES" ]; then
821
1174
  echo "Checking mock density in test files..."
@@ -827,7 +1180,8 @@ else
827
1180
  'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
828
1181
  'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
829
1182
  '\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
830
- c=$(grep -o -c "$kw" "$test_file" 2>/dev/null || echo "0")
1183
+ c=$(grep -c "$kw" "$test_file" 2>/dev/null)
1184
+ c=${c:-0}
831
1185
  MOCK_COUNT=$((MOCK_COUNT + c))
832
1186
  done
833
1187
 
@@ -873,7 +1227,7 @@ else
873
1227
  if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
874
1228
  echo ""
875
1229
  echo "❌ BLOCKED - Tests FAILED"
876
- exit
1230
+ exit 1
877
1231
  else
878
1232
  echo "✅ PASSED - Unit tests passed."
879
1233
  fi
@@ -1073,14 +1427,58 @@ else
1073
1427
  echo "ℹ️ Coverage enforcement not applicable for ${PROJECT_LANG}"
1074
1428
  fi
1075
1429
  echo "✅ PASSED - Coverage check completed."
1430
+
1431
+ # ============================================================================
1432
+ # Stage 3: Per-file coverage check for NEW files (Issue #103)
1433
+ # New files must have ≥80% coverage individually.
1434
+ # ============================================================================
1435
+ if [ "$PROJECT_LANG" = "typescript" ] && [ -f "coverage/coverage-summary.json" ]; then
1436
+ 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)
1437
+ if [ -n "$NEW_SOURCE_FILES" ]; then
1438
+ echo ""
1439
+ echo " └─ Per-file coverage check for new files:"
1440
+ for new_file in $NEW_SOURCE_FILES; do
1441
+ # Normalize path for coverage-summary.json lookup
1442
+ NORMALIZED_PATH=$(echo "$new_file" | sed 's|^\./||')
1443
+ FILE_COV=$(node -e "
1444
+ try {
1445
+ const fs = require('fs');
1446
+ const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
1447
+ const entry = data['$NORMALIZED_PATH'];
1448
+ if (entry && entry.lines) {
1449
+ console.log(Math.round(entry.lines.pct));
1450
+ } else {
1451
+ console.log('no_data');
1452
+ }
1453
+ } catch(e) { console.log('parse_error'); }
1454
+ " 2>/dev/null)
1455
+ if [ "$FILE_COV" = "no_data" ] || [ "$FILE_COV" = "parse_error" ]; then
1456
+ echo " ⚠️ $new_file — no coverage data (file may be excluded or not exercised)"
1457
+ elif [ "$FILE_COV" -lt 80 ]; then
1458
+ echo " ❌ $new_file — ${FILE_COV}% coverage (below 80% threshold)"
1459
+ NEW_FILE_COV_FAIL=true
1460
+ else
1461
+ echo " ✅ $new_file — ${FILE_COV}% coverage"
1462
+ fi
1463
+ done
1464
+ if [ "${NEW_FILE_COV_FAIL:-false}" = "true" ]; then
1465
+ echo ""
1466
+ echo "❌ BLOCKED - New file(s) below 80% coverage threshold"
1467
+ echo "Add tests to cover the new files before committing."
1468
+ exit 1
1469
+ fi
1470
+ fi
1471
+ fi
1076
1472
  fi
1077
1473
  GATE_5_STATUS="PASS"
1474
+ record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START"
1078
1475
 
1079
1476
  # ============================================================================
1080
1477
  # GATE 6: Architecture & Tech Debt (Combines Old Gate 8 - Boy Scout and Gate 9 - Architecture)
1081
1478
  # ============================================================================
1082
- 2>&1 echo ""
1083
- 2>&1 echo "→ Gate 6: Architecture & tech debt..."
1479
+ 2>&1 echo ""
1480
+ 2>&1 echo "→ Gate 6: Architecture & tech debt..."
1481
+ GATE_6_START=$(gate_start_ms)
1084
1482
 
1085
1483
  # First Part: Architecture Validation (previously Gate 9)
1086
1484
  2>&1 echo " └─ Architecture validation:"
@@ -1110,18 +1508,34 @@ else
1110
1508
  echo " Install with: npm install --save-dev @archlinter/cli"
1111
1509
  exit 1
1112
1510
  fi
1113
- echo " Running archlint for TypeScript..."
1114
- ARCHLINT_OUTPUT=$($ARCHLINT_CMD scan . -f table --quiet 2>&1)
1115
- ARCHLINT_EXIT=$?
1116
- echo "$ARCHLINT_OUTPUT" | tail -30
1117
- if [ "$ARCHLINT_EXIT" -ne 0 ]; then
1118
- echo ""
1119
- echo "❌ BLOCKED - Architecture violations detected"
1120
- echo "Fix the architecture violations above before committing."
1121
- exit 1
1122
- fi
1123
- echo " TypeScript architecture validation completed."
1124
- ;;
1511
+ echo " Running archlint for TypeScript..."
1512
+ # Use diff mode with baseline for ratchet (block only new violations)
1513
+ # Fall back to raw scan if no baseline exists (new project, zero-tolerance)
1514
+ if [ -f ".architecture-baseline.json" ]; then
1515
+ ARCHLINT_OUTPUT=$($ARCHLINT_CMD diff .architecture-baseline.json --fail-on high 2>&1)
1516
+ else
1517
+ ARCHLINT_OUTPUT=$($ARCHLINT_CMD scan . -f table --quiet 2>&1)
1518
+ fi
1519
+ ARCHLINT_EXIT=$?
1520
+ echo "$ARCHLINT_OUTPUT" | tail -30
1521
+ if [ "$ARCHLINT_EXIT" -ne 0 ]; then
1522
+ echo ""
1523
+ echo "❌ BLOCKED - Architecture violations detected"
1524
+ if [ -f ".architecture-baseline.json" ]; then
1525
+ echo "New architecture violations found (baseline ratchet mode)."
1526
+ echo "Fix the new violations above, or update the baseline with: npx @archlinter/cli snapshot -o .architecture-baseline.json"
1527
+ else
1528
+ echo "Fix the architecture violations above before committing."
1529
+ echo "Or generate a baseline for gradual adoption: npx @archlinter/cli snapshot -o .architecture-baseline.json"
1530
+ fi
1531
+ exit 1
1532
+ fi
1533
+ if [ -f ".architecture-baseline.json" ]; then
1534
+ echo " ✅ TypeScript architecture validation completed (baseline ratchet mode)."
1535
+ else
1536
+ echo " ✅ TypeScript architecture validation completed (zero-tolerance mode). no baseline found."
1537
+ fi
1538
+ ;;
1125
1539
  "python")
1126
1540
  if [ -f ".import-linter.yml" ] || [ -f "import_linter_config.yml" ]; then
1127
1541
  if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
@@ -1131,7 +1545,7 @@ else
1131
1545
  else
1132
1546
  echo " ❌ BLOCKED - import-linter not installed"
1133
1547
  echo " Install with: pip install import-linter"
1134
- exit
1548
+ exit 1
1135
1549
  fi
1136
1550
  else
1137
1551
  echo " ℹ️ No .import-linter.yml found - skipping Python architecture"
@@ -1147,7 +1561,7 @@ else
1147
1561
  else
1148
1562
  echo " ❌ BLOCKED - arch-go not installed"
1149
1563
  echo " Install with: go install github.com/arch-go/arch-go@latest"
1150
- exit
1564
+ exit 1
1151
1565
  fi
1152
1566
  else
1153
1567
  echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
@@ -1183,10 +1597,11 @@ else
1183
1597
  else
1184
1598
  echo ""
1185
1599
  echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
1186
- echo " Required configuration file 'architecture.yaml' is NOT found."
1600
+ echo " Required configuration file (.archlint.yaml / architecture.yaml) is NOT found."
1187
1601
  echo " Architecture Quality Gate requires explicit architecture constraints."
1188
1602
  echo ""
1189
- echo " Create architecture.yaml with:"
1603
+ echo " Create .archlint.yaml with: npx archlint init --no-interactive"
1604
+ echo " Or architecture.yaml for custom Clean Architecture layer definitions:"
1190
1605
  echo " layers:"
1191
1606
  echo " - name: api"
1192
1607
  echo " paths: [\"src/api/**\"]"
@@ -1199,7 +1614,7 @@ else
1199
1614
  echo ""
1200
1615
  echo " After creating architecture.yaml, retry the commit."
1201
1616
  echo " (To skip with warning, create .architecture-skip file)"
1202
- exit
1617
+ exit 1
1203
1618
  fi
1204
1619
  fi
1205
1620
  fi
@@ -1243,7 +1658,7 @@ else
1243
1658
  echo " - NEW files: must have zero warnings"
1244
1659
  echo " - MODIFIED files: cannot increase warnings from baseline"
1245
1660
  echo " - Files with ≤5 warnings: must clear to zero"
1246
- exit
1661
+ exit 1
1247
1662
  fi
1248
1663
 
1249
1664
  # Check output for violations
@@ -1253,7 +1668,7 @@ else
1253
1668
  echo "$BOY_SCOUT_OUTPUT"
1254
1669
  echo ""
1255
1670
  echo "❌ BLOCKED - Boy Scout Rule violations detected ($VIOLATION_COUNT)"
1256
- exit
1671
+ exit 1
1257
1672
  fi
1258
1673
 
1259
1674
  echo " ✅ PASSED - Boy Scout Rule compliance."
@@ -1269,6 +1684,7 @@ else
1269
1684
  fi
1270
1685
  fi
1271
1686
  GATE_6_STATUS="PASS"
1687
+ record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCKED:-0}" "$GATE_6_START"
1272
1688
 
1273
1689
  # ============================================================================
1274
1690
 
@@ -1277,8 +1693,9 @@ GATE_6_STATUS="PASS"
1277
1693
  # Detects security issues in Infrastructure as Code files
1278
1694
  # Tools: checkov (recommended), hadolint, kube-score, tflint
1279
1695
  # ============================================================================
1280
- 2>&1 echo ""
1281
- 2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
1696
+ 2>&1 echo ""
1697
+ 2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
1698
+ GATE_7_START=$(gate_start_ms)
1282
1699
 
1283
1700
  # Check if any IaC files are changed
1284
1701
  IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
@@ -1306,7 +1723,7 @@ else
1306
1723
  echo "Fix the security issues above before committing."
1307
1724
  echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
1308
1725
  GATE_7_STATUS="FAIL"
1309
- exit
1726
+ exit 1
1310
1727
  fi
1311
1728
  else
1312
1729
  echo "ℹ️ SKIP - IaC adapter not found"
@@ -1314,6 +1731,7 @@ else
1314
1731
  GATE_7_STATUS="SKIP"
1315
1732
  fi
1316
1733
  fi
1734
+ record_gate_audit "gate-7" "iac-security" "$GATE_7_STATUS" "0" "$GATE_7_START"
1317
1735
 
1318
1736
 
1319
1737
  # GATE 8: Secret Scanning (gitleaks)
@@ -1321,8 +1739,9 @@ fi
1321
1739
  # Tool: gitleaks -- https://github.com/gitleaks/gitleaks
1322
1740
  # ============================================================================
1323
1741
 
1324
- 2>&1 echo ""
1325
- 2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
1742
+ 2>&1 echo ""
1743
+ 2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
1744
+ GATE_8_START=$(gate_start_ms)
1326
1745
 
1327
1746
  # Gitleaks availability check
1328
1747
  GITLEAKS_CMD=""
@@ -1357,7 +1776,7 @@ if [ -n "$GITLEAKS_CMD" ]; then
1357
1776
  echo " 3. Use git secret or vault for sensitive data"
1358
1777
  echo ""
1359
1778
  echo "See: https://github.com/gitleaks/gitleaks"
1360
- exit
1779
+ exit 1
1361
1780
  else
1362
1781
  echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
1363
1782
  echo " ✅ Secret Scanning (SKIP, gitleaks error)"
@@ -1369,6 +1788,7 @@ else
1369
1788
  echo " ✅ Secret Scanning (SKIP, gitleaks not installed)"
1370
1789
  GATE_8_STATUS="SKIP"
1371
1790
  fi
1791
+ record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
1372
1792
 
1373
1793
  # Switch back to original directory if we were in a subdirectory
1374
1794
  if [ -n "$ORIGINAL_DIR" ]; then
@@ -1381,8 +1801,9 @@ fi
1381
1801
  # Tool: semgrep -- https://semgrep.dev
1382
1802
  # ============================================================================
1383
1803
 
1384
- 2>&1 echo ""
1385
- 2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
1804
+ 2>&1 echo ""
1805
+ 2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
1806
+ GATE_9_START=$(gate_start_ms)
1386
1807
 
1387
1808
  GATE_9_STATUS="PASS"
1388
1809
 
@@ -1395,11 +1816,11 @@ elif [ -f "$HOME/.local/bin/semgrep" ]; then
1395
1816
  fi
1396
1817
 
1397
1818
  if [ -z "$SEMGREP_CMD" ]; then
1398
- echo " ℹ️ semgrep not installed — SAST scanning unavailable"
1819
+ echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
1399
1820
  echo " Install: brew install semgrep | pip install semgrep"
1400
1821
  echo " Pre-cache rules: semgrep --config=p/security-audit"
1401
- echo " Semgrep SAST (SKIP, semgrep not installed)"
1402
- GATE_9_STATUS="SKIP"
1822
+ echo " Gate 9: SAST Security (WARN, semgrep not installed)"
1823
+ GATE_9_STATUS="WARN"
1403
1824
  else
1404
1825
  # Get staged files filtered to Semgrep-supported languages
1405
1826
  SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
@@ -1485,7 +1906,7 @@ except:
1485
1906
  echo "$FINDING_DETAILS"
1486
1907
  echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
1487
1908
  GATE_9_STATUS="FAIL"
1488
- exit
1909
+ exit 1
1489
1910
  else
1490
1911
  echo ""
1491
1912
  echo " ✅ PASSED - No critical/high vulnerabilities"
@@ -1503,9 +1924,7 @@ except:
1503
1924
  fi
1504
1925
  fi
1505
1926
  fi
1506
-
1507
- # ============================================================================
1508
- # QUALITY REPORT GENERATION
1927
+ record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
1509
1928
  # ============================================================================
1510
1929
 
1511
1930
  generate_quality_report() {
@@ -1522,7 +1941,7 @@ generate_quality_report() {
1522
1941
 
1523
1942
  for gate in 1 2 3 4 5 6 7 8 9; do
1524
1943
  local status_var="GATE_${gate}_STATUS"
1525
- if [ "${!status_var}" = "PASS" ] || [ "${!status_var}" = "SKIP" ]; then
1944
+ if [ "${!status_var}" = "PASS" ]; then
1526
1945
  PASSED_COUNT=$((PASSED_COUNT + 1))
1527
1946
  fi
1528
1947
  done
@@ -1628,6 +2047,31 @@ generate_quality_report() {
1628
2047
  }
1629
2048
  ENDJSON
1630
2049
 
2050
+ # ── Persistent per-run report (Issue #101) ────────────────────────────────────────
2051
+ local REPORTS_DIR=".xp-gate/reports/pre-commit"
2052
+ local REPORT_TS
2053
+ REPORT_TS=$(date -u +"%Y-%m-%d-%H%M%S")
2054
+ local PERSISTED_REPORT_FILE="${REPORTS_DIR}/${REPORT_TS}.json"
2055
+ mkdir -p "$REPORTS_DIR"
2056
+ python3 - <<PY 2>/dev/null || cp "$REPORT_FILE" "$PERSISTED_REPORT_FILE"
2057
+ import json
2058
+ from pathlib import Path
2059
+ branch_report = json.loads(Path('$REPORT_FILE').read_text())
2060
+ changed_files = '''${CHANGED_FILES:-}'''.splitlines()
2061
+ report = {
2062
+ 'timestamp': '$TIMESTAMP',
2063
+ 'trigger': 'pre-commit',
2064
+ 'branch': '$BRANCH',
2065
+ 'commit': '$COMMIT_HASH',
2066
+ 'changed_files': [f for f in changed_files if f],
2067
+ 'overall': branch_report.get('overall', {}),
2068
+ 'gates': branch_report.get('gates', {}),
2069
+ 'warnings': [],
2070
+ 'errors': [],
2071
+ }
2072
+ Path('$PERSISTED_REPORT_FILE').write_text(json.dumps(report, indent=2) + '\n')
2073
+ PY
2074
+
1631
2075
  # ── History append (unchanged — append-only for trend) ────────────────────────────
1632
2076
  local GATES_JSON="{"
1633
2077
  GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-PASS}\",\"name\":\"Code Quality\"},"
@@ -1661,6 +2105,7 @@ ENDJSON
1661
2105
  echo ""
1662
2106
  echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
1663
2107
  echo " Branch status: $REPORT_FILE"
2108
+ echo " Run report: $PERSISTED_REPORT_FILE"
1664
2109
  echo " History saved: $HISTORY_FILE"
1665
2110
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1666
2111
  echo ""
@@ -1668,4 +2113,4 @@ ENDJSON
1668
2113
 
1669
2114
  generate_quality_report
1670
2115
 
1671
- exit
2116
+ exit 0