testguard-cli 0.6.0 → 0.8.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +489 -0
  2. package/README.md +287 -34
  3. package/package.json +2 -2
  4. package/spec/GATE-SEMANTICS.md +210 -8
  5. package/spec/README.md +32 -11
  6. package/spec/lib/validate.mjs +125 -14
  7. package/spec/lib/wilson.mjs +112 -0
  8. package/spec/schemas/baseline.schema.json +7 -7
  9. package/spec/schemas/brief.schema.json +12 -12
  10. package/spec/schemas/calibration.schema.json +96 -26
  11. package/spec/schemas/claims.schema.json +129 -25
  12. package/spec/schemas/common.schema.json +127 -29
  13. package/spec/schemas/evidence.schema.json +175 -73
  14. package/spec/schemas/gate.schema.json +12 -12
  15. package/spec/schemas/ignore.schema.json +4 -4
  16. package/spec/schemas/replay.schema.json +10 -10
  17. package/spec/schemas/status.schema.json +25 -25
  18. package/src/cli.mjs +20 -5
  19. package/src/commands/claims.mjs +22 -2
  20. package/src/commands/probe.mjs +24 -4
  21. package/src/commands/replay.mjs +1 -1
  22. package/src/gate/changed.mjs +9 -2
  23. package/src/probe/attribution.mjs +123 -0
  24. package/src/probe/classify.mjs +18 -2
  25. package/src/probe/cost.mjs +205 -0
  26. package/src/probe/discover.mjs +14 -6
  27. package/src/probe/inject.mjs +41 -9
  28. package/src/probe/mocks.mjs +2 -0
  29. package/src/probe/probe.mjs +235 -52
  30. package/src/probe/progress.mjs +87 -0
  31. package/src/probe/pyimports.mjs +287 -0
  32. package/src/probe/rank.mjs +2 -0
  33. package/src/probe/runners/index.mjs +38 -11
  34. package/src/probe/runners/jest.mjs +13 -1
  35. package/src/probe/runners/playwright.mjs +9 -0
  36. package/src/probe/runners/python/_testguard_pytest_plugin.py +132 -0
  37. package/src/probe/runners/python/_testguard_report.py +141 -0
  38. package/src/probe/runners/python/_testguard_unittest_main.py +168 -0
  39. package/src/probe/runners/python.mjs +316 -0
  40. package/src/probe/runners/shared.mjs +10 -1
  41. package/src/probe/runners/vitest.mjs +13 -1
  42. package/src/replay/replay.mjs +50 -22
  43. package/src/scaffold/producers.python.mjs +209 -0
  44. package/src/scaffold/scaffold.mjs +86 -32
  45. package/src/util/glob.mjs +7 -1
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # TestGuard
2
2
 
