feat: Intelligence Pipeline v3 — full implementation

Multi-stage evidence-grounded inference architecture replacing the
monolithic 9B model extraction pipeline. CPU-first specialist services
handle routine extraction while the 9B vLLM model is preserved for
semantic adjudication of ambiguous cases.

Key components:
- Capability-aware inference gateway (OpenAI-compatible + Ollama)
- Endpoint registry with DB migrations and REST API
- Sentence-aware document segmenter (property tests)
- Deterministic financial parsing with offset integrity
- Symbol resolution with ambiguity detection
- Specialist service (GLiNER2, dynamic batching, K8s deployment)
- Company-specific sentiment (FinBERT, calibration)
- Retrieval-based novelty and duplicate detection
- Confidence calibration pipeline
- Deterministic routing engine (property tests)
- 9B adjudication layer with VRAM gating
- Stock-specific impact model (features, labels, baseline, trained)
- Pipeline orchestrator (state machine, queues, leases, feature flags)
- Bounded parallelism (async workers, semaphore, load shedding)
- Observability (tracing, metrics, alerts)
- Compatibility adapter (v3→v2 golden mapping tests)
- Shadow/canary promotion framework
- Active learning and fine-tuning pipeline

Test results: 1,161 tests pass, ruff lint clean.
All 282 spec tasks completed.
This commit is contained in:
Celes Renata
2026-07-13 02:14:59 +00:00
parent 84634a365e
commit a72f336ad1
227 changed files with 50403 additions and 0 deletions
@@ -0,0 +1,776 @@
"""Tests for evidence verification, entailment, coverage metrics, rejected store, and metrics.
Covers:
- Valid offset verification
- Invalid offset detection (text mismatch, out of bounds)
- Entity-evidence association
- Numeric consistency (value found / not found in evidence)
- Rejected candidate storage with reason codes
- RejectedCandidateStore (store, get_by_pipeline_run, get_by_reason)
- Entailment baseline (keyword overlap and exact match)
- Coverage metrics computation
- VerificationMetrics aggregation (unsupported-claim and evidence-coverage rates)
- Full verification report
"""
from __future__ import annotations
import pytest
from services.intelligence_pipeline_v3.verification.coverage import (
FieldEvidence,
compute_coverage,
)
from services.intelligence_pipeline_v3.verification.entailment import (
EntailmentVerifier,
)
from services.intelligence_pipeline_v3.verification.metrics import (
VerificationMetrics,
compute_verification_metrics,
)
from services.intelligence_pipeline_v3.verification.models import (
RejectedCandidate,
RejectionReason,
VerificationReport,
)
from services.intelligence_pipeline_v3.verification.rejected_store import (
RejectedCandidateStore,
)
from services.intelligence_pipeline_v3.verification.verifier import (
Candidate,
Entity,
EvidenceSpan,
EvidenceVerifier,
NumericFact,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
SOURCE_TEXT = (
"Apple Inc. reported revenue of $94.8 billion for Q1 2024, "
"beating analyst expectations of $92.0 billion. "
"CEO Tim Cook said the company saw strong growth in services."
)
@pytest.fixture
def source_text() -> str:
return SOURCE_TEXT
@pytest.fixture
def valid_spans(source_text: str) -> list[EvidenceSpan]:
"""Spans that exactly match the source text at declared offsets."""
return [
EvidenceSpan(
id="span-1",
start_char=0,
end_char=10,
text=source_text[0:10], # "Apple Inc."
),
EvidenceSpan(
id="span-2",
start_char=11,
end_char=58,
text=source_text[11:58],
),
EvidenceSpan(
id="span-3",
start_char=60,
end_char=107,
text=source_text[60:107],
),
]
@pytest.fixture
def verifier() -> EvidenceVerifier:
return EvidenceVerifier()
# ---------------------------------------------------------------------------
# Test: Valid offset verification
# ---------------------------------------------------------------------------
class TestOffsetVerification:
def test_valid_offsets_pass(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan], source_text: str
):
results = verifier.verify_offsets(valid_spans, source_text)
assert len(results) == 3
assert all(r.valid for r in results)
assert all(r.reason is None for r in results)
def test_text_mismatch_detected(self, verifier: EvidenceVerifier, source_text: str):
"""Span with text that doesn't match source at the declared offset."""
bad_span = EvidenceSpan(
id="span-bad",
start_char=0,
end_char=10,
text="Google LLC", # Wrong — source has "Apple Inc."
)
results = verifier.verify_offsets([bad_span], source_text)
assert len(results) == 1
assert not results[0].valid
assert "Text mismatch" in results[0].reason
def test_offset_out_of_bounds(self, verifier: EvidenceVerifier, source_text: str):
"""Span with end_char beyond source text length."""
bad_span = EvidenceSpan(
id="span-oob",
start_char=0,
end_char=len(source_text) + 100,
text="doesn't matter",
)
results = verifier.verify_offsets([bad_span], source_text)
assert len(results) == 1
assert not results[0].valid
assert "out of bounds" in results[0].reason.lower()
def test_invalid_range_end_before_start(self, verifier: EvidenceVerifier, source_text: str):
"""Span where end_char <= start_char."""
bad_span = EvidenceSpan(
id="span-reversed",
start_char=10,
end_char=5,
text="x",
)
results = verifier.verify_offsets([bad_span], source_text)
assert len(results) == 1
assert not results[0].valid
assert "Invalid range" in results[0].reason
# ---------------------------------------------------------------------------
# Test: Entity-evidence association
# ---------------------------------------------------------------------------
class TestEntityAssociation:
def test_entity_found_in_evidence(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
entity = Entity(
id="ent-1", literal_text="Apple Inc.", evidence_ids=["span-1"]
)
result = verifier.verify_entity_association(entity, valid_spans)
assert result.valid
assert result.reason is None
def test_entity_case_insensitive(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Entity matching should be case-insensitive."""
entity = Entity(
id="ent-2", literal_text="apple inc.", evidence_ids=["span-1"]
)
result = verifier.verify_entity_association(entity, valid_spans)
assert result.valid
def test_entity_not_in_evidence(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Entity text is not present in any linked span."""
entity = Entity(
id="ent-3", literal_text="Microsoft", evidence_ids=["span-1", "span-2"]
)
result = verifier.verify_entity_association(entity, valid_spans)
assert not result.valid
assert "not found" in result.reason.lower()
def test_entity_with_nonexistent_span_id(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Entity references a span ID that doesn't exist."""
entity = Entity(
id="ent-4", literal_text="Apple", evidence_ids=["span-nonexistent"]
)
result = verifier.verify_entity_association(entity, valid_spans)
assert not result.valid
# ---------------------------------------------------------------------------
# Test: Numeric consistency
# ---------------------------------------------------------------------------
class TestNumericConsistency:
def test_literal_value_found(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Literal value string is found directly in evidence text."""
fact = NumericFact(
id="fact-1",
literal_value="$94.8 billion",
normalized_value=94.8,
evidence_ids=["span-2"],
)
result = verifier.verify_numeric_consistency(fact, valid_spans)
assert result.valid
assert result.found_value == "$94.8 billion"
def test_normalized_value_match(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Normalized value matches a number in evidence (without exact literal)."""
fact = NumericFact(
id="fact-2",
literal_value="92 billion", # Not exact match
normalized_value=92.0,
evidence_ids=["span-3"],
)
result = verifier.verify_numeric_consistency(fact, valid_spans)
assert result.valid
assert result.found_value == "92.0"
def test_value_not_found(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
"""Value doesn't appear in any linked evidence."""
fact = NumericFact(
id="fact-3",
literal_value="$200 billion",
normalized_value=200.0,
evidence_ids=["span-2", "span-3"],
)
result = verifier.verify_numeric_consistency(fact, valid_spans)
assert not result.valid
assert result.found_value is None
assert "not found" in result.reason.lower()
def test_tolerance_matching(self, valid_spans: list[EvidenceSpan]):
"""Values within tolerance should match."""
verifier = EvidenceVerifier(numeric_tolerance=0.02) # 2% tolerance
fact = NumericFact(
id="fact-4",
literal_value="93.8",
normalized_value=93.8, # Within 2% of 94.8
evidence_ids=["span-2"],
)
result = verifier.verify_numeric_consistency(fact, valid_spans)
assert result.valid
# ---------------------------------------------------------------------------
# Test: Rejected candidate storage (in verifier)
# ---------------------------------------------------------------------------
class TestRejectedCandidates:
def test_offset_rejection_stored(self, verifier: EvidenceVerifier, source_text: str):
bad_span = EvidenceSpan(
id="span-bad",
start_char=0,
end_char=10,
text="WRONG TEXT",
)
verifier.verify_offsets([bad_span], source_text)
rejected = verifier.rejected_candidates
assert len(rejected) == 1
assert rejected[0].rejection_reason == RejectionReason.TEXT_MISMATCH
assert rejected[0].candidate_type == "evidence_span"
assert rejected[0].stage == "offset_verification"
def test_entity_rejection_stored(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
entity = Entity(
id="ent-bad", literal_text="Nonexistent Corp", evidence_ids=["span-1"]
)
verifier.verify_entity_association(entity, valid_spans)
rejected = verifier.rejected_candidates
assert len(rejected) == 1
assert rejected[0].rejection_reason == RejectionReason.ENTITY_NOT_IN_EVIDENCE
assert rejected[0].candidate_type == "entity"
assert rejected[0].candidate_data["entity_id"] == "ent-bad"
def test_numeric_rejection_stored(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
fact = NumericFact(
id="fact-bad",
literal_value="$999",
normalized_value=999.0,
evidence_ids=["span-2"],
)
verifier.verify_numeric_consistency(fact, valid_spans)
rejected = verifier.rejected_candidates
assert len(rejected) == 1
assert rejected[0].rejection_reason == RejectionReason.NUMERIC_INCONSISTENCY
assert rejected[0].candidate_type == "fact"
def test_reset_clears_rejected(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
entity = Entity(
id="ent-x", literal_text="Nothing", evidence_ids=["span-1"]
)
verifier.verify_entity_association(entity, valid_spans)
assert len(verifier.rejected_candidates) == 1
verifier.reset()
assert len(verifier.rejected_candidates) == 0
def test_rejection_has_timestamp(
self, verifier: EvidenceVerifier, valid_spans: list[EvidenceSpan]
):
entity = Entity(
id="ent-ts", literal_text="Nobody", evidence_ids=["span-1"]
)
verifier.verify_entity_association(entity, valid_spans)
rejected = verifier.rejected_candidates
assert rejected[0].timestamp is not None
# ---------------------------------------------------------------------------
# Test: RejectedCandidateStore
# ---------------------------------------------------------------------------
class TestRejectedCandidateStore:
def test_store_and_retrieve_by_run(self):
store = RejectedCandidateStore()
rc = RejectedCandidate(
candidate_type="entity",
candidate_data={"entity_id": "e1", "text": "Apple"},
rejection_reason=RejectionReason.ENTITY_NOT_IN_EVIDENCE,
stage="entity_verification",
)
store.store(rc, run_id="run-001")
results = store.get_by_pipeline_run("run-001")
assert len(results) == 1
assert results[0].candidate_data["entity_id"] == "e1"
def test_retrieve_empty_run(self):
store = RejectedCandidateStore()
results = store.get_by_pipeline_run("nonexistent-run")
assert results == []
def test_store_and_retrieve_by_reason(self):
store = RejectedCandidateStore()
rc1 = RejectedCandidate(
candidate_type="entity",
candidate_data={"id": "e1"},
rejection_reason=RejectionReason.ENTITY_NOT_IN_EVIDENCE,
stage="entity_verification",
)
rc2 = RejectedCandidate(
candidate_type="fact",
candidate_data={"id": "f1"},
rejection_reason=RejectionReason.NUMERIC_INCONSISTENCY,
stage="numeric_verification",
)
rc3 = RejectedCandidate(
candidate_type="entity",
candidate_data={"id": "e2"},
rejection_reason=RejectionReason.ENTITY_NOT_IN_EVIDENCE,
stage="entity_verification",
)
store.store(rc1, run_id="run-1")
store.store(rc2, run_id="run-1")
store.store(rc3, run_id="run-2")
by_entity = store.get_by_reason(RejectionReason.ENTITY_NOT_IN_EVIDENCE)
assert len(by_entity) == 2
by_numeric = store.get_by_reason(RejectionReason.NUMERIC_INCONSISTENCY)
assert len(by_numeric) == 1
def test_store_batch(self):
store = RejectedCandidateStore()
batch = [
RejectedCandidate(
candidate_type="entity",
candidate_data={"id": f"e{i}"},
rejection_reason=RejectionReason.INVALID_OFFSET,
stage="offset_verification",
)
for i in range(5)
]
store.store_batch(batch, run_id="run-batch")
assert store.count() == 5
assert len(store.get_by_pipeline_run("run-batch")) == 5
def test_count_by_reason(self):
store = RejectedCandidateStore()
store.store(RejectedCandidate(
candidate_type="span",
candidate_data={},
rejection_reason=RejectionReason.INVALID_OFFSET,
stage="offset",
))
store.store(RejectedCandidate(
candidate_type="span",
candidate_data={},
rejection_reason=RejectionReason.INVALID_OFFSET,
stage="offset",
))
store.store(RejectedCandidate(
candidate_type="fact",
candidate_data={},
rejection_reason=RejectionReason.NUMERIC_INCONSISTENCY,
stage="numeric",
))
counts = store.count_by_reason()
assert counts["invalid_offset"] == 2
assert counts["numeric_inconsistency"] == 1
def test_clear(self):
store = RejectedCandidateStore()
store.store(RejectedCandidate(
candidate_type="entity",
candidate_data={},
rejection_reason=RejectionReason.ENTITY_NOT_IN_EVIDENCE,
stage="test",
), run_id="run-1")
assert store.count() == 1
store.clear()
assert store.count() == 0
assert store.get_by_pipeline_run("run-1") == []
assert store.get_by_reason(RejectionReason.ENTITY_NOT_IN_EVIDENCE) == []
# ---------------------------------------------------------------------------
# Test: Entailment baseline (keyword overlap)
# ---------------------------------------------------------------------------
class TestEntailment:
def test_exact_match_entailment(self):
ev = EntailmentVerifier()
result = ev.verify_claim(
claim="reported revenue of $94.8 billion",
evidence="Apple Inc. reported revenue of $94.8 billion for Q1 2024",
)
assert result.entailed
assert result.confidence == 1.0
assert result.method == "exact_match"
def test_keyword_overlap_entailed(self):
ev = EntailmentVerifier(keyword_threshold=0.5)
result = ev.verify_claim(
claim="Apple revenue grew significantly",
evidence="Apple Inc. reported record revenue growth of 15% year-over-year",
)
assert result.entailed
assert result.method == "keyword_overlap"
assert result.confidence >= 0.5
def test_keyword_overlap_not_entailed(self):
ev = EntailmentVerifier(keyword_threshold=0.6)
result = ev.verify_claim(
claim="Microsoft acquired a gaming company",
evidence="Apple Inc. reported revenue of $94.8 billion for Q1 2024",
)
assert not result.entailed
assert result.method == "keyword_overlap"
assert result.confidence < 0.6
def test_empty_claim(self):
ev = EntailmentVerifier()
result = ev.verify_claim(claim="", evidence="Some evidence text")
assert not result.entailed
assert result.confidence == 0.0
def test_empty_evidence(self):
ev = EntailmentVerifier()
result = ev.verify_claim(claim="Some claim", evidence="")
assert not result.entailed
assert result.confidence == 0.0
def test_batch_verification(self):
ev = EntailmentVerifier()
claims = [
"reported revenue",
"completely unrelated topic about cats",
]
evidence = "Apple reported revenue of $94.8 billion"
results = ev.verify_claims_batch(claims, evidence)
assert len(results) == 2
assert results[0].entailed # "reported revenue" is in evidence
assert not results[1].entailed # cats not related
def test_model_version_present(self):
"""EntailmentResult includes model_version field."""
ev = EntailmentVerifier()
result = ev.verify_claim(
claim="revenue growth",
evidence="The company reported strong revenue growth this quarter.",
)
assert result.model_version == "keyword_overlap_v1"
# ---------------------------------------------------------------------------
# Test: Coverage metrics
# ---------------------------------------------------------------------------
class TestCoverageMetrics:
def test_full_coverage(self):
fields = [
FieldEvidence(field_id="f1", field_name="revenue", evidence_ids=["s1", "s2"]),
FieldEvidence(field_id="f2", field_name="eps", evidence_ids=["s2"]),
]
verified = {"s1", "s2", "s3"}
metrics = compute_coverage(fields, verified)
assert metrics.total_fields == 2
assert metrics.supported_fields == 2
assert metrics.coverage_rate == 1.0
assert metrics.unsupported_claims == []
assert metrics.unsupported_rate == 0.0
def test_partial_coverage(self):
fields = [
FieldEvidence(field_id="f1", field_name="revenue", evidence_ids=["s1"]),
FieldEvidence(field_id="f2", field_name="eps", evidence_ids=["s4"]),
FieldEvidence(field_id="f3", field_name="guidance", evidence_ids=["s2"]),
]
verified = {"s1", "s2", "s3"}
metrics = compute_coverage(fields, verified)
assert metrics.total_fields == 3
assert metrics.supported_fields == 2
assert metrics.coverage_rate == pytest.approx(2 / 3)
assert metrics.unsupported_claims == ["f2"]
assert metrics.unsupported_rate == pytest.approx(1 / 3)
def test_no_coverage(self):
fields = [
FieldEvidence(field_id="f1", field_name="revenue", evidence_ids=["s99"]),
FieldEvidence(field_id="f2", field_name="eps", evidence_ids=["s100"]),
]
verified = {"s1", "s2"}
metrics = compute_coverage(fields, verified)
assert metrics.total_fields == 2
assert metrics.supported_fields == 0
assert metrics.coverage_rate == 0.0
assert len(metrics.unsupported_claims) == 2
assert metrics.unsupported_rate == 1.0
def test_empty_fields(self):
"""No fields to verify means perfect coverage by definition."""
metrics = compute_coverage([], {"s1", "s2"})
assert metrics.total_fields == 0
assert metrics.coverage_rate == 1.0
assert metrics.unsupported_rate == 0.0
def test_field_with_no_evidence_ids(self):
"""Field with empty evidence_ids is unsupported."""
fields = [
FieldEvidence(field_id="f1", field_name="revenue", evidence_ids=[]),
]
verified = {"s1", "s2"}
metrics = compute_coverage(fields, verified)
assert metrics.supported_fields == 0
assert metrics.unsupported_claims == ["f1"]
# ---------------------------------------------------------------------------
# Test: VerificationMetrics (unsupported-claim and evidence-coverage rates)
# ---------------------------------------------------------------------------
class TestVerificationMetrics:
def test_single_report_all_pass(self):
reports = [
VerificationReport(
total_candidates=10,
verified=10,
rejected=0,
coverage_rate=1.0,
rejection_breakdown={},
)
]
metrics = compute_verification_metrics(reports)
assert metrics.total_checked == 10
assert metrics.passed_count == 10
assert metrics.failed_count == 0
assert metrics.evidence_coverage_rate == 1.0
assert metrics.unsupported_claim_rate == 0.0
assert metrics.per_reason_counts == {}
def test_single_report_some_failures(self):
reports = [
VerificationReport(
total_candidates=10,
verified=7,
rejected=3,
coverage_rate=0.7,
rejection_breakdown={
"entity_not_in_evidence": 2,
"unsupported_claim": 1,
},
)
]
metrics = compute_verification_metrics(reports)
assert metrics.total_checked == 10
assert metrics.passed_count == 7
assert metrics.failed_count == 3
assert metrics.evidence_coverage_rate == 0.7
assert metrics.unsupported_claim_rate == pytest.approx(0.1)
assert metrics.per_reason_counts["entity_not_in_evidence"] == 2
assert metrics.per_reason_counts["unsupported_claim"] == 1
def test_multiple_reports_aggregated(self):
reports = [
VerificationReport(
total_candidates=5,
verified=4,
rejected=1,
coverage_rate=0.8,
rejection_breakdown={"invalid_offset": 1},
),
VerificationReport(
total_candidates=10,
verified=8,
rejected=2,
coverage_rate=0.8,
rejection_breakdown={
"numeric_inconsistency": 1,
"unsupported_claim": 1,
},
),
]
metrics = compute_verification_metrics(reports)
assert metrics.total_checked == 15
assert metrics.passed_count == 12
assert metrics.failed_count == 3
assert metrics.evidence_coverage_rate == pytest.approx(12 / 15)
assert metrics.unsupported_claim_rate == pytest.approx(1 / 15)
assert metrics.per_reason_counts["invalid_offset"] == 1
assert metrics.per_reason_counts["numeric_inconsistency"] == 1
assert metrics.per_reason_counts["unsupported_claim"] == 1
def test_empty_reports(self):
metrics = compute_verification_metrics([])
assert metrics.total_checked == 0
assert metrics.passed_count == 0
assert metrics.failed_count == 0
assert metrics.evidence_coverage_rate == 1.0
assert metrics.unsupported_claim_rate == 0.0
def test_metrics_is_frozen_dataclass(self):
"""VerificationMetrics should be immutable."""
metrics = compute_verification_metrics([])
assert isinstance(metrics, VerificationMetrics)
# ---------------------------------------------------------------------------
# Test: Full verification report
# ---------------------------------------------------------------------------
class TestFullVerificationReport:
def test_all_candidates_verified(self, source_text: str):
verifier = EvidenceVerifier()
spans = [
EvidenceSpan(
id="s1",
start_char=0,
end_char=10,
text=source_text[0:10],
),
]
candidates = [
Candidate(
candidate_type="entity",
candidate_id="c1",
candidate_data={"name": "Apple Inc."},
evidence_ids=["s1"],
literal_text="Apple Inc.",
),
]
report = verifier.verify_all(candidates, spans, source_text)
assert report.total_candidates == 1
assert report.verified == 1
assert report.rejected == 0
assert report.coverage_rate == 1.0
def test_mixed_verification(self, source_text: str):
verifier = EvidenceVerifier()
spans = [
EvidenceSpan(
id="s1",
start_char=0,
end_char=10,
text=source_text[0:10],
),
EvidenceSpan(
id="s2",
start_char=11,
end_char=58,
text=source_text[11:58],
),
]
candidates = [
Candidate(
candidate_type="entity",
candidate_id="c1",
candidate_data={"name": "Apple"},
evidence_ids=["s1"],
literal_text="Apple Inc.",
),
Candidate(
candidate_type="entity",
candidate_id="c2",
candidate_data={"name": "Microsoft"},
evidence_ids=["s1", "s2"],
literal_text="Microsoft",
),
]
report = verifier.verify_all(candidates, spans, source_text)
assert report.total_candidates == 2
assert report.verified == 1
assert report.rejected == 1
assert report.coverage_rate == 0.5
assert RejectionReason.ENTITY_NOT_IN_EVIDENCE.value in report.rejection_breakdown
def test_invalid_span_cascades_to_candidate(self, source_text: str):
"""If a candidate's only span is invalid, the candidate is rejected."""
verifier = EvidenceVerifier()
bad_span = EvidenceSpan(
id="s-bad",
start_char=0,
end_char=10,
text="WRONG TEXT", # Doesn't match source
)
candidates = [
Candidate(
candidate_type="entity",
candidate_id="c1",
candidate_data={"name": "test"},
evidence_ids=["s-bad"],
literal_text="Apple",
),
]
report = verifier.verify_all(candidates, [bad_span], source_text)
assert report.rejected == 1
assert report.verified == 0
def test_numeric_candidate_in_full_report(self, source_text: str):
verifier = EvidenceVerifier()
spans = [
EvidenceSpan(
id="s1",
start_char=11,
end_char=58,
text=source_text[11:58],
),
]
candidates = [
Candidate(
candidate_type="fact",
candidate_id="c1",
candidate_data={"type": "revenue"},
evidence_ids=["s1"],
literal_text="$94.8 billion",
normalized_value=94.8,
),
]
report = verifier.verify_all(candidates, spans, source_text)
assert report.verified == 1
assert report.rejected == 0