fix(build-gate): resolve commands by toolchain (#40)
This commit is contained in:
@@ -28,6 +28,7 @@ CORREX kernel team.
|
||||
- Stage events are recorded by `DefaultStageExecutionEventMapper`; the mapper must emit events for every outcome (success, failure, skip).
|
||||
- `WorkflowGraph` is built from config/TOML at session start; it is immutable during a session.
|
||||
- `StageConfig.autoBuildGate` is a compiler-set request for a runtime build gate; it is used on write-declaring freestyle stages only when no explicit build expectation exists.
|
||||
- `BuildExpectation` resolves toolchain-scoped command aliases (`node.build`, `jvm.build`) before legacy flat aliases, keeping profile snapshots replayable.
|
||||
- Cycle detection (`CycleExtractor`) runs during graph validation (`core:validation`), not at runtime.
|
||||
|
||||
## Verification
|
||||
|
||||
+14
-1
@@ -7,7 +7,10 @@ package com.correx.core.transitions.graph
|
||||
* stays stack-specific and operator-owned while the *scope* is a fixed vocabulary the planner can't
|
||||
* invent.
|
||||
*
|
||||
* [commandAlias] names the `[project.commands]` alias to run for this scope (null = no gate):
|
||||
* [commandAlias] names the `[commands]` alias to run for this scope (null = no gate). A
|
||||
* toolchain-specific command is stored as `<toolchain>.<alias>` in the replay-bound profile map;
|
||||
* this corresponds to TOML such as `[commands.node]`. The resolver prefers that scoped command and
|
||||
* falls back to the flat alias for existing profiles.
|
||||
* NONE → nothing (early stages where the project is not yet runnable), MODULE → typecheck,
|
||||
* PROJECT → build, TESTS → test. A stage whose profile lacks the alias skips the gate with a warning
|
||||
* rather than failing — the vocabulary degrades safely when the operator hasn't configured commands.
|
||||
@@ -19,6 +22,16 @@ enum class BuildExpectation(val commandAlias: String?) {
|
||||
TESTS("test"),
|
||||
;
|
||||
|
||||
/**
|
||||
* Resolves this gate's command from a profile snapshot. The profile remains a flat map in the
|
||||
* event vocabulary, so nested TOML command tables are represented by dotted keys such as
|
||||
* `node.build`. This makes the toolchain choice replayable without changing session events.
|
||||
*/
|
||||
fun commandFor(profileCommands: Map<String, String>, toolchain: String?): String? {
|
||||
val alias = commandAlias ?: return null
|
||||
return toolchain?.let { profileCommands["$it.$alias"] } ?: profileCommands[alias]
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Parses a plan's `build_expectation` string; null for an unrecognized value so the compiler rejects it. */
|
||||
fun fromPlan(raw: String?): BuildExpectation? = when (raw?.trim()?.lowercase()) {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.correx.core.transitions.graph
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BuildExpectationTest {
|
||||
|
||||
@Test
|
||||
fun `toolchain command overrides flat command for matching produced kind`() {
|
||||
val commands = mapOf(
|
||||
"build" to "./gradlew assemble",
|
||||
"node.build" to "npm --prefix frontend run build",
|
||||
)
|
||||
|
||||
assertEquals("npm --prefix frontend run build", BuildExpectation.PROJECT.commandFor(commands, "node"))
|
||||
assertEquals("./gradlew assemble", BuildExpectation.PROJECT.commandFor(commands, "jvm"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flat command remains compatible when a toolchain has no override`() {
|
||||
assertEquals(
|
||||
"./gradlew check",
|
||||
BuildExpectation.TESTS.commandFor(mapOf("test" to "./gradlew check"), "jvm"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `none has no command`() {
|
||||
assertNull(BuildExpectation.NONE.commandFor(mapOf("build" to "ignored"), "node"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user