Appearance
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:
| Layer | Path | Source of facts |
|---|---|---|
| Python worker | locus-workers/contradiction_rag/detector.py (CrossDocumentContradictionDetector) | typed ExtractedClauseFact list (CLI pipeline / regression pack) |
| SoR (authoritative for tenants) | DossierAnalyticsService.ScanAndPersistContradictionsAsync | regex hits on persisted OCR pages |
| UI | ContradictionRadar.tsx (radar), PDF export POST /api/exports/contradictions | contradiction_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 boxClauseCitation 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)
| Topic | Rule | Output |
|---|---|---|
| Numeric (interest margin) | (\text{Spread Delta} = \lvert V_A - V_B \rvert \ge 0.05%) | Conflicting Interest Rate / Margin, CRITICAL |
| Date / tenor | normalized ISO dates differ; take-out bond maturity exceeds concession expiry | Maturity 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 radar | variance above 10 bps or 50 M FCFA | supports confirm / escalate / dismiss (mandatory rationale) |
Thresholds as implemented
| Check | Worker (detector.py) | SoR (DossierAnalyticsService) |
|---|---|---|
| Rate / margin | flag 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 = |
| Maturity | any differing normalized value across documents → CRITICAL | years 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 → CRITICAL | not scanned by SoR regex pass |
| Negative pledge | differing 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 type | Source |
|---|---|
| Interest margin | InterestMargin regex over each OCR page → (docId, filename, page, rate, snippet) |
| Maturity | MaturityDate 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}).
| Field | Meaning |
|---|---|
Category | MARGIN_LEAKAGE | TENOR_MISMATCH |
Severity | CRITICAL | WARNING | INFO |
Status | OPEN → ACKNOWLEDGED → RESOLVED / WAIVED (waiver/resolution needs rationale) |
DocAId, DocBId | documents compared |
CitationAId, CitationBId | page-level citations |
Summary | French 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)
| Document | Claim | Contradiction |
|---|---|---|
02_convention_pret_relais_53m_eur.pdf | EURIBOR 3M + 7.20 % (Art. 5.2); take-out bond 30 Nov 2031 (Art. 7.1) | — |
03_term_sheet_syndication.pdf | EURIBOR 3M + 6.75 %; restated EBITDA 42.10 B FCFA | 45 bps lower margin; +7.25 B FCFA add-back over statutory 34.85 B |
04_convention_concession_etat_gabonais.pdf | concession 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.