testguard-cli 0.6.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.
- package/CHANGELOG.md +373 -0
- package/README.md +277 -27
- package/package.json +2 -2
- package/spec/GATE-SEMANTICS.md +112 -5
- package/spec/README.md +28 -10
- package/spec/lib/validate.mjs +84 -8
- package/spec/schemas/baseline.schema.json +7 -7
- package/spec/schemas/brief.schema.json +12 -12
- package/spec/schemas/calibration.schema.json +5 -5
- package/spec/schemas/claims.schema.json +122 -25
- package/spec/schemas/common.schema.json +127 -29
- package/spec/schemas/evidence.schema.json +169 -73
- package/spec/schemas/gate.schema.json +12 -12
- package/spec/schemas/ignore.schema.json +4 -4
- package/spec/schemas/replay.schema.json +10 -10
- package/spec/schemas/status.schema.json +25 -25
- package/src/cli.mjs +20 -5
- package/src/commands/claims.mjs +22 -2
- package/src/commands/probe.mjs +24 -4
- package/src/gate/changed.mjs +9 -2
- package/src/probe/attribution.mjs +123 -0
- package/src/probe/classify.mjs +18 -2
- package/src/probe/cost.mjs +145 -0
- package/src/probe/discover.mjs +14 -6
- package/src/probe/inject.mjs +41 -9
- package/src/probe/mocks.mjs +2 -0
- package/src/probe/probe.mjs +235 -52
- package/src/probe/progress.mjs +87 -0
- package/src/probe/pyimports.mjs +287 -0
- package/src/probe/rank.mjs +2 -0
- package/src/probe/runners/index.mjs +38 -11
- package/src/probe/runners/jest.mjs +13 -1
- package/src/probe/runners/playwright.mjs +9 -0
- package/src/probe/runners/python/_testguard_pytest_plugin.py +132 -0
- package/src/probe/runners/python/_testguard_report.py +141 -0
- package/src/probe/runners/python/_testguard_unittest_main.py +168 -0
- package/src/probe/runners/python.mjs +316 -0
- package/src/probe/runners/shared.mjs +10 -1
- package/src/probe/runners/vitest.mjs +13 -1
- package/src/replay/replay.mjs +3 -2
- package/src/scaffold/producers.python.mjs +209 -0
- package/src/scaffold/scaffold.mjs +86 -32
- package/src/util/glob.mjs +7 -1
package/spec/lib/validate.mjs
CHANGED
|
@@ -7,6 +7,13 @@ import { fingerprint } from './fingerprint.mjs';
|
|
|
7
7
|
const schemaDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'schemas');
|
|
8
8
|
|
|
9
9
|
export const KINDS = Object.freeze(['claims', 'evidence', 'baseline', 'ignore', 'calibration', 'brief', 'status', 'gate', 'replay']);
|
|
10
|
+
/**
|
|
11
|
+
* How a document says it tried to falsify its claims. Absent means
|
|
12
|
+
* `fault-injection`, so every document written before the field existed is
|
|
13
|
+
* held to exactly the rules it was written against.
|
|
14
|
+
*/
|
|
15
|
+
export const methodOf = (x) => x?.method ?? 'fault-injection';
|
|
16
|
+
const isInjection = (x) => methodOf(x) === 'fault-injection';
|
|
10
17
|
export const PASSING_VERDICTS = Object.freeze(new Set(['killed']));
|
|
11
18
|
|
|
12
19
|
const ajv = new Ajv2020({ strict: true, allErrors: true });
|
|
@@ -15,7 +22,7 @@ for (const f of readdirSync(schemaDir).filter((n) => n.endsWith('.schema.json'))
|
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
const schemaFor = (kind) => {
|
|
18
|
-
const v = ajv.getSchema(`urn:
|
|
25
|
+
const v = ajv.getSchema(`urn:claimspec:v1:${kind}`);
|
|
19
26
|
if (!v) throw new RangeError(`unknown Guard-spec kind: ${kind}`);
|
|
20
27
|
return v;
|
|
21
28
|
};
|
|
@@ -33,10 +40,15 @@ const semantic = {
|
|
|
33
40
|
const p = `/claims/${ci}/faults/${fi}`;
|
|
34
41
|
if (seenFault.has(f.id)) errors.push({ path: `${p}/id`, message: `duplicate fault id "${f.id}" in claim "${c.id}"` });
|
|
35
42
|
seenFault.add(f.id);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (f
|
|
43
|
+
// Anchor arithmetic is a property of fault injection. A probe that does
|
|
44
|
+
// not substitute text has no `find` to compare, and `undefined ===
|
|
45
|
+
// undefined` would report every one of them as a no-op.
|
|
46
|
+
if (isInjection(f)) {
|
|
47
|
+
const occ = f.occurrence ?? 1;
|
|
48
|
+
const hits = f.expectHits ?? 1;
|
|
49
|
+
if (occ > hits) errors.push({ path: `${p}/occurrence`, message: `occurrence ${occ} exceeds expectHits ${hits}` });
|
|
50
|
+
if (f.find === f.replace) errors.push({ path: `${p}/replace`, message: 'replace is identical to find; the fault is a no-op' });
|
|
51
|
+
}
|
|
40
52
|
});
|
|
41
53
|
});
|
|
42
54
|
return errors;
|
|
@@ -44,9 +56,15 @@ const semantic = {
|
|
|
44
56
|
|
|
45
57
|
evidence(doc) {
|
|
46
58
|
const errors = [];
|
|
59
|
+
const injection = isInjection(doc.run);
|
|
47
60
|
const n = doc.run.confirmRuns;
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
// N-run agreement is what fault injection means by "confirmed". A method
|
|
62
|
+
// that does not re-run anything has no N, and a rule about one would be a
|
|
63
|
+
// rule about a number that is not there.
|
|
64
|
+
if (injection) {
|
|
65
|
+
if (n < 3 && doc.run.provisional !== true) errors.push({ path: '/run/provisional', message: `confirmRuns ${n} is below 3; the run must declare provisional: true` });
|
|
66
|
+
if (n >= 3 && doc.run.provisional === true) errors.push({ path: '/run/provisional', message: `confirmRuns ${n} is confirmed; provisional must be absent or false` });
|
|
67
|
+
}
|
|
50
68
|
if (doc.run.runners && doc.run.runner && !doc.run.runners.some((x) => x.name === doc.run.runner.name)) errors.push({ path: '/run/runners', message: 'runners must include the project runner named in run.runner' });
|
|
51
69
|
if (doc.run.repo.ignoredDirty && doc.run.repo.snapshot) errors.push({ path: '/run/repo/ignoredDirty', message: 'a working-tree snapshot has no ignored dirty files: the tree was probed as it is' });
|
|
52
70
|
doc.records.forEach((r, i) => {
|
|
@@ -54,6 +72,23 @@ const semantic = {
|
|
|
54
72
|
const expected = fingerprint({ claimId: r.claim.id, subjectId: r.subject.id, file: r.subject.file ?? '', verdict: r.verdict });
|
|
55
73
|
if (r.fingerprint !== expected) errors.push({ path: `${p}/fingerprint`, message: `fingerprint does not match spec derivation (expected ${expected})` });
|
|
56
74
|
|
|
75
|
+
// What the schema no longer requires, the validator still does — for the
|
|
76
|
+
// one method that means it. Fault injection without defenders, inputs or
|
|
77
|
+
// runs is not a leaner document, it is a verdict with nothing behind it;
|
|
78
|
+
// relaxing the schema was to let a scanner conform, never to let an
|
|
79
|
+
// injecting tool stop showing its work.
|
|
80
|
+
if (injection) {
|
|
81
|
+
for (const k of ['defenders', 'inputs']) {
|
|
82
|
+
if (!r[k]) errors.push({ path: `${p}/${k}`, message: `fault-injection requires ${k} on every record` });
|
|
83
|
+
}
|
|
84
|
+
for (const k of ['baselineRuns', 'probeRuns']) {
|
|
85
|
+
if (!Array.isArray(r.detail[k])) errors.push({ path: `${p}/detail/${k}`, message: `fault-injection requires detail.${k} on every record` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Every rule below reads those fields, so a record that is already
|
|
89
|
+
// malformed must not also throw on the way to being reported.
|
|
90
|
+
if (!injection || !r.defenders || !r.inputs || !Array.isArray(r.detail.baselineRuns) || !Array.isArray(r.detail.probeRuns)) return;
|
|
91
|
+
|
|
57
92
|
const { baselineRuns, probeRuns } = r.detail;
|
|
58
93
|
if (r.verdict === 'killed' || r.verdict === 'survived') {
|
|
59
94
|
if (baselineRuns.length !== n) errors.push({ path: `${p}/detail/baselineRuns`, message: `${r.verdict} requires exactly confirmRuns (${n}) baseline runs, got ${baselineRuns.length}` });
|
|
@@ -99,11 +134,52 @@ const semantic = {
|
|
|
99
134
|
}
|
|
100
135
|
for (const s of r.defenders.signals ?? []) {
|
|
101
136
|
if (s.signal === 'unasserted-annotated' && !s.reason) errors.push({ path: `${p}/defenders/signals`, message: `unasserted-annotated on ${s.file} requires the annotation's reason` });
|
|
102
|
-
|
|
137
|
+
// An attribute patch (Python) leaves the file a defender, so it belongs
|
|
138
|
+
// to `resolved` and not to `mocking`; every other signal is about a file
|
|
139
|
+
// that replaced the module and must be listed as mocking it.
|
|
140
|
+
if (s.signal === 'target-attribute-patched') {
|
|
141
|
+
if (!s.reason) errors.push({ path: `${p}/defenders/signals`, message: `target-attribute-patched on ${s.file} requires the patched attributes as its reason` });
|
|
142
|
+
if (!r.defenders.resolved.includes(s.file)) errors.push({ path: `${p}/defenders/signals`, message: `${s.file} patches attributes of the subject but is not a resolved defender` });
|
|
143
|
+
} else if (!(r.defenders.mocking ?? []).includes(s.file)) {
|
|
144
|
+
errors.push({ path: `${p}/defenders/signals`, message: `${s.file} carries a mock signal but is not listed in defenders.mocking` });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// A module the defenders never imported cannot have been killed by them.
|
|
148
|
+
if (r.detail.targetNotImported && r.verdict === 'killed') {
|
|
149
|
+
errors.push({ path: `${p}/detail/targetNotImported`, message: 'the subject was never imported, so the defenders cannot have killed the fault' });
|
|
150
|
+
}
|
|
151
|
+
// The control is only ever charged on a would-be survivor, so a `reached`
|
|
152
|
+
// record must be one, and `not-reached` must have become unverifiable.
|
|
153
|
+
if (r.detail.negativeControl === 'reached' && r.verdict !== 'survived') {
|
|
154
|
+
errors.push({ path: `${p}/detail/negativeControl`, message: `negativeControl "reached" belongs to a survivor, not to ${r.verdict}` });
|
|
155
|
+
}
|
|
156
|
+
if (r.detail.negativeControl === 'not-reached' && !(r.verdict === 'unverifiable' && r.detail.reason === 'subject-not-executed')) {
|
|
157
|
+
errors.push({ path: `${p}/detail/negativeControl`, message: 'negativeControl "not-reached" requires verdict unverifiable with reason subject-not-executed' });
|
|
158
|
+
}
|
|
159
|
+
if (r.detail.reason === 'subject-not-executed' && r.detail.negativeControl !== 'not-reached') {
|
|
160
|
+
errors.push({ path: `${p}/detail/reason`, message: 'subject-not-executed requires the negativeControl that established it' });
|
|
161
|
+
}
|
|
162
|
+
// Python's import provenance and the general control answer the same
|
|
163
|
+
// question. A document where they disagree describes a run that cannot
|
|
164
|
+
// have happened.
|
|
165
|
+
if (r.detail.targetNotImported && r.detail.negativeControl === 'reached') {
|
|
166
|
+
errors.push({ path: `${p}/detail/negativeControl`, message: 'the subject was never imported, so the negative control cannot have reached it' });
|
|
103
167
|
}
|
|
104
168
|
if (r.verdict === 'unverifiable' && !r.detail.reason) {
|
|
105
169
|
errors.push({ path: `${p}/detail/reason`, message: 'unverifiable requires a reason (e.g. anchor-missing, anchor-ambiguous)' });
|
|
106
170
|
}
|
|
171
|
+
// `probe-error` is the one reason that names nothing on its own: it says
|
|
172
|
+
// the run threw, not what threw. Without the message the record is a
|
|
173
|
+
// dead end for anyone reading the document instead of rerunning it.
|
|
174
|
+
if (r.detail.reason === 'probe-error' && !r.detail.message) {
|
|
175
|
+
errors.push({ path: `${p}/detail/message`, message: 'probe-error requires a message naming what threw' });
|
|
176
|
+
}
|
|
177
|
+
// In-place is the mode where a vanished target is the user's own file
|
|
178
|
+
// gone; it raises rather than records. A document claiming otherwise
|
|
179
|
+
// describes a run that cannot have happened.
|
|
180
|
+
if (r.detail.restoreSkipped && doc.run.mode === 'in-place') {
|
|
181
|
+
errors.push({ path: `${p}/detail/restoreSkipped`, message: 'restoreSkipped cannot occur in in-place mode, where a missing target is raised' });
|
|
182
|
+
}
|
|
107
183
|
});
|
|
108
184
|
return errors;
|
|
109
185
|
},
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "urn:claimspec:v1:baseline",
|
|
4
|
+
"title": "claimspec v1 — baseline",
|
|
5
5
|
"description": "Frozen fingerprints of findings that existed when the baseline was taken. A finding whose fingerprint appears here is suppressed up to `count` occurrences; everything else is new and gates. Only non-passing verdicts are ever fingerprinted.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["schemaVersion", "tool", "createdAt", "fingerprints"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": { "type": "string" },
|
|
10
10
|
"schemaVersion": { "const": 1 },
|
|
11
|
-
"tool": { "$ref": "urn:
|
|
12
|
-
"createdAt": { "$ref": "urn:
|
|
13
|
-
"head": { "$ref": "urn:
|
|
11
|
+
"tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
|
|
12
|
+
"createdAt": { "$ref": "urn:claimspec:v1:common#/$defs/isoDateTime" },
|
|
13
|
+
"head": { "$ref": "urn:claimspec:v1:common#/$defs/gitSha" },
|
|
14
14
|
"dirty": {
|
|
15
15
|
"description": "Whether the working tree had uncommitted changes when the evidence behind this baseline was taken.",
|
|
16
16
|
"type": "boolean"
|
|
17
17
|
},
|
|
18
18
|
"snapshot": {
|
|
19
19
|
"description": "When the evidence behind this baseline was a working-tree snapshot: that throwaway commit. `head` is then the parent of the commit that will carry the tests; `baseline --restamp` moves `head` once a clean probe reproduces the same fingerprints.",
|
|
20
|
-
"$ref": "urn:
|
|
20
|
+
"$ref": "urn:claimspec:v1:common#/$defs/gitSha"
|
|
21
21
|
},
|
|
22
22
|
"restampedAt": {
|
|
23
23
|
"description": "Set when `head` was moved by `baseline --restamp` (same fingerprints, clean tree). `createdAt` keeps the original freeze time.",
|
|
24
|
-
"$ref": "urn:
|
|
24
|
+
"$ref": "urn:claimspec:v1:common#/$defs/isoDateTime"
|
|
25
25
|
},
|
|
26
26
|
"fingerprints": {
|
|
27
27
|
"type": "object",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "urn:claimspec:v1:brief",
|
|
4
|
+
"title": "claimspec v1 — agent brief",
|
|
5
5
|
"description": "What to tell an agent before it writes code. `text` is the rendered block for an additionalContext payload; `items` is the same content structured so other tools can compose briefs. Ranked, capped, and never a single score.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["schemaVersion", "tool", "generatedAt", "heading", "summary", "items", "text"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": { "type": "string" },
|
|
10
10
|
"schemaVersion": { "const": 1 },
|
|
11
|
-
"tool": { "$ref": "urn:
|
|
12
|
-
"generatedAt": { "$ref": "urn:
|
|
13
|
-
"head": { "$ref": "urn:
|
|
11
|
+
"tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
|
|
12
|
+
"generatedAt": { "$ref": "urn:claimspec:v1:common#/$defs/isoDateTime" },
|
|
13
|
+
"head": { "$ref": "urn:claimspec:v1:common#/$defs/gitSha" },
|
|
14
14
|
"heading": {
|
|
15
15
|
"description": "The markdown heading that opens `text`, e.g. `## TEST BLINDSPOT CONTEXT`.",
|
|
16
16
|
"type": "string",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"required": ["ref", "files"],
|
|
52
52
|
"properties": {
|
|
53
53
|
"ref": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
54
|
-
"files": { "type": "array", "minItems": 1, "items": { "$ref": "urn:
|
|
54
|
+
"files": { "type": "array", "minItems": 1, "items": { "$ref": "urn:claimspec:v1:common#/$defs/uncoveredFile" } }
|
|
55
55
|
},
|
|
56
56
|
"additionalProperties": false
|
|
57
57
|
},
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
"type": "object",
|
|
63
63
|
"required": ["fingerprint", "claimId", "statement", "verdict", "severity", "isNew"],
|
|
64
64
|
"properties": {
|
|
65
|
-
"fingerprint": { "$ref": "urn:
|
|
66
|
-
"claimId": { "$ref": "urn:
|
|
67
|
-
"subjectId": { "$ref": "urn:
|
|
65
|
+
"fingerprint": { "$ref": "urn:claimspec:v1:common#/$defs/sha256" },
|
|
66
|
+
"claimId": { "$ref": "urn:claimspec:v1:common#/$defs/identifier" },
|
|
67
|
+
"subjectId": { "$ref": "urn:claimspec:v1:common#/$defs/identifier" },
|
|
68
68
|
"statement": { "type": "string", "minLength": 1, "maxLength": 2000 },
|
|
69
|
-
"verdict": { "$ref": "urn:
|
|
70
|
-
"severity": { "$ref": "urn:
|
|
71
|
-
"file": { "$ref": "urn:
|
|
69
|
+
"verdict": { "$ref": "urn:claimspec:v1:common#/$defs/verdict" },
|
|
70
|
+
"severity": { "$ref": "urn:claimspec:v1:common#/$defs/severity" },
|
|
71
|
+
"file": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" },
|
|
72
72
|
"rank": { "type": "number" },
|
|
73
73
|
"hint": {
|
|
74
74
|
"description": "One line the agent can act on, e.g. which assertion shape left the gap.",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:
|
|
4
|
-
"title": "
|
|
3
|
+
"$id": "urn:claimspec:v1:calibration",
|
|
4
|
+
"title": "claimspec v1 — calibration",
|
|
5
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
6
|
"type": "object",
|
|
7
7
|
"required": ["schemaVersion", "tool", "computedAt", "method", "confidence", "bucketBy", "source", "buckets"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": { "type": "string" },
|
|
10
10
|
"schemaVersion": { "const": 1 },
|
|
11
|
-
"tool": { "$ref": "urn:
|
|
12
|
-
"computedAt": { "$ref": "urn:
|
|
11
|
+
"tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
|
|
12
|
+
"computedAt": { "$ref": "urn:claimspec:v1:common#/$defs/isoDateTime" },
|
|
13
13
|
"method": {
|
|
14
14
|
"description": "Interval method. `wilson` is the only method defined in v1.",
|
|
15
15
|
"enum": ["wilson"]
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"confidence": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1 },
|
|
18
18
|
"bucketBy": {
|
|
19
19
|
"description": "The field findings are bucketed on, e.g. `faultClass` or `severity`.",
|
|
20
|
-
"$ref": "urn:
|
|
20
|
+
"$ref": "urn:claimspec:v1:common#/$defs/identifier"
|
|
21
21
|
},
|
|
22
22
|
"source": {
|
|
23
23
|
"description": "Where the ground-truth labels came from.",
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "urn:
|
|
4
|
-
"title": "
|
|
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": [
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"claims"
|
|
10
|
+
],
|
|
8
11
|
"properties": {
|
|
9
|
-
"$schema": {
|
|
10
|
-
|
|
12
|
+
"$schema": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"schemaVersion": {
|
|
16
|
+
"const": 1
|
|
17
|
+
},
|
|
11
18
|
"claims": {
|
|
12
19
|
"type": "array",
|
|
13
|
-
"items": {
|
|
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": [
|
|
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:
|
|
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": [
|
|
51
|
+
"required": [
|
|
52
|
+
"kind"
|
|
53
|
+
],
|
|
36
54
|
"properties": {
|
|
37
|
-
"kind": {
|
|
38
|
-
|
|
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": {
|
|
43
|
-
|
|
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": {
|
|
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": {
|
|
89
|
+
"items": {
|
|
90
|
+
"$ref": "#/$defs/fault"
|
|
91
|
+
}
|
|
54
92
|
},
|
|
55
93
|
"tags": {
|
|
56
94
|
"type": "array",
|
|
57
|
-
"items": {
|
|
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.
|
|
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": [
|
|
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:
|
|
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
|
|
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": {
|
|
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
|
}
|