kernel: deterministic static_check stage (§B-§5) + critique-outcome producer (§B-§6)

Two reviewer-reliability tracks, both deterministic and unit-verified (no model/network).

§B-§5 — static_check stage seam. The StaticAnalysisRunner + StaticFindingsRecordedEvent +
reviewer-context filter already existed; what was missing was a stage that runs the tool and
emits the event. Added:
- StaticCheckStageExecutor: reads stage metadata (static_tool/static_argv), runs the configured
  command via StaticAnalysisRunner, returns a StaticFindingsRecordedEvent. No-op (empty findings)
  when no runner is wired or no command is configured — safe to carry unconfigured.
- A deterministic-stage seam in DefaultSessionOrchestrator.enterStage: any stage with
  metadata["stage_type"] == "static_check" is run by the executor instead of the LLM subagent,
  then advances on its (unconditional) exit edge.
- TomlWorkflowLoader: stage_type/static_tool/static_argv fields → StageConfig.metadata.
- role_pipeline.toml: a static_check stage between implementer and reviewer (no-op until
  static_argv + a CommandRunner are set; activation is live-QA-gated, see StaticAnalysisRunner doc).

§B-§6 — critique-outcome producer. The CritiqueFinding type + CriticCalibrationProjection existed
but nothing fed them. Added:
- CritiqueFindingsRecordedEvent (+ CritiqueVerdict): the producing side — a critic's findings +
  verdict for one review iteration, carrying modelHash for per-model calibration.
- CritiqueOutcomeCorrelator: pure loop-resolution logic deciding UPHELD (fixed between rounds) /
  DISMISSED (persisted into an approved final) / INCONCLUSIVE (open at a non-approved terminal),
  per critic (role + modelHash) and per finding id.
- A hook in completeWorkflow/failWorkflow that correlates recorded findings into
  CritiqueOutcomeCorrelatedEvents at loop resolution — no-op when none recorded, idempotent.
  (LLM-side finding emission stays a separate model-gated activation.)

Tests: StaticCheckStageExecutorTest (4), StaticCheckStageTest integration (1, fake runner →
event + transition), CritiqueOutcomeCorrelatorTest (8), CritiqueCalibrationWiringTest integration
(1, seeded findings → outcomes at completion), updated RolePipelineWorkflowTest. Full suites for
core:events/kernel/critique, infrastructure:workflow, testing:integration green; detekt clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:14:00 +00:00
parent c2a1e6d76d
commit e46777e29f
14 changed files with 809 additions and 23 deletions
@@ -1,9 +1,38 @@
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
/** A critic's verdict for one review iteration. */
@Serializable
enum class CritiqueVerdict { APPROVED, CHANGES_REQUESTED }
/**
* The findings a critic (plan critic or code reviewer) raised in one review iteration, plus its
* [verdict] for that iteration. This is the **producing side** of critique calibration: the
* reviewer-loop runtime correlates the sequence of these across iterations into
* [CritiqueOutcomeCorrelatedEvent]s (see CritiqueOutcomeCorrelator), which the
* `:core:critique` projection then folds into per-(model, role) precision.
*
* [iteration] is the refinement-loop iteration (1-based) this review belongs to, so the
* correlator can order reviews and tell whether a finding was fixed between rounds. [modelHash]
* identifies the model that produced the findings (carried through to the outcome event so
* calibration is per-model).
*/
@Serializable
@SerialName("CritiqueFindingsRecorded")
data class CritiqueFindingsRecordedEvent(
val sessionId: SessionId,
val stageId: StageId,
val role: CritiqueRole,
val modelHash: String,
val iteration: Int,
val verdict: CritiqueVerdict,
val findings: List<CritiqueFinding>,
) : EventPayload
/**
* How a prior [CritiqueFinding] actually turned out once the surrounding loop resolved.
* UPHELD = the finding was confirmed/actioned; DISMISSED = rejected as a false positive;
@@ -15,6 +15,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.CritiqueFindingsRecordedEvent
import com.correx.core.events.events.CritiqueOutcomeCorrelatedEvent
import com.correx.core.events.events.RouterNarrationEvent
import com.correx.core.events.events.OperatorProfileBoundEvent
@@ -142,6 +143,7 @@ val eventModule = SerializersModule {
subclass(IdeaDiscardedEvent::class)
subclass(IdeaPromotedEvent::class)
subclass(CritiqueOutcomeCorrelatedEvent::class)
subclass(CritiqueFindingsRecordedEvent::class)
subclass(StageCheckpointPassedEvent::class)
subclass(StageCheckpointFailedEvent::class)
subclass(PossibleContradictionFlaggedEvent::class)