testguard-cli 0.3.0__tar.gz → 0.4.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 (162) hide show
  1. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/workflows/ci.yml +20 -0
  2. testguard_cli-0.4.0/.gitignore +22 -0
  3. testguard_cli-0.4.0/.testguard/baseline.json +11 -0
  4. testguard_cli-0.4.0/.testguard/status.json +32 -0
  5. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/AGENTS.md +10 -0
  6. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/CHANGELOG.md +36 -0
  7. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/PKG-INFO +12 -7
  8. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/README.md +11 -6
  9. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/action.yml +1 -1
  10. testguard_cli-0.4.0/fixtures/known-answer-jest/.gitignore +2 -0
  11. testguard_cli-0.4.0/fixtures/known-answer-jest/README.md +7 -0
  12. testguard_cli-0.4.0/fixtures/known-answer-jest/expected.json +54 -0
  13. testguard_cli-0.4.0/fixtures/known-answer-jest/jest.config.js +1 -0
  14. testguard_cli-0.4.0/fixtures/known-answer-jest/package.json +7 -0
  15. testguard_cli-0.4.0/fixtures/known-answer-jest/src/export.js +5 -0
  16. testguard_cli-0.4.0/fixtures/known-answer-jest/src/redact.js +62 -0
  17. testguard_cli-0.4.0/fixtures/known-answer-jest/test/flaky.test.js +13 -0
  18. testguard_cli-0.4.0/fixtures/known-answer-jest/test/redact.test.js +35 -0
  19. testguard_cli-0.4.0/fixtures/known-answer-jest/testguard.claims.json +297 -0
  20. testguard_cli-0.4.0/package-lock.json +5700 -0
  21. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/package.json +2 -1
  22. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/packaging/homebrew/testguard.rb +1 -1
  23. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/pyproject.toml +1 -1
  24. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/brief/brief.mjs +1 -1
  25. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/cli.mjs +6 -0
  26. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/claims.mjs +1 -1
  27. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/probe.mjs +1 -0
  28. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/init/init.mjs +6 -2
  29. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/discover.mjs +1 -1
  30. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/probe.mjs +17 -13
  31. testguard_cli-0.4.0/src/probe/runners/index.mjs +20 -0
  32. testguard_cli-0.4.0/src/probe/runners/jest.mjs +9 -0
  33. testguard_cli-0.3.0/src/probe/runner-vitest.mjs → testguard_cli-0.4.0/src/probe/runners/shared.mjs +29 -21
  34. testguard_cli-0.4.0/src/probe/runners/vitest.mjs +7 -0
  35. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/status/status.mjs +1 -1
  36. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/init.test.mjs +8 -0
  37. testguard_cli-0.4.0/test/probe.jest.test.mjs +52 -0
  38. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/runner-command.test.mjs +3 -3
  39. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/runner-vitest.test.mjs +9 -3
  40. testguard_cli-0.3.0/.gitignore +0 -18
  41. testguard_cli-0.3.0/package-lock.json +0 -1231
  42. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.codex/hooks.json +0 -0
  43. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.gitattributes +0 -0
  44. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  45. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  46. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  47. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/dependabot.yml +0 -0
  48. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/scripts/install-smoke.mjs +0 -0
  49. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/scripts/sync-release-version.mjs +0 -0
  50. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/workflows/auto-merge.yml +0 -0
  51. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/workflows/release.yml +0 -0
  52. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/workflows/scheduled-release.yml +0 -0
  53. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.github/workflows/supply-chain.yml +0 -0
  54. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.npmignore +0 -0
  55. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.npmrc +0 -0
  56. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/.pre-commit-hooks.yaml +0 -0
  57. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/CLAUDE.md +0 -0
  58. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  59. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/CONTRIBUTING.md +0 -0
  60. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/LICENSE +0 -0
  61. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/PRIVACY.md +0 -0
  62. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/SECURITY.md +0 -0
  63. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/SUPPORT.md +0 -0
  64. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/bench/README.md +0 -0
  65. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/cli/testguard.mjs +0 -0
  66. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/.gitignore +0 -0
  67. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/README.md +0 -0
  68. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/expected.json +0 -0
  69. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/package.json +0 -0
  70. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/src/export.mjs +0 -0
  71. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/src/redact.mjs +0 -0
  72. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/test/flaky.test.mjs +0 -0
  73. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/test/redact.test.mjs +0 -0
  74. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/testguard.claims.json +0 -0
  75. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/fixtures/known-answer/vitest.config.mjs +0 -0
  76. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/GATE-SEMANTICS.md +0 -0
  77. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/README.md +0 -0
  78. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/baseline.json +0 -0
  79. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/brief.json +0 -0
  80. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/calibration.json +0 -0
  81. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/claims.json +0 -0
  82. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/evidence-provisional.json +0 -0
  83. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/evidence.json +0 -0
  84. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/ignore.json +0 -0
  85. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/examples/status.json +0 -0
  86. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
  87. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/baseline.zero-count.json +0 -0
  88. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
  89. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
  90. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/calibration.p-outside-ci.json +0 -0
  91. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/calibration.positives-exceed-n.json +0 -0
  92. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.absolute-path.json +0 -0
  93. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
  94. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
  95. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.noop-fault.json +0 -0
  96. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
  97. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
  98. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
  99. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
  100. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
  101. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
  102. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.provisional-flag-on-confirmed-run.json +0 -0
  103. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
  104. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.single-run-without-provisional.json +0 -0
  105. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +0 -0
  106. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
  107. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
  108. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
  109. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
  110. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/ignore.short-reason.json +0 -0
  111. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/status.clean-with-new-findings.json +0 -0
  112. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/invalid/status.write-test-without-target.json +0 -0
  113. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/conformance/schemas.test.mjs +0 -0
  114. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/lib/fingerprint.mjs +0 -0
  115. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/lib/validate.mjs +0 -0
  116. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/baseline.schema.json +0 -0
  117. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/brief.schema.json +0 -0
  118. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/calibration.schema.json +0 -0
  119. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/claims.schema.json +0 -0
  120. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/common.schema.json +0 -0
  121. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/evidence.schema.json +0 -0
  122. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/ignore.schema.json +0 -0
  123. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/spec/schemas/status.schema.json +0 -0
  124. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/baseline/baseline.mjs +0 -0
  125. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/claims/annotations.mjs +0 -0
  126. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/claims/load.mjs +0 -0
  127. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/baseline.mjs +0 -0
  128. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/brief.mjs +0 -0
  129. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/init.mjs +0 -0
  130. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/scaffold.mjs +0 -0
  131. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/commands/status.mjs +0 -0
  132. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/evidence/writer.mjs +0 -0
  133. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/git.mjs +0 -0
  134. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/init/templates/SKILL.md +0 -0
  135. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/classify.mjs +0 -0
  136. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/inject.mjs +0 -0
  137. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/rank.mjs +0 -0
  138. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/probe/worktree.mjs +0 -0
  139. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/render.mjs +0 -0
  140. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/scaffold/producers.mjs +0 -0
  141. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/scaffold/scaffold.mjs +0 -0
  142. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/util/glob.mjs +0 -0
  143. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/src/util/hash.mjs +0 -0
  144. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/annotations.test.mjs +0 -0
  145. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/baseline.test.mjs +0 -0
  146. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/brief.test.mjs +0 -0
  147. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/classify.test.mjs +0 -0
  148. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/discover.test.mjs +0 -0
  149. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/glob.test.mjs +0 -0
  150. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/inject.test.mjs +0 -0
  151. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/probe-preconditions.test.mjs +0 -0
  152. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/probe.fixture.test.mjs +0 -0
  153. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/rank-aliases.test.mjs +0 -0
  154. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/scaffold.test.mjs +0 -0
  155. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/snapshot.test.mjs +0 -0
  156. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/status.test.mjs +0 -0
  157. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/worktree.test.mjs +0 -0
  158. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/test/writer.test.mjs +0 -0
  159. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/testguard.claims.json +0 -0
  160. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/testguard_cli/__init__.py +0 -0
  161. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/testguard_cli/wrapper.py +0 -0
  162. {testguard_cli-0.3.0 → testguard_cli-0.4.0}/vitest.config.mjs +0 -0
