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
@@ -52,7 +52,7 @@ class InfrastructureModuleModelTest {
val descriptor = InfrastructureModule.modelConfigToDescriptor(config)
assertEquals("bare-model", descriptor.modelId)
assertEquals(8192, descriptor.contextSize)
assertEquals(24_576, descriptor.contextSize)
assertEquals(0, descriptor.capabilities.size)
}
@@ -0,0 +1,98 @@
package com.correx.infrastructure.workflow
import com.correx.core.events.events.PlanGroundingVerdict
import com.correx.core.transitions.graph.BuildExpectation
import com.correx.core.transitions.graph.WorkflowGraph
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.PathMatcher
/** Grounding verdict plus the compact findings handed back to the architect on a non-pass. */
data class PlanGroundingResult(
val verdict: PlanGroundingVerdict,
val findings: List<String>,
)
/**
* Deterministic plan grounding (design 2026-07-15 seam 1). Pure over the compiled [WorkflowGraph]
* plus recorded workspace facts (repo-map paths + project-profile command aliases) — zero inference,
* zero I/O, so it is replay-safe by construction and complements the structural [PlanLinter].
*
* v1 grounds **build prerequisites** (spec row 4): a stage that will run a real build command
* ([BuildExpectation] resolves to a profile alias that IS configured) against a build manifest that
* neither exists in the workspace ([repoMapPaths]) nor is produced by any plan stage's declared
* writes is doomed — the gate would fail forever with nothing to create the prerequisite. That is a
* `return_to_architect` finding, caught before lock. A stage whose plan scaffolds the manifest
* earlier, or whose profile has no command (the gate degrades to a safe skip), is grounded.
*
* Path/symbol reference grounding (spec rows 2/3) needs a grounding-grade symbol index and is
* deferred; those assumptions resolve to `unknown`, never `present`, so this phase never fabricates a
* "present" verdict it cannot prove.
*/
object PlanGrounder {
fun ground(
graph: WorkflowGraph,
repoMapPaths: Set<String>,
profileCommands: Map<String, String>,
): PlanGroundingResult {
val manifestInWorkspace = repoMapPaths.any(::isBuildManifest)
val manifestProducedByPlan = graph.stages.values
.flatMap { it.writeManifest + it.expectedFiles }
.any(::isBuildManifest)
val prerequisiteAvailable = manifestInWorkspace || manifestProducedByPlan
// Files any stage in the plan will create — the "will exist by run time" set for scope grounding.
val createdPaths = graph.stages.values.flatMap { it.expectedFiles + it.writeManifest }
val findings = graph.stages.entries.flatMap { (id, cfg) ->
buildList {
val alias = cfg.buildExpectation.takeIf { it != BuildExpectation.NONE }?.commandAlias
val commandConfigured = alias != null && !profileCommands[alias].isNullOrBlank()
if (commandConfigured && !prerequisiteAvailable) {
add(
"stage ${id.value}: declares a ${cfg.buildExpectation} build ('$alias') but the workspace " +
"has no build manifest and no plan stage creates one — add a bootstrap stage that " +
"produces the manifest/config, or verify the prerequisite before this stage runs.",
)
}
// Scope grounding (design 2026-07-15 seam 1, spec row 2): a stage confined to a `touches`
// scope that matches NOTHING in the recorded repo map AND that no plan stage creates a file
// under is grounded on a directory that will never exist — the exact session-a35cf1e3
// "scoped to EXISTING frontend/ that isn't there" failure. Caught at stage 0, not stage 11.
cfg.touches
.filter { glob -> !scopeSatisfied(glob, repoMapPaths, createdPaths) }
.forEach { glob ->
add(
"stage ${id.value}: scoped to '$glob' but nothing under it exists in the repo map " +
"and no plan stage creates a file there — scaffold that path first or fix the scope.",
)
}
}
}
return PlanGroundingResult(
verdict = if (findings.isEmpty()) PlanGroundingVerdict.PASS else PlanGroundingVerdict.RETURN_TO_ARCHITECT,
findings = findings,
)
}
/** True if any existing repo-map path or any plan-created path falls under the [glob] scope. */
private fun scopeSatisfied(glob: String, repoMapPaths: Set<String>, createdPaths: List<String>): Boolean {
val matcher: PathMatcher = runCatching {
FileSystems.getDefault().getPathMatcher("glob:${glob.trimStart('/')}")
}.getOrElse { return true } // ponytail: an unparseable glob is not a grounding failure — don't block on it.
fun matches(p: String) = runCatching { matcher.matches(Path.of(p.trimStart('/'))) }.getOrDefault(false)
return repoMapPaths.any(::matches) || createdPaths.any(::matches)
}
private fun isBuildManifest(path: String): Boolean =
path.substringAfterLast('/').lowercase() in BUILD_MANIFEST_FILENAMES
// Mirrors the runtime BUILD_PREREQUISITE_FILENAMES in SessionOrchestratorPreconditions — the same
// files that trip a repeated-reference precondition failure at run time are the prerequisites
// grounded here at plan time (design #167/#170 complement).
private val BUILD_MANIFEST_FILENAMES = setOf(
"package.json", "tsconfig.json", "vite.config.ts", "build.gradle", "build.gradle.kts", "pom.xml",
)
}
@@ -0,0 +1,91 @@
package com.correx.infrastructure.workflow
import com.correx.core.events.events.PlanGroundingVerdict
import com.correx.core.events.types.StageId
import com.correx.core.transitions.conditions.AlwaysTrue
import com.correx.core.transitions.graph.BuildExpectation
import com.correx.core.transitions.graph.StageConfig
import com.correx.core.transitions.graph.TransitionEdge
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.events.types.TransitionId
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class PlanGrounderTest {
private fun graph(vararg stages: Pair<String, StageConfig>) = WorkflowGraph(
id = "test",
stages = stages.associate { StageId(it.first) to it.second },
transitions = stages.toList().zipWithNext { (f, _), (t, _) ->
TransitionEdge(TransitionId("$f-$t"), StageId(f), StageId(t), AlwaysTrue)
}.toSet(),
start = StageId(stages.first().first),
)
private val cmds = mapOf("build" to "npm run build", "typecheck" to "tsc --noEmit")
@Test
fun `stage building against a missing prerequisite is returned to architect`() {
val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
val r = PlanGrounder.ground(g, repoMapPaths = setOf("src/main.ts"), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.RETURN_TO_ARCHITECT, r.verdict)
assertTrue(r.findings.single().contains("impl"))
}
@Test
fun `manifest present in repo map grounds the build`() {
val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
val r = PlanGrounder.ground(g, repoMapPaths = setOf("package.json"), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
@Test
fun `a plan stage that scaffolds the manifest grounds a later build`() {
val g = graph(
"scaffold" to StageConfig(writeManifest = listOf("package.json")),
"impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT),
)
val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
@Test
fun `no configured command degrades to a safe pass`() {
val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.PROJECT))
val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = emptyMap())
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
@Test
fun `a stage scoped to a path that neither exists nor is created is ungrounded`() {
val g = graph("impl" to StageConfig(touches = listOf("frontend/**")))
val r = PlanGrounder.ground(g, repoMapPaths = setOf("backend/main.kt"), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.RETURN_TO_ARCHITECT, r.verdict)
assertTrue(r.findings.single().contains("frontend/**"))
}
@Test
fun `a scope satisfied by an existing repo path grounds`() {
val g = graph("impl" to StageConfig(touches = listOf("frontend/**")))
val r = PlanGrounder.ground(g, repoMapPaths = setOf("frontend/App.tsx"), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
@Test
fun `a scope satisfied by a file a plan stage creates grounds`() {
val g = graph(
"scaffold" to StageConfig(expectedFiles = listOf("frontend/index.html")),
"impl" to StageConfig(touches = listOf("frontend/**")),
)
val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
@Test
fun `NONE expectation never grounds`() {
val g = graph("impl" to StageConfig(buildExpectation = BuildExpectation.NONE))
val r = PlanGrounder.ground(g, repoMapPaths = emptySet(), profileCommands = cmds)
assertEquals(PlanGroundingVerdict.PASS, r.verdict)
}
}