Use when the active repository is a .NET solution (`.sln` / `.csproj` present) and the software-engineer must produce falsifiable evidence for the quality gates. Most gates are captured once, after the story's last work commit; the G10 RED capture is taken **at RED**, before any production code, and cannot be reconstructed later. Provides the concrete `dotnet` / `stryker` commands and how their outputs map onto the tech-agnostic schema.
Installs into .claude/skills of the current project.
Are you the author of Quality Gates Dotnet?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/sebastiendegodez-quality-gates-dotnet)
---
name: quality-gates-dotnet
description: Use when the active repository is a .NET solution (`.sln` / `.csproj` present) and the software-engineer must produce falsifiable evidence for the quality gates. Most gates are captured once, after the story's last work commit; the G10 RED capture is taken **at RED**, before any production code, and cannot be reconstructed later. Provides the concrete `dotnet` / `stryker` commands and how their outputs map onto the tech-agnostic schema.
---
# Quality Gates — .NET Adapter
Concrete recipes that bind the gates of `quality-gates-evidence-contract` to the
.NET toolchain. Loaded ONLY when the producer detects a .NET stack.
## Detection
Activate this adapter when ANY of the following resolves at the repo root:
- `*.sln` or `*.slnx`
- `Directory.Packages.props`
- `**/*.csproj`
If multiple stacks coexist, run each adapter and concatenate gate entries.
## Output paths (relative to repo root)
```
.copilot-tracking/skraft-plans/{projectSlug}/evidence/{date}/{story}/
```
Throughout this file, `$EV` is shorthand for that directory — one per story, never shared.
Create it before any redirect. Log references drop the `.copilot-tracking/skraft-plans/{projectSlug}/`
prefix: `evidence/{date}/{story}/qg-tests.stdout`.
## G1 / G2 — Tests pass
Run the full suite once and partition the results into two gate entries by
filtering the produced TRX, OR run the acceptance project alone for G1 and the
full suite for G2 (recommended on small repos).
```bash
mkdir -p "$EV"
dotnet test --nologo \
--logger "trx;LogFileName=qg-tests.trx" \
--results-directory "$EV" \
> "$EV/qg-tests.stdout" 2>&1
echo $? > "$EV/qg-tests.exit"
shasum -a 256 "$EV/qg-tests.stdout" | awk '{print $1}' > "$EV/qg-tests.stdout.sha256"
```
Populate the contract:
- `command_executed` = the verbatim line above
- `exit_code_ref` = `evidence/{date}/{story}/qg-tests.exit`
- `stdout_ref` = `evidence/{date}/{story}/qg-tests.stdout`
- `stdout_sha256` = contents of `qg-tests.stdout.sha256`
- `stdout_tail` = `tail -n 40 "$EV/qg-tests.stdout"`
- `metrics.tests_total` / `_passed` / `_failed` parsed from the TRX `<ResultSummary outcome="..." />`
attribute and `<Counters total="..." passed="..." failed="..." />`
## G3 — Build passes
Implicit in `dotnet test` for most repos. If the team wants an explicit gate:
```bash
dotnet build --nologo --no-incremental \
> "$EV/qg-build.stdout" 2>&1
echo $? > "$EV/qg-build.exit"
shasum -a 256 "$EV/qg-build.stdout" | awk '{print $1}' > "$EV/qg-build.stdout.sha256"
```
## G4 — Static analysis
Use the analyzers wired into the build (Roslyn analyzers, `TreatWarningsAsErrors=true`).
If the build is clean, G4 inherits its evidence from G3 and sets
`stdout_ref`/`exit_code_ref` to the G3 files. No fresh command needed.
## G5 — Architecture rules
If the repo carries a `*.ArchitectureTests` project (NetArchTest / ArchUnitNET):
```bash
dotnet test --nologo \
--filter "FullyQualifiedName~Architecture" \
--logger "trx;LogFileName=qg-arch.trx" \
--results-directory "$EV" \
> "$EV/qg-arch.stdout" 2>&1
echo $? > "$EV/qg-arch.exit"
shasum -a 256 "$EV/qg-arch.stdout" | awk '{print $1}' > "$EV/qg-arch.stdout.sha256"
```
If absent, mark G5 `status: "not_applicable"` with `rationale: "no architecture tests project"`.
## G6 — Mutation score
Mutation configuration is repository infrastructure, not an ephemeral agent command.
Scaffold it once from this adapter, review it, and commit both root files. The script
discovers scope inputs, then delegates JSON generation and defaults to official
`dotnet stryker init`; it does not hand-render Stryker's schema:
```bash
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/configure-mutation.sh" --root "$PWD"
```
The scripts ship with the plugin, never in the consumer repository: `$SKRAFT_PLUGIN_ROOT`
is the plugin root the SessionStart hook exports and states in the session context; where
the variable is empty, use that absolute path. The configure script writes:
- `stryker-config-core.json` — whole solution, Domain/Application source globs, 100.
- `stryker-config-boundary.json` — whole solution, API/Infrastructure source globs, 80.
Canonical mode requires `.Domain`, `.Application`, `.API`, and `.Infrastructure`
projects. Multiple bounded contexts are allowed. If one solution cannot be selected
unambiguously, pass `--solution`. For a BFF/non-standard layout, never invent missing
projects; pass explicit source globs for both scopes:
```bash
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/configure-mutation.sh" --root "$PWD" --solution Storefront.sln \
--core-mutate "**/Storefront/Core/**/*.cs" \
--boundary-mutate "**/Storefront/Adapters/**/*.cs"
```
Generation is idempotent. A differing existing config is preserved; `--force` is
allowed only after reviewing why regeneration should replace customization. Never add
`ignore-mutations` or broad exclusions to buy a score.
### Gate execution and evidence
Run checked-in configs through bundled wrappers. Each wrapper validates its config,
passes the whole solution to Stryker once, captures the aggregate JSON report, and
returns the gate verdict as its exit code. The verdict is the score recomputed from the
tested mutants: a report with zero mutants, a mutant no test ran against, or a full run
with no tested mutant fails. The last stdout line states the score or the failure:
```bash
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/mutation-core.sh" --root "$PWD" --evidence "$EV"
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/mutation-boundary.sh" --root "$PWD" --evidence "$EV"
```
Core MUST pass before boundary starts. `--config <path>` may select an equivalent custom
location. `--expected` is refused. Do NOT read a report score and judge it in prose.
Populate two G6 entries, one per scope:
- `{ "id": "G6", "scope": "core", … }` — `command_executed` the core line above,
`stdout_ref` / `exit_code_ref` = `evidence/{date}/{story}/qg-mutation.stdout` / `.exit`,
`stdout_sha256` = contents of `qg-mutation.stdout.sha256`;
- `{ "id": "G6", "scope": "boundary", … }` — same with `qg-mutation-boundary.*`.
A solution without API/Infrastructure projects records the boundary entry
`not_applicable` with its rationale.
### Cadence
- At the start of each TDD cycle, record `PREV=$(git rev-parse HEAD)` — the previous
cycle's last commit (for the first cycle it equals `phaseHistory.DELIVER.baseSha` from
`state.mjs get --field phaseHistory`).
- In the cycle's COMMIT & VERIFY, before the cycle's commit, skip mutation when neither
`git diff --name-only "$PREV"` (staged and unstaged changes) nor
`git ls-files --others --exclude-standard` (new files) lists a core production file.
Otherwise run the core wrapper with `--since "$PREV"`:
it mutates only what this cycle changed, and passes when no core mutant changed. Its
evidence is a checkpoint, never the G6 entry — write it to a scratch directory, not
`$EV`.
- Once, after the story's last work commit: the full core then boundary wrapper runs
into `$EV`. Those two runs are the G6 evidence.
### Local debugging
Developers can invoke the same root config directly; a narrow `--mutate` override is
diagnostic only.
```bash
dotnet stryker --config-file stryker-config-core.json
dotnet stryker --config-file stryker-config-boundary.json
```
## G7 — No mocks in Domain/Application
```bash
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/no-mocks-in-core.sh" --root "$PWD" --evidence "$EV"
```
The script scans every `*.Domain` and `*.Application` project at any depth, plus the
`*.Domain.*Tests`, `*.Application.*Tests` and `*.UnitTests` projects, for mocking
frameworks (Moq, NSubstitute, FakeItEasy, AutoMoq). It writes `qg-mocks.stdout` (one hit
per line, empty when clean), `.exit` and `.stdout.sha256`; its exit is the verdict.
Populate G7 with `status: "pass"` only on exit 0; `stdout_ref` = `evidence/{date}/{story}/qg-mocks.stdout`.
## G11 — Line coverage of Domain and Application
```bash
bash "$SKRAFT_PLUGIN_ROOT/skills/quality-gates-dotnet/scripts/coverage-core.sh" --root "$PWD" --evidence "$EV"
```
The script runs the solution's tests once with the XPlat Code Coverage collector
(`coverlet.collector`, present in the `dotnet new xunit` template; add it to a test project
that lacks it), sums the Cobertura line counts of every `*.Domain` and `*.Application`
package, and exits non-zero below 100%, when a test fails, or when no core package was
measured. The bar lives in the script; `--threshold` is refused. Populate G11 with its
`command_executed`, `stdout_ref` / `exit_code_ref` = `evidence/{date}/{story}/qg-coverage.stdout` /
`.exit` and `stdout_sha256` from `qg-coverage.stdout.sha256`.
## G8 — Conventional commits
Already enforceable from the Git tree alone — no fresh tool run. The producer fills
`commits_covered[].subject` from `git log --format='%s' <range>`; the lens checks
each full message against the contract's G8 rules.
## G9 — Test integrity (RED→GREEN snapshots)
For every TDD cycle, capture both snapshots when each commit lands:
```bash
mkdir -p "$EV/snapshots"
# at RED:
git show HEAD:tests/MonAssurance.UnitTests/Eligibilite/SomeTests.cs \
> "$EV/snapshots/red-1-SomeTests.cs"
# at GREEN (after the implementation commit):
git show HEAD:tests/MonAssurance.UnitTests/Eligibilite/SomeTests.cs \
> "$EV/snapshots/green-1-SomeTests.cs"
```
The producer records `red_commit`, `green_commit`, and the two snapshot paths in
the contract. The lens diffs the two snapshots and FAILS G9 if any line was
removed or mutated in a pre-existing assertion (only additions are allowed —
that is the Iron Rule of Tests, mechanically verifiable).
## G10 — RED observed
For every TDD cycle, capture the failing run **at RED**, before the
implementation lands. Run the G1/G2 test command narrowed to the cycle's test:
```bash
mkdir -p "$EV"
# at RED, for cycle {cycle} of story {story} — BEFORE writing the implementation:
dotnet test --nologo \
--filter "FullyQualifiedName~SomeTests" \
> "$EV/qg-red-{cycle}.stdout" 2>&1
echo $? > "$EV/qg-red-{cycle}.exit"
shasum -a 256 "$EV/qg-red-{cycle}.stdout" | awk '{print $1}' \
> "$EV/qg-red-{cycle}.stdout.sha256"
```
This capture CANNOT be reconstructed afterwards: once the implementation is in,
the same command returns green. Run it while the cycle is red or the evidence
does not exist.
Populate the matching entry of `test_integrity.cycles[]`:
- `red_stdout_ref` = `evidence/{date}/{story}/qg-red-{cycle}.stdout`
- `red_stdout_sha256` = contents of `qg-red-{cycle}.stdout.sha256`
- `red_exit_code_ref` = `evidence/{date}/{story}/qg-red-{cycle}.exit`
G10's `gates[]` entry carries `status` (and `rationale`) **only** — no
`command_executed`, no `exit_code_ref`. One story runs N RED commands but has a
single gate entry, and the generic "exit code MUST be `0` for pass" rule is
inverted here; the per-cycle `red_*` fields above are where its evidence lives.
The recorded exit code MUST be NON-zero: a `0` means the test never failed and
G10 is `status: "fail"`. G10 attests the RED *run*, nothing about commit SHAs —
G9 keeps the commit/snapshot job unchanged.
## Producer flow at the end of the story
1. `mkdir -p "$EV/snapshots"`.
2. Run G1/G2, G3 (if separate), G4 (if separate), G5, G6 core then boundary, G7, G11 —
each through its command or script, redirecting stdout and exit code into `$EV`.
3. For each cycle, dump RED + GREEN snapshots from `git show`.
4. For each cycle, check the G10 RED captures taken at RED time are present in `$EV`
(`qg-red-{cycle}.stdout` / `.exit` / `.stdout.sha256`) with a non-zero exit. They are
NOT re-runnable here — a missing capture is `status: "fail"`, never `not_applicable`.
5. `repo_root_rev = git rev-parse HEAD` — the last work commit.
6. Build `commits_covered[]` from `git log --format='%H%x09%s' {DELIVER baseSha}..HEAD` and
`git show --name-only --format= <sha>` per commit: every commit since DELIVER started.
7. Assemble `$EV/qg-{story}.json` per `quality-gates-evidence-contract` (v4).
8. Commit `$EV` alone: `git add "$EV" && git commit -s -m 'chore({feature}): record quality evidence for {story}'`.
9. `node "$SKRAFT_PLUGIN_ROOT/src/cli/qg-verify.mjs" --log "$EV/qg-{story}.json"` must print
`"verdict": "pass"`; otherwise fix the gate or the log, never the verifier's input.
If a tool is unavailable in the environment (no Stryker installed, no SDK), the
gate is `status: "fail"` with the captured stderr — NOT `not_applicable`. The
contract treats unverifiable gates strictly so the lens can collapse them to
`inconclusive` upstream.