@boyingliu01/xp-gate 0.35.24 → 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 (68) hide show
  1. package/adapter-common.sh +55 -2
  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/iac.sh +45 -5
  7. package/bin/xp-gate.js +18 -11
  8. package/gate-10.sh +18 -3
  9. package/gate-13-compiler-strictness.sh +181 -0
  10. package/gate-3.sh +1 -1
  11. package/gate-4.sh +1 -1
  12. package/gate-9.sh +37 -2
  13. package/gates/common.ts +9 -2
  14. package/hooks/adapter-common.sh +55 -2
  15. package/hooks/gate-10.sh +18 -3
  16. package/hooks/gate-13-compiler-strictness.sh +181 -0
  17. package/hooks/gate-3.sh +1 -1
  18. package/hooks/gate-4.sh +1 -1
  19. package/hooks/gate-9.sh +37 -2
  20. package/hooks/gate-registry.sh +161 -0
  21. package/hooks/lib/test-failure.sh +29 -4
  22. package/hooks/pre-commit +122 -3
  23. package/lib/__tests__/agent-runtime-journal.test.js +42 -18
  24. package/lib/__tests__/check-single-file.test.js +55 -4
  25. package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
  26. package/lib/__tests__/doctor-delphi.test.js +175 -0
  27. package/lib/__tests__/install-skill-full-dir.test.js +45 -2
  28. package/lib/__tests__/install-skill.test.js +84 -3
  29. package/lib/__tests__/mirror-sync.test.js +23 -5
  30. package/lib/__tests__/uninstall-global-residue.test.js +192 -28
  31. package/lib/__tests__/update-skill-flags.test.js +78 -0
  32. package/lib/__tests__/update-skill.test.js +156 -3
  33. package/lib/detect-deps.js +24 -0
  34. package/lib/doctor.js +80 -12
  35. package/lib/install-skill.js +67 -18
  36. package/lib/mattpocock-license-verdict.json +1 -1
  37. package/lib/uninstall.js +96 -2
  38. package/lib/update-skill.js +143 -2
  39. package/mock-policy/AGENTS.md +4 -4
  40. package/mutation/AGENTS.md +4 -4
  41. package/package.json +1 -1
  42. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  43. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  44. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  45. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  46. package/plugins/dsh/lib/index.d.ts.map +1 -1
  47. package/plugins/dsh/lib/index.js +7 -4
  48. package/plugins/dsh/package.json +1 -1
  49. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  50. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  51. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  52. package/plugins/dsh/src/index.test.ts +25 -0
  53. package/plugins/dsh/src/index.ts +7 -4
  54. package/plugins/opencode/package.json +1 -1
  55. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  56. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  57. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  58. package/plugins/qoder/plugin.json +1 -1
  59. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  60. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  61. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  62. package/principles/AGENTS.md +4 -4
  63. package/principles/adapters/csharp.ts +309 -0
  64. package/principles/analyzer.ts +5 -0
  65. package/scripts/delphi-external-review.cjs +130 -101
  66. package/skills/delphi-review/AGENTS.md +4 -4
  67. package/skills/sprint-flow/AGENTS.md +4 -4
  68. 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/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/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/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
package/gates/common.ts CHANGED
@@ -183,8 +183,15 @@ export function getTempDir(): string {
183
183
 
184
184
  // ── File Filtering Helpers ──
185
185
 
186
- /** Source file extensions that lizard can analyze. */
187
- const LIZARD_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$/;
186
+ /**
187
+ * Source file extensions that lizard can analyze.
188
+ *
189
+ * `cs` belongs here because lizard parses C# natively (#717). Omitting it was
190
+ * never a tool limitation: lizard reports the CCN 11-22 functions the issue
191
+ * names, and the gate dropped those files before lizard was invoked, so the
192
+ * report said "no source files to check" for a tree full of them.
193
+ */
194
+ const LIZARD_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt|cs)$/;
188
195
 
189
196
  /** File extensions that semgrep supports. */
190
197
  const SEMGREP_EXTENSIONS = /\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$/;