@@ -102,6 +102,26 @@ jobs:
102
102
  console.log('fixture verdicts match expected.json:', ev.records.length);
103
103
  "
104
104
 
105
+ - name: probe the jest fixture (through the bin, --runner jest)
106
+ shell: bash
107
+ run: |
108
+ S=$(mktemp -d)
109
+ cp -R fixtures/known-answer-jest/. "$S/"
110
+ rm -rf "$S/node_modules" "$S/.flake-counter" "$S/.testguard"
111
+ ln -s "$GITHUB_WORKSPACE/node_modules" "$S/node_modules"
112
+ git -C "$S" init -q && git -C "$S" -c user.email=ci@example.invalid -c user.name=ci add -A && git -C "$S" -c user.email=ci@example.invalid -c user.name=ci commit -qm fixture
113
+ status=0; node cli/testguard.mjs probe "$S" --runner jest --budget 60000 --quiet --no-escalate || status=$?
114
+ [ "$status" -eq 1 ] || { echo "::error::expected exit 1, got $status"; exit 1; }
115
+ node --input-type=module -e "
116
+ import { readFileSync } from 'node:fs';
117
+ const ev = JSON.parse(readFileSync('$S/.testguard/evidence.json','utf8'));
118
+ const exp = JSON.parse(readFileSync('fixtures/known-answer-jest/expected.json','utf8')).expected;
119
+ if (ev.run.runner.name !== 'jest') { console.error('runner was', ev.run.runner.name); process.exit(1); }
120
+ const bad = ev.records.filter(r => exp[r.claim.id+'/'+r.subject.id].verdict !== r.verdict);
121
+ if (bad.length) { console.error('MISMATCH', bad.map(r => r.claim.id+'/'+r.subject.id+':'+r.verdict)); process.exit(1); }
122
+ console.log('jest fixture verdicts match expected.json:', ev.records.length);
123
+ "
124
+
105
125
  # ── Self-verification: TestGuard is governed by the standard it defines.
