testguard-cli 0.15.1 → 0.17.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 (64) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +196 -17
  3. package/package.json +1 -1
  4. package/spec/GATE-SEMANTICS.md +316 -0
  5. package/spec/README.md +27 -2
  6. package/spec/lib/authoring-input.mjs +40 -0
  7. package/spec/lib/origins.mjs +56 -0
  8. package/spec/lib/validate.mjs +129 -1
  9. package/spec/schemas/annotations.schema.json +65 -0
  10. package/spec/schemas/authoring-input.schema.json +67 -0
  11. package/spec/schemas/brief.schema.json +2 -0
  12. package/spec/schemas/claims.schema.json +8 -8
  13. package/spec/schemas/common.schema.json +73 -0
  14. package/spec/schemas/evidence.schema.json +41 -8
  15. package/spec/schemas/status.schema.json +31 -2
  16. package/src/brief/brief.mjs +11 -4
  17. package/src/claims/annotation-placement.mjs +357 -0
  18. package/src/claims/annotations.mjs +61 -2
  19. package/src/cli.mjs +40 -5
  20. package/src/commands/admit.mjs +21 -3
  21. package/src/commands/annotations.mjs +67 -0
  22. package/src/commands/claims.mjs +46 -6
  23. package/src/commands/probe.mjs +50 -7
  24. package/src/commands/replay.mjs +7 -0
  25. package/src/commands/scaffold.mjs +49 -2
  26. package/src/commands/sweep.mjs +7 -0
  27. package/src/evidence/writer.mjs +12 -4
  28. package/src/gate/changed.mjs +5 -1
  29. package/src/git.mjs +4 -4
  30. package/src/init/init.mjs +6 -0
  31. package/src/init/templates/SKILL.md +78 -0
  32. package/src/probe/attribution.mjs +19 -0
  33. package/src/probe/contention.mjs +20 -2
  34. package/src/probe/cost.mjs +22 -9
  35. package/src/probe/inject.mjs +2 -6
  36. package/src/probe/origin-policy.mjs +69 -0
  37. package/src/probe/policy-freshness.mjs +74 -0
  38. package/src/probe/probe.mjs +74 -45
  39. package/src/probe/runners/discovery.mjs +2 -0
  40. package/src/probe/runners/jest.mjs +2 -2
  41. package/src/probe/runners/lifecycle.mjs +58 -0
  42. package/src/probe/runners/playwright.mjs +6 -2
  43. package/src/probe/runners/python.mjs +10 -4
  44. package/src/probe/runners/shared.mjs +12 -6
  45. package/src/probe/runners/vitest.mjs +3 -3
  46. package/src/probe/universe.mjs +47 -0
  47. package/src/probe/worktree.mjs +3 -1
  48. package/src/render.mjs +17 -1
  49. package/src/replay/replay.mjs +8 -4
  50. package/src/scaffold/admission.mjs +117 -0
  51. package/src/scaffold/append.mjs +41 -0
  52. package/src/scaffold/apply-append.mjs +71 -0
  53. package/src/scaffold/fix-input.mjs +119 -0
  54. package/src/scaffold/fix-paths.mjs +90 -0
  55. package/src/scaffold/intent.mjs +2 -0
  56. package/src/scaffold/open-append.mjs +44 -0
  57. package/src/scaffold/plan-append.mjs +37 -0
  58. package/src/scaffold/plan-intent-input.mjs +38 -0
  59. package/src/scaffold/prepare-append.mjs +46 -0
  60. package/src/scaffold/request.mjs +30 -0
  61. package/src/scaffold/scaffold.mjs +23 -10
  62. package/src/status/status.mjs +42 -10
  63. package/src/sweep/pool.mjs +33 -0
  64. package/src/sweep/sweep.mjs +9 -4
@@ -4,10 +4,13 @@ import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fingerprint } from './fingerprint.mjs';
6
6
  import { reproduces, decimals, MAX_PLACES } from './wilson.mjs';
7
+ import { isDeepStrictEqual } from 'node:util';
8
+ import { recordedOriginSummary } from './origins.mjs';
9
+ import { authoringInputErrors } from './authoring-input.mjs';
7
10
 
8
11
  const schemaDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'schemas');
9
12
 
