feat(orchestration): plan grounding, positive-pattern mining, stage-prompt audition rig

Vikunja #167/#168/#169.

#167 PlanGrounder (infrastructure/workflow): build-prereq + touches-scope
existence grounding, emits PlanGroundingEvaluatedEvent. Deterministic fold
over recorded workspace/plan events (invariants #8/#9).

#168 positive-pattern mining (SessionOrchestratorPlanPatterns): mine
SuccessfulPlanShape from cross-session log — a locked plan whose own
workflow later completed — and inject the closest-resembling plan shape as
L0/SYSTEM advisory context for the planning stage. Keyword-Jaccard
resemblance, no LLM/embedder, replays identically. Advisory only (#3).

#169 stage-prompt audit + CORREX_STAGE_GUIDANCE ON/OFF toggle in
SessionOrchestratorExecution. RunCommand gains --intent to seed a
freestyle session over REST.

Also: workspace verification events + concept-compiler wiring. ./gradlew
check green.
This commit is contained in:
2026-07-16 14:55:56 +04:00
parent ed7efb6072
commit 53d8108189
27 changed files with 1379 additions and 53 deletions
@@ -22,12 +22,21 @@ data class ConceptPromotedEvent(
// A dedicated system session (ConceptCompilerService.SYSTEM_SESSION) — concepts are cross-session,
// so they live on their own stream rather than polluting any user session's replay.
val sessionId: SessionId,
// Gate-agnostic fingerprint of the recurring failure (see FailureFingerprint).
// Gate-agnostic fingerprint of the recurring failure (see FailureFingerprint) — the last-seen
// instance, used as the L3 instance-recall id.
val fingerprint: String,
// Reusable class key `gate + normalized(signature)` the concept is promoted on (design §"class key").
// Defaulted for back-compat with v1 events that predate class-key promotion.
val classKey: String = "",
// The gate the failure recurred on ("contract" | "review" | "plan-compile" | "lint" | "static" | …).
val gate: String,
// Templated, retrieval-friendly concept text injected into L3.
val conceptText: String,
// Count of validated fixes observed at promotion time (≥ threshold).
val occurrences: Int,
// Reference to the validated fix: the CAS path + post-image hash of the file the resolving stage
// wrote to clear this failure (design 2026-07-12-acr-concept-compiler.md §"carry the fix").
// Null when no file write was observed between the retry and the completion. Additive/back-compat.
val fixPath: String? = null,
val fixHash: String? = null,
) : EventPayload
@@ -74,6 +74,34 @@ data class CapabilityGapDetectedEvent(
val timestampMs: Long,
) : EventPayload
/** Verdict of the deterministic plan-grounding phase (design 2026-07-15 seam 1). */
enum class PlanGroundingVerdict { PASS, RETURN_TO_ARCHITECT, CLARIFICATION_REQUIRED }
/**
* The compiled plan was evaluated against recorded workspace facts BEFORE lock (design 2026-07-15
* §"Seam 1: plan grounding before lock"). Deterministic, pure over the plan graph plus the session's
* recorded [RepoMapComputedEvent] and [ProjectProfileBoundEvent] — no inference, no live FS read, so
* replay reads the recorded verdict back (invariants #8/#9). A non-[PlanGroundingVerdict.PASS] verdict
* blocks the lock and returns compact [findings] to the architect, so a doomed scaffold fails at stage
* 0 rather than after downstream files accumulate.
*
* v1 grounds build prerequisites: a stage that will run a build/typecheck/test command against a
* prerequisite (build manifest) that neither exists in the recorded repo map nor is produced by any
* plan stage is ungrounded. Path/symbol-reference grounding (spec rows 2/3) needs a grounding-grade
* symbol index and is deferred — those assumptions resolve to `unknown`, never `present`.
*
* [stateKey] pins the workspace snapshot (repo-map hash) the grounding was evaluated against.
*/
@Serializable
@SerialName("PlanGroundingEvaluated")
data class PlanGroundingEvaluatedEvent(
val sessionId: SessionId,
val planId: String,
val stateKey: String,
val verdict: PlanGroundingVerdict,
val findings: List<String> = emptyList(),
) : EventPayload
/** Outcome of the bounded LLM "are you sure?" reflection pass over a [CapabilityGapDetectedEvent]. */
enum class CapabilityGapVerdict { RESOLVED, NEEDS_TOOL }
@@ -168,3 +168,23 @@ data class RetrySalvageDecidedEvent(
val decision: SalvageDecision,
val rationale: String,
) : EventPayload
/**
* A stage repeatedly blocked on a missing build prerequisite (see repeatedBuildCriticalReferenceBlock,
* design 2026-07-15 §#170) AND the stage both holds file_write and declares the path in-scope, so the
* orchestrator grants it ONE dedicated bootstrap turn to create/repair that prerequisite — separately
* budgeted, NOT charged against the normal stage retry counter. This marker records the grant: it caps
* the bootstrap at one attempt per stage (a second block escalates to recovery) and scopes the
* REFERENCE_EXISTS block window so historical blocks from before the grant don't re-trip the gate on
* the fresh turn. Recorded (invariant #9) so replay reproduces the same bounded bootstrap.
*/
@Serializable
@SerialName("BuildPrerequisiteBootstrapAttempted")
data class BuildPrerequisiteBootstrapAttemptedEvent(
val sessionId: SessionId,
val stageId: StageId,
// The build-critical workspace-relative path the stage is granted a turn to create/repair.
val path: String,
// The gate evidence (repeatedBuildCriticalReferenceBlock reason) that triggered the grant.
val evidence: String,
) : EventPayload
@@ -0,0 +1,32 @@
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
/**
* The known-good workspace invariant (design 2026-07-15 §"Known-good workspace invariant", seam 2).
* After a code/config-writing stage runs its deterministic build/test gate, the orchestrator records
* ONE verification observation binding the [passed] result to the [stateKey] of the workspace that was
* actually verified. The next implementation stage may treat the workspace as `known-good` only when
* a passing observation's [stateKey] still equals the current recorded state key — a later write
* changes the key, so a dirty workspace can never be mistaken for the state that passed.
*
* The [stateKey] is derived purely from the recorded FileWritten manifest (path → latest post-image
* hash), so it is replay-safe: replay reads this event and the key back, never re-running [command]
* or rescanning the filesystem (invariants #8/#9). [changedPaths] are the write-declaring stage's
* paths at verification time; [expectation] is the deterministic build vocabulary (MODULE/PROJECT/
* TESTS) that selected [command] — the model never chooses the truth test.
*/
@Serializable
@SerialName("WorkspaceVerificationObserved")
data class WorkspaceVerificationObservedEvent(
val sessionId: SessionId,
val stageId: StageId,
val stateKey: String,
val expectation: String,
val command: String,
val passed: Boolean,
val changedPaths: List<String> = emptyList(),
) : EventPayload
@@ -65,6 +65,9 @@ import com.correx.core.events.events.RepoMapComputedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.RetrySalvageDecidedEvent
import com.correx.core.events.events.FailureTicketOpenedEvent
import com.correx.core.events.events.BuildPrerequisiteBootstrapAttemptedEvent
import com.correx.core.events.events.WorkspaceVerificationObservedEvent
import com.correx.core.events.events.PlanGroundingEvaluatedEvent
import com.correx.core.events.events.OutsidePathAccessGrantedEvent
import com.correx.core.events.events.WorkspaceStateObservedEvent
import com.correx.core.events.events.RiskAssessedEvent
@@ -158,6 +161,9 @@ val eventModule = SerializersModule {
subclass(RetryAttemptedEvent::class)
subclass(RetrySalvageDecidedEvent::class)
subclass(FailureTicketOpenedEvent::class)
subclass(BuildPrerequisiteBootstrapAttemptedEvent::class)
subclass(WorkspaceVerificationObservedEvent::class)
subclass(PlanGroundingEvaluatedEvent::class)
subclass(OutsidePathAccessGrantedEvent::class)
subclass(RefinementIterationEvent::class)
subclass(RepoMapComputedEvent::class)
@@ -1,6 +1,7 @@
package com.correx.core.kernel.concept
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
@@ -11,28 +12,59 @@ import com.correx.core.sessions.projections.Projection
/** Default validated-fix count a fingerprint must reach before promotion (design §"Promotion rule"). */
const val DEFAULT_PROMOTION_THRESHOLD = 3
/** A recurring failure signature and its resolution tally, keyed by [RetryAttemptedEvent.fingerprint]. */
/**
* A recurring failure and its resolution tally, keyed by [classKey] (design 2026-07-12
* §"Promote to a class key"). The class key is `gate + normalized(signature)`, so near-identical
* failures that differ only in volatile tokens (paths, line numbers, hashes) aggregate into one
* reusable concept instead of a fresh cluster per exact fingerprint. The exact [fingerprint] is
* retained for the last-seen instance (retrieval-friendly, and the L3 instance-recall id).
*/
data class ConceptCluster(
val classKey: String,
val fingerprint: String,
val gate: String,
// First line of the recurring failure reason — the retrieval-friendly signature.
val signature: String,
// Count of times a stage retrying this fingerprint went on to complete = validated failure→fix.
// Count of times a stage retrying this class went on to complete = validated failure→fix.
val validatedFixes: Int = 0,
// Any StageFailed / WorkflowFailed observed while this fingerprint was open: the fix did NOT hold.
// Any StageFailed / WorkflowFailed observed while this class was open: the fix did NOT hold.
val contradicted: Boolean = false,
// The validated fix: CAS path + post-image hash of the last file the resolving stage wrote before
// completing. Null until a fix→complete pair carries a file write. First captured ref wins.
val fixPath: String? = null,
val fixHash: String? = null,
)
/** A fingerprint currently being fought by a stage — resolved by the next StageCompleted/Failed. */
data class OpenFailure(val fingerprint: String, val gate: String, val signature: String)
/** A failure currently being fought by a stage — resolved by the next StageCompleted/Failed. */
data class OpenFailure(val classKey: String, val fingerprint: String, val gate: String, val signature: String)
/**
* Normalize a failure signature into a reusable class dimension: lowercase, collapse digit runs and
* quoted/pathish tokens to placeholders so `src/App.tsx:12` and `src/Nav.tsx:88` map to one class.
* Deterministic (replay-safe) — pure string transform, no environment read.
*/
fun conceptClassKey(gate: String, signature: String): String {
val normalized = signature.lowercase()
.replace(Regex("""['"`][^'"`]*['"`]"""), "<str>")
.replace(Regex("""\b[\w./\\-]+\.[a-z0-9]{1,5}\b"""), "<path>")
.replace(Regex("""\d+"""), "#")
.replace(Regex("""\s+"""), " ")
.trim()
return "$gate:$normalized"
}
data class ConceptCompilerState(
// Keyed by classKey (design §"Promote to a class key").
val clusters: Map<String, ConceptCluster> = emptyMap(),
// fingerprints already emitted as ConceptPromotedEvent — folded back so replay never re-promotes.
// classKeys already emitted as ConceptPromotedEvent — folded back so replay never re-promotes.
val promoted: Set<String> = emptySet(),
// Per-stage open failure the stage is retrying; keyed by "sessionId/stageId" so concurrent sessions
// in one cross-session fold don't collide.
val open: Map<String, OpenFailure> = emptyMap(),
// Last file write observed per session — the candidate validated-fix ref attached when the session's
// open failure resolves as fixed. Keyed by sessionId; execution is sequential so one stage writes at
// a time. Cleared on resolve so a later stage's writes don't back-attribute to an earlier fix.
val lastWrite: Map<String, Pair<String, String>> = emptyMap(),
) {
/**
* Clusters that have earned promotion but haven't been emitted yet: validated ≥ [threshold], never
@@ -40,7 +72,7 @@ data class ConceptCompilerState(
*/
fun promotable(threshold: Int = DEFAULT_PROMOTION_THRESHOLD): List<ConceptCluster> =
clusters.values.filter {
it.fingerprint !in promoted && !it.contradicted && it.validatedFixes >= threshold
it.classKey !in promoted && !it.contradicted && it.validatedFixes >= threshold
}
}
@@ -62,27 +94,42 @@ class ConceptCompilerProjection : Projection<ConceptCompilerState> {
is RetryAttemptedEvent -> {
val key = "${p.sessionId.value}/${p.stageId.value}"
val sig = p.failureReason.lineSequence().firstOrNull()?.take(SIGNATURE_MAX)?.trim().orEmpty()
state.copy(open = state.open + (key to OpenFailure(p.fingerprint, p.gate, sig)))
val classKey = conceptClassKey(p.gate, sig)
state.copy(open = state.open + (key to OpenFailure(classKey, p.fingerprint, p.gate, sig)))
}
is FileWrittenEvent -> p.postImageHash?.let {
state.copy(lastWrite = state.lastWrite + (p.sessionId.value to (p.path to it)))
} ?: state
is StageCompletedEvent -> resolve(state, "${p.sessionId.value}/${p.stageId.value}", fixed = true)
is StageFailedEvent -> resolve(state, "${p.sessionId.value}/${p.stageId.value}", fixed = false)
is WorkflowFailedEvent -> failAllInSession(state, p.sessionId.value)
is ConceptPromotedEvent -> state.copy(promoted = state.promoted + p.fingerprint)
is ConceptPromotedEvent -> state.copy(promoted = state.promoted + p.classKey)
else -> state
}
private fun resolve(state: ConceptCompilerState, key: String, fixed: Boolean): ConceptCompilerState {
val failure = state.open[key] ?: return state
val existing = state.clusters[failure.fingerprint]
?: ConceptCluster(failure.fingerprint, failure.gate, failure.signature)
val sessionId = key.substringBefore('/')
val existing = state.clusters[failure.classKey]
?: ConceptCluster(failure.classKey, failure.fingerprint, failure.gate, failure.signature)
val updated = if (fixed) {
existing.copy(validatedFixes = existing.validatedFixes + 1)
val fix = state.lastWrite[sessionId]
existing.copy(
// Keep the latest instance's fingerprint/signature for recall.
fingerprint = failure.fingerprint,
signature = failure.signature,
validatedFixes = existing.validatedFixes + 1,
// First captured ref wins — keep the earliest validated fix as the canonical resolution.
fixPath = existing.fixPath ?: fix?.first,
fixHash = existing.fixHash ?: fix?.second,
)
} else {
existing.copy(contradicted = true)
}
return state.copy(
clusters = state.clusters + (failure.fingerprint to updated),
clusters = state.clusters + (failure.classKey to updated),
open = state.open - key,
lastWrite = state.lastWrite - sessionId,
)
}
@@ -65,11 +65,15 @@ internal val EVIDENCE_PATH_RE = Regex("""[\w./@-]+\.[A-Za-z0-9]+""")
// Deterministic gate id → capability the failing stage must hold to change the failure condition.
// A gate not listed here is not eligible for recovery routing (retries in place as before).
// Gate id for a repeated missing build prerequisite (design #163/#170). Its own separate handling
// (bounded bootstrap turn / recovery routing) is triggered off this id before the normal retry path.
internal const val WORKSPACE_PRECONDITION_GATE = "workspace_precondition"
internal val GATE_REQUIRED_CAPABILITY: Map<String, String> = mapOf(
"execution" to "file_write",
"contract" to "file_write",
"static_analysis" to "file_write",
"workspace_precondition" to "file_write",
WORKSPACE_PRECONDITION_GATE to "file_write",
)
internal const val CURATED_STAGE_OPERATING_GUIDANCE = """## Operating guidance
@@ -254,6 +254,16 @@ internal suspend fun DefaultSessionOrchestrator.enterStage(
return StepResult.Continue(ctx.copy(stageCount = ctx.stageCount + 1))
}
val refreshedState = orchestrationRepository.getState(ctx.sessionId)
// A repeated missing-build-prerequisite block is not an ordinary retry: it takes a
// bounded, separately-budgeted precondition-resolution path (design #170) that never
// charges the stage retry counter. FallThrough = defer to the normal recovery routing.
if (result.gate == WORKSPACE_PRECONDITION_GATE) {
when (val outcome = buildPrerequisiteDecision(ctx, stageId, result, refreshedState)) {
PrerequisiteOutcome.Bootstrap -> continue // re-run stage, no retry charged
PrerequisiteOutcome.FallThrough -> Unit // non-writable → recovery routing below
is PrerequisiteOutcome.Done -> return outcome.result
}
}
// Retry-agency invariant: a gate this stage lacks the capability to fix must not
// be retried in place (futile). Route to a recovery stage that holds the
// capability, if one exists and the route budget remains.
@@ -0,0 +1,69 @@
package com.correx.core.kernel.orchestration
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.types.ContextEntryId
import com.correx.core.kernel.concept.ConceptCompilerProjection
import com.correx.core.transitions.graph.StageConfig
import java.util.UUID
/**
* Deterministic delivery of promoted capability-accretion concepts into a stage's context
* (design 2026-07-12-acr-concept-compiler.md §"Delivery" — replaces the L3 top-k lottery as the
* PRIMARY channel; L3 remains an instance-recall fallback). A [ConceptPromotedEvent] is the
* authoritative record of a validated failure→fix class; injecting it as an L0/system block tells a
* stage about a known resolution BEFORE it re-derives the same fix.
*
* Relevance filter (avoids the broadcast the design's verdict #1 rejected): only inject a concept a
* stage could actually act on — a concept whose gate requires a capability ([GATE_REQUIRED_CAPABILITY])
* the stage does not hold is skipped (no point telling a write-less verifier about a file_write fix).
* Gates with no required capability (lint/review) are broadly relevant. Bounded to [limit] by
* occurrences so context stays small. Pure over the promoted set → same input, same injection.
*/
internal fun selectPromotedConcepts(
promoted: List<ConceptPromotedEvent>,
allowedTools: Set<String>,
contradicted: Set<String> = emptySet(),
limit: Int = MAX_PROMOTED_CONCEPTS,
): List<ConceptPromotedEvent> =
promoted
// Honor a contradiction observed AFTER promotion (design 2026-07-12 §"Decay/contradiction"):
// a concept whose class later failed to hold must not keep being delivered as a known fix.
.filter { it.classKey.ifBlank { it.fingerprint } !in contradicted }
.filter { concept -> GATE_REQUIRED_CAPABILITY[concept.gate]?.let { it in allowedTools } ?: true }
.distinctBy { it.classKey.ifBlank { it.fingerprint } }
.sortedByDescending { it.occurrences }
.take(limit)
/** L0/system context entries for the concepts relevant to [stageConfig], read from the whole log. */
internal suspend fun SessionOrchestrator.promotedConceptEntries(stageConfig: StageConfig): List<ContextEntry> {
val events = eventStore.allEvents().toList()
val promoted = events.mapNotNull { it.payload as? ConceptPromotedEvent }
if (promoted.isEmpty()) return emptyList()
// Fold the same projection the compiler uses to find classes contradicted since promotion.
val projection = ConceptCompilerProjection()
val state = events.fold(projection.initial()) { s, e -> projection.apply(s, e) }
val contradicted = state.clusters.values.filter { it.contradicted }.map { it.classKey }.toSet()
return selectPromotedConcepts(promoted, stageConfig.effectiveAllowedTools, contradicted).map { concept ->
// Deliver the FIX, not just a nag (design §"carry the fix"): when the accreted concept carries a
// validated-fix ref, point the stage at the file that resolved this class before, so it applies a
// known resolution instead of re-deriving one.
val fixPointer = concept.fixPath?.let { " A prior validated fix modified: $it — mirror that resolution." }
?: ""
val content = "Known ${concept.gate} failure class (accreted from prior validated runs). " +
concept.conceptText + fixPointer
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = content,
sourceType = "promotedConcept",
sourceId = concept.classKey.ifBlank { concept.fingerprint },
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
)
}
}
private const val MAX_PROMOTED_CONCEPTS = 3
@@ -81,7 +81,10 @@ internal suspend fun SessionOrchestrator.executeStage(
),
)
} ?: emptyList()
val operatingGuidance = listOf(
// ponytail: env toggle so the prompt-audition harness (scripts/prompt-audition.sh, task #169) can
// A/B the curated guidance block against a trimmed baseline without a rebuild. CORREX_STAGE_GUIDANCE=off
// drops it; anything else (default) keeps it. Promote to a real config knob only if this outlives the audit.
val operatingGuidance = if (System.getenv("CORREX_STAGE_GUIDANCE") == "off") emptyList() else listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
@@ -92,6 +95,16 @@ internal suspend fun SessionOrchestrator.executeStage(
role = EntryRole.SYSTEM,
),
)
// Capability accretion (design 2026-07-12-acr-concept-compiler.md): deterministically inject the
// validated failure→fix concepts this stage could act on, so a known resolution is delivered
// BEFORE the stage re-derives it. Primary channel; L3 remains the instance-recall fallback.
val promotedConcepts = promotedConceptEntries(stageConfig)
// Positive-pattern accretion (design 2026-07-12 §"broaden beyond failure→fix"): show the architect a
// prior successful plan shape for a similar intent so it plans from a validated decomposition, not zero.
val successfulPlanShapes = successfulPlanShapeEntries(sessionId, stageConfig)
// Known-good workspace invariant (design 2026-07-15 seam 2): tell the stage whether the last
// green build is still valid for the current workspace state, or stale and needing re-verification.
val verifiedBaseline = verifiedBaselineEntries(sessionId)
// A stage re-entered to repair a gate failure has its own (often generative/"scaffold") prompt
// SUPPRESSED: that mandate is what drove it to overwrite real files with stubs on re-entry. The
// recovery-ticket entry (buildRecoveryTicketEntry) is the sole mandate here — it already carries
@@ -250,7 +263,7 @@ internal suspend fun SessionOrchestrator.executeStage(
?.let { buildRemainingDeltaEntry(contractFailureItems(it)) }
?.let { listOf(it) } ?: emptyList()
var accumulatedEntries = stampBuckets(
systemPrompt + operatingGuidance + intentEntries + profileEntries + projectProfileEntries + agentInstructionsEntries +
systemPrompt + operatingGuidance + promotedConcepts + successfulPlanShapes + verifiedBaseline + intentEntries + profileEntries + projectProfileEntries + agentInstructionsEntries +
journalEntries + repoMapEntries + claimedTaskEntries +
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries +
clarificationEntries + retryFeedbackEntries + recoveryTicketEntries + remainingDeltaEntries,
@@ -402,7 +415,7 @@ internal suspend fun SessionOrchestrator.executeStage(
approvalModeFor(session.state.boundProfile?.approvalMode),
inferenceResult.response.reasoning)
repeatedBuildCriticalReferenceBlock(sessionId, stageId)?.let { reason ->
return StageExecutionResult.Failure(reason, retryable = true, gate = "workspace_precondition")
return StageExecutionResult.Failure(reason, retryable = true, gate = WORKSPACE_PRECONDITION_GATE)
}
val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") }
if (fatalEntry != null) {
@@ -112,7 +112,12 @@ internal suspend fun SessionOrchestrator.runExecutionGate(
val run = runner.run(workspaceRoot, command)
val finding = StaticAnalysisFinding(command, run.exitCode, run.output.takeLast(STATIC_ANALYSIS_SUMMARY_CAP))
emit(sessionId, StaticAnalysisCompletedEvent(sessionId, stageId, listOf(finding)))
if (finding.clean) return StageExecutionResult.Success(emptyList())
if (finding.clean) {
// Known-good workspace invariant (design 2026-07-15 seam 2): bind this green result to the
// workspace state key so the next stage can trust it only while the workspace is unchanged.
recordWorkspaceVerification(sessionId, stageId, expectation, command, passed = true)
return StageExecutionResult.Success(emptyList())
}
log.warn(
"[Orchestrator] execution gate failed session={} stage={} expectation={} command={}",
@@ -0,0 +1,108 @@
@file:Suppress("MatchingDeclarationName") // SessionOrchestrator* extension group, not a single-class file.
package com.correx.core.kernel.orchestration
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.SessionId
import com.correx.core.transitions.graph.StageConfig
import java.util.UUID
/**
* Positive-pattern accretion (design 2026-07-12-acr-concept-compiler.md §"Broaden beyond failure→fix"):
* the failure→fix rail learns from what BROKE; this learns from what WORKED. When the architect is about
* to plan, it is shown the plan SHAPE (stage-id sequence) of a prior run whose intent resembled this
* one and that actually completed — so run N+1 starts from a decomposition already validated by run N,
* instead of re-deriving structure from zero every time.
*
* Fully deterministic (invariants #8/#9): a pure fold over recorded events (initial intent, locked plan,
* workflow completion) across all sessions. Intent resemblance is a keyword Jaccard overlap — no LLM, no
* embedder — so it replays identically. Advisory only: it injects a reference shape as SYSTEM context;
* the architect still authors the plan (invariant #3 LLM-proposes/core-decides).
*/
/** A completed run reduced to what a future planner can reuse: its intent keywords and plan shape. */
internal data class SuccessfulPlanShape(
val sessionId: String,
val intentKeywords: Set<String>,
val stageSequence: List<String>,
)
/** Stopword-trimmed lowercase word set of an intent — the deterministic resemblance key. */
internal fun intentKeywords(intent: String): Set<String> =
intent.lowercase()
.split(Regex("""[^a-z0-9]+"""))
.filter { it.length > 2 && it !in INTENT_STOPWORDS }
.toSet()
/** Jaccard overlap of two keyword sets — 0.0 when either is empty. */
internal fun keywordOverlap(a: Set<String>, b: Set<String>): Double {
if (a.isEmpty() || b.isEmpty()) return 0.0
val inter = a.intersect(b).size.toDouble()
return inter / a.union(b).size
}
/**
* Mine successful plan shapes from [events] (cross-session log). A session qualifies when its locked
* plan's workflow later emits a [WorkflowCompletedEvent] for that SAME workflow id — i.e. the executed
* plan (not just the planning phase) reached completion. [exclude] drops the current session so a run
* never learns from itself.
*/
internal fun mineSuccessfulPlanShapes(events: List<StoredEvent>, exclude: String): List<SuccessfulPlanShape> {
val intents = events.mapNotNull { it.payload as? InitialIntentEvent }.associate { it.sessionId.value to it.intent }
val locked = events.mapNotNull { it.payload as? ExecutionPlanLockedEvent }
val completedWorkflows = events.mapNotNull { it.payload as? WorkflowCompletedEvent }
.map { it.sessionId.value to it.workflowId }.toSet()
return locked.mapNotNull { plan ->
val sid = plan.sessionId.value
val intent = intents[sid] ?: return@mapNotNull null
// The executed plan completed only if a completion for its OWN workflow id was recorded.
if (sid == exclude || (sid to plan.workflowId) !in completedWorkflows) return@mapNotNull null
SuccessfulPlanShape(sid, intentKeywords(intent), plan.stageIds)
}
}
/**
* L0/SYSTEM entry naming the closest matching prior successful plan shape — but only for the PLANNING
* stage (the one producing an `execution_plan`), and only when resemblance clears [MIN_INTENT_OVERLAP]
* so an unrelated run's shape is not mistaken for guidance.
*/
internal suspend fun SessionOrchestrator.successfulPlanShapeEntries(
sessionId: SessionId,
stageConfig: StageConfig,
): List<ContextEntry> {
val isPlanningStage = stageConfig.produces.any { it.kind.id == "execution_plan" }
if (!isPlanningStage) return emptyList()
val here = initialIntent(sessionId)?.let(::intentKeywords).orEmpty()
if (here.isEmpty()) return emptyList()
val best = mineSuccessfulPlanShapes(eventStore.allEvents().toList(), exclude = sessionId.value)
.map { it to keywordOverlap(here, it.intentKeywords) }
.filter { it.second >= MIN_INTENT_OVERLAP }
.maxByOrNull { it.second } ?: return emptyList()
val content = "## A plan shape that worked before\nA prior run with a similar goal completed " +
"successfully using this stage sequence — reuse its structure where it fits, adapt where the " +
"goal differs:\n${best.first.stageSequence.joinToString(" → ")}"
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = content,
sourceType = "successfulPlanShape",
sourceId = best.first.sessionId,
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
),
)
}
private const val MIN_INTENT_OVERLAP = 0.34
private val INTENT_STOPWORDS = setOf(
"the", "and", "for", "with", "that", "this", "add", "make", "use", "using", "into", "from", "all",
"new", "app", "should", "must", "will", "can", "its", "was", "are", "you", "your", "then",
)
@@ -1,26 +1,52 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.BuildPrerequisiteBootstrapAttemptedEvent
import com.correx.core.events.events.ClarificationQuestion
import com.correx.core.events.events.ClarificationRequestedEvent
import com.correx.core.events.events.ClarificationAnswer
import com.correx.core.events.events.ClarificationAnsweredEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.ToolCallAssessedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.risk.RiskAction
import com.correx.core.events.types.ClarificationRequestId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.StageConfig
import kotlinx.coroutines.CompletableDeferred
import java.nio.file.FileSystems
import java.nio.file.Path
import java.util.UUID
/**
* Repeated attempts to read a missing build prerequisite are no longer mere ReAct noise. Once the
* same build-critical path has been durably blocked three times in one stage, return a retryable
* gate failure so the normal recovery ladder can bootstrap it (or request a different owner).
* same build-critical path has been durably BLOCK'd three times *within the current bootstrap
* window* (design 2026-07-15 §#163/#170), return a retryable gate failure so the orchestrator can
* grant a bounded precondition-resolution turn (or route to a stage that can create the file).
*
* The window is scoped to events after the last [BuildPrerequisiteBootstrapAttemptedEvent] for this
* stage: without that, the historical blocks that triggered the FIRST detection would re-trip the
* gate on the very first round of the granted bootstrap turn, before the model ever gets to write
* the file. Scoping resets the count so the bootstrap turn starts clean and only *new* blocks
* (the model failed to create it) escalate.
*/
internal fun SessionOrchestrator.repeatedBuildCriticalReferenceBlock(
sessionId: SessionId,
stageId: StageId,
): String? {
val events = eventStore.read(sessionId)
val requests = events
val windowStart = events
.filter { (it.payload as? BuildPrerequisiteBootstrapAttemptedEvent)?.stageId == stageId }
.maxOfOrNull { it.sequence } ?: Long.MIN_VALUE
val windowed = events.filter { it.sequence > windowStart }
val requests = windowed
.mapNotNull { it.payload as? ToolInvocationRequestedEvent }
.filter { it.stageId == stageId }
.associateBy { it.invocationId }
val paths = events
val paths = windowed
.mapNotNull { it.payload as? ToolCallAssessedEvent }
.filter { it.stageId == stageId && it.disposition == RiskAction.BLOCK }
.filter { event -> event.issues.any { it.code == REFERENCE_EXISTS_CODE } }
@@ -33,11 +59,176 @@ internal fun SessionOrchestrator.repeatedBuildCriticalReferenceBlock(
"(${repeated.value} blocked attempts). Create or repair the project setup before continuing."
}
/**
* Which bounded precondition-resolution path a repeated build-prerequisite block takes (design #170).
* Pure over the three observable facts — deterministic/replay-safe, unit-tested in isolation.
*/
internal enum class PrerequisiteAction { BOOTSTRAP, ROUTE_TO_RECOVERY, CLARIFY, ESCALATE }
/**
* - not writable → the stage can't create the file; route to a stage that can ([ROUTE_TO_RECOVERY]).
* - writable but the path is outside the stage's declared scope → don't guess who owns project
* setup, ask ([CLARIFY]).
* - writable, in-scope, bootstrap budget left → grant the one dedicated turn ([BOOTSTRAP]).
* - writable, in-scope, budget spent → the turn didn't clear it; escalate to recovery ([ESCALATE]).
*/
internal fun decidePrerequisiteAction(
writable: Boolean,
inScope: Boolean,
bootstrapBudgetLeft: Boolean,
): PrerequisiteAction = when {
!writable -> PrerequisiteAction.ROUTE_TO_RECOVERY
!inScope -> PrerequisiteAction.CLARIFY
bootstrapBudgetLeft -> PrerequisiteAction.BOOTSTRAP
else -> PrerequisiteAction.ESCALATE
}
/** Extract the quoted build-prerequisite path from a [repeatedBuildCriticalReferenceBlock] reason. */
internal fun buildPrerequisitePath(reason: String): String? =
PREREQUISITE_PATH_RE.find(reason)?.groupValues?.get(1)
/**
* Is [path] within this stage's declared territory? A stage with no declared scope (empty [touches]
* and [writeManifest]) is unconstrained → owns everything → in-scope. Otherwise the path must match a
* declared glob or be a concretely-declared file. Reused glob semantics from ManifestContainmentRule.
*/
internal fun pathInDeclaredScope(stage: StageConfig, path: String): Boolean {
val globs = stage.touches + stage.writeManifest
return globs.isEmpty() || // unconstrained stage owns everything
path in stage.expectedFiles ||
globs.any { FileSystems.getDefault().getPathMatcher("glob:$it").matches(Path.of(path)) }
}
/** How many bootstrap turns this stage has already been granted (the separate one-shot budget). */
internal fun bootstrapAttemptCount(events: List<StoredEvent>, stageId: StageId): Int =
events.count { (it.payload as? BuildPrerequisiteBootstrapAttemptedEvent)?.stageId == stageId }
/**
* What the step loop should do after a repeated build-prerequisite block on a writable-or-not stage.
* [FallThrough] = not our special case (non-writable) — let the normal recovery routing handle it;
* [Bootstrap] = re-run the stage this once (loop), the separate budget already charged;
* [Done] = a terminal/routed [StepResult] to return.
*/
internal sealed interface PrerequisiteOutcome {
data object Bootstrap : PrerequisiteOutcome
data object FallThrough : PrerequisiteOutcome
data class Done(val result: StepResult) : PrerequisiteOutcome
}
/**
* Decide and enact the bounded precondition-resolution path for a `workspace_precondition` failure,
* WITHOUT touching the normal stage retry budget (design #170). Side effects (recording the bootstrap
* grant, requesting clarification, routing to recovery) are performed here; the returned outcome tells
* the step loop whether to loop, fall through, or return.
*/
internal suspend fun DefaultSessionOrchestrator.buildPrerequisiteDecision(
ctx: EnrichedExecutionContext,
stageId: StageId,
failure: com.correx.core.transitions.execution.StageExecutionResult.Failure,
state: OrchestrationState,
): PrerequisiteOutcome {
val stage = ctx.graph.stages[stageId]
val path = buildPrerequisitePath(failure.reason)
if (stage == null || path == null) return PrerequisiteOutcome.FallThrough
val events = repositories.eventStore.read(ctx.sessionId)
val action = decidePrerequisiteAction(
writable = "file_write" in stage.allowedTools,
inScope = pathInDeclaredScope(stage, path),
bootstrapBudgetLeft = bootstrapAttemptCount(events, stageId) < BOOTSTRAP_BUDGET,
)
return when (action) {
PrerequisiteAction.ROUTE_TO_RECOVERY -> PrerequisiteOutcome.FallThrough
PrerequisiteAction.BOOTSTRAP -> {
emit(
ctx.sessionId,
BuildPrerequisiteBootstrapAttemptedEvent(ctx.sessionId, stageId, path, failure.reason),
)
log.info(
"[Orchestrator] granting build-prerequisite bootstrap turn session={} stage={} path={}",
ctx.sessionId.value, stageId.value, path,
)
PrerequisiteOutcome.Bootstrap
}
PrerequisiteAction.CLARIFY -> requestBuildPrerequisiteClarification(ctx, stageId, path)
PrerequisiteAction.ESCALATE -> PrerequisiteOutcome.Done(
routeToRecovery(ctx, stageId, failure.gate, "file_write", failure.reason, state)
?: StepResult.Terminal(
failWorkflow(
ctx.sessionId,
stageId,
"build prerequisite '$path' unresolved after bootstrap: ${failure.reason}",
retryExhausted = true,
),
),
)
}
}
/**
* The missing prerequisite lies outside the writable stage's declared scope: ownership of project
* setup is ambiguous, so record a clarification request and pause rather than guess (design #170
* point 5). After the operator answers, loop to re-run the stage with the answer in context. Bounded
* by [tuning].maxClarificationRounds; a spent budget fails the workflow rather than looping.
*/
private suspend fun DefaultSessionOrchestrator.requestBuildPrerequisiteClarification(
ctx: EnrichedExecutionContext,
stageId: StageId,
path: String,
): PrerequisiteOutcome {
val priorRounds = repositories.eventStore.read(ctx.sessionId)
.count { (it.payload as? ClarificationRequestedEvent)?.stageId == stageId }
if (priorRounds >= tuning.maxClarificationRounds) {
return PrerequisiteOutcome.Done(
StepResult.Terminal(
failWorkflow(
ctx.sessionId,
stageId,
"build prerequisite '$path' is outside stage ${stageId.value}'s declared scope " +
"and clarification budget is spent",
retryExhausted = true,
),
),
)
}
val requestId = ClarificationRequestId(UUID.randomUUID().toString())
val deferred = CompletableDeferred<List<ClarificationAnswer>>()
pendingClarifications[requestId] = deferred
emit(ctx.sessionId, OrchestrationPausedEvent(ctx.sessionId, stageId, "CLARIFICATION_PENDING"))
emit(
ctx.sessionId,
ClarificationRequestedEvent(
requestId = requestId,
sessionId = ctx.sessionId,
stageId = stageId,
questions = listOf(
ClarificationQuestion(
id = "build_prerequisite",
prompt = "Stage ${stageId.value} is blocked on a missing build prerequisite " +
"'$path' that is outside its declared scope. Should this stage create it, or " +
"does another stage own project setup?",
header = "Missing setup",
),
),
),
)
return try {
val answers = deferred.await()
emit(ctx.sessionId, ClarificationAnsweredEvent(requestId, ctx.sessionId, stageId, answers))
emit(ctx.sessionId, OrchestrationResumedEvent(ctx.sessionId, stageId))
PrerequisiteOutcome.Bootstrap
} finally {
pendingClarifications.remove(requestId)
}
}
private fun isBuildCriticalPath(path: String): Boolean {
val name = path.substringAfterLast('/').lowercase()
return name in BUILD_PREREQUISITE_FILENAMES
}
// At most one dedicated bootstrap turn per stage (design #170 acceptance: "at most one").
private const val BOOTSTRAP_BUDGET = 1
private val PREREQUISITE_PATH_RE = Regex("""missing build prerequisite '([^']+)'""")
private const val REFERENCE_EXISTS_REPEAT_LIMIT = 3
private val BUILD_PREREQUISITE_FILENAMES = setOf(
"package.json", "tsconfig.json", "vite.config.ts", "build.gradle", "build.gradle.kts", "pom.xml",
@@ -0,0 +1,92 @@
package com.correx.core.kernel.orchestration
import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkspaceVerificationObservedEvent
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.BuildExpectation
import java.util.UUID
/**
* Replay-safe workspace state key (design 2026-07-15 seam 2). The workspace's verified identity is
* the folded FileWritten manifest — each path mapped to its latest recorded post-image hash. Hashing
* the sorted map yields a key that changes iff a file changed, so a verification observation stamped
* with this key is invalidated automatically by any later write. Derived purely from recorded events
* (never rescans the filesystem), so replay reproduces the same key (invariants #8/#9).
*/
internal fun workspaceStateKey(events: List<StoredEvent>): String {
val latest = linkedMapOf<String, String>()
events.mapNotNull { it.payload as? FileWrittenEvent }
.forEach { fw -> latest[fw.path] = fw.postImageHash ?: "" }
val canonical = latest.entries.sortedBy { it.key }.joinToString("\n") { "${it.key}=${it.value}" }
return Integer.toHexString(canonical.hashCode())
}
/**
* Record the known-good verification observation for a passing build gate, binding the result to the
* current [workspaceStateKey]. Called from the execution gate on a clean run so the next stage can
* tell a still-valid baseline from a stale one. Emits nothing for [BuildExpectation.NONE] (no build
* claim manufactured — spec: a docs-only/no-write stage does not fabricate a compiler assertion).
*/
internal suspend fun SessionOrchestrator.recordWorkspaceVerification(
sessionId: SessionId,
stageId: StageId,
expectation: BuildExpectation,
command: String,
passed: Boolean,
) {
val events = eventStore.read(sessionId)
emit(
sessionId,
WorkspaceVerificationObservedEvent(
sessionId = sessionId,
stageId = stageId,
stateKey = workspaceStateKey(events),
expectation = expectation.name,
command = command,
passed = passed,
changedPaths = stageWrittenPaths(sessionId, stageId),
),
)
}
/**
* The `verified baseline` L0 entry for the next implementation stage (design 2026-07-15 seam 2). If
* the latest passing [WorkspaceVerificationObservedEvent]'s state key still equals the CURRENT state
* key, the workspace is known-good and the entry states the verified command + changed paths. If the
* workspace changed since (key mismatch), the entry says the baseline is stale and the stage must
* re-verify — it never claims a skipped compiler assertion passed. Empty when nothing has verified yet.
*/
internal suspend fun SessionOrchestrator.verifiedBaselineEntries(sessionId: SessionId): List<ContextEntry> {
val events = eventStore.read(sessionId)
val lastPass = events.mapNotNull { it.payload as? WorkspaceVerificationObservedEvent }
.lastOrNull { it.passed } ?: return emptyList()
val current = workspaceStateKey(events)
val fresh = current == lastPass.stateKey
val content = if (fresh) {
"## Verified baseline (known-good)\nThe workspace passed `${lastPass.command}` " +
"(${lastPass.expectation}) at the current state. Build/typecheck is green as of these paths: " +
"${lastPass.changedPaths.joinToString(", ").ifBlank { "(no declared write paths)" }}. " +
"Build on it; do not re-derive what already compiles."
} else {
"## Verified baseline (STALE)\nThe last green verification (`${lastPass.command}`) was for an " +
"earlier workspace state; files changed since, so it is no longer authoritative. Re-run the " +
"build gate before treating the workspace as known-good."
}
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
content = content,
sourceType = "verifiedBaseline",
sourceId = lastPass.stateKey,
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
),
)
}
@@ -3,6 +3,7 @@ package com.correx.core.kernel.concept
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
@@ -11,6 +12,7 @@ import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.ToolInvocationId
import com.correx.core.events.types.TransitionId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
@@ -56,13 +58,29 @@ class ConceptCompilerProjectionTest {
private fun failed(stage: String = "impl", session: String = "s1") =
stored(StageFailedEvent(SessionId(session), StageId(stage), TransitionId("t"), "x"), session)
private fun wrote(path: String, hash: String? = "cas1", session: String = "s1") =
stored(
FileWrittenEvent(
invocationId = ToolInvocationId("inv"),
sessionId = SessionId(session),
path = path,
postImageHash = hash,
preExisted = true,
timestampMs = 1L,
),
session,
)
private fun fold(events: List<StoredEvent>) =
events.fold(projection.initial(), projection::apply)
// All helpers default to reason "boom\ndetail" on gate "lint" → the single class key "lint:boom".
private val ConceptCompilerState.only get() = clusters.values.single()
@Test
fun `retry then StageCompleted counts one validated fix`() {
val state = fold(listOf(retry("fp1"), completed()))
assertEquals(1, state.clusters["fp1"]?.validatedFixes)
assertEquals(1, state.only.validatedFixes)
assertTrue(state.promotable().isEmpty())
}
@@ -75,12 +93,27 @@ class ConceptCompilerProjectionTest {
retry("fp1", session = "sc"), completed(session = "sc"),
),
)
assertEquals(3, state.clusters["fp1"]?.validatedFixes)
assertEquals(listOf("fp1"), state.promotable().map { it.fingerprint })
assertEquals(3, state.only.validatedFixes)
assertEquals(listOf("lint:boom"), state.promotable().map { it.classKey })
}
@Test
fun `StageFailed while fingerprint open contradicts the cluster`() {
fun `distinct fingerprints of the same failure class aggregate into one concept`() {
// Same normalized signature, different volatile tokens (paths/line numbers) → one class key.
val state = fold(
listOf(
retry("fpA", session = "sa", reason = "TS2322 in 'src/App.tsx:12'"), completed(session = "sa"),
retry("fpB", session = "sb", reason = "TS2322 in 'src/Nav.tsx:88'"), completed(session = "sb"),
retry("fpC", session = "sc", reason = "TS2322 in 'src/Home.tsx:5'"), completed(session = "sc"),
),
)
assertEquals(1, state.clusters.size)
assertEquals(3, state.only.validatedFixes)
assertEquals(listOf("lint:ts# in <str>"), state.promotable().map { it.classKey })
}
@Test
fun `StageFailed while class open contradicts the cluster`() {
val state = fold(
listOf(
retry("fp1", session = "sa"), completed(session = "sa"),
@@ -88,37 +121,56 @@ class ConceptCompilerProjectionTest {
retry("fp1", session = "sc"), failed(session = "sc"),
),
)
assertTrue(state.clusters["fp1"]!!.contradicted)
assertTrue(state.only.contradicted)
assertTrue(state.promotable().isEmpty())
}
@Test
fun `WorkflowFailed contradicts every open fingerprint in that session`() {
fun `WorkflowFailed contradicts every open class in that session`() {
val state = fold(
listOf(
retry("fp1", stage = "a", session = "sa"),
retry("fp2", stage = "b", session = "sa"),
retry("fp1", stage = "a", session = "sa", reason = "alpha broke"),
retry("fp2", stage = "b", session = "sa", reason = "beta broke"),
stored(WorkflowFailedEvent(SessionId("sa"), StageId("a"), "dead", true), "sa"),
),
)
assertTrue(state.clusters["fp1"]!!.contradicted)
assertTrue(state.clusters["fp2"]!!.contradicted)
assertEquals(2, state.clusters.size)
assertTrue(state.clusters.values.all { it.contradicted })
}
@Test
fun `promotion is idempotent - a promoted fingerprint is not promotable again`() {
fun `promotion is idempotent - a promoted class is not promotable again`() {
val base = listOf(
retry("fp1", session = "sa"), completed(session = "sa"),
retry("fp1", session = "sb"), completed(session = "sb"),
retry("fp1", session = "sc"), completed(session = "sc"),
)
val promoted = base + stored(ConceptPromotedEvent(SessionId("sc"), "fp1", "lint", "concept", 3))
val promoted = base + stored(ConceptPromotedEvent(SessionId("sc"), "fp1", "lint:boom", "lint", "concept", 3))
assertTrue(fold(promoted).promotable().isEmpty())
}
@Test
fun `a file written between retry and completion is captured as the validated fix ref`() {
val state = fold(listOf(retry("fp1"), wrote("src/App.tsx", "hashA"), completed()))
assertEquals("src/App.tsx", state.only.fixPath)
assertEquals("hashA", state.only.fixHash)
}
@Test
fun `first validated fix ref wins and a later stage's writes do not back-attribute`() {
val state = fold(
listOf(
retry("fp1", session = "sa"), wrote("first.kt", "h1", session = "sa"), completed(session = "sa"),
retry("fp1", session = "sb"), wrote("second.kt", "h2", session = "sb"), completed(session = "sb"),
),
)
assertEquals("first.kt", state.only.fixPath)
assertEquals("h1", state.only.fixHash)
}
@Test
fun `signature is the first line of the failure reason`() {
val state = fold(listOf(retry("fp1", reason = "detekt: MagicNumber\ntrace..."), completed()))
assertEquals("detekt: MagicNumber", state.clusters["fp1"]?.signature)
assertEquals("detekt: MagicNumber", state.only.signature)
}
}
@@ -0,0 +1,92 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.BuildPrerequisiteBootstrapAttemptedEvent
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.transitions.graph.StageConfig
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class BuildPrerequisiteDecisionTest {
private val reason =
"stage impl repeatedly referenced missing build prerequisite 'frontend/package.json' " +
"(3 blocked attempts). Create or repair the project setup before continuing."
@Test
fun `decision maps the three facts to one bounded path`() {
// not writable → route to a stage that can create the file.
assertEquals(
PrerequisiteAction.ROUTE_TO_RECOVERY,
decidePrerequisiteAction(writable = false, inScope = true, bootstrapBudgetLeft = true),
)
// writable but out of scope → ask, don't guess.
assertEquals(
PrerequisiteAction.CLARIFY,
decidePrerequisiteAction(writable = true, inScope = false, bootstrapBudgetLeft = true),
)
// writable, in scope, budget → grant the one bootstrap turn.
assertEquals(
PrerequisiteAction.BOOTSTRAP,
decidePrerequisiteAction(writable = true, inScope = true, bootstrapBudgetLeft = true),
)
// writable, in scope, budget spent → escalate (the one turn didn't clear it).
assertEquals(
PrerequisiteAction.ESCALATE,
decidePrerequisiteAction(writable = true, inScope = true, bootstrapBudgetLeft = false),
)
}
@Test
fun `path is extracted from the block reason`() {
assertEquals("frontend/package.json", buildPrerequisitePath(reason))
assertNull(buildPrerequisitePath("some unrelated failure"))
}
@Test
fun `an unconstrained stage owns everything but a scoped stage only its territory`() {
val unconstrained = StageConfig()
assertTrue(pathInDeclaredScope(unconstrained, "frontend/package.json"))
val scoped = StageConfig(touches = listOf("frontend/**"))
assertTrue(pathInDeclaredScope(scoped, "frontend/package.json"))
assertFalse(pathInDeclaredScope(scoped, "backend/package.json"))
val byExpected = StageConfig(touches = listOf("src/**"), expectedFiles = listOf("package.json"))
assertTrue(pathInDeclaredScope(byExpected, "package.json"))
}
@Test
fun `bootstrap budget counts prior grants for this stage only`() {
val stage = StageId("impl")
assertEquals(0, bootstrapAttemptCount(emptyList(), stage))
val events = listOf(
bootstrap(stage, "package.json"),
bootstrap(StageId("other"), "package.json"),
)
assertEquals(1, bootstrapAttemptCount(events, stage))
}
private var seq = 0L
private fun bootstrap(stageId: StageId, path: String) = StoredEvent(
metadata = EventMetadata(
eventId = EventId("e${seq++}"),
sessionId = SessionId("s1"),
timestamp = Instant.parse("2026-01-01T00:00:00Z"),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
sequence = seq,
sessionSequence = seq,
payload = BuildPrerequisiteBootstrapAttemptedEvent(SessionId("s1"), stageId, path, "why") as EventPayload,
)
}
@@ -0,0 +1,72 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.ExecutionPlanLockedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class PlanPatternMiningTest {
@Test
fun `only a session whose executed plan completed is mined`() {
val events =
intent("s1", "build a react frontend that compiles") +
locked("s1", "wf-s1", listOf("scaffold", "impl")) +
completed("s1", "wf-s1") + // executed plan completed → mined
intent("s2", "build a react frontend that compiles") +
locked("s2", "wf-s2", listOf("a", "b")) // no completion → not mined
val shapes = mineSuccessfulPlanShapes(events, exclude = "current")
assertEquals(listOf("scaffold", "impl"), shapes.single().stageSequence)
}
@Test
fun `the current session is never mined from itself`() {
val events = intent("me", "x y z frontend") + locked("me", "wf", listOf("a")) + completed("me", "wf")
assertTrue(mineSuccessfulPlanShapes(events, exclude = "me").isEmpty())
}
@Test
fun `keyword overlap ignores stopwords and short tokens`() {
val a = intentKeywords("Build a React frontend that compiles")
val b = intentKeywords("Create a React frontend with routing")
assertTrue("react" in a && "frontend" in a)
assertTrue("the" !in a && "a" !in a)
assertTrue(keywordOverlap(a, b) > 0.0)
assertEquals(0.0, keywordOverlap(a, emptySet()))
}
private var seq = 0L
private fun ev(sid: String, payload: EventPayload) = listOf(
StoredEvent(
metadata = EventMetadata(
eventId = EventId("e${seq++}"),
sessionId = SessionId(sid),
timestamp = Instant.parse("2026-01-01T00:00:00Z"),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
sequence = seq,
sessionSequence = seq,
payload = payload,
),
)
private fun intent(sid: String, text: String) = ev(sid, InitialIntentEvent(SessionId(sid), text))
private fun locked(sid: String, wf: String, stages: List<String>) = ev(
sid,
ExecutionPlanLockedEvent(SessionId(sid), ArtifactId("execution_plan"), wf, stages, stages.first()),
)
private fun completed(sid: String, wf: String) =
ev(sid, WorkflowCompletedEvent(SessionId(sid), StageId("end"), 2, wf))
}
@@ -0,0 +1,48 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.types.SessionId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class SelectPromotedConceptsTest {
private fun concept(classKey: String, gate: String, occ: Int) = ConceptPromotedEvent(
sessionId = SessionId("__concept_compiler__"),
fingerprint = classKey,
classKey = classKey,
gate = gate,
conceptText = "text for $classKey",
occurrences = occ,
)
@Test
fun `a file-write-requiring concept is withheld from a stage lacking file_write`() {
val promoted = listOf(concept("contract:x", "contract", 5))
assertTrue(selectPromotedConcepts(promoted, allowedTools = setOf("shell")).isEmpty())
assertEquals(1, selectPromotedConcepts(promoted, allowedTools = setOf("file_write")).size)
}
@Test
fun `gates with no required capability are broadly relevant`() {
val promoted = listOf(concept("lint:y", "lint", 3))
assertEquals(1, selectPromotedConcepts(promoted, allowedTools = setOf("shell")).size)
}
@Test
fun `a concept contradicted after promotion is withheld`() {
val promoted = listOf(concept("lint:y", "lint", 4))
assertTrue(
selectPromotedConcepts(promoted, allowedTools = setOf("shell"), contradicted = setOf("lint:y")).isEmpty(),
)
assertEquals(1, selectPromotedConcepts(promoted, allowedTools = setOf("shell")).size)
}
@Test
fun `top concepts by occurrence are kept within the cap`() {
val promoted = (1..5).map { concept("lint:$it", "lint", occ = it) }
val picked = selectPromotedConcepts(promoted, allowedTools = emptySet(), limit = 3)
assertEquals(listOf("lint:5", "lint:4", "lint:3"), picked.map { it.classKey })
}
}
@@ -0,0 +1,59 @@
package com.correx.core.kernel.orchestration
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.EventPayload
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.ToolInvocationId
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
class WorkspaceStateKeyTest {
@Test
fun `key is stable for the same manifest and independent of write order`() {
val a = workspaceStateKey(listOf(wrote("a.kt", "h1"), wrote("b.kt", "h2")))
val b = workspaceStateKey(listOf(wrote("b.kt", "h2"), wrote("a.kt", "h1")))
assertEquals(a, b)
}
@Test
fun `a later write to a file changes the key`() {
val before = workspaceStateKey(listOf(wrote("a.kt", "h1")))
val after = workspaceStateKey(listOf(wrote("a.kt", "h1"), wrote("a.kt", "h2")))
assertNotEquals(before, after, "a changed post-image must invalidate a prior baseline")
}
@Test
fun `latest post-image hash wins for a repeatedly-written path`() {
val single = workspaceStateKey(listOf(wrote("a.kt", "h2")))
val rewritten = workspaceStateKey(listOf(wrote("a.kt", "h1"), wrote("a.kt", "h2")))
assertEquals(single, rewritten)
}
private var seq = 0L
private fun wrote(path: String, hash: String) = StoredEvent(
metadata = EventMetadata(
eventId = EventId("e${seq++}"),
sessionId = SessionId("s1"),
timestamp = Instant.parse("2026-01-01T00:00:00Z"),
schemaVersion = 1,
causationId = null,
correlationId = null,
),
sequence = seq,
sessionSequence = seq,
payload = FileWrittenEvent(
invocationId = ToolInvocationId("inv"),
sessionId = SessionId("s1"),
path = path,
postImageHash = hash,
preExisted = false,
timestampMs = 1L,
) as EventPayload,
)
}