feat(validation): artifact-emission repair ladder + empty-schema fix
Deterministic repair pipeline (extraction, classification, policy, gated LLM-repair rung) with ArtifactRepairAttempted/Failed events. Also fix JsonSchemaValidator: a schema with no declared properties describes 'any object' — unknown-property enforcement now only applies against a declared shape (empty-properties schemas were rejecting every real artifact).
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
package com.correx.core.kernel.orchestration
|
||||
|
||||
import com.correx.core.validation.artifact.ArtifactFailure
|
||||
|
||||
/**
|
||||
* What to do when the artifact-emission repair ladder (deterministic + one LLM rung) still can't
|
||||
* produce a schema-valid artifact. Each maps onto machinery the orchestrator already has.
|
||||
* See docs/specs/2026-07-04-artifact-emission-pipeline.md §3.4.
|
||||
*/
|
||||
sealed interface ArtifactPolicyDecision {
|
||||
data object RetryProducer : ArtifactPolicyDecision // → retryStageOrFail (retryable failure)
|
||||
data object RegenerateArtifactOnly : ArtifactPolicyDecision // → re-run the tools-less emission nudge
|
||||
data object Abort : ArtifactPolicyDecision // → non-retryable failure
|
||||
data object EscalateHuman : ArtifactPolicyDecision // → approval gate / OrchestrationPaused
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure default policy. `attemptsSoFar` is the repair attempts already spent this stage.
|
||||
* Mapping (spec §3.4): UNSAFE→Abort; CONTRADICTORY→RetryProducer; MISSING_INFO→RegenerateArtifactOnly
|
||||
* first, then RetryProducer; FORMATTING/SCHEMA→RetryProducer (deterministic + LLM rung already tried);
|
||||
* exhausted→EscalateHuman when an approver is attached, else Abort.
|
||||
*/
|
||||
fun decide(
|
||||
failure: ArtifactFailure,
|
||||
attemptsSoFar: Int,
|
||||
maxAttempts: Int,
|
||||
hasApprover: Boolean,
|
||||
): ArtifactPolicyDecision {
|
||||
if (failure == ArtifactFailure.UNSAFE) return ArtifactPolicyDecision.Abort
|
||||
val base = when (failure) {
|
||||
ArtifactFailure.MISSING_INFO -> if (attemptsSoFar == 0) {
|
||||
ArtifactPolicyDecision.RegenerateArtifactOnly
|
||||
} else {
|
||||
ArtifactPolicyDecision.RetryProducer
|
||||
}
|
||||
else -> ArtifactPolicyDecision.RetryProducer // FORMATTING, SCHEMA, CONTRADICTORY
|
||||
}
|
||||
// Exhaustion guard: a retry-flavoured decision with no budget left escalates (with an approver) or aborts.
|
||||
return if (attemptsSoFar >= maxAttempts) {
|
||||
if (hasApprover) ArtifactPolicyDecision.EscalateHuman else ArtifactPolicyDecision.Abort
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.correx.core.kernel.orchestration
|
||||
|
||||
import com.correx.core.validation.artifact.ArtifactFailure
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ArtifactPolicyDecisionTest {
|
||||
|
||||
private fun decideFresh(f: ArtifactFailure) = decide(f, attemptsSoFar = 0, maxAttempts = Int.MAX_VALUE, hasApprover = false)
|
||||
|
||||
@Test
|
||||
fun `unsafe always aborts, even with budget and approver`() {
|
||||
assertEquals(ArtifactPolicyDecision.Abort, decide(ArtifactFailure.UNSAFE, 0, 5, hasApprover = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `schema and formatting retry the producer`() {
|
||||
assertEquals(ArtifactPolicyDecision.RetryProducer, decideFresh(ArtifactFailure.SCHEMA))
|
||||
assertEquals(ArtifactPolicyDecision.RetryProducer, decideFresh(ArtifactFailure.FORMATTING))
|
||||
assertEquals(ArtifactPolicyDecision.RetryProducer, decideFresh(ArtifactFailure.CONTRADICTORY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `missing info regenerates artifact-only on first attempt then retries producer`() {
|
||||
assertEquals(ArtifactPolicyDecision.RegenerateArtifactOnly, decide(ArtifactFailure.MISSING_INFO, 0, 5, false))
|
||||
assertEquals(ArtifactPolicyDecision.RetryProducer, decide(ArtifactFailure.MISSING_INFO, 1, 5, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exhausted budget escalates with approver, aborts without`() {
|
||||
assertEquals(ArtifactPolicyDecision.EscalateHuman, decide(ArtifactFailure.SCHEMA, 3, 3, hasApprover = true))
|
||||
assertEquals(ArtifactPolicyDecision.Abort, decide(ArtifactFailure.SCHEMA, 3, 3, hasApprover = false))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user