@boyingliu01/xp-gate 0.35.23 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/adapter-common.sh +78 -36
  2. package/adapters/csharp.sh +245 -0
  3. package/adapters/gate-3.sh +1 -1
  4. package/adapters/gate-4.sh +1 -1
  5. package/adapters/gate-9.sh +37 -2
  6. package/adapters/go.sh +6 -11
  7. package/adapters/iac.sh +45 -5
  8. package/adapters/python.sh +6 -11
  9. package/bin/xp-gate.js +18 -11
  10. package/gate-10.sh +18 -3
  11. package/gate-13-compiler-strictness.sh +181 -0
  12. package/gate-3.sh +1 -1
  13. package/gate-4.sh +1 -1
  14. package/gate-9.sh +37 -2
  15. package/gates/common.ts +9 -2
  16. package/hooks/adapter-common.sh +78 -36
  17. package/hooks/gate-10.sh +18 -3
  18. package/hooks/gate-13-compiler-strictness.sh +181 -0
  19. package/hooks/gate-3.sh +1 -1
  20. package/hooks/gate-4.sh +1 -1
  21. package/hooks/gate-9.sh +37 -2
  22. package/hooks/gate-registry.sh +161 -0
  23. package/hooks/lib/test-failure.sh +29 -4
  24. package/hooks/pre-commit +122 -3
  25. package/lib/__tests__/agent-runtime-journal.test.js +42 -18
  26. package/lib/__tests__/check-single-file.test.js +55 -4
  27. package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
  28. package/lib/__tests__/doctor-delphi.test.js +178 -3
  29. package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
  30. package/lib/__tests__/install-skill-full-dir.test.js +45 -2
  31. package/lib/__tests__/install-skill.test.js +191 -23
  32. package/lib/__tests__/mirror-sync.test.js +23 -5
  33. package/lib/__tests__/uninstall-global-residue.test.js +394 -9
  34. package/lib/__tests__/update-skill-flags.test.js +78 -0
  35. package/lib/__tests__/update-skill.test.js +334 -57
  36. package/lib/detect-deps.js +24 -0
  37. package/lib/doctor.js +80 -12
  38. package/lib/install-skill.js +85 -21
  39. package/lib/mattpocock-license-verdict.json +1 -1
  40. package/lib/uninstall.js +143 -12
  41. package/lib/update-skill.js +239 -28
  42. package/mock-policy/AGENTS.md +4 -4
  43. package/mutation/AGENTS.md +4 -4
  44. package/package.json +1 -1
  45. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  47. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  48. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  49. package/plugins/dsh/lib/index.d.ts.map +1 -1
  50. package/plugins/dsh/lib/index.js +7 -4
  51. package/plugins/dsh/package.json +1 -1
  52. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  53. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  54. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  55. package/plugins/dsh/src/index.test.ts +25 -0
  56. package/plugins/dsh/src/index.ts +7 -4
  57. package/plugins/opencode/package.json +1 -1
  58. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  59. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  60. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  61. package/plugins/qoder/plugin.json +1 -1
  62. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  63. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  64. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  65. package/principles/AGENTS.md +4 -4
  66. package/principles/adapters/csharp.ts +309 -0
  67. package/principles/analyzer.ts +5 -0
  68. package/scripts/delphi-external-review.cjs +130 -101
  69. package/skills/delphi-review/AGENTS.md +4 -4
  70. package/skills/sprint-flow/AGENTS.md +4 -4
  71. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -1180,3 +1180,184 @@ EOF
1180
1180
  # stage for no benefit.
1181
1181
  return "$GATE13_PASS"
1182
1182
  }
