Skip to content

Contradictions ​

Cross-document verification: ingest discrete factual claims from distinct legal and financial documents, align them by topic and flag inconsistencies before credit approval. All checks are deterministic (no LLM).

Two implementations coexist:

LayerPathSource of facts
Python workerlocus-workers/contradiction_rag/detector.py (CrossDocumentContradictionDetector)typed ExtractedClauseFact list (CLI pipeline / regression pack)
SoR (authoritative for tenants)DossierAnalyticsService.ScanAndPersistContradictionsAsyncregex hits on persisted OCR pages
UIContradictionRadar.tsx (radar), PDF export POST /api/exports/contradictionscontradiction_alerts hydrated from SoR

Fact model (ExtractedClauseFact) ​

Each verified claim (technical documentation §4.3):

python
@dataclass
class ExtractedClauseFact:
    fact_id: str
    topic: str              # 'INTEREST_RATE', 'MATURITY_DATE', 'EBITDA_DEFINITION', etc.
    normalized_value: Any   # 6.75, 7.20, '2028-12-31', 34850000000
    display_value: str      # 'EURIBOR 3M + 6.75%'
    citation: ClauseCitation# Document name, page number, bounding box

ClauseCitation carries citation_id, document_id, document_name, page_number, bounding_box (x0, top, x1, bottom), extracted_text, section_ref. Worker topics seen in code: INTEREST_RATE, MATURITY_DATE, FACILITY_AMOUNT, COVENANT_LEVERAGE, NEGATIVE_PLEDGE, EBITDA_DEFINITION.

The detector groups facts by topic and only compares facts from different documents (citation.document_id).

Thresholds ​

Documented target rules (technical documentation §4.3 / §10.10) ​

TopicRuleOutput
Numeric (interest margin)(\text{Spread Delta} = \lvert V_A - V_B \rvert \ge 0.05%)Conflicting Interest Rate / Margin, CRITICAL
Date / tenornormalized ISO dates differ; take-out bond maturity exceeds concession expiryMaturity Date Inconsistency / Tenor Mismatch
Accounting definition (EBITDA add-backs)(\text{EBITDA Delta} = \lvert V_{\text{Deck}} - V_{\text{Statutory}} \rvert > 5%)Material EBITDA Definition & Quantification Discrepancy
Materiality for the radarvariance above 10 bps or 50 M FCFAsupports confirm / escalate / dismiss (mandatory rationale)

Thresholds as implemented ​

CheckWorker (detector.py)SoR (DossierAnalyticsService)
Rate / marginflag if (\lvert r_1 - r_2 \rvert > 0.05) (percentage points = 5 bps); severity CRITICAL if ≥ 0.25 else MAJOR; variance_metric = "{bps} bps discrepancy"`bps =
Maturityany differing normalized value across documents → CRITICALyears extracted from date tokens; skip equal years; gap ≥ 2 years → CRITICAL, else WARNING; category TENOR_MISMATCH
EBITDA definition(\lvert\Delta\rvert > 100,000,000) FCFA → CRITICALnot scanned by SoR regex pass
Negative pledgediffering normalized values → CRITICAL ("Prior Lien Disclosed")—

Severity mapping to the alert object (BuildContradictionAlertObject): CRITICAL → CRITICAL, WARNING → MAJOR; topic mapping MARGIN_LEAKAGE → INTEREST_RATE, TENOR_MISMATCH → MATURITY_DATE, otherwise COVENANT. Finding severity in AnalysisResult: CRITICAL stays, WARNING → HIGH.

SoR regex extraction ​

Hit typeSource
Interest marginInterestMargin regex over each OCR page → (docId, filename, page, rate, snippet)
MaturityMaturityDate regex: `maturité

Pairs are de-duplicated with keys MARGIN:{docA|docB}:{rateA}:{rateB} and TENOR:{docA|docB}:{yearA}:{yearB}. Each scan replaces all ContradictionRecord rows of the dossier.

Persist ​

DossierAnalyticsService.ScanAndPersistContradictionsAsync writes ContradictionRecord rows (margin leakage, tenor mismatch, etc.) with CitationAId / CitationBId pointing at page-level cites when regex hits exist (cite_{docId}_p{page}).

FieldMeaning
CategoryMARGIN_LEAKAGE | TENOR_MISMATCH
SeverityCRITICAL | WARNING | INFO
StatusOPEN → ACKNOWLEDGED → RESOLVED / WAIVED (waiver/resolution needs rationale)
DocAId, DocBIddocuments compared
CitationAId, CitationBIdpage-level citations
SummaryFrench sentence with filenames, values and delta ({bps} bps or {gap} an(s))

Side effects of a run: alerts of category CONTRADICTION are emitted for CRITICAL/HIGH findings (EmitAlertsAsync), and the corresponding AnalysisFinding (type: CONTRADICTION, varianceMetric: "{bps} bps" or "{gap} ans") lands in the AnalysisResult.

Alert shape ​

BuildContradictionAlertObject loads Citation + Document:

  • Real page / filename
  • Bbox mapped to UI shape or null (never {0,0,0,0})
json
{
  "id": "contra_…", "topic": "INTEREST_RATE", "severity": "CRITICAL",
  "doc_a": { "document_name": "…", "page": "<from Citation row>", "citation": { … } },
  "doc_b": { "document_name": "…", "page": "<from Citation row>", "citation": { … } }
}

Worker alert (ContradictionAlert.to_dict) uses alert_id, title, topic, severity (CRITICAL|MAJOR|MINOR), variance_metric, explanation, and doc_a / doc_b blocks with document_name, page, value, text, citation.

Reference case (SEEG, test fixture) ​

DocumentClaimContradiction
02_convention_pret_relais_53m_eur.pdfEURIBOR 3M + 7.20 % (Art. 5.2); take-out bond 30 Nov 2031 (Art. 7.1)—
03_term_sheet_syndication.pdfEURIBOR 3M + 6.75 %; restated EBITDA 42.10 B FCFA45 bps lower margin; +7.25 B FCFA add-back over statutory 34.85 B
04_convention_concession_etat_gabonais.pdfconcession expires 31 Dec 2028 (Art. 4.2)3-year tenor mismatch vs 2031 bond ("35-month trap" in copilot)

The regression pack (reference_regression_pack.test.ts) locks the 45 bps margin discrepancy and the 2028 vs 2031 trap; data-room completeness flags missing DGI tax quitus and BEAC Art. 42 declaration as fatal blockers.

Resolution workflow & exports ​

Radar supports confirmation, escalation, dismissal with mandatory rationale; dismissed contradictions are excluded from the 10-chapter memo drafter. Memo statements are tagged [FAIT], [DÉDUCTION], [QUESTION] — contradictions surface as [QUESTION]. Copilot tools: audit_contradictions_radar, explain_concession_maturity_trap.

UI ​

Contradiction radar: Voir sur le PDF → workbench PDF viewer (InteractivePdfViewer). Highlight is skipped when the bbox is unusable — see Citations (R4).

Tests ​

tests/test_contradiction_rag.py (test_rate_contradiction, test_maturity_contradiction, test_ebitda_definition_contradiction), locus-web/tests/sor_dossier_analytics_unit.test.ts, credit_lab_and_dataroom.test.ts.

Source of trust for Locus. If this conflicts with code, code + IMPLEMENTATION_TRACKER.md win.