ctxo-mcp 0.3.0 → 0.3.1

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.
package/README.md CHANGED
@@ -0,0 +1,188 @@
1
+ # Ctxo
2
+
3
+ **Code intelligence for AI agents — one call instead of hundreds.**
4
+
5
+ AI coding assistants waste context window reading files one by one, still missing dependencies. Ctxo gives them the full picture in a single MCP call: symbol graphs, blast radius, git intent, and risk scores.
6
+
7
+ ```
8
+ Context per query set (full codebase investigation):
9
+
10
+ Manual ████████████████████████████████████████ 140,000 tokens
11
+ Ctxo █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2,900 tokens → 48x less
12
+
13
+ Tool calls per query set:
14
+
15
+ Manual ████████████████████████████████████████ 409+ calls
16
+ Ctxo █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8 calls → 51x fewer
17
+
18
+ Context after 10 investigation rounds:
19
+
20
+ Manual ██████████████████████████████████████████ 140% — OOM at round 7
21
+ Ctxo █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2.9% — 97% free for coding
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```Shell
27
+ # 1. Index your codebase
28
+ npx ctxo-mcp index
29
+
30
+ # 2. Add to your IDE's MCP config
31
+ # (see IDE Setup below)
32
+
33
+ # 3. Start using — your AI assistant now has 14 code intelligence tools
34
+ ```
35
+
36
+ ## IDE Setup
37
+
38
+ **Claude Code / Cursor / Windsurf / Cline** — `.mcp.json`:
39
+
40
+ ```JSON
41
+ { "mcpServers": { "ctxo": { "command": "npx", "args": ["-y", "ctxo-mcp"] } } }
42
+ ```
43
+
44
+ **VS Code (Copilot)** — `.vscode/mcp.json`:
45
+
46
+ ```JSON
47
+ { "servers": { "ctxo": { "type": "stdio", "command": "npx", "args": ["-y", "ctxo-mcp"] } } }
48
+ ```
49
+
50
+ **Zed** — `settings.json`:
51
+
52
+ ```JSON
53
+ { "context_servers": { "ctxo": { "command": { "path": "npx", "args": ["-y", "ctxo-mcp"] } } } }
54
+ ```
55
+
56
+ ## 14 Tools
57
+
58
+ | Tool | What it does |
59
+ | --------------------------- | ---------------------------------------------------------------- |
60
+ | `get_logic_slice` | Symbol + transitive dependencies (L1-L4 progressive detail) |
61
+ | `get_blast_radius` | What breaks if this changes (3-tier: confirmed/likely/potential) |
62
+ | `get_architectural_overlay` | Project layer map (Domain/Infrastructure/Adapter) |
63
+ | `get_why_context` | Git commit intent + anti-pattern warnings (reverts, rollbacks) |
64
+ | `get_change_intelligence` | Complexity x churn composite score |
65
+ | `find_dead_code` | Unreachable symbols, unused exports, scaffolding markers |
66
+ | `get_context_for_task` | Task-optimized context (fix/extend/refactor/understand) |
67
+ | `get_ranked_context` | BM25 + PageRank search within token budget |
68
+ | `search_symbols` | Symbol name/regex search across index |
69
+ | `get_changed_symbols` | Symbols in recently changed files (git diff) |
70
+ | `find_importers` | Reverse dependency lookup ("who uses this?") |
71
+ | `get_class_hierarchy` | Class inheritance tree (ancestors + descendants) |
72
+ | `get_symbol_importance` | PageRank centrality ranking |
73
+ | `get_pr_impact` | Full PR risk assessment in a single call |
74
+
75
+ ## Tool Selection Guide
76
+
77
+ ```
78
+ Reviewing a PR? → get_pr_impact
79
+ About to modify code? → get_blast_radius → get_why_context
80
+ Understanding a symbol? → get_context_for_task(taskType: "understand")
81
+ Fixing a bug? → get_context_for_task(taskType: "fix")
82
+ Refactoring? → get_context_for_task(taskType: "refactor")
83
+ Don't know the name? → search_symbols or get_ranked_context
84
+ Finding unused code? → find_dead_code
85
+ Safe to delete? → find_importers
86
+ Onboarding? → get_architectural_overlay → get_symbol_importance
87
+ ```
88
+
89
+ ## CLI Commands
90
+
91
+ ```Shell
92
+ npx ctxo-mcp index # Build full codebase index
93
+ npx ctxo-mcp index --check # CI gate: fail if index stale
94
+ npx ctxo-mcp index --skip-history # Fast re-index without git history
95
+ npx ctxo-mcp watch # File watcher for incremental re-index
96
+ npx ctxo-mcp init # Install git hooks (post-commit, post-merge)
97
+ npx ctxo-mcp status # Show index manifest
98
+ npx ctxo-mcp sync # Rebuild SQLite cache from committed JSON
99
+ ```
100
+
101
+ ## Features
102
+
103
+ **Response Envelope** — All responses include `_meta` with item counts, truncation info, and drill-in hints. Large results auto-truncated at 8KB (configurable via `CTXO_RESPONSE_LIMIT`).
104
+
105
+ **Intent Filtering** — 4 tools accept `intent` parameter for keyword-based result filtering. `get_blast_radius(symbolId, intent: "test")` returns only test-related impacts.
106
+
107
+ **Tool Annotations** — All tools declare `readOnlyHint: true`, `idempotentHint: true`, `openWorldHint: false` for safe auto-approval in agent frameworks.
108
+
109
+ **Privacy Masking** — AWS keys, GCP service accounts, Azure connection strings, JWTs, private IPs, env secrets automatically redacted. Extensible via `.ctxo/masking.json`.
110
+
111
+ **Debug Mode** — `DEBUG=ctxo:*` for all debug output, or `DEBUG=ctxo:git,ctxo:storage` for specific namespaces.
112
+
113
+ **Per-tool savings vs manual approach:**
114
+
115
+ ```
116
+ Manual Tokens Ctxo Tokens Savings
117
+ get_logic_slice ████████ 1,950 █ 150 92%
118
+ get_blast_radius ███ 800 ██ 600 25%
119
+ get_overlay ████████████ 25K██ 500 98%
120
+ get_why_context █ 200 █ 200 0%
121
+ get_change_intelligence ████████ 2,100 ▏ 50 98%
122
+ find_dead_code █████████ 5,000 ████ 2,000 60%
123
+ ────────────────────────────────────────────────────────────
124
+ TOTAL 35,050 tokens 3,500 tokens 90%
125
+ 329+ calls 6 calls 98%
126
+ ```
127
+
128
+ ## Agentic AI Usage
129
+
130
+ **Claude Agent SDK:**
131
+
132
+ ```TypeScript
133
+ import { query } from "@anthropic-ai/claude-agent-sdk";
134
+
135
+ for await (const message of query({
136
+ prompt: "Analyze the blast radius of AuthService",
137
+ options: {
138
+ mcpServers: { ctxo: { command: "npx", args: ["-y", "ctxo-mcp"] } },
139
+ allowedTools: ["mcp__ctxo__*"]
140
+ }
141
+ })) { /* ... */ }
142
+ ```
143
+
144
+ **OpenAI Agents SDK:**
145
+
146
+ ```Python
147
+ from agents import Agent, Runner
148
+ from agents.mcp import MCPServerStdio
149
+
150
+ async with MCPServerStdio(params={"command": "npx", "args": ["ctxo-mcp"]}) as ctxo:
151
+ agent = Agent(name="Reviewer", mcp_servers=[ctxo])
152
+ result = await Runner.run(agent, "Review the PR impact")
153
+ ```
154
+
155
+ See [Agentic AI Integration Guide](docs/agentic-ai-integration.md) for LangChain, raw MCP client, and CI/CD examples.
156
+
157
+ ## Multi-Language Support
158
+
159
+ | Language | Parser | Tier | Features |
160
+ | --------------------- | ----------- | ------ | ---------------------------------------------------------------- |
161
+ | TypeScript/JavaScript | ts-morph | Full | Type-aware resolution, cross-file imports, `this.method()` calls |
162
+ | Go | tree-sitter | Syntax | Structs, interfaces, functions, methods, import edges |
163
+ | C# | tree-sitter | Syntax | Classes, interfaces, methods, enums, namespace qualification |
164
+
165
+ ## How It Works
166
+
167
+ Ctxo builds a **committed JSON index** (`.ctxo/index/`) that captures symbols, dependency edges, git history, and co-change data. The MCP server reads this index to answer queries — no runtime parsing, no external services.
168
+
169
+ ```
170
+ .ctxo/
171
+ index/ ← committed (per-file JSON, reviewable in PRs)
172
+ .cache/ ← gitignored (local SQLite, auto-rebuilt)
173
+ config.yaml ← committed (team settings)
174
+ masking.json ← committed (custom masking patterns)
175
+ ```
176
+
177
+ ## Links
178
+
179
+ * [npm](https://www.npmjs.com/package/ctxo-mcp)
180
+ * [Changelog](CHANGELOG.md)
181
+ * [LLM Reference](llms-full.txt)
182
+ * [Validation Runbook](docs/runbook/mcp-validation/mcp-validation.md)
183
+ * [Architecture](docs/artifacts/architecture.md)
184
+ * [PRD](docs/artifacts/prd.md)
185
+
186
+ ## License
187
+
188
+ MIT
@@ -3,10 +3,43 @@ import {
3
3
  JsonIndexReader
4
4
  } from "./chunk-N6GPODUY.js";
5
5
 
6
+ // src/core/logger.ts
7
+ function isDebugEnabled(namespace) {
8
+ const debugEnv = process.env["DEBUG"] ?? "";
9
+ if (!debugEnv) return false;
10
+ const patterns = debugEnv.split(",").map((p) => p.trim());
11
+ return patterns.some((p) => {
12
+ if (p === "*" || p === "ctxo:*") return true;
13
+ if (p === namespace) return true;
14
+ if (p.endsWith(":*") && namespace.startsWith(p.slice(0, -1))) return true;
15
+ return false;
16
+ });
17
+ }
18
+ function createLogger(namespace) {
19
+ const prefix = `[${namespace}]`;
20
+ return {
21
+ debug(message, ...args) {
22
+ if (isDebugEnabled(namespace)) {
23
+ console.error(`${prefix} DEBUG ${message}`, ...args);
24
+ }
25
+ },
26
+ info(message, ...args) {
27
+ console.error(`${prefix} ${message}`, ...args);
28
+ },
29
+ warn(message, ...args) {
30
+ console.error(`${prefix} WARN ${message}`, ...args);
31
+ },
32
+ error(message, ...args) {
33
+ console.error(`${prefix} ERROR ${message}`, ...args);
34
+ }
35
+ };
36
+ }
37
+
6
38
  // src/adapters/storage/sqlite-storage-adapter.ts
7
39
  import initSqlJs from "sql.js";
8
40
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
9
41
  import { join, dirname } from "path";
42
+ var log = createLogger("ctxo:storage");
10
43
  var SqliteStorageAdapter = class {
11
44
  db;
12
45
  dbPath;
@@ -23,7 +56,7 @@ var SqliteStorageAdapter = class {
23
56
  this.db = new SQL.Database(buffer);
24
57
  this.verifyIntegrity();
25
58
  } catch {
26
- console.error("[ctxo:sqlite] Corrupt DB detected, rebuilding from JSON index");
59
+ log.warn("Corrupt DB detected, rebuilding from JSON index");
27
60
  this.db = new SQL.Database();
28
61
  this.createTables();
29
62
  }
@@ -106,7 +139,7 @@ var SqliteStorageAdapter = class {
106
139
  );
107
140
  for (const sym of fileIndex.symbols) {
108
141
  db.run(
109
- "INSERT INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)",
142
+ "INSERT OR REPLACE INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)",
110
143
  [sym.symbolId, sym.name, sym.kind, fileIndex.file, sym.startLine, sym.endLine]
111
144
  );
112
145
  }
@@ -237,7 +270,7 @@ var SqliteStorageAdapter = class {
237
270
  );
238
271
  for (const sym of fileIndex.symbols) {
239
272
  db.run(
240
- "INSERT INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)",
273
+ "INSERT OR REPLACE INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)",
241
274
  [sym.symbolId, sym.name, sym.kind, fileIndex.file, sym.startLine, sym.endLine]
242
275
  );
243
276
  }
@@ -273,7 +306,7 @@ var SqliteStorageAdapter = class {
273
306
  try {
274
307
  this.persist();
275
308
  } catch (err) {
276
- console.error(`[ctxo:sqlite] Failed to persist DB: ${err.message}`);
309
+ log.error(`Failed to persist DB: ${err.message}`);
277
310
  }
278
311
  }
279
312
  deleteFileData(db, filePath) {
@@ -321,10 +354,12 @@ var SqliteStorageAdapter = class {
321
354
 
322
355
  // src/adapters/git/simple-git-adapter.ts
323
356
  import { simpleGit } from "simple-git";
357
+ var log2 = createLogger("ctxo:git");
324
358
  var SimpleGitAdapter = class {
325
359
  git;
326
360
  constructor(projectRoot) {
327
361
  this.git = simpleGit(projectRoot);
362
+ log2.debug("Initialized with root: %s", projectRoot);
328
363
  }
329
364
  async isAvailable() {
330
365
  try {
@@ -336,27 +371,27 @@ var SimpleGitAdapter = class {
336
371
  }
337
372
  async getCommitHistory(filePath, maxCount) {
338
373
  try {
339
- const log = await this.git.log({
374
+ const log3 = await this.git.log({
340
375
  file: filePath,
341
376
  "--follow": null,
342
377
  ...maxCount ? { maxCount } : {}
343
378
  });
344
- return log.all.map((entry) => ({
379
+ return log3.all.map((entry) => ({
345
380
  hash: entry.hash,
346
381
  message: entry.message,
347
382
  date: entry.date,
348
383
  author: entry.author_name
349
384
  }));
350
385
  } catch (err) {
351
- console.error(`[ctxo:git] Failed to get history for ${filePath}: ${err.message}`);
386
+ log2.error(` Failed to get history for ${filePath}: ${err.message}`);
352
387
  return [];
353
388
  }
354
389
  }
355
390
  async getBatchHistory(maxCount = 20) {
356
391
  try {
357
- const log = await this.git.log({ maxCount: maxCount * 50, "--name-only": null });
392
+ const log3 = await this.git.log({ maxCount: maxCount * 50, "--name-only": null });
358
393
  const result = /* @__PURE__ */ new Map();
359
- for (const entry of log.all) {
394
+ for (const entry of log3.all) {
360
395
  const record = {
361
396
  hash: entry.hash,
362
397
  message: entry.message,
@@ -378,7 +413,7 @@ var SimpleGitAdapter = class {
378
413
  }
379
414
  return result;
380
415
  } catch (err) {
381
- console.error(`[ctxo:git] Batch history failed: ${err.message}`);
416
+ log2.error(` Batch history failed: ${err.message}`);
382
417
  return /* @__PURE__ */ new Map();
383
418
  }
384
419
  }
@@ -387,19 +422,19 @@ var SimpleGitAdapter = class {
387
422
  const diff = await this.git.diffSummary([since]);
388
423
  return diff.files.map((f) => f.file.replace(/\\/g, "/"));
389
424
  } catch (err) {
390
- console.error(`[ctxo:git] Failed to get changed files since ${since}: ${err.message}`);
425
+ log2.error(` Failed to get changed files since ${since}: ${err.message}`);
391
426
  return [];
392
427
  }
393
428
  }
394
429
  async getFileChurn(filePath) {
395
430
  try {
396
- const log = await this.git.log({ file: filePath, "--follow": null });
431
+ const log3 = await this.git.log({ file: filePath, "--follow": null });
397
432
  return {
398
433
  filePath,
399
- commitCount: log.total
434
+ commitCount: log3.total
400
435
  };
401
436
  } catch (err) {
402
- console.error(`[ctxo:git] Failed to get churn for ${filePath}: ${err.message}`);
437
+ log2.error(` Failed to get churn for ${filePath}: ${err.message}`);
403
438
  return { filePath, commitCount: 0 };
404
439
  }
405
440
  }
@@ -516,10 +551,11 @@ function loadCoChangeMap(matrix) {
516
551
  }
517
552
 
518
553
  export {
554
+ createLogger,
519
555
  SqliteStorageAdapter,
520
556
  RevertDetector,
521
557
  SimpleGitAdapter,
522
558
  aggregateCoChanges,
523
559
  loadCoChangeMap
524
560
  };
525
- //# sourceMappingURL=chunk-VFHBBTYM.js.map
561
+ //# sourceMappingURL=chunk-XSHNN6PU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/logger.ts","../src/adapters/storage/sqlite-storage-adapter.ts","../src/adapters/git/simple-git-adapter.ts","../src/core/why-context/revert-detector.ts","../src/core/co-change/co-change-analyzer.ts"],"sourcesContent":["/**\n * Structured logger for Ctxo — all output goes to stderr (stdout reserved for MCP JSON-RPC).\n *\n * Supports debug mode via DEBUG environment variable:\n * DEBUG=ctxo:* — all debug output\n * DEBUG=ctxo:git — only git adapter debug\n * DEBUG=ctxo:index — only indexing debug\n * DEBUG=ctxo:mcp — only MCP handler debug\n * DEBUG=ctxo:storage — only storage adapter debug\n * DEBUG=ctxo:masking — only masking pipeline debug\n *\n * Always outputs: info, warn, error (regardless of DEBUG).\n * Only outputs debug when matching namespace is enabled.\n */\n\nfunction isDebugEnabled(namespace: string): boolean {\n const debugEnv = process.env['DEBUG'] ?? '';\n if (!debugEnv) return false;\n const patterns = debugEnv.split(',').map(p => p.trim());\n return patterns.some(p => {\n if (p === '*' || p === 'ctxo:*') return true;\n if (p === namespace) return true;\n if (p.endsWith(':*') && namespace.startsWith(p.slice(0, -1))) return true;\n return false;\n });\n}\n\nexport interface Logger {\n debug(message: string, ...args: unknown[]): void;\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n error(message: string, ...args: unknown[]): void;\n}\n\nexport function createLogger(namespace: string): Logger {\n const prefix = `[${namespace}]`;\n\n return {\n debug(message: string, ...args: unknown[]) {\n if (isDebugEnabled(namespace)) {\n console.error(`${prefix} DEBUG ${message}`, ...args);\n }\n },\n info(message: string, ...args: unknown[]) {\n console.error(`${prefix} ${message}`, ...args);\n },\n warn(message: string, ...args: unknown[]) {\n console.error(`${prefix} WARN ${message}`, ...args);\n },\n error(message: string, ...args: unknown[]) {\n console.error(`${prefix} ERROR ${message}`, ...args);\n },\n };\n}\n","import initSqlJs, { type Database } from 'sql.js';\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport type { FileIndex, GraphEdge, SymbolNode } from '../../core/types.js';\nimport type { IStoragePort } from '../../ports/i-storage-port.js';\nimport { JsonIndexReader } from './json-index-reader.js';\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:storage');\n\nexport class SqliteStorageAdapter implements IStoragePort {\n private db: Database | undefined;\n private readonly dbPath: string;\n private readonly ctxoRoot: string;\n\n constructor(ctxoRoot: string) {\n this.ctxoRoot = ctxoRoot;\n this.dbPath = join(ctxoRoot, '.cache', 'symbols.db');\n }\n\n async init(): Promise<void> {\n const SQL = await initSqlJs();\n\n if (existsSync(this.dbPath)) {\n try {\n const buffer = readFileSync(this.dbPath);\n this.db = new SQL.Database(buffer);\n this.verifyIntegrity();\n } catch {\n log.warn('Corrupt DB detected, rebuilding from JSON index');\n this.db = new SQL.Database();\n this.createTables();\n }\n } else {\n this.db = new SQL.Database();\n this.createTables();\n }\n\n // Always rebuild from JSON — it is the source of truth\n this.rebuildFromJson();\n }\n\n async initEmpty(): Promise<void> {\n const SQL = await initSqlJs();\n this.db = new SQL.Database();\n this.createTables();\n }\n\n private database(): Database {\n if (!this.db) {\n throw new Error('SqliteStorageAdapter not initialized. Call init() first.');\n }\n return this.db;\n }\n\n private verifyIntegrity(): void {\n const db = this.database();\n const result = db.exec('PRAGMA integrity_check');\n const firstRow = result[0]?.values[0];\n if (!firstRow || firstRow[0] !== 'ok') {\n throw new Error('SQLite integrity check failed');\n }\n\n const tables = db.exec(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('symbols', 'edges', 'files')\",\n );\n if (!tables[0] || tables[0].values.length < 3) {\n throw new Error('Missing required tables');\n }\n }\n\n private createTables(): void {\n const db = this.database();\n db.run(`\n CREATE TABLE IF NOT EXISTS files (\n file_path TEXT PRIMARY KEY,\n last_modified INTEGER NOT NULL\n )\n `);\n db.run(`\n CREATE TABLE IF NOT EXISTS symbols (\n symbol_id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n kind TEXT NOT NULL,\n file_path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL\n )\n `);\n db.run(`\n CREATE TABLE IF NOT EXISTS edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_symbol TEXT NOT NULL,\n to_symbol TEXT NOT NULL,\n kind TEXT NOT NULL\n )\n `);\n db.run('CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path)');\n db.run('CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_symbol)');\n db.run('CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_symbol)');\n }\n\n private rebuildFromJson(): void {\n const reader = new JsonIndexReader(this.ctxoRoot);\n const indices = reader.readAll();\n\n if (indices.length > 0) {\n this.bulkWrite(indices);\n }\n }\n\n writeSymbolFile(fileIndex: FileIndex): void {\n const db = this.database();\n\n db.run('BEGIN TRANSACTION');\n try {\n this.deleteFileData(db, fileIndex.file);\n\n db.run(\n 'INSERT INTO files (file_path, last_modified) VALUES (?, ?)',\n [fileIndex.file, fileIndex.lastModified],\n );\n\n for (const sym of fileIndex.symbols) {\n db.run(\n 'INSERT OR REPLACE INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)',\n [sym.symbolId, sym.name, sym.kind, fileIndex.file, sym.startLine, sym.endLine],\n );\n }\n\n for (const edge of fileIndex.edges) {\n db.run(\n 'INSERT INTO edges (from_symbol, to_symbol, kind) VALUES (?, ?, ?)',\n [edge.from, edge.to, edge.kind],\n );\n }\n\n db.run('COMMIT');\n this.persistIfNeeded();\n } catch (err) {\n db.run('ROLLBACK');\n throw err;\n }\n }\n\n /**\n * Read symbol file from SQLite cache.\n * NOTE: SQLite only caches symbols + edges (graph topology).\n * intent, antiPatterns, and complexity live in committed JSON index only.\n * Use JsonIndexReader for full FileIndex data.\n */\n readSymbolFile(relativePath: string): FileIndex | undefined {\n const db = this.database();\n\n const fileResult = db.exec(\n 'SELECT file_path, last_modified FROM files WHERE file_path = ?',\n [relativePath],\n );\n if (!fileResult[0] || fileResult[0].values.length === 0) {\n return undefined;\n }\n\n const [filePath, lastModified] = fileResult[0].values[0] as [string, number];\n\n const symbols = this.getSymbolsForFile(db, filePath);\n const edges = this.getEdgesForFile(db, filePath);\n\n return {\n file: filePath,\n lastModified,\n symbols,\n edges,\n // Not stored in SQLite — use JsonIndexReader for these fields\n intent: [],\n antiPatterns: [],\n };\n }\n\n listIndexedFiles(): string[] {\n const db = this.database();\n const result = db.exec('SELECT file_path FROM files ORDER BY file_path');\n if (!result[0]) return [];\n return result[0].values.map((row) => row[0] as string);\n }\n\n deleteSymbolFile(relativePath: string): void {\n const db = this.database();\n this.deleteFileData(db, relativePath);\n }\n\n getSymbolById(symbolId: string): SymbolNode | undefined {\n const db = this.database();\n const result = db.exec(\n 'SELECT symbol_id, name, kind, start_line, end_line FROM symbols WHERE symbol_id = ?',\n [symbolId],\n );\n if (!result[0] || result[0].values.length === 0) {\n return undefined;\n }\n\n const [sid, name, kind, startLine, endLine] = result[0].values[0] as [string, string, string, number, number];\n return { symbolId: sid, name, kind: kind as SymbolNode['kind'], startLine, endLine };\n }\n\n getEdgesFrom(symbolId: string): GraphEdge[] {\n const db = this.database();\n const result = db.exec(\n 'SELECT from_symbol, to_symbol, kind FROM edges WHERE from_symbol = ?',\n [symbolId],\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n from: row[0] as string,\n to: row[1] as string,\n kind: row[2] as GraphEdge['kind'],\n }));\n }\n\n getEdgesTo(symbolId: string): GraphEdge[] {\n const db = this.database();\n const result = db.exec(\n 'SELECT from_symbol, to_symbol, kind FROM edges WHERE to_symbol = ?',\n [symbolId],\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n from: row[0] as string,\n to: row[1] as string,\n kind: row[2] as GraphEdge['kind'],\n }));\n }\n\n getAllSymbols(): SymbolNode[] {\n const db = this.database();\n const result = db.exec(\n 'SELECT symbol_id, name, kind, start_line, end_line FROM symbols',\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n symbolId: row[0] as string,\n name: row[1] as string,\n kind: row[2] as SymbolNode['kind'],\n startLine: row[3] as number,\n endLine: row[4] as number,\n }));\n }\n\n getAllEdges(): GraphEdge[] {\n const db = this.database();\n const result = db.exec(\n 'SELECT from_symbol, to_symbol, kind FROM edges',\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n from: row[0] as string,\n to: row[1] as string,\n kind: row[2] as GraphEdge['kind'],\n }));\n }\n\n bulkWrite(indices: FileIndex[]): void {\n const db = this.database();\n\n db.run('BEGIN TRANSACTION');\n try {\n for (const fileIndex of indices) {\n this.deleteFileData(db, fileIndex.file);\n\n db.run(\n 'INSERT INTO files (file_path, last_modified) VALUES (?, ?)',\n [fileIndex.file, fileIndex.lastModified],\n );\n\n for (const sym of fileIndex.symbols) {\n db.run(\n 'INSERT OR REPLACE INTO symbols (symbol_id, name, kind, file_path, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?)',\n [sym.symbolId, sym.name, sym.kind, fileIndex.file, sym.startLine, sym.endLine],\n );\n }\n\n for (const edge of fileIndex.edges) {\n db.run(\n 'INSERT INTO edges (from_symbol, to_symbol, kind) VALUES (?, ?, ?)',\n [edge.from, edge.to, edge.kind],\n );\n }\n }\n\n db.run('COMMIT');\n this.persistIfNeeded();\n } catch (err) {\n db.run('ROLLBACK');\n throw err;\n }\n }\n\n persist(): void {\n const db = this.database();\n const data = db.export();\n const buffer = Buffer.from(data);\n mkdirSync(dirname(this.dbPath), { recursive: true });\n writeFileSync(this.dbPath, buffer);\n }\n\n close(): void {\n if (this.db) {\n this.persist();\n this.db.close();\n this.db = undefined;\n }\n }\n\n private persistIfNeeded(): void {\n try {\n this.persist();\n } catch (err) {\n log.error(`Failed to persist DB: ${(err as Error).message}`);\n }\n }\n\n private deleteFileData(db: Database, filePath: string): void {\n // Delete edges originating from this file's symbols\n const symResult = db.exec(\n 'SELECT symbol_id FROM symbols WHERE file_path = ?',\n [filePath],\n );\n if (symResult[0]) {\n for (const row of symResult[0].values) {\n db.run('DELETE FROM edges WHERE from_symbol = ?', [row[0]]);\n }\n }\n db.run('DELETE FROM symbols WHERE file_path = ?', [filePath]);\n db.run('DELETE FROM files WHERE file_path = ?', [filePath]);\n }\n\n private getSymbolsForFile(db: Database, filePath: string): SymbolNode[] {\n const result = db.exec(\n 'SELECT symbol_id, name, kind, start_line, end_line FROM symbols WHERE file_path = ? ORDER BY start_line',\n [filePath],\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n symbolId: row[0] as string,\n name: row[1] as string,\n kind: row[2] as SymbolNode['kind'],\n startLine: row[3] as number,\n endLine: row[4] as number,\n }));\n }\n\n private getEdgesForFile(db: Database, filePath: string): GraphEdge[] {\n const result = db.exec(\n `SELECT e.from_symbol, e.to_symbol, e.kind FROM edges e\n INNER JOIN symbols s ON e.from_symbol = s.symbol_id\n WHERE s.file_path = ?`,\n [filePath],\n );\n if (!result[0]) return [];\n return result[0].values.map((row) => ({\n from: row[0] as string,\n to: row[1] as string,\n kind: row[2] as GraphEdge['kind'],\n }));\n }\n}\n","import { simpleGit, type SimpleGit } from 'simple-git';\nimport type { IGitPort } from '../../ports/i-git-port.js';\nimport type { CommitRecord, ChurnData } from '../../core/types.js';\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:git');\n\nexport class SimpleGitAdapter implements IGitPort {\n private readonly git: SimpleGit;\n\n constructor(projectRoot: string) {\n this.git = simpleGit(projectRoot);\n log.debug('Initialized with root: %s', projectRoot);\n }\n\n async isAvailable(): Promise<boolean> {\n try {\n await this.git.version();\n return true;\n } catch {\n return false;\n }\n }\n\n async getCommitHistory(filePath: string, maxCount?: number): Promise<CommitRecord[]> {\n try {\n const log = await this.git.log({\n file: filePath,\n '--follow': null,\n ...(maxCount ? { maxCount } : {}),\n });\n\n return log.all.map((entry) => ({\n hash: entry.hash,\n message: entry.message,\n date: entry.date,\n author: entry.author_name,\n }));\n } catch (err) {\n log.error(` Failed to get history for ${filePath}: ${(err as Error).message}`);\n return [];\n }\n }\n\n async getBatchHistory(maxCount = 20): Promise<Map<string, CommitRecord[]>> {\n try {\n const log = await this.git.log({ maxCount: maxCount * 50, '--name-only': null });\n const result = new Map<string, CommitRecord[]>();\n\n for (const entry of log.all) {\n const record: CommitRecord = {\n hash: entry.hash,\n message: entry.message,\n date: entry.date,\n author: entry.author_name,\n };\n\n const diff = entry.diff;\n const files: string[] = diff?.files?.map((f: { file: string }) => f.file.replace(/\\\\/g, '/')) ?? [];\n\n for (const file of files) {\n let list = result.get(file);\n if (!list) { list = []; result.set(file, list); }\n if (list.length < maxCount) {\n list.push(record);\n }\n }\n }\n\n return result;\n } catch (err) {\n log.error(` Batch history failed: ${(err as Error).message}`);\n return new Map();\n }\n }\n\n async getChangedFiles(since: string): Promise<string[]> {\n try {\n const diff = await this.git.diffSummary([since]);\n return diff.files.map((f) => f.file.replace(/\\\\/g, '/'));\n } catch (err) {\n log.error(` Failed to get changed files since ${since}: ${(err as Error).message}`);\n return [];\n }\n }\n\n async getFileChurn(filePath: string): Promise<ChurnData> {\n try {\n const log = await this.git.log({ file: filePath, '--follow': null });\n\n return {\n filePath,\n commitCount: log.total,\n };\n } catch (err) {\n log.error(` Failed to get churn for ${filePath}: ${(err as Error).message}`);\n return { filePath, commitCount: 0 };\n }\n }\n}\n","import type { CommitRecord, AntiPattern } from '../types.js';\n\n// Explicit revert patterns\nconst REVERT_QUOTED_PATTERN = /^Revert \"(.+)\"$/;\nconst REVERT_PREFIX_PATTERN = /^revert:\\s*(.+)$/i;\n\n// Undo/rollback patterns\nconst UNDO_PATTERN = /^undo[:\\s]/i;\nconst ROLLBACK_PATTERN = /^rollback[:\\s]/i;\n\n// Indirect revert indicators (keywords in commit message body)\nconst INDIRECT_KEYWORDS = [\n /\\brevert(?:s|ed|ing)?\\b/i,\n /\\broll(?:s|ed|ing)?\\s*back\\b/i,\n /\\bundo(?:es|ne|ing)?\\b/i,\n /\\bbacked?\\s*out\\b/i,\n /\\bremov(?:e|es|ed|ing)\\s+(?:broken|buggy|faulty)\\b/i,\n];\n\nexport class RevertDetector {\n detect(commits: readonly CommitRecord[]): AntiPattern[] {\n const antiPatterns: AntiPattern[] = [];\n\n for (const commit of commits) {\n if (!commit.message) continue;\n\n if (this.isRevert(commit.message)) {\n antiPatterns.push({\n hash: commit.hash,\n message: commit.message,\n date: commit.date,\n });\n }\n }\n\n return antiPatterns;\n }\n\n private isRevert(message: string): boolean {\n // Explicit patterns (high confidence)\n if (REVERT_QUOTED_PATTERN.test(message)) return true;\n if (REVERT_PREFIX_PATTERN.test(message)) return true;\n if (UNDO_PATTERN.test(message)) return true;\n if (ROLLBACK_PATTERN.test(message)) return true;\n\n // Indirect indicators (keyword search in full message)\n for (const pattern of INDIRECT_KEYWORDS) {\n if (pattern.test(message)) return true;\n }\n\n return false;\n }\n}\n","import type { CoChangeEntry, CoChangeMatrix, FileIndex } from '../types.js';\n\nconst MIN_SHARED_COMMITS = 2;\nconst MIN_FREQUENCY = 0.1;\n\nexport function aggregateCoChanges(indices: readonly FileIndex[]): CoChangeMatrix {\n // Step 1: Build commit → files map\n const commitToFiles = new Map<string, Set<string>>();\n const fileCommitCounts = new Map<string, number>();\n\n for (const idx of indices) {\n const file = idx.file;\n let count = 0;\n for (const intent of idx.intent) {\n count++;\n let files = commitToFiles.get(intent.hash);\n if (!files) {\n files = new Set();\n commitToFiles.set(intent.hash, files);\n }\n files.add(file);\n }\n if (count > 0) {\n fileCommitCounts.set(file, count);\n }\n }\n\n // Step 2: Count shared commits per file pair\n const pairCounts = new Map<string, number>();\n for (const files of commitToFiles.values()) {\n if (files.size < 2) continue;\n const sorted = [...files].sort();\n for (let i = 0; i < sorted.length; i++) {\n for (let j = i + 1; j < sorted.length; j++) {\n const key = `${sorted[i]}|${sorted[j]}`;\n pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);\n }\n }\n }\n\n // Step 3: Calculate frequency and filter\n const entries: CoChangeEntry[] = [];\n for (const [key, sharedCommits] of pairCounts) {\n if (sharedCommits < MIN_SHARED_COMMITS) continue;\n\n const [file1, file2] = key.split('|') as [string, string];\n const count1 = fileCommitCounts.get(file1) ?? 0;\n const count2 = fileCommitCounts.get(file2) ?? 0;\n const minCount = Math.min(count1, count2);\n if (minCount === 0) continue;\n\n const frequency = Math.round((sharedCommits / minCount) * 1000) / 1000;\n if (frequency < MIN_FREQUENCY) continue;\n\n entries.push({ file1, file2, sharedCommits, frequency });\n }\n\n entries.sort((a, b) => b.frequency - a.frequency || b.sharedCommits - a.sharedCommits);\n\n return {\n version: 1,\n timestamp: Math.floor(Date.now() / 1000),\n entries,\n };\n}\n\nexport function loadCoChangeMap(matrix: CoChangeMatrix): Map<string, CoChangeEntry[]> {\n const map = new Map<string, CoChangeEntry[]>();\n for (const entry of matrix.entries) {\n let list1 = map.get(entry.file1);\n if (!list1) { list1 = []; map.set(entry.file1, list1); }\n list1.push(entry);\n\n let list2 = map.get(entry.file2);\n if (!list2) { list2 = []; map.set(entry.file2, list2); }\n list2.push(entry);\n }\n return map;\n}\n"],"mappings":"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","names":["log"]}
@@ -4,7 +4,7 @@ import {
4
4
  SimpleGitAdapter,
5
5
  SqliteStorageAdapter,
6
6
  aggregateCoChanges
7
- } from "./chunk-VFHBBTYM.js";
7
+ } from "./chunk-XSHNN6PU.js";
8
8
  import {
9
9
  JsonIndexReader,
10
10
  SYMBOL_KINDS
@@ -383,7 +383,8 @@ var init_csharp_adapter = __esm({
383
383
  if (!this.isPublic(child)) continue;
384
384
  const name = child.childForFieldName("name")?.text;
385
385
  if (!name) continue;
386
- const qualifiedName = `${className}.${name}`;
386
+ const paramCount = this.countParameters(child);
387
+ const qualifiedName = `${className}.${name}(${paramCount})`;
387
388
  const range = this.nodeToLineRange(child);
388
389
  symbols.push({
389
390
  symbolId: this.buildSymbolId(filePath, qualifiedName, "method"),
@@ -469,8 +470,9 @@ var init_csharp_adapter = __esm({
469
470
  if (!this.isPublic(child)) continue;
470
471
  const methodName = child.childForFieldName("name")?.text;
471
472
  if (!methodName) continue;
473
+ const paramCount = this.countParameters(child);
472
474
  metrics.push({
473
- symbolId: this.buildSymbolId(filePath, `${qualifiedClass}.${methodName}`, "method"),
475
+ symbolId: this.buildSymbolId(filePath, `${qualifiedClass}.${methodName}(${paramCount})`, "method"),
474
476
  cyclomatic: this.countCyclomaticComplexity(child, CSHARP_BRANCH_TYPES)
475
477
  });
476
478
  }
@@ -481,6 +483,15 @@ var init_csharp_adapter = __esm({
481
483
  }
482
484
  }
483
485
  // ── Helpers ─────────────────────────────────────────────────
486
+ countParameters(methodNode) {
487
+ const paramList = methodNode.childForFieldName("parameters");
488
+ if (!paramList) return 0;
489
+ let count = 0;
490
+ for (let i = 0; i < paramList.childCount; i++) {
491
+ if (paramList.child(i).type === "parameter") count++;
492
+ }
493
+ return count;
494
+ }
484
495
  isPublic(node) {
485
496
  for (let i = 0; i < node.childCount; i++) {
486
497
  const child = node.child(i);
@@ -1843,4 +1854,4 @@ Usage:
1843
1854
  export {
1844
1855
  CliRouter
1845
1856
  };
1846
- //# sourceMappingURL=cli-router-PJ6DSIWJ.js.map
1857
+ //# sourceMappingURL=cli-router-NRUGPICL.js.map