1183
+
1184
+ # ============================================================================
1185
+ # C# / .NET PATH (#717 / REQ-005)
1186
+ #
1187
+ # Measured motivation, same shape as the C/C++ and Java paths above: `dotnet
1188
+ # build` on a solution with default settings reports NOTHING for a converted
1189
+ # nullable warning or an obsolete API, because MSBuild's default
1190
+ # WarningLevel/TreatWarningsAsErrors is whatever the project happens to declare.
1191
+ # `-warnaserror -p:Nullable=enable` turns the same source into hard errors --
1192
+ # which is exactly what a *compiler strictness* gate is for, and what no other
1193
+ # gate in the chain does (Gate 1 runs `dotnet format`, which formats and runs
1194
+ # analyzers but does not fail on the full compiler warning set).
1195
+ #
1196
+ # Verdict codes follow the file-level contract: 0 PASS / 1 FAIL / 2 BLOCK (tool
1197
+ # ran but output untrustworthy) / 3 WARN (tool absent, timeout, or warn-only).
1198
+ #
1199
+ # Deliberately driven by the adapter's own toolchain discovery rather than a
1200
+ # second copy of it: two independent "where is dotnet" implementations is how
1201
+ # detection and gates drift apart.
1202
+ # ============================================================================
1203
+
1204
+ # Locate the .NET SDK exactly as adapters/csharp.sh does.
1205
+ gate13_csharp_dotnet() {
1206
+ local d
1207
+ d="${DOTNET_BIN:-}"
1208
+ if [ -z "$d" ]; then
1209
+ d=$(command -v dotnet 2>/dev/null) || d=""
1210
+ fi
1211
+ [ -n "$d" ] || return 1
1212
+ "$d" --version >/dev/null 2>&1 || return 1
1213
+ printf '%s\n' "$d"
1214
+ }
1215
+
1216
+ # Print the staged .cs files (not the whole build output) that Gate 13 judges.
1217
+ gate13_csharp_sources() {
1218
+ git diff --cached --name-only --diff-filter=d -- '*.cs' 2>/dev/null
1219
+ }
1220
+
1221
+ # Print the .NET entry point to build: a solution, else a project file.
1222
+ #
1223
+ # Bare `dotnet build` in the repo root fails with MSB1003 whenever the project
1224
+ # lives in a subdirectory -- the normal layout (src/App/App.csproj) and the one
1225
+ # every fixture here uses. Measured: bare build produced no diagnostics at all
1226
+ # while `dotnet build P/P.csproj` produced the two real warnings. Resolving the
1227
+ # entry point explicitly is what makes the gate see anything.
1228
+ gate13_csharp_entry_point() {
1229
+ local f
1230
+ # A .sln is the widest scope and builds every project in it.
1231
+ f=$(git ls-files '*.sln' 2>/dev/null | head -1)
1232
+ if [ -n "$f" ]; then printf '%s
1233
+ ' "$f"; return 0; fi
1234
+ f=$(git ls-files '*.??proj' 2>/dev/null | head -1)
1235
+ if [ -n "$f" ]; then printf '%s
1236
+ ' "$f"; return 0; fi
1237
+ # Fall back to the working tree for a not-yet-tracked project.
1238
+ for f in *.sln *.??proj; do
1239
+ [ -f "$f" ] && { printf '%s
1240
+ ' "$f"; return 0; }
1241
+ done
1242
+ return 1
1243
+ }
1244
+
1245
+ # Is there a buildable .NET entry point to compile at all?
1246
+ gate13_csharp_has_manifest() {
1247
+ local f
1248
+ for f in *.sln *.csproj Directory.Build.props global.json; do
1249
+ [ -f "$f" ] && return 0
1250
+ done
1251
+ # A nested layout is normal (src/App/App.csproj).
1252
+ [ -n "$(git ls-files '*.*proj' '*.sln' 2>/dev/null | head -1)" ] && return 0
1253
+ return 1
1254
+ }
1255
+
1256
+ # Count compiler warnings FROM CHANGED FILES only.
1257
+ #
1258
+ # `dotnet build` reports diagnostics for the whole project, so counting every
1259
+ # warning would make a one-line change inherit the project's entire back-catalog
1260
+ # and block on work the author never touched -- the false-BLOCK that Gate 13's
1261
+ # baseline mechanism exists to prevent. Only lines naming a file this commit
1262
+ # stages are counted; everything else is baseline territory.
1263
+ gate13_csharp_changed_warning_count() {
1264
+ local dotnet="$1" out entry stage_files
1265
+ stage_files=$(git diff --cached --name-only --diff-filter=d 2>/dev/null)
1266
+ [ -n "$stage_files" ] || return 1
1267
+
1268
+ # -clp:NoSummary keeps the summary table out of the parse; --nologo drops the
1269
+ # banner.
1270
+ #
1271
+ # --no-incremental is REQUIRED, not an optimisation tradeoff. MSBuild skips
1272
+ # compilation when its outputs are newer than the sources, and a skipped
1273
+ # compile emits no diagnostics -- measured: the same tree reported 2 warnings
1274
+ # from a clean build and 0 from an immediately following incremental one. A
1275
+ # strictness gate that only sees warnings the first time is a gate that
1276
+ # silently passes forever after, which is precisely the failure mode #717
1277
+ # exists to remove.
1278
+ #
1279
+ # The entry point is passed explicitly: bare `dotnet build` in the repo root
1280
+ # dies with MSB1003 whenever the project sits in a subdirectory, emitting no
1281
+ # diagnostics at all.
1282
+ entry=$(gate13_csharp_entry_point) || return 1
1283
+ out=$("$dotnet" build "$entry" --nologo --no-incremental -clp:NoSummary 2>&1)
1284
+
1285
+ # Report distinct CHANGED FILES, not diagnostic instances: the caller speaks in
1286
+ # files ("N changed C# file(s) carry compiler warnings"), and one file with two
1287
+ # warnings is one file to fix.
1288
+ #
1289
+ # Two normalisations are needed and both were measured, not assumed:
1290
+ # - dotnet prints ABSOLUTE Windows paths while git reports repo-relative
1291
+ # ones, so a suffix match is what relates them;
1292
+ # - the separators differ, so the diagnostic path is folded to "/" first.
1293
+ # Comparing raw strings matched nothing and reported 0 warnings on a file that
1294
+ # has two.
1295
+ printf '%s\n' "$out" \
1296
+ | grep -E ': (warning|error) [A-Z]+[0-9]+' \
1297
+ | sed -n 's/^\(.*\)(.*$/\1/p' \
1298
+ | tr '\\' '/' \
1299
+ | sort -u \
1300
+ | while IFS= read -r f; do
1301
+ [ -n "$f" ] || continue
1302
+ while IFS= read -r s; do
1303
+ [ -n "$s" ] || continue
1304
+ case "$f" in
1305
+ *"$s") printf '%s\n' "$s"; break ;;
1306
+ esac
1307
+ done <<EOF3
1308
+ $stage_files
1309
+ EOF3
1310
+ done \
1311
+ | sort -u | wc -l | tr -d ' '
1312
+ }
1313
+
1314
+ gate13_run_csharp_analysis() {
1315
+ local dotnet sources tier changed_count base
1316
+
1317
+ # gate13_is_disabled takes no argument: it reads the single repo-wide
1318
+ # XP_GATE_DISABLE_13 switch, which disables every language path together.
1319
+ if gate13_is_disabled; then
1320
+ gate13_reason "compiler strictness disabled (XP_GATE_DISABLE_13=1)"
1321
+ return "$GATE13_PASS"
1322
+ fi
1323
+
1324
+ sources=$(gate13_csharp_sources)
1325
+ if [ -z "$sources" ]; then
1326
+ return "$GATE13_PASS"
1327
+ fi
1328
+
1329
+ if ! gate13_csharp_has_manifest; then
1330
+ # .cs files staged with no project to compile them into. Not a pass -- the
1331
+ # gate genuinely could not judge them -- but not a block either.
1332
+ gate13_reason "no .sln/.csproj found; C# sources not compiled"
1333
+ echo " ⏭️ SKIPPED - no .NET project to compile (.cs staged without a manifest)"
1334
+ return "$GATE13_WARN"
1335
+ fi
1336
+
1337
+ dotnet=$(gate13_csharp_dotnet) || {
1338
+ gate13_reason "dotnet SDK not found"
1339
+ echo " ⏭️ SKIPPED - dotnet SDK not installed"
1340
+ return "$GATE13_WARN"
1341
+ }
1342
+
1343
+ tier=$(gate13_resolve_tier)
1344
+ changed_count=$(gate13_csharp_changed_warning_count "$dotnet") || changed_count=0
1345
+ changed_count=${changed_count:-0}
1346
+
1347
+ if [ "$changed_count" -eq 0 ]; then
1348
+ echo " ✅ PASSED - C# compiler strictness (no warnings in changed files)"
1349
+ return "$GATE13_PASS"
1350
+ fi
1351
+
1352
+ echo " ℹ️ ${changed_count} changed C# file(s) carry compiler warnings"
1353
+
1354
+ if [ "$tier" = "warn-only" ]; then
1355
+ echo " ⚠️ WARN - ${changed_count} C# file(s) with compiler warnings (warn-only tier)"
1356
+ return "$GATE13_WARN"
1357
+ fi
1358
+
1359
+ echo " ❌ BLOCKED - ${changed_count} changed C# file(s) with compiler warnings"
1360
+ echo " Turn the warnings into zero, or record a baseline:"
1361
+ echo " xp-gate compiler-baseline create"
1362
+ return "$GATE13_FAIL"
1363
+ }
package/hooks/gate-3.sh CHANGED
@@ -363,7 +363,7 @@ else
363
363
  LIZARD_PATH=$(eval echo "$LIZARD_CMD")
