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.
Files changed (71) hide show
  1. clean_docs/__init__.py +26 -0
  2. clean_docs/__main__.py +3 -0
  3. clean_docs/accessibility.py +182 -0
  4. clean_docs/adapters/__init__.py +1 -0
  5. clean_docs/adapters/event_capture.py +56 -0
  6. clean_docs/adapters/mdx_dependencies.json +714 -0
  7. clean_docs/adapters/mdx_parser.mjs +29992 -0
  8. clean_docs/applicability.py +330 -0
  9. clean_docs/audit.py +1120 -0
  10. clean_docs/bootstrap.py +507 -0
  11. clean_docs/capabilities.py +200 -0
  12. clean_docs/changed.py +452 -0
  13. clean_docs/claims.py +840 -0
  14. clean_docs/cli.py +1612 -0
  15. clean_docs/context.py +307 -0
  16. clean_docs/corpus.py +377 -0
  17. clean_docs/demo.py +369 -0
  18. clean_docs/doctor.py +184 -0
  19. clean_docs/emit/__init__.py +4 -0
  20. clean_docs/emit/llms_txt.py +102 -0
  21. clean_docs/emit/stepwise.py +168 -0
  22. clean_docs/engine.py +324 -0
  23. clean_docs/errors.py +20 -0
  24. clean_docs/evaluation.py +867 -0
  25. clean_docs/execution.py +138 -0
  26. clean_docs/explain.py +123 -0
  27. clean_docs/extractors/__init__.py +19 -0
  28. clean_docs/extractors/command.py +51 -0
  29. clean_docs/extractors/inventory.py +176 -0
  30. clean_docs/extractors/json_pointer.py +84 -0
  31. clean_docs/extractors/python_literal.py +104 -0
  32. clean_docs/extractors/static.py +111 -0
  33. clean_docs/feedback.py +1390 -0
  34. clean_docs/impact.py +1624 -0
  35. clean_docs/improvements.py +1178 -0
  36. clean_docs/inventory.py +474 -0
  37. clean_docs/isolation.py +157 -0
  38. clean_docs/manifest.py +898 -0
  39. clean_docs/mdx.py +272 -0
  40. clean_docs/migration.py +121 -0
  41. clean_docs/models.py +194 -0
  42. clean_docs/outcomes.py +296 -0
  43. clean_docs/performance.py +123 -0
  44. clean_docs/phrasing.py +448 -0
  45. clean_docs/plugins.py +249 -0
  46. clean_docs/policy.py +536 -0
  47. clean_docs/projections.py +255 -0
  48. clean_docs/regions.py +75 -0
  49. clean_docs/release.py +232 -0
  50. clean_docs/renderers.py +57 -0
  51. clean_docs/residue.py +311 -0
  52. clean_docs/review_contracts.py +862 -0
  53. clean_docs/review_ledger.py +297 -0
  54. clean_docs/review_limits.py +9 -0
  55. clean_docs/sensitivity.py +602 -0
  56. clean_docs/snapshot.py +212 -0
  57. clean_docs/standard.py +281 -0
  58. clean_docs/standards/default.json +309 -0
  59. clean_docs/standards/exemplars.md +87 -0
  60. clean_docs/standards/v0-migrations.json +26 -0
  61. clean_docs/symbols.py +47 -0
  62. clean_docs/templates.py +96 -0
  63. clean_docs/verdict.py +1397 -0
  64. clean_docs/visuals.py +346 -0
  65. clean_docs/write_gate.py +170 -0
  66. sourcebound-1.2.1.dist-info/LICENSE +21 -0
  67. sourcebound-1.2.1.dist-info/METADATA +109 -0
  68. sourcebound-1.2.1.dist-info/RECORD +71 -0
  69. sourcebound-1.2.1.dist-info/WHEEL +5 -0
  70. sourcebound-1.2.1.dist-info/entry_points.txt +2 -0
  71. 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())