fix(build-gate): resolve commands by toolchain (#40)

This commit is contained in:
kami
2026-07-21 02:18:04 +04:00
parent 119f59d637
commit 6e844ef1e1
15 changed files with 157 additions and 6 deletions
+1
View File
@@ -16,6 +16,7 @@ CORREX kernel team. Config schema changes affect all consumers — coordinate wi
- `EditableConfig` — partial config for live patch operations (used by the TUI config editor).
- `OperatorProfile` — operator-level persona and behavior settings.
- `ProjectProfile` / `ProjectProfileLoader` / `ProjectProfileWriter` — project-scoped profile; loaded at session bind time.
- Project-profile commands support flat aliases plus toolchain-specific TOML tables such as `[commands.node]`; nested entries bind into replay-safe dotted keys (for example `node.build`).
- `AgentInstructions` / `AgentInstructionsLoader` — per-role prompt fragments loaded from config.
## Work Guidance
@@ -23,7 +23,13 @@ object ProjectProfileLoader {
return ProjectProfile(
about = SimpleToml.asString(rootKeys["about"], ""),
conventions = SimpleToml.asStringList(rootKeys["conventions"]),
commands = commandsSection.mapValues { (_, v) -> v.toString() },
commands = commandsSection.mapValues { (_, v) -> v.toString() } +
sections.filterKeys { it.startsWith("commands.") }
.flatMap { (section, values) ->
val toolchain = section.removePrefix("commands.")
values.map { (alias, value) -> "$toolchain.$alias" to value.toString() }
}
.toMap(),
)
}
}
@@ -26,13 +26,23 @@ object ProjectProfileWriter {
b.append("conventions = ").append(list(profile.conventions)).append('\n')
}
if (profile.commands.isNotEmpty()) {
val flatCommands = profile.commands.filterKeys { '.' !in it }
val scopedCommands = profile.commands.filterKeys { '.' in it }
.entries.groupBy({ it.key.substringBefore('.') }, { it.key.substringAfter('.') to it.value })
if (flatCommands.isNotEmpty()) {
if (b.isNotEmpty()) b.append('\n')
b.append("[commands]\n")
profile.commands.forEach { (key, value) ->
flatCommands.forEach { (key, value) ->
b.append(key).append(" = ").append(str(value)).append('\n')
}
}
scopedCommands.forEach { (toolchain, commands) ->
if (b.isNotEmpty()) b.append('\n')
b.append("[commands.").append(toolchain).append("]\n")
commands.forEach { (alias, value) ->
b.append(alias).append(" = ").append(str(value)).append('\n')
}
}
return b.toString()
}
@@ -62,6 +62,31 @@ class ProjectProfileLoaderTest {
assertEquals(mapOf("test" to "./gradlew check"), p.commands)
}
@Test
fun `toolchain command tables are represented as dotted command keys`() {
val root = tempRoot()
Files.writeString(
Paths.get(root, ".correx", "project.toml"),
"""
[commands]
build = "./gradlew assemble"
[commands.node]
build = "npm --prefix frontend run build"
test = "npm --prefix frontend test"
""".trimIndent(),
)
assertEquals(
mapOf(
"build" to "./gradlew assemble",
"node.build" to "npm --prefix frontend run build",
"node.test" to "npm --prefix frontend test",
),
ProjectProfileLoader.load(root).commands,
)
}
@Test
fun `malformed file returns default without throwing`() {
val root = tempRoot()
@@ -24,6 +24,7 @@ class ProjectProfileWriterTest {
commands = mapOf(
"build" to "./gradlew build",
"test" to "./gradlew check",
"node.build" to "npm --prefix frontend run build",
),
)
@@ -36,6 +37,16 @@ class ProjectProfileWriterTest {
assertEquals(profile, loaded)
}
@Test
fun `writer serializes scoped commands as TOML subtables`() {
val serialized = ProjectProfileWriter.serialize(
ProjectProfile(commands = mapOf("node.build" to "npm run build")),
)
assertTrue(serialized.contains("[commands.node]"))
assertTrue(serialized.contains("build = \"npm run build\""))
}
@Test
fun `an empty profile serializes to an empty string and skips empty sections`() {
val serialized = ProjectProfileWriter.serialize(ProjectProfile())