testguard-cli 0.1.2__tar.gz → 0.2.0__tar.gz

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 (132) hide show
  1. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/scripts/install-smoke.mjs +3 -1
  2. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/AGENTS.md +2 -0
  3. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/CHANGELOG.md +62 -0
  4. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/PKG-INFO +62 -12
  5. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/README.md +61 -11
  6. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/action.yml +1 -1
  7. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/README.md +1 -0
  8. testguard_cli-0.2.0/fixtures/known-answer/expected.json +53 -0
  9. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/testguard.claims.json +155 -34
  10. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/package-lock.json +2 -2
  11. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/package.json +1 -1
  12. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/packaging/homebrew/testguard.rb +1 -1
  13. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/pyproject.toml +1 -1
  14. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/GATE-SEMANTICS.md +10 -4
  15. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/baseline.json +2 -1
  16. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/evidence.json +9 -3
  17. testguard_cli-0.2.0/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +537 -0
  18. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/lib/validate.mjs +3 -0
  19. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/baseline.schema.json +4 -0
  20. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/claims.schema.json +1 -1
  21. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/evidence.schema.json +26 -2
  22. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/baseline/baseline.mjs +1 -1
  23. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/brief/brief.mjs +1 -1
  24. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/cli.mjs +12 -2
  25. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/commands/baseline.mjs +1 -1
  26. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/commands/claims.mjs +4 -2
  27. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/commands/probe.mjs +11 -2
  28. testguard_cli-0.2.0/src/commands/scaffold.mjs +34 -0
  29. testguard_cli-0.2.0/src/git.mjs +58 -0
  30. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/classify.mjs +2 -1
  31. testguard_cli-0.2.0/src/probe/discover.mjs +12 -0
  32. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/probe.mjs +47 -10
  33. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/rank.mjs +14 -0
  34. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/render.mjs +13 -3
  35. testguard_cli-0.2.0/src/scaffold/producers.mjs +85 -0
  36. testguard_cli-0.2.0/src/scaffold/scaffold.mjs +118 -0
  37. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/brief.test.mjs +12 -1
  38. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/classify.test.mjs +2 -0
  39. testguard_cli-0.2.0/test/discover.test.mjs +22 -0
  40. testguard_cli-0.2.0/test/probe-preconditions.test.mjs +37 -0
  41. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/probe.fixture.test.mjs +56 -1
  42. testguard_cli-0.2.0/test/scaffold.test.mjs +133 -0
  43. testguard_cli-0.2.0/test/snapshot.test.mjs +38 -0
  44. testguard_cli-0.2.0/testguard.claims.json +347 -0
  45. testguard_cli-0.1.2/fixtures/known-answer/expected.json +0 -15
  46. testguard_cli-0.1.2/src/git.mjs +0 -32
  47. testguard_cli-0.1.2/testguard.claims.json +0 -120
  48. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.gitattributes +0 -0
  49. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  50. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  51. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  52. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/dependabot.yml +0 -0
  53. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/scripts/sync-release-version.mjs +0 -0
  54. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/workflows/auto-merge.yml +0 -0
  55. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  56. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/workflows/release.yml +0 -0
  57. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/workflows/scheduled-release.yml +0 -0
  58. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.github/workflows/supply-chain.yml +0 -0
  59. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.gitignore +0 -0
  60. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.npmignore +0 -0
  61. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.npmrc +0 -0
  62. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/.pre-commit-hooks.yaml +0 -0
  63. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/CLAUDE.md +0 -0
  64. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  65. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/CONTRIBUTING.md +0 -0
  66. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/LICENSE +0 -0
  67. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/PRIVACY.md +0 -0
  68. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/SECURITY.md +0 -0
  69. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/SUPPORT.md +0 -0
  70. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/bench/README.md +0 -0
  71. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/cli/testguard.mjs +0 -0
  72. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/.gitignore +0 -0
  73. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/package.json +0 -0
  74. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/src/export.mjs +0 -0
  75. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/src/redact.mjs +0 -0
  76. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/test/flaky.test.mjs +0 -0
  77. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/test/redact.test.mjs +0 -0
  78. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/fixtures/known-answer/vitest.config.mjs +0 -0
  79. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/README.md +0 -0
  80. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/brief.json +0 -0
  81. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/calibration.json +0 -0
  82. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/claims.json +0 -0
  83. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/examples/ignore.json +0 -0
  84. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
  85. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/baseline.zero-count.json +0 -0
  86. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
  87. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
  88. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/calibration.p-outside-ci.json +0 -0
  89. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/calibration.positives-exceed-n.json +0 -0
  90. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.absolute-path.json +0 -0
  91. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
  92. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
  93. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.noop-fault.json +0 -0
  94. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
  95. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
  96. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
  97. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
  98. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
  99. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
  100. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
  101. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
  102. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
  103. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
  104. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
  105. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/invalid/ignore.short-reason.json +0 -0
  106. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/conformance/schemas.test.mjs +0 -0
  107. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/lib/fingerprint.mjs +0 -0
  108. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/brief.schema.json +0 -0
  109. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/calibration.schema.json +0 -0
  110. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/common.schema.json +0 -0
  111. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/spec/schemas/ignore.schema.json +0 -0
  112. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/claims/annotations.mjs +0 -0
  113. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/claims/load.mjs +0 -0
  114. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/commands/brief.mjs +0 -0
  115. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/evidence/writer.mjs +0 -0
  116. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/inject.mjs +0 -0
  117. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/runner-vitest.mjs +0 -0
  118. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/probe/worktree.mjs +0 -0
  119. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/util/glob.mjs +0 -0
  120. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/src/util/hash.mjs +0 -0
  121. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/annotations.test.mjs +0 -0
  122. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/baseline.test.mjs +0 -0
  123. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/glob.test.mjs +0 -0
  124. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/inject.test.mjs +0 -0
  125. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/rank-aliases.test.mjs +0 -0
  126. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/runner-command.test.mjs +0 -0
  127. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/runner-vitest.test.mjs +0 -0
  128. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/worktree.test.mjs +0 -0
  129. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/test/writer.test.mjs +0 -0
  130. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/testguard_cli/__init__.py +0 -0
  131. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/testguard_cli/wrapper.py +0 -0
  132. {testguard_cli-0.1.2 → testguard_cli-0.2.0}/vitest.config.mjs +0 -0
