@ctxo/cli 0.10.0 → 0.11.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
@@ -97,6 +97,24 @@ gate:
97
97
  | Cursor | no (no blocking pre-edit hook) | yes | yes |
98
98
  | Other | no | where supported | yes |
99
99
 
100
+ ## Supported Languages
101
+
102
+ | Language | Plugin | Tier | Notes |
103
+ |---|---|---|---|
104
+ | TypeScript / JavaScript | `@ctxo/lang-typescript` | full | ts-morph; type-aware cross-file resolution |
105
+ | Go | `@ctxo/lang-go` | syntax | tree-sitter; exported-symbol analysis |
106
+ | C# | `@ctxo/lang-csharp` | full | Roslyn; .NET SDK 8+ for full tier, tree-sitter fallback |
107
+ | Java | `@ctxo/lang-java` | syntax / full | tree-sitter syntax tier built-in; full tier via `@ctxo/lang-java-analyzer` companion (requires JRE 17+) |
108
+
109
+ Install via the CLI shortcut:
110
+
111
+ ```bash
112
+ ctxo install typescript go csharp # syntax or full tier auto-detected
113
+ ctxo install java # installs syntax tier; adds full-tier analyzer when JRE 17+ detected
114
+ ctxo install java --full-tier # force full-tier analyzer install
115
+ ctxo install java --syntax-only # skip analyzer, syntax tier only
116
+ ```
117
+
100
118
  ## Links
101
119
 
102
120
  * [Docs](https://alperhankendi.github.io/Ctxo/docs/) quick start, MCP tools, CLI reference, integrations
@@ -2,9 +2,10 @@
2
2
  import {
3
3
  atomicWrite,
4
4
  sweepStaleTmpFiles
5
- } from "./chunk-6EKERTJ7.js";
5
+ } from "./chunk-RFEXMBTG.js";
6
+ import "./chunk-5DSKZJNN.js";
6
7
  export {
7
8
  atomicWrite,
8
9
  sweepStaleTmpFiles
9
10
  };
10
- //# sourceMappingURL=atomic-write-AQNYJFAG.js.map
11
+ //# sourceMappingURL=atomic-write-BAFH464Q.js.map
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/logger.ts
4
+ function isDebugEnabled(namespace) {
5
+ const debugEnv = process.env["DEBUG"] ?? "";
6
+ if (!debugEnv) return false;
7
+ const patterns = debugEnv.split(",").map((p) => p.trim());
8
+ return patterns.some((p) => {
9
+ if (p === "*" || p === "ctxo:*") return true;
10
+ if (p === namespace) return true;
11
+ if (p.endsWith(":*") && namespace.startsWith(p.slice(0, -1))) return true;
12
+ return false;
13
+ });
14
+ }
15
+ function createLogger(namespace) {
16
+ const prefix = `[${namespace}]`;
17
+ return {
18
+ debug(message, ...args) {
19
+ if (isDebugEnabled(namespace)) {
20
+ console.error(`${prefix} DEBUG ${message}`, ...args);
21
+ }
22
+ },
23
+ info(message, ...args) {
24
+ console.error(`${prefix} ${message}`, ...args);
25
+ },
26
+ warn(message, ...args) {
27
+ console.error(`${prefix} WARN ${message}`, ...args);
28
+ },
29
+ error(message, ...args) {
30
+ console.error(`${prefix} ERROR ${message}`, ...args);
31
+ }
32
+ };
33
+ }
34
+
35
+ export {
36
+ createLogger
37
+ };
38
+ //# sourceMappingURL=chunk-5DSKZJNN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/logger.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"],"mappings":";;;AAeA,SAAS,eAAe,WAA4B;AAClD,QAAM,WAAW,QAAQ,IAAI,OAAO,KAAK;AACzC,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACtD,SAAO,SAAS,KAAK,OAAK;AACxB,QAAI,MAAM,OAAO,MAAM,SAAU,QAAO;AACxC,QAAI,MAAM,UAAW,QAAO;AAC5B,QAAI,EAAE,SAAS,IAAI,KAAK,UAAU,WAAW,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,QAAO;AACrE,WAAO;AAAA,EACT,CAAC;AACH;AASO,SAAS,aAAa,WAA2B;AACtD,QAAM,SAAS,IAAI,SAAS;AAE5B,SAAO;AAAA,IACL,MAAM,YAAoB,MAAiB;AACzC,UAAI,eAAe,SAAS,GAAG;AAC7B,gBAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,MACrD;AAAA,IACF;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,IAAI,OAAO,IAAI,GAAG,IAAI;AAAA,IAC/C;AAAA,IACA,KAAK,YAAoB,MAAiB;AACxC,cAAQ,MAAM,GAAG,MAAM,SAAS,OAAO,IAAI,GAAG,IAAI;AAAA,IACpD;AAAA,IACA,MAAM,YAAoB,MAAiB;AACzC,cAAQ,MAAM,GAAG,MAAM,UAAU,OAAO,IAAI,GAAG,IAAI;AAAA,IACrD;AAAA,EACF;AACF;","names":[]}
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ createLogger
4
+ } from "./chunk-5DSKZJNN.js";
2
5
 
