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:
2026-07-05 18:04:36 +04:00
parent 24f3b843bb
commit f90f2ab39d
11 changed files with 462 additions and 1 deletions
@@ -50,3 +50,40 @@ data class ArtifactContentStoredEvent(
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
/**
* A near-miss producer output was routed through the artifact-emission repair ladder. Only the
* nondeterministic rung (`rung = "LLM"`) is replay-relevant — the deterministic rungs recompute —
* but both are recorded for observability. See docs/specs/2026-07-04-artifact-emission-pipeline.md.
*/
@Serializable
@SerialName("ArtifactRepairAttempted")
data class ArtifactRepairAttemptedEvent(
val artifactId: ArtifactId,
val classification: String, // ArtifactFailure name
val rung: String, // "DETERMINISTIC" | "LLM"
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
/** The repair ladder produced a schema-valid artifact. [contentHash] feeds the normal store path. */
@Serializable
@SerialName("ArtifactRepairResolved")
data class ArtifactRepairResolvedEvent(
val artifactId: ArtifactId,
val contentHash: ArtifactId,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
/** The repair ladder gave up; [decision] records the policy dispatch (retry/regenerate/abort/escalate). */
@Serializable
@SerialName("ArtifactRepairFailed")
data class ArtifactRepairFailedEvent(
val artifactId: ArtifactId,
val classification: String,
val decision: String,
val reason: String,
val sessionId: SessionId,
val stageId: StageId,
) : EventPayload
@@ -7,6 +7,9 @@ import com.correx.core.events.events.ApprovalGrantExpiredEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRepairAttemptedEvent
import com.correx.core.events.events.ArtifactRepairFailedEvent
import com.correx.core.events.events.ArtifactRepairResolvedEvent
import com.correx.core.events.events.ArtifactValidatedEvent
import com.correx.core.events.events.ArtifactValidatingEvent
import com.correx.core.events.events.BriefEchoMismatchEvent
@@ -120,6 +123,9 @@ val eventModule = SerializersModule {
subclass(ApprovalGrantCreatedEvent::class)
subclass(ApprovalGrantExpiredEvent::class)
subclass(ArtifactContentStoredEvent::class)
subclass(ArtifactRepairAttemptedEvent::class)
subclass(ArtifactRepairResolvedEvent::class)
subclass(ArtifactRepairFailedEvent::class)
subclass(ArtifactCreatedEvent::class)
subclass(ArtifactValidatingEvent::class)
subclass(ArtifactValidatedEvent::class)
@@ -0,0 +1,33 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.ArtifactRepairAttemptedEvent
import com.correx.core.events.events.ArtifactRepairFailedEvent
import com.correx.core.events.events.ArtifactRepairResolvedEvent
import com.correx.core.events.events.EventPayload
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlin.test.Test
import kotlin.test.assertEquals
class ArtifactRepairEventSerializationTest {
private fun roundTrip(sample: EventPayload) =
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
eventJson.encodeToString(EventPayload.serializer(), sample)))
@Test
fun `ArtifactRepairAttempted round-trips`() {
roundTrip(ArtifactRepairAttemptedEvent(ArtifactId("a"), "SCHEMA", "LLM", SessionId("s"), StageId("st")))
}
@Test
fun `ArtifactRepairResolved round-trips`() {
roundTrip(ArtifactRepairResolvedEvent(ArtifactId("a"), ArtifactId("hash"), SessionId("s"), StageId("st")))
}
@Test
fun `ArtifactRepairFailed round-trips`() {
roundTrip(ArtifactRepairFailedEvent(ArtifactId("a"), "UNSAFE", "Abort", "path escapes", SessionId("s"), StageId("st")))
}
}