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,45 +1,136 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:guard-spec:v1:evidence",
4
- "title": "Guard spec v1 — evidence file",
3
+ "$id": "urn:claimspec:v1:evidence",
4
+ "title": "claimspec v1 \u2014 evidence file",
5
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
6
  "type": "object",
7
- "required": ["schemaVersion", "tool", "run", "records"],
7
+ "required": [
8
+ "schemaVersion",
9
+ "tool",
10
+ "run",
11
+ "records"
12
+ ],
8
13
  "properties": {
9
- "$schema": { "type": "string" },
10
- "schemaVersion": { "const": 1 },
11
- "tool": { "$ref": "urn:guard-spec:v1:common#/$defs/tool" },
14
+ "$schema": {
15
+ "type": "string"
16
+ },
17
+ "schemaVersion": {
18
+ "const": 1
19
+ },
20
+ "tool": {
21
+ "$ref": "urn:claimspec:v1:common#/$defs/tool"
22
+ },
12
23
  "run": {
13
24
  "type": "object",
14
- "required": ["id", "startedAt", "repo", "confirmRuns", "mode"],
25
+ "required": [
26
+ "id",
27
+ "startedAt",
28
+ "repo"
29
+ ],
15
30
  "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" },
31
+ "id": {
32
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
33
+ },
34
+ "startedAt": {
35
+ "$ref": "urn:claimspec:v1:common#/$defs/isoDateTime"
36
+ },
37
+ "finishedAt": {
38
+ "$ref": "urn:claimspec:v1:common#/$defs/isoDateTime"
39
+ },
19
40
  "repo": {
20
41
  "type": "object",
21
- "required": ["head", "dirty"],
42
+ "required": [
43
+ "head",
44
+ "dirty"
45
+ ],
22
46
  "properties": {
23
- "head": { "$ref": "urn:guard-spec:v1:common#/$defs/gitSha" },
24
- "dirty": { "type": "boolean" },
47
+ "head": {
48
+ "$ref": "urn:claimspec:v1:common#/$defs/gitSha"
49
+ },
50
+ "dirty": {
51
+ "type": "boolean"
52
+ },
25
53
  "snapshot": {
26
54
  "description": "When the working tree was probed instead of `head`: the dangling commit that captured it. Not a ref; may be garbage-collected.",
27
- "$ref": "urn:guard-spec:v1:common#/$defs/gitSha"
55
+ "$ref": "urn:claimspec:v1:common#/$defs/gitSha"
56
+ },
57
+ "ignoredDirty": {
58
+ "description": "Defender/target files that had uncommitted changes when `head` was probed as committed (an explicit --ref, or --ignore-dirty). The working-tree versions of these files are NOT what the verdicts describe.",
59
+ "type": "array",
60
+ "minItems": 1,
61
+ "items": {
62
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
63
+ }
28
64
  }
29
65
  },
30
66
  "additionalProperties": false
31
67
  },
32
68
  "runner": {
33
69
  "type": "object",
34
- "required": ["name"],
70
+ "required": [
71
+ "name"
72
+ ],
35
73
  "properties": {
36
- "name": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
37
- "version": { "type": "string", "maxLength": 64 }
74
+ "name": {
75
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
76
+ },
77
+ "version": {
78
+ "type": "string",
79
+ "maxLength": 64
80
+ },
81
+ "source": {
82
+ "description": "`project`: the runner package resolved from the project and its own bin script ran. `path`: no such package in the project; an executable of that name on PATH ran instead (a legitimate monorepo setup, but not the project's pinned version).",
83
+ "enum": [
84
+ "project",
85
+ "path"
86
+ ]
87
+ }
88
+ },
89
+ "additionalProperties": false
90
+ },
91
+ "serial": {
92
+ "description": "True when the defenders were run one test file at a time. Recorded because a serial run is slower and its durations are not comparable with a parallel one.",
93
+ "type": "boolean"
94
+ },
95
+ "contention": {
96
+ "description": "Other test runners that were already running when this probe started. Best effort, never a verdict: it exists so a reader of a TIMEOUT or FLAKY-DEFENDER can tell the machine was loaded.",
97
+ "type": "object",
98
+ "required": [
99
+ "detected",
100
+ "runners"
101
+ ],
102
+ "properties": {
103
+ "detected": {
104
+ "const": true
105
+ },
106
+ "runners": {
107
+ "type": "array",
108
+ "minItems": 1,
109
+ "items": {
110
+ "type": "object",
111
+ "required": [
112
+ "pid",
113
+ "command"
114
+ ],
115
+ "properties": {
116
+ "pid": {
117
+ "type": "integer",
118
+ "minimum": 1
119
+ },
120
+ "command": {
121
+ "type": "string",
122
+ "minLength": 1,
123
+ "maxLength": 200
124
+ }
125
+ },
126
+ "additionalProperties": false
127
+ }
128
+ }
38
129
  },
39
130
  "additionalProperties": false
40
131
  },
41
132
  "confirmRuns": {
42
- "description": "N — how many times the defenders were run both unmodified (baseline) and with each fault applied.",
133
+ "description": "N \u2014 how many times the defenders were run both unmodified (baseline) and with each fault applied.",
43
134
  "type": "integer",
44
135
  "minimum": 1
45
136
  },
@@ -49,33 +140,119 @@
49
140
  },
