testguard-cli 0.1.1__tar.gz → 0.1.3__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.1 → testguard_cli-0.1.3}/.github/scripts/install-smoke.mjs +1 -1
  2. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/supply-chain.yml +2 -2
  3. testguard_cli-0.1.3/CHANGELOG.md +148 -0
  4. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/PKG-INFO +48 -9
  5. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/README.md +47 -8
  6. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/action.yml +1 -1
  7. testguard_cli-0.1.3/cli/testguard.mjs +14 -0
  8. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/README.md +1 -0
  9. testguard_cli-0.1.3/fixtures/known-answer/expected.json +53 -0
  10. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/testguard.claims.json +155 -34
  11. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/package-lock.json +2 -2
  12. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/package.json +1 -1
  13. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/packaging/homebrew/testguard.rb +2 -2
  14. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/pyproject.toml +1 -1
  15. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/GATE-SEMANTICS.md +12 -6
  16. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/baseline.json +2 -1
  17. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/evidence.json +9 -3
  18. testguard_cli-0.1.3/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +537 -0
  19. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/lib/validate.mjs +3 -0
  20. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/baseline.schema.json +4 -0
  21. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/claims.schema.json +1 -1
  22. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/common.schema.json +1 -1
  23. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/evidence.schema.json +26 -2
  24. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/baseline/baseline.mjs +1 -1
  25. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/brief/brief.mjs +1 -1
  26. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/cli.mjs +11 -1
  27. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/baseline.mjs +4 -2
  28. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/claims.mjs +7 -4
  29. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/probe.mjs +19 -5
  30. testguard_cli-0.1.3/src/git.mjs +58 -0
  31. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/classify.mjs +2 -1
  32. testguard_cli-0.1.3/src/probe/discover.mjs +12 -0
  33. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/probe.mjs +87 -19
  34. testguard_cli-0.1.3/src/probe/rank.mjs +144 -0
  35. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/runner-vitest.mjs +61 -2
  36. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/worktree.mjs +28 -11
  37. testguard_cli-0.1.3/src/render.mjs +39 -0
  38. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/brief.test.mjs +22 -0
  39. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/classify.test.mjs +5 -0
  40. testguard_cli-0.1.3/test/discover.test.mjs +22 -0
  41. testguard_cli-0.1.3/test/probe-preconditions.test.mjs +37 -0
  42. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/probe.fixture.test.mjs +91 -1
  43. testguard_cli-0.1.3/test/rank-aliases.test.mjs +35 -0
  44. testguard_cli-0.1.3/test/runner-command.test.mjs +34 -0
  45. testguard_cli-0.1.3/test/snapshot.test.mjs +38 -0
  46. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/worktree.test.mjs +28 -1
  47. testguard_cli-0.1.3/testguard.claims.json +316 -0
  48. testguard_cli-0.1.1/CHANGELOG.md +0 -57
  49. testguard_cli-0.1.1/cli/testguard.mjs +0 -6
  50. testguard_cli-0.1.1/fixtures/known-answer/expected.json +0 -15
  51. testguard_cli-0.1.1/src/git.mjs +0 -32
  52. testguard_cli-0.1.1/src/probe/rank.mjs +0 -51
  53. testguard_cli-0.1.1/src/render.mjs +0 -28
  54. testguard_cli-0.1.1/testguard.claims.json +0 -120
  55. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.gitattributes +0 -0
  56. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  57. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  58. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  59. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/dependabot.yml +0 -0
  60. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/scripts/sync-release-version.mjs +0 -0
  61. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/auto-merge.yml +0 -0
  62. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/ci.yml +0 -0
  63. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/release.yml +0 -0
  64. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.github/workflows/scheduled-release.yml +0 -0
  65. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.gitignore +0 -0
  66. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.npmignore +0 -0
  67. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.npmrc +0 -0
  68. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/.pre-commit-hooks.yaml +0 -0
  69. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/AGENTS.md +0 -0
  70. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CLAUDE.md +0 -0
  71. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CODE_OF_CONDUCT.md +0 -0
  72. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/CONTRIBUTING.md +0 -0
  73. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/LICENSE +0 -0
  74. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/PRIVACY.md +0 -0
  75. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/SECURITY.md +0 -0
  76. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/SUPPORT.md +0 -0
  77. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/bench/README.md +0 -0
  78. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/.gitignore +0 -0
  79. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/package.json +0 -0
  80. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/src/export.mjs +0 -0
  81. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/src/redact.mjs +0 -0
  82. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/test/flaky.test.mjs +0 -0
  83. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/test/redact.test.mjs +0 -0
  84. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/fixtures/known-answer/vitest.config.mjs +0 -0
  85. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/README.md +0 -0
  86. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/brief.json +0 -0
  87. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/calibration.json +0 -0
  88. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/claims.json +0 -0
  89. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/examples/ignore.json +0 -0
  90. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
  91. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/baseline.zero-count.json +0 -0
  92. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
  93. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
  94. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/calibration.p-outside-ci.json +0 -0
  95. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/calibration.positives-exceed-n.json +0 -0
  96. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.absolute-path.json +0 -0
  97. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
  98. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
  99. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.noop-fault.json +0 -0
  100. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
  101. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
  102. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
  103. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
  104. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
  105. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
  106. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
  107. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
  108. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
  109. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
  110. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
  111. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/invalid/ignore.short-reason.json +0 -0
  112. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/conformance/schemas.test.mjs +0 -0
  113. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/lib/fingerprint.mjs +0 -0
  114. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/brief.schema.json +0 -0
  115. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/calibration.schema.json +0 -0
  116. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/spec/schemas/ignore.schema.json +0 -0
  117. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/claims/annotations.mjs +0 -0
  118. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/claims/load.mjs +0 -0
  119. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/commands/brief.mjs +0 -0
  120. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/evidence/writer.mjs +0 -0
  121. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/probe/inject.mjs +0 -0
  122. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/util/glob.mjs +0 -0
  123. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/src/util/hash.mjs +0 -0
  124. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/annotations.test.mjs +0 -0
  125. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/baseline.test.mjs +0 -0
  126. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/glob.test.mjs +0 -0
  127. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/inject.test.mjs +0 -0
  128. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/runner-vitest.test.mjs +0 -0
  129. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/test/writer.test.mjs +0 -0
  130. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/testguard_cli/__init__.py +0 -0
  131. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/testguard_cli/wrapper.py +0 -0
  132. {testguard_cli-0.1.1 → testguard_cli-0.1.3}/vitest.config.mjs +0 -0
