@kentwynn/kgraph 0.1.20 → 0.1.22

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
@@ -51,6 +51,7 @@ KGraph stores the reusable parts locally:
51
51
  - What files exist and what language they use.
52
52
  - What symbols each source file defines.
53
53
  - Which files import each other.
54
+ - Which TypeScript/JavaScript functions and methods directly call each other when KGraph can infer it cheaply.
54
55
  - Which notes, decisions, debugging findings, and gotchas were captured from prior sessions.
55
56
  - Which cognition references are current, mixed, stale, or unresolved after code moves.
56
57
 
@@ -62,6 +63,14 @@ kgraph "blog admin token usage"
62
63
 
63
64
  Instead of reading the whole repo, it gets a compact starting point: relevant files, symbols, relationships, domains, prior notes, and stale references to watch.
64
65
 
66
+ When you need change impact instead of broad context:
67
+
68
+ ```bash
69
+ kgraph impact Button
70
+ ```
71
+
72
+ That shows matched files/symbols, files importing the target, known callers/callees, related cognition, and simple risk signals.
73
+
65
74
  ## Install
66
75
 
67
76
  Use the published CLI:
@@ -142,14 +151,21 @@ kgraph doctor
142
151
  kgraph doctor --quality
143
152
  ```
144
153
 
145
- Checks whether the workspace is initialized, maps exist, inbox notes are pending, and configured integrations point to real files. Use `--quality` when context shows stale/noisy cognition references.
154
+ Checks whether the workspace is initialized, maps exist, inbox notes are pending, and configured integrations point to real files. Use `--quality` when context shows stale/noisy cognition references, unresolved local imports, unresolved call edges, duplicate cognition titles, or generated files in the scan.
146
155
 
147
156
  ```bash
148
157
  kgraph repair --dry-run
149
158
  kgraph repair
150
159
  ```
151
160
 
152
- `repair --dry-run` previews cleanup for noisy cognition references, such as framework names recorded as files or local variables recorded as symbols. `repair` applies that cleanup. Run repair intentionally when stale references make context noisy; it is not part of every normal workflow.
161
+ `repair --dry-run` previews cleanup for noisy cognition references, such as framework names recorded as files or local variables recorded as symbols. `repair` applies only the safe noisy-reference cleanup; broader quality findings stay report-only. Run repair intentionally when stale references make context noisy; it is not part of every normal workflow.
162
+
163
+ ```bash
164
+ kgraph impact "Button"
165
+ kgraph impact "createSession" --json
166
+ ```
167
+
168
+ Show practical impact for a file, symbol, or topic: matched files/symbols, import users, callers, callees, ownership edges, related cognition, and risk hints.
153
169
 
154
170
  ## Optional Step Commands
155
171
 
@@ -186,10 +202,11 @@ Open the local interactive dependency graph at `http://localhost:4242`.
186
202
  ```bash
187
203
  kgraph history
188
204
  kgraph history --last 10
205
+ kgraph history "blog button"
189
206
  kgraph history --json
190
207
  ```
191
208
 
192
- Show processed cognition sessions.
209
+ Show processed cognition sessions. Add a query to find historical work by title, summary, file, symbol, or note body.
193
210
 
194
211
  ## AI Tool Integrations
195
212
 
@@ -240,7 +257,7 @@ The files are local, inspectable, and human-readable. There is no database, tele
240
257
 
241
258
  KGraph deeply scans:
242
259
 
243
- - TypeScript and JavaScript
260
+ - TypeScript and JavaScript, including lightweight function/method call relationships
244
261
  - Python
245
262
  - Go
246
263
  - Rust
@@ -248,7 +265,7 @@ KGraph deeply scans:
248
265
  - C and C++
249
266
  - C#
250
267
 
251
- Other common file types still appear in the file map with generic metadata, so context queries can still point to docs, config, SQL, CSS, HTML, YAML, and similar files.
268
+ Other languages keep practical file, import, and symbol depth without full call graph analysis. Common file types still appear in the file map with generic metadata, so context queries can still point to docs, config, SQL, CSS, HTML, YAML, and similar files.
252
269
 
253
270
  ## Visualization
254
271
 
@@ -256,7 +273,7 @@ Other common file types still appear in the file map with generic metadata, so c
256
273
  kgraph visualize