364
364
 
365
365
  # Get changed source files for complexity check (includes test files)
366
- CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
366
+ CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt|cs)$' || true)
367
367
 
368
368
  # Grade exactly the population the baseline was written from. The two must
369
369
  # match or the ratchet reports pre-existing code as "new", which is a false
package/hooks/gate-4.sh CHANGED
@@ -76,7 +76,7 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
76
76
 
77
77
  else
78
78
  # Get source files to check against principles
79
- PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
79
+ PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm|cs)$' || true)
80
80
 
81
81
  # One path per line, into an array. Passing $PRINCIPLES_FILES unquoted relies on
82
82
  # word splitting to turn the list into separate argv entries, which also splits
package/hooks/gate-9.sh CHANGED
@@ -1,3 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ⚠️ DEAD CODE — NOT EXECUTED. Read this before changing anything here.
4
+ # ============================================================================
5
+ # This file is NOT sourced by githooks/pre-commit or githooks/pre-push (both
6
+ # source count = 0; verify with: grep -c "gate-9.sh" githooks/pre-commit).
7
+ # It is shipped by githooks/install.sh but never runs.
8
+ #
9
+ # The gate that actually runs semgrep SAST is githooks/gate-10.sh, and it
10
+ # reports as `gate10_sast`. Gate 9 in the report is `gate9_build_integrity`
11
+ # (Build Integrity), a different gate entirely.
12
+ #
13
+ # This mislabelling is not cosmetic: in #717 a C# SAST fix was first written
14
+ # into THIS file, where it changed nothing, while .cs stayed ungoverned. The
15
+ # C# ruleset loading that works lives in gate-10.sh. Do not relocate it here.
16
+ #
17
+ # Kept rather than deleted because githooks/install.sh:GATE_SCRIPTS ships it;
18
+ # removing it is a separate change. See issue #717 walkthrough MC-03.
19
+ # ============================================================================
1
20
  # ============================================================================
