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