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).
This commit is contained in:
2026-06-11 14:39:05 +04:00
parent 81a21bfe57
commit f521ac89e5
2 changed files with 28 additions and 2 deletions
@@ -84,6 +84,8 @@ class RepoMapIndexer(
"ts" to Regex("""(?m)^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type|enum)\s+(?<name>[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+(?<name>[A-Za-z_][A-Za-z0-9_]*)"""),
// Markdown: h1h3 headings as "symbols" — deeper levels are noise relative to retrieval value.
"md" to Regex("""(?m)^#{1,3}\s+(?<name>.+?)\s*$"""),
)
}
}
@@ -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())