testguard-cli 0.5.0 → 0.7.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +628 -0
  2. package/README.md +536 -42
  3. package/package.json +2 -2
  4. package/spec/GATE-SEMANTICS.md +166 -2
  5. package/spec/README.md +29 -10
  6. package/spec/lib/validate.mjs +140 -8
  7. package/spec/schemas/baseline.schema.json +13 -5
  8. package/spec/schemas/brief.schema.json +12 -12
  9. package/spec/schemas/calibration.schema.json +5 -5
  10. package/spec/schemas/claims.schema.json +122 -25
  11. package/spec/schemas/common.schema.json +131 -29
  12. package/spec/schemas/evidence.schema.json +476 -69
  13. package/spec/schemas/gate.schema.json +12 -12
  14. package/spec/schemas/ignore.schema.json +4 -4
  15. package/spec/schemas/replay.schema.json +85 -0
  16. package/spec/schemas/status.schema.json +258 -56
  17. package/src/admit/admit.mjs +17 -0
  18. package/src/baseline/baseline.mjs +31 -2
  19. package/src/brief/brief.mjs +71 -7
  20. package/src/claims/removed.mjs +161 -0
  21. package/src/cli.mjs +68 -10
  22. package/src/commands/admit.mjs +116 -0
  23. package/src/commands/baseline.mjs +17 -3
  24. package/src/commands/brief.mjs +13 -7
  25. package/src/commands/claims.mjs +55 -6
  26. package/src/commands/init.mjs +19 -6
  27. package/src/commands/mcp.mjs +12 -0
  28. package/src/commands/probe.mjs +35 -7
  29. package/src/commands/replay.mjs +63 -0
  30. package/src/commands/status.mjs +3 -1
  31. package/src/gate/changed.mjs +9 -2
  32. package/src/git.mjs +5 -0
  33. package/src/init/init.mjs +182 -29
  34. package/src/init/templates/SKILL.md +3 -3
  35. package/src/mcp/server.mjs +104 -0
  36. package/src/mcp/tools.mjs +114 -0
  37. package/src/probe/attribution.mjs +123 -0
  38. package/src/probe/classify.mjs +18 -2
  39. package/src/probe/contention.mjs +60 -0
  40. package/src/probe/cost.mjs +145 -0
  41. package/src/probe/discover.mjs +20 -3
  42. package/src/probe/independence.mjs +59 -0
  43. package/src/probe/inject.mjs +41 -9
  44. package/src/probe/mocks.mjs +122 -0
  45. package/src/probe/probe.mjs +331 -40
  46. package/src/probe/progress.mjs +87 -0
  47. package/src/probe/pyimports.mjs +287 -0
  48. package/src/probe/rank.mjs +75 -15
  49. package/src/probe/runners/index.mjs +79 -8
  50. package/src/probe/runners/jest.mjs +15 -3
  51. package/src/probe/runners/playwright.mjs +142 -0
  52. package/src/probe/runners/python/_testguard_pytest_plugin.py +132 -0
  53. package/src/probe/runners/python/_testguard_report.py +141 -0
  54. package/src/probe/runners/python/_testguard_unittest_main.py +168 -0
  55. package/src/probe/runners/python.mjs +316 -0
  56. package/src/probe/runners/shared.mjs +80 -14
  57. package/src/probe/runners/vitest.mjs +16 -3
  58. package/src/render.mjs +2 -2
  59. package/src/replay/label.mjs +64 -0
  60. package/src/replay/replay.mjs +310 -0
  61. package/src/scaffold/producers.mjs +217 -6
  62. package/src/scaffold/producers.python.mjs +209 -0
  63. package/src/scaffold/scaffold.mjs +90 -30
  64. package/src/status/status.mjs +47 -6
  65. package/src/util/glob.mjs +7 -1
@@ -1,27 +1,43 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:guard-spec:v1:claims",
4
- "title": "Guard spec v1 — claims file",
3
+ "$id": "urn:claimspec:v1:claims",
4
+ "title": "claimspec v1 \u2014 claims file",
5
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
6
  "type": "object",
7
- "required": ["schemaVersion", "claims"],
7
+ "required": [
8
+ "schemaVersion",
9
+ "claims"
10
+ ],
8
11
  "properties": {
9
- "$schema": { "type": "string" },
10
- "schemaVersion": { "const": 1 },
12
+ "$schema": {
13
+ "type": "string"
14
+ },
15
+ "schemaVersion": {
16
+ "const": 1
17
+ },
11
18
  "claims": {
12
19
  "type": "array",
13
- "items": { "$ref": "#/$defs/claim" }
20
+ "items": {
21
+ "$ref": "#/$defs/claim"
22
+ }
14
23
  }
15
24
  },
