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:
@@ -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)
|
||||
|
||||
+33
@@ -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")))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user