feat(kernel): analyst entity grounding (role-reliability §3)
Local analysts hallucinate file paths the same as planners. A stage can now
opt in with `ground_references = true`: after it produces its brief, every
workspace-relative file path the brief named is checked for existence, and a
path that doesn't exist fails the stage (retryable) with the misses fed back
("Reference only files you have read") so the model re-grounds.
Grounding probes the filesystem directly rather than the repo map — the map
is recency-ranked and depth/extension-limited, not a complete existence
oracle, so "absent from the map" can't be a hard failure. Existence is a
nondeterministic observation, so the result is recorded as a
BriefGroundingCheckedEvent (invariant #9); replay reads it back and never
re-probes.
- BriefGroundingCheckedEvent + GroundingReference, registered in eventModule
- BriefReferenceExtractor: pure, deterministic pull of relative file-path
references from a brief artifact JSON (needs a '/' and a dotted extension;
skips modules, bare names, URLs, absolute/parent paths — so coarse
"subsystem" mentions the brief is allowed to make aren't false-flagged)
- SessionOrchestrator.groundBriefReferences on the post-verifyProduces path,
gated by stage metadata; uses the existing worldProbe
- TomlWorkflowLoader `ground_references` → metadata; role_pipeline analyst
opts in
Scoped to file references with a '/' + extension (not bare basenames, which
a depth-limited probe can't resolve), so a flagged miss is high-confidence.
The symbol-grounding half of §3 is left for a real symbol index.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
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
|
||||
|
||||
/** One file-path reference from a brief and whether it grounds to an existing workspace entry. */
|
||||
@Serializable
|
||||
data class GroundingReference(
|
||||
val reference: String,
|
||||
val grounded: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* Environment observation (invariant #9): the workspace-relative file paths a stage's brief
|
||||
* referenced, each checked for existence at the moment the stage produced its artifact. Replay
|
||||
* reads these recorded facts and never re-probes the filesystem. A reference that does not
|
||||
* ground is a hallucinated path — the failure analysts share with planners — and fails the
|
||||
* stage so the model re-grounds (role-reliability §3 entity grounding).
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("BriefGroundingChecked")
|
||||
data class BriefGroundingCheckedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val artifactId: String,
|
||||
val references: List<GroundingReference>,
|
||||
) : 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.BriefGroundingCheckedEvent
|
||||
import com.correx.core.events.events.ChatSessionStartedEvent
|
||||
import com.correx.core.events.events.ChatTurnEvent
|
||||
import com.correx.core.events.events.RouterNarrationEvent
|
||||
@@ -99,6 +100,7 @@ val eventModule = SerializersModule {
|
||||
subclass(RepoMapComputedEvent::class)
|
||||
subclass(WorkspaceStateObservedEvent::class)
|
||||
subclass(RepoKnowledgeRetrievedEvent::class)
|
||||
subclass(BriefGroundingCheckedEvent::class)
|
||||
subclass(RiskAssessedEvent::class)
|
||||
subclass(ChatSessionStartedEvent::class)
|
||||
subclass(ChatTurnEvent::class)
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.correx.core.events.serialization
|
||||
|
||||
import com.correx.core.events.events.BriefGroundingCheckedEvent
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.GroundingReference
|
||||
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 BriefGroundingCheckedEventSerializationTest {
|
||||
|
||||
private val sample = BriefGroundingCheckedEvent(
|
||||
sessionId = SessionId("sess-1"),
|
||||
stageId = StageId("analyst"),
|
||||
artifactId = "analyst",
|
||||
references = listOf(
|
||||
GroundingReference("core/validation/JsonSchemaValidator.kt", grounded = true),
|
||||
GroundingReference("core/ghost/Phantom.kt", grounded = false),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `round-trips as polymorphic EventPayload`() {
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||
assertTrue(encoded.contains("\"type\":\"BriefGroundingChecked\""), "SerialName must be present: $encoded")
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(sample, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decodes hand-written BriefGroundingChecked JSON`() {
|
||||
val json = """
|
||||
{"type":"BriefGroundingChecked","sessionId":"s","stageId":"analyst","artifactId":"analyst",
|
||||
"references":[{"reference":"a/b.kt","grounded":false}]}
|
||||
""".trimIndent()
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json)
|
||||
assertTrue(decoded is BriefGroundingCheckedEvent)
|
||||
assertEquals(false, (decoded as BriefGroundingCheckedEvent).references.single().grounded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user