epic-12: after epic audit and init commit

This commit is contained in:
2026-05-16 11:42:00 +04:00
commit c77277af0b
461 changed files with 28958 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:inference"))
}
@@ -0,0 +1,34 @@
package com.correx.core.transitions.analysis
import com.correx.core.events.types.StageId
import com.correx.core.transitions.analysis.canonicalization.CycleCanonicalizer.canonicalize
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.state.CycleDfs
import com.correx.core.transitions.state.VisitState
internal class CycleExtractor(
private val adjacencyBuilder: DeterministicAdjacencyBuilder,
private val dfs: CycleDfs,
) {
fun extract(graph: WorkflowGraph): List<DetectedCycle> {
val adjacency = adjacencyBuilder.build(graph)
val state = mutableMapOf<StageId, VisitState>()
val path = mutableListOf<StageId>()
val cycles = mutableListOf<DetectedCycle>()
val sortedNodes = graph.stageIds
.sortedBy { it.value }
for (node in sortedNodes) {
if (state[node] == null) {
dfs.dfs(node, adjacency, state, path, cycles)
}
}
return canonicalize(cycles)
}
}
@@ -0,0 +1,9 @@
package com.correx.core.transitions.analysis
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.TransitionEdge
data class DetectedCycle(
val nodes: List<StageId>,
val edges: List<TransitionEdge>
)
@@ -0,0 +1,21 @@
package com.correx.core.transitions.analysis
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.events.types.StageId
internal class DeterministicAdjacencyBuilder {
fun build(graph: WorkflowGraph): Map<StageId, List<TransitionEdge>> {
val grouped = graph.transitions
.groupBy { it.from }
return grouped.mapValues { (_, edges) ->
edges.sortedWith(
compareBy<TransitionEdge> { it.from.value }
.thenBy { it.id.value }
.thenBy { it.to.value }
)
}
}
}
@@ -0,0 +1,36 @@
package com.correx.core.transitions.analysis.canonicalization
import com.correx.core.transitions.analysis.DetectedCycle
internal object CycleCanonicalizer {
fun canonicalize(
cycles: List<DetectedCycle>
): List<DetectedCycle> {
val normalized = cycles.map { cycle ->
if (cycle.nodes.isEmpty()) return@map cycle
val open = cycle.nodes.dropLast(1)
val minIndex = open
.withIndex()
.minBy { it.value.value }
.index
val rotatedNodes = open.drop(minIndex) + open.take(minIndex)
val rotatedEdges = cycle.edges.drop(minIndex) + cycle.edges.take(minIndex)
val closed = rotatedNodes + rotatedNodes.first()
DetectedCycle(closed, rotatedEdges)
}
return normalized
.distinctBy { cycle ->
val nodeKey = cycle.nodes.joinToString("->") { it.value }
val edgeKey = cycle.edges.joinToString("->") { it.id.value }
"$nodeKey|$edgeKey"
}
.sortedBy { it.nodes.first().value }
}
}
@@ -0,0 +1,10 @@
package com.correx.core.transitions.evaluation
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
data class EvaluationContext(
val sessionId: SessionId,
val currentStage: StageId,
val variables: Map<String, String>
)
@@ -0,0 +1,10 @@
package com.correx.core.transitions.evaluation
import com.correx.core.transitions.graph.TransitionCondition
interface TransitionConditionEvaluator {
fun evaluate(
condition: TransitionCondition,
context: EvaluationContext
): Boolean
}
@@ -0,0 +1,13 @@
package com.correx.core.transitions.execution
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
data class StageExecutionRequest(
val sessionId: String,
val from: StageId,
val to: StageId,
val transitionId: TransitionId,
val context: EvaluationContext
)
@@ -0,0 +1,12 @@
package com.correx.core.transitions.execution
sealed interface StageExecutionResult {
data class Success(
val producedArtifacts: List<String>
) : StageExecutionResult
data class Failure(
val reason: String,
val retryable: Boolean,
) : StageExecutionResult
}
@@ -0,0 +1,7 @@
package com.correx.core.transitions.execution
interface StageExecutor {
fun execute(
request: StageExecutionRequest
): StageExecutionResult
}
@@ -0,0 +1,15 @@
package com.correx.core.transitions.graph
import com.correx.core.events.types.StageId
internal fun WorkflowGraph.sortedStages(): List<StageId> =
stages.keys.sortedBy { it.value }
internal fun WorkflowGraph.sortedTransitions(): List<TransitionEdge> =
transitions.sortedWith(
compareBy(
{ it.from.value },
{ it.id.value },
{ it.to.value }
)
)
@@ -0,0 +1,18 @@
package com.correx.core.transitions.graph
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.ModelCapability
import kotlinx.serialization.Serializable
@Serializable
data class StageConfig(
val requiredCapabilities: Set<ModelCapability> = emptySet(),
val tokenBudget: Int = 4096,
val generationConfig: GenerationConfig = GenerationConfig(
temperature = 0.7,
topP = 1.0,
maxTokens = 2048,
),
val allowedTools: Set<String> = emptySet(),
val maxRetries: Int = 3,
)
@@ -0,0 +1,7 @@
package com.correx.core.transitions.graph
import com.correx.core.transitions.evaluation.EvaluationContext
fun interface TransitionCondition {
fun evaluate(context: EvaluationContext): Boolean
}
@@ -0,0 +1,11 @@
package com.correx.core.transitions.graph
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
data class TransitionEdge(
val id: TransitionId,
val from: StageId,
val to: StageId,
val condition: TransitionCondition
)
@@ -0,0 +1,14 @@
package com.correx.core.transitions.graph
import com.correx.core.events.types.StageId
data class WorkflowGraph(
val stages: Map<StageId, StageConfig>,
val transitions: Set<TransitionEdge>,
val start: StageId,
) {
val stageIds: Set<StageId> get() = stages.keys
init {
require(start in stages) { "start stage must exist in stages" }
}
}
@@ -0,0 +1,47 @@
package com.correx.core.transitions.mapper
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.transitions.execution.StageExecutionRequest
import com.correx.core.transitions.execution.StageExecutionResult
import com.correx.core.events.types.SessionId
class DefaultStageExecutionEventMapper : StageExecutionEventMapper {
override fun toEvents(
request: StageExecutionRequest,
result: StageExecutionResult
): List<EventPayload> {
val baseTransition = TransitionExecutedEvent(
sessionId = SessionId(request.sessionId),
from = request.from,
to = request.to,
transitionId = request.transitionId
)
return when (result) {
is StageExecutionResult.Success -> listOf(
baseTransition,
StageCompletedEvent(
sessionId = SessionId(request.sessionId),
stageId = request.to,
transitionId = request.transitionId
)
)
is StageExecutionResult.Failure -> listOf(
baseTransition,
StageFailedEvent(
sessionId = SessionId(request.sessionId),
stageId = request.to,
transitionId = request.transitionId,
reason = result.reason
)
)
}
}
}
@@ -0,0 +1,12 @@
package com.correx.core.transitions.mapper
import com.correx.core.events.events.EventPayload
import com.correx.core.transitions.execution.StageExecutionRequest
import com.correx.core.transitions.execution.StageExecutionResult
interface StageExecutionEventMapper {
fun toEvents(
request: StageExecutionRequest,
result: StageExecutionResult
): List<EventPayload>
}
@@ -0,0 +1,15 @@
package com.correx.core.transitions.policy
sealed interface CyclePolicy {
data class Retry(
val maxAttempts: Int
) : CyclePolicy
data class Refinement(
val maxIterations: Int
) : CyclePolicy
data class Approval(
val timeoutMs: Long
) : CyclePolicy
}
@@ -0,0 +1,6 @@
package com.correx.core.transitions.policy
data class CyclePolicyBinding(
val cycle: CycleSignature,
val policy: CyclePolicy
)
@@ -0,0 +1,12 @@
package com.correx.core.transitions.policy
class CyclePolicyResolver(
private val bindings: Set<CyclePolicyBinding>
) {
fun resolve(signature: CycleSignature): CyclePolicy? {
return bindings
.firstOrNull { it.cycle == signature }
?.policy
}
}
@@ -0,0 +1,9 @@
package com.correx.core.transitions.policy
import com.correx.core.events.types.StageId
import java.util.SortedSet
data class CycleSignature(
val nodes: SortedSet<StageId>,
val edges: SortedSet<Pair<StageId, StageId>>,
)
@@ -0,0 +1,19 @@
package com.correx.core.transitions.policy
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.TransitionEdge
import java.util.TreeSet
object CycleSignatureFactory {
fun from(nodes: List<StageId>, edges: List<TransitionEdge>): CycleSignature {
val nodeSet = TreeSet<StageId>(compareBy { it.value })
nodeSet.addAll(nodes)
val edgeSet = TreeSet<Pair<StageId, StageId>>(
compareBy({ it.first.value }, { it.second.value })
)
edges.forEach { edgeSet.add(it.from to it.to) }
return CycleSignature(nodes = nodeSet, edges = edgeSet)
}
}
@@ -0,0 +1,23 @@
package com.correx.core.transitions.policy
class PolicyValidation {
fun validate(
bindings: Set<CyclePolicyBinding>,
knownCycles: Set<CycleSignature>
): List<String> {
val errors = mutableListOf<String>()
val bound = bindings.map { it.cycle }.toSet()
val unbound = knownCycles - bound
if (unbound.isNotEmpty()) {
errors += unbound.map {
"Cycle $it has no policy binding"
}
}
return errors
}
}
@@ -0,0 +1,35 @@
package com.correx.core.transitions.resolution
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.transitions.evaluation.TransitionConditionEvaluator
import com.correx.core.transitions.graph.WorkflowGraph
class DefaultTransitionResolver(
private val evaluator: TransitionConditionEvaluator
) : TransitionResolver {
@Suppress("ReturnCount")
override fun resolve(
graph: WorkflowGraph,
context: EvaluationContext
): TransitionDecision {
val outgoing = graph.transitions
.asSequence()
.filter { it.from == context.currentStage }
.sortedWith(TransitionOrdering.comparator)
.toList()
if (outgoing.isEmpty()) return TransitionDecision.NoMatch
for (edge in outgoing) {
val result = evaluator.evaluate(edge.condition, context)
if (result) {
return TransitionDecision.Move(
transitionId = edge.id,
to = edge.to
)
}
}
return TransitionDecision.Stay
}
}
@@ -0,0 +1,20 @@
package com.correx.core.transitions.resolution
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
sealed interface TransitionDecision {
data class Move(
val transitionId: TransitionId,
val to: StageId
) : TransitionDecision
data object Stay : TransitionDecision
data class Blocked(
val reason: String
) : TransitionDecision
data object NoMatch : TransitionDecision
}
@@ -0,0 +1,10 @@
package com.correx.core.transitions.resolution
import com.correx.core.transitions.graph.TransitionEdge
object TransitionOrdering {
val comparator: Comparator<TransitionEdge> =
compareBy<TransitionEdge> { it.from.value }
.thenBy { it.id.value }
.thenBy { it.to.value }
}
@@ -0,0 +1,11 @@
package com.correx.core.transitions.resolution
import com.correx.core.transitions.evaluation.EvaluationContext
import com.correx.core.transitions.graph.WorkflowGraph
interface TransitionResolver {
fun resolve(
graph: WorkflowGraph,
context: EvaluationContext
): TransitionDecision
}
@@ -0,0 +1,41 @@
package com.correx.core.transitions.state
import com.correx.core.transitions.analysis.DetectedCycle
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.events.types.StageId
internal class CycleDfs {
fun dfs(
node: StageId,
adjacency: Map<StageId, List<TransitionEdge>>,
state: MutableMap<StageId, VisitState>,
path: MutableList<StageId>,
cycles: MutableList<DetectedCycle>
) {
state[node] = VisitState.VISITING
path.add(node)
val edges = adjacency[node].orEmpty()
for (edge in edges) {
val next = edge.to
when (state[next]) {
VisitState.VISITING -> {
val cycleStart = path.indexOf(next)
if (cycleStart >= 0) {
val cycleNodes = path.subList(cycleStart, path.size) + next
val cycleEdges = cycleNodes.zipWithNext { from, to ->
adjacency[from]?.firstOrNull { it.to == to }
}.filterNotNull()
cycles.add(DetectedCycle(nodes = cycleNodes, edges = cycleEdges))
}
}
VisitState.VISITED -> Unit
null -> dfs(next, adjacency, state, path, cycles)
}
}
path.removeAt(path.lastIndex)
state[node] = VisitState.VISITED
}
}
@@ -0,0 +1,6 @@
package com.correx.core.transitions.state
internal enum class VisitState {
VISITING,
VISITED
}
@@ -0,0 +1,56 @@
package com.correx.core.transitions.analysis.canonicalization
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.transitions.analysis.DetectedCycle
import com.correx.core.transitions.graph.TransitionEdge
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class CycleCanonicalizerTest {
private fun edge(id: String, from: String, to: String) = TransitionEdge(
id = TransitionId(id),
from = StageId(from),
to = StageId(to),
condition = { true }
)
@Test
fun `same node set but different edges produce two entries`() {
// A -> B -> A with edge t1, and A -> B -> A with edge t2 — different edges
val cycleWithT1 = DetectedCycle(
nodes = listOf(StageId("A"), StageId("B"), StageId("A")),
edges = listOf(edge("t1", "A", "B"), edge("t1b", "B", "A"))
)
val cycleWithT2 = DetectedCycle(
nodes = listOf(StageId("A"), StageId("B"), StageId("A")),
edges = listOf(edge("t2", "A", "B"), edge("t2b", "B", "A"))
)
val result = CycleCanonicalizer.canonicalize(listOf(cycleWithT1, cycleWithT2))
assertEquals(2, result.size)
}
@Test
fun `two rotations of the same cycle dedup to one entry`() {
// Cycle A -> B -> C -> A, represented starting at B or starting at A
val eAB = edge("t1", "A", "B")
val eBC = edge("t2", "B", "C")
val eCA = edge("t3", "C", "A")
val startAtA = DetectedCycle(
nodes = listOf(StageId("A"), StageId("B"), StageId("C"), StageId("A")),
edges = listOf(eAB, eBC, eCA)
)
val startAtB = DetectedCycle(
nodes = listOf(StageId("B"), StageId("C"), StageId("A"), StageId("B")),
edges = listOf(eBC, eCA, eAB)
)
val result = CycleCanonicalizer.canonicalize(listOf(startAtA, startAtB))
assertEquals(1, result.size)
}
}