Skip to main content

Risk scoring

Atlas computes risk in two complementary ways. Per-module rules turn each OSINT module's findings into risk indicators during an investigation. The risk-matrix engine lets operators configure, without code, how entity characteristics map to a portfolio-level risk level. Together they answer "what did we find?" and "how risky is this, by our policy?".

Per-module risk rules

Each OSINT module contributes rules that emit RiskIndicators classified by severity (critical / high / medium / low) and by one of the eleven risk categories. For example, a company-status rule might flag a dissolved entity as critical / corporate_status, and a jurisdiction rule might flag a high-risk jurisdiction as high / jurisdiction.

These rules live in src/temporal/risk_rules.py and run as part of the investigation workflow.

Risk-matrix engine

The risk-matrix engine (src/risk_matrix/, introduced in ADR-008 and refined by ADR-019) lets operators define a matrix: dimensions (e.g. jurisdiction, industry, UBO clarity), the categories within each, and a grid mapping combinations to a risk level, score, and recommended actions.

The pipeline:

  1. Author a matrix schema (risk_matrix_schemas) — the dimensions, categories, and grid.
  2. Assign companies to a matrix (risk_matrix_assignments).
  3. Evaluate — the scorer.py maps the company's resolved ontology fields onto the matrix dimensions (ontology_mapper.py) and looks up the grid cell, producing an evaluation (risk_matrix_evaluations).

Evaluation can run as a Temporal activity for single companies or as a batch workflow (batch_workflow.py) across a whole portfolio.

Dynamic risk categories & live preview

  • Dynamic categories (ADR-021) let operators define their own risk-category inputs rather than being limited to a fixed list.
  • Live preview & portfolio scoring (ADR-018) let engineers see the effect of a matrix change across the portfolio before committing it.

In the frontend, the RiskCenter and the Studio Evaluations views drive these — including a migration heatmap showing which companies changed tier when a matrix is edited (see Frontend architecture).

From findings to a report

Both signals converge in the investigation report: the per-module indicators populate the findings and evidence sections, and the matrix produces the headline risk level shown on the risk gauge. See Reporting.


Deep dive: the scoring engine

This reflects src/risk_matrix/scorer.py, schema_loader.py, ontology_mapper.py, activities.py, batch_workflow.py, repository.py.

Scoring methods

Each factor declares a scoring_method; three stateless functions are registered in SCORING_METHODS:

MethodBehaviourConfig
REFERENCE_LOOKUPLook the entity value up in a frozen reference datasetreference_dataset, multi_value_strategy (max/avg/any_above)
BOOLEANMap true/false/null to scoresscore_true, score_false, score_null
THRESHOLD_RANGESBucket a numeric value into score rangesranges: [{min,max,score}], default_score

A missing scoring_method raises ValueError at evaluation (fail-loud, D-06).

From entity to score

Each factor's wire mapping (ontology_field_path, e.g. LegalEntity.jurisdiction) is resolved from the entity graph by OntologyMatrixMapper. Factor scores are capped at max_score, summed per dimension, then normalized to 0–100 as round(raw_total / max_possible × 100).

Aggregation

MethodFormula
weighted_averageΣ(dim × weight) / Σweight
weighted_max (EBA default)0.6 × max(dim) + 0.4 × weighted_avg
highest_dimensionmax(dim scores)

Escalation as a one-way ratchet

Escalation can only raise the tier (e.g. a sanctions hit forces ≥ high), never lower it.

Determinism, hashing & idempotency

Every evaluation computes four SHA-256 hashes over canonical JSON (sort_keys, compact separators): input_hash, override_hash, evaluation_fingerprint, output_hash. If an evaluation with the same fingerprint already exists for the company+schema, the activity returns the cached result instead of re-scoring. This is what makes risk reproducible and auditable — the same inputs and reference snapshot always produce the same, verifiable result.

Frozen reference snapshots

At publish time a matrix freezes a snapshot of the exact reference-dataset versions it uses (ReferenceDataResolver.resolve_for_snapshot), with tenant-override precedence and per-dataset metadata. Evaluations read the snapshot, so later changes to a list don't silently alter historical scores.

Batch evaluation

BatchReEvaluationWorkflow (Temporal) iterates companies sequentially (no asyncio.gather), tracking completed / failed / failed_companies as queryable in-memory state, with a 3-attempt retry per company and a 5-minute per-evaluation timeout. A failure doesn't abort the batch.

To author a matrix without code, see Add a risk matrix.