feat(workflow): ExecutionPlanCompiler + ExecutionPlanLockedEvent (Slice 3)

- ExecutionPlanLockedEvent: registered, replay-deterministic record of the
  locked plan (CAS ref + compiled stage/start ids).
- ExecutionPlanCompiler: validated execution_plan JSON -> WorkflowGraph,
  reusing ConditionSpec.toCondition(); inline prompts via metadata[promptInline];
  malformed plans throw WorkflowValidationException.
- Replay test: locked event + plan JSON recompiles to the identical graph,
  no architect re-run, no FS reads.
This commit is contained in:
2026-06-08 02:33:57 +04:00
parent be78eaa80f
commit 6c8c5e2ad9
8 changed files with 356 additions and 0 deletions
+2
View File
@@ -14,4 +14,6 @@ dependencies {
testImplementation(project(":core:approvals"))
testImplementation(project(":testing:fixtures"))
testImplementation(project(":infrastructure:persistence"))
testImplementation(project(":infrastructure:workflow"))
testImplementation(project(":core:artifacts"))
}
@@ -0,0 +1,103 @@
import com.correx.core.artifacts.kind.ConfigArtifactKind
import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry
import com.correx.core.artifacts.kind.JsonSchema
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.infrastructure.persistence.InMemoryEventStore
import com.correx.infrastructure.workflow.ExecutionPlanCompiler
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class ExecutionPlanLockedReplayTest {
private val planJson = """
{
"goal": "replay test plan",
"stages": [
{
"id": "fetch",
"prompt": "Fetch the data",
"produces": "patch",
"needs": [],
"tools": []
},
{
"id": "transform",
"prompt": "Transform the data",
"produces": "patch",
"needs": ["patch"],
"tools": []
}
],
"edges": [
{
"from": "fetch",
"to": "transform",
"condition": { "type": "always_true" }
},
{
"from": "transform",
"to": "done",
"condition": { "type": "always_true" }
}
]
}
""".trimIndent()
@Test
fun `locked plan event recompiles to identical WorkflowGraph`(): Unit = runBlocking {
val sessionId = SessionId("replay-session-1")
val planArtifactId = ArtifactId("cas://plan-artifact-001")
// CAS store: keyed by ArtifactId -> plan JSON (no FS access — replay determinism)
val casStore = mutableMapOf<ArtifactId, String>()
casStore[planArtifactId] = planJson
val lockedEvent = ExecutionPlanLockedEvent(
sessionId = sessionId,
planArtifactId = planArtifactId,
workflowId = "replay-workflow",
stageIds = listOf("fetch", "transform"),
startStageId = "fetch",
)
val store = InMemoryEventStore()
store.append(
NewEvent(
metadata = EventMetadata(
eventId = EventId("locked-evt-1"),
sessionId = sessionId,
timestamp = Clock.System.now(),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
payload = lockedEvent,
)
)
// Replay: read event back, fetch JSON from CAS, recompile — no FS, no LLM
val replayedEvent = store.read(sessionId)
.map { it.payload }
.filterIsInstance<ExecutionPlanLockedEvent>()
.single()
val replayedJson = casStore.getValue(replayedEvent.planArtifactId)
val trivialSchema = JsonSchema(type = "object")
val registry = DefaultArtifactKindRegistry().also {
it.register(ConfigArtifactKind(id = "patch", schema = trivialSchema, llmEmitted = true))
}
val compiler = ExecutionPlanCompiler(registry)
val graph = compiler.compile(replayedJson, replayedEvent.workflowId)
assertEquals(replayedEvent.stageIds.toSet(), graph.stages.keys.map { it.value }.toSet())
assertEquals(replayedEvent.startStageId, graph.start.value)
}
}