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
+11
View File
@@ -0,0 +1,11 @@
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.serialization'
}
dependencies {
implementation(project(":core:events"))
implementation(project(":core:sessions"))
implementation(project(":core:transitions"))
}
@@ -0,0 +1,10 @@
package com.correx.core.validation.approval
import com.correx.core.events.types.SessionId
import com.correx.core.validation.model.ValidationReport
data class ApprovalRequest(
val sessionId: SessionId?,
val riskSummary: ValidationRiskStats,
val validationReport: ValidationReport
)
@@ -0,0 +1,46 @@
package com.correx.core.validation.approval
import com.correx.core.validation.model.ValidationReport
import com.correx.core.validation.model.ValidationSeverity
class ApprovalTrigger {
/**
* Evaluates whether a validation report requires human approval.
*
* All approvals produced here are implicitly **Tier.T2 (REVERSIBLE)** — validation decisions
* affect workflow progression but not external state. This tier will be made explicit on
* [ApprovalRequest] once the risk model (Epic 12, task 4) is wired in.
*/
fun evaluate(report: ValidationReport): ApprovalRequest? {
val errors = report.sections
.flatMap { it.issues }
.count { it.severity == ValidationSeverity.ERROR }
val warnings = report.sections
.flatMap { it.issues }
.count { it.severity == ValidationSeverity.WARNING }
val hasCyclePolicyIssue = report.sections
.flatMap { it.issues }
.any { it.code == "CYCLE_POLICY_MISSING" }
val risk = ValidationRiskStats(
errorCount = errors,
warningCount = warnings,
hasCyclePoliciesMissing = hasCyclePolicyIssue
)
// decision boundary (still NOT execution)
if (errors > 0 || hasCyclePolicyIssue) {
return ApprovalRequest(
sessionId = null,
riskSummary = risk,
validationReport = report
)
}
return null
}
}
@@ -0,0 +1,7 @@
package com.correx.core.validation.approval
data class RiskSummary(
val errorCount: Int,
val warningCount: Int,
val hasCyclePoliciesMissing: Boolean
)
@@ -0,0 +1,7 @@
package com.correx.core.validation.approval
data class ValidationRiskStats(
val errorCount: Int,
val warningCount: Int,
val hasCyclePoliciesMissing: Boolean,
)
@@ -0,0 +1,54 @@
package com.correx.core.validation.graph
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationLocation
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.pipeline.Validator
class GraphValidator : Validator {
override fun validate(context: ValidationContext): ValidationSection {
val graph = context.graph
val issues = mutableListOf<ValidationIssue>()
// 1. start node exists (redundant safety check)
if (graph.start !in graph.stages) {
issues += ValidationIssue(
code = "GRAPH_START_INVALID",
message = "Start stage does not exist in graph",
severity = ValidationSeverity.ERROR
)
}
// 2. dangling transitions
graph.transitions.forEach { edge ->
if (edge.from !in graph.stages || edge.to !in graph.stages) {
issues += ValidationIssue(
code = "GRAPH_DANGLING_TRANSITION",
message = "Transition references missing stage",
severity = ValidationSeverity.ERROR,
location = ValidationLocation.Graph(
transitionId = edge.id
)
)
}
}
// 3. cycles (ONLY attach, no interpretation)
context.detectedCycles.forEach { cycle ->
issues += ValidationIssue(
code = "GRAPH_CYCLE_DETECTED",
message = "Cycle detected: ${cycle.nodes}",
severity = ValidationSeverity.INFO
)
}
return ValidationSection(
name = "graph",
issues = issues
)
}
}
@@ -0,0 +1,13 @@
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
data class ValidationContext(
val graph: WorkflowGraph,
val detectedCycles: List<DetectedCycle> = emptyList(),
val cyclePolicies: Set<CyclePolicyBinding> = emptySet(),
val sessionState: SessionState? = null,
)
@@ -0,0 +1,7 @@
package com.correx.core.validation.model
data class ValidationIssue(
val code: String,
val message: String,
val severity: ValidationSeverity,
val location: ValidationLocation? = null)
@@ -0,0 +1,16 @@
package com.correx.core.validation.model
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
sealed interface ValidationLocation {
data class Graph(
val stageId: StageId? = null,
val transitionId: TransitionId? = null
) : ValidationLocation
data class Session(
val sessionId: SessionId
) : ValidationLocation
}
@@ -0,0 +1,10 @@
package com.correx.core.validation.model
data class ValidationReport(
val sections: List<ValidationSection>
) {
fun hasErrors(): Boolean =
sections.any { section ->
section.issues.any { it.severity == ValidationSeverity.ERROR }
}
}
@@ -0,0 +1,6 @@
package com.correx.core.validation.model
data class ValidationSection(
val name: String,
val issues: List<ValidationIssue> = emptyList(),
val metadata: Map<String, String> = emptyMap())
@@ -0,0 +1,7 @@
package com.correx.core.validation.model
enum class ValidationSeverity {
INFO,
WARNING,
ERROR
}
@@ -0,0 +1,14 @@
package com.correx.core.validation.pipeline
import com.correx.core.validation.approval.ApprovalRequest
import com.correx.core.validation.model.ValidationReport
sealed interface ValidationOutcome {
data class Passed(val report: ValidationReport) : ValidationOutcome
/**
* @param retryable set by the validator based on failure type; orchestrator must read this
* and never override it.
*/
data class Rejected(val report: ValidationReport, val retryable: Boolean) : ValidationOutcome
data class NeedsApproval(val request: ApprovalRequest) : ValidationOutcome
}
@@ -0,0 +1,32 @@
package com.correx.core.validation.pipeline
import com.correx.core.validation.approval.ApprovalTrigger
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationReport
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
class ValidationPipeline(
private val validators: List<Validator>,
private val approvalTrigger: ApprovalTrigger? = null
) {
fun validate(context: ValidationContext): ValidationOutcome {
val sections = mutableListOf<ValidationSection>()
for (validator in validators) {
val section = validator.validate(context)
sections += section
if (section.issues.any { it.severity == ValidationSeverity.ERROR }) {
// Structural validation errors are not retryable — the graph must be fixed.
return ValidationOutcome.Rejected(ValidationReport(sections), retryable = false)
}
}
val report = ValidationReport(sections)
val approvalRequest = approvalTrigger?.evaluate(report)
return if (approvalRequest != null) {
ValidationOutcome.NeedsApproval(approvalRequest)
} else {
ValidationOutcome.Passed(report)
}
}
}
@@ -0,0 +1,8 @@
package com.correx.core.validation.pipeline
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationSection
fun interface Validator {
fun validate(context: ValidationContext): ValidationSection
}
@@ -0,0 +1,8 @@
package com.correx.core.validation.semantic
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
interface SemanticRule {
fun validate(context: ValidationContext): List<ValidationIssue>
}
@@ -0,0 +1,20 @@
package com.correx.core.validation.semantic
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.pipeline.Validator
class SemanticValidator(
private val rules: List<SemanticRule>
) : Validator {
override fun validate(context: ValidationContext): ValidationSection {
val issues = rules.flatMap { it.validate(context) }
return ValidationSection(
name = "semantic",
issues = issues
)
}
}
@@ -0,0 +1,37 @@
package com.correx.core.validation.semantic.rules
import com.correx.core.transitions.policy.CycleSignature
import com.correx.core.transitions.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(
private val requirePolicyForCycles: Boolean
) : SemanticRule {
override fun validate(context: ValidationContext): List<ValidationIssue> {
if (!requirePolicyForCycles) return emptyList()
val knownSignatures = context.detectedCycles
.map { CycleSignatureFactory.from(it.nodes, it.edges) }
.toSet()
val bound = context.cyclePolicies
.map { it.cycle }
.toSet()
val unbound = knownSignatures - bound
return unbound.map { signature ->
ValidationIssue(
code = "CYCLE_POLICY_MISSING",
message = "Cycle has no policy binding: $signature",
severity = ValidationSeverity.WARNING,
location = null
)
}
}
}
@@ -0,0 +1,60 @@
package com.correx.core.validation.session
import com.correx.core.sessions.SessionStatus
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.pipeline.Validator
class SessionValidator : Validator {
override fun validate(context: ValidationContext): ValidationSection {
val state = context.sessionState ?: return ValidationSection(
name = "session",
issues = emptyList()
)
val issues = mutableListOf<ValidationIssue>()
val createdAt = state.createdAt
val updatedAt = state.updatedAt
// 1. temporal consistency
if (createdAt != null && updatedAt != null) {
if (createdAt > updatedAt) {
issues += ValidationIssue(
code = "SESSION_TIME_INCONSISTENT",
message = "createdAt is after updatedAt",
severity = ValidationSeverity.ERROR
)
}
}
// 2. invalid transitions sanity
if (state.invalidTransitions < 0) {
issues += ValidationIssue(
code = "SESSION_INVALID_TRANSITIONS_NEGATIVE",
message = "invalidTransitions cannot be negative",
severity = ValidationSeverity.ERROR
)
}
// 3. status sanity (light semantic check only)
if (state.status == SessionStatus.FAILED &&
state.invalidTransitions == 0
) {
issues += ValidationIssue(
code = "SESSION_SUSPICIOUS_FAILURE_STATE",
message = "FAILED session with zero invalid transitions",
severity = ValidationSeverity.WARNING
)
}
return ValidationSection(
name = "session",
issues = issues
)
}
}
@@ -0,0 +1,72 @@
package com.correx.core.validation.transition
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.resolution.TransitionOrdering
import com.correx.core.validation.model.ValidationContext
import com.correx.core.validation.model.ValidationIssue
import com.correx.core.validation.model.ValidationLocation
import com.correx.core.validation.model.ValidationSection
import com.correx.core.validation.model.ValidationSeverity
import com.correx.core.validation.pipeline.Validator
class TransitionValidator(
private val ordering: Comparator<TransitionEdge> = TransitionOrdering.comparator
) : Validator {
override fun validate(context: ValidationContext): ValidationSection {
val graph = context.graph
val issues = mutableListOf<ValidationIssue>()
// group transitions by source node
val grouped = graph.transitions.groupBy { it.from }
// 1. endpoint integrity
graph.transitions.forEach { edge ->
if (edge.from !in graph.stages) {
issues += ValidationIssue(
code = "TRANSITION_INVALID_FROM",
message = "Transition references missing 'from' stage",
severity = ValidationSeverity.ERROR,
location = ValidationLocation.Graph(
transitionId = edge.id
)
)
}
if (edge.to !in graph.stages) {
issues += ValidationIssue(
code = "TRANSITION_INVALID_TO",
message = "Transition references missing 'to' stage",
severity = ValidationSeverity.ERROR,
location = ValidationLocation.Graph(
transitionId = edge.id
)
)
}
}
// 2. deterministic ordering check per node
grouped.forEach { (from, edges) ->
val sorted = edges.sortedWith(ordering)
// detect duplicate ordering signatures (ambiguity risk)
val signatures = sorted.map { it.to }.toSet()
if (signatures.size != sorted.size) {
issues += ValidationIssue(
code = "TRANSITION_NON_DETERMINISTIC_ORDER",
message = "Non-deterministic ordering detected for node $from",
severity = ValidationSeverity.WARNING,
location = ValidationLocation.Graph(stageId = from)
)
}
}
return ValidationSection(
name = "transition",
issues = issues
)
}
}