51 lines
3.6 KiB
Markdown
51 lines
3.6 KiB
Markdown
# 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.
|