feat(kernel): brief echo-back gate (plan-pipeline-addenda A1)
New brief_echo stage before the planner in role_pipeline: the model restates the analyst brief as a structured artifact, and a deterministic gate (checkBriefEcho, opt-in via brief_echo=true) diffs the restatement against the original brief. On divergence — dropped requirements (Jaccard coverage < 0.34) or hallucinated files — it emits BriefEchoMismatchEvent and fails the stage retryably, so a misread brief never reaches plan generation. The diff (BriefEchoDiff) is a pure function of two recorded artifacts, so it is replay-safe and records no observation; the event fires only on mismatch, for audit. Symbols are recorded but non-blocking in v1. Mirrors the groundBrief- References gate. role_pipeline only; freestyle_planning wiring is a follow-up. Runtime install (like research): copy brief_echo.json + brief_echo.md and the [[artifacts]] block into ~/.config/correx.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Emitted when the model's restatement of the analyst brief diverges beyond the acceptance
|
||||
* threshold: it dropped requirements or hallucinated file references. Recorded as a domain
|
||||
* signal for audit and narration. The gate then fails the stage (retryable) so the pipeline
|
||||
* cannot reach plan generation on a misread brief.
|
||||
*
|
||||
* Determinism note: this is NOT an environment observation (invariant #9). The diff is a pure
|
||||
* function of two already-recorded artifacts (the analysis brief and the brief_echo), so it is
|
||||
* deterministically recomputable on replay without re-emitting this event. The event is emitted
|
||||
* only on mismatch, as a domain signal.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("BriefEchoMismatch")
|
||||
data class BriefEchoMismatchEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val uncoveredRequirements: List<String>,
|
||||
val hallucinatedFiles: List<String>,
|
||||
val hallucinatedSymbols: List<String>,
|
||||
) : EventPayload
|
||||
@@ -8,6 +8,7 @@ import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatingEvent
|
||||
import com.correx.core.events.events.BriefEchoMismatchEvent
|
||||
import com.correx.core.events.events.BriefGroundingCheckedEvent
|
||||
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||
import com.correx.core.events.events.ClarificationAnsweredEvent
|
||||
@@ -108,6 +109,7 @@ val eventModule = SerializersModule {
|
||||
subclass(WorkspaceStateObservedEvent::class)
|
||||
subclass(RepoKnowledgeRetrievedEvent::class)
|
||||
subclass(BriefGroundingCheckedEvent::class)
|
||||
subclass(BriefEchoMismatchEvent::class)
|
||||
subclass(RiskAssessedEvent::class)
|
||||
subclass(ChatSessionStartedEvent::class)
|
||||
subclass(ChatTurnEvent::class)
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.correx.core.events.serialization
|
||||
|
||||
import com.correx.core.events.events.BriefEchoMismatchEvent
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BriefEchoMismatchEventSerializationTest {
|
||||
|
||||
private val sample = BriefEchoMismatchEvent(
|
||||
sessionId = SessionId("sess-1"),
|
||||
stageId = StageId("brief_echo"),
|
||||
uncoveredRequirements = listOf("must support retries", "must emit an event"),
|
||||
hallucinatedFiles = listOf("core/ghost/Phantom.kt"),
|
||||
hallucinatedSymbols = listOf("GhostService"),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `round-trips as polymorphic EventPayload`() {
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||
assertTrue(encoded.contains("\"type\":\"BriefEchoMismatch\""), "SerialName must be present: $encoded")
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(sample, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodes hand-written BriefEchoMismatch JSON`() {
|
||||
val json = """
|
||||
{"type":"BriefEchoMismatch","sessionId":"s","stageId":"brief_echo",
|
||||
"uncoveredRequirements":["req-1"],"hallucinatedFiles":["a/b.kt"],"hallucinatedSymbols":[]}
|
||||
""".trimIndent()
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
|
||||
assertTrue(decoded is BriefEchoMismatchEvent)
|
||||
val event = decoded as BriefEchoMismatchEvent
|
||||
assertEquals(listOf("req-1"), event.uncoveredRequirements)
|
||||
assertEquals(listOf("a/b.kt"), event.hallucinatedFiles)
|
||||
assertTrue(event.hallucinatedSymbols.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty lists round-trip correctly`() {
|
||||
val empty = sample.copy(
|
||||
uncoveredRequirements = emptyList(),
|
||||
hallucinatedFiles = emptyList(),
|
||||
hallucinatedSymbols = emptyList(),
|
||||
)
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), empty)
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(empty, decoded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user