feat(events,server): ExecutionPlanRejectedEvent — freestyle compile failure visible in log
Both silent-failure branches in FreestyleDriver.lockAndRun now emit ExecutionPlanRejectedEvent (source=compile or missing_content) before returning, satisfying invariant #1. Event registered in eventModule for correct polymorphic serialization.
This commit is contained in:
@@ -2,6 +2,7 @@ package com.correx.apps.server.freestyle
|
|||||||
|
|
||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||||
|
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||||
import com.correx.core.events.events.NewEvent
|
import com.correx.core.events.events.NewEvent
|
||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.events.types.ArtifactId
|
import com.correx.core.events.types.ArtifactId
|
||||||
@@ -32,11 +33,13 @@ class FreestyleDriver(
|
|||||||
suspend fun lockAndRun(sessionId: SessionId) {
|
suspend fun lockAndRun(sessionId: SessionId) {
|
||||||
val json = planContent(sessionId) ?: run {
|
val json = planContent(sessionId) ?: run {
|
||||||
log.warn("freestyle: no execution_plan content for session={}", sessionId.value)
|
log.warn("freestyle: no execution_plan content for session={}", sessionId.value)
|
||||||
|
emitRejected(sessionId, "no execution_plan content produced by planning phase", "missing_content")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val graph = runCatching { compiler.compile(json, "freestyle-${sessionId.value}") }
|
val graph = runCatching { compiler.compile(json, "freestyle-${sessionId.value}") }
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
log.error("freestyle: plan failed to compile: {}", it.message)
|
log.error("freestyle: plan failed to compile: {}", it.message)
|
||||||
|
emitRejected(sessionId, "plan failed to compile: ${it.message}", "compile")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
eventStore.append(
|
eventStore.append(
|
||||||
@@ -61,6 +64,26 @@ class FreestyleDriver(
|
|||||||
runPhase2(sessionId, graph, config)
|
runPhase2(sessionId, graph, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun emitRejected(sessionId: SessionId, reason: String, source: String) {
|
||||||
|
eventStore.append(
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(UUID.randomUUID().toString()),
|
||||||
|
sessionId = sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = ExecutionPlanRejectedEvent(
|
||||||
|
sessionId = sessionId,
|
||||||
|
reason = reason,
|
||||||
|
source = source,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(FreestyleDriver::class.java)
|
private val log = LoggerFactory.getLogger(FreestyleDriver::class.java)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.correx.core.artifacts.kind.ConfigArtifactKind
|
|||||||
import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry
|
import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry
|
||||||
import com.correx.core.artifacts.kind.JsonSchema
|
import com.correx.core.artifacts.kind.JsonSchema
|
||||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||||
|
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||||
@@ -123,6 +124,12 @@ class FreestyleDriverTest {
|
|||||||
|
|
||||||
assertTrue(lockedEvents.isEmpty(), "No lock event expected for malformed plan")
|
assertTrue(lockedEvents.isEmpty(), "No lock event expected for malformed plan")
|
||||||
assertEquals(0, runPhase2Invocations, "runPhase2 must not be called for malformed plan")
|
assertEquals(0, runPhase2Invocations, "runPhase2 must not be called for malformed plan")
|
||||||
|
|
||||||
|
val rejectedEvents = eventStore.read(sessionId)
|
||||||
|
.map { it.payload }
|
||||||
|
.filterIsInstance<ExecutionPlanRejectedEvent>()
|
||||||
|
assertEquals(1, rejectedEvents.size, "Expected exactly one ExecutionPlanRejectedEvent")
|
||||||
|
assertEquals("compile", rejectedEvents.single().source)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -153,5 +160,11 @@ class FreestyleDriverTest {
|
|||||||
|
|
||||||
assertTrue(lockedEvents.isEmpty(), "No lock event when plan content is absent")
|
assertTrue(lockedEvents.isEmpty(), "No lock event when plan content is absent")
|
||||||
assertEquals(0, runPhase2Invocations, "runPhase2 must not be called when plan is absent")
|
assertEquals(0, runPhase2Invocations, "runPhase2 must not be called when plan is absent")
|
||||||
|
|
||||||
|
val rejectedEvents = eventStore.read(sessionId)
|
||||||
|
.map { it.payload }
|
||||||
|
.filterIsInstance<ExecutionPlanRejectedEvent>()
|
||||||
|
assertEquals(1, rejectedEvents.size, "Expected exactly one ExecutionPlanRejectedEvent")
|
||||||
|
assertEquals("missing_content", rejectedEvents.single().source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,17 @@ data class ExecutionPlanLockedEvent(
|
|||||||
val stageIds: List<String>,
|
val stageIds: List<String>,
|
||||||
val startStageId: String,
|
val startStageId: String,
|
||||||
) : EventPayload
|
) : EventPayload
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The architect's execution_plan could not become the phase-2 graph: content missing,
|
||||||
|
* compile/validation failure, or operator rejection at the plan-review gate. Terminal for
|
||||||
|
* the freestyle run unless re-planned; recorded so replay explains why phase 2 never ran.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("ExecutionPlanRejected")
|
||||||
|
data class ExecutionPlanRejectedEvent(
|
||||||
|
val sessionId: SessionId,
|
||||||
|
val reason: String,
|
||||||
|
/** "compile" | "missing_content" | "operator" */
|
||||||
|
val source: String,
|
||||||
|
) : EventPayload
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
|||||||
import com.correx.core.events.events.ContextTruncatedEvent
|
import com.correx.core.events.events.ContextTruncatedEvent
|
||||||
import com.correx.core.events.events.EventPayload
|
import com.correx.core.events.events.EventPayload
|
||||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||||
|
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||||
import com.correx.core.events.events.JournalCompactedEvent
|
import com.correx.core.events.events.JournalCompactedEvent
|
||||||
import com.correx.core.events.events.FileWrittenEvent
|
import com.correx.core.events.events.FileWrittenEvent
|
||||||
import com.correx.core.events.events.L3MemoryRetrievedEvent
|
import com.correx.core.events.events.L3MemoryRetrievedEvent
|
||||||
@@ -102,6 +103,7 @@ val eventModule = SerializersModule {
|
|||||||
subclass(L3MemoryRetrievedEvent::class)
|
subclass(L3MemoryRetrievedEvent::class)
|
||||||
subclass(ContextTruncatedEvent::class)
|
subclass(ContextTruncatedEvent::class)
|
||||||
subclass(ExecutionPlanLockedEvent::class)
|
subclass(ExecutionPlanLockedEvent::class)
|
||||||
|
subclass(ExecutionPlanRejectedEvent::class)
|
||||||
subclass(JournalCompactedEvent::class)
|
subclass(JournalCompactedEvent::class)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package com.correx.core.events.serialization
|
||||||
|
|
||||||
|
import com.correx.core.events.events.EventPayload
|
||||||
|
import com.correx.core.events.events.ExecutionPlanRejectedEvent
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ExecutionPlanRejectedEventSerializationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ExecutionPlanRejectedEvent round-trips as polymorphic EventPayload`() {
|
||||||
|
val sample: EventPayload = ExecutionPlanRejectedEvent(
|
||||||
|
sessionId = SessionId("s"),
|
||||||
|
reason = "plan failed to compile: Unknown artifact kind",
|
||||||
|
source = "compile",
|
||||||
|
)
|
||||||
|
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||||
|
assertTrue(encoded.contains("\"type\":\"ExecutionPlanRejected\""), "SerialName must be present: $encoded")
|
||||||
|
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||||
|
assertEquals(sample, decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user