feat(events): WorkspaceStateObservedEvent, RepoKnowledgeRetrievedEvent, stateKey on RepoMapComputedEvent

This commit is contained in:
2026-06-11 13:11:12 +04:00
parent 72e7002623
commit 823c2e0ade
6 changed files with 205 additions and 0 deletions
@@ -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
@Serializable
data class RepoKnowledgeHit(
val path: String,
val text: String,
val score: Float,
)
/**
* Result of semantic retrieval over the embedded repo map for a stage (invariant #9):
* recorded at retrieval time; replay and downstream context assembly read the hits from
* this event, never re-query the vector index.
*/
@Serializable
@SerialName("RepoKnowledgeRetrieved")
data class RepoKnowledgeRetrievedEvent(
val sessionId: SessionId,
val stageId: StageId,
val query: String,
val hits: List<RepoKnowledgeHit>,
) : EventPayload
@@ -13,6 +13,23 @@ data class RepoMapEntry(
val symbols: List<String> = emptyList(),
)
/**
* Workspace content-state observed at session start (invariant #9). stateKey identifies the
* exact content state: "git:<HEAD>" for a clean git worktree, "fp:<sha256>" (stat-only
* fingerprint) for dirty or non-git workspaces. Downstream reuse decisions read this event,
* never re-probe the environment.
*/
@Serializable
@SerialName("WorkspaceStateObserved")
data class WorkspaceStateObservedEvent(
val sessionId: SessionId,
val workspaceRoot: String,
val stateKey: String,
val source: String,
val branch: String? = null,
val dirty: Boolean = false,
) : EventPayload
/**
* 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
@@ -25,4 +42,5 @@ data class RepoMapComputedEvent(
val repoRoot: String,
val entries: List<RepoMapEntry>,
val computedAt: Instant,
val stateKey: String? = null,
) : EventPayload
@@ -31,8 +31,10 @@ 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.RepoKnowledgeRetrievedEvent
import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.WorkspaceStateObservedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.PreemptRedirectBlockedEvent
@@ -95,6 +97,8 @@ val eventModule = SerializersModule {
subclass(RetryAttemptedEvent::class)
subclass(RefinementIterationEvent::class)
subclass(RepoMapComputedEvent::class)
subclass(WorkspaceStateObservedEvent::class)
subclass(RepoKnowledgeRetrievedEvent::class)
subclass(RiskAssessedEvent::class)
subclass(ChatSessionStartedEvent::class)
subclass(ChatTurnEvent::class)
@@ -0,0 +1,53 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
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 RepoKnowledgeRetrievedEventSerializationTest {
@Test
fun `RepoKnowledgeRetrievedEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = RepoKnowledgeRetrievedEvent(
sessionId = SessionId("s"),
stageId = StageId("impl"),
query = "implement the parser",
hits = listOf(RepoKnowledgeHit(path = "src/Parser.kt", text = "src/Parser.kt: Parser", score = 0.92f)),
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"type\":\"RepoKnowledgeRetrieved\""), "SerialName must be present: $encoded")
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(), encoded))
}
@Test
fun `empty hits list round-trips`() {
val sample: EventPayload = RepoKnowledgeRetrievedEvent(
sessionId = SessionId("s"),
stageId = StageId("stage"),
query = "some query",
hits = emptyList(),
)
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
eventJson.encodeToString(EventPayload.serializer(), sample)))
}
@Test
fun `multiple hits round-trip correctly`() {
val sample: EventPayload = RepoKnowledgeRetrievedEvent(
sessionId = SessionId("sess"),
stageId = StageId("architect"),
query = "context builder",
hits = listOf(
RepoKnowledgeHit(path = "core/context/ContextBuilder.kt", text = "class ContextBuilder", score = 0.95f),
RepoKnowledgeHit(path = "core/context/DefaultContextBuilder.kt", text = "class DefaultContextBuilder", score = 0.88f),
),
)
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
eventJson.encodeToString(EventPayload.serializer(), sample)))
}
}
@@ -0,0 +1,48 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.RepoMapEntry
import com.correx.core.events.types.SessionId
import kotlinx.datetime.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class RepoMapComputedEventSerializationTest {
@Test
fun `RepoMapComputedEvent with stateKey round-trips`() {
val sample: EventPayload = RepoMapComputedEvent(
sessionId = SessionId("s"),
repoRoot = "/repo",
entries = listOf(RepoMapEntry(path = "src/Foo.kt", score = 0.9)),
computedAt = Instant.parse("2026-06-11T00:00:00Z"),
stateKey = "git:abc123",
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"stateKey\":\"git:abc123\""), "stateKey must be in JSON: $encoded")
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(), encoded))
}
@Test
fun `RepoMapComputedEvent without stateKey field decodes with null`() {
val json = """{"type":"RepoMapComputed","sessionId":"s","repoRoot":"/repo","entries":[],"computedAt":"2026-06-11T00:00:00Z"}"""
val decoded = eventJson.decodeFromString(EventPayload.serializer(), json) as RepoMapComputedEvent
assertNull(decoded.stateKey)
}
@Test
fun `RepoMapComputedEvent with null stateKey round-trips`() {
val sample: EventPayload = RepoMapComputedEvent(
sessionId = SessionId("s"),
repoRoot = "/repo",
entries = emptyList(),
computedAt = Instant.parse("2026-06-11T00:00:00Z"),
stateKey = null,
)
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
eventJson.encodeToString(EventPayload.serializer(), sample)))
}
}
@@ -0,0 +1,55 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.WorkspaceStateObservedEvent
import com.correx.core.events.types.SessionId
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class WorkspaceStateObservedEventSerializationTest {
@Test
fun `WorkspaceStateObservedEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = WorkspaceStateObservedEvent(
sessionId = SessionId("s1"),
workspaceRoot = "/home/user/project",
stateKey = "git:abc1234",
source = "git",
branch = "main",
dirty = false,
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(encoded.contains("\"type\":\"WorkspaceStateObserved\""), "SerialName must be present: $encoded")
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(), encoded))
}
@Test
fun `fingerprint source without branch round-trips`() {
val sample: EventPayload = WorkspaceStateObservedEvent(
sessionId = SessionId("s2"),
workspaceRoot = "/repo",
stateKey = "fp:9f8e7d",
source = "fingerprint",
)
assertEquals(sample, eventJson.decodeFromString(EventPayload.serializer(),
eventJson.encodeToString(EventPayload.serializer(), sample)))
}
@Test
fun `dirty git workspace round-trips with dirty flag`() {
val sample: EventPayload = WorkspaceStateObservedEvent(
sessionId = SessionId("s3"),
workspaceRoot = "/dirty/repo",
stateKey = "fp:deadbeef",
source = "fingerprint",
branch = "feature",
dirty = true,
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded) as WorkspaceStateObservedEvent
assertEquals(true, decoded.dirty)
assertNull(null) // branch can be non-null
}
}