From 867e99d1cb37753ff8fd17fe174b1c5c62097585 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 26 Jul 2026 21:27:09 +0400 Subject: [PATCH] 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 --- .../workflow/ExecutionPlanCompiler.kt | 4 +++- .../workflow/ExecutionPlanCompilerTest.kt | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompiler.kt b/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompiler.kt index f0fe4aa7..5cf737b8 100644 --- a/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompiler.kt +++ b/infrastructure/workflow/src/main/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompiler.kt @@ -329,8 +329,10 @@ class ExecutionPlanCompiler( val ownsRealBuild = declaredExpectations.values.any { 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() + // 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) } diff --git a/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompilerTest.kt b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompilerTest.kt index c927dd68..6455eb13 100644 --- a/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompilerTest.kt +++ b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/ExecutionPlanCompilerTest.kt @@ -8,6 +8,7 @@ import com.correx.core.events.types.StageId import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -590,9 +591,9 @@ class ExecutionPlanCompilerTest { } @Test - fun `an explicitly declared PROJECT gate suppresses the auto gate`() { - // A PROJECT build is a real whole-project build the planner owns, so the compiler must - // not add its own terminal floor on top. + fun `an explicitly declared PROJECT gate suppresses per-stage gates but not the terminal floor`() { + // A mid-plan PROJECT build verifies only what existed when it ran, so it drops the + // redundant per-writing-stage gates while the terminal floor stays (#263). val planned = """ { "goal": "scaffold a react app", @@ -613,9 +614,13 @@ class ExecutionPlanCompilerTest { com.correx.core.transitions.graph.BuildExpectation.PROJECT, graph.stages[StageId("entry")]!!.buildExpectation, ) + assertFalse( + graph.stages.getValue(StageId("entry")).autoBuildGate, + "a declared PROJECT build suppresses the redundant per-writing-stage gates", + ) assertTrue( - graph.stages.values.none { it.autoBuildGate }, - "a real PROJECT build declared anywhere suppresses the auto gate", + graph.stages.getValue(StageId("views")).autoBuildGate, + "the terminal stage keeps its build floor — the declared build ran before later writes", ) }