Add a new module that builds a structural map of the repository by parsing source files with tree-sitter, building a cross-file reference graph weighted by IDF, ranking files with PageRank, and rendering a token-budgeted summary of the most important files and their signatures. Stage 1 — Core module (src/context/repoMap/): Symbol extraction via web-tree-sitter WASM, IDF-weighted reference graph via graphology, PageRank ranking, token-budgeted rendering via js-tiktoken cl100k_base, disk cache with mtime invalidation. Supports TypeScript, JavaScript, and Python. 10 tests. Stage 2 — RepoMap tool (src/tools/RepoMapTool/): buildTool wrapper registered in src/tools.ts. Read-only, concurrency-safe. Supports focus_files, focus_symbols, and max_tokens parameters. 9 tests. Stage 3 — Integration: Auto-injection into session context behind REPO_MAP feature flag (off by default). /repomap slash command with --tokens, --focus, --stats, and --invalidate flags. User-facing docs in docs/repo-map.md. 13 tests. With the flag off, the system context is byte-identical to previous behavior. Dependencies: web-tree-sitter, tree-sitter-wasms, graphology, graphology-pagerank, graphology-operators, js-tiktoken Tests: 32 new, 621 total passing, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
73 lines
1.8 KiB
TypeScript
73 lines
1.8 KiB
TypeScript
import type { FileTags, Tag } from './types.js'
|
|
import type { RankedFile } from './pagerank.js'
|
|
import { countTokens } from './tokenize.js'
|
|
|
|
/**
|
|
* Render a token-budgeted repo map from ranked files and their tags.
|
|
*
|
|
* Format per file:
|
|
* path/to/file.ts:
|
|
* ⋮
|
|
* signature line for def 1
|
|
* ⋮
|
|
* signature line for def 2
|
|
* ⋮
|
|
*
|
|
* Files that don't fit within the budget are dropped entirely.
|
|
*/
|
|
export function renderMap(
|
|
rankedFiles: RankedFile[],
|
|
fileTagsMap: Map<string, FileTags>,
|
|
maxTokens: number,
|
|
): { map: string; tokenCount: number; fileCount: number } {
|
|
const sections: string[] = []
|
|
let currentTokens = 0
|
|
let fileCount = 0
|
|
|
|
for (const { path } of rankedFiles) {
|
|
const ft = fileTagsMap.get(path)
|
|
if (!ft) continue
|
|
|
|
// Only include definitions in the rendered output
|
|
const defs = ft.tags
|
|
.filter(t => t.kind === 'def')
|
|
.sort((a, b) => a.line - b.line)
|
|
|
|
if (defs.length === 0) continue
|
|
|
|
const section = renderFileSection(path, defs)
|
|
const sectionTokens = countTokens(section)
|
|
|
|
// Would this section bust the budget?
|
|
if (currentTokens + sectionTokens > maxTokens) {
|
|
// Don't include partial files — drop entirely
|
|
break
|
|
}
|
|
|
|
sections.push(section)
|
|
currentTokens += sectionTokens
|
|
fileCount++
|
|
}
|
|
|
|
const map = sections.join('\n')
|
|
return { map, tokenCount: currentTokens, fileCount }
|
|
}
|
|
|
|
function renderFileSection(path: string, defs: Tag[]): string {
|
|
const lines: string[] = [`${path}:`]
|
|
let lastLine = 0
|
|
|
|
for (const def of defs) {
|
|
// Add elision marker if there's a gap
|
|
if (def.line > lastLine + 1) {
|
|
lines.push('⋮')
|
|
}
|
|
lines.push(` ${def.signature}`)
|
|
lastLine = def.line
|
|
}
|
|
|
|
// Trailing elision marker
|
|
lines.push('⋮')
|
|
return lines.join('\n')
|
|
}
|