feat(context): overlay sibling-stage writes onto repo retrieval
Files written by earlier stages this session aren't in the session-start repo map and were never embedded, so semantic retrieval is structurally blind to them. Overlay each stage's FileWrittenEvent post-images as deterministic hits (score 1.0) leading the semantic hits, deduped by path — no reindex, no embed. Descriptors derive via the comment-free sourcedesc describe() so agent-written content can't inject prose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,9 +48,18 @@ import com.correx.testing.fixtures.cyclePolicyMissingValidator
|
||||
import com.correx.testing.fixtures.inference.MockInferenceProvider
|
||||
import com.correx.testing.fixtures.transitions.TransitionFixtures
|
||||
import com.correx.testing.kernel.MockSessionEventReplayer
|
||||
import com.correx.core.approvals.Tier
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.ArtifactValidatedEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.FileWrittenEvent
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.ToolInvocationId
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.Clock
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -197,6 +206,67 @@ class NeedsArtifactInjectionTest {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice 6: a file written by an EARLIER stage this session must surface to a later stage as a
|
||||
* deterministic retrieval hit (a "relevantFiles" entry naming the path), even with no embedder —
|
||||
* the session-start repo map can't contain it and semantic retrieval never indexed it.
|
||||
*/
|
||||
@Test
|
||||
fun `file written by an earlier stage surfaces to a later stage as a relevant file`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("write-overlay-1")
|
||||
val stageA = StageId("A")
|
||||
val stageB = StageId("B")
|
||||
val inv = ToolInvocationId("inv-A-1")
|
||||
val siblingPath = "src/Sibling.kt"
|
||||
|
||||
// Seed stage A's write into the log before the run; sessionWrittenHits projects it for stage B.
|
||||
listOf(
|
||||
ToolInvocationRequestedEvent(
|
||||
invocationId = inv, sessionId = sessionId, stageId = stageA,
|
||||
toolName = "file_write", tier = Tier.T2,
|
||||
request = ToolRequest(
|
||||
invocationId = inv, sessionId = sessionId, stageId = stageA,
|
||||
toolName = "file_write", parameters = mapOf("path" to siblingPath),
|
||||
),
|
||||
),
|
||||
FileWrittenEvent(
|
||||
invocationId = inv, sessionId = sessionId, path = siblingPath,
|
||||
postImageHash = "deadbeef", preExisted = false, timestampMs = 1,
|
||||
),
|
||||
).forEach { payload ->
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(java.util.UUID.randomUUID().toString()),
|
||||
sessionId = sessionId, timestamp = Clock.System.now(),
|
||||
schemaVersion = 1, causationId = null, correlationId = null,
|
||||
),
|
||||
payload = payload,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// stageC makes stageB non-terminal so stageB actually gets a context pack built (terminal
|
||||
// stages are entered as end-markers without assembling context).
|
||||
val stageC = StageId("C")
|
||||
val graph = WorkflowGraph(
|
||||
id = "write-overlay-graph",
|
||||
stages = mapOf(stageA to StageConfig(), stageB to StageConfig(), stageC to StageConfig()),
|
||||
transitions = setOf(
|
||||
TransitionEdge(id = TransitionId("t1"), from = stageA, to = stageB, condition = { true }),
|
||||
TransitionEdge(id = TransitionId("t2"), from = stageB, to = stageC, condition = { true }),
|
||||
),
|
||||
start = stageA,
|
||||
)
|
||||
buildOrchestrator(InferenceFixtures.fixedRouter()).run(sessionId, graph, defaultConfig)
|
||||
|
||||
val relevant = capturedEntries.filter { it.sourceType == "relevantFiles" }
|
||||
assertTrue(
|
||||
relevant.any { it.content.contains(siblingPath) },
|
||||
"Expected a relevantFiles entry naming $siblingPath, got: ${relevant.map { it.content }}",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stage with empty needs adds no neededArtifact entries`(): Unit = runBlocking {
|
||||
val sessionId = SessionId("needs-test-2")
|
||||
|
||||
Reference in New Issue
Block a user