# Ledger Risk Schema Prep — PREP-LEDGER-RISK-66-001/002 Status: Prep complete (2025-11-20) Owners: Findings Ledger Guild · Risk Engine Guild Scope: Contract + data model for PREP-LEDGER-RISK-66-001/002 (risk scoring fields and deterministic upsert). ## Field definitions (canonical finding projection) - `risk_score` (numeric, 0–100, 2dp) — monotonic per `(finding_id, profile_version)`; computed by Risk Engine. - `risk_severity` (enum) — derived mapping: `critical >= 90`, `high >= 70`, `medium >= 40`, `low >= 10`, `informational < 10`. - `risk_profile_version` (string) — semantic version of the scoring policy/profile; required. - `risk_explanation_id` (uuid/string) — pointer to Risk Engine explanation payload stored in Risk service (not duplicated in ledger). - `risk_event_sequence` (long) — ledger sequence of the last applied risk event; enforces monotonic updates. - `risk_updated_at` (ISO-8601 UTC) — when the score was last written. ## Storage and indexes (MongoDB) - Collection: `findings` (existing). Add fields above to the projection document. - Unique compound index: `{ tenant: 1, finding_id: 1, risk_profile_version: 1 }`. - Query helper index for exports/UI: `{ tenant: 1, risk_severity: 1, risk_score: -1, observed_at: -1 }`. - TTL: none; scores are historical but superseded by deterministic upsert described below. ## Deterministic upsert flow (LEDGER-RISK-66-002) 1. Risk Engine emits `RiskScoreApplied` event with `{tenant, finding_id, profile_version, score, explanation_id, event_sequence}`. 2. Handler loads current projection by `(tenant, finding_id)`; compares `(profile_version, event_sequence)`: - If incoming `event_sequence` < stored `risk_event_sequence` → ignore (idempotent). - If equal → idempotent update allowed only when score/severity unchanged. - If greater → write new values and set `risk_event_sequence = event_sequence`. 3. All writes recorded in ledger append with same event_sequence for audit; projection updates deterministic by sequence ordering. 4. Exports (`/ledger/export/findings`) surface these fields; snapshot bundles reuse the same shape. ## API/SDK contract hooks - OAS baseline will mark all four fields in the finding shapes (canonical + compact) as optional today, required once migrations finish. - `/ledger/export/findings` filters: `risk_profile_version` (already reserved), add `risk_severity` and `risk_score_min/max` in the next OAS bump. - UI/SDK must treat missing `risk_profile_version` as “not yet scored”. ## Migration/rollout plan (LEDGER-RISK-66-001) - Step 1: Add fields and indexes behind feature flag `RiskScoringEnabled` (default off). - Step 2: Backfill for latest profile per tenant using Risk Engine batch export; write via deterministic upsert to enforce ordering. - Step 3: Enable streaming ingestion of `RiskScoreApplied` events; monitor lag via metric `ledger_risk_score_apply_lag_seconds`. - Step 4: Flip default for `RiskScoringEnabled` to on after backfill success criteria: - 99.9% of existing findings have `risk_profile_version` populated. - No rejected events due to sequence regressions in the last 24h. - Step 5: Update OAS/SDK to mark fields required; notify UI/Export consumers. ## Observability - Log: `ledger.risk.apply` with tenant, finding_id, profile_version, score, event_sequence, applied (bool). - Metrics: `ledger_risk_apply_total{result}`; `ledger_risk_score_latest{severity}` gauges per tenant. - Tracing: span `ledger.risk.apply` tagging `profile_version`, `event_sequence`, `idempotent`. ## Handoff - This document is the prep artefact for PREP-LEDGER-RISK-66-001/002. Implementation tasks wire schema + deterministic upsert and extend exports/OAS accordingly.