fix(build-gate): a mid-plan declared build never suppresses the terminal floor (#263)

autoGateStages returned an empty set as soon as any stage declared
PROJECT/TESTS, so a plan that builds at stage 3 of 9 had nothing
verifying the six stages written after it. Keep the suppression for the
redundant per-writing-stage gates, always keep the terminal stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 21:27:09 +04:00
parent f61864ff1d
commit 867e99d1cb
2 changed files with 13 additions and 6 deletions
@@ -329,8 +329,10 @@ class ExecutionPlanCompiler(
val ownsRealBuild = declaredExpectations.values.any { val ownsRealBuild = declaredExpectations.values.any {
it == BuildExpectation.PROJECT || it == BuildExpectation.TESTS it == BuildExpectation.PROJECT || it == BuildExpectation.TESTS
} }
if (ownsRealBuild) return emptySet()
val writing = plan.stages.filter { it.writes.any { path -> path.isNotBlank() } }.map { it.id }.toSet() val writing = plan.stages.filter { it.writes.any { path -> path.isNotBlank() } }.map { it.id }.toSet()
// A mid-plan PROJECT/TESTS declaration verifies nothing written after it, so it suppresses
// the per-writing-stage gates (those builds would be redundant) but never the terminal floor.
if (ownsRealBuild) return setOf(terminalStageId(plan))
return if (writing.isEmpty()) emptySet() else writing + terminalStageId(plan) return if (writing.isEmpty()) emptySet() else writing + terminalStageId(plan)
} }
@@ -8,6 +8,7 @@ import com.correx.core.events.types.StageId
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -590,9 +591,9 @@ class ExecutionPlanCompilerTest {
} }
@Test @Test
fun `an explicitly declared PROJECT gate suppresses the auto gate`() { fun `an explicitly declared PROJECT gate suppresses per-stage gates but not the terminal floor`() {
// A PROJECT build is a real whole-project build the planner owns, so the compiler must // A mid-plan PROJECT build verifies only what existed when it ran, so it drops the
// not add its own terminal floor on top. // redundant per-writing-stage gates while the terminal floor stays (#263).
val planned = """ val planned = """
{ {
"goal": "scaffold a react app", "goal": "scaffold a react app",
@@ -613,9 +614,13 @@ class ExecutionPlanCompilerTest {
com.correx.core.transitions.graph.BuildExpectation.PROJECT, com.correx.core.transitions.graph.BuildExpectation.PROJECT,
graph.stages[StageId("entry")]!!.buildExpectation, graph.stages[StageId("entry")]!!.buildExpectation,
) )
assertFalse(
graph.stages.getValue(StageId("entry")).autoBuildGate,
"a declared PROJECT build suppresses the redundant per-writing-stage gates",
)
assertTrue( assertTrue(
graph.stages.values.none { it.autoBuildGate }, graph.stages.getValue(StageId("views")).autoBuildGate,
"a real PROJECT build declared anywhere suppresses the auto gate", "the terminal stage keeps its build floor — the declared build ran before later writes",
) )
} }