ctxgraph-code 0.5.1__tar.gz → 0.6.0__tar.gz

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.
Files changed (39) hide show
  1. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/PKG-INFO +1 -1
  2. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/pyproject.toml +1 -1
  3. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/cli.py +299 -4
  4. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/hooks.py +2 -1
  5. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/storage.py +36 -0
  6. ctxgraph_code-0.6.0/src/ctxgraph_code/render/__init__.py +20 -0
  7. ctxgraph_code-0.6.0/src/ctxgraph_code/render/mermaid.py +146 -0
  8. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/PKG-INFO +1 -1
  9. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/SOURCES.txt +3 -1
  10. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/README.md +0 -0
  11. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/setup.cfg +0 -0
  12. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__init__.py +0 -0
  13. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__main__.py +0 -0
  14. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/__init__.py +0 -0
  15. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
  16. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
  17. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
  18. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
  19. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py +0 -0
  20. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py +0 -0
  21. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/languages.py +0 -0
  22. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/__init__.py +0 -0
  23. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/build_status.py +0 -0
  24. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/global_paths.py +0 -0
  25. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/init.py +0 -0
  26. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/settings.py +0 -0
  27. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/__init__.py +0 -0
  28. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/patterns.py +0 -0
  29. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/__init__.py +0 -0
  30. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/builder.py +0 -0
  31. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/models.py +0 -0
  32. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/query.py +0 -0
  33. /ctxgraph_code-0.5.1/src/ctxgraph_code/render.py → /ctxgraph_code-0.6.0/src/ctxgraph_code/render/_text.py +0 -0
  34. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/__init__.py +0 -0
  35. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/visualizer.py +0 -0
  36. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
  37. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
  38. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/requires.txt +0 -0
  39. {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxgraph-code
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
5
5
  Author: ctxgraph-code contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ctxgraph-code"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  description = "Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -268,7 +268,12 @@ SLASH_COMMAND_TEMPLATE = """# ctxgraph-code: Code Relationship Graph
268
268
  - `ctxgraph-code overview --dir <name>` -- Show project structure for a specific graph
269
269
  - `ctxgraph-code symbols <path>` -- List classes/functions defined in a file
270
270
  - `ctxgraph-code context "task"` -- Generate a focused context summary
271
+ - `ctxgraph-code subgraph "task"` -- Extract a focused subgraph with inline source for a task
272
+ - `ctxgraph-code diff` -- Compare graph with filesystem to find new/removed/changed files
273
+ - `ctxgraph-code mermaid --type classDiagram` -- Export graph as Mermaid diagram
271
274
  - `ctxgraph-code view --dir <name>` -- Visualize a graph interactively
275
+ - `ctxgraph-code build-status` -- Show last build status and timing
276
+ - `ctxgraph-code install-slash` -- (Re)install this slash command
272
277
 
273
278
  **Tip:** Use `--dir <name>` to scope queries to a per-directory graph.
274
279
  When passing a file path like `auth/login.py`, the correct graph is auto-detected.
@@ -386,13 +391,13 @@ def _build_dirs_parallel(path, exts, user_patterns, top_dirs, graphs_dir, jobs,
386
391
  edges = stats.get("total_edges", 0)
387
392
  t = stats.get("elapsed_seconds", 0)
388
393
  console.print(
389
- f" [green][/green] {label}/ "
394
+ f" [green]OK[/green] {label}/ "
390
395
  f"({files} files, {nodes} nodes, {edges} edges, {t}s)"
391
396
  )
392
397
  except Exception as e:
393
398
  results.append((label, str(e)))
394
399
  err_count += 1
395
- console.print(f" [red][/red] {label}/ ([red]{e}[/red])")
400
+ console.print(f" [red]FAIL[/red] {label}/ ([red]{e}[/red])")
396
401
 
397
402
  elapsed_total = time.time() - _build_progress_start
398
403
  if err_count:
@@ -819,7 +824,7 @@ def setup(
819
824
  )
820
825
 
821
826
 
822
- @app.command(name="build-status")
827
+ @app.command(name="build-status")
823
828
  def build_status(
824
829
  repo_path: Optional[str] = typer.Option(
825
830
  None, "--repo", "-r", help="Repository path"
@@ -1105,10 +1110,300 @@ def version():
1105
1110
  try:
1106
1111
  ver = _v("ctxgraph-code")
1107
1112
  except Exception:
1108
- ver = "0.5.0"
1113
+ ver = "0.6.0"
1109
1114
  console.print(f"ctxgraph-code version [bold]{ver}[/bold]")
1110
1115
 
1111
1116
 
1117
+ @app.command()
1118
+ def mermaid(
1119
+ repo_path: Optional[str] = typer.Option(
1120
+ None, "--repo", "-r", help="Repository path"
1121
+ ),
1122
+ output: Optional[str] = typer.Option(
1123
+ None, "--output", "-o", help="Save Mermaid output to file"
1124
+ ),
1125
+ max_nodes: int = typer.Option(
1126
+ 50, "--max-nodes", "-n", help="Maximum nodes in diagram"
1127
+ ),
1128
+ dir_name: Optional[str] = typer.Option(
1129
+ None, "--dir", "-d", help="Directory graph to use"
1130
+ ),
1131
+ diagram_type: str = typer.Argument(
1132
+ "classDiagram", help="Diagram type: classDiagram, flowchart, sequence"
1133
+ ),
1134
+ ):
1135
+ """Export the graph as a Mermaid diagram.
1136
+
1137
+ Supported diagram types: classDiagram, flowchart, sequence.
1138
+ Outputs to console by default, or to a file with --output.
1139
+ """
1140
+ path = Path(repo_path).resolve() if repo_path else Path.cwd()
1141
+ storage = _resolve_storage(path, dir_name=dir_name)
1142
+
1143
+ from ctxgraph_code.render.mermaid import MermaidError, render_mermaid
1144
+
1145
+ try:
1146
+ result = render_mermaid(
1147
+ storage,
1148
+ output_type=diagram_type,
1149
+ max_nodes=max_nodes,
1150
+ )
1151
+ except MermaidError as e:
1152
+ console.print(f"[red]{e}[/red]")
1153
+ raise typer.Exit(1)
1154
+
1155
+ if output:
1156
+ out_path = Path(output)
1157
+ out_path.write_text(result, encoding="utf-8")
1158
+ console.print(f"[green]Mermaid diagram saved to [bold]{out_path}[/bold][/green]")
1159
+ else:
1160
+ console.print(result)
1161
+
1162
+
1163
+ @app.command()
1164
+ def subgraph(
1165
+ query: str = typer.Argument(..., help="Task description"),
1166
+ repo_path: Optional[str] = typer.Option(
1167
+ None, "--repo", "-r", help="Repository path"
1168
+ ),
1169
+ max_nodes: int = typer.Option(
1170
+ 10, "--max-nodes", "-n", help="Maximum nodes in subgraph"
1171
+ ),
1172
+ dir_name: Optional[str] = typer.Option(
1173
+ None, "--dir", "-d", help="Directory graph to query"
1174
+ ),
1175
+ ):
1176
+ """Extract a focused subgraph relevant to a task description.
1177
+
1178
+ Returns matching nodes, their relationships, and inline source code
1179
+ for a compact context window.
1180
+ """
1181
+ import hashlib
1182
+
1183
+ path = Path(repo_path).resolve() if repo_path else Path.cwd()
1184
+ storage = _resolve_storage(path, dir_name=dir_name)
1185
+
1186
+ results = search_relevant_nodes(storage, query, max_nodes=max_nodes)
1187
+ if not results:
1188
+ console.print("[yellow]No relevant nodes found for subgraph.[/yellow]")
1189
+ raise typer.Exit()
1190
+
1191
+ matched_ids = {n.id for n, _ in results}
1192
+ edge_ids: set[str] = set()
1193
+ edges = storage.get_edges_for_nodes(matched_ids)
1194
+ for e in edges:
1195
+ if e.source_id in matched_ids:
1196
+ edge_ids.add(e.target_id)
1197
+ if e.target_id in matched_ids:
1198
+ edge_ids.add(e.source_id)
1199
+
1200
+ all_ids = matched_ids | edge_ids
1201
+ node_map: dict[str, object] = {}
1202
+ for nid in all_ids:
1203
+ n = storage.get_node(nid)
1204
+ if n:
1205
+ node_map[nid] = n
1206
+
1207
+ file_nodes_in_subgraph = {
1208
+ n for n in node_map.values()
1209
+ if hasattr(n, 'type') and n.type == "file"
1210
+ }
1211
+
1212
+ lines = [
1213
+ f"Subgraph for: {query}",
1214
+ f"Nodes: {len(matched_ids)} seed + {len(edge_ids)} related = {len(all_ids)} total",
1215
+ "",
1216
+ ]
1217
+
1218
+ # Group by file
1219
+ file_groups: dict[str, list] = {}
1220
+ for n in sorted(node_map.values(), key=lambda x: x.path or ""):
1221
+ fp = getattr(n, 'path', None) or ""
1222
+ file_groups.setdefault(fp, []).append(n)
1223
+
1224
+ for fp, nodes in sorted(file_groups.items()):
1225
+ file_node = next((n for n in nodes if n.type == "file"), None)
1226
+ if file_node:
1227
+ tag = "F"
1228
+ lines.append(f" [{tag}] [bold]{file_node.name}[/bold] [blue]{file_node.path or '-'}[/blue]")
1229
+ if file_node.summary:
1230
+ lines.append(f" {file_node.summary}")
1231
+ symbols = [n for n in nodes if n.type in ("class", "function")]
1232
+ if symbols:
1233
+ for s in symbols:
1234
+ tag = "C" if s.type == "class" else "M"
1235
+ lines.append(f" [{tag}] {s.name} (line {s.lineno})")
1236
+ if s.summary:
1237
+ lines.append(f" {s.summary}")
1238
+
1239
+ if edges:
1240
+ lines.append("")
1241
+ lines.append(" Relationships:")
1242
+ for e in edges[:15]:
1243
+ src = node_map.get(e.source_id)
1244
+ tgt = node_map.get(e.target_id)
1245
+ src_name = src.name if src else e.source_id
1246
+ tgt_name = tgt.name if tgt else e.target_id
1247
+ lines.append(f" {src_name} --[{e.relation}]--> {tgt_name}")
1248
+
1249
+ # Inline source for each file node
1250
+ lines.append("")
1251
+ for n in sorted(file_nodes_in_subgraph, key=lambda x: x.path or ""):
1252
+ fp = path / n.path if n.path else None
1253
+ if fp and fp.is_file():
1254
+ try:
1255
+ code = fp.read_text(encoding="utf-8", errors="replace")
1256
+ lines.append(f" === {n.path} ===")
1257
+ snippet = code.splitlines()[:60]
1258
+ lines.extend(snippet)
1259
+ if len(code.splitlines()) > 60:
1260
+ lines.append(f"... ({len(code.splitlines()) - 60} more lines)")
1261
+ lines.append("")
1262
+ except OSError:
1263
+ pass
1264
+
1265
+ console.print("\n".join(lines))
1266
+
1267
+
1268
+ @app.command()
1269
+ def diff(
1270
+ repo_path: Optional[str] = typer.Option(
1271
+ None, "--repo", "-r", help="Repository path"
1272
+ ),
1273
+ dir_name: Optional[str] = typer.Option(
1274
+ None, "--dir", "-d", help="Directory graph to compare"
1275
+ ),
1276
+ ref_branch: Optional[str] = typer.Option(
1277
+ None, "--ref", help="Git reference/branch to diff against (requires git)"
1278
+ ),
1279
+ ):
1280
+ """Compare the graph with the filesystem.
1281
+
1282
+ Shows files that have been added, removed, or changed since the
1283
+ graph was built. Use --ref for a git-aware diff against a branch.
1284
+ """
1285
+ import hashlib
1286
+ import json as j
1287
+
1288
+ path = Path(repo_path).resolve() if repo_path else Path.cwd()
1289
+ storage = _resolve_storage(path, dir_name=dir_name)
1290
+
1291
+ new_files: list[str] = []
1292
+ removed_files: list[str] = []
1293
+ changed_files: list[str] = []
1294
+
1295
+ graph_paths = set(storage.get_file_paths())
1296
+ hashes_json = storage.get_metadata("content_hashes")
1297
+ stored_hashes: dict[str, str] = {}
1298
+ if hashes_json:
1299
+ try:
1300
+ stored_hashes = j.loads(hashes_json)
1301
+ except (j.JSONDecodeError, OSError):
1302
+ pass
1303
+
1304
+ if ref_branch:
1305
+ try:
1306
+ import subprocess
1307
+ result = subprocess.run(
1308
+ ["git", "diff", "--name-only", ref_branch],
1309
+ capture_output=True, text=True, check=False, cwd=str(path),
1310
+ )
1311
+ if result.returncode == 0:
1312
+ git_files = set(result.stdout.strip().splitlines()) if result.stdout.strip() else set()
1313
+ for gf in git_files:
1314
+ if gf not in graph_paths:
1315
+ new_files.append(gf)
1316
+ for gp in graph_paths:
1317
+ if gp not in git_files:
1318
+ removed_files.append(gp)
1319
+ # Changed files = git diff --name-only from HEAD
1320
+ head_result = subprocess.run(
1321
+ ["git", "diff", "--name-only", "HEAD"],
1322
+ capture_output=True, text=True, check=False, cwd=str(path),
1323
+ )
1324
+ if head_result.returncode == 0:
1325
+ changed_files = [f for f in head_result.stdout.strip().splitlines() if f in graph_paths]
1326
+ else:
1327
+ console.print(f"[red]git diff failed: {result.stderr.strip()}[/red]")
1328
+ console.print("[yellow]Falling back to filesystem comparison...[/yellow]")
1329
+ ref_branch = None
1330
+ except FileNotFoundError:
1331
+ console.print("[yellow]Git not found. Falling back to filesystem comparison...[/yellow]")
1332
+ ref_branch = None
1333
+
1334
+ if not ref_branch:
1335
+ for gp in graph_paths:
1336
+ fp = path / gp
1337
+ if not fp.is_file():
1338
+ removed_files.append(gp)
1339
+
1340
+ src_extensions = {".py", ".js", ".ts", ".tsx", ".go", ".rs", ".c", ".h", ".cpp", ".java", ".rb", ".kt", ".swift"}
1341
+ for root, _dirs, files in os.walk(path):
1342
+ _dirs[:] = [d for d in _dirs if not d.startswith(".") and d not in ("node_modules", "venv", ".venv", "env", "dist", "build", "__pycache__")]
1343
+ for f in files:
1344
+ ext = os.path.splitext(f)[1].lower()
1345
+ if ext not in src_extensions:
1346
+ continue
1347
+ rel_path = os.path.relpath(os.path.join(root, f), path).replace("\\", "/")
1348
+ if rel_path not in graph_paths:
1349
+ new_files.append(rel_path)
1350
+
1351
+ # Content hash comparison for changed detection
1352
+ if stored_hashes:
1353
+ for rel_path, stored_hash in stored_hashes.items():
1354
+ fp = path / rel_path
1355
+ if fp.is_file():
1356
+ try:
1357
+ actual = hashlib.sha256(
1358
+ fp.read_text(encoding="utf-8", errors="replace")
1359
+ .encode("utf-8")
1360
+ ).hexdigest()
1361
+ if actual != stored_hash:
1362
+ changed_files.append(rel_path)
1363
+ except OSError:
1364
+ changed_files.append(rel_path)
1365
+
1366
+ lines = ["Graph vs Filesystem Diff", ""]
1367
+ if new_files:
1368
+ lines.append(f" [green]+ {len(new_files)} new file(s):[/green]")
1369
+ for f in sorted(new_files)[:10]:
1370
+ lines.append(f" + {f}")
1371
+ if len(new_files) > 10:
1372
+ lines.append(f" ... and {len(new_files) - 10} more")
1373
+ else:
1374
+ lines.append(" [green]+ No new files[/green]")
1375
+
1376
+ if removed_files:
1377
+ lines.append(f"")
1378
+ lines.append(f" [red]- {len(removed_files)} removed file(s):[/red]")
1379
+ for f in sorted(removed_files)[:10]:
1380
+ lines.append(f" - {f}")
1381
+ if len(removed_files) > 10:
1382
+ lines.append(f" ... and {len(removed_files) - 10} more")
1383
+ else:
1384
+ lines.append(f"")
1385
+ lines.append(" [red]- No removed files[/red]")
1386
+
1387
+ if changed_files:
1388
+ lines.append(f"")
1389
+ lines.append(f" [yellow]~ {len(changed_files)} changed file(s):[/yellow]")
1390
+ for f in sorted(changed_files)[:10]:
1391
+ lines.append(f" ~ {f}")
1392
+ if len(changed_files) > 10:
1393
+ lines.append(f" ... and {len(changed_files) - 10} more")
1394
+ else:
1395
+ lines.append(f"")
1396
+ lines.append(" [yellow]~ No changed files[/yellow]")
1397
+
1398
+ lines.append("")
1399
+ if new_files or removed_files or changed_files:
1400
+ lines.append("[yellow]Run [bold]ctxgraph-code build --incremental[/bold] to update the graph.[/yellow]")
1401
+ else:
1402
+ lines.append("[green]Graph is up to date with the filesystem.[/green]")
1403
+
1404
+ console.print("\n".join(lines))
1405
+
1406
+
1112
1407
  if __name__ == "__main__":
1113
1408
  app()
1114
1409
 
@@ -144,7 +144,8 @@ def compute_hint_summary(repo_path: Path) -> Optional[str]:
144
144
  )
145
145
 
146
146
  lines.append(
147
- "Use `ctxgraph-code probe \"<question>\"` to search or "
147
+ "Use `ctxgraph-code probe \"<question>\"`, "
148
+ "`ctxgraph-code subgraph \"<task>\"`, or "
148
149
  "`/ctxgraph-code` for help."
149
150
  )
150
151
 
@@ -224,6 +224,42 @@ class Storage:
224
224
  )
225
225
  self.conn.commit()
226
226
 
227
+ def get_nodes_by_file_path(self, file_path: str) -> list[Node]:
228
+ rows = self.conn.execute(
229
+ "SELECT * FROM nodes WHERE path = ?", (file_path,)
230
+ ).fetchall()
231
+ return [
232
+ Node(
233
+ id=r["id"],
234
+ type=r["type"],
235
+ name=r["name"],
236
+ path=r["path"],
237
+ parent_id=r["parent_id"],
238
+ summary=r["summary"],
239
+ importance=r["importance"],
240
+ size_bytes=r["size_bytes"],
241
+ lineno=r["lineno"],
242
+ )
243
+ for r in rows
244
+ ]
245
+
246
+ def get_file_paths(self) -> list[str]:
247
+ rows = self.conn.execute(
248
+ "SELECT DISTINCT path FROM nodes WHERE type = 'file' AND path IS NOT NULL"
249
+ ).fetchall()
250
+ return [r[0] for r in rows]
251
+
252
+ def get_content_hash(self, file_path: str) -> Optional[str]:
253
+ hashes_json = self.get_metadata("content_hashes")
254
+ if not hashes_json:
255
+ return None
256
+ try:
257
+ import json
258
+ stored = json.loads(hashes_json)
259
+ return stored.get(file_path)
260
+ except (json.JSONDecodeError, OSError):
261
+ return None
262
+
227
263
  def save_metadata(self, key: str, value: str):
228
264
  self.conn.execute(
229
265
  "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)",
@@ -0,0 +1,20 @@
1
+ from ctxgraph_code.render._text import (
2
+ render_context,
3
+ render_deps,
4
+ render_overview,
5
+ render_symbols,
6
+ render_treeview,
7
+ render_usedby,
8
+ )
9
+
10
+ from ctxgraph_code.render.mermaid import render_mermaid as render_mermaid
11
+
12
+ __all__ = [
13
+ "render_context",
14
+ "render_deps",
15
+ "render_overview",
16
+ "render_symbols",
17
+ "render_treeview",
18
+ "render_usedby",
19
+ "render_mermaid",
20
+ ]
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from ctxgraph_code.graph.storage import Storage
6
+
7
+
8
+ class MermaidError(Exception):
9
+ pass
10
+
11
+
12
+ def render_mermaid(
13
+ storage: Storage,
14
+ output_type: str = "classDiagram",
15
+ max_nodes: int = 50,
16
+ ) -> str:
17
+ output_type = output_type or "classDiagram"
18
+ supported = {"classDiagram", "flowchart", "sequence"}
19
+ if output_type not in supported:
20
+ raise MermaidError(
21
+ f"Unsupported diagram type '{output_type}'. "
22
+ f"Choose from: {', '.join(sorted(supported))}"
23
+ )
24
+
25
+ all_nodes = storage.get_all_nodes()
26
+ all_edges = storage.get_all_edges()
27
+
28
+ if output_type == "classDiagram":
29
+ return _render_class_diagram(all_nodes, all_edges, max_nodes)
30
+ elif output_type == "flowchart":
31
+ return _render_flowchart(all_nodes, all_edges, max_nodes)
32
+ elif output_type == "sequence":
33
+ return _render_sequence(all_nodes, all_edges, max_nodes)
34
+ return ""
35
+
36
+
37
+ def _safe_id(name: str) -> str:
38
+ safe = "".join(c if c.isalnum() or c == "_" else "_" for c in name)
39
+ if safe and safe[0].isdigit():
40
+ safe = "n" + safe
41
+ return safe or "node"
42
+
43
+
44
+ def _render_class_diagram(nodes: list, edges: list, max_nodes: int) -> str:
45
+ lines = ["classDiagram"]
46
+ added = 0
47
+
48
+ class_nodes = [n for n in nodes if n.type == "class"]
49
+ file_nodes = [n for n in nodes if n.type == "file"]
50
+
51
+ for cls in class_nodes[:max_nodes]:
52
+ nid = _safe_id(cls.name)
53
+ lines.append(f" class {nid} {{")
54
+ if cls.summary:
55
+ lines.append(f" +{cls.summary}")
56
+ lines.append(" }")
57
+ added += 1
58
+ if added >= max_nodes:
59
+ break
60
+
61
+ if added < max_nodes:
62
+ remaining = max_nodes - added
63
+ for f in file_nodes[:remaining]:
64
+ nid = _safe_id(f.name)
65
+ label = f.path or f.name
66
+ lines.append(f" class {nid} {{")
67
+ lines.append(f" +File: {label}")
68
+ if f.summary:
69
+ lines.append(f" +{f.summary}")
70
+ lines.append(" }")
71
+
72
+ inheritance_edges = [e for e in edges if e.relation == "inherits"]
73
+ for e in inheritance_edges:
74
+ src_name = _find_node_name(nodes, e.source_id)
75
+ tgt_name = _find_node_name(nodes, e.target_id)
76
+ if src_name and tgt_name:
77
+ lines.append(f" {_safe_id(src_name)} --|> {_safe_id(tgt_name)}")
78
+
79
+ lines.append("")
80
+ return "\n".join(lines)
81
+
82
+
83
+ def _render_flowchart(nodes: list, edges: list, max_nodes: int) -> str:
84
+ lines = ["flowchart TD"]
85
+ node_map = {n.id: n for n in nodes}
86
+ shown: set[str] = set()
87
+
88
+ sorted_edges = sorted(edges, key=lambda e: e.weight or 1.0, reverse=True)
89
+ for e in sorted_edges[:max_nodes * 2]:
90
+ src = node_map.get(e.source_id)
91
+ tgt = node_map.get(e.target_id)
92
+ if not src or not tgt:
93
+ continue
94
+ for n in (src, tgt):
95
+ if n.id not in shown and len(shown) < max_nodes:
96
+ nid = _safe_id(n.name)
97
+ label = n.path or n.name
98
+ lines.append(f" {nid}[\"{label}\"]")
99
+ shown.add(n.id)
100
+ if src.id in shown and tgt.id in shown:
101
+ src_id = _safe_id(src.name)
102
+ tgt_id = _safe_id(tgt.name)
103
+ rel = e.relation or "link"
104
+ lines.append(f" {src_id} -->|{rel}| {tgt_id}")
105
+
106
+ if not shown and nodes:
107
+ for n in nodes[:max_nodes]:
108
+ nid = _safe_id(n.name)
109
+ label = n.path or n.name
110
+ lines.append(f" {nid}[\"{label}\"]")
111
+
112
+ lines.append("")
113
+ return "\n".join(lines)
114
+
115
+
116
+ def _render_sequence(nodes: list, edges: list, max_nodes: int) -> str:
117
+ lines = ["sequenceDiagram"]
118
+ participants: set[str] = set()
119
+ node_map = {n.id: n for n in nodes}
120
+
121
+ sorted_edges = sorted(edges, key=lambda e: e.weight or 1.0, reverse=True)
122
+ for e in sorted_edges[:max_nodes * 2]:
123
+ src = node_map.get(e.source_id)
124
+ tgt = node_map.get(e.target_id)
125
+ if not src or not tgt:
126
+ continue
127
+ for label, n in [("", src), ("", tgt)]:
128
+ if n.id not in participants:
129
+ safe = _safe_id(n.name)
130
+ display = n.path or n.name
131
+ lines.append(f" participant {safe} as \"{display}\"")
132
+ participants.add(n.id)
133
+ src_safe = _safe_id(src.name)
134
+ tgt_safe = _safe_id(tgt.name)
135
+ rel = e.relation or "calls"
136
+ lines.append(f" {src_safe}->>+{tgt_safe}: {rel}")
137
+
138
+ lines.append("")
139
+ return "\n".join(lines)
140
+
141
+
142
+ def _find_node_name(nodes: list, node_id: str) -> Optional[str]:
143
+ for n in nodes:
144
+ if n.id == node_id:
145
+ return n.name
146
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxgraph-code
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Code knowledge graph for Claude Code. Build a relationship graph of your Python codebase and query it during coding sessions.
5
5
  Author: ctxgraph-code contributors
6
6
  License: MIT
@@ -3,7 +3,6 @@ pyproject.toml
3
3
  src/ctxgraph_code/__init__.py
4
4
  src/ctxgraph_code/__main__.py
5
5
  src/ctxgraph_code/cli.py
6
- src/ctxgraph_code/render.py
7
6
  src/ctxgraph_code.egg-info/PKG-INFO
8
7
  src/ctxgraph_code.egg-info/SOURCES.txt
9
8
  src/ctxgraph_code.egg-info/dependency_links.txt
@@ -31,5 +30,8 @@ src/ctxgraph_code/graph/builder.py
31
30
  src/ctxgraph_code/graph/models.py
32
31
  src/ctxgraph_code/graph/query.py
33
32
  src/ctxgraph_code/graph/storage.py
33
+ src/ctxgraph_code/render/__init__.py
34
+ src/ctxgraph_code/render/_text.py
35
+ src/ctxgraph_code/render/mermaid.py
34
36
  src/ctxgraph_code/view/__init__.py
35
37
  src/ctxgraph_code/view/visualizer.py
File without changes
File without changes