50
141
  "mode": {
51
142
  "description": "`worktree` = faults were applied in a scratch git worktree (default). `in-place` = the user's working tree was mutated and restored.",
52
- "enum": ["worktree", "in-place"]
143
+ "enum": [
144
+ "worktree",
145
+ "in-place"
146
+ ]
147
+ },
148
+ "runners": {
149
+ "description": "Every runner that produced part of this evidence when defenders were split by file (the project runner plus, e.g., Playwright for the files under its testDir). Absent when one runner ran everything.",
150
+ "type": "array",
151
+ "minItems": 1,
152
+ "items": {
153
+ "type": "object",
154
+ "required": [
155
+ "name"
156
+ ],
157
+ "properties": {
158
+ "name": {
159
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
160
+ },
161
+ "version": {
162
+ "type": "string",
163
+ "maxLength": 64
164
+ }
165
+ },
166
+ "additionalProperties": false
167
+ }
168
+ },
169
+ "method": {
170
+ "$ref": "urn:claimspec:v1:common#/$defs/method"
53
171
  }
54
172
  },
55
- "additionalProperties": false
173
+ "additionalProperties": false,
174
+ "allOf": [
175
+ {
176
+ "if": {
177
+ "anyOf": [
178
+ {
179
+ "properties": {
180
+ "method": {
181
+ "const": "fault-injection"
182
+ }
183
+ },
184
+ "required": [
185
+ "method"
186
+ ]
187
+ },
188
+ {
189
+ "not": {
190
+ "required": [
191
+ "method"
192
+ ]
193
+ }
194
+ }
195
+ ]
196
+ },
197
+ "then": {
198
+ "$comment": "Absent `method` means fault-injection, so every evidence file written before the field existed is held to exactly the same requirements as before. `confirmRuns` and `mode` are what injection means by a confirmed run and by where the mutation happened; a method that mutates nothing has neither.",
199
+ "properties": {
200
+ "confirmRuns": true,
201
+ "mode": true
202
+ },
203
+ "required": [
204
+ "confirmRuns",
205
+ "mode"
206
+ ]
207
+ }
208
+ }
209
+ ]
56
210
  },
57
211
  "records": {
58
212
  "type": "array",
59
- "items": { "$ref": "#/$defs/record" }
213
+ "items": {
214
+ "$ref": "#/$defs/record"
215
+ }
60
216
  }
61
217
  },
62
218
  "additionalProperties": false,
63
219
  "$defs": {
64
220
  "testRun": {
65
221
  "type": "object",
66
- "required": ["outcome", "durationMs"],
222
+ "required": [
223
+ "outcome",
224
+ "durationMs"
225
+ ],
67
226
  "properties": {
68
227
  "outcome": {
69
228
  "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.",
70
- "enum": ["pass", "fail", "timeout", "error"]
229
+ "enum": [
230
+ "pass",
231
+ "fail",
232
+ "timeout",
233
+ "error"
234
+ ]
71
235
  },
72
236
  "tests": {
73
237
  "type": "object",
74
- "required": ["total", "passed", "failed"],
238
+ "required": [
239
+ "total",
240
+ "passed",
241
+ "failed"
242
+ ],
75
243
  "properties": {
76
- "total": { "type": "integer", "minimum": 0 },
77
- "passed": { "type": "integer", "minimum": 0 },
78
- "failed": { "type": "integer", "minimum": 0 }
244
+ "total": {
245
+ "type": "integer",
246
+ "minimum": 0
247
+ },
248
+ "passed": {
249
+ "type": "integer",
250
+ "minimum": 0
251
+ },
252
+ "failed": {
253
+ "type": "integer",
254
+ "minimum": 0
255
+ }
79
256
  },
80
257
  "additionalProperties": false
81
258
  },
@@ -84,60 +261,115 @@
84
261
  "type": "integer",
85
262
  "minimum": 0
86
263
  },
87
- "durationMs": { "type": "integer", "minimum": 0 }
264
+ "durationMs": {
265
+ "type": "integer",
266
+ "minimum": 0
267
+ }
88
268
  },
89
269
  "additionalProperties": false
90
270
  },
