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:
2026-07-19 01:20:37 +04:00
parent 7b90944b61
commit 1b58bc325e
57 changed files with 1205 additions and 115 deletions
@@ -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()