feat(events): WorkspaceStateObservedEvent, RepoKnowledgeRetrievedEvent, stateKey on RepoMapComputedEvent
This commit is contained in:
@@ -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(),
|
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.
|
* 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
|
* 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 repoRoot: String,
|
||||||
val entries: List<RepoMapEntry>,
|
val entries: List<RepoMapEntry>,
|
||||||
val computedAt: Instant,
|
val computedAt: Instant,
|
||||||
|
val stateKey: String? = null,
|
||||||
) : EventPayload
|
) : 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.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
import com.correx.core.events.events.RefinementIterationEvent
|
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.RepoMapComputedEvent
|
||||||
import com.correx.core.events.events.RetryAttemptedEvent
|
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.RiskAssessedEvent
|
||||||
import com.correx.core.events.events.StageCompletedEvent
|
import com.correx.core.events.events.StageCompletedEvent
|
||||||
import com.correx.core.events.events.PreemptRedirectBlockedEvent
|
import com.correx.core.events.events.PreemptRedirectBlockedEvent
|
||||||
@@ -95,6 +97,8 @@ val eventModule = SerializersModule {
|
|||||||
subclass(RetryAttemptedEvent::class)
|
subclass(RetryAttemptedEvent::class)
|
||||||
subclass(RefinementIterationEvent::class)
|
subclass(RefinementIterationEvent::class)
|
||||||
subclass(RepoMapComputedEvent::class)
|
subclass(RepoMapComputedEvent::class)
|
||||||
|
subclass(WorkspaceStateObservedEvent::class)
|
||||||
|
subclass(RepoKnowledgeRetrievedEvent::class)
|
||||||
subclass(RiskAssessedEvent::class)
|
subclass(RiskAssessedEvent::class)
|
||||||
subclass(ChatSessionStartedEvent::class)
|
subclass(ChatSessionStartedEvent::class)
|
||||||
subclass(ChatTurnEvent::class)
|
subclass(ChatTurnEvent::class)
|
||||||
|
|||||||
+53
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user