@youichi/repograph 0.1.0 → 0.1.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
@@ -17,13 +17,13 @@ The project pins `codebase-memory-mcp@0.12.1`. The initial installation download
17
17
  Run these commands from any directory:
18
18
 
19
19
  ```bash
20
- # Export all nodes and relationships: functions, files, modules, calls, imports, inheritance, etc.
20
+ # Export all indexed nodes and relationships, plus source-derived class members and dependencies
21
21
  npx @youichi/repograph export ../anureo-sdk
22
22
 
23
23
  # Specify an output file
24
24
  npx @youichi/repograph export /path/to/repository --out .repograph/repository.json
25
25
 
26
- # Keep only CALLS edges, functions, methods, and the endpoints of those edges
26
+ # Export only CALLS edges, functions/methods, and their endpoints
27
27
  npx @youichi/repograph export ../anureo-sdk --kind calls --out .repograph/calls.json
28
28
 
29
29
  # Show available options
@@ -35,7 +35,7 @@ The default output is `.repograph/graph.json` under the current working director
35
35
  | Option | Default | Purpose |
36
36
  | --- | --- | --- |
37
37
  | `--out`, `-o` | `.repograph/graph.json` | JSON output path |
38
- | `--kind` | `all` | `all` exports all relationships; `calls` exports the call graph |
38
+ | `--kind` | `all` | `all` includes class structure and dependencies; `calls` keeps only direct call relationships |
39
39
  | `--project` | Directory name + hash of the absolute path | Override the upstream index name |
40
40
  | `--page-size` | `500` | Rows per query batch, from 1 to 10000 |
41
41
 
@@ -86,10 +86,37 @@ The following is a simplified example. Actual properties depend on the language
86
86
  }
87
87
  ```
88
88
 
89
- Node IDs and edge IDs are strings in separate namespaces. An edge's `source` and `target` refer to node IDs. IDs link records within the current export and are not guaranteed to remain stable across indexing runs. `indexReport` preserves the upstream indexing report; its counts still describe the original complete index in `calls` mode.
89
+ Node IDs and edge IDs are strings in separate namespaces. An edge's `source` and `target` refer to node IDs. IDs link records within the current export and are not guaranteed to remain stable across indexing runs. `indexReport` preserves the upstream indexing report; its counts describe the upstream index and can differ from the exported graph after filtering or adding source-derived class data.
90
90
 
91
91
  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
92
 
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.
94
+
95
+ For example, `Customer` with an `address?: Address` field and a `save(input: Address): Result` method includes:
96
+
97
+ ```json
98
+ {
99
+ "labels": ["Class"],
100
+ "name": "Customer",
101
+ "members": [
102
+ {
103
+ "name": "address",
104
+ "kind": "field",
105
+ "type": "Address",
106
+ "filePath": "model.ts",
107
+ "startLine": 6,
108
+ "endLine": 6,
109
+ "visibility": "public",
110
+ "static": false,
111
+ "optional": true,
112
+ "readonly": false
113
+ }
114
+ ]
115
+ }
116
+ ```
117
+
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.
119
+
93
120
  For example, `function load(id: string, count = 1, ...tags: string[]): Promise<string[]>` produces:
94
121
 
95
122
  ```json
@@ -140,11 +167,16 @@ bun run test:integration
140
167
 
141
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.
142
169
 
170
+ ## Publishing
171
+
172
+ Releases are published from GitHub Actions when a version tag is pushed. Before the first release, configure npm Trusted Publishing for package `@youichi/repograph` with GitHub user `hi-youichi`, repository `repograph`, and workflow file `publish-npm.yml`; allow direct `npm publish` for that publisher. Then update `package.json` to the new version and push the matching tag, for example `v0.1.1`. The workflow checks that the tag matches the package version, installs dependencies, runs typecheck and unit tests, then builds and publishes with npm provenance. No npm token is stored in GitHub Secrets.
173
+
143
174
  ## Limitations
144
175
 
145
176
  - 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.
146
177
  - 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.
147
178
  - `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.
148
180
  - 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.
149
181
  - Results are assembled in memory before being written to disk. Large repositories require sufficient memory, and upstream evaluation limits cause an error.
150
182
  - 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,192 @@
