claimproofkit 0.1.0__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.
claimproofkit/cli.py ADDED
@@ -0,0 +1,136 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """``claimproofkit`` CLI — mirrors the Swift executable's commands and exit codes.
5
+
6
+ Exit codes: 0 published / gate passed, 2 blocked, 3 benchmark gate failed,
7
+ 64 usage, 65 unreadable input, 73 provenance trace write failed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sys
14
+ import time
15
+ from typing import List, Optional
16
+
17
+ from .benchmark import BenchmarkCorpus, BenchmarkRunner
18
+ from .models import EvidencePassage, SourceType, VerificationPolicy
19
+ from .provenance import ProvenanceExporter
20
+ from .report import benchmark_report_json, render_benchmark_markdown, render_report_markdown
21
+ from .verifier import ClaimVerifier
22
+
23
+ USAGE = """Usage:
24
+ claimproofkit verify <input.json> [--provenance <trace.json>]
25
+ claimproofkit benchmark <corpus.json> [--json]
26
+
27
+ --provenance writes a DProvenanceKit-compatible trace manifest of the
28
+ verification run alongside the report.
29
+
30
+ Exit codes: 0 published, 2 blocked, 3 benchmark gate failed,
31
+ 64 usage, 65 unreadable input, 73 provenance trace write failed.
32
+
33
+ Backward compatibility: claimproofkit <input.json> runs verification.
34
+ """
35
+
36
+
37
+ def _usage(code: int) -> int:
38
+ sys.stderr.write(USAGE)
39
+ return code
40
+
41
+
42
+ def _run_verification(path: str, provenance_path: Optional[str]) -> int:
43
+ try:
44
+ with open(path, encoding="utf-8") as handle:
45
+ data = json.load(handle)
46
+ evidence = [
47
+ EvidencePassage(
48
+ id=p["id"],
49
+ source=p["source"],
50
+ source_type=SourceType(p["sourceType"]),
51
+ text=p["text"],
52
+ )
53
+ for p in data["evidence"]
54
+ ]
55
+ policy = VerificationPolicy()
56
+ report = ClaimVerifier(policy).verify_document(data["document"], evidence, created_at=time.time())
57
+ except Exception as error: # noqa: BLE001 - CLI boundary
58
+ sys.stderr.write(f"claimproofkit: {error}\n")
59
+ return 65
60
+
61
+ # The report is always emitted, even if the trace write fails afterwards.
62
+ print(render_report_markdown(report))
63
+ if provenance_path is not None:
64
+ try:
65
+ trace = ProvenanceExporter().trace(report, policy)
66
+ with open(provenance_path, "w", encoding="utf-8") as handle:
67
+ handle.write(trace.manifest_json())
68
+ sys.stderr.write(
69
+ f"provenance trace written to {provenance_path} (run fingerprint {trace.run_fingerprint})\n"
70
+ )
71
+ except Exception as error: # noqa: BLE001 - CLI boundary
72
+ sys.stderr.write(f"claimproofkit: failed to write provenance trace to {provenance_path}: {error}\n")
73
+ return 73
74
+ return 0 if report.is_safe_to_publish else 2
75
+
76
+
77
+ def _run_benchmark(path: str, as_json: bool) -> int:
78
+ try:
79
+ with open(path, encoding="utf-8") as handle:
80
+ corpus = BenchmarkCorpus.from_dict(json.load(handle))
81
+ except Exception as error: # noqa: BLE001 - CLI boundary
82
+ sys.stderr.write(f"claimproofkit: {error}\n")
83
+ return 65
84
+ report = BenchmarkRunner().run(corpus)
85
+ print(benchmark_report_json(report) if as_json else render_benchmark_markdown(report))
86
+ return 0 if report.passes_safety_gate else 3
87
+
88
+
89
+ def main(argv: Optional[List[str]] = None) -> int:
90
+ # The Swift CLI writes UTF-8 bytes unconditionally; without this, a
91
+ # non-UTF-8 stdout (e.g. cp1252) crashes mid-print with exit 1 and breaks
92
+ # the documented exit-code contract.
93
+ for stream in (sys.stdout, sys.stderr):
94
+ reconfigure = getattr(stream, "reconfigure", None)
95
+ if reconfigure is not None:
96
+ reconfigure(encoding="utf-8", errors="backslashreplace")
97
+ arguments = list(sys.argv[1:] if argv is None else argv)
98
+ if "--help" in arguments or "-h" in arguments:
99
+ sys.stderr.write(USAGE)
100
+ return 0
101
+ if not arguments:
102
+ return _usage(64)
103
+
104
+ command = arguments[0]
105
+ if command == "verify":
106
+ path: Optional[str] = None
107
+ provenance: Optional[str] = None
108
+ rest = arguments[1:]
109
+ while rest:
110
+ argument = rest.pop(0)
111
+ if argument == "--provenance":
112
+ if provenance is not None or not rest:
113
+ return _usage(64)
114
+ provenance = rest.pop(0)
115
+ elif path is None:
116
+ path = argument
117
+ else:
118
+ return _usage(64)
119
+ if path is None:
120
+ return _usage(64)
121
+ return _run_verification(path, provenance)
122
+ if command == "benchmark":
123
+ rest = arguments[1:]
124
+ if len(rest) == 1:
125
+ return _run_benchmark(rest[0], as_json=False)
126
+ if len(rest) == 2 and rest[1] == "--json":
127
+ return _run_benchmark(rest[0], as_json=True)
128
+ return _usage(64)
129
+ if len(arguments) == 1:
130
+ # Backward compatibility: a lone path runs verification.
131
+ return _run_verification(command, None)
132
+ return _usage(64)
133
+
134
+
135
+ if __name__ == "__main__":
136
+ raise SystemExit(main())
@@ -0,0 +1,113 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Claim extraction — mirrors the Swift `ClaimExtractor.swift`.
5
+
6
+ Claim ids and ranges use UTF-16 code-unit offsets (Swift extracts with
7
+ NSRegularExpression over NSString), so ids like ``claim-46-0`` are identical
8
+ across both implementations even for documents containing astral-plane
9
+ characters.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from typing import List
16
+
17
+ from ._text import split_words, swift_lower, swift_trim_whitespace
18
+ from .models import Claim, ClaimType
19
+
20
+ # ICU's non-multiline `$` (NSRegularExpression) matches at end of string AND
21
+ # before a final line terminator; Python's non-MULTILINE `$` matches at end
22
+ # and before a final "\n". Combined with the `[^.!?\n]+` class and the
23
+ # Swift-parity trimming below, the extracted claim text and fingerprint are
24
+ # identical for every terminator (\n, \r\n, U+0085, U+2028, U+2029) —
25
+ # verified differentially against the Swift CLI.
26
+ _SENTENCE = re.compile(r"[^.!?\n]+(?:[.!?]+|$)")
27
+
28
+ # ICU regex `\s` does NOT include the information separators U+001C-U+001F;
29
+ # Python's Unicode `\s` does, so the whitespace class is spelled out to match
30
+ # ICU's set exactly.
31
+ _ICU_WS = "\\t-\\r \\x85\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000"
32
+ _CLAUSE_SEPARATOR = re.compile(f"[{_ICU_WS}]+(?:and|but)[{_ICU_WS}]+", re.IGNORECASE)
33
+
34
+ _NON_CLAIMS = ("please note", "this document is not legal advice")
35
+
36
+
37
+ def _utf16_offset(document: str, char_index: int) -> int:
38
+ """Number of UTF-16 code units before ``char_index`` (Swift NSRange origin)."""
39
+ prefix = document[:char_index]
40
+ return sum(2 if ord(ch) > 0xFFFF else 1 for ch in prefix)
41
+
42
+
43
+ def _utf16_length(text: str) -> int:
44
+ return sum(2 if ord(ch) > 0xFFFF else 1 for ch in text)
45
+
46
+
47
+ def _is_material_claim(text: str) -> bool:
48
+ if not text or text.endswith("?"):
49
+ return False
50
+ if len(split_words(text)) < 4:
51
+ return False
52
+ lower = swift_lower(text)
53
+ return not any(marker in lower for marker in _NON_CLAIMS)
54
+
55
+
56
+ def _atomic_clauses(sentence: str) -> List[str]:
57
+ # Swift replaces separator matches with a U+001F sentinel and then splits
58
+ # on it, so a pre-existing U+001F in the sentence also acts as a clause
59
+ # boundary. Reproduce that exactly rather than using re.split.
60
+ sentinel = _CLAUSE_SEPARATOR.sub("\x1f", sentence)
61
+ parts = [swift_trim_whitespace(part) for part in sentinel.split("\x1f")]
62
+ parts = [p for p in parts if p]
63
+ # Swift's split(separator: " ") drops empty subsequences, so runs of spaces
64
+ # must not inflate the word count here.
65
+ if len(parts) > 1 and all(len([w for w in p.split(" ") if w]) >= 3 for p in parts):
66
+ return parts
67
+ return [sentence]
68
+
69
+
70
+ def _classify(claim: str) -> ClaimType:
71
+ lower = swift_lower(claim)
72
+ if " recommends " in lower or lower.startswith("recommend "):
73
+ return ClaimType.RECOMMENDATION
74
+ if " therefore " in lower or " violated " in lower:
75
+ return ClaimType.LEGAL_CONCLUSION
76
+ if (
77
+ " law requires " in lower
78
+ or " statute " in lower
79
+ or " rule requires " in lower
80
+ or " must be filed" in lower
81
+ or " shall be filed" in lower
82
+ ):
83
+ return ClaimType.LEGAL_RULE
84
+ if " calculated " in lower or " equals " in lower:
85
+ return ClaimType.CALCULATION
86
+ if '"' in claim:
87
+ return ClaimType.QUOTATION
88
+ return ClaimType.FACTUAL
89
+
90
+
91
+ class ClaimExtractor:
92
+ """Deterministic sentence/clause extractor, spec-locked with the Swift one."""
93
+
94
+ def extract(self, document: str) -> List[Claim]:
95
+ claims: List[Claim] = []
96
+ for match in _SENTENCE.finditer(document):
97
+ raw = match.group(0)
98
+ text = swift_trim_whitespace(raw)
99
+ if not _is_material_claim(text):
100
+ continue
101
+ start = _utf16_offset(document, match.start())
102
+ length = _utf16_length(raw)
103
+ for index, clause in enumerate(_atomic_clauses(text)):
104
+ claims.append(
105
+ Claim(
106
+ id=f"claim-{start}-{index}",
107
+ text=clause,
108
+ type=_classify(clause),
109
+ parent_text=None if clause == text else text,
110
+ range=(start, start + length),
111
+ )
112
+ )
113
+ return claims
@@ -0,0 +1,134 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Claim Proof Specification v1 identity algorithms.
5
+
6
+ Mirrors the Swift `ProofIdentity.swift` byte-for-byte. Every function here is
7
+ locked by the golden vectors vendored under ``conformance/vectors`` (whose
8
+ source of truth is the Swift repo's ConformanceHarness).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import math
16
+ from typing import Any
17
+
18
+ from .models import EvidencePassage, ProofReport, VerificationPolicy
19
+
20
+ SPEC_VERSION = "1.0"
21
+ VERIFIER_IDENTIFIER = "cpk-lexical"
22
+ VERIFIER_VERSION = "1.0"
23
+
24
+
25
+ def sha256_hex(text: str) -> str:
26
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
27
+
28
+
29
+ def swift_number(value: Any) -> str:
30
+ """Render a number the way Swift's ``JSONEncoder`` does.
31
+
32
+ Integral Doubles print without a fractional part (``Double(1)`` → ``"1"``)
33
+ up to the magnitude where Swift switches to exponent form (1e16); above
34
+ that, and for all non-integral values, Swift uses shortest round-trip
35
+ rendering, which matches Python's ``repr`` for finite floats. Negative
36
+ zero stays ``"-0"``.
37
+ """
38
+ if isinstance(value, bool): # bool is an int subclass; keep JSON booleans out
39
+ raise TypeError("swift_number does not take booleans")
40
+ if isinstance(value, int):
41
+ return str(value)
42
+ if isinstance(value, float):
43
+ if not math.isfinite(value):
44
+ raise ValueError("non-finite numbers are not canonically encodable")
45
+ if value == 0 and math.copysign(1, value) < 0:
46
+ return "-0"
47
+ if value.is_integer() and abs(value) < 1e16:
48
+ return str(int(value))
49
+ return repr(value)
50
+ raise TypeError(f"not a number: {value!r}")
51
+
52
+
53
+ def swift_interpolation(value: Any) -> str:
54
+ """Render a number the way Swift STRING INTERPOLATION does — distinct from
55
+ JSON encoding: ``"\\(Double(1))"`` is ``"1.0"``, not ``"1"``. Python's
56
+ ``repr`` for floats matches Swift's Double description exactly for the
57
+ shortest-round-trip cases used in fingerprint listings.
58
+ """
59
+ if isinstance(value, bool):
60
+ raise TypeError("swift_interpolation does not take booleans")
61
+ if isinstance(value, int):
62
+ return str(value)
63
+ if isinstance(value, float):
64
+ if not math.isfinite(value):
65
+ raise ValueError("non-finite numbers are not interpolatable")
66
+ return repr(value)
67
+ raise TypeError(f"not a number: {value!r}")
68
+
69
+
70
+ def _json_string(text: str) -> str:
71
+ # Swift's JSONEncoder escapes forward slashes by default (the bridge encodes
72
+ # rawJSON with plain .sortedKeys), so the canonical form does too.
73
+ return json.dumps(text, ensure_ascii=False).replace("/", "\\/")
74
+
75
+
76
+ def _canonical_fragment(value: Any) -> str:
77
+ if isinstance(value, str):
78
+ return _json_string(value)
79
+ if isinstance(value, bool):
80
+ return "true" if value else "false"
81
+ if isinstance(value, (int, float)):
82
+ return swift_number(value)
83
+ if isinstance(value, dict):
84
+ items = sorted(value.items(), key=lambda kv: kv[0])
85
+ inner = ",".join(f"{_json_string(k)}:{_canonical_fragment(v)}" for k, v in items)
86
+ return "{" + inner + "}"
87
+ if isinstance(value, (list, tuple)):
88
+ return "[" + ",".join(_canonical_fragment(v) for v in value) + "]"
89
+ if value is None:
90
+ return "null"
91
+ raise TypeError(f"not canonically encodable: {type(value).__name__}")
92
+
93
+
94
+ def canonical_json(value: Any) -> str:
95
+ """Canonical payload encoding (Spec §2): sorted keys, UTF-8, no extra
96
+ whitespace, Swift number rendering. Byte-identical to Swift's
97
+ ``JSONEncoder`` with ``.sortedKeys`` for the payload types used on the wire.
98
+ """
99
+ return _canonical_fragment(value)
100
+
101
+
102
+ def evidence_content_fingerprint(passage: EvidencePassage) -> str:
103
+ """Spec §5: SHA-256 over the exact UTF-8 bytes of the passage text."""
104
+ return sha256_hex(passage.text)
105
+
106
+
107
+ def report_fingerprint(report: ProofReport) -> str:
108
+ """Spec §3: SHA-256 over the domain-separated verdict signature string."""
109
+ signature = "".join(f"{v.claim.fingerprint}:{v.status.value}|" for v in report.verdicts)
110
+ return sha256_hex("cpk:v1:report:" + signature)
111
+
112
+
113
+ def policy_fingerprint(
114
+ policy: VerificationPolicy,
115
+ verifier: str = VERIFIER_IDENTIFIER,
116
+ verifier_version: str = VERIFIER_VERSION,
117
+ ) -> str:
118
+ """Spec §4: SHA-256 over the frozen newline-delimited key:value listing.
119
+
120
+ The Swift reference builds this listing with STRING INTERPOLATION (not
121
+ JSONEncoder), so integral thresholds render as ``1.0``, never ``1``.
122
+ """
123
+ canonical = "\n".join(
124
+ [
125
+ f"specVersion:{SPEC_VERSION}",
126
+ f"verifier:{verifier}",
127
+ f"verifierVersion:{verifier_version}",
128
+ f"supportedThreshold:{swift_interpolation(float(policy.supported_threshold))}",
129
+ f"ambiguousThreshold:{swift_interpolation(float(policy.ambiguous_threshold))}",
130
+ f"contradictionThreshold:{swift_interpolation(float(policy.contradiction_threshold))}",
131
+ f"maximumEvidenceMatches:{policy.maximum_evidence_matches}",
132
+ ]
133
+ )
134
+ return sha256_hex(canonical)
@@ -0,0 +1,133 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Core data model — mirrors the Swift ClaimProofKit `Models.swift` exactly.
5
+
6
+ Claim Proof Specification v1 locks the claim fingerprint algorithm here; the
7
+ rest of the identity surface lives in :mod:`claimproofkit.identity`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from typing import Optional, Tuple
15
+
16
+
17
+ class SourceType(str, Enum):
18
+ EVIDENCE = "evidence"
19
+ DECLARATION = "declaration"
20
+ COURT_RECORD = "courtRecord"
21
+ STATUTE = "statute"
22
+ REGULATION = "regulation"
23
+ COURT_RULE = "courtRule"
24
+ CASE_LAW = "caseLaw"
25
+ CALCULATION_INPUT = "calculationInput"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class EvidencePassage:
31
+ id: str
32
+ source: str
33
+ text: str
34
+ source_type: SourceType = SourceType.UNKNOWN
35
+
36
+
37
+ class ClaimType(str, Enum):
38
+ FACTUAL = "factual"
39
+ LEGAL_RULE = "legalRule"
40
+ LEGAL_CONCLUSION = "legalConclusion"
41
+ CALCULATION = "calculation"
42
+ RECOMMENDATION = "recommendation"
43
+ QUOTATION = "quotation"
44
+ UNKNOWN = "unknown"
45
+
46
+
47
+ def make_claim_fingerprint(text: str) -> str:
48
+ """FNV-1a 64-bit over the lowercased UTF-8 claim text, unpadded lowercase hex.
49
+
50
+ Spec §1. Byte-for-byte identical to Swift's ``Claim.makeFingerprint``:
51
+ offset basis 14695981039346656037, prime 1099511628211, XOR-then-wrapping-
52
+ multiply per byte, rendered via ``String(hash, radix: 16)`` (no zero padding).
53
+ Lowercasing uses ``swift_lower`` (no Final_Sigma rule) and hashes the
54
+ original scalars — no Unicode normalization, matching Swift exactly.
55
+ """
56
+ from ._text import swift_lower
57
+
58
+ fnv = 14_695_981_039_346_656_037
59
+ for byte in swift_lower(text).encode("utf-8"):
60
+ fnv ^= byte
61
+ fnv = (fnv * 1_099_511_628_211) & 0xFFFF_FFFF_FFFF_FFFF
62
+ return format(fnv, "x")
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Claim:
67
+ id: str
68
+ text: str
69
+ type: ClaimType = ClaimType.UNKNOWN
70
+ parent_text: Optional[str] = None
71
+ fingerprint: str = ""
72
+ range: Optional[Tuple[int, int]] = None # UTF-16 code-unit offsets, like Swift
73
+
74
+ def __post_init__(self) -> None:
75
+ if not self.fingerprint:
76
+ object.__setattr__(self, "fingerprint", make_claim_fingerprint(self.text))
77
+
78
+
79
+ class SupportStatus(str, Enum):
80
+ DIRECTLY_SUPPORTED = "directlySupported"
81
+ SUPPORTED_BY_INFERENCE = "supportedByInference"
82
+ PARTIALLY_SUPPORTED = "partiallySupported"
83
+ CONTRADICTED = "contradicted"
84
+ AMBIGUOUS = "ambiguous"
85
+ UNSUPPORTED = "unsupported"
86
+ NOT_VERIFIABLE = "notVerifiable"
87
+
88
+
89
+ APPROVED_STATUSES = frozenset(
90
+ {SupportStatus.DIRECTLY_SUPPORTED, SupportStatus.SUPPORTED_BY_INFERENCE}
91
+ )
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class EvidenceMatch:
96
+ passage: EvidencePassage
97
+ score: float
98
+ matched_terms: Tuple[str, ...]
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class ClaimVerdict:
103
+ claim: Claim
104
+ status: SupportStatus
105
+ confidence: float
106
+ evidence: Tuple[EvidenceMatch, ...]
107
+ explanation: str
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class ProofReport:
112
+ verdicts: Tuple[ClaimVerdict, ...]
113
+ version: int = 1
114
+ created_at: float = 0.0 # POSIX seconds; explicit, never implicit "now"
115
+
116
+ @property
117
+ def is_safe_to_publish(self) -> bool:
118
+ return bool(self.verdicts) and all(v.status in APPROVED_STATUSES for v in self.verdicts)
119
+
120
+ @property
121
+ def support_rate(self) -> float:
122
+ if not self.verdicts:
123
+ return 0.0
124
+ supported = sum(1 for v in self.verdicts if v.status in APPROVED_STATUSES)
125
+ return supported / len(self.verdicts)
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class VerificationPolicy:
130
+ supported_threshold: float = 0.80
131
+ ambiguous_threshold: float = 0.40
132
+ contradiction_threshold: float = 0.55
133
+ maximum_evidence_matches: int = 3
@@ -0,0 +1,145 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Pluggable verification pipeline — mirrors the Swift `VerificationProtocols.swift`
5
+ and `VerificationPipeline.swift` contracts so local or external verifiers
6
+ (semantic entailment, retrieval, human review) can be substituted without
7
+ changing the report contract. Swift's async protocol methods map to plain
8
+ Python methods; substitute async implementations by overriding `verify_document`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ from typing import List, Sequence, Tuple
17
+
18
+ from .extractor import ClaimExtractor
19
+ from .models import Claim, ClaimVerdict, EvidencePassage, ProofReport, SupportStatus
20
+ from .verifier import ClaimVerifier
21
+
22
+
23
+ class AtomicClaimExtracting(ABC):
24
+ @abstractmethod
25
+ def extract(self, document: str) -> List[Claim]: ...
26
+
27
+
28
+ class EvidenceRetrieving(ABC):
29
+ @abstractmethod
30
+ def retrieve(
31
+ self, claim: Claim, corpus: Sequence[EvidencePassage], limit: int
32
+ ) -> List[EvidencePassage]: ...
33
+
34
+
35
+ class ClaimEntailmentVerifying(ABC):
36
+ @abstractmethod
37
+ def verify(self, claim: Claim, evidence: Sequence[EvidencePassage]) -> ClaimVerdict: ...
38
+
39
+
40
+ class ProofPolicyEvaluating(ABC):
41
+ @abstractmethod
42
+ def evaluate(self, report: ProofReport) -> "ProofDecision": ...
43
+
44
+
45
+ class TrustReportGenerating(ABC):
46
+ @abstractmethod
47
+ def generate(self, verdicts: Sequence[ClaimVerdict]) -> ProofReport: ...
48
+
49
+
50
+ class Disposition(str, Enum):
51
+ ALLOW = "allow"
52
+ REQUIRE_REVIEW = "requireReview"
53
+ BLOCK = "block"
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class ProofDecision:
58
+ disposition: Disposition
59
+ blocking_claim_ids: Tuple[str, ...] = ()
60
+ review_claim_ids: Tuple[str, ...] = ()
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class PipelineResult:
65
+ report: ProofReport
66
+ decision: ProofDecision
67
+
68
+
69
+ class PassthroughEvidenceRetriever(EvidenceRetrieving):
70
+ def retrieve(
71
+ self, claim: Claim, corpus: Sequence[EvidencePassage], limit: int
72
+ ) -> List[EvidencePassage]:
73
+ return list(corpus[: max(1, limit)])
74
+
75
+
76
+ class LexicalEntailmentVerifier(ClaimEntailmentVerifying):
77
+ def __init__(self, verifier: "ClaimVerifier | None" = None):
78
+ self._verifier = verifier if verifier is not None else ClaimVerifier()
79
+
80
+ def verify(self, claim: Claim, evidence: Sequence[EvidencePassage]) -> ClaimVerdict:
81
+ return self._verifier.verify(claim, evidence)
82
+
83
+
84
+ class DefaultTrustReportGenerator(TrustReportGenerating):
85
+ def generate(self, verdicts: Sequence[ClaimVerdict]) -> ProofReport:
86
+ return ProofReport(verdicts=tuple(verdicts))
87
+
88
+
89
+ class ConservativeProofPolicy(ProofPolicyEvaluating):
90
+ def evaluate(self, report: ProofReport) -> ProofDecision:
91
+ blocking = tuple(
92
+ v.claim.id
93
+ for v in report.verdicts
94
+ if v.status in (SupportStatus.CONTRADICTED, SupportStatus.UNSUPPORTED)
95
+ )
96
+ review = tuple(
97
+ v.claim.id
98
+ for v in report.verdicts
99
+ if v.status
100
+ in (SupportStatus.PARTIALLY_SUPPORTED, SupportStatus.AMBIGUOUS, SupportStatus.NOT_VERIFIABLE)
101
+ )
102
+ if blocking:
103
+ return ProofDecision(
104
+ disposition=Disposition.BLOCK, blocking_claim_ids=blocking, review_claim_ids=review
105
+ )
106
+ if review:
107
+ return ProofDecision(disposition=Disposition.REQUIRE_REVIEW, review_claim_ids=review)
108
+ return ProofDecision(disposition=Disposition.ALLOW)
109
+
110
+
111
+ class VerificationPipeline:
112
+ def __init__(
113
+ self,
114
+ extractor: "AtomicClaimExtracting | None" = None,
115
+ retriever: "EvidenceRetrieving | None" = None,
116
+ verifier: "ClaimEntailmentVerifying | None" = None,
117
+ policy: "ProofPolicyEvaluating | None" = None,
118
+ report_generator: "TrustReportGenerating | None" = None,
119
+ evidence_limit: int = 20,
120
+ ):
121
+ self._extractor = extractor if extractor is not None else _DefaultExtractor()
122
+ self._retriever = retriever if retriever is not None else PassthroughEvidenceRetriever()
123
+ self._verifier = verifier if verifier is not None else LexicalEntailmentVerifier()
124
+ self._policy = policy if policy is not None else ConservativeProofPolicy()
125
+ self._report_generator = (
126
+ report_generator if report_generator is not None else DefaultTrustReportGenerator()
127
+ )
128
+ self._evidence_limit = max(1, evidence_limit)
129
+
130
+ def verify_document(self, document: str, corpus: Sequence[EvidencePassage]) -> PipelineResult:
131
+ claims = self._extractor.extract(document)
132
+ verdicts = []
133
+ for claim in claims:
134
+ evidence = self._retriever.retrieve(claim, corpus, self._evidence_limit)
135
+ verdicts.append(self._verifier.verify(claim, evidence))
136
+ report = self._report_generator.generate(verdicts)
137
+ return PipelineResult(report=report, decision=self._policy.evaluate(report))
138
+
139
+
140
+ class _DefaultExtractor(AtomicClaimExtracting):
141
+ def __init__(self) -> None:
142
+ self._extractor = ClaimExtractor()
143
+
144
+ def extract(self, document: str) -> List[Claim]:
145
+ return self._extractor.extract(document)