91
271
  "record": {
92
272
  "type": "object",
93
- "required": ["fingerprint", "claim", "subject", "verdict", "detail", "defenders", "inputs"],
273
+ "required": [
274
+ "fingerprint",
275
+ "claim",
276
+ "subject",
277
+ "verdict",
278
+ "detail"
279
+ ],
94
280
  "properties": {
95
281
  "fingerprint": {
96
282
  "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.",
97
- "$ref": "urn:guard-spec:v1:common#/$defs/sha256"
283
+ "$ref": "urn:claimspec:v1:common#/$defs/sha256"
98
284
  },
99
285
  "claim": {
100
286
  "type": "object",
101
- "required": ["id", "statement", "severity", "source"],
287
+ "required": [
288
+ "id",
289
+ "statement",
290
+ "severity",
291
+ "source"
292
+ ],
102
293
  "properties": {
103
- "id": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
104
- "statement": { "type": "string", "minLength": 1, "maxLength": 2000 },
105
- "severity": { "$ref": "urn:guard-spec:v1:common#/$defs/severity" },
294
+ "id": {
295
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
296
+ },
297
+ "statement": {
298
+ "type": "string",
299
+ "minLength": 1,
300
+ "maxLength": 2000
301
+ },
302
+ "severity": {
303
+ "$ref": "urn:claimspec:v1:common#/$defs/severity"
304
+ },
106
305
  "source": {
107
306
  "type": "object",
108
- "required": ["kind"],
307
+ "required": [
308
+ "kind"
309
+ ],
109
310
  "properties": {
110
- "kind": { "enum": ["spec", "adr", "annotation", "comment", "manual"] },
111
- "ref": { "type": "string", "maxLength": 512 }
311
+ "kind": {
312
+ "enum": [
313
+ "spec",
314
+ "adr",
315
+ "annotation",
316
+ "comment",
317
+ "manual"
318
+ ]
319
+ },
320
+ "ref": {
321
+ "type": "string",
322
+ "maxLength": 512
323
+ }
112
324
  },
113
325
  "additionalProperties": false
114
326
  },
115
- "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" }
327
+ "producedBy": {
328
+ "$ref": "urn:claimspec:v1:common#/$defs/provenance"
329
+ }
116
330
  },
117
331
  "additionalProperties": false
118
332
  },
119
333
  "subject": {
120
334
  "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.",
121
335
  "type": "object",
122
- "required": ["kind", "id"],
336
+ "required": [
337
+ "kind",
338
+ "id"
339
+ ],
123
340
  "properties": {
124
- "kind": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
125
- "id": { "$ref": "urn:guard-spec:v1:common#/$defs/identifier" },
126
- "description": { "type": "string", "maxLength": 2000 },
127
- "file": { "$ref": "urn:guard-spec:v1:common#/$defs/repoPath" },
128
- "faultClass": { "$ref": "urn:guard-spec:v1:common#/$defs/faultClass" },
129
- "producedBy": { "$ref": "urn:guard-spec:v1:common#/$defs/provenance" },
341
+ "kind": {
342
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
343
+ },
344
+ "id": {
345
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
346
+ },
347
+ "description": {
348
+ "type": "string",
349
+ "maxLength": 2000
350
+ },
351
+ "file": {
352
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
353
+ },
354
+ "faultClass": {
355
+ "$ref": "urn:claimspec:v1:common#/$defs/faultClass"
356
+ },
357
+ "producedBy": {
358
+ "$ref": "urn:claimspec:v1:common#/$defs/provenance"
359
+ },
130
360
  "contentHash": {
131
- "description": "sha256 of the subject's content (for a fault: find + newline + replace). Excluded from the fingerprint on purpose, so repairing an anchor does not churn a baseline — and recorded here on purpose, so weakening a fault to make a finding disappear is visible.",
132
- "$ref": "urn:guard-spec:v1:common#/$defs/sha256"
361
+ "description": "sha256 of the subject's content (for a fault: find + newline + replace). Excluded from the fingerprint on purpose, so repairing an anchor does not churn a baseline \u2014 and recorded here on purpose, so weakening a fault to make a finding disappear is visible.",
362
+ "$ref": "urn:claimspec:v1:common#/$defs/sha256"
133
363
  }
134
364
  },
135
365
  "additionalProperties": true
136
366
  },
137
- "verdict": { "$ref": "urn:guard-spec:v1:common#/$defs/verdict" },
367
+ "verdict": {
368
+ "$ref": "urn:claimspec:v1:common#/$defs/verdict"
369
+ },
138
370
  "detail": {
139
371
  "type": "object",
140
- "required": ["baselineRuns", "probeRuns"],
372
+ "required": [],
141
373
  "properties": {
142
374
  "reason": {
143
375
  "description": "Machine-readable qualifier, e.g. `anchor-missing`, `anchor-ambiguous`, `defenders-not-green`, `replacement-does-not-compile`.",
@@ -147,50 +379,209 @@
147
379
  "baselineRuns": {
148
380
  "description": "Defenders run on unmodified source, N times. Anything short of N/N pass is `flaky-defender`.",
149
381
  "type": "array",
150
- "items": { "$ref": "#/$defs/testRun" }
382
+ "items": {
383
+ "$ref": "#/$defs/testRun"
384
+ }
151
385
  },
152
386
  "probeRuns": {
153
387
  "description": "Defenders run with the fault applied, N times.",
154
388
  "type": "array",
155
- "items": { "$ref": "#/$defs/testRun" }
389
+ "items": {
390
+ "$ref": "#/$defs/testRun"
391
+ }
156
392
  },
157
393
  "escalated": {
158
394
  "description": "True when the fault survived its declared defenders and a broader test set was then run.",
159
395
  "type": "boolean"
160
396
  },
397
+ "escalationStoppedEarly": {
398
+ "description": "Why the escalation loop ran fewer than N times. `no-common-failure`: no test failed in every run so far, so no further run could name an undeclared killer. Stopping can only fail to NAME a killer, never upgrade a verdict.",
399
+ "enum": [
400
+ "no-common-failure"
401
+ ]
402
+ },
161
403
  "escalationRuns": {
162
404
  "type": "array",
163
- "items": { "$ref": "#/$defs/testRun" }
405
+ "items": {
406
+ "$ref": "#/$defs/testRun"
407
+ }
164
408
  },
165
409
  "undeclaredKillers": {
166
410
  "description": "With reason `killed-by-undeclared-tests`: the tests (as `file::name`) that failed in every escalation run. Names what to add to `defendedBy`.",
167
411
  "type": "array",
168
412
  "minItems": 1,
169
- "items": { "type": "string", "minLength": 1, "maxLength": 1024 }
413
+ "items": {
414
+ "type": "string",
415
+ "minLength": 1,
416
+ "maxLength": 1024
417
+ }
418
+ },
419
+ "flakeRate": {
420
+ "description": "Observed instability of the defenders on UNMODIFIED source: `failures` of `runs` baseline runs failed with the source untouched. Only runs that actually ran are counted (`pass`/`fail`); a load error or a timeout measures nothing about stability and is excluded from both numbers. `failures > 0` means the verdict is `flaky-defender`; the degree is for whoever fixes the defender, and it is the first input to calibration.",
421
+ "type": "object",
422
+ "required": [
423
+ "runs",
424
+ "failures"
425
+ ],
426
+ "properties": {
427
+ "runs": {
428
+ "type": "integer",
429
+ "minimum": 1
430
+ },
431
+ "failures": {
432
+ "type": "integer",
433
+ "minimum": 0
434
+ }
435
+ },
436
+ "additionalProperties": false
437
+ },
438
+ "independence": {
439
+ "description": "L3 signal, on `killed` records only: was the killing defender last touched by the same change (or the same author) as the fault's target? A signal for ranking and reporting; it never changes a verdict.",
440
+ "type": "object",
441
+ "required": [
442
+ "class"
443
+ ],
444
+ "properties": {
445
+ "class": {
446
+ "enum": [
447
+ "co-authored",
448
+ "separate-change",
449
+ "unknown"
450
+ ]
451
+ },
452
+ "defenderCommit": {
453
+ "$ref": "urn:claimspec:v1:common#/$defs/gitSha"
454
+ },
455
+ "targetCommit": {
456
+ "$ref": "urn:claimspec:v1:common#/$defs/gitSha"
457
+ },
458
+ "sameAuthor": {
459
+ "type": "boolean"
460
+ }
461
+ },
462
+ "additionalProperties": false
463
+ },
464
+ "negativeControl": {
465
+ "description": "The result of replacing the subject with content its loader cannot parse, and running the defenders once. `reached`: they went red, so they do execute this file and a `survived` verdict is about their assertions. `not-reached`: they stayed green, so they never execute it and the verdict becomes `unverifiable` with reason `subject-not-executed`. Charged only where a false answer is expensive \u2014 on a would-be survivor \u2014 because a kill already proves the defenders reached the code. Absent when the control was not run or the runner cannot say what \"cannot compile\" means for its language.",
466
+ "enum": [
467
+ "reached",
468
+ "not-reached"
469
+ ]
470
+ },
471
+ "targetNotImported": {
472
+ "description": "True when, with the fault applied, no defender caused the subject's module to be imported at all. Reported by runners that can tell (Python), never inferred. It does not change the verdict, and it is the one thing that makes a `survived` unreadable: the tests were not blind to the fault, they never reached it. A module imported from OUTSIDE the tree being probed is not recorded here \u2014 that run is refused outright, because every verdict in it would be false.",
473
+ "const": true
474
+ },
475
+ "message": {
476
+ "description": "Human-readable detail for a reason that carries one \u2014 today `probe-error`, whose first line names what threw. Never parsed; a reader should not have to reproduce the run to learn what happened.",
477
+ "type": "string",
478
+ "maxLength": 1024
479
+ },
480
+ "restoreSkipped": {
481
+ "description": "The fault was applied but the restore found nothing to put back: the target file no longer existed. In worktree mode that is a disturbed run rather than a failed one \u2014 the mutation only ever existed inside a scratch worktree, and something outside the process deleted it \u2014 but the verdicts in that run were measured against a tree that was being removed underneath them, and a reader has to be able to see that. In `--in-place` mode it never appears: there a missing target is the user's own file gone, and the restore throws.",
482
+ "enum": [
483
+ "target-missing"
484
+ ]
170
485
  },
171
486
  "anchor": {
172
487
  "description": "With an anchor-related reason: how many times `find` occurred versus how many were expected.",
173
488
  "type": "object",
174
- "required": ["hits", "expected"],
489
+ "required": [
490
+ "hits",
491
+ "expected"
492
+ ],
175
493
  "properties": {
176
- "hits": { "type": "integer", "minimum": 0 },
177
- "expected": { "type": "integer", "minimum": 1 }
494
+ "hits": {
495
+ "type": "integer",
496
+ "minimum": 0
497
+ },
498
+ "expected": {
499
+ "type": "integer",
500
+ "minimum": 1
501
+ }
178
502
  },
179
503
  "additionalProperties": false
180
504
  }
181
505
  },
182
- "additionalProperties": false
506
+ "additionalProperties": false,
507
+ "description": "Everything behind the verdict. Under `fault-injection` the runs are mandatory and the validator enforces them; a method that runs no tests carries whatever its own evidence is."
183
508
  },
184
509
  "defenders": {
185
510
  "type": "object",
186
- "required": ["requested", "resolved", "nocover"],
511
+ "required": [
512
+ "requested",
513
+ "resolved",
514
+ "nocover"
515
+ ],
187
516
  "properties": {
188
- "requested": { "type": "array", "items": { "type": "string" } },
189
- "resolved": { "type": "array", "items": { "$ref": "urn:guard-spec:v1:common#/$defs/repoPath" } },
190
- "nocover": { "type": "boolean" },
517
+ "requested": {
518
+ "type": "array",
519
+ "items": {
520
+ "type": "string"
521
+ }
522
+ },
523
+ "resolved": {
524
+ "type": "array",
525
+ "items": {
526
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
527
+ }
528
+ },
529
+ "nocover": {
530
+ "type": "boolean"
531
+ },
191
532
  "discovered": {
192
533
  "description": "True when no defenders were declared and `resolved` was found by static import of the subject's file.",
193
534
  "type": "boolean"
535
+ },
536
+ "mocking": {
537
+ "description": "Candidate test files that import the subject's file but mock it (`vi.mock`/`jest.mock`). Discovered candidates that mock are excluded from `resolved`; a declared defender that mocks stays in `resolved` and is listed here, because a declared defender that mocks the subject is a broken evidence chain.",
538
+ "type": "array",
539
+ "items": {
540
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
541
+ }
542
+ },
543
+ "signals": {
544
+ "description": "Static, annotation-grade signals about the defenders. They never change a verdict. `mocked-never-asserted`: the file mocks the subject and never asserts on anything imported from it. `unasserted-annotated`: the same, but the author annotated the mock with a reason (`// unasserted: <why>`), recorded here \u2014 silenced, never hidden. `target-attribute-patched`: a Python defender that patches individual attributes of the subject (`patch(\"pkg.mod.fn\")`) rather than the module. Unlike a JavaScript module mock it does NOT disqualify the file \u2014 a fault anywhere else in the module is still detectable \u2014 so the file stays in `resolved`; `reason` names the patched attributes, which are where that test is least likely to notice anything.",
545
+ "type": "array",
546
+ "items": {
547
+ "type": "object",
548
+ "required": [
549
+ "file",
550
+ "signal"
551
+ ],
552
+ "properties": {
553
+ "file": {
554
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
555
+ },
556
+ "signal": {
557
+ "enum": [
558
+ "mocked-never-asserted",
559
+ "unasserted-annotated",
560
+ "target-attribute-patched"
561
+ ]
562
+ },
563
+ "reason": {
564
+ "type": "string",
565
+ "minLength": 1,
566
+ "maxLength": 512
567
+ }
568
+ },
569
+ "additionalProperties": false
570
+ }
571
+ },
572
+ "byRunner": {
573
+ "description": "When the resolved defenders ran under more than one runner: runner name \u2192 the files it ran. Every file appears in `resolved`. Absent when the project runner ran them all.",
574
+ "type": "object",
575
+ "propertyNames": {
576
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
577
+ },
578
+ "additionalProperties": {
579
+ "type": "array",
580
+ "minItems": 1,
581
+ "items": {
582
+ "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
583
+ }
584
+ }
194
585
  }
195
586
  },
196
587
  "additionalProperties": false
@@ -198,38 +589,54 @@
198
589
  "inputs": {
199
590
  "description": "Content hashes of everything the verdict depends on, so an unchanged claim can reuse its prior verdict.",
200
591
  "type": "object",
201
- "required": ["targetHash", "defenderHashes"],
592
+ "required": [
593
+ "targetHash",
594
+ "defenderHashes"
595
+ ],
202
596
  "properties": {
203
- "targetHash": { "$ref": "urn:guard-spec:v1:common#/$defs/sha256" },
597
+ "targetHash": {
598
+ "$ref": "urn:claimspec:v1:common#/$defs/sha256"
599
+ },
204
600
  "defenderHashes": {
205
601
  "type": "object",
206
- "additionalProperties": { "$ref": "urn:guard-spec:v1:common#/$defs/sha256" }
602
+ "additionalProperties": {
603
+ "$ref": "urn:claimspec:v1:common#/$defs/sha256"
604
+ }
207
605
  }
208
606
  },
209
607
  "additionalProperties": false
210
608
  },
211
609
  "reusedFrom": {
212
610
  "description": "Run id whose verdict was reused because `inputs` were unchanged.",
213
- "$ref": "urn:guard-spec:v1:common#/$defs/identifier"
611
+ "$ref": "urn:claimspec:v1:common#/$defs/identifier"
214
612
  },
215
613
  "rank": {
216
614
  "description": "Additive ordering signal. Never alters `verdict`.",
217
615
  "type": "object",
218
- "required": ["score"],
616
+ "required": [
617
+ "score"
618
+ ],
219
619
  "properties": {
220
- "score": { "type": "number" },
620
+ "score": {
621
+ "type": "number"
622
+ },
221
623
  "blastRadius": {
222
624
  "description": "Number of source files that import the subject's file, transitively or directly as the tool documents.",
223
625
  "type": "integer",
224
626
  "minimum": 0
225
627
  },
226
- "tier": { "type": "string", "maxLength": 64 }
628
+ "tier": {
629
+ "type": "string",
630
+ "maxLength": 64
631
+ }
227
632
  },
228
633
  "additionalProperties": false
229
634
  },
230
635
  "annotations": {
231
636
  "type": "array",
232
- "items": { "$ref": "urn:guard-spec:v1:common#/$defs/annotation" }
637
+ "items": {
638
+ "$ref": "urn:claimspec:v1:common#/$defs/annotation"
639
+ }
233
640
  }
234
641
  },
235
642
  "additionalProperties": false