3
+ <!-- docguard:quality negation-load off — this README explains a tool defined by what must not happen; the negations are the product. -->
4
+ <!-- docguard:quality passive-voice off — verdicts and artifacts are the subjects throughout ("a fault is applied", "evidence is written"); naming an actor would misdescribe a tool nobody operates interactively. -->
5
+
3
6
  [![CI](https://github.com/raccioly/testguard/actions/workflows/ci.yml/badge.svg)](https://github.com/raccioly/testguard/actions/workflows/ci.yml)
4
7
  [![npm](https://img.shields.io/npm/v/testguard-cli.svg)](https://www.npmjs.com/package/testguard-cli)
5
8
  [![PyPI](https://img.shields.io/pypi/v/testguard-cli.svg)](https://pypi.org/project/testguard-cli/)
@@ -7,9 +10,8 @@
7
10
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
8
11
  [![deps](https://img.shields.io/badge/runtime%20deps-1%20pinned-brightgreen.svg)](./package.json)
9
12
 
10
- > Proves that a test suite actually defends the claims a project makes — by
11
- > injecting the faults those claims say cannot happen, and reporting every
12
- > fault the tests fail to detect.
13
+ > Breaks your code on purpose and reports every promise your tests did not
14
+ > notice breaking.
13
15
 
14
16
  **Not a test generator. A claim verifier.** Test generation is what happens
15
17
  after a claim turns out to be unfalsifiable.
@@ -22,6 +24,65 @@ surface ↔ code). All three run one loop:
22
24
  > declare what must be true → try mechanically to falsify it → freeze a
23
25
  > baseline → gate only the delta → brief the agent before it writes code.
24
26
 
27
+ ## In one minute
28
+
29
+ **Coverage tells you a line ran. It never tells you anyone checked the
30
+ result.** So a codebase can be fully covered and completely undefended, and
31
+ nothing in CI will say a word.
32
+
33
+ Here is a real test, from this repository's own fixture:
34
+
35
+ ```js
36
+ expect(store.writeAudit).toHaveBeenCalledWith(
37
+ expect.objectContaining({ action: 'MASK', scope: 'g1', ruleCount: 1 }),
38
+ ); // `content` is never named — so nothing checks it
39
+ ```
40
+
41
+ `objectContaining` ignores keys it does not list. Swap the redacted text for
42
+ the **raw secret** and this test still passes. Coverage of that line: 100%.
43
+ The audit log now leaks the very thing it exists to protect.
44
+
45
+ **The method: break the code deliberately, then watch what the tests do.**
46
+
47
+ - **killed** — you broke it, a test failed. Good. That behaviour is genuinely
48
+ defended.
49
+ - **SURVIVED** — you broke it, everything stayed green. A blind spot.
50
+
51
+ The word trips everyone once: it is the *fault* that survived, not the test.
52
+ **SURVIVED is the bad news.** A healthy report is full of `killed`.
53
+
54
+ But *"did the tests fail?"* is a sloppy question — a red suite is not proof of
55
+ detection. So there are seven verdicts, and each means something different:
56
+
57
+ | Verdict | What it means |
58
+ |---|---|
59
+ | `killed` | A test body ran and rejected the behaviour. The only good outcome. |
60
+ | `SURVIVED` | Everything passed. A real blind spot in the tests. |
61
+ | `NOCOVER` | No test even looks at this code. Not "weak tests" — *no* tests. |
62
+ | `UNVERIFIABLE` | The fault could not be applied: its anchor moved, or matches twice. Nothing was learned. |
63
+ | `FAULT-INVALID` | The break itself was broken — it did not compile. Our fault, not yours. |
64
+ | `TIMEOUT` | The suite hung. A hang is not a detection. |
65
+ | `FLAKY-DEFENDER` | The tests are not reliable enough on untouched code to be asked the question. |
66
+
67
+ Every ambiguity rounds toward *unproven*: three runs rather than one, a green
68
+ baseline required before any fault is injected, and a timeout, a load failure
69
+ or a mixed result is never a kill. The reason is the constraint the whole
70
+ design follows from — **a tool that reports everything as caught is worse than
71
+ no tool at all, because nobody questions good news.**
72
+
73
+ **What is not new:** breaking code to test your tests is *mutation testing*,
74
+ and it dates to the 1970s. Stryker, PIT, mutmut and Cosmic Ray all do it.
75
+
76
+ **What is different is the question.** Classic mutation testing mutates
77
+ everything mechanically and hands you *"mutation score: 73%"* — a number that
78
+ is not actionable, not auditable, and cannot tell you which promise is at
79
+ risk. TestGuard binds every fault to a **stated claim**, so the output is not
80
+ a score but a finding: *"Your project says a missing scope fails closed.
81
+ Nothing checks that."* That is the difference between a metric and an audit.
82
+
83
+ 📄 **[Read the three-page technical brief (PDF)](docs/testguard-explained.pdf)**
84
+ — the idea on page one, mechanics and prior art after it.
85
+
25
86
  ## Why
26
87
 
27
88
  Coverage cannot tell a test that pins *correct* behaviour from one that pins
@@ -65,14 +126,34 @@ raises coverage. TestGuard admits it because it fails when the claim is false.
65
126
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
66
127
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
67
128
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
68
- | GitHub Action | `uses: raccioly/testguard@v0.6.0` — see [`action.yml`](./action.yml) |
129
+ | GitHub Action | `uses: raccioly/testguard@v0.8.0` — see [`action.yml`](./action.yml) |
69
130
  | 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) |
131
+ | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
71
132
 
72
133
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
73
134
  less than that many days ago (`ENOVERSIONS`); install that one with
74
135
  `npm i -D testguard-cli --min-release-age=0`.
75
136
 
137
+ ## Usage
138
+
139
+ ```bash
140
+ npx testguard-cli init # install the agent layer at the git root
141
+ npx testguard-cli status --json # where the project is, and the ONE next action
142
+ npx testguard-cli claims # what does this project claim, and is each claim probeable?
143
+ npx testguard-cli scaffold src/auth.ts # propose faults for a file, as a draft to keep or drop
144
+ npx testguard-cli probe # try to falsify each claim; report what the tests missed
145
+ npx testguard-cli admit test/auth.test.ts --claim AUTH-ADMIN # does this test satisfy the two-gate rule?
146
+ npx testguard-cli baseline # freeze today's unproven findings; only new ones gate
147
+ npx testguard-cli gate --changed origin/main # fail when a changed source file carries no claim
148
+ npx testguard-cli brief --text # tell the agent where the suite is blind, before it writes
149
+ npx testguard-cli replay --since v1.0..HEAD # would this suite have caught the bugs that escaped?
150
+ npx testguard-cli mcp # serve the read-only loop over MCP, on stdio
151
+ ```
152
+
153
+ Exit codes are the contract: `0` nothing new to prove, `1` unproven claims or
154
+ unclaimed changes, `2` a precondition failed and nothing was probed, `3` usage.
155
+ Every command accepts `--json`.
156
+
76
157
  ## How it works
77
158
 
78
159
  ```bash
@@ -146,14 +227,39 @@ npx testguard-cli admit test/x.test.ts --claim X # is this test green on HEAD
146
227
  there is, and the exact signature of one escaped bug in the field
147
228
  reports. Silence it, visibly, with `// unasserted: <why>` above the mock.
148
229
 
149
- Runners: **vitest** and **jest** (`--runner auto` picks the first that
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.
230
+ Runners: **vitest**, **jest**, **Playwright** and **Python** (`--runner
231
+ auto` picks the first of vitest, jest, python that resolves). A JavaScript
232
+ runner is resolved from the **project's own package** first — its pinned
233
+ version, its own bin script — and only then from an executable on PATH,
234
+ which the evidence records as `runner.source: "path"`; `npx` is never
235
+ asked, because its cache answers for packages a project does not have.
236
+ Anything else goes through `--runner-cmd`. Each runner is proven against
237
+ its own copy of the known-answer fixture.
238
+
239
+ **Python** runs under `pytest` when the project's interpreter can import
240
+ it and stdlib `unittest` when it cannot, and the evidence records the
241
+ engine that actually ran — `--runner pytest` or `--runner unittest` pins
242
+ the choice and fails rather than falling back. Nothing is installed into
243
+ the project: the reporters ship inside TestGuard and reach the interpreter
244
+ through `PYTHONPATH`, so a codebase whose test dependencies are the
245
+ standard library stays that way. The interpreter is the project's
246
+ `.venv`/`venv`, the active `VIRTUAL_ENV`, or `--python <path>`. A `.py`
247
+ defender runs under Python whatever the project runner is, so one claim
248
+ can be defended by a vitest test and a pytest test at once.
249
+
250
+ Two things Python forces that JavaScript does not. **`patch("pkg.mod.fn")`
251
+ is not `vi.mock`**: it replaces one attribute, so the file is still a
252
+ defender of every other fault in that module — it keeps its place and
253
+ carries a `target-attribute-patched` signal naming the attributes. And
254
+ **the fault must be the code that ran**: a strict editable install puts an
255
+ import hook ahead of `sys.path`, so Python can load the original file while
256
+ TestGuard faults the copy — a green baseline, every claim `SURVIVED`, and a
257
+ report that reads as a devastating finding while being entirely false. With
258
+ the fault applied, TestGuard asks which file was actually imported and
259
+ refuses the run when it is outside the tree being probed. When no defender
260
+ imported the subject at all, the run continues and the record says so
261
+ (`detail.targetNotImported`), because that is a fact about the defenders'
262
+ reach and not about their assertions.
157
263
 
158
264
  The two-gate rule, as one verb: `testguard admit <test-file> --claim <ID>`
159
265
  runs the claim's faults against your uncommitted test and answers
@@ -239,15 +345,18 @@ most resembles, which is the join key that turns an uninterpretable mutation
239
345
  score into a statement with a sample size:
240
346
 
241
347
  ```
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]
348
+ guard-removed missed 7/9 p=0.78 ci [0.45, 0.94]
349
+ field-dropped missed 4/4 p=1.00 ci [0.51, 1.00]
244
350
  ```
245
351
 
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.
352
+ Read as *"when a real bug of this class escapes, how often does the suite
353
+ miss it"* — a miss rate, so a high `p` is bad. It is written as a
354
+ `calibration` document beside the replay one. `caught`, `blind` and
355
+ `nocover` are measurements and enter the ratio, `nocover` as a miss: leaving
356
+ it out would score a project with no tests at all for a subsystem *better*
357
+ than one with weak tests. `flaky` and `unverifiable` are failed measurements
358
+ and enter neither side. Every `p` and `ci` is recomputable from `n` and the
359
+ miss count, and the validator recomputes them.
251
360
 
252
361
  **The open question this exists to answer.** Does a calibration learned on a
253
362
  repository *with* history transfer to a greenfield one that has none? AI
@@ -318,12 +427,12 @@ detected base that does not resolve is a warning for `status` and `brief`
318
427
  # GitHub Actions
319
428
  - uses: actions/checkout@v4
320
429
  with: { fetch-depth: 0 }
321
- - uses: raccioly/testguard@v0.6.0
430
+ - uses: raccioly/testguard@v0.8.0
322
431
  with: { command: gate }
323
432
 
324
433
  # GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
325
434
  include:
326
- - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.5.0/packaging/gitlab/testguard.gitlab-ci.yml'
435
+ - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml'
327
436
  inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
328
437
  ```
329
438
 
@@ -384,6 +493,45 @@ is the same one as everywhere else: a `claim` entry in
384
493
  Removal is allowed — claims can be wrong, superseded or split — and it is
385
494
  never silent.
386
495
 
496
+ ### Probing a Python project
497
+
498
+ Nothing is installed into your project. TestGuard carries its own reporters and
499
+ puts them on the interpreter's `PYTHONPATH`, so a codebase whose test
500
+ dependencies are the standard library keeps none.
501
+
502
+ ```bash
503
+ npx testguard-cli probe . # auto: pytest if importable, else stdlib unittest
504
+ npx testguard-cli probe . --runner unittest # pin the stdlib engine
505
+ npx testguard-cli probe . --python .venv/bin/python
506
+ ```
507
+
508
+ The evidence records the engine that actually ran, and `--runner pytest` fails
509
+ rather than quietly using `unittest` — which engine ran changes what the
510
+ evidence means.
511
+
512
+ Three things behave differently from JavaScript, and each one is a place a
513
+ naive port would have produced a confident wrong answer:
514
+
515
+ - **Defenders are matched by module name**, the way Python itself matches, so
516
+ a test that reaches your package through `sys.path`, a `conftest.py` or an
517
+ installed distribution is found either way.
518
+ - **`patch("pkg.mod.fn")` is not `vi.mock`.** It replaces one attribute, so the
519
+ file still detects a fault anywhere else in that module: it stays a defender
520
+ and carries a `target-attribute-patched` signal naming the attributes. Only a
521
+ patch of the module *itself* removes a defender.
522
+ - **The fault has to be the code that ran.** A strict editable install puts an
523
+ import hook ahead of `sys.path`, so Python can load your original file while
524
+ TestGuard faults its copy — green baseline, every claim `SURVIVED`, and a
525
+ report that reads as a catastrophe while being entirely false. With the fault
526
+ applied TestGuard asks which file was really imported and **refuses the run**
527
+ if it came from outside the tree being probed. If no defender imported the
528
+ subject at all, the run continues and the record says so
529
+ (`detail.targetNotImported`): that is a fact about the tests' reach, not
530
+ about their assertions, and the two must not read alike.
531
+
532
+ `testguard scaffold path/to/module.py` proposes Python faults the same way it
533
+ does for JavaScript.
534
+
387
535
  ### Run it from any harness
388
536
 
389
537
  The operating loop above lives in a Claude Code skill and a session-start
@@ -452,6 +600,92 @@ things make that safe:
452
600
  previous verdict, and makes reviewing that edit the next action. Editing a
453
601
  claim is allowed — claims can be wrong — but it is never invisible.
454
602
 
603
+ ### Watching a probe that is still running
604
+
605
+ A probe runs the defenders N times clean and N times per fault, which takes
606
+ minutes. It used to print its stage line only when `stderr` was a terminal —
607
+ so CI, a redirected log and an agent harness saw **nothing at all** for the
608
+ whole run, which is indistinguishable from a hang. A gate you cannot tell
609
+ from a hang is a gate people start killing.
610
+
611
+ The rewriting is a rendering choice, not a reason to withhold the
612
+ information:
613
+
614
+ | `--progress` | What you get |
615
+ |---|---|
616
+ | `auto` (default) | `tty` at a terminal, `plain` everywhere else |
617
+ | `tty` | one line, rewritten in place — for a human watching |
618
+ | `plain` | one append-only line per stage — a log file, CI, an agent |
619
+ | `ndjson` | one JSON object per line, stages **and** verdicts as they land |
620
+ | `none` | silence |
621
+
622
+ ```bash
623
+ npx testguard-cli probe . > probe.log 2>&1 # now reports; used to be silent
624
+ npx testguard-cli probe . --progress ndjson 2>events.ndjson
625
+ ```
626
+
627
+ ```
628
+ {"event":"stage","claim":"TG-COST-NAMES-SHARED-DEFENDERS","fault":"F1","stage":"baseline","run":1,"of":3,"at":"…"}
629
+ {"event":"verdict","claim":"TG-COST-NAMES-SHARED-DEFENDERS","fault":"F1","verdict":"killed","severity":"medium","file":"src/probe/cost.mjs","at":"…"}
630
+ ```
631
+
632
+ **Progress always goes to stderr**, so `--json` leaves stdout as one document
633
+ and nothing else. `--quiet` and `--json` imply `none`, and an explicit
634
+ `--progress` overrides both: an operator who asks for a stream of events has
635
+ said what they want.
636
+
637
+ ### What the probe costs, and why a gate gets slow
638
+
639
+ A probe's wall clock is not a function of how many claims you have. Per claim
640
+ it is roughly
641
+
642
+ ```
643
+ (baseline runs + faults × --confirm) × the cost of that claim's defender SET
644
+ ```
645
+
646
+ so one slow acceptance test, named as a defender by five claims, is paid for
647
+ thirty times. The symptom is a gate that takes twenty minutes; the cause is a
648
+ single line in the claims file, and nothing in the output used to say which.
649
+
650
+ ```bash
651
+ npx testguard-cli claims . --cost # read back from the last probe's evidence
652
+ npx testguard-cli probe . --cost # what the run you just made spent
653
+ ```
654
+
655
+ ```
656
+ 63 fault records cost 24m across 378 defender runs.
657
+
658
+ most expensive claims
659
+ 4m 58s TG-FAULT-EDIT-VISIBLE 1 fault · 6 runs
660
+
661
+ defender files, by the time of the runs that included them
662
+ (a claim runs its whole defender set at once, so these overlap and do not sum to the total)
663
+ 24m 3s test/probe.fixture.test.mjs 5 claims
664
+
665
+ shared defenders — each claim pays the file's full cost again
666
+ 24m 3s test/probe.fixture.test.mjs
667
+ named by TG-ESCALATION-N-RUNS, TG-FAULT-EDIT-VISIBLE, …
668
+ ```
669
+
670
+ Every number is read back out of the `durationMs` the probe already records.
671
+ `--cost` re-measures nothing, spawns nothing, and costs the price of reading
672
+ one JSON file.
673
+
674
+ **The per-file figures overlap.** A run executes a claim's whole defender set
675
+ at once, so the runner never says how much of it belonged to which file. A
676
+ file's `ms` is the time of every run that *included* it: an upper bound on
677
+ what removing it could save, which is the decision you are making. Only
678
+ `totalMs` is additive.
679
+
680
+ **What to do about a shared defender.** Extract the decision it proves into a
681
+ pure function, unit-test that, and point the claim at the unit test. This is
682
+ not a testing trick — it is the same reason `classify()` in this codebase is
683
+ pure. A rule you can state in three lines should not need a fifty-second
684
+ acceptance test to falsify it. When the extraction is right the claim still
685
+ fails on the fault; when it is wrong the claim SURVIVES, and the probe tells
686
+ you so before you have shipped a weaker gate. Do not narrow `defendedBy` and
687
+ assume — re-probe the claim and read the verdict.
688
+
455
689
  ### Authoring faults mechanically
456
690
 
457
691
  Writing faults by hand means reading the code to find exact anchors. Two
@@ -475,6 +709,18 @@ npx testguard-cli scaffold src/auth.ts --claim AUTH-ADMIN # every proposal und
475
709
  | `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
710
  | `handler-dropped` | an `on<Event>={…}` prop removed, whether it is its own line or inline in the tag; the control renders and does nothing |
477
711
 
712
+ The table above is JavaScript. `scaffold` reads Python too, and proposes the
713
+ same fault classes in Python syntax: `if <guard>:` → `if False:`,
714
+ `return <check>` → `return True`, `verify=True` → `verify=False`, a
715
+ parameter-derived argument swapped for `None`, a key dropped from a payload
716
+ `dict` or an allow-list. Two differences are deliberate. A statement is
717
+ removed by replacing it with `pass`, never by deleting the line, because a
718
+ block whose only statement is gone is an `IndentationError`; and a line that
719
+ leaves a bracket open (`COLOURS = {`) is never removed at all. Both exist
720
+ because a fault that cannot compile is a `fault-invalid` verdict — a probe run
721
+ spent saying nothing about the tests. The two JSX shapes (`element-removed`,
722
+ `handler-dropped`) have no Python meaning and are absent rather than faked.
723
+
478
724
  Every proposal's `find` is the exact line with `expectHits`/`occurrence`
479
725
  computed from the file, so it is verifiable by construction; provenance is
480
726
  `producer: derived`; `defendedBy` is prefilled from the tests that import
@@ -507,21 +753,28 @@ redacted text for the raw input and the test stays green. `probe` reports it
507
753
  as `SURVIVED`; the fixture's [README](fixtures/known-answer/README.md) walks
508
754
  through every verdict.
509
755
 
510
- ## Status
756
+ The same blind spot, in Python, is
757
+ [`fixtures/known-answer-python/`](fixtures/known-answer-python/README.md) —
758
+ run under **both** stdlib `unittest` and `pytest`, which have different report
759
+ shapes and different notions of failure and must still agree on all sixteen
760
+ verdicts.
511
761
 
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
517
- [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
762
+ ## Status
518
763
 
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
764
+ **v0.5.** Eleven commands (`status`, `init`, `claims`, `probe`, `admit`,
765
+ `replay`, `baseline`, `brief`, `gate`, `scaffold`, `mcp`), vitest, jest,
766
+ Playwright and Python (pytest / stdlib unittest) runners, hand-authored faults
767
+ plus a mechanical scaffold for JavaScript and Python, an agent operating layer
768
+ (`status`, `init`) and a change gate (`gate`). The contract spine — eight JSON
769
+ Schemas shared with the other Guard tools — is under [`spec/`](spec/). One
770
+ exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20, and
771
+ Python ≥ 3.8 only when probing Python.
772
+
773
+ Not yet: test generation (the acceptance half, `admit`, exists; the generating
774
+ half stays the agent's), runners beyond those four, AST-aware producers, and
775
+ the transfer of a calibration between repositories — `replay` measures it now;
776
+ whether it carries to a repository with no history is unproven. Each is
522
777
  designed for; none is claimed.
523
- vitest, jest and Playwright, AST-aware producers, and calibration of fault classes
524
- against real escaped bugs. Each is designed for; none is claimed.
525
778
 
526
779
  ## Licence
527
780
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "testguard-cli",
3
- "version": "0.6.0",
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.",
3
+ "version": "0.8.0",
4
+ "description": "Breaks your code on purpose and reports every promise your tests did not notice breaking. Mutation testing bound to the claims a project makes — vitest, jest, Playwright, pytest and unittest.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {