testguard-cli 0.1.0

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.
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:guard-spec:v1:calibration",
4
+ "title": "Guard spec v1 — calibration",
5
+ "description": "P(finding is real) per bucket, with a confidence interval and the sample size behind it. A label without n is a vibe; this is the format that stops it being one.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "tool", "computedAt", "method", "confidence", "bucketBy", "source", "buckets"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "tool": { "$ref": "urn:guard-spec:v1:common#/$defs/tool" },
12
+ "computedAt": { "$ref": "urn:guard-spec:v1:common#/$defs/isoDateTime" },
13
+ "method": {
14
+ "description": "Interval method. `wilson` is the only method defined in v1.",
15
+ "enum": ["wilson"]
16
+ },
17
+ "confidence": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1 },
18
+ "bucketBy": {
19
+ "description": "The field findings are bucketed on, e.g. `faultClass` or `severity`.",
20
+ "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
21
+ },
22
+ "source": {
23
+ "description": "Where the ground-truth labels came from.",
24
+ "type": "object",
25
+ "required": ["kind"],
26
+ "properties": {
27
+ "kind": { "enum": ["bug-replay", "human-label", "mixed"] },
28
+ "ref": { "type": "string", "maxLength": 512 }
29
+ },
30
+ "additionalProperties": false
31
+ },
32
+ "buckets": {
33
+ "type": "object",
34
+ "propertyNames": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
35
+ "additionalProperties": {
36
+ "type": "object",
37
+ "required": ["n", "positives", "p", "ci"],
38
+ "properties": {
39
+ "n": { "type": "integer", "minimum": 0 },
40
+ "positives": { "type": "integer", "minimum": 0 },
41
+ "p": { "type": "number", "minimum": 0, "maximum": 1 },
42
+ "ci": {
43
+ "type": "array",
44
+ "minItems": 2,
45
+ "maxItems": 2,
46
+ "items": { "type": "number", "minimum": 0, "maximum": 1 }
47
+ }
48
+ },
49
+ "additionalProperties": false
50
+ }
51
+ }
52
+ },
53
+ "additionalProperties": false
54
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:guard-spec:v1:claims",
4
+ "title": "Guard spec v1 — claims file",
5
+ "description": "What must be true, and how to try to falsify it. A claim is a statement the project makes; each fault is one concrete way that statement could be false, expressed as a deterministic source change. This file is code: its `replace` strings execute under the test runner. Treat it with the same review discipline as source.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "claims"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "claims": {
12
+ "type": "array",
13
+ "items": { "$ref": "#/$defs/claim" }
14
+ }
15
+ },
16
+ "additionalProperties": false,
17
+ "$defs": {
18
+ "claim": {
19
+ "type": "object",
20
+ "required": ["id", "statement", "source", "severity", "producedBy", "faults"],
21
+ "properties": {
22
+ "id": {
23
+ "description": "Stable identifier. Renaming it changes every fingerprint derived from it.",
24
+ "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
25
+ },
26
+ "statement": {
27
+ "description": "The claim in plain language, as the project states it.",
28
+ "type": "string",
29
+ "minLength": 1,
30
+ "maxLength": 2000
31
+ },
32
+ "source": {
33
+ "description": "Where the claim comes from. Spec- and ADR-sourced claims are human-governed; annotation- and comment-sourced claims may have been written by the same agent that wrote the tests, and should be ranked accordingly.",
34
+ "type": "object",
35
+ "required": ["kind"],
36
+ "properties": {
37
+ "kind": { "enum": ["spec", "adr", "annotation", "comment", "manual"] },
38
+ "ref": { "type": "string", "maxLength": 512 }
39
+ },
40
+ "additionalProperties": false
41
+ },
42
+ "severity": { "$ref": "urn:guard-spec:v1:common#/$defs/severity" },
43
+ "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" },
44
+ "defendedBy": {
45
+ "description": "Globs (repo-relative) for the test files that supposedly defend this claim. Empty or absent means: let the runner discover related tests. If the globs resolve to zero files the verdict is `nocover`.",
46
+ "type": "array",
47
+ "items": { "type": "string", "minLength": 1 },
48
+ "uniqueItems": true
49
+ },
50
+ "faults": {
51
+ "type": "array",
52
+ "minItems": 1,
53
+ "items": { "$ref": "#/$defs/fault" }
54
+ },
55
+ "tags": {
56
+ "type": "array",
57
+ "items": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
58
+ "uniqueItems": true
59
+ }
60
+ },
61
+ "additionalProperties": false
62
+ },
63
+ "fault": {
64
+ "description": "One concrete way the claim could be false. Applied as an exact-string substitution; the schema is producer-agnostic — a human, an operator, an agent, or a derivation may all emit this shape.",
65
+ "type": "object",
66
+ "required": ["id", "description", "faultClass", "file", "find", "replace", "producedBy"],
67
+ "properties": {
68
+ "id": {
69
+ "description": "Unique within the claim.",
70
+ "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
71
+ },
72
+ "description": { "type": "string", "minLength": 1, "maxLength": 2000 },
73
+ "faultClass": { "$ref": "urn:guard-spec:v1:common#/$defs/faultClass" },
74
+ "file": { "$ref": "urn:guard-spec:v1:common#/$defs/repoPath" },
75
+ "find": {
76
+ "description": "Exact text to locate in `file`. Must occur exactly `expectHits` times or the fault is `unverifiable`.",
77
+ "type": "string",
78
+ "minLength": 1,
79
+ "maxLength": 20000
80
+ },
81
+ "replace": {
82
+ "description": "Exact replacement text. May be empty.",
83
+ "type": "string",
84
+ "maxLength": 20000
85
+ },
86
+ "occurrence": {
87
+ "description": "Which occurrence of `find` to replace (1-based).",
88
+ "type": "integer",
89
+ "minimum": 1,
90
+ "default": 1
91
+ },
92
+ "expectHits": {
93
+ "description": "How many times `find` must occur in `file`. Any other count means the anchor has rotted or is ambiguous → `unverifiable`, never a silent skip.",
94
+ "type": "integer",
95
+ "minimum": 1,
96
+ "default": 1
97
+ },
98
+ "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" }
99
+ },
100
+ "additionalProperties": false
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,94 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:guard-spec:v1:common",
4
+ "title": "Guard spec v1 — shared definitions",
5
+ "description": "Definitions shared by every Guard-spec format. Referenced as urn:guard-spec:v1:common#/$defs/<name>.",
6
+ "$defs": {
7
+ "sha256": {
8
+ "type": "string",
9
+ "pattern": "^[a-f0-9]{64}$"
10
+ },
11
+ "gitSha": {
12
+ "type": "string",
13
+ "pattern": "^[a-f0-9]{7,40}$"
14
+ },
15
+ "isoDateTime": {
16
+ "type": "string",
17
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"
18
+ },
19
+ "identifier": {
20
+ "type": "string",
21
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$",
22
+ "maxLength": 128
23
+ },
24
+ "repoPath": {
25
+ "description": "Path relative to the repository root. Absolute paths and parent traversal are rejected.",
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "pattern": "^(?![/\\\\])(?!\\.\\.(?:[/\\\\]|$))(?!.*[/\\\\]\\.\\.(?:[/\\\\]|$)).*$"
29
+ },
30
+ "severity": {
31
+ "enum": ["critical", "high", "medium", "low"]
32
+ },
33
+ "verdict": {
34
+ "description": "Closed set. Only `killed` is a pass. Every other verdict gates by default (see GATE-SEMANTICS.md).",
35
+ "enum": [
36
+ "killed",
37
+ "survived",
38
+ "nocover",
39
+ "unverifiable",
40
+ "timeout",
41
+ "fault-invalid",
42
+ "flaky-defender"
43
+ ]
44
+ },
45
+ "faultClass": {
46
+ "description": "Taxonomy of the injected change. Required on every fault so calibration (fault class → real-bug predictiveness) can be computed later.",
47
+ "enum": [
48
+ "guard-removed",
49
+ "condition-forced",
50
+ "statement-deleted",
51
+ "variable-swap",
52
+ "literal-changed",
53
+ "call-removed",
54
+ "return-altered",
55
+ "exception-swallowed",
56
+ "other"
57
+ ]
58
+ },
59
+ "provenance": {
60
+ "description": "Who or what produced an artifact. `human` = authored by a person; `agent` = proposed by an LLM agent; `operator` = emitted by a mechanical mutation operator; `derived` = computed from another artifact (e.g. a bug log).",
61
+ "type": "object",
62
+ "required": ["producer"],
63
+ "properties": {
64
+ "producer": { "enum": ["human", "agent", "operator", "derived"] },
65
+ "by": { "type": "string", "maxLength": 256 },
66
+ "reviewedBy": { "type": "string", "maxLength": 256 },
67
+ "at": { "$ref": "#/$defs/isoDateTime" }
68
+ },
69
+ "additionalProperties": false
70
+ },
71
+ "tool": {
72
+ "type": "object",
73
+ "required": ["name", "version"],
74
+ "properties": {
75
+ "name": { "$ref": "#/$defs/identifier" },
76
+ "version": { "type": "string", "minLength": 1, "maxLength": 64 }
77
+ },
78
+ "additionalProperties": false
79
+ },
80
+ "annotation": {
81
+ "description": "Strictly additive. An annotation never changes, suppresses, or drops a finding; it only attaches context to it.",
82
+ "type": "object",
83
+ "required": ["kind", "text"],
84
+ "properties": {
85
+ "kind": { "enum": ["note", "accepted-risk", "false-positive", "tracked", "wontfix"] },
86
+ "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
87
+ "by": { "type": "string", "maxLength": 256 },
88
+ "at": { "$ref": "#/$defs/isoDateTime" },
89
+ "ref": { "type": "string", "maxLength": 512 }
90
+ },
91
+ "additionalProperties": false
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,206 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:guard-spec:v1:evidence",
4
+ "title": "Guard spec v1 — evidence file",
5
+ "description": "One run's findings, on disk, in a shape an auditor can read without the tool. Each record binds a claim to a subject (for TestGuard, an injected fault) and a verdict, with every run that produced the verdict.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "tool", "run", "records"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "tool": { "$ref": "urn:guard-spec:v1:common#/$defs/tool" },
12
+ "run": {
13
+ "type": "object",
14
+ "required": ["id", "startedAt", "repo", "confirmRuns", "mode"],
15
+ "properties": {
16
+ "id": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
17
+ "startedAt": { "$ref": "urn:guard-spec:v1:common#/$defs/isoDateTime" },
18
+ "finishedAt": { "$ref": "urn:guard-spec:v1:common#/$defs/isoDateTime" },
19
+ "repo": {
20
+ "type": "object",
21
+ "required": ["head", "dirty"],
22
+ "properties": {
23
+ "head": { "$ref": "urn:guard-spec:v1:common#/$defs/gitSha" },
24
+ "dirty": { "type": "boolean" }
25
+ },
26
+ "additionalProperties": false
27
+ },
28
+ "runner": {
29
+ "type": "object",
30
+ "required": ["name"],
31
+ "properties": {
32
+ "name": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
33
+ "version": { "type": "string", "maxLength": 64 }
34
+ },
35
+ "additionalProperties": false
36
+ },
37
+ "confirmRuns": {
38
+ "description": "N — how many times the defenders were run both unmodified (baseline) and with each fault applied.",
39
+ "type": "integer",
40
+ "minimum": 1
41
+ },
42
+ "mode": {
43
+ "description": "`worktree` = faults were applied in a scratch git worktree (default). `in-place` = the user's working tree was mutated and restored.",
44
+ "enum": ["worktree", "in-place"]
45
+ }
46
+ },
47
+ "additionalProperties": false
48
+ },
49
+ "records": {
50
+ "type": "array",
51
+ "items": { "$ref": "#/$defs/record" }
52
+ }
53
+ },
54
+ "additionalProperties": false,
55
+ "$defs": {
56
+ "testRun": {
57
+ "type": "object",
58
+ "required": ["outcome", "durationMs"],
59
+ "properties": {
60
+ "outcome": {
61
+ "description": "`fail` means at least one test failed with the suite otherwise loading and running. `error` means the suite could not load or run (import/syntax error). `timeout` means the runner exceeded its budget.",
62
+ "enum": ["pass", "fail", "timeout", "error"]
63
+ },
64
+ "tests": {
65
+ "type": "object",
66
+ "required": ["total", "passed", "failed"],
67
+ "properties": {
68
+ "total": { "type": "integer", "minimum": 0 },
69
+ "passed": { "type": "integer", "minimum": 0 },
70
+ "failed": { "type": "integer", "minimum": 0 }
71
+ },
72
+ "additionalProperties": false
73
+ },
74
+ "assertionFailures": {
75
+ "description": "Test-level failures that are neither timeouts nor load failures: the test body ran and rejected the behaviour, whether by an assertion or by an exception the fault caused. Only these count toward `killed`. A timeout or a suite that fails to load is not evidence that the suite defends the claim.",
76
+ "type": "integer",
77
+ "minimum": 0
78
+ },
79
+ "durationMs": { "type": "integer", "minimum": 0 }
80
+ },
81
+ "additionalProperties": false
82
+ },
83
+ "record": {
84
+ "type": "object",
85
+ "required": ["fingerprint", "claim", "subject", "verdict", "detail", "defenders", "inputs"],
86
+ "properties": {
87
+ "fingerprint": {
88
+ "description": "sha256 of `<claim.id>\\n<subject.id>\\n<subject.file>\\n<verdict>`. Derived from identities and outcome, never from `find`/`replace`, so repairing a rotted anchor does not churn the baseline.",
89
+ "$ref": "urn:guard-spec:v1:common#/$defs/sha256"
90
+ },
91
+ "claim": {
92
+ "type": "object",
93
+ "required": ["id", "statement", "severity", "source"],
94
+ "properties": {
95
+ "id": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
96
+ "statement": { "type": "string", "minLength": 1, "maxLength": 2000 },
97
+ "severity": { "$ref": "urn:guard-spec:v1:common#/$defs/severity" },
98
+ "source": {
99
+ "type": "object",
100
+ "required": ["kind"],
101
+ "properties": {
102
+ "kind": { "enum": ["spec", "adr", "annotation", "comment", "manual"] },
103
+ "ref": { "type": "string", "maxLength": 512 }
104
+ },
105
+ "additionalProperties": false
106
+ },
107
+ "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" }
108
+ },
109
+ "additionalProperties": false
110
+ },
111
+ "subject": {
112
+ "description": "What was tried against the claim. For TestGuard `kind` is `fault`. Other adopters use their own kinds (e.g. `doc-statement`, `finding`); extra properties are permitted here and nowhere else.",
113
+ "type": "object",
114
+ "required": ["kind", "id"],
115
+ "properties": {
116
+ "kind": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
117
+ "id": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
118
+ "description": { "type": "string", "maxLength": 2000 },
119
+ "file": { "$ref": "urn:guard-spec:v1:common#/$defs/repoPath" },
120
+ "faultClass": { "$ref": "urn:guard-spec:v1:common#/$defs/faultClass" },
121
+ "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" }
122
+ },
123
+ "additionalProperties": true
124
+ },
125
+ "verdict": { "$ref": "urn:guard-spec:v1:common#/$defs/verdict" },
126
+ "detail": {
127
+ "type": "object",
128
+ "required": ["baselineRuns", "probeRuns"],
129
+ "properties": {
130
+ "reason": {
131
+ "description": "Machine-readable qualifier, e.g. `anchor-missing`, `anchor-ambiguous`, `defenders-not-green`, `replacement-does-not-compile`.",
132
+ "type": "string",
133
+ "maxLength": 256
134
+ },
135
+ "baselineRuns": {
136
+ "description": "Defenders run on unmodified source, N times. Anything short of N/N pass is `flaky-defender`.",
137
+ "type": "array",
138
+ "items": { "$ref": "#/$defs/testRun" }
139
+ },
140
+ "probeRuns": {
141
+ "description": "Defenders run with the fault applied, N times.",
142
+ "type": "array",
143
+ "items": { "$ref": "#/$defs/testRun" }
144
+ },
145
+ "escalated": {
146
+ "description": "True when the fault survived its declared defenders and a broader test set was then run.",
147
+ "type": "boolean"
148
+ },
149
+ "escalationRuns": {
150
+ "type": "array",
151
+ "items": { "$ref": "#/$defs/testRun" }
152
+ }
153
+ },
154
+ "additionalProperties": false
155
+ },
156
+ "defenders": {
157
+ "type": "object",
158
+ "required": ["requested", "resolved", "nocover"],
159
+ "properties": {
160
+ "requested": { "type": "array", "items": { "type": "string" } },
161
+ "resolved": { "type": "array", "items": { "$ref": "urn:guard-spec:v1:common#/$defs/repoPath" } },
162
+ "nocover": { "type": "boolean" }
163
+ },
164
+ "additionalProperties": false
165
+ },
166
+ "inputs": {
167
+ "description": "Content hashes of everything the verdict depends on, so an unchanged claim can reuse its prior verdict.",
168
+ "type": "object",
169
+ "required": ["targetHash", "defenderHashes"],
170
+ "properties": {
171
+ "targetHash": { "$ref": "urn:guard-spec:v1:common#/$defs/sha256" },
172
+ "defenderHashes": {
173
+ "type": "object",
174
+ "additionalProperties": { "$ref": "urn:guard-spec:v1:common#/$defs/sha256" }
175
+ }
176
+ },
177
+ "additionalProperties": false
178
+ },
179
+ "reusedFrom": {
180
+ "description": "Run id whose verdict was reused because `inputs` were unchanged.",
181
+ "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
182
+ },
183
+ "rank": {
184
+ "description": "Additive ordering signal. Never alters `verdict`.",
185
+ "type": "object",
186
+ "required": ["score"],
187
+ "properties": {
188
+ "score": { "type": "number" },
189
+ "blastRadius": {
190
+ "description": "Number of source files that import the subject's file, transitively or directly as the tool documents.",
191
+ "type": "integer",
192
+ "minimum": 0
193
+ },
194
+ "tier": { "type": "string", "maxLength": 64 }
195
+ },
196
+ "additionalProperties": false
197
+ },
198
+ "annotations": {
199
+ "type": "array",
200
+ "items": { "$ref": "urn:guard-spec:v1:common#/$defs/annotation" }
201
+ }
202
+ },
203
+ "additionalProperties": false
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:guard-spec:v1:ignore",
4
+ "title": "Guard spec v1 — structured ignore",
5
+ "description": "Reviewable scoping. Every entry carries a reason. Tools may also accept a plain gitignore-syntax text file as shorthand for path entries without reasons; this structured form is what an auditor reads.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "entries"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "schemaVersion": { "const": 1 },
11
+ "entries": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "required": ["kind", "pattern", "reason"],
16
+ "properties": {
17
+ "kind": {
18
+ "description": "`path` = glob over repo paths; `claim` = claim id; `fault` = `<claimId>/<faultId>`; `fingerprint` = an evidence fingerprint.",
19
+ "enum": ["path", "claim", "fault", "fingerprint"]
20
+ },
21
+ "pattern": { "type": "string", "minLength": 1, "maxLength": 512 },
22
+ "reason": { "type": "string", "minLength": 8, "maxLength": 2000 },
23
+ "by": { "type": "string", "maxLength": 256 },
24
+ "at": { "$ref": "urn:guard-spec:v1:common#/$defs/isoDateTime" },
25
+ "expires": {
26
+ "description": "After this instant the entry no longer applies. Tools warn on expired entries.",
27
+ "$ref": "urn:guard-spec:v1:common#/$defs/isoDateTime"
28
+ }
29
+ },
30
+ "additionalProperties": false
31
+ }
32
+ }
33
+ },
34
+ "additionalProperties": false
35
+ }
@@ -0,0 +1,34 @@
1
+ const SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
2
+
3
+ /** Freeze every non-passing finding. `killed` is never debt, so it is never fingerprinted. */
4
+ export function buildBaseline(evidence, { createdAt = new Date().toISOString() } = {}) {
5
+ const fingerprints = {};
6
+ for (const r of evidence.records) {
7
+ if (r.verdict === 'killed') continue;
8
+ fingerprints[r.fingerprint] = (fingerprints[r.fingerprint] ?? 0) + 1;
9
+ }
10
+ return { schemaVersion: 1, tool: evidence.tool, createdAt, head: evidence.run.repo.head, fingerprints };
11
+ }
12
+
13
+ /**
14
+ * Split records into what gates and what does not. Baseline suppression is
15
+ * up-to-count per fingerprint; the severity floor only decides whether a
16
+ * new finding turns CI red, never whether it is reported.
17
+ */
18
+ export function gate(records, baseline, { severityFloor = 'low' } = {}) {
19
+ const remaining = { ...(baseline?.fingerprints ?? {}) };
20
+ const out = { new: [], baselined: [], belowFloor: [], killed: [] };
21
+ for (const r of records) {
22
+ if (r.verdict === 'killed') {
23
+ out.killed.push(r);
24
+ } else if ((remaining[r.fingerprint] ?? 0) > 0) {
25
+ remaining[r.fingerprint]--;
26
+ out.baselined.push(r);
27
+ } else if (SEVERITY_RANK[r.claim.severity] < SEVERITY_RANK[severityFloor]) {
28
+ out.belowFloor.push(r);
29
+ } else {
30
+ out.new.push(r);
31
+ }
32
+ }
33
+ return out;
34
+ }
@@ -0,0 +1,87 @@
1
+ import { gate } from '../baseline/baseline.mjs';
2
+ import { summarize, formatVerdict } from '../render.mjs';
3
+
4
+ export const HEADING = '## TEST BLINDSPOT CONTEXT';
5
+ const ORDER = ['survived', 'nocover', 'unverifiable', 'fault-invalid', 'timeout', 'flaky-defender'];
6
+
7
+ /** One line the agent can act on. Names the mechanism, never just the verdict. */
8
+ export function hintFor(r) {
9
+ const defenders = r.defenders.resolved.join(', ');
10
+ switch (r.verdict) {
11
+ case 'survived':
12
+ return r.detail.reason === 'killed-by-undeclared-tests'
13
+ ? `Only tests outside its declared defenders (${defenders}) catch this; fix the claim's defendedBy or move the assertion.`
14
+ : `${defenders} stayed green with this fault applied; add an assertion that fails on it and passes on HEAD.`;
15
+ case 'nocover':
16
+ return `No test file matches ${r.defenders.requested.join(', ') || '(no defenders declared)'}; nothing defends this claim.`;
17
+ case 'unverifiable':
18
+ return `Anchor ${r.detail.reason}; re-author fault ${r.subject.id} in the claims file before trusting this claim.`;
19
+ case 'fault-invalid':
20
+ return `Replacement does not load (${r.detail.reason}); fix the fault definition, not the code.`;
21
+ case 'timeout':
22
+ return 'Defenders time out with this fault applied; a hang is not a detection.';
23
+ case 'flaky-defender':
24
+ return `${defenders} not reliably green (${r.detail.reason}); fix the flake before trusting any verdict here.`;
25
+ default:
26
+ return '';
27
+ }
28
+ }
29
+
30
+ function orderItems(a, b) {
31
+ return (b.isNew - a.isNew) || (ORDER.indexOf(a.verdict) - ORDER.indexOf(b.verdict)) || ((b.rank ?? 0) - (a.rank ?? 0));
32
+ }
33
+
34
+ export function buildBrief(evidence, baseline, { max = 20, generatedAt = new Date().toISOString() } = {}) {
35
+ const g = gate(evidence.records, baseline);
36
+ const toItem = (r, isNew) => ({
37
+ fingerprint: r.fingerprint,
38
+ claimId: r.claim.id,
39
+ subjectId: r.subject.id,
40
+ statement: r.claim.statement,
41
+ verdict: r.verdict,
42
+ severity: r.claim.severity,
43
+ ...(r.subject.file ? { file: r.subject.file } : {}),
44
+ ...(r.rank ? { rank: r.rank.score } : {}),
45
+ hint: hintFor(r),
46
+ isNew,
47
+ });
48
+ const items = [...g.new.map((r) => toItem(r, true)), ...g.belowFloor.map((r) => toItem(r, true)), ...g.baselined.map((r) => toItem(r, false))]
49
+ .sort(orderItems)
50
+ .slice(0, max);
51
+ const summary = {
52
+ claims: new Set(evidence.records.map((r) => r.claim.id)).size,
53
+ byVerdict: summarize(evidence.records),
54
+ new: g.new.length + g.belowFloor.length,
55
+ baselined: g.baselined.length,
56
+ };
57
+ const doc = { schemaVersion: 1, tool: evidence.tool, generatedAt, head: evidence.run.repo.head, heading: HEADING, summary, items, text: '' };
58
+ doc.text = renderBriefText(doc, { hasBaseline: Boolean(baseline), total: evidence.records.length });
59
+ return doc;
60
+ }
61
+
62
+ export function renderBriefText(brief, { hasBaseline, total }) {
63
+ const unproven = total - (brief.summary.byVerdict.killed ?? 0);
64
+ const lines = [
65
+ brief.heading,
66
+ '',
67
+ `testguard ${brief.tool.version}${brief.head ? ` @ ${brief.head.slice(0, 12)}` : ''} — ${brief.summary.claims} claims, ${total} faults probed, ${unproven} unproven` +
68
+ (hasBaseline ? ` (${brief.summary.new} new since baseline).` : ' (no baseline; everything is new).'),
69
+ ];
70
+ if (brief.items.length === 0) {
71
+ lines.push('', 'Every probed claim is defended. Keep it that way: new claims need a fault and a test that fails on it.');
72
+ return lines.join('\n') + '\n';
73
+ }
74
+ lines.push(
75
+ '',
76
+ 'Where the test suite is blind, ranked. A SURVIVED fault means its defenders stayed green while the claim was false.',
77
+ 'Do not close these by asserting current behaviour; write a test that fails on the described fault and passes on HEAD.',
78
+ '',
79
+ );
80
+ brief.items.forEach((it, i) => {
81
+ lines.push(`${i + 1}. ${it.isNew ? '[NEW] ' : ''}${formatVerdict(it.verdict)} ${it.claimId}/${it.subjectId ?? '?'} (${it.severity})${it.file ? ` ${it.file}` : ''}`);
82
+ lines.push(` claim: ${it.statement}`);
83
+ if (it.hint) lines.push(` ${it.hint}`);
84
+ });
85
+ if (unproven > brief.items.length) lines.push('', `… and ${unproven - brief.items.length} more in the evidence file.`);
86
+ return lines.join('\n') + '\n';
87
+ }