106
126
  # testguard.claims.json states invariants of the tool itself; every
107
127
  # fault must be killed by the tool's own tests. Exit 0 = all defended. ──
@@ -0,0 +1,22 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ .DS_Store
5
+ *.log
6
+ # TestGuard: baseline.json and status.json ARE committed (the frozen contract and its snapshot); the rest is regenerated per run
7
+ .testguard/evidence.json
8
+ .testguard/evidence-provisional.json
9
+ .testguard/evidence-partial.json
10
+ .testguard/brief.json
11
+ .testguard/scaffold-*.json
12
+ # Local agent tooling — not part of the public project
13
+ .wolf/
14
+ .claude/
15
+ CLAUDE.local.md
16
+ graphify-out/
17
+ .vitest/
18
+ # Python build artefacts
19
+ dist/
20
+ build/
21
+ *.egg-info/
22
+ __pycache__/
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tool": {
4
+ "name": "testguard",
5
+ "version": "0.3.0"
6
+ },
7
+ "createdAt": "2026-09-17T15:24:13.597Z",
8
+ "head": "ac32ce504fc42f9b50389a5a8092a0e2ca1c5c9d",
9
+ "dirty": true,
10
+ "fingerprints": {}
11
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tool": {
4
+ "name": "testguard",
5
+ "version": "0.3.0"
6
+ },
7
+ "generatedAt": "2026-09-17T15:24:13.654Z",
8
+ "state": "clean",
9
+ "next": {
10
+ "action": "none",
11
+ "command": "testguard probe",
12
+ "why": "Every claim is defended. Re-probe after changing code, tests or claims; add a claim for every new invariant."
13
+ },
14
+ "provisional": false,
15
+ "counts": {
16
+ "claims": 12,
17
+ "faults": 12,
18
+ "byVerdict": {
19
+ "killed": 12
20
+ },
21
+ "new": 0,
22
+ "baselined": 0
23
+ },
24
+ "stale": [],
25
+ "changedFaults": [],
26
+ "findings": [],
27
+ "paths": {
28
+ "claims": "testguard.claims.json",
29
+ "evidence": ".testguard/evidence.json",
30
+ "baseline": ".testguard/baseline.json"
31
+ }
32
+ }
@@ -31,6 +31,7 @@ node cli/testguard.mjs scaffold src/probe/classify.mjs --json # what the produ
31
31
  - `fixtures/known-answer/expected.json` — the oracle. Never edit it to match output.
32
32
  - `testguard.claims.json` — claims about this codebase, probed in CI.
33
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.
34
+ - `src/probe/runners/` — one module per runner (`name`, `testGlobs`, `check`, `tests`, `run`) over `shared.mjs` (jest-compatible report parsing, budgeted process runner). A new runner needs its own fixture copy with the same `expected.json`.
34
35
  - `src/status/status.mjs` — the state machine every rendering derives from. A new state or action is a spec change (`status.schema.json`) and a skill-template change (`src/init/templates/SKILL.md`) in the same PR.