16
25
  "additionalProperties": false,
17
26
  "$defs": {
18
27
  "claim": {
19
28
  "type": "object",
20
- "required": ["id", "statement", "source", "severity", "producedBy", "faults"],
29
+ "required": [
30
+ "id",
31
+ "statement",
32
+ "source",
33
+ "severity",
34
+ "producedBy",
35
+ "faults"
36
+ ],
21
37
  "properties": {
22
38
  "id": {
23
39
  "description": "Stable identifier. Renaming it changes every fingerprint derived from it.",
24
- "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
40
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
25
41
  },
26
42
  "statement": {
27
43
  "description": "The claim in plain language, as the project states it.",
@@ -32,46 +48,82 @@
32
48
  "source": {
33
49
  "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
50
  "type": "object",
35
- "required": ["kind"],
51
+ "required": [
52
+ "kind"
53
+ ],
36
54
  "properties": {
37
- "kind": { "enum": ["spec", "adr", "annotation", "comment", "manual"] },
38
- "ref": { "type": "string", "maxLength": 512 }
55
+ "kind": {
56
+ "enum": [
57
+ "spec",
58
+ "adr",
59
+ "annotation",
60
+ "comment",
61
+ "manual"
62
+ ]
63
+ },
64
+ "ref": {
65
+ "type": "string",
66
+ "maxLength": 512
67
+ }
39
68
  },
40
69
  "additionalProperties": false
41
70
  },
42
- "severity": { "$ref": "urn:guard-spec:v1:common#/$defs/severity" },
43
- "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" },
71
+ "severity": {
72
+ "$ref": "urn:claimspec:v1:common#/$defs/severity"
73
+ },
74
+ "producedBy": {
75
+ "$ref": "urn:claimspec:v1:common#/$defs/provenance"
76
+ },
44
77
  "defendedBy": {
45
78
  "description": "Globs (project-relative) for the test files that supposedly defend this claim. Empty or absent means: discover defenders as the test files that import each fault's target file (relative paths and resolved aliases). If nothing resolves, the verdict is `nocover`.",
46
79
  "type": "array",
47
- "items": { "type": "string", "minLength": 1 },
80
+ "items": {
81
+ "type": "string",
82
+ "minLength": 1
83
+ },
48
84
  "uniqueItems": true
49
85
  },
50
86
  "faults": {
51
87
  "type": "array",
52
88
  "minItems": 1,
53
- "items": { "$ref": "#/$defs/fault" }
89
+ "items": {
90
+ "$ref": "#/$defs/fault"
91
+ }
54
92
  },
55
93
  "tags": {
56
94
  "type": "array",
57
- "items": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
95
+ "items": {
96
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
97
+ },
58
98
  "uniqueItems": true
59
99
  }
60
100
  },
61
101
  "additionalProperties": false
62
102
  },
63
103
  "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.",
104
+ "description": "One concrete way the claim could be false, and how a tool would try to make it so. `method` says which; it defaults to `fault-injection`, whose shape is an exact-string substitution and is unchanged. Producer-agnostic: a human, an operator, an agent or a derivation may all emit this shape, and every artifact says which.",
65
105
  "type": "object",
66
- "required": ["id", "description", "faultClass", "file", "find", "replace", "producedBy"],
106
+ "required": [
107
+ "id",
108
+ "description",
109
+ "producedBy"
110
+ ],
67
111
  "properties": {
68
112
  "id": {
69
113
  "description": "Unique within the claim.",
70
- "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
114
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
115
+ },
116
+ "description": {
117
+ "type": "string",
118
+ "minLength": 1,
119
+ "maxLength": 2000
120
+ },
121
+ "faultClass": {
122
+ "$ref": "urn:claimspec:v1:common#/$defs/faultClass"
123
+ },
124
+ "file": {
125
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
71
126
  },
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
127
  "find": {
76
128
  "description": "Exact text to locate in `file`. Must occur exactly `expectHits` times or the fault is `unverifiable`.",
77
129
  "type": "string",
@@ -90,14 +142,59 @@
90
142
  "default": 1
91
143
  },
92
144
  "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.",
145
+ "description": "How many times `find` must occur in `file`. Any other count means the anchor has rotted or is ambiguous \u2192 `unverifiable`, never a silent skip.",
94
146
  "type": "integer",
95
147
  "minimum": 1,
96
148
  "default": 1
97
149
  },
98
- "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" }
150
+ "producedBy": {
151
+ "$ref": "urn:claimspec:v1:common#/$defs/provenance"
152
+ },
153
+ "method": {
154
+ "$ref": "urn:claimspec:v1:common#/$defs/method"
155
+ }
99
156
  },
