wip(freestyle/acr): grounding & edit-tool fixes + ACR-compiler experiment
This branch's uncommitted WIP, committed together (entangled at file level). Distinct pieces of work: Freestyle QA fixes (this session): - FileEditTool: pre-validate replace anchor in validateRequest — reject a missing/ambiguous target BEFORE the approval gate, mirroring read/write's file-not-found / read-before-write pre-checks. Shared not-found/ambiguous messages between validate and execute so they can't drift. - PlanGrounder: add `scanned` flag; when no RepoMapComputedEvent was recorded, repoMapPaths is "unknown" not "empty workspace" — skip scope grounding (which proves a path ABSENT) so real paths (apps/server/**) aren't falsely rejected. Build-manifest check still runs. - FreestyleDriver: wire scanned=(repoMap!=null); on plan rejection emit a session-terminal WorkflowFailedEvent so a rejected run reads FAILED, not the COMPLETED-lie (last verdict was the planning-phase WorkflowCompleted). - ServerModule: resolve project-memory workspace root from the session's bound workspace (sessionWorkspaceRoot) instead of boot-static pm.repoRoot(), fixing the workspace-binding divergence (correx vs empty scratch dir). Retire tracked in Vikunja #266. - LaunchRegistrationRaceTest: join registered jobs before asserting launchCount — computeIfAbsent returns the Job immediately but the fire-and-forget launch body lagged awaitAll (the 49-vs-50 flake). ACR concept-compiler experiment (pre-existing WIP on this branch): - ExecutionPlanCompiler/Model/PlanLinter, #264 needs-seam (sessionArtifacts), LSP diagnostics subsystem (LspDiagnosticEvents/Runner/Lsp4j), BootWorkspace, config surface, workflow prompts/schemas, orchestrator advance-don't-rerun. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,9 @@ import com.correx.core.approvals.model.DomainApprovalRequest
|
||||
import com.correx.core.artifacts.DefaultArtifactReducer
|
||||
import com.correx.core.events.EventDispatcher
|
||||
import com.correx.core.events.events.FailureTicketOpenedEvent
|
||||
import com.correx.core.events.events.StaticAnalysisCompletedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
import com.correx.core.events.execution.RetryPolicy
|
||||
import com.correx.core.events.types.ProviderId
|
||||
@@ -424,6 +427,7 @@ class RecoveryRoutingTest {
|
||||
private fun buildMultiToolOrchestrator(
|
||||
staticOutput: String = "App.tsx: TS2322 type mismatch",
|
||||
provider: InferenceProvider = StageRoutingProvider(),
|
||||
staticExitCode: Int = 1,
|
||||
): Pair<DefaultSessionOrchestrator, InMemoryEventStore> {
|
||||
val eventStore = InMemoryEventStore()
|
||||
val shell = ShellTool(allowedExecutables = setOf("true"), workingDir = workspace)
|
||||
@@ -471,7 +475,7 @@ class RecoveryRoutingTest {
|
||||
workspacePolicy = WorkspacePolicy(workspace),
|
||||
// Gate names the file impl wrote, so route-to-owner can resolve the author from the manifest.
|
||||
staticAnalysisRunner = StaticAnalysisRunner { _, _ ->
|
||||
StaticAnalysisRunResult(exitCode = 1, output = staticOutput)
|
||||
StaticAnalysisRunResult(exitCode = staticExitCode, output = staticOutput)
|
||||
},
|
||||
approvalEngine = object : ApprovalEngine {
|
||||
override fun evaluate(
|
||||
@@ -498,6 +502,84 @@ class RecoveryRoutingTest {
|
||||
return orchestrator to eventStore
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-writing review terminal records static analysis before workflow completion`(): Unit = runBlocking {
|
||||
val (orchestrator, eventStore) = buildMultiToolOrchestrator(
|
||||
staticOutput = "static floor clean",
|
||||
staticExitCode = 0,
|
||||
)
|
||||
val sessionId = SessionId("review-terminal-static-floor")
|
||||
val graph = WorkflowGraph(
|
||||
id = "freestyle-review-terminal",
|
||||
stages = mapOf(
|
||||
StageId("impl") to StageConfig(
|
||||
allowedTools = setOf("file_write"),
|
||||
writeManifest = listOf("App.tsx"),
|
||||
),
|
||||
StageId("review") to StageConfig(
|
||||
allowedTools = setOf("shell"),
|
||||
staticAnalysis = listOf("correx-static-floor -- App.tsx"),
|
||||
autoBuildGate = true,
|
||||
metadata = mapOf("role" to "reviewer"),
|
||||
),
|
||||
),
|
||||
transitions = setOf(
|
||||
TransitionEdge(TransitionId("impl->review"), StageId("impl"), StageId("review")) { true },
|
||||
TransitionEdge(TransitionId("review->done"), StageId("review"), StageId("done")) { true },
|
||||
),
|
||||
start = StageId("impl"),
|
||||
)
|
||||
|
||||
orchestrator.run(sessionId, graph, OrchestrationConfig())
|
||||
|
||||
val payloads = eventStore.read(sessionId).map { it.payload }
|
||||
val staticIndex = payloads.indexOfFirst {
|
||||
it is StaticAnalysisCompletedEvent && it.stageId == StageId("review")
|
||||
}
|
||||
val completedIndex = payloads.indexOfFirst { it is WorkflowCompletedEvent }
|
||||
assertTrue(staticIndex >= 0, "the non-writing review terminal must execute its static floor")
|
||||
assertTrue(completedIndex > staticIndex, "static analysis must be durably recorded before completion")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `review loop routes to recovery after exactly three back edges and never emits a fourth`(): Unit =
|
||||
runBlocking {
|
||||
val (orchestrator, eventStore) = buildMultiToolOrchestrator()
|
||||
val sessionId = SessionId("review-loop-convergence")
|
||||
val graph = WorkflowGraph(
|
||||
id = "review-loop-convergence",
|
||||
stages = mapOf(
|
||||
StageId("impl") to StageConfig(allowedTools = setOf("file_write")),
|
||||
StageId("review") to StageConfig(
|
||||
allowedTools = setOf("shell"),
|
||||
metadata = mapOf("role" to "reviewer"),
|
||||
),
|
||||
StageId("recovery") to StageConfig(
|
||||
allowedTools = setOf("shell", "file_write"),
|
||||
metadata = mapOf("role" to "recovery"),
|
||||
),
|
||||
),
|
||||
transitions = setOf(
|
||||
TransitionEdge(TransitionId("impl->review"), StageId("impl"), StageId("review")) { true },
|
||||
TransitionEdge(TransitionId("review->impl"), StageId("review"), StageId("impl")) { true },
|
||||
),
|
||||
start = StageId("impl"),
|
||||
)
|
||||
|
||||
val result = orchestrator.run(sessionId, graph, OrchestrationConfig())
|
||||
|
||||
assertTrue(result is WorkflowResult.Failed)
|
||||
val payloads = eventStore.read(sessionId).map { it.payload }
|
||||
val reviewBackEdges = payloads.filterIsInstance<TransitionExecutedEvent>()
|
||||
.filter { it.from == StageId("review") && it.to == StageId("impl") }
|
||||
assertEquals(3, reviewBackEdges.size, "the fourth rejected review must not enter rework")
|
||||
val ticket = payloads.filterIsInstance<FailureTicketOpenedEvent>()
|
||||
.single { it.gate == "review_loop" }
|
||||
assertEquals(StageId("review"), ticket.stageId)
|
||||
assertEquals(StageId("recovery"), ticket.routeTo)
|
||||
assertTrue(payloads.any { it is WorkflowFailedEvent })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write-less gate failure routes the ticket to the file's author (route-to-owner)`(): Unit = runBlocking {
|
||||
val (orchestrator, eventStore) = buildMultiToolOrchestrator()
|
||||
|
||||
Reference in New Issue
Block a user