diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index 0db6e038..2c4fba1e 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -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) }, diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt deleted file mode 100644 index bc500bf0..00000000 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyResolver.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.correx.core.transitions.policy - -class CyclePolicyResolver( - private val bindings: Set -) { - - fun resolve(signature: CycleSignature): CyclePolicy? { - return bindings - .firstOrNull { it.cycle == signature } - ?.policy - } -} diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt b/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt deleted file mode 100644 index 4a32a407..00000000 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/PolicyValidation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.correx.core.transitions.policy - -class PolicyValidation { - fun validate( - bindings: Set, - knownCycles: Set - ): List { - - val errors = mutableListOf() - - 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 - } -} diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationContext.kt b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationContext.kt index 2ff5fc52..ee3b7a86 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationContext.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/model/ValidationContext.kt @@ -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, diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicy.kt similarity index 85% rename from core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt rename to core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicy.kt index d9af09a1..6d3091a1 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicy.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicy.kt @@ -1,4 +1,4 @@ -package com.correx.core.transitions.policy +package com.correx.core.validation.policy sealed interface CyclePolicy { data class Retry( diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicyBinding.kt similarity index 68% rename from core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt rename to core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicyBinding.kt index 94ef89dd..ec22935c 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CyclePolicyBinding.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CyclePolicyBinding.kt @@ -1,4 +1,4 @@ -package com.correx.core.transitions.policy +package com.correx.core.validation.policy data class CyclePolicyBinding( val cycle: CycleSignature, diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignature.kt b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignature.kt similarity index 80% rename from core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignature.kt rename to core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignature.kt index 584542f4..72370368 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignature.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignature.kt @@ -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.* diff --git a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignatureFactory.kt b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignatureFactory.kt similarity index 93% rename from core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignatureFactory.kt rename to core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignatureFactory.kt index f1b14ce1..31eae8e2 100644 --- a/core/transitions/src/main/kotlin/com/correx/core/transitions/policy/CycleSignatureFactory.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/policy/CycleSignatureFactory.kt @@ -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 diff --git a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CycleExitRule.kt similarity index 91% rename from core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt rename to core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CycleExitRule.kt index 371937be..009a7bce 100644 --- a/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CyclePolicyBindingRule.kt +++ b/core/validation/src/main/kotlin/com/correx/core/validation/semantic/rules/CycleExitRule.kt @@ -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 { diff --git a/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt b/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt index 783d1920..5331f7b9 100644 --- a/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt +++ b/testing/integration/src/test/kotlin/ValidationPipelineIntegrationTest.kt @@ -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) + } }