fix: complete all P2 audit findings — dead code, NPE guard, hygiene

P2-1: Guard SessionId NPE with safe-call in SessionsReducer
P2-2: Remove 16 dead event classes + reducer branches; replace with live orchestration events in tests
P2-3: Standardize ChatInput divergences — guard CancelSession, single-arg ProtocolError
P2-4: Remove dead TuiToolRecord.diff and ToolDisplayStatus.REQUESTED
P2-5: Remove dead streamLive from SessionEventBridge
P2-6: Extract SessionsReducerContext data class for 6+ param method
P2-7: Replace StageId("none") sentinel with TypeId.NONE
P2-9: Add TypeId.random() factory on all type-alias IDs
P2-10: Add KDoc on schemaVersion documenting reserved-for-migration
P2-8: Verified RouterReducer string-template already correct (TypeId value class toString)
This commit is contained in:
2026-05-29 01:01:25 +04:00
parent 8ea3c381ef
commit 7936251d6b
32 changed files with 240 additions and 608 deletions
@@ -1,11 +1,7 @@
package com.correx.core.artifacts
import com.correx.core.artifacts.model.ArtifactRelationship
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRejectedEvent
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
import com.correx.core.events.events.ArtifactSupersededEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.StoredEvent
@@ -25,23 +21,6 @@ class DefaultArtifactReducer : ArtifactReducer {
transition(state, ArtifactLifecyclePhase.VALIDATING, ArtifactLifecyclePhase.CREATED)
is ArtifactValidatedEvent ->
transition(state, ArtifactLifecyclePhase.VALIDATED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactRejectedEvent ->
transition(state, ArtifactLifecyclePhase.REJECTED, ArtifactLifecyclePhase.VALIDATING)
is ArtifactSupersededEvent ->
transition(state, ArtifactLifecyclePhase.SUPERSEDED, ArtifactLifecyclePhase.VALIDATED)
is ArtifactArchivedEvent ->
transitionFromAny(
state,
ArtifactLifecyclePhase.ARCHIVED,
ArtifactLifecyclePhase.VALIDATED,
ArtifactLifecyclePhase.REJECTED,
)
is ArtifactRelationshipAddedEvent -> {
val rel = ArtifactRelationship(p.sourceId, p.targetId, p.relationshipType)
Result.success(
state.copy(lineage = state.lineage.copy(relationships = state.lineage.relationships + rel))
)
}
else -> Result.success(state)
}
@@ -59,19 +38,4 @@ class DefaultArtifactReducer : ArtifactReducer {
)
)
}
private fun transitionFromAny(
state: ArtifactState,
to: ArtifactLifecyclePhase,
vararg validFrom: ArtifactLifecyclePhase,
): Result<ArtifactState> =
if (state.phase in validFrom) {
Result.success(state.copy(phase = to))
} else {
Result.failure(
IllegalStateException(
"Invalid artifact transition: ${state.phase}$to (expected one of ${validFrom.toList()})"
)
)
}
}
@@ -1,23 +1,8 @@
package com.correx.core.context
import com.correx.core.context.state.ContextState
import com.correx.core.events.events.ContextBuildingFailedEvent
import com.correx.core.events.events.ContextBuildingInterruptedEvent
import com.correx.core.events.events.ContextBuildingStartedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.StoredEvent
class DefaultContextReducer : ContextReducer {
override fun reduce(state: ContextState, event: StoredEvent): ContextState =
when (val p = event.payload) {
is ContextBuildingStartedEvent -> state.copy(buildingInProgress = true, interrupted = false)
is ContextPackBuiltEvent -> state.copy(
buildingInProgress = false,
interrupted = false,
builtPackIds = state.builtPackIds + p.contextPackId,
)
is ContextBuildingFailedEvent -> state.copy(buildingInProgress = false, interrupted = false)
is ContextBuildingInterruptedEvent -> state.copy(buildingInProgress = false, interrupted = true)
else -> state
}
override fun reduce(state: ContextState, event: StoredEvent): ContextState = state
}
@@ -1,7 +1,6 @@
package com.correx.core.events.events
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.ArtifactRelationshipType
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName
@@ -23,33 +22,6 @@ data class ArtifactValidatedEvent(
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("ArtifactSuperseded")
data class ArtifactSupersededEvent(
val artifactId: ArtifactId,
val supersededById: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("ArtifactRelationshipAdded")
data class ArtifactRelationshipAddedEvent(
val sourceId: ArtifactId,
val targetId: ArtifactId,
val relationshipType: ArtifactRelationshipType,
val sessionId: SessionId,
) : EventPayload
@Serializable
@SerialName("ArtifactRejected")
data class ArtifactRejectedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
@Serializable
@SerialName("ArtifactCreated")
data class ArtifactCreatedEvent(
@@ -58,11 +30,3 @@ data class ArtifactCreatedEvent(
val stageId: StageId,
val schemaVersion: Int,
) : EventPayload
@Serializable
@SerialName("ArtifactArchived")
data class ArtifactArchivedEvent(
val artifactId: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@@ -1,62 +1,10 @@
package com.correx.core.events.events
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("LayerTruncated")
data class LayerTruncatedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesDropped: Int,
val reason: String,
) : EventPayload
@Serializable
@SerialName("ContextBuildingStarted")
data class ContextBuildingStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("ContextBuildingFailed")
data class ContextBuildingFailedEvent(
val sessionId: SessionId,
val stageId: StageId,
val reason: String,
) : EventPayload
// Emitted during replay when a session ended with buildingInProgress=true and no completion/failure event followed.
@Serializable
@SerialName("ContextBuildingInterrupted")
data class ContextBuildingInterruptedEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@Serializable
@SerialName("CompressionApplied")
data class CompressionAppliedEvent(
val contextPackId: ContextPackId,
val layer: String,
val entriesRemoved: Int,
val strategyApplied: String,
) : EventPayload
@Serializable
@SerialName("ContextPackBuilt")
data class ContextPackBuiltEvent(
val contextPackId: ContextPackId,
val sessionId: SessionId,
val stageId: StageId,
val budgetUsed: Int,
val budgetLimit: Int,
) : EventPayload
@Serializable
@SerialName("SteeringNoteAdded")
data class SteeringNoteAddedEvent(
@@ -12,6 +12,11 @@ data class EventMetadata(
val eventId: EventId,
val sessionId: SessionId,
val timestamp: Instant,
/**
* Reserved for future event schema migration. Currently always hardcoded to `1`.
* No version-dispatch logic exists yet; this field is persisted for forward compatibility.
* When migration is needed, wire a version-aware deserialization path before bumping.
*/
val schemaVersion: Int,
val causationId: CausationId?,
val correlationId: CorrelationId?
@@ -4,41 +4,6 @@ import com.correx.core.events.types.SessionId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("SessionStarted")
data class SessionStartedEvent(
val sessionId: SessionId,
val initialContextId: String? = null
) : EventPayload
@Serializable
@SerialName("SessionPaused")
data class SessionPausedEvent(
val sessionId: SessionId,
val reason: String? = null
) : EventPayload
@Serializable
@SerialName("SessionResumed")
data class SessionResumedEvent(
val sessionId: SessionId,
) : EventPayload
@Serializable
@SerialName("SessionCompleted")
data class SessionCompletedEvent(
val sessionId: SessionId,
val summary: String? = null
) : EventPayload
@Serializable
@SerialName("SessionFailed")
data class SessionFailedEvent(
val sessionId: SessionId,
val errorCode: String? = null,
val errorMessage: String? = null
) : EventPayload
@Serializable
@SerialName("ChatSessionStarted")
data class ChatSessionStartedEvent(
@@ -6,14 +6,6 @@ import com.correx.core.events.types.TransitionId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("StageStarted")
data class StageStartedEvent(
val sessionId: SessionId,
val stageId: StageId,
val transitionId: TransitionId
) : EventPayload
@Serializable
@SerialName("StageCompleted")
data class StageCompletedEvent(
@@ -4,39 +4,23 @@ import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalGrantCreatedEvent
import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactArchivedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRejectedEvent
import com.correx.core.events.events.ArtifactRelationshipAddedEvent
import com.correx.core.events.events.ArtifactSupersededEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.CompressionAppliedEvent
import com.correx.core.events.events.ContextBuildingFailedEvent
import com.correx.core.events.events.ContextBuildingInterruptedEvent
import com.correx.core.events.events.ContextBuildingStartedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.ChatSessionStartedEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.InferenceFailedEvent
import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.LayerTruncatedEvent
import com.correx.core.events.events.ModelLoadedEvent
import com.correx.core.events.events.ModelUnloadedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionFailedEvent
import com.correx.core.events.events.ChatSessionStartedEvent
import com.correx.core.events.events.SessionPausedEvent
import com.correx.core.events.events.SessionResumedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
@@ -62,13 +46,7 @@ val eventModule = SerializersModule {
subclass(ToolExecutionCompletedEvent::class)
subclass(ToolExecutionFailedEvent::class)
subclass(ToolExecutionRejectedEvent::class)
subclass(SessionStartedEvent::class)
subclass(SessionPausedEvent::class)
subclass(SessionResumedEvent::class)
subclass(SessionCompletedEvent::class)
subclass(SessionFailedEvent::class)
subclass(SteeringNoteAddedEvent::class)
subclass(StageStartedEvent::class)
subclass(StageFailedEvent::class)
subclass(StageCompletedEvent::class)
subclass(TransitionExecutedEvent::class)
@@ -76,19 +54,9 @@ val eventModule = SerializersModule {
subclass(ApprovalDecisionResolvedEvent::class)
subclass(ApprovalGrantCreatedEvent::class)
subclass(ApprovalGrantExpiredEvent::class)
subclass(ContextBuildingStartedEvent::class)
subclass(ContextPackBuiltEvent::class)
subclass(CompressionAppliedEvent::class)
subclass(LayerTruncatedEvent::class)
subclass(ContextBuildingFailedEvent::class)
subclass(ContextBuildingInterruptedEvent::class)
subclass(ArtifactCreatedEvent::class)
subclass(ArtifactValidatingEvent::class)
subclass(ArtifactValidatedEvent::class)
subclass(ArtifactRejectedEvent::class)
subclass(ArtifactSupersededEvent::class)
subclass(ArtifactArchivedEvent::class)
subclass(ArtifactRelationshipAddedEvent::class)
subclass(InferenceFailedEvent::class)
subclass(InferenceCompletedEvent::class)
subclass(InferenceStartedEvent::class)
@@ -1,6 +1,7 @@
package com.correx.core.utils
import kotlinx.serialization.Serializable
import java.util.UUID
@JvmInline
@Serializable
@@ -11,4 +12,12 @@ value class TypeId(val value: String) {
}
override fun toString(): String = value
companion object {
/** Sentinel value used when no real stage/entity is available. */
val NONE = TypeId("none")
/** Create a new random ID using a UUID string. */
fun random(): TypeId = TypeId(UUID.randomUUID().toString())
}
}
@@ -2,21 +2,20 @@ package com.correx.core.events
import com.correx.core.approvals.ApprovalOutcome
import com.correx.core.approvals.ApprovalStatus
import com.correx.core.approvals.GrantScope
import com.correx.core.approvals.Tier
import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ContextPackBuiltEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
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.serialization.eventJson
import com.correx.core.events.types.ApprovalDecisionId
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ContextPackId
import com.correx.core.events.types.RiskSummaryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
@@ -35,7 +34,6 @@ class EventSerializationHardeningTest {
private val ts = Instant.parse("2026-01-01T00:00:00Z")
private val payloads: List<Pair<String, EventPayload>> = listOf(
"SessionStarted" to SessionStartedEvent(sessionId = sessionId),
"ToolExecutionFailed" to ToolExecutionFailedEvent(
invocationId = ToolInvocationId("inv-1"),
sessionId = sessionId,
@@ -51,12 +49,10 @@ class EventSerializationHardeningTest {
resolutionTimestamp = ts,
reason = null
),
"ContextPackBuilt" to ContextPackBuiltEvent(
contextPackId = ContextPackId("cp-1"),
"SteeringNoteAdded" to SteeringNoteAddedEvent(
sessionId = sessionId,
content = "test steering note",
stageId = stageId,
budgetUsed = 100,
budgetLimit = 4096
),
"OrchestrationPaused" to OrchestrationPausedEvent(
sessionId = sessionId,
@@ -69,7 +65,17 @@ class EventSerializationHardeningTest {
riskSummaryId = RiskSummaryId("rs-1"),
level = RiskLevel.MEDIUM,
action = RiskAction.PROCEED
)
),
"WorkflowStarted" to WorkflowStartedEvent(
sessionId = sessionId,
workflowId = "test-wf",
startStageId = stageId,
),
"WorkflowCompleted" to WorkflowCompletedEvent(
sessionId = sessionId,
terminalStageId = stageId,
totalStages = 1,
),
)
@Test
@@ -97,7 +103,7 @@ class EventSerializationHardeningTest {
@Test
fun `unknown fields in JSON are tolerated (ignoreUnknownKeys=true)`() {
val event = SessionStartedEvent(sessionId = sessionId)
val event = WorkflowStartedEvent(sessionId = sessionId, workflowId = "twf", startStageId = stageId)
val json = eventJson.encodeToString(EventPayload.serializer(), event)
val withExtra = json.replace("{", "{\"bogusField\":123,")
assertDoesNotThrow {
@@ -103,7 +103,7 @@ class DefaultRouterContextBuilder(
return ContextPack(
id = ContextPackId("${state.sessionId?.value ?: "unknown"}-router-pack"),
sessionId = state.sessionId ?: SessionId("unknown"),
stageId = state.currentStageId ?: StageId("none"),
stageId = state.currentStageId ?: StageId.NONE,
layers = layers,
budgetUsed = budgetUsed,
budgetLimit = budget.limit,
@@ -46,7 +46,7 @@ class DefaultRouterFacade(
history.add(RouterTurn(role = TurnRole.USER, content = input, timestamp = Clock.System.now()))
val stateWithHistory = state.copy(conversationHistory = history.toList())
val effectiveStageId = state.currentStageId ?: StageId("none")
val effectiveStageId = state.currentStageId ?: StageId.NONE
val contextPack = routerContextBuilder.build(stateWithHistory, config.tokenBudget)
val provider = inferenceRouter.route(effectiveStageId, setOf(ModelCapability.General))
@@ -1,13 +1,7 @@
package com.correx.core.sessions
import com.correx.core.events.events.SessionCompletedEvent
import com.correx.core.events.events.SessionFailedEvent
import com.correx.core.events.events.SessionPausedEvent
import com.correx.core.events.events.SessionResumedEvent
import com.correx.core.events.events.SessionStartedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StageStartedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.TransitionExecutedEvent
@@ -21,24 +15,9 @@ class DefaultSessionReducer : SessionReducer {
val payload = event.payload
val newStatus = when (payload) {
is SessionStartedEvent ->
SessionStatus.ACTIVE
is SessionPausedEvent ->
SessionStatus.PAUSED
is SessionResumedEvent ->
SessionStatus.ACTIVE
is SessionCompletedEvent ->
SessionStatus.COMPLETED
is SessionFailedEvent,
is StageFailedEvent ->
SessionStatus.FAILED
is StageStartedEvent,
is StageCompletedEvent,
is TransitionExecutedEvent ->
SessionStatus.ACTIVE