2
21
  # GATE 9: Semgrep SAST Security Scan
3
22
  # Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
@@ -112,8 +131,22 @@ EOF
112
131
  # code is demoted to "did the tool itself run". --error stays OFF: it would
113
132
  # add nothing the JSON does not already provide, and keeping it off leaves
114
133
  # failure exit codes unambiguous for the #475 diagnostics path.
134
+ # #717: p/security-audit carries NO C# rules. Measured: a .cs file with a
135
+ # textbook SQL injection (string-concatenated SqlCommand) and a
136
+ # Process.Start command injection produced 0 findings under
137
+ # p/security-audit and 1 ERROR under p/csharp. Since gate-9.sh:45 lists
138
+ # `cs` in the file filter, .cs was accepted and then scanned by a ruleset
139
+ # that cannot see C# -- the identical "filter looks like coverage" defect
140
+ # this issue removed from Gate 4. p/csharp is loaded IN ADDITION to
141
+ # p/security-audit (not instead of: the security-audit rules still apply
142
+ # to the other languages in a mixed commit).
143
+ SEMGREP_RULES=(--config=p/security-audit)
144
+ if printf "%s
145
+ " "$SEMGREP_FILES" | grep -qE ".cs$"; then
146
+ SEMGREP_RULES+=(--config=p/csharp)
147
+ fi
115
148
  SEMGREP_STDOUT_FILE=$(mktemp)
116
- SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
149
+ SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan "${SEMGREP_RULES[@]}" --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
117
150
  SEMGREP_EXIT=$?
118
151
 
119
152
  # One node pass over the JSON: first line = verdict counts, remaining lines =
@@ -191,7 +224,9 @@ console.log("");});
191
224
  echo " ❌ BLOCKED — blocking-severity vulnerability found"
192
225
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
193
226
  echo "$FINDING_DETAILS"
194
- echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
227
+ # #717: name BOTH rulesets. A C# finding comes from p/csharp, so
228
+ # pointing the author at p/security-audit alone reproduces zero hits.
229
+ echo " Run 'semgrep scan --config=p/security-audit --config=p/csharp' to review all findings."
195
230
  GATE_9_STATUS="FAIL"
196
231
  exit 1
