feat(critique): structured CritiqueFinding + per-model calibration projection
- CritiqueFinding/CritiqueSeverity/CritiqueRole shared type (BACKLOG B6) - CritiqueOutcomeCorrelatedEvent + serialization registration (BACKLOG C-A3) - core/critique module: CriticCalibration state/reducer/projector, keyed by (modelHash, role) - schema+projection only; live reviewer-loop producer wiring deferred Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* How a prior [CritiqueFinding] actually turned out once the surrounding loop resolved.
|
||||
* UPHELD = the finding was confirmed/actioned; DISMISSED = rejected as a false positive;
|
||||
* INCONCLUSIVE = no signal either way.
|
||||
*/
|
||||
@Serializable
|
||||
enum class CritiqueOutcome { UPHELD, DISMISSED, INCONCLUSIVE }
|
||||
|
||||
/**
|
||||
* Correlates one prior [CritiqueFinding] (by [findingId]) with how it actually turned out, so a
|
||||
* critic's precision can be tracked per model and per role over time (the calibration projection
|
||||
* in `:core:critique`). [modelHash] identifies the model that produced the finding; [severity]
|
||||
* is carried so calibration can be sliced by severity band without re-reading the finding.
|
||||
*
|
||||
* This is the recording half only — who decides UPHELD/DISMISSED lives in the reviewer-loop
|
||||
* runtime and is wired separately.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("CritiqueOutcomeCorrelated")
|
||||
data class CritiqueOutcomeCorrelatedEvent(
|
||||
val sessionId: SessionId,
|
||||
val findingId: String,
|
||||
val role: CritiqueRole,
|
||||
val modelHash: String,
|
||||
val severity: CritiqueSeverity,
|
||||
val outcome: CritiqueOutcome,
|
||||
) : EventPayload
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.correx.core.events.events
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** Which critic role produced a [CritiqueFinding]. */
|
||||
@Serializable
|
||||
enum class CritiqueRole { PLAN_CRITIC, CODE_REVIEWER }
|
||||
|
||||
/** Severity of a [CritiqueFinding], highest-impact first. */
|
||||
@Serializable
|
||||
enum class CritiqueSeverity { BLOCKER, MAJOR, MINOR, NIT }
|
||||
|
||||
/**
|
||||
* One structured finding emitted by a critic (plan critic) or reviewer (code reviewer),
|
||||
* replacing the free-text verdict prose those roles produced before. Shared by both roles so
|
||||
* findings can be counted, ranked, and calibrated uniformly.
|
||||
*
|
||||
* [id] is a stable identifier for the finding, used to correlate it with its eventual outcome
|
||||
* (see [CritiqueOutcomeCorrelatedEvent]). [category] is a free-form classification
|
||||
* (e.g. "correctness", "missing_requirement", "style"). [location] optionally points at a
|
||||
* `file:line` or an artifact/stage reference the finding concerns.
|
||||
*
|
||||
* Distinct from `core.validation.ValidationIssue`, which models *structural* validation
|
||||
* (graph/schema) rather than a critic's judgement.
|
||||
*/
|
||||
@Serializable
|
||||
data class CritiqueFinding(
|
||||
val id: String,
|
||||
val role: CritiqueRole,
|
||||
val severity: CritiqueSeverity,
|
||||
val category: String,
|
||||
val message: String,
|
||||
val location: String? = null,
|
||||
)
|
||||
@@ -13,6 +13,7 @@ import com.correx.core.events.events.ChatSessionStartedEvent
|
||||
import com.correx.core.events.events.ClarificationAnsweredEvent
|
||||
import com.correx.core.events.events.ClarificationRequestedEvent
|
||||
import com.correx.core.events.events.ChatTurnEvent
|
||||
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
|
||||
import com.correx.core.events.events.RouterNarrationEvent
|
||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||
@@ -127,6 +128,7 @@ val eventModule = SerializersModule {
|
||||
subclass(WorkflowProposedEvent::class)
|
||||
subclass(IdeaCapturedEvent::class)
|
||||
subclass(IdeaDiscardedEvent::class)
|
||||
subclass(CritiqueOutcomeCorrelatedEvent::class)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.correx.core.events.serialization
|
||||
|
||||
import com.correx.core.events.events.CritiqueOutcome
|
||||
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
|
||||
import com.correx.core.events.events.CritiqueRole
|
||||
import com.correx.core.events.events.CritiqueSeverity
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.types.SessionId
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CritiqueCalibrationEventSerializationTest {
|
||||
|
||||
@Test
|
||||
fun `CritiqueOutcomeCorrelatedEvent round-trips as polymorphic EventPayload`() {
|
||||
val sample: EventPayload = CritiqueOutcomeCorrelatedEvent(
|
||||
sessionId = SessionId("s"),
|
||||
findingId = "f-1",
|
||||
role = CritiqueRole.CODE_REVIEWER,
|
||||
modelHash = "sha256:abc",
|
||||
severity = CritiqueSeverity.MAJOR,
|
||||
outcome = CritiqueOutcome.UPHELD,
|
||||
)
|
||||
val encoded = eventJson.encodeToString(EventPayload.serializer(), sample)
|
||||
assertTrue(
|
||||
encoded.contains("\"type\":\"CritiqueOutcomeCorrelated\""),
|
||||
"SerialName must be present: $encoded",
|
||||
)
|
||||
val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded)
|
||||
assertEquals(sample, decoded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user