= core-risk

== purpose

Assesses operational risk at decision points by evaluating validation reports, orchestration state, and inference history. Produces a `RiskSummary` that drives approval tier selection and determines whether an operation may proceed, requires user prompting, or must be blocked.

== responsibilities

* Evaluate validation errors, cycle detection, repeated failures, and inference timeouts
* Map risk signals to a risk level (LOW, MEDIUM, HIGH, CRITICAL)
* Derive a recommended action (PROCEED, PROMPT_USER, BLOCK) from the risk level
* Provide a no-op implementation for deterministic replay paths

== non-responsibilities

* Does not enforce approvals — that belongs to `core:approvals`
* Does not define policy rules — that belongs to `core:policies`
* Does not emit events directly (signals are consumed by the approval layer)
* Does not mutate state

== key types

=== RiskAssessor
* **kind**: interface
* **purpose**: Contract for evaluating a `RiskContext` and producing a `RiskSummary`.

=== DefaultRiskAssessor
* **kind**: class
* **purpose**: Production implementation that inspects validation errors, cycles, retry exhaustion, and inference timeouts from the context.

=== NoOpRiskAssessor
* **kind**: class
* **purpose**: Returns `RiskSummary(LOW, emptyList(), PROCEED)` unconditionally. Used during deterministic replay to avoid running live risk assessment.

=== RiskContext
* **kind**: data class
* **purpose**: Immutable snapshot of all signals available for risk assessment at a decision point.
* **fields**: `validationReport` (validation issues for the current stage), `orchestrationState` (retry count, failure reason, retry policy), `inferenceState` (inference history for timeout detection)

=== TierMapping
* **kind**: file-level extension functions
* **purpose**: Maps `RiskLevel` to `Tier` (LOW→T1, MEDIUM→T2, HIGH→T3, CRITICAL→T4) and maps individual `RiskSignal` instances to their `RiskLevel`.
* **variants**:
  * `RiskLevel.toApprovalTier()` — maps to `core:approvals` Tier
  * `RiskLevel.toRiskAction()` — maps to `RiskAction` (PROCEED, PROMPT_USER, BLOCK); marked `internal`
  * `RiskSignal.toRiskLevel()` — per-signal severity (ValidationErrors→MEDIUM, CycleWithoutExit→MEDIUM, InferenceTimeout→MEDIUM, RepeatedFailure→HIGH); marked `internal`

== event flow

*Inbound:* None directly. The `RiskContext` is assembled from:
* `ValidationReport` (produced by `core:validation`)
* `OrchestrationState` (from `core:kernel`)
* `InferenceState` (from `core:inference`)

*Outbound:* None. `RiskSummary` is returned synchronously to the caller (approval or orchestration layer).

== integration points

* `core:events.risk` — `RiskSummary`, `RiskLevel`, `RiskSignal`, `RiskAction` (event types)
* `core:approvals` — `Tier` enum
* `core:validation.model` — `ValidationReport`, `ValidationSeverity`
* `core:inference` — `InferenceState`, `InferenceStatus`
* `core:kernel` — `OrchestrationState`

== invariants

* `NoOpRiskAssessor` must be used on replay paths — live assessment on replay would produce non-deterministic results
* `RiskContext.orchestrationState` is populated from config before the first assessment call, not from `StageOutcome` (which lives in `core:kernel` and cannot be imported due to dependency direction)

== PlantUML diagram

[plantuml, core-risk, "png"]
----
include::../../diagrams/core-risk.puml[]
----




== known issues

* Signal-to-level mapping is hardcoded in `TierMapping.kt` and not configurable
* No support for custom or external risk signals

== open questions

* Should risk signal mapping become configurable or rule-driven?
* Should risk assessment emit events for audit trail purposes?
