@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.
- package/adapter-common.sh +78 -36
- package/adapters/csharp.sh +245 -0
- package/adapters/gate-3.sh +1 -1
- package/adapters/gate-4.sh +1 -1
- package/adapters/gate-9.sh +37 -2
- package/adapters/go.sh +6 -11
- package/adapters/iac.sh +45 -5
- package/adapters/python.sh +6 -11
- package/bin/xp-gate.js +18 -11
- package/gate-10.sh +18 -3
- package/gate-13-compiler-strictness.sh +181 -0
- package/gate-3.sh +1 -1
- package/gate-4.sh +1 -1
- package/gate-9.sh +37 -2
- package/gates/common.ts +9 -2
- package/hooks/adapter-common.sh +78 -36
- package/hooks/gate-10.sh +18 -3
- package/hooks/gate-13-compiler-strictness.sh +181 -0
- package/hooks/gate-3.sh +1 -1
- package/hooks/gate-4.sh +1 -1
- package/hooks/gate-9.sh +37 -2
- package/hooks/gate-registry.sh +161 -0
- package/hooks/lib/test-failure.sh +29 -4
- package/hooks/pre-commit +122 -3
- package/lib/__tests__/agent-runtime-journal.test.js +42 -18
- package/lib/__tests__/check-single-file.test.js +55 -4
- package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
- package/lib/__tests__/doctor-delphi.test.js +178 -3
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
- package/lib/__tests__/install-skill-full-dir.test.js +45 -2
- package/lib/__tests__/install-skill.test.js +191 -23
- package/lib/__tests__/mirror-sync.test.js +23 -5
- package/lib/__tests__/uninstall-global-residue.test.js +394 -9
- package/lib/__tests__/update-skill-flags.test.js +78 -0
- package/lib/__tests__/update-skill.test.js +334 -57
- package/lib/detect-deps.js +24 -0
- package/lib/doctor.js +80 -12
- package/lib/install-skill.js +85 -21
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/uninstall.js +143 -12
- package/lib/update-skill.js +239 -28
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/lib/index.d.ts.map +1 -1
- package/plugins/dsh/lib/index.js +7 -4
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/index.test.ts +25 -0
- package/plugins/dsh/src/index.ts +7 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/csharp.ts +309 -0
- package/principles/analyzer.ts +5 -0
- package/scripts/delphi-external-review.cjs +130 -101
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
package/adapter-common.sh
CHANGED
|
@@ -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
|
-
|
|
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,41 +144,42 @@ detect_project_lang() {
|
|
|
104
144
|
fi
|
|
105
145
|
}
|
|
106
146
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# Tier 2: Project nested layout (githooks/adapters/lang.sh)
|
|
119
|
-
elif [ -f "githooks/adapters/${lang}.sh" ]; then
|
|
120
|
-
adapter_file="githooks/adapters/${lang}.sh"
|
|
121
|
-
elif [ -f "./githooks/adapters/${lang}.sh" ]; then
|
|
122
|
-
adapter_file="./githooks/adapters/${lang}.sh"
|
|
123
|
-
fi
|
|
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.
|
|
124
158
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# REMOVED in #688: route_to_adapter()
|
|
161
|
+
#
|
|
162
|
+
# It was a dispatcher with zero production callers. pre-commit sources an
|
|
163
|
+
# adapter and calls `run_static_analysis` / `run_lint` / `run_tests` /
|
|
164
|
+
# `run_coverage` directly; nothing ever went through this function, so its
|
|
165
|
+
# `case "$action"` enumeration was dead too -- including the "add an
|
|
166
|
+
# `architecture` arm and it'll work" assumption in #688, which would have been
|
|
167
|
+
# a no-op.
|
|
168
|
+
#
|
|
169
|
+
# Its two never-invoked dependants are gone with it: `run_architecture()` in
|
|
170
|
+
# adapters/python.sh and adapters/go.sh. Gate 6 already implements both checks
|
|
171
|
+
# INLINE in pre-commit, and does it better than the adapter versions did:
|
|
172
|
+
# * Python: detects 4 config carriers (.import-linter.yml /
|
|
173
|
+
# import_linter_config.yml / .importlinter / pyproject [tool.importlinter]),
|
|
174
|
+
# where the adapter checked 2, and BLOCKs when lint-imports is missing
|
|
175
|
+
# instead of `require_tool ... || return 0` silently passing.
|
|
176
|
+
# * Go: reads the checker's own exit status via PIPESTATUS[0] (#630), where a
|
|
177
|
+
# `arch-go check | head` pipeline would have reported head's status.
|
|
178
|
+
# Wiring the adapter version back in would therefore have been a regression,
|
|
179
|
+
# so the dead code was deleted instead of connected.
|
|
180
|
+
#
|
|
181
|
+
# tests: githooks/__tests__/issue-688-dead-dispatcher-removed.test.bats
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
142
183
|
|
|
143
184
|
check_if_tool_available() {
|
|
144
185
|
local tool_name="$1"
|
|
@@ -221,7 +262,8 @@ detect_iac_project() {
|
|
|
221
262
|
|
|
222
263
|
# Check for Kubernetes manifests (YAML with apiVersion/kind)
|
|
223
264
|
if [ -n "$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')" ]; then
|
|
224
|
-
local yaml_file
|
|
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')
|
|
225
267
|
if grep -qE "^(apiVersion|kind):" "$yaml_file" 2>/dev/null; then
|
|
226
268
|
has_iac=true
|
|
227
269
|
fi
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# C# / .NET adapter (#717)
|
|
3
|
+
#
|
|
4
|
+
# Contract: pre-commit sources this file and calls run_static_analysis /
|
|
5
|
+
# run_lint / run_tests / run_coverage directly (#688 — no dispatcher sits in
|
|
6
|
+
# between). Every function must therefore exist and must return a status the
|
|
7
|
+
# caller can turn into a verdict.
|
|
8
|
+
#
|
|
9
|
+
# Two lessons from the sibling adapters are load-bearing here:
|
|
10
|
+
#
|
|
11
|
+
# 1. #706 (kotlin.sh): a failed BUILD must not swallow an analyzer that works
|
|
12
|
+
# on its own. `dotnet format` reads .cs sources and needs no successful
|
|
13
|
+
# build, so it runs regardless of what the build said. The build failure is
|
|
14
|
+
# recorded and reported, never hidden.
|
|
15
|
+
#
|
|
16
|
+
# 2. #703/#708 (java.sh): "tool not installed" and "tool ran and failed" need
|
|
17
|
+
# different fixes, so they get different messages. A missing dotnet is a
|
|
18
|
+
# SKIP with an install hint, not a BLOCK -- the project may not be a .NET
|
|
19
|
+
# one after all.
|
|
20
|
+
#
|
|
21
|
+
# Windows note (#717): on this machine `dotnet test` aborts before running a
|
|
22
|
+
# single case with `Win32Exception (5) Access is denied` from the VSTest
|
|
23
|
+
# testhost -- an environment fault, reproducible under both git-bash and
|
|
24
|
+
# PowerShell. run_tests() below therefore reports "test host did not start"
|
|
25
|
+
# separately from "tests failed", because a gate that reads the former as the
|
|
26
|
+
# latter would block every commit in a working repo.
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Project discovery
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
# Print the directory that owns the build, or nothing.
|
|
33
|
+
#
|
|
34
|
+
# A .sln at the root is the usual shape; a bare .csproj is the single-project
|
|
35
|
+
# shape; Directory.Build.props can declare a tree with neither. All three are
|
|
36
|
+
# accepted because `dotnet` itself accepts all three.
|
|
37
|
+
_detect_dotnet_root() {
|
|
38
|
+
local found
|
|
39
|
+
found=$(find . -maxdepth 2 -name "*.sln" -type f -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
|
|
40
|
+
if [ -n "$found" ]; then
|
|
41
|
+
dirname "$found"
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
found=$(find . -maxdepth 2 -name "*.csproj" -type f -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
|
|
45
|
+
if [ -n "$found" ]; then
|
|
46
|
+
dirname "$found"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if [ -f "Directory.Build.props" ]; then
|
|
50
|
+
echo "."
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Echo the dotnet entry point, or nothing when the SDK is absent.
|
|
57
|
+
#
|
|
58
|
+
# Deliberately does NOT bundle the bare `dotnet` invocation into a single
|
|
59
|
+
# string: the .exe suffix matters on Windows and callers need the tokens.
|
|
60
|
+
_detect_dotnet() {
|
|
61
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
62
|
+
echo "dotnet"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
if command -v dotnet.exe >/dev/null 2>&1; then
|
|
66
|
+
echo "dotnet.exe"
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Print the first staged/changed .cs file, or nothing. Used only for messages.
|
|
73
|
+
_csharp_changed_hint() {
|
|
74
|
+
printf '%s\n' "${CHANGED_FILES:-}" | grep -E '\.cs$' | sed -n '1p'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Code quality (Gate 1)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
run_static_analysis() {
|
|
82
|
+
local dotnet_root dotnet ver build_out build_rc format_out format_rc
|
|
83
|
+
dotnet_root=$(_detect_dotnet_root) || {
|
|
84
|
+
echo "No .NET project marker found (.sln / *.csproj / Directory.Build.props)"
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dotnet=$(_detect_dotnet) || {
|
|
89
|
+
# SKIP, not BLOCK: no SDK means nothing was measured, and the caller maps a
|
|
90
|
+
# non-zero return to "tools unavailable" which it reports honestly.
|
|
91
|
+
echo "dotnet SDK not found"
|
|
92
|
+
echo " Install: winget install Microsoft.DotNet.SDK.8 | brew install --cask dotnet-sdk | apt-get install dotnet-sdk-8.0"
|
|
93
|
+
return 2
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ver=$("$dotnet" --version 2>/dev/null | sed -n '1p')
|
|
97
|
+
echo "dotnet SDK ${ver:-unknown} (project: ${dotnet_root})"
|
|
98
|
+
|
|
99
|
+
# --- build (recorded, not authoritative) -------------------------------
|
|
100
|
+
# `--nologo` keeps the banner out; `-warnaserror` is Gate 13's job, not Gate
|
|
101
|
+
# 1's, so the build here only needs to answer "does this compile".
|
|
102
|
+
build_out=$("$dotnet" build "$dotnet_root" --nologo -v quiet 2>&1)
|
|
103
|
+
build_rc=$?
|
|
104
|
+
if [ "$build_rc" -ne 0 ]; then
|
|
105
|
+
echo "BUILD FAILED (dotnet build exited $build_rc):"
|
|
106
|
+
printf '%s\n' "$build_out" | grep -iE "error|fail" | sed -n '1,10p'
|
|
107
|
+
echo " Analysis still runs below: dotnet format reads sources directly"
|
|
108
|
+
echo " and its verdict does not depend on the build."
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# --- analyzer (authoritative) -----------------------------------------
|
|
112
|
+
# `dotnet format --verify-no-changes` is the closest C# equivalent of
|
|
113
|
+
# `eslint --max-warnings 0`: formatting + the configured analyzers, exit 0
|
|
114
|
+
# only when everything is clean.
|
|
115
|
+
format_out=$("$dotnet" format "$dotnet_root" --verify-no-changes --verbosity quiet 2>&1)
|
|
116
|
+
format_rc=$?
|
|
117
|
+
|
|
118
|
+
# (#717) A command that could not RUN is not a command that found problems.
|
|
119
|
+
# 127 = not found, 126 = not executable. Without this arm a broken or shadowed
|
|
120
|
+
# `dotnet` made `dotnet format` exit 127, which the code below turned into
|
|
121
|
+
# "127 change(s) required or analyzer finding(s)" and returned 1 -- reported to
|
|
122
|
+
# the user as a code-quality FAILURE of their sources. Measured before the fix:
|
|
123
|
+
# a stub `dotnet` exiting 127 produced exactly that message.
|
|
124
|
+
case "$format_rc" in
|
|
125
|
+
126|127)
|
|
126
|
+
echo "dotnet format could not execute (exit ${format_rc}) — NOT a source verdict"
|
|
127
|
+
echo " The 'dotnet' on PATH is missing or not executable."
|
|
128
|
+
return 2
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
if [ "$format_rc" -eq 0 ]; then
|
|
133
|
+
echo "FORMAT OK - dotnet format reports no changes required"
|
|
134
|
+
else
|
|
135
|
+
printf '%s\n' "$format_out" | sed -n '1,20p'
|
|
136
|
+
echo "dotnet format: ${format_rc} change(s) required or analyzer finding(s)"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# A real finding wins over a build failure: the build failure is already
|
|
140
|
+
# printed above, and returning 1 here makes the caller report the stronger
|
|
141
|
+
# statement. Returning the build's status when analysis is clean would hide
|
|
142
|
+
# which of the two actually happened.
|
|
143
|
+
if [ "$format_rc" -ne 0 ]; then
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
# Same reasoning for the build: 126/127 means the build never ran, so there is
|
|
147
|
+
# no "compiles / does not compile" answer to report.
|
|
148
|
+
case "$build_rc" in
|
|
149
|
+
126|127) echo "dotnet build could not execute (exit ${build_rc}) — NOT a build verdict"; return 2 ;;
|
|
150
|
+
esac
|
|
151
|
+
return "$build_rc"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
run_lint() {
|
|
155
|
+
run_static_analysis
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Tests (Gate 5)
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
# Succeeds when any project in the tree declares a test dependency.
|
|
163
|
+
#
|
|
164
|
+
# Without this probe `dotnet test` on a solution with no test project exits 0
|
|
165
|
+
# and prints nothing, which reads as "all tests passed" for a project that has
|
|
166
|
+
# no tests at all -- the same silent-success shape #717 is about.
|
|
167
|
+
_has_test_project() {
|
|
168
|
+
local root="${1:-.}"
|
|
169
|
+
find "$root" -maxdepth 3 -name "*.csproj" -type f -not -path "./.git/*" 2>/dev/null | while IFS= read -r proj; do
|
|
170
|
+
if grep -qiE 'Microsoft\.NET\.Test\.Sdk|Microsoft\.TestPlatform<xunit|NUnit|MSTest\.TestFramework' "$proj" 2>/dev/null; then
|
|
171
|
+
printf '%s\n' "$proj"
|
|
172
|
+
break
|
|
173
|
+
fi
|
|
174
|
+
done | sed -n '1p'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
run_tests() {
|
|
178
|
+
local dotnet_root dotnet out rc
|
|
179
|
+
dotnet_root=$(_detect_dotnet_root) || { echo "No .NET project marker found"; return 0; }
|
|
180
|
+
dotnet=$(_detect_dotnet) || { echo "dotnet SDK not found"; return 2; }
|
|
181
|
+
|
|
182
|
+
local test_proj
|
|
183
|
+
test_proj=$(_has_test_project "$dotnet_root")
|
|
184
|
+
if [ -z "$test_proj" ]; then
|
|
185
|
+
echo "No test project found in ${dotnet_root}"
|
|
186
|
+
echo " A .csproj must reference Microsoft.NET.Test.Sdk / xunit / NUnit / MSTest to run under dotnet test"
|
|
187
|
+
return 2
|
|
188
|
+
fi
|
|
189
|
+
echo "Test project: ${test_proj}"
|
|
190
|
+
|
|
191
|
+
out=$("$dotnet" test "$test_proj" --nologo 2>&1)
|
|
192
|
+
rc=$?
|
|
193
|
+
printf '%s\n' "$out" | sed -n '1,25p'
|
|
194
|
+
|
|
195
|
+
if [ "$rc" -ne 0 ] && printf '%s\n' "$out" | grep -qiE 'Win32Exception|Access is denied|拒绝访问|test host 进程已退出|Testhost.*exited'; then
|
|
196
|
+
# The testhost never started, so nothing was verified. Reporting this as a
|
|
197
|
+
# test FAILURE would block every commit in a repo whose tests are fine;
|
|
198
|
+
# reporting it as PASS would be a lie. 2 is the "could not execute" channel
|
|
199
|
+
# the adapters already use (java.sh's G1 contract).
|
|
200
|
+
echo "dotnet test could not start the test host (environment fault, not a test verdict)"
|
|
201
|
+
echo " Known on this host: VSTest testhost aborts with Win32Exception (5) even when invoked outside git-bash"
|
|
202
|
+
return 2
|
|
203
|
+
fi
|
|
204
|
+
return "$rc"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Coverage (Gate 5)
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
#
|
|
211
|
+
# Silently returning 0 here would let Gate 5 read "no coverage file" as "no
|
|
212
|
+
# coverage requirement". The output just tells the caller which artifact the
|
|
213
|
+
# gate is looking for; the threshold verdict stays with the caller.
|
|
214
|
+
run_coverage() {
|
|
215
|
+
local dotnet_root dotnet
|
|
216
|
+
dotnet_root=$(_detect_dotnet_root) || { echo "No .NET project marker found"; return 0; }
|
|
217
|
+
dotnet=$(_detect_dotnet) || { echo "dotnet SDK not found"; return 2; }
|
|
218
|
+
|
|
219
|
+
local test_proj
|
|
220
|
+
test_proj=$(_has_test_project "$dotnet_root")
|
|
221
|
+
if [ -z "$test_proj" ]; then
|
|
222
|
+
echo "No test project found in ${dotnet_root} — coverage not measurable"
|
|
223
|
+
return 2
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
echo "Collecting coverage: dotnet test --collect:\"XPlat Code Coverage\""
|
|
227
|
+
local out rc
|
|
228
|
+
out=$("$dotnet" test "$test_proj" --nologo --collect:"XPlat Code Coverage" 2>&1)
|
|
229
|
+
rc=$?
|
|
230
|
+
printf '%s\n' "$out" | sed -n '1,15p'
|
|
231
|
+
|
|
232
|
+
local report
|
|
233
|
+
report=$(find "$dotnet_root" -name "coverage.cobertura.xml" -type f -newermt "-10 minutes" 2>/dev/null | sed -n '1p; 1q')
|
|
234
|
+
if [ -z "$report" ]; then
|
|
235
|
+
report=$(find "$dotnet_root" -name "coverage.cobertura.xml" -type f 2>/dev/null | sed -n '1p; 1q')
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [ -z "$report" ]; then
|
|
239
|
+
echo "coverage.cobertura.xml not produced — coverage NOT measured"
|
|
240
|
+
echo " Requires coverlet.collector in the test project"
|
|
241
|
+
return 2
|
|
242
|
+
fi
|
|
243
|
+
echo "Coverage report: ${report}"
|
|
244
|
+
return "$rc"
|
|
245
|
+
}
|
package/adapters/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/adapters/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/adapters/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
|
|
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
|
-
|
|
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/adapters/go.sh
CHANGED
|
@@ -43,17 +43,12 @@ run_lint() {
|
|
|
43
43
|
return $?
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
run_architecture()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
echo "Running Go architecture checks (arch-go)..."
|
|
54
|
-
arch-go check
|
|
55
|
-
return $?
|
|
56
|
-
}
|
|
46
|
+
# REMOVED in #688: run_architecture()
|
|
47
|
+
# Never invoked -- its only would-be caller, route_to_adapter(), had no callers
|
|
48
|
+
# either. Gate 6 implements arch-go in pre-commit (grep "arch-go"), and reads
|
|
49
|
+
# the checker's own exit status via PIPESTATUS[0] (#630) instead of letting a
|
|
50
|
+
# `| head` pipeline report head's. See githooks/adapter-common.sh for the full
|
|
51
|
+
# rationale.
|
|
57
52
|
|
|
58
53
|
run_tests() {
|
|
59
54
|
require_tool go "Go SDK" || return 1
|
package/adapters/iac.sh
CHANGED
|
@@ -78,7 +78,47 @@ run_static_analysis() {
|
|
|
78
78
|
local exit_code=0
|
|
79
79
|
|
|
80
80
|
# Try checkov first (recommended - supports multiple platforms)
|
|
81
|
-
|
|
81
|
+
# Resolve to an executable that actually RUNS, not merely one that exists. On
|
|
82
|
+
# MSYS2/Git Bash pip leaves an extensionless `checkov` shim whose shebang
|
|
83
|
+
# hands a MSYS path to the Windows interpreter, which fails every time:
|
|
84
|
+
# can't open file 'D:\\c\\Users\\...\\Scripts\\checkov'
|
|
85
|
+
# `command -v checkov` still succeeds, so a presence-only probe picks the
|
|
86
|
+
# broken shim and the gate reports "the scanner crashed" on IaC commits that
|
|
87
|
+
# are perfectly fine. Probing the version is the only way to tell the two
|
|
88
|
+
# apart; checkov.cmd is the form that works here.
|
|
89
|
+
#
|
|
90
|
+
# A timeout wraps the probe because the failure mode being screened for is a
|
|
91
|
+
# shim that does not answer. Observed breakage exits 2 at once, but a hung
|
|
92
|
+
# wrapper must not block the commit forever. `timeout` is absent on stock
|
|
93
|
+
# macOS, so it is used only when available; the probe is a local version
|
|
94
|
+
# print either way.
|
|
95
|
+
local checkov_bin=""
|
|
96
|
+
local _checkov_candidate
|
|
97
|
+
local _checkov_seen_nonexec=0
|
|
98
|
+
local _checkov_probe
|
|
99
|
+
for _checkov_candidate in checkov checkov.exe checkov.cmd; do
|
|
100
|
+
command -v "$_checkov_candidate" >/dev/null 2>&1 || continue
|
|
101
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
102
|
+
_checkov_probe="timeout 20 $_checkov_candidate --version"
|
|
103
|
+
else
|
|
104
|
+
_checkov_probe="$_checkov_candidate --version"
|
|
105
|
+
fi
|
|
106
|
+
if $_checkov_probe >/dev/null 2>&1; then
|
|
107
|
+
checkov_bin=$(command -v "$_checkov_candidate")
|
|
108
|
+
break
|
|
109
|
+
fi
|
|
110
|
+
_checkov_seen_nonexec=1
|
|
111
|
+
done
|
|
112
|
+
# "Not installed" and "installed but unusable" both end up without a scanner,
|
|
113
|
+
# and the fallbacks below keep the gate useful -- but they are not the same
|
|
114
|
+
# situation, and silently treating a broken install as absent hides a lost
|
|
115
|
+
# scanner. Say which one it is.
|
|
116
|
+
if [ -z "$checkov_bin" ] && [ "$_checkov_seen_nonexec" -eq 1 ]; then
|
|
117
|
+
echo " ⚠️ checkov is on PATH but none of its forms could be executed;"
|
|
118
|
+
echo " treating it as unusable and falling back to other scanners."
|
|
119
|
+
echo " Reinstall with: pip install -U checkov"
|
|
120
|
+
fi
|
|
121
|
+
if [ -n "$checkov_bin" ]; then
|
|
82
122
|
echo "Running checkov IaC security scan..."
|
|
83
123
|
|
|
84
124
|
# checkov 3.x exits 0 on "found failures" (we parse the output for those),
|
|
@@ -114,7 +154,7 @@ run_static_analysis() {
|
|
|
114
154
|
echo "Scanning Terraform files..."
|
|
115
155
|
for file in $tf_files; do
|
|
116
156
|
if [ -f "$file" ]; then
|
|
117
|
-
|
|
157
|
+
"$checkov_bin" --file "$file" $checkov_args >> "$checkov_log" 2>&1
|
|
118
158
|
record_checkov_run $? "$file"
|
|
119
159
|
fi
|
|
120
160
|
done
|
|
@@ -135,7 +175,7 @@ run_static_analysis() {
|
|
|
135
175
|
echo "Scanning Kubernetes manifests..."
|
|
136
176
|
for file in $k8s_only; do
|
|
137
177
|
if [ -f "$file" ]; then
|
|
138
|
-
|
|
178
|
+
"$checkov_bin" --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
|
|
139
179
|
record_checkov_run $? "$file"
|
|
140
180
|
fi
|
|
141
181
|
done
|
|
@@ -145,7 +185,7 @@ run_static_analysis() {
|
|
|
145
185
|
echo "Scanning GitHub Actions workflows..."
|
|
146
186
|
for file in $gh_files; do
|
|
147
187
|
if [ -f "$file" ]; then
|
|
148
|
-
|
|
188
|
+
"$checkov_bin" --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
|
|
149
189
|
record_checkov_run $? "$file"
|
|
150
190
|
fi
|
|
151
191
|
done
|
|
@@ -156,7 +196,7 @@ run_static_analysis() {
|
|
|
156
196
|
echo "Scanning Dockerfiles..."
|
|
157
197
|
for file in $docker_files; do
|
|
158
198
|
if [ -f "$file" ]; then
|
|
159
|
-
|
|
199
|
+
"$checkov_bin" --file "$file" $checkov_args >> "$checkov_log" 2>&1
|
|
160
200
|
record_checkov_run $? "$file"
|
|
161
201
|
fi
|
|
162
202
|
done
|
package/adapters/python.sh
CHANGED
|
@@ -180,17 +180,12 @@ run_lint() {
|
|
|
180
180
|
fi
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
run_architecture()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
echo "Running Python architecture checks (import-linter)..."
|
|
191
|
-
lint-imports
|
|
192
|
-
return $?
|
|
193
|
-
}
|
|
183
|
+
# REMOVED in #688: run_architecture()
|
|
184
|
+
# Never invoked -- its only would-be caller, route_to_adapter(), had no callers
|
|
185
|
+
# either. Gate 6 implements import-linter in pre-commit (grep "import-linter"),
|
|
186
|
+
# and more thoroughly: it detects 4 config carriers instead of the 2 checked
|
|
187
|
+
# here, and BLOCKs on a missing lint-imports rather than `|| return 0` passing.
|
|
188
|
+
# See githooks/adapter-common.sh for the full rationale.
|
|
194
189
|
|
|
195
190
|
run_tests() {
|
|
196
191
|
require_tool pytest "pytest" || return 1
|
package/bin/xp-gate.js
CHANGED
|
@@ -228,18 +228,25 @@ const COMMANDS = {
|
|
|
228
228
|
'update-skill': {
|
|
229
229
|
description: 'Update installed skill(s)',
|
|
230
230
|
run: subargs => {
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
// Extract the valueless flags BEFORE taking subargs[0] as the name.
|
|
232
|
+
//
|
|
233
|
+
// #347 fixed this for --all only, so `xp-gate update-skill --check` still
|
|
234
|
+
// took `--check` as a skill name and failed with "--check is not
|
|
235
|
+
// installed" (#725). Splitting the flags out once keeps the two cases from
|
|
236
|
+
// drifting apart again.
|
|
237
|
+
// Read the flag list from SKILL_FLAGS_BY_COMMAND so a flag added there is
|
|
238
|
+
// never mistaken for a skill name here -- that drift is what broke #347 and
|
|
239
|
+
// then #725 in the same place.
|
|
240
|
+
const flags = SKILL_FLAGS_BY_COMMAND['update-skill'] || [];
|
|
241
|
+
const present = flags.filter((f) => subargs.includes(f));
|
|
242
|
+
const rest = subargs.filter((s) => !flags.includes(s));
|
|
243
|
+
|
|
244
|
+
const options = parseOptions(rest.slice(1), 'update-skill');
|
|
245
|
+
for (const flag of present) {
|
|
246
|
+
options[flag.replace(/^--/, '')] = true;
|
|
242
247
|
}
|
|
248
|
+
// An explicit name wins for the single-skill path; --all/--check ignore it.
|
|
249
|
+
const name = present.length > 0 ? undefined : rest[0];
|
|
243
250
|
updateSkill(name, options).then(code => process.exit(code));
|
|
244
251
|
},
|
|
245
252
|
usage: 'xp-gate update-skill [<name>] [--all] [--check]'
|