1
+ // src/classes.ts
2
+ import { createHash } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { extname, resolve } from "node:path";
5
+ import ts from "typescript";
6
+ var CLASS_LABELS = new Set(["Class", "Interface", "Struct", "Trait"]);
7
+ var SCRIPT_KINDS = {
8
+ ".ts": ts.ScriptKind.TS,
9
+ ".tsx": ts.ScriptKind.TSX,
10
+ ".mts": ts.ScriptKind.TS,
11
+ ".cts": ts.ScriptKind.TS,
12
+ ".js": ts.ScriptKind.JS,
13
+ ".jsx": ts.ScriptKind.JSX,
14
+ ".mjs": ts.ScriptKind.JS,
15
+ ".cjs": ts.ScriptKind.JS
16
+ };
17
+ function modifiers(node) {
18
+ return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : [];
19
+ }
20
+ function memberName(name) {
21
+ if (!name)
22
+ return;
23
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
24
+ return name.text;
25
+ if (ts.isStringLiteral(name) || ts.isNumericLiteral(name))
26
+ return name.text;
27
+ if (ts.isComputedPropertyName(name) && (ts.isStringLiteral(name.expression) || ts.isNumericLiteral(name.expression)))
28
+ return name.expression.text;
29
+ return;
30
+ }
31
+ function typeNames(type, into = new Set) {
32
+ const visit = (node) => {
33
+ if (ts.isTypeReferenceNode(node)) {
34
+ const name = node.typeName.getText().split(".").at(-1);
35
+ if (name)
36
+ into.add(name);
37
+ }
38
+ ts.forEachChild(node, visit);
39
+ };
40
+ visit(type);
41
+ return into;
42
+ }
43
+ function sourceLine(file, node) {
44
+ return file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1;
45
+ }
46
+ function sourceEndLine(file, node) {
47
+ return file.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
48
+ }
49
+ function toMember(file, filePath, node) {
50
+ if (!ts.isPropertyDeclaration(node) && !ts.isPropertySignature(node) && !ts.isGetAccessorDeclaration(node) && !ts.isSetAccessorDeclaration(node))
51
+ return;
52
+ const name = memberName(node.name);
53
+ if (name === undefined)
54
+ return;
55
+ const flags = modifiers(node);
56
+ const visibility = flags.some((flag) => flag.kind === ts.SyntaxKind.PrivateKeyword) || name.startsWith("#") ? "private" : flags.some((flag) => flag.kind === ts.SyntaxKind.ProtectedKeyword) ? "protected" : "public";
57
+ const kind = ts.isGetAccessorDeclaration(node) ? "getter" : ts.isSetAccessorDeclaration(node) ? "setter" : "field";
58
+ return {
59
+ name,
60
+ kind,
61
+ type: node.type?.getText(file) ?? null,
62
+ filePath,
63
+ startLine: sourceLine(file, node),
64
+ endLine: sourceEndLine(file, node),
65
+ visibility,
66
+ static: flags.some((flag) => flag.kind === ts.SyntaxKind.StaticKeyword),
67
+ optional: !!node.questionToken,
68
+ readonly: flags.some((flag) => flag.kind === ts.SyntaxKind.ReadonlyKeyword)
69
+ };
70
+ }
71
+ function declaredType(node) {
72
+ if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node))
73
+ return node.type;
74
+ return;
75
+ }
76
+ function isClassLike(node) {
77
+ return ts.isClassDeclaration(node) || ts.isClassExpression(node) || ts.isInterfaceDeclaration(node);
78
+ }
79
+ function hash(value) {
80
+ return createHash("sha256").update(value).digest("hex").slice(0, 20);
81
+ }
82
+ async function addClassMetadata(repositoryPath, nodes, edges) {
83
+ const byFileAndName = new Map;
84
+ const classNodes = nodes.filter((node) => node.labels.some((label) => CLASS_LABELS.has(label)));
85
+ for (const node of classNodes) {
86
+ const key = `${node.filePath}\x00${node.name}`;
87
+ byFileAndName.set(key, [...byFileAndName.get(key) ?? [], node]);
88
+ }
89
+ const byName = new Map;
90
+ for (const node of classNodes)
91
+ byName.set(node.name, [...byName.get(node.name) ?? [], node]);
92
+ const sources = new Map;
93
+ const paths = new Set(classNodes.map((node) => node.filePath).filter(Boolean));
94
+ for (const filePath of paths) {
95
+ const scriptKind = SCRIPT_KINDS[extname(filePath).toLowerCase()];
96
+ if (scriptKind === undefined)
97
+ continue;
98
+ try {
99
+ const text = await readFile(resolve(repositoryPath, filePath), "utf8");
100
+ sources.set(filePath, ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, scriptKind));
101
+ } catch {}
102
+ }
103
+ const dependencies = new Map;
104
+ 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) => {
106
+ if (source.id === target.id)
107
+ return;
108
+ const pair = `${source.id}\x00${target.id}`;
109
+ if (existingDependencyPairs.has(pair))
110
+ return;
111
+ const id = `derived:${hash(`${source.id}\x00${target.id}\x00${reason}\x00${member}`)}`;
112
+ dependencies.set(id, {
113
+ id,
114
+ source: source.id,
115
+ target: target.id,
116
+ type: "DEPENDS_ON",
117
+ properties: { derivedFrom: "typescript-ast", reason, member }
118
+ });
119
+ };
120
+ for (const [filePath, file] of sources) {
121
+ const visit = (node) => {
122
+ if (!isClassLike(node) || !node.name) {
123
+ ts.forEachChild(node, visit);
124
+ return;
125
+ }
126
+ const name = node.name.text;
127
+ const candidates = byFileAndName.get(`${filePath}\x00${name}`) ?? [];
128
+ const start = sourceLine(file, node);
129
+ const owner = candidates.find((candidate) => candidate.startLine === start) ?? (candidates.length === 1 ? candidates[0] : undefined);
130
+ if (!owner) {
131
+ ts.forEachChild(node, visit);
132
+ return;
133
+ }
134
+ const members = [];
135
+ const collect = (member) => {
136
+ const value = toMember(file, filePath, member);
137
+ const propertyType = declaredType(member);
138
+ if (value) {
139
+ members.push(value);
140
+ if (propertyType) {
141
+ 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);
144
+ if (target)
145
+ addDependency(owner, target, "field-type", value.name);
146
+ }
147
+ }
148
+ }
149
+ if ((ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) && (member.type || member.parameters.length)) {
150
+ for (const parameter of member.parameters) {
151
+ if (!parameter.type)
152
+ continue;
153
+ 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);
156
+ if (target)
157
+ addDependency(owner, target, "method-parameter", member.name.getText(file));
158
+ }
159
+ }
160
+ if (member.type) {
161
+ 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);
164
+ if (target)
165
+ addDependency(owner, target, "method-return", member.name.getText(file));
166
+ }
167
+ }
168
+ }
169
+ };
170
+ for (const member of node.members)
171
+ collect(member);
172
+ if (members.length)
173
+ owner.members = members;
174
+ ts.forEachChild(node, visit);
175
+ };
176
+ visit(file);
177
+ }
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)]));
180
+ const nodesById = new Map(nodes.map((node) => [node.id, node]));
181
+ for (const edge of edges.filter((item) => item.type === "CALLS")) {
182
+ const source = methodOwners.get(edge.source);
183
+ const targetNode = nodesById.get(edge.target);
184
+ const target = targetNode && methodOwners.get(targetNode.id) || (targetNode?.labels.some((label) => CLASS_LABELS.has(label)) ? targetNode : undefined);
185
+ if (source && target)
186
+ addDependency(source, target, "method-call", targetNode?.name ?? "");
187
+ }
188
+ edges.push(...dependencies.values());
189
+ }
190
+ export {
191
+ addClassMetadata
192
+ };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { basename, resolve } from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import { collectRows, runTool } from "./cbm.js";
6
6
  import { addCallables } from "./callables.js";