197
232
  else
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # GATE REGISTRY — the single source of truth for "which gate is which"
4
+ # ============================================================================
5
+ #
6
+ # WHY THIS FILE EXISTS
7
+ #
8
+ # Gate numbers alone are not self-describing, and the numbers are not stable
9
+ # facts about the repository — they are labels. Three sources disagreed about
10
+ # what "Gate 9" is:
11
+ #
12
+ # root AGENTS.md -> Semgrep SAST (WRONG)
13
+ # githooks/AGENTS.md -> Build Integrity (right)
14
+ # pre-commit actual -> Build Integrity (right)
15
+ #
16
+ # In issue #717 a C# SAST fix was written into githooks/gate-9.sh, because the
17
+ # root document said Gate 9 was SAST. SAST is gate 10. gate-9.sh is not sourced
18
+ # by any hook and never executes, so the fix changed nothing while appearing to
19
+ # land. The defect was not "the number was hard to remember" — it was that
20
+ # nothing connected a gate's NAME to the FILE that runs it.
21
+ #
22
+ # This registry makes that connection explicit and testable. The drift test
23
+ # (githooks/__tests__/gate-registry.test.bats) asserts every `impl` below
24
+ # against the code, so a document that disagrees with reality, or a fix that
25
+ # lands in a file nobody sources, fails immediately.
26
+ #
27
+ # COLUMNS
28
+ # name stable semantic id — use this in prose, commits, and tests
29
+ # number display label only. Mirrors the report key (gateN_*) for backward
30
+ # compatibility; do NOT treat it as the identity of the gate.
31
+ # impl where the gate actually runs. One of:
32
+ # <script>.sh a file sourced/executed by pre-commit
33
+ # inline implemented inside pre-commit itself
34
+ # blocks YES the gate can reject a commit
35
+ # NO the gate reports but cannot reject
36
+ #
37
+ # USAGE
38
+ # source githooks/gate-registry.sh
39
+ # gate_name_for_file gate-10.sh # -> sast
40
+ # gate_impl sast # -> gate-10.sh
41
+ # gate_number sast # -> 10
42
+ # gate_can_block sast # -> YES
43
+ # gate_registry_dump # -> one line per gate
44
+ #
45
+ # ⚠️ Adding a gate: add a row here AND update the drift test's expectations.
46
+ # A gate that exists only in pre-commit and not here is a gate nobody can
47
+ # look up by name — which is how #717's wrong-file patch happened.
48
+ # ============================================================================
49
+
50
+ # name|number|impl|blocks
51
+ GATE_REGISTRY_ROWS=(
52
+ "version-consistency|0|inline|YES"
53
+ "code-quality|1|inline|NO"
54
+ "duplicate-code|2|inline|NO"
55
+ "complexity|3|gate-3.sh|YES"
56
+ "principles|4|gate-4.sh|YES"
57
+ "tests-coverage|5|inline|YES"
58
+ "architecture|6|inline|YES"
59
+ "iac-security|7|gate-7.sh|YES"
60
+ "secret-scanning|8|gate-8.sh|YES"
61
+ "build-integrity|9|inline|NO"
62
+ "sast|10|gate-10.sh|YES"
63
+ "sprint-flow|11|sprint-gate.sh|YES"
64
+ "file-hygiene|12|gate-12-file-hygiene.sh|NO"
65
+ "compiler-strictness|13|gate-13-compiler-strictness.sh|YES"
66
+ )
67
+
68
+ # Human-readable title for each name. Kept separate from the row above so the
69
+ # title can change without touching the lookup keys.
70
+ _gate_title() {
71
+ case "$1" in
72
+ version-consistency) echo "Version Consistency" ;;
73
+ code-quality) echo "Code Quality (Static + Lint)" ;;
74
+ duplicate-code) echo "Duplicate Code" ;;
75
+ complexity) echo "Cyclomatic Complexity" ;;
76
+ principles) echo "Clean Code + SOLID" ;;
77
+ tests-coverage) echo "Tests + Coverage" ;;
78
+ architecture) echo "Architecture + Boy Scout Rule" ;;
79
+ iac-security) echo "IaC Security Scanning" ;;
80
+ secret-scanning) echo "Secret Scanning" ;;
81
+ build-integrity) echo "Build Integrity" ;;
82
+ sast) echo "SAST Security Scan" ;;
83
+ sprint-flow) echo "Sprint Flow Enforcement" ;;
84
+ file-hygiene) echo "File Hygiene Check" ;;
85
+ compiler-strictness) echo "Compiler Strictness" ;;
86
+ *) return 1 ;;
87
+ esac
88
+ }
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Lookups. Each returns non-zero when the name is unknown, so a typo fails
92
+ # loudly instead of silently matching nothing.
93
+ # ---------------------------------------------------------------------------
94
+
95
+ # gate_row <name> -> echoes the raw row, or returns 1.
96
+ gate_row() {
97
+ local want="$1" row
98
+ for row in "${GATE_REGISTRY_ROWS[@]}"; do
99
+ case "$row" in
100
+ "$want"\|*) printf '%s\n' "$row"; return 0 ;;
101
+ esac
102
+ done
103
+ return 1
104
+ }
105
+
106
+ # _gate_field <name> <1-based field index>
107
+ _gate_field() {
108
+ local row
109
+ row=$(gate_row "$1") || return 1
110
+ printf '%s\n' "$row" | cut -d'|' -f"$2"
111
+ }
112
+
113
+ gate_number() { _gate_field "$1" 2; }
114
+ gate_impl() { _gate_field "$1" 3; }
115
+ gate_blocks() { _gate_field "$1" 4; }
116
+ gate_can_block() { [ "$(gate_blocks "$1")" = "YES" ] && echo YES || echo NO; }
117
+ gate_title() { _gate_title "$1"; }
118
+
119
+ # gate_name_for_number <N> -> echoes the semantic name.
120
+ gate_name_for_number() {
121
+ local want="$1" row
122
+ for row in "${GATE_REGISTRY_ROWS[@]}"; do
123
+ case "$row" in
124
+ *\|"$want"\|*) printf '%s\n' "${row%%|*}"; return 0 ;;
125
+ esac
126
+ done
127
+ return 1
128
+ }
129
+
130
+ # gate_name_for_file <file> -> semantic name of the gate implemented by <file>.
131
+ # Returns 1 for `inline` gates and unknown files. This is the lookup that would
132
+ # have caught #717: gate-9.sh resolves to build-integrity, not sast.
133
+ gate_name_for_file() {
134
+ local want="$1" row impl name
135
+ for row in "${GATE_REGISTRY_ROWS[@]}"; do
136
+ name="${row%%|*}"
137
+ impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
138
+ [ "$impl" = "$want" ] && { printf '%s\n' "$name"; return 0; }
139
+ done
140
+ return 1
141
+ }
142
+
143
+ # List every script the registry claims the hook runs.
144
+ gate_all_impls() {
145
+ local row impl
146
+ for row in "${GATE_REGISTRY_ROWS[@]}"; do
147
+ impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
148
+ [ "$impl" != "inline" ] && printf '%s\n' "$impl"
149
+ done
150
+ }
151
+
152
+ gate_registry_dump() {
153
+ local row name num impl blocks
154
+ for row in "${GATE_REGISTRY_ROWS[@]}"; do
155
+ name="${row%%|*}"
156
+ num=$(printf '%s\n' "$row" | cut -d'|' -f2)
157
+ impl=$(printf '%s\n' "$row" | cut -d'|' -f3)
158
+ blocks=$(printf '%s\n' "$row" | cut -d'|' -f4)
159
+ printf '%-3s %-22s %-34s %-6s %s\n' "$num" "$name" "$(_gate_title "$name")" "$blocks" "$impl"
160
+ done
161
+ }
@@ -31,6 +31,20 @@ has_real_failure_marker() {
31
31
  is_runner_infrastructure_error() {
32
32
  local _out="$1"
33
33
  [ -n "$_out" ] || return 1
34
+ # #717: dotnet/VSTest's testhost failing to START is the same class of event
35
+ # -- the runner could not run, so no test was judged. Its signature is a
36
+ # Win32Exception from ProcessManager.OpenProcess (the testhost cannot open a
37
+ # handle to its own parent) followed by "Testhost process exited with error".
38
+ # Without this arm, a .NET repo on a machine with that permission fault would
39
+ # report "Tests FAILED" on every commit for a reason that has nothing to do
40
+ # with its tests.
41
+ #
42
+ # A real dotnet failure ("Failed!", "Failed: 3") never carries these strings,
43
+ # so the has_real_failure_marker guard below still lets genuine failures block.
44
+ if printf '%s' "$_out" | grep -qE 'Win32Exception|Testhost.*(exited with error|已退出)|test host.*(could not|did not) start'; then
45
+ has_real_failure_marker "$_out" && return 1
46
+ return 0
47
+ fi
34
48
  printf '%s' "$_out" | grep -qE 'Unhandled Error' || return 1
35
49
  has_real_failure_marker "$_out" && return 1
36
50
  return 0
@@ -116,10 +130,21 @@ handle_test_failure() {
116
130
  local _ctx="$2"
117
131
  if is_runner_infrastructure_error "$_out"; then
118
132
  echo ""
119
- echo "⏭️ SKIPPED - vitest runner infrastructure error (no test failed)"
120
- echo " Detected: Unhandled Errors with 0 failed tests."
121
- echo " Known Windows issue: EPERM on vitest temp/ssr cache (#454)."
122
- echo " Workaround: npx vitest run --no-file-parallelism <targets>"
133
+ # The advice has to match the runner that failed (#717). A C# developer
134
+ # handed the vitest EPERM workaround would chase a cache problem in a
135
+ # language they are not using, and the real cause -- the testhost could not
136
+ # open a handle to its own parent -- would never be named.
137
+ if printf '%s' "$_out" | grep -qE 'Win32Exception|Testhost.*(exited with error|已退出)|test host.*(could not|did not) start'; then
138
+ echo "⏭️ SKIPPED - dotnet test host could not start (no test was judged)"
139
+ echo " Detected: VSTest testhost exited before running a test."
140
+ echo " This is an environment fault, NOT a test verdict -- it must not block."
141
+ echo " Check: antivirus / endpoint protection blocking process handles, or run the suite outside this path."
142
+ else
143
+ echo "⏭️ SKIPPED - vitest runner infrastructure error (no test failed)"
144
+ echo " Detected: Unhandled Errors with 0 failed tests."
145
+ echo " Known Windows issue: EPERM on vitest temp/ssr cache (#454)."
146
+ echo " Workaround: npx vitest run --no-file-parallelism <targets>"
147
+ fi
123
148
  TESTS_SKIPPED=true
124
149
  TESTS_EXIT_CODE=0
125
150
  return 0
package/hooks/pre-commit CHANGED
@@ -271,6 +271,17 @@ if [ -z "$GATE_DIR" ]; then
271
271
  GATE_DIR="$ADAPTER_DIR"
272
272
  fi
273
273
 
274
+ # Gate registry (#717 walkthrough): binds each gate's NAME to the file that
275
+ # runs it, so a gate can be referred to by what it does rather than by a
276
+ # positional number. Optional by design — a consumer repo installed from an
277
+ # older package will not have it, and the hooks must keep working there.
278
+ _GATE_REGISTRY="$GATE_DIR/gate-registry.sh"
279
+ [ -f "$_GATE_REGISTRY" ] || _GATE_REGISTRY="$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-registry.sh"
280
+ if [ -f "$_GATE_REGISTRY" ]; then
281
+ # shellcheck source=/dev/null
282
+ . "$_GATE_REGISTRY"
283
+ fi
284
+
274
285
  # Trap: ensure quality report generated on ANY exit (pass or fail).
275
286
  # Using a guard prevents recursion when 'exit' is called from inside the trap.
276
287
  # IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
@@ -1267,6 +1278,14 @@ if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
1267
1278
  if [ "$(_detect_ext_lang "ps1" "psm1")" = "1" ]; then
1268
1279
  PROJECT_LANGS="$PROJECT_LANGS powershell"
1269
1280
  fi
1281
+ # #717: without this arm a staged .cs file never produced a `csharp` entry in
1282
+ # PROJECT_LANGS, so the per-language loop at the adapter-routing site had no
1283
+ # turn in which to resolve adapters/csharp.sh -- the adapter was unreachable
1284
+ # no matter how correct it was. Detection on extensions (not manifests) is why
1285
+ # one arm is enough: a repo staging .cs always lands here.
1286
+ if [ "$(_detect_ext_lang "cs" "csx")" = "1" ]; then
1287
+ PROJECT_LANGS="$PROJECT_LANGS csharp"
1288
+ fi
1270
1289
  # IaC: .tf, .tfvars, Dockerfile, docker-compose*, .yaml/.yml in kube context
1271
1290
  if echo "$CHANGED_FILES" | grep -qE '(\.tf$|\.tfvars$|Dockerfile|docker-compose)' 2>/dev/null; then
1272
1291
  PROJECT_LANGS="$PROJECT_LANGS iac"
@@ -2256,7 +2275,18 @@ else
2256
2275
  if [ "$LINT_EXIT" -ne 0 ]; then
2257
2276
  echo "ℹ️ Lint check had issues (may be non-blocking)"
2258
2277
  fi
2259
-
2278
+
2279
+ # #717: rc=2 is the adapters' "could not execute" channel (no toolchain,
2280
+ # or a tool that refused to start), distinct from rc=1 "ran and found
2281
+ # problems". Collapsing both into one SKIPPED line -- as this branch did
2282
+ # -- makes a permanently-broken analyzer indistinguishable from a clean
2283
+ # one, which is the silent-failure shape #717 is about. The gate still
2284
+ # does not block (tool-missing must not block), but the reason is now
2285
+ # named in the log instead of implied.
2286
+ if [ "$ANALYSIS_EXIT" -eq 2 ] || [ "$LINT_EXIT" -eq 2 ]; then
2287
+ echo "⏭️ SKIPPED - Code quality (${CURRENT_LANG} toolchain could not execute — NOT a pass)"
2288
+ fi
2289
+
2260
2290
  if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
2261
2291
  echo "✅ PASSED - Language-specific code quality checks."
2262
2292
  else
@@ -4307,8 +4337,72 @@ JAVAARCHROOTS
4307
4337
  fi
4308
4338
  fi
4309
4339
  ;;
4340
+ "csharp")
4341
+ # #717: C# had no arm at all, so it fell through to the `*)` default
4342
+ # below and was reported as "not configured for csharp" -- a language
4343
+ # the gate silently could not measure, with no way to configure it
4344
+ # either, because the config file was never looked for.
4345
+ #
4346
+ # Architecture for .NET is normally expressed as *tests* (NetArchTest,
4347
+ # ArchUnitNET, or a plain xUnit project asserting layering). There is no
4348
+ # universal CLI, so this arm keys off a config the project opts into,
4349
+ # exactly like the Go arm's arch-go.yaml.
4350
+ if [ -f "arch-csharp.yaml" ] || [ -f ".xp-gate/arch-csharp.yaml" ]; then
4351
+ _cs_arch_cfg="arch-csharp.yaml"
4352
+ [ -f "$_cs_arch_cfg" ] || _cs_arch_cfg=".xp-gate/arch-csharp.yaml"
4353
+ # Find an architecture test project: a .csproj whose name or contents
4354
+ # mention architecture tests. Guessing one fixed path is the #602
4355
+ # defect, which reported "not configured" for repos that had them.
4356
+ _cs_arch_proj=""
4357
+ while IFS= read -r _p; do
4358
+ [ -n "$_p" ] || continue
4359
+ if grep -qiE "architecture|archunit|netarchtest" "$_p" 2>/dev/null; then
4360
+ _cs_arch_proj="$_p"
4361
+ break
4362
+ fi
4363
+ done <<CSARCHPROJ
4364
+ $(git ls-files '*.csproj' 2>/dev/null)
4365
+ CSARCHPROJ
4366
+
4367
+ if [ -z "$_cs_arch_proj" ]; then
4368
+ echo " ℹ️ No architecture test project found - skipping C# architecture"
4369
+ echo " ⏭️ SKIPPED - C# architecture validation (no arch test project)"
4370
+ if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
4371
+ GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
4372
+ fi
4373
+ elif ! command -v dotnet >/dev/null 2>&1; then
4374
+ echo " ⏭️ SKIPPED - C# architecture validation (dotnet SDK not installed)"
4375
+ if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
4376
+ GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="tool_missing"
4377
+ fi
4378
+ else
4379
+ echo " Running C# architecture tests ($_cs_arch_proj)..."
4380
+ # Same reading as the Go/Java arms (#630/#602): the checker's own
4381
+ # status decides the verdict. Piping into `tail` and printing
4382
+ # "completed" unconditionally is the fail-open shape those issues
4383
+ # removed; PIPESTATUS[0] is what keeps this arm from repeating it.
4384
+ dotnet test "$_cs_arch_proj" --nologo 2>&1 | tail -20
4385
+ _cs_arch_rc="${PIPESTATUS[0]}"
4386
+ if [ "$_cs_arch_rc" -ne 0 ]; then
4387
+ echo ""
4388
+ echo "❌ BLOCKED - C# architecture violations ($_cs_arch_proj, exit $_cs_arch_rc)"
4389
+ echo "Fix the layering violation(s) above before committing."
4390
+ if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
4391
+ GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_tests_failed"
4392
+ fi
4393
+ exit 1
4394
+ fi
4395
+ echo " ✅ C# architecture validation completed."
4396
+ fi
4397
+ else
4398
+ echo " ℹ️ No arch-csharp.yaml found - skipping C# architecture"
4399
+ echo " ⏭️ SKIPPED - C# architecture validation (no config)"
4400
+ if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
4401
+ GATE_6_REASON_BY_LANG["$CURRENT_LANG"]="architecture_not_configured"
4402
+ fi
4403
+ fi
4404
+ ;;
4310
4405
  *)
4311
- echo " ℹ️ Architecture validation not configured for $CURRENT_LANG"
4312
4406
  echo " ⏭️ SKIPPED - Architecture validation (not configured for $CURRENT_LANG)"
4313
4407
  # #537: archlint ships no detector for this language. Say so in a way
4314
4408
  # the quality report can read, instead of leaving the dimension silent.
@@ -4987,6 +5081,19 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
4987
5081
  # each other, and the only way past was to edit a generated file by hand.
4988
5082
  case "$_mf" in
4989
5083
  */__tests__/*) continue ;;
5084
+ # AGENTS.md is documentation, not a hook artifact: sync-package-content.js
5085
+ # copies the hook suite, and no AGENTS.md exists under hooks/. Demanding a
5086
+ # mirror for it reported a BLOCK on every doc edit (harmless only because
5087
+ # GATE_FAILED was never read -- see below).
5088
+ */AGENTS.md) continue ;;
5089
+ # `githooks/adapters/` mirrors to `src/npm-package/adapters/`, NOT under
5090
+ # `hooks/` -- that is what scripts/check-hook-mirror.sh declares (line 6)
5091
+ # and what sync-package-content.js writes. The blanket `githooks/*` rule
5092
+ # below sent it to `src/npm-package/hooks/adapters/<f>`, a path that has
5093
+ # never existed, so ANY edit to an adapter was un-committable with
5094
+ # "has no mirror at". It stayed hidden because nobody had edited an
5095
+ # adapter since this gate was narrowed to diff files.
5096
+ githooks/adapters/*) _mm="src/npm-package/adapters/${_mf#githooks/adapters/}" ;;
4990
5097
  githooks/*) _mm="src/npm-package/hooks/${_mf#githooks/}" ;;
4991
5098
  plugins/*/src/*) _mm=$(printf '%s' "$_mf" | sed 's|^plugins/\([^/]*\)/src/|src/npm-package/plugins/\1/src/|') ;;
4992
5099
  *) continue ;;
@@ -5006,7 +5113,15 @@ if [ -n "$MIRROR_STAGED" ] && printf '%s\n' "$MIRROR_STAGED" | grep -qE '^(githo
5006
5113
  echo "❌ BLOCKED - Gate 12b: $MIRROR_FAIL mirror parity violation(s)."
5007
5114
  echo " The mirror is what gets published; a mirror-only fix is silently"
5008
5115
  echo " reverted by the next prepack (v0.27.3 shipped that bug)."
5009
- GATE_FAILED=1
5116
+ # BLOCKS, and must actually block. This used to set GATE_FAILED=1, a
5117
+ # variable that appears exactly once in this file -- the assignment --
5118
+ # and is read nowhere, so the gate printed BLOCKED on every doc edit and
5119
+ # let the commit through. A gate that reports a verdict nobody consumes is
5120
+ # the same failure as a filter that accepts a file no adapter claims:
5121
+ # it looks like enforcement. exit is the contract every other blocking
5122
+ # gate here uses (see Gate 13: `exit "$_g13_worst"`).
5123
+ GATE_12_STATUS=WARN
5124
+ exit 1
5010
5125
  else
5011
5126
  echo "✅ PASSED - Gate 12b: mirror parity (touched files)"
5012
5127
  fi
@@ -5061,6 +5176,10 @@ elif [ -f "$GATE_DIR/gate-13-compiler-strictness.sh" ] || [ -f "$(git rev-parse
5061
5176
 
5062
5177
  gate13_run_cpp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
5063
5178
  gate13_run_java_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
5179
+ # #717: C# joins the same reduce-the-worst loop. Without this call the
5180
+ # function would exist but never execute -- the "silently skipped" shape
5181
+ # #717 is about, one level up.
5182
+ gate13_run_csharp_analysis; _g13_rc=$?; _g13_note "$_g13_rc"
5064
5183
  gate13_run_analyzer; _g13_rc=$?; _g13_note "$_g13_rc"
5065
5184
 
5066
5185
  exit "$_g13_worst"