Skip to content

Dossier → OCR → analysis ​

End-to-end flow from "Nouveau dossier" to a polled AnalysisResult. Every arrow below is an actual HTTP boundary in the code; "SoR" = locus-backend.

text
Nouveau dossier → POST /api/locus/deals
Deal Room upload → SHA-256 → R2 Put → POST /api/documents
  → extractPages (native / harvest / Tesseract)
  → POST /api/documents/{id}/ocr-result → ExtractedPages + Citations
  → PersistFinancialExtract → FinancialStatements / ExtractedCells / SourceCitationId
Enqueue analysis → AnalysisOrchestrator → Python worker
  → Contradictions, Blockers, Alerts, RatioSnapshots
Deal GET → financial_periods + contradiction_alerts from SoR

1. Full sequence ​

mermaid
sequenceDiagram
    autonumber
    actor A as Analyst
    participant UI as DealRoomExplorer (browser)
    participant BFF as locus-web BFF
    participant R2 as Cloudflare R2
    participant SoR as locus-backend
    participant W as locus-workers :8000
    participant Q as Job store (Redis)

    A->>UI: Nouveau dossier
    UI->>BFF: POST /api/locus/deals (CreateDealSchema)
    BFF->>SoR: POST /api/deals (Bearer session, dossier:create)
    SoR-->>BFF: 201 deal_* (status ORIGINATION)

    A->>UI: Upload PDF in Data Room
    UI->>BFF: POST /api/locus/documents/upload (multipart, ≤ 40 MB, dealroom:upload)
    BFF->>BFF: sha256Plaintext(bytes); AES-256-GCM pack if keyed
    BFF->>R2: PutObject tenants/{tenantId}/dossiers/{dossier}/{doc}/…
    BFF->>SoR: POST /api/documents {id, dossierId, s3Key, sha256Hash, …}
    alt same Sha256Hash on same dossier
        SoR-->>BFF: 409 DUPLICATE_DOCUMENT
        BFF-->>UI: 409
    else new
        SoR-->>BFF: 201 (OcrStatus=PENDING, audit DOCUMENT_UPLOAD)
    end
    BFF->>BFF: extractPagesWithEngine (native → harvest)
    opt harvest insufficient
        BFF->>W: POST /api/v1/ocr {filename, contentBase64}
        W-->>BFF: pages[] + words[] (x0,y0,x1,y1,confidence)
    end
    BFF->>SoR: POST /api/documents/{id}/ocr-result {pages[]}
    SoR->>SoR: replace ExtractedPages, Citations cite_{doc}_p{n}, SearchDocuments
    SoR->>W: POST /api/v1/extract-syscohada {text, periodLabel}
    SoR->>SoR: FinancialStatement + ExtractedCells + RatioSnapshot + Blockers
    SoR-->>BFF: {ocrStatus: COMPLETED, pageCount}
    BFF-->>UI: meta + downloadUrl /api/locus/documents/{id}/content

    A->>UI: Lancer l'analyse
    UI->>BFF: POST /api/locus/deals/{id}/analysis (dossier:write)
    BFF->>SoR: POST /api/deals/{id}/analysis
    SoR->>Q: save AnalysisJob (Queued, 5%)
    SoR-->>BFF: 202 + Location /api/analysisjobs/{jobId}
    BFF-->>UI: {jobId, status, pollUrl}
    loop poll
        UI->>BFF: GET /api/locus/analysisjobs/{jobId} (dossier:read)
        BFF->>SoR: GET /api/analysisjobs/{jobId}
        SoR-->>UI: {status, progressPercent, currentPhase, durationSeconds, result?}
    end
    SoR->>W: POST /api/v1/analyze (best-effort ping)
    SoR->>SoR: RunFullDossierAnalysisAsync → Postgres
    UI->>BFF: GET /api/locus/deals/{id}
    BFF->>SoR: GET /api/deals/{id}
    SoR-->>UI: financial_periods, contradiction_alerts

2. Endpoint ledger ​

StepLayerEndpointPermissionResult / error codes
Create dossierBFF → SoRPOST /api/locus/deals → POST /api/dealsdossier:create201; 400 VALIDATION_ERROR
UploadBFFPOST /api/locus/documents/uploaddealroom:upload201 meta; 503 R2_NOT_CONFIGURED / DOCUMENT_ENCRYPTION_REQUIRED; 400 > 40 MB; 409 duplicate
Register documentSoRPOST /api/documentsdealroom:upload400 INVALID_SHA256, 400 INVALID_S3_KEY, 409 DUPLICATE_DOCUMENT
OCR escalationWorkerPOST /api/v1/ocrinternal413 OCR_TOO_LARGE, 503 OCR_ENGINE_UNAVAILABLE
Persist OCRSoRPOST /api/documents/{id}/ocr-resultdealroom:upload400 OCR_UNREADABLE (OcrStatus=FAILED), else COMPLETED
Extract SYSCOHADAWorkerPOST /api/v1/extract-syscohadainternalfalls back to in-process ExtractCellsFromText when < 3 values or worker offline
Verify citationBFF → SoRPOST /api/locus/documents/{id}/verify-citationdealroom:read_approved±5 px; 404 MISSING_DOCUMENT
Enqueue analysisBFF → SoRPOST /api/locus/deals/{id}/analysis → POST /api/deals/{dealId}/analysisdossier:write202 + Location
PollBFF → SoRGET /api/locus/analysisjobs/{jobId} → GET /api/analysisjobs/{jobId}dossier:read200 / 404
HistoryBFF → SoRGET /api/locus/deals/{id}/jobs → GET /api/deals/{dealId}/jobsdossier:readlist
ReconciliationBFF → SoRGET /api/locus/deals/{id}/reconciliationfinancial:readcovenantCertificationBlocked
Cell overrideBFF → SoRPOST /api/locus/deals/{id}/cells/{cellId}/overridefinancial:overridesee OCR override SOP

