testguard-cli 0.7.0__tar.gz → 0.8.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 (297) hide show
  1. testguard_cli-0.8.0/.github/scripts/check-cost-budget.mjs +43 -0
  2. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/workflows/ci.yml +8 -0
  3. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/CHANGELOG.md +116 -0
  4. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/PKG-INFO +15 -12
  5. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/README.md +14 -11
  6. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/action.yml +1 -1
  7. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/DATA-MODEL.md +3 -1
  8. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/REQUIREMENTS.md +4 -4
  9. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/TEST-SPEC.md +2 -2
  10. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/package-lock.json +2 -2
  11. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/package.json +1 -1
  12. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/packaging/gitlab/testguard.gitlab-ci.yml +2 -2
  13. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/packaging/homebrew/testguard.rb +1 -1
  14. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/pyproject.toml +1 -1
  15. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/GATE-SEMANTICS.md +98 -3
  16. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/README.md +5 -2
  17. testguard_cli-0.8.0/spec/conformance/examples/calibration-websec.json +98 -0
  18. testguard_cli-0.8.0/spec/conformance/examples/calibration.json +51 -0
  19. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/claims-methods.json +14 -0
  20. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence-scan-method.json +5 -1
  21. testguard_cli-0.8.0/spec/conformance/invalid/calibration.backoff-cell-not-wilson.json +98 -0
  22. testguard_cli-0.8.0/spec/conformance/invalid/calibration.breakdown-does-not-sum-to-n.json +56 -0
  23. testguard_cli-0.8.0/spec/conformance/invalid/calibration.ci-not-wilson.json +44 -0
  24. testguard_cli-0.8.0/spec/conformance/invalid/calibration.compound-key-arity-mismatch.json +107 -0
  25. testguard_cli-0.8.0/spec/conformance/invalid/calibration.fallback-without-basis.json +97 -0
  26. testguard_cli-0.7.0/spec/conformance/examples/calibration.json → testguard_cli-0.8.0/spec/conformance/invalid/calibration.p-not-positives-over-n.json +2 -2
  27. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/calibration.p-outside-ci.json +1 -1
  28. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/calibration.positives-exceed-n.json +1 -1
  29. testguard_cli-0.8.0/spec/conformance/invalid/claims.method-detail-on-a-specified-method.json +37 -0
  30. testguard_cli-0.8.0/spec/conformance/invalid/evidence.method-detail-on-an-injecting-run.json +192 -0
  31. testguard_cli-0.8.0/spec/conformance/schemas.test.mjs +132 -0
  32. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/lib/validate.mjs +41 -6
  33. testguard_cli-0.8.0/spec/lib/wilson.mjs +112 -0
  34. testguard_cli-0.8.0/spec/schemas/calibration.schema.json +124 -0
  35. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/claims.schema.json +8 -1
  36. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/evidence.schema.json +6 -0
  37. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/replay.mjs +1 -1
  38. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/cost.mjs +60 -0
  39. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/replay/replay.mjs +47 -20
  40. testguard_cli-0.8.0/test/calibration.test.mjs +73 -0
  41. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/cost.test.mjs +62 -1
  42. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/replay.test.mjs +5 -39
  43. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/testguard.claims.json +344 -9
  44. testguard_cli-0.8.0/testguard.cost-budget.json +37 -0
  45. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/testguard.ignore.json +7 -0
  46. testguard_cli-0.7.0/spec/conformance/schemas.test.mjs +0 -63
  47. testguard_cli-0.7.0/spec/schemas/calibration.schema.json +0 -54
  48. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.agents/skills/testguard/SKILL.md +0 -0
  49. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.codex/hooks.json +0 -0
  50. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.docguard.json +0 -0
  51. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.docguardignore +0 -0
  52. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.gitattributes +0 -0
  53. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  54. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  55. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  56. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/dependabot.yml +0 -0
  57. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/scripts/build-one-pager.mjs +0 -0
  58. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/scripts/check-python-fixture.mjs +0 -0
  59. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/scripts/install-smoke.mjs +0 -0
  60. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/scripts/sync-release-version.mjs +0 -0
  61. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/workflows/auto-merge.yml +0 -0
  62. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/workflows/release.yml +0 -0
  63. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/workflows/scheduled-release.yml +0 -0
  64. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.github/workflows/supply-chain.yml +0 -0
  65. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.gitignore +0 -0
  66. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.npmignore +0 -0
  67. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.npmrc +0 -0
  68. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.pre-commit-config.yaml +0 -0
  69. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/.pre-commit-hooks.yaml +0 -0
  70. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/AGENTS.md +0 -0
  71. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/CLAUDE.md +0 -0
  72. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/CODE_OF_CONDUCT.md +0 -0
  73. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/CONTRIBUTING.md +0 -0
  74. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/DRIFT-LOG.md +0 -0
  75. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/LICENSE +0 -0
  76. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/PRIVACY.md +0 -0
  77. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/SECURITY.md +0 -0
  78. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/SUPPORT.md +0 -0
  79. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/bench/README.md +0 -0
  80. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/cli/testguard.mjs +0 -0
  81. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs/testguard-explained.html +0 -0
  82. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs/testguard-explained.pdf +0 -0
  83. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/ARCHITECTURE.md +0 -0
  84. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/ENVIRONMENT.md +0 -0
  85. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/docs-canonical/SECURITY.md +0 -0
  86. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/.gitignore +0 -0
  87. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/README.md +0 -0
  88. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/expected.json +0 -0
  89. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/package.json +0 -0
  90. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/src/export.mjs +0 -0
  91. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/src/redact.mjs +0 -0
  92. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/test/export-mocked.test.mjs +0 -0
  93. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/test/flaky.test.mjs +0 -0
  94. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/test/redact.test.mjs +0 -0
  95. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/testguard.claims.json +0 -0
  96. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer/vitest.config.mjs +0 -0
  97. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/.gitignore +0 -0
  98. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/README.md +0 -0
  99. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/expected.json +0 -0
  100. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/jest.config.js +0 -0
  101. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/package.json +0 -0
  102. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/src/export.js +0 -0
  103. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/src/redact.js +0 -0
  104. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/test/export-mocked.test.js +0 -0
  105. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/test/flaky.test.js +0 -0
  106. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/test/redact.test.js +0 -0
  107. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-jest/testguard.claims.json +0 -0
  108. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/.gitignore +0 -0
  109. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/README.md +0 -0
  110. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/e2e/flaky.spec.mjs +0 -0
  111. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/e2e/toggle.spec.mjs +0 -0
  112. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/expected.json +0 -0
  113. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/package-lock.json +0 -0
  114. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/package.json +0 -0
  115. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/playwright.config.mjs +0 -0
  116. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/src/toggle.mjs +0 -0
  117. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/test/toggle.test.mjs +0 -0
  118. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/test-results/.last-run.json +0 -0
  119. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/testguard.claims.json +0 -0
  120. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-playwright/vitest.config.mjs +0 -0
  121. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/.gitignore +0 -0
  122. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/README.md +0 -0
  123. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/demo/__init__.py +0 -0
  124. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/demo/export.py +0 -0
  125. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/demo/orphan.py +0 -0
  126. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/demo/pipeline.py +0 -0
  127. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/demo/redact.py +0 -0
  128. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/expected.json +0 -0
  129. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/testguard.claims.json +0 -0
  130. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/tests/test_export_mocked.py +0 -0
  131. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/tests/test_flaky.py +0 -0
  132. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/tests/test_patched.py +0 -0
  133. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/fixtures/known-answer-python/tests/test_redact.py +0 -0
  134. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/baseline.json +0 -0
  135. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/brief.json +0 -0
  136. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/claims.json +0 -0
  137. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence-disturbed-run.json +0 -0
  138. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence-negative-control.json +0 -0
  139. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence-provisional.json +0 -0
  140. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence-python.json +0 -0
  141. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/evidence.json +0 -0
  142. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/gate.json +0 -0
  143. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/ignore.json +0 -0
  144. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/replay.json +0 -0
  145. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/examples/status.json +0 -0
  146. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
  147. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/baseline.zero-count.json +0 -0
  148. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
  149. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
  150. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.absolute-path.json +0 -0
  151. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
  152. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.fault-injection-without-a-substitution.json +0 -0
  153. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
  154. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.noop-fault.json +0 -0
  155. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
  156. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
  157. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.annotated-signal-without-reason.json +0 -0
  158. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.attribute-signal-not-a-defender.json +0 -0
  159. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.attribute-signal-without-attributes.json +0 -0
  160. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.byrunner-file-not-resolved.json +0 -0
  161. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.contention-without-runners.json +0 -0
  162. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.discovered-defender-mocks-subject.json +0 -0
  163. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.fault-injection-without-its-evidence.json +0 -0
  164. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
  165. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.ignored-dirty-on-snapshot.json +0 -0
  166. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
  167. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
  168. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
  169. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.negative-control-not-reached-but-survived.json +0 -0
  170. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.negative-control-reached-on-a-kill.json +0 -0
  171. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.probe-error-without-message.json +0 -0
  172. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.provisional-flag-on-confirmed-run.json +0 -0
  173. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
  174. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.restore-skipped-in-place.json +0 -0
  175. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.single-run-without-provisional.json +0 -0
  176. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.target-not-imported-but-killed.json +0 -0
  177. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +0 -0
  178. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
  179. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
  180. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
  181. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/gate.counts-disagree.json +0 -0
  182. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/gate.exit-zero-with-uncovered.json +0 -0
  183. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/gate.ignore-cover-without-pattern.json +0 -0
  184. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
  185. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/ignore.short-reason.json +0 -0
  186. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/replay.blind-with-a-failing-run.json +0 -0
  187. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/replay.duplicate-patch-id.json +0 -0
  188. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/status.claim-without-file.json +0 -0
  189. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/status.clean-with-new-findings.json +0 -0
  190. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/status.unclaimed-hidden-behind-unproven.json +0 -0
  191. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/conformance/invalid/status.write-test-without-target.json +0 -0
  192. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/lib/fingerprint.mjs +0 -0
  193. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/baseline.schema.json +0 -0
  194. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/brief.schema.json +0 -0
  195. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/common.schema.json +0 -0
  196. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/gate.schema.json +0 -0
  197. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/ignore.schema.json +0 -0
  198. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/replay.schema.json +0 -0
  199. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/spec/schemas/status.schema.json +0 -0
  200. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/admit/admit.mjs +0 -0
  201. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/baseline/baseline.mjs +0 -0
  202. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/brief/brief.mjs +0 -0
  203. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/claims/annotations.mjs +0 -0
  204. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/claims/load.mjs +0 -0
  205. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/claims/removed.mjs +0 -0
  206. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/cli.mjs +0 -0
  207. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/admit.mjs +0 -0
  208. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/baseline.mjs +0 -0
  209. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/brief.mjs +0 -0
  210. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/claims.mjs +0 -0
  211. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/gate.mjs +0 -0
  212. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/init.mjs +0 -0
  213. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/mcp.mjs +0 -0
  214. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/probe.mjs +0 -0
  215. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/scaffold.mjs +0 -0
  216. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/commands/status.mjs +0 -0
  217. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/evidence/writer.mjs +0 -0
  218. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/gate/changed.mjs +0 -0
  219. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/git.mjs +0 -0
  220. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/init/init.mjs +0 -0
  221. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/init/templates/SKILL.md +0 -0
  222. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/mcp/server.mjs +0 -0
  223. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/mcp/tools.mjs +0 -0
  224. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/attribution.mjs +0 -0
  225. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/classify.mjs +0 -0
  226. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/contention.mjs +0 -0
  227. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/discover.mjs +0 -0
  228. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/independence.mjs +0 -0
  229. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/inject.mjs +0 -0
  230. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/mocks.mjs +0 -0
  231. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/probe.mjs +0 -0
  232. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/progress.mjs +0 -0
  233. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/pyimports.mjs +0 -0
  234. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/rank.mjs +0 -0
  235. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/index.mjs +0 -0
  236. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/jest.mjs +0 -0
  237. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/playwright.mjs +0 -0
  238. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/python/_testguard_pytest_plugin.py +0 -0
  239. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/python/_testguard_report.py +0 -0
  240. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/python/_testguard_unittest_main.py +0 -0
  241. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/python.mjs +0 -0
  242. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/shared.mjs +0 -0
  243. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/runners/vitest.mjs +0 -0
  244. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/probe/worktree.mjs +0 -0
  245. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/render.mjs +0 -0
  246. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/replay/label.mjs +0 -0
  247. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/scaffold/producers.mjs +0 -0
  248. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/scaffold/producers.python.mjs +0 -0
  249. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/scaffold/scaffold.mjs +0 -0
  250. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/status/status.mjs +0 -0
  251. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/util/glob.mjs +0 -0
  252. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/src/util/hash.mjs +0 -0
  253. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/admit.test.mjs +0 -0
  254. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/annotations.test.mjs +0 -0
  255. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/attribution.test.mjs +0 -0
  256. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/baseline.test.mjs +0 -0
  257. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/brief.test.mjs +0 -0
  258. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/claims-since.test.mjs +0 -0
  259. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/classify.test.mjs +0 -0
  260. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/contention.test.mjs +0 -0
  261. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/discover.test.mjs +0 -0
  262. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/gate.test.mjs +0 -0
  263. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/gitlab-template.test.mjs +0 -0
  264. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/glob.test.mjs +0 -0
  265. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/independence.test.mjs +0 -0
  266. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/init.test.mjs +0 -0
  267. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/inject.test.mjs +0 -0
  268. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/mcp.test.mjs +0 -0
  269. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/mocks.test.mjs +0 -0
  270. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/negative-control.test.mjs +0 -0
  271. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe-error.test.mjs +0 -0
  272. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe-preconditions.test.mjs +0 -0
  273. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe.fixture.test.mjs +0 -0
  274. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe.jest.test.mjs +0 -0
  275. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe.playwright.test.mjs +0 -0
  276. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/probe.python.test.mjs +0 -0
  277. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/progress.test.mjs +0 -0
  278. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/pyimports.test.mjs +0 -0
  279. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/rank-aliases.test.mjs +0 -0
  280. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/release-sync.test.mjs +0 -0
  281. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/runner-argv.test.mjs +0 -0
  282. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/runner-command.test.mjs +0 -0
  283. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/runner-playwright.test.mjs +0 -0
  284. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/runner-python.test.mjs +0 -0
  285. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/runner-vitest.test.mjs +0 -0
  286. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/samples/playwright-report.json +0 -0
  287. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/samples/python-report-collect-error.json +0 -0
  288. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/samples/python-report.json +0 -0
  289. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/scaffold.python.test.mjs +0 -0
  290. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/scaffold.test.mjs +0 -0
  291. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/snapshot.test.mjs +0 -0
  292. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/status.test.mjs +0 -0
  293. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/worktree.test.mjs +0 -0
  294. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/test/writer.test.mjs +0 -0
  295. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/testguard_cli/__init__.py +0 -0
  296. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/testguard_cli/wrapper.py +0 -0
  297. {testguard_cli-0.7.0 → testguard_cli-0.8.0}/vitest.config.mjs +0 -0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail CI when the self-probe gate outgrows its budget.
4
+ *
5
+ * The decision lives in `src/probe/cost.mjs` as a pure function, unit-tested
6
+ * and covered by a self-claim; this script is only wiring — read the evidence,
7
+ * read the budget, print, choose an exit code.
8
+ *
9
+ * usage: check-cost-budget.mjs <evidence.json> [previous-evidence.json]
10
+ */
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { costReport, checkCostBudget, renderCostBudget } from '../../src/probe/cost.mjs';
15
+
16
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+ const [evidencePath, previousPath] = process.argv.slice(2);
18
+ if (!evidencePath) {
19
+ console.error('usage: check-cost-budget.mjs <evidence.json> [previous-evidence.json]');
20
+ process.exit(2);
21
+ }
22
+
23
+ const read = (p) => JSON.parse(readFileSync(resolve(p), 'utf8'));
24
+ const budget = read(join(ROOT, 'testguard.cost-budget.json'));
25
+
26
+ // A previous run is informational only. Its absence must never fail the check:
27
+ // the first run on a branch, or a cold cache, has nothing to compare against.
28
+ let previousMs;
29
+ if (previousPath) {
30
+ try {
31
+ previousMs = costReport(read(previousPath).records).totalMs;
32
+ } catch {
33
+ previousMs = undefined;
34
+ }
35
+ }
36
+
37
+ const decision = checkCostBudget(costReport(read(evidencePath).records), { budgetSeconds: budget.seconds, previousMs });
38
+ const text = renderCostBudget(decision);
39
+ console.log(text);
40
+ if (!decision.ok) {
41
+ console.log(`::error::${text.split('\n')[0]}`);
42
+ process.exit(1);
43
+ }
@@ -210,6 +210,14 @@ jobs:
210
210
  if: matrix.node-version == 22
211
211
  run: node cli/testguard.mjs probe . --budget 180000 --no-escalate --cost --out .testguard/ci-self-evidence.json
212
212
 
213
+ # The gate went 9.6 -> 23.6 min across one merged PR and nothing said a
214
+ # word, because --cost only ever printed into a log. `always()` so the
215
+ # cost is reported even when the probe itself failed: a run that got
216
+ # slower AND found a survivor should say both, not just the second.
217
+ - name: The gate must stay inside its cost budget
218
+ if: always() && matrix.node-version == 22
219
+ run: node .github/scripts/check-cost-budget.mjs .testguard/ci-self-evidence.json
220
+
213
221
  # A stable, branch-named artifact so a developer can fetch the evidence
214
222
  # CI produced without knowing a run id (README: read CI's evidence
215
223
  # locally). One Node leg is enough; the verdicts are identical.
@@ -7,6 +7,122 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-09-18
11
+
12
+ A calibration you can quote.
13
+
14
+ A calibration document now says what its number means and where its labels
15
+ came from, and it reproduces: `p` and every interval are recomputed by the
16
+ validator from `n`, `positives`, `method` and `confidence`, at the document's
17
+ own precision — the spec's own example had carried a truncated bound since
18
+ the day it was written, and nothing could notice. `nocover` counts as the
19
+ miss it is, so a project with no tests at all for a subsystem no longer
20
+ outscores one with weak tests. The format also carries what
21
+ websec-validator's shipped table already said — corpus, caveat, a producer
22
+ floor, backoff tiers, a labelled prior — so a second tool's honesty survives
23
+ translation; that table is now a conformance example, and "adoptable" means
24
+ the file validates rather than a sentence saying so. Underneath: the
25
+ self-probe gate is held to a cost budget (#89), and a reserved method has
26
+ somewhere to put its data (#91).
27
+
28
+ ### Added
29
+ - **A calibration can now say what it measures and where it came from** (#82).
30
+ websec-validator's shipped `calibration.json` already carried `corpus`,
31
+ `min_n`, `caveat`, `evidence_status` and `limitation` — the fields that say
32
+ "indicative, do not quote this number" — and claimspec had nowhere to put
33
+ them, so adopting it would have stripped that honesty in translation. It
34
+ also had nowhere to say what `p` *was*: testguard's is a miss rate where
35
+ high is bad, websec's is P(real) where high is good, and both validated
36
+ identically. The schema gains `measures` (a registry: `escape-missed`,
37
+ `finding-real`), provenance in `source` (`corpus`, `caveat`, `limitation`,
38
+ `evidenceStatus`, a `tool-oracle` kind, and an unconstrained `detail` on the
39
+ `methodDetail` pattern), a producer floor `minN` that consumers may raise
40
+ and never lower, ordered `backoff` tiers so a class→label→prior cascade is
41
+ expressible, a `fallback` whose shape says it is a guess, a per-cell
42
+ `breakdown` that sums to `n`, `p: null` at `n = 0`, and `|`-compound bucket
43
+ keys whose arity the validator checks. Every field is optional and absence
44
+ means *unattributed* — readable, not quotable — so no existing document
45
+ breaks; testguard's emitter always writes them and a self-claim guards it.
46
+ The proof of adoption is a file, not a sentence: websec-validator's table,
47
+ translated field for field, is now `spec/conformance/examples/calibration-websec.json`
48
+ and validates with every number reproducing. `GATE-SEMANTICS.md` records
49
+ the merge rule (sum counts, recompute, never average; only when `measures`
50
+ and `bucketBy` agree) that websec's shipped-plus-local overlay already
51
+ depends on.
52
+ - **The gate is now gated on its own cost** (#89). `--cost` has printed the
53
+ self-probe's wall clock into every CI log since #67, and the gate still went
54
+ from 9.6 to 23.6 minutes across one merged change without anything saying a
55
+ word. A measurement nothing gates on is not a check — which is the premise
56
+ this tool rests on, applied to itself.
57
+ `testguard.cost-budget.json` carries the ceiling, its measurement and why it
58
+ is set where it is. CI fails when the probe exceeds it and opens with the
59
+ most expensive claims, so the first thing a failure shows is where to look.
60
+ Raising it is a committed diff with a reason: growth is allowed, going
61
+ unnoticed is not.
62
+ The budget is on the **total**, never a per-fault average. The regression
63
+ that prompted this took the corpus from 59 faults to 91 while per-fault cost
64
+ barely moved — an average would have reported everything fine while the gate
65
+ tripled, and a test says so. A budget that is missing or not a positive
66
+ number is refused rather than treated as unlimited, so a typo cannot quietly
67
+ disable the gate.
68
+
69
+
70
+ ### Fixed
71
+ - **The cost budget is set against what CI measures.** 950 s was 18% over
72
+ the local 804 s figure and only 4.6% over the 906 s CI already measured on
73
+ `main`, so the first pull request to add any claim tripped it — #94 passed
74
+ its own leg only because the cache reused verdicts, and `main` went red at
75
+ 981 s from scratch the moment it merged. Raised to 1100 s (~14% over the
76
+ 962 s CI measures with #82's re-pointed defenders), with the CI figures
77
+ recorded beside the local one so the next person budgets against the
78
+ number the check actually sees. The heavy claims the failure now names are
79
+ the next #73-style cut.
80
+ - **Calibration's pure tests moved off the replay fixture** (the #73 pattern).
81
+ Three calibration claims were defended by `test/replay.test.mjs`, which
82
+ builds a scripted git corpus on every run; their kills come from pure
83
+ `calibrationFrom` tests that need none of it. The cost budget fired on the
84
+ first full run — 1022 s against 950 s, the top rows being exactly those
85
+ claims — so the tests now live in `test/calibration.test.mjs`, the claims
86
+ point there, and every fault was re-probed after the move, not assumed.
87
+ - **A cost-budget failure names its claims.** `checkCostBudget` read `id`
88
+ from a report whose claims carry `claimId`, so the one line meant to say
89
+ *which* claims to look at printed `undefined` five times — and its unit test
90
+ fed the shape the code wished for, so it passed. Found the first time the
91
+ budget actually fired. The test now goes through `costReport`, and
92
+ `TG-COST-BUDGET-NAMES-THE-CLAIM` guards it.
93
+ - **A calibration counts `nocover` as a miss** (#82). `nocover` — no test
94
+ even imports the broken file — is the worst replay outcome, and the ratio
95
+ excluded it: a project with no tests at all for a subsystem scored *better*
96
+ than one with weak tests, because its worst outcomes left the denominator
97
+ before the ratio was taken. It now enters both sides, as it does in Stryker
98
+ and PIT, and the replay summary says how many misses were blind and how many
99
+ had no test at all. The schema description, the emitter's docstring and
100
+ `GATE-SEMANTICS.md` had also disagreed about what `p` was — "P(finding is
101
+ real)" in one place, the miss rate in another, and the docstring's two
102
+ sentences conditioning on different denominators — and now say one thing:
103
+ for testguard, the share of real escaped bugs the suite failed to catch.
104
+ - **A calibration's arithmetic is checked, not trusted.** The validator
105
+ recomputed nothing: it checked that `p` sat inside `ci` and would pass a
106
+ document with every number invented. It now recomputes `p` from
107
+ `positives / n` and `ci` from `n`, `positives`, `method` and `confidence`
108
+ with `spec/lib/wilson.mjs` — one implementation, shared with the emitter —
109
+ at the document's own precision, accepting either the exact quantile or the
110
+ textbook 1.96. The spec's own example had carried a truncated lower bound
111
+ (6/30 → `0.09`; Wilson gives 0.0950 → `0.10`) since the day it was written,
112
+ and websec-validator's shipped table reproduces exactly.
113
+ - **A reserved method can now say something.** #81 reserved `assertion` and
114
+ `scan` without required fields, and `fault` / `detail` are both
115
+ `additionalProperties: false` — so a probe using a reserved method could
116
+ declare that it existed and **nothing whatsoever about it**: not which rule
117
+ was checked, not where. That is not a reservation, it is a dead end, and
118
+ DocGuard's writer would have hit it on its first line.
119
+ Both now carry `methodDetail`: an object the spec deliberately does not
120
+ constrain, on the probe and on the record. It is **forbidden under
121
+ `fault-injection`**, whose shape is specified and must not acquire a junk
122
+ drawer — a self-claim fails if that ever stops being enforced — and it is a
123
+ staging area rather than a permanent home. What the first real consumer puts
124
+ there is the evidence for what the specified shape should become.
125
+
10
126
  ## [0.7.0] - 2026-09-18
11
127
 
12
128
  Python, and a fault that cannot hide.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: testguard-cli
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Breaks your code on purpose and reports every promise your tests did not notice breaking. Mutation testing bound to the claims a project makes — vitest, jest, Playwright, pytest and unittest. Ships 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
@@ -150,9 +150,9 @@ raises coverage. TestGuard admits it because it fails when the claim is false.
150
150
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
151
151
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
152
152
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
153
- | GitHub Action | `uses: raccioly/testguard@v0.7.0` — see [`action.yml`](./action.yml) |
153
+ | GitHub Action | `uses: raccioly/testguard@v0.8.0` — see [`action.yml`](./action.yml) |
154
154
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
155
- | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.7.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
155
+ | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
156
156
 
157
157
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
158
158
  less than that many days ago (`ENOVERSIONS`); install that one with
@@ -369,15 +369,18 @@ most resembles, which is the join key that turns an uninterpretable mutation
369
369
  score into a statement with a sample size:
370
370
 
371
371
  ```
372
- guard-removed blind 7/9 p=0.78 ci [0.45, 0.94]
373
- field-dropped blind 4/4 p=1.00 ci [0.51, 1.00]
372
+ guard-removed missed 7/9 p=0.78 ci [0.45, 0.94]
373
+ field-dropped missed 4/4 p=1.00 ci [0.51, 1.00]
374
374
  ```
375
375
 
376
- Read as *"when a fault of this class survives, how often does that
377
- correspond to a bug that really escaped"*. It is written as a
378
- `calibration` document beside the replay one. Only `caught` and `blind`
379
- carry information; the other verdicts are excluded from both sides of the
380
- ratio.
376
+ Read as *"when a real bug of this class escapes, how often does the suite
377
+ miss it"* — a miss rate, so a high `p` is bad. It is written as a
378
+ `calibration` document beside the replay one. `caught`, `blind` and
379
+ `nocover` are measurements and enter the ratio, `nocover` as a miss: leaving
380
+ it out would score a project with no tests at all for a subsystem *better*
381
+ than one with weak tests. `flaky` and `unverifiable` are failed measurements
382
+ and enter neither side. Every `p` and `ci` is recomputable from `n` and the
383
+ miss count, and the validator recomputes them.
381
384
 
382
385
  **The open question this exists to answer.** Does a calibration learned on a
383
386
  repository *with* history transfer to a greenfield one that has none? AI
@@ -448,12 +451,12 @@ detected base that does not resolve is a warning for `status` and `brief`
448
451
  # GitHub Actions
449
452
  - uses: actions/checkout@v4
450
453
  with: { fetch-depth: 0 }
451
- - uses: raccioly/testguard@v0.7.0
454
+ - uses: raccioly/testguard@v0.8.0
452
455
  with: { command: gate }
453
456
 
454
457
  # GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
455
458
  include:
456
- - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.7.0/packaging/gitlab/testguard.gitlab-ci.yml'
459
+ - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml'
457
460
  inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
458
461
  ```
459
462
 
@@ -126,9 +126,9 @@ raises coverage. TestGuard admits it because it fails when the claim is false.
126
126
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
127
127
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
128
128
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
129
- | GitHub Action | `uses: raccioly/testguard@v0.7.0` — see [`action.yml`](./action.yml) |
129
+ | GitHub Action | `uses: raccioly/testguard@v0.8.0` — see [`action.yml`](./action.yml) |
130
130
  | pre-commit | `repo: https://github.com/raccioly/testguard`, hooks `testguard-claims`, `testguard-probe` |
131
- | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.7.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
131
+ | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
132
132
 
133
133
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
134
134
  less than that many days ago (`ENOVERSIONS`); install that one with
@@ -345,15 +345,18 @@ most resembles, which is the join key that turns an uninterpretable mutation
345
345
  score into a statement with a sample size:
346
346
 
347
347
  ```
348
- guard-removed blind 7/9 p=0.78 ci [0.45, 0.94]
349
- field-dropped blind 4/4 p=1.00 ci [0.51, 1.00]
348
+ guard-removed missed 7/9 p=0.78 ci [0.45, 0.94]
349
+ field-dropped missed 4/4 p=1.00 ci [0.51, 1.00]
350
350
  ```
351
351
 
352
- Read as *"when a fault of this class survives, how often does that
353
- correspond to a bug that really escaped"*. It is written as a
354
- `calibration` document beside the replay one. Only `caught` and `blind`
355
- carry information; the other verdicts are excluded from both sides of the
356
- ratio.
352
+ Read as *"when a real bug of this class escapes, how often does the suite
353
+ miss it"* — a miss rate, so a high `p` is bad. It is written as a
354
+ `calibration` document beside the replay one. `caught`, `blind` and
355
+ `nocover` are measurements and enter the ratio, `nocover` as a miss: leaving
356
+ it out would score a project with no tests at all for a subsystem *better*
357
+ than one with weak tests. `flaky` and `unverifiable` are failed measurements
358
+ and enter neither side. Every `p` and `ci` is recomputable from `n` and the
359
+ miss count, and the validator recomputes them.
357
360
 
358
361
  **The open question this exists to answer.** Does a calibration learned on a
359
362
  repository *with* history transfer to a greenfield one that has none? AI
@@ -424,12 +427,12 @@ detected base that does not resolve is a warning for `status` and `brief`
424
427
  # GitHub Actions
425
428
  - uses: actions/checkout@v4
426
429
  with: { fetch-depth: 0 }
427
- - uses: raccioly/testguard@v0.7.0
430
+ - uses: raccioly/testguard@v0.8.0
428
431
  with: { command: gate }
429
432
 
430
433
  # GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
431
434
  include:
432
- - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.7.0/packaging/gitlab/testguard.gitlab-ci.yml'
435
+ - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml'
433
436
  inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
434
437
  ```
435
438
 
@@ -46,7 +46,7 @@ inputs:
46
46
  version:
47
47
  description: 'testguard-cli version to run'
48
48
  required: false
49
- default: '0.7.0'
49
+ default: '0.8.0'
50
50
  node-version:
51
51
  description: 'Node.js version (20+)'
52
52
  required: false
@@ -42,7 +42,9 @@ defaults to `fault-injection`, whose required fields — `file`, `find`,
42
42
  baseline and probe runs on evidence — are unchanged and are enforced by the
43
43
  validator rather than merely by the schema. `assertion` and `scan` are
44
44
  reserved names for tools that verify by reading or scanning; their fields are
45
- defined by their first consumer, not in advance.
45
+ defined by their first consumer, not in advance. Until then a reserved-method
46
+ probe and record carry `methodDetail`, an unconstrained object that is
47
+ forbidden under `fault-injection`.
46
48
  `spec/lib/validate.mjs` applies the schema **and** the semantic rules a schema
47
49
  cannot express. `spec/lib/fingerprint.mjs` is the single fingerprint
48
50
  implementation every tool must use.
@@ -37,7 +37,7 @@
37
37
  | NFR-04 | Every emitted document conforms to the published schema before it is written | `writeSpecDoc` refuses non-conforming output |
38
38
  | NFR-05 | Verdicts are reproducible and attributable to a commit | evidence records `repo.head`, the snapshot when the working tree was probed, and the runner and its source |
39
39
  | NFR-06 | A verdict is never optimistic under uncertainty | contention recorded; flaky and timeout verdicts gate; escalation cannot upgrade a verdict; a survivor is unverifiable until the defenders are shown to fail because of the subject |
40
- | NFR-07 | Cost grows with what changed, not with the size of the project | verdict reuse on unchanged inputs; blast-radius-targeted escalation |
40
+ | NFR-07 | Cost grows with what changed, not with the size of the project | verdict reuse on unchanged inputs; blast-radius-targeted escalation; a committed cost budget gated in CI, so growth is signed for rather than discovered |
41
41
  | NFR-08 | Portable contract | schemas are JSON Schema 2020-12, readable and implementable without this codebase |
42
42
 
43
43
  ## Success Criteria
@@ -97,7 +97,7 @@ fails the pull request naming the unclaimed file, and `status` reports
97
97
  | FR-10 | `src/probe/runners/` | fixture acceptance per runner, `TG-RUNNER-FROM-PROJECT-FIRST` |
98
98
  | FR-11 | `src/scaffold/producers.mjs` | `test/scaffold.test.mjs`, `TG-SCAFFOLD-ANCHORS-HIT` |
99
99
  | FR-12 | `src/admit/admit.mjs` | `test/admit.test.mjs`, `TG-ADMIT-NEEDS-ALL-KILLED` |
100
- | FR-13 | `src/replay/` | `test/replay.test.mjs`, `TG-REPLAY-FLAKY-IS-NEVER-CAUGHT` |
100
+ | FR-13 | `src/replay/` | `test/replay.test.mjs`, `test/calibration.test.mjs`, `TG-REPLAY-FLAKY-IS-NEVER-CAUGHT`, `TG-CALIBRATION-COUNTS-NOCOVER-AS-A-MISS` |
101
101
  | FR-14 | `src/mcp/` | `test/mcp.test.mjs` |
102
102
  | NFR-01 | `src/init/init.mjs`, runner resolution | `TG-INIT-HOOK-NO-NETWORK`, `TG-README-HOOK-MATCHES-THE-CODE` |
103
103
  | NFR-02 | `package.json` | `npm run test:install` in CI |
@@ -105,8 +105,8 @@ fails the pull request naming the unclaimed file, and `status` reports
105
105
  | NFR-04 | `src/evidence/writer.mjs` | `test/writer.test.mjs` |
106
106
  | NFR-05 | `src/probe/probe.mjs` | `TG-FINGERPRINT-VERDICT`, `TG-FAULT-EDIT-VISIBLE` |
107
107
  | NFR-06 | `src/probe/contention.mjs`, `classify.mjs` | `TG-CONTENTION-NEVER-FAILS-A-PROBE`, `TG-ESCALATION-N-RUNS`, `TG-ONE-BAD-FAULT-KEEPS-THE-EVIDENCE`, `TG-A-PRECONDITION-STILL-REFUSES-THE-RUN`, `TG-A-PROBE-ERROR-IS-NEVER-REUSED`, `TG-SURVIVOR-PROVES-THE-SUBJECT-RUNS`, `TG-CONTROL-READS-ANY-RED-AS-REACHED`, `TG-FATAL-EDIT-CANNOT-COMPILE` |
108
- | NFR-07 | verdict reuse, `src/probe/rank.mjs` | `test/probe.fixture.test.mjs` reuse case |
109
- | NFR-08 | `spec/` | `spec/conformance/schemas.test.mjs` |
108
+ | NFR-07 | verdict reuse, `src/probe/rank.mjs`, `src/probe/cost.mjs` | `test/probe.fixture.test.mjs` reuse case, `test/cost.test.mjs`, `TG-COST-BUDGET-ACTUALLY-GATES` |
109
+ | NFR-08 | `spec/` | `spec/conformance/schemas.test.mjs`, `TG-WILSON-REPRODUCES-AT-THE-DOCUMENTS-PRECISION` |
110
110
 
111
111
  ## Revision History
112
112
 
@@ -20,7 +20,7 @@ the test that defends it, and CI fails if any injected fault survives.
20
20
  | Unit | `test/*.test.mjs` | One pure function's branches, without a runner | milliseconds |
21
21
  | Conformance | `spec/conformance/` | Every valid example validates; every must-reject document is rejected for the defect its filename names | ~1 s |
22
22
  | Fixture acceptance | `test/probe.fixture.test.mjs`, `test/probe.jest.test.mjs`, `test/probe.playwright.test.mjs` | The whole pipeline reproduces a known-answer oracle, one per runner | 10–50 s each |
23
- | Self-verification | `testguard.claims.json`, run by `npm run self:probe` | The tool's own invariants survive fault injection | **24 min** cold (measured 2026-09-18: 59 faults, `--serial`, idle machine; 1002 s baseline + 888 s probe); seconds when verdicts are reused, which is why CI restores the previous evidence from cache |
23
+ | Self-verification | `testguard.claims.json`, run by `npm run self:probe` | The tool's own invariants survive fault injection | **13.4 min** cold (measured 2026-09-18 at `cf5a083`: 96 faults, 576 defender runs, 804 s, `--serial`, idle machine); seconds when verdicts are reused, which is why CI restores the previous evidence from cache. Budgeted at 950 s in `testguard.cost-budget.json` and **gated** in CI — the figure above went 9.6 → 23.6 min across one merged change while `--cost` printed it into a log nobody read |
24
24
  | Install smoke | `.github/scripts/install-smoke.mjs` | The **packed tarball** runs with production dependencies only | ~20 s |
25
25
  | Runtime budget | `ci.yml` | The suite has not silently started walking the wrong tree | gate, not a test |
26
26
 
@@ -77,7 +77,7 @@ What is gated instead:
77
77
  | `src/status/status.mjs` | `test/status.test.mjs` |
78
78
  | `src/brief/brief.mjs` | `test/brief.test.mjs` |
79
79
  | `src/init/init.mjs` | `test/init.test.mjs` |
80
- | `src/replay/` | `test/replay.test.mjs` |
80
+ | `src/replay/` | `test/replay.test.mjs`, `test/calibration.test.mjs` (the pure half: `calibrationFrom`, `wilson`) |
81
81
  | `src/mcp/` | `test/mcp.test.mjs` |
82
82
  | `spec/lib/` | `spec/conformance/schemas.test.mjs` |
83
83
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "testguard-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "testguard-cli",
9
- "version": "0.6.0",
9
+ "version": "0.8.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "ajv": "8.20.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testguard-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Breaks your code on purpose and reports every promise your tests did not notice breaking. Mutation testing bound to the claims a project makes — vitest, jest, Playwright, pytest and unittest.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@
3
3
  # Include it from a project's .gitlab-ci.yml, pinned to a release:
4
4
  #
5
5
  # include:
6
- # - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.7.0/packaging/gitlab/testguard.gitlab-ci.yml'
6
+ # - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.8.0/packaging/gitlab/testguard.gitlab-ci.yml'
7
7
  # inputs:
8
8
  # dir: backend # where testguard.claims.json lives (default ".")
9
9
  # post_note: true # post the brief as a merge-request note (needs TESTGUARD_GITLAB_TOKEN, api scope)
@@ -29,7 +29,7 @@ spec:
29
29
  inputs:
30
30
  version:
31
31
  description: testguard-cli version to run
32
- default: "0.7.0"
32
+ default: "0.8.0"
33
33
  dir:
34
34
  description: directory whose test runner is invoked (where testguard.claims.json lives)
35
35
  default: "."
@@ -13,7 +13,7 @@
13
13
  class Testguard < Formula
14
14
  desc "Proves a test suite defends the claims a project makes"
15
15
  homepage "https://github.com/raccioly/testguard"
16
- url "https://registry.npmjs.org/testguard-cli/-/testguard-cli-0.7.0.tgz"
16
+ url "https://registry.npmjs.org/testguard-cli/-/testguard-cli-0.8.0.tgz"
17
17
  sha256 "385d69f9d3c153b934d9c1cb6a2c754eb9b221b0a8c0ba8b805858384d2d4678"
18
18
  license "MIT"
19
19
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "testguard-cli"
7
- version = "0.7.0"
7
+ version = "0.8.0"
8
8
  description = "Breaks your code on purpose and reports every promise your tests did not notice breaking. Mutation testing bound to the claims a project makes — vitest, jest, Playwright, pytest and unittest. Ships the Node.js CLI (requires Node.js 20+)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -167,6 +167,14 @@ Rules that follow from the table:
167
167
  under `fault-injection`, and the validator enforces them there in full: the
168
168
  relaxation is for a tool that injects nothing, never a licence for an
169
169
  injecting tool to stop showing its work.
170
+ A reserved method carries its own data in `methodDetail`, on the probe and
171
+ on the record: an object the spec deliberately does not constrain, so a tool
172
+ can say which rule it checked and where. Without it, "reserved" would mean a
173
+ probe that can declare it exists and nothing about it, which is not a
174
+ reservation but a dead end. It is **forbidden under `fault-injection`**,
175
+ whose shape is specified and must not acquire a junk drawer, and it is a
176
+ staging area rather than a permanent home: what the first consumer puts
177
+ there is the evidence for what the specified shape should become.
170
178
  `assertion` and `scan` are **reserved**. Their required fields are
171
179
  deliberately unspecified and will be defined by their first real consumer,
172
180
  with its own conformance examples written from the shape that tool actually
@@ -201,9 +209,96 @@ Two rules that follow:
201
209
  2. **The fix's own test is removed before the run.** It proves nothing about
202
210
  what the suite knew before the fix existed.
203
211
 
204
- Only `caught` and `blind` carry information. `nocover`, `flaky` and
205
- `unverifiable` are excluded from both sides of a calibration ratio, because a
206
- number computed over them would mean nothing.
212
+ `caught`, `blind` and `nocover` are measurements and enter the calibration
213
+ ratio: `caught` as a hit, the other two as misses. `nocover` counts as a miss
214
+ because it is one — the worst kind. Leaving it out would let a project with
215
+ **no tests at all** for a subsystem score better than one with weak tests,
216
+ since its worst outcomes would leave the denominator before the ratio is
217
+ taken; Stryker and PIT count no-coverage mutants against the headline score
218
+ for the same reason. `flaky` and `unverifiable` are failed measurements and
219
+ enter neither side.
220
+
221
+ ### Calibration arithmetic is reproducible
222
+
223
+ A calibration cell declares `n` and `positives`, and with them has declared
224
+ `p`; a document declares `method: wilson` at `confidence`, and with them has
225
+ declared every `ci`. The validator recomputes both with `lib/wilson.mjs` —
226
+ the single implementation, as `lib/fingerprint.mjs` is for fingerprints — and
227
+ a cell that does not reproduce does not conform. Before this rule the
228
+ validator checked only that `p` lay inside `ci`, which a document with every
229
+ number invented satisfies, and the spec's own example shipped a truncated
230
+ lower bound (6/30 → `0.09`, where Wilson gives 0.0950 → `0.10`) that nothing
231
+ could notice.
232
+
233
+ - **Precision is the document's own**: the most decimal places any `p` or
234
+ `ci` value shows, capped at 6. Values are compared after rounding to that
235
+ precision, so a producer at 3 dp and one at 4 dp both reproduce and a
236
+ truncated bound does not.
237
+ - **z may be the exact quantile or the textbook rounding** (1.96, 2.576,
238
+ 1.645). The two differ by up to 9.2e-6 in the interval — enough to flip a
239
+ rounding boundary at 4 dp — so either reproduces, and a document never fails
240
+ for having used the number in the textbook.
241
+ - **`p` is `positives / n`.** A smoothed or shrunk point estimate is a
242
+ different method and must say so; under `wilson` the point estimate is the
243
+ observed proportion.
244
+
245
+ ### Calibration provenance, backoff and merging
246
+
247
+ Everything beyond a document's primary buckets is optional and additive, in
248
+ the way `method` is on claims and evidence: absence has a defined meaning, and
249
+ no document written before a field existed becomes invalid. The meaning of
250
+ absence is **unattributed** — readable, auditable, and not a source of
251
+ numbers. A tool that emits calibrations always emits these fields; a
252
+ self-claim guards that it does.
253
+
254
+ - **`measures`** says what `p` is the probability of. It is the one field
255
+ that lets two calibrations be told apart: testguard's `escape-missed` is
256
+ P(a real escaped bug of this class was not caught), and high is bad;
257
+ websec-validator's `finding-real` is P(a reported finding of this class is a
258
+ real vulnerability), and high is good. A document without it cannot be
259
+ quoted and cannot be merged. Registry: `escape-missed`, `finding-real`. A
260
+ new value is added here by the tool that first emits it.
261
+ - **`source`** carries provenance. `corpus` names external ground-truth
262
+ sources (a bug-replay's is the repository's own history, which `ref` already
263
+ names); `caveat` is the one sentence a consumer shows beside any quoted
264
+ number; `limitation` is the longer story; `evidenceStatus` says whether the
265
+ labels were reviewed; `kind: tool-oracle` is a tool confirming its own
266
+ findings, as websec-validator's dynamic probes do. `detail` is the tool's
267
+ own provenance, unconstrained, on the `methodDetail` pattern. A number
268
+ quoted without its `caveat` is a misquote.
269
+ - **`minN`** is the producer's floor. A cell below it is not quoted on its
270
+ own; the consumer backs off a tier. A consumer may raise it — a gate that
271
+ blocks a merge wants more certainty than a report — and may never lower
272
+ it: lowering it resurrects a cell the producer suppressed. Absent, the
273
+ producer sets no floor and the consumer applies its own. A spec-level
274
+ constant would be wrong in both directions: websec-validator's 5 would
275
+ suppress every cell of a corpus the size of DocGuard's benchmark.
276
+ - **`backoff`** is the ordered list of coarser tiers consulted below `minN`.
277
+ Each tier is held to the same arithmetic as the primary. Keys may be
278
+ compound (`attackClass|confidence` backs off to `confidence`), and every key
279
+ in a tier has as many `|`-parts as its `bucketBy` — a key with fewer was
280
+ translated from another table and lost a dimension on the way.
281
+ - **`fallback`** is what remains when no tier has a cell above `minN`: a
282
+ labelled guess, not a measurement, and anything quoted from it carries
283
+ `n = 0`, `ci = [0, 1]` and its `basis`. It lives in the document so that
284
+ "what did it assume when it had no data?" has one auditable answer instead
285
+ of one per consumer, each unlabelled.
286
+ - **`breakdown`** splits a cell's `n` by outcome when the producer can say
287
+ (testguard: `caught` / `blind` / `nocover`), and sums to `n`. It lets a
288
+ consumer recover a narrower rate — misses among covered code, say —
289
+ without the producer publishing a second, misleading headline.
290
+
291
+ **Merging.** Two calibrations merge by summing `n` and `positives` per cell
292
+ and recomputing `p` and `ci` from the sums — never by averaging `p` or `ci`,
293
+ which have no meaning combined. They merge only when `measures`, `bucketBy`
294
+ and `confidence` agree, and the merged document's `source.kind` is `mixed`.
295
+ This is how websec-validator folds an operator's own confirmed samples over
296
+ its shipped table, and it works only because cells carry raw counts.
297
+
298
+ **The second producer is a conformance example.** websec-validator's shipped
299
+ table, translated field for field, is `conformance/examples/calibration-websec.json`
300
+ — corpus, caveat, limitation, floor, backoff tier and labelled fallback all
301
+ present. That file, not a sentence in a README, is what "adoptable" means.
207
302
 
208
303
  ## Baseline and delta
209
304
 
@@ -28,7 +28,7 @@ that carry claims, baselines, scoping and briefs do not care.
28
28
  | `evidence` | [`schemas/evidence.schema.json`](schemas/evidence.schema.json) | One run's findings on disk, readable without the tool. Every verdict carries every run that produced it. |
29
29
  | `baseline` | [`schemas/baseline.schema.json`](schemas/baseline.schema.json) | Frozen fingerprints of existing debt. Gate only what is new. |
30
30
  | `ignore` | [`schemas/ignore.schema.json`](schemas/ignore.schema.json) | Reviewable scoping. Every entry has a reason. |
31
- | `calibration` | [`schemas/calibration.schema.json`](schemas/calibration.schema.json) | P(finding is real) per bucket, with a Wilson interval and the sample size behind it. |
31
+ | `calibration` | [`schemas/calibration.schema.json`](schemas/calibration.schema.json) | The observed rate of a bucket's outcome — what it measures is in the document — with a Wilson interval and the sample size behind it. Every number is recomputable and the validator recomputes it; provenance, a producer floor, backoff tiers and a labelled fallback ride along so a second tool's honesty survives translation. |
32
32
  | `brief` | [`schemas/brief.schema.json`](schemas/brief.schema.json) | What to tell an agent before it writes code — ranked, capped, never a single score, and carrying the one next action. |
33
33
  | `status` | [`schemas/status.schema.json`](schemas/status.schema.json) | Where the project is and what happens next — the single machine-readable truth every human rendering derives from. Surfaces faults whose content changed since they were probed, and changed files that carry no claim. |
34
34
  | `replay` | [`schemas/replay.schema.json`](schemas/replay.schema.json) | Would this suite have caught the bugs that already escaped? Replays real fix commits: revert the source, remove the test the fix shipped, run what remains. A replayed bug is ground truth — a human already confirmed it was a defect — which is what a fault model is calibrated against. |
@@ -65,7 +65,10 @@ consumer. It moves to its own repository only when a second tool adopts it.
65
65
 
66
66
  Adoption is additive and optional: an existing tool gains an output writer, and
67
67
  nothing it already detects or already writes changes. That holds today for
68
- `baseline`, `ignore`, `brief` and `status`. It does **not** yet hold for
68
+ `baseline`, `ignore`, `brief`, `status` and `calibration` — websec-validator's
69
+ shipped calibration table is `conformance/examples/calibration-websec.json`,
70
+ translated field for field with its corpus, caveat, floor, backoff and
71
+ fallback intact. It does **not** yet hold for
69
72
  `claims` and `evidence`, whose required fields assume fault injection
70
73
  (`find`/`replace` anchors, `confirmRuns`, `baselineRuns`/`probeRuns`,
71
74
  `defenders`) — a tool that scans or reads would have to emit empty arrays to