@@ -30,8 +30,10 @@ try {
30
30
  const bin = join(consumer, 'node_modules', '.bin', process.platform === 'win32' ? 'testguard.cmd' : 'testguard');
31
31
  const version = run(bin, ['--version'], consumer).trim();
32
32
  const claims = run(bin, ['claims', '.'], consumer);
33
+ const draft = JSON.parse(run(bin, ['scaffold', 'src/redact.mjs', '--json'], consumer));
34
+ if (!draft.claims?.length) throw new Error('scaffold produced no claims from the installed tarball');
33
35
  if (!/^\d+\.\d+\.\d+/.test(version)) throw new Error(`unexpected --version output: ${version}`);
34
- if (!claims.includes('8 claims')) throw new Error(`claims did not list the fixture:\n${claims}`);
36
+ if (!/^\d+ claims in /.test(claims)) throw new Error(`claims did not list the fixture:\n${claims}`);
35
37
  const deps = Object.keys(JSON.parse(run(npm, ['ls', '--omit=dev', '--json', '--depth=0'], consumer)).dependencies ?? {});
36
38
  console.log(`install smoke OK — testguard ${version} runs from the installed tarball; consumer deps: ${deps.join(', ')}`);
37
39
  } finally {
@@ -19,6 +19,7 @@ npm run test:spec # conformance suite only
19
19
  npm run self:probe # TestGuard probes its own claims; must exit 0
20
20
  npm run test:install # the packed tarball must run with production deps only
21
21
  node cli/testguard.mjs claims fixtures/known-answer
22
+ node cli/testguard.mjs scaffold src/probe/classify.mjs --json # what the producers propose for a file
22
23
  ```
23
24
 
24
25
  ## Key files
@@ -29,6 +30,7 @@ node cli/testguard.mjs claims fixtures/known-answer
29
30
  - `src/probe/probe.mjs` — orchestrator: isolation → baseline (cached per defender set) → apply → probe → escalate → restore → classify → rank.
30
31
  - `fixtures/known-answer/expected.json` — the oracle. Never edit it to match output.
31
32
  - `testguard.claims.json` — claims about this codebase, probed in CI.
33
+ - `src/scaffold/producers.mjs` — the five fault shapes. Deterministic line heuristics; no AST, no LLM. A new shape needs a synthetic-file test and a README row.
32
34
 
33
35
  ## Rules
34
36
 
@@ -7,6 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-09-17
11
+
12
+ ### Added
13
+
14
+ - **`testguard scaffold <file>`** — mechanical fault producer (#6). Proposes
15
+ the five shapes both field reports found behind ~80% of hand-written
16
+ faults: guard forced false, single-line guard or state change removed,
17
+ `return <check>` → `return true`, security literal weakened, check call
18
+ removed. Every proposal is an exact-line anchor with `expectHits` and
19
+ `occurrence` computed from the file (verifiable by construction; anything
20
+ `locate()` would reject is never emitted), `producedBy: derived`,
21
+ `defendedBy` prefilled from the tests that import the module, grouped
22
+ under a preceding `@claim` annotation or by enclosing function.
23
+ Statements are `TODO:` placeholders; the output is a draft under
24
+ `.testguard/`, never the claims file. `--claim <ID>` puts everything under
25
+ one claim and copies it if it exists; `--json` prints instead.
26
+ - Self-claim `TG-SCAFFOLD-ANCHORS-HIT`; install smoke exercises `scaffold`.
27
+
28
+ ## [0.1.3] - 2026-09-17
29
+
30
+ From a second field report on a real codebase (456 tests, 27 claims, 35
31
+ faults; 13 survived on the first run, two critical claims with zero coverage).
32
+
33
+ ### Fixed
34
+
35
+ - **Worktree mode probed HEAD while reading the claims file from the working
36
+ tree**, so uncommitted defender changes were silently ignored — the same
37
+ survivors came back with no hint why. `probe` now refuses (exit 2) when any
38
+ resolved defender or fault target has uncommitted changes, naming the files
39
+ and the commit it would have probed. Every summary names the commit probed.
40
+ - `killed-by-undeclared-tests` never said which tests killed the fault; the
41
+ author could not fix `defendedBy` without grepping the suite. Evidence now
42
+ carries `detail.undeclaredKillers` and the CLI names the files.
43
+ - `anchor-ambiguous` did not say how many hits; `detail.anchor { hits,
44
+ expected }` is recorded and printed.
45
+ - A replacement with an unbalanced paren was `suite-failed-to-load`, not
46
+ `replacement-does-not-compile`: esbuild/vitest wording is now matched.
47
+ - The claims schema promised defender discovery for an absent `defendedBy`;
48
+ the tool answered `nocover`. Discovery is implemented: the test files that
49
+ import the fault's target (relative or alias), recorded as
50
+ `defenders.discovered`. `nocover` now means exactly "no test file imports
51
+ this source".
52
+ - `baseline.json` records `dirty`, as evidence already did.
53
+
54
+ ### Added
55
+
56
+ - `--include-dirty`: snapshot the working tree (tracked edits and untracked,
57
+ non-ignored files) into a throwaway commit and probe that. HEAD, index and
58
+ the user's tree are never touched; `run.repo.snapshot` records the commit.
59
+ - A one-line progress indicator on stderr (TTY only) so a minute of silence
60
+ is not mistaken for a hang.
61
+ - Fixture: a claim with no `defendedBy` whose defender is discovered.
62
+
63
+ ### Changed
64
+
65
+ - **Default output shows only unproven faults plus a killed count.**
66
+ `--verbose` restores the full stream.
67
+ - The `survived` hint reminds the author to check that the fault is
68
+ observable at all before writing a test for it.
69
+ - README: `$schema` path for consumers, `min-release-age` note, the
70
+ worktree-vs-working-tree rule, `brief` writes `brief.json` by default.
71
+
10
72
  ## [0.1.2] - 2026-09-17
11
73
 
12
74
  From a field report on a real codebase (63 test files, 458 tests, 39 faults).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: testguard-cli
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Proves a test suite actually defends the claims a project makes: injects the faults those claims forbid and reports every one the tests miss. Python wrapper for the Node.js CLI (requires Node.js 20+).
5
5
  Project-URL: Homepage, https://github.com/raccioly/testguard
6
6
  Project-URL: Documentation, https://github.com/raccioly/testguard#readme
@@ -76,9 +76,13 @@ tests were written against the survivors, 39/39 were killed.
76
76
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
77
77
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
78
78
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
79
- | GitHub Action | `uses: raccioly/testguard@v0.1.2` — see [`action.yml`](./action.yml) |
79
+ | GitHub Action | `uses: raccioly/testguard@v0.2.0` — see [`action.yml`](./action.yml) |
80
80
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
81
81
 
82
+ Projects that set `min-release-age` in `.npmrc` cannot see a version published
83
+ less than that many days ago (`ENOVERSIONS`); install that one with
84
+ `npm i -D testguard-cli --min-release-age=0`.
85
+
82
86
  ## How it works
83
87
 
84
88
  ```bash
@@ -86,11 +90,14 @@ npx testguard-cli claims # what does this project claim, and is every claim
86
90
  npx testguard-cli probe # try to falsify each claim; report what the tests missed
87
91
  npx testguard-cli baseline # freeze today's unproven findings; from now on only new ones gate
88
92
  npx testguard-cli brief # tell the agent where the suite is blind, before it writes
93
+ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to keep or drop
89
94
  ```
90
95
 
91
- 1. **Claims** live in `testguard.claims.json`: a statement, where it comes
92
- from, which tests supposedly defend it, and one or more *faults* — each a
93
- deterministic source change that would make the statement false. Every
96
+ 1. **Claims** live in `testguard.claims.json` (editors validate it against
97
+ `"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
98
+ a statement, where it comes from, which tests supposedly defend it, and
99
+ one or more *faults* — each a deterministic source change that would make
100
+ the statement false. Every
94
101
  claim and every fault records who produced it. `testguard claims`
95
102
  validates the file and reports drift against `@claim <ID>` annotations in
96
103
  source. Test files are deliberately not scanned — a claim asserted by a test is the authorship trap the tool exists for — and annotation ids must contain a hyphen so prose is never mistaken for one.
@@ -115,10 +122,23 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
115
122
  written to `.testguard/evidence.json` — validated against the spec before
116
123
  it is written.
117
124
 
125
+ Worktree mode probes a **commit**. If a defender or target file has
126
+ uncommitted changes, `probe` refuses and says so — otherwise your new
127
+ tests would be silently absent and the same survivors would come back
128
+ with no hint why. `--include-dirty` snapshots the working tree (tracked
129
+ edits and new files) into a throwaway commit and probes that; your tree,
130
+ HEAD and index are never touched. Every summary names the commit probed.
131
+
132
+ A claim with no `defendedBy` has its defenders **discovered**: the test
133
+ files that import the fault's target, by relative path or resolved alias.
134
+ `NOCOVER` then means exactly "no test file imports this source".
135
+
118
136
  Practical loop: first pass `--no-escalate` (escalation re-runs the whole
119
- suite N times per survivor); iterate on one claim with
120
- `--claim <ID> --in-place` only the files faults are applied to must be
121
- committed, test files may be dirty; final pass with defaults. A custom
137
+ suite N times per survivor); iterate on one claim with `--claim <ID>` and
138
+ either `--include-dirty` or `--in-place` (only fault target files must be
139
+ clean there; test files may be dirty); final pass with defaults. By default
140
+ the stream shows only unproven faults plus a killed count — `--verbose`
141
+ shows every fault. A custom
122
142
  runner (`pnpm --filter`, a specific config) goes in