3. Page extraction paths (ocr-ingest.ts) ​

OrderPath (ExtractResult.path)ConditionisScannedocrConfidence
1nativeNon-binary / UTF-8 text without %PDF magicfalse0.97
2scanned_harvestPrintable-string harvest from binary PDF ≥ 40 charstrue0.85
3tesseractHarvest insufficient → runWorkerTesseractOcr → worker /api/v1/ocr (fra+eng)trueworker mean confidence
—OCR_UNREADABLEall of the above fail → FAILED, request a clean copy——

Upload enqueues DOCUMENT_OCR on the BullMQ task queue (when REDIS_URL set) so upload + hash stays inside the 2 s SLA; SoR persist runs in the background.

4. Analysis job lifecycle ​

JobStatus: Queued → Processing → Completed | Failed.

progressPercentcurrentPhase
5Queued in Redis Job Buffer
20Loading OCR pages from PostgreSQL SoR
45Dispatching HTTP Request to Python Analytical Worker
70SYSCOHADA extract, ratios, contradictions, blockers, alerts → Postgres
100Completed

RunFullDossierAnalysisAsync steps (engine locus-dossier-analytics 3.0.0):

  1. A1 — for each financial-looking document: infer fiscal year → extract cells → PersistLineCitations → UpsertFinancialStatement → UpsertRatioSnapshot.
  2. A2 — ScanAndPersistContradictionsAsync over OCR pages (see Contradictions).
  3. B1 — UpsertBlockers against CompletenessRules.
  4. B2 — EmitAlertsAsync from contradictions, covenant breaches, fatal blockers.
  5. Term-sheet facility lines → workspace payload (review grids); RefreshWorkspacePayloadAsync.
  6. Build the typed AnalysisResult (contract).

Failure modes: ANALYSIS_NO_OCR when the dossier has no OCR pages; TENANT_REQUIRED when the job has no org id; worker offline is not fatal.

Completeness rules (BLK-*):

IdMissing documentSeverity
BLK-FIN-01SYSCOHADA financial statementsFATAL
BLK-LOAN-01Loan / facility agreementFATAL
BLK-TS-01Syndication term sheetMAJOR
BLK-LEGAL-01Independent legal opinionFATAL
BLK-CONC-01Concession agreement (if applicable)MAJOR

5. Persistence map ​

TableWritten byNotes
DocumentsPOST /api/documentsS3Key, Sha256Hash (plaintext digest), OcrStatus
ExtractedPagesocr-resultId = pg_{docId}_{n}, WordsJson, OcrConfidence (default 0.9 if 0)
Citationsocr-result + line matchpage cite cite_{docId}_p{n}; line cite cite_{docId}_p{page}_{lineCode}
SearchDocumentsocr-resultocr_{docId}_p{n}, EntityType=document_page, target /workspace/deal-room?project=…
FinancialStatementsanalyticsfs_{dossierId}_{year}, ActifPassifDelta, ReconciliationState
ExtractedCellsanalyticsSourceCitationId null when unmatched
RatioSnapshotsanalyticsratio_{dossier}_{year}_{scenario}, FormulaId=STD_SYSCOHADA_V2
ContradictionsanalyticsMARGIN_LEAKAGE / TENOR_MISMATCH
Blockers, alertsanalyticsBLK-*; alert categories CONTRADICTION, COVENANT, BLOCKER
AuditEventsallDOCUMENT_UPLOAD, DOCUMENT_OCR_PERSIST, CELL_OVERRIDE, …

6. Citations (R4) ​

  • Page cites: cite_{docId}_p{n} from PersistOcr; BoundingBox is the first OCR word box, or [] when there are no words.
  • Line cites when OCR words match taxonomy: cite_{docId}_p{page}_{lineCode} on cells.
  • Contradiction alerts hydrate from Citation rows — no invented bboxes.
  • UI: Voir source / Voir sur le PDF; highlight skipped if bbox unusable.

See Citations (R4).

7. Reading the result ​

GET /api/deals/{id} returns financial_periods (with source_citations) and contradiction_alerts hydrated from SoR rows. An empty tenant gets an empty deal — there is no demo overlay for authenticated tenants.

Override ​

Analyst cell override SOP: OCR override.

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