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.
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/PKG-INFO +1 -1
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/pyproject.toml +1 -1
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/cli.py +299 -4
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/hooks.py +2 -1
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/storage.py +36 -0
- ctxgraph_code-0.6.0/src/ctxgraph_code/render/__init__.py +20 -0
- ctxgraph_code-0.6.0/src/ctxgraph_code/render/mermaid.py +146 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/PKG-INFO +1 -1
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/SOURCES.txt +3 -1
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/README.md +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/setup.cfg +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/__main__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/importer.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/semantic.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/python/symbols.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/languages.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/build_status.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/global_paths.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/init.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/config/settings.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/exclude/patterns.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/builder.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/models.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/graph/query.py +0 -0
- /ctxgraph_code-0.5.1/src/ctxgraph_code/render.py → /ctxgraph_code-0.6.0/src/ctxgraph_code/render/_text.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/__init__.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/view/visualizer.py +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/dependency_links.txt +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/entry_points.txt +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/requires.txt +0 -0
- {ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ctxgraph-code"
|
|
7
|
-
version = "0.
|
|
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]
|
|
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]
|
|
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.
|
|
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>\"
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/__init__.py
RENAMED
|
File without changes
|
{ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/analyzer.py
RENAMED
|
File without changes
|
{ctxgraph_code-0.5.1 → ctxgraph_code-0.6.0}/src/ctxgraph_code/analyzers/treesitter/languages.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|