refactor: merge cycle policy into semantic validation layer

Wire SemanticValidator into the production validator pipeline and
consolidate cycle-policy types under core:validation.

- Add SemanticValidator(CycleExitRule(requirePolicyForCycles=false))
  to the prod ValidationPipeline (no-op default, preserves behavior)
- Move CyclePolicy/Binding/Signature/Factory from core:transitions.policy
  to core:validation.policy (validation already owns ValidationContext)
- Rename CyclePolicyBindingRule -> CycleExitRule (issue code unchanged)
- Delete dead CyclePolicyResolver + PolicyValidation
This commit is contained in:
2026-05-30 12:57:01 +04:00
parent d68c76ee3c
commit 32d15de034
10 changed files with 52 additions and 45 deletions
@@ -35,6 +35,8 @@ import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.transitions.resolution.DefaultTransitionResolver
import com.correx.core.validation.artifact.ArtifactPayloadValidator
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.core.validation.semantic.SemanticValidator
import com.correx.core.validation.semantic.rules.CycleExitRule
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.artifactscas.DefaultMaterializingArtifactWriter
import com.correx.infrastructure.inference.DefaultProviderRegistry
@@ -106,7 +108,16 @@ fun main() {
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
contextPackBuilder = DefaultContextPackBuilder(DefaultContextCompressor()),
inferenceRouter = inferenceRouter,
validationPipeline = ValidationPipeline(validators = listOf(ArtifactPayloadValidator(artifactStore))),
validationPipeline = ValidationPipeline(
validators = listOf(
ArtifactPayloadValidator(artifactStore),
SemanticValidator(
rules = listOf(
CycleExitRule(requirePolicyForCycles = false),
),
),
),
),
approvalEngine = approvalEngine,
riskAssessor = DefaultRiskAssessor(),
promptResolver = { path -> InfrastructureModule.createPromptLoader().load(path) },
@@ -1,12 +0,0 @@
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
}
}
@@ -1,23 +0,0 @@
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
}
}
@@ -3,7 +3,7 @@ package com.correx.core.validation.model
import com.correx.core.sessions.SessionState
import com.correx.core.transitions.analysis.DetectedCycle
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.policy.CyclePolicyBinding
import com.correx.core.validation.policy.CyclePolicyBinding
data class ValidationContext(
val graph: WorkflowGraph,
@@ -1,4 +1,4 @@
package com.correx.core.transitions.policy
package com.correx.core.validation.policy
sealed interface CyclePolicy {
data class Retry(
@@ -1,4 +1,4 @@
package com.correx.core.transitions.policy
package com.correx.core.validation.policy
data class CyclePolicyBinding(
val cycle: CycleSignature,
@@ -1,4 +1,4 @@
package com.correx.core.transitions.policy
package com.correx.core.validation.policy
import com.correx.core.events.types.StageId
import java.util.*
@@ -1,4 +1,4 @@
package com.correx.core.transitions.policy
package com.correx.core.validation.policy
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.TransitionEdge
@@ -1,12 +1,12 @@
package com.correx.core.validation.semantic.rules
import com.correx.core.transitions.policy.CycleSignatureFactory
import com.correx.core.validation.policy.CycleSignatureFactory
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.semantic.SemanticRule
class CyclePolicyBindingRule(
class CycleExitRule(
private val requirePolicyForCycles: Boolean
) : SemanticRule {
@@ -10,7 +10,7 @@ import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.pipeline.ValidationOutcome
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.core.validation.semantic.SemanticValidator
import com.correx.core.validation.semantic.rules.CyclePolicyBindingRule
import com.correx.core.validation.semantic.rules.CycleExitRule
import com.correx.core.validation.transition.TransitionValidator
import com.correx.testing.fixtures.CycleFixtures
import com.correx.testing.fixtures.WorkflowFixtures
@@ -27,7 +27,7 @@ class ValidationPipelineIntegrationTest {
TransitionValidator(TransitionOrdering.comparator),
SemanticValidator(
rules = listOf(
CyclePolicyBindingRule(requirePolicyForCycles = true),
CycleExitRule(requirePolicyForCycles = true),
),
),
),
@@ -70,4 +70,35 @@ class ValidationPipelineIntegrationTest {
assertInstanceOf(ValidationOutcome.Rejected::class.java, outcome)
assertFalse((outcome as ValidationOutcome.Rejected).retryable)
}
@Test
fun `SemanticValidator with requirePolicyForCycles = false ignores unbound cycles`(): Unit = runBlocking {
// Verify that when requirePolicyForCycles is false, SemanticValidator returns no issues
// even if there are unbound cycles. This is the production default behavior.
val graph = WorkflowFixtures.simpleGraph()
val cycles = listOf(CycleFixtures.simpleCycle())
val pipelineWithFalseFlagValidator = ValidationPipeline(
validators = listOf(
SemanticValidator(
rules = listOf(
CycleExitRule(requirePolicyForCycles = false),
),
),
),
approvalTrigger = null
)
val context = ValidationContext(
graph = graph,
detectedCycles = cycles,
cyclePolicies = emptySet(),
)
val outcome = pipelineWithFalseFlagValidator.validate(context)
// With requirePolicyForCycles = false, the rule returns no issues,
// so the outcome should be Passed (not NeedsApproval).
assertInstanceOf(ValidationOutcome.Passed::class.java, outcome)
}
}