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/__init__.py +95 -0
- claimproofkit/_text.py +94 -0
- claimproofkit/benchmark.py +407 -0
- claimproofkit/cli.py +136 -0
- claimproofkit/extractor.py +113 -0
- claimproofkit/identity.py +134 -0
- claimproofkit/models.py +133 -0
- claimproofkit/pipeline.py +145 -0
- claimproofkit/provenance.py +279 -0
- claimproofkit/py.typed +0 -0
- claimproofkit/report.py +219 -0
- claimproofkit/verifier.py +239 -0
- claimproofkit-0.1.0.dist-info/METADATA +95 -0
- claimproofkit-0.1.0.dist-info/RECORD +19 -0
- claimproofkit-0.1.0.dist-info/WHEEL +5 -0
- claimproofkit-0.1.0.dist-info/entry_points.txt +2 -0
- claimproofkit-0.1.0.dist-info/licenses/LICENSE +203 -0
- claimproofkit-0.1.0.dist-info/licenses/NOTICE +6 -0
- claimproofkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
claimproofkit/report.py
ADDED
|
@@ -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)
|