diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/RepoMapEvents.kt b/core/events/src/main/kotlin/com/correx/core/events/events/RepoMapEvents.kt new file mode 100644 index 00000000..f3d2f0a9 --- /dev/null +++ b/core/events/src/main/kotlin/com/correx/core/events/events/RepoMapEvents.kt @@ -0,0 +1,28 @@ +package com.correx.core.events.events + +import com.correx.core.events.types.SessionId +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** A single ranked entry in a repo map: a path, its relevance [score], and symbol names. */ +@Serializable +data class RepoMapEntry( + val path: String, + val score: Double, + val symbols: List = emptyList(), +) + +/** + * Environment observation (invariant #9): the repo map computed once at the moment it ran. + * Replay reads these recorded facts and never re-scans the filesystem. Paths + symbol names + * only — never file bodies — so it is referenced, not inlined. + */ +@Serializable +@SerialName("RepoMapComputed") +data class RepoMapComputedEvent( + val sessionId: SessionId, + val repoRoot: String, + val entries: List, + val computedAt: Instant, +) : EventPayload diff --git a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt index 1db44e84..e608d4eb 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt @@ -24,6 +24,7 @@ import com.correx.core.events.events.ModelUnloadedEvent import com.correx.core.events.events.OrchestrationPausedEvent import com.correx.core.events.events.OrchestrationResumedEvent import com.correx.core.events.events.RefinementIterationEvent +import com.correx.core.events.events.RepoMapComputedEvent import com.correx.core.events.events.RetryAttemptedEvent import com.correx.core.events.events.RiskAssessedEvent import com.correx.core.events.events.StageCompletedEvent @@ -80,6 +81,7 @@ val eventModule = SerializersModule { subclass(WorkflowCompletedEvent::class) subclass(RetryAttemptedEvent::class) subclass(RefinementIterationEvent::class) + subclass(RepoMapComputedEvent::class) subclass(RiskAssessedEvent::class) subclass(ChatSessionStartedEvent::class) subclass(ChatTurnEvent::class) diff --git a/testing/replay/src/test/kotlin/RepoMapReplayTest.kt b/testing/replay/src/test/kotlin/RepoMapReplayTest.kt new file mode 100644 index 00000000..de0f3822 --- /dev/null +++ b/testing/replay/src/test/kotlin/RepoMapReplayTest.kt @@ -0,0 +1,47 @@ +import com.correx.core.events.events.EventMetadata +import com.correx.core.events.events.NewEvent +import com.correx.core.events.events.RepoMapComputedEvent +import com.correx.core.events.events.RepoMapEntry +import com.correx.core.events.events.StoredEvent +import com.correx.core.events.types.EventId +import com.correx.core.events.types.SessionId +import com.correx.core.sessions.projections.Projection +import com.correx.core.sessions.projections.replay.DefaultEventReplayer +import com.correx.infrastructure.persistence.InMemoryEventStore +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Instant +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +/** Collects the latest recorded repo map purely from events — never touches the filesystem. */ +private class RepoMapProjection : Projection> { + override fun initial(): List = emptyList() + override fun apply(state: List, event: StoredEvent): List = + when (val p = event.payload) { + is RepoMapComputedEvent -> p.entries + else -> state + } +} + +class RepoMapReplayTest { + + @Test + fun `repo map is reconstructed from the recorded event without re-scanning`(): Unit = runBlocking { + val sessionId = SessionId("s") + val store = InMemoryEventStore() + val entries = listOf( + RepoMapEntry("src/Main.kt", 0.9, listOf("main", "boot")), + RepoMapEntry("src/Util.kt", 0.4, listOf("clamp")), + ) + store.append( + NewEvent( + EventMetadata(EventId("rm"), sessionId, Instant.parse("2026-06-04T00:00:00Z"), 1, null, null), + RepoMapComputedEvent(sessionId, "/repo", entries, Instant.parse("2026-06-04T00:00:00Z")), + ), + ) + + val rebuilt = DefaultEventReplayer(store, RepoMapProjection()).rebuild(sessionId) + + assertEquals(entries, rebuilt) + } +}