From f521ac89e53eecd0c90e72f2ad013e60641991ab Mon Sep 17 00:00:00 2001 From: kami Date: Thu, 11 Jun 2026 14:39:05 +0400 Subject: [PATCH] feat(memory): markdown heading extraction in RepoMapIndexer h1-h3 headings become the file's symbols, so design docs and GDDs are embedded and retrievable alongside code. .md files now also participate in workspace fingerprinting (INDEXED_EXTENSIONS derives from pattern keys). --- .../apps/server/memory/RepoMapIndexer.kt | 2 ++ .../apps/server/memory/RepoMapIndexerTest.kt | 28 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/memory/RepoMapIndexer.kt b/apps/server/src/main/kotlin/com/correx/apps/server/memory/RepoMapIndexer.kt index 71d30b44..61ac841f 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/memory/RepoMapIndexer.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/memory/RepoMapIndexer.kt @@ -84,6 +84,8 @@ class RepoMapIndexer( "ts" to Regex("""(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+(?[A-Za-z_$][A-Za-z0-9_$]*)"""), // GDScript: column-0 declarations only, so indented inner-class members never match. "gd" to Regex("""(?m)^(?:static\s+)?(?:func|class_name|class|signal|enum)\s+(?[A-Za-z_][A-Za-z0-9_]*)"""), + // Markdown: h1–h3 headings as "symbols" — deeper levels are noise relative to retrieval value. + "md" to Regex("""(?m)^#{1,3}\s+(?.+?)\s*$"""), ) } } diff --git a/apps/server/src/test/kotlin/com/correx/apps/server/memory/RepoMapIndexerTest.kt b/apps/server/src/test/kotlin/com/correx/apps/server/memory/RepoMapIndexerTest.kt index 9bf79d90..f609b72b 100644 --- a/apps/server/src/test/kotlin/com/correx/apps/server/memory/RepoMapIndexerTest.kt +++ b/apps/server/src/test/kotlin/com/correx/apps/server/memory/RepoMapIndexerTest.kt @@ -23,7 +23,7 @@ class RepoMapIndexerTest { root.resolve("build/Generated.kt").writeText("class Generated\n") // Non-source file must be ignored. - root.resolve("README.md").writeText("# hi\n") + root.resolve("notes.txt").writeText("scratch\n") val entries = RepoMapIndexer(maxDepth = 4, ignoreGlobs = listOf("build")).index(root) @@ -31,7 +31,7 @@ class RepoMapIndexerTest { assertTrue(paths.contains("New.kt")) assertTrue(paths.contains("Old.kt")) assertFalse(paths.any { it.contains("build") }, "ignored dir leaked: $paths") - assertFalse(paths.contains("README.md"), "non-source file leaked") + assertFalse(paths.contains("notes.txt"), "non-source file leaked") // Most-recently-modified ranks first. assertEquals("New.kt", entries.first().path) @@ -75,6 +75,30 @@ class RepoMapIndexerTest { assertFalse(entry.symbols.contains("help"), "indented inner method must not be matched") } + @Test + fun `extracts markdown headings as symbols`(@TempDir root: Path) { + root.resolve("design.md").writeText( + """ + # Game Design Document + intro text with an inline # hash + + ## Combat System + body + + ### Damage Formula + #### Too Deep To Matter + """.trimIndent(), + ) + + val entry = RepoMapIndexer().index(root).single() + assertEquals("design.md", entry.path) + assertTrue( + entry.symbols.containsAll(listOf("Game Design Document", "Combat System", "Damage Formula")), + "missing headings in: ${entry.symbols}", + ) + assertFalse(entry.symbols.any { it.contains("Too Deep") }, "h4+ headings must not be matched") + } + @Test fun `empty or missing root yields empty map`(@TempDir root: Path) { assertTrue(RepoMapIndexer().index(root).isEmpty())