10
- export const KINDS = Object.freeze(['claims', 'evidence', 'baseline', 'ignore', 'calibration', 'brief', 'status', 'gate', 'replay', 'sweep', 'concerns']);
13
+ export const KINDS = Object.freeze(['claims', 'evidence', 'baseline', 'ignore', 'calibration', 'brief', 'status', 'gate', 'replay', 'sweep', 'concerns', 'annotations', 'authoring-input']);
11
14
  /**
12
15
  * How a document says it tried to falsify its claims. Absent means
13
16
  * `fault-injection`, so every document written before the field existed is
@@ -46,8 +49,113 @@ const nestedErrors = (nested = [], prefix = '/nested', occupied = []) => {
46
49
  return errors;
47
50
  };
48
51
 
52
+ // A projected summary is not source authority. Only evidence retains the
53
+ // complete claim projections needed to reproduce each kind and mixed count.
54
+ function originPolicyErrors(kind, doc) {
55
+ const errors = [];
56
+ const policies = [[doc.originPolicy, '/originPolicy'], [kind === 'status' ? doc.run?.originPolicy : undefined, '/run/originPolicy']];
57
+ const identities = (rows) => rows.map((r) => JSON.stringify([r.claimId, r.faultId])).sort();
58
+ for (const [p, path] of policies) {
59
+ if (!p) continue;
60
+ const fail = (message) => errors.push({ path, message });
61
+ for (const field of ['eligibleKinds', 'ineligibleClaims', 'unavailableReasons']) {
62
+ if (!isDeepStrictEqual(p[field], [...p[field]].sort())) fail(`${field} must be normalized in sorted order`);
63
+ }
64
+ const failures = p.ineligibleClaims.length + p.nonKilledFaults.length;
65
+ const expectedState = p.unavailableReasons.length ? 'unavailable' : failures ? 'failed' : 'passed';
66
+ if (p.state !== expectedState) fail('policy state must match refusal reasons and unsuppressed failures');
67
+ if (p.state !== 'unavailable' && (!p.claims || !p.faults)) fail('an evaluated policy requires a nonempty claim and fault universe');
68
+ if (p.ineligibleClaims.length > p.claims) fail('ineligible claims exceed the current claim denominator');
69
+ if (p.state !== 'unavailable' && p.nonKilledFaults.length > p.faults) fail('non-killed faults exceed the complete fault denominator');
70
+ if (kind === 'evidence') {
71
+ const actual = [...new Set(doc.records.filter((r) => r.verdict !== 'killed').map((r) => JSON.stringify([r.claim.id, r.subject.id])))].sort();
72
+ if (!isDeepStrictEqual(identities(p.nonKilledFaults), actual)) fail('policy must retain every actual non-killed fault identity');
73
+ if (p.state !== 'unavailable') {
74
+ const claims = new Set(doc.records.map((r) => r.claim.id));
75
+ const faults = new Set(doc.records.map((r) => JSON.stringify([r.claim.id, r.subject.id])));
76
+ if (p.claims !== claims.size || p.faults !== faults.size || faults.size !== doc.records.length) fail('evaluated policy denominators require complete unique recorded identities');
77
+ const ineligible = [...new Set(doc.records.filter((r) => !p.eligibleKinds.includes(r.claim.source.kind)).map((r) => r.claim.id))].sort();
78
+ if (!isDeepStrictEqual(p.ineligibleClaims, ineligible)) fail('policy eligibility must match recorded declarations');
79
+ if (!isInjection(doc.run) || doc.run.provisional || doc.run.confirmRuns < 3) fail('evaluated policy requires confirmed fault-injection evidence');
80
+ if (recordedOriginSummary(doc.records).claims.mixed) fail('evaluated policy cannot use mixed recorded origins');
81
+ }
82
+ }
83
+ if (kind === 'status' && (p.claims !== doc.counts.claims || p.faults !== doc.counts.faults)) fail('policy denominators must match current status claims and faults');
84
+ if (kind === 'brief' && p.state !== 'unavailable' && (p.claims !== doc.summary.claims || p.faults !== Object.values(doc.summary.byVerdict).reduce((a, b) => a + b, 0))) fail('recorded policy denominators must match the uncapped brief summary');
85
+ }
86
+ if (kind === 'status' && doc.run) {
87
+ const p = doc.run.originPolicy;
88
+ const expected = p?.state === 'unavailable' ? 2 : p?.state === 'failed' || doc.run.newSinceBaseline > 0 ? 1 : 0;
89
+ if (p && doc.run.exitCode !== expected) errors.push({ path: '/run/exitCode', message: 'exit code must preserve policy refusal/failure before baseline findings' });
90
+ if (!p && doc.run.exitCode === 2) errors.push({ path: '/run/exitCode', message: 'completed probe exit 2 requires an unavailable origin policy' });
91
+ if (p && (doc.run.scope || doc.run.provisional)) errors.push({ path: '/run/originPolicy', message: 'origin policy cannot certify a partial or provisional invocation' });
92
+ }
93
+ return errors;
94
+ }
95
+
96
+ function originErrors(kind, doc) {
97
+ if (!doc.origins) return [];
98
+ const errors = [], o = doc.origins;
99
+ const fail = (message) => errors.push({ path: '/origins', message });
100
+ const sum = (counts) => Object.values(counts).reduce((a, b) => a + b, 0);
101
+ if (sum(o.claims.byKind) + o.claims.mixed !== o.claims.total) fail('claim origin buckets plus mixed must equal the distinct claim total');
102
+ if (o.records && sum(o.records.byKind) !== o.records.total) fail('record origin buckets must equal the record total');
103
+ if (kind === 'status') {
104
+ if (o.basis !== 'declared' || o.records || o.claims.mixed !== 0) fail('status origins must describe unmixed current declarations without recorded counts');
105
+ if (o.claims.total !== doc.counts.claims) fail('declared origin total must equal current claim count');
106
+ } else {
107
+ if (o.basis !== 'recorded' || !o.records) fail('evidence and brief origins require recorded basis and record counts');
108
+ if (kind === 'evidence' && !isDeepStrictEqual(o, recordedOriginSummary(doc.records))) fail('recorded origins must reproduce every actual record and distinct claim declaration');
109
+ if (kind === 'brief' && (o.claims.total !== doc.summary.claims || o.records?.total !== sum(doc.summary.byVerdict))) fail('brief origin totals must match the complete summary, not capped items');
110
+ }
111
+ return errors;
112
+ }
113
+
49
114
  // Rules JSON Schema cannot express. Each returns an array of {path, message}.
50
115
  const semantic = {
116
+ 'authoring-input': authoringInputErrors,
117
+ annotations(doc) {
118
+ const errors = [];
119
+ const fail = (path, message) => errors.push({ path, message });
120
+ const selected = new Set(doc.selected), seenFiles = new Set(), seenIds = new Set();
121
+ for (const [bucket, entries] of [['targets', doc.targets], ['refused', doc.refused]]) {
122
+ for (const [i, entry] of entries.entries()) {
123
+ if (seenFiles.has(entry.file)) fail(`/${bucket}/${i}/file`, 'target must occur in exactly one admission bucket');
124
+ if (bucket === 'targets' && (entry.file.includes('\\') || /^[A-Za-z]:/.test(entry.file) || entry.file.split('/').some((part) => !part || part === '.' || part === '..'))) fail(`/${bucket}/${i}/file`, 'admitted target must be a canonical relative path');
125
+ seenFiles.add(entry.file);
126
+ for (const id of entry.claimIds) {
127
+ if (!selected.has(id)) fail(`/${bucket}/${i}/claimIds`, 'target claim must be selected');
128
+ seenIds.add(id);
129
+ }
130
+ }
131
+ }
132
+ if (doc.selected.some((id) => !seenIds.has(id))) fail('/selected', 'every selected claim must have a target or explicit refusal');
133
+ if (doc.mode === 'preview') {
134
+ if (doc.outcome) fail('/outcome', 'preview never has a write outcome');
135
+ if (doc.state !== (doc.refused.length ? 'refused' : 'preview')) fail('/state', 'preview state must reflect all admission refusals');
136
+ return errors;
137
+ }
138
+ if (!doc.outcome) { fail('/outcome', 'apply requires a write outcome'); return errors; }
139
+ if (doc.refused.length && doc.state !== 'refused') fail('/state', 'any admission refusal blocks all writes');
140
+ const o = doc.outcome;
141
+ const adds = doc.targets.filter((t) => t.action === 'add').map((t) => t.file);
142
+ const unchanged = doc.targets.filter((t) => t.action === 'unchanged').map((t) => t.file);
143
+ const same = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
144
+ if (o.touched.some((file) => !adds.includes(file))) fail('/outcome/touched', 'only admitted add targets can be touched');
145
+ if (o.changed.some((file) => !o.touched.includes(file))) fail('/outcome/changed', 'verified changes must be touched');
146
+ if (!same(o.unchanged, unchanged)) fail('/outcome/unchanged', 'unchanged files must match unchanged targets');
147
+ for (const [i, failure] of o.failed.entries()) {
148
+ if (failure.file !== null && !doc.targets.some((t) => t.file === failure.file)) fail(`/outcome/failed/${i}/file`, 'failed file must be an admitted target or null');
149
+ }
150
+ if (doc.state === 'applied') {
151
+ if (doc.refused.length || o.failed.length || !same(o.changed, adds) || !same(o.touched, adds) || !o.recoveryDir || o.lockRelease?.released !== true) fail('/state', 'applied requires every admitted target verified, no refusals/failures, recovery and released owned lock');
152
+ } else if (doc.state === 'refused') {
153
+ if (o.touched.length || o.changed.length || (!doc.refused.length && !o.failed.length)) fail('/state', 'refused requires no touched files and an explicit refusal/failure');
154
+ } else if (doc.state === 'partial') {
155
+ if (!o.touched.length || !o.failed.length || !o.recoveryDir) fail('/state', 'partial requires touched files, explicit failure and retained recovery');
156
+ } else fail('/state', 'apply cannot be a preview');
157
+ return errors;
158
+ },
51
159
  claims(doc) {
52
160
  const errors = [];
53
161
  const seenClaim = new Set();
@@ -75,6 +183,11 @@ const semantic = {
75
183
 
76
184
  evidence(doc) {
77
185
  const errors = [];
186
+ if (doc.run.measurements) {
187
+ const m = doc.run.measurements;
188
+ if (Math.abs(m.elapsedMs - m.runnerMs - m.overheadMs) > 0.001) errors.push({ path: '/run/measurements', message: 'elapsedMs must equal runnerMs plus overheadMs' });
189
+ }
190
+ if (doc.run.serial && doc.run.workers !== undefined && doc.run.workers !== 1) errors.push({ path: '/run/workers', message: 'serial execution requires one worker' });
78
191
  const injection = isInjection(doc.run);
79
192
  const n = doc.run.confirmRuns;
80
193
  // N-run agreement is what fault injection means by "confirmed". A method
@@ -146,6 +259,15 @@ const semantic = {
146
259
  if (r.detail.undeclaredKillers && r.detail.reason !== 'killed-by-undeclared-tests') {
147
260
  errors.push({ path: `${p}/detail/undeclaredKillers`, message: 'undeclaredKillers is only meaningful with reason killed-by-undeclared-tests' });
148
261
  }
262
+ if (r.defenders.selectionSource) {
263
+ const empty = r.defenders.requested.length === 0;
264
+ if ((r.defenders.selectionSource === 'discovery' && !empty)
265
+ || (r.defenders.selectionSource === 'claim' && empty)
266
+ || (empty && r.defenders.discovered !== true)
267
+ || (!empty && r.defenders.discovered === true)) {
268
+ errors.push({ path: `${p}/defenders/selectionSource`, message: 'defender selection origin, requested set and discovery flag disagree' });
269
+ }
270
+ }
149
271
  if (r.defenders.byRunner) {
150
272
  const resolved = new Set(r.defenders.resolved);
151
273
  for (const [runner, files] of Object.entries(r.defenders.byRunner)) for (const f of files) if (!resolved.has(f)) errors.push({ path: `${p}/defenders/byRunner/${runner}`, message: `${f} ran under ${runner} but is not a resolved defender` });
@@ -261,6 +383,10 @@ const semantic = {
261
383
 
262
384
  status(doc) {
263
385
  const errors = [];
386
+ if (doc.cost?.measurements) {
387
+ const m = doc.cost.measurements;
388
+ if (Math.abs(m.elapsedMs - m.runnerMs - m.overheadMs) > 0.001) errors.push({ path: '/cost/measurements', message: 'elapsedMs must equal runnerMs plus overheadMs' });
389
+ }
264
390
  if (doc.changes?.nested) {
265
391
  errors.push(...nestedErrors(doc.changes.nested, '/changes/nested', doc.changes.uncovered.map((file) => file.file)));
266
392
  const nestedCount = doc.changes.nested.reduce((count, entry) => count + entry.files.length, 0);
@@ -463,5 +589,7 @@ export function validate(kind, doc) {
463
589
  };
464
590
  }
465
591
  const errors = semantic[kind] ? semantic[kind](doc) : [];
592
+ errors.push(...originErrors(kind, doc));
593
+ errors.push(...originPolicyErrors(kind, doc));
466
594
  return { ok: errors.length === 0, errors };
467
595
  }
@@ -0,0 +1,65 @@
1
+ {
2
+ "$id": "urn:claimspec:v1:annotations",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "claimspec v1 — annotation authoring",
5
+ "description": "File-level authoring only. Not verification or intent authentication. No source contents or reusable approval token.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "tool", "mode", "state", "selected", "targets", "refused"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
12
+ "mode": { "enum": ["preview", "apply"] },
13
+ "state": { "enum": ["preview", "applied", "refused", "partial"] },
14
+ "selected": { "$ref": "#/$defs/ids" },
15
+ "targets": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "object", "additionalProperties": false, "required": ["file", "claimIds", "action"],
19
+ "properties": {
20
+ "file": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" },
21
+ "claimIds": { "type": "array", "$ref": "#/$defs/ids", "minItems": 1 },
22
+ "action": { "enum": ["add", "unchanged"] }
23
+ }
24
+ }
25
+ },
26
+ "refused": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object", "additionalProperties": false, "required": ["file", "claimIds", "reason"],
30
+ "properties": {
31
+ "file": { "type": "string", "minLength": 1 },
32
+ "claimIds": { "type": "array", "$ref": "#/$defs/ids", "minItems": 1 },
33
+ "reason": { "enum": ["unsupported-claim-id", "unsupported-method", "unsafe-path", "excluded-target", "unsupported-source", "symlink-target", "not-regular-file", "nested-project", "source-too-large", "target-missing", "target-unreadable", "target-changed", "invalid-encoding", "mixed-newlines", "plan-source-budget", "proposed-source-too-large", "anchor-missing", "anchor-ambiguous"] }
34
+ }
35
+ }
36
+ },
37
+ "outcome": {
38
+ "type": "object", "additionalProperties": false, "required": ["changed", "touched", "unchanged", "failed"],
39
+ "properties": {
40
+ "changed": { "$ref": "#/$defs/files" },
41
+ "touched": { "$ref": "#/$defs/files" },
42
+ "unchanged": { "$ref": "#/$defs/files" },
43
+ "failed": {
44
+ "type": "array", "items": {
45
+ "type": "object", "additionalProperties": false, "required": ["file", "stage", "reason"],
46
+ "properties": {
47
+ "file": { "type": ["string", "null"] },
48
+ "stage": { "enum": ["preparation", "all-target-preflight", "started-journal", "target-preflight", "source-write", "write-verification", "completion-journal", "lock-release", "result-journal"] },
49
+ "reason": { "type": "string", "minLength": 1 }
50
+ }
51
+ }
52
+ },
53
+ "recoveryDir": { "type": "string", "minLength": 1 },
54
+ "lockRelease": {
55
+ "type": "object", "additionalProperties": false, "required": ["released"],
56
+ "properties": { "released": { "type": "boolean" }, "reason": { "type": "string", "minLength": 1 } }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ "$defs": {
62
+ "ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "urn:claimspec:v1:common#/$defs/identifier" } },
63
+ "files": { "type": "array", "uniqueItems": true, "items": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" } }
64
+ }
65
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "$id": "urn:claimspec:v1:authoring-input",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "title": "claimspec v1 — read-only authoring input",
5
+ "description": "Metadata-only input handoff. Not claims, verification, freshness authority or authenticated intent. No source text or absolute roots.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "tool", "purpose", "verification", "next", "input"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
12
+ "purpose": { "const": "authoring-input" },
13
+ "verification": { "const": "not-performed" },
14
+ "next": { "const": "supply-independent-intent" },
15
+ "input": { "oneOf": [{ "$ref": "#/$defs/document" }, { "$ref": "#/$defs/fix" }] }
16
+ },
17
+ "$defs": {
18
+ "objectId": { "type": "string", "pattern": "^(?:[a-f0-9]{40}|[a-f0-9]{64})$" },
19
+ "document": {
20
+ "type": "object", "additionalProperties": false,
21
+ "required": ["kind", "file", "hash", "bytes"],
22
+ "properties": {
23
+ "kind": { "const": "document" },
24
+ "file": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" },
25
+ "hash": { "$ref": "urn:claimspec:v1:common#/$defs/sha256" },
26
+ "bytes": { "type": "integer", "minimum": 0, "maximum": 2097152 }
27
+ }
28
+ },
29
+ "fix": {
30
+ "type": "object", "additionalProperties": false,
31
+ "required": ["kind", "commit", "parent", "subject", "scope", "paths", "counts"],
32
+ "properties": {
33
+ "kind": { "const": "fix" },
34
+ "commit": { "$ref": "#/$defs/objectId" },
35
+ "parent": { "$ref": "#/$defs/objectId" },
36
+ "subject": { "type": "string", "maxLength": 1024 },
37
+ "scope": { "enum": ["project-root", "selected-nested-project"] },
38
+ "paths": { "type": "array", "maxItems": 256, "items": { "$ref": "#/$defs/path" } },
39
+ "counts": {
40
+ "type": "object", "additionalProperties": false,
41
+ "required": ["total", "supported", "deleted", "unsupported", "excluded"],
42
+ "properties": {
43
+ "total": { "$ref": "#/$defs/count" },
44
+ "supported": { "$ref": "#/$defs/count" },
45
+ "deleted": { "$ref": "#/$defs/count" },
46
+ "unsupported": { "$ref": "#/$defs/count" },
47
+ "excluded": { "$ref": "#/$defs/count" }
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "count": { "type": "integer", "minimum": 0, "maximum": 256 },
53
+ "path": {
54
+ "type": "object", "additionalProperties": false,
55
+ "required": ["file", "status", "oldMode", "newMode", "oldId", "newId", "disposition"],
56
+ "properties": {
57
+ "file": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" },
58
+ "status": { "enum": ["A", "M", "D", "T"] },
59
+ "oldMode": { "type": "string", "pattern": "^[0-7]{6}$" },
60
+ "newMode": { "type": "string", "pattern": "^[0-7]{6}$" },
61
+ "oldId": { "$ref": "#/$defs/objectId" },
62
+ "newId": { "$ref": "#/$defs/objectId" },
63
+ "disposition": { "enum": ["supported", "deleted", "unsupported"] }
64
+ }
65
+ }
66
+ }
67
+ }
@@ -6,6 +6,8 @@
6
6
  "type": "object",
7
7
  "required": ["schemaVersion", "tool", "generatedAt", "heading", "summary", "items", "text"],
8
8
  "properties": {
9
+ "originPolicy": { "$ref": "urn:claimspec:v1:common#/$defs/originPolicy" },
10
+ "origins": { "$ref": "urn:claimspec:v1:common#/$defs/originSummary" },
9
11
  "$schema": { "type": "string" },
10
12
  "schemaVersion": { "const": 1 },
11
13
  "tool": { "$ref": "urn:claimspec:v1:common#/$defs/tool" },
@@ -46,20 +46,14 @@
46
46
  "maxLength": 2000
47
47
  },
48
48
  "source": {
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.",
49
+ "description": "Declared origin of claim intent. A source label or opaque reference does not authenticate independence, chronology or correctness. Producer provenance and measured test independence are separate.",
50
50
  "type": "object",
51
51
  "required": [
52
52
  "kind"
53
53
  ],
54
54
  "properties": {
55
55
  "kind": {
56
- "enum": [
57
- "spec",
58
- "adr",
59
- "annotation",
60
- "comment",
61
- "manual"
62
- ]
56
+ "$ref": "urn:claimspec:v1:common#/$defs/claimSourceKind"
63
57
  },
64
58
  "ref": {
65
59
  "type": "string",
@@ -121,6 +115,12 @@
121
115
  "faultClass": {
122
116
  "$ref": "urn:claimspec:v1:common#/$defs/faultClass"
123
117
  },
118
+ "defendedBy": {
119
+ "description": "Overrides this fault's claim-level defenders when present. An explicit empty list requests discovery; absence inherits the claim. Selection changes require re-probing, never assumed detection.",
120
+ "type": "array",
121
+ "items": { "type": "string", "minLength": 1 },
122
+ "uniqueItems": true
123
+ },
124
124
  "file": {
125
125
  "$ref": "urn:claimspec:v1:common#/$defs/repoPath"
126
126
  },
@@ -4,6 +4,79 @@
4
4
  "title": "claimspec v1 \u2014 shared definitions",
5
5
  "description": "Definitions shared by every Guard-spec format. Referenced as urn:claimspec:v1:common#/$defs/<name>.",
6
6
  "$defs": {
7
+ "originPolicy": {
8
+ "description": "An explicitly selected declared-origin policy, not authenticated independence. Stored results describe their run, not current freshness authority.",
9
+ "type": "object",
10
+ "required": ["state", "eligibleKinds", "claims", "faults", "ineligibleClaims", "nonKilledFaults", "unavailableReasons"],
11
+ "properties": {
12
+ "state": { "enum": ["passed", "failed", "unavailable"] },
13
+ "eligibleKinds": { "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true, "items": { "$ref": "#/$defs/claimSourceKind" } },
14
+ "claims": { "$ref": "#/$defs/originCount" },
15
+ "faults": { "$ref": "#/$defs/originCount" },
16
+ "ineligibleClaims": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/identifier" } },
17
+ "nonKilledFaults": {
18
+ "type": "array", "uniqueItems": true,
19
+ "items": {
20
+ "type": "object", "required": ["claimId", "faultId"],
21
+ "properties": { "claimId": { "$ref": "#/$defs/identifier" }, "faultId": { "$ref": "#/$defs/identifier" } },
22
+ "additionalProperties": false
23
+ }
24
+ },
25
+ "unavailableReasons": {
26
+ "type": "array", "uniqueItems": true,
27
+ "items": { "enum": ["invalid-claims", "invalid-evidence", "empty-universe", "stale-inputs", "provisional-evidence", "incompatible-method", "missing-record", "extra-record", "duplicate-record", "claim-metadata-mismatch", "fault-mismatch"] }
28
+ }
29
+ },
30
+ "additionalProperties": false
31
+ },
32
+ "originCount": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
33
+ "originCounts": {
34
+ "type": "object",
35
+ "required": ["spec", "adr", "annotation", "comment", "manual", "doc", "bug", "incident", "review", "inferred"],
36
+ "properties": {
37
+ "spec": { "$ref": "#/$defs/originCount" },
38
+ "adr": { "$ref": "#/$defs/originCount" },
39
+ "annotation": { "$ref": "#/$defs/originCount" },
40
+ "comment": { "$ref": "#/$defs/originCount" },
41
+ "manual": { "$ref": "#/$defs/originCount" },
42
+ "doc": { "$ref": "#/$defs/originCount" },
43
+ "bug": { "$ref": "#/$defs/originCount" },
44
+ "incident": { "$ref": "#/$defs/originCount" },
45
+ "review": { "$ref": "#/$defs/originCount" },
46
+ "inferred": { "$ref": "#/$defs/originCount" }
47
+ },
48
+ "additionalProperties": false
49
+ },
50
+ "originSummary": {
51
+ "description": "Declared origins, not authenticated independence. Distinct claim identities and actual recorded projections have separate denominators. Evidence validation recomputes this from every record; projected status/brief documents can only bind arithmetic and totals.",
52
+ "type": "object",
53
+ "required": ["basis", "claims"],
54
+ "properties": {
55
+ "basis": { "enum": ["declared", "recorded"] },
56
+ "claims": {
57
+ "type": "object", "required": ["total", "byKind", "mixed"],
58
+ "properties": {
59
+ "total": { "$ref": "#/$defs/originCount" },
60
+ "byKind": { "$ref": "#/$defs/originCounts" },
61
+ "mixed": { "$ref": "#/$defs/originCount" }
62
+ },
63
+ "additionalProperties": false
64
+ },
65
+ "records": {
66
+ "type": "object", "required": ["total", "byKind"],
67
+ "properties": {
68
+ "total": { "$ref": "#/$defs/originCount" },
69
+ "byKind": { "$ref": "#/$defs/originCounts" }
70
+ },
71
+ "additionalProperties": false
72
+ }
73
+ },
74
+ "additionalProperties": false
75
+ },
76
+ "claimSourceKind": {
77
+ "description": "Declared origin of claim intent, not authenticated independence or an evidence-strength order. References remain opaque; producer provenance and measured test independence are separate.",
78
+ "enum": ["spec", "adr", "annotation", "comment", "manual", "doc", "bug", "incident", "review", "inferred"]
79
+ },
7
80
  "nestedProject": {
8
81
  "description": "Changed files delegated to a separately validated descendant TestGuard project. This is not coverage or evidence; run that project's gate separately.",
9
82
  "type": "object",
@@ -11,6 +11,8 @@
11
11
  "records"
12
12
  ],
13
13
  "properties": {
14
+ "originPolicy": { "$ref": "urn:claimspec:v1:common#/$defs/originPolicy" },
15
+ "origins": { "$ref": "urn:claimspec:v1:common#/$defs/originSummary" },
14
16
  "$schema": {
15
17
  "type": "string"
16
18
  },
@@ -168,6 +170,39 @@
168
170
  },
169
171
  "method": {
170
172
  "$ref": "urn:claimspec:v1:common#/$defs/method"
173
+ },
174
+ "measurements": {
175
+ "type": "object",
176
+ "required": [
177
+ "elapsedMs",
178
+ "runnerMs",
179
+ "overheadMs",
180
+ "runnerInvocations"
181
+ ],
182
+ "properties": {
183
+ "elapsedMs": {
184
+ "type": "number",
185
+ "minimum": 0
186
+ },
187
+ "runnerMs": {
188
+ "type": "number",
189
+ "minimum": 0
190
+ },
191
+ "overheadMs": {
192
+ "type": "number",
193
+ "minimum": 0
194
+ },
195
+ "runnerInvocations": {
196
+ "type": "integer",
197
+ "minimum": 0
198
+ }
199
+ },
200
+ "additionalProperties": false
201
+ },
202
+ "workers": {
203
+ "type": "integer",
204
+ "minimum": 1,
205
+ "description": "Requested built-in runner worker ceiling; Python is serial. Absent for custom commands, whose concurrency is user-managed."
171
206
  }
172
207
  },
173
208
  "additionalProperties": false,
@@ -309,13 +344,7 @@
309
344
  ],
310
345
  "properties": {
311
346
  "kind": {
312
- "enum": [
313
- "spec",
314
- "adr",
315
- "annotation",
316
- "comment",
317
- "manual"
318
- ]
347
+ "$ref": "urn:claimspec:v1:common#/$defs/claimSourceKind"
319
348
  },
320
349
  "ref": {
321
350
  "type": "string",
@@ -520,6 +549,10 @@
520
549
  "nocover"
521
550
  ],
522
551
  "properties": {
552
+ "selectionSource": {
553
+ "description": "Origin of the requested defender selection: explicit fault override, inherited non-empty claim declaration, or automatic discovery. Optional only for legacy evidence; a changed origin invalidates reuse.",
554
+ "enum": ["fault", "claim", "discovery"]
555
+ },
523
556
  "requested": {
524
557
  "type": "array",
525
558
  "items": {
@@ -660,7 +693,7 @@
660
693
  "additionalProperties": false
661
694
  },
662
695
  "reusedFrom": {
663
- "description": "Run id whose verdict was reused because `inputs` were unchanged.",
696
+ "description": "Run id whose verdict was reused because fault, defender and recorded claim metadata inputs were unchanged. A changed claim statement, severity, source or producer requires a fresh probe.",
664
697
  "$ref": "urn:claimspec:v1:common#/$defs/identifier"
665
698
  },
666
699
  "rank": {
@@ -13,6 +13,8 @@
13
13
  "counts"
14
14
  ],
15
15
  "properties": {
16
+ "originPolicy": { "$ref": "urn:claimspec:v1:common#/$defs/originPolicy" },
17
+ "origins": { "$ref": "urn:claimspec:v1:common#/$defs/originSummary" },
16
18
  "$schema": {
17
19
  "type": "string"
18
20
  },
@@ -157,12 +159,13 @@
157
159
  "type": "object",
158
160
  "required": ["id", "evidence", "provisional", "records", "newSinceBaseline", "exitCode"],
159
161
  "properties": {
162
+ "originPolicy": { "$ref": "urn:claimspec:v1:common#/$defs/originPolicy" },
160
163
  "id": { "type": "string", "minLength": 1, "maxLength": 128 },
161
164
  "evidence": { "type": "string", "minLength": 1, "maxLength": 4096 },
162
165
  "provisional": { "type": "boolean" },
163
166
  "records": { "type": "integer", "minimum": 0 },
164
167
  "newSinceBaseline": { "type": "integer", "minimum": 0 },
165
- "exitCode": { "enum": [0, 1] },
168
+ "exitCode": { "enum": [0, 1, 2] },
166
169
  "scope": {
167
170
  "type": "object",
168
171
  "required": ["requestedClaims", "requestedCount", "probedClaims", "probedCount"],
@@ -191,10 +194,29 @@
191
194
  "type": "object",
192
195
  "required": ["totalMs", "totalRuns", "reusedRecords", "records", "claims", "defenders", "sharedDefenders"],
193
196
  "properties": {
197
+ "measurements": {"type": "object", "required": ["elapsedMs", "runnerMs", "overheadMs", "runnerInvocations"], "properties": {"elapsedMs": {"type": "number", "minimum": 0}, "runnerMs": {"type": "number", "minimum": 0}, "overheadMs": {"type": "number", "minimum": 0}, "runnerInvocations": {"type": "integer", "minimum": 0}}, "additionalProperties": false},
194
198
  "totalMs": { "type": "number", "minimum": 0 },
195
199
  "totalRuns": { "type": "integer", "minimum": 0 },
196
200
  "reusedRecords": { "type": "integer", "minimum": 0 },
197
201
  "records": { "type": "integer", "minimum": 0 },
202
+ "faults": {
203
+ "description": "Recorded costs and selected defenders per fault; optional for legacy reports.",
204
+ "type": "array",
205
+ "items": {
206
+ "type": "object",
207
+ "required": ["claimId", "faultId", "ms", "runs", "reused", "defenders"],
208
+ "properties": {
209
+ "claimId": { "$ref": "urn:claimspec:v1:common#/$defs/identifier" },
210
+ "faultId": { "$ref": "urn:claimspec:v1:common#/$defs/identifier" },
211
+ "ms": { "type": "number", "minimum": 0 },
212
+ "runs": { "type": "integer", "minimum": 0 },
213
+ "reused": { "type": "boolean" },
214
+ "selectionSource": { "enum": ["fault", "claim", "discovery"] },
215
+ "defenders": { "type": "array", "uniqueItems": true, "items": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" } }
216
+ },
217
+ "additionalProperties": false
218
+ }
219
+ },
198
220
  "claims": {
199
221
  "type": "array",
200
222
  "items": {
@@ -283,7 +305,7 @@
283
305
  }
284
306
  },
285
307
  "notes": {
286
- "description": "Informational lines that never change `state`: e.g. a baseline frozen from a snapshot whose head predates the commit that carries its tests, with the way to re-stamp it.",
308
+ "description": "Informational lines that never change state or next verification action: e.g. annotation-link advice (not verification or authenticated provenance), or a baseline snapshot with the way to re-stamp it.",
287
309
  "type": "array",
288
310
  "items": { "type": "string", "minLength": 1, "maxLength": 512 }
289
311
  },
@@ -483,6 +505,13 @@
483
505
  "file": { "$ref": "urn:claimspec:v1:common#/$defs/repoPath" },
484
506
  "ms": { "type": "number", "minimum": 0 },
485
507
  "runs": { "type": "integer", "minimum": 0 },
508
+ "faults": {
509
+ "description": "Distinct claimId/faultId identities selecting this file, not confirmation runs.",
510
+ "type": "array",
511
+ "minItems": 1,
512
+ "uniqueItems": true,
513
+ "items": { "type": "string", "minLength": 1 }
514
+ },
486
515
  "claims": {
487
516
  "type": "array",
488
517
  "minItems": 1,