@kentwynn/kgraph 0.1.20 → 0.1.21

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
 
@@ -240,7 +241,7 @@ The files are local, inspectable, and human-readable. There is no database, tele
240
241
 
241
242
  KGraph deeply scans:
242
243
 
243
- - TypeScript and JavaScript
244
+ - TypeScript and JavaScript, including lightweight function/method call relationships
244
245
  - Python
245
246
  - Go
246
247
  - Rust
@@ -248,7 +249,7 @@ KGraph deeply scans:
248
249
  - C and C++
249
250
  - C#
250
251
 
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.
252
+ 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
253
 
253
254
  ## Visualization
254
255
 
@@ -256,7 +257,7 @@ Other common file types still appear in the file map with generic metadata, so c
256
257
  kgraph visualize
257
258
  ```
258
259
 
259
- The graph shows files, symbols, imports, cognition notes, and relationship edges. Cognition notes are colored by reference health:
260
+ 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
261
 
261
262
  - current
262
263
  - mixed
@@ -39,6 +39,13 @@ export async function queryContext(workspace, config, maps, query) {
39
39
  ...domain.item.symbols,
40
40
  ]),
41
41
  ]);
42
+ for (const relationship of maps.relationshipMap.relationships) {
43
+ if (relatedIds.has(relationship.sourceId) ||
44
+ relatedIds.has(relationship.targetId)) {
45
+ relatedIds.add(relationship.sourceId);
46
+ relatedIds.add(relationship.targetId);
47
+ }
48
+ }
42
49
  const rankedRelationships = rankByFields(query, maps.relationshipMap.relationships, [
43
50
  { name: 'source', value: (relationship) => relationship.sourceId },
44
51
  { name: 'target', value: (relationship) => relationship.targetId },
@@ -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.21",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {