feat(journal): compaction-aware DecisionJournalRenderer

This commit is contained in:
2026-06-08 10:02:34 +04:00
parent c360862b85
commit 4592d5ce18
2 changed files with 89 additions and 4 deletions
@@ -3,12 +3,17 @@ package com.correx.core.journal
import com.correx.core.journal.model.DecisionJournalState import com.correx.core.journal.model.DecisionJournalState
class DecisionJournalRenderer(private val keepLast: Int = 40) { class DecisionJournalRenderer(private val keepLast: Int = 40) {
fun render(state: DecisionJournalState): String { fun render(state: DecisionJournalState, summaryText: String? = null): String {
if (state.records.isEmpty()) return "" if (state.records.isEmpty() && summaryText == null) return ""
val shown = state.records.takeLast(keepLast)
return buildString { return buildString {
appendLine("## Decision history (chronological — ground truth)") appendLine("## Decision history (chronological — ground truth)")
shown.forEach { appendLine("- [${it.kind}] ${it.summary}") } if (summaryText != null) {
appendLine(summaryText)
if (state.lowSalienceOmittedCount > 0) {
appendLine("+${state.lowSalienceOmittedCount} routine transitions/retries omitted")
}
}
state.records.takeLast(keepLast).forEach { appendLine("- [${it.kind}] ${it.summary}") }
}.trimEnd() }.trimEnd()
} }
} }
@@ -0,0 +1,80 @@
package com.correx.core.journal
import com.correx.core.journal.model.DecisionJournalState
import com.correx.core.journal.model.DecisionKind
import com.correx.core.journal.model.DecisionRecord
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class DecisionJournalRendererTest {
private val renderer = DecisionJournalRenderer()
private fun record(seq: Long, kind: DecisionKind = DecisionKind.INTENT, summary: String = "summary-$seq") =
DecisionRecord(sequence = seq, kind = kind, summary = summary)
@Test
fun `empty state with no summaryText returns empty string`() {
val state = DecisionJournalState()
assertEquals("", renderer.render(state))
}
@Test
fun `uncompacted state renders records without omitted line`() {
val state = DecisionJournalState(
records = listOf(record(1), record(2)),
)
val result = renderer.render(state)
assertTrue(result.contains("- [INTENT] summary-1"))
assertTrue(result.contains("- [INTENT] summary-2"))
assertFalse(result.contains("omitted"))
}
@Test
fun `summaryText appears before records`() {
val state = DecisionJournalState(
records = listOf(record(1)),
lowSalienceOmittedCount = 3,
)
val result = renderer.render(state, summaryText = "Compaction summary here")
val lines = result.lines()
val headerIdx = lines.indexOfFirst { it.startsWith("## Decision history") }
val summaryIdx = lines.indexOfFirst { it == "Compaction summary here" }
val recordIdx = lines.indexOfFirst { it.startsWith("- [INTENT]") }
assertTrue(headerIdx >= 0)
assertTrue(summaryIdx > headerIdx)
assertTrue(recordIdx > summaryIdx)
}
@Test
fun `count line appears when lowSalienceOmittedCount is positive`() {
val state = DecisionJournalState(
records = listOf(record(1)),
lowSalienceOmittedCount = 5,
)
val result = renderer.render(state, summaryText = "Some summary")
assertTrue(result.contains("+5 routine transitions/retries omitted"))
}
@Test
fun `no count line when lowSalienceOmittedCount is zero with summaryText`() {
val state = DecisionJournalState(
records = listOf(record(1)),
lowSalienceOmittedCount = 0,
)
val result = renderer.render(state, summaryText = "Some summary")
assertFalse(result.contains("omitted"))
}
@Test
fun `summaryText with empty records list renders header and summary`() {
val state = DecisionJournalState(records = emptyList(), lowSalienceOmittedCount = 2)
val result = renderer.render(state, summaryText = "Compacted summary")
assertTrue(result.contains("## Decision history"))
assertTrue(result.contains("Compacted summary"))
assertTrue(result.contains("+2 routine transitions/retries omitted"))
assertFalse(result.contains("- ["))
}
}