sourcebound 1.2.1__py3-none-any.whl
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.
- clean_docs/__init__.py +26 -0
- clean_docs/__main__.py +3 -0
- clean_docs/accessibility.py +182 -0
- clean_docs/adapters/__init__.py +1 -0
- clean_docs/adapters/event_capture.py +56 -0
- clean_docs/adapters/mdx_dependencies.json +714 -0
- clean_docs/adapters/mdx_parser.mjs +29992 -0
- clean_docs/applicability.py +330 -0
- clean_docs/audit.py +1120 -0
- clean_docs/bootstrap.py +507 -0
- clean_docs/capabilities.py +200 -0
- clean_docs/changed.py +452 -0
- clean_docs/claims.py +840 -0
- clean_docs/cli.py +1612 -0
- clean_docs/context.py +307 -0
- clean_docs/corpus.py +377 -0
- clean_docs/demo.py +369 -0
- clean_docs/doctor.py +184 -0
- clean_docs/emit/__init__.py +4 -0
- clean_docs/emit/llms_txt.py +102 -0
- clean_docs/emit/stepwise.py +168 -0
- clean_docs/engine.py +324 -0
- clean_docs/errors.py +20 -0
- clean_docs/evaluation.py +867 -0
- clean_docs/execution.py +138 -0
- clean_docs/explain.py +123 -0
- clean_docs/extractors/__init__.py +19 -0
- clean_docs/extractors/command.py +51 -0
- clean_docs/extractors/inventory.py +176 -0
- clean_docs/extractors/json_pointer.py +84 -0
- clean_docs/extractors/python_literal.py +104 -0
- clean_docs/extractors/static.py +111 -0
- clean_docs/feedback.py +1390 -0
- clean_docs/impact.py +1624 -0
- clean_docs/improvements.py +1178 -0
- clean_docs/inventory.py +474 -0
- clean_docs/isolation.py +157 -0
- clean_docs/manifest.py +898 -0
- clean_docs/mdx.py +272 -0
- clean_docs/migration.py +121 -0
- clean_docs/models.py +194 -0
- clean_docs/outcomes.py +296 -0
- clean_docs/performance.py +123 -0
- clean_docs/phrasing.py +448 -0
- clean_docs/plugins.py +249 -0
- clean_docs/policy.py +536 -0
- clean_docs/projections.py +255 -0
- clean_docs/regions.py +75 -0
- clean_docs/release.py +232 -0
- clean_docs/renderers.py +57 -0
- clean_docs/residue.py +311 -0
- clean_docs/review_contracts.py +862 -0
- clean_docs/review_ledger.py +297 -0
- clean_docs/review_limits.py +9 -0
- clean_docs/sensitivity.py +602 -0
- clean_docs/snapshot.py +212 -0
- clean_docs/standard.py +281 -0
- clean_docs/standards/default.json +309 -0
- clean_docs/standards/exemplars.md +87 -0
- clean_docs/standards/v0-migrations.json +26 -0
- clean_docs/symbols.py +47 -0
- clean_docs/templates.py +96 -0
- clean_docs/verdict.py +1397 -0
- clean_docs/visuals.py +346 -0
- clean_docs/write_gate.py +170 -0
- sourcebound-1.2.1.dist-info/LICENSE +21 -0
- sourcebound-1.2.1.dist-info/METADATA +109 -0
- sourcebound-1.2.1.dist-info/RECORD +71 -0
- sourcebound-1.2.1.dist-info/WHEEL +5 -0
- sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
- sourcebound-1.2.1.dist-info/top_level.txt +1 -0
clean_docs/verdict.py
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
"""Compose one coverage-stating pull-request verdict from deterministic evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable, Mapping
|
|
11
|
+
|
|
12
|
+
from clean_docs import __version__
|
|
13
|
+
from clean_docs.errors import ConfigurationError
|
|
14
|
+
from clean_docs.execution import ExecutionPolicy
|
|
15
|
+
from clean_docs.impact import ImpactPlan, build_impact_plan
|
|
16
|
+
from clean_docs.models import ClaimBinding
|
|
17
|
+
from clean_docs.outcomes import RepositoryEvidence, collect_repository_evidence
|
|
18
|
+
from clean_docs.sensitivity import RECEIPT_SCHEMA, load_json_object
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
VERDICT_SCHEMA = "sourcebound.pr-verdict.v1"
|
|
22
|
+
NON_CLAIMS = (
|
|
23
|
+
"unbound prose is not certified",
|
|
24
|
+
"judgment prose is not certified",
|
|
25
|
+
"mutation sensitivity is not semantic correctness",
|
|
26
|
+
"review-contract co-change is not semantic correctness",
|
|
27
|
+
"catalog coverage is not prose coverage",
|
|
28
|
+
"gate readiness is not observation completeness",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class VerdictFinding:
|
|
34
|
+
id: str
|
|
35
|
+
rule: str
|
|
36
|
+
level: str
|
|
37
|
+
path: str
|
|
38
|
+
message: str
|
|
39
|
+
repair: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class MutationReceiptSummary:
|
|
44
|
+
sha256: str
|
|
45
|
+
state: str
|
|
46
|
+
relationship_id: str
|
|
47
|
+
plan_sha256: str | None
|
|
48
|
+
semantic_relationship_authorized: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class PullRequestVerdict:
|
|
53
|
+
state: str
|
|
54
|
+
requested_base: str
|
|
55
|
+
merge_base: str
|
|
56
|
+
head: str
|
|
57
|
+
manifest: str
|
|
58
|
+
manifest_sha256: str
|
|
59
|
+
impact: ImpactPlan
|
|
60
|
+
evidence: RepositoryEvidence
|
|
61
|
+
findings: tuple[VerdictFinding, ...]
|
|
62
|
+
mutation_receipts: tuple[MutationReceiptSummary, ...]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def ok(self) -> bool:
|
|
66
|
+
return self.state == "ready"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def digest(self) -> str:
|
|
70
|
+
return _digest(self._payload())
|
|
71
|
+
|
|
72
|
+
def _payload(self) -> dict[str, object]:
|
|
73
|
+
bindings = self.evidence.bindings
|
|
74
|
+
projections = self.evidence.projections
|
|
75
|
+
source_claims = self.evidence.source_claims
|
|
76
|
+
inventory = self.evidence.inventory
|
|
77
|
+
changed = self.evidence.changed
|
|
78
|
+
assert changed is not None
|
|
79
|
+
skipped_bindings = {
|
|
80
|
+
result.binding_id
|
|
81
|
+
for result in bindings
|
|
82
|
+
if result.state == "skipped-untrusted-execution"
|
|
83
|
+
}
|
|
84
|
+
configured_bindings = {
|
|
85
|
+
binding.id: binding for binding in self.evidence.manifest.bindings
|
|
86
|
+
}
|
|
87
|
+
skipped_commands = sorted(
|
|
88
|
+
binding.command
|
|
89
|
+
for binding_id in skipped_bindings
|
|
90
|
+
if isinstance(
|
|
91
|
+
(binding := configured_bindings.get(binding_id)),
|
|
92
|
+
ClaimBinding,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
counts = {
|
|
96
|
+
state: sum(item.coverage == state for item in inventory.items)
|
|
97
|
+
for state in ("bound", "cataloged", "ignored", "standard-gap")
|
|
98
|
+
}
|
|
99
|
+
mechanism_states = {
|
|
100
|
+
mechanism: {
|
|
101
|
+
"total": sum(result.binding_type == mechanism for result in bindings),
|
|
102
|
+
"current": sum(
|
|
103
|
+
result.binding_type == mechanism and not result.changed
|
|
104
|
+
for result in bindings
|
|
105
|
+
),
|
|
106
|
+
"drifted": sum(
|
|
107
|
+
result.binding_type == mechanism
|
|
108
|
+
and result.changed
|
|
109
|
+
and result.state != "skipped-untrusted-execution"
|
|
110
|
+
for result in bindings
|
|
111
|
+
),
|
|
112
|
+
"skipped": sum(
|
|
113
|
+
result.binding_type == mechanism
|
|
114
|
+
and result.state == "skipped-untrusted-execution"
|
|
115
|
+
for result in bindings
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
for mechanism in ("region", "command-pin", "symbol", "plugin")
|
|
119
|
+
}
|
|
120
|
+
review_contract_states = {
|
|
121
|
+
state: sum(result.state == state for result in self.impact.review_contracts)
|
|
122
|
+
for state in (
|
|
123
|
+
"unaffected",
|
|
124
|
+
"review-recommended",
|
|
125
|
+
"cochanged",
|
|
126
|
+
"unknown",
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
observation_state = (
|
|
130
|
+
"unknown"
|
|
131
|
+
if review_contract_states["unknown"]
|
|
132
|
+
else "review-recommended"
|
|
133
|
+
if review_contract_states["review-recommended"]
|
|
134
|
+
else "clear"
|
|
135
|
+
)
|
|
136
|
+
return {
|
|
137
|
+
"schema": VERDICT_SCHEMA,
|
|
138
|
+
"producer": {"name": "sourcebound", "version": __version__},
|
|
139
|
+
"state": self.state,
|
|
140
|
+
"ready": self.ok,
|
|
141
|
+
"gate": {
|
|
142
|
+
"state": self.state,
|
|
143
|
+
"ready": self.ok,
|
|
144
|
+
},
|
|
145
|
+
"observations": {
|
|
146
|
+
"state": observation_state,
|
|
147
|
+
"complete": review_contract_states["unknown"] == 0,
|
|
148
|
+
"total": len(self.impact.review_contracts),
|
|
149
|
+
"counts": review_contract_states,
|
|
150
|
+
},
|
|
151
|
+
"scope": "required-gates-and-changed-surface",
|
|
152
|
+
"read_only": True,
|
|
153
|
+
"refs": {
|
|
154
|
+
"requested_base": self.requested_base,
|
|
155
|
+
"merge_base": self.merge_base,
|
|
156
|
+
"head": self.head,
|
|
157
|
+
},
|
|
158
|
+
"inputs": {
|
|
159
|
+
"manifest": self.manifest,
|
|
160
|
+
"manifest_sha256": self.manifest_sha256,
|
|
161
|
+
"impact_plan_sha256": self.impact.digest,
|
|
162
|
+
},
|
|
163
|
+
"execution": {
|
|
164
|
+
"mode": ExecutionPolicy.STATIC_ONLY.value,
|
|
165
|
+
"repository_commands": "skipped",
|
|
166
|
+
"plugins": "skipped",
|
|
167
|
+
"skipped_binding_ids": sorted(skipped_bindings),
|
|
168
|
+
"skipped_command_ids": skipped_commands,
|
|
169
|
+
"skipped_plugin_ids": sorted(
|
|
170
|
+
plugin.id for plugin in self.evidence.manifest.plugins
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
"audit": {
|
|
174
|
+
"ok": self.evidence.audit.ok,
|
|
175
|
+
"active_documents": len(self.evidence.audit.documents),
|
|
176
|
+
"findings": len(self.evidence.audit.findings),
|
|
177
|
+
"baselined": len(self.evidence.audit.baselined_findings),
|
|
178
|
+
"stale_baseline": len(self.evidence.audit.stale_baseline),
|
|
179
|
+
"baseline_current": not self.evidence.audit.stale_baseline,
|
|
180
|
+
"unsupported_documents": list(
|
|
181
|
+
self.evidence.audit.unsupported_documents
|
|
182
|
+
),
|
|
183
|
+
},
|
|
184
|
+
"mechanisms": {
|
|
185
|
+
**mechanism_states,
|
|
186
|
+
"source-claim": {
|
|
187
|
+
"total": (
|
|
188
|
+
0 if source_claims is None else len(source_claims.results)
|
|
189
|
+
),
|
|
190
|
+
"current": (
|
|
191
|
+
0
|
|
192
|
+
if source_claims is None
|
|
193
|
+
else sum(
|
|
194
|
+
result.status == "current"
|
|
195
|
+
for result in source_claims.results
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
"drifted": (
|
|
199
|
+
0
|
|
200
|
+
if source_claims is None
|
|
201
|
+
else sum(
|
|
202
|
+
result.status == "drift" for result in source_claims.results
|
|
203
|
+
)
|
|
204
|
+
),
|
|
205
|
+
"missing": (
|
|
206
|
+
0 if source_claims is None else len(source_claims.missing)
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
"projection": {
|
|
210
|
+
"total": len(projections),
|
|
211
|
+
"current": sum(not result.changed for result in projections),
|
|
212
|
+
"stale": sum(result.changed for result in projections),
|
|
213
|
+
},
|
|
214
|
+
"review-contract": {
|
|
215
|
+
"total": len(self.impact.review_contracts),
|
|
216
|
+
**review_contract_states,
|
|
217
|
+
"semantic_correctness_checked": False,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
"changed_surface": {
|
|
221
|
+
"files": list(changed.changed_files),
|
|
222
|
+
"required": len(changed.required),
|
|
223
|
+
"gaps": len(changed.gaps),
|
|
224
|
+
"ignored": len(changed.ignored),
|
|
225
|
+
"unknown": len(self.impact.unknown),
|
|
226
|
+
"coverage_complete": self.impact.coverage_complete,
|
|
227
|
+
"impact": self.impact.impact,
|
|
228
|
+
"impact_findings": {
|
|
229
|
+
"required": len(self.impact.required),
|
|
230
|
+
"recommended": len(self.impact.recommended),
|
|
231
|
+
"unrelated": len(self.impact.unrelated),
|
|
232
|
+
"unknown": len(self.impact.unknown),
|
|
233
|
+
},
|
|
234
|
+
"artifacts": [asdict(artifact) for artifact in self.impact.artifacts],
|
|
235
|
+
"unsupported_documents": list(self.impact.unsupported_documents),
|
|
236
|
+
},
|
|
237
|
+
"coverage": {
|
|
238
|
+
"inventory_total": len(inventory.items),
|
|
239
|
+
"directly_bound": counts["bound"],
|
|
240
|
+
"catalog_only": counts["cataloged"],
|
|
241
|
+
"ignored": counts["ignored"],
|
|
242
|
+
"unsupported_or_unknown": counts["standard-gap"],
|
|
243
|
+
"unbound_prose_checked": False,
|
|
244
|
+
},
|
|
245
|
+
"mutation_receipts": [
|
|
246
|
+
asdict(receipt) for receipt in self.mutation_receipts
|
|
247
|
+
],
|
|
248
|
+
"review_contracts": [
|
|
249
|
+
result.as_dict() for result in self.impact.review_contracts
|
|
250
|
+
],
|
|
251
|
+
"findings": [asdict(finding) for finding in self.findings],
|
|
252
|
+
"non_claims": list(NON_CLAIMS),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def as_dict(self) -> dict[str, object]:
|
|
256
|
+
payload = self._payload()
|
|
257
|
+
payload["digest"] = self.digest
|
|
258
|
+
return payload
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _digest(value: object) -> str:
|
|
262
|
+
encoded = json.dumps(
|
|
263
|
+
value,
|
|
264
|
+
sort_keys=True,
|
|
265
|
+
separators=(",", ":"),
|
|
266
|
+
ensure_ascii=False,
|
|
267
|
+
).encode()
|
|
268
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _object(
|
|
272
|
+
value: object,
|
|
273
|
+
where: str,
|
|
274
|
+
keys: frozenset[str],
|
|
275
|
+
) -> dict[str, object]:
|
|
276
|
+
if not isinstance(value, dict):
|
|
277
|
+
raise ConfigurationError(f"{where} must be an object")
|
|
278
|
+
actual = frozenset(value)
|
|
279
|
+
if actual != keys:
|
|
280
|
+
raise ConfigurationError(f"{where} fields are invalid")
|
|
281
|
+
return value
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _string(value: object, where: str) -> str:
|
|
285
|
+
if not isinstance(value, str) or not value:
|
|
286
|
+
raise ConfigurationError(f"{where} must be a non-empty string")
|
|
287
|
+
return value
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _boolean(value: object, where: str) -> bool:
|
|
291
|
+
if not isinstance(value, bool):
|
|
292
|
+
raise ConfigurationError(f"{where} must be a boolean")
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _count(value: object, where: str) -> int:
|
|
297
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
298
|
+
raise ConfigurationError(f"{where} must be a non-negative integer")
|
|
299
|
+
return value
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _strings(value: object, where: str) -> list[str]:
|
|
303
|
+
if not isinstance(value, (list, tuple)) or not all(
|
|
304
|
+
isinstance(item, str) for item in value
|
|
305
|
+
):
|
|
306
|
+
raise ConfigurationError(f"{where} must be a list of strings")
|
|
307
|
+
return list(value)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _sha256_or_none(value: object, where: str) -> str | None:
|
|
311
|
+
if value is None:
|
|
312
|
+
return None
|
|
313
|
+
digest = _string(value, where)
|
|
314
|
+
if len(digest) != 64 or any(
|
|
315
|
+
character not in "0123456789abcdef" for character in digest
|
|
316
|
+
):
|
|
317
|
+
raise ConfigurationError(f"{where} must be a lowercase SHA-256 digest")
|
|
318
|
+
return digest
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _validate_count_group(
|
|
322
|
+
value: object,
|
|
323
|
+
where: str,
|
|
324
|
+
fields: tuple[str, ...],
|
|
325
|
+
) -> dict[str, int]:
|
|
326
|
+
data = _object(value, where, frozenset(("total", *fields)))
|
|
327
|
+
counts = {
|
|
328
|
+
field: _count(data[field], f"{where}.{field}") for field in ("total", *fields)
|
|
329
|
+
}
|
|
330
|
+
if counts["total"] != sum(counts[field] for field in fields):
|
|
331
|
+
raise ConfigurationError(f"{where} counts do not sum to total")
|
|
332
|
+
return counts
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _validate_locator_evidence(
|
|
336
|
+
value: object,
|
|
337
|
+
where: str,
|
|
338
|
+
) -> tuple[str, str | None, str | None]:
|
|
339
|
+
data = _object(
|
|
340
|
+
value,
|
|
341
|
+
where,
|
|
342
|
+
frozenset(
|
|
343
|
+
{
|
|
344
|
+
"id",
|
|
345
|
+
"path",
|
|
346
|
+
"extractor",
|
|
347
|
+
"locator",
|
|
348
|
+
"base_digest",
|
|
349
|
+
"head_digest",
|
|
350
|
+
"state",
|
|
351
|
+
}
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
_string(data["id"], f"{where}.id")
|
|
355
|
+
_string(data["path"], f"{where}.path")
|
|
356
|
+
extractor = data["extractor"]
|
|
357
|
+
if extractor not in {
|
|
358
|
+
"markdown-section",
|
|
359
|
+
"python-symbol",
|
|
360
|
+
"structured-data",
|
|
361
|
+
}:
|
|
362
|
+
raise ConfigurationError(f"{where}.extractor is invalid")
|
|
363
|
+
_string(data["locator"], f"{where}.locator")
|
|
364
|
+
base_digest = _sha256_or_none(data["base_digest"], f"{where}.base_digest")
|
|
365
|
+
head_digest = _sha256_or_none(data["head_digest"], f"{where}.head_digest")
|
|
366
|
+
state = data["state"]
|
|
367
|
+
if state not in {"added", "changed", "removed", "unchanged", "unknown"}:
|
|
368
|
+
raise ConfigurationError(f"{where}.state is invalid")
|
|
369
|
+
if state == "added" and not (base_digest is None and head_digest is not None):
|
|
370
|
+
raise ConfigurationError(f"{where} added state contradicts its digests")
|
|
371
|
+
if state == "removed" and not (base_digest is not None and head_digest is None):
|
|
372
|
+
raise ConfigurationError(f"{where} removed state contradicts its digests")
|
|
373
|
+
if state == "changed" and not (
|
|
374
|
+
base_digest is not None
|
|
375
|
+
and head_digest is not None
|
|
376
|
+
and base_digest != head_digest
|
|
377
|
+
):
|
|
378
|
+
raise ConfigurationError(f"{where} changed state contradicts its digests")
|
|
379
|
+
if state == "unchanged" and not (
|
|
380
|
+
base_digest is not None and base_digest == head_digest
|
|
381
|
+
):
|
|
382
|
+
raise ConfigurationError(f"{where} unchanged state contradicts its digests")
|
|
383
|
+
if state == "unknown" and base_digest is not None and head_digest is not None:
|
|
384
|
+
raise ConfigurationError(f"{where} unknown state contradicts its digests")
|
|
385
|
+
return state, base_digest, head_digest
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _validate_review_contract(
|
|
389
|
+
value: object,
|
|
390
|
+
where: str,
|
|
391
|
+
) -> str:
|
|
392
|
+
data = _object(
|
|
393
|
+
value,
|
|
394
|
+
where,
|
|
395
|
+
frozenset(
|
|
396
|
+
{
|
|
397
|
+
"id",
|
|
398
|
+
"mode",
|
|
399
|
+
"state",
|
|
400
|
+
"sources",
|
|
401
|
+
"targets",
|
|
402
|
+
"semantic_correctness_checked",
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
_string(data["id"], f"{where}.id")
|
|
407
|
+
if data["mode"] != "observe":
|
|
408
|
+
raise ConfigurationError(f"{where}.mode must be observe")
|
|
409
|
+
if data["semantic_correctness_checked"] is not False:
|
|
410
|
+
raise ConfigurationError(f"{where}.semantic_correctness_checked must be false")
|
|
411
|
+
source_values = data["sources"]
|
|
412
|
+
target_values = data["targets"]
|
|
413
|
+
if not isinstance(source_values, list) or not source_values:
|
|
414
|
+
raise ConfigurationError(f"{where}.sources must be a non-empty list")
|
|
415
|
+
if not isinstance(target_values, list) or not target_values:
|
|
416
|
+
raise ConfigurationError(f"{where}.targets must be a non-empty list")
|
|
417
|
+
source_states = [
|
|
418
|
+
_validate_locator_evidence(item, f"{where}.sources[{index}]")[0]
|
|
419
|
+
for index, item in enumerate(source_values)
|
|
420
|
+
]
|
|
421
|
+
target_results = [
|
|
422
|
+
_validate_locator_evidence(item, f"{where}.targets[{index}]")
|
|
423
|
+
for index, item in enumerate(target_values)
|
|
424
|
+
]
|
|
425
|
+
target_states = [result[0] for result in target_results]
|
|
426
|
+
target_head_digests = [result[2] for result in target_results]
|
|
427
|
+
expected = (
|
|
428
|
+
"unknown"
|
|
429
|
+
if "unknown" in source_states + target_states
|
|
430
|
+
or any(digest is None for digest in target_head_digests)
|
|
431
|
+
else "unaffected"
|
|
432
|
+
if not any(state in {"added", "changed", "removed"} for state in source_states)
|
|
433
|
+
else "cochanged"
|
|
434
|
+
if all(state in {"added", "changed"} for state in target_states)
|
|
435
|
+
else "review-recommended"
|
|
436
|
+
)
|
|
437
|
+
state = data["state"]
|
|
438
|
+
if state != expected:
|
|
439
|
+
raise ConfigurationError(f"{where}.state contradicts locator evidence")
|
|
440
|
+
return expected
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _derive_gate_state(
|
|
444
|
+
finding_levels: Iterable[str],
|
|
445
|
+
*,
|
|
446
|
+
coverage_complete: bool,
|
|
447
|
+
impact: str,
|
|
448
|
+
) -> str:
|
|
449
|
+
levels = tuple(finding_levels)
|
|
450
|
+
if "error" in levels:
|
|
451
|
+
return "not_ready"
|
|
452
|
+
if not coverage_complete or impact == "unknown" or "warning" in levels:
|
|
453
|
+
return "unknown"
|
|
454
|
+
return "ready"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _validate_findings(value: object) -> tuple[str, ...]:
|
|
458
|
+
if not isinstance(value, list):
|
|
459
|
+
raise ConfigurationError("verdict findings must be a list")
|
|
460
|
+
levels: list[str] = []
|
|
461
|
+
for finding in value:
|
|
462
|
+
finding_data = _object(
|
|
463
|
+
finding,
|
|
464
|
+
"verdict finding",
|
|
465
|
+
frozenset({"id", "rule", "level", "path", "message", "repair"}),
|
|
466
|
+
)
|
|
467
|
+
for field in ("id", "rule", "path", "message", "repair"):
|
|
468
|
+
_string(finding_data[field], f"verdict finding {field}")
|
|
469
|
+
if finding_data["level"] not in {"error", "warning", "note"}:
|
|
470
|
+
raise ConfigurationError("verdict finding level is invalid")
|
|
471
|
+
levels.append(finding_data["level"])
|
|
472
|
+
return tuple(levels)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _validate_legacy_verdict_payload(payload: Mapping[str, object]) -> None:
|
|
476
|
+
allowed = (
|
|
477
|
+
frozenset({"schema", "state", "ready", "findings", "digest"}),
|
|
478
|
+
frozenset({"schema", "producer", "state", "ready", "findings", "digest"}),
|
|
479
|
+
)
|
|
480
|
+
if frozenset(payload) not in allowed:
|
|
481
|
+
raise ConfigurationError("legacy verdict fields are invalid")
|
|
482
|
+
state = payload["state"]
|
|
483
|
+
if state not in {"ready", "not_ready", "unknown"}:
|
|
484
|
+
raise ConfigurationError("legacy verdict state is invalid")
|
|
485
|
+
ready = _boolean(payload["ready"], "legacy verdict.ready")
|
|
486
|
+
if ready != (state == "ready"):
|
|
487
|
+
raise ConfigurationError("legacy verdict ready contradicts state")
|
|
488
|
+
if "producer" in payload:
|
|
489
|
+
producer = _object(
|
|
490
|
+
payload["producer"],
|
|
491
|
+
"legacy verdict.producer",
|
|
492
|
+
frozenset({"name", "version"}),
|
|
493
|
+
)
|
|
494
|
+
if producer["name"] != "sourcebound":
|
|
495
|
+
raise ConfigurationError("legacy verdict.producer.name must be sourcebound")
|
|
496
|
+
_string(producer["version"], "legacy verdict.producer.version")
|
|
497
|
+
findings = payload["findings"]
|
|
498
|
+
if not isinstance(findings, list):
|
|
499
|
+
raise ConfigurationError("legacy verdict findings must be a list")
|
|
500
|
+
if findings:
|
|
501
|
+
_validate_findings(findings)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def validate_verdict_payload(payload: Mapping[str, object]) -> None:
|
|
505
|
+
"""Reject a serialized verdict that cannot be the output of this schema."""
|
|
506
|
+
if payload.get("schema") != VERDICT_SCHEMA:
|
|
507
|
+
raise ConfigurationError(f"verdict schema must be {VERDICT_SCHEMA}")
|
|
508
|
+
digest = payload.get("digest")
|
|
509
|
+
if _sha256_or_none(digest, "verdict digest") is None:
|
|
510
|
+
raise ConfigurationError("verdict digest is missing")
|
|
511
|
+
unsigned = {key: value for key, value in payload.items() if key != "digest"}
|
|
512
|
+
if _digest(unsigned) != digest:
|
|
513
|
+
raise ConfigurationError("verdict digest does not match its payload")
|
|
514
|
+
if "gate" not in payload:
|
|
515
|
+
_validate_legacy_verdict_payload(payload)
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
data = _object(
|
|
519
|
+
dict(payload),
|
|
520
|
+
"verdict",
|
|
521
|
+
frozenset(
|
|
522
|
+
{
|
|
523
|
+
"schema",
|
|
524
|
+
"producer",
|
|
525
|
+
"state",
|
|
526
|
+
"ready",
|
|
527
|
+
"gate",
|
|
528
|
+
"observations",
|
|
529
|
+
"scope",
|
|
530
|
+
"read_only",
|
|
531
|
+
"refs",
|
|
532
|
+
"inputs",
|
|
533
|
+
"execution",
|
|
534
|
+
"audit",
|
|
535
|
+
"mechanisms",
|
|
536
|
+
"changed_surface",
|
|
537
|
+
"coverage",
|
|
538
|
+
"mutation_receipts",
|
|
539
|
+
"review_contracts",
|
|
540
|
+
"findings",
|
|
541
|
+
"non_claims",
|
|
542
|
+
"digest",
|
|
543
|
+
}
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
producer = _object(
|
|
547
|
+
data["producer"],
|
|
548
|
+
"verdict.producer",
|
|
549
|
+
frozenset({"name", "version"}),
|
|
550
|
+
)
|
|
551
|
+
if producer["name"] != "sourcebound":
|
|
552
|
+
raise ConfigurationError("verdict.producer.name must be sourcebound")
|
|
553
|
+
_string(producer["version"], "verdict.producer.version")
|
|
554
|
+
|
|
555
|
+
states = {"ready", "not_ready", "unknown"}
|
|
556
|
+
state = data["state"]
|
|
557
|
+
if state not in states:
|
|
558
|
+
raise ConfigurationError("verdict state is invalid")
|
|
559
|
+
ready = _boolean(data["ready"], "verdict.ready")
|
|
560
|
+
if ready != (state == "ready"):
|
|
561
|
+
raise ConfigurationError("verdict ready contradicts state")
|
|
562
|
+
gate = _object(
|
|
563
|
+
data["gate"],
|
|
564
|
+
"verdict.gate",
|
|
565
|
+
frozenset({"state", "ready"}),
|
|
566
|
+
)
|
|
567
|
+
if gate["state"] not in states:
|
|
568
|
+
raise ConfigurationError("verdict.gate.state is invalid")
|
|
569
|
+
gate_ready = _boolean(gate["ready"], "verdict.gate.ready")
|
|
570
|
+
if gate["state"] != state or gate_ready != ready:
|
|
571
|
+
raise ConfigurationError("legacy verdict aliases must match gate")
|
|
572
|
+
|
|
573
|
+
if data["scope"] != "required-gates-and-changed-surface":
|
|
574
|
+
raise ConfigurationError("verdict scope is invalid")
|
|
575
|
+
if data["read_only"] is not True:
|
|
576
|
+
raise ConfigurationError("verdict read_only must be true")
|
|
577
|
+
|
|
578
|
+
refs = _object(
|
|
579
|
+
data["refs"],
|
|
580
|
+
"verdict.refs",
|
|
581
|
+
frozenset({"requested_base", "merge_base", "head"}),
|
|
582
|
+
)
|
|
583
|
+
for field in ("requested_base", "merge_base", "head"):
|
|
584
|
+
_string(refs[field], f"verdict.refs.{field}")
|
|
585
|
+
|
|
586
|
+
inputs = _object(
|
|
587
|
+
data["inputs"],
|
|
588
|
+
"verdict.inputs",
|
|
589
|
+
frozenset({"manifest", "manifest_sha256", "impact_plan_sha256"}),
|
|
590
|
+
)
|
|
591
|
+
_string(inputs["manifest"], "verdict.inputs.manifest")
|
|
592
|
+
if (
|
|
593
|
+
_sha256_or_none(
|
|
594
|
+
inputs["manifest_sha256"],
|
|
595
|
+
"verdict.inputs.manifest_sha256",
|
|
596
|
+
)
|
|
597
|
+
is None
|
|
598
|
+
):
|
|
599
|
+
raise ConfigurationError("verdict.inputs.manifest_sha256 is missing")
|
|
600
|
+
if (
|
|
601
|
+
_sha256_or_none(
|
|
602
|
+
inputs["impact_plan_sha256"],
|
|
603
|
+
"verdict.inputs.impact_plan_sha256",
|
|
604
|
+
)
|
|
605
|
+
is None
|
|
606
|
+
):
|
|
607
|
+
raise ConfigurationError("verdict.inputs.impact_plan_sha256 is missing")
|
|
608
|
+
|
|
609
|
+
execution = _object(
|
|
610
|
+
data["execution"],
|
|
611
|
+
"verdict.execution",
|
|
612
|
+
frozenset(
|
|
613
|
+
{
|
|
614
|
+
"mode",
|
|
615
|
+
"repository_commands",
|
|
616
|
+
"plugins",
|
|
617
|
+
"skipped_binding_ids",
|
|
618
|
+
"skipped_command_ids",
|
|
619
|
+
"skipped_plugin_ids",
|
|
620
|
+
}
|
|
621
|
+
),
|
|
622
|
+
)
|
|
623
|
+
if execution["mode"] != ExecutionPolicy.STATIC_ONLY.value:
|
|
624
|
+
raise ConfigurationError("verdict.execution.mode is invalid")
|
|
625
|
+
if execution["repository_commands"] != "skipped":
|
|
626
|
+
raise ConfigurationError(
|
|
627
|
+
"verdict.execution.repository_commands must be skipped"
|
|
628
|
+
)
|
|
629
|
+
if execution["plugins"] != "skipped":
|
|
630
|
+
raise ConfigurationError("verdict.execution.plugins must be skipped")
|
|
631
|
+
for field in (
|
|
632
|
+
"skipped_binding_ids",
|
|
633
|
+
"skipped_command_ids",
|
|
634
|
+
"skipped_plugin_ids",
|
|
635
|
+
):
|
|
636
|
+
values = _strings(execution[field], f"verdict.execution.{field}")
|
|
637
|
+
if values != sorted(set(values)):
|
|
638
|
+
raise ConfigurationError(
|
|
639
|
+
f"verdict.execution.{field} must be sorted and unique"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
audit = _object(
|
|
643
|
+
data["audit"],
|
|
644
|
+
"verdict.audit",
|
|
645
|
+
frozenset(
|
|
646
|
+
{
|
|
647
|
+
"ok",
|
|
648
|
+
"active_documents",
|
|
649
|
+
"findings",
|
|
650
|
+
"baselined",
|
|
651
|
+
"stale_baseline",
|
|
652
|
+
"baseline_current",
|
|
653
|
+
"unsupported_documents",
|
|
654
|
+
}
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
audit_ok = _boolean(audit["ok"], "verdict.audit.ok")
|
|
658
|
+
audit_findings = _count(audit["findings"], "verdict.audit.findings")
|
|
659
|
+
stale_baseline = _count(
|
|
660
|
+
audit["stale_baseline"],
|
|
661
|
+
"verdict.audit.stale_baseline",
|
|
662
|
+
)
|
|
663
|
+
_count(audit["active_documents"], "verdict.audit.active_documents")
|
|
664
|
+
_count(audit["baselined"], "verdict.audit.baselined")
|
|
665
|
+
baseline_current = _boolean(
|
|
666
|
+
audit["baseline_current"],
|
|
667
|
+
"verdict.audit.baseline_current",
|
|
668
|
+
)
|
|
669
|
+
_strings(
|
|
670
|
+
audit["unsupported_documents"],
|
|
671
|
+
"verdict.audit.unsupported_documents",
|
|
672
|
+
)
|
|
673
|
+
if baseline_current != (stale_baseline == 0):
|
|
674
|
+
raise ConfigurationError(
|
|
675
|
+
"verdict.audit.baseline_current contradicts stale_baseline"
|
|
676
|
+
)
|
|
677
|
+
if audit_ok != (audit_findings == 0 and stale_baseline == 0):
|
|
678
|
+
raise ConfigurationError("verdict.audit.ok contradicts audit counts")
|
|
679
|
+
|
|
680
|
+
mechanisms = _object(
|
|
681
|
+
data["mechanisms"],
|
|
682
|
+
"verdict.mechanisms",
|
|
683
|
+
frozenset(
|
|
684
|
+
{
|
|
685
|
+
"region",
|
|
686
|
+
"command-pin",
|
|
687
|
+
"symbol",
|
|
688
|
+
"plugin",
|
|
689
|
+
"source-claim",
|
|
690
|
+
"projection",
|
|
691
|
+
"review-contract",
|
|
692
|
+
}
|
|
693
|
+
),
|
|
694
|
+
)
|
|
695
|
+
for mechanism in ("region", "command-pin", "symbol", "plugin"):
|
|
696
|
+
_validate_count_group(
|
|
697
|
+
mechanisms[mechanism],
|
|
698
|
+
f"verdict.mechanisms.{mechanism}",
|
|
699
|
+
("current", "drifted", "skipped"),
|
|
700
|
+
)
|
|
701
|
+
source_claim = _object(
|
|
702
|
+
mechanisms["source-claim"],
|
|
703
|
+
"verdict.mechanisms.source-claim",
|
|
704
|
+
frozenset({"total", "current", "drifted", "missing"}),
|
|
705
|
+
)
|
|
706
|
+
source_claim_counts = {
|
|
707
|
+
field: _count(
|
|
708
|
+
source_claim[field],
|
|
709
|
+
f"verdict.mechanisms.source-claim.{field}",
|
|
710
|
+
)
|
|
711
|
+
for field in ("total", "current", "drifted", "missing")
|
|
712
|
+
}
|
|
713
|
+
if source_claim_counts["total"] != (
|
|
714
|
+
source_claim_counts["current"] + source_claim_counts["drifted"]
|
|
715
|
+
):
|
|
716
|
+
raise ConfigurationError(
|
|
717
|
+
"verdict.mechanisms.source-claim counts do not sum to total"
|
|
718
|
+
)
|
|
719
|
+
_validate_count_group(
|
|
720
|
+
mechanisms["projection"],
|
|
721
|
+
"verdict.mechanisms.projection",
|
|
722
|
+
("current", "stale"),
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
review_mechanism = _object(
|
|
726
|
+
mechanisms["review-contract"],
|
|
727
|
+
"verdict.mechanisms.review-contract",
|
|
728
|
+
frozenset(
|
|
729
|
+
{
|
|
730
|
+
"total",
|
|
731
|
+
"unaffected",
|
|
732
|
+
"review-recommended",
|
|
733
|
+
"cochanged",
|
|
734
|
+
"unknown",
|
|
735
|
+
"semantic_correctness_checked",
|
|
736
|
+
}
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
if review_mechanism["semantic_correctness_checked"] is not False:
|
|
740
|
+
raise ConfigurationError(
|
|
741
|
+
"verdict.mechanisms.review-contract semantic check must be false"
|
|
742
|
+
)
|
|
743
|
+
review_counts = {
|
|
744
|
+
field: _count(
|
|
745
|
+
review_mechanism[field],
|
|
746
|
+
f"verdict.mechanisms.review-contract.{field}",
|
|
747
|
+
)
|
|
748
|
+
for field in (
|
|
749
|
+
"total",
|
|
750
|
+
"unaffected",
|
|
751
|
+
"review-recommended",
|
|
752
|
+
"cochanged",
|
|
753
|
+
"unknown",
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
if review_counts["total"] != sum(
|
|
757
|
+
review_counts[field]
|
|
758
|
+
for field in (
|
|
759
|
+
"unaffected",
|
|
760
|
+
"review-recommended",
|
|
761
|
+
"cochanged",
|
|
762
|
+
"unknown",
|
|
763
|
+
)
|
|
764
|
+
):
|
|
765
|
+
raise ConfigurationError(
|
|
766
|
+
"verdict.mechanisms.review-contract counts do not sum to total"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
changed = _object(
|
|
770
|
+
data["changed_surface"],
|
|
771
|
+
"verdict.changed_surface",
|
|
772
|
+
frozenset(
|
|
773
|
+
{
|
|
774
|
+
"files",
|
|
775
|
+
"required",
|
|
776
|
+
"gaps",
|
|
777
|
+
"ignored",
|
|
778
|
+
"unknown",
|
|
779
|
+
"coverage_complete",
|
|
780
|
+
"impact",
|
|
781
|
+
"impact_findings",
|
|
782
|
+
"artifacts",
|
|
783
|
+
"unsupported_documents",
|
|
784
|
+
}
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
_strings(changed["files"], "verdict.changed_surface.files")
|
|
788
|
+
for field in ("required", "gaps", "ignored", "unknown"):
|
|
789
|
+
_count(changed[field], f"verdict.changed_surface.{field}")
|
|
790
|
+
coverage_complete = _boolean(
|
|
791
|
+
changed["coverage_complete"],
|
|
792
|
+
"verdict.changed_surface.coverage_complete",
|
|
793
|
+
)
|
|
794
|
+
if changed["impact"] not in {"none", "recommended", "required", "unknown"}:
|
|
795
|
+
raise ConfigurationError("verdict.changed_surface.impact is invalid")
|
|
796
|
+
impact_counts_data = _object(
|
|
797
|
+
changed["impact_findings"],
|
|
798
|
+
"verdict.changed_surface.impact_findings",
|
|
799
|
+
frozenset({"required", "recommended", "unrelated", "unknown"}),
|
|
800
|
+
)
|
|
801
|
+
impact_counts = {
|
|
802
|
+
field: _count(
|
|
803
|
+
impact_counts_data[field],
|
|
804
|
+
f"verdict.changed_surface.impact_findings.{field}",
|
|
805
|
+
)
|
|
806
|
+
for field in ("required", "recommended", "unrelated", "unknown")
|
|
807
|
+
}
|
|
808
|
+
expected_impact = (
|
|
809
|
+
"unknown"
|
|
810
|
+
if impact_counts["unknown"]
|
|
811
|
+
else "required"
|
|
812
|
+
if impact_counts["required"]
|
|
813
|
+
else "recommended"
|
|
814
|
+
if impact_counts["recommended"]
|
|
815
|
+
else "none"
|
|
816
|
+
)
|
|
817
|
+
if changed["impact"] != expected_impact:
|
|
818
|
+
raise ConfigurationError(
|
|
819
|
+
"verdict.changed_surface.impact contradicts finding counts"
|
|
820
|
+
)
|
|
821
|
+
if changed["unknown"] != impact_counts["unknown"]:
|
|
822
|
+
raise ConfigurationError("verdict.changed_surface unknown counts disagree")
|
|
823
|
+
if coverage_complete != (impact_counts["unknown"] == 0):
|
|
824
|
+
raise ConfigurationError(
|
|
825
|
+
"verdict.changed_surface coverage contradicts unknown count"
|
|
826
|
+
)
|
|
827
|
+
artifacts = changed["artifacts"]
|
|
828
|
+
if not isinstance(artifacts, list):
|
|
829
|
+
raise ConfigurationError("verdict.changed_surface.artifacts must be a list")
|
|
830
|
+
for index, artifact_value in enumerate(artifacts):
|
|
831
|
+
artifact = _object(
|
|
832
|
+
artifact_value,
|
|
833
|
+
f"verdict.changed_surface.artifacts[{index}]",
|
|
834
|
+
frozenset(
|
|
835
|
+
{
|
|
836
|
+
"id",
|
|
837
|
+
"path",
|
|
838
|
+
"change",
|
|
839
|
+
"base_blob",
|
|
840
|
+
"head_blob",
|
|
841
|
+
"adapter",
|
|
842
|
+
"decision",
|
|
843
|
+
"may_expose_public_surface",
|
|
844
|
+
"coverage",
|
|
845
|
+
"graph_roots",
|
|
846
|
+
}
|
|
847
|
+
),
|
|
848
|
+
)
|
|
849
|
+
_string(artifact["id"], f"verdict artifact {index}.id")
|
|
850
|
+
_string(artifact["path"], f"verdict artifact {index}.path")
|
|
851
|
+
if artifact["change"] not in {"added", "modified", "removed"}:
|
|
852
|
+
raise ConfigurationError(f"verdict artifact {index}.change is invalid")
|
|
853
|
+
for field in ("base_blob", "head_blob"):
|
|
854
|
+
if artifact[field] is not None:
|
|
855
|
+
_string(artifact[field], f"verdict artifact {index}.{field}")
|
|
856
|
+
_string(artifact["adapter"], f"verdict artifact {index}.adapter")
|
|
857
|
+
_string(artifact["decision"], f"verdict artifact {index}.decision")
|
|
858
|
+
_boolean(
|
|
859
|
+
artifact["may_expose_public_surface"],
|
|
860
|
+
f"verdict artifact {index}.may_expose_public_surface",
|
|
861
|
+
)
|
|
862
|
+
if artifact["coverage"] not in {
|
|
863
|
+
"adapter-covered",
|
|
864
|
+
"document-direct",
|
|
865
|
+
"generated",
|
|
866
|
+
"graph-covered",
|
|
867
|
+
"unknown",
|
|
868
|
+
"unrelated-covered",
|
|
869
|
+
}:
|
|
870
|
+
raise ConfigurationError(f"verdict artifact {index}.coverage is invalid")
|
|
871
|
+
_strings(artifact["graph_roots"], f"verdict artifact {index}.graph_roots")
|
|
872
|
+
_strings(
|
|
873
|
+
changed["unsupported_documents"],
|
|
874
|
+
"verdict.changed_surface.unsupported_documents",
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
coverage = _object(
|
|
878
|
+
data["coverage"],
|
|
879
|
+
"verdict.coverage",
|
|
880
|
+
frozenset(
|
|
881
|
+
{
|
|
882
|
+
"inventory_total",
|
|
883
|
+
"directly_bound",
|
|
884
|
+
"catalog_only",
|
|
885
|
+
"ignored",
|
|
886
|
+
"unsupported_or_unknown",
|
|
887
|
+
"unbound_prose_checked",
|
|
888
|
+
}
|
|
889
|
+
),
|
|
890
|
+
)
|
|
891
|
+
coverage_counts = {
|
|
892
|
+
field: _count(coverage[field], f"verdict.coverage.{field}")
|
|
893
|
+
for field in (
|
|
894
|
+
"inventory_total",
|
|
895
|
+
"directly_bound",
|
|
896
|
+
"catalog_only",
|
|
897
|
+
"ignored",
|
|
898
|
+
"unsupported_or_unknown",
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
if coverage_counts["inventory_total"] != sum(
|
|
902
|
+
coverage_counts[field]
|
|
903
|
+
for field in (
|
|
904
|
+
"directly_bound",
|
|
905
|
+
"catalog_only",
|
|
906
|
+
"ignored",
|
|
907
|
+
"unsupported_or_unknown",
|
|
908
|
+
)
|
|
909
|
+
):
|
|
910
|
+
raise ConfigurationError("verdict coverage counts do not sum to total")
|
|
911
|
+
if coverage["unbound_prose_checked"] is not False:
|
|
912
|
+
raise ConfigurationError("verdict.coverage.unbound_prose_checked must be false")
|
|
913
|
+
|
|
914
|
+
review_contracts = data["review_contracts"]
|
|
915
|
+
if not isinstance(review_contracts, list):
|
|
916
|
+
raise ConfigurationError("verdict.review_contracts must be a list")
|
|
917
|
+
calculated_review_counts = {
|
|
918
|
+
"unaffected": 0,
|
|
919
|
+
"review-recommended": 0,
|
|
920
|
+
"cochanged": 0,
|
|
921
|
+
"unknown": 0,
|
|
922
|
+
}
|
|
923
|
+
for index, contract in enumerate(review_contracts):
|
|
924
|
+
contract_state = _validate_review_contract(
|
|
925
|
+
contract,
|
|
926
|
+
f"verdict.review_contracts[{index}]",
|
|
927
|
+
)
|
|
928
|
+
calculated_review_counts[contract_state] += 1
|
|
929
|
+
if len(review_contracts) != review_counts["total"] or any(
|
|
930
|
+
calculated_review_counts[field] != review_counts[field]
|
|
931
|
+
for field in calculated_review_counts
|
|
932
|
+
):
|
|
933
|
+
raise ConfigurationError(
|
|
934
|
+
"verdict review-contract mechanism counts disagree with evidence"
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
observations = _object(
|
|
938
|
+
data["observations"],
|
|
939
|
+
"verdict.observations",
|
|
940
|
+
frozenset({"state", "complete", "total", "counts"}),
|
|
941
|
+
)
|
|
942
|
+
observation_counts_data = _object(
|
|
943
|
+
observations["counts"],
|
|
944
|
+
"verdict.observations.counts",
|
|
945
|
+
frozenset(calculated_review_counts),
|
|
946
|
+
)
|
|
947
|
+
observation_counts = {
|
|
948
|
+
field: _count(
|
|
949
|
+
observation_counts_data[field],
|
|
950
|
+
f"verdict.observations.counts.{field}",
|
|
951
|
+
)
|
|
952
|
+
for field in calculated_review_counts
|
|
953
|
+
}
|
|
954
|
+
observation_total = _count(
|
|
955
|
+
observations["total"],
|
|
956
|
+
"verdict.observations.total",
|
|
957
|
+
)
|
|
958
|
+
if observation_counts != calculated_review_counts or observation_total != len(
|
|
959
|
+
review_contracts
|
|
960
|
+
):
|
|
961
|
+
raise ConfigurationError(
|
|
962
|
+
"verdict observations disagree with review-contract evidence"
|
|
963
|
+
)
|
|
964
|
+
expected_observation_state = (
|
|
965
|
+
"unknown"
|
|
966
|
+
if observation_counts["unknown"]
|
|
967
|
+
else "review-recommended"
|
|
968
|
+
if observation_counts["review-recommended"]
|
|
969
|
+
else "clear"
|
|
970
|
+
)
|
|
971
|
+
if observations["state"] != expected_observation_state:
|
|
972
|
+
raise ConfigurationError("verdict observation state contradicts its counts")
|
|
973
|
+
observation_complete = _boolean(
|
|
974
|
+
observations["complete"],
|
|
975
|
+
"verdict.observations.complete",
|
|
976
|
+
)
|
|
977
|
+
if observation_complete != (observation_counts["unknown"] == 0):
|
|
978
|
+
raise ConfigurationError(
|
|
979
|
+
"verdict observation completeness contradicts unknown count"
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
mutation_receipts = data["mutation_receipts"]
|
|
983
|
+
if not isinstance(mutation_receipts, list):
|
|
984
|
+
raise ConfigurationError("verdict.mutation_receipts must be a list")
|
|
985
|
+
for index, receipt_value in enumerate(mutation_receipts):
|
|
986
|
+
receipt = _object(
|
|
987
|
+
receipt_value,
|
|
988
|
+
f"verdict.mutation_receipts[{index}]",
|
|
989
|
+
frozenset(
|
|
990
|
+
{
|
|
991
|
+
"sha256",
|
|
992
|
+
"state",
|
|
993
|
+
"relationship_id",
|
|
994
|
+
"plan_sha256",
|
|
995
|
+
"semantic_relationship_authorized",
|
|
996
|
+
}
|
|
997
|
+
),
|
|
998
|
+
)
|
|
999
|
+
_sha256_or_none(
|
|
1000
|
+
receipt["sha256"],
|
|
1001
|
+
f"verdict.mutation_receipts[{index}].sha256",
|
|
1002
|
+
)
|
|
1003
|
+
if receipt["state"] not in {
|
|
1004
|
+
"sensitive",
|
|
1005
|
+
"insensitive",
|
|
1006
|
+
"invalid",
|
|
1007
|
+
"unsupported",
|
|
1008
|
+
}:
|
|
1009
|
+
raise ConfigurationError(
|
|
1010
|
+
f"verdict.mutation_receipts[{index}].state is invalid"
|
|
1011
|
+
)
|
|
1012
|
+
_string(
|
|
1013
|
+
receipt["relationship_id"],
|
|
1014
|
+
f"verdict.mutation_receipts[{index}].relationship_id",
|
|
1015
|
+
)
|
|
1016
|
+
plan_digest = _sha256_or_none(
|
|
1017
|
+
receipt["plan_sha256"],
|
|
1018
|
+
f"verdict.mutation_receipts[{index}].plan_sha256",
|
|
1019
|
+
)
|
|
1020
|
+
if receipt["state"] in {"sensitive", "insensitive"} and plan_digest is None:
|
|
1021
|
+
raise ConfigurationError(
|
|
1022
|
+
f"verdict.mutation_receipts[{index}] requires a plan digest"
|
|
1023
|
+
)
|
|
1024
|
+
if receipt["semantic_relationship_authorized"] is not False:
|
|
1025
|
+
raise ConfigurationError(
|
|
1026
|
+
f"verdict.mutation_receipts[{index}] cannot authorize semantics"
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
finding_levels = _validate_findings(data["findings"])
|
|
1030
|
+
expected_gate_state = _derive_gate_state(
|
|
1031
|
+
finding_levels,
|
|
1032
|
+
coverage_complete=coverage_complete,
|
|
1033
|
+
impact=changed["impact"],
|
|
1034
|
+
)
|
|
1035
|
+
if state != expected_gate_state:
|
|
1036
|
+
raise ConfigurationError(
|
|
1037
|
+
"verdict gate state contradicts validated evidence"
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
non_claims = _strings(data["non_claims"], "verdict.non_claims")
|
|
1041
|
+
if tuple(non_claims) != NON_CLAIMS:
|
|
1042
|
+
raise ConfigurationError("verdict non_claims are invalid")
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def _git(root: Path, *args: str) -> bytes:
|
|
1046
|
+
try:
|
|
1047
|
+
process = subprocess.run(
|
|
1048
|
+
["git", "-C", str(root), *args],
|
|
1049
|
+
capture_output=True,
|
|
1050
|
+
timeout=30,
|
|
1051
|
+
check=False,
|
|
1052
|
+
)
|
|
1053
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1054
|
+
raise ConfigurationError(f"cannot inspect verdict repository: {exc}") from exc
|
|
1055
|
+
if process.returncode != 0:
|
|
1056
|
+
detail = process.stderr.decode(errors="replace").strip()
|
|
1057
|
+
raise ConfigurationError(detail or f"git {' '.join(args)} failed")
|
|
1058
|
+
return process.stdout
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def _finding(
|
|
1062
|
+
rule: str,
|
|
1063
|
+
level: str,
|
|
1064
|
+
path: str,
|
|
1065
|
+
message: str,
|
|
1066
|
+
repair: str,
|
|
1067
|
+
) -> VerdictFinding:
|
|
1068
|
+
return VerdictFinding(
|
|
1069
|
+
_digest([rule, path, message]),
|
|
1070
|
+
rule,
|
|
1071
|
+
level,
|
|
1072
|
+
path,
|
|
1073
|
+
message,
|
|
1074
|
+
repair,
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _mutation_summary(path: Path, head: str) -> MutationReceiptSummary:
|
|
1079
|
+
if path.is_symlink():
|
|
1080
|
+
raise ConfigurationError("mutation receipt must not be a symbolic link")
|
|
1081
|
+
receipt, raw = load_json_object(path, "mutation receipt")
|
|
1082
|
+
if receipt.get("schema") != RECEIPT_SCHEMA:
|
|
1083
|
+
raise ConfigurationError(f"mutation receipt schema must be {RECEIPT_SCHEMA}")
|
|
1084
|
+
repository = receipt.get("repository")
|
|
1085
|
+
inputs = receipt.get("inputs")
|
|
1086
|
+
if not isinstance(repository, dict) or repository.get("commit") != head:
|
|
1087
|
+
raise ConfigurationError("mutation receipt commit does not match verdict head")
|
|
1088
|
+
if not isinstance(inputs, dict) or not isinstance(inputs.get("relationship"), dict):
|
|
1089
|
+
raise ConfigurationError("mutation receipt has no relationship identity")
|
|
1090
|
+
relationship_id = inputs["relationship"].get("id")
|
|
1091
|
+
if not isinstance(relationship_id, str) or not relationship_id:
|
|
1092
|
+
raise ConfigurationError("mutation receipt relationship id is invalid")
|
|
1093
|
+
state = receipt.get("state")
|
|
1094
|
+
if state not in {"sensitive", "insensitive", "invalid", "unsupported"}:
|
|
1095
|
+
raise ConfigurationError("mutation receipt state is invalid")
|
|
1096
|
+
semantic = receipt.get("semantic_relationship_authorized")
|
|
1097
|
+
if semantic is not False:
|
|
1098
|
+
raise ConfigurationError(
|
|
1099
|
+
"mutation receipt cannot authorize a semantic relationship"
|
|
1100
|
+
)
|
|
1101
|
+
mutation = receipt.get("mutation")
|
|
1102
|
+
plan_sha256 = None
|
|
1103
|
+
if mutation is not None:
|
|
1104
|
+
if not isinstance(mutation, dict):
|
|
1105
|
+
raise ConfigurationError("mutation receipt plan is invalid")
|
|
1106
|
+
plan_sha256 = mutation.get("plan_sha256")
|
|
1107
|
+
if not isinstance(plan_sha256, str):
|
|
1108
|
+
raise ConfigurationError("mutation receipt plan digest is missing")
|
|
1109
|
+
plan = {key: value for key, value in mutation.items() if key != "plan_sha256"}
|
|
1110
|
+
if _digest(plan) != plan_sha256:
|
|
1111
|
+
raise ConfigurationError("mutation receipt plan digest does not match")
|
|
1112
|
+
if state in {"sensitive", "insensitive"} and plan_sha256 is None:
|
|
1113
|
+
raise ConfigurationError("mutation receipt state requires a mutation plan")
|
|
1114
|
+
return MutationReceiptSummary(
|
|
1115
|
+
hashlib.sha256(raw).hexdigest(),
|
|
1116
|
+
state,
|
|
1117
|
+
relationship_id,
|
|
1118
|
+
plan_sha256,
|
|
1119
|
+
False,
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _collect_findings(
|
|
1124
|
+
evidence: RepositoryEvidence,
|
|
1125
|
+
impact: ImpactPlan,
|
|
1126
|
+
) -> tuple[VerdictFinding, ...]:
|
|
1127
|
+
findings: list[VerdictFinding] = []
|
|
1128
|
+
for audit_finding in evidence.audit.findings:
|
|
1129
|
+
findings.append(
|
|
1130
|
+
_finding(
|
|
1131
|
+
audit_finding.rule,
|
|
1132
|
+
"error",
|
|
1133
|
+
audit_finding.path,
|
|
1134
|
+
audit_finding.detail,
|
|
1135
|
+
f"sourcebound explain {audit_finding.rule}",
|
|
1136
|
+
)
|
|
1137
|
+
)
|
|
1138
|
+
for stale_finding in evidence.audit.stale_baseline:
|
|
1139
|
+
findings.append(
|
|
1140
|
+
_finding(
|
|
1141
|
+
"stale-baseline",
|
|
1142
|
+
"error",
|
|
1143
|
+
stale_finding.path,
|
|
1144
|
+
f"resolved {stale_finding.rule} debt remains in the accepted baseline",
|
|
1145
|
+
"sourcebound audit --update-baseline",
|
|
1146
|
+
)
|
|
1147
|
+
)
|
|
1148
|
+
for result in evidence.bindings:
|
|
1149
|
+
if result.changed and result.state != "skipped-untrusted-execution":
|
|
1150
|
+
findings.append(
|
|
1151
|
+
_finding(
|
|
1152
|
+
"binding-drift",
|
|
1153
|
+
"error",
|
|
1154
|
+
result.doc,
|
|
1155
|
+
f"{result.binding_type} binding {result.binding_id} is stale",
|
|
1156
|
+
(
|
|
1157
|
+
f"sourcebound drive --binding {result.binding_id}"
|
|
1158
|
+
if result.binding_type == "region"
|
|
1159
|
+
else "repair the configured relationship, then run "
|
|
1160
|
+
f"sourcebound check --binding {result.binding_id}"
|
|
1161
|
+
),
|
|
1162
|
+
)
|
|
1163
|
+
)
|
|
1164
|
+
for result in evidence.projections:
|
|
1165
|
+
if result.changed:
|
|
1166
|
+
findings.append(
|
|
1167
|
+
_finding(
|
|
1168
|
+
"projection-drift",
|
|
1169
|
+
"error",
|
|
1170
|
+
result.doc,
|
|
1171
|
+
f"projection {result.binding_id} is stale",
|
|
1172
|
+
"sourcebound project",
|
|
1173
|
+
)
|
|
1174
|
+
)
|
|
1175
|
+
if evidence.source_claims is not None:
|
|
1176
|
+
for source_claim in evidence.source_claims.results:
|
|
1177
|
+
if source_claim.status == "drift":
|
|
1178
|
+
findings.append(
|
|
1179
|
+
_finding(
|
|
1180
|
+
"source-claim-drift",
|
|
1181
|
+
"error",
|
|
1182
|
+
source_claim.doc,
|
|
1183
|
+
f"accepted source claim {source_claim.id} is stale",
|
|
1184
|
+
"update the documented value or accepted relationship, "
|
|
1185
|
+
"then run sourcebound claims",
|
|
1186
|
+
)
|
|
1187
|
+
)
|
|
1188
|
+
for missing in evidence.source_claims.missing:
|
|
1189
|
+
findings.append(
|
|
1190
|
+
_finding(
|
|
1191
|
+
"source-claim-missing",
|
|
1192
|
+
"error",
|
|
1193
|
+
missing.doc,
|
|
1194
|
+
f"accepted source claim {missing.id} is missing",
|
|
1195
|
+
"restore the documented claim and source locator or remove "
|
|
1196
|
+
"the obsolete relationship from .sourcebound.yml",
|
|
1197
|
+
)
|
|
1198
|
+
)
|
|
1199
|
+
assert evidence.changed is not None
|
|
1200
|
+
for changed_finding in evidence.changed.required:
|
|
1201
|
+
level = "warning" if changed_finding.rule == "execution-skipped" else "error"
|
|
1202
|
+
findings.append(
|
|
1203
|
+
_finding(
|
|
1204
|
+
changed_finding.rule,
|
|
1205
|
+
level,
|
|
1206
|
+
changed_finding.doc or changed_finding.source,
|
|
1207
|
+
changed_finding.message,
|
|
1208
|
+
changed_finding.repair,
|
|
1209
|
+
)
|
|
1210
|
+
)
|
|
1211
|
+
for coverage_gap in evidence.changed.gaps:
|
|
1212
|
+
findings.append(
|
|
1213
|
+
_finding(
|
|
1214
|
+
coverage_gap.rule,
|
|
1215
|
+
"warning",
|
|
1216
|
+
coverage_gap.doc or coverage_gap.source,
|
|
1217
|
+
coverage_gap.message,
|
|
1218
|
+
coverage_gap.repair,
|
|
1219
|
+
)
|
|
1220
|
+
)
|
|
1221
|
+
for impact_unknown in impact.unknown:
|
|
1222
|
+
findings.append(
|
|
1223
|
+
_finding(
|
|
1224
|
+
impact_unknown.rule,
|
|
1225
|
+
"warning",
|
|
1226
|
+
(impact_unknown.paths[0] if impact_unknown.paths else impact.manifest),
|
|
1227
|
+
impact_unknown.message,
|
|
1228
|
+
"resolve the unsupported surface or declare its disposition, "
|
|
1229
|
+
"then rerun sourcebound plan",
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
1232
|
+
for contract in impact.review_contracts:
|
|
1233
|
+
targets = contract.targets or contract.sources
|
|
1234
|
+
path = targets[0].path if targets else impact.manifest
|
|
1235
|
+
if contract.state == "review-recommended":
|
|
1236
|
+
findings.append(
|
|
1237
|
+
_finding(
|
|
1238
|
+
"review-contract-review-recommended",
|
|
1239
|
+
"note",
|
|
1240
|
+
path,
|
|
1241
|
+
f"review contract {contract.contract_id} observed source "
|
|
1242
|
+
"change without substantive target change",
|
|
1243
|
+
"review the configured target against the source evidence; "
|
|
1244
|
+
"update it only if the documented guidance changed",
|
|
1245
|
+
)
|
|
1246
|
+
)
|
|
1247
|
+
elif contract.state == "unknown":
|
|
1248
|
+
findings.append(
|
|
1249
|
+
_finding(
|
|
1250
|
+
"review-contract-unknown",
|
|
1251
|
+
"note",
|
|
1252
|
+
path,
|
|
1253
|
+
f"review contract {contract.contract_id} could not resolve "
|
|
1254
|
+
"every configured locator",
|
|
1255
|
+
"repair the unresolved review-contract locator, then rerun "
|
|
1256
|
+
"sourcebound verdict",
|
|
1257
|
+
)
|
|
1258
|
+
)
|
|
1259
|
+
unique = {finding.id: finding for finding in findings}
|
|
1260
|
+
return tuple(
|
|
1261
|
+
sorted(
|
|
1262
|
+
unique.values(),
|
|
1263
|
+
key=lambda item: (item.level, item.path, item.rule, item.id),
|
|
1264
|
+
)
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def build_pr_verdict(
|
|
1269
|
+
root: Path,
|
|
1270
|
+
manifest_path: Path,
|
|
1271
|
+
*,
|
|
1272
|
+
base: str,
|
|
1273
|
+
head: str,
|
|
1274
|
+
mutation_receipt_paths: tuple[Path, ...] = (),
|
|
1275
|
+
) -> PullRequestVerdict:
|
|
1276
|
+
root = root.resolve()
|
|
1277
|
+
status = _git(
|
|
1278
|
+
root,
|
|
1279
|
+
"status",
|
|
1280
|
+
"--porcelain=v1",
|
|
1281
|
+
"-z",
|
|
1282
|
+
"--untracked-files=all",
|
|
1283
|
+
)
|
|
1284
|
+
if status:
|
|
1285
|
+
raise ConfigurationError("verdict requires a clean caller worktree")
|
|
1286
|
+
head_sha = _git(root, "rev-parse", head).decode().strip()
|
|
1287
|
+
caller_head = _git(root, "rev-parse", "HEAD").decode().strip()
|
|
1288
|
+
if caller_head != head_sha:
|
|
1289
|
+
raise ConfigurationError("verdict head must match the checked-out commit")
|
|
1290
|
+
impact = build_impact_plan(
|
|
1291
|
+
root,
|
|
1292
|
+
manifest_path,
|
|
1293
|
+
base=base,
|
|
1294
|
+
head=head_sha,
|
|
1295
|
+
use_cache=False,
|
|
1296
|
+
execution_policy=ExecutionPolicy.STATIC_ONLY,
|
|
1297
|
+
)
|
|
1298
|
+
evidence = collect_repository_evidence(
|
|
1299
|
+
root,
|
|
1300
|
+
manifest_path,
|
|
1301
|
+
base=impact.merge_base,
|
|
1302
|
+
head=head_sha,
|
|
1303
|
+
execution_policy=ExecutionPolicy.STATIC_ONLY,
|
|
1304
|
+
use_cache=False,
|
|
1305
|
+
)
|
|
1306
|
+
findings = _collect_findings(evidence, impact)
|
|
1307
|
+
state = _derive_gate_state(
|
|
1308
|
+
(finding.level for finding in findings),
|
|
1309
|
+
coverage_complete=impact.coverage_complete,
|
|
1310
|
+
impact=impact.impact,
|
|
1311
|
+
)
|
|
1312
|
+
try:
|
|
1313
|
+
manifest_relative = manifest_path.resolve().relative_to(root).as_posix()
|
|
1314
|
+
except ValueError as exc:
|
|
1315
|
+
raise ConfigurationError(
|
|
1316
|
+
"verdict manifest must be inside the repository"
|
|
1317
|
+
) from exc
|
|
1318
|
+
summaries = tuple(
|
|
1319
|
+
_mutation_summary(path, head_sha) for path in mutation_receipt_paths
|
|
1320
|
+
)
|
|
1321
|
+
return PullRequestVerdict(
|
|
1322
|
+
state,
|
|
1323
|
+
impact.requested_base,
|
|
1324
|
+
impact.merge_base,
|
|
1325
|
+
head_sha,
|
|
1326
|
+
manifest_relative,
|
|
1327
|
+
impact.manifest_digest,
|
|
1328
|
+
impact,
|
|
1329
|
+
evidence,
|
|
1330
|
+
findings,
|
|
1331
|
+
summaries,
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def render_verdict_payload_sarif(payload: Mapping[str, object]) -> str:
|
|
1336
|
+
"""Render SARIF from one already-computed verdict payload."""
|
|
1337
|
+
validate_verdict_payload(payload)
|
|
1338
|
+
producer = payload.get("producer")
|
|
1339
|
+
findings = payload["findings"]
|
|
1340
|
+
assert isinstance(producer, dict)
|
|
1341
|
+
assert isinstance(findings, list)
|
|
1342
|
+
version = producer.get("version")
|
|
1343
|
+
if not isinstance(version, str):
|
|
1344
|
+
raise ConfigurationError("verdict producer version is invalid")
|
|
1345
|
+
rules = sorted(
|
|
1346
|
+
{str(finding["rule"]) for finding in findings if isinstance(finding, dict)}
|
|
1347
|
+
)
|
|
1348
|
+
sarif = {
|
|
1349
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
1350
|
+
"version": "2.1.0",
|
|
1351
|
+
"runs": [
|
|
1352
|
+
{
|
|
1353
|
+
"tool": {
|
|
1354
|
+
"driver": {
|
|
1355
|
+
"name": "sourcebound",
|
|
1356
|
+
"version": version,
|
|
1357
|
+
"rules": [
|
|
1358
|
+
{
|
|
1359
|
+
"id": rule,
|
|
1360
|
+
"shortDescription": {"text": rule.replace("-", " ")},
|
|
1361
|
+
}
|
|
1362
|
+
for rule in rules
|
|
1363
|
+
],
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1366
|
+
"properties": {
|
|
1367
|
+
"cleanDocsVerdictState": payload["state"],
|
|
1368
|
+
"cleanDocsVerdictDigest": payload["digest"],
|
|
1369
|
+
},
|
|
1370
|
+
"results": [
|
|
1371
|
+
{
|
|
1372
|
+
"ruleId": finding["rule"],
|
|
1373
|
+
"level": finding["level"],
|
|
1374
|
+
"message": {
|
|
1375
|
+
"text": (
|
|
1376
|
+
f"{finding['message']}. Repair: {finding['repair']}"
|
|
1377
|
+
)
|
|
1378
|
+
},
|
|
1379
|
+
"partialFingerprints": {"cleanDocsFindingId": finding["id"]},
|
|
1380
|
+
"locations": [
|
|
1381
|
+
{
|
|
1382
|
+
"physicalLocation": {
|
|
1383
|
+
"artifactLocation": {"uri": finding["path"]}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
],
|
|
1387
|
+
}
|
|
1388
|
+
for finding in findings
|
|
1389
|
+
],
|
|
1390
|
+
}
|
|
1391
|
+
],
|
|
1392
|
+
}
|
|
1393
|
+
return json.dumps(sarif, indent=2, sort_keys=True) + "\n"
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def render_verdict_sarif(verdict: PullRequestVerdict) -> str:
|
|
1397
|
+
return render_verdict_payload_sarif(verdict.as_dict())
|