feat(tools): collapse-consecutive-duplicates compression rule
adr-0003 §1.3 dedup: collapse runs of identical adjacent lines into `line (×N)`, order-preserving and lossless (count retained). Wired into ShellTool's default spec before HeadTail so the head/tail window holds distinct content. Safe as a default, unlike DropMatching.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.tools.compression
|
package com.correx.core.tools.compression
|
||||||
|
|
||||||
|
import com.correx.core.tools.compression.CompressionRule.CollapseConsecutiveDuplicates
|
||||||
import com.correx.core.tools.compression.CompressionRule.DropMatching
|
import com.correx.core.tools.compression.CompressionRule.DropMatching
|
||||||
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
||||||
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
||||||
@@ -35,6 +36,7 @@ class DeclarativeCompressor(private val spec: OutputCompressionSpec) : ToolOutpu
|
|||||||
?.let { regex -> lines.filterNot { regex.containsMatchIn(it) } }
|
?.let { regex -> lines.filterNot { regex.containsMatchIn(it) } }
|
||||||
?: lines
|
?: lines
|
||||||
is HeadTail -> applyHeadTail(rule, lines)
|
is HeadTail -> applyHeadTail(rule, lines)
|
||||||
|
CollapseConsecutiveDuplicates -> collapseConsecutive(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyHeadTail(rule: HeadTail, lines: List<String>): List<String> {
|
private fun applyHeadTail(rule: HeadTail, lines: List<String>): List<String> {
|
||||||
@@ -44,4 +46,25 @@ class DeclarativeCompressor(private val spec: OutputCompressionSpec) : ToolOutpu
|
|||||||
"… $elided lines elided …" +
|
"… $elided lines elided …" +
|
||||||
lines.takeLast(rule.tail)
|
lines.takeLast(rule.tail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun collapseConsecutive(lines: List<String>): List<String> {
|
||||||
|
if (lines.isEmpty()) return lines
|
||||||
|
val out = ArrayList<String>(lines.size)
|
||||||
|
var prev = lines.first()
|
||||||
|
var count = 1
|
||||||
|
for (line in lines.drop(1)) {
|
||||||
|
if (line == prev) {
|
||||||
|
count++
|
||||||
|
} else {
|
||||||
|
out += render(prev, count)
|
||||||
|
prev = line
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += render(prev, count)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render(line: String, count: Int): String =
|
||||||
|
if (count > 1) "$line (×$count)" else line
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,13 @@ sealed interface CompressionRule {
|
|||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("head_tail")
|
@SerialName("head_tail")
|
||||||
data class HeadTail(val head: Int, val tail: Int) : CompressionRule
|
data class HeadTail(val head: Int, val tail: Int) : CompressionRule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapse runs of identical adjacent lines into a single line, appending an
|
||||||
|
* occurrence count (e.g. `Error: timeout (×14)`) when the run is longer than
|
||||||
|
* one. Order-preserving; never drops unique content — safe as a default.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("collapse_consecutive_duplicates")
|
||||||
|
data object CollapseConsecutiveDuplicates : CompressionRule
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.core.tools.compression
|
package com.correx.core.tools.compression
|
||||||
|
|
||||||
|
import com.correx.core.tools.compression.CompressionRule.CollapseConsecutiveDuplicates
|
||||||
import com.correx.core.tools.compression.CompressionRule.DropMatching
|
import com.correx.core.tools.compression.CompressionRule.DropMatching
|
||||||
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
||||||
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
||||||
@@ -50,6 +51,20 @@ class DeclarativeCompressorTest {
|
|||||||
assertEquals(raw, out)
|
assertEquals(raw, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CollapseConsecutiveDuplicates collapses runs of identical adjacent lines with a count`() {
|
||||||
|
val raw = "Error: timeout\nError: timeout\nError: timeout\nok\nError: timeout"
|
||||||
|
val out = compress(OutputCompressionSpec(listOf(CollapseConsecutiveDuplicates)), raw)
|
||||||
|
assertEquals("Error: timeout (×3)\nok\nError: timeout", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CollapseConsecutiveDuplicates leaves singletons and non-adjacent duplicates untouched`() {
|
||||||
|
val raw = "a\nb\na"
|
||||||
|
val out = compress(OutputCompressionSpec(listOf(CollapseConsecutiveDuplicates)), raw)
|
||||||
|
assertEquals(raw, out)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `read_file spec strips blanks then leading whitespace`() {
|
fun `read_file spec strips blanks then leading whitespace`() {
|
||||||
val spec = OutputCompressionSpec(listOf(StripBlankLines, StripLeadingWhitespace))
|
val spec = OutputCompressionSpec(listOf(StripBlankLines, StripLeadingWhitespace))
|
||||||
@@ -102,7 +117,10 @@ class DeclarativeCompressorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `spec round-trips through JSON with every rule variant (config-reachable)`() {
|
fun `spec round-trips through JSON with every rule variant (config-reachable)`() {
|
||||||
val spec = OutputCompressionSpec(
|
val spec = OutputCompressionSpec(
|
||||||
rules = listOf(StripBlankLines, StripLeadingWhitespace, DropMatching("^Progress: "), HeadTail(3, 3)),
|
rules = listOf(
|
||||||
|
StripBlankLines, StripLeadingWhitespace, DropMatching("^Progress: "),
|
||||||
|
HeadTail(3, 3), CollapseConsecutiveDuplicates,
|
||||||
|
),
|
||||||
bypassOnError = false,
|
bypassOnError = false,
|
||||||
)
|
)
|
||||||
val encoded = Json.encodeToString(OutputCompressionSpec.serializer(), spec)
|
val encoded = Json.encodeToString(OutputCompressionSpec.serializer(), spec)
|
||||||
|
|||||||
+9
-1
@@ -2,6 +2,7 @@ package com.correx.infrastructure.tools.shell
|
|||||||
|
|
||||||
import com.correx.core.approvals.Tier
|
import com.correx.core.approvals.Tier
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
|
import com.correx.core.tools.compression.CompressionRule.CollapseConsecutiveDuplicates
|
||||||
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
import com.correx.core.tools.compression.CompressionRule.HeadTail
|
||||||
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
import com.correx.core.tools.compression.CompressionRule.StripBlankLines
|
||||||
import com.correx.core.tools.compression.DeclarativeCompressor
|
import com.correx.core.tools.compression.DeclarativeCompressor
|
||||||
@@ -50,7 +51,14 @@ class ShellTool(
|
|||||||
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
|
setOf(ToolCapability.SHELL_EXEC, ToolCapability.PROCESS_SPAWN)
|
||||||
override val paramRoles: Map<String, ParamRole> = mapOf("argv" to ParamRole.EXEC_COMMAND)
|
override val paramRoles: Map<String, ParamRole> = mapOf("argv" to ParamRole.EXEC_COMMAND)
|
||||||
override val outputCompressor: ToolOutputCompressor = DeclarativeCompressor(
|
override val outputCompressor: ToolOutputCompressor = DeclarativeCompressor(
|
||||||
OutputCompressionSpec(listOf(StripBlankLines, HeadTail(HEAD_LINES, TAIL_LINES)), bypassOnError = true),
|
OutputCompressionSpec(
|
||||||
|
// Collapse repeated identical lines (retries, warning spam) before truncating, so the
|
||||||
|
// head/tail window holds distinct content rather than duplicates. Dedup is lossless
|
||||||
|
// (count is preserved) and order-preserving, so it is safe as a default — unlike a
|
||||||
|
// progress-dropping regex, which can silently eat real output.
|
||||||
|
listOf(StripBlankLines, CollapseConsecutiveDuplicates, HeadTail(HEAD_LINES, TAIL_LINES)),
|
||||||
|
bypassOnError = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun validateRequest(request: ToolRequest): ValidationResult {
|
override fun validateRequest(request: ToolRequest): ValidationResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user