7
+ import { addClassMetadata } from "./classes.js";
7
8
  var NODE_QUERY = `MATCH (n) RETURN id(n) AS id, labels(n) AS labels,
8
9
  n.name AS name, n.qualified_name AS qualifiedName, n.file_path AS filePath,
9
10
  n.start_line AS startLine, n.end_line AS endLine, properties(n) AS properties`;
@@ -118,6 +119,10 @@ async function exportGraph(repository, options = {}, run = runTool) {
118
119
  const selectedNodes = kind === "calls" ? nodes.filter((node) => endpoints.has(node.id) || node.labels.some((label) => label === "Function" || label === "Method")) : nodes;
119
120
  options.onProgress?.("Extracting callable signatures");
120
121
  addCallables(selectedNodes);
122
+ if (kind === "all") {
123
+ options.onProgress?.("Extracting class members and type dependencies");
124
+ addClassMetadata(repositoryPath, nodes, edges);
125
+ }
121
126
  return {
122
127
  schemaVersion: 1,
123
128
  generatedAt: new Date().toISOString(),
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@youichi/repograph",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Export repository code graphs with codebase-memory-mcp.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/hi-youichi/repograph.git"
8
+ },
5
9
  "type": "module",
6
10
  "bin": {
7
11
  "repograph": "dist/cli.js"
@@ -12,7 +16,7 @@
12
16
  "typecheck": "tsc --noEmit",
13
17
  "test": "bun test",
14
18
  "test:integration": "REPOGRAPH_INTEGRATION=1 bun test test/integration.test.ts",
15
- "build": "bun build ./src/cli.ts ./src/index.ts ./src/cbm.ts ./src/callables.ts --target node --outdir dist --external \"*\"",
19
+ "build": "bun build ./src/cli.ts ./src/index.ts ./src/cbm.ts ./src/callables.ts ./src/classes.ts --target node --outdir dist --external \"*\"",
16
20
  "prepack": "bun run build"
17
21
  },
18
22
  "dependencies": {
@@ -30,5 +34,8 @@
30
34
  },
31
35
  "files": [
32
36
  "dist"
33
- ]
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
34
41
  }