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:
- Author a matrix schema (
risk_matrix_schemas) — the dimensions, categories, and grid. - Assign companies to a matrix (
risk_matrix_assignments). - Evaluate — the
scorer.pymaps 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:
| Method | Behaviour | Config |
|---|---|---|
REFERENCE_LOOKUP | Look the entity value up in a frozen reference dataset | reference_dataset, multi_value_strategy (max/avg/any_above) |
BOOLEAN | Map true/false/null to scores | score_true, score_false, score_null |
THRESHOLD_RANGES | Bucket a numeric value into score ranges | ranges: [{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
| Method | Formula |
|---|---|
weighted_average | Σ(dim × weight) / Σweight |
weighted_max (EBA default) | 0.6 × max(dim) + 0.4 × weighted_avg |
highest_dimension | max(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.