35
36
 
36
37
  ## Rules
@@ -56,3 +57,12 @@ node cli/testguard.mjs scaffold src/probe/classify.mjs --json # what the produ
56
57
  Dependabot minor/patch PRs and automated release PRs auto-merge when the exact
57
58
  CI run is green on all three Node legs. Majors and anything else wait for a
58
59
  human. Release publishing is OIDC-only; there are no tokens in this repo.
60
+
61
+ <!-- testguard:begin -->
62
+ ## TestGuard
63
+
64
+ This project's tests are verified by [TestGuard](https://github.com/raccioly/testguard).
65
+ Before writing or changing tests, run `testguard status --json` and follow `next`.
66
+ The full operating loop and the verdict table are in `.claude/skills/testguard/SKILL.md`.
67
+ Never make a fault die by editing `testguard.claims.json`; write the test. Claim edits are recorded in the evidence.
68
+ <!-- testguard:end -->
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-09-17
11
+
12
+ ### Added
13
+
14
+ - **jest runner.** `--runner vitest|jest|auto` (default `auto`: the first
15
+ that resolves in the project). jest runs with `--ci --json --runTestsByPath`
16
+ so file arguments are exact paths, not regexes; its `Exceeded timeout`
17
+ wording is recognised as a timeout, never a kill; `__tests__/` files are
18
+ collected for escalation. `run.runner.name` records which runner produced
19
+ the evidence.
20
+ - A CommonJS edition of the known-answer fixture (`fixtures/known-answer-jest`)
21
+ with the same claims and the same expected verdicts; the acceptance suite
22
+ and CI prove jest against it exactly as vitest is proven against the ESM
23
+ one.
24
+
25
+ ### Changed
26
+
27
+ - Runners are now modules behind one interface (`src/probe/runners/`); the
28
+ vitest path is unchanged and still reproduces its oracle.
29
+
30
+ ## [0.3.1] - 2026-09-17
31
+
32
+ Dogfooding `init` and `status` on this repository.
33
+
34
+ ### Fixed
35
+
36
+ - `init` no longer adds per-file `.gitignore` lines to a project that already
37
+ ignores `.testguard/` wholesale; it notes instead that `baseline.json` is
38
+ meant to be committed.
39
+ - The brief no longer says "everything is new" when nothing is unproven.
40
+
41
+ ### Changed
42
+
43
+ - This repository now carries its own `AGENTS.md` TestGuard section, written
44
+ by `testguard init`.
45
+
10
46
  ## [0.3.0] - 2026-09-17
11
47
 
12
48
  The agent operating layer. TestGuard is meant to be run by AI agents; this
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: testguard-cli
3
- Version: 0.3.0
3
+ Version: 0.4.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,7 +76,7 @@ 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.3.0` — see [`action.yml`](./action.yml) |
79
+ | GitHub Action | `uses: raccioly/testguard@v0.4.0` — see [`action.yml`](./action.yml) |
80
80
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
81
81
 
82
82
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
@@ -135,6 +135,11 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
135
135
  files that import the fault's target, by relative path or resolved alias.
136
136
  `NOCOVER` then means exactly "no test file imports this source".
137
137
 
138
+ Runners: **vitest** and **jest** (`--runner auto` picks the first that
139
+ resolves; both read the same jest-compatible JSON report). Anything else
140
+ goes through `--runner-cmd`. Each runner is proven against its own copy
141
+ of the known-answer fixture.
142
+
138
143
  Practical loop: first pass `--no-escalate` (escalation re-runs the whole
139
144
  suite N times per survivor); iterate on one claim with `--claim <ID>` and
140
145
  either `--include-dirty` or `--in-place` (only fault target files must be
@@ -239,14 +244,14 @@ through every verdict.
239
244
 
240
245
  ## Status
241
246
 
242
- **v0.3.** Seven commands, vitest runner, hand-authored faults plus a
243
- mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
247
+ **v0.4.** Seven commands, vitest and jest runners, hand-authored faults plus
248
+ a mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
244
249
  spine — six JSON Schemas shared with the other Guard tools — is under
245
250
  [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
246
251
 
247
- Not yet: test generation (the two-gate acceptance loop), other runners,
248
- AST-aware producers, and calibration of fault classes against real escaped
249
- bugs. Each is designed for; none is claimed.
252
+ Not yet: test generation (the two-gate acceptance loop), runners beyond
253
+ vitest and jest, AST-aware producers, and calibration of fault classes
254
+ against real escaped bugs. Each is designed for; none is claimed.
250
255
 
251
256
  ## Licence
252
257
 
@@ -52,7 +52,7 @@ 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.3.0` — see [`action.yml`](./action.yml) |
55
+ | GitHub Action | `uses: raccioly/testguard@v0.4.0` — see [`action.yml`](./action.yml) |
56
56
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
57
57
 