257
274
  ```
258
275
 
259
- The graph shows files, symbols, imports, cognition notes, and relationship edges. Cognition notes are colored by reference health:
276
+ The graph shows files, symbols, imports, TypeScript/JavaScript call edges, ownership edges, cognition notes, and relationship edges. Cognition notes are colored by reference health:
260
277
 
261
278
  - current
262
279
  - mixed
@@ -318,6 +335,7 @@ The release workflow builds, tests, packs, publishes the npm package on version
318
335
  - Explicit: no daemon and no hidden background process.
319
336
  - Inspectable: generated knowledge is JSON, YAML, and Markdown.
320
337
  - Deterministic first: useful ranking without requiring embeddings or a model.
338
+ - Practical impact: context, history, quality, and impact commands should answer coding questions directly from local maps.
321
339
  - Assistant-friendly: one normal command, with lower-level commands available when needed.
322
340
 
323
341
  ## Roadmap
@@ -111,6 +111,10 @@ export function printQualityReport(report) {
111
111
  console.log(`Mixed/stale/unresolved notes: ${report.mixedOrStaleCount}`);
112
112
  console.log(`Noisy file refs: ${report.noisyFileRefCount}`);
113
113
  console.log(`Noisy symbol refs: ${report.noisySymbolRefCount}`);
114
+ console.log(`Unresolved local imports: ${report.unresolvedLocalImportCount}`);
115
+ console.log(`Unresolved call edges: ${report.unresolvedCallCount}`);
116
+ console.log(`Duplicate cognition titles: ${report.duplicateTitleCount}`);
117
+ console.log(`Generated files scanned: ${report.generatedFileScanCount}`);
114
118
  if (report.changes.length === 0) {
115
119
  return;
116
120
  }
@@ -3,6 +3,8 @@ export interface HistoryEntry {
3
3
  timestamp: Date;
4
4
  filename: string;
5
5
  title: string;
6
+ summary?: string;
7
+ text?: string;
6
8
  author?: string;
7
9
  }
8
10
  export declare function registerHistoryCommand(program: Command): void;
@@ -13,4 +15,4 @@ export declare function readHistoryEntries(processedPath: string, rootPath: stri
13
15
  * (colons and dot replaced by dashes when written to disk)
14
16
  */
15
17
  export declare function parseTimestampFromFilename(filename: string): Date | undefined;
16
- export declare function renderHistory(entries: HistoryEntry[], useColor?: boolean): string;
18
+ export declare function renderHistory(entries: HistoryEntry[], useColor?: boolean, query?: string): string;
@@ -4,32 +4,43 @@ import { readFile, readdir } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { assertWorkspace, pathExists } from '../../storage/kgraph-paths.js';
7
+ import { rankByFields } from '../../context/ranking.js';
7
8
  import { KGraphError, runCommand } from '../errors.js';
8
9
  const execFileAsync = promisify(execFile);
9
10
  export function registerHistoryCommand(program) {
10
11
  program
11
- .command('history')
12
+ .command('history [query...]')
12
13
  .description('Show a timeline of processed cognition sessions')
13
14
  .option('--last <n>', 'Show only the last N entries')
14
15
  .option('--json', 'Print JSON output')
15
- .action((options) => runCommand(async () => {
16
+ .action((queryParts = [], options) => runCommand(async () => {
16
17
  const workspace = await assertWorkspace(process.cwd());
17
18
  const entries = await readHistoryEntries(workspace.processedInteractionsPath, workspace.rootPath);
19
+ const query = queryParts.join(' ').trim();
18
20
  const limit = options.last !== undefined ? parseInt(options.last, 10) : 0;
19
21
  if (options.last !== undefined && (isNaN(limit) || limit < 1)) {
20
22
  throw new KGraphError('--last must be a positive integer.');
21
23
  }
22
- const shown = limit > 0 ? entries.slice(-limit) : entries;
24
+ const matched = query
25
+ ? rankByFields(query, entries, [
26
+ { name: 'title', value: (entry) => entry.title },
27
+ { name: 'summary', value: (entry) => entry.summary },
28
+ { name: 'content', value: (entry) => entry.text },
29
+ { name: 'filename', value: (entry) => entry.filename },
30
+ ]).map((entry) => entry.item)
31
+ : entries;
32
+ const shown = limit > 0 ? matched.slice(-limit) : matched;
23
33
  if (options.json) {
24
34
  console.log(JSON.stringify(shown.map((e) => ({
25
35
  timestamp: e.timestamp.toISOString(),
26
36
  filename: e.filename,
27
37
  title: e.title,
38
+ ...(e.summary !== undefined ? { summary: e.summary } : {}),
28
39
  ...(e.author !== undefined ? { author: e.author } : {}),
29
40
  })), null, 2));
30
41
  }
31
42
  else {
32
- console.log(renderHistory(shown));
43
+ console.log(renderHistory(shown, undefined, query));
33
44
  }
34
45
  }));
35
46
  }
@@ -50,9 +61,10 @@ export async function readHistoryEntries(processedPath, rootPath) {
50
61
  const filePath = path.join(processedPath, filename);
51
62
  const content = await readFile(filePath, 'utf8');
52
63
  const title = content.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? filename;
64
+ const summary = content.match(/^## Summary\s+([\s\S]*?)(?:\n## |\n# |$)/m)?.[1]?.trim();
53
65
  const relPath = path.relative(rootPath, filePath);
54
66
  const author = await getGitAuthor(rootPath, relPath);
55
- entries.push({ timestamp, filename, title, author });
67
+ entries.push({ timestamp, filename, title, summary, text: content, author });
56
68
  }
57
69
  return entries;
58
70
  }
@@ -69,14 +81,14 @@ export function parseTimestampFromFilename(filename) {
69
81
  const d = new Date(iso);
70
82
  return isNaN(d.getTime()) ? undefined : d;
71
83
  }
72
- export function renderHistory(entries, useColor = supportsColor()) {
84
+ export function renderHistory(entries, useColor = supportsColor(), query = '') {
73
85
  const chalk = new Chalk({ level: useColor ? 3 : 0 });
74
86
  if (entries.length === 0) {
75
87
  return ('\n' +
76
88
  chalk.dim(' No processed cognition notes found. Write Markdown notes to .kgraph/inbox/ and run `kgraph update`.') +
77
89
  '\n');
78
90
  }
79
- const header = ` ${chalk.bold('KGraph History')} ${chalk.dim(`· ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`)}`;
91
+ const header = ` ${chalk.bold('KGraph History')} ${chalk.dim(`· ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}${query ? ` matching "${query}"` : ''}`)}`;
80
92
  const lines = ['', header, ''];
81
93
  const titleWidth = Math.max(...entries.map((e) => e.title.length));
82
94
  for (const entry of entries) {
@@ -86,6 +98,9 @@ export function renderHistory(entries, useColor = supportsColor()) {
86
98
  ? chalk.cyan(`by ${entry.author}`)
87
99
  : chalk.dim('(uncommitted)');
88
100
  lines.push(` ${when} ${title} ${who}`);
101
+ if (entry.summary) {
102
+ lines.push(` ${chalk.dim(entry.summary.split('\n')[0])}`);
103
+ }
89
104
  }
90
105
  lines.push('');
91
106
  return lines.join('\n');
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import { type ImpactResponse } from '../../context/impact.js';
3
+ export declare function registerImpactCommand(program: Command): void;
4
+ export declare function renderImpactMarkdown(response: ImpactResponse): string;
@@ -0,0 +1,51 @@
1
+ import { loadConfig } from '../../config/config.js';
2
+ import { analyzeImpact } from '../../context/impact.js';
3
+ import { readCognitionNotes } from '../../storage/cognition-store.js';
4
+ import { assertWorkspace } from '../../storage/kgraph-paths.js';
5
+ import { mapsExist, readMaps } from '../../storage/map-store.js';
6
+ import { KGraphError, runCommand } from '../errors.js';
7
+ export function registerImpactCommand(program) {
8
+ program
9
+ .command('impact <query>')
10
+ .description('Show practical impact for a file, symbol, or topic')
11
+ .option('--json', 'Print JSON output')
12
+ .action((query, options) => runCommand(async () => {
13
+ if (!query.trim()) {
14
+ throw new KGraphError('Query cannot be empty.');
15
+ }
16
+ const workspace = await assertWorkspace(process.cwd());
17
+ if (!(await mapsExist(workspace))) {
18
+ throw new KGraphError('KGraph maps are missing. Run `kgraph scan` first.');
19
+ }
20
+ const [config, maps, cognition] = await Promise.all([
21
+ loadConfig(workspace),
22
+ readMaps(workspace),
23
+ readCognitionNotes(workspace),
24
+ ]);
25
+ const response = analyzeImpact(query, maps, cognition, config.maxContextItems);
26
+ console.log(options.json ? JSON.stringify(response, null, 2) : renderImpactMarkdown(response));
27
+ }));
28
+ }
29
+ export function renderImpactMarkdown(response) {
30
+ const lines = [`# KGraph Impact`, ``, `Query: ${response.query}`, ``];
31
+ lines.push('## Matched Files', '');
32
+ lines.push(...formatList(response.files.map((file) => `- ${file.item.path} (${file.reasons.join(', ')})`)));
33
+ lines.push('', '## Matched Symbols', '');
34
+ lines.push(...formatList(response.symbols.map((symbol) => `- ${symbol.item.name} in ${symbol.item.filePath}`)));
35
+ lines.push('', '## Imported By', '');
36
+ lines.push(...formatList(response.importedBy.map((file) => `- ${file}`)));
37
+ lines.push('', '## Called By', '');
38
+ lines.push(...formatList(response.callers.map((rel) => `- ${rel.sourceId} calls ${rel.targetId} (${rel.confidence})`)));
39
+ lines.push('', '## Calls', '');
40
+ lines.push(...formatList(response.calls.map((rel) => `- ${rel.sourceId} calls ${rel.targetId} (${rel.confidence})`)));
41
+ lines.push('', '## Ownership', '');
42
+ lines.push(...formatList(response.ownership.map((rel) => `- ${rel.sourceId} owns ${rel.targetId} (${rel.confidence})`)));
43
+ lines.push('', '## Related Cognition', '');
44
+ lines.push(...formatList(response.relatedCognition.map((note) => `- ${note.title} [${note.referencesStatus}]`)));
45
+ lines.push('', '## Risk', '');
46
+ lines.push(...formatList(response.risk.map((item) => `- ${item}`)));
47
+ return lines.join('\n');
48
+ }
49
+ function formatList(items) {
50
+ return items.length > 0 ? items : ['- None'];
51
+ }
package/dist/cli/help.js CHANGED
@@ -28,13 +28,14 @@ export function renderRootHelp(useColor = supportsColor()) {
28
28
  theme.bold('Workflows'),
29
29
  command('scan', 'Optional: refresh only file, symbol, import, and relationship maps'),
30
30
  command('context "auth token refresh"', 'Optional: return context without scanning or updating'),
31
+ command('impact "Button"', 'Show imports, callers, calls, cognition, and risk'),
31
32
  command('update', 'Optional: process only .kgraph/inbox Markdown cognition notes'),
32
33
  command('doctor', 'Check workspace health and next actions'),
33
34
  command('doctor --quality', 'Report stale/noisy cognition references'),
34
35
  command('repair --dry-run', 'Preview cognition reference cleanup'),
35
36
  command('repair', 'Clean noisy stale cognition references'),
36
37
  command('visualize', 'Interactive dependency graph at http://localhost:4242'),
37
- command('history', 'Timeline of processed cognition sessions'),
38
+ command('history "blog button"', 'Search processed cognition sessions'),
38
39
  '',
39
40
  theme.bold('Integrations'),
40
41
  command('integrate list', 'Show configured AI tool integrations'),
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { registerContextCommand } from './commands/context.js';
7
7
  import { registerDoctorCommand } from './commands/doctor.js';
8
8
  import { registerHistoryCommand } from './commands/history.js';
9
+ import { registerImpactCommand } from './commands/impact.js';
9
10
  import { registerInitCommand } from './commands/init.js';
10
11
  import { registerIntegrateCommand } from './commands/integrate.js';
11
12
  import { registerRepairCommand } from './commands/repair.js';
@@ -39,6 +40,7 @@ export function createProgram() {
39
40
  registerScanCommand(program);
40
41
  registerUpdateCommand(program);
41
42
  registerContextCommand(program);
43
+ registerImpactCommand(program);
42
44
  registerIntegrateCommand(program);
43
45
  registerVisualizeCommand(program);
44
46
  registerHistoryCommand(program);
@@ -1,6 +1,6 @@
1
1
  import type { KGraphWorkspace } from '../types/config.js';
2
2
  import type { ReferenceStatus } from '../types/cognition.js';
3
- import type { FileMap, SymbolMap } from '../types/maps.js';
3
+ import type { DependencyMap, FileMap, RelationshipMap, SymbolMap } from '../types/maps.js';
4
4
  export interface CognitionRepairChange {
5
5
  noteId: string;
6
6
  title: string;
@@ -13,13 +13,21 @@ export interface CognitionQualityReport {
13
13
  mixedOrStaleCount: number;
14
14
  noisyFileRefCount: number;
15
15
  noisySymbolRefCount: number;
16
+ unresolvedLocalImportCount: number;
17
+ unresolvedCallCount: number;
18
+ duplicateTitleCount: number;
19
+ generatedFileScanCount: number;
16
20
  changes: CognitionRepairChange[];
17
21
  }
18
22
  export declare function analyzeCognitionQuality(workspace: KGraphWorkspace, maps: {
19
23
  fileMap: FileMap;
20
24
  symbolMap: SymbolMap;
25
+ dependencyMap?: DependencyMap;
26
+ relationshipMap?: RelationshipMap;
21
27
  }): Promise<CognitionQualityReport>;
22
28
  export declare function repairCognition(workspace: KGraphWorkspace, maps: {
23
29
  fileMap: FileMap;
24
30
  symbolMap: SymbolMap;
31
+ dependencyMap?: DependencyMap;
32
+ relationshipMap?: RelationshipMap;
25
33
  }, dryRun?: boolean): Promise<CognitionQualityReport>;
@@ -10,6 +10,10 @@ export async function analyzeCognitionQuality(workspace, maps) {
10
10
  mixedOrStaleCount: notes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
11
11
  noisyFileRefCount: changes.reduce((total, change) => total + change.removedFileRefs.length, 0),
12
12
  noisySymbolRefCount: changes.reduce((total, change) => total + change.removedSymbolRefs.length, 0),
13
+ unresolvedLocalImportCount: countUnresolvedLocalImports(maps.dependencyMap),
14
+ unresolvedCallCount: countUnresolvedCalls(maps.symbolMap, maps.relationshipMap),
15
+ duplicateTitleCount: countDuplicateTitles(notes),
16
+ generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
13
17
  changes,
14
18
  };
15
19
  }
@@ -37,9 +41,51 @@ export async function repairCognition(workspace, maps, dryRun = false) {
37
41
  mixedOrStaleCount: nextNotes.filter((note) => ['mixed', 'stale', 'unresolved'].includes(note.referencesStatus)).length,
38
42
  noisyFileRefCount: changes.reduce((total, change) => total + change.removedFileRefs.length, 0),
39
43
  noisySymbolRefCount: changes.reduce((total, change) => total + change.removedSymbolRefs.length, 0),
44
+ unresolvedLocalImportCount: countUnresolvedLocalImports(maps.dependencyMap),
45
+ unresolvedCallCount: countUnresolvedCalls(maps.symbolMap, maps.relationshipMap),
46
+ duplicateTitleCount: countDuplicateTitles(nextNotes),
47
+ generatedFileScanCount: countGeneratedScannedFiles(maps.fileMap),
40
48
  changes,
41
49
  };
42
50
  }
51
+ function countUnresolvedLocalImports(dependencyMap) {
52
+ return (dependencyMap?.dependencies.filter((dependency) => dependency.kind === 'local' && !dependency.resolvedFile).length ?? 0);
53
+ }
54
+ function countUnresolvedCalls(symbolMap, relationshipMap) {
55
+ const symbolIds = new Set(symbolMap.symbols.map((symbol) => symbol.id));
56
+ const symbolNames = new Set(symbolMap.symbols.map((symbol) => symbol.name));
57
+ return (relationshipMap?.relationships.filter((relationship) => relationship.relationshipType === 'calls' &&
58
+ relationship.targetType === 'symbol' &&
59
+ !symbolIds.has(relationship.targetId) &&
60
+ !symbolNames.has(relationship.targetId) &&
61
+ ![...symbolNames].some((name) => relationship.targetId.endsWith(`#${name}`))).length ?? 0);
62
+ }
63
+ function countDuplicateTitles(notes) {
64
+ const seen = new Set();
65
+ const duplicates = new Set();
66
+ for (const note of notes) {
67
+ const key = note.title.trim().toLowerCase();
68
+ if (!key)
69
+ continue;
70
+ if (seen.has(key))
71
+ duplicates.add(key);
72
+ seen.add(key);
73
+ }
74
+ return duplicates.size;
75
+ }
76
+ function countGeneratedScannedFiles(fileMap) {
77
+ return fileMap.files.filter((file) => [
78
+ '.agents/',
79
+ '.claude/',
80
+ '.cursor/',
81
+ '.windsurf/',
82
+ '.clinerules/',
83
+ '.github/prompts/',
84
+ 'AGENTS.md',
85
+ 'CLAUDE.md',
86
+ 'GEMINI.md',
87
+ ].some((prefix) => file.path === prefix || file.path.startsWith(prefix))).length;
88
+ }
43
89
  function analyzeNote(note, maps) {
44
90
  const filePaths = new Set(maps.fileMap.files.map((file) => file.path));
45
91
  const symbolNames = new Set(maps.symbolMap.symbols.map((symbol) => symbol.name));
@@ -12,6 +12,7 @@ export async function queryContext(workspace, config, maps, query) {
12
12
  { name: 'name', value: (symbol) => symbol.name },
13
13
  { name: 'path', value: (symbol) => symbol.filePath },
14
14
  { name: 'kind', value: (symbol) => symbol.kind },
15
+ { name: 'parent', value: (symbol) => symbol.parentName },
15
16
  ]).slice(0, max);
16
17
  const relevantCognition = rankByFields(query, cognition, [
17
18
  { name: 'title', value: (note) => note.title },
@@ -39,6 +40,13 @@ export async function queryContext(workspace, config, maps, query) {
39
40
  ...domain.item.symbols,
40
41
  ]),
41
42
  ]);
43
+ for (const relationship of maps.relationshipMap.relationships) {
44
+ if (relatedIds.has(relationship.sourceId) ||
45
+ relatedIds.has(relationship.targetId)) {
46
+ relatedIds.add(relationship.sourceId);
47
+ relatedIds.add(relationship.targetId);
48
+ }
49
+ }
42
50
  const rankedRelationships = rankByFields(query, maps.relationshipMap.relationships, [
43
51
  { name: 'source', value: (relationship) => relationship.sourceId },
44
52
  { name: 'target', value: (relationship) => relationship.targetId },
@@ -0,0 +1,20 @@
1
+ import type { CognitionNote } from '../types/cognition.js';
2
+ import type { DependencyMap, FileMap, Relationship, RelationshipMap, SymbolMap } from '../types/maps.js';
3
+ import { type Ranked } from './ranking.js';
4
+ export interface ImpactResponse {
5
+ query: string;
6
+ files: Ranked<FileMap['files'][number]>[];
7
+ symbols: Ranked<SymbolMap['symbols'][number]>[];
8
+ importedBy: string[];
9
+ callers: Relationship[];
10
+ calls: Relationship[];
11
+ ownership: Relationship[];
12
+ relatedCognition: CognitionNote[];
13
+ risk: string[];
14
+ }
15
+ export declare function analyzeImpact(query: string, maps: {
16
+ fileMap: FileMap;
17
+ symbolMap: SymbolMap;
18
+ dependencyMap: DependencyMap;
19
+ relationshipMap: RelationshipMap;
20
+ }, cognition: CognitionNote[], max?: number): ImpactResponse;
@@ -0,0 +1,72 @@
1
+ import { rankByFields } from './ranking.js';
2
+ export function analyzeImpact(query, maps, cognition, max = 8) {
3
+ const files = rankByFields(query, maps.fileMap.files, [
4
+ { name: 'path', value: (file) => file.path },
5
+ { name: 'language', value: (file) => file.language },
6
+ ]).slice(0, max);
7
+ const symbols = rankByFields(query, maps.symbolMap.symbols, [
8
+ { name: 'name', value: (symbol) => symbol.name },
9
+ { name: 'path', value: (symbol) => symbol.filePath },
10
+ { name: 'kind', value: (symbol) => symbol.kind },
11
+ { name: 'parent', value: (symbol) => symbol.parentName },
12
+ ]).slice(0, max);
13
+ const filePaths = new Set([
14
+ ...files.map((file) => file.item.path),
15
+ ...symbols.map((symbol) => symbol.item.filePath),
16
+ ]);
17
+ const symbolIds = new Set(symbols.map((symbol) => symbol.item.id));
18
+ const symbolNames = new Set(symbols.map((symbol) => symbol.item.name));
19
+ const importHints = new Set([
20
+ ...[...filePaths].map((file) => basenameWithoutExtension(file).toLowerCase()),
21
+ ...[...symbolNames].map((name) => name.toLowerCase()),
22
+ ]);
23
+ const importedBy = unique(maps.dependencyMap.dependencies
24
+ .filter((dep) => (dep.resolvedFile && filePaths.has(dep.resolvedFile)) ||
25
+ [...importHints].some((hint) => hint && dep.specifier.toLowerCase().includes(hint)))
26
+ .map((dep) => dep.fromFile)).slice(0, max);
27
+ const calls = maps.relationshipMap.relationships
28
+ .filter((rel) => rel.relationshipType === 'calls' &&
29
+ (symbolIds.has(rel.sourceId) || symbolNames.has(rel.sourceId)))
30
+ .slice(0, max);
31
+ const callers = maps.relationshipMap.relationships
32
+ .filter((rel) => rel.relationshipType === 'calls' &&
33
+ (symbolIds.has(rel.targetId) || symbolNames.has(rel.targetId) || [...symbolNames].some((name) => rel.targetId.endsWith(`#${name}`))))
34
+ .slice(0, max);
35
+ const ownership = maps.relationshipMap.relationships
36
+ .filter((rel) => rel.relationshipType === 'symbol-contains' &&
37
+ (symbolIds.has(rel.sourceId) || symbolIds.has(rel.targetId)))
38
+ .slice(0, max);
39
+ const relatedCognition = cognition
40
+ .filter((note) => note.relatedFiles.some((file) => filePaths.has(file)) ||
41
+ note.relatedSymbols.some((symbol) => symbolNames.has(symbol) || symbolIds.has(symbol)))
42
+ .slice(0, max);
43
+ const risk = [];
44
+ if (importedBy.length > 2)
45
+ risk.push(`Shared file imported by ${importedBy.length} files`);
46
+ if (callers.length > 2)
47
+ risk.push(`Symbol called by ${callers.length} known callers`);
48
+ if (relatedCognition.some((note) => note.referencesStatus !== 'current')) {
49
+ risk.push('Related cognition has stale or mixed references');
50
+ }
51
+ if (calls.some((rel) => rel.confidence === 'low') || callers.some((rel) => rel.confidence === 'low')) {
52
+ risk.push('Some call relationships are low confidence');
53
+ }
54
+ return {
55
+ query,
56
+ files,
57
+ symbols,
58
+ importedBy,
59
+ callers,
60
+ calls,
61
+ ownership,
62
+ relatedCognition,
63
+ risk,
64
+ };
65
+ }
66
+ function unique(items) {
67
+ return [...new Set(items)];
68
+ }
69
+ function basenameWithoutExtension(filePath) {
70
+ const basename = filePath.split('/').pop() ?? filePath;
71
+ return basename.replace(/\.[^.]+$/, '');
72
+ }
@@ -1,9 +1,9 @@
1
1
  export function tokenize(query) {
2
- return query
2
+ return expandTokens(query
3
3
  .toLowerCase()
4
4
  .split(/[^a-z0-9_$./-]+/)
5
5
  .map((token) => token.trim())
6
- .filter(Boolean);
6
+ .filter(Boolean));
7
7
  }
8
8
  export function rankByFields(query, items, fields) {
9
9
  const tokens = tokenize(query);
@@ -14,14 +14,19 @@ export function rankByFields(query, items, fields) {
14
14
  for (const field of fields) {
15
15
  const value = field.value(item);
16
16
  const values = Array.isArray(value) ? value : value ? [value] : [];
17
- const haystack = values.join(' ').toLowerCase();
17
+ const haystack = values.flatMap((value) => [value, splitIdentifier(value).join(' ')]).join(' ').toLowerCase();
18
18
  for (const token of tokens) {
19
19
  if (haystack.includes(token)) {
20
- const baseScore = field.name === 'path' || field.name === 'name' ? 3 : 1;
20
+ const baseScore = field.name === 'path' || field.name === 'name'
21
+ ? 4
22
+ : field.name === 'summary' || field.name === 'title'
23
+ ? 2
24
+ : 1;
21
25
  const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
26
  const wordBoundary = new RegExp(`\\b${escaped}\\b`).test(haystack);
23
- score += baseScore + (wordBoundary ? 2 : 0);
24
- reasons.push(`${field.name} matched "${token}"${wordBoundary ? ' (exact)' : ''}`);
27
+ const exactValue = values.some((value) => value.toLowerCase() === token);
28
+ score += baseScore + (wordBoundary ? 2 : 0) + (exactValue ? 4 : 0);
29
+ reasons.push(`${field.name} matched "${token}"${wordBoundary || exactValue ? ' (exact)' : ''}`);
25
30
  }
26
31
  }
27
32
  }
@@ -30,3 +35,13 @@ export function rankByFields(query, items, fields) {
30
35
  .filter((ranked) => ranked.score > 0)
31
36
  .sort((a, b) => b.score - a.score);
32
37
  }
38
+ function expandTokens(tokens) {
39
+ return [...new Set(tokens.flatMap((token) => [token, ...splitIdentifier(token)]))];
40
+ }
41
+ function splitIdentifier(value) {
42
+ return value
43
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
44
+ .split(/[^A-Za-z0-9_$]+/)
45
+ .map((part) => part.toLowerCase())
46
+ .filter((part) => part.length > 1);
47
+ }
@@ -15,13 +15,14 @@ Before exploring the repository, run \`kgraph "<topic>"\` to refresh repo intell
15
15
  2. Run \`kgraph "<topic>"\`. This refreshes maps, processes pending inbox notes, and returns focused context in one command.
16
16
  3. Use the returned files, symbols, relationships, and cognition before broad exploration.
17
17
  4. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
18
- 5. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
19
- 6. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
20
- 7. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
21
- 8. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
22
- 9. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
23
- 10. Run \`kgraph visualize\` when the user wants to inspect the dependency graph opens an interactive graph at http://localhost:4242 with PNG export.
24
- 11. Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
18
+ 5. Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
19
+ 6. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
20
+ 7. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
21
+ 8. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
22
+ 9. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
23
+ 10. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
24
+ 11. Run \`kgraph visualize\` when the user wants to inspect the dependency graph opens an interactive graph at http://localhost:4242 with PNG export.
25
+ 12. Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
25
26
 
26
27
  The inbox note must use this structure:
27
28
  \`\`\`markdown
@@ -64,11 +65,16 @@ Any implementation or product decision future sessions should know.
64
65
  {
65
66
  path: '.claude/commands/kgraph-visualize.md',
66
67
  content: `Run \`kgraph visualize\` to start an interactive dependency graph at http://localhost:4242. Opens in browser automatically. Use \`--no-open\` to print URL only, \`--port <n>\` for a custom port.
68
+ `,
69
+ },
70
+ {
71
+ path: '.claude/commands/kgraph-impact.md',
72
+ content: `Run \`kgraph impact "$ARGUMENTS"\` to show matched files/symbols, import users, callers, callees, related cognition, and risk hints.
67
73
  `,
68
74
  },
69
75
  {
70
76
  path: '.claude/commands/kgraph-history.md',
71
- content: `Run \`kgraph history\` to show a timeline of all processed cognition sessions. Includes git author attribution when available. Use \`--last <n>\` to limit entries, \`--json\` for machine-readable output.
77
+ content: `Run \`kgraph history\` or \`kgraph history "$ARGUMENTS"\` to show processed cognition sessions. Includes git author attribution when available. Use \`--last <n>\` to limit entries, \`--json\` for machine-readable output.
72
78
  `,
73
79
  },
74
80
  ],
@@ -6,12 +6,13 @@ export const clineAdapter = {
6
6
 
7
7
  - **Before exploring the repository**, run \`kgraph "<topic>"\` to refresh maps, process pending inbox notes, and load focused repo intelligence. Use the returned files, symbols, relationships, and cognition before any broad exploration.
8
8
  - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
+ - Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
9
10
  - At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
10
11
  - If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
11
12
  - Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
12
13
  - Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
13
14
  - Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
14
15
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
15
- - Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
16
+ - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
16
17
  `,
17
18
  };
@@ -22,13 +22,14 @@ Workflow:
22
22
  2. Run \`kgraph "<topic>"\` before broad repo exploration. This refreshes maps, processes pending inbox notes, and returns focused context in one command.
23
23
  3. Use KGraph's returned files, symbols, relationships, and cognition as navigation hints.
24
24
  4. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
25
- 5. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
26
- 6. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
27
- 7. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
28
- 8. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
29
- 9. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
30
- 10. Run \`kgraph visualize\` when the user wants to inspect the dependency graph opens an interactive graph at http://localhost:4242 with PNG export.
31
- 11. Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
25
+ 5. Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
26
+ 6. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
27
+ 7. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
28
+ 8. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
29
+ 9. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
30
+ 10. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
31
+ 11. Run \`kgraph visualize\` when the user wants to inspect the dependency graph opens an interactive graph at http://localhost:4242 with PNG export.
32
+ 12. Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
32
33
 
33
34
  The inbox note must use this structure:
34
35
  \`\`\`markdown
@@ -6,11 +6,12 @@ export const copilotAdapter = {
6
6
 
7
7
  1. **Before exploring the repository**, run \`kgraph "<topic>"\` to refresh maps, process pending inbox notes, and load focused repo intelligence. Use the returned files, symbols, relationships, and cognition before any broad exploration.
8
8
  2. Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
- 3. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
10
- 4. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
11
- 5. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
12
- 6. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
13
- 7. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
9
+ 3. Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
10
+ 4. At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
11
+ 5. If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
12
+ 6. Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
13
+ 7. Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
14
+ 8. Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
14
15
 
15
16
  The inbox note must use this structure:
16
17
  \`\`\`markdown
@@ -112,6 +113,17 @@ One or two sentences describing what was done.
112
113
  ## Decisions
113
114
  Any architectural or implementation decisions made.
114
115
  \`\`\`
116
+ `,
117
+ },
118
+ {
119
+ path: '.github/prompts/kgraph-impact.prompt.md',
120
+ content: `---
121
+ description: Show KGraph change impact for a file, symbol, or topic
122
+ agent: agent
123
+ argument-hint: "File, symbol, or topic"
124
+ ---
125
+
126
+ Run \`kgraph impact "$ARGUMENTS"\` to show matched files/symbols, import users, callers, callees, related cognition, and risk hints.
115
127
  `,
116
128
  },
117
129
  {
@@ -119,9 +131,10 @@ Any architectural or implementation decisions made.
119
131
  content: `---
120
132
  description: Show timeline of KGraph cognition sessions with git attribution
121
133
  agent: agent
134
+ argument-hint: "Optional topic"
122
135
  ---
123
136
 
124
- Run \`kgraph history\` to display the timeline of all processed cognition sessions. Summarize who contributed what and when. Use \`--last <n>\` to limit entries.
137
+ Run \`kgraph history\` or \`kgraph history "$ARGUMENTS"\` to display processed cognition sessions. Summarize who contributed what and when. Use \`--last <n>\` to limit entries.
125
138
  `,
126
139
  },
127
140
  ],