3
6
  // src/adapters/storage/json-index-reader.ts
4
7
  import { readFileSync, readdirSync, existsSync, realpathSync } from "fs";
@@ -39,6 +42,16 @@ var SymbolIdSchema = z.string().min(1).refine(
39
42
  },
40
43
  { message: 'Symbol ID must match format "<file>::<name>::<kind>"' }
41
44
  );
45
+ var EdgeTargetSchema = z.string().refine(
46
+ (s) => {
47
+ const parts = s.split("::");
48
+ if (parts.length !== 3 && parts.length !== 2) return false;
49
+ if (parts.some((p) => p.length === 0)) return false;
50
+ const kind = parts[parts.length - 1];
51
+ return SYMBOL_KINDS.includes(kind);
52
+ },
53
+ { message: "edge target must be <file>::<name>::<kind> or <name>::<kind>" }
54
+ );
42
55
  var SymbolNodeSchema = z.object({
43
56
  symbolId: SymbolIdSchema,
44
57
  name: z.string().min(1),
@@ -52,7 +65,9 @@ var SymbolNodeSchema = z.object({
52
65
  });
53
66
  var GraphEdgeSchema = z.object({
54
67
  from: SymbolIdSchema,
55
- to: SymbolIdSchema,
68
+ // strict 3-part: from is always a local, resolved symbol
69
+ to: EdgeTargetSchema,
70
+ // 3-part OR 2-part name-ref for cross-file/unresolved targets
56
71
  kind: EdgeKindSchema,
57
72
  typeOnly: z.boolean().optional()
58
73
  });
@@ -83,6 +98,7 @@ var FileIndexSchema = z.object({
83
98
  });
84
99
 
85
100
  // src/adapters/storage/json-index-reader.ts
101
+ var log = createLogger("ctxo:json-reader");
86
102
  var JsonIndexReader = class {
87
103
  indexDir;
88
104
  constructor(ctxoRoot) {
@@ -107,14 +123,48 @@ var JsonIndexReader = class {
107
123
  const raw = readFileSync(absolutePath, "utf-8");
108
124
  const data = JSON.parse(raw);
109
125
  const result = FileIndexSchema.safeParse(data);
110
- if (!result.success) {
111
- const rel = relative(this.indexDir, absolutePath);
126
+ if (result.success) {
127
+ return result.data;
128
+ }
129
+ const rel = relative(this.indexDir, absolutePath);
130
+ const raw_data = data;
131
+ if (typeof raw_data["file"] !== "string" || !raw_data["file"] || typeof raw_data["lastModified"] !== "number") {
132
+ console.error(
133
+ `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`
134
+ );
135
+ return void 0;
136
+ }
137
+ const rawSymbols = Array.isArray(raw_data["symbols"]) ? raw_data["symbols"] : [];
138
+ const rawEdges = Array.isArray(raw_data["edges"]) ? raw_data["edges"] : [];
139
+ const rawIntent = Array.isArray(raw_data["intent"]) ? raw_data["intent"] : [];
140
+ const rawAntiPatterns = Array.isArray(raw_data["antiPatterns"]) ? raw_data["antiPatterns"] : [];
141
+ const validSymbols = rawSymbols.filter((s) => SymbolNodeSchema.safeParse(s).success);
142
+ const validEdges = rawEdges.filter((e) => GraphEdgeSchema.safeParse(e).success);
143
+ const validIntent = rawIntent.filter((i) => CommitIntentSchema.safeParse(i).success);
144
+ const validAntiPatterns = rawAntiPatterns.filter((a) => AntiPatternSchema.safeParse(a).success);
145
+ const droppedSymbols = rawSymbols.length - validSymbols.length;
146
+ const droppedEdges = rawEdges.length - validEdges.length;
147
+ const droppedIntent = rawIntent.length - validIntent.length;
148
+ const droppedAntiPatterns = rawAntiPatterns.length - validAntiPatterns.length;
149
+ if (droppedSymbols > 0 || droppedEdges > 0 || droppedIntent > 0 || droppedAntiPatterns > 0) {
150
+ log.warn(
151
+ `${rel}: dropped ${droppedSymbols} invalid symbol(s), ${droppedEdges} invalid edge(s), ${droppedIntent} invalid intent(s), ${droppedAntiPatterns} invalid antiPattern(s) \u2014 kept ${validSymbols.length} symbol(s), ${validEdges.length} edge(s), ${validIntent.length} intent(s), ${validAntiPatterns.length} antiPattern(s)`
152
+ );
153
+ }
154
+ const recovered = FileIndexSchema.safeParse({
155
+ ...raw_data,
156
+ symbols: validSymbols,
157
+ edges: validEdges,
158
+ intent: validIntent,
159
+ antiPatterns: validAntiPatterns
160
+ });
161
+ if (!recovered.success) {
112
162
  console.error(
113
163
  `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`
114
164
  );
115
165
  return void 0;
116
166
  }
117
- return result.data;
167
+ return recovered.data;
118
168
  } catch (err) {
119
169
  const rel = relative(this.indexDir, absolutePath);
120
170
  console.error(
@@ -146,4 +196,4 @@ export {
146
196
  DetailLevelSchema,
147
197
  JsonIndexReader
148
198
  };
149
- //# sourceMappingURL=chunk-2TVHKCCU.js.map
199
+ //# sourceMappingURL=chunk-CDGQGCVN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/storage/json-index-reader.ts","../src/core/types.ts"],"sourcesContent":["import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport {\n FileIndexSchema,\n SymbolNodeSchema,\n GraphEdgeSchema,\n CommitIntentSchema,\n AntiPatternSchema,\n type FileIndex,\n} from '../../core/types.js';\nimport { createLogger } from '../../core/logger.js';\n\nconst log = createLogger('ctxo:json-reader');\n\nexport class JsonIndexReader {\n private readonly indexDir: string;\n\n constructor(ctxoRoot: string) {\n this.indexDir = join(ctxoRoot, 'index');\n }\n\n readAll(): FileIndex[] {\n if (!existsSync(this.indexDir)) {\n return [];\n }\n\n const jsonFiles = this.collectJsonFiles(this.indexDir);\n const results: FileIndex[] = [];\n\n for (const filePath of jsonFiles) {\n const parsed = this.readSingle(filePath);\n if (parsed) {\n results.push(parsed);\n }\n }\n\n return results;\n }\n\n readSingle(absolutePath: string): FileIndex | undefined {\n try {\n const raw = readFileSync(absolutePath, 'utf-8');\n const data: unknown = JSON.parse(raw);\n const result = FileIndexSchema.safeParse(data);\n\n if (result.success) {\n return result.data;\n }\n\n // Strict parse failed — attempt resilient recovery: keep valid items, drop invalid ones.\n const rel = relative(this.indexDir, absolutePath);\n const raw_data = data as Record<string, unknown>;\n\n // Must have at least a valid file name and lastModified to be recoverable.\n if (typeof raw_data['file'] !== 'string' || !raw_data['file'] ||\n typeof raw_data['lastModified'] !== 'number') {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n const rawSymbols = Array.isArray(raw_data['symbols']) ? raw_data['symbols'] : [];\n const rawEdges = Array.isArray(raw_data['edges']) ? raw_data['edges'] : [];\n const rawIntent = Array.isArray(raw_data['intent']) ? raw_data['intent'] : [];\n const rawAntiPatterns = Array.isArray(raw_data['antiPatterns']) ? raw_data['antiPatterns'] : [];\n\n const validSymbols = rawSymbols.filter((s) => SymbolNodeSchema.safeParse(s).success);\n const validEdges = rawEdges.filter((e) => GraphEdgeSchema.safeParse(e).success);\n const validIntent = rawIntent.filter((i) => CommitIntentSchema.safeParse(i).success);\n const validAntiPatterns = rawAntiPatterns.filter((a) => AntiPatternSchema.safeParse(a).success);\n\n const droppedSymbols = rawSymbols.length - validSymbols.length;\n const droppedEdges = rawEdges.length - validEdges.length;\n const droppedIntent = rawIntent.length - validIntent.length;\n const droppedAntiPatterns = rawAntiPatterns.length - validAntiPatterns.length;\n\n if (droppedSymbols > 0 || droppedEdges > 0 || droppedIntent > 0 || droppedAntiPatterns > 0) {\n log.warn(\n `${rel}: dropped ${droppedSymbols} invalid symbol(s), ${droppedEdges} invalid edge(s), ${droppedIntent} invalid intent(s), ${droppedAntiPatterns} invalid antiPattern(s) — kept ${validSymbols.length} symbol(s), ${validEdges.length} edge(s), ${validIntent.length} intent(s), ${validAntiPatterns.length} antiPattern(s)`,\n );\n }\n\n // Re-parse with the filtered arrays to get a fully-typed FileIndex.\n const recovered = FileIndexSchema.safeParse({\n ...raw_data,\n symbols: validSymbols,\n edges: validEdges,\n intent: validIntent,\n antiPatterns: validAntiPatterns,\n });\n\n if (!recovered.success) {\n console.error(\n `[ctxo:json-reader] Invalid schema in ${rel}: ${result.error.message}`,\n );\n return undefined;\n }\n\n return recovered.data;\n } catch (err) {\n const rel = relative(this.indexDir, absolutePath);\n console.error(\n `[ctxo:json-reader] Failed to read ${rel}: ${(err as Error).message}`,\n );\n return undefined;\n }\n }\n\n private collectJsonFiles(dir: string, visited: Set<string> = new Set()): string[] {\n const realDir = realpathSync(dir);\n if (visited.has(realDir)) return []; // Guard against symlink loops\n visited.add(realDir);\n\n const files: string[] = [];\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isSymbolicLink()) continue; // Skip symlinks entirely\n\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Skip non-FileIndex directories under .ctxo/index/\n if (entry.name === 'communities.history') continue;\n files.push(...this.collectJsonFiles(fullPath, visited));\n } else if (\n entry.name.endsWith('.json') &&\n entry.name !== 'co-changes.json' &&\n entry.name !== 'communities.json' &&\n entry.name !== 'drift-events.json' &&\n entry.name !== 'boundary-violations.json'\n ) {\n files.push(fullPath);\n }\n }\n\n return files;\n }\n}\n","import { z } from 'zod';\n\n// ── Symbol Kinds ────────────────────────────────────────────────\n\nexport const SYMBOL_KINDS = [\n 'function',\n 'class',\n 'interface',\n 'method',\n 'variable',\n 'type',\n] as const;\n\nexport const SymbolKindSchema = z.enum(SYMBOL_KINDS);\nexport type SymbolKind = z.infer<typeof SymbolKindSchema>;\n\n// ── Edge Kinds ──────────────────────────────────────────────────\n\nexport const EDGE_KINDS = [\n 'imports',\n 'calls',\n 'extends',\n 'implements',\n 'uses',\n] as const;\n\nexport const EdgeKindSchema = z.enum(EDGE_KINDS);\nexport type EdgeKind = z.infer<typeof EdgeKindSchema>;\n\n// ── Detail Levels ───────────────────────────────────────────────\n\nexport const DETAIL_LEVELS = [1, 2, 3, 4] as const;\n\nexport const DetailLevelSchema = z.union([\n z.literal(1),\n z.literal(2),\n z.literal(3),\n z.literal(4),\n]);\nexport type DetailLevel = z.infer<typeof DetailLevelSchema>;\n\n// ── Symbol ID ───────────────────────────────────────────────────\n\n/**\n * Format: \"<relativeFile>::<name>::<kind>\"\n * Example: \"src/foo.ts::myFn::function\"\n */\nexport const SymbolIdSchema = z\n .string()\n .min(1)\n .refine(\n (value) => {\n const parts = value.split('::');\n if (parts.length !== 3) return false;\n const [file, name, kind] = parts;\n if (!file || !name || !kind) return false;\n return SymbolKindSchema.safeParse(kind).success;\n },\n { message: 'Symbol ID must match format \"<file>::<name>::<kind>\"' },\n );\nexport type SymbolId = z.infer<typeof SymbolIdSchema>;\n\n// ── Edge Target ─────────────────────────────────────────────────\n\n/**\n * An edge target: either a full 3-part symbol ID (\"<file>::<name>::<kind>\")\n * or a 2-part name-reference (\"<name>::<kind>\") for cross-file / unresolved\n * targets. The graph builder resolves name-references by name at query time.\n */\nexport const EdgeTargetSchema = z.string().refine(\n (s) => {\n const parts = s.split('::');\n if (parts.length !== 3 && parts.length !== 2) return false;\n if (parts.some((p) => p.length === 0)) return false;\n const kind = parts[parts.length - 1];\n return (SYMBOL_KINDS as readonly string[]).includes(kind as string);\n },\n { message: 'edge target must be <file>::<name>::<kind> or <name>::<kind>' },\n);\nexport type EdgeTarget = z.infer<typeof EdgeTargetSchema>;\n\n// ── Symbol Node ─────────────────────────────────────────────────\n\nexport const SymbolNodeSchema = z\n .object({\n symbolId: SymbolIdSchema,\n name: z.string().min(1),\n kind: SymbolKindSchema,\n startLine: z.number().int().nonnegative(),\n endLine: z.number().int().nonnegative(),\n startOffset: z.number().int().nonnegative().optional(),\n endOffset: z.number().int().nonnegative().optional(),\n })\n .refine((node) => node.endLine >= node.startLine, {\n message: 'endLine must be >= startLine',\n });\nexport type SymbolNode = z.infer<typeof SymbolNodeSchema>;\n\n// ── Graph Edge ──────────────────────────────────────────────────\n\nexport const GraphEdgeSchema = z.object({\n from: SymbolIdSchema, // strict 3-part: from is always a local, resolved symbol\n to: EdgeTargetSchema, // 3-part OR 2-part name-ref for cross-file/unresolved targets\n kind: EdgeKindSchema,\n typeOnly: z.boolean().optional(),\n});\nexport type GraphEdge = z.infer<typeof GraphEdgeSchema>;\n\n// ── Commit Intent ───────────────────────────────────────────────\n\nexport const CommitIntentSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n kind: z.literal('commit'),\n});\nexport type CommitIntent = z.infer<typeof CommitIntentSchema>;\n\n// ── Anti-Pattern ────────────────────────────────────────────────\n\nexport const AntiPatternSchema = z.object({\n hash: z.string().min(1),\n message: z.string(),\n date: z.string().min(1),\n});\nexport type AntiPattern = z.infer<typeof AntiPatternSchema>;\n\n// ── File Index ──────────────────────────────────────────────────\n\nexport const ComplexityMetricsSchema = z.object({\n symbolId: z.string().min(1),\n cyclomatic: z.number().nonnegative(),\n});\n\nexport const FileIndexSchema = z.object({\n file: z.string().min(1),\n lastModified: z.number().nonnegative(),\n contentHash: z.string().optional(),\n symbols: z.array(SymbolNodeSchema),\n edges: z.array(GraphEdgeSchema),\n complexity: z.array(ComplexityMetricsSchema).optional(),\n intent: z.array(CommitIntentSchema),\n antiPatterns: z.array(AntiPatternSchema),\n});\nexport type FileIndex = z.infer<typeof FileIndexSchema>;\n\n// ── Logic-Slice Result ──────────────────────────────────────────\n\nexport interface LogicSliceResult {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n}\n\n// ── Formatted Slice ─────────────────────────────────────────────\n\nexport interface TruncationInfo {\n readonly truncated: true;\n readonly reason: 'token_budget_exceeded';\n}\n\nexport interface FormattedSlice {\n readonly root: SymbolNode;\n readonly dependencies: readonly SymbolNode[];\n readonly edges: readonly GraphEdge[];\n readonly level: DetailLevel;\n readonly levelDescription?: string;\n readonly truncation?: TruncationInfo;\n}\n\n// ── Complexity & Churn ──────────────────────────────────────────\n\nexport interface ComplexityMetrics {\n readonly symbolId: string;\n readonly cyclomatic: number;\n}\n\nexport interface ChurnData {\n readonly filePath: string;\n readonly commitCount: number;\n}\n\nexport const SCORE_BANDS = ['low', 'medium', 'high'] as const;\nexport type ScoreBand = (typeof SCORE_BANDS)[number];\n\nexport interface ChangeIntelligenceScore {\n readonly symbolId: string;\n readonly complexity: number;\n readonly churn: number;\n readonly composite: number;\n readonly band: ScoreBand;\n}\n\n// ── Why-Context Result ──────────────────────────────────────────\n\nexport interface WhyContextResult {\n readonly commitHistory: readonly CommitIntent[];\n readonly antiPatternWarnings: readonly AntiPattern[];\n readonly changeIntelligence?: ChangeIntelligenceScore;\n}\n\n// ── Co-Change Analysis ─────────────────────────────────────────\n\nexport interface CoChangeEntry {\n readonly file1: string;\n readonly file2: string;\n readonly sharedCommits: number;\n readonly frequency: number;\n}\n\nexport interface CoChangeMatrix {\n readonly version: 1;\n readonly timestamp: number;\n readonly entries: readonly CoChangeEntry[];\n}\n\n// ── Commit Record (from git adapter) ────────────────────────────\n\nexport interface CommitRecord {\n readonly hash: string;\n readonly message: string;\n readonly date: string;\n readonly author: string;\n}\n\n// ── Blame Line (from git adapter) ───────────────────────────────\n\nexport interface BlameLine {\n readonly hash: string;\n readonly lineNumber: number;\n readonly author: string;\n readonly date: string;\n}\n\n// ── Search Types ───────────────────────────────────────────────\n\nexport interface SearchResult {\n symbolId: string;\n name: string;\n kind: string;\n filePath: string;\n /** BM25 relevance score (higher = more relevant) */\n relevanceScore: number;\n /** PageRank importance score (0-1) */\n importanceScore: number;\n /** Final combined score after all boosts */\n combinedScore: number;\n}\n\nexport interface SearchMetrics {\n porterHits: number;\n trigramHits: number;\n phase2Activated: boolean;\n fuzzyApplied: boolean;\n latencyMs: number;\n}\n\nexport interface FuzzyCorrection {\n originalQuery: string;\n correctedQuery: string;\n corrections: Array<{\n original: string;\n corrected: string;\n distance: number;\n }>;\n}\n\nexport interface SearchResponse {\n query: string;\n results: SearchResult[];\n metrics: SearchMetrics;\n fuzzyCorrection?: FuzzyCorrection;\n}\n\n// ── Community Detection ────────────────────────────────────────\n\nexport interface CommunityEntry {\n readonly symbolId: string;\n readonly communityId: number;\n readonly communityLabel: string;\n}\n\nexport interface GodNode {\n readonly symbolId: string;\n readonly bridgedCommunities: readonly number[];\n readonly centralityScore: number;\n}\n\nexport const EDGE_QUALITY = ['full', 'syntax-only', 'mixed'] as const;\nexport type EdgeQuality = (typeof EDGE_QUALITY)[number];\n\nexport interface CommunitySnapshot {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly modularity: number;\n readonly communities: readonly CommunityEntry[];\n readonly godNodes: readonly GodNode[];\n readonly edgeQuality: EdgeQuality;\n readonly crossClusterEdges: number;\n}\n\nexport interface DriftEvent {\n readonly symbolId: string;\n readonly movedFrom: { readonly id: number; readonly label: string };\n readonly movedTo: { readonly id: number; readonly label: string };\n readonly firstSeenInNewCluster: string;\n}\n\nexport interface BoundaryViolation {\n readonly from: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly to: {\n readonly symbolId: string;\n readonly communityId: number;\n readonly label: string;\n };\n readonly edgeKind: EdgeKind;\n readonly historicalEdgesBetweenClusters: number;\n readonly severity: 'high' | 'medium';\n}\n\n// ── Persisted architectural artifacts ──────────────────────────\n// Written by `ctxo index` next to communities.json so the static\n// HTML visualizer can render Architecture views without re-running\n// detectors in the browser.\n\nexport type ConfidenceLevel = 'high' | 'medium' | 'low';\n\nexport interface DriftEventsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n readonly snapshotsAvailable: number;\n readonly hint?: string;\n readonly events: readonly DriftEvent[];\n}\n\nexport interface BoundaryViolationsArtifact {\n readonly version: 1;\n readonly computedAt: string;\n readonly commitSha: string;\n readonly confidence: ConfidenceLevel;\n readonly snapshotsAvailable: number;\n readonly hint?: string;\n readonly violations: readonly BoundaryViolation[];\n}\n"],"mappings":"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","names":[]}
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- atomicWrite,
4
- createLogger
5
- } from "./chunk-6EKERTJ7.js";
3
+ atomicWrite
4
+ } from "./chunk-RFEXMBTG.js";
6
5
  import {
7
6
  JsonIndexReader
8
- } from "./chunk-2TVHKCCU.js";
7
+ } from "./chunk-CDGQGCVN.js";
8
+ import {
9
+ createLogger
10
+ } from "./chunk-5DSKZJNN.js";
9
11
 
10
12
  // src/adapters/storage/sqlite-storage-adapter.ts
11
13
  import initSqlJs from "sql.js";
@@ -888,15 +890,23 @@ function makeGlobMatcher(patterns) {
888
890
  var SymbolGraph = class {
889
891
  nodes = /* @__PURE__ */ new Map();
890
892
  nodesByFileAndName = /* @__PURE__ */ new Map();
893
+ nodesByNameAndKind = /* @__PURE__ */ new Map();
891
894
  forwardEdges = /* @__PURE__ */ new Map();
892
895
  reverseEdges = /* @__PURE__ */ new Map();
893
896
  edgeSet = /* @__PURE__ */ new Set();
894
897
  addNode(node) {
895
898
  this.nodes.set(node.symbolId, node);
896
- const [p0, p1] = node.symbolId.split("::");
899
+ const parts = node.symbolId.split("::");
900
+ const [p0, p1] = parts;
897
901
  if (p0 !== void 0 && p1 !== void 0) {
898
902
  this.nodesByFileAndName.set(`${p0}::${p1}`, node);
899
903
  }
904
+ if (parts.length === 3) {
905
+ const nk = `${parts[1]}::${parts[2]}`;
906
+ const list = this.nodesByNameAndKind.get(nk);
907
+ if (list) list.push(node);
908
+ else this.nodesByNameAndKind.set(nk, [node]);
909
+ }
900
910
  }
901
911
  addEdge(edge) {
902
912
  const resolvedFrom = this.resolveNodeId(edge.from);
@@ -917,7 +927,8 @@ var SymbolGraph = class {
917
927
  }
918
928
  resolveNodeId(id) {
919
929
  if (this.nodes.has(id)) return id;
920
- const [p0, p1] = id.split("::");
930
+ const segs = id.split("::");
931
+ const [p0, p1] = segs;
921
932
  if (p0 !== void 0 && p1 !== void 0) {
922
933
  const fuzzyKey = `${p0}::${p1}`;
923
934
  const match = this.nodesByFileAndName.get(fuzzyKey);
@@ -929,18 +940,41 @@ var SymbolGraph = class {
929
940
  if (altMatch) return altMatch.symbolId;
930
941
  }
931
942
  }
943
+ const nameKind = this.extractNameKind(segs);
944
+ if (nameKind) {
945
+ const candidates = this.nodesByNameAndKind.get(nameKind);
946
+ if (candidates && candidates.length === 1) return candidates[0].symbolId;
947
+ }
932
948
  return id;
933
949
  }
934
950
  resolveNodeFuzzy(id) {
935
- const [p0, p1] = id.split("::");
951
+ const segs = id.split("::");
952
+ const [p0, p1] = segs;
936
953
  if (p0 !== void 0 && p1 !== void 0) {
937
954
  const match = this.nodesByFileAndName.get(`${p0}::${p1}`);
938
955
  if (match) return match;
939
956
  const jsToTs = p0.replace(/\.js$/, ".ts");
940
957
  if (jsToTs !== p0) {
941
- return this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
958
+ const tsMatch = this.nodesByFileAndName.get(`${jsToTs}::${p1}`);
959
+ if (tsMatch) return tsMatch;
942
960
  }
943
961
  }
962
+ const nameKind = this.extractNameKind(segs);
963
+ if (nameKind) {
964
+ const candidates = this.nodesByNameAndKind.get(nameKind);
965
+ if (candidates && candidates.length === 1) return candidates[0];
966
+ }
967
+ return void 0;
968
+ }
969
+ /**
970
+ * Extracts a `name::kind` key from the segments of a reference ID, supporting:
971
+ * - 2-part: `Bar::class` → `Bar::class`
972
+ * - 3-part: `pkg.Bar::helper::method` → `helper::method`
973
+ * Returns undefined for 1-part IDs (no kind information available).
974
+ */
975
+ extractNameKind(segs) {
976
+ if (segs.length === 3) return `${segs[1]}::${segs[2]}`;
977
+ if (segs.length === 2) return `${segs[0]}::${segs[1]}`;
944
978
  return void 0;
945
979
  }
946
980
  getForwardEdges(symbolId) {
@@ -1762,4 +1796,4 @@ export {
1762
1796
  watchSnapshotMinFileChanges,
1763
1797
  makeGlobMatcher
1764
1798
  };
1765
- //# sourceMappingURL=chunk-I6HLEK4G.js.map
1799
+ //# sourceMappingURL=chunk-HZ4UGA6J.js.map