testguard-cli 0.9.0__tar.gz → 0.9.1__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 (302) hide show
  1. testguard_cli-0.9.1/.github/scripts/build-one-pager.mjs +95 -0
  2. testguard_cli-0.9.1/.github/scripts/check-cost-budget.mjs +79 -0
  3. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/scripts/sync-release-version.mjs +7 -0
  4. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/workflows/auto-merge.yml +1 -1
  5. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/workflows/scheduled-release.yml +71 -6
  6. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/CHANGELOG.md +106 -0
  7. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/PKG-INFO +8 -7
  8. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/README.md +7 -6
  9. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/action.yml +1 -1
  10. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs/testguard-explained.html +80 -7
  11. testguard_cli-0.9.1/docs/testguard-explained.pdf +0 -0
  12. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/REQUIREMENTS.md +2 -2
  13. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/package-lock.json +2 -2
  14. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/package.json +1 -1
  15. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/packaging/gitlab/testguard.gitlab-ci.yml +2 -2
  16. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/packaging/homebrew/testguard.rb +2 -2
  17. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/pyproject.toml +1 -1
  18. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/cost.mjs +43 -6
  19. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/classify.test.mjs +35 -0
  20. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/cost.test.mjs +65 -0
  21. testguard_cli-0.9.1/test/label.test.mjs +35 -0
  22. testguard_cli-0.9.1/test/one-pager.test.mjs +88 -0
  23. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/release-sync.test.mjs +6 -2
  24. testguard_cli-0.9.1/test/replay-verdict.test.mjs +43 -0
  25. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/replay.test.mjs +0 -58
  26. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/testguard.claims.json +113 -5
  27. testguard_cli-0.9.1/testguard.cost-budget.json +96 -0
  28. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/testguard.ignore.json +1 -1
  29. testguard_cli-0.9.0/.github/scripts/build-one-pager.mjs +0 -49
  30. testguard_cli-0.9.0/.github/scripts/check-cost-budget.mjs +0 -43
  31. testguard_cli-0.9.0/docs/testguard-explained.pdf +0 -0
  32. testguard_cli-0.9.0/testguard.cost-budget.json +0 -82
  33. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.agents/skills/testguard/SKILL.md +0 -0
  34. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.codex/hooks.json +0 -0
  35. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.docguard.json +0 -0
  36. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.docguardignore +0 -0
  37. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.gitattributes +0 -0
  38. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  39. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  40. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  41. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/dependabot.yml +0 -0
  42. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/scripts/check-python-fixture.mjs +0 -0
  43. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/scripts/install-smoke.mjs +0 -0
  44. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/workflows/ci.yml +0 -0
  45. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/workflows/release.yml +0 -0
  46. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.github/workflows/supply-chain.yml +0 -0
  47. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.gitignore +0 -0
  48. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.npmignore +0 -0
  49. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.npmrc +0 -0
  50. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.pre-commit-config.yaml +0 -0
  51. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/.pre-commit-hooks.yaml +0 -0
  52. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/AGENTS.md +0 -0
  53. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/CLAUDE.md +0 -0
  54. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/CODE_OF_CONDUCT.md +0 -0
  55. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/CONTRIBUTING.md +0 -0
  56. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/DRIFT-LOG.md +0 -0
  57. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/LICENSE +0 -0
  58. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/PRIVACY.md +0 -0
  59. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/SECURITY.md +0 -0
  60. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/SUPPORT.md +0 -0
  61. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/bench/README.md +0 -0
  62. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/cli/testguard.mjs +0 -0
  63. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/ARCHITECTURE.md +0 -0
  64. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/DATA-MODEL.md +0 -0
  65. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/ENVIRONMENT.md +0 -0
  66. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/SECURITY.md +0 -0
  67. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/docs-canonical/TEST-SPEC.md +0 -0
  68. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/.gitignore +0 -0
  69. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/README.md +0 -0
  70. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/expected.json +0 -0
  71. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/package.json +0 -0
  72. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/src/export.mjs +0 -0
  73. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/src/redact.mjs +0 -0
  74. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/test/export-mocked.test.mjs +0 -0
  75. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/test/flaky.test.mjs +0 -0
  76. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/test/redact.test.mjs +0 -0
  77. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/testguard.claims.json +0 -0
  78. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer/vitest.config.mjs +0 -0
  79. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/.gitignore +0 -0
  80. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/README.md +0 -0
  81. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/expected.json +0 -0
  82. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/jest.config.js +0 -0
  83. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/package.json +0 -0
  84. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/src/export.js +0 -0
  85. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/src/redact.js +0 -0
  86. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/test/export-mocked.test.js +0 -0
  87. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/test/flaky.test.js +0 -0
  88. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/test/redact.test.js +0 -0
  89. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-jest/testguard.claims.json +0 -0
  90. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/.gitignore +0 -0
  91. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/README.md +0 -0
  92. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/e2e/flaky.spec.mjs +0 -0
  93. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/e2e/toggle.spec.mjs +0 -0
  94. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/expected.json +0 -0
  95. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/package-lock.json +0 -0
  96. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/package.json +0 -0
  97. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/playwright.config.mjs +0 -0
  98. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/src/toggle.mjs +0 -0
  99. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/test/toggle.test.mjs +0 -0
  100. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/test-results/.last-run.json +0 -0
  101. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/testguard.claims.json +0 -0
  102. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-playwright/vitest.config.mjs +0 -0
  103. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/.gitignore +0 -0
  104. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/README.md +0 -0
  105. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/demo/__init__.py +0 -0
  106. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/demo/export.py +0 -0
  107. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/demo/orphan.py +0 -0
  108. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/demo/pipeline.py +0 -0
  109. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/demo/redact.py +0 -0
  110. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/expected.json +0 -0
  111. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/testguard.claims.json +0 -0
  112. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/tests/test_export_mocked.py +0 -0
  113. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/tests/test_flaky.py +0 -0
  114. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/tests/test_patched.py +0 -0
  115. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/fixtures/known-answer-python/tests/test_redact.py +0 -0
  116. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/GATE-SEMANTICS.md +0 -0
  117. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/README.md +0 -0
  118. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/baseline.json +0 -0
  119. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/brief.json +0 -0
  120. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/calibration-websec.json +0 -0
  121. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/calibration.json +0 -0
  122. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/claims-methods.json +0 -0
  123. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/claims.json +0 -0
  124. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence-disturbed-run.json +0 -0
  125. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence-negative-control.json +0 -0
  126. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence-provisional.json +0 -0
  127. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence-python.json +0 -0
  128. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence-scan-method.json +0 -0
  129. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/evidence.json +0 -0
  130. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/gate.json +0 -0
  131. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/ignore.json +0 -0
  132. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/replay.json +0 -0
  133. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/examples/status.json +0 -0
  134. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/baseline.bad-fingerprint-key.json +0 -0
  135. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/baseline.zero-count.json +0 -0
  136. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/brief.text-without-heading.json +0 -0
  137. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/brief.unknown-verdict-key.json +0 -0
  138. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.backoff-cell-not-wilson.json +0 -0
  139. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.breakdown-does-not-sum-to-n.json +0 -0
  140. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.ci-not-wilson.json +0 -0
  141. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.compound-key-arity-mismatch.json +0 -0
  142. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.fallback-without-basis.json +0 -0
  143. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.p-not-positives-over-n.json +0 -0
  144. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.p-outside-ci.json +0 -0
  145. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/calibration.positives-exceed-n.json +0 -0
  146. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.absolute-path.json +0 -0
  147. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.duplicate-fault-id.json +0 -0
  148. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.fault-injection-without-a-substitution.json +0 -0
  149. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.method-detail-on-a-specified-method.json +0 -0
  150. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.missing-provenance.json +0 -0
  151. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.noop-fault.json +0 -0
  152. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.occurrence-exceeds-hits.json +0 -0
  153. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/claims.parent-traversal.json +0 -0
  154. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.annotated-signal-without-reason.json +0 -0
  155. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.attribute-signal-not-a-defender.json +0 -0
  156. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.attribute-signal-without-attributes.json +0 -0
  157. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.byrunner-file-not-resolved.json +0 -0
  158. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.contention-without-runners.json +0 -0
  159. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.discovered-defender-mocks-subject.json +0 -0
  160. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.fault-injection-without-its-evidence.json +0 -0
  161. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.flaky-defender-without-flakiness.json +0 -0
  162. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.ignored-dirty-on-snapshot.json +0 -0
  163. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.killed-by-non-assertion.json +0 -0
  164. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.killed-on-red-baseline.json +0 -0
  165. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.killed-without-n-runs.json +0 -0
  166. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.method-detail-on-an-injecting-run.json +0 -0
  167. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.negative-control-not-reached-but-survived.json +0 -0
  168. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.negative-control-reached-on-a-kill.json +0 -0
  169. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.probe-error-without-message.json +0 -0
  170. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.provisional-flag-on-confirmed-run.json +0 -0
  171. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.rank-extra-property.json +0 -0
  172. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.restore-skipped-in-place.json +0 -0
  173. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.single-run-without-provisional.json +0 -0
  174. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.target-not-imported-but-killed.json +0 -0
  175. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.undeclared-killers-without-reason.json +0 -0
  176. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.unknown-verdict.json +0 -0
  177. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.unverifiable-without-reason.json +0 -0
  178. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/evidence.wrong-fingerprint.json +0 -0
  179. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/gate.counts-disagree.json +0 -0
  180. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/gate.exit-zero-with-uncovered.json +0 -0
  181. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/gate.ignore-cover-without-pattern.json +0 -0
  182. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/ignore.missing-reason.json +0 -0
  183. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/ignore.short-reason.json +0 -0
  184. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/replay.blind-with-a-failing-run.json +0 -0
  185. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/replay.duplicate-patch-id.json +0 -0
  186. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/status.claim-without-file.json +0 -0
  187. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/status.clean-with-new-findings.json +0 -0
  188. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/status.unclaimed-hidden-behind-unproven.json +0 -0
  189. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/invalid/status.write-test-without-target.json +0 -0
  190. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/conformance/schemas.test.mjs +0 -0
  191. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/lib/fingerprint.mjs +0 -0
  192. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/lib/validate.mjs +0 -0
  193. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/lib/wilson.mjs +0 -0
  194. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/baseline.schema.json +0 -0
  195. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/brief.schema.json +0 -0
  196. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/calibration.schema.json +0 -0
  197. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/claims.schema.json +0 -0
  198. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/common.schema.json +0 -0
  199. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/evidence.schema.json +0 -0
  200. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/gate.schema.json +0 -0
  201. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/ignore.schema.json +0 -0
  202. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/replay.schema.json +0 -0
  203. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/spec/schemas/status.schema.json +0 -0
  204. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/admit/admit.mjs +0 -0
  205. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/baseline/baseline.mjs +0 -0
  206. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/brief/brief.mjs +0 -0
  207. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/claims/annotations.mjs +0 -0
  208. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/claims/load.mjs +0 -0
  209. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/claims/removed.mjs +0 -0
  210. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/cli.mjs +0 -0
  211. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/admit.mjs +0 -0
  212. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/baseline.mjs +0 -0
  213. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/brief.mjs +0 -0
  214. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/claims.mjs +0 -0
  215. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/gate.mjs +0 -0
  216. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/init.mjs +0 -0
  217. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/mcp.mjs +0 -0
  218. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/probe.mjs +0 -0
  219. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/replay.mjs +0 -0
  220. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/scaffold.mjs +0 -0
  221. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/commands/status.mjs +0 -0
  222. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/evidence/writer.mjs +0 -0
  223. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/gate/changed.mjs +0 -0
  224. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/git.mjs +0 -0
  225. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/init/init.mjs +0 -0
  226. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/init/templates/SKILL.md +0 -0
  227. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/mcp/server.mjs +0 -0
  228. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/mcp/tools.mjs +0 -0
  229. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/attribution.mjs +0 -0
  230. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/classify.mjs +0 -0
  231. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/contention.mjs +0 -0
  232. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/discover.mjs +0 -0
  233. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/independence.mjs +0 -0
  234. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/inject.mjs +0 -0
  235. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/mocks.mjs +0 -0
  236. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/probe.mjs +0 -0
  237. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/progress.mjs +0 -0
  238. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/pyimports.mjs +0 -0
  239. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/rank.mjs +0 -0
  240. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/index.mjs +0 -0
  241. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/jest.mjs +0 -0
  242. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/playwright.mjs +0 -0
  243. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/python/_testguard_pytest_plugin.py +0 -0
  244. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/python/_testguard_report.py +0 -0
  245. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/python/_testguard_unittest_main.py +0 -0
  246. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/python.mjs +0 -0
  247. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/shared.mjs +0 -0
  248. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/runners/vitest.mjs +0 -0
  249. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/probe/worktree.mjs +0 -0
  250. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/render.mjs +0 -0
  251. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/replay/label.mjs +0 -0
  252. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/replay/replay.mjs +0 -0
  253. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/scaffold/producers.mjs +0 -0
  254. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/scaffold/producers.python.mjs +0 -0
  255. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/scaffold/scaffold.mjs +0 -0
  256. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/status/status.mjs +0 -0
  257. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/util/glob.mjs +0 -0
  258. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/src/util/hash.mjs +0 -0
  259. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/admit.test.mjs +0 -0
  260. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/annotations.test.mjs +0 -0
  261. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/attribution.test.mjs +0 -0
  262. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/baseline.test.mjs +0 -0
  263. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/brief.test.mjs +0 -0
  264. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/calibration.test.mjs +0 -0
  265. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/claims-since.test.mjs +0 -0
  266. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/contention.test.mjs +0 -0
  267. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/discover.test.mjs +0 -0
  268. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/gate.test.mjs +0 -0
  269. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/gitlab-template.test.mjs +0 -0
  270. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/glob.test.mjs +0 -0
  271. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/independence.test.mjs +0 -0
  272. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/init.test.mjs +0 -0
  273. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/inject.test.mjs +0 -0
  274. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/mcp.test.mjs +0 -0
  275. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/mocks.test.mjs +0 -0
  276. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/negative-control.test.mjs +0 -0
  277. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe-error.test.mjs +0 -0
  278. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe-preconditions.test.mjs +0 -0
  279. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe.fixture.test.mjs +0 -0
  280. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe.jest.test.mjs +0 -0
  281. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe.playwright.test.mjs +0 -0
  282. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/probe.python.test.mjs +0 -0
  283. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/progress.test.mjs +0 -0
  284. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/pyimports.test.mjs +0 -0
  285. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/rank-aliases.test.mjs +0 -0
  286. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/runner-argv.test.mjs +0 -0
  287. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/runner-command.test.mjs +0 -0
  288. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/runner-playwright.test.mjs +0 -0
  289. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/runner-python.test.mjs +0 -0
  290. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/runner-vitest.test.mjs +0 -0
  291. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/samples/playwright-report.json +0 -0
  292. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/samples/python-report-collect-error.json +0 -0
  293. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/samples/python-report.json +0 -0
  294. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/scaffold.python.test.mjs +0 -0
  295. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/scaffold.test.mjs +0 -0
  296. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/snapshot.test.mjs +0 -0
  297. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/status.test.mjs +0 -0
  298. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/worktree.test.mjs +0 -0
  299. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/test/writer.test.mjs +0 -0
  300. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/testguard_cli/__init__.py +0 -0
  301. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/testguard_cli/wrapper.py +0 -0
  302. {testguard_cli-0.9.0 → testguard_cli-0.9.1}/vitest.config.mjs +0 -0
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Render docs/testguard-explained.html to PDF with headless Chrome.
4
+ *
5
+ * Chrome rather than a library because this repository has one pinned runtime
6
+ * dependency and keeps it that way: a PDF renderer would be the second, and it
7
+ * would be needed only to build a document nobody imports.
8
+ *
9
+ * Set CHROME to override the binary.
10
+ *
11
+ * (no flags) render; exit 0 with a notice when no Chrome is present,
12
+ * so a checkout without one can still run every other
13
+ * script.
14
+ * --require-chrome a missing Chrome is a failure. The release uses this:
15
+ * there, "skipped the render" means shipping a PDF that
16
+ * disagrees with the HTML beside it, which is the exact
17
+ * drift this script exists to prevent — and it would pass
18
+ * silently, which is worse than failing.
19
+ *
20
+ * After a successful render the page count is checked against the number of
21
+ * `.page` divs in the source. That is not a nicety: `.page` sets
22
+ * `min-height: 279.4mm` and `page-break-after: always`, which is a request,
23
+ * not a constraint. A section a few pixels too tall spills onto a second
24
+ * physical sheet with no warning from Chrome, renumbering nothing and
25
+ * contradicting the "Page N of M" footers the document prints itself. It
26
+ * happened while updating the brief for 0.8.1 and cost two extra pages.
27
+ */
28
+ import { existsSync, readFileSync } from 'node:fs';
29
+ import { spawnSync } from 'node:child_process';
30
+ import { dirname, join, resolve } from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
34
+ const src = join(root, 'docs', 'testguard-explained.html');
35
+ const out = join(root, 'docs', 'testguard-explained.pdf');
36
+ const requireChrome = process.argv.slice(2).includes('--require-chrome');
37
+
38
+ // CHROME overrides rather than merely leads: a binary named explicitly and not
39
+ // found is a mistake worth hearing about, not a reason to quietly render with
40
+ // some other browser the machine happens to have.
41
+ const candidates = process.env.CHROME ? [process.env.CHROME] : [
42
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
43
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
44
+ '/usr/bin/google-chrome',
45
+ '/usr/bin/chromium',
46
+ '/usr/bin/chromium-browser',
47
+ ];
48
+
49
+ const chrome = candidates.find((c) => existsSync(c));
50
+ if (!chrome) {
51
+ const how = process.env.CHROME
52
+ ? `CHROME=${process.env.CHROME} does not exist`
53
+ : 'set CHROME=<path> to force';
54
+ if (requireChrome) {
55
+ console.error(`no Chrome found and --require-chrome was given; refusing to leave a stale PDF (${how})`);
56
+ process.exit(1);
57
+ }
58
+ console.log(`no Chrome found; skipping PDF build (${how})`);
59
+ process.exit(0);
60
+ }
61
+
62
+ const r = spawnSync(chrome, [
63
+ '--headless',
64
+ '--disable-gpu',
65
+ '--no-sandbox',
66
+ '--no-pdf-header-footer',
67
+ `--print-to-pdf=${out}`,
68
+ `file://${src}`,
69
+ ], { encoding: 'utf8' });
70
+
71
+ if (r.status !== 0 || !existsSync(out)) {
72
+ console.error(r.stderr || 'chrome produced no PDF');
73
+ process.exit(1);
74
+ }
75
+
76
+ /** The sheets Chrome actually produced. `/Type /Pages` is the tree node, not a sheet. */
77
+ const sheets = (readFileSync(out).toString('latin1').match(/\/Type\s*\/Page(?![s/\w])/g) ?? []).length;
78
+ /** The sheets the document intends: one `.page` div each. */
79
+ const intended = (readFileSync(src, 'utf8').match(/<div class="page"[\s>]/g) ?? []).length;
80
+
81
+ if (intended === 0) {
82
+ console.error('docs/testguard-explained.html: no `.page` divs found — the page-count guard cannot run');
83
+ process.exit(1);
84
+ }
85
+ if (sheets !== intended) {
86
+ console.error(
87
+ `docs/testguard-explained.pdf: ${sheets} sheets rendered from ${intended} \`.page\` divs.\n` +
88
+ 'A .page taller than its sheet spills silently. Letter at margin 0 is 816x1056 CSS px: load the\n' +
89
+ 'HTML at that viewport and look for a .page whose getBoundingClientRect().height exceeds 1056.\n' +
90
+ 'Measuring at any other width is meaningless, because the text reflows.',
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ console.log(`docs/testguard-explained.pdf written with ${chrome.split('/').pop()} — ${sheets} pages, one per .page div`);
@@ -0,0 +1,79 @@
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 { appendFileSync, 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 report = costReport(read(evidencePath).records);
38
+ const decision = checkCostBudget(report, {
39
+ budgetSeconds: budget.seconds,
40
+ perClaimSeconds: budget.perClaimSeconds,
41
+ previousMs,
42
+ });
43
+ const text = renderCostBudget(decision);
44
+ console.log(text);
45
+
46
+ // The gate's own history is the argument for writing this where it is seen:
47
+ // the run went 9.6 -> 23.6 minutes across one merged pull request and nothing
48
+ // said a word, because --cost only ever printed into a log nobody opens while
49
+ // it is passing. A new claim that quietly costs 51 s is the same failure one
50
+ // size down. A job summary is read without opening anything, so the number is
51
+ // in front of a reviewer while the change is still a change.
52
+ if (process.env.GITHUB_STEP_SUMMARY) {
53
+ const rows = (report.claims ?? []).slice(0, 10)
54
+ .map((c) => `| \`${c.claimId}\` | ${(c.ms / 1000).toFixed(1)}s | ${c.runs} |`)
55
+ .join('\n');
56
+ appendFileSync(process.env.GITHUB_STEP_SUMMARY, [
57
+ `## Self-probe cost \u2014 ${decision.ok ? 'within budget' : 'OVER BUDGET'}`,
58
+ '',
59
+ '```',
60
+ text,
61
+ '```',
62
+ '',
63
+ '<details><summary>Most expensive claims</summary>',
64
+ '',
65
+ '| claim | cost | runs |',
66
+ '| --- | ---: | ---: |',
67
+ rows,
68
+ '',
69
+ 'A claim pays for every test in the file it names, so the fix for an expensive',
70
+ 'claim is usually a cheaper defender rather than a bigger budget.',
71
+ '</details>',
72
+ '',
73
+ ].join('\n'));
74
+ }
75
+
76
+ if (!decision.ok) {
77
+ console.log(`::error::${text.split('\n')[0]}`);
78
+ process.exit(1);
79
+ }
@@ -95,6 +95,13 @@ const surfaces = [
95
95
  ['README.md', /testguard\/v\d+\.\d+\.\d+\/packaging/g, `testguard/v${version}/packaging`],
96
96
  ['packaging/gitlab/testguard.gitlab-ci.yml', /testguard\/v\d+\.\d+\.\d+\/packaging/g, `testguard/v${version}/packaging`],
97
97
  ['packaging/gitlab/testguard.gitlab-ci.yml', /(\n version:\n description: [^\n]*\n default: ")\d+\.\d+\.\d+(")/, `$1${version}$2`],
98
+ // The technical brief's masthead. Nothing rendered it for three releases, so
99
+ // it still read v0.6 while the tool shipped 0.8.1. The HTML is the surface;
100
+ // the PDF beside it is a build artifact of this file, which is why the
101
+ // release runs build-one-pager.mjs --require-chrome straight after the sync.
102
+ // Sync the two separately and they disagree — the failure that is worse than
103
+ // being stale, because the document then contradicts itself.
104
+ ['docs/testguard-explained.html', /(Technical Brief · v)\d+\.\d+\.\d+( · MIT)/, `$1${version}$2`],
98
105
  ];
99
106
 
100
107
  if (listSurfaces) {
@@ -53,7 +53,7 @@ jobs:
53
53
  // three files the bump writes — test/release-sync.test.mjs fails if
54
54
  // a surface is missing, which is how the GitLab template's absence
55
55
  // (it would have held every release PR for review) was found.
56
- const RELEASE_SURFACES = new Set(['package.json', 'package-lock.json', 'pyproject.toml', 'CHANGELOG.md', 'action.yml', 'packaging/homebrew/testguard.rb', 'README.md', 'packaging/gitlab/testguard.gitlab-ci.yml']);
56
+ const RELEASE_SURFACES = new Set(['package.json', 'package-lock.json', 'pyproject.toml', 'CHANGELOG.md', 'action.yml', 'packaging/homebrew/testguard.rb', 'README.md', 'packaging/gitlab/testguard.gitlab-ci.yml', 'docs/testguard-explained.html', 'docs/testguard-explained.pdf']);
57
57
 
58
58
  if (author === 'dependabot[bot]') {
59
59
  const m = /from (\d+)\.(\d+)\.\S+ to (\d+)\.(\d+)\./.exec(pr.title || '');
@@ -8,8 +8,15 @@ name: Scheduled Release
8
8
  # GITHUB_TOKEN-created PRs do not trigger `pull_request` workflows, so nothing
9
9
  # would ever start the required checks and auto-merge would wait forever. This
10
10
  # workflow therefore dispatches ci.yml on the release branch itself:
11
- # `workflow_dispatch` is the one event GITHUB_TOKEN may trigger. After the
12
- # three Node legs go green, auto-merge completes. No human step remains.
11
+ # `workflow_dispatch` is the one event GITHUB_TOKEN may trigger.
12
+ #
13
+ # That is not the whole story, and v0.9.0 proved it. The `pull_request` runs
14
+ # the PR creates anyway are held at `action_required`, because the repository
15
+ # requires approval from first-time contributors and `github-actions[bot]` is
16
+ # permanently one. Held runs keep check suites QUEUED on the head sha, so the
17
+ # rollup never becomes terminal and auto-merge waits — with every required
18
+ # check already green. This workflow now approves those runs itself, which
19
+ # GITHUB_TOKEN is permitted to do. Only then does no human step remain.
13
20
 
14
21
  on:
15
22
  schedule:
@@ -85,6 +92,11 @@ jobs:
85
92
  } > /tmp/entry.md
86
93
  awk '/^## \[Unreleased\]/ { print; print ""; while ((getline line < "/tmp/entry.md") > 0) print line; next } { print }' CHANGELOG.md > /tmp/CHANGELOG.new && mv /tmp/CHANGELOG.new CHANGELOG.md
87
94
  node .github/scripts/sync-release-version.mjs
95
+ # The sync just wrote the brief's masthead; the committed PDF still
96
+ # shows the old one. --require-chrome because a skipped render here
97
+ # would ship an HTML and a PDF that disagree, silently and at exit 0.
98
+ # ubuntu-latest carries /usr/bin/google-chrome.
99
+ node .github/scripts/build-one-pager.mjs --require-chrome
88
100
  echo "NEW_VERSION=$NEW" >> "$GITHUB_ENV"
89
101
 
90
102
  - name: Verify the tree is still green
@@ -117,9 +129,11 @@ jobs:
117
129
  # DERIVED from the sync script, never retyped here: when the GitLab
118
130
  # template became a surface it was left out of the hand-written list,
119
131
  # so the first release to reach this guard would have been rejected
120
- # for writing a file the release is supposed to write. The three
121
- # extras are the ones the bump step writes directly.
122
- ALLOWED=$(node .github/scripts/sync-release-version.mjs --list-surfaces; printf 'package.json\npackage-lock.json\nCHANGELOG.md\n')
132
+ # for writing a file the release is supposed to write. The four
133
+ # extras are the ones the bump step writes directly: three from the
134
+ # bump itself, plus the brief's PDF, which is rendered from a surface
135
+ # rather than being one.
136
+ ALLOWED=$(node .github/scripts/sync-release-version.mjs --list-surfaces; printf 'package.json\npackage-lock.json\nCHANGELOG.md\ndocs/testguard-explained.pdf\n')
123
137
  echo "Release surfaces allowed in this diff:"; printf '%s\n' "$ALLOWED" | sed 's/^/ /'
124
138
  CHANGED=$(git diff --name-only HEAD --)
125
139
  for f in $CHANGED; do
@@ -134,7 +148,7 @@ jobs:
134
148
  git add -A
135
149
  git commit -m "release: v${NEW_VERSION} — automated weekly batch"
136
150
  git push origin "$BRANCH"
137
- printf 'Automated weekly release. Version bump + CHANGELOG entry + synced version surfaces. Merging tags and publishes to npm and PyPI via release.yml.\n\nA PR opened by GITHUB_TOKEN gets no `pull_request` run, so this workflow dispatched ci.yml on `%s` itself. Auto-merge is armed; it completes once the three Node legs pass. If the checks never appear: `gh workflow run ci.yml --ref %s`.\n' "$BRANCH" "$BRANCH" > /tmp/pr-body.md
151
+ printf 'Automated weekly release. Version bump + CHANGELOG entry + synced version surfaces. Merging tags and publishes to npm and PyPI via release.yml.\n\nTwo things a GITHUB_TOKEN PR needs done for it, both handled by this workflow: it raises no `pull_request` event, so ci.yml was dispatched on `%s` itself; and the `pull_request` runs it does create are held at `action_required` (github-actions[bot] is permanently a first-time contributor), so those were approved too. Auto-merge is armed and completes once the three Node legs pass.\n\nIf it sits anyway: `gh workflow run ci.yml --ref %s` for missing checks, or check for runs still awaiting approval on the head sha.\n' "$BRANCH" "$BRANCH" > /tmp/pr-body.md
138
152
  # A bare `gh pr create` failure here reads as a GraphQL error with no
139
153
  # hint of the cause, which is how v0.8.1 lost its release PR after
140
154
  # everything else in the release had succeeded.
@@ -155,6 +169,57 @@ jobs:
155
169
  else
156
170
  echo "✅ $HEAD_SHA already has $RUNS check run(s)"
157
171
  fi
172
+
173
+ # ── The third GITHUB_TOKEN wall, and the last one. ──
174
+ # The `pull_request` runs this PR creates are held at
175
+ # `action_required`: the repository requires approval from
176
+ # first-time contributors, and `github-actions[bot]` is
177
+ # PERMANENTLY one — it never authors a merged commit, so it can
178
+ # never graduate. Those held runs leave check suites QUEUED on the
179
+ # head sha forever, the status rollup never reaches a terminal
180
+ # state, and auto-merge waits on it even though the required
181
+ # contexts (test 20/22/24) are already green from the dispatched
182
+ # run above. v0.9.0 sat here until a human clicked Approve.
183
+ #
184
+ # GITHUB_TOKEN may clear it: `actions: write` carries the approve
185
+ # permission, measured 201 on a bot PR's own held run. It may not
186
+ # approve a REVIEW, which is a different gate and still ours alone.
187
+ #
188
+ # Runs appear asynchronously, so poll. Approving an already-running
189
+ # run answers 403 "not waiting for approval" — success, not failure.
190
+ echo "Clearing held workflow runs on $HEAD_SHA"
191
+ approved=0
192
+ for attempt in 1 2 3 4 5 6; do
193
+ HELD=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${HEAD_SHA}" \
194
+ --jq '[.workflow_runs[] | select(.status == "action_required" or .conclusion == "action_required") | .id] | .[]' 2>/dev/null || true)
195
+ for id in $HELD; do
196
+ if gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${id}/approve" >/dev/null 2>&1; then
197
+ echo " approved run $id"
198
+ approved=$((approved + 1))
199
+ else
200
+ echo " run $id needed no approval"
201
+ fi
202
+ done
203
+ # Stop as soon as the picture is settled, rather than always
204
+ # paying the full poll: nothing held after something was
205
+ # approved means we are done, and nothing held by the third
206
+ # look means the gate is not applying to this run at all.
207
+ if [ -z "$HELD" ] && { [ "$approved" -gt 0 ] || [ "$attempt" -ge 3 ]; }; then
208
+ break
209
+ fi
210
+ sleep 10
211
+ done
212
+
213
+ # Never leave this silent: a run still held here means auto-merge
214
+ # will wait forever, which is the failure this whole block exists
215
+ # to remove. Say so, and name what to click.
216
+ STILL=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${HEAD_SHA}" \
217
+ --jq '[.workflow_runs[] | select(.status == "action_required" or .conclusion == "action_required") | .html_url] | .[]' 2>/dev/null || true)
218
+ if [ -n "$STILL" ]; then
219
+ echo "::error::Workflow runs are still held for approval, so auto-merge on $PR will wait forever. Approve them, or re-run this workflow: $STILL"
220
+ exit 1
221
+ fi
222
+ echo "✅ no run is held for approval; auto-merge can complete"
158
223
  else
159
224
  cat /tmp/pr-err
160
225
  if grep -q 'not permitted to create or approve pull requests' /tmp/pr-err; then
@@ -7,6 +7,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.1] - 2026-09-19
11
+
12
+ Automated weekly release — everything merged since `v0.9.0`.
13
+
14
+ ### Changed
15
+
16
+ - docs: the brief catches up with the tool, and stops falling behind (#112)
17
+ - chore(homebrew): sha256 for v0.9.0 (#110)
18
+ - feat(cost): two ceilings, the report where it is read, and three claims off expensive defenders (#113)
19
+ - fix(release): approve the runs GITHUB_TOKEN's own PR leaves held (#111)
20
+
21
+
22
+ ### Added
23
+ - **A second cost ceiling, on the claim.** The total alone cannot see the
24
+ failure it was meant to catch: a new calibration claim cost 51 s for two
25
+ faults purely by being written into `replay.test.mjs` rather than
26
+ `calibration.test.mjs`, and re-pointing it took it to 4.9 s while the total
27
+ never moved. A per-fault average is worse — the regression that prompted
28
+ this budget raised the fault count 59 to 91 while per-fault cost barely
29
+ moved, so an average would have reported everything fine.
30
+
31
+ `perClaimSeconds` is 100 s, just above today's worst claim
32
+ (`TG-ADMIT-NEEDS-ALL-KILLED`, **87 s in CI** for one fault), so the rule
33
+ reads: no new claim may be worse than the worst we already have. Calibrated
34
+ against CI and not the local figure — 75 s, set from that claim's local
35
+ 66.5 s, tripped on the first CI run. This file already warns about exactly
36
+ that for the total (950 s was 18% over local but 4.6% over CI, and the first
37
+ PR adding a claim tripped it); the same error applies one level down. A breach names the claim
38
+ and its remedy — a cheaper defender, not a bigger budget. The field is
39
+ optional and additive: a budget without one behaves exactly as before.
40
+ - **The cost report goes to the CI job summary, not only the log.** The gate
41
+ went from 9.6 to 23.6 minutes across one merged pull request and nothing
42
+ said a word, because `--cost` only ever printed into a log nobody opens
43
+ while it is passing. The summary is read without opening anything, so the
44
+ number is in front of a reviewer while the change is still a change.
45
+
46
+ ### Performance
47
+ - **Three claims moved off expensive shared defenders: 65 s, ~30 s and ~30 s
48
+ down to under 2 s each.** All three faulted a *pure* function while naming
49
+ an integration test file, and a claim pays for every test in the file it
50
+ names. `TG-SURVIVOR-PROVES-THE-SUBJECT-RUNS` faults `classify()` but was
51
+ defended by `negative-control.test.mjs`, which spawns real runners against
52
+ fixture repositories; `TG-LABEL-NEVER-GUESSES` faults `labelDiff()` and
53
+ `TG-REPLAY-FLAKY-IS-NEVER-CAUGHT` faults `classifyReplay()`, both defended
54
+ by `replay.test.mjs`, which builds a scripted git corpus.
55
+
56
+ Two were a straight move of an already-isolated `describe` block into
57
+ `test/label.test.mjs` and `test/replay-verdict.test.mjs`; only the `classify`
58
+ case needed a test written. Each was proved with `admit` — passes on HEAD and
59
+ fails on every fault, 3/3 — *before* the expensive defender was dropped, and
60
+ never the other way round. The integration files keep their remaining claims,
61
+ where the fixture is the point.
62
+
63
+ ### Changed
64
+ - **The cost budget is 1230 s, from 1350.** 1350 was set defensively against a
65
+ reuse-affected 1123 s, because this budget is defined against a from-scratch
66
+ probe and none had run yet. `release.yml` then measured **1081 s** at 129
67
+ faults on the v0.9.0 release commit with no reuse — so the six field-report
68
+ claims cost +82 s, not the +120 s a per-fault average predicted. 1230 is
69
+ ~14% over 1081, the same headroom rule that set 1100 against 962. Leaving it
70
+ at 1350 would have let a 25% regression pass unnoticed.
71
+
72
+ ### Fixed
73
+ - **The release no longer waits for a human to click Approve.** v0.9.0 sat with
74
+ every required check green and merged nothing. The `pull_request` runs a
75
+ release PR creates are held at `action_required`, because the repository
76
+ requires approval from first-time contributors and `github-actions[bot]` is
77
+ permanently one — it never authors a merged commit, so it can never
78
+ graduate. Held runs leave check suites QUEUED on the head sha, the status
79
+ rollup never reaches a terminal state, and auto-merge waits on it even though
80
+ `test (20/22/24)` are already green from the dispatched run.
81
+
82
+ `scheduled-release.yml` now approves those runs itself. `GITHUB_TOKEN` is
83
+ permitted to do this — `actions: write` carries the approve permission,
84
+ measured 201 against a bot PR's own held run — and the workflow already held
85
+ that permission, so nothing was granted and no secret was added. It may still
86
+ not approve a *review*, which is a different gate and remains ours alone.
87
+ A run left held after the poll is an `::error::` naming the run, never a
88
+ silent wait.
89
+
90
+ This is the third `GITHUB_TOKEN` wall in the same chain, after "raises no
91
+ `pull_request` event" (#105) and "its push raises no `push` event" (the
92
+ hourly sweep in release.yml). All three have the same root: an action taken
93
+ by `GITHUB_TOKEN` does not trigger the event that would normally follow it.
94
+
95
+ ### Documentation
96
+ - **The technical brief describes the tool that exists.** It was last rendered
97
+ when it covered one verb, `probe`, and said nothing about `replay` and its
98
+ calibration — the empirical answer to whether an injected fault resembles a
99
+ real bug, which is the question a document arguing "evidence, not output"
100
+ can least afford to omit. A new page covers the eleven verbs and the
101
+ calibration, carrying the README's own framing: the greenfield transfer is
102
+ unproven, and `replay` is the instrument rather than the answer. The verdict
103
+ table, the cost formula and the prior-art section were checked against the
104
+ code and left alone.
105
+ - **The brief's PDF is rendered by the release instead of by memory.** Its
106
+ masthead read `v0.6` through three releases because nothing ever ran
107
+ `build-one-pager.mjs`. The HTML masthead is now a version surface and
108
+ `scheduled-release.yml` re-renders the PDF in the same step as the sync —
109
+ the same step, because an HTML and a PDF that disagree are worse than a
110
+ document uniformly out of date. `--require-chrome` makes a missing browser
111
+ fail the release rather than silently commit a stale PDF, and every render
112
+ must produce exactly one sheet per `.page` div: a section a few pixels too
113
+ tall spills onto a second sheet with no warning from Chrome, contradicting
114
+ the "Page N of M" footers the document prints about itself.
115
+
10
116
  ## [0.9.0] - 2026-09-18
11
117
 
12
118
  Automated weekly release — everything merged since `v0.8.1`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: testguard-cli
3
- Version: 0.9.0
3
+ Version: 0.9.1
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
@@ -104,8 +104,9 @@ risk. TestGuard binds every fault to a **stated claim**, so the output is not
104
104
  a score but a finding: *"Your project says a missing scope fails closed.
105
105
  Nothing checks that."* That is the difference between a metric and an audit.
106
106
 
107
- 📄 **[Read the three-page technical brief (PDF)](docs/testguard-explained.pdf)**
108
- — the idea on page one, mechanics and prior art after it.
107
+ 📄 **[Read the six-page technical brief (PDF)](docs/testguard-explained.pdf)**
108
+ — the idea on page one, then the field evidence, the anatomy of a run, the
109
+ verdicts, the loop and its calibration, and the prior art.
109
110
 
110
111
  ## Why
111
112
 
@@ -150,9 +151,9 @@ raises coverage. TestGuard admits it because it fails when the claim is false.
150
151
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
151
152
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
152
153
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
153
- | GitHub Action | `uses: raccioly/testguard@v0.9.0` — see [`action.yml`](./action.yml) |
154
+ | GitHub Action | `uses: raccioly/testguard@v0.9.1` — see [`action.yml`](./action.yml) |
154
155
  | 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.9.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
156
+ | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.9.1/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
156
157
 
157
158
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
158
159
  less than that many days ago (`ENOVERSIONS`); install that one with
@@ -482,12 +483,12 @@ detected base that does not resolve is a warning for `status` and `brief`
482
483
  # GitHub Actions
483
484
  - uses: actions/checkout@v4
484
485
  with: { fetch-depth: 0 }
485
- - uses: raccioly/testguard@v0.9.0
486
+ - uses: raccioly/testguard@v0.9.1
486
487
  with: { command: gate }
487
488
 
488
489
  # GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
489
490
  include:
490
- - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.9.0/packaging/gitlab/testguard.gitlab-ci.yml'
491
+ - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.9.1/packaging/gitlab/testguard.gitlab-ci.yml'
491
492
  inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
492
493
  ```
493
494
 
@@ -80,8 +80,9 @@ risk. TestGuard binds every fault to a **stated claim**, so the output is not
80
80
  a score but a finding: *"Your project says a missing scope fails closed.
81
81
  Nothing checks that."* That is the difference between a metric and an audit.
82
82
 
83
- 📄 **[Read the three-page technical brief (PDF)](docs/testguard-explained.pdf)**
84
- — the idea on page one, mechanics and prior art after it.
83
+ 📄 **[Read the six-page technical brief (PDF)](docs/testguard-explained.pdf)**
84
+ — the idea on page one, then the field evidence, the anatomy of a run, the
85
+ verdicts, the loop and its calibration, and the prior art.
85
86
 
86
87
  ## Why
87
88
 
@@ -126,9 +127,9 @@ raises coverage. TestGuard admits it because it fails when the claim is false.
126
127
  | npm | `npm i -D testguard-cli` then `npx testguard probe` |
127
128
  | pip | `pip install testguard-cli` then `testguard probe` (needs Node ≥ 20) |
128
129
  | Homebrew | `brew tap raccioly/tap && brew install testguard` |
129
- | GitHub Action | `uses: raccioly/testguard@v0.9.0` — see [`action.yml`](./action.yml) |
130
+ | GitHub Action | `uses: raccioly/testguard@v0.9.1` — see [`action.yml`](./action.yml) |
130
131
  | 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.9.0/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
132
+ | GitLab CI | `include: - remote: https://raw.githubusercontent.com/raccioly/testguard/v0.9.1/packaging/gitlab/testguard.gitlab-ci.yml` with `inputs:` — see [`packaging/gitlab/`](./packaging/gitlab/testguard.gitlab-ci.yml) |
132
133
 
133
134
  Projects that set `min-release-age` in `.npmrc` cannot see a version published
134
135
  less than that many days ago (`ENOVERSIONS`); install that one with
@@ -458,12 +459,12 @@ detected base that does not resolve is a warning for `status` and `brief`
458
459
  # GitHub Actions
459
460
  - uses: actions/checkout@v4
460
461
  with: { fetch-depth: 0 }
461
- - uses: raccioly/testguard@v0.9.0
462
+ - uses: raccioly/testguard@v0.9.1
462
463
  with: { command: gate }
463
464
 
464
465
  # GitLab CI — the component-shaped template: gate + probe, brief as an artifact and, opted in, as a merge-request note
465
466
  include:
466
- - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.9.0/packaging/gitlab/testguard.gitlab-ci.yml'
467
+ - remote: 'https://raw.githubusercontent.com/raccioly/testguard/v0.9.1/packaging/gitlab/testguard.gitlab-ci.yml'
467
468
  inputs: { dir: backend, post_note: true } # post_note needs TESTGUARD_GITLAB_TOKEN (api scope); one note, updated in place
468
469
  ```
469
470
 
@@ -46,7 +46,7 @@ inputs:
46
46
  version:
47
47
  description: 'testguard-cli version to run'
48
48
  required: false
49
- default: '0.9.0'
49
+ default: '0.9.1'
50
50
  node-version:
51
51
  description: 'Node.js version (20+)'
52
52
  required: false