feat: surface risk rationale to operator in approval prompt

Carry the full RiskSummary (level, signals, rationale) inline on
ApprovalRequestedEvent instead of discarding it after assessment. The two
server-side DTO builders (live ApprovalCoordinator path and replay
DomainEventMapper path) now render real risk level, recommended action,
signal factors, and the [code] rationale lines into the approval WebSocket
message, with a consistent enum-name fallback when no assessment ran (tool
path). RiskSummary/RiskSignal made @Serializable for event embedding;
shared RiskSummary.toDto() lives next to RiskSummaryDto.
This commit is contained in:
2026-05-30 19:08:34 +04:00
parent 8e6a3e1470
commit 780a00229e
11 changed files with 158 additions and 4 deletions
@@ -894,6 +894,7 @@ abstract class SessionOrchestrator(
tier = domainRequest.tier,
validationReportId = domainRequest.validationReportId,
riskSummaryId = domainRequest.riskSummaryId,
riskSummary = domainRequest.riskSummary,
sessionId = sessionId,
stageId = stageId,
projectId = null,
@@ -938,6 +939,9 @@ abstract class SessionOrchestrator(
),
)
val riskSummaryId = RiskSummaryId(UUID.randomUUID().toString())
// RiskAssessedEvent records the indexed level+action summary; ApprovalRequestedEvent carries the full
// authoritative RiskSummary (signals + rationale). Both derive from the same assessment value so
// they cannot diverge.
emit(
sessionId,
RiskAssessedEvent(sessionId, stageId, riskSummaryId, riskSummary.level, riskSummary.recommendedAction),
@@ -947,6 +951,7 @@ abstract class SessionOrchestrator(
tier = riskSummary.level.toApprovalTier(),
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
riskSummaryId = riskSummaryId,
riskSummary = riskSummary,
timestamp = Clock.System.now(),
)
}