123
143
  `--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
124
144
  see your `node_modules`, pass `--node-modules <dir>`.
@@ -127,7 +147,8 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
127
147
  source and defenders are unchanged reuse their prior verdict, so a probe
128
148
  in CI costs only what changed.
129
149
  4. **Brief** turns evidence plus baseline into a ranked, capped
130
- `## TEST BLINDSPOT CONTEXT` block. Wire it into an agent's session start
150
+ `## TEST BLINDSPOT CONTEXT` block, printed and also written to
151
+ `.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
131
152
  — for Claude Code, in `.claude/settings.json`:
132
153
 
133
154
  ```json
@@ -139,6 +160,34 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
139
160
  `--text` prints only, and exits 0 silently when there is no evidence yet,
140
161
  so the hook can never break a session.
141
162
 
163
+ ### Authoring faults mechanically
164
+
165
+ Writing faults by hand means reading the code to find exact anchors. Two
166
+ field reports found that ~80% of hand-written faults are one of five shapes,
167
+ so `scaffold` proposes them for you:
168
+
169
+ ```bash
170
+ npx testguard-cli scaffold src/auth.ts # → .testguard/scaffold-auth.json (a draft, never your claims file)
171
+ npx testguard-cli scaffold src/auth.ts --claim AUTH-ADMIN # every proposal under one claim; copies it if it exists
172
+ ```
173
+
174
+ | Shape | What it proposes |
175
+ |---|---|
176
+ | `condition-forced` | `if (<guard>) {` → `if (false) {` — a guard is a `!…` condition or one whose body returns, throws or 4xx-es |
177
+ | `statement-deleted` | a single-line guard (`if (…) return …;`) or a state change (`x = …;`) removed |
178
+ | `return-altered` | `return <check>;` (`===`, `.includes(`, `&&`, …) → `return true;` |
179
+ | `literal-changed` | `httpOnly`/`secure` flipped, `sameSite` → `none`, a cost/rounds → `1`, a ttl/tolerance/window/limit ×1000 |
180
+ | `call-removed` | a bare `verify…()` / `validate…()` / `check…()` / `authorize…()` call removed |
181
+
182
+ Every proposal's `find` is the exact line with `expectHits`/`occurrence`
183
+ computed from the file, so it is verifiable by construction; provenance is
184
+ `producer: derived`; `defendedBy` is prefilled from the tests that import
185
+ the module; proposals are grouped under a preceding `@claim <ID>` annotation
186
+ or by enclosing function. Statements are `TODO:` placeholders — a proposal
187
+ becomes a claim only when a human states what it defends. Deterministic
188
+ heuristics, no AST, no LLM; a proposal the tool cannot anchor is never
189
+ emitted.
190
+
142
191
  **Commit `.testguard/baseline.json`; ignore `evidence.json` and `brief.json`.**
143
192
  The baseline is the frozen contract; the other two are regenerated per run.
144
193
 
@@ -164,13 +213,14 @@ through every verdict.
164
213
 
165
214
  ## Status
166
215
 
167
- **v0.1.** Four commands, vitest runner, hand-authored faults. The contract
216
+ **v0.2.** Five commands, vitest runner, hand-authored faults plus a
217
+ mechanical scaffold for the five common shapes. The contract
168
218
  spine — six JSON Schemas shared with the other Guard tools — is under
169
219
  [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
170
220
 
171
221
  Not yet: test generation (the two-gate acceptance loop), other runners,
172
- mechanical fault producers, and calibration of fault classes against real
173
- escaped bugs. Each is designed for; none is claimed.
222
+ AST-aware producers, and calibration of fault classes against real escaped
223
+ bugs. Each is designed for; none is claimed.
174
224
 
175
225
  ## Licence
176
226
 
@@ -52,9 +52,13 @@ tests were written against the survivors, 39/39 were killed.
52
52
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
53
53
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
54
54
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
55
- | GitHub Action | `uses: raccioly/testguard@v0.1.2` — see [`action.yml`](./action.yml) |
55
+ | GitHub Action | `uses: raccioly/testguard@v0.2.0` — see [`action.yml`](./action.yml) |
56
56
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
57
57
 
58
+ Projects that set `min-release-age` in `.npmrc` cannot see a version published
59
+ less than that many days ago (`ENOVERSIONS`); install that one with
60
+ `npm i -D testguard-cli --min-release-age=0`.
61
+
58
62
  ## How it works
59
63
 
60
64
  ```bash
@@ -62,11 +66,14 @@ npx testguard-cli claims # what does this project claim, and is every claim
62
66
  npx testguard-cli probe # try to falsify each claim; report what the tests missed
63
67
  npx testguard-cli baseline # freeze today's unproven findings; from now on only new ones gate
64
68
  npx testguard-cli brief # tell the agent where the suite is blind, before it writes
69
+ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to keep or drop
65
70
  ```
66
71
 
67
- 1. **Claims** live in `testguard.claims.json`: a statement, where it comes
68
- from, which tests supposedly defend it, and one or more *faults* — each a
69
- deterministic source change that would make the statement false. Every
72
+ 1. **Claims** live in `testguard.claims.json` (editors validate it against
73
+ `"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
74
+ a statement, where it comes from, which tests supposedly defend it, and
75
+ one or more *faults* — each a deterministic source change that would make
76
+ the statement false. Every
70
77
  claim and every fault records who produced it. `testguard claims`
71
78
  validates the file and reports drift against `@claim <ID>` annotations in
72
79
  source. Test files are deliberately not scanned — a claim asserted by a test is the authorship trap the tool exists for — and annotation ids must contain a hyphen so prose is never mistaken for one.
@@ -91,10 +98,23 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
91
98
  written to `.testguard/evidence.json` — validated against the spec before
92
99
  it is written.
93
100
 
101
+ Worktree mode probes a **commit**. If a defender or target file has
102
+ uncommitted changes, `probe` refuses and says so — otherwise your new
103
+ tests would be silently absent and the same survivors would come back
104
+ with no hint why. `--include-dirty` snapshots the working tree (tracked
105
+ edits and new files) into a throwaway commit and probes that; your tree,
106
+ HEAD and index are never touched. Every summary names the commit probed.
107
+
108
+ A claim with no `defendedBy` has its defenders **discovered**: the test
109
+ files that import the fault's target, by relative path or resolved alias.
110
+ `NOCOVER` then means exactly "no test file imports this source".
111
+
94
112
  Practical loop: first pass `--no-escalate` (escalation re-runs the whole
95
- suite N times per survivor); iterate on one claim with
96
- `--claim <ID> --in-place` only the files faults are applied to must be
97
- committed, test files may be dirty; final pass with defaults. A custom
113
+ suite N times per survivor); iterate on one claim with `--claim <ID>` and
114
+ either `--include-dirty` or `--in-place` (only fault target files must be
115
+ clean there; test files may be dirty); final pass with defaults. By default
116
+ the stream shows only unproven faults plus a killed count — `--verbose`
117
+ shows every fault. A custom
98
118
  runner (`pnpm --filter`, a specific config) goes in
99
119
  `--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
100
120
  see your `node_modules`, pass `--node-modules <dir>`.
@@ -103,7 +123,8 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
103
123
  source and defenders are unchanged reuse their prior verdict, so a probe
104
124
  in CI costs only what changed.
105
125
  4. **Brief** turns evidence plus baseline into a ranked, capped
106
- `## TEST BLINDSPOT CONTEXT` block. Wire it into an agent's session start
126
+ `## TEST BLINDSPOT CONTEXT` block, printed and also written to
127
+ `.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
107
128
  — for Claude Code, in `.claude/settings.json`:
108
129
 
109
130
  ```json
@@ -115,6 +136,34 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
115
136
  `--text` prints only, and exits 0 silently when there is no evidence yet,
116
137
  so the hook can never break a session.
117
138
 
139
+ ### Authoring faults mechanically
140
+
141
+ Writing faults by hand means reading the code to find exact anchors. Two
142
+ field reports found that ~80% of hand-written faults are one of five shapes,
143
+ so `scaffold` proposes them for you:
144
+
145
+ ```bash
146
+ npx testguard-cli scaffold src/auth.ts # → .testguard/scaffold-auth.json (a draft, never your claims file)
147
+ npx testguard-cli scaffold src/auth.ts --claim AUTH-ADMIN # every proposal under one claim; copies it if it exists
148
+ ```
149
+
150
+ | Shape | What it proposes |
151
+ |---|---|
152
+ | `condition-forced` | `if (<guard>) {` → `if (false) {` — a guard is a `!…` condition or one whose body returns, throws or 4xx-es |
153
+ | `statement-deleted` | a single-line guard (`if (…) return …;`) or a state change (`x = …;`) removed |
154
+ | `return-altered` | `return <check>;` (`===`, `.includes(`, `&&`, …) → `return true;` |
155
+ | `literal-changed` | `httpOnly`/`secure` flipped, `sameSite` → `none`, a cost/rounds → `1`, a ttl/tolerance/window/limit ×1000 |
156
+ | `call-removed` | a bare `verify…()` / `validate…()` / `check…()` / `authorize…()` call removed |
157
+
158
+ Every proposal's `find` is the exact line with `expectHits`/`occurrence`
159
+ computed from the file, so it is verifiable by construction; provenance is
160
+ `producer: derived`; `defendedBy` is prefilled from the tests that import
161
+ the module; proposals are grouped under a preceding `@claim <ID>` annotation
162
+ or by enclosing function. Statements are `TODO:` placeholders — a proposal
163
+ becomes a claim only when a human states what it defends. Deterministic
164
+ heuristics, no AST, no LLM; a proposal the tool cannot anchor is never
165
+ emitted.
166
+
118
167
  **Commit `.testguard/baseline.json`; ignore `evidence.json` and `brief.json`.**
119
168
  The baseline is the frozen contract; the other two are regenerated per run.
120
169
 
@@ -140,13 +189,14 @@ through every verdict.
140
189
 
141
190
  ## Status
142
191
 
143
- **v0.1.** Four commands, vitest runner, hand-authored faults. The contract
192
+ **v0.2.** Five commands, vitest runner, hand-authored faults plus a
193
+ mechanical scaffold for the five common shapes. The contract
144
194
  spine — six JSON Schemas shared with the other Guard tools — is under
145
195
  [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
146
196
 
147
197
  Not yet: test generation (the two-gate acceptance loop), other runners,
148
- mechanical fault producers, and calibration of fault classes against real
149
- escaped bugs. Each is designed for; none is claimed.
198
+ AST-aware producers, and calibration of fault classes against real escaped
199
+ bugs. Each is designed for; none is claimed.
150
200
 
151
201
  ## Licence
152
202
 
@@ -38,7 +38,7 @@ inputs:
38
38
  version:
39
39
  description: 'testguard-cli version to run'
40
40
  required: false
41
- default: '0.1.2'
41
+ default: '0.2.0'
42
42
  node-version:
43
43
  description: 'Node.js version (20+)'
44
44
  required: false
@@ -24,6 +24,7 @@ evidence comes from running against a real codebase with real history.
24
24
  | REDACT-006 | `mask()` replaces with equal-length asterisks | F1 syntax error | fault-invalid |
25
25
  | EXPORT-001 | Exported rows never include `content` | F1 keeps content | nocover |
26
26
  | FLAKY-001 | (defender is flaky) | F1 anything | flaky-defender |
27
+ | DISCOVER-001 | Every match is replaced (no `defendedBy`) | F1 loop body removed | killed — defenders **discovered** by import |
27
28
 
28
29
  ## The exhibit — REDACT-001/F1
29
30
 
@@ -0,0 +1,53 @@
1
+ {
2
+ "confirmRuns": 3,
3
+ "expected": {
4
+ "REDACT-001/F1": {
5
+ "verdict": "survived",
6
+ "why": "The audit-row test asserts with objectContaining and omits `content`; a row carrying the raw input passes."
7
+ },
8
+ "REDACT-001/F2": {
9
+ "verdict": "killed",
10
+ "why": "The masked-output test asserts the secret is absent."
11
+ },
12
+ "REDACT-002/F1": {
13
+ "verdict": "killed",
14
+ "why": "The invalid-rule test asserts compileRules does not throw."
15
+ },
16
+ "REDACT-003/F1": {
17
+ "verdict": "survived",
18
+ "why": "No test exercises a missing scope; the fail-closed guard is undefended."
19
+ },
20
+ "REDACT-004/F1": {
21
+ "verdict": "timeout",
22
+ "why": "Tests awaiting redact() hit the 1s test timeout; a timeout is not an assertion failure and is not a kill."
23
+ },
24
+ "REDACT-005/F1": {
25
+ "verdict": "unverifiable",
26
+ "reason": "anchor-missing",
27
+ "why": "The find string is not present in the source."
28
+ },
29
+ "REDACT-005/F2": {
30
+ "verdict": "unverifiable",
31
+ "reason": "anchor-ambiguous",
32
+ "why": "`return null;` occurs twice; expectHits is 1."
33
+ },
34
+ "REDACT-006/F1": {
35
+ "verdict": "fault-invalid",
36
+ "reason": "replacement-does-not-compile",
37
+ "why": "The replacement introduces a syntax error; the suite cannot load."
38
+ },
39
+ "EXPORT-001/F1": {
40
+ "verdict": "nocover",
41
+ "why": "test/export.test.mjs does not exist; no defender resolves."
42
+ },
43
+ "FLAKY-001/F1": {
44
+ "verdict": "flaky-defender",
45
+ "reason": "defenders-not-green",
46
+ "why": "test/flaky.test.mjs fails every other run; the baseline is not green N/N."
47
+ },
48
+ "DISCOVER-001/F1": {
49
+ "verdict": "killed",
50
+ "why": "No defendedBy; test/redact.test.mjs is discovered because it imports src/redact.mjs, and its masked-output test fails."
51
+ }
52
+ }
53
+ }