@@ -31,7 +31,7 @@ try {
31
31
  const version = run(bin, ['--version'], consumer).trim();
32
32
  const claims = run(bin, ['claims', '.'], consumer);
33
33
  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}`);
34
+ if (!/^\d+ claims in /.test(claims)) throw new Error(`claims did not list the fixture:\n${claims}`);
35
35
  const deps = Object.keys(JSON.parse(run(npm, ['ls', '--omit=dev', '--json', '--depth=0'], consumer)).dependencies ?? {});
36
36
  console.log(`install smoke OK — testguard ${version} runs from the installed tarball; consumer deps: ${deps.join(', ')}`);
37
37
  } finally {
@@ -17,8 +17,8 @@ permissions:
17
17
  jobs:
18
18
  osv-scan-pr:
19
19
  if: github.event_name == 'pull_request'
20
- uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
20
+ uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
21
21
 
22
22
  osv-scan-scheduled:
23
23
  if: github.event_name != 'pull_request'
24
- uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
24
+ uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
@@ -0,0 +1,148 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.3] - 2026-09-17
11
+
12
+ From a second field report on a real codebase (456 tests, 27 claims, 35
13
+ faults; 13 survived on the first run, two critical claims with zero coverage).
14
+
15
+ ### Fixed
16
+
17
+ - **Worktree mode probed HEAD while reading the claims file from the working
18
+ tree**, so uncommitted defender changes were silently ignored — the same
19
+ survivors came back with no hint why. `probe` now refuses (exit 2) when any
20
+ resolved defender or fault target has uncommitted changes, naming the files
21
+ and the commit it would have probed. Every summary names the commit probed.
22
+ - `killed-by-undeclared-tests` never said which tests killed the fault; the
23
+ author could not fix `defendedBy` without grepping the suite. Evidence now
24
+ carries `detail.undeclaredKillers` and the CLI names the files.
25
+ - `anchor-ambiguous` did not say how many hits; `detail.anchor { hits,
26
+ expected }` is recorded and printed.
27
+ - A replacement with an unbalanced paren was `suite-failed-to-load`, not
28
+ `replacement-does-not-compile`: esbuild/vitest wording is now matched.
29
+ - The claims schema promised defender discovery for an absent `defendedBy`;
30
+ the tool answered `nocover`. Discovery is implemented: the test files that
31
+ import the fault's target (relative or alias), recorded as
32
+ `defenders.discovered`. `nocover` now means exactly "no test file imports
33
+ this source".
34
+ - `baseline.json` records `dirty`, as evidence already did.
35
+
36
+ ### Added
37
+
38
+ - `--include-dirty`: snapshot the working tree (tracked edits and untracked,
39
+ non-ignored files) into a throwaway commit and probe that. HEAD, index and
40
+ the user's tree are never touched; `run.repo.snapshot` records the commit.
41
+ - A one-line progress indicator on stderr (TTY only) so a minute of silence
42
+ is not mistaken for a hang.
43
+ - Fixture: a claim with no `defendedBy` whose defender is discovered.
44
+
45
+ ### Changed
46
+
47
+ - **Default output shows only unproven faults plus a killed count.**
48
+ `--verbose` restores the full stream.
49
+ - The `survived` hint reminds the author to check that the fault is
50
+ observable at all before writing a test for it.
51
+ - README: `$schema` path for consumers, `min-release-age` note, the
52
+ worktree-vs-working-tree rule, `brief` writes `brief.json` by default.
53
+
54
+ ## [0.1.2] - 2026-09-17
55
+
56
+ From a field report on a real codebase (63 test files, 458 tests, 39 faults).
57
+
58
+ ### Fixed
59
+
60
+ - **A runner that cannot be resolved was reported as `FLAKY-DEFENDER`.** In
61
+ the scratch worktree, a symlinked `node_modules` (the sibling/auto-worktree
62
+ layout) was invisible, vitest failed to load, and the load error was
63
+ classified as flaky tests — blaming the wrong party. Symlinked
64
+ `node_modules` are now linked to their resolved target; the runner is
65
+ checked before any verdict and an unresolvable one is a precondition
66
+ failure (exit 2) with the fix in the message; defenders that fail to
67
+ *load* during a baseline are `UNVERIFIABLE` (`defenders-failed-to-load`),
68
+ never flaky.
69
+ - **Blast radius ignored path aliases.** `tsconfig`/`jsconfig` `paths` (with
70
+ `baseUrl` and relative `extends`) and `package.json#imports` are resolved,
71
+ so a module imported 83 times via `@/…` no longer ranks as if nothing
72
+ imported it. Bare package specifiers remain ignored, as documented.
73
+ - Summary said "N unproven claims" when N counted faults; it now reports
74
+ unproven faults *and* the distinct claims they belong to.
75
+ - `testguard claims` never showed which claims carry a `@claim` annotation;
76
+ it now reports the count and marks each annotated row.
77
+ - Piping output to a closed reader (`testguard claims | head`) no longer
78
+ crashes with an `EPIPE` stack trace.
79
+
80
+ ### Added
81
+
82
+ - `--claim <ID,ID>` probes only the named claims and writes
83
+ `.testguard/evidence-partial.json`, keeping the canonical evidence intact —
84
+ turns a fix-loop iteration from minutes into seconds.
85
+ - `--runner-cmd "<cmd>"` with `{files}` and `{out}` placeholders for
86
+ monorepos, custom configs and other package managers.
87
+ - `--node-modules <dir>` (or `TESTGUARD_NODE_MODULES`) to link a specific
88
+ `node_modules` into the scratch worktree.
89
+
90
+ ### Changed
91
+
92
+ - The `--in-place` precondition message says what it means: only fault
93
+ target files must be clean; test files may be dirty.
94
+ - `testguard baseline` prints the two `.gitignore` lines for the regenerated
95
+ files instead of leaving it to the README.
96
+ - With no baseline, the ranked block is not printed a second time under the
97
+ per-fault stream.
98
+ - `--quiet` is documented precisely.
99
+ - Staged Homebrew formula carries the sha256 of the published 0.1.1 tarball.
100
+
101
+ ## [0.1.1] - 2026-09-17
102
+
103
+ ### Fixed
104
+
105
+ - **0.1.0 did not run when installed.** `ajv`, which validates every document
106
+ against the spec, was declared as a devDependency, so `testguard` crashed on
107
+ startup from npm, `npx`, `pip` and Homebrew. It is now an exact-pinned
108
+ runtime dependency — the project's one dependency.
109
+ - CI and the release job now pack the tarball, install it into a scratch
110
+ project with production dependencies only, and run the CLI from there
111
+ (`npm run test:install`). The suite alone runs from the checkout and could
112
+ not see this class of defect.
113
+
114
+ ### Changed
115
+
116
+ - Homebrew formula carries the sha256 of the published tarball; description
117
+ shortened to satisfy `brew audit --strict`.
118
+
119
+ ## [0.1.0] - 2026-09-17
120
+
121
+ First release. A claim verifier, not a test generator.
122
+
123
+ ### Added
124
+
125
+ - **Contract spine** (`spec/`): six shared JSON Schemas — claims, evidence,
126
+ baseline, ignore, calibration, brief — identified as `urn:guard-spec:v1:*`,
127
+ with a validator that enforces the semantic rules a schema cannot express,
128
+ a single fingerprint derivation, `GATE-SEMANTICS.md`, and a conformance
129
+ corpus (one valid document per kind, 22 must-reject documents).
130
+ - **`testguard claims`** — validates the claims file and reports drift
131
+ against `@claim <ID>` annotations in source.
132
+ - **`testguard probe`** — applies each fault in a scratch git worktree,
133
+ confirms the defenders green N times, runs them N times with the fault,
134
+ escalates survivors to the whole suite with N-run attribution, restores,
135
+ classifies into a closed verdict set (`killed`, `survived`, `nocover`,
136
+ `unverifiable`, `timeout`, `fault-invalid`, `flaky-defender`), ranks, and
137
+ writes evidence validated against the spec. `--ref` pins the probed commit.
138
+ Verdicts are reused when the target, defenders and N are unchanged.
139
+ - **`testguard baseline`** — freezes every non-passing fingerprint so only
140
+ new findings gate; severity floor via `--severity`.
141
+ - **`testguard brief`** — a ranked, capped `## TEST BLINDSPOT CONTEXT` block
142
+ for an agent's session start; `--text` is hook-safe.
143
+ - **Known-answer fixture** with a genuine `objectContaining` blind spot and a
144
+ case for every verdict; probed end to end in the test suite and in CI.
145
+ - **Self-verification**: `testguard.claims.json` states invariants of the
146
+ tool itself, probed by the tool in CI.
147
+ - Distribution: npm (`testguard-cli`), PyPI wrapper (`testguard-cli`),
148
+ GitHub Action, Homebrew formula (staged), pre-commit hooks.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: testguard-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -59,6 +59,15 @@ on known-broken code. The largest gap was a compliance-critical path with
59
59
  100% coverage, where the one assertion that mattered used