58
58
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
@@ -111,6 +111,11 @@ npx testguard-cli scaffold src/x.ts # propose faults for a file, as a draft to
111
111
  files that import the fault's target, by relative path or resolved alias.
112
112
  `NOCOVER` then means exactly "no test file imports this source".
113
113
 
114
+ Runners: **vitest** and **jest** (`--runner auto` picks the first that
115
+ resolves; both read the same jest-compatible JSON report). Anything else
116
+ goes through `--runner-cmd`. Each runner is proven against its own copy
117
+ of the known-answer fixture.
118
+
114
119
  Practical loop: first pass `--no-escalate` (escalation re-runs the whole
115
120
  suite N times per survivor); iterate on one claim with `--claim <ID>` and
116
121
  either `--include-dirty` or `--in-place` (only fault target files must be
@@ -215,14 +220,14 @@ through every verdict.
215
220
 
216
221
  ## Status
217
222
 
218
- **v0.3.** Seven commands, vitest runner, hand-authored faults plus a
219
- mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
223
+ **v0.4.** Seven commands, vitest and jest runners, hand-authored faults plus
224
+ a mechanical scaffold, and an agent operating layer (`status`, `init`). The contract
220
225
  spine — six JSON Schemas shared with the other Guard tools — is under
221
226
  [`spec/`](spec/). One exact-pinned runtime dependency (`ajv`, for schema validation); Node ≥ 20.
222
227
 
223
- Not yet: test generation (the two-gate acceptance loop), other runners,
224
- AST-aware producers, and calibration of fault classes against real escaped
225
- bugs. Each is designed for; none is claimed.
228
+ Not yet: test generation (the two-gate acceptance loop), runners beyond
229
+ vitest and jest, AST-aware producers, and calibration of fault classes
230
+ against real escaped bugs. Each is designed for; none is claimed.
226
231
 
227
232
  ## Licence
228
233
 
@@ -38,7 +38,7 @@ inputs:
38
38
  version:
39
39
  description: 'testguard-cli version to run'
40
40
  required: false
41
- default: '0.3.0'
41
+ default: '0.4.0'
42
42
  node-version:
43
43
  description: 'Node.js version (20+)'
44
44
  required: false
@@ -0,0 +1,2 @@
1
+ .flake-counter
2
+ node_modules/
@@ -0,0 +1,7 @@
1
+ # Known-answer fixture — jest edition
2
+
3
+ The same nine claims and eleven faults as [`../known-answer`](../known-answer), in
4
+ CommonJS, run by **jest** (`jest.config.js`: `testTimeout: 1000`). Every verdict in
5
+ `expected.json` is the same. Its test suite is flaky by design (`test/flaky.test.js`).
6
+
7
+ It proves the jest runner adapter reproduces the oracle; the ESM fixture proves vitest.
@@ -0,0 +1,54 @@
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 jest testTimeout; 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.js 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.js 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.js is discovered because it imports src/redact.js, and its masked-output test fails."
51
+ }
52
+ },
53
+ "runner": "jest"
54
+ }
@@ -0,0 +1 @@
1
+ module.exports = { testEnvironment: 'node', testTimeout: 1000, testMatch: ['**/test/**/*.test.js'] };
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "testguard-known-answer-fixture-jest",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "description": "The known-answer fixture in CommonJS for jest. Same claims, same expected verdicts. FLAKY BY DESIGN (test/flaky.test.js).",
6
+ "scripts": { "test": "jest" }
7
+ }
@@ -0,0 +1,5 @@
1
+ // Export of audit rows. No test file imports this module — on purpose (the `nocover` case).
2
+ function exportRows(rows) {
3
+ return rows.map(({ content, ...rest }) => rest);
4
+ }
5
+ module.exports = { exportRows };
@@ -0,0 +1,62 @@
1
+ // Redaction engine for the known-answer fixture (CommonJS, for jest).
2
+ // Every claim in ../testguard.claims.json is about this file.
3
+
4
+ /** Compile a rule's pattern; an invalid pattern yields null and is skipped. */
5
+ function safeRegex(pattern) {
6
+ try {
7
+ return new RegExp(pattern, 'g');
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ /** Find a rule by id. */
14
+ function findRule(rules, id) {
15
+ for (const rule of rules) {
16
+ if (rule.id === id) return rule;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ /** Compile all rules. An invalid pattern is skipped; it never aborts the scan. */
22
+ function compileRules(rules) {
23
+ const compiled = [];
24
+ for (const rule of rules) {
25
+ const re = safeRegex(rule.pattern);
26
+ if (re === null) continue;
27
+ compiled.push({ ...rule, re });
28
+ }
29
+ return compiled;
30
+ }
31
+
32
+ /** Replace every match of every rule with asterisks of equal length. */
33
+ function mask(input, rules) {
34
+ let out = input;
35
+ for (const rule of compileRules(rules)) {
36
+ out = out.replace(rule.re, (m) => '*'.repeat(m.length));
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /**
42
+ * Redact a message and write an audit row when anything was masked.
43
+ * @claim REDACT-003 Fails closed: a missing scope is an error, never an unscoped scan.
44
+ * The audit row must never carry the original input.
45
+ */
46
+ async function redact(input, rules, ctx, store) {
47
+ if (!ctx || !ctx.scope) {
48
+ throw new Error('redact: scope is required');
49
+ }
50
+ const redacted = mask(input, rules);
51
+ if (redacted !== input) {
52
+ await store.writeAudit({
53
+ action: 'MASK',
54
+ scope: ctx.scope,
55
+ ruleCount: rules.length,
56
+ content: redacted,
57
+ });
58
+ }
59
+ return redacted;
60
+ }
61
+
62
+ module.exports = { findRule, compileRules, mask, redact };
@@ -0,0 +1,13 @@
1
+ const { readFileSync, writeFileSync } = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ // Deterministically flaky: fails on odd-numbered runs (the `flaky-defender` case).
5
+ const counterFile = path.join(__dirname, '..', '.flake-counter');
6
+
7
+ it('is flaky by design (fails every other run)', () => {
8
+ let n = 0;
9
+ try { n = Number(readFileSync(counterFile, 'utf8')) || 0; } catch {}
10
+ n += 1;
11
+ writeFileSync(counterFile, String(n));
12
+ expect(n % 2).toBe(0);
13
+ });
@@ -0,0 +1,35 @@
1
+ const { mask, redact, compileRules } = require('../src/redact');
2
+
3
+ const SECRET = 'FIXTURE_SECRET_7731';
4
+ const RULES = [{ id: 'r1', pattern: 'FIXTURE_SECRET_\\d+' }];
5
+ const CTX = { scope: 'g1' };
6
+
7
+ describe('redaction', () => {
8
+ it('masks the secret out of the returned text', () => {
9
+ const out = mask(`hello ${SECRET} bye`, RULES);
10
+ expect(out).not.toContain(SECRET);
11
+ expect(out).toBe('hello ******************* bye');
12
+ });
13
+
14
+ it('skips a rule whose pattern is invalid without throwing', () => {
15
+ const rules = [{ id: 'bad', pattern: '(' }, ...RULES];
16
+ expect(() => compileRules(rules)).not.toThrow();
17
+ expect(compileRules(rules)).toHaveLength(1);
18
+ expect(mask(SECRET, rules)).not.toContain(SECRET);
19
+ });
20
+
21
+ it('writes an audit row when something was masked', async () => {
22
+ const store = { writeAudit: jest.fn().mockResolvedValue(undefined) };
23
+ await redact(`hello ${SECRET}`, RULES, CTX, store);
24
+ // THE BLIND SPOT: objectContaining ignores unlisted keys, and `content` is not listed.
25
+ expect(store.writeAudit).toHaveBeenCalledTimes(1);
26
+ expect(store.writeAudit).toHaveBeenCalledWith(expect.objectContaining({ action: 'MASK', scope: 'g1', ruleCount: 1 }));
27
+ });
28
+
29
+ it('does not write an audit row when nothing matched', async () => {
30
+ const store = { writeAudit: jest.fn() };
31
+ const out = await redact('nothing here', RULES, CTX, store);
32
+ expect(out).toBe('nothing here');
33
+ expect(store.writeAudit).not.toHaveBeenCalled();
34
+ });
35
+ });