@@ -11,13 +11,14 @@ alwaysApply: true
11
11
 
12
12
  - **Before exploring the repository**, run \`kgraph "<topic>"\` to refresh maps, process pending inbox notes, and load focused repo intelligence. Use the returned files, symbols, relationships, and cognition before any broad exploration.
13
13
  - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
14
+ - Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
14
15
  - At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
15
16
  - If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
16
17
  - Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
17
18
  - Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
18
19
  - Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
19
20
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
20
- - Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
21
+ - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
21
22
  `,
22
23
  obsoleteCommandFiles: ['.cursor/rules/kgraph-commands.mdc'],
23
24
  };
@@ -6,12 +6,13 @@ export const geminiAdapter = {
6
6
 
7
7
  - **Before exploring the repository**, run \`kgraph "<topic>"\` to refresh maps, process pending inbox notes, and load focused repo intelligence. Use the returned files, symbols, relationships, and cognition before any broad exploration.
8
8
  - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
+ - Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
9
10
  - At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
10
11
  - If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
11
12
  - Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
12
13
  - Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
13
14
  - Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
14
15
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
15
- - Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
16
+ - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
16
17
  `,
17
18
  };
@@ -6,12 +6,13 @@ export const windsurfAdapter = {
6
6
 
7
7
  - **Before exploring the repository**, run \`kgraph "<topic>"\` to refresh maps, process pending inbox notes, and load focused repo intelligence. Use the returned files, symbols, relationships, and cognition before any broad exploration.
8
8
  - Run \`kgraph doctor\` when setup, maps, inbox processing, or integrations look wrong. Run \`kgraph doctor --quality\` when context shows stale/noisy cognition references.
9
+ - Run \`kgraph impact "<file-or-symbol>"\` when the user asks what a change may affect. Run \`kgraph history "<topic>"\` when prior work or decisions matter.
9
10
  - At the end of any session that changed repository files, check the KGraph capture workflow before finishing.
10
11
  - If the file change has future value, write one concise Markdown note to \`.kgraph/inbox/<slug>.md\`; otherwise explicitly skip capture as trivial.
11
12
  - Skip capture only for read-only work, trivial formatting, typo-only docs, dependency-only churn, mechanical cleanup with no future value, or sessions where no repo files changed.
12
13
  - Do not run KGraph repeatedly. Run it once at the start with \`kgraph "<topic>"\`. If repo files changed, write any needed inbox note first, then run \`kgraph\` once at the end.
13
14
  - Run \`kgraph repair --dry-run\` before cleanup when stale/noisy cognition needs fixing. Run \`kgraph repair\` only when the user asks to apply that cleanup.
14
15
  - Run \`kgraph visualize\` to open the interactive dependency graph at http://localhost:4242 with PNG export.
15
- - Run \`kgraph history\` to review the timeline of past cognition sessions with git author attribution.
16
+ - Run \`kgraph history\` or \`kgraph history "<topic>"\` to review past cognition sessions with git author attribution.
16
17
  `,
17
18
  };
