feat(kernel): reviewer static-first infra (B§5)

- StaticFinding/StaticFindingSeverity + StaticFindingsRecordedEvent (registered + test)
- StaticAnalysisRunner over an injectable CommandRunner; parses compiler/ktlint/detekt
  finding formats (lenient) into StaticFindings
- excludeStaticFindingsFromReview pure filter, live-wired into SessionOrchestrator
  context assembly so static-tool findings are kept out of the reviewer's context
- static_check pipeline stage left as a documented TODO (needs an event-emitting
  deterministic stage-type seam that doesn't exist yet)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 15:36:06 +00:00
parent eb9b1aee65
commit 447fc7a43e
8 changed files with 459 additions and 2 deletions
@@ -0,0 +1,40 @@
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
enum class StaticFindingSeverity { ERROR, WARNING, INFO }
/**
* One deterministic finding from a static-analysis tool (compiler, ktlint, detekt, …).
* [tool] is the tool name; [file]/[line]/[column] locate it; [ruleId] is the tool's rule
* identifier when it emits one (e.g. detekt's `[MagicNumber]`).
*/
@Serializable
data class StaticFinding(
val tool: String,
val file: String,
val line: Int,
val column: Int = 0,
val severity: StaticFindingSeverity,
val message: String,
val ruleId: String? = null,
)
/**
* Deterministic static-analysis output recorded before review so the reviewer can be given a
* clean, static-issue-free context (BACKLOG §B-§5): the compile/lint/format findings a tool
* already caught are mechanically excluded from the reviewer's context, so the LLM reviewer
* spends its attention on semantic review rather than re-finding what the build already reports.
*/
@Serializable
@SerialName("StaticFindingsRecorded")
data class StaticFindingsRecordedEvent(
val sessionId: SessionId,
val stageId: StageId,
val tool: String,
val findings: List<StaticFinding>,
) : EventPayload
@@ -49,6 +49,7 @@ 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.SourceFetchedEvent
import com.correx.core.events.events.StaticFindingsRecordedEvent
import com.correx.core.events.events.StageCheckpointFailedEvent
import com.correx.core.events.events.StageCheckpointPassedEvent
import com.correx.core.events.events.StageCompletedEvent
@@ -143,6 +144,7 @@ val eventModule = SerializersModule {
subclass(SourceFetchedEvent::class)
subclass(LowQualityExtractionEvent::class)
subclass(EgressHostsGrantedEvent::class)
subclass(StaticFindingsRecordedEvent::class)
}
}
@@ -0,0 +1,48 @@
package com.correx.core.events.serialization
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StaticFinding
import com.correx.core.events.events.StaticFindingSeverity
import com.correx.core.events.events.StaticFindingsRecordedEvent
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 StaticAnalysisEventSerializationTest {
@Test
fun `StaticFindingsRecordedEvent round-trips as polymorphic EventPayload`() {
val sample: EventPayload = StaticFindingsRecordedEvent(
sessionId = SessionId("s"),
stageId = StageId("static_check"),
tool = "detekt",
findings = listOf(
StaticFinding(
tool = "detekt",
file = "src/Main.kt",
line = 12,
column = 5,
severity = StaticFindingSeverity.WARNING,
message = "Magic number found",
ruleId = "MagicNumber",
),
StaticFinding(
tool = "kotlinc",
file = "src/Other.kt",
line = 3,
severity = StaticFindingSeverity.ERROR,
message = "unresolved reference: foo",
),
),
)
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
assertTrue(
encoded.contains("\"type\":\"StaticFindingsRecorded\""),
"SerialName must be present: $encoded",
)
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
assertEquals(sample, decoded)
}
}