100
- "additionalProperties": false
157
+ "additionalProperties": false,
158
+ "allOf": [
159
+ {
160
+ "if": {
161
+ "anyOf": [
162
+ {
163
+ "properties": {
164
+ "method": {
165
+ "const": "fault-injection"
166
+ }
167
+ },
168
+ "required": [
169
+ "method"
170
+ ]
171
+ },
172
+ {
173
+ "not": {
174
+ "required": [
175
+ "method"
176
+ ]
177
+ }
178
+ }
179
+ ]
180
+ },
181
+ "then": {
182
+ "$comment": "The default method. Absent `method` means fault-injection, so every claims file written before the field existed is held to exactly the same requirements as before. The property stubs are what ajv's strict mode needs to accept `required` here; the real definitions stay above.",
183
+ "properties": {
184
+ "faultClass": true,
185
+ "file": true,
186
+ "find": true,
187
+ "replace": true
188
+ },
189
+ "required": [
190
+ "faultClass",
191
+ "file",
192
+ "find",
193
+ "replace"
194
+ ]
195
+ }
196
+ }
197
+ ]
101
198
  }
102
199
  }
103
200
  }
@@ -1,8 +1,8 @@
1
1
  {
2
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>.",
3
+ "$id": "urn:claimspec:v1:common",
4
+ "title": "claimspec v1 \u2014 shared definitions",
5
+ "description": "Definitions shared by every Guard-spec format. Referenced as urn:claimspec:v1:common#/$defs/<name>.",
6
6
  "$defs": {
7
7
  "sha256": {
8
8
  "type": "string",
@@ -28,7 +28,12 @@
28
28
  "pattern": "^(?![/\\\\])(?!\\.\\.(?:[/\\\\]|$))(?!.*[/\\\\]\\.\\.(?:[/\\\\]|$)).*$"
29
29
  },
30
30
  "severity": {
31
- "enum": ["critical", "high", "medium", "low"]
31
+ "enum": [
32
+ "critical",
33
+ "high",
34
+ "medium",
35
+ "low"
36
+ ]
32
37
  },
33
38
  "verdict": {
34
39
  "description": "Closed set. Only `killed` is a pass. Every other verdict gates by default (see GATE-SEMANTICS.md). `unverifiable` covers a fault that could not be applied (anchor missing/ambiguous) and a claim whose defenders failed to load (`defenders-failed-to-load`).",
@@ -43,7 +48,7 @@
43
48
  ]
44
49
  },
45
50
  "faultClass": {
46
- "description": "Taxonomy of the injected change. Required on every fault so calibration (fault class → real-bug predictiveness) can be computed later.",
51
+ "description": "Taxonomy of the injected change. Required on every fault so calibration (fault class \u2192 real-bug predictiveness) can be computed later. `field-dropped` = a field removed from a persisted/transmitted object, allow-list, schema or merge; `argument-swapped` = a call kept with a parameter-derived argument replaced by a trivial value; `element-removed` = a UI element no longer rendered; `handler-dropped` = an event handler no longer wired.",
47
52
  "enum": [
48
53
  "guard-removed",
49
54
  "condition-forced",
@@ -53,69 +58,166 @@
53
58
  "call-removed",
54
59
  "return-altered",
55
60
  "exception-swallowed",
61
+ "field-dropped",
62
+ "argument-swapped",
63
+ "element-removed",
64
+ "handler-dropped",
56
65
  "other"
57
66
  ]
58
67
  },
59
68
  "provenance": {
60
69
  "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
70
  "type": "object",
62
- "required": ["producer"],
71
+ "required": [
72
+ "producer"
73
+ ],
63
74
  "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" }
75
+ "producer": {
76
+ "enum": [
77
+ "human",
78
+ "agent",
79
+ "operator",
80
+ "derived"
81
+ ]
82
+ },
83
+ "by": {
84
+ "type": "string",
85
+ "maxLength": 256
86
+ },
87
+ "reviewedBy": {
88
+ "type": "string",
89
+ "maxLength": 256
90
+ },
91
+ "at": {
92
+ "$ref": "#/$defs/isoDateTime"
93
+ }
68
94
  },
69
95
  "additionalProperties": false
70
96
  },
71
97
  "tool": {
72
98
  "type": "object",
73
- "required": ["name", "version"],
99
+ "required": [
100
+ "name",
101
+ "version"
102
+ ],
74
103
  "properties": {
75
- "name": { "$ref": "#/$defs/identifier" },
76
- "version": { "type": "string", "minLength": 1, "maxLength": 64 }
104
+ "name": {
105
+ "$ref": "#/$defs/identifier"
106
+ },
107
+ "version": {
108
+ "type": "string",
109
+ "minLength": 1,
110
+ "maxLength": 64
111
+ }
77
112
  },
78
113
  "additionalProperties": false
79
114
  },
