feat(kernel): brief echo-back gate (C-A1)
- BriefEcho model + BriefEchoMismatchEvent (registered + serialization test) - BriefEchoExtractor: parses the planner's brief_echo block and the original brief's referenced-files/symbols/acceptance-criteria dimensions - BriefEchoComparator: conservative diff — a dropped acceptance criterion or an invented file path halts; dropping a file/symbol alone does not - checkBriefEcho gate chained into the post-stage flow after groundBriefReferences; opt-in via stageConfig.metadata["briefEcho"], fails the stage (retryable) on divergence with re-grounding feedback - prompt/workflow activation (planner.md emitting brief_echo; role_pipeline metadata) left as live-QA follow-up Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* A planner's structured restatement of the brief it was handed: the files it must touch, the
|
||||
* symbols it must reach, and the acceptance criteria it must satisfy. The harness diffs this echo
|
||||
* against the original brief before any plan is generated, so a divergence halts at the cheapest
|
||||
* point in the pipeline. Also reused as the diff input shape for both sides of the comparison.
|
||||
*/
|
||||
@Serializable
|
||||
data class BriefEcho(
|
||||
val referencedFiles: Set<String> = emptySet(),
|
||||
val symbols: Set<String> = emptySet(),
|
||||
val acceptanceCriteria: Set<String> = emptySet(),
|
||||
)
|
||||
|
||||
/**
|
||||
* A stage echoed the brief but the echo diverged from the original, so the plan is rejected
|
||||
* before generation ("halt before plan generation"). Recorded so replay explains the halt.
|
||||
*
|
||||
* - [droppedFiles] / [droppedSymbols] / [droppedAcceptanceCriteria]: present in the original brief,
|
||||
* missing from the echo — the planner failed to carry them forward.
|
||||
* - [inventedFiles]: file paths in the echo that are not in the original brief — hallucinated paths.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("BriefEchoMismatch")
|
||||
data class BriefEchoMismatchEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val droppedFiles: Set<String>,
|
||||
val droppedSymbols: Set<String>,
|
||||
val droppedAcceptanceCriteria: Set<String>,
|
||||
val inventedFiles: Set<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
|
||||
@@ -109,6 +110,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)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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 BriefEchoEventSerializationTest {
|
||||
|
||||
@Test
|
||||
fun `BriefEchoMismatchEvent round-trips as polymorphic EventPayload`() {
|
||||
val sample: EventPayload = BriefEchoMismatchEvent(
|
||||
sessionId = SessionId("s"),
|
||||
stageId = StageId("plan"),
|
||||
droppedFiles = setOf("core/kernel/Foo.kt"),
|
||||
droppedSymbols = setOf("Foo"),
|
||||
droppedAcceptanceCriteria = setOf("must compile"),
|
||||
inventedFiles = setOf("core/kernel/Bar.kt"),
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user