testguard-cli 0.4.0 → 0.6.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/CHANGELOG.md +291 -0
- package/README.md +321 -27
- package/package.json +1 -1
- package/spec/GATE-SEMANTICS.md +109 -1
- package/spec/README.md +4 -2
- package/spec/lib/validate.mjs +75 -1
- package/spec/schemas/baseline.schema.json +8 -0
- package/spec/schemas/brief.schema.json +11 -1
- package/spec/schemas/common.schema.json +32 -1
- package/spec/schemas/evidence.schema.json +374 -63
- package/spec/schemas/gate.schema.json +52 -0
- package/spec/schemas/replay.schema.json +85 -0
- package/spec/schemas/status.schema.json +269 -45
- package/src/admit/admit.mjs +17 -0
- package/src/baseline/baseline.mjs +31 -2
- package/src/brief/brief.mjs +96 -8
- package/src/claims/removed.mjs +161 -0
- package/src/cli.mjs +69 -12
- package/src/commands/admit.mjs +116 -0
- package/src/commands/baseline.mjs +17 -3
- package/src/commands/brief.mjs +26 -12
- package/src/commands/claims.mjs +35 -6
- package/src/commands/gate.mjs +47 -0
- package/src/commands/init.mjs +19 -6
- package/src/commands/mcp.mjs +12 -0
- package/src/commands/probe.mjs +12 -3
- package/src/commands/replay.mjs +63 -0
- package/src/commands/status.mjs +4 -1
- package/src/gate/changed.mjs +222 -0
- package/src/git.mjs +5 -0
- package/src/init/init.mjs +182 -29
- package/src/init/templates/SKILL.md +21 -3
- package/src/mcp/server.mjs +104 -0
- package/src/mcp/tools.mjs +114 -0
- package/src/probe/contention.mjs +60 -0
- package/src/probe/discover.mjs +12 -3
- package/src/probe/independence.mjs +59 -0
- package/src/probe/mocks.mjs +120 -0
- package/src/probe/probe.mjs +126 -18
- package/src/probe/rank.mjs +73 -15
- package/src/probe/runners/index.mjs +49 -5
- package/src/probe/runners/jest.mjs +3 -3
- package/src/probe/runners/playwright.mjs +133 -0
- package/src/probe/runners/shared.mjs +70 -13
- package/src/probe/runners/vitest.mjs +4 -3
- package/src/render.mjs +2 -2
- package/src/replay/label.mjs +64 -0
- package/src/replay/replay.mjs +309 -0
- package/src/scaffold/producers.mjs +217 -6
- package/src/scaffold/scaffold.mjs +9 -3
- package/src/status/status.mjs +88 -7
package/README.md
CHANGED
|
@@ -44,6 +44,19 @@ re-implemented the authorization logic *inside the test* and asserted
|
|
|
44
44
|
against the copy: fifteen green tests, zero detection. After wrapper-level
|
|
45
45
|
tests were written against the survivors, 39/39 were killed.
|
|
46
46
|
|
|
47
|
+
The peer-reviewed picture in 2026 says the same thing from the other side.
|
|
48
|
+
Coverage and mutation score of LLM-generated suites track effectiveness only
|
|
49
|
+
when the code under test is assumed correct; once it may be buggy they "no
|
|
50
|
+
longer serve as reliable indicators" ([Zhao, Zhou and Cohen, ISSTA 2026](https://arxiv.org/abs/2607.22880)).
|
|
51
|
+
Buggy code steers a model toward tests that assert the bug, and prompting
|
|
52
|
+
with the specification is the mitigation that works ([arXiv 2607.22883](https://arxiv.org/abs/2607.22883)) —
|
|
53
|
+
which is why a claim here comes from intent, a fault is bound to the claim,
|
|
54
|
+
and the brief hands the agent the claim before it writes. And agents
|
|
55
|
+
saturate whatever tests they can see, with the gap to held-out tests
|
|
56
|
+
growing about 28 points per tenfold increase in code size ([SpecBench](https://arxiv.org/abs/2605.21384)).
|
|
57
|
+
Every generator on the market admits a test because it compiles, passes and
|
|
58
|
+
raises coverage. TestGuard admits it because it fails when the claim is false.
|
|
59
|
+
|
|
47
60
|
## Install
|
|
48
61
|
|
|
49
62
|
| How | Command |
|
|
@@ -52,8 +65,9 @@ tests were written against the survivors, 39/39 were killed.
|
|
|
52
65
|
| npm | `npm i -D testguard-cli` then `npx testguard probe` |
|
|
53
66
|
| pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
|
|
54
67
|
| Homebrew | `brew tap raccioly/tap && brew install testguard` |
|
|
55
|
-
| GitHub Action | `uses: raccioly/testguard@v0.
|
|
68
|
+
| GitHub Action | `uses: raccioly/testguard@v0.6.0` — see [`action.yml`](./action.yml) |
|
|
56
69
|
| pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
|
|
70
|
+
| GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.5.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
|
|
57
71
|
|
|
58
72
|
Projects that set `min-release-age` in `.npmrc` cannot see a version published
|
|
59
73
|
less than that many days ago (`ENOVERSIONS`); install that one with
|
|
@@ -68,7 +82,9 @@ npx testguard-cli claims # what does this project claim, and is every claim
|
|
|
68
82
|
npx testguard-cli probe # try to falsify each claim; report what the tests missed
|
|
69
83
|
npx testguard-cli baseline # freeze today's unproven findings; from now on only new ones gate
|
|
70
84
|
npx testguard-cli brief # tell the agent where the suite is blind, before it writes
|
|
85
|
+
npx testguard-cli gate --changed origin/main # fail when a changed source file carries no claim at all
|
|
71
86
|
npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to keep or drop
|
|
87
|
+
npx testguard-cli admit test/x.test.ts --claim X # is this test green on HEAD and does it fail on every fault of X? ADMITTED or NOT ADMITTED
|
|
72
88
|
```
|
|
73
89
|
|
|
74
90
|
1. **Claims** live in `testguard.claims.json` (editors validate it against
|
|
@@ -82,7 +98,12 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
|
|
|
82
98
|
2. **Probe** confirms the defenders are green N times unmodified, applies
|
|
83
99
|
each fault in a scratch git worktree (your tree is never touched), runs
|
|
84
100
|
the defenders N times, re-runs survivors against the whole suite with
|
|
85
|
-
N-run attribution
|
|
101
|
+
N-run attribution (up to N: it stops as soon as no test has failed in
|
|
102
|
+
every run, because from there none can be named a killer), restores, and
|
|
103
|
+
classifies. It warns when another test runner is already running — a
|
|
104
|
+
contended machine turns a slow suite into a `TIMEOUT` verdict about the
|
|
105
|
+
load, not the claim — records that on the evidence, and offers `--serial`
|
|
106
|
+
to run one test file at a time. Verdicts are a closed set:
|
|
86
107
|
|
|
87
108
|
| Verdict | Meaning |
|
|
88
109
|
|---|---|
|
|
@@ -92,7 +113,7 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
|
|
|
92
113
|
| `UNVERIFIABLE` | the fault's anchor is missing or ambiguous — loud, never a skip |
|
|
93
114
|
| `TIMEOUT` | the defenders hung; a hang is not a detection |
|
|
94
115
|
| `FAULT-INVALID` | the replacement does not load — a bad fault, not a finding |
|
|
95
|
-
| `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
|
|
116
|
+
| `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs — `detail.flakeRate` says how often (`failures` of `runs` on unmodified source) |
|
|
96
117
|
|
|
97
118
|
Never a single score. Findings are ranked by severity, claim provenance
|
|
98
119
|
and blast radius (relative imports, `tsconfig` path aliases and
|
|
@@ -101,20 +122,48 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
|
|
|
101
122
|
it is written.
|
|
102
123
|
|
|
103
124
|
Worktree mode probes a **commit**. If a defender or target file has
|
|
104
|
-
uncommitted changes
|
|
105
|
-
tests would be silently absent and the
|
|
106
|
-
with no hint why. `--include-dirty`
|
|
107
|
-
edits and new files) into a throwaway
|
|
108
|
-
HEAD and index are never touched.
|
|
125
|
+
uncommitted changes and you asked for the implicit HEAD, `probe` refuses
|
|
126
|
+
and says so — otherwise your new tests would be silently absent and the
|
|
127
|
+
same survivors would come back with no hint why. `--include-dirty`
|
|
128
|
+
snapshots the working tree (tracked edits and new files) into a throwaway
|
|
129
|
+
commit and probes that; your tree, HEAD and index are never touched. An
|
|
130
|
+
**explicit `--ref`** (a pre-fix commit in a post-mortem, say) is honoured
|
|
131
|
+
over a dirty tree, as is `--ignore-dirty`: a warning names the files and
|
|
132
|
+
the evidence records them as `repo.ignoredDirty`, so the run says what it
|
|
133
|
+
did not look at. Every summary names the commit probed.
|
|
109
134
|
|
|
110
135
|
A claim with no `defendedBy` has its defenders **discovered**: the test
|
|
111
|
-
files that import the fault's target, by relative path or resolved alias
|
|
112
|
-
`
|
|
136
|
+
files that import the fault's target, by relative path or resolved alias
|
|
137
|
+
(tsconfig `paths`, through `extends` and `references`, vite/vitest
|
|
138
|
+
`resolve.alias`, package.json `imports`) — **minus the files that mock
|
|
139
|
+
it**. A test that `vi.mock`s / `jest.mock`s the target cannot detect any
|
|
140
|
+
fault in it; counting it would make `NOCOVER` under-report and waste runs.
|
|
141
|
+
`NOCOVER` therefore means exactly "no test file imports this source
|
|
142
|
+
without mocking it". `claims` prints the split (`18 import · 16 mock · 2
|
|
143
|
+
can detect`), the evidence lists the mocking files, and a mocking file
|
|
144
|
+
that never `expect(...)`s anything imported from the target carries the
|
|
145
|
+
static signal `mocked-never-asserted` — the cheapest blind-spot signal
|
|
146
|
+
there is, and the exact signature of one escaped bug in the field
|
|
147
|
+
reports. Silence it, visibly, with `// unasserted: <why>` above the mock.
|
|
113
148
|
|
|
114
149
|
Runners: **vitest** and **jest** (`--runner auto` picks the first that
|
|
115
|
-
resolves; both read the same jest-compatible JSON report).
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
resolves; both read the same jest-compatible JSON report). A runner is
|
|
151
|
+
resolved from the **project's own package** first — its pinned version,
|
|
152
|
+
its own bin script — and only then from an executable on PATH, which the
|
|
153
|
+
evidence records as `runner.source: "path"`; `npx` is never asked,
|
|
154
|
+
because its cache answers for packages a project does not have. Anything
|
|
155
|
+
else goes through `--runner-cmd`. Each runner is proven against its own
|
|
156
|
+
copy of the known-answer fixture.
|
|
157
|
+
|
|
158
|
+
The two-gate rule, as one verb: `testguard admit <test-file> --claim <ID>`
|
|
159
|
+
runs the claim's faults against your uncommitted test and answers
|
|
160
|
+
`ADMITTED` (exit 0: the test is green on unmodified HEAD and fails on
|
|
161
|
+
every fault, N/N) or `NOT ADMITTED` (exit 1: the first blocking fault and
|
|
162
|
+
what to do about it). The test must be a declared or discovered defender
|
|
163
|
+
of the claim. It is sugar over `probe --claim <ID> --include-dirty`, so
|
|
164
|
+
it can never disagree with the gate. Every generator on the market admits
|
|
165
|
+
a test because it compiles, passes and raises coverage; `admit` admits it
|
|
166
|
+
because it fails when the claim is false.
|
|
118
167
|
|
|
119
168
|
Practical loop: first pass `--no-escalate` (escalation re-runs the whole
|
|
120
169
|
suite N times per survivor); iterate on one claim with `--claim <ID>` and
|
|
@@ -131,20 +180,237 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
|
|
|
131
180
|
3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
|
|
132
181
|
what was already known and exit non-zero only on what is new. Claims whose
|
|
133
182
|
source and defenders are unchanged reuse their prior verdict, so a probe
|
|
134
|
-
in CI costs only what changed.
|
|
183
|
+
in CI costs only what changed. A baseline frozen from `--include-dirty`
|
|
184
|
+
evidence records the snapshot and points at the *parent* of the commit
|
|
185
|
+
that will carry your tests; after you commit, a clean `probe` plus
|
|
186
|
+
`baseline --restamp` moves it to that commit — only when the fingerprints
|
|
187
|
+
are identical, never otherwise. `status` notes a baseline that predates
|
|
188
|
+
HEAD without making it a state.
|
|
135
189
|
4. **Brief** turns evidence plus baseline into a ranked, capped
|
|
136
190
|
`## TEST BLINDSPOT CONTEXT` block, printed and also written to
|
|
137
|
-
`.testguard/brief.json` (`--text` prints only
|
|
191
|
+
`.testguard/brief.json` (`--text` prints only; `--markdown` prints the same brief as a
|
|
192
|
+
merge-request note for the human reviewer, unclaimed changes first). Wire it into an agent's session start
|
|
138
193
|
— for Claude Code, in `.claude/settings.json`:
|
|
139
194
|
|
|
140
195
|
```json
|
|
141
196
|
{ "hooks": { "SessionStart": [ { "hooks": [
|
|
142
|
-
{ "type": "command", "command": "
|
|
197
|
+
{ "type": "command", "command": "node_modules/.bin/testguard brief --text 2>/dev/null || { command -v testguard >/dev/null 2>&1 && testguard brief --text 2>/dev/null; } || true" }
|
|
143
198
|
] } ] } }
|
|
144
199
|
```
|
|
145
200
|
|
|
146
|
-
`--text` prints only, and exits 0 silently when there is no evidence yet
|
|
147
|
-
|
|
201
|
+
`--text` prints only, and exits 0 silently when there is no evidence yet;
|
|
202
|
+
the command resolves the project's own install, then a `testguard` on
|
|
203
|
+
`PATH`, and ends in `true` — so the hook can never break a session. There
|
|
204
|
+
is no `npx` in it, in any form: `--no-install` resolves the package from
|
|
205
|
+
the registry before declining to install it, so it is quiet rather than
|
|
206
|
+
offline. The brief's first line says which install answered
|
|
207
|
+
(`local install` or `global`), so a stale one is visible.
|
|
208
|
+
|
|
209
|
+
### Would this suite have caught the bugs that already escaped?
|
|
210
|
+
|
|
211
|
+
An injected fault is a fault somebody thought of. A bug that actually shipped
|
|
212
|
+
is ground truth: a human already confirmed it was a defect, and there is no
|
|
213
|
+
equivalent-mutant argument to have about it.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx testguard-cli replay --since HEAD~50..HEAD --max 10
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For each fix commit in the range — one that changes source **and** a test
|
|
220
|
+
together — `replay` checks it out in a scratch worktree, reverts only its
|
|
221
|
+
source files to the parent, **deletes the test the fix shipped** (that test
|
|
222
|
+
proves nothing about what the suite knew before it existed), and runs the
|
|
223
|
+
tests that import the reverted code:
|
|
224
|
+
|
|
225
|
+
| Verdict | Meaning |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `caught` | a remaining test failed by assertion, every run. The suite knew. |
|
|
228
|
+
| `blind` | the suite stayed green on known-broken code. |
|
|
229
|
+
| `nocover` | no test imports the reverted files — worse than blind. |
|
|
230
|
+
| `flaky` | the runs disagreed, so nothing can be concluded. A flaky failure reads as detection, which biases this metric *optimistically*; mixed runs are never `caught`. |
|
|
231
|
+
| `unverifiable` | the revert did not apply, or the suite could not load. |
|
|
232
|
+
|
|
233
|
+
One patch counts once (`git patch-id`), because a dual-branch topology
|
|
234
|
+
carries the same fix under two or three shas. **`replay` reports and never
|
|
235
|
+
gates**: a bug that escaped is history, not a regression in this change.
|
|
236
|
+
|
|
237
|
+
Each replayed bug is also labelled with the injected-fault class its diff
|
|
238
|
+
most resembles, which is the join key that turns an uninterpretable mutation
|
|
239
|
+
score into a statement with a sample size:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
guard-removed blind 7/9 p=0.78 ci [0.45, 0.94]
|
|
243
|
+
field-dropped blind 4/4 p=1.00 ci [0.51, 1.00]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Read as *"when a fault of this class survives, how often does that
|
|
247
|
+
correspond to a bug that really escaped"*. It is written as a
|
|
248
|
+
`calibration` document beside the replay one. Only `caught` and `blind`
|
|
249
|
+
carry information; the other verdicts are excluded from both sides of the
|
|
250
|
+
ratio.
|
|
251
|
+
|
|
252
|
+
**The open question this exists to answer.** Does a calibration learned on a
|
|
253
|
+
repository *with* history transfer to a greenfield one that has none? AI
|
|
254
|
+
authored code has no history, so replay cannot help it directly and
|
|
255
|
+
calibration is the only bridge. That transfer is unproven, it is the core
|
|
256
|
+
product bet, and this is the instrument for testing it — not the answer.
|
|
257
|
+
|
|
258
|
+
### Read CI's evidence locally
|
|
259
|
+
|
|
260
|
+
Verdict reuse makes a probe cheap, but the evidence lives where `probe` ran
|
|
261
|
+
and is gitignored. On a fresh clone, or on a laptop where CI does the
|
|
262
|
+
probing, the session-start brief is empty and `status` says `unprobed` while
|
|
263
|
+
the default branch has full evidence. Point either command at CI's document:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
testguard status . --evidence .testguard/ci/ci-self-evidence.json
|
|
267
|
+
testguard brief . --text --evidence .testguard/ci/ci-self-evidence.json
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
A foreign document is not trusted blindly. `status` marks it
|
|
271
|
+
`evidenceSource: provided`, prints the commit it describes next to the
|
|
272
|
+
commit in your tree, and still computes staleness from the recorded input
|
|
273
|
+
hashes — so a file you have edited since CI probed it goes `evidence-stale`
|
|
274
|
+
for exactly those claims.
|
|
275
|
+
|
|
276
|
+
`testguard init --ci-evidence github` (or `gitlab`) writes
|
|
277
|
+
`.testguard/fetch-ci-evidence.sh`, which downloads the branch-named artifact
|
|
278
|
+
with the platform CLI you already have and then briefs from it. **It is an
|
|
279
|
+
on-demand helper, not a hook**: the session-start hook never touches the
|
|
280
|
+
network, and the helper exits 0 with a message when the CLI or the artifact
|
|
281
|
+
is missing. TestGuard itself still makes no network calls.
|
|
282
|
+
|
|
283
|
+
### Every change needs a claim
|
|
284
|
+
|
|
285
|
+
`probe` can only verify claims that exist. Every escaped defect in the field
|
|
286
|
+
reports so far was a **claim gap**: the feature shipped green with zero
|
|
287
|
+
claims, and a verifier with no claim about a feature is silent about it by
|
|
288
|
+
construction. `gate` closes that hole on the delta:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npx testguard-cli gate --changed origin/main # in a PR: the files changed since the base branch
|
|
292
|
+
npx testguard-cli gate --changed HEAD --include-dirty # before a commit: the working tree, staged or not
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Every changed source file must carry a fault, resolve as a defender of a
|
|
296
|
+
claim (test files), or be excused by an unexpired `path` entry in
|
|
297
|
+
`testguard.ignore.json` — with a reason a reviewer will accept. One
|
|
298
|
+
unclaimed file exits `1`; there is no percentage. Every reliance on an ignore
|
|
299
|
+
entry is printed, so a reviewer sees *why* the gate passed; an expired entry
|
|
300
|
+
excuses nothing. Non-source files and documented never-claimed patterns
|
|
301
|
+
(`*.d.ts`, `*.config.*`, fixtures, mocks; `--explain` lists them) are
|
|
302
|
+
excluded and said so; `--strict` fails a change that evaluated nothing.
|
|
303
|
+
|
|
304
|
+
With a reference known, `status --changed <ref>` reports `unclaimed-changes`
|
|
305
|
+
**before** any evidence state and makes the claim the next action; the brief
|
|
306
|
+
lists the unclaimed files first. The claim is written before more code.
|
|
307
|
+
|
|
308
|
+
**In CI the base is detected** — GitHub Actions (`GITHUB_BASE_REF`) and GitLab
|
|
309
|
+
merge request pipelines (`CI_MERGE_REQUEST_DIFF_BASE_SHA`, then
|
|
310
|
+
`CI_MERGE_REQUEST_TARGET_BRANCH_NAME`); `TESTGUARD_CHANGED_REF` overrides both.
|
|
311
|
+
The base must exist locally: GitHub — `actions/checkout` with `fetch-depth: 0`;
|
|
312
|
+
GitLab — the diff base sha needs nothing extra on a merge request pipeline,
|
|
313
|
+
the branch name needs `GIT_DEPTH: 0` or a `git fetch origin <target>`. A
|
|
314
|
+
detected base that does not resolve is a warning for `status` and `brief`
|
|
315
|
+
(they keep working) and an error for `gate` (its whole job is the measurement).
|
|
316
|
+
|
|
317
|
+
```yaml
|
|
318
|
+
# GitHub Actions
|
|
319
|
+
- uses: actions/checkout@v4
|
|
320
|
+
with: { fetch-depth: 0 }
|
|
321
|
+
- uses: raccioly/testguard@v0.6.0
|
|
322
|
+
with: { command: gate }
|
|
323
|
+
|
|
324
|
+
# GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
|
|
325
|
+
include:
|
|
326
|
+
- remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.5.0/packaging/gitlab/testguard.gitlab-ci.yml'
|
|
327
|
+
inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The template carries `spec: inputs:` (`version`, `dir`, `image`, `severity`,
|
|
331
|
+
`confirm`, `budget`, `no_escalate`, `strict`, `post_note`, `stage`), so
|
|
332
|
+
mirrored into a GitLab project it is a catalog component that a compliance
|
|
333
|
+
framework can require on every project in a group. The CLI itself never
|
|
334
|
+
talks to the network; only the job posts, and only when told to.
|
|
335
|
+
|
|
336
|
+
### Properties
|
|
337
|
+
|
|
338
|
+
- **Deterministic measurement.** No model decides a verdict. Faults are
|
|
339
|
+
string substitutions from a reviewed file; verdicts come from the test
|
|
340
|
+
runner's structured report, confirmed N times. The same inputs give the
|
|
341
|
+
same evidence, which is what makes it replayable for an auditor.
|
|
342
|
+
- **No network, no telemetry.** The CLI never opens a socket. The
|
|
343
|
+
session-start hook resolves an installed binary and never fetches one.
|
|
344
|
+
Nothing leaves the machine unless a CI job you configured posts it.
|
|
345
|
+
- **Artifacts are data in the repository.** Claims, evidence, baseline and
|
|
346
|
+
brief are JSON validated against a published schema before they are
|
|
347
|
+
written. Nothing is healed, regenerated or re-targeted at run time; a
|
|
348
|
+
claims file is code, and is reviewed like code.
|
|
349
|
+
- **Never optimistic.** A timeout, a load failure, a mixed N-run result, a
|
|
350
|
+
flaky defender or a missing anchor is reported as unproven, never rounded
|
|
351
|
+
toward green. The one pass is `killed`; everything else gates.
|
|
352
|
+
- **One exact-pinned runtime dependency** (`ajv`), Node ≥ 20, MIT.
|
|
353
|
+
|
|
354
|
+
### What TestGuard is not
|
|
355
|
+
|
|
356
|
+
- **Not a test generator.** It judges a test the agent wrote (`admit`); the
|
|
357
|
+
generating half stays with the agent, where the market is putting it.
|
|
358
|
+
- **Not a mutation-score dashboard.** No blanket mutants, no single score, no
|
|
359
|
+
threshold. Faults are few and bound to stated claims; findings are ranked,
|
|
360
|
+
never summed.
|
|
361
|
+
- **Not a self-healing runner.** A defender that adapts itself to a change is
|
|
362
|
+
a defender that did not observe it; TestGuard reports that, it does not do
|
|
363
|
+
it.
|
|
364
|
+
- **Not a coverage tool.** A line executed says nothing about whether an
|
|
365
|
+
assertion would notice. `NOCOVER` here means no test even imports the
|
|
366
|
+
file; everything above that is measured by injecting the fault.
|
|
367
|
+
|
|
368
|
+
A claim that **disappeared** is invisible to all of the above: `probe`
|
|
369
|
+
verifies the claims that exist, `gate` sees the source file covered by some
|
|
370
|
+
other claim, and `status` says clean. Deleting a claim is therefore cheaper
|
|
371
|
+
than weakening its fault — and `changedFaults` exists precisely because
|
|
372
|
+
weakening was the cheap escape. So:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npx testguard-cli claims --since origin/main # what existed there and does not now
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`removed-claim` and `removed-fault` gate; a rename that keeps the statement
|
|
379
|
+
verbatim is reported as `renamed-claim` and does not. Where evidence is
|
|
380
|
+
present, the line names the verdict the claim last had, because *"it was
|
|
381
|
+
SURVIVED when it was removed"* is the sentence that matters. The escape hatch
|
|
382
|
+
is the same one as everywhere else: a `claim` entry in
|
|
383
|
+
`testguard.ignore.json` with a reason, expiring if you give it an `expires`.
|
|
384
|
+
Removal is allowed — claims can be wrong, superseded or split — and it is
|
|
385
|
+
never silent.
|
|
386
|
+
|
|
387
|
+
### Run it from any harness
|
|
388
|
+
|
|
389
|
+
The operating loop above lives in a Claude Code skill and a session-start
|
|
390
|
+
hook. Both vanish the moment the harness is Cursor, Codex, Devin or whatever
|
|
391
|
+
comes next — and the harness is exactly the layer most likely to change. So
|
|
392
|
+
the same documents are also served over the Model Context Protocol:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
npx testguard-cli mcp # JSON-RPC 2.0 on stdio
|
|
396
|
+
npx testguard-cli init --mcp # prints the config for Claude Code, Cursor and Codex
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Five tools, all **read-only**: `testguard_status`, `testguard_brief`,
|
|
400
|
+
`testguard_claims`, `testguard_evidence`, `testguard_next_command`.
|
|
401
|
+
|
|
402
|
+
**No tool runs a probe.** A probe is long-running, budgeted, and the person
|
|
403
|
+
should see it happen, so `next_command` hands back the exact shell line for
|
|
404
|
+
the agent to run in its own terminal. Nothing here writes a file either —
|
|
405
|
+
editing a claims file through a connector would defeat the point of recording
|
|
406
|
+
every fault edit. The server declares only a `tools` capability: no prompts,
|
|
407
|
+
no resources, no sampling.
|
|
408
|
+
|
|
409
|
+
It is hand-written against a pinned protocol version rather than built on the
|
|
410
|
+
official SDK, because this tool has one exact-pinned runtime dependency and
|
|
411
|
+
keeps it that way. The surface is three methods and five tools and will not
|
|
412
|
+
grow; tests speak the wire format to a real child process and check each
|
|
413
|
+
tool against the CLI's own `--json` output, so the two cannot drift.
|
|
148
414
|
|
|
149
415
|
### Built for agents to run
|
|
150
416
|
|
|
@@ -156,11 +422,30 @@ things make that safe:
|
|
|
156
422
|
working tree. Every human rendering — the CLI text, the session-start
|
|
157
423
|
brief, the skill — derives from it, so they cannot disagree. Every command
|
|
158
424
|
accepts `--json`.
|
|
159
|
-
- **An installable operating loop.** `testguard init` writes
|
|
425
|
+
- **An installable operating loop.** `testguard init [dir]` writes the
|
|
426
|
+
**agent layer at the git root**, where agent sessions run —
|
|
160
427
|
`.claude/skills/testguard/SKILL.md` (state → action, verdict → the only
|
|
161
428
|
acceptable fix, the two-gate rule for any test the agent writes), the
|
|
162
|
-
`brief --text` session-start hook, an `AGENTS.md` section and the
|
|
163
|
-
`.gitignore` lines.
|
|
429
|
+
`brief --text` session-start hook, an `AGENTS.md` section — and the
|
|
430
|
+
**project layer** (`.gitignore` lines) beside the claims file. A second
|
|
431
|
+
project in the same repository adds a hook line and an `AGENTS.md`
|
|
432
|
+
bullet; `--here` keeps everything in the subdirectory. The hook
|
|
433
|
+
resolves the project's `node_modules/.bin/testguard`, then the git root's,
|
|
434
|
+
then a `testguard` on `PATH` via `command -v`, then does nothing — **no
|
|
435
|
+
`npx` in any form, so no path to the network**; a pre-0.6 `npx -y` or
|
|
436
|
+
`npx --no-install` hook is replaced. A written file that
|
|
437
|
+
`.gitignore` swallows is reported, not offered for commit. Idempotent.
|
|
438
|
+
- **Independence is recorded (L3).** `probe` measures *power* — would this
|
|
439
|
+
test notice if the code were wrong. It also records, on every kill, whether
|
|
440
|
+
the killing test was last touched by the same change (or the same author) as
|
|
441
|
+
the code it guards: `detail.independence` is `co-authored`,
|
|
442
|
+
`separate-change` or `unknown`. A co-authored kill is legitimate — a fix
|
|
443
|
+
*should* ship with its regression test — but a repository where every kill
|
|
444
|
+
is co-authored has no independent verification, whatever its claim
|
|
445
|
+
verification rate says. It is a **signal**: ranking reads it, verdicts never
|
|
446
|
+
do, and the brief says it in one line. Nothing else in this category
|
|
447
|
+
measures it, and agents saturate the tests they can see
|
|
448
|
+
([SpecBench](https://arxiv.org/abs/2605.21384)).
|
|
164
449
|
- **Gaming is visible.** The cheapest way to make a survivor disappear is to
|
|
165
450
|
weaken its fault, not to write a test. Evidence records every fault's
|
|
166
451
|
content hash; `status` lists any fault edited after it survived, with its
|
|
@@ -170,7 +455,7 @@ things make that safe:
|
|
|
170
455
|
### Authoring faults mechanically
|
|
171
456
|
|
|
172
457
|
Writing faults by hand means reading the code to find exact anchors. Two
|
|
173
|
-
field reports found that ~80% of hand-written faults are one of
|
|
458
|
+
field reports found that ~80% of hand-written faults are one of nine shapes,
|
|
174
459
|
so `scaffold` proposes them for you:
|
|
175
460
|
|
|
176
461
|
```bash
|
|
@@ -185,6 +470,10 @@ npx testguard-cli scaffold src/auth.ts --claim AUTH-ADMIN # every proposal und
|
|
|
185
470
|
| `return-altered` | `return <check>;` (`===`, `.includes(`, `&&`, …) → `return true;` |
|
|
186
471
|
| `literal-changed` | `httpOnly`/`secure` flipped, `sameSite` → `none`, a cost/rounds → `1`, a ttl/tolerance/window/limit ×1000 |
|
|
187
472
|
| `call-removed` | a bare `verify…()` / `validate…()` / `check…()` / `authorize…()` call removed |
|
|
473
|
+
| `field-dropped` | a field removed from an object that is returned, built by an arrow, assigned to a payload-ish name, passed to a `save`/`update`/`send`/`write`/… call, or is a `z.object({…})`-style schema; a string entry removed from an allow-list array; a `...base,` line or inline `{ ...base, … }` merge dropped. Only a line that can go on its own (balanced, comma-terminated or followed by the closer); never inside tests, fixtures or migrations |
|
|
474
|
+
| `argument-swapped` | a call kept, its first argument swapped for `undefined` (and `{}` when the argument is itself a call) — only when that argument is derived from a parameter of the enclosing function or a request-like value (`req`, `ctx`, `event`, …). The seam fault: `decide(deriveFrom(input), now)` → `decide(undefined, now)` |
|
|
475
|
+
| `element-removed` | a one-line JSX element — self-closing (`<Toggle … />`) or paired (`<button …>Save</button>`) — removed; the UI shape behind "the toggle is invisible", killable by a browser-layer defender |
|
|
476
|
+
| `handler-dropped` | an `on<Event>={…}` prop removed, whether it is its own line or inline in the tag; the control renders and does nothing |
|
|
188
477
|
|
|
189
478
|
Every proposal's `find` is the exact line with `expectHits`/`occurrence`
|
|
190
479
|
computed from the file, so it is verifiable by construction; provenance is
|
|
@@ -220,13 +509,18 @@ through every verdict.
|
|
|
220
509
|
|
|
221
510
|
## Status
|
|
222
511
|
|
|
223
|
-
**v0.
|
|
224
|
-
|
|
225
|
-
|
|
512
|
+
**v0.5.** Eleven commands (`status`, `init`, `claims`, `probe`, `admit`, `replay`, `baseline`, `brief`, `gate`, `scaffold`, `mcp`), vitest and jest runners, hand-authored faults plus
|
|
513
|
+
**v0.5.** Nine commands (`status`, `init`, `claims`, `probe`, `admit`, `baseline`, `brief`, `gate`, `scaffold`), vitest, jest and Playwright runners, hand-authored faults plus
|
|
514
|
+
a mechanical scaffold, an agent operating layer (`status`, `init`) and a
|
|
515
|
+
change gate (`gate`). The contract
|
|
516
|
+
spine — eight JSON Schemas shared with the other Guard tools — is under
|
|
226
517
|
[`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
|
|
227
518
|
|
|
228
|
-
Not yet: test generation (the
|
|
229
|
-
vitest and jest, AST-aware producers, and
|
|
519
|
+
Not yet: test generation (the acceptance half, `admit`, exists; the generating half stays the agent's), runners beyond
|
|
520
|
+
vitest and jest, AST-aware producers, and the transfer of a calibration between repositories — `replay` measures it
|
|
521
|
+
now; whether it carries to a repository with no history is unproven. Each is
|
|
522
|
+
designed for; none is claimed.
|
|
523
|
+
vitest, jest and Playwright, AST-aware producers, and calibration of fault classes
|
|
230
524
|
against real escaped bugs. Each is designed for; none is claimed.
|
|
231
525
|
|
|
232
526
|
## Licence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testguard-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Proves a test suite defends the claims a project makes: injects the faults those claims forbid and reports every one the tests fail to detect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/spec/GATE-SEMANTICS.md
CHANGED
|
@@ -32,7 +32,14 @@ Rules that follow from the table:
|
|
|
32
32
|
timeout does not, and a suite that fails to load does not: neither is
|
|
33
33
|
evidence that the suite defends the claim. Tools parse the runner's
|
|
34
34
|
structured report, never its exit code, because the exit code cannot tell
|
|
35
|
-
these apart.
|
|
35
|
+
these apart. A runner's own retry mechanism does not change this: a test
|
|
36
|
+
that failed and then passed on retry (Playwright's `flaky`) is a
|
|
37
|
+
**non-green run** even when the runner exits 0, and a test the runner
|
|
38
|
+
reports as timed out (`timedOut`) is a timeout, never an assertion
|
|
39
|
+
failure. When one claim's defenders run under several runners, their
|
|
40
|
+
runs merge pessimistically — any load error is an error, any timeout is
|
|
41
|
+
a timeout, any failure is a failure — and the evidence names which file
|
|
42
|
+
ran under which runner.
|
|
36
43
|
4. **A mixed result is not a kill.** If some of the N probe runs fail and
|
|
37
44
|
others pass, the defender's response to the fault is nondeterministic.
|
|
38
45
|
Reporting `killed` would be the optimistic bias flakiness introduces;
|
|
@@ -43,6 +50,11 @@ Rules that follow from the table:
|
|
|
43
50
|
and `detail.undeclaredKillers` names the tests, so the author can fix
|
|
44
51
|
`defendedBy`. The claim's stated evidence chain is broken even though the
|
|
45
52
|
suite is not blind. It gates, and ranks below a true survivor.
|
|
53
|
+
Attribution runs **up to N times**, and stops as soon as no test has failed
|
|
54
|
+
in every run so far: from there no further run can name a killer. The
|
|
55
|
+
evidence records the early stop (`detail.escalationStoppedEarly`). Stopping
|
|
56
|
+
can only fail to *name* a killer, never upgrade a verdict, which is the
|
|
57
|
+
pessimistic side.
|
|
46
58
|
6. **A verdict names the commit it is about.** Evidence records `repo.head`;
|
|
47
59
|
when the working tree was probed instead, `repo.snapshot` holds the
|
|
48
60
|
throwaway commit that captured it. A tool must refuse to probe a commit
|
|
@@ -57,6 +69,35 @@ Rules that follow from the table:
|
|
|
57
69
|
changed since it was probed, with its previous verdict. The change is
|
|
58
70
|
allowed; it is never invisible.
|
|
59
71
|
|
|
72
|
+
## Replay reports; it never gates
|
|
73
|
+
|
|
74
|
+
A replayed bug is history. It escaped, by definition, which means the suite
|
|
75
|
+
missed it; a gate on that is a gate nobody can pass on a first run, and it
|
|
76
|
+
would only teach teams to stop looking. `replay` therefore always exits `0`
|
|
77
|
+
unless it could not run at all.
|
|
78
|
+
|
|
79
|
+
Its verdicts mirror the probe's, for the same reasons:
|
|
80
|
+
|
|
81
|
+
| Verdict | Meaning |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `caught` | a remaining test failed **by assertion** on the reverted source, every run. The suite knew. |
|
|
84
|
+
| `blind` | the suite stayed green on known-broken code. |
|
|
85
|
+
| `nocover` | no test imports the reverted files. Worse than `blind`: nothing was even tried. |
|
|
86
|
+
| `unverifiable` | the revert did not apply, the suite failed to load, or it timed out. Carries a `reason`. |
|
|
87
|
+
| `flaky` | the runs disagreed. A flaky failure reads as "the suite caught it", so flakiness biases this metric **optimistically** — mixed runs are never `caught`. |
|
|
88
|
+
|
|
89
|
+
Two rules that follow:
|
|
90
|
+
|
|
91
|
+
1. **One patch, one row.** De-duplicate by `git patch-id`; a dual-branch
|
|
92
|
+
topology carries the same fix under two or three shas, and counting it
|
|
93
|
+
twice corrupts the corpus a calibration is computed from.
|
|
94
|
+
2. **The fix's own test is removed before the run.** It proves nothing about
|
|
95
|
+
what the suite knew before the fix existed.
|
|
96
|
+
|
|
97
|
+
Only `caught` and `blind` carry information. `nocover`, `flaky` and
|
|
98
|
+
`unverifiable` are excluded from both sides of a calibration ratio, because a
|
|
99
|
+
number computed over them would mean nothing.
|
|
100
|
+
|
|
60
101
|
## Baseline and delta
|
|
61
102
|
|
|
62
103
|
A baseline freezes the fingerprints of every non-passing finding at a point
|
|
@@ -72,6 +113,15 @@ derived from identities and outcome, never from the fault's `find`/`replace`
|
|
|
72
113
|
text, so repairing a rotted anchor does not churn the baseline — while a
|
|
73
114
|
change of verdict on the same claim+subject does surface as new.
|
|
74
115
|
|
|
116
|
+
A baseline records the commit its evidence was taken at. When that evidence
|
|
117
|
+
came from a working-tree snapshot, the baseline says so (`snapshot`) and its
|
|
118
|
+
`head` is the **parent** of the commit that will carry the tests. A tool may
|
|
119
|
+
offer to re-stamp such a baseline onto a later commit, but only when a clean
|
|
120
|
+
probe of that commit reproduced exactly the same fingerprints; otherwise the
|
|
121
|
+
user re-probes and freezes a new baseline. A frozen contract is never
|
|
122
|
+
silently rewritten, and a status document notes a baseline that predates
|
|
123
|
+
HEAD without turning that into a state.
|
|
124
|
+
|
|
75
125
|
Adopting tools may reconcile an existing baseline format (for example a
|
|
76
126
|
`{version, fingerprints:{hash:count}}` file) by mapping it onto this shape;
|
|
77
127
|
the suppress-up-to-count semantics are identical.
|
|
@@ -84,6 +134,64 @@ the suppress-up-to-count semantics are identical.
|
|
|
84
134
|
for reasonless `path` entries.
|
|
85
135
|
- An annotation is **strictly additive**. It never changes, suppresses, or
|
|
86
136
|
drops a finding. Ranking may read annotations; verdicts never do.
|
|
137
|
+
- A **signal** is a static, annotation-grade fact about a defender, recorded
|
|
138
|
+
on the evidence and never a verdict. `mocked-never-asserted`: a test file
|
|
139
|
+
mocks the subject's module and never asserts on anything imported from it.
|
|
140
|
+
An author may silence it with `unasserted: <reason>` above the mock; the
|
|
141
|
+
tool then records `unasserted-annotated` with the reason — silenced, never
|
|
142
|
+
hidden. A file that mocks the subject is never a discovered defender: a
|
|
143
|
+
mock cannot detect a fault in what it replaced.
|
|
144
|
+
|
|
145
|
+
## Claim coverage of a change
|
|
146
|
+
|
|
147
|
+
`probe` asks whether the tests defend the claims that exist. It says nothing
|
|
148
|
+
about code that has no claim, by construction — and every escaped defect the
|
|
149
|
+
field reports share was a *claim gap*, not a defender gap: the feature shipped
|
|
150
|
+
green with zero claims. A repository with ten old claims and one new,
|
|
151
|
+
unclaimed module exits 0 under `probe` forever.
|
|
152
|
+
|
|
153
|
+
The change gate closes that hole. Given a reference, a tool measures the
|
|
154
|
+
**delta** — the files changed since `merge-base(ref, HEAD)`, or since that
|
|
155
|
+
merge-base in the working tree — file by file:
|
|
156
|
+
|
|
157
|
+
- A changed file is **excluded** when it is not source (by extension) or
|
|
158
|
+
matches a documented never-claimed pattern (`*.d.ts`, `*.config.*`,
|
|
159
|
+
fixtures, mocks, snapshots, the tool's own directory). Exclusions are
|
|
160
|
+
listed, never silent.
|
|
161
|
+
- A changed source file is **covered** when at least one fault anchors to it,
|
|
162
|
+
when it is a test file that resolves as a defender of some claim, or when an
|
|
163
|
+
**unexpired** `path` ignore entry excuses it.
|
|
164
|
+
- Anything else is **uncovered**. The unit is the file, on purpose: a fault
|
|
165
|
+
anchors to a file, so the file is the smallest unit the rest of the
|
|
166
|
+
contract already understands.
|
|
167
|
+
|
|
168
|
+
Rules:
|
|
169
|
+
|
|
170
|
+
1. **One uncovered file gates.** Exit `1`. There is no threshold and no
|
|
171
|
+
percentage; a percentage is how the gap hid before.
|
|
172
|
+
2. **Every reliance on an ignore entry is reported.** The gate may pass
|
|
173
|
+
*because of* an excuse; a reviewer reads which entries carried it, with
|
|
174
|
+
their reasons and expiry. An expired entry excuses nothing and is reported
|
|
175
|
+
as expired.
|
|
176
|
+
3. **A test file needs a claim too.** A test that defends no claim is the
|
|
177
|
+
authorship trap the pattern exists for; it is uncovered until a claim
|
|
178
|
+
names it in `defendedBy` (or discovery resolves it as a defender).
|
|
179
|
+
4. **Never a silent pass on nothing.** A non-empty change whose files were
|
|
180
|
+
all excluded passes with an explicit "0 evaluated" line; a tool offers a
|
|
181
|
+
strict mode that fails it instead. An empty change is an honest `0`.
|
|
182
|
+
5. **The reference is never guessed.** An explicit flag, or a CI-provided base
|
|
183
|
+
branch, or an error. An upstream that already contains the change has an
|
|
184
|
+
empty diff and would pass trivially.
|
|
185
|
+
6. **Unclaimed changes precede every evidence state.** When the status
|
|
186
|
+
document knows a reference and finds uncovered files, its state is
|
|
187
|
+
`unclaimed-changes` and its next action is to write the claim, before any
|
|
188
|
+
`unproven` finding is surfaced. The brief renders them first. The claim is
|
|
189
|
+
written before more code.
|
|
190
|
+
|
|
191
|
+
Exit codes: `0` every changed source file is claimed or excused (or nothing
|
|
192
|
+
changed); `1` at least one uncovered file (or strict mode over an
|
|
193
|
+
all-excluded change); `2` the change cannot be evaluated (unresolvable
|
|
194
|
+
reference, invalid claims or ignore file, no repository); `3` no reference.
|
|
87
195
|
|
|
88
196
|
## Severity floor
|
|
89
197
|
|
package/spec/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Guard spec — the contract spine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Eight formats that tools following the Guard pattern share. They share
|
|
4
4
|
*formats*, not code: [`docguard-cli`](https://www.npmjs.com/package/docguard-cli)
|
|
5
5
|
is Node ESM, [`websec-validator`](https://pypi.org/project/websec-validator/)
|
|
6
6
|
is Python, and porting one runtime into the other is not worth it. A tool
|
|
@@ -21,7 +21,9 @@ The pattern all such tools run:
|
|
|
21
21
|
| `ignore` | [`schemas/ignore.schema.json`](schemas/ignore.schema.json) | Reviewable scoping. Every entry has a reason. |
|
|
22
22
|
| `calibration` | [`schemas/calibration.schema.json`](schemas/calibration.schema.json) | P(finding is real) per bucket, with a Wilson interval and the sample size behind it. |
|
|
23
23
|
| `brief` | [`schemas/brief.schema.json`](schemas/brief.schema.json) | What to tell an agent before it writes code — ranked, capped, never a single score, and carrying the one next action. |
|
|
24
|
-
| `status` | [`schemas/status.schema.json`](schemas/status.schema.json) | Where the project is and what happens next — the single machine-readable truth every human rendering derives from. Surfaces faults whose content changed since they were probed. |
|
|
24
|
+
| `status` | [`schemas/status.schema.json`](schemas/status.schema.json) | Where the project is and what happens next — the single machine-readable truth every human rendering derives from. Surfaces faults whose content changed since they were probed, and changed files that carry no claim. |
|
|
25
|
+
| `replay` | [`schemas/replay.schema.json`](schemas/replay.schema.json) | Would this suite have caught the bugs that already escaped? Replays real fix commits: revert the source, remove the test the fix shipped, run what remains. A replayed bug is ground truth — a human already confirmed it was a defect — which is what a fault model is calibrated against. |
|
|
26
|
+
| `gate` | [`schemas/gate.schema.json`](schemas/gate.schema.json) | Claim coverage of one change: which changed files carry a claim, which are excused (and by which ignore entry), which are unclaimed. The delta gate for code that has no claim yet. |
|
|
25
27
|
|
|
26
28
|
Shared definitions (verdicts, fault classes, provenance, annotations) live in
|
|
27
29
|
[`schemas/common.schema.json`](schemas/common.schema.json). Gate behaviour —
|