60
60
  `expect.objectContaining({...})` and omitted the field carrying the data.
61
61
 
62
+ A second, independent run on a different AI-authored codebase (63 test
63
+ files, 458 tests, 24 hand-written security claims, 39 faults): **21 of 39
64
+ faults survived a fully green suite — 9 of them critical.** Super-admin
65
+ gating, membership checks, cookie flags and the whole authorization callback
66
+ could be disabled without a single test noticing. One test file had
67
+ re-implemented the authorization logic *inside the test* and asserted
68
+ against the copy: fifteen green tests, zero detection. After wrapper-level
69
+ tests were written against the survivors, 39/39 were killed.
70
+
62
71
  ## Install
63
72
 
64
73
  | How | Command |
@@ -67,9 +76,13 @@ on known-broken code. The largest gap was a compliance-critical path with
67
76
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
68
77
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
69
78
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
70
- | GitHub Action | `uses: raccioly/testguard@v0.1.1` — see [`action.yml`](./action.yml) |
79
+ | GitHub Action | `uses: raccioly/testguard@v0.1.3` — see [`action.yml`](./action.yml) |
71
80
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
72
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
+
73
86
  ## How it works
74
87
 
75
88
  ```bash
@@ -79,12 +92,14 @@ npx testguard-cli baseline # freeze today's unproven findings; from now on on
79
92
  npx testguard-cli brief # tell the agent where the suite is blind, before it writes
80
93
  ```
