testguard-cli 0.5.0 → 0.7.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 +628 -0
- package/README.md +536 -42
- package/package.json +2 -2
- package/spec/GATE-SEMANTICS.md +166 -2
- package/spec/README.md +29 -10
- package/spec/lib/validate.mjs +140 -8
- package/spec/schemas/baseline.schema.json +13 -5
- package/spec/schemas/brief.schema.json +12 -12
- package/spec/schemas/calibration.schema.json +5 -5
- package/spec/schemas/claims.schema.json +122 -25
- package/spec/schemas/common.schema.json +131 -29
- package/spec/schemas/evidence.schema.json +476 -69
- package/spec/schemas/gate.schema.json +12 -12
- package/spec/schemas/ignore.schema.json +4 -4
- package/spec/schemas/replay.schema.json +85 -0
- package/spec/schemas/status.schema.json +258 -56
- package/src/admit/admit.mjs +17 -0
- package/src/baseline/baseline.mjs +31 -2
- package/src/brief/brief.mjs +71 -7
- package/src/claims/removed.mjs +161 -0
- package/src/cli.mjs +68 -10
- package/src/commands/admit.mjs +116 -0
- package/src/commands/baseline.mjs +17 -3
- package/src/commands/brief.mjs +13 -7
- package/src/commands/claims.mjs +55 -6
- package/src/commands/init.mjs +19 -6
- package/src/commands/mcp.mjs +12 -0
- package/src/commands/probe.mjs +35 -7
- package/src/commands/replay.mjs +63 -0
- package/src/commands/status.mjs +3 -1
- package/src/gate/changed.mjs +9 -2
- package/src/git.mjs +5 -0
- package/src/init/init.mjs +182 -29
- package/src/init/templates/SKILL.md +3 -3
- package/src/mcp/server.mjs +104 -0
- package/src/mcp/tools.mjs +114 -0
- package/src/probe/attribution.mjs +123 -0
- package/src/probe/classify.mjs +18 -2
- package/src/probe/contention.mjs +60 -0
- package/src/probe/cost.mjs +145 -0
- package/src/probe/discover.mjs +20 -3
- package/src/probe/independence.mjs +59 -0
- package/src/probe/inject.mjs +41 -9
- package/src/probe/mocks.mjs +122 -0
- package/src/probe/probe.mjs +331 -40
- package/src/probe/progress.mjs +87 -0
- package/src/probe/pyimports.mjs +287 -0
- package/src/probe/rank.mjs +75 -15
- package/src/probe/runners/index.mjs +79 -8
- package/src/probe/runners/jest.mjs +15 -3
- package/src/probe/runners/playwright.mjs +142 -0
- package/src/probe/runners/python/_testguard_pytest_plugin.py +132 -0
- package/src/probe/runners/python/_testguard_report.py +141 -0
- package/src/probe/runners/python/_testguard_unittest_main.py +168 -0
- package/src/probe/runners/python.mjs +316 -0
- package/src/probe/runners/shared.mjs +80 -14
- package/src/probe/runners/vitest.mjs +16 -3
- package/src/render.mjs +2 -2
- package/src/replay/label.mjs +64 -0
- package/src/replay/replay.mjs +310 -0
- package/src/scaffold/producers.mjs +217 -6
- package/src/scaffold/producers.python.mjs +209 -0
- package/src/scaffold/scaffold.mjs +90 -30
- package/src/status/status.mjs +47 -6
- package/src/util/glob.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,640 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-09-18
|
|
11
|
+
|
|
12
|
+
Python, and a fault that cannot hide.
|
|
13
|
+
|
|
14
|
+
TestGuard now probes Python projects with nothing installed into them, and it
|
|
15
|
+
no longer takes a green suite at its word: a fault applied to code the tests
|
|
16
|
+
never execute used to be reported as SURVIVED — an audit finding that reads as
|
|
17
|
+
devastating and is entirely false — and is now `unverifiable`, because the
|
|
18
|
+
defenders are made to prove they can fail because of that file before a
|
|
19
|
+
survival about it is allowed to stand.
|
|
20
|
+
|
|
21
|
+
Alongside: the shared spec is named `claimspec` and no longer assumes fault
|
|
22
|
+
injection is the only way to falsify a claim, one bad fault can no longer cost
|
|
23
|
+
the evidence for the whole run, and the self-probe gate was measured rather
|
|
24
|
+
than guessed at.
|
|
25
|
+
|
|
26
|
+
### Upgrading
|
|
27
|
+
|
|
28
|
+
**Some claims that were `survived` will become `unverifiable`, and your frozen
|
|
29
|
+
baseline will not suppress them.** This is the intended consequence of the
|
|
30
|
+
negative control, and it is worth reading before you re-freeze anything.
|
|
31
|
+
|
|
32
|
+
A fingerprint is derived from the claim, the subject, the file and **the
|
|
33
|
+
verdict**. A claim whose subject the defenders never execute was reported
|
|
34
|
+
`survived`; it is now `unverifiable` with reason `subject-not-executed`. That
|
|
35
|
+
is a different fingerprint, so a baseline frozen before this release does not
|
|
36
|
+
contain it, and it gates as new.
|
|
37
|
+
|
|
38
|
+
**The finding is real.** It says the defenders do not execute that file at all,
|
|
39
|
+
so every earlier verdict about it was a statement about their reach rather than
|
|
40
|
+
their assertions — including the `survived` your baseline was suppressing. A
|
|
41
|
+
`survived` you had accepted as known debt was, in these cases, not a measurement
|
|
42
|
+
of anything.
|
|
43
|
+
|
|
44
|
+
What to do, in order:
|
|
45
|
+
|
|
46
|
+
1. `testguard probe` and read the records carrying
|
|
47
|
+
`detail.negativeControl: "not-reached"`. Each names a file no defender
|
|
48
|
+
loads.
|
|
49
|
+
2. **Prefer fixing the defenders.** A claim whose subject is never imported is
|
|
50
|
+
the cheapest blind spot you will ever find — cheaper than a survivor,
|
|
51
|
+
because nothing was even tried.
|
|
52
|
+
3. If you are accepting it as existing debt for now, `testguard baseline`
|
|
53
|
+
re-freezes today's findings so only newer ones gate. Do this **after**
|
|
54
|
+
looking at them, not instead of.
|
|
55
|
+
|
|
56
|
+
`baseline --restamp` is **not** the command for this: it moves a baseline's
|
|
57
|
+
`head` onto a clean identical probe, and it requires the same fingerprints.
|
|
58
|
+
|
|
59
|
+
Nothing else in this release requires migration. The `claimspec` rename changes
|
|
60
|
+
no emitted document, the `method` field defaults to `fault-injection` so every
|
|
61
|
+
existing claims and evidence file stays valid, and the gate work changed which
|
|
62
|
+
tests defend which claims, never a verdict.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **A five-page technical brief**, [`docs/testguard-explained.pdf`](docs/testguard-explained.pdf):
|
|
67
|
+
the whole idea on page one, the field measurements on page two, then
|
|
68
|
+
mechanics, architecture and prior art. Six diagrams, including a dot matrix
|
|
69
|
+
of the 39 injected faults from the second field report — 21 survived a fully
|
|
70
|
+
green suite, 39/39 killed after tests were written against the survivors. Built from
|
|
71
|
+
`docs/testguard-explained.html` with `node .github/scripts/build-one-pager.mjs`
|
|
72
|
+
(headless Chrome, so the one pinned runtime dependency stays one).
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
- **A probe says which method produced it, and fault injection is no longer
|
|
76
|
+
assumed** (#81). `claims` required `file`, `find` and `replace` on every
|
|
77
|
+
fault; `evidence` required `confirmRuns`, `mode`, `defenders`, `inputs` and
|
|
78
|
+
the baseline and probe runs on every record. All of those are what *fault
|
|
79
|
+
injection* means by showing its work — and DocGuard verifies claims by
|
|
80
|
+
reading code while websec-validator scans a surface, so neither could
|
|
81
|
+
conform without emitting empty arrays, which is lying, and the one thing
|
|
82
|
+
this format exists to make impossible.
|
|
83
|
+
A probe now carries `method`, and a run says which method produced it.
|
|
84
|
+
**It defaults to `fault-injection`, so every document written before the
|
|
85
|
+
field existed validates unchanged** — verified against this repository's own
|
|
86
|
+
88-claim file and every conformance example, none of which carries the field.
|
|
87
|
+
The injection requirements did not disappear, they moved: the schema asks
|
|
88
|
+
for them only under `fault-injection`, and the validator enforces them there
|
|
89
|
+
in full. Relaxing the schema so a scanning tool can conform is not a licence
|
|
90
|
+
for an injecting tool to stop showing its work, and a self-claim now says so.
|
|
91
|
+
`assertion` and `scan` are **reserved**: their required fields are
|
|
92
|
+
deliberately unspecified, to be defined by their first real consumer with
|
|
93
|
+
conformance examples written from the shape that tool actually has.
|
|
94
|
+
Designing them for an absent tool is exactly the mistake that made this
|
|
95
|
+
change necessary — the spec was drafted against a single consumer and
|
|
96
|
+
hard-coded its assumptions as requirements.
|
|
97
|
+
- **The self-probe gate, re-measured and cut** (#73). `--cost` at `ffaaa81`
|
|
98
|
+
reported **1414 s across 546 defender runs for 91 faults** — back where #67
|
|
99
|
+
started, because #77 took the corpus from 59 faults to 91. The gate did not
|
|
100
|
+
decay; the workload grew, and nothing gated on that.
|
|
101
|
+
The issue's original table pointed at `test/replay.test.mjs`. Measurement
|
|
102
|
+
disagreed: that file is 138 s across *six* claims, about 23 s each, the
|
|
103
|
+
cheapest per-claim cost in the top ten — the table had read a cumulative
|
|
104
|
+
column as a per-claim one. The real cost was four claims paying an
|
|
105
|
+
end-to-end fixture probe, 33 to 37 s a run, to falsify decisions that are
|
|
106
|
+
pure functions.
|
|
107
|
+
Each was re-pointed at a test that does not need the expensive setup, and
|
|
108
|
+
**each was then re-probed rather than assumed**: 7 faults, 7 killed, 92 s
|
|
109
|
+
where they had cost about 860 s.
|
|
110
|
+
- `checkProvenance` is now exported and unit-tested. It was already a
|
|
111
|
+
standalone function, and only a 33 s Python fixture probe could falsify it.
|
|
112
|
+
- `argvFor` is pure, and its own comment says it is "exported so it is
|
|
113
|
+
falsifiable without spawning anything" — while the claim still paid two
|
|
114
|
+
fixture probes. `test/runner-argv.test.mjs` asserts the argv directly in
|
|
115
|
+
126 ms, and covers `selectRunner`'s unknown-name guard too.
|
|
116
|
+
- The isolation fault needed new work before it could move: forcing
|
|
117
|
+
isolation to in-place left every fast test green, because they all probe
|
|
118
|
+
in place anyway, so only the 37 s fixture noticed. Caught by injecting the
|
|
119
|
+
fault by hand before trusting the re-point — the same trap that made two
|
|
120
|
+
of five claims survive in #67.
|
|
121
|
+
- **`TG-PIPELINE-REPRODUCES-THE-ORACLE` is restated as
|
|
122
|
+
`TG-PROBE-NEVER-EDITS-THE-TREE-IT-MEASURES`**, both faults carried over
|
|
123
|
+
unchanged, the old id retired through the ignore file with a reason.
|
|
124
|
+
The old statement promised that a probe "reproduces the known-answer
|
|
125
|
+
fixture's verdicts exactly", and **no fault in it ever falsified that
|
|
126
|
+
half**: one removes the restore, the other forces in-place isolation, and
|
|
127
|
+
both are about tree safety. A claim that is defended and still over-promises
|
|
128
|
+
is the one thing a probe cannot catch, so the statement was narrowed to what
|
|
129
|
+
its faults actually prove — swapping the defender alone would have kept the
|
|
130
|
+
over-promise and merely made it cheaper. The fixture oracle still runs on
|
|
131
|
+
every `npm test` and in CI; it is no longer the defender of a claim whose
|
|
132
|
+
cost it dominated.
|
|
133
|
+
- **The shared spec is named `claimspec`.** Schemas are now identified as
|
|
134
|
+
`urn:claimspec:v1:<kind>` rather than `urn:guard-spec:v1:<kind>`.
|
|
135
|
+
**No emitted document changes**: the identifier lives only in the schemas'
|
|
136
|
+
own `$id`/`$ref`, the validator's registry and the documentation, so every
|
|
137
|
+
evidence file, baseline, brief and status already written stays valid and
|
|
138
|
+
keeps validating. Nothing needs migrating.
|
|
139
|
+
The old name said which family of tools the formats came from. The name that
|
|
140
|
+
matters is what they are *for* — claims, the evidence that tried to falsify
|
|
141
|
+
them, and gating only the delta — and most of the tools that could adopt
|
|
142
|
+
them (a scanner with an existing-violations problem, a mutation tester, an
|
|
143
|
+
agent harness that needs `brief` and `status`) do no fault injection at all.
|
|
144
|
+
`spec/README.md` now says so, and says plainly which kinds a second tool can
|
|
145
|
+
adopt today and which cannot (#81).
|
|
146
|
+
- **The README leads with the plain explanation.** The research evidence is
|
|
147
|
+
still there and still matters, but it is not the first thing a newcomer
|
|
148
|
+
meets: an "In one minute" section now opens with what coverage cannot tell
|
|
149
|
+
you, the `objectContaining` exhibit, killed vs SURVIVED, the seven verdicts,
|
|
150
|
+
and — stated plainly — that mutation testing is from the 1970s and the novel
|
|
151
|
+
part is binding every fault to a claim.
|
|
152
|
+
- The one-line descriptions on npm, PyPI and the GitHub Action now say what the
|
|
153
|
+
tool does rather than what category it belongs to.
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
- **A vanished scratch worktree no longer costs the whole probe** (#64). A
|
|
157
|
+
parallel session cleaning up its *own* leaked scratch worktrees deleted this
|
|
158
|
+
run's while it was mid-fault; `restore()` then tried to write the original
|
|
159
|
+
back into a directory that was gone, threw `ENOENT` out of a `finally`, and
|
|
160
|
+
the probe died with no evidence file at all — every verdict already decided
|
|
161
|
+
thrown away, and an exit code `GATE-SEMANTICS.md` does not define.
|
|
162
|
+
In worktree mode the restore is belt-and-braces: the mutation only ever
|
|
163
|
+
existed inside a scratch worktree that is discarded at the end of the run, so
|
|
164
|
+
a target that has vanished is already restored in every sense that matters.
|
|
165
|
+
It is now recorded as `detail.restoreSkipped: "target-missing"` — a reader
|
|
166
|
+
can still see that the tree moved underneath the run — and the probe
|
|
167
|
+
continues. Under `--in-place` the same condition is the user's own file gone
|
|
168
|
+
and is still raised, loudly; so is any other failure to write the original
|
|
169
|
+
back, in both modes, because a permission error can mean a mutated file left
|
|
170
|
+
on disk.
|
|
171
|
+
More generally, **one bad fault no longer costs the evidence for the other
|
|
172
|
+
forty**. An unexpected throw inside a fault's probe ends *that fault* as
|
|
173
|
+
`unverifiable` with reason `probe-error` and a `detail.message` naming what
|
|
174
|
+
threw; the run finishes and writes its document. The verdict gates, so the
|
|
175
|
+
failure is loud rather than absorbed into a green run, and a `probe-error`
|
|
176
|
+
record is never reused by a later run — it says something about the run, not
|
|
177
|
+
about the code. A **precondition** failure is the deliberate exception: the
|
|
178
|
+
interpreter loading the source from outside the probed tree, or a runner that
|
|
179
|
+
does not resolve, is a statement about every verdict in the run and still
|
|
180
|
+
refuses it outright.
|
|
181
|
+
- **A prior verdict is no longer reused across a fault edit.** `probe` decided
|
|
182
|
+
reuse from the source hash, the defender hashes and the defender set — never
|
|
183
|
+
from the fault itself. So editing a fault's `find` or `replace` kept the
|
|
184
|
+
verdict measured against the fault it replaced: a weakened fault held the
|
|
185
|
+
`survived` it earned before, and a repaired anchor held `unverifiable`, with
|
|
186
|
+
the evidence reporting a result nobody had measured against the file's
|
|
187
|
+
current contents. `status` flagged the changed hash afterwards, but by then
|
|
188
|
+
the evidence had already said something untrue — and for a tool whose
|
|
189
|
+
premise is that a fault edit can never be invisible, that was the wrong
|
|
190
|
+
default. Reuse is now also keyed on the fault's content hash; a prior record
|
|
191
|
+
that carries none is probed again, because the cost of re-measuring is a run
|
|
192
|
+
and the cost of the other direction is a verdict nobody took.
|
|
193
|
+
Found by using the tool on itself: repairing two rotted anchors left both
|
|
194
|
+
claims reporting `UNVERIFIABLE (reused)` on the next probe.
|
|
195
|
+
|
|
196
|
+
### Added
|
|
197
|
+
|
|
198
|
+
- **A survivor now has to prove the defenders execute the subject** (#74). A
|
|
199
|
+
green baseline makes one failure mode structurally impossible — a misread
|
|
200
|
+
runner cannot flatter the suite, because a kill requires the defenders to
|
|
201
|
+
pass on unmodified source first. It says nothing about the opposite
|
|
202
|
+
direction. If the fault is applied to code the test process never executes,
|
|
203
|
+
the baseline is green, every probe run is green, and **every claim is
|
|
204
|
+
reported SURVIVED**. That output reads as a devastating audit finding and is
|
|
205
|
+
entirely false, and nothing in the run contradicts it, because every
|
|
206
|
+
individual check passed.
|
|
207
|
+
This is not hypothetical: a strict editable Python install registers a meta
|
|
208
|
+
path finder that is consulted before `sys.path`, so the interpreter loads the
|
|
209
|
+
original module while TestGuard faults the copy in its scratch worktree.
|
|
210
|
+
Measured, not argued — a fault independently verified to fail 2 of 4 tests
|
|
211
|
+
reported `4 passed`.
|
|
212
|
+
A would-be `survived` is now charged one more run: the subject is replaced
|
|
213
|
+
with content its loader cannot parse, and the defenders run once. Going red
|
|
214
|
+
proves they execute it (`detail.negativeControl: "reached"`) and the verdict
|
|
215
|
+
stands. Staying green proves they do not, and the verdict becomes
|
|
216
|
+
`unverifiable` with reason `subject-not-executed`. Charged **only** on a
|
|
217
|
+
survivor — a kill already proves the defenders reached the code — and cached
|
|
218
|
+
per subject and defender set. Each runner supplies what "cannot compile"
|
|
219
|
+
means for its language; one that cannot say does not guess, and no control is
|
|
220
|
+
run.
|
|
221
|
+
The predicate is *"the run did not stay green"*, not *"a test failed"*: an
|
|
222
|
+
unparseable module usually fails to **load**, which the runner reports as an
|
|
223
|
+
error with no tests run at all.
|
|
224
|
+
|
|
225
|
+
- **A runner pinned to one engine keeps every capability the module has.**
|
|
226
|
+
`pinned()` rebuilt the runner as an explicit allow-list of properties, so
|
|
227
|
+
`--runner pytest` and `--runner unittest` silently lost the negative control
|
|
228
|
+
that `--runner python` has: a survivor probed through either flag was never
|
|
229
|
+
checked, and an unreachable subject reported `survived` exactly as before.
|
|
230
|
+
Found by the Python fixture, not by the unit test, which iterated the four
|
|
231
|
+
runner modules rather than the registry the tool actually resolves through —
|
|
232
|
+
it was checking the code that was written instead of the code that runs. The
|
|
233
|
+
test now iterates `RUNNERS`, which is what makes the next capability added to
|
|
234
|
+
a runner fail loudly instead of quietly.
|
|
235
|
+
|
|
236
|
+
- **`--progress auto|tty|plain|ndjson|none` on `probe`** (#68). A probe used
|
|
237
|
+
to print its stage line only when `stderr` was a terminal, so CI, a
|
|
238
|
+
redirected log and an agent harness saw nothing at all for the whole run —
|
|
239
|
+
indistinguishable from a hang, and a gate you cannot tell from a hang is a
|
|
240
|
+
gate people start killing.
|
|
241
|
+
The carriage-return rewriting is a *rendering* choice, not a reason to
|
|
242
|
+
withhold the information. `auto` (the default) keeps the rewritten line at a
|
|
243
|
+
terminal and prints append-only lines everywhere else. `ndjson` emits one
|
|
244
|
+
JSON object per line for stages **and** for verdicts as they land, so a
|
|
245
|
+
machine can watch a probe without waiting for the document at the end.
|
|
246
|
+
Progress always goes to **stderr**, so `--json` leaves stdout as one
|
|
247
|
+
parseable document. `--quiet` and `--json` imply `none`; an explicit
|
|
248
|
+
`--progress` overrides both, because an operator who asks for a stream of
|
|
249
|
+
events has said what they want.
|
|
250
|
+
Self-claims `TG-PROGRESS-NOT-ONLY-FOR-A-TTY`, `TG-PROGRESS-NEVER-ON-STDOUT`
|
|
251
|
+
and `TG-PROGRESS-EXPLICIT-WINS`, all probed and killed.
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
- **`--cost`, on `probe` and `claims`** — what the defenders actually cost,
|
|
255
|
+
per claim and per defender file, read back out of the `durationMs` the
|
|
256
|
+
probe already records. It re-measures nothing and spawns nothing.
|
|
257
|
+
A probe's wall clock is `(baseline runs + faults × --confirm) × the cost of
|
|
258
|
+
the claim's defender SET`, per claim, so one slow acceptance test named by
|
|
259
|
+
five claims is paid for thirty times — and until now nothing in the output
|
|
260
|
+
said which file that was. The report names it, under **shared defenders**,
|
|
261
|
+
with the claims that pay for it.
|
|
262
|
+
Per-file figures are an **upper bound, not a share**: a run executes the
|
|
263
|
+
whole defender set at once, so the runner never says how much belonged to
|
|
264
|
+
which file. They overlap by construction and do not sum to the total, which
|
|
265
|
+
the report says on its own face. Only `totalMs` is additive.
|
|
266
|
+
|
|
267
|
+
- **DocGuard governs this repository's documentation.** Six canonical
|
|
268
|
+
documents under `docs-canonical/` — architecture, data model, security,
|
|
269
|
+
test spec, environment and requirements — written against v0.6.0 rather
|
|
270
|
+
than generated as templates, plus `DRIFT-LOG.md` for deliberate,
|
|
271
|
+
recorded deviations. Every validator passes: structure, doc sections,
|
|
272
|
+
changelog, test spec, environment, security, freshness, traceability,
|
|
273
|
+
docs-coverage, document lifecycle and TODO tracking. Structural maturity
|
|
274
|
+
99/100.
|
|
275
|
+
- **Requirements are traceable to tests.** Fourteen functional and eight
|
|
276
|
+
non-functional requirements, each carrying an `@req` annotation in the
|
|
277
|
+
test that verifies it, and a traceability matrix that must agree with
|
|
278
|
+
those annotations. 27/27 traceability checks pass.
|
|
279
|
+
- **Doc drift is caught automatically, not reviewed by hand.** `ci.yml`
|
|
280
|
+
runs `docguard guard` on every pull request — authoritative, unbypassable
|
|
281
|
+
and visible in review — and reports the maturity score without gating on
|
|
282
|
+
it. `.pre-commit-config.yaml` gives contributors the same checks locally,
|
|
283
|
+
opt-in per clone, alongside the claims check, the change gate and the
|
|
284
|
+
probe on pre-push.
|
|
285
|
+
- Every configuration file in the repository is now documented in
|
|
286
|
+
`ARCHITECTURE.md`, including the distinction between
|
|
287
|
+
`.pre-commit-hooks.yaml` (what we publish for consumers) and
|
|
288
|
+
`.pre-commit-config.yaml` (what contributors to this repository use).
|
|
289
|
+
README gains the `Usage` section the Standard README spec expects.
|
|
290
|
+
- **A Python runner** (`--runner python|pytest|unittest`, and `auto` when no
|
|
291
|
+
JavaScript runner resolves). `pytest` when the project's interpreter can
|
|
292
|
+
import it, stdlib `unittest` when it cannot; the engine that actually ran is
|
|
293
|
+
what `run.runner.name` records, and `--runner pytest` / `--runner unittest`
|
|
294
|
+
pin the choice and fail rather than fall back. **Nothing is installed into
|
|
295
|
+
the project under test**: the reporters ship inside TestGuard and reach the
|
|
296
|
+
interpreter through `PYTHONPATH`, so a codebase whose test dependencies are
|
|
297
|
+
the standard library stays one. A `.py` defender runs under Python whatever
|
|
298
|
+
the project runner is, so one claim can be defended by a vitest test and a
|
|
299
|
+
pytest test at once.
|
|
300
|
+
- `--python <path>` (or `TESTGUARD_PYTHON`) names the interpreter. Otherwise:
|
|
301
|
+
the active `VIRTUAL_ENV`, then the project's `.venv`/`venv`/`.env`, then
|
|
302
|
+
`python3` on PATH. An interpreter you name is the only one tried — TestGuard
|
|
303
|
+
never quietly substitutes another, which would change what the tests ran
|
|
304
|
+
against without changing anything the evidence says.
|
|
305
|
+
- **An import-provenance precondition, and `detail.targetNotImported`.** A
|
|
306
|
+
green baseline proves the harness is not reporting everything as broken. It
|
|
307
|
+
proves nothing about the opposite direction, and Python can reach it: a
|
|
308
|
+
strict editable install puts an import hook ahead of `sys.path`, so the
|
|
309
|
+
interpreter loads the original file while TestGuard faults the copy. The
|
|
310
|
+
baseline stays green, every claim is reported `SURVIVED`, and the result
|
|
311
|
+
reads as a devastating finding while being entirely false. With the fault
|
|
312
|
+
applied, TestGuard now asks which file was actually imported: loaded from
|
|
313
|
+
outside the tree being probed **refuses the run**; never imported at all is
|
|
314
|
+
recorded as `detail.targetNotImported` and warned about, because an import
|
|
315
|
+
in a branch the fault does not reach is legitimate.
|
|
316
|
+
- **`target-attribute-patched`**, a new defender signal. `patch("pkg.mod.fn")`
|
|
317
|
+
is not `vi.mock("./mod")`: it replaces one attribute, so the file still
|
|
318
|
+
detects a fault anywhere else in that module. Applying the JavaScript rule
|
|
319
|
+
would have dropped such files and reported `nocover` for well-defended
|
|
320
|
+
claims. Only a patch of the module itself removes a defender; an attribute
|
|
321
|
+
patch keeps it and names the attributes.
|
|
322
|
+
- Python fault producers for `testguard scaffold`: `condition-forced`,
|
|
323
|
+
`statement-deleted`, `return-altered`, `literal-changed`, `call-removed`,
|
|
324
|
+
`field-dropped` and `argument-swapped`, in Python syntax. A statement is
|
|
325
|
+
removed by replacing it with `pass` rather than deleting the line, and a line
|
|
326
|
+
that leaves a bracket open is never removed — both because a fault that
|
|
327
|
+
cannot compile is a probe run that says nothing about the tests.
|
|
328
|
+
- Python defender discovery, mock-awareness and blast radius: matched by module
|
|
329
|
+
name, the way Python itself matches, so `import`, `from … import`, aliases,
|
|
330
|
+
relative imports and `importlib.import_module` all resolve.
|
|
331
|
+
- `fixtures/known-answer-python/`: sixteen faults covering every verdict, each
|
|
332
|
+
verified by hand before it became the oracle, and reproduced under **both**
|
|
333
|
+
engines.
|
|
334
|
+
|
|
335
|
+
### Changed
|
|
336
|
+
|
|
337
|
+
- **The self-probe gate no longer runs a fifty-second acceptance test thirty
|
|
338
|
+
times.** Five claims named `test/probe.fixture.test.mjs` as their only
|
|
339
|
+
defender, which is what made the release gate take ~24 minutes. The
|
|
340
|
+
decisions those claims are about — how escalation attributes an undeclared
|
|
341
|
+
killer, what counts as a measurable run for the flake rate, which binary a
|
|
342
|
+
runner invokes, what makes a fault edit visible — are rules you can state in
|
|
343
|
+
three lines, and none of them needs a fixture to falsify. They are now pure
|
|
344
|
+
functions in `src/probe/attribution.mjs` and exported `argvFor` builders in
|
|
345
|
+
the runners, unit-tested in `test/attribution.test.mjs` (~0.5s).
|
|
346
|
+
This mirrors why `classify()` has always been pure: the order of its checks
|
|
347
|
+
*is* the spec, and every branch should be falsifiable without a runner.
|
|
348
|
+
No claim was weakened to get there. Each re-pointed claim was re-probed and
|
|
349
|
+
still **kills** its faults; two of them SURVIVED on the first narrowing
|
|
350
|
+
attempt, which is exactly the failure mode `--cost` now makes visible before
|
|
351
|
+
someone ships a cheaper, weaker gate.
|
|
352
|
+
**Measured on the same machine, serially, from scratch: 24.1 min → 9.6 min**
|
|
353
|
+
(68 faults, 68 killed, 0 unproven). `--cost` names what is left: the jest
|
|
354
|
+
and replay suites now dominate, at 207s and 148s of defender time.
|
|
355
|
+
|
|
356
|
+
- Style exceptions in the canonical documents are declared with a reason
|
|
357
|
+
(`docguard:quality ... off — why`) rather than satisfied by rewording.
|
|
358
|
+
This tool is defined by what must not happen — no network, no telemetry,
|
|
359
|
+
never optimistic under uncertainty — and stating those positively would
|
|
360
|
+
misdescribe them. A declared, reasoned exception is auditable; contorted
|
|
361
|
+
prose is not.
|
|
362
|
+
- `--runner` accepts `python`, `pytest` and `unittest`; `--runner auto` tries
|
|
363
|
+
vitest, then jest, then python. A JavaScript project with neither vitest nor
|
|
364
|
+
jest installed now also reports why Python did not resolve.
|
|
365
|
+
- The GitHub Action and the GitLab template take `runner` and `python` inputs.
|
|
366
|
+
|
|
367
|
+
### Fixed
|
|
368
|
+
|
|
369
|
+
- `gate --changed` treated a changed `.py` file as `non-source` and excluded it,
|
|
370
|
+
so it could never be reported as uncovered. Python is source; Python test
|
|
371
|
+
files (`test_*.py`, `*_test.py`) are tests.
|
|
372
|
+
- `walk()` descended into `.venv`, `venv`, `site-packages`, `__pycache__`,
|
|
373
|
+
`.tox`, `.nox` and the pytest/mypy/ruff caches. Every `test_*.py` of every
|
|
374
|
+
installed package was collected as a test file of the project under probe.
|
|
375
|
+
- `testguard claims` printed a JavaScript comment marker (`// unasserted:`)
|
|
376
|
+
when suggesting how to annotate a mock in a Python file.
|
|
377
|
+
- A `--runner-cmd` whose reporter emits something other than the jest-compatible
|
|
378
|
+
JSON shape produced `unverifiable / defenders-failed-to-load` with no message
|
|
379
|
+
at all. The verdict was already safe — a report with no recognised tests is a
|
|
380
|
+
non-green baseline, never a pass — but nothing said why. It now names the
|
|
381
|
+
mismatch.
|
|
382
|
+
|
|
383
|
+
## [0.6.0] - 2026-09-17
|
|
384
|
+
|
|
385
|
+
Everything the field asked for. Two independent field reports on private
|
|
386
|
+
AI-authored codebases drove eleven issues; this release closes all of them,
|
|
387
|
+
alongside the roadmap work that landed beside it.
|
|
388
|
+
|
|
389
|
+
**Read this before upgrading.** Defender discovery is now mock-aware: a test
|
|
390
|
+
that `vi.mock`s or `jest.mock`s the subject can detect no fault in it and is
|
|
391
|
+
no longer counted. Claims defended only by such tests therefore move to
|
|
392
|
+
`NOCOVER`, which is the truth those verdicts always should have told. Expect
|
|
393
|
+
new findings on the first probe after upgrading, and re-freeze your baseline
|
|
394
|
+
once you have read them — they are not regressions in your code, they are
|
|
395
|
+
blind spots that were previously invisible.
|
|
396
|
+
|
|
397
|
+
### Added
|
|
398
|
+
|
|
399
|
+
- **`testguard mcp`** — the operating loop, served over the Model Context
|
|
400
|
+
Protocol on stdio, so it survives a change of agent harness. The loop has
|
|
401
|
+
lived in a Claude Code skill and a session-start hook, and both vanish the
|
|
402
|
+
moment the harness is Cursor, Codex or whatever comes next. Five tools,
|
|
403
|
+
all **read-only**: `testguard_status`, `testguard_brief`,
|
|
404
|
+
`testguard_claims`, `testguard_evidence` and `testguard_next_command`.
|
|
405
|
+
**No tool runs a probe** — a probe is long-running, budgeted and the
|
|
406
|
+
person should see it happen, so `next_command` returns the shell line
|
|
407
|
+
instead; and no tool writes a file, because editing a claims file through
|
|
408
|
+
a connector would defeat recording every fault edit. The server declares
|
|
409
|
+
only a `tools` capability, and a tool that throws returns an `isError`
|
|
410
|
+
tool result rather than a protocol error, so one bad call never costs the
|
|
411
|
+
connection.
|
|
412
|
+
Hand-written against a pinned protocol version rather than built on the
|
|
413
|
+
official SDK: this tool keeps its single exact-pinned runtime dependency,
|
|
414
|
+
and the surface is three methods that will not grow. The tests speak the
|
|
415
|
+
wire format to a real child process and check every tool against the CLI's
|
|
416
|
+
own `--json` output, so the two cannot drift.
|
|
417
|
+
`init --mcp` prints the config for Claude Code, Cursor and Codex — printed,
|
|
418
|
+
never written, because a harness config is the person's file. Self-claims
|
|
419
|
+
`TG-MCP-IS-READ-ONLY` and
|
|
420
|
+
`TG-MCP-TOOL-FAILURE-IS-NOT-A-PROTOCOL-ERROR`. (#30)
|
|
421
|
+
|
|
422
|
+
- **Contention detection and `--serial`** (#26). `probe` looks for other test
|
|
423
|
+
runners before the first run, warns naming their pids, and records them on
|
|
424
|
+
the evidence (`run.contention`) — a contended machine turns a slow suite
|
|
425
|
+
into a `TIMEOUT` or `FLAKY-DEFENDER` verdict about the load, not the claim,
|
|
426
|
+
and a later reader of that verdict can now tell. `--serial` runs one test
|
|
427
|
+
file at a time (`--no-file-parallelism`, `--runInBand`, `--workers=1`) and
|
|
428
|
+
is recorded as `run.serial`. Best effort by design: an unreadable process
|
|
429
|
+
list is no detection and never fails a probe.
|
|
430
|
+
|
|
431
|
+
- **`claims --since <ref>`** — a claim that disappeared was invisible to every
|
|
432
|
+
other check: `probe` verifies what is there, `gate` sees the file covered by
|
|
433
|
+
another claim, `status` says clean. Since deleting a claim is cheaper than
|
|
434
|
+
weakening its fault (which `changedFaults` already surfaces), removal is now
|
|
435
|
+
reported: `removed-claim` and `removed-fault` gate, a rename that keeps the
|
|
436
|
+
statement verbatim is `renamed-claim` and does not, and where evidence exists
|
|
437
|
+
the finding names the verdict the claim last had. Excused by a `claim` entry
|
|
438
|
+
in `testguard.ignore.json` with a reason, with expiry honoured as everywhere
|
|
439
|
+
else. This repository's CI runs it on its own pull requests. Found the hard
|
|
440
|
+
way: a `--theirs` conflict resolution dropped two self-claims during this
|
|
441
|
+
release and nothing noticed. Self-claim `TG-REMOVED-CLAIM-IS-A-FINDING`. (#57)
|
|
442
|
+
|
|
443
|
+
- **`testguard replay --since <range>`** — would this suite have caught the
|
|
444
|
+
bugs that already escaped? For each fix commit in the range (one that
|
|
445
|
+
changes source *and* a test together), it reverts only the source to the
|
|
446
|
+
parent in a scratch worktree, removes the test the fix shipped, and runs the
|
|
447
|
+
tests that import the reverted code: `caught`, `blind`, `nocover`, `flaky`
|
|
448
|
+
or `unverifiable`. One patch counts once (`git patch-id`). It reports and
|
|
449
|
+
never gates — a bug that escaped is history, not a regression in this
|
|
450
|
+
change. An injected fault is one somebody thought of; a bug that shipped is
|
|
451
|
+
ground truth, with no equivalent-mutant argument to have about it.
|
|
452
|
+
- **Fault-class labelling and the first calibration.** Each replayed bug is
|
|
453
|
+
labelled with the injected-fault class its diff most resembles — the
|
|
454
|
+
scaffold producers read in reverse, deterministic, `other` rather than a
|
|
455
|
+
guess — and a `calibration` document is written beside the replay one:
|
|
456
|
+
per class, the share of real escaped bugs the suite missed, with a Wilson
|
|
457
|
+
interval and n. Only `caught` and `blind` carry information; the rest are
|
|
458
|
+
excluded from both sides. `calibration.schema.json` has had no producer
|
|
459
|
+
until now. The open question it exists to answer — does a calibration
|
|
460
|
+
learned on a repository with history transfer to a greenfield one — stays
|
|
461
|
+
open; this is the instrument, not the answer. (#35)
|
|
462
|
+
- New spec kind **`replay`** (`replay.schema.json`), with semantic rules:
|
|
463
|
+
`caught` needs every run to fail by assertion, `blind` needs every run to
|
|
464
|
+
pass, `flaky` needs runs that disagree, `nocover` cannot have run tests,
|
|
465
|
+
and a duplicate patch-id is rejected. Conformance example plus two
|
|
466
|
+
must-reject documents. Self-claims
|
|
467
|
+
`TG-REPLAY-FLAKY-IS-NEVER-CAUGHT`, `TG-REPLAY-DEDUPES-BY-PATCH`,
|
|
468
|
+
`TG-CALIBRATION-EXCLUDES-UNINFORMATIVE` and `TG-LABEL-NEVER-GUESSES`.
|
|
469
|
+
|
|
470
|
+
- **`baseline --restamp`** (#25). A baseline frozen from `--include-dirty`
|
|
471
|
+
evidence now records the snapshot commit and, once you commit, a clean
|
|
472
|
+
`probe` plus `--restamp` moves its `head` to that commit — only when the
|
|
473
|
+
fingerprints are identical and the tree is clean; a frozen contract is
|
|
474
|
+
never silently rewritten. `status` gains informational `notes` (never a
|
|
475
|
+
state): a baseline frozen from a snapshot or a dirty tree that predates
|
|
476
|
+
HEAD, or one whose head is not an ancestor of HEAD.
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
- **Mock-aware defender discovery** (#20). A test file that `vi.mock`s /
|
|
480
|
+
`jest.mock`s the target cannot detect any fault in it and is no longer a
|
|
481
|
+
discovered defender; `NOCOVER` now means "no test imports this source
|
|
482
|
+
without mocking it". The evidence lists such files under
|
|
483
|
+
`defenders.mocking` (a *declared* defender that mocks the subject stays,
|
|
484
|
+
and is listed, as a broken evidence chain); `claims` prints the split
|
|
485
|
+
(`18 import · 16 mock · 2 can detect`).
|
|
486
|
+
- **`mocked-never-asserted`** (#21): a static signal for a test that mocks
|
|
487
|
+
the target and never `expect(...)`s anything imported from it — the exact
|
|
488
|
+
signature of an escaped bug in a field report. Recorded on the evidence
|
|
489
|
+
(`defenders.signals`), printed by `claims`, carried into the brief's hints.
|
|
490
|
+
`// unasserted: <why>` above the mock silences it visibly
|
|
491
|
+
(`unasserted-annotated` with the reason).
|
|
492
|
+
- Alias resolution follows tsconfig `references` (the Vite layout, where
|
|
493
|
+
`paths` live in `tsconfig.app.json`) and reads vite/vitest `resolve.alias`
|
|
494
|
+
as text, so importers from nested `__tests__/` directories are found.
|
|
495
|
+
- Both known-answer fixtures gain a claim whose only importing test mocks
|
|
496
|
+
the module: expected `NOCOVER`, with the mocking file and the signal
|
|
497
|
+
recorded.
|
|
498
|
+
|
|
499
|
+
- **`testguard admit <test-file> --claim <ID>`** — the two-gate rule as a
|
|
500
|
+
named verb. Sugar over `probe --claim <ID> --include-dirty --no-escalate`:
|
|
501
|
+
the named test must be a declared or discovered defender of the claim
|
|
502
|
+
(exit `3` otherwise, with the `defendedBy` line to add); `ADMITTED`
|
|
503
|
+
(exit `0`) only when every fault of the claim is `killed` N/N on defenders
|
|
504
|
+
that were green N/N unmodified; anything else is `NOT ADMITTED` (exit `1`)
|
|
505
|
+
and names the first blocking fault with the hint the brief would give.
|
|
506
|
+
`--fault <FID>` judges one fault, `--confirm 1` gives a provisional
|
|
507
|
+
`ADMITTED?`, `--json` returns `{admitted, provisional, faults[], evidence,
|
|
508
|
+
command}`. Evidence goes to `.testguard/evidence-partial.json`; nothing new
|
|
509
|
+
in the evidence schema. The market's acceptance signal for a generated
|
|
510
|
+
test is "compiles, passes, raises coverage"; this one is "fails when the
|
|
511
|
+
claim is false", and it is now one command. `status.next` for `unproven`
|
|
512
|
+
and the installed skill's fix loop point at it. Self-claim
|
|
513
|
+
`TG-ADMIT-NEEDS-ALL-KILLED`.
|
|
514
|
+
|
|
515
|
+
### Changed
|
|
516
|
+
|
|
517
|
+
- **`init` installs the agent layer at the git root** (#23): the skill,
|
|
518
|
+
the session-start hook and the `AGENTS.md` section go where agent
|
|
519
|
+
sessions run; the `.gitignore` lines stay beside the claims file. A
|
|
520
|
+
second project in the same repository adds a hook line and an
|
|
521
|
+
`AGENTS.md` bullet; `--here` keeps the old placement. A written file
|
|
522
|
+
that `.gitignore` swallows is reported (exit 1), never offered for
|
|
523
|
+
commit.
|
|
524
|
+
- **The session-start hook resolves an installed `testguard` and contains no
|
|
525
|
+
form of `npx`** (#24): the project's `node_modules/.bin`, then the git
|
|
526
|
+
root's, then `command -v testguard`, then nothing — it exits 0 with no
|
|
527
|
+
output rather than break a session. Any earlier npx hook, `-y` or
|
|
528
|
+
`--no-install`, is replaced on the next `init`. The brief's first line
|
|
529
|
+
names the install that answered.
|
|
530
|
+
|
|
531
|
+
`--no-install` is **quiet, not offline**, which is why it is gone too:
|
|
532
|
+
measured, `npx --no-install --loglevel=http testguard-cli --version` in a
|
|
533
|
+
project with nothing installed logs
|
|
534
|
+
`npm http fetch GET 200 https://registry.npmjs.org/testguard-cli`, and
|
|
535
|
+
against an unreachable registry it exits non-zero. npm resolves the
|
|
536
|
+
packument before deciding not to install.
|
|
537
|
+
|
|
538
|
+
- `probe` honours an explicit `--ref` (even `--ref HEAD`) when defender or
|
|
539
|
+
target files are dirty, and gains `--ignore-dirty` for the implicit HEAD
|
|
540
|
+
(#19). Both warn with the file names and record them in the evidence as
|
|
541
|
+
`repo.ignoredDirty` (spec: new optional field; a snapshot run can never
|
|
542
|
+
carry it). The refusal stays for the implicit HEAD without the flag — that
|
|
543
|
+
is the silent-mismatch trap the check exists for.
|
|
544
|
+
|
|
545
|
+
- CI: TestGuard's self-probe runs on one Node leg instead of three, without
|
|
546
|
+
escalation, and restores the previous run's evidence from the cache so
|
|
547
|
+
unchanged claims reuse their verdicts. The verdicts do not depend on the
|
|
548
|
+
Node minor; the from-scratch probe was ~14 minutes per leg.
|
|
549
|
+
|
|
550
|
+
### Fixed
|
|
551
|
+
|
|
552
|
+
- **The README described a hook the code no longer writes.** Three passages
|
|
553
|
+
still documented the `npx --no-install` fallback that this release removes,
|
|
554
|
+
one of them repeating the "never fetches from the network" wording the
|
|
555
|
+
measurement above disproves — in the security-relevant paragraph, in a
|
|
556
|
+
release about detection power. Corrected, and now checked mechanically:
|
|
557
|
+
every `||`-chained `brief --text` command in the README must be one
|
|
558
|
+
`hookCommand()` actually emits, and the phrase "falls back to npx" is
|
|
559
|
+
banned outright. Both regressions were verified to fail the check before it
|
|
560
|
+
was accepted. Self-claim `TG-README-HOOK-MATCHES-THE-CODE`.
|
|
561
|
+
- **A claim can be defended and still over-promise, and no probe can catch
|
|
562
|
+
that.** `TG-INIT-HOOK-NO-NETWORK` was killed 3/3 on every run while the
|
|
563
|
+
code it guarded could still reach the registry: the fault matched the
|
|
564
|
+
code, the code matched the test, and the *statement* was the thing that
|
|
565
|
+
was wrong. Fault injection measures whether a test would notice the code
|
|
566
|
+
changing; it cannot measure whether the sentence a human wrote is true of
|
|
567
|
+
the world. That is the standing limit of this method, and the only remedy
|
|
568
|
+
is reading claims against reality — which is what happened here. The
|
|
569
|
+
statement and its fault were corrected together.
|
|
570
|
+
- **The session-start hook now contains no form of `npx`.** It fell back to
|
|
571
|
+
`npx --no-install`, and `--no-install` is quiet rather than offline:
|
|
572
|
+
measured, `npx --no-install --loglevel=http testguard-cli --version` in a
|
|
573
|
+
project with nothing installed logs
|
|
574
|
+
`npm http fetch GET 200 https://registry.npmjs.org/testguard-cli`, and
|
|
575
|
+
against an unreachable registry it exits non-zero. npm resolves the
|
|
576
|
+
packument before deciding not to install. The fallback is now a
|
|
577
|
+
`command -v` lookup, which covers a global install with no network at all.
|
|
578
|
+
`TG-INIT-HOOK-NO-NETWORK` was defended while its statement over-promised;
|
|
579
|
+
the statement and its fault are corrected together.
|
|
580
|
+
- The PATH branch is braced. `a || b && c` binds as `(a || b) && c` in sh, so
|
|
581
|
+
the unbraced form printed the brief **twice** whenever the local binary
|
|
582
|
+
succeeded. The tests now execute the hook in all three cases (local, PATH,
|
|
583
|
+
neither) rather than matching its text.
|
|
584
|
+
- `init` recognises an earlier `npx --no-install` hook as well as `npx -y`,
|
|
585
|
+
and its which-project check no longer mistakes `brief --text 2>/dev/null`
|
|
586
|
+
for a hook belonging to a directory — that misfire meant a legacy root hook
|
|
587
|
+
was never replaced.
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
- **`replay` scopes to the project directory.** In a monorepo a fix commit
|
|
591
|
+
routinely touches several packages; the harness kept the out-of-project
|
|
592
|
+
files in the revert, so every cross-package fix came back
|
|
593
|
+
`revert-did-not-apply` — a tooling failure wearing the costume of a
|
|
594
|
+
verdict. Found on a real corpus, where it was two of the first three
|
|
595
|
+
commits. A commit with no source-and-test pair inside the project is no
|
|
596
|
+
longer a candidate at all. Self-claim `TG-REPLAY-SCOPES-TO-THE-PROJECT`.
|
|
597
|
+
- **`replay` reverts a file the fix ADDED by removing it**, and calls a commit
|
|
598
|
+
whose source is entirely new `no-prior-version` rather than a failed revert.
|
|
599
|
+
A fix routinely adds a helper as well as changing a module, and a file that
|
|
600
|
+
did not exist at the parent cannot be checked out of it: on a real corpus
|
|
601
|
+
this turned **nineteen of forty** commits into `revert-did-not-apply` — a
|
|
602
|
+
tooling failure reading as a verdict and hiding every real result behind it.
|
|
603
|
+
An addition is not a bug the suite could have caught, so it never enters a
|
|
604
|
+
calibration. Self-claim
|
|
605
|
+
`TG-REPLAY-ADDED-FILE-IS-REMOVED-NOT-CHECKED-OUT`.
|
|
606
|
+
- `replay --out <path>` now writes the calibration beside it rather than into
|
|
607
|
+
the project's `.testguard/`: the two documents are one result and splitting
|
|
608
|
+
them loses the pairing.
|
|
609
|
+
|
|
10
610
|
## [0.5.0] - 2026-09-17
|
|
11
611
|
|
|
12
612
|
The change gate: unclaimed code is now a finding.
|
|
13
613
|
|
|
14
614
|
### Added
|
|
15
615
|
|
|
616
|
+
- **Playwright runner, selected per file** (#15). A defender under
|
|
617
|
+
`playwright.config.*`'s `testDir` runs under Playwright whatever the
|
|
618
|
+
project runner is; one claim may list a unit test and a browser spec, and
|
|
619
|
+
the evidence records `defenders.byRunner` and `run.runners`. Playwright's
|
|
620
|
+
statuses map onto the verdict rules: `timedOut` is a timeout, never a
|
|
621
|
+
kill; `flaky` (failed, passed on retry) is a non-green run, so a defender
|
|
622
|
+
that only passes on retry is `FLAKY-DEFENDER` even though Playwright exits
|
|
623
|
+
0. Runs from several runners merge pessimistically. `--runner playwright`
|
|
624
|
+
selects it for a whole project. A browserless mixed fixture
|
|
625
|
+
(`fixtures/known-answer-playwright`, with its own dependencies) is the
|
|
626
|
+
oracle, verified by hand; a dedicated CI job runs it.
|
|
627
|
+
- **Two UI `scaffold` shapes**: `element-removed` (a one-line JSX element,
|
|
628
|
+
self-closing or paired) and `handler-dropped` (an `on<Event>={…}` prop, on
|
|
629
|
+
its own line or inline). Never inside tests, fixtures or migrations.
|
|
630
|
+
|
|
631
|
+
- **Two `scaffold` shapes and two fault classes.** `field-dropped` (#14):
|
|
632
|
+
a field removed from an object that is returned, built by an arrow,
|
|
633
|
+
assigned to a payload-ish name, passed to a persistence/transport call,
|
|
634
|
+
or is a schema; an entry removed from an allow-list; a spread base dropped
|
|
635
|
+
from a merge. Only lines that can go on their own; never inside tests,
|
|
636
|
+
fixtures or migrations. `argument-swapped` (#22): a call kept with its
|
|
637
|
+
parameter-derived first argument swapped for `undefined` (and `{}` when
|
|
638
|
+
the argument is a call) — the seam fault a field report found behind an
|
|
639
|
+
escaped bug that no earlier shape could express. The known-answer fixture
|
|
640
|
+
gains one of each, verified by hand: the dropped `content` field
|
|
641
|
+
**survives** (the same `objectContaining` blind spot as the exhibit), the
|
|
642
|
+
swapped argument is killed.
|
|
643
|
+
|
|
16
644
|
- **`testguard gate --changed <ref>`** — the change gate. Every escaped
|
|
17
645
|
defect in the field reports was a *claim gap*: the feature shipped green
|
|
18
646
|
with zero claims, and `probe` is silent about unclaimed code by
|