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:
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+98
@@ -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",
|
||||
)
|
||||
}
|
||||
+91
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user