@ngommans/codefocus 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ngommans/codefocus might be problematic. Click here for more details.
- package/README.md +2 -2
- package/dist/{chunk-ITVAEU6K.js → chunk-472RLVFC.js} +2 -23
- package/dist/chunk-472RLVFC.js.map +1 -0
- package/dist/chunk-FQ3L6YEU.js +542 -0
- package/dist/chunk-FQ3L6YEU.js.map +1 -0
- package/dist/chunk-ZIVIJRW3.js +24 -0
- package/dist/chunk-ZIVIJRW3.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{mcp-7WYTXIQS.js → mcp-3YW6JWHG.js} +33 -6
- package/dist/mcp-3YW6JWHG.js.map +1 -0
- package/dist/mcp-server.js +37 -8
- package/dist/mcp-server.js.map +1 -1
- package/dist/{query-PS6QVPXP.js → query-64CFCXTD.js} +5 -3
- package/dist/{query-PS6QVPXP.js.map → query-64CFCXTD.js.map} +1 -1
- package/dist/watcher-6WHIBMPS.js +72 -0
- package/dist/watcher-6WHIBMPS.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-ITVAEU6K.js.map +0 -1
- package/dist/mcp-7WYTXIQS.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/query.ts","../src/config.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { resolve } from \"node:path\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { IndexDatabase, type SymbolRow } from \"../db.js\";\nimport { resolveRoot } from \"../root.js\";\nimport {\n loadScoringConfig,\n DEFAULT_SCORING_CONFIG,\n type ScoringConfig,\n} from \"../config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { DirectedGraph } = require(\"graphology\");\nconst pagerank = require(\"graphology-metrics/centrality/pagerank\");\nconst { getEncoding } = require(\"js-tiktoken\");\n\n// ── types ───────────────────────────────────────────────────────────────\n\ninterface ScoredSymbol {\n symbol: SymbolRow;\n score: number;\n matchType: \"exact\" | \"contains\" | \"graph\";\n depth: number;\n}\n\ninterface FileSection {\n filePath: string;\n symbols: ScoredSymbol[];\n score: number;\n startLine: number;\n endLine: number;\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────\n\n/**\n * Compute PageRank scores for all files in the index.\n */\nfunction computeFilePagerank(db: IndexDatabase): Map<string, number> {\n const graph = new DirectedGraph();\n\n for (const file of db.getAllFiles()) {\n graph.addNode(file.path);\n }\n\n for (const edge of db.getFileImportEdges()) {\n const key = `${edge.source_file}->${edge.target_file}`;\n if (!graph.hasEdge(key)) {\n graph.addEdgeWithKey(key, edge.source_file, edge.target_file);\n }\n }\n\n const ranks: Record<string, number> =\n graph.order > 0 ? pagerank(graph, { getEdgeWeight: null }) : {};\n\n return new Map(Object.entries(ranks));\n}\n\n/**\n * Use FTS5 to find symbols whose containing file matches the search term.\n * Scores FTS5 hits by proximity to symbol boundaries: a match inside a\n * function body promotes that function, not the entire file.\n *\n * Spike 8 A1: Weighted term frequency — files where the term appears more\n * often (relative to file length) get a log-scaled boost.\n * Spike 8 A2: Symbol-name proximity — files containing a symbol whose name\n * includes the search term get a 1.5x boost.\n */\nfunction ftsSeededSymbols(\n db: IndexDatabase,\n term: string,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const ftsResults = db.searchContent(term);\n if (ftsResults.length === 0) return [];\n\n const results: ScoredSymbol[] = [];\n const maxRank = Math.max(...ftsResults.map((r) => r.rank));\n\n for (const fts of ftsResults) {\n // Normalize FTS5 BM25 score to 0-0.4 range (below exact match, above graph)\n let normalizedScore = maxRank > 0 ? (fts.rank / maxRank) * 0.4 : 0.2;\n\n // Get file content to find which symbols contain the search term\n const absPath = resolve(rootDir, fts.file_path);\n let content: string;\n try {\n content = readFileSync(absPath, \"utf-8\");\n } catch {\n continue;\n }\n\n const lines = content.split(\"\\n\");\n const termLower = term.toLowerCase();\n\n // Find line numbers where the term appears\n const matchingLines: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].toLowerCase().includes(termLower)) {\n matchingLines.push(i + 1); // 1-indexed\n }\n }\n\n if (matchingLines.length === 0) continue;\n\n // A1: Weighted term frequency — boost files where the term is\n // structurally important (many mentions) vs. incidental (1-2 mentions).\n // Uses absolute count: log2(1 + count). Large files with many mentions\n // score higher than small files with few, unlike density-based metrics\n // which penalise important large files.\n const tfBoost = Math.log2(1 + matchingLines.length);\n normalizedScore *= tfBoost;\n\n // A2: Symbol-name proximity boost — if any symbol in this file has a\n // name or signature that includes the search term, the file is more\n // relevant than one where the term only appears in code comments.\n const fileSymbols = db.getSymbolsByFile(fts.file_path);\n const hasSymbolNameMatch = fileSymbols.some(\n (s) =>\n s.name.toLowerCase().includes(termLower) ||\n (s.signature?.toLowerCase().includes(termLower) ?? false),\n );\n if (hasSymbolNameMatch) {\n normalizedScore *= config.symbolProximityBoost;\n }\n\n // Get symbols in this file and score by proximity\n for (const sym of fileSymbols) {\n if (!sym.id) continue;\n\n // Check if any match is within this symbol's line range\n const containsMatch = matchingLines.some(\n (line) => line >= sym.start_line && line <= sym.end_line,\n );\n\n if (containsMatch) {\n // Match is inside this symbol's body — high relevance\n results.push({\n symbol: sym,\n score: normalizedScore,\n matchType: \"contains\" as const,\n depth: 0,\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Search for symbols matching the term, then expand via graph traversal\n * up to `maxDepth` hops.\n */\nfunction searchAndExpand(\n db: IndexDatabase,\n term: string,\n maxDepth: number,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const scored = new Map<number, ScoredSymbol>();\n\n // Phase 1: Direct symbol name matches\n const directMatches = db.findSymbols(term);\n\n for (const sym of directMatches) {\n if (!sym.id) continue;\n const isExact = sym.name.toLowerCase() === term.toLowerCase();\n scored.set(sym.id, {\n symbol: sym,\n score: isExact ? 1.0 : 0.5,\n matchType: isExact ? \"exact\" : \"contains\",\n depth: 0,\n });\n }\n\n // Phase 1b: FTS5 full-text content search (secondary seed)\n // Use only when symbol-name matching returns no results\n if (scored.size === 0) {\n const ftsSymbols = ftsSeededSymbols(db, term, rootDir, config);\n for (const ss of ftsSymbols) {\n if (ss.symbol.id && !scored.has(ss.symbol.id)) {\n scored.set(ss.symbol.id, ss);\n }\n }\n }\n\n // Phase 2: Graph expansion (BFS) with edge-weight scoring\n // Weight by reference type: import (0.4) > type_ref (0.2)\n // Dampen high-degree hub nodes: multiply score by 1/sqrt(fileDegree)\n // Uses total degree (in + out) to catch both importers (cli.ts) and\n // importees (db.ts) that act as hubs\n if (maxDepth > 0) {\n // Precompute file-level total degree (in + out) from import graph\n const fileDegree = new Map<string, number>();\n for (const edge of db.getFileImportEdges()) {\n fileDegree.set(\n edge.source_file,\n (fileDegree.get(edge.source_file) ?? 0) + 1,\n );\n fileDegree.set(\n edge.target_file,\n (fileDegree.get(edge.target_file) ?? 0) + 1,\n );\n }\n\n let frontier = new Set(scored.keys());\n\n for (let hop = 1; hop <= maxDepth && frontier.size > 0; hop++) {\n const nextFrontier = new Set<number>();\n\n for (const symbolId of frontier) {\n // Walk outgoing references (what this symbol depends on)\n const outRefs = db.getOutgoingReferences(symbolId);\n for (const ref of outRefs) {\n if (!scored.has(ref.target_id)) {\n const sym = db.getSymbolById(ref.target_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.target_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.target_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.target_id);\n }\n }\n }\n\n // Walk incoming references (what depends on this symbol)\n const inRefs = db.getIncomingReferences(symbolId);\n for (const ref of inRefs) {\n if (!scored.has(ref.source_id)) {\n const sym = db.getSymbolById(ref.source_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.source_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.source_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.source_id);\n }\n }\n }\n }\n\n frontier = nextFrontier;\n }\n }\n\n return Array.from(scored.values());\n}\n\n/**\n * Group scored symbols by file and compute file-level sections.\n */\nfunction groupByFile(\n scoredSymbols: ScoredSymbol[],\n pagerankScores: Map<string, number>,\n): FileSection[] {\n const fileMap = new Map<string, ScoredSymbol[]>();\n\n for (const ss of scoredSymbols) {\n const fp = ss.symbol.file_path;\n if (!fileMap.has(fp)) fileMap.set(fp, []);\n fileMap.get(fp)!.push(ss);\n }\n\n const sections: FileSection[] = [];\n\n for (const [filePath, symbols] of fileMap) {\n symbols.sort((a, b) => a.symbol.start_line - b.symbol.start_line);\n\n // File score: max symbol score, with pagerank boost only for direct matches\n // Graph-expanded files should not get pagerank boost (it undoes hub dampening)\n const maxSymScore = Math.max(...symbols.map((s) => s.score));\n const hasDirectMatch = symbols.some((s) => s.matchType !== \"graph\");\n const prScore = hasDirectMatch ? (pagerankScores.get(filePath) ?? 0) : 0;\n const fileScore = maxSymScore + prScore;\n\n // Combined line range (union of all symbol ranges)\n const startLine = Math.min(...symbols.map((s) => s.symbol.start_line));\n const endLine = Math.max(...symbols.map((s) => s.symbol.end_line));\n\n sections.push({\n filePath,\n symbols,\n score: fileScore,\n startLine,\n endLine,\n });\n }\n\n // Sort by score descending, then alphabetically\n sections.sort(\n (a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath),\n );\n\n return sections;\n}\n\n/**\n * B1: Apply relevance threshold cutoff.\n * Drop sections whose score falls below scoreFloorRatio * maxScore.\n */\nfunction applyScoreFloor(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length === 0) return sections;\n const maxScore = sections[0].score;\n const floor = config.scoreFloorRatio * maxScore;\n return sections.filter((s) => s.score >= floor);\n}\n\n/**\n * B3: Detect the \"elbow\" in the score distribution.\n * Returns the index of the last section to keep (inclusive), or null\n * if no significant elbow is found.\n *\n * Looks for the first gap where the relative drop between consecutive\n * sorted scores exceeds elbowDropRatio. Only triggers when the score\n * below the gap is less than 30% of the max score, preventing cuts\n * between clustered high-relevance results.\n */\nfunction findElbow(\n scores: number[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): number | null {\n if (scores.length <= 1) return null;\n const maxScore = scores[0];\n for (let i = 0; i < scores.length - 1; i++) {\n const drop = (scores[i] - scores[i + 1]) / scores[i];\n if (drop > config.elbowDropRatio && scores[i + 1] < 0.3 * maxScore) {\n return i;\n }\n }\n return null;\n}\n\n/**\n * B3: Apply elbow detection to trim sections.\n * Only applies when there are 3+ sections to avoid over-trimming small\n * result sets where graph expansion may be genuinely useful.\n */\nfunction applyElbow(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length < 3) return sections;\n const scores = sections.map((s) => s.score);\n const elbowIdx = findElbow(scores, config);\n if (elbowIdx !== null) {\n return sections.slice(0, elbowIdx + 1);\n }\n return sections;\n}\n\n/**\n * Read source lines from a file, returning the specified range.\n */\nfunction readSourceLines(\n rootDir: string,\n filePath: string,\n startLine: number,\n endLine: number,\n): string | null {\n const absPath = resolve(rootDir, filePath);\n try {\n const content = readFileSync(absPath, \"utf-8\");\n const lines = content.split(\"\\n\");\n // Lines are 1-indexed in the database\n const start = Math.max(0, startLine - 1);\n const end = Math.min(lines.length, endLine);\n return lines.slice(start, end).join(\"\\n\");\n } catch {\n return null;\n }\n}\n\n/**\n * Render a file section with header and source code.\n */\nfunction renderSection(rootDir: string, section: FileSection): string {\n const symbolNames = section.symbols\n .map((s) => s.symbol.name)\n .filter((name, i, arr) => arr.indexOf(name) === i)\n .slice(0, 5);\n\n const nameList = symbolNames.join(\", \");\n const header = `── ${section.filePath}:${section.startLine}-${section.endLine} (${nameList}) ──`;\n\n const source = readSourceLines(\n rootDir,\n section.filePath,\n section.startLine,\n section.endLine,\n );\n\n if (source === null) {\n return `${header}\\n[file not readable]`;\n }\n\n return `${header}\\n${source}`;\n}\n\n// ── core query logic (shared by CLI and MCP server) ───────────────────\n\nexport function runQueryCore(\n root: string,\n term: string,\n budget: number,\n depth: number,\n options?: { stats?: boolean; config?: ScoringConfig },\n): string {\n const dbPath = resolve(root, \".codefocus\", \"index.db\");\n\n if (!existsSync(dbPath)) {\n throw new Error(\n `no index found at ${dbPath}\\nRun 'codefocus index --root ${root}' first.`,\n );\n }\n\n const config = options?.config ?? loadScoringConfig(root);\n\n const db = new IndexDatabase(dbPath);\n try {\n // 1. Search + graph expansion\n const scoredSymbols = searchAndExpand(db, term, depth, root, config);\n\n if (scoredSymbols.length === 0) {\n return `No results found for \"${term}\"`;\n }\n\n // 2. Compute file PageRank\n const pagerankScores = computeFilePagerank(db);\n\n // 3. Group by file and rank\n const allSections = groupByFile(scoredSymbols, pagerankScores);\n\n // 4. Phase B: Apply relevance thresholds\n // B1: Score floor — drop sections below scoreFloorRatio * maxScore\n const afterFloor = applyScoreFloor(allSections, config);\n // B3: Elbow detection — find natural gap in score distribution\n const sections = applyElbow(afterFloor, config);\n\n // 5. Render with token budget enforcement + B2 marginal value\n const enc = getEncoding(\"cl100k_base\");\n const blocks: string[] = [];\n let tokenCount = 0;\n let totalSymbols = 0;\n let truncated = false;\n\n for (const section of sections) {\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n\n // B2: Marginal value per token — stop when value density is too low.\n // Only apply after the first block (always include the top result).\n if (blocks.length > 0 && blockTokens > 0) {\n const marginalValue = section.score / blockTokens;\n if (marginalValue < config.minMarginalValue) {\n break;\n }\n }\n\n if (tokenCount + blockTokens <= budget) {\n blocks.push(block);\n tokenCount += blockTokens;\n totalSymbols += section.symbols.length;\n continue;\n }\n\n // Try partial fit — include header + as many source lines as budget allows\n const lines = block.split(\"\\n\");\n let partial = lines[0];\n let partialTokens = enc.encode(partial).length;\n\n if (tokenCount + partialTokens > budget) {\n truncated = true;\n break;\n }\n\n for (let i = 1; i < lines.length; i++) {\n const candidate = partial + \"\\n\" + lines[i];\n const candidateTokens = enc.encode(candidate).length;\n if (tokenCount + candidateTokens > budget) break;\n partial = candidate;\n partialTokens = candidateTokens;\n }\n\n blocks.push(partial);\n tokenCount += partialTokens;\n totalSymbols += section.symbols.length;\n truncated = true;\n break;\n }\n\n // Compute confidence from score distribution (B3 enhancement)\n // Use the post-threshold sections for confidence calculation\n const allScores = sections.map((s) => s.score).sort((a, b) => b - a);\n const topScore = allScores[0] ?? 0;\n const medianScore =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length / 2)]\n : 0;\n const hasExact = sections.some((s) =>\n s.symbols.some((ss) => ss.matchType === \"exact\"),\n );\n // B3: Enhanced confidence — factor in score concentration\n // High: exact match and scores are tightly clustered (elbow removed noise)\n // Low: no exact matches or scores are very spread out\n const scoreRange = allScores.length > 1\n ? (allScores[0] - allScores[allScores.length - 1]) / allScores[0]\n : 0;\n const confidence: \"high\" | \"medium\" | \"low\" =\n hasExact && (medianScore >= 0.3 || sections.length <= 2)\n ? \"high\"\n : !hasExact || medianScore < 0.1\n ? scoreRange < 0.3 ? \"medium\" : \"low\"\n : \"medium\";\n\n // Build output string\n const output: string[] = [];\n\n // YAML front matter\n const shown = blocks.length;\n const frontMatter = [\n \"---\",\n `query: ${term}`,\n `tokens: ${tokenCount}`,\n `files: ${shown}`,\n `symbols: ${totalSymbols}`,\n `truncated: ${truncated}`,\n `confidence: ${confidence}`,\n `top_score: ${topScore.toFixed(2)}`,\n `median_score: ${medianScore.toFixed(2)}`,\n \"---\",\n ].join(\"\\n\");\n\n output.push(frontMatter);\n output.push(\"\");\n output.push(blocks.join(\"\\n\\n\"));\n\n // --stats: verbose scoring detail\n if (options?.stats) {\n output.push(\"\");\n const totalCandidates = allSections.length;\n const afterFloorCount = afterFloor.length;\n const afterElbowCount = sections.length;\n if (totalCandidates > afterElbowCount) {\n output.push(\n `[stats] filtering: ${totalCandidates} candidates → ${afterFloorCount} after B1 floor → ${afterElbowCount} after B3 elbow`,\n );\n }\n for (const section of sections.slice(0, shown)) {\n const topSym = section.symbols.reduce(\n (best, s) => (s.score > best.score ? s : best),\n section.symbols[0],\n );\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n const marginal = blockTokens > 0\n ? (section.score / blockTokens).toFixed(6)\n : \"N/A\";\n output.push(\n `[stats] ${section.filePath.padEnd(40)} score=${section.score.toFixed(2)} match=${topSym.matchType.padEnd(8)} depth=${topSym.depth} value/tok=${marginal}`,\n );\n }\n const p25 =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length * 0.75)]\n : 0;\n const minScore = allScores[allScores.length - 1] ?? 0;\n output.push(\n `[stats] score distribution: max=${topScore.toFixed(2)} median=${medianScore.toFixed(2)} p25=${p25.toFixed(2)} min=${minScore.toFixed(2)}`,\n );\n }\n\n // Compact footer for backward compatibility with benchmark parsing\n const total = sections.length;\n const parts = [\n `[query] ${shown} files, ${totalSymbols} symbols, ~${tokenCount} tokens`,\n ];\n if (truncated) {\n parts.push(`(budget: ${budget}, truncated)`);\n }\n if (total > shown) {\n parts.push(`(${total - shown} more files not shown)`);\n }\n output.push(`\\n${parts.join(\" \")}`);\n\n return output.join(\"\\n\");\n } finally {\n db.close();\n }\n}\n\n// ── command entry point ────────────────────────────────────────────────\n\nexport async function runQuery(\n positional: string[],\n flags: Record<string, string | boolean>,\n): Promise<void> {\n if (flags.help) {\n console.log(`codefocus query — Search and return ranked code context\n\nUsage: codefocus query <search-term> [options]\n\nOptions:\n --budget <tokens> Token budget for output (default: 8000)\n --depth <n> Max graph traversal depth (default: 2)\n --root <path> Root directory of indexed project (default: auto-detect)\n --stats Show verbose scoring detail per section\n --help Show this help message\n\nExamples:\n codefocus query handleSync\n codefocus query Calculator --budget 4000\n codefocus query \"runIndex\" --depth 3`);\n return;\n }\n\n const term = positional[0];\n if (!term) {\n console.error(\"Error: query requires a search term\");\n process.exitCode = 2;\n return;\n }\n\n const root = resolveRoot(flags.root);\n const config = loadScoringConfig(root);\n\n const budget = parseInt(String(flags.budget || String(config.defaultBudget)), 10);\n if (isNaN(budget) || budget <= 0) {\n console.error(\"Error: --budget must be a positive integer\");\n process.exitCode = 2;\n return;\n }\n\n const depth = parseInt(String(flags.depth || String(config.defaultDepth)), 10);\n if (isNaN(depth) || depth < 0) {\n console.error(\"Error: --depth must be a non-negative integer\");\n process.exitCode = 2;\n return;\n }\n\n const result = runQueryCore(root, term, budget, depth, {\n stats: !!flags.stats,\n config,\n });\n console.log(result);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n// ── scoring config types ──────────────────────────────────────────────\n\nexport interface ScoringConfig {\n /** B1: Drop sections scoring below this fraction of the top score (0-1) */\n scoreFloorRatio: number;\n /** B3: Relative drop between consecutive scores that signals an elbow (0-1) */\n elbowDropRatio: number;\n /** B2: Minimum marginal value (score / tokens) to include a section */\n minMarginalValue: number;\n /** A1: TF boost formula — \"log2\" uses log2(1 + count) */\n tfBoostFormula: \"log2\";\n /** A2: Multiplier for files containing a symbol whose name matches the term */\n symbolProximityBoost: number;\n /** Graph edge weight for import references */\n importEdgeWeight: number;\n /** Graph edge weight for type_ref references */\n typeRefWeight: number;\n /** Default token budget for query output */\n defaultBudget: number;\n /** Default graph traversal depth */\n defaultDepth: number;\n}\n\n// ── defaults ──────────────────────────────────────────────────────────\n\nexport const DEFAULT_SCORING_CONFIG: Readonly<ScoringConfig> = {\n scoreFloorRatio: 0.20,\n elbowDropRatio: 0.60,\n minMarginalValue: 0.00003,\n tfBoostFormula: \"log2\",\n symbolProximityBoost: 1.5,\n importEdgeWeight: 0.4,\n typeRefWeight: 0.2,\n defaultBudget: 8000,\n defaultDepth: 2,\n};\n\n// ── config file resolution ────────────────────────────────────────────\n\nconst CONFIG_FILENAMES = [\n \".codefocus/config.json\",\n \"codefocus.config.json\",\n] as const;\n\n/**\n * Load scoring config for a project. Looks for config files in:\n * 1. `.codefocus/config.json`\n * 2. `codefocus.config.json`\n *\n * Any keys found in the `scoring` object override the defaults.\n * Throws on invalid JSON, unknown keys, wrong types, or out-of-bounds values.\n */\nexport function loadScoringConfig(rootDir: string): ScoringConfig {\n for (const filename of CONFIG_FILENAMES) {\n const configPath = resolve(rootDir, filename);\n if (!existsSync(configPath)) continue;\n\n let raw: string;\n try {\n raw = readFileSync(configPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Cannot read config file ${configPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `Invalid JSON in ${configPath}`,\n );\n }\n\n if (parsed && typeof parsed === \"object\" && \"scoring\" in parsed) {\n const scoring = (parsed as Record<string, unknown>).scoring;\n if (typeof scoring !== \"object\" || scoring === null) {\n throw new Error(\n `\"scoring\" in ${configPath} must be an object`,\n );\n }\n return mergeConfig(scoring as Record<string, unknown>, configPath);\n }\n\n // Config file exists but has no \"scoring\" key — use defaults\n return { ...DEFAULT_SCORING_CONFIG };\n }\n\n return { ...DEFAULT_SCORING_CONFIG };\n}\n\n// ── parameter bounds ──────────────────────────────────────────────────\n\ninterface ParamBound {\n min: number;\n max: number;\n integer?: boolean;\n description: string;\n}\n\nconst PARAM_BOUNDS: Record<string, ParamBound> = {\n scoreFloorRatio: { min: 0, max: 1, description: \"B1 score floor ratio (0–1)\" },\n elbowDropRatio: { min: 0, max: 1, description: \"B3 elbow drop ratio (0–1)\" },\n minMarginalValue: { min: 0, max: 1, description: \"B2 min marginal value (0–1)\" },\n symbolProximityBoost: { min: 1, max: 10, description: \"A2 symbol proximity multiplier (1–10)\" },\n importEdgeWeight: { min: 0, max: 1, description: \"import edge weight (0–1)\" },\n typeRefWeight: { min: 0, max: 1, description: \"type_ref edge weight (0–1)\" },\n defaultBudget: { min: 1, max: 100_000, integer: true, description: \"default token budget (1–100000)\" },\n defaultDepth: { min: 0, max: 10, integer: true, description: \"default graph depth (0–10)\" },\n};\n\n/**\n * Validate a single config value against its bounds.\n * Throws a descriptive error if the value is out of range.\n */\nfunction validateParam(\n key: string,\n value: unknown,\n configPath: string,\n): number {\n const bound = PARAM_BOUNDS[key];\n if (!bound) {\n throw new Error(\n `Unknown scoring parameter \"${key}\" in ${configPath}`,\n );\n }\n\n if (typeof value !== \"number\" || !isFinite(value)) {\n throw new Error(\n `Invalid value for \"${key}\" in ${configPath}: expected a number, got ${typeof value}`,\n );\n }\n\n if (bound.integer && !Number.isInteger(value)) {\n throw new Error(\n `Invalid value for \"${key}\" in ${configPath}: expected an integer, got ${value}` +\n `\\n ${bound.description}`,\n );\n }\n\n if (value < bound.min || value > bound.max) {\n throw new Error(\n `Out-of-bounds value for \"${key}\" in ${configPath}: ${value}` +\n `\\n ${bound.description}`,\n );\n }\n\n return value;\n}\n\n/**\n * Merge user-provided scoring overrides with defaults.\n * Throws on unknown keys, wrong types, or out-of-bounds values.\n */\nfunction mergeConfig(\n overrides: Record<string, unknown>,\n configPath: string,\n): ScoringConfig {\n const config = { ...DEFAULT_SCORING_CONFIG };\n const knownKeys = new Set([...Object.keys(PARAM_BOUNDS), \"tfBoostFormula\"]);\n\n for (const key of Object.keys(overrides)) {\n if (!knownKeys.has(key)) {\n throw new Error(\n `Unknown scoring parameter \"${key}\" in ${configPath}` +\n `\\n Valid parameters: ${[...knownKeys].join(\", \")}`,\n );\n }\n }\n\n for (const key of Object.keys(PARAM_BOUNDS)) {\n if (key in overrides) {\n const validated = validateParam(key, overrides[key], configPath);\n (config as Record<string, unknown>)[key] = validated;\n }\n }\n\n if (\"tfBoostFormula\" in overrides) {\n if (overrides.tfBoostFormula !== \"log2\") {\n throw new Error(\n `Invalid value for \"tfBoostFormula\" in ${configPath}: \"${String(overrides.tfBoostFormula)}\"` +\n `\\n Currently only \"log2\" is supported`,\n );\n }\n config.tfBoostFormula = \"log2\";\n }\n\n return config;\n}\n\n/**\n * Serialize current config to JSON (for --emit-config).\n */\nexport function serializeConfig(config: ScoringConfig): string {\n return JSON.stringify({ scoring: config }, null, 2);\n}\n"],"mappings":"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","names":["resolve","existsSync","readFileSync","require","resolve","readFileSync","existsSync"]}
|
|
1
|
+
{"version":3,"sources":["../src/commands/query.ts","../src/config.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { resolve } from \"node:path\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { IndexDatabase, type SymbolRow } from \"../db.js\";\nimport { resolveRoot } from \"../root.js\";\nimport {\n loadScoringConfig,\n DEFAULT_SCORING_CONFIG,\n type ScoringConfig,\n} from \"../config.js\";\n\nconst require = createRequire(import.meta.url);\nconst { DirectedGraph } = require(\"graphology\");\nconst pagerank = require(\"graphology-metrics/centrality/pagerank\");\nconst { getEncoding } = require(\"js-tiktoken\");\n\n// ── types ───────────────────────────────────────────────────────────────\n\ninterface ScoredSymbol {\n symbol: SymbolRow;\n score: number;\n matchType: \"exact\" | \"contains\" | \"graph\";\n depth: number;\n}\n\ninterface FileSection {\n filePath: string;\n symbols: ScoredSymbol[];\n score: number;\n startLine: number;\n endLine: number;\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────\n\n/**\n * Compute PageRank scores for all files in the index.\n */\nfunction computeFilePagerank(db: IndexDatabase): Map<string, number> {\n const graph = new DirectedGraph();\n\n for (const file of db.getAllFiles()) {\n graph.addNode(file.path);\n }\n\n for (const edge of db.getFileImportEdges()) {\n const key = `${edge.source_file}->${edge.target_file}`;\n if (!graph.hasEdge(key)) {\n graph.addEdgeWithKey(key, edge.source_file, edge.target_file);\n }\n }\n\n const ranks: Record<string, number> =\n graph.order > 0 ? pagerank(graph, { getEdgeWeight: null }) : {};\n\n return new Map(Object.entries(ranks));\n}\n\n/**\n * Use FTS5 to find symbols whose containing file matches the search term.\n * Scores FTS5 hits by proximity to symbol boundaries: a match inside a\n * function body promotes that function, not the entire file.\n *\n * Spike 8 A1: Weighted term frequency — files where the term appears more\n * often (relative to file length) get a log-scaled boost.\n * Spike 8 A2: Symbol-name proximity — files containing a symbol whose name\n * includes the search term get a 1.5x boost.\n */\nfunction ftsSeededSymbols(\n db: IndexDatabase,\n term: string,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const ftsResults = db.searchContent(term);\n if (ftsResults.length === 0) return [];\n\n const results: ScoredSymbol[] = [];\n const maxRank = Math.max(...ftsResults.map((r) => r.rank));\n\n for (const fts of ftsResults) {\n // Normalize FTS5 BM25 score to 0-0.4 range (below exact match, above graph)\n let normalizedScore = maxRank > 0 ? (fts.rank / maxRank) * 0.4 : 0.2;\n\n // Get file content to find which symbols contain the search term\n const absPath = resolve(rootDir, fts.file_path);\n let content: string;\n try {\n content = readFileSync(absPath, \"utf-8\");\n } catch {\n continue;\n }\n\n const lines = content.split(\"\\n\");\n const termLower = term.toLowerCase();\n\n // Find line numbers where the term appears\n const matchingLines: number[] = [];\n for (let i = 0; i < lines.length; i++) {\n if (lines[i].toLowerCase().includes(termLower)) {\n matchingLines.push(i + 1); // 1-indexed\n }\n }\n\n if (matchingLines.length === 0) continue;\n\n // A1: Weighted term frequency — boost files where the term is\n // structurally important (many mentions) vs. incidental (1-2 mentions).\n // Uses absolute count: log2(1 + count). Large files with many mentions\n // score higher than small files with few, unlike density-based metrics\n // which penalise important large files.\n const tfBoost = Math.log2(1 + matchingLines.length);\n normalizedScore *= tfBoost;\n\n // A2: Symbol-name proximity boost — if any symbol in this file has a\n // name or signature that includes the search term, the file is more\n // relevant than one where the term only appears in code comments.\n const fileSymbols = db.getSymbolsByFile(fts.file_path);\n const hasSymbolNameMatch = fileSymbols.some(\n (s) =>\n s.name.toLowerCase().includes(termLower) ||\n (s.signature?.toLowerCase().includes(termLower) ?? false),\n );\n if (hasSymbolNameMatch) {\n normalizedScore *= config.symbolProximityBoost;\n }\n\n // Get symbols in this file and score by proximity\n for (const sym of fileSymbols) {\n if (!sym.id) continue;\n\n // Check if any match is within this symbol's line range\n const containsMatch = matchingLines.some(\n (line) => line >= sym.start_line && line <= sym.end_line,\n );\n\n if (containsMatch) {\n // Match is inside this symbol's body — high relevance\n results.push({\n symbol: sym,\n score: normalizedScore,\n matchType: \"contains\" as const,\n depth: 0,\n });\n }\n }\n }\n\n return results;\n}\n\n/**\n * Search for symbols matching the term, then expand via graph traversal\n * up to `maxDepth` hops.\n */\nfunction searchAndExpand(\n db: IndexDatabase,\n term: string,\n maxDepth: number,\n rootDir: string,\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): ScoredSymbol[] {\n const scored = new Map<number, ScoredSymbol>();\n\n // Phase 1: Direct symbol name matches\n const directMatches = db.findSymbols(term);\n\n for (const sym of directMatches) {\n if (!sym.id) continue;\n const isExact = sym.name.toLowerCase() === term.toLowerCase();\n scored.set(sym.id, {\n symbol: sym,\n score: isExact ? 1.0 : 0.5,\n matchType: isExact ? \"exact\" : \"contains\",\n depth: 0,\n });\n }\n\n // Phase 1b: FTS5 full-text content search (secondary seed)\n // Use only when symbol-name matching returns no results\n if (scored.size === 0) {\n const ftsSymbols = ftsSeededSymbols(db, term, rootDir, config);\n for (const ss of ftsSymbols) {\n if (ss.symbol.id && !scored.has(ss.symbol.id)) {\n scored.set(ss.symbol.id, ss);\n }\n }\n }\n\n // Phase 2: Graph expansion (BFS) with edge-weight scoring\n // Weight by reference type: import (0.4) > type_ref (0.2)\n // Dampen high-degree hub nodes: multiply score by 1/sqrt(fileDegree)\n // Uses total degree (in + out) to catch both importers (cli.ts) and\n // importees (db.ts) that act as hubs\n if (maxDepth > 0) {\n // Precompute file-level total degree (in + out) from import graph\n const fileDegree = new Map<string, number>();\n for (const edge of db.getFileImportEdges()) {\n fileDegree.set(\n edge.source_file,\n (fileDegree.get(edge.source_file) ?? 0) + 1,\n );\n fileDegree.set(\n edge.target_file,\n (fileDegree.get(edge.target_file) ?? 0) + 1,\n );\n }\n\n let frontier = new Set(scored.keys());\n\n for (let hop = 1; hop <= maxDepth && frontier.size > 0; hop++) {\n const nextFrontier = new Set<number>();\n\n for (const symbolId of frontier) {\n // Walk outgoing references (what this symbol depends on)\n const outRefs = db.getOutgoingReferences(symbolId);\n for (const ref of outRefs) {\n if (!scored.has(ref.target_id)) {\n const sym = db.getSymbolById(ref.target_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.target_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.target_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.target_id);\n }\n }\n }\n\n // Walk incoming references (what depends on this symbol)\n const inRefs = db.getIncomingReferences(symbolId);\n for (const ref of inRefs) {\n if (!scored.has(ref.source_id)) {\n const sym = db.getSymbolById(ref.source_id);\n if (sym) {\n const baseWeight =\n ref.ref_type === \"import\" ? config.importEdgeWeight : config.typeRefWeight;\n const deg = fileDegree.get(ref.source_file) ?? 0;\n const hubDampening =\n deg > 1 ? 1 / Math.sqrt(deg) : 1;\n const graphScore = (baseWeight / hop) * hubDampening;\n\n scored.set(ref.source_id, {\n symbol: sym,\n score: graphScore,\n matchType: \"graph\",\n depth: hop,\n });\n nextFrontier.add(ref.source_id);\n }\n }\n }\n }\n\n frontier = nextFrontier;\n }\n }\n\n return Array.from(scored.values());\n}\n\n/**\n * Group scored symbols by file and compute file-level sections.\n */\nfunction groupByFile(\n scoredSymbols: ScoredSymbol[],\n pagerankScores: Map<string, number>,\n): FileSection[] {\n const fileMap = new Map<string, ScoredSymbol[]>();\n\n for (const ss of scoredSymbols) {\n const fp = ss.symbol.file_path;\n if (!fileMap.has(fp)) fileMap.set(fp, []);\n fileMap.get(fp)!.push(ss);\n }\n\n const sections: FileSection[] = [];\n\n for (const [filePath, symbols] of fileMap) {\n symbols.sort((a, b) => a.symbol.start_line - b.symbol.start_line);\n\n // File score: max symbol score, with pagerank boost only for direct matches\n // Graph-expanded files should not get pagerank boost (it undoes hub dampening)\n const maxSymScore = Math.max(...symbols.map((s) => s.score));\n const hasDirectMatch = symbols.some((s) => s.matchType !== \"graph\");\n const prScore = hasDirectMatch ? (pagerankScores.get(filePath) ?? 0) : 0;\n const fileScore = maxSymScore + prScore;\n\n // Combined line range (union of all symbol ranges)\n const startLine = Math.min(...symbols.map((s) => s.symbol.start_line));\n const endLine = Math.max(...symbols.map((s) => s.symbol.end_line));\n\n sections.push({\n filePath,\n symbols,\n score: fileScore,\n startLine,\n endLine,\n });\n }\n\n // Sort by score descending, then alphabetically\n sections.sort(\n (a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath),\n );\n\n return sections;\n}\n\n/**\n * B1: Apply relevance threshold cutoff.\n * Drop sections whose score falls below scoreFloorRatio * maxScore.\n */\nfunction applyScoreFloor(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length === 0) return sections;\n const maxScore = sections[0].score;\n const floor = config.scoreFloorRatio * maxScore;\n return sections.filter((s) => s.score >= floor);\n}\n\n/**\n * B3: Detect the \"elbow\" in the score distribution.\n * Returns the index of the last section to keep (inclusive), or null\n * if no significant elbow is found.\n *\n * Looks for the first gap where the relative drop between consecutive\n * sorted scores exceeds elbowDropRatio. Only triggers when the score\n * below the gap is less than 30% of the max score, preventing cuts\n * between clustered high-relevance results.\n */\nfunction findElbow(\n scores: number[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): number | null {\n if (scores.length <= 1) return null;\n const maxScore = scores[0];\n for (let i = 0; i < scores.length - 1; i++) {\n const drop = (scores[i] - scores[i + 1]) / scores[i];\n if (drop > config.elbowDropRatio && scores[i + 1] < 0.3 * maxScore) {\n return i;\n }\n }\n return null;\n}\n\n/**\n * B3: Apply elbow detection to trim sections.\n * Only applies when there are 3+ sections to avoid over-trimming small\n * result sets where graph expansion may be genuinely useful.\n */\nfunction applyElbow(\n sections: FileSection[],\n config: ScoringConfig = DEFAULT_SCORING_CONFIG,\n): FileSection[] {\n if (sections.length < 3) return sections;\n const scores = sections.map((s) => s.score);\n const elbowIdx = findElbow(scores, config);\n if (elbowIdx !== null) {\n return sections.slice(0, elbowIdx + 1);\n }\n return sections;\n}\n\n/**\n * Read source lines from a file, returning the specified range.\n */\nfunction readSourceLines(\n rootDir: string,\n filePath: string,\n startLine: number,\n endLine: number,\n): string | null {\n const absPath = resolve(rootDir, filePath);\n try {\n const content = readFileSync(absPath, \"utf-8\");\n const lines = content.split(\"\\n\");\n // Lines are 1-indexed in the database\n const start = Math.max(0, startLine - 1);\n const end = Math.min(lines.length, endLine);\n return lines.slice(start, end).join(\"\\n\");\n } catch {\n return null;\n }\n}\n\n/**\n * Render a file section with header and source code.\n */\nfunction renderSection(rootDir: string, section: FileSection): string {\n const symbolNames = section.symbols\n .map((s) => s.symbol.name)\n .filter((name, i, arr) => arr.indexOf(name) === i)\n .slice(0, 5);\n\n const nameList = symbolNames.join(\", \");\n const header = `── ${section.filePath}:${section.startLine}-${section.endLine} (${nameList}) ──`;\n\n const source = readSourceLines(\n rootDir,\n section.filePath,\n section.startLine,\n section.endLine,\n );\n\n if (source === null) {\n return `${header}\\n[file not readable]`;\n }\n\n return `${header}\\n${source}`;\n}\n\n// ── core query logic (shared by CLI and MCP server) ───────────────────\n\nexport function runQueryCore(\n root: string,\n term: string,\n budget: number,\n depth: number,\n options?: { stats?: boolean; config?: ScoringConfig },\n): string {\n const dbPath = resolve(root, \".codefocus\", \"index.db\");\n\n if (!existsSync(dbPath)) {\n throw new Error(\n `no index found at ${dbPath}\\nRun 'codefocus index --root ${root}' first.`,\n );\n }\n\n const config = options?.config ?? loadScoringConfig(root);\n\n const db = new IndexDatabase(dbPath);\n try {\n // 1. Search + graph expansion\n const scoredSymbols = searchAndExpand(db, term, depth, root, config);\n\n if (scoredSymbols.length === 0) {\n return `No results found for \"${term}\"`;\n }\n\n // 2. Compute file PageRank\n const pagerankScores = computeFilePagerank(db);\n\n // 3. Group by file and rank\n const allSections = groupByFile(scoredSymbols, pagerankScores);\n\n // 4. Phase B: Apply relevance thresholds\n // B1: Score floor — drop sections below scoreFloorRatio * maxScore\n const afterFloor = applyScoreFloor(allSections, config);\n // B3: Elbow detection — find natural gap in score distribution\n const sections = applyElbow(afterFloor, config);\n\n // 5. Render with token budget enforcement + B2 marginal value\n const enc = getEncoding(\"cl100k_base\");\n const blocks: string[] = [];\n let tokenCount = 0;\n let totalSymbols = 0;\n let truncated = false;\n\n for (const section of sections) {\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n\n // B2: Marginal value per token — stop when value density is too low.\n // Only apply after the first block (always include the top result).\n if (blocks.length > 0 && blockTokens > 0) {\n const marginalValue = section.score / blockTokens;\n if (marginalValue < config.minMarginalValue) {\n break;\n }\n }\n\n if (tokenCount + blockTokens <= budget) {\n blocks.push(block);\n tokenCount += blockTokens;\n totalSymbols += section.symbols.length;\n continue;\n }\n\n // Try partial fit — include header + as many source lines as budget allows\n const lines = block.split(\"\\n\");\n let partial = lines[0];\n let partialTokens = enc.encode(partial).length;\n\n if (tokenCount + partialTokens > budget) {\n truncated = true;\n break;\n }\n\n for (let i = 1; i < lines.length; i++) {\n const candidate = partial + \"\\n\" + lines[i];\n const candidateTokens = enc.encode(candidate).length;\n if (tokenCount + candidateTokens > budget) break;\n partial = candidate;\n partialTokens = candidateTokens;\n }\n\n blocks.push(partial);\n tokenCount += partialTokens;\n totalSymbols += section.symbols.length;\n truncated = true;\n break;\n }\n\n // Compute confidence from score distribution (B3 enhancement)\n // Use the post-threshold sections for confidence calculation\n const allScores = sections.map((s) => s.score).sort((a, b) => b - a);\n const topScore = allScores[0] ?? 0;\n const medianScore =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length / 2)]\n : 0;\n const hasExact = sections.some((s) =>\n s.symbols.some((ss) => ss.matchType === \"exact\"),\n );\n // B3: Enhanced confidence — factor in score concentration\n // High: exact match and scores are tightly clustered (elbow removed noise)\n // Low: no exact matches or scores are very spread out\n const scoreRange = allScores.length > 1\n ? (allScores[0] - allScores[allScores.length - 1]) / allScores[0]\n : 0;\n const confidence: \"high\" | \"medium\" | \"low\" =\n hasExact && (medianScore >= 0.3 || sections.length <= 2)\n ? \"high\"\n : !hasExact || medianScore < 0.1\n ? scoreRange < 0.3 ? \"medium\" : \"low\"\n : \"medium\";\n\n // Build output string\n const output: string[] = [];\n\n // YAML front matter\n const shown = blocks.length;\n const frontMatter = [\n \"---\",\n `query: ${term}`,\n `tokens: ${tokenCount}`,\n `files: ${shown}`,\n `symbols: ${totalSymbols}`,\n `truncated: ${truncated}`,\n `confidence: ${confidence}`,\n `top_score: ${topScore.toFixed(2)}`,\n `median_score: ${medianScore.toFixed(2)}`,\n \"---\",\n ].join(\"\\n\");\n\n output.push(frontMatter);\n output.push(\"\");\n output.push(blocks.join(\"\\n\\n\"));\n\n // --stats: verbose scoring detail\n if (options?.stats) {\n output.push(\"\");\n const totalCandidates = allSections.length;\n const afterFloorCount = afterFloor.length;\n const afterElbowCount = sections.length;\n if (totalCandidates > afterElbowCount) {\n output.push(\n `[stats] filtering: ${totalCandidates} candidates → ${afterFloorCount} after B1 floor → ${afterElbowCount} after B3 elbow`,\n );\n }\n for (const section of sections.slice(0, shown)) {\n const topSym = section.symbols.reduce(\n (best, s) => (s.score > best.score ? s : best),\n section.symbols[0],\n );\n const block = renderSection(root, section);\n const blockTokens = enc.encode(block).length;\n const marginal = blockTokens > 0\n ? (section.score / blockTokens).toFixed(6)\n : \"N/A\";\n output.push(\n `[stats] ${section.filePath.padEnd(40)} score=${section.score.toFixed(2)} match=${topSym.matchType.padEnd(8)} depth=${topSym.depth} value/tok=${marginal}`,\n );\n }\n const p25 =\n allScores.length > 0\n ? allScores[Math.floor(allScores.length * 0.75)]\n : 0;\n const minScore = allScores[allScores.length - 1] ?? 0;\n output.push(\n `[stats] score distribution: max=${topScore.toFixed(2)} median=${medianScore.toFixed(2)} p25=${p25.toFixed(2)} min=${minScore.toFixed(2)}`,\n );\n }\n\n // Compact footer for backward compatibility with benchmark parsing\n const total = sections.length;\n const parts = [\n `[query] ${shown} files, ${totalSymbols} symbols, ~${tokenCount} tokens`,\n ];\n if (truncated) {\n parts.push(`(budget: ${budget}, truncated)`);\n }\n if (total > shown) {\n parts.push(`(${total - shown} more files not shown)`);\n }\n output.push(`\\n${parts.join(\" \")}`);\n\n return output.join(\"\\n\");\n } finally {\n db.close();\n }\n}\n\n// ── command entry point ────────────────────────────────────────────────\n\nexport async function runQuery(\n positional: string[],\n flags: Record<string, string | boolean>,\n): Promise<void> {\n if (flags.help) {\n console.log(`codefocus query — Search and return ranked code context\n\nUsage: codefocus query <search-term> [options]\n\nOptions:\n --budget <tokens> Token budget for output (default: 8000)\n --depth <n> Max graph traversal depth (default: 2)\n --root <path> Root directory of indexed project (default: auto-detect)\n --stats Show verbose scoring detail per section\n --help Show this help message\n\nExamples:\n codefocus query handleSync\n codefocus query Calculator --budget 4000\n codefocus query \"runIndex\" --depth 3`);\n return;\n }\n\n const term = positional[0];\n if (!term) {\n console.error(\"Error: query requires a search term\");\n process.exitCode = 2;\n return;\n }\n\n const root = resolveRoot(flags.root);\n const config = loadScoringConfig(root);\n\n const budget = parseInt(String(flags.budget || String(config.defaultBudget)), 10);\n if (isNaN(budget) || budget <= 0) {\n console.error(\"Error: --budget must be a positive integer\");\n process.exitCode = 2;\n return;\n }\n\n const depth = parseInt(String(flags.depth || String(config.defaultDepth)), 10);\n if (isNaN(depth) || depth < 0) {\n console.error(\"Error: --depth must be a non-negative integer\");\n process.exitCode = 2;\n return;\n }\n\n const result = runQueryCore(root, term, budget, depth, {\n stats: !!flags.stats,\n config,\n });\n console.log(result);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\n// ── scoring config types ──────────────────────────────────────────────\n\nexport interface ScoringConfig {\n /** B1: Drop sections scoring below this fraction of the top score (0-1) */\n scoreFloorRatio: number;\n /** B3: Relative drop between consecutive scores that signals an elbow (0-1) */\n elbowDropRatio: number;\n /** B2: Minimum marginal value (score / tokens) to include a section */\n minMarginalValue: number;\n /** A1: TF boost formula — \"log2\" uses log2(1 + count) */\n tfBoostFormula: \"log2\";\n /** A2: Multiplier for files containing a symbol whose name matches the term */\n symbolProximityBoost: number;\n /** Graph edge weight for import references */\n importEdgeWeight: number;\n /** Graph edge weight for type_ref references */\n typeRefWeight: number;\n /** Default token budget for query output */\n defaultBudget: number;\n /** Default graph traversal depth */\n defaultDepth: number;\n}\n\n// ── defaults ──────────────────────────────────────────────────────────\n\nexport const DEFAULT_SCORING_CONFIG: Readonly<ScoringConfig> = {\n scoreFloorRatio: 0.20,\n elbowDropRatio: 0.60,\n minMarginalValue: 0.00003,\n tfBoostFormula: \"log2\",\n symbolProximityBoost: 1.5,\n importEdgeWeight: 0.4,\n typeRefWeight: 0.2,\n defaultBudget: 8000,\n defaultDepth: 2,\n};\n\n// ── config file resolution ────────────────────────────────────────────\n\nconst CONFIG_FILENAMES = [\n \".codefocus/config.json\",\n \"codefocus.config.json\",\n] as const;\n\n/**\n * Load scoring config for a project. Looks for config files in:\n * 1. `.codefocus/config.json`\n * 2. `codefocus.config.json`\n *\n * Any keys found in the `scoring` object override the defaults.\n * Throws on invalid JSON, unknown keys, wrong types, or out-of-bounds values.\n */\nexport function loadScoringConfig(rootDir: string): ScoringConfig {\n for (const filename of CONFIG_FILENAMES) {\n const configPath = resolve(rootDir, filename);\n if (!existsSync(configPath)) continue;\n\n let raw: string;\n try {\n raw = readFileSync(configPath, \"utf-8\");\n } catch (err) {\n throw new Error(\n `Cannot read config file ${configPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `Invalid JSON in ${configPath}`,\n );\n }\n\n if (parsed && typeof parsed === \"object\" && \"scoring\" in parsed) {\n const scoring = (parsed as Record<string, unknown>).scoring;\n if (typeof scoring !== \"object\" || scoring === null) {\n throw new Error(\n `\"scoring\" in ${configPath} must be an object`,\n );\n }\n return mergeConfig(scoring as Record<string, unknown>, configPath);\n }\n\n // Config file exists but has no \"scoring\" key — use defaults\n return { ...DEFAULT_SCORING_CONFIG };\n }\n\n return { ...DEFAULT_SCORING_CONFIG };\n}\n\n// ── parameter bounds ──────────────────────────────────────────────────\n\ninterface ParamBound {\n min: number;\n max: number;\n integer?: boolean;\n description: string;\n}\n\nconst PARAM_BOUNDS: Record<string, ParamBound> = {\n scoreFloorRatio: { min: 0, max: 1, description: \"B1 score floor ratio (0–1)\" },\n elbowDropRatio: { min: 0, max: 1, description: \"B3 elbow drop ratio (0–1)\" },\n minMarginalValue: { min: 0, max: 1, description: \"B2 min marginal value (0–1)\" },\n symbolProximityBoost: { min: 1, max: 10, description: \"A2 symbol proximity multiplier (1–10)\" },\n importEdgeWeight: { min: 0, max: 1, description: \"import edge weight (0–1)\" },\n typeRefWeight: { min: 0, max: 1, description: \"type_ref edge weight (0–1)\" },\n defaultBudget: { min: 1, max: 100_000, integer: true, description: \"default token budget (1–100000)\" },\n defaultDepth: { min: 0, max: 10, integer: true, description: \"default graph depth (0–10)\" },\n};\n\n/**\n * Validate a single config value against its bounds.\n * Throws a descriptive error if the value is out of range.\n */\nfunction validateParam(\n key: string,\n value: unknown,\n configPath: string,\n): number {\n const bound = PARAM_BOUNDS[key];\n if (!bound) {\n throw new Error(\n `Unknown scoring parameter \"${key}\" in ${configPath}`,\n );\n }\n\n if (typeof value !== \"number\" || !isFinite(value)) {\n throw new Error(\n `Invalid value for \"${key}\" in ${configPath}: expected a number, got ${typeof value}`,\n );\n }\n\n if (bound.integer && !Number.isInteger(value)) {\n throw new Error(\n `Invalid value for \"${key}\" in ${configPath}: expected an integer, got ${value}` +\n `\\n ${bound.description}`,\n );\n }\n\n if (value < bound.min || value > bound.max) {\n throw new Error(\n `Out-of-bounds value for \"${key}\" in ${configPath}: ${value}` +\n `\\n ${bound.description}`,\n );\n }\n\n return value;\n}\n\n/**\n * Merge user-provided scoring overrides with defaults.\n * Throws on unknown keys, wrong types, or out-of-bounds values.\n */\nfunction mergeConfig(\n overrides: Record<string, unknown>,\n configPath: string,\n): ScoringConfig {\n const config = { ...DEFAULT_SCORING_CONFIG };\n const knownKeys = new Set([...Object.keys(PARAM_BOUNDS), \"tfBoostFormula\"]);\n\n for (const key of Object.keys(overrides)) {\n if (!knownKeys.has(key)) {\n throw new Error(\n `Unknown scoring parameter \"${key}\" in ${configPath}` +\n `\\n Valid parameters: ${[...knownKeys].join(\", \")}`,\n );\n }\n }\n\n for (const key of Object.keys(PARAM_BOUNDS)) {\n if (key in overrides) {\n const validated = validateParam(key, overrides[key], configPath);\n (config as Record<string, unknown>)[key] = validated;\n }\n }\n\n if (\"tfBoostFormula\" in overrides) {\n if (overrides.tfBoostFormula !== \"log2\") {\n throw new Error(\n `Invalid value for \"tfBoostFormula\" in ${configPath}: \"${String(overrides.tfBoostFormula)}\"` +\n `\\n Currently only \"log2\" is supported`,\n );\n }\n config.tfBoostFormula = \"log2\";\n }\n\n return config;\n}\n\n/**\n * Serialize current config to JSON (for --emit-config).\n */\nexport function serializeConfig(config: ScoringConfig): string {\n return JSON.stringify({ scoring: config }, null, 2);\n}\n"],"mappings":"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","names":["resolve","existsSync","readFileSync","require","resolve","readFileSync","existsSync"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
indexProject
|
|
3
|
+
} from "./chunk-FQ3L6YEU.js";
|
|
4
|
+
import "./chunk-472RLVFC.js";
|
|
5
|
+
|
|
6
|
+
// src/watcher.ts
|
|
7
|
+
import { extname } from "path";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
10
|
+
var IGNORE_PATTERNS = [
|
|
11
|
+
"**/node_modules/**",
|
|
12
|
+
"**/dist/**",
|
|
13
|
+
"**/.codefocus/**",
|
|
14
|
+
"**/__tests__/**",
|
|
15
|
+
"**/*.test.*",
|
|
16
|
+
"**/*.spec.*",
|
|
17
|
+
"**/*.d.ts"
|
|
18
|
+
];
|
|
19
|
+
async function startWatcher(options) {
|
|
20
|
+
const { rootDir, dbPath, onReindex, debounceMs = 300 } = options;
|
|
21
|
+
const chokidar = await import("chokidar");
|
|
22
|
+
let debounceTimer = null;
|
|
23
|
+
let isIndexing = false;
|
|
24
|
+
const watcher = chokidar.watch("**/*.{ts,tsx,js,jsx}", {
|
|
25
|
+
cwd: rootDir,
|
|
26
|
+
ignored: IGNORE_PATTERNS,
|
|
27
|
+
ignoreInitial: true,
|
|
28
|
+
persistent: true
|
|
29
|
+
});
|
|
30
|
+
function scheduleReindex() {
|
|
31
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
32
|
+
debounceTimer = setTimeout(async () => {
|
|
33
|
+
if (isIndexing) return;
|
|
34
|
+
isIndexing = true;
|
|
35
|
+
try {
|
|
36
|
+
const stats = await indexProject(rootDir, dbPath);
|
|
37
|
+
onReindex?.(stats);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
console.error(`[watch] Re-index error: ${message}`);
|
|
41
|
+
} finally {
|
|
42
|
+
isIndexing = false;
|
|
43
|
+
}
|
|
44
|
+
}, debounceMs);
|
|
45
|
+
}
|
|
46
|
+
watcher.on("add", (_path) => scheduleReindex());
|
|
47
|
+
watcher.on("change", (_path) => scheduleReindex());
|
|
48
|
+
watcher.on("unlink", (_path) => scheduleReindex());
|
|
49
|
+
return {
|
|
50
|
+
close() {
|
|
51
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
52
|
+
watcher.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function getGitChangedFiles(rootDir) {
|
|
57
|
+
try {
|
|
58
|
+
const result = execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
59
|
+
cwd: rootDir,
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
timeout: 1e4
|
|
62
|
+
});
|
|
63
|
+
return result.trim().split("\n").filter((f) => f && TS_EXTENSIONS.has(extname(f)));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
getGitChangedFiles,
|
|
70
|
+
startWatcher
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=watcher-6WHIBMPS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/watcher.ts"],"sourcesContent":["import { resolve, extname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport { indexProject, type IndexStats } from \"./indexer.js\";\n\nconst TS_EXTENSIONS = new Set([\".ts\", \".tsx\", \".js\", \".jsx\"]);\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/.codefocus/**\",\n \"**/__tests__/**\",\n \"**/*.test.*\",\n \"**/*.spec.*\",\n \"**/*.d.ts\",\n];\n\nexport interface WatcherOptions {\n rootDir: string;\n dbPath: string;\n /** Callback invoked after each re-index */\n onReindex?: (stats: IndexStats) => void;\n /** Debounce delay in ms (default: 300) */\n debounceMs?: number;\n}\n\nexport interface Watcher {\n /** Stop watching and clean up */\n close(): void;\n}\n\n/**\n * Watch for file changes and trigger incremental re-indexing.\n * Uses chokidar for reliable cross-platform file system watching.\n * Changes are debounced to avoid re-indexing on every keystroke.\n */\nexport async function startWatcher(options: WatcherOptions): Promise<Watcher> {\n const { rootDir, dbPath, onReindex, debounceMs = 300 } = options;\n\n const chokidar = await import(\"chokidar\");\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let isIndexing = false;\n\n const watcher = chokidar.watch(\"**/*.{ts,tsx,js,jsx}\", {\n cwd: rootDir,\n ignored: IGNORE_PATTERNS,\n ignoreInitial: true,\n persistent: true,\n });\n\n function scheduleReindex(): void {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(async () => {\n if (isIndexing) return;\n isIndexing = true;\n try {\n const stats = await indexProject(rootDir, dbPath);\n onReindex?.(stats);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`[watch] Re-index error: ${message}`);\n } finally {\n isIndexing = false;\n }\n }, debounceMs);\n }\n\n watcher.on(\"add\", (_path: string) => scheduleReindex());\n watcher.on(\"change\", (_path: string) => scheduleReindex());\n watcher.on(\"unlink\", (_path: string) => scheduleReindex());\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n\n/**\n * Get list of files changed since last git commit.\n * Useful for git-hook-triggered incremental re-indexing.\n */\nexport function getGitChangedFiles(rootDir: string): string[] {\n try {\n const result = execFileSync(\"git\", [\"diff\", \"--name-only\", \"HEAD\"], {\n cwd: rootDir,\n encoding: \"utf-8\",\n timeout: 10_000,\n });\n\n return result\n .trim()\n .split(\"\\n\")\n .filter((f) => f && TS_EXTENSIONS.has(extname(f)));\n } catch {\n return [];\n }\n}\n"],"mappings":";;;;;;AAAA,SAAkB,eAAe;AACjC,SAAS,oBAAoB;AAG7B,IAAM,gBAAgB,oBAAI,IAAI,CAAC,OAAO,QAAQ,OAAO,MAAM,CAAC;AAC5D,IAAM,kBAAkB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqBA,eAAsB,aAAa,SAA2C;AAC5E,QAAM,EAAE,SAAS,QAAQ,WAAW,aAAa,IAAI,IAAI;AAEzD,QAAM,WAAW,MAAM,OAAO,UAAU;AAExC,MAAI,gBAAsD;AAC1D,MAAI,aAAa;AAEjB,QAAM,UAAU,SAAS,MAAM,wBAAwB;AAAA,IACrD,KAAK;AAAA,IACL,SAAS;AAAA,IACT,eAAe;AAAA,IACf,YAAY;AAAA,EACd,CAAC;AAED,WAAS,kBAAwB;AAC/B,QAAI,cAAe,cAAa,aAAa;AAC7C,oBAAgB,WAAW,YAAY;AACrC,UAAI,WAAY;AAChB,mBAAa;AACb,UAAI;AACF,cAAM,QAAQ,MAAM,aAAa,SAAS,MAAM;AAChD,oBAAY,KAAK;AAAA,MACnB,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,gBAAQ,MAAM,2BAA2B,OAAO,EAAE;AAAA,MACpD,UAAE;AACA,qBAAa;AAAA,MACf;AAAA,IACF,GAAG,UAAU;AAAA,EACf;AAEA,UAAQ,GAAG,OAAO,CAAC,UAAkB,gBAAgB,CAAC;AACtD,UAAQ,GAAG,UAAU,CAAC,UAAkB,gBAAgB,CAAC;AACzD,UAAQ,GAAG,UAAU,CAAC,UAAkB,gBAAgB,CAAC;AAEzD,SAAO;AAAA,IACL,QAAQ;AACN,UAAI,cAAe,cAAa,aAAa;AAC7C,cAAQ,MAAM;AAAA,IAChB;AAAA,EACF;AACF;AAMO,SAAS,mBAAmB,SAA2B;AAC5D,MAAI;AACF,UAAM,SAAS,aAAa,OAAO,CAAC,QAAQ,eAAe,MAAM,GAAG;AAAA,MAClE,KAAK;AAAA,MACL,UAAU;AAAA,MACV,SAAS;AAAA,IACX,CAAC;AAED,WAAO,OACJ,KAAK,EACL,MAAM,IAAI,EACV,OAAO,CAAC,MAAM,KAAK,cAAc,IAAI,QAAQ,CAAC,CAAC,CAAC;AAAA,EACrD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db.ts","../src/root.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { mkdirSync, existsSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nconst require = createRequire(import.meta.url);\nconst Database = require(\"better-sqlite3\");\n\nexport interface FileRow {\n path: string;\n content_hash: string;\n language: string;\n last_indexed: number;\n}\n\nexport interface SymbolRow {\n id?: number;\n file_path: string;\n name: string;\n kind: string;\n start_byte: number;\n end_byte: number;\n start_line: number;\n end_line: number;\n start_column: number;\n end_column: number;\n signature: string | null;\n}\n\nexport interface ImportRow {\n id?: number;\n file_path: string;\n specifier: string;\n source_path: string | null;\n raw_module: string;\n is_type_only: number;\n}\n\nexport interface ReferenceRow {\n id?: number;\n source_symbol_id: number;\n target_symbol_id: number;\n ref_type: string;\n}\n\nexport class IndexDatabase {\n private db: InstanceType<typeof Database>;\n\n constructor(dbPath: string) {\n const dir = dirname(dbPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n this.db = new Database(dbPath);\n this.db.pragma(\"journal_mode = WAL\");\n this.db.pragma(\"foreign_keys = ON\");\n this.init();\n }\n\n private init(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS files (\n path TEXT PRIMARY KEY,\n content_hash TEXT NOT NULL,\n language TEXT,\n last_indexed INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS symbols (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n file_path TEXT NOT NULL,\n name TEXT NOT NULL,\n kind TEXT NOT NULL,\n start_byte INTEGER,\n end_byte INTEGER,\n start_line INTEGER,\n end_line INTEGER,\n start_column INTEGER,\n end_column INTEGER,\n signature TEXT,\n FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE\n );\n\n CREATE TABLE IF NOT EXISTS imports (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n file_path TEXT NOT NULL,\n specifier TEXT NOT NULL,\n source_path TEXT,\n raw_module TEXT NOT NULL,\n is_type_only INTEGER DEFAULT 0,\n FOREIGN KEY (file_path) REFERENCES files(path) ON DELETE CASCADE\n );\n\n CREATE TABLE IF NOT EXISTS \"references\" (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n source_symbol_id INTEGER NOT NULL,\n target_symbol_id INTEGER NOT NULL,\n ref_type TEXT NOT NULL DEFAULT 'import',\n FOREIGN KEY (source_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE,\n FOREIGN KEY (target_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);\n CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);\n CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);\n CREATE INDEX IF NOT EXISTS idx_imports_file ON imports(file_path);\n CREATE INDEX IF NOT EXISTS idx_imports_source ON imports(source_path);\n CREATE INDEX IF NOT EXISTS idx_refs_source ON \"references\"(source_symbol_id);\n CREATE INDEX IF NOT EXISTS idx_refs_target ON \"references\"(target_symbol_id);\n\n CREATE VIRTUAL TABLE IF NOT EXISTS content_fts USING fts5(\n file_path,\n content,\n tokenize='unicode61'\n );\n `);\n }\n\n clearFile(filePath: string): void {\n this.db.prepare(\"DELETE FROM symbols WHERE file_path = ?\").run(filePath);\n this.db.prepare(\"DELETE FROM imports WHERE file_path = ?\").run(filePath);\n this.db.prepare(\"DELETE FROM files WHERE path = ?\").run(filePath);\n }\n\n clearAllImports(): void {\n this.db.prepare(\"DELETE FROM imports\").run();\n }\n\n clearAllReferences(): void {\n this.db.prepare('DELETE FROM \"references\"').run();\n }\n\n upsertFile(file: FileRow): void {\n this.db\n .prepare(\n `INSERT OR REPLACE INTO files (path, content_hash, language, last_indexed)\n VALUES (@path, @content_hash, @language, @last_indexed)`,\n )\n .run(file);\n }\n\n insertSymbol(symbol: SymbolRow): number {\n const result = this.db\n .prepare(\n `INSERT INTO symbols (file_path, name, kind, start_byte, end_byte, start_line, end_line, start_column, end_column, signature)\n VALUES (@file_path, @name, @kind, @start_byte, @end_byte, @start_line, @end_line, @start_column, @end_column, @signature)`,\n )\n .run(symbol);\n return Number(result.lastInsertRowid);\n }\n\n insertImport(imp: ImportRow): number {\n const result = this.db\n .prepare(\n `INSERT INTO imports (file_path, specifier, source_path, raw_module, is_type_only)\n VALUES (@file_path, @specifier, @source_path, @raw_module, @is_type_only)`,\n )\n .run(imp);\n return Number(result.lastInsertRowid);\n }\n\n insertReference(ref: ReferenceRow): void {\n this.db\n .prepare(\n `INSERT INTO \"references\" (source_symbol_id, target_symbol_id, ref_type)\n VALUES (@source_symbol_id, @target_symbol_id, @ref_type)`,\n )\n .run(ref);\n }\n\n upsertFileContent(filePath: string, content: string): void {\n // Delete old entry first (FTS5 doesn't support REPLACE)\n this.db.prepare(\"DELETE FROM content_fts WHERE file_path = ?\").run(filePath);\n this.db\n .prepare(\"INSERT INTO content_fts (file_path, content) VALUES (?, ?)\")\n .run(filePath, content);\n }\n\n /**\n * Search file content using FTS5 full-text search.\n * Returns files with BM25 relevance scores and matching line ranges.\n */\n searchContent(\n term: string,\n ): Array<{ file_path: string; rank: number }> {\n // FTS5 rank is negative (more negative = more relevant), so we negate it\n return this.db\n .prepare(\n `SELECT file_path, -rank AS rank\n FROM content_fts\n WHERE content_fts MATCH ?\n ORDER BY rank DESC`,\n )\n .all(term) as Array<{ file_path: string; rank: number }>;\n }\n\n getFileHash(filePath: string): string | undefined {\n const row = this.db\n .prepare(\"SELECT content_hash FROM files WHERE path = ?\")\n .get(filePath) as { content_hash: string } | undefined;\n return row?.content_hash;\n }\n\n getSymbolsByFile(filePath: string): SymbolRow[] {\n return this.db\n .prepare(\"SELECT * FROM symbols WHERE file_path = ?\")\n .all(filePath) as SymbolRow[];\n }\n\n getSymbolById(id: number): SymbolRow | undefined {\n return this.db\n .prepare(\"SELECT * FROM symbols WHERE id = ?\")\n .get(id) as SymbolRow | undefined;\n }\n\n getSymbolByName(name: string): SymbolRow | undefined {\n return this.db\n .prepare(\"SELECT * FROM symbols WHERE name = ?\")\n .get(name) as SymbolRow | undefined;\n }\n\n findSymbolsByName(name: string): SymbolRow[] {\n return this.db\n .prepare(\"SELECT * FROM symbols WHERE name LIKE ?\")\n .all(`%${name}%`) as SymbolRow[];\n }\n\n findSymbols(name: string, kind?: string): SymbolRow[] {\n if (kind && kind !== \"all\") {\n return this.db\n .prepare(\n \"SELECT * FROM symbols WHERE name LIKE ? AND kind = ? ORDER BY file_path, start_line\",\n )\n .all(`%${name}%`, kind) as SymbolRow[];\n }\n return this.db\n .prepare(\n \"SELECT * FROM symbols WHERE name LIKE ? ORDER BY file_path, start_line\",\n )\n .all(`%${name}%`) as SymbolRow[];\n }\n\n /** Get all file-to-file import edges (for file-level graph). */\n getFileImportEdges(): Array<{\n source_file: string;\n target_file: string;\n specifiers: string;\n has_type_only: number;\n }> {\n return this.db\n .prepare(\n `SELECT file_path AS source_file,\n source_path AS target_file,\n GROUP_CONCAT(specifier, ', ') AS specifiers,\n MAX(is_type_only) AS has_type_only\n FROM imports\n WHERE source_path IS NOT NULL\n GROUP BY file_path, source_path\n ORDER BY file_path, source_path`,\n )\n .all() as Array<{\n source_file: string;\n target_file: string;\n specifiers: string;\n has_type_only: number;\n }>;\n }\n\n /** Get outgoing symbol references (symbols this symbol depends on). */\n getOutgoingReferences(\n symbolId: number,\n ): Array<{\n target_id: number;\n target_name: string;\n target_kind: string;\n target_file: string;\n target_line: number;\n ref_type: string;\n }> {\n return this.db\n .prepare(\n `SELECT s.id AS target_id, s.name AS target_name, s.kind AS target_kind,\n s.file_path AS target_file, s.start_line AS target_line, r.ref_type\n FROM \"references\" r\n JOIN symbols s ON r.target_symbol_id = s.id\n WHERE r.source_symbol_id = ?\n ORDER BY s.file_path, s.start_line`,\n )\n .all(symbolId) as Array<{\n target_id: number;\n target_name: string;\n target_kind: string;\n target_file: string;\n target_line: number;\n ref_type: string;\n }>;\n }\n\n /** Get incoming symbol references (symbols that depend on this symbol). */\n getIncomingReferences(\n symbolId: number,\n ): Array<{\n source_id: number;\n source_name: string;\n source_kind: string;\n source_file: string;\n source_line: number;\n ref_type: string;\n }> {\n return this.db\n .prepare(\n `SELECT s.id AS source_id, s.name AS source_name, s.kind AS source_kind,\n s.file_path AS source_file, s.start_line AS source_line, r.ref_type\n FROM \"references\" r\n JOIN symbols s ON r.source_symbol_id = s.id\n WHERE r.target_symbol_id = ?\n ORDER BY s.file_path, s.start_line`,\n )\n .all(symbolId) as Array<{\n source_id: number;\n source_name: string;\n source_kind: string;\n source_file: string;\n source_line: number;\n ref_type: string;\n }>;\n }\n\n /** Get in-degree (number of incoming references) for a symbol. */\n getSymbolInDegree(symbolId: number): number {\n const row = this.db\n .prepare(\n 'SELECT COUNT(*) as count FROM \"references\" WHERE target_symbol_id = ?',\n )\n .get(symbolId) as { count: number };\n return row.count;\n }\n\n /** Get all files in the index. */\n getAllFiles(): FileRow[] {\n return this.db\n .prepare(\"SELECT * FROM files ORDER BY path\")\n .all() as FileRow[];\n }\n\n countFiles(): number {\n const row = this.db\n .prepare(\"SELECT COUNT(*) as count FROM files\")\n .get() as { count: number };\n return row.count;\n }\n\n countSymbols(): number {\n const row = this.db\n .prepare(\"SELECT COUNT(*) as count FROM symbols\")\n .get() as { count: number };\n return row.count;\n }\n\n countImports(): number {\n const row = this.db\n .prepare(\"SELECT COUNT(*) as count FROM imports\")\n .get() as { count: number };\n return row.count;\n }\n\n countReferences(): number {\n const row = this.db\n .prepare('SELECT COUNT(*) as count FROM \"references\"')\n .get() as { count: number };\n return row.count;\n }\n\n transaction<T>(fn: () => T): T {\n return this.db.transaction(fn)();\n }\n\n close(): void {\n this.db.close();\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\n\nconst ROOT_MARKERS = [\".git\", \"package.json\"] as const;\n\n/**\n * Walk up from `startDir` looking for a directory that contains a root\n * marker (`.git` directory or `package.json`). Returns the first match,\n * or `null` if the filesystem root is reached without finding one.\n *\n * `.git` is checked first so that in a monorepo the repository root wins\n * over a nested package.json.\n */\nexport function findProjectRoot(startDir: string): string | null {\n let dir = resolve(startDir);\n // eslint-disable-next-line no-constant-condition\n while (true) {\n for (const marker of ROOT_MARKERS) {\n if (existsSync(resolve(dir, marker))) return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) return null; // filesystem root\n dir = parent;\n }\n}\n\n/**\n * Resolve the effective project root:\n * 1. Explicit `--root` flag (highest priority)\n * 2. Auto-detected via `.git` / `package.json` walk-up\n * 3. Current working directory (fallback)\n */\nexport function resolveRoot(flagRoot: string | boolean | undefined): string {\n if (typeof flagRoot === \"string\" && flagRoot) return resolve(flagRoot);\n return findProjectRoot(process.cwd()) ?? resolve(\".\");\n}\n"],"mappings":"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","names":["require","existsSync","dirname"]}
|
package/dist/mcp-7WYTXIQS.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { resolve } from \"node:path\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { IndexDatabase } from \"./db.js\";\nimport { resolveRoot } from \"./root.js\";\n\nconst require = createRequire(import.meta.url);\nconst pkg = require(\"../package.json\");\n\n// ── shared state ────────────────────────────────────────────────────────\n// The MCP server keeps the database connection and tiktoken encoder warm\n// so that subsequent tool calls pay ~10ms instead of ~400ms cold start.\n\nlet db: IndexDatabase | null = null;\nlet rootDir: string = \"\";\n\nfunction getDb(root: string): IndexDatabase {\n if (db && rootDir === root) return db;\n const dbPath = resolve(root, \".codefocus\", \"index.db\");\n if (!existsSync(dbPath)) {\n throw new Error(\n `no index found at ${dbPath}\\nRun 'codefocus index --root ${root}' first.`,\n );\n }\n if (db) db.close();\n db = new IndexDatabase(dbPath);\n rootDir = root;\n return db;\n}\n\n// ── MCP server setup ────────────────────────────────────────────────────\n\nexport function createMcpServer(defaultRoot?: string): McpServer {\n const server = new McpServer({\n name: \"codefocus\",\n version: pkg.version,\n });\n\n const effectiveRoot = defaultRoot ?? resolveRoot(undefined);\n\n // ── tool: query ─────────────────────────────────────────────────────\n\n server.tool(\n \"query\",\n \"Search the codebase and return ranked, budget-constrained code context. \" +\n \"Returns YAML front matter with confidence metadata followed by relevant source code sections.\",\n {\n term: z.string().describe(\"Search term (symbol name or keyword)\"),\n budget: z\n .number()\n .int()\n .positive()\n .default(8000)\n .describe(\"Token budget for output (default: 8000)\"),\n depth: z\n .number()\n .int()\n .min(0)\n .default(2)\n .describe(\"Max graph traversal depth (default: 2)\"),\n root: z\n .string()\n .optional()\n .describe(\"Root directory of indexed project\"),\n },\n async ({ term, budget, depth, root }) => {\n const r = root ?? effectiveRoot;\n // Import runQuery's internal logic so we can capture output\n const output = await captureQueryOutput(r, term, budget, depth);\n return { content: [{ type: \"text\" as const, text: output }] };\n },\n );\n\n // ── tool: find ──────────────────────────────────────────────────────\n\n server.tool(\n \"find\",\n \"Quick symbol lookup by name with optional kind filter. \" +\n \"Returns symbol names, kinds, file paths, and line numbers.\",\n {\n symbol: z.string().describe(\"Symbol name to search for\"),\n kind: z\n .enum([\n \"function\",\n \"class\",\n \"interface\",\n \"type\",\n \"enum\",\n \"variable\",\n \"method\",\n \"all\",\n ])\n .default(\"all\")\n .describe(\"Filter by symbol kind (default: all)\"),\n root: z\n .string()\n .optional()\n .describe(\"Root directory of indexed project\"),\n },\n async ({ symbol, kind, root }) => {\n const r = root ?? effectiveRoot;\n const database = getDb(r);\n const results = database.findSymbols(symbol, kind);\n\n if (results.length === 0) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `No symbols found matching \"${symbol}\"${kind !== \"all\" ? ` (kind: ${kind})` : \"\"}`,\n },\n ],\n };\n }\n\n const lines: string[] = [\n `Found ${results.length} symbol${results.length !== 1 ? \"s\" : \"\"} matching \"${symbol}\"${kind !== \"all\" ? ` (kind: ${kind})` : \"\"}:`,\n \"\",\n ];\n\n for (const sym of results) {\n const sig = sym.signature ? ` ${sym.signature}` : \"\";\n lines.push(\n ` ${sym.name} ${sym.kind} ${sym.file_path}:${sym.start_line}${sig}`,\n );\n }\n\n return { content: [{ type: \"text\" as const, text: lines.join(\"\\n\") }] };\n },\n );\n\n // ── tool: graph ─────────────────────────────────────────────────────\n\n server.tool(\n \"graph\",\n \"Show the dependency graph for a file or symbol. \" +\n \"Displays incoming and/or outgoing edges as an indented tree.\",\n {\n target: z\n .string()\n .describe(\"File path or symbol name to show graph for\"),\n direction: z\n .enum([\"both\", \"incoming\", \"outgoing\"])\n .default(\"both\")\n .describe(\"Graph direction (default: both)\"),\n root: z\n .string()\n .optional()\n .describe(\"Root directory of indexed project\"),\n },\n async ({ target, direction, root }) => {\n const r = root ?? effectiveRoot;\n const output = captureGraphOutput(r, target, direction);\n return { content: [{ type: \"text\" as const, text: output }] };\n },\n );\n\n // ── tool: map ───────────────────────────────────────────────────────\n\n server.tool(\n \"map\",\n \"High-level codebase overview ranked by PageRank connectivity. \" +\n \"Returns file paths with their top symbols and signatures.\",\n {\n budget: z\n .number()\n .int()\n .positive()\n .default(2000)\n .describe(\"Token budget for output (default: 2000)\"),\n root: z\n .string()\n .optional()\n .describe(\"Root directory of indexed project\"),\n },\n async ({ budget, root }) => {\n const r = root ?? effectiveRoot;\n const output = captureMapOutput(r, budget);\n return { content: [{ type: \"text\" as const, text: output }] };\n },\n );\n\n return server;\n}\n\n// ── output capture helpers ──────────────────────────────────────────────\n// These re-implement the command logic to return strings instead of\n// writing to stdout, keeping the database connection warm.\n\nasync function captureQueryOutput(\n root: string,\n term: string,\n budget: number,\n depth: number,\n): Promise<string> {\n const { runQueryCore } = await import(\"./commands/query.js\");\n return runQueryCore(root, term, budget, depth);\n}\n\nfunction captureGraphOutput(\n root: string,\n target: string,\n direction: \"both\" | \"incoming\" | \"outgoing\",\n): string {\n const database = getDb(root);\n\n const isFile =\n target.includes(\"/\") || target.includes(\"\\\\\") || /\\.\\w+$/.test(target);\n\n if (isFile) {\n return renderFileGraphToString(database, target, direction);\n } else {\n return renderSymbolGraphToString(database, target, direction);\n }\n}\n\nfunction renderFileGraphToString(\n database: IndexDatabase,\n target: string,\n direction: \"both\" | \"incoming\" | \"outgoing\",\n): string {\n const require = createRequire(import.meta.url);\n const { DirectedGraph } = require(\"graphology\");\n\n const graph = new DirectedGraph();\n for (const file of database.getAllFiles()) {\n graph.addNode(file.path);\n }\n for (const edge of database.getFileImportEdges()) {\n const key = `${edge.source_file}->${edge.target_file}`;\n if (graph.hasEdge(key)) {\n const existing = graph.getEdgeAttributes(key);\n graph.setEdgeAttribute(\n key,\n \"specifiers\",\n `${existing.specifiers}, ${edge.specifiers}`,\n );\n } else {\n graph.addEdgeWithKey(key, edge.source_file, edge.target_file, {\n specifiers: edge.specifiers,\n });\n }\n }\n\n if (!graph.hasNode(target)) {\n return `Error: file \"${target}\" not found in the index`;\n }\n\n const sections: string[] = [];\n\n if (direction === \"outgoing\" || direction === \"both\") {\n const outEdges: string[] = [];\n graph.forEachOutEdge(\n target,\n (\n _edge: string,\n attrs: { specifiers: string },\n _src: string,\n tgt: string,\n ) => {\n outEdges.push(` ${tgt} (imports: ${attrs.specifiers})`);\n },\n );\n sections.push(\n outEdges.length > 0\n ? `Dependencies (outgoing):\\n${outEdges.join(\"\\n\")}`\n : \"Dependencies (outgoing): (none)\",\n );\n }\n\n if (direction === \"incoming\" || direction === \"both\") {\n const inEdges: string[] = [];\n graph.forEachInEdge(\n target,\n (\n _edge: string,\n attrs: { specifiers: string },\n src: string,\n ) => {\n inEdges.push(` ${src} (imports: ${attrs.specifiers})`);\n },\n );\n sections.push(\n inEdges.length > 0\n ? `Dependents (incoming):\\n${inEdges.join(\"\\n\")}`\n : \"Dependents (incoming): (none)\",\n );\n }\n\n return `${target}\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\nfunction renderSymbolGraphToString(\n database: IndexDatabase,\n target: string,\n direction: \"both\" | \"incoming\" | \"outgoing\",\n): string {\n const symbols = database.findSymbolsByName(target);\n const exactMatch = symbols.find((s) => s.name === target);\n const sym = exactMatch ?? symbols[0];\n\n if (!sym || !sym.id) {\n return `Error: symbol \"${target}\" not found in the index`;\n }\n\n const heading = `${sym.name} (${sym.kind}) — ${sym.file_path}:${sym.start_line}`;\n const sections: string[] = [];\n\n if (direction === \"outgoing\" || direction === \"both\") {\n const refs = database.getOutgoingReferences(sym.id);\n const lines = refs.map(\n (r) =>\n ` ${r.target_name} (${r.target_kind}) — ${r.target_file}:${r.target_line} [${r.ref_type}]`,\n );\n sections.push(\n lines.length > 0\n ? `Dependencies (outgoing):\\n${lines.join(\"\\n\")}`\n : \"Dependencies (outgoing): (none)\",\n );\n }\n\n if (direction === \"incoming\" || direction === \"both\") {\n const refs = database.getIncomingReferences(sym.id);\n const lines = refs.map(\n (r) =>\n ` ${r.source_name} (${r.source_kind}) — ${r.source_file}:${r.source_line} [${r.ref_type}]`,\n );\n sections.push(\n lines.length > 0\n ? `Dependents (incoming):\\n${lines.join(\"\\n\")}`\n : \"Dependents (incoming): (none)\",\n );\n }\n\n return `${heading}\\n\\n${sections.join(\"\\n\\n\")}`;\n}\n\nfunction captureMapOutput(root: string, budget: number): string {\n const require = createRequire(import.meta.url);\n const { DirectedGraph } = require(\"graphology\");\n const pagerank = require(\"graphology-metrics/centrality/pagerank\");\n const { getEncoding } = require(\"js-tiktoken\");\n\n const database = getDb(root);\n const files = database.getAllFiles();\n\n if (files.length === 0) {\n return \"[map] Index is empty — no files to map.\";\n }\n\n // Compute PageRank\n const graph = new DirectedGraph();\n for (const file of files) {\n graph.addNode(file.path);\n }\n for (const edge of database.getFileImportEdges()) {\n const key = `${edge.source_file}->${edge.target_file}`;\n if (!graph.hasEdge(key)) {\n graph.addEdgeWithKey(key, edge.source_file, edge.target_file);\n }\n }\n const ranks: Record<string, number> =\n graph.order > 0 ? pagerank(graph, { getEdgeWeight: null }) : {};\n\n const rankedFiles = files\n .map((f) => ({ path: f.path, rank: ranks[f.path] ?? 0 }))\n .sort((a, b) => b.rank - a.rank || a.path.localeCompare(b.path));\n\n const enc = getEncoding(\"cl100k_base\");\n const blocks: string[] = [];\n let tokenCount = 0;\n let truncated = false;\n\n for (const file of rankedFiles) {\n const symbols = database.getSymbolsByFile(file.path);\n symbols.sort((a, b) => a.start_line - b.start_line);\n const lines: string[] = [file.path];\n for (const sym of symbols) {\n if (sym.kind === \"variable\" && !sym.signature) continue;\n const label = sym.signature ?? `${sym.kind} ${sym.name}`;\n lines.push(` ${label}`);\n }\n const block = lines.join(\"\\n\");\n const blockTokens = enc.encode(block).length;\n\n if (tokenCount + blockTokens <= budget) {\n blocks.push(block);\n tokenCount += blockTokens;\n continue;\n }\n\n const blockLines = block.split(\"\\n\");\n let partial = blockLines[0];\n let partialTokens = enc.encode(partial).length;\n\n if (tokenCount + partialTokens > budget) {\n truncated = true;\n break;\n }\n\n for (let i = 1; i < blockLines.length; i++) {\n const candidate = partial + \"\\n\" + blockLines[i];\n const candidateTokens = enc.encode(candidate).length;\n if (tokenCount + candidateTokens > budget) break;\n partial = candidate;\n partialTokens = candidateTokens;\n }\n\n blocks.push(partial);\n tokenCount += partialTokens;\n truncated = true;\n break;\n }\n\n const shown = blocks.length;\n const total = rankedFiles.length;\n const parts = [`[map] ${shown}/${total} files, ~${tokenCount} tokens`];\n if (truncated) {\n parts.push(`(budget: ${budget}, truncated)`);\n }\n\n return blocks.join(\"\\n\\n\") + `\\n\\n${parts.join(\" \")}`;\n}\n\n// ── serve command entry point ───────────────────────────────────────────\n\nexport async function runServe(\n _positional: string[],\n flags: Record<string, string | boolean>,\n): Promise<void> {\n if (flags.help) {\n console.error(`codefocus serve — Start MCP server (stdio transport)\n\nUsage: codefocus serve [options]\n\nOptions:\n --root <path> Root directory of indexed project (default: auto-detect)\n --help Show this help message\n\nThe MCP server exposes four tools:\n query Search and return ranked code context\n find Quick symbol lookup\n graph Show dependency graph for a file or symbol\n map High-level codebase overview\n\nThe server keeps the database connection and tiktoken encoder warm,\nso subsequent tool calls complete in ~10ms instead of ~400ms.\n\nUsage with Claude Code:\n Add to .mcp.json:\n {\n \"mcpServers\": {\n \"codefocus\": {\n \"command\": \"npx\",\n \"args\": [\"codefocus\", \"serve\", \"--root\", \"/path/to/project\"]\n }\n }\n }`);\n return;\n }\n\n const root = resolveRoot(flags.root);\n const dbPath = resolve(root, \".codefocus\", \"index.db\");\n\n if (!existsSync(dbPath)) {\n console.error(\n `Error: no index found at ${dbPath}\\nRun 'codefocus index --root ${root}' first.`,\n );\n process.exitCode = 1;\n return;\n }\n\n // Pre-warm the database connection\n getDb(root);\n console.error(`[codefocus] MCP server starting (root: ${root})`);\n console.error(`[codefocus] Database: ${dbPath}`);\n\n const server = createMcpServer(root);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n console.error(`[codefocus] MCP server ready (4 tools: query, find, graph, map)`);\n}\n"],"mappings":"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","names":["require"]}
|