From d552148048e6fc41f43dd9fbfa11265c779eec78 Mon Sep 17 00:00:00 2001 From: kami Date: Thu, 4 Jun 2026 01:00:49 +0400 Subject: [PATCH] =?UTF-8?q?feat(journal):=20ProjectMemoryDistiller=20?= =?UTF-8?q?=E2=80=94=20pure=20repo-scoped=20decision=20distillation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/journal/ProjectMemoryDistiller.kt | 15 ++++++++ .../test/kotlin/ProjectMemoryDistillerTest.kt | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 core/journal/src/main/kotlin/com/correx/core/journal/ProjectMemoryDistiller.kt create mode 100644 testing/projections/src/test/kotlin/ProjectMemoryDistillerTest.kt diff --git a/core/journal/src/main/kotlin/com/correx/core/journal/ProjectMemoryDistiller.kt b/core/journal/src/main/kotlin/com/correx/core/journal/ProjectMemoryDistiller.kt new file mode 100644 index 00000000..f4924069 --- /dev/null +++ b/core/journal/src/main/kotlin/com/correx/core/journal/ProjectMemoryDistiller.kt @@ -0,0 +1,15 @@ +package com.correx.core.journal + +import com.correx.core.journal.model.DecisionJournalState + +/** + * Distils a decision journal into durable, repo-scoped memory lines for persistence to the + * cross-session L3 store. Pure transform (no I/O) so `core:journal` stays dependent only on + * `core:events`; the actual L3 upsert/retrieve lives in the wiring layer that owns the store. + * + * Each line is namespaced by [repoRoot] so retrieval can attribute a decision to its project. + */ +class ProjectMemoryDistiller(private val keepLast: Int = 40) { + fun distill(state: DecisionJournalState, repoRoot: String): List = + state.records.takeLast(keepLast).map { "[$repoRoot] [${it.kind}] ${it.summary}" } +} diff --git a/testing/projections/src/test/kotlin/ProjectMemoryDistillerTest.kt b/testing/projections/src/test/kotlin/ProjectMemoryDistillerTest.kt new file mode 100644 index 00000000..098260d3 --- /dev/null +++ b/testing/projections/src/test/kotlin/ProjectMemoryDistillerTest.kt @@ -0,0 +1,35 @@ +import com.correx.core.journal.ProjectMemoryDistiller +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.assertTrue +import org.junit.jupiter.api.Test + +class ProjectMemoryDistillerTest { + + @Test + fun `distils records into repo-namespaced lines`() { + val state = DecisionJournalState( + records = listOf( + DecisionRecord(1, DecisionKind.STEERING, "User steering: use jwt"), + DecisionRecord(2, DecisionKind.TRANSITION, "Advanced plan → impl (t1)"), + ), + ) + val lines = ProjectMemoryDistiller().distill(state, "/home/me/repo") + + assertEquals(2, lines.size) + assertEquals("[/home/me/repo] [STEERING] User steering: use jwt", lines[0]) + assertTrue(lines[1].startsWith("[/home/me/repo] [TRANSITION]")) + } + + @Test + fun `keepLast bounds the distilled output`() { + val records = (1..50).map { DecisionRecord(it.toLong(), DecisionKind.ARTIFACT, "v$it") } + val lines = ProjectMemoryDistiller(keepLast = 10).distill(DecisionJournalState(records), "/r") + + assertEquals(10, lines.size) + assertTrue(lines.last().contains("v50")) + assertTrue(lines.first().contains("v41")) + } +}