80
115
  "uncoveredFile": {
81
116
  "description": "A changed source or test file that no claim covers and no unexpired ignore entry excuses. Shared by the gate and status documents.",
82
117
  "type": "object",
83
- "required": ["file", "kind", "suggestion"],
118
+ "required": [
119
+ "file",
120
+ "kind",
121
+ "suggestion"
122
+ ],
84
123
  "properties": {
85
- "file": { "$ref": "#/$defs/repoPath" },
86
- "kind": { "enum": ["source", "test"] },
124
+ "file": {
125
+ "$ref": "#/$defs/repoPath"
126
+ },
127
+ "kind": {
128
+ "enum": [
129
+ "source",
130
+ "test"
131
+ ]
132
+ },
87
133
  "nearestClaimId": {
88
- "description": "The claim whose fault files share the longest directory prefix with this file — where to attach, not a verdict.",
134
+ "description": "The claim whose fault files share the longest directory prefix with this file \u2014 where to attach, not a verdict.",
89
135
  "$ref": "#/$defs/identifier"
90
136
  },
91
- "suggestion": { "type": "string", "minLength": 1, "maxLength": 1024 }
137
+ "suggestion": {
138
+ "type": "string",
139
+ "minLength": 1,
140
+ "maxLength": 1024
141
+ }
92
142
  },
93
143
  "additionalProperties": false
94
144
  },
95
145
  "ignoreReliance": {
96
146
  "description": "An ignore entry the gate relied on (or would have, had it not expired), with the changed files it matched.",
97
147
  "type": "object",
98
- "required": ["pattern", "reason", "files"],
148
+ "required": [
149
+ "pattern",
150
+ "reason",
151
+ "files"
152
+ ],
99
153
  "properties": {
100
- "pattern": { "type": "string", "minLength": 1, "maxLength": 512 },
101
- "reason": { "type": "string", "minLength": 8, "maxLength": 2000 },
102
- "expires": { "$ref": "#/$defs/isoDateTime" },
103
- "files": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/repoPath" } }
154
+ "pattern": {
155
+ "type": "string",
156
+ "minLength": 1,
157
+ "maxLength": 512
158
+ },
159
+ "reason": {
160
+ "type": "string",
161
+ "minLength": 8,
162
+ "maxLength": 2000
163
+ },
164
+ "expires": {
165
+ "$ref": "#/$defs/isoDateTime"
166
+ },
167
+ "files": {
168
+ "type": "array",
169
+ "minItems": 1,
170
+ "items": {
171
+ "$ref": "#/$defs/repoPath"
172
+ }
173
+ }
104
174
  },
105
175
  "additionalProperties": false
106
176
  },
107
177
  "annotation": {
108
178
  "description": "Strictly additive. An annotation never changes, suppresses, or drops a finding; it only attaches context to it.",
109
179
  "type": "object",
110
- "required": ["kind", "text"],
180
+ "required": [
181
+ "kind",
182
+ "text"
183
+ ],
111
184
  "properties": {
112
- "kind": { "enum": ["note", "accepted-risk", "false-positive", "tracked", "wontfix"] },
113
- "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
114
- "by": { "type": "string", "maxLength": 256 },
115
- "at": { "$ref": "#/$defs/isoDateTime" },
116
- "ref": { "type": "string", "maxLength": 512 }
185
+ "kind": {
186
+ "enum": [
187
+ "note",
188
+ "accepted-risk",
189
+ "false-positive",
190
+ "tracked",
191
+ "wontfix"
192
+ ]
193
+ },
194
+ "text": {
195
+ "type": "string",
196
+ "minLength": 1,
197
+ "maxLength": 4000
198
+ },
199
+ "by": {
200
+ "type": "string",
201
+ "maxLength": 256
202
+ },
203
+ "at": {
204
+ "$ref": "#/$defs/isoDateTime"
205
+ },
206
+ "ref": {
207
+ "type": "string",
208
+ "maxLength": 512
209
+ }
117
210
  },
118
211
  "additionalProperties": false
212
+ },
213
+ "method": {
214
+ "description": "How a tool tries to falsify a claim. `fault-injection` mutates source and runs the claim's defenders; it is the only method fully specified in v1, and the default when the field is absent, so every document written before this field existed stays valid. `assertion` (a claim checked by reading code, as a documentation guard does) and `scan` (a surface checked by a scanner) are RESERVED: their required fields are deliberately not specified here and will be defined by their first real consumer. Inventing them for an absent tool is the mistake that made this discriminator necessary.",
215
+ "enum": [
216
+ "fault-injection",
217
+ "assertion",
218
+ "scan"
219
+ ],
220
+ "default": "fault-injection"
119
221
  }
120
222
  }
121
223
  }