@youichi/repograph 0.1.1 → 0.1.2

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
@@ -1,5 +1,7 @@
1
1
  # repograph
2
2
 
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
3
5
  Export repository indexes as JSON graphs for other programs using the [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) CLI. No language model, API key, or MCP client is required.
4
6
 
5
7
  ## Installation
@@ -90,7 +92,7 @@ Node IDs and edge IDs are strings in separate namespaces. An edge's `source` and
90
92
 
91
93
  TypeScript and JavaScript `Function` and `Method` nodes include `callable` metadata in both `all` and `calls` exports. The exporter parses upstream `properties.signature` and `properties.return_type` with the TypeScript parser, without loading the target project's dependencies or inferring types. Supported file extensions are `.ts`, `.tsx`, `.mts`, `.cts`, `.js`, `.jsx`, `.mjs`, and `.cjs`. Other languages and node labels omit `callable`. The original `properties` remain intact, and this additive field retains `schemaVersion: 1`.
92
94
 
93
- In `all` exports, TypeScript and JavaScript `Class` and `Interface` nodes include a `members` array for declared fields and accessors. Entries retain declared type text (or `null` when absent), source lines, visibility, static, optional, and readonly flags. The exporter also adds `DEPENDS_ON` edges for resolvable field, method-parameter, and method-return type references, plus method calls between classes. These source-derived edges have `properties.derivedFrom: "typescript-ast"` and a `reason`; they are omitted when a referenced class cannot be resolved without ambiguity. `calls` exports intentionally omit these class members and dependencies. Other languages retain whatever class, field, inheritance, implementation, and dependency nodes and edges the upstream indexer supplies.
95
+ In `all` exports, class-like nodes include a normalized `members` array whenever the upstream indexer emits `Field` or `Property` nodes. Each member also remains a graph node and receives a `HAS_PROPERTY` edge from its owning class. Entries include declared type text when available, source lines, visibility, static, optional, and readonly flags. The exporter derives `DEPENDS_ON` edges from upstream field and method type metadata when the target class resolves unambiguously. TypeScript and JavaScript receive an AST fallback for declared fields, accessors, and type references when upstream field nodes are absent. Derived edges identify their source in `properties.derivedFrom` (`upstream-index` or `typescript-ast`) and include a `reason`. `calls` exports intentionally omit class members and these dependencies. Available fields and relationships still depend on what the upstream indexer extracts for each language.
94
96
 
95
97
  For example, `Customer` with an `address?: Address` field and a `save(input: Address): Result` method includes:
96
98
 
@@ -115,7 +117,7 @@ For example, `Customer` with an `address?: Address` field and a `save(input: Add
115
117
  }
116
118
  ```
117
119
 
118
- The graph also contains edges such as `{ "source": "<Customer id>", "target": "<Address id>", "type": "DEPENDS_ON", "properties": { "derivedFrom": "typescript-ast", "reason": "field-type", "member": "address" } }`. Existing upstream `INHERITS` and `IMPLEMENTS` edges are retained as-is.
120
+ The graph also contains a `HAS_PROPERTY` edge from `Customer` to its `address` field node and an edge such as `{ "source": "<Customer id>", "target": "<Address id>", "type": "DEPENDS_ON", "properties": { "derivedFrom": "upstream-index", "reason": "field-type", "member": "address" } }`. Existing upstream `INHERITS` and `IMPLEMENTS` edges are retained as-is.
119
121
 
120
122
  For example, `function load(id: string, count = 1, ...tags: string[]): Promise<string[]>` produces:
121
123
 
@@ -163,9 +165,12 @@ for (const node of graph.nodes) {
163
165
  bun run typecheck
164
166
  bun test
165
167
  bun run test:integration
168
+ bun run test:languages
166
169
  ```
167
170
 
168
- Unit tests cover pagination, cursors, truncation handling, file line counts, and structured callable metadata. The integration test uses the real native backend to index a temporary TypeScript project, checks cross-file calls, isolated functions, parameter and return annotations, methods, and export counts, and cleans up its temporary index.
171
+ Unit tests cover pagination, cursors, truncation handling, file line counts, structured callable metadata, and completion of asynchronous class enrichment. Integration tests use the real native backend for the original multi-file TypeScript project and a corpus covering all 162 vendored grammars in the pinned backend. The language suite exercises the CLI's JSON output and the API's `calls` projection, definition preservation, file line counts, graph integrity, parse diagnostics, isolated functions, and class metadata. All 66 caller/callee samples require a resolved `CALLS` edge in both export modes. Temporary repositories and indexes are removed after each suite.
172
+
173
+ See [the language corpus](test/fixtures/languages/README.md) for coverage boundaries, fixture provenance, and the optional JSON coverage report. The presence of a vendored grammar does not guarantee that the upstream CLI can discover it as a standalone source file.
169
174
 
170
175
  ## Publishing
171
176
 
@@ -176,7 +181,7 @@ Releases are published from GitHub Actions when a version tag is pushed. Before
176
181
  - Exports contain the static relationships actually indexed upstream and do not guarantee coverage of all runtime calls. Support for reflection, dynamic dispatch, and framework conventions varies by language.
177
182
  - The upstream indexer skips `node_modules`, files matching ignore rules, and other excluded inputs. Use the skip and parse counts in `indexReport` to assess coverage.
178
183
  - `calls` mode retains only `CALLS` edges. It does not treat `CALL_REFERENCE` or `USAGE` as direct calls. The upstream analyzer may also record recursion in properties while omitting self-call edges.
179
- - Source-derived class members and type dependencies currently cover TypeScript and JavaScript files. Other languages depend on the class, field, and relationship data available from the pinned upstream indexer; type references are not inferred from unannotated initializers.
184
+ - The exporter normalizes upstream class fields and type metadata without language-specific adapters. The pinned indexer does not emit every field or type relationship for every language, so missing upstream information cannot be reconstructed generically. Type references are not inferred from unannotated initializers.
180
185
  - Keep source files and the project's index unchanged during export, and prevent other processes from rebuilding the index concurrently. Exports are not transactional snapshots across queries.
181
186
  - Results are assembled in memory before being written to disk. Large repositories require sufficient memory, and upstream evaluation limits cause an error.
182
187
  - Place output outside the target repository to prevent a later indexing run from treating exported files as input. Upstream indexes remain in the local cache.
@@ -0,0 +1,189 @@
1
+ # repograph
2
+
3
+ [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ 使用 [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) CLI 将代码仓库索引导出为 JSON 图数据,供其他程序使用。无需语言模型、API 密钥或 MCP 客户端。
6
+
7
+ ## 安装
8
+
9
+ 需要 Node.js 22 或更高版本。通过 `npx` 直接运行:
10
+
11
+ ```bash
12
+ npx @youichi/repograph export /path/to/repository
13
+ ```
14
+
15
+ 本项目固定使用 `codebase-memory-mcp@0.12.1`。首次安装会下载适用于当前平台的原生二进制文件,需要访问 npm 和 GitHub。后续索引操作均在本地运行。无需全局安装,也无需通过 `codebase-memory-mcp install` 配置编辑器或智能体。
16
+
17
+ ## 导出图数据
18
+
19
+ 可在任意目录运行以下命令:
20
+
21
+ ```bash
22
+ # 导出索引中的全部节点和关系,以及从源码提取的类成员和依赖
23
+ npx @youichi/repograph export ../anureo-sdk
24
+
25
+ # 指定输出文件
26
+ npx @youichi/repograph export /path/to/repository --out .repograph/repository.json
27
+
28
+ # 仅导出 CALLS 边、函数和方法,以及这些边的端点
29
+ npx @youichi/repograph export ../anureo-sdk --kind calls --out .repograph/calls.json
30
+
31
+ # 查看可用选项
32
+ npx @youichi/repograph --help
33
+ ```
34
+
35
+ 默认输出路径为当前工作目录下的 `.repograph/graph.json`。进度信息写入 stderr;成功后,stdout 会显示输出路径以及节点和边的数量。失败时以退出码 1 结束,部分结果不会覆盖已有的输出文件。
36
+
37
+ | 选项 | 默认值 | 用途 |
38
+ | --- | --- | --- |
39
+ | `--out`, `-o` | `.repograph/graph.json` | JSON 输出路径 |
40
+ | `--kind` | `all` | `all` 包含类结构和依赖;`calls` 仅保留直接调用关系 |
41
+ | `--project` | 目录名 + 绝对路径的哈希值 | 自定义上游索引名称 |
42
+ | `--page-size` | `500` | 每批查询的行数,范围为 1 至 10000 |
43
+
44
+ 每次导出都会先刷新索引,再分别读取节点和边。查询支持分页和响应游标。如果检测到求值限制、截断后缺少后续游标、重复 ID、缺失端点或数量不匹配,导出会停止。调用图会保留没有调用边的函数和方法。
45
+
46
+ ## 输出结构
47
+
48
+ 以下为简化示例。实际属性取决于编程语言和上游分析结果:
49
+
50
+ ```json
51
+ {
52
+ "schemaVersion": 1,
53
+ "generatedAt": "2026-11-12T00:00:00.000Z",
54
+ "repositoryPath": "/path/to/repository",
55
+ "project": "repository-0123456789ab",
56
+ "kind": "calls",
57
+ "indexReport": { "status": "indexed", "nodes": 2, "edges": 1 },
58
+ "files": {
59
+ "main.ts": { "lines": 7 }
60
+ },
61
+ "nodes": [
62
+ {
63
+ "id": "1",
64
+ "labels": ["Function"],
65
+ "name": "checkout",
66
+ "qualifiedName": "repository.main.checkout",
67
+ "filePath": "main.ts",
68
+ "startLine": 1,
69
+ "endLine": 3,
70
+ "properties": {},
71
+ "callable": { "parameters": [], "returnType": null }
72
+ },
73
+ {
74
+ "id": "2",
75
+ "labels": ["Function"],
76
+ "name": "validate",
77
+ "qualifiedName": "repository.main.validate",
78
+ "filePath": "main.ts",
79
+ "startLine": 5,
80
+ "endLine": 7,
81
+ "properties": {},
82
+ "callable": { "parameters": [], "returnType": null }
83
+ }
84
+ ],
85
+ "edges": [
86
+ { "id": "1", "source": "1", "target": "2", "type": "CALLS", "properties": {} }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ 节点 ID 和边 ID 均为字符串,分属独立的命名空间。边的 `source` 和 `target` 指向节点 ID。这些 ID 用于关联当前导出中的记录,不保证在多次索引之间保持稳定。`indexReport` 保留上游索引报告;其中的数量描述的是上游索引,在过滤或添加从源码提取的类数据后,可能与导出的图数据不同。
92
+
93
+ 在 `all` 和 `calls` 两种导出模式中,TypeScript 和 JavaScript 的 `Function`、`Method` 节点均包含 `callable` 元数据。导出器使用 TypeScript 解析器解析上游的 `properties.signature` 和 `properties.return_type`,不会加载目标项目的依赖或推断类型。支持的文件扩展名为 `.ts`、`.tsx`、`.mts`、`.cts`、`.js`、`.jsx`、`.mjs` 和 `.cjs`。其他语言和节点标签不包含 `callable`。原始 `properties` 保持不变,新增此字段后仍使用 `schemaVersion: 1`。
94
+
95
+ 在 `all` 模式下,只要上游索引器生成了 `Field` 或 `Property` 节点,类及类似结构的节点就会包含规范化的 `members` 数组。每个成员同时保留为图节点,并通过一条从所属类指向该成员的 `HAS_PROPERTY` 边关联。成员条目包含可用的声明类型文本、源码行号、可见性,以及静态、可选和只读标记。当目标类可被唯一确定时,导出器会根据上游字段和方法的类型元数据派生 `DEPENDS_ON` 边。如果缺少上游字段节点,TypeScript 和 JavaScript 会通过 AST 补充解析声明的字段、访问器和类型引用。派生边通过 `properties.derivedFrom` 标明来源(`upstream-index` 或 `typescript-ast`),并包含 `reason`。`calls` 模式不包含类成员及这些依赖。可用字段和关系仍取决于上游索引器对各语言的提取能力。
96
+
97
+ 例如,若 `Customer` 包含 `address?: Address` 字段和 `save(input: Address): Result` 方法,导出结果会包含:
98
+
99
+ ```json
100
+ {
101
+ "labels": ["Class"],
102
+ "name": "Customer",
103
+ "members": [
104
+ {
105
+ "name": "address",
106
+ "kind": "field",
107
+ "type": "Address",
108
+ "filePath": "model.ts",
109
+ "startLine": 6,
110
+ "endLine": 6,
111
+ "visibility": "public",
112
+ "static": false,
113
+ "optional": true,
114
+ "readonly": false
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ 图中还会包含一条从 `Customer` 指向其 `address` 字段节点的 `HAS_PROPERTY` 边,以及类似 `{ "source": "<Customer id>", "target": "<Address id>", "type": "DEPENDS_ON", "properties": { "derivedFrom": "upstream-index", "reason": "field-type", "member": "address" } }` 的边。上游已有的 `INHERITS` 和 `IMPLEMENTS` 边会原样保留。
121
+
122
+ 例如,`function load(id: string, count = 1, ...tags: string[]): Promise<string[]>` 会生成:
123
+
124
+ ```json
125
+ {
126
+ "callable": {
127
+ "parameters": [
128
+ { "name": "id", "type": "string", "optional": false, "rest": false },
129
+ { "name": "count", "type": null, "optional": true, "rest": false },
130
+ { "name": "tags", "type": "string[]", "optional": false, "rest": true }
131
+ ],
132
+ "returnType": "Promise<string[]>"
133
+ }
134
+ }
135
+ ```
136
+
137
+ 参数顺序与签名一致。`name` 保留源码中的绑定名称或解构模式。`type` 为声明的类型文本,没有类型注解时为 `null`。带有 `?` 标记或默认初始值的参数,其 `optional` 为 true;`rest` 用于标识 `...` 参数。默认值不会触发类型推断。如果上游签名包含 TypeScript 源码中的 `this` 参数,该参数也会保留。
138
+
139
+ `parameters: []` 表示已知参数列表为空;`parameters: null` 表示上游签名缺失或无法解析。`returnType` 不包含开头的冒号,没有类型注解时为 `null`;显式声明的 `void`、`never` 和 `Promise<T>` 会保留。如果签名与返回类型注解组合后格式无效,两个字段均为 `null`。缺失的类型不会被替换为 `any` 或 `void`。导出器仅导出每个上游节点保留的签名,不会重建独立的重载、成对的访问器、泛型参数声明或上游遗漏的符号。
140
+
141
+ `files` 以各个 `File` 节点的 `filePath` 为键,通过 `lines` 记录文件的实际行数,包括空行和注释。空文件为 `0` 行,文件末尾的换行符不会额外增加一行。支持 LF、CRLF 和 CR 换行符。无法读取的文件,其行数为 `null`。行数在导出时直接从源码文件读取,不根据上游节点的 `properties.lines` 推断。`all` 和 `calls` 模式都会保留完整索引中的文件信息,即使调用图已过滤掉文件节点。不会扫描索引之外的文件。
142
+
143
+ ## 在 TypeScript 中使用
144
+
145
+ 本项目也导出了一个轻量封装,内部调用 CLI:
146
+
147
+ ```typescript
148
+ import { exportGraph } from "./src/index.ts";
149
+
150
+ const graph = await exportGraph("../anureo-sdk", {
151
+ kind: "calls",
152
+ onProgress: console.error,
153
+ });
154
+
155
+ console.log(graph.nodes, graph.edges);
156
+
157
+ for (const node of graph.nodes) {
158
+ if (node.callable) console.log(node.name, node.callable.parameters, node.callable.returnType);
159
+ }
160
+ ```
161
+
162
+ ## 开发与验证
163
+
164
+ ```bash
165
+ bun run typecheck
166
+ bun test
167
+ bun run test:integration
168
+ bun run test:languages
169
+ ```
170
+
171
+ 单元测试覆盖分页、游标、截断处理、文件行数、结构化的可调用对象元数据,以及异步类成员提取完成后才返回的行为。集成测试使用真实原生后端,覆盖原有多文件 TypeScript 项目和固定依赖中的全部 162 种语法样例,校验 CLI JSON 输出、API 的 `calls` 图、定义保留、文件行数、图完整性、解析诊断、孤立函数和类元数据。66 组调用样例均要求在两种导出模式中生成 `CALLS` 边。测试结束后清理临时仓库和索引。
172
+
173
+ 覆盖边界、样例来源和可选 JSON 报告见[全语言测试说明](test/fixtures/languages/README.md)。内置语法不一定能通过上游 CLI 作为独立源码文件被识别。
174
+
175
+ ## 发布
176
+
177
+ 推送版本标签后,GitHub Actions 会发布新版本。首次发布前,请为 npm 包 `@youichi/repograph` 配置 Trusted Publishing,使用 GitHub 用户 `hi-youichi`、仓库 `repograph` 和工作流文件 `publish-npm.yml`,并允许该发布方直接执行 `npm publish`。随后将 `package.json` 更新为新版本,再推送对应标签,例如 `v0.1.1`。工作流会检查标签是否与包版本匹配、安装依赖、运行类型检查和单元测试,然后构建并发布,同时附带 npm provenance 来源证明。GitHub Secrets 中不存储 npm 令牌。
178
+
179
+ ## 限制
180
+
181
+ - 导出结果包含上游实际索引到的静态关系,不保证覆盖所有运行时调用。对反射、动态分派和框架约定的支持因语言而异。
182
+ - 上游索引器会跳过 `node_modules`、匹配忽略规则的文件及其他被排除的输入。可通过 `indexReport` 中的跳过和解析数量评估覆盖范围。
183
+ - `calls` 模式仅保留 `CALLS` 边,不会将 `CALL_REFERENCE` 或 `USAGE` 视为直接调用。上游分析器也可能在属性中记录递归信息,而不生成自调用边。
184
+ - 导出器无需针对各语言编写适配器,即可规范化上游类字段和类型元数据。固定版本的索引器并不会为每种语言生成全部字段或类型关系,因此无法用通用方式重建上游缺失的信息。不会根据没有类型注解的初始值推断类型引用。
185
+ - 导出期间请保持源码文件和项目索引不变,并防止其他进程同时重建索引。导出结果不是跨查询的事务性快照。
186
+ - 结果会先在内存中组装,再写入磁盘。大型仓库需要足够内存;触及上游求值限制时会报错。
187
+ - 请将输出放在目标仓库之外,以免后续索引将导出文件视为输入。上游索引会保留在本地缓存中。
188
+
189
+ 本项目目前仅导出 JSON,可使用 Graphviz、Cytoscape 或其他前端工具进行可视化。
package/dist/classes.js CHANGED
@@ -40,6 +40,24 @@ function typeNames(type, into = new Set) {
40
40
  visit(type);
41
41
  return into;
42
42
  }
43
+ function referencedNames(typeText) {
44
+ return new Set(typeText.match(/[\p{L}_$][\p{L}\p{N}_$]*/gu) ?? []);
45
+ }
46
+ function propertyText(node, ...keys) {
47
+ for (const key of keys) {
48
+ const value = node.properties[key];
49
+ if (typeof value === "string" && value.trim())
50
+ return value.replace(/^\s*:\s*/, "").trim();
51
+ }
52
+ return;
53
+ }
54
+ function propertyTypes(value) {
55
+ if (typeof value === "string")
56
+ return [value];
57
+ if (Array.isArray(value))
58
+ return value.filter((item) => typeof item === "string");
59
+ return [];
60
+ }
43
61
  function sourceLine(file, node) {
44
62
  return file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1;
45
63
  }
@@ -89,6 +107,14 @@ async function addClassMetadata(repositoryPath, nodes, edges) {
89
107
  const byName = new Map;
90
108
  for (const node of classNodes)
91
109
  byName.set(node.name, [...byName.get(node.name) ?? [], node]);
110
+ const byQualifiedName = new Map;
111
+ for (const node of classNodes) {
112
+ byQualifiedName.set(node.qualifiedName, [...byQualifiedName.get(node.qualifiedName) ?? [], node]);
113
+ }
114
+ const resolveClass = (name, filePath) => {
115
+ const candidates = byName.get(name) ?? [];
116
+ return candidates.find((item) => item.filePath === filePath) ?? (candidates.length === 1 ? candidates[0] : undefined);
117
+ };
92
118
  const sources = new Map;
93
119
  const paths = new Set(classNodes.map((node) => node.filePath).filter(Boolean));
94
120
  for (const filePath of paths) {
@@ -101,8 +127,9 @@ async function addClassMetadata(repositoryPath, nodes, edges) {
101
127
  } catch {}
102
128
  }
103
129
  const dependencies = new Map;
130
+ const propertyEdges = new Map;
104
131
  const existingDependencyPairs = new Set(edges.filter((edge) => edge.type === "DEPENDS_ON").map((edge) => `${edge.source}\x00${edge.target}`));
105
- const addDependency = (source, target, reason, member) => {
132
+ const addDependency = (source, target, reason, member, derivedFrom) => {
106
133
  if (source.id === target.id)
107
134
  return;
108
135
  const pair = `${source.id}\x00${target.id}`;
@@ -114,9 +141,72 @@ async function addClassMetadata(repositoryPath, nodes, edges) {
114
141
  source: source.id,
115
142
  target: target.id,
116
143
  type: "DEPENDS_ON",
117
- properties: { derivedFrom: "typescript-ast", reason, member }
144
+ properties: { derivedFrom, reason, member }
118
145
  });
119
146
  };
147
+ for (const field of nodes.filter((node) => node.labels.includes("Field") || node.labels.includes("Property"))) {
148
+ const parent = field.properties.parent_class;
149
+ if (typeof parent !== "string")
150
+ continue;
151
+ const candidates = byQualifiedName.get(parent) ?? [];
152
+ const owner = candidates.find((item) => item.filePath === field.filePath) ?? (candidates.length === 1 ? candidates[0] : undefined);
153
+ if (!owner)
154
+ continue;
155
+ const visibilityText = propertyText(field, "visibility", "access")?.toLowerCase();
156
+ const member = {
157
+ name: field.name,
158
+ kind: "field",
159
+ type: propertyText(field, "return_type", "type", "field_type") ?? null,
160
+ filePath: field.filePath,
161
+ startLine: field.startLine ?? owner.startLine ?? 1,
162
+ endLine: field.endLine ?? field.startLine ?? owner.endLine ?? 1,
163
+ visibility: visibilityText === "private" || visibilityText === "protected" ? visibilityText : "public",
164
+ static: field.properties.static === true || field.properties.is_static === true,
165
+ optional: field.properties.optional === true || field.properties.is_optional === true,
166
+ readonly: field.properties.readonly === true || field.properties.is_readonly === true
167
+ };
168
+ owner.members = [...owner.members ?? [], member];
169
+ const edgeId = `derived:${hash(`${owner.id}\x00${field.id}\x00HAS_PROPERTY`)}`;
170
+ propertyEdges.set(edgeId, {
171
+ id: edgeId,
172
+ source: owner.id,
173
+ target: field.id,
174
+ type: "HAS_PROPERTY",
175
+ properties: { derivedFrom: "upstream-index" }
176
+ });
177
+ if (member.type) {
178
+ for (const typeName of referencedNames(member.type)) {
179
+ const target = resolveClass(typeName, field.filePath);
180
+ if (target)
181
+ addDependency(owner, target, "field-type", field.name, "upstream-index");
182
+ }
183
+ }
184
+ }
185
+ for (const method of nodes.filter((node) => node.labels.includes("Method"))) {
186
+ const parent = method.properties.parent_class;
187
+ if (typeof parent !== "string")
188
+ continue;
189
+ const candidates = byQualifiedName.get(parent) ?? [];
190
+ const owner = candidates.find((item) => item.filePath === method.filePath) ?? (candidates.length === 1 ? candidates[0] : undefined);
191
+ if (!owner)
192
+ continue;
193
+ const parameterTypes = propertyTypes(method.properties.param_types);
194
+ for (const typeText of parameterTypes) {
195
+ for (const typeName of referencedNames(typeText)) {
196
+ const target = resolveClass(typeName, method.filePath);
197
+ if (target)
198
+ addDependency(owner, target, "method-parameter", method.name, "upstream-index");
199
+ }
200
+ }
201
+ const returnType = propertyText(method, "return_type", "returnType");
202
+ if (returnType) {
203
+ for (const typeName of referencedNames(returnType)) {
204
+ const target = resolveClass(typeName, method.filePath);
205
+ if (target)
206
+ addDependency(owner, target, "method-return", method.name, "upstream-index");
207
+ }
208
+ }
209
+ }
120
210
  for (const [filePath, file] of sources) {
121
211
  const visit = (node) => {
122
212
  if (!isClassLike(node) || !node.name) {
@@ -139,10 +229,9 @@ async function addClassMetadata(repositoryPath, nodes, edges) {
139
229
  members.push(value);
140
230
  if (propertyType) {
141
231
  for (const typeName of typeNames(propertyType)) {
142
- const targets = byName.get(typeName) ?? [];
143
- const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
232
+ const target = resolveClass(typeName, filePath);
144
233
  if (target)
145
- addDependency(owner, target, "field-type", value.name);
234
+ addDependency(owner, target, "field-type", value.name, "typescript-ast");
146
235
  }
147
236
  }
148
237
  }
@@ -151,41 +240,45 @@ async function addClassMetadata(repositoryPath, nodes, edges) {
151
240
  if (!parameter.type)
152
241
  continue;
153
242
  for (const typeName of typeNames(parameter.type)) {
154
- const targets = byName.get(typeName) ?? [];
155
- const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
243
+ const target = resolveClass(typeName, filePath);
156
244
  if (target)
157
- addDependency(owner, target, "method-parameter", member.name.getText(file));
245
+ addDependency(owner, target, "method-parameter", member.name.getText(file), "typescript-ast");
158
246
  }
159
247
  }
160
248
  if (member.type) {
161
249
  for (const typeName of typeNames(member.type)) {
162
- const targets = byName.get(typeName) ?? [];
163
- const target = targets.length === 1 ? targets[0] : targets.find((item) => item.filePath === filePath);
250
+ const target = resolveClass(typeName, filePath);
164
251
  if (target)
165
- addDependency(owner, target, "method-return", member.name.getText(file));
252
+ addDependency(owner, target, "method-return", member.name.getText(file), "typescript-ast");
166
253
  }
167
254
  }
168
255
  }
169
256
  };
170
257
  for (const member of node.members)
171
258
  collect(member);
172
- if (members.length)
173
- owner.members = members;
259
+ for (const member of members) {
260
+ const isAlreadyIndexed = (owner.members ?? []).some((existing) => existing.name === member.name && existing.filePath === member.filePath && existing.startLine === member.startLine && existing.kind === member.kind);
261
+ if (!isAlreadyIndexed)
262
+ owner.members = [...owner.members ?? [], member];
263
+ }
174
264
  ts.forEachChild(node, visit);
175
265
  };
176
266
  visit(file);
177
267
  }
178
- const classesByQualifiedName = new Map(classNodes.map((node) => [node.qualifiedName, node]));
179
- const methodOwners = new Map(nodes.filter((node) => node.labels.includes("Method") && typeof node.properties.parent_class === "string").map((node) => [node.id, classesByQualifiedName.get(node.properties.parent_class)]));
268
+ const classForQualifiedName = (qualifiedName, filePath) => {
269
+ const candidates = byQualifiedName.get(qualifiedName) ?? [];
270
+ return candidates.find((item) => item.filePath === filePath) ?? (candidates.length === 1 ? candidates[0] : undefined);
271
+ };
272
+ const methodOwners = new Map(nodes.filter((node) => node.labels.includes("Method") && typeof node.properties.parent_class === "string").map((node) => [node.id, classForQualifiedName(node.properties.parent_class, node.filePath)]));
180
273
  const nodesById = new Map(nodes.map((node) => [node.id, node]));
181
274
  for (const edge of edges.filter((item) => item.type === "CALLS")) {
182
275
  const source = methodOwners.get(edge.source);
183
276
  const targetNode = nodesById.get(edge.target);
184
277
  const target = targetNode && methodOwners.get(targetNode.id) || (targetNode?.labels.some((label) => CLASS_LABELS.has(label)) ? targetNode : undefined);
185
278
  if (source && target)
186
- addDependency(source, target, "method-call", targetNode?.name ?? "");
279
+ addDependency(source, target, "method-call", targetNode?.name ?? "", "codebase-memory-mcp");
187
280
  }
188
- edges.push(...dependencies.values());
281
+ edges.push(...propertyEdges.values(), ...dependencies.values());
189
282
  }
190
283
  export {
191
284
  addClassMetadata
package/dist/index.js CHANGED
@@ -121,7 +121,7 @@ async function exportGraph(repository, options = {}, run = runTool) {
121
121
  addCallables(selectedNodes);
122
122
  if (kind === "all") {
123
123
  options.onProgress?.("Extracting class members and type dependencies");
124
- addClassMetadata(repositoryPath, nodes, edges);
124
+ await addClassMetadata(repositoryPath, nodes, edges);
125
125
  }
126
126
  return {
127
127
  schemaVersion: 1,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@youichi/repograph",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Export repository code graphs with codebase-memory-mcp.",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/hi-youichi/repograph.git"
7
+ "url": "git+https://github.com/hi-youichi/repograph.git"
8
8
  },
9
9
  "type": "module",
10
10
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "start": "bun run src/cli.ts",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "test": "bun test",
18
- "test:integration": "REPOGRAPH_INTEGRATION=1 bun test test/integration.test.ts",
18
+ "test:integration": "REPOGRAPH_INTEGRATION=1 bun test test/integration.test.ts test/languages.integration.test.ts",
19
+ "test:languages": "REPOGRAPH_INTEGRATION=1 bun test test/languages.integration.test.ts",
19
20
  "build": "bun build ./src/cli.ts ./src/index.ts ./src/cbm.ts ./src/callables.ts ./src/classes.ts --target node --outdir dist --external \"*\"",
20
21
  "prepack": "bun run build"
21
22
  },