@@ -6,11 +6,14 @@ export function extractTsSymbols(sourceText, filePath) {
6
6
  const dependencies = [];
7
7
  const relationships = [];
8
8
  const warnings = [];
9
+ const symbolIdsByNode = new Map();
10
+ const symbolsByName = new Map();
11
+ const importedBindings = new Map();
9
12
  const addSymbol = (name, kind, node, exported = false, parentName) => {
10
13
  const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
11
14
  const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
12
15
  const id = [filePath, kind, parentName, name, start.line + 1, end.line + 1].filter(Boolean).join("#");
13
- symbols.push({
16
+ const symbol = {
14
17
  id,
15
18
  name,
16
19
  kind,
@@ -19,7 +22,12 @@ export function extractTsSymbols(sourceText, filePath) {
19
22
  endLine: end.line + 1,
20
23
  exported,
21
24
  parentName
22
- });
25
+ };
26
+ symbols.push(symbol);
27
+ symbolIdsByNode.set(node, id);
28
+ const byName = symbolsByName.get(name) ?? [];
29
+ byName.push(symbol);
30
+ symbolsByName.set(name, byName);
23
31
  relationships.push({
24
32
  sourceType: "file",
25
33
  sourceId: filePath,
@@ -28,8 +36,9 @@ export function extractTsSymbols(sourceText, filePath) {
28
36
  relationshipType: "contains",
29
37
  confidence: "high"
30
38
  });
39
+ return symbol;
31
40
  };
32
- const visit = (node, parentName) => {
41
+ const collectSymbols = (node, parentName) => {
33
42
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
34
43
  const specifier = node.moduleSpecifier.text;
35
44
  const dependency = {
@@ -48,6 +57,7 @@ export function extractTsSymbols(sourceText, filePath) {
48
57
  relationshipType: "import",
49
58
  confidence: dependency.resolvedFile ? "high" : "medium"
50
59
  });
60
+ collectImportedBindings(node, specifier, dependency.resolvedFile, importedBindings);
51
61
  }
52
62
  if (ts.isExportDeclaration(node)) {
53
63
  const name = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : "export";
@@ -66,10 +76,18 @@ export function extractTsSymbols(sourceText, filePath) {
66
76
  }
67
77
  }
68
78
  if (ts.isClassDeclaration(node) && node.name) {
69
- addSymbol(node.name.text, "class", node, isExported(node), parentName);
79
+ const classSymbol = addSymbol(node.name.text, "class", node, isExported(node), parentName);
70
80
  node.members.forEach((member) => {
71
81
  if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
72
- addSymbol(member.name.text, "method", member, false, node.name?.text);
82
+ const methodSymbol = addSymbol(member.name.text, "method", member, false, node.name?.text);
83
+ relationships.push({
84
+ sourceType: "symbol",
85
+ sourceId: classSymbol.id,
86
+ targetType: "symbol",
87
+ targetId: methodSymbol.id,
88
+ relationshipType: "symbol-contains",
89
+ confidence: "high"
90
+ });
73
91
  }
74
92
  });
75
93
  }
@@ -79,16 +97,85 @@ export function extractTsSymbols(sourceText, filePath) {
79
97
  if (ts.isTypeAliasDeclaration(node)) {
80
98
  addSymbol(node.name.text, "type", node, isExported(node), parentName);
81
99
  }
82
- ts.forEachChild(node, (child) => visit(child, parentName));
100
+ ts.forEachChild(node, (child) => collectSymbols(child, parentName));
101
+ };
102
+ const collectCalls = (node, currentSymbolId) => {
103
+ const nextSymbolId = symbolIdsByNode.get(node) ?? currentSymbolId;
104
+ if (ts.isCallExpression(node) && nextSymbolId) {
105
+ const target = resolveCallTarget(node, symbolsByName, importedBindings);
106
+ if (target) {
107
+ relationships.push({
108
+ sourceType: "symbol",
109
+ sourceId: nextSymbolId,
110
+ targetType: target.targetType,
111
+ targetId: target.targetId,
112
+ relationshipType: "calls",
113
+ confidence: target.confidence
114
+ });
115
+ }
116
+ }
117
+ ts.forEachChild(node, (child) => collectCalls(child, nextSymbolId));
83
118
  };
84
119
  try {
85
- visit(sourceFile);
120
+ collectSymbols(sourceFile);
121
+ collectCalls(sourceFile);
86
122
  }
87
123
  catch (error) {
88
124
  warnings.push(error instanceof Error ? error.message : String(error));
89
125
  }
90
126
  return { symbols, dependencies, relationships, warnings };
91
127
  }
128
+ function collectImportedBindings(node, specifier, resolvedFile, importedBindings) {
129
+ const clause = node.importClause;
130
+ if (!clause) {
131
+ return;
132
+ }
133
+ if (clause.name) {
134
+ importedBindings.set(clause.name.text, { specifier, resolvedFile });
135
+ }
136
+ const bindings = clause.namedBindings;
137
+ if (bindings && ts.isNamedImports(bindings)) {
138
+ for (const element of bindings.elements) {
139
+ importedBindings.set(element.name.text, { specifier, resolvedFile });
140
+ }
141
+ }
142
+ }
143
+ function resolveCallTarget(node, symbolsByName, importedBindings) {
144
+ const expression = node.expression;
145
+ if (ts.isIdentifier(expression)) {
146
+ const localSymbols = symbolsByName
147
+ .get(expression.text)
148
+ ?.filter((symbol) => symbol.kind === "function" || symbol.kind === "method");
149
+ if (localSymbols?.[0]) {
150
+ return {
151
+ targetType: "symbol",
152
+ targetId: localSymbols[0].id,
153
+ confidence: "high"
154
+ };
155
+ }
156
+ const imported = importedBindings.get(expression.text);
157
+ if (imported) {
158
+ return {
159
+ targetType: "symbol",
160
+ targetId: imported.resolvedFile ? `${imported.resolvedFile}#${expression.text}` : expression.text,
161
+ confidence: imported.resolvedFile ? "medium" : "low"
162
+ };
163
+ }
164
+ return {
165
+ targetType: "symbol",
166
+ targetId: expression.text,
167
+ confidence: "low"
168
+ };
169
+ }
170
+ if (ts.isPropertyAccessExpression(expression)) {
171
+ return {
172
+ targetType: "symbol",
173
+ targetId: expression.getText(),
174
+ confidence: "low"
175
+ };
176
+ }
177
+ return undefined;
178
+ }
92
179
  function isExported(node) {
93
180
  return ts.canHaveModifiers(node) && Boolean(ts.getModifiers(node)?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword));
94
181
  }
@@ -1,7 +1,7 @@
1
1
  export type ScanStatus = "mapped" | "generic" | "failed";
2
2
  export type DependencyKind = "local" | "package" | "unknown";
3
3
  export type SymbolKind = "function" | "class" | "method" | "type" | "interface" | "export" | "import";
4
- export type RelationshipType = "import" | "contains" | "mentions" | "belongs-to-domain" | "stale-reference" | "moved-from";
4
+ export type RelationshipType = "import" | "contains" | "symbol-contains" | "calls" | "mentions" | "belongs-to-domain" | "stale-reference" | "moved-from";
5
5
  export interface RepositoryFile {
6
6
  id: string;
7
7
  path: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {