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.
@@ -0,0 +1,279 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """DProvenanceKit bridge — mirrors the Swift `ClaimProofProvenance` target.
5
+
6
+ Emits envelopes wire-compatible with DProvenanceKit's
7
+ ``TraceEvent<AnyTraceableEvent>`` (camelCase keys, Swift reference-date
8
+ timestamps), with canonical payload bytes per Trace Specification v1 §2 and
9
+ the §5 run fingerprint. Parity with the Swift bridge is locked by the
10
+ ``provenance_bridge.json`` conformance vector.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import json
17
+ import uuid
18
+ from dataclasses import dataclass, field
19
+ from typing import Dict, List, Optional, Sequence
20
+
21
+ from .identity import (
22
+ SPEC_VERSION,
23
+ canonical_json,
24
+ evidence_content_fingerprint,
25
+ policy_fingerprint,
26
+ report_fingerprint,
27
+ swift_number,
28
+ )
29
+ from .models import ClaimVerdict, ProofReport, VerificationPolicy
30
+
31
+ BRIDGE_VERSION = "1.0"
32
+
33
+ CLAIM_EXTRACTED = "cpk_claim_extracted"
34
+ CLAIM_VERIFIED = "cpk_claim_verified"
35
+ REPORT_FINALIZED = "cpk_report_finalized"
36
+
37
+ PRIORITY_TELEMETRY = 0
38
+ PRIORITY_DIAGNOSTIC = 1
39
+ PRIORITY_STRUCTURAL = 2
40
+ PRIORITY_CRITICAL = 3
41
+
42
+ EDGE_DERIVED_FROM = "derivedFrom"
43
+ EDGE_VERIFIED_BY = "verifiedBy"
44
+
45
+ _SWIFT_REFERENCE_EPOCH = 978_307_200.0 # 2001-01-01T00:00:00Z in POSIX seconds
46
+
47
+
48
+ def _claim_extracted_payload(verdict: ClaimVerdict) -> Dict[str, object]:
49
+ claim = verdict.claim
50
+ return {
51
+ "claimID": claim.id,
52
+ "claimType": claim.type.value,
53
+ "fingerprint": claim.fingerprint,
54
+ "text": claim.text,
55
+ }
56
+
57
+
58
+ def _claim_verified_payload(verdict: ClaimVerdict) -> Dict[str, object]:
59
+ return {
60
+ "claimFingerprint": verdict.claim.fingerprint,
61
+ "confidence": verdict.confidence,
62
+ "evidenceFingerprints": [
63
+ evidence_content_fingerprint(match.passage) for match in verdict.evidence
64
+ ],
65
+ "status": verdict.status.value,
66
+ }
67
+
68
+
69
+ def _report_finalized_payload(
70
+ report: ProofReport, policy: VerificationPolicy
71
+ ) -> Dict[str, object]:
72
+ counts: Dict[str, int] = {}
73
+ for verdict in report.verdicts:
74
+ counts[verdict.status.value] = counts.get(verdict.status.value, 0) + 1
75
+ return {
76
+ "policyFingerprint": policy_fingerprint(policy),
77
+ "reportFingerprint": report_fingerprint(report),
78
+ "safeToPublish": report.is_safe_to_publish,
79
+ "specVersion": SPEC_VERSION,
80
+ "statusCounts": counts,
81
+ }
82
+
83
+
84
+ def semantic_key(verdict: ClaimVerdict) -> str:
85
+ """Runtime equivalence hint, mirroring the Swift bridge (never serialized)."""
86
+ return f"{CLAIM_VERIFIED}:{verdict.claim.fingerprint}:{verdict.status.value}"
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class ErasedProvenancePayload:
91
+ type_identifier: str
92
+ priority_value: int
93
+ raw_json: str
94
+
95
+ def to_wire(self) -> Dict[str, object]:
96
+ return {
97
+ "typeIdentifier": self.type_identifier,
98
+ "priorityValue": self.priority_value,
99
+ "rawJSON": self.raw_json,
100
+ }
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class ProvenanceEventEnvelope:
105
+ id: uuid.UUID
106
+ run_id: uuid.UUID
107
+ context_id: str
108
+ engine_name: str
109
+ schema_version: int
110
+ sequence: int
111
+ payload: ErasedProvenancePayload
112
+ timestamp: float # POSIX seconds
113
+ span_id: Optional[str] = None
114
+ parent_span_id: Optional[str] = None
115
+
116
+ def to_wire(self) -> Dict[str, object]:
117
+ wire: Dict[str, object] = {
118
+ "id": str(self.id).upper(), # Swift UUID strings are uppercase
119
+ "runID": str(self.run_id).upper(),
120
+ "contextID": self.context_id,
121
+ "engineName": self.engine_name,
122
+ "schemaVersion": self.schema_version,
123
+ "sequence": self.sequence,
124
+ "payload": self.payload.to_wire(),
125
+ # Swift Codable Date: seconds since the 2001 reference date.
126
+ "timestamp": _RawNumber(self.timestamp - _SWIFT_REFERENCE_EPOCH),
127
+ }
128
+ if self.span_id is not None:
129
+ wire["spanID"] = self.span_id
130
+ if self.parent_span_id is not None:
131
+ wire["parentSpanID"] = self.parent_span_id
132
+ return wire
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class ProvenanceEdge:
137
+ source_id: uuid.UUID
138
+ target_id: uuid.UUID
139
+ type: str
140
+
141
+ def to_wire(self) -> Dict[str, object]:
142
+ return {
143
+ "sourceID": str(self.source_id).upper(),
144
+ "targetID": str(self.target_id).upper(),
145
+ "type": self.type,
146
+ }
147
+
148
+
149
+ class _RawNumber(float):
150
+ """Marker so the wire encoder renders a float Swift-style ("1" not "1.0")."""
151
+
152
+
153
+ def _wire_default(value: object) -> object:
154
+ raise TypeError(f"not wire-encodable: {type(value).__name__}")
155
+
156
+
157
+ def _encode_wire(value: object, indent: int = 2) -> str:
158
+ """Pretty, sorted-keys JSON with Swift-style number rendering."""
159
+
160
+ def transform(node: object) -> object:
161
+ if isinstance(node, _RawNumber):
162
+ # json.dumps would render 1.0; Swift renders 1. Wrap via int when exact.
163
+ return int(node) if float(node).is_integer() else float(node)
164
+ if isinstance(node, dict):
165
+ return {k: transform(v) for k, v in node.items()}
166
+ if isinstance(node, list):
167
+ return [transform(v) for v in node]
168
+ return node
169
+
170
+ return json.dumps(transform(value), sort_keys=True, indent=indent, separators=(",", " : "), ensure_ascii=False, default=_wire_default)
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class ProvenanceTrace:
175
+ events: List[ProvenanceEventEnvelope]
176
+ edges: List[ProvenanceEdge]
177
+ run_fingerprint: str
178
+ report_fingerprint: str
179
+ policy_fingerprint: str
180
+ engine_name: str
181
+ context_id: str
182
+
183
+ def events_json(self) -> str:
184
+ """Bare JSON array of envelopes — decodes in the Swift DProvenanceKit as
185
+ ``[TraceEvent<AnyTraceableEvent>]`` with a default ``JSONDecoder``."""
186
+ return _encode_wire([event.to_wire() for event in self.events])
187
+
188
+ def manifest_json(self) -> str:
189
+ """Full manifest matching the Swift bridge's ``ProvenanceManifest``."""
190
+ return _encode_wire(
191
+ {
192
+ "bridgeVersion": BRIDGE_VERSION,
193
+ "specVersion": SPEC_VERSION,
194
+ "engineName": self.engine_name,
195
+ "contextID": self.context_id,
196
+ "runFingerprint": self.run_fingerprint,
197
+ "reportFingerprint": self.report_fingerprint,
198
+ "policyFingerprint": self.policy_fingerprint,
199
+ "events": [event.to_wire() for event in self.events],
200
+ "edges": [edge.to_wire() for edge in self.edges],
201
+ }
202
+ )
203
+
204
+
205
+ def run_fingerprint_of(events: Sequence[ProvenanceEventEnvelope]) -> str:
206
+ """Trace Specification v1 §5: SHA-1 over ``type:engine|`` per event in order."""
207
+ digest = hashlib.sha1()
208
+ for event in events:
209
+ digest.update(f"{event.payload.type_identifier}:{event.engine_name}|".encode("utf-8"))
210
+ return digest.hexdigest()
211
+
212
+
213
+ @dataclass(frozen=True)
214
+ class ProvenanceExporter:
215
+ engine_name: str = "ClaimProofKit"
216
+ context_id: str = "claimproof"
217
+ schema_version: int = 1
218
+
219
+ def trace(
220
+ self,
221
+ report: ProofReport,
222
+ policy: VerificationPolicy,
223
+ run_id: Optional[uuid.UUID] = None,
224
+ ) -> ProvenanceTrace:
225
+ """Builds the trace for a report. ``policy`` is deliberately required —
226
+ ``ProofReport`` does not carry the policy it was produced under, so the
227
+ caller must pass the one actually used."""
228
+ rid = run_id if run_id is not None else uuid.uuid4()
229
+ events: List[ProvenanceEventEnvelope] = []
230
+ edges: List[ProvenanceEdge] = []
231
+
232
+ def append(payload_dict: Dict[str, object], type_identifier: str, priority: int) -> uuid.UUID:
233
+ envelope = ProvenanceEventEnvelope(
234
+ id=uuid.uuid4(),
235
+ run_id=rid,
236
+ context_id=self.context_id,
237
+ engine_name=self.engine_name,
238
+ schema_version=self.schema_version,
239
+ sequence=len(events),
240
+ payload=ErasedProvenancePayload(
241
+ type_identifier=type_identifier,
242
+ priority_value=priority,
243
+ raw_json=canonical_json(payload_dict),
244
+ ),
245
+ timestamp=report.created_at,
246
+ )
247
+ events.append(envelope)
248
+ return envelope.id
249
+
250
+ verdict_event_ids: List[uuid.UUID] = []
251
+ for verdict in report.verdicts:
252
+ extracted_id = append(
253
+ _claim_extracted_payload(verdict), CLAIM_EXTRACTED, PRIORITY_STRUCTURAL
254
+ )
255
+ verified_id = append(
256
+ _claim_verified_payload(verdict), CLAIM_VERIFIED, PRIORITY_CRITICAL
257
+ )
258
+ edges.append(
259
+ ProvenanceEdge(source_id=extracted_id, target_id=verified_id, type=EDGE_VERIFIED_BY)
260
+ )
261
+ verdict_event_ids.append(verified_id)
262
+
263
+ report_event_id = append(
264
+ _report_finalized_payload(report, policy), REPORT_FINALIZED, PRIORITY_CRITICAL
265
+ )
266
+ for verdict_event_id in verdict_event_ids:
267
+ edges.append(
268
+ ProvenanceEdge(source_id=verdict_event_id, target_id=report_event_id, type=EDGE_DERIVED_FROM)
269
+ )
270
+
271
+ return ProvenanceTrace(
272
+ events=events,
273
+ edges=edges,
274
+ run_fingerprint=run_fingerprint_of(events),
275
+ report_fingerprint=report_fingerprint(report),
276
+ policy_fingerprint=policy_fingerprint(policy),
277
+ engine_name=self.engine_name,
278
+ context_id=self.context_id,
279
+ )
claimproofkit/py.typed ADDED
File without changes
@@ -0,0 +1,219 @@
1
+ # Copyright 2026 Daniel Kissel
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Human-readable and machine-readable report rendering — mirrors the Swift
5
+ `ReportRenderer.swift` and `BenchmarkReportRenderer.swift`, plus the Swift
6
+ CLI's ``--json`` benchmark wire format (camelCase, sorted keys)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ from typing import Dict, List
13
+
14
+ from .benchmark import BenchmarkReport
15
+ from .models import APPROVED_STATUSES, ProofReport
16
+
17
+
18
+ def _swift_rounded(value: float) -> int:
19
+ """Swift's ``Double.rounded()`` — half away from zero (non-negative inputs)."""
20
+ return int(math.floor(value + 0.5))
21
+
22
+
23
+ def render_report_markdown(report: ProofReport) -> str:
24
+ percent = _swift_rounded(report.support_rate * 100)
25
+ supported = sum(1 for v in report.verdicts if v.status in APPROVED_STATUSES)
26
+ lines: List[str] = [
27
+ "# ClaimProof Report",
28
+ "",
29
+ f"**Publish status:** {'PASS' if report.is_safe_to_publish else 'BLOCKED'}",
30
+ f"**Supported claims:** {percent}% ({supported}/{len(report.verdicts)})",
31
+ "",
32
+ ]
33
+ for verdict in report.verdicts:
34
+ lines.append(f"## [{verdict.status.value.upper()}] {verdict.claim.text}")
35
+ lines.append("")
36
+ lines.append(f"Type: `{verdict.claim.type.value}` · Fingerprint: `{verdict.claim.fingerprint}`")
37
+ lines.append("")
38
+ lines.append(verdict.explanation)
39
+ for match in verdict.evidence:
40
+ coverage = _swift_rounded(match.score * 100)
41
+ lines.append(f"- `{match.passage.id}` {match.passage.source} — {coverage}% term coverage")
42
+ lines.append(f" > {match.passage.text.replace(chr(10), ' ')}")
43
+ lines.append("")
44
+ return "\n".join(lines)
45
+
46
+
47
+ def _percent(value: float) -> str:
48
+ return f"{value * 100:.1f}%"
49
+
50
+
51
+ def render_benchmark_markdown(report: BenchmarkReport) -> str:
52
+ failures = [r for r in report.claim_results if not r.passed]
53
+ dangerous = [r for r in report.claim_results if r.dangerously_approved]
54
+ t = report.thresholds
55
+ lines: List[str] = [
56
+ f"# ClaimProofKit Benchmark {report.corpus_version}",
57
+ "",
58
+ f"**Safety gate:** {'PASS' if report.passes_safety_gate else 'FAIL'}",
59
+ f"**Corpus:** {report.corpus_name}",
60
+ "",
61
+ "| Metric | Result | Required |",
62
+ "| --- | ---: | ---: |",
63
+ f"| Verdict accuracy | {_percent(report.verdict_accuracy)} | ≥ {_percent(t.minimum_verdict_accuracy)} |",
64
+ f"| False-supported rate | {_percent(report.false_supported_rate)} | ≤ {_percent(t.maximum_false_supported_rate)} |",
65
+ f"| Contradiction recall | {_percent(report.contradiction_recall)} | ≥ {_percent(t.minimum_contradiction_recall)} |",
66
+ f"| Extraction precision | {_percent(report.extraction_precision)} | ≥ {_percent(t.minimum_extraction_precision)} |",
67
+ f"| Extraction recall | {_percent(report.extraction_recall)} | ≥ {_percent(t.minimum_extraction_recall)} |",
68
+ "",
69
+ f"Cases: {len(report.claim_results)} verdict · {len(report.extraction_results)} extraction · "
70
+ f"{len(failures)} exact-verdict failures · {len(dangerous)} dangerous approvals",
71
+ "",
72
+ "## Ship criteria",
73
+ "",
74
+ f"**Ship criteria:** {'MET' if report.meets_ship_criteria else 'NOT MET'} — necessary on this development corpus, sufficient only on independently labeled realistic documents.",
75
+ "",
76
+ "| Metric | Result | Required |",
77
+ "| --- | ---: | ---: |",
78
+ f"| Supported-claim precision | {_percent(report.supported_precision)} | ≥ {_percent(t.minimum_supported_precision)} |",
79
+ f"| Unsupported/contradicted recall | {_percent(report.unsupported_recall)} | ≥ {_percent(t.minimum_unsupported_recall)} |",
80
+ "",
81
+ "| Diagnostic (report-only) | Result |",
82
+ "| --- | ---: |",
83
+ f"| Warning precision | {_percent(report.warning_precision)} |",
84
+ f"| Abstention precision | {_percent(report.abstention_precision)} |",
85
+ f"| Abstention recall | {_percent(report.abstention_recall)} |",
86
+ f"| Expected calibration error | {report.calibration.expected_calibration_error:.3f} |",
87
+ "",
88
+ "## Per-category outcomes",
89
+ "",
90
+ "| Category | Cases | Exact | Dangerous approvals |",
91
+ "| --- | ---: | ---: | ---: |",
92
+ ]
93
+ for key in sorted(report.category_breakdown):
94
+ metric = report.category_breakdown[key]
95
+ lines.append(
96
+ f"| {key} | {metric.case_count} | {metric.exact_match_count} ({_percent(metric.exact_match_rate)}) "
97
+ f"| {metric.dangerous_approval_count} |"
98
+ )
99
+ lines.extend(
100
+ [
101
+ "",
102
+ "## Calibration",
103
+ "",
104
+ "| Confidence | Cases | Avg confidence | Exact-match rate |",
105
+ "| --- | ---: | ---: | ---: |",
106
+ ]
107
+ )
108
+ for bin_ in report.calibration.bins:
109
+ if bin_.case_count > 0:
110
+ lines.append(
111
+ f"| {bin_.lower_bound:.1f}–{bin_.upper_bound:.1f} | {bin_.case_count} "
112
+ f"| {bin_.average_confidence:.2f} | {_percent(bin_.exact_match_rate)} |"
113
+ )
114
+ if failures:
115
+ lines.extend(["", "## Verdict mismatches", ""])
116
+ for failure in failures:
117
+ lines.append(
118
+ f"- `{failure.id}` [{failure.category}]: expected `{failure.expected.value}`, "
119
+ f"predicted `{failure.predicted.value}`"
120
+ )
121
+ extraction_failures = [
122
+ r for r in report.extraction_results if r.false_positive_count > 0 or r.false_negative_count > 0
123
+ ]
124
+ if extraction_failures:
125
+ lines.extend(["", "## Extraction mismatches", ""])
126
+ for failure in extraction_failures:
127
+ lines.append(f"- `{failure.id}`: {failure.false_positive_count} extra, {failure.false_negative_count} missed")
128
+ return "\n".join(lines)
129
+
130
+
131
+ def benchmark_report_wire(report: BenchmarkReport) -> Dict[str, object]:
132
+ """The Swift CLI's ``--json`` shape (camelCase keys), for cross-implementation
133
+ comparison of full benchmark runs."""
134
+ t = report.thresholds
135
+ return {
136
+ "corpusVersion": report.corpus_version,
137
+ "corpusName": report.corpus_name,
138
+ "thresholds": {
139
+ "minimumVerdictAccuracy": t.minimum_verdict_accuracy,
140
+ "maximumFalseSupportedRate": t.maximum_false_supported_rate,
141
+ "minimumContradictionRecall": t.minimum_contradiction_recall,
142
+ "minimumExtractionPrecision": t.minimum_extraction_precision,
143
+ "minimumExtractionRecall": t.minimum_extraction_recall,
144
+ "minimumSupportedPrecision": t.minimum_supported_precision,
145
+ "minimumUnsupportedRecall": t.minimum_unsupported_recall,
146
+ },
147
+ "claimResults": [
148
+ {
149
+ "id": r.id,
150
+ "category": r.category,
151
+ "expected": r.expected.value,
152
+ "predicted": r.predicted.value,
153
+ "confidence": r.confidence,
154
+ "passed": r.passed,
155
+ "dangerouslyApproved": r.dangerously_approved,
156
+ }
157
+ for r in report.claim_results
158
+ ],
159
+ "extractionResults": [
160
+ {
161
+ "id": r.id,
162
+ "expected": list(r.expected),
163
+ "predicted": list(r.predicted),
164
+ "truePositiveCount": r.true_positive_count,
165
+ "falsePositiveCount": r.false_positive_count,
166
+ "falseNegativeCount": r.false_negative_count,
167
+ }
168
+ for r in report.extraction_results
169
+ ],
170
+ "verdictAccuracy": report.verdict_accuracy,
171
+ "falseSupportedRate": report.false_supported_rate,
172
+ "contradictionRecall": report.contradiction_recall,
173
+ "extractionPrecision": report.extraction_precision,
174
+ "extractionRecall": report.extraction_recall,
175
+ "supportedPrecision": report.supported_precision,
176
+ "unsupportedRecall": report.unsupported_recall,
177
+ "warningPrecision": report.warning_precision,
178
+ "abstentionPrecision": report.abstention_precision,
179
+ "abstentionRecall": report.abstention_recall,
180
+ "statusMetrics": {
181
+ status: {
182
+ "expectedCount": m.expected_count,
183
+ "predictedCount": m.predicted_count,
184
+ "matchCount": m.match_count,
185
+ "precision": m.precision,
186
+ "recall": m.recall,
187
+ }
188
+ for status, m in report.status_metrics.items()
189
+ },
190
+ "categoryBreakdown": {
191
+ category: {
192
+ "caseCount": m.case_count,
193
+ "exactMatchCount": m.exact_match_count,
194
+ "dangerousApprovalCount": m.dangerous_approval_count,
195
+ "exactMatchRate": m.exact_match_rate,
196
+ }
197
+ for category, m in report.category_breakdown.items()
198
+ },
199
+ "calibration": {
200
+ "bins": [
201
+ {
202
+ "lowerBound": b.lower_bound,
203
+ "upperBound": b.upper_bound,
204
+ "caseCount": b.case_count,
205
+ "averageConfidence": b.average_confidence,
206
+ "exactMatchRate": b.exact_match_rate,
207
+ }
208
+ for b in report.calibration.bins
209
+ ],
210
+ "expectedCalibrationError": report.calibration.expected_calibration_error,
211
+ },
212
+ "confusionMatrix": report.confusion_matrix,
213
+ "passesSafetyGate": report.passes_safety_gate,
214
+ "meetsShipCriteria": report.meets_ship_criteria,
215
+ }
216
+
217
+
218
+ def benchmark_report_json(report: BenchmarkReport) -> str:
219
+ return json.dumps(benchmark_report_wire(report), sort_keys=True, indent=2, ensure_ascii=False)