@@ -38,8 +38,46 @@ run_without_git_context() (
38
38
  "$@"
39
39
  )
40
40
 
41
+ # ---------------------------------------------------------------------------
42
+ # C# / .NET manifests (#717)
43
+ #
44
+ # A .NET tree can declare itself three ways: a project file (*.csproj), a
45
+ # solution (*.sln), or a Directory.Build.props that carries the real settings
46
+ # for a whole directory subtree with no project file of its own. All three are
47
+ # checked, because a repo may carry only the latter two.
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # Succeeds when the current directory holds a .NET manifest at depth 1.
51
+ has_dotnet_manifest() {
52
+ [ -f "global.json" ] && return 0
53
+ [ -f "Directory.Build.props" ] && return 0
54
+ [ -f "Directory.Build.targets" ] && return 0
55
+ # `find -maxdepth 1` rather than a glob loop: a repo with thousands of files
56
+ # in one directory would expand a `*.csproj` glob into an unusable argv.
57
+ # -not -path "./.*" keeps a stray file under .git/.vscode from declaring the
58
+ # project language.
59
+ [ -n "$(find . -maxdepth 1 -type f \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null | sed -n '1p; 1q')" ]
60
+ }
61
+
62
+ # Succeeds when a .cs source exists below the current directory.
63
+ has_csharp_sources() {
64
+ [ -n "$(find . -name "*.cs" -type f -not -path "./.git/*" -not -path "*/node_modules/*" -not -path "*/bin/*" -not -path "*/obj/*" 2>/dev/null | sed -n '1p; 1q')" ]
65
+ }
66
+
41
67
  detect_project_lang() {
42
- if [ -f "tsconfig.json" ]; then
68
+ # #717: the C# arms come FIRST, before tsconfig.json / package.json /
69
+ # .sh sniffing. Detection below is an exclusive first-match chain, and a .NET
70
+ # repo routinely carries a JS or shell half (the dsh-aot-runner shape in the
71
+ # issue is 2031 lines of C# plus ~900 lines of JS). Ordered after the
72
+ # TypeScript arm, that repo reported "typescript" and every C# file was
73
+ # invisible to language-routed gates -- the silent skip #717 is about.
74
+ #
75
+ # Placing C# first cannot steal an existing project: none of the languages
76
+ # matched afterwards ever ship a .csproj/.sln/Directory.Build.props, so the
77
+ # pre-existing arms are reached unchanged whenever those files are absent.
78
+ if has_dotnet_manifest; then
79
+ echo "csharp"
80
+ elif [ -f "tsconfig.json" ]; then
43
81
  echo "typescript"
44
82
  elif [ -f "pyproject.toml" ] || [ -f "requirements.txt" ] || [ -f "setup.py" ]; then
45
83
  echo "python"
@@ -88,6 +126,8 @@ detect_project_lang() {
88
126
  else
89
127
  echo "dart"
90
128
  fi
129
+ elif [ -n "$(find . -name "*.cs" -type f -not -path "./.git/*" -not -path "*/bin/*" -not -path "*/obj/*" | sed -n '1p; 1q')" ]; then
130
+ echo "csharp"
91
131
  elif [ -n "$(find . -name "*.swift" -type f | sed -n '1p; 1q')" ]; then
92
132
  echo "swift"
93
133
  elif [ -n "$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" -type f | sed -n '1p; 1q')" ]; then
@@ -104,6 +144,18 @@ detect_project_lang() {
104
144
  fi
105
145
  }
106
146
 
147
+ # ---------------------------------------------------------------------------
148
+ # NOTE (#717): a `detect_project_langs()` collector was added here and then
149
+ # REMOVED before this change landed. It had zero production callers -- only its
150
+ # own tests -- which is exactly the dead-dispatch shape #688 deleted from this
151
+ # same file (see below). The gates do NOT route through it: pre-commit collects
152
+ # languages with its own `_detect_ext_lang` into `PROJECT_LANGS`, and that is
153
+ # what actually decides which adapters run (AC-717-13/13b pin the dual-language
154
+ # case). Keeping a second, unconsumed collector beside it would have meant two
155
+ # language-detection paths drifting apart, with the untested one looking
156
+ # authoritative. If multi-language routing is ever reworked, the change belongs
157
+ # in pre-commit's collector, not in a parallel helper here.
158
+
107
159
  # ---------------------------------------------------------------------------
108
160
  # REMOVED in #688: route_to_adapter()
109
161
  #
@@ -210,7 +262,8 @@ detect_iac_project() {
210
262
 
211
263
  # Check for Kubernetes manifests (YAML with apiVersion/kind)
212
264
  if [ -n "$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')" ]; then
213
- local yaml_file=$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
265
+ local yaml_file
266
+ yaml_file=$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
214
267
  if grep -qE "^(apiVersion|kind):" "$yaml_file" 2>/dev/null; then
215
268
  has_iac=true
216
269
  fi
package/hooks/gate-10.sh CHANGED
@@ -170,12 +170,25 @@ EOF
170
170
  # --no-verify, which is worse than any verdict it could have produced.
171
171
  # `-k` after the primary signal: pysemgrep spinning in a syscall ignores
172
172
  # SIGTERM, and the shipped 0.29 code used the same `-k 10` shape.
173
+ # #717: p/security-audit carries NO C# rules. Measured on a .cs file with
174
+ # a textbook SQL injection (string-concatenated SqlCommand) plus a
175
+ # Process.Start command injection: p/security-audit 0 findings, p/csharp 1
176
+ # ERROR (csharp-sqli). Line 69 has listed `cs` in the filter all along, so
177
+ # .cs was accepted and then scanned by a ruleset that cannot see C# -- the
178
+ # identical "filter looks like coverage" defect this issue removed from
179
+ # Gate 4. p/csharp is loaded IN ADDITION so the security-audit rules still
180
+ # cover the other languages in a mixed commit.
181
+ SEMGREP_RULES=(--config=p/security-audit)
182
+ if printf '%s
183
+ ' "$SEMGREP_FILES" | grep -qE ".cs$"; then
184
+ SEMGREP_RULES+=(--config=p/csharp)
185
+ fi
173
186
  SEMGREP_TIMEOUT_S="${XP_GATE_SEMGREP_TIMEOUT:-180}"
174
187
  SEMGREP_TIMED_OUT=false
175
188
  if command -v timeout >/dev/null 2>&1; then
176
189
  SEMGREP_STDOUT_FILE=$(mktemp)
177
190
  SEMGREP_OUTPUT=$(timeout -k 10 "$SEMGREP_TIMEOUT_S" \
178
- "$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
191
+ "$SEMGREP_CMD" scan "${SEMGREP_RULES[@]}" --json --disable-version-check \
179
192
  "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
180
193
  SEMGREP_EXIT=$?
181
194
  # 124 = timeout's own "I gave up" code. 137 = 128+SIGKILL, which is what
@@ -194,7 +207,7 @@ EOF
194
207
  # trade -- but the unbounded window is now stated.
195
208
  echo " ⚠️ 'timeout' command not available — semgrep runs UNBOUNDED (install coreutils or set up gtimeout)"
196
209
  SEMGREP_STDOUT_FILE=$(mktemp)
197
- SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
210
+ SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan "${SEMGREP_RULES[@]}" --json --disable-version-check \
198
211
  "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
199
212
  SEMGREP_EXIT=$?
200
213
  fi
@@ -299,7 +312,9 @@ console.log("");});
299
312
  echo " ❌ BLOCKED — blocking-severity vulnerability found"
300
313
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
301
314
  echo "$FINDING_DETAILS"
302
- echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
315
+ # #717: a C# finding comes from p/csharp; naming only security-audit
316
+ # would send the author to a ruleset that reproduces zero hits.
317
+ echo " Run 'semgrep scan --config=p/security-audit --config=p/csharp' to review all findings."
303
318
  GATE_10_STATUS="FAIL"
304
319
  # Consumed by the sourcing parent (pre-commit report JSON), not here.
305
320
  # shellcheck disable=SC2034
@@ -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