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:
@@ -4,6 +4,7 @@ import com.correx.apps.server.protocol.ClientMessage
|
|||||||
import com.correx.apps.server.protocol.ProtocolSerializer
|
import com.correx.apps.server.protocol.ProtocolSerializer
|
||||||
import com.correx.apps.server.protocol.RiskSummaryDto
|
import com.correx.apps.server.protocol.RiskSummaryDto
|
||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
|
import com.correx.apps.server.protocol.toDto
|
||||||
import com.correx.core.approvals.ApprovalOutcome
|
import com.correx.core.approvals.ApprovalOutcome
|
||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
@@ -11,6 +12,7 @@ import com.correx.core.approvals.model.ApprovalContext
|
|||||||
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
import com.correx.core.approvals.model.ApprovalScopeIdentity
|
||||||
import com.correx.core.config.ApprovalConfig
|
import com.correx.core.config.ApprovalConfig
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
|
import com.correx.core.events.risk.RiskAction
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
@@ -60,10 +62,11 @@ open class ApprovalCoordinator(
|
|||||||
sessionId = event.sessionId,
|
sessionId = event.sessionId,
|
||||||
requestId = event.requestId,
|
requestId = event.requestId,
|
||||||
tier = event.tier,
|
tier = event.tier,
|
||||||
riskSummary = RiskSummaryDto(
|
riskSummary = event.riskSummary?.toDto() ?: RiskSummaryDto(
|
||||||
level = event.tier.name,
|
level = event.tier.name,
|
||||||
factors = emptyList(),
|
factors = emptyList(),
|
||||||
recommendedAction = "Review and approve or reject",
|
recommendedAction = RiskAction.PROMPT_USER.name,
|
||||||
|
rationale = emptyList(),
|
||||||
),
|
),
|
||||||
toolName = event.toolName,
|
toolName = event.toolName,
|
||||||
preview = event.preview,
|
preview = event.preview,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.correx.apps.server.bridge
|
|||||||
import com.correx.apps.server.protocol.PauseReason
|
import com.correx.apps.server.protocol.PauseReason
|
||||||
import com.correx.apps.server.protocol.RiskSummaryDto
|
import com.correx.apps.server.protocol.RiskSummaryDto
|
||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
|
import com.correx.apps.server.protocol.toDto
|
||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
@@ -23,6 +24,7 @@ import com.correx.core.events.events.ToolInvocationRequestedEvent
|
|||||||
import com.correx.core.events.events.TransitionExecutedEvent
|
import com.correx.core.events.events.TransitionExecutedEvent
|
||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
|
import com.correx.core.events.risk.RiskAction
|
||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -219,7 +221,12 @@ private fun mapApprovalRequested(
|
|||||||
sessionId = p.sessionId,
|
sessionId = p.sessionId,
|
||||||
requestId = p.requestId,
|
requestId = p.requestId,
|
||||||
tier = p.tier,
|
tier = p.tier,
|
||||||
riskSummary = RiskSummaryDto("unknown", emptyList(), ""),
|
riskSummary = p.riskSummary?.toDto() ?: RiskSummaryDto(
|
||||||
|
level = p.tier.name,
|
||||||
|
factors = emptyList(),
|
||||||
|
recommendedAction = RiskAction.PROMPT_USER.name,
|
||||||
|
rationale = emptyList(),
|
||||||
|
),
|
||||||
toolName = p.toolName,
|
toolName = p.toolName,
|
||||||
preview = p.preview,
|
preview = p.preview,
|
||||||
sequence = seq,
|
sequence = seq,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.correx.apps.server.protocol
|
package com.correx.apps.server.protocol
|
||||||
|
|
||||||
|
import com.correx.core.events.risk.RiskSignal
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -7,6 +9,7 @@ data class RiskSummaryDto(
|
|||||||
val level: String,
|
val level: String,
|
||||||
val factors: List<String>,
|
val factors: List<String>,
|
||||||
val recommendedAction: String,
|
val recommendedAction: String,
|
||||||
|
val rationale: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -75,3 +78,17 @@ data class WorkflowDto(
|
|||||||
val workflowId: String,
|
val workflowId: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun RiskSummary.toDto(): RiskSummaryDto = RiskSummaryDto(
|
||||||
|
level = level.name,
|
||||||
|
factors = signals.map { signal ->
|
||||||
|
when (signal) {
|
||||||
|
is RiskSignal.CycleWithoutExit -> "Cycle without exit: ${signal.cycleId}"
|
||||||
|
is RiskSignal.RepeatedFailure -> "Repeated failure: ${signal.reason} (${signal.count}×)"
|
||||||
|
is RiskSignal.ValidationErrors -> "Validation errors: ${signal.errorCount}"
|
||||||
|
is RiskSignal.InferenceTimeout -> "Inference timeout: ${signal.elapsedMs}ms"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recommendedAction = recommendedAction.name,
|
||||||
|
rationale = rationale,
|
||||||
|
)
|
||||||
|
|||||||
+28
@@ -12,6 +12,10 @@ import com.correx.core.events.events.NewEvent
|
|||||||
import com.correx.core.events.events.StoredEvent
|
import com.correx.core.events.events.StoredEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
|
import com.correx.core.events.risk.RiskAction
|
||||||
|
import com.correx.core.events.risk.RiskLevel
|
||||||
|
import com.correx.core.events.risk.RiskSignal
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.RiskSummaryId
|
import com.correx.core.events.types.RiskSummaryId
|
||||||
@@ -207,4 +211,28 @@ class ApprovalCoordinatorWiringTest {
|
|||||||
coord.handleResponse(msg, otherSessionId)
|
coord.handleResponse(msg, otherSessionId)
|
||||||
assertNull(coord.lookupSession(requestId))
|
assertNull(coord.lookupSession(requestId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onApprovalRequested with non-null riskSummary registers session without error`(): Unit = runBlocking {
|
||||||
|
val gateway = RecordingGateway()
|
||||||
|
val coord = ApprovalCoordinator(gateway, ApprovalConfig(timeoutMs = 300_000L), scope)
|
||||||
|
val riskSummary = RiskSummary(
|
||||||
|
level = RiskLevel.HIGH,
|
||||||
|
signals = listOf(RiskSignal.ValidationErrors(errorCount = 2)),
|
||||||
|
recommendedAction = RiskAction.PROMPT_USER,
|
||||||
|
rationale = listOf("[ERR001] Schema mismatch"),
|
||||||
|
)
|
||||||
|
val event = ApprovalRequestedEvent(
|
||||||
|
requestId = requestId,
|
||||||
|
tier = Tier.T3,
|
||||||
|
validationReportId = ValidationReportId("vr-1"),
|
||||||
|
riskSummaryId = null,
|
||||||
|
riskSummary = riskSummary,
|
||||||
|
sessionId = sessionId,
|
||||||
|
stageId = null,
|
||||||
|
projectId = null,
|
||||||
|
)
|
||||||
|
coord.onApprovalRequested(event)
|
||||||
|
assertEquals(sessionId, coord.lookupSession(requestId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import com.correx.core.events.events.TransitionExecutedEvent
|
|||||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||||
import com.correx.core.events.events.WorkflowFailedEvent
|
import com.correx.core.events.events.WorkflowFailedEvent
|
||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
|
import com.correx.core.events.risk.RiskAction
|
||||||
|
import com.correx.core.events.risk.RiskLevel
|
||||||
|
import com.correx.core.events.risk.RiskSignal
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
@@ -348,7 +352,7 @@ class DomainEventMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ApprovalRequestedEvent maps to ApprovalRequired`(): Unit = runTest {
|
fun `ApprovalRequestedEvent maps to ApprovalRequired with null riskSummary falls back to tier name`(): Unit = runTest {
|
||||||
val requestId = ApprovalRequestId("req-1")
|
val requestId = ApprovalRequestId("req-1")
|
||||||
val event = storedEvent(
|
val event = storedEvent(
|
||||||
ApprovalRequestedEvent(
|
ApprovalRequestedEvent(
|
||||||
@@ -356,6 +360,7 @@ class DomainEventMapperTest {
|
|||||||
tier = Tier.T3,
|
tier = Tier.T3,
|
||||||
validationReportId = ValidationReportId("vr-1"),
|
validationReportId = ValidationReportId("vr-1"),
|
||||||
riskSummaryId = null,
|
riskSummaryId = null,
|
||||||
|
riskSummary = null,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
projectId = null,
|
projectId = null,
|
||||||
@@ -367,6 +372,42 @@ class DomainEventMapperTest {
|
|||||||
assertNull(result.toolName)
|
assertNull(result.toolName)
|
||||||
assertNull(result.preview)
|
assertNull(result.preview)
|
||||||
assertEquals(event.sequence, result.sequence)
|
assertEquals(event.sequence, result.sequence)
|
||||||
|
assertEquals("T3", result.riskSummary.level)
|
||||||
|
assertEquals(emptyList<String>(), result.riskSummary.rationale)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ApprovalRequestedEvent maps to ApprovalRequired with riskSummary propagates rationale`(): Unit = runTest {
|
||||||
|
val requestId = ApprovalRequestId("req-2")
|
||||||
|
val riskSummary = RiskSummary(
|
||||||
|
level = RiskLevel.HIGH,
|
||||||
|
signals = listOf(
|
||||||
|
RiskSignal.ValidationErrors(errorCount = 3),
|
||||||
|
RiskSignal.RepeatedFailure(reason = "parse error", count = 2),
|
||||||
|
),
|
||||||
|
recommendedAction = RiskAction.PROMPT_USER,
|
||||||
|
rationale = listOf("[ERR001] Schema mismatch", "[ERR002] Missing required field"),
|
||||||
|
)
|
||||||
|
val event = storedEvent(
|
||||||
|
ApprovalRequestedEvent(
|
||||||
|
requestId = requestId,
|
||||||
|
tier = Tier.T3,
|
||||||
|
validationReportId = ValidationReportId("vr-2"),
|
||||||
|
riskSummaryId = null,
|
||||||
|
riskSummary = riskSummary,
|
||||||
|
sessionId = sessionId,
|
||||||
|
stageId = stageId,
|
||||||
|
projectId = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L) as ServerMessage.ApprovalRequired
|
||||||
|
assertEquals(requestId, result.requestId)
|
||||||
|
assertEquals("HIGH", result.riskSummary.level)
|
||||||
|
assertEquals("PROMPT_USER", result.riskSummary.recommendedAction)
|
||||||
|
assertEquals(listOf("[ERR001] Schema mismatch", "[ERR002] Missing required field"), result.riskSummary.rationale)
|
||||||
|
assertEquals(2, result.riskSummary.factors.size)
|
||||||
|
assertTrue(result.riskSummary.factors[0].contains("Validation errors"))
|
||||||
|
assertTrue(result.riskSummary.factors[1].contains("Repeated failure"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+2
@@ -2,6 +2,7 @@ package com.correx.core.approvals.model
|
|||||||
|
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.UserSteering
|
import com.correx.core.approvals.UserSteering
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.CausationId
|
import com.correx.core.events.types.CausationId
|
||||||
import com.correx.core.events.types.CorrelationId
|
import com.correx.core.events.types.CorrelationId
|
||||||
@@ -16,6 +17,7 @@ data class DomainApprovalRequest(
|
|||||||
val tier: Tier,
|
val tier: Tier,
|
||||||
val validationReportId: ValidationReportId,
|
val validationReportId: ValidationReportId,
|
||||||
val riskSummaryId: RiskSummaryId?,
|
val riskSummaryId: RiskSummaryId?,
|
||||||
|
val riskSummary: RiskSummary? = null,
|
||||||
val timestamp: Instant,
|
val timestamp: Instant,
|
||||||
val causationId: CausationId? = null,
|
val causationId: CausationId? = null,
|
||||||
val correlationId: CorrelationId? = null,
|
val correlationId: CorrelationId? = null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.correx.core.approvals.ApprovalStatus
|
|||||||
import com.correx.core.approvals.GrantScope
|
import com.correx.core.approvals.GrantScope
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.approvals.UserSteering
|
import com.correx.core.approvals.UserSteering
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import com.correx.core.events.types.ApprovalDecisionId
|
import com.correx.core.events.types.ApprovalDecisionId
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
import com.correx.core.events.types.GrantId
|
import com.correx.core.events.types.GrantId
|
||||||
@@ -24,6 +25,7 @@ data class ApprovalRequestedEvent(
|
|||||||
val tier: Tier,
|
val tier: Tier,
|
||||||
val validationReportId: ValidationReportId,
|
val validationReportId: ValidationReportId,
|
||||||
val riskSummaryId: RiskSummaryId?,
|
val riskSummaryId: RiskSummaryId?,
|
||||||
|
val riskSummary: RiskSummary? = null,
|
||||||
val sessionId: SessionId,
|
val sessionId: SessionId,
|
||||||
val stageId: StageId?,
|
val stageId: StageId?,
|
||||||
val projectId: ProjectId?,
|
val projectId: ProjectId?,
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
package com.correx.core.events.risk
|
package com.correx.core.events.risk
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
sealed class RiskSignal {
|
sealed class RiskSignal {
|
||||||
|
@Serializable
|
||||||
|
@SerialName("CycleWithoutExit")
|
||||||
data class CycleWithoutExit(val cycleId: String) : RiskSignal()
|
data class CycleWithoutExit(val cycleId: String) : RiskSignal()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("RepeatedFailure")
|
||||||
data class RepeatedFailure(val reason: String, val count: Int) : RiskSignal()
|
data class RepeatedFailure(val reason: String, val count: Int) : RiskSignal()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("ValidationErrors")
|
||||||
data class ValidationErrors(val errorCount: Int) : RiskSignal()
|
data class ValidationErrors(val errorCount: Int) : RiskSignal()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("InferenceTimeout")
|
||||||
data class InferenceTimeout(val elapsedMs: Long) : RiskSignal()
|
data class InferenceTimeout(val elapsedMs: Long) : RiskSignal()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.correx.core.events.risk
|
package com.correx.core.events.risk
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class RiskSummary(
|
data class RiskSummary(
|
||||||
val level: RiskLevel,
|
val level: RiskLevel,
|
||||||
val signals: List<RiskSignal>,
|
val signals: List<RiskSignal>,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.correx.core.approvals.ApprovalOutcome
|
|||||||
import com.correx.core.approvals.ApprovalStatus
|
import com.correx.core.approvals.ApprovalStatus
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
|
||||||
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.RiskAssessedEvent
|
import com.correx.core.events.events.RiskAssessedEvent
|
||||||
@@ -13,13 +14,17 @@ import com.correx.core.events.events.WorkflowCompletedEvent
|
|||||||
import com.correx.core.events.events.WorkflowStartedEvent
|
import com.correx.core.events.events.WorkflowStartedEvent
|
||||||
import com.correx.core.events.risk.RiskAction
|
import com.correx.core.events.risk.RiskAction
|
||||||
import com.correx.core.events.risk.RiskLevel
|
import com.correx.core.events.risk.RiskLevel
|
||||||
|
import com.correx.core.events.risk.RiskSignal
|
||||||
|
import com.correx.core.events.risk.RiskSummary
|
||||||
import com.correx.core.events.serialization.eventJson
|
import com.correx.core.events.serialization.eventJson
|
||||||
import com.correx.core.events.types.ApprovalDecisionId
|
import com.correx.core.events.types.ApprovalDecisionId
|
||||||
import com.correx.core.events.types.ApprovalRequestId
|
import com.correx.core.events.types.ApprovalRequestId
|
||||||
|
import com.correx.core.events.types.ProjectId
|
||||||
import com.correx.core.events.types.RiskSummaryId
|
import com.correx.core.events.types.RiskSummaryId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
|
import com.correx.core.events.types.ValidationReportId
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
@@ -110,4 +115,30 @@ class EventSerializationHardeningTest {
|
|||||||
eventJson.decodeFromString(EventPayload.serializer(), withExtra)
|
eventJson.decodeFromString(EventPayload.serializer(), withExtra)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ApprovalRequestedEvent with non-null riskSummary round-trips through EventPayload polymorphic serializer`() {
|
||||||
|
val riskSummary = RiskSummary(
|
||||||
|
level = RiskLevel.HIGH,
|
||||||
|
signals = listOf(
|
||||||
|
RiskSignal.ValidationErrors(errorCount = 3),
|
||||||
|
RiskSignal.RepeatedFailure(reason = "parse error", count = 2),
|
||||||
|
),
|
||||||
|
recommendedAction = RiskAction.PROMPT_USER,
|
||||||
|
rationale = listOf("[ERR001] Schema mismatch", "[ERR002] Missing required field"),
|
||||||
|
)
|
||||||
|
val event: EventPayload = ApprovalRequestedEvent(
|
||||||
|
requestId = ApprovalRequestId("req-rt-1"),
|
||||||
|
tier = Tier.T3,
|
||||||
|
validationReportId = ValidationReportId("vr-rt-1"),
|
||||||
|
riskSummaryId = null,
|
||||||
|
riskSummary = riskSummary,
|
||||||
|
sessionId = sessionId,
|
||||||
|
stageId = stageId,
|
||||||
|
projectId = ProjectId("proj-1"),
|
||||||
|
)
|
||||||
|
val json = eventJson.encodeToString(EventPayload.serializer(), event)
|
||||||
|
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
|
||||||
|
assertEquals(event, decoded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -894,6 +894,7 @@ abstract class SessionOrchestrator(
|
|||||||
tier = domainRequest.tier,
|
tier = domainRequest.tier,
|
||||||
validationReportId = domainRequest.validationReportId,
|
validationReportId = domainRequest.validationReportId,
|
||||||
riskSummaryId = domainRequest.riskSummaryId,
|
riskSummaryId = domainRequest.riskSummaryId,
|
||||||
|
riskSummary = domainRequest.riskSummary,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
stageId = stageId,
|
stageId = stageId,
|
||||||
projectId = null,
|
projectId = null,
|
||||||
@@ -938,6 +939,9 @@ abstract class SessionOrchestrator(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
val riskSummaryId = RiskSummaryId(UUID.randomUUID().toString())
|
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(
|
emit(
|
||||||
sessionId,
|
sessionId,
|
||||||
RiskAssessedEvent(sessionId, stageId, riskSummaryId, riskSummary.level, riskSummary.recommendedAction),
|
RiskAssessedEvent(sessionId, stageId, riskSummaryId, riskSummary.level, riskSummary.recommendedAction),
|
||||||
@@ -947,6 +951,7 @@ abstract class SessionOrchestrator(
|
|||||||
tier = riskSummary.level.toApprovalTier(),
|
tier = riskSummary.level.toApprovalTier(),
|
||||||
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
|
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
|
||||||
riskSummaryId = riskSummaryId,
|
riskSummaryId = riskSummaryId,
|
||||||
|
riskSummary = riskSummary,
|
||||||
timestamp = Clock.System.now(),
|
timestamp = Clock.System.now(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user