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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,297 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
Everything the field asked for. Two independent field reports on private
|
|
13
|
+
AI-authored codebases drove eleven issues; this release closes all of them,
|
|
14
|
+
alongside the roadmap work that landed beside it.
|
|
15
|
+
|
|
16
|
+
**Read this before upgrading.** Defender discovery is now mock-aware: a test
|
|
17
|
+
that `vi.mock`s or `jest.mock`s the subject can detect no fault in it and is
|
|
18
|
+
no longer counted. Claims defended only by such tests therefore move to
|
|
19
|
+
`NOCOVER`, which is the truth those verdicts always should have told. Expect
|
|
20
|
+
new findings on the first probe after upgrading, and re-freeze your baseline
|
|
21
|
+
once you have read them — they are not regressions in your code, they are
|
|
22
|
+
blind spots that were previously invisible.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`testguard mcp`** — the operating loop, served over the Model Context
|
|
27
|
+
Protocol on stdio, so it survives a change of agent harness. The loop has
|
|
28
|
+
lived in a Claude Code skill and a session-start hook, and both vanish the
|
|
29
|
+
moment the harness is Cursor, Codex or whatever comes next. Five tools,
|
|
30
|
+
all **read-only**: `testguard_status`, `testguard_brief`,
|
|
31
|
+
`testguard_claims`, `testguard_evidence` and `testguard_next_command`.
|
|
32
|
+
**No tool runs a probe** — a probe is long-running, budgeted and the
|
|
33
|
+
person should see it happen, so `next_command` returns the shell line
|
|
34
|
+
instead; and no tool writes a file, because editing a claims file through
|
|
35
|
+
a connector would defeat recording every fault edit. The server declares
|
|
36
|
+
only a `tools` capability, and a tool that throws returns an `isError`
|
|
37
|
+
tool result rather than a protocol error, so one bad call never costs the
|
|
38
|
+
connection.
|
|
39
|
+
Hand-written against a pinned protocol version rather than built on the
|
|
40
|
+
official SDK: this tool keeps its single exact-pinned runtime dependency,
|
|
41
|
+
and the surface is three methods that will not grow. The tests speak the
|
|
42
|
+
wire format to a real child process and check every tool against the CLI's
|
|
43
|
+
own `--json` output, so the two cannot drift.
|
|
44
|
+
`init --mcp` prints the config for Claude Code, Cursor and Codex — printed,
|
|
45
|
+
never written, because a harness config is the person's file. Self-claims
|
|
46
|
+
`TG-MCP-IS-READ-ONLY` and
|
|
47
|
+
`TG-MCP-TOOL-FAILURE-IS-NOT-A-PROTOCOL-ERROR`. (#30)
|
|
48
|
+
|
|
49
|
+
- **Contention detection and `--serial`** (#26). `probe` looks for other test
|
|
50
|
+
runners before the first run, warns naming their pids, and records them on
|
|
51
|
+
the evidence (`run.contention`) — a contended machine turns a slow suite
|
|
52
|
+
into a `TIMEOUT` or `FLAKY-DEFENDER` verdict about the load, not the claim,
|
|
53
|
+
and a later reader of that verdict can now tell. `--serial` runs one test
|
|
54
|
+
file at a time (`--no-file-parallelism`, `--runInBand`, `--workers=1`) and
|
|
55
|
+
is recorded as `run.serial`. Best effort by design: an unreadable process
|
|
56
|
+
list is no detection and never fails a probe.
|
|
57
|
+
|
|
58
|
+
- **`claims --since <ref>`** — a claim that disappeared was invisible to every
|
|
59
|
+
other check: `probe` verifies what is there, `gate` sees the file covered by
|
|
60
|
+
another claim, `status` says clean. Since deleting a claim is cheaper than
|
|
61
|
+
weakening its fault (which `changedFaults` already surfaces), removal is now
|
|
62
|
+
reported: `removed-claim` and `removed-fault` gate, a rename that keeps the
|
|
63
|
+
statement verbatim is `renamed-claim` and does not, and where evidence exists
|
|
64
|
+
the finding names the verdict the claim last had. Excused by a `claim` entry
|
|
65
|
+
in `testguard.ignore.json` with a reason, with expiry honoured as everywhere
|
|
66
|
+
else. This repository's CI runs it on its own pull requests. Found the hard
|
|
67
|
+
way: a `--theirs` conflict resolution dropped two self-claims during this
|
|
68
|
+
release and nothing noticed. Self-claim `TG-REMOVED-CLAIM-IS-A-FINDING`. (#57)
|
|
69
|
+
|
|
70
|
+
- **`testguard replay --since <range>`** — would this suite have caught the
|
|
71
|
+
bugs that already escaped? For each fix commit in the range (one that
|
|
72
|
+
changes source *and* a test together), it reverts only the source to the
|
|
73
|
+
parent in a scratch worktree, removes the test the fix shipped, and runs the
|
|
74
|
+
tests that import the reverted code: `caught`, `blind`, `nocover`, `flaky`
|
|
75
|
+
or `unverifiable`. One patch counts once (`git patch-id`). It reports and
|
|
76
|
+
never gates — a bug that escaped is history, not a regression in this
|
|
77
|
+
change. An injected fault is one somebody thought of; a bug that shipped is
|
|
78
|
+
ground truth, with no equivalent-mutant argument to have about it.
|
|
79
|
+
- **Fault-class labelling and the first calibration.** Each replayed bug is
|
|
80
|
+
labelled with the injected-fault class its diff most resembles — the
|
|
81
|
+
scaffold producers read in reverse, deterministic, `other` rather than a
|
|
82
|
+
guess — and a `calibration` document is written beside the replay one:
|
|
83
|
+
per class, the share of real escaped bugs the suite missed, with a Wilson
|
|
84
|
+
interval and n. Only `caught` and `blind` carry information; the rest are
|
|
85
|
+
excluded from both sides. `calibration.schema.json` has had no producer
|
|
86
|
+
until now. The open question it exists to answer — does a calibration
|
|
87
|
+
learned on a repository with history transfer to a greenfield one — stays
|
|
88
|
+
open; this is the instrument, not the answer. (#35)
|
|
89
|
+
- New spec kind **`replay`** (`replay.schema.json`), with semantic rules:
|
|
90
|
+
`caught` needs every run to fail by assertion, `blind` needs every run to
|
|
91
|
+
pass, `flaky` needs runs that disagree, `nocover` cannot have run tests,
|
|
92
|
+
and a duplicate patch-id is rejected. Conformance example plus two
|
|
93
|
+
must-reject documents. Self-claims
|
|
94
|
+
`TG-REPLAY-FLAKY-IS-NEVER-CAUGHT`, `TG-REPLAY-DEDUPES-BY-PATCH`,
|
|
95
|
+
`TG-CALIBRATION-EXCLUDES-UNINFORMATIVE` and `TG-LABEL-NEVER-GUESSES`.
|
|
96
|
+
|
|
97
|
+
- **`baseline --restamp`** (#25). A baseline frozen from `--include-dirty`
|
|
98
|
+
evidence now records the snapshot commit and, once you commit, a clean
|
|
99
|
+
`probe` plus `--restamp` moves its `head` to that commit — only when the
|
|
100
|
+
fingerprints are identical and the tree is clean; a frozen contract is
|
|
101
|
+
never silently rewritten. `status` gains informational `notes` (never a
|
|
102
|
+
state): a baseline frozen from a snapshot or a dirty tree that predates
|
|
103
|
+
HEAD, or one whose head is not an ancestor of HEAD.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
- **Mock-aware defender discovery** (#20). A test file that `vi.mock`s /
|
|
107
|
+
`jest.mock`s the target cannot detect any fault in it and is no longer a
|
|
108
|
+
discovered defender; `NOCOVER` now means "no test imports this source
|
|
109
|
+
without mocking it". The evidence lists such files under
|
|
110
|
+
`defenders.mocking` (a *declared* defender that mocks the subject stays,
|
|
111
|
+
and is listed, as a broken evidence chain); `claims` prints the split
|
|
112
|
+
(`18 import · 16 mock · 2 can detect`).
|
|
113
|
+
- **`mocked-never-asserted`** (#21): a static signal for a test that mocks
|
|
114
|
+
the target and never `expect(...)`s anything imported from it — the exact
|
|
115
|
+
signature of an escaped bug in a field report. Recorded on the evidence
|
|
116
|
+
(`defenders.signals`), printed by `claims`, carried into the brief's hints.
|
|
117
|
+
`// unasserted: <why>` above the mock silences it visibly
|
|
118
|
+
(`unasserted-annotated` with the reason).
|
|
119
|
+
- Alias resolution follows tsconfig `references` (the Vite layout, where
|
|
120
|
+
`paths` live in `tsconfig.app.json`) and reads vite/vitest `resolve.alias`
|
|
121
|
+
as text, so importers from nested `__tests__/` directories are found.
|
|
122
|
+
- Both known-answer fixtures gain a claim whose only importing test mocks
|
|
123
|
+
the module: expected `NOCOVER`, with the mocking file and the signal
|
|
124
|
+
recorded.
|
|
125
|
+
|
|
126
|
+
- **`testguard admit <test-file> --claim <ID>`** — the two-gate rule as a
|
|
127
|
+
named verb. Sugar over `probe --claim <ID> --include-dirty --no-escalate`:
|
|
128
|
+
the named test must be a declared or discovered defender of the claim
|
|
129
|
+
(exit `3` otherwise, with the `defendedBy` line to add); `ADMITTED`
|
|
130
|
+
(exit `0`) only when every fault of the claim is `killed` N/N on defenders
|
|
131
|
+
that were green N/N unmodified; anything else is `NOT ADMITTED` (exit `1`)
|
|
132
|
+
and names the first blocking fault with the hint the brief would give.
|
|
133
|
+
`--fault <FID>` judges one fault, `--confirm 1` gives a provisional
|
|
134
|
+
`ADMITTED?`, `--json` returns `{admitted, provisional, faults[], evidence,
|
|
135
|
+
command}`. Evidence goes to `.testguard/evidence-partial.json`; nothing new
|
|
136
|
+
in the evidence schema. The market's acceptance signal for a generated
|
|
137
|
+
test is "compiles, passes, raises coverage"; this one is "fails when the
|
|
138
|
+
claim is false", and it is now one command. `status.next` for `unproven`
|
|
139
|
+
and the installed skill's fix loop point at it. Self-claim
|
|
140
|
+
`TG-ADMIT-NEEDS-ALL-KILLED`.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
|
|
144
|
+
- **`init` installs the agent layer at the git root** (#23): the skill,
|
|
145
|
+
the session-start hook and the `AGENTS.md` section go where agent
|
|
146
|
+
sessions run; the `.gitignore` lines stay beside the claims file. A
|
|
147
|
+
second project in the same repository adds a hook line and an
|
|
148
|
+
`AGENTS.md` bullet; `--here` keeps the old placement. A written file
|
|
149
|
+
that `.gitignore` swallows is reported (exit 1), never offered for
|
|
150
|
+
commit.
|
|
151
|
+
- **The session-start hook resolves an installed `testguard` and contains no
|
|
152
|
+
form of `npx`** (#24): the project's `node_modules/.bin`, then the git
|
|
153
|
+
root's, then `command -v testguard`, then nothing — it exits 0 with no
|
|
154
|
+
output rather than break a session. Any earlier npx hook, `-y` or
|
|
155
|
+
`--no-install`, is replaced on the next `init`. The brief's first line
|
|
156
|
+
names the install that answered.
|
|
157
|
+
|
|
158
|
+
`--no-install` is **quiet, not offline**, which is why it is gone too:
|
|
159
|
+
measured, `npx --no-install --loglevel=http testguard-cli --version` in a
|
|
160
|
+
project with nothing installed logs
|
|
161
|
+
`npm http fetch GET 200 https://registry.npmjs.org/testguard-cli`, and
|
|
162
|
+
against an unreachable registry it exits non-zero. npm resolves the
|
|
163
|
+
packument before deciding not to install.
|
|
164
|
+
|
|
165
|
+
- `probe` honours an explicit `--ref` (even `--ref HEAD`) when defender or
|
|
166
|
+
target files are dirty, and gains `--ignore-dirty` for the implicit HEAD
|
|
167
|
+
(#19). Both warn with the file names and record them in the evidence as
|
|
168
|
+
`repo.ignoredDirty` (spec: new optional field; a snapshot run can never
|
|
169
|
+
carry it). The refusal stays for the implicit HEAD without the flag — that
|
|
170
|
+
is the silent-mismatch trap the check exists for.
|
|
171
|
+
|
|
172
|
+
- CI: TestGuard's self-probe runs on one Node leg instead of three, without
|
|
173
|
+
escalation, and restores the previous run's evidence from the cache so
|
|
174
|
+
unchanged claims reuse their verdicts. The verdicts do not depend on the
|
|
175
|
+
Node minor; the from-scratch probe was ~14 minutes per leg.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- **The README described a hook the code no longer writes.** Three passages
|
|
180
|
+
still documented the `npx --no-install` fallback that this release removes,
|
|
181
|
+
one of them repeating the "never fetches from the network" wording the
|
|
182
|
+
measurement above disproves — in the security-relevant paragraph, in a
|
|
183
|
+
release about detection power. Corrected, and now checked mechanically:
|
|
184
|
+
every `||`-chained `brief --text` command in the README must be one
|
|
185
|
+
`hookCommand()` actually emits, and the phrase "falls back to npx" is
|
|
186
|
+
banned outright. Both regressions were verified to fail the check before it
|
|
187
|
+
was accepted. Self-claim `TG-README-HOOK-MATCHES-THE-CODE`.
|
|
188
|
+
- **A claim can be defended and still over-promise, and no probe can catch
|
|
189
|
+
that.** `TG-INIT-HOOK-NO-NETWORK` was killed 3/3 on every run while the
|
|
190
|
+
code it guarded could still reach the registry: the fault matched the
|
|
191
|
+
code, the code matched the test, and the *statement* was the thing that
|
|
192
|
+
was wrong. Fault injection measures whether a test would notice the code
|
|
193
|
+
changing; it cannot measure whether the sentence a human wrote is true of
|
|
194
|
+
the world. That is the standing limit of this method, and the only remedy
|
|
195
|
+
is reading claims against reality — which is what happened here. The
|
|
196
|
+
statement and its fault were corrected together.
|
|
197
|
+
- **The session-start hook now contains no form of `npx`.** It fell back to
|
|
198
|
+
`npx --no-install`, and `--no-install` is quiet rather than offline:
|
|
199
|
+
measured, `npx --no-install --loglevel=http testguard-cli --version` in a
|
|
200
|
+
project with nothing installed logs
|
|
201
|
+
`npm http fetch GET 200 https://registry.npmjs.org/testguard-cli`, and
|
|
202
|
+
against an unreachable registry it exits non-zero. npm resolves the
|
|
203
|
+
packument before deciding not to install. The fallback is now a
|
|
204
|
+
`command -v` lookup, which covers a global install with no network at all.
|
|
205
|
+
`TG-INIT-HOOK-NO-NETWORK` was defended while its statement over-promised;
|
|
206
|
+
the statement and its fault are corrected together.
|
|
207
|
+
- The PATH branch is braced. `a || b && c` binds as `(a || b) && c` in sh, so
|
|
208
|
+
the unbraced form printed the brief **twice** whenever the local binary
|
|
209
|
+
succeeded. The tests now execute the hook in all three cases (local, PATH,
|
|
210
|
+
neither) rather than matching its text.
|
|
211
|
+
- `init` recognises an earlier `npx --no-install` hook as well as `npx -y`,
|
|
212
|
+
and its which-project check no longer mistakes `brief --text 2>/dev/null`
|
|
213
|
+
for a hook belonging to a directory — that misfire meant a legacy root hook
|
|
214
|
+
was never replaced.
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
- **`replay` scopes to the project directory.** In a monorepo a fix commit
|
|
218
|
+
routinely touches several packages; the harness kept the out-of-project
|
|
219
|
+
files in the revert, so every cross-package fix came back
|
|
220
|
+
`revert-did-not-apply` — a tooling failure wearing the costume of a
|
|
221
|
+
verdict. Found on a real corpus, where it was two of the first three
|
|
222
|
+
commits. A commit with no source-and-test pair inside the project is no
|
|
223
|
+
longer a candidate at all. Self-claim `TG-REPLAY-SCOPES-TO-THE-PROJECT`.
|
|
224
|
+
- **`replay` reverts a file the fix ADDED by removing it**, and calls a commit
|
|
225
|
+
whose source is entirely new `no-prior-version` rather than a failed revert.
|
|
226
|
+
A fix routinely adds a helper as well as changing a module, and a file that
|
|
227
|
+
did not exist at the parent cannot be checked out of it: on a real corpus
|
|
228
|
+
this turned **nineteen of forty** commits into `revert-did-not-apply` — a
|
|
229
|
+
tooling failure reading as a verdict and hiding every real result behind it.
|
|
230
|
+
An addition is not a bug the suite could have caught, so it never enters a
|
|
231
|
+
calibration. Self-claim
|
|
232
|
+
`TG-REPLAY-ADDED-FILE-IS-REMOVED-NOT-CHECKED-OUT`.
|
|
233
|
+
- `replay --out <path>` now writes the calibration beside it rather than into
|
|
234
|
+
the project's `.testguard/`: the two documents are one result and splitting
|
|
235
|
+
them loses the pairing.
|
|
236
|
+
|
|
237
|
+
## [0.5.0] - 2026-09-17
|
|
238
|
+
|
|
239
|
+
The change gate: unclaimed code is now a finding.
|
|
240
|
+
|
|
241
|
+
### Added
|
|
242
|
+
|
|
243
|
+
- **Playwright runner, selected per file** (#15). A defender under
|
|
244
|
+
`playwright.config.*`'s `testDir` runs under Playwright whatever the
|
|
245
|
+
project runner is; one claim may list a unit test and a browser spec, and
|
|
246
|
+
the evidence records `defenders.byRunner` and `run.runners`. Playwright's
|
|
247
|
+
statuses map onto the verdict rules: `timedOut` is a timeout, never a
|
|
248
|
+
kill; `flaky` (failed, passed on retry) is a non-green run, so a defender
|
|
249
|
+
that only passes on retry is `FLAKY-DEFENDER` even though Playwright exits
|
|
250
|
+
0. Runs from several runners merge pessimistically. `--runner playwright`
|
|
251
|
+
selects it for a whole project. A browserless mixed fixture
|
|
252
|
+
(`fixtures/known-answer-playwright`, with its own dependencies) is the
|
|
253
|
+
oracle, verified by hand; a dedicated CI job runs it.
|
|
254
|
+
- **Two UI `scaffold` shapes**: `element-removed` (a one-line JSX element,
|
|
255
|
+
self-closing or paired) and `handler-dropped` (an `on<Event>={…}` prop, on
|
|
256
|
+
its own line or inline). Never inside tests, fixtures or migrations.
|
|
257
|
+
|
|
258
|
+
- **Two `scaffold` shapes and two fault classes.** `field-dropped` (#14):
|
|
259
|
+
a field removed from an object that is returned, built by an arrow,
|
|
260
|
+
assigned to a payload-ish name, passed to a persistence/transport call,
|
|
261
|
+
or is a schema; an entry removed from an allow-list; a spread base dropped
|
|
262
|
+
from a merge. Only lines that can go on their own; never inside tests,
|
|
263
|
+
fixtures or migrations. `argument-swapped` (#22): a call kept with its
|
|
264
|
+
parameter-derived first argument swapped for `undefined` (and `{}` when
|
|
265
|
+
the argument is a call) — the seam fault a field report found behind an
|
|
266
|
+
escaped bug that no earlier shape could express. The known-answer fixture
|
|
267
|
+
gains one of each, verified by hand: the dropped `content` field
|
|
268
|
+
**survives** (the same `objectContaining` blind spot as the exhibit), the
|
|
269
|
+
swapped argument is killed.
|
|
270
|
+
|
|
271
|
+
- **`testguard gate --changed <ref>`** — the change gate. Every escaped
|
|
272
|
+
defect in the field reports was a *claim gap*: the feature shipped green
|
|
273
|
+
with zero claims, and `probe` is silent about unclaimed code by
|
|
274
|
+
construction. `gate` measures the files changed since
|
|
275
|
+
`merge-base(ref, HEAD)` (or in the working tree with `--include-dirty`)
|
|
276
|
+
and exits `1` when any changed source file carries no fault, does not
|
|
277
|
+
resolve as a defender (test files), and is not excused by an unexpired
|
|
278
|
+
`path` entry in `testguard.ignore.json`. Every reliance on an ignore entry
|
|
279
|
+
is printed with its reason; expired entries excuse nothing. Non-source
|
|
280
|
+
files and documented never-claimed patterns are excluded and listed
|
|
281
|
+
(`--explain`, `--exclude <glob>`); `--strict` fails a change that
|
|
282
|
+
evaluated nothing. The base branch is detected in GitHub Actions and
|
|
283
|
+
GitLab CI (`TESTGUARD_CHANGED_REF` overrides). A new spec kind,
|
|
284
|
+
`gate.schema.json`, with conformance examples.
|
|
285
|
+
- **`status --changed <ref>`** — a new state `unclaimed-changes` and action
|
|
286
|
+
`claim` that precede every evidence state; `next.file` names the first
|
|
287
|
+
unclaimed file. `brief` renders unclaimed files before the findings, and
|
|
288
|
+
briefs them even when there is no evidence yet.
|
|
289
|
+
- `testguard.ignore.json` at the repository root: TestGuard's own excused
|
|
290
|
+
paths (dispatch, thin command wrappers, rendering), with reasons.
|
|
291
|
+
- Nine new self-claims: the gate, status and brief invariants, explicit
|
|
292
|
+
`--changed` being required, `init` idempotency, and — because the gate
|
|
293
|
+
flagged it on its own pull request — the release version-sync check,
|
|
294
|
+
which had no test before. The gate runs on this repository's own pull
|
|
295
|
+
requests.
|
|
296
|
+
- GitHub Action `command: gate` with a `changed-ref` input; pre-commit hook
|
|
297
|
+
`testguard-gate`; a GitLab CI template under `packaging/gitlab/` with
|
|
298
|
+
`testguard:gate` (merge request pipelines, measured against
|
|
299
|
+
`CI_MERGE_REQUEST_DIFF_BASE_SHA`) and `testguard:probe`.
|
|
300
|
+
|
|
10
301
|
## [0.4.0] - 2026-09-17
|
|
11
302
|
|
|
12
303
|
### Added
|