feat(validation): capability-gap reflection rung (#30 part 2)
Bounded LLM "are you sure?" pass over the capability gaps part 1 detects, run once at plan-lock before the operator is asked to approve — honest mistakes self-correct without escalation; only a genuine tool need reaches the human. - CapabilityGapReflector: fun-interface seam in core:kernel (plain-data in/out, no infrastructure:workflow dep), mirroring SemanticReviewer/SalvageJudge. - CapabilityGapReflectorImpl (apps/server): one InferenceRouter call for the whole gap batch, no per-gap loop, no retry; runCatching degrades to a safe RESOLVED-advisory default on any failure (unroutable/timeout/malformed JSON) — a broken pass can never manufacture a NEEDS_TOOL escalation or a grant. - CapabilityGapReflectedEvent(verdict RESOLVED|NEEDS_TOOL) recorded per gap and registered in eventModule; replay reads the event, never re-invokes inference (invariants #7/#8/#9). - FreestyleDriver.lockAndRun: RESOLVED is advisory (recorded, plan untouched — invariant #3); NEEDS_TOOL is appended to the existing requestPlanApproval preview so the operator decides — no auto-grant anywhere (#4/#5). - reflector nullable -> degrades to part-1 behavior when unwired. Verified: ./gradlew :core:events:test :core:kernel:test :infrastructure:workflow:test :apps:server:test green.
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
package com.correx.core.kernel.orchestration
|
||||
|
||||
import com.correx.core.events.events.CapabilityGapVerdict
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.tools.contract.ToolCapability
|
||||
|
||||
/** One capability gap finding from [com.correx.infrastructure.workflow.CapabilityGapDetector], as
|
||||
* plain data so this module does not depend on infrastructure:workflow. */
|
||||
data class CapabilityGapInput(
|
||||
val stageId: String,
|
||||
val impliedCapability: ToolCapability,
|
||||
val phrase: String,
|
||||
val grantedCapabilities: Set<ToolCapability>,
|
||||
)
|
||||
|
||||
/** Result of reflecting on a single [CapabilityGapInput]. */
|
||||
data class CapabilityGapReflection(
|
||||
val stageId: String,
|
||||
val impliedCapability: ToolCapability,
|
||||
val verdict: CapabilityGapVerdict,
|
||||
val detail: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Seam for the Vikunja #30 part 2 reflection rung: consulted once, bounded, on the capability gaps
|
||||
* a plan-compile pass turned up, before any of them are surfaced to the operator. Like the other gate
|
||||
* seams ([SemanticReviewer], [SalvageJudge]), the inference-touching implementation is injected so
|
||||
* the deterministic core never runs inference itself and the caller stays unit-testable with a fake.
|
||||
*
|
||||
* A single call covers the whole batch of gaps for one plan — there is no per-gap loop and no retry;
|
||||
* the caller records the result as a [com.correx.core.events.events.CapabilityGapReflectedEvent] per
|
||||
* gap and never re-invokes this on replay (invariant #8 — the reflection is nondeterministic,
|
||||
* invariant #9 requires recording the outcome, not re-observing it).
|
||||
*/
|
||||
fun interface CapabilityGapReflector {
|
||||
suspend fun reflect(sessionId: SessionId, gaps: List<CapabilityGapInput>): List<CapabilityGapReflection>
|
||||
}
|
||||
Reference in New Issue
Block a user