81
94
 
82
- 1. **Claims** live in `testguard.claims.json`: a statement, where it comes
83
- from, which tests supposedly defend it, and one or more *faults* — each a
84
- deterministic source change that would make the statement false. Every
95
+ 1. **Claims** live in `testguard.claims.json` (editors validate it against
96
+ `"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
97
+ a statement, where it comes from, which tests supposedly defend it, and
98
+ one or more *faults* — each a deterministic source change that would make
99
+ the statement false. Every
85
100
  claim and every fault records who produced it. `testguard claims`
86
101
  validates the file and reports drift against `@claim <ID>` annotations in
87
- source.
102
+ 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.
88
103
  2. **Probe** confirms the defenders are green N times unmodified, applies
89
104
  each fault in a scratch git worktree (your tree is never touched), runs
90
105
  the defenders N times, re-runs survivors against the whole suite with
@@ -101,14 +116,38 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
101
116
  | `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
102
117
 
103
118
  Never a single score. Findings are ranked by severity, claim provenance
104
- and blast radius, and written to `.testguard/evidence.json` validated
105
- against the spec before it is written.
119
+ and blast radius (relative imports, `tsconfig` path aliases and
120
+ `package.json#imports` are resolved; bare package names are not), and
121
+ written to `.testguard/evidence.json` — validated against the spec before
122
+ it is written.
123
+
124
+ Worktree mode probes a **commit**. If a defender or target file has
125
+ uncommitted changes, `probe` refuses and says so — otherwise your new
126
+ tests would be silently absent and the same survivors would come back
127
+ with no hint why. `--include-dirty` snapshots the working tree (tracked
128
+ edits and new files) into a throwaway commit and probes that; your tree,
129
+ HEAD and index are never touched. Every summary names the commit probed.
130
+
131
+ A claim with no `defendedBy` has its defenders **discovered**: the test
132
+ files that import the fault's target, by relative path or resolved alias.
133
+ `NOCOVER` then means exactly "no test file imports this source".
134
+
135
+ Practical loop: first pass `--no-escalate` (escalation re-runs the whole
136
+ suite N times per survivor); iterate on one claim with `--claim <ID>` and
137
+ either `--include-dirty` or `--in-place` (only fault target files must be
138
+ clean there; test files may be dirty); final pass with defaults. By default
139
+ the stream shows only unproven faults plus a killed count — `--verbose`
140
+ shows every fault. A custom
141
+ runner (`pnpm --filter`, a specific config) goes in
142
+ `--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
143
+ see your `node_modules`, pass `--node-modules <dir>`.
106
144
  3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
107
145
  what was already known and exit non-zero only on what is new. Claims whose
108
146
  source and defenders are unchanged reuse their prior verdict, so a probe
109
147
  in CI costs only what changed.
110
148
  4. **Brief** turns evidence plus baseline into a ranked, capped
111
- `## TEST BLINDSPOT CONTEXT` block. Wire it into an agent's session start
149
+ `## TEST BLINDSPOT CONTEXT` block, printed and also written to
150
+ `.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
112
151
  — for Claude Code, in `.claude/settings.json`:
113
152
 
114
153
  ```json
@@ -35,6 +35,15 @@ on known-broken code. The largest gap was a compliance-critical path with
35
35
  100% coverage, where the one assertion that mattered used
36
36
  `expect.objectContaining({...})` and omitted the field carrying the data.
37
37
 
38
+ A second, independent run on a different AI-authored codebase (63 test
39
+ files, 458 tests, 24 hand-written security claims, 39 faults): **21 of 39
40
+ faults survived a fully green suite — 9 of them critical.** Super-admin
41
+ gating, membership checks, cookie flags and the whole authorization callback
42
+ could be disabled without a single test noticing. One test file had
43
+ re-implemented the authorization logic *inside the test* and asserted
44
+ against the copy: fifteen green tests, zero detection. After wrapper-level
45
+ tests were written against the survivors, 39/39 were killed.
46
+
38
47
  ## Install
39
48
 
40
49
  | How | Command |
@@ -43,9 +52,13 @@ on known-broken code. The largest gap was a compliance-critical path with
43
52
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
44
53
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
45
54
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
46
- | GitHub Action | `uses: raccioly/testguard@v0.1.1` — see [`action.yml`](./action.yml) |
55
+ | GitHub Action | `uses: raccioly/testguard@v0.1.3` — see [`action.yml`](./action.yml) |
47
56
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
48
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
+
49
62
  ## How it works
50
63
 
51
64
  ```bash
@@ -55,12 +68,14 @@ npx testguard-cli baseline # freeze today's unproven findings; from now on on
55
68
  npx testguard-cli brief # tell the agent where the suite is blind, before it writes
56
69
  ```
57
70
 
58
- 1. **Claims** live in `testguard.claims.json`: a statement, where it comes
59
- from, which tests supposedly defend it, and one or more *faults* — each a
60
- deterministic source change that would make the statement false. Every
71
+ 1. **Claims** live in `testguard.claims.json` (editors validate it against
72
+ `"$schema": "./node_modules/testguard-cli/spec/schemas/claims.schema.json"`):
73
+ a statement, where it comes from, which tests supposedly defend it, and
74
+ one or more *faults* — each a deterministic source change that would make
75
+ the statement false. Every
61
76
  claim and every fault records who produced it. `testguard claims`
62
77
  validates the file and reports drift against `@claim <ID>` annotations in
63
- source.
78
+ 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.
64
79
  2. **Probe** confirms the defenders are green N times unmodified, applies
65
80
  each fault in a scratch git worktree (your tree is never touched), runs
66
81
  the defenders N times, re-runs survivors against the whole suite with
@@ -77,14 +92,38 @@ npx testguard-cli brief # tell the agent where the suite is blind, before
77
92
  | `FLAKY-DEFENDER` | the defenders are not reliably green, or disagreed across runs |
78
93
 
79
94
  Never a single score. Findings are ranked by severity, claim provenance
80
- and blast radius, and written to `.testguard/evidence.json` validated
81
- against the spec before it is written.
95
+ and blast radius (relative imports, `tsconfig` path aliases and
96
+ `package.json#imports` are resolved; bare package names are not), and
97
+ written to `.testguard/evidence.json` — validated against the spec before
98
+ it is written.
99
+
100
+ Worktree mode probes a **commit**. If a defender or target file has
101
+ uncommitted changes, `probe` refuses and says so — otherwise your new
102
+ tests would be silently absent and the same survivors would come back
103
+ with no hint why. `--include-dirty` snapshots the working tree (tracked
104
+ edits and new files) into a throwaway commit and probes that; your tree,
105
+ HEAD and index are never touched. Every summary names the commit probed.
106
+
107
+ A claim with no `defendedBy` has its defenders **discovered**: the test
108
+ files that import the fault's target, by relative path or resolved alias.
109
+ `NOCOVER` then means exactly "no test file imports this source".
110
+
111
+ Practical loop: first pass `--no-escalate` (escalation re-runs the whole
112
+ suite N times per survivor); iterate on one claim with `--claim <ID>` and
113
+ either `--include-dirty` or `--in-place` (only fault target files must be
114
+ clean there; test files may be dirty); final pass with defaults. By default
115
+ the stream shows only unproven faults plus a killed count — `--verbose`
116
+ shows every fault. A custom
117
+ runner (`pnpm --filter`, a specific config) goes in
118
+ `--runner-cmd "<cmd> {files} … {out}"`; if the scratch worktree cannot
119
+ see your `node_modules`, pass `--node-modules <dir>`.
82
120
  3. **Baseline** freezes every non-passing fingerprint. Later probes suppress
83
121
  what was already known and exit non-zero only on what is new. Claims whose
84
122
  source and defenders are unchanged reuse their prior verdict, so a probe
85
123
  in CI costs only what changed.
86
124
  4. **Brief** turns evidence plus baseline into a ranked, capped
87
- `## TEST BLINDSPOT CONTEXT` block. Wire it into an agent's session start
125
+ `## TEST BLINDSPOT CONTEXT` block, printed and also written to
126
+ `.testguard/brief.json` (`--text` prints only). Wire it into an agent's session start
88
127
  — for Claude Code, in `.claude/settings.json`:
89
128
 
90
129
  ```json
@@ -38,7 +38,7 @@ inputs:
38
38
  version:
39
39
  description: 'testguard-cli version to run'
40
40
  required: false
41
- default: '0.1.1'
41
+ default: '0.1.3'
42
42
  node-version:
43
43
  description: 'Node.js version (20+)'
44
44
  required: false
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.mjs';
3
+
4
+ // `testguard … | head` closes stdout early; that is not an error worth a stack trace.
5
+ for (const stream of [process.stdout, process.stderr]) {
6
+ stream.on('error', (err) => {
7
+ if (err.code === 'EPIPE') process.exit(0);
8
+ throw err;
9
+ });
10
+ }
11
+
12
+ main(process.argv.slice(2)).then((code) => {
13
+ process.exitCode = code;
14
+ });
@@ -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
+ }