codegraphy-lite 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ """CodeGraphy Lite — Standalone Open-Source Visual Architecture Graph & MCP Server.
2
+
3
+ Lightweight, local-first interactive architecture visualization and Model Context Protocol (MCP) server.
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __all__ = ["__version__"]
codegraphy_lite/cli.py ADDED
@@ -0,0 +1,113 @@
1
+ """CLI entrypoint for CodeGraphy Lite.
2
+
3
+ Usage:
4
+ codegraphy-lite [path] [--port 8000] [--host 127.0.0.1] [--no-browser] [--scan]
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ import threading
12
+ import time
13
+ import webbrowser
14
+ from pathlib import Path
15
+
16
+ import uvicorn
17
+
18
+ from codegraphy_lite import __version__
19
+ from codegraphy_lite.scanner import build_graph_and_workspace, scan_project
20
+ from codegraphy_lite.server import app
21
+ from codegraphy_lite.storage import get_storage_dir, load_graph, save_graph, save_workspace, set_active_root
22
+
23
+
24
+ def open_browser_later(url: str, delay_sec: float = 1.0):
25
+ def _open():
26
+ time.sleep(delay_sec)
27
+ try:
28
+ webbrowser.open(url)
29
+ except Exception:
30
+ pass
31
+
32
+ t = threading.Thread(target=_open, daemon=True)
33
+ t.start()
34
+
35
+
36
+ def main():
37
+ parser = argparse.ArgumentParser(
38
+ prog="codegraphy-lite",
39
+ description="CodeGraphy Lite — Standalone Open-Source Visual Architecture Graph & MCP Server",
40
+ )
41
+ parser.add_argument(
42
+ "path",
43
+ nargs="?",
44
+ default=".",
45
+ help="Target project directory to visualize (default: current directory)",
46
+ )
47
+ parser.add_argument(
48
+ "--port",
49
+ type=int,
50
+ default=8000,
51
+ help="HTTP server port (default: 8000)",
52
+ )
53
+ parser.add_argument(
54
+ "--host",
55
+ default="127.0.0.1",
56
+ help="HTTP server bind host (default: 127.0.0.1)",
57
+ )
58
+ parser.add_argument(
59
+ "--no-browser",
60
+ action="store_true",
61
+ help="Do not automatically launch the browser",
62
+ )
63
+ parser.add_argument(
64
+ "--scan",
65
+ action="store_true",
66
+ help="Force automatic project directory scan on startup (up to 35 files)",
67
+ )
68
+ parser.add_argument(
69
+ "-v", "--version",
70
+ action="version",
71
+ version=f"codegraphy-lite {__version__}",
72
+ )
73
+
74
+ args = parser.parse_args()
75
+
76
+ project_root = Path(args.path).resolve()
77
+ if not project_root.exists() or not project_root.is_dir():
78
+ print(f"Error: Directory '{project_root}' does not exist.", file=sys.stderr)
79
+ sys.exit(1)
80
+
81
+ set_active_root(project_root)
82
+
83
+ # Check if graph exists; if not or if --scan requested, run Lite scanner
84
+ storage_dir = get_storage_dir(project_root)
85
+ existing_graph = load_graph(project_root)
86
+
87
+ if args.scan or not existing_graph.nodes:
88
+ print(f"[*] Scanning project architecture in '{project_root}' (Lite limit: max 35 files)...")
89
+ try:
90
+ preview = scan_project(project_root)
91
+ graph, ws = build_graph_and_workspace(preview)
92
+ save_graph(graph, project_root)
93
+ save_workspace(ws, project_root)
94
+ print(f"[+] Discovered {preview.total_files} files, {len(graph.nodes)} topics, {len(graph.edges)} dependency edges.")
95
+ except Exception as e:
96
+ print(f"[!] Warning: Initial project scan skipped ({e})", file=sys.stderr)
97
+
98
+ url = f"http://{args.host}:{args.port}"
99
+ print(f"\n=======================================================")
100
+ print(f" CodeGraphy Lite v{__version__} (Open Source Edition)")
101
+ print(f" Visual Architecture Canvas & MCP Server")
102
+ print(f" Project Root: {project_root}")
103
+ print(f" Web UI: {url}")
104
+ print(f"=======================================================\n")
105
+
106
+ if not args.no_browser:
107
+ open_browser_later(url, delay_sec=0.8)
108
+
109
+ uvicorn.run(app, host=args.host, port=args.port, log_level="info")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
codegraphy_lite/mcp.py ADDED
@@ -0,0 +1,338 @@
1
+ """Model Context Protocol (MCP) STDIO Server for CodeGraphy Lite.
2
+
3
+ Exposes 3 basic workspace/graph architectural tools to Claude Code, Cursor, Windsurf,
4
+ and agentic coding assistants via standard JSON-RPC 2.0 over STDIO.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from codegraphy_lite import __version__
17
+ from codegraphy_lite.models import GraphState, WorkspaceModel
18
+ from codegraphy_lite.scanner import extract_ast_signatures
19
+ from codegraphy_lite.storage import get_active_root, load_graph, load_workspace, set_active_root
20
+
21
+ PROTOCOL_VERSION = "2024-11-05"
22
+ SERVER_NAME = "codegraphy-lite-mcp"
23
+ MAX_PAYLOAD_CHARS = 32000
24
+
25
+ SECRET_PATTERNS = [
26
+ re.compile(r"\b(?:sk|whsec)_[a-zA-Z0-9_\-]{16,}\b"),
27
+ re.compile(r"\bsk-(?:live|test|ant|proj)?[a-zA-Z0-9_\-]{16,}\b"),
28
+ re.compile(r"\bAIza[0-9A-Za-z\-_]{35}\b"),
29
+ re.compile(r"\bghp_[0-9a-zA-Z]{36}\b"),
30
+ re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
31
+ ]
32
+
33
+
34
+ def scrub_secrets(val: Any) -> Any:
35
+ """Redacts potential API tokens and keys."""
36
+ if isinstance(val, str):
37
+ text = val
38
+ for pat in SECRET_PATTERNS:
39
+ text = pat.sub("[REDACTED_SECRET]", text)
40
+ return text
41
+ if isinstance(val, dict):
42
+ return {k: scrub_secrets(v) for k, v in val.items()}
43
+ if isinstance(val, list):
44
+ return [scrub_secrets(item) for item in val]
45
+ return val
46
+
47
+
48
+ TOOLS_MANIFEST = [
49
+ {
50
+ "name": "list_workspace_topics",
51
+ "description": "Lists all architectural topics/nodes in the workspace with their status, lanes, attached files, and descriptions.",
52
+ "inputSchema": {
53
+ "type": "object",
54
+ "properties": {
55
+ "status": {
56
+ "type": "string",
57
+ "enum": ["all", "todo", "in_progress", "done"],
58
+ "description": "Filter topics by status (default: all)",
59
+ }
60
+ },
61
+ },
62
+ },
63
+ {
64
+ "name": "get_topic_context",
65
+ "description": "Retrieves comprehensive architectural context for a topic, including 1-hop connected dependencies and AST-extracted signatures from attached files.",
66
+ "inputSchema": {
67
+ "type": "object",
68
+ "properties": {
69
+ "topic": {
70
+ "type": "string",
71
+ "description": "Topic ID or label to inspect",
72
+ },
73
+ "include_ast": {
74
+ "type": "boolean",
75
+ "description": "Whether to include AST-extracted class/function signatures (default: true)",
76
+ "default": True,
77
+ },
78
+ },
79
+ "required": ["topic"],
80
+ },
81
+ },
82
+ {
83
+ "name": "get_architecture_summary",
84
+ "description": "Returns a high-level architectural overview: total topics, dependency edges, lanes, status distribution, and key architectural modules.",
85
+ "inputSchema": {
86
+ "type": "object",
87
+ "properties": {},
88
+ },
89
+ },
90
+ ]
91
+
92
+
93
+ class CodeGraphyLiteMCP:
94
+ def __init__(self, root_dir: Optional[str | Path] = None) -> None:
95
+ if root_dir:
96
+ set_active_root(root_dir)
97
+ elif "CODEGRAPHY_ROOT" in os.environ:
98
+ set_active_root(os.environ["CODEGRAPHY_ROOT"])
99
+
100
+ @property
101
+ def root(self) -> Path:
102
+ return get_active_root()
103
+
104
+ def handle_list_workspace_topics(self, args: Dict[str, Any]) -> Dict[str, Any]:
105
+ graph = load_graph(self.root)
106
+ status_filter = args.get("status", "all")
107
+
108
+ topics = []
109
+ for n in graph.nodes:
110
+ d = n.data
111
+ if status_filter != "all" and d.status != status_filter:
112
+ continue
113
+
114
+ incoming = sum(1 for e in graph.edges if e.target == n.id)
115
+ outgoing = sum(1 for e in graph.edges if e.source == n.id)
116
+ files = [f.filepath for f in d.attached_files]
117
+
118
+ topics.append({
119
+ "id": n.id,
120
+ "label": d.label,
121
+ "status": d.status,
122
+ "lane_id": d.lane_id,
123
+ "description": d.description,
124
+ "files_count": len(files),
125
+ "files": files,
126
+ "incoming_dependencies": incoming,
127
+ "outgoing_dependencies": outgoing,
128
+ })
129
+
130
+ return {
131
+ "root_path": str(self.root),
132
+ "total_topics": len(topics),
133
+ "topics": topics,
134
+ }
135
+
136
+ def handle_get_topic_context(self, args: Dict[str, Any]) -> Dict[str, Any]:
137
+ query = args.get("topic", "").strip().lower()
138
+ if not query:
139
+ raise ValueError("Parameter 'topic' is required.")
140
+ include_ast = args.get("include_ast", True)
141
+
142
+ graph = load_graph(self.root)
143
+ target = None
144
+ for n in graph.nodes:
145
+ if n.id.lower() == query or n.data.label.lower() == query:
146
+ target = n
147
+ break
148
+
149
+ if not target:
150
+ raise ValueError(f"Topic '{query}' not found in workspace.")
151
+
152
+ # Dependencies
153
+ incoming_ids = [e.source for e in graph.edges if e.target == target.id]
154
+ outgoing_ids = [e.target for e in graph.edges if e.source == target.id]
155
+
156
+ def resolve_label(nid: str) -> str:
157
+ m = next((x.data.label for x in graph.nodes if x.id == nid), nid)
158
+ return m
159
+
160
+ # Files & AST signatures
161
+ files_data = []
162
+ for f in target.data.attached_files:
163
+ file_info: Dict[str, Any] = {"filepath": f.filepath}
164
+ full_p = self.root / f.filepath
165
+ if full_p.exists() and full_p.is_file():
166
+ file_info["exists"] = True
167
+ if include_ast:
168
+ try:
169
+ content = full_p.read_text(encoding="utf-8", errors="ignore")
170
+ file_info["signatures"] = extract_ast_signatures(content)
171
+ except Exception as e:
172
+ file_info["signatures"] = f"# Error extracting AST: {e}"
173
+ else:
174
+ file_info["exists"] = False
175
+ file_info["signatures"] = "# File not found on disk"
176
+ files_data.append(file_info)
177
+
178
+ return {
179
+ "topic_id": target.id,
180
+ "label": target.data.label,
181
+ "status": target.data.status,
182
+ "description": target.data.description,
183
+ "connected_dependencies": {
184
+ "incoming": [resolve_label(x) for x in incoming_ids],
185
+ "outgoing": [resolve_label(x) for x in outgoing_ids],
186
+ },
187
+ "attached_files": files_data,
188
+ }
189
+
190
+ def handle_get_architecture_summary(self, args: Dict[str, Any]) -> Dict[str, Any]:
191
+ graph = load_graph(self.root)
192
+ ws = load_workspace(self.root)
193
+
194
+ status_counts = {"todo": 0, "in_progress": 0, "done": 0}
195
+ for n in graph.nodes:
196
+ st = n.data.status
197
+ if st in status_counts:
198
+ status_counts[st] += 1
199
+
200
+ degree_map: Dict[str, int] = {}
201
+ for e in graph.edges:
202
+ degree_map[e.source] = degree_map.get(e.source, 0) + 1
203
+ degree_map[e.target] = degree_map.get(e.target, 0) + 1
204
+
205
+ top_components = sorted(
206
+ [{"topic_id": k, "degree": v} for k, v in degree_map.items()],
207
+ key=lambda x: x["degree"],
208
+ reverse=True,
209
+ )[:6]
210
+
211
+ return {
212
+ "project_name": ws.name or self.root.name,
213
+ "root_path": str(self.root),
214
+ "edition": "CodeGraphy Lite (Open Source)",
215
+ "total_topics": len(graph.nodes),
216
+ "total_dependencies": len(graph.edges),
217
+ "status_distribution": status_counts,
218
+ "lanes": [l.title for l in ws.lanes],
219
+ "top_connected_topics": top_components,
220
+ "pro_features_available": [
221
+ "AST Smart Context (-85% tokens)",
222
+ "Blast Radius (Reverse Impact)",
223
+ "Test Impact Static Analysis",
224
+ "Git Architecture Diff Overlay",
225
+ "Full 11 MCP Tools Suite",
226
+ ],
227
+ }
228
+
229
+ def call_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
230
+ if name == "list_workspace_topics":
231
+ return self.handle_list_workspace_topics(args)
232
+ elif name == "get_topic_context":
233
+ return self.handle_get_topic_context(args)
234
+ elif name == "get_architecture_summary":
235
+ return self.handle_get_architecture_summary(args)
236
+ else:
237
+ raise ValueError(
238
+ f"Unknown or proprietary tool '{name}'. "
239
+ "CodeGraphy Lite includes 3 core workspace tools. "
240
+ "Upgrade to CodeGraphy Pro for the full 11-tool suite."
241
+ )
242
+
243
+ def dispatch(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
244
+ req_id = request.get("id")
245
+ method = request.get("method")
246
+ params = request.get("params", {})
247
+
248
+ if method == "initialize":
249
+ return {
250
+ "jsonrpc": "2.0",
251
+ "id": req_id,
252
+ "result": {
253
+ "protocolVersion": PROTOCOL_VERSION,
254
+ "serverInfo": {
255
+ "name": SERVER_NAME,
256
+ "version": __version__,
257
+ },
258
+ "capabilities": {
259
+ "tools": {"listChanged": False},
260
+ },
261
+ },
262
+ }
263
+ elif method == "tools/list":
264
+ return {
265
+ "jsonrpc": "2.0",
266
+ "id": req_id,
267
+ "result": {"tools": TOOLS_MANIFEST},
268
+ }
269
+ elif method == "tools/call":
270
+ tool_name = params.get("name")
271
+ args = params.get("arguments", {})
272
+ try:
273
+ raw_res = self.call_tool(tool_name, args)
274
+ scrubbed = scrub_secrets(raw_res)
275
+ text_out = json.dumps(scrubbed, indent=2, ensure_ascii=False)
276
+ if len(text_out) > MAX_PAYLOAD_CHARS:
277
+ text_out = text_out[:MAX_PAYLOAD_CHARS] + "\n... [Payload truncated for Lite MCP]"
278
+ return {
279
+ "jsonrpc": "2.0",
280
+ "id": req_id,
281
+ "result": {
282
+ "content": [{"type": "text", "text": text_out}],
283
+ "isError": False,
284
+ },
285
+ }
286
+ except Exception as e:
287
+ return {
288
+ "jsonrpc": "2.0",
289
+ "id": req_id,
290
+ "result": {
291
+ "content": [{"type": "text", "text": f"Error: {str(e)}"}],
292
+ "isError": True,
293
+ },
294
+ }
295
+ elif method == "ping":
296
+ return {"jsonrpc": "2.0", "id": req_id, "result": {}}
297
+ elif method in ("notifications/initialized", "$/cancelRequest"):
298
+ return None
299
+ return {
300
+ "jsonrpc": "2.0",
301
+ "id": req_id,
302
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
303
+ }
304
+
305
+
306
+ def run_stdio_server():
307
+ server = CodeGraphyLiteMCP()
308
+ while True:
309
+ try:
310
+ line = sys.stdin.readline()
311
+ if not line:
312
+ break
313
+ line = line.strip()
314
+ if not line:
315
+ continue
316
+ req = json.loads(line)
317
+ res = server.dispatch(req)
318
+ if res is not None:
319
+ sys.stdout.write(json.dumps(res, ensure_ascii=False) + "\n")
320
+ sys.stdout.flush()
321
+ except (KeyboardInterrupt, SystemExit):
322
+ break
323
+ except Exception as e:
324
+ err_res = {
325
+ "jsonrpc": "2.0",
326
+ "id": None,
327
+ "error": {"code": -32603, "message": f"MCP STDIO Server Error: {str(e)}"},
328
+ }
329
+ sys.stdout.write(json.dumps(err_res) + "\n")
330
+ sys.stdout.flush()
331
+
332
+
333
+ def main():
334
+ run_stdio_server()
335
+
336
+
337
+ if __name__ == "__main__":
338
+ main()
@@ -0,0 +1,204 @@
1
+ """Data models for CodeGraphy Lite.
2
+
3
+ Clean, standalone Pydantic models for React Flow graph, workspace domain,
4
+ topics, lanes, tasks, and preview scanner payloads.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from typing import Any, Dict, List, Literal, Optional, Tuple
12
+ from pydantic import BaseModel, Field, model_validator
13
+
14
+
15
+ def utc_now_iso() -> str:
16
+ return datetime.now(timezone.utc).isoformat()
17
+
18
+
19
+ def new_id(prefix: str = "") -> str:
20
+ uid = str(uuid.uuid4())
21
+ return f"{prefix}_{uid[:8]}" if prefix else uid
22
+
23
+
24
+ NodeStatus = Literal["todo", "in_progress", "done"]
25
+
26
+
27
+ class FileReference(BaseModel):
28
+ filepath: str
29
+
30
+
31
+ class NodeData(BaseModel):
32
+ id: str
33
+ label: str
34
+ description: str = ""
35
+ status: NodeStatus = "todo"
36
+ lane_id: Optional[str] = None
37
+ task_id: Optional[str] = None
38
+ parent_topic_id: Optional[str] = None
39
+ subtopic_count: int = 0
40
+ done_subtopic_count: int = 0
41
+ progress_percent: int = 0
42
+ attached_files: List[FileReference] = Field(default_factory=list)
43
+
44
+
45
+ class Position(BaseModel):
46
+ x: float = 0.0
47
+ y: float = 0.0
48
+
49
+
50
+ class ReactFlowNode(BaseModel):
51
+ id: str
52
+ type: str = "contextNode"
53
+ position: Position = Field(default_factory=Position)
54
+ data: NodeData
55
+
56
+
57
+ class EdgeData(BaseModel):
58
+ id: str
59
+ source: str
60
+ target: str
61
+ sourceHandle: Optional[str] = None
62
+ targetHandle: Optional[str] = None
63
+
64
+
65
+ class GraphState(BaseModel):
66
+ nodes: List[ReactFlowNode] = Field(default_factory=list)
67
+ edges: List[EdgeData] = Field(default_factory=list)
68
+
69
+
70
+ class FileSource(BaseModel):
71
+ id: str = Field(default_factory=lambda: new_id("src"))
72
+ path: str
73
+ type: Literal["file", "directory"] = "file"
74
+ recursive: bool = True
75
+ include: List[str] = Field(default_factory=lambda: [".py", ".ts", ".tsx", ".js", ".md", ".json"])
76
+ exclude: List[str] = Field(
77
+ default_factory=lambda: [
78
+ ".git", "node_modules", "dist", "build", ".venv", "venv",
79
+ "__pycache__", "release", ".codegraphy", ".archflow"
80
+ ]
81
+ )
82
+
83
+
84
+ class TopicLane(BaseModel):
85
+ id: str = Field(default_factory=lambda: new_id("lane"))
86
+ title: str = ""
87
+ name: Optional[str] = None
88
+ order: int = 0
89
+ collapsed: bool = False
90
+ color: str = "#38bdf8"
91
+
92
+ @model_validator(mode="before")
93
+ @classmethod
94
+ def sync_name_and_title(cls, data: Any) -> Any:
95
+ if isinstance(data, dict):
96
+ t = data.get("title") or data.get("name") or "Lane"
97
+ data["title"] = t
98
+ data["name"] = t
99
+ return data
100
+
101
+ def model_dump(self, *args, **kwargs) -> Dict[str, Any]:
102
+ d = super().model_dump(*args, **kwargs)
103
+ val = d.get("name") or d.get("title") or ""
104
+ d["name"] = val
105
+ d["title"] = val
106
+ return d
107
+
108
+
109
+ class TaskItem(BaseModel):
110
+ id: str = Field(default_factory=lambda: new_id("task"))
111
+ title: str
112
+ description: str = ""
113
+ status: NodeStatus = "todo"
114
+ lane_id: Optional[str] = None
115
+ assigned_user_id: Optional[str] = None
116
+ progress: int = 0
117
+ topic_ids: List[str] = Field(default_factory=list)
118
+ updated_at: str = Field(default_factory=utc_now_iso)
119
+
120
+
121
+ class Topic(BaseModel):
122
+ id: str
123
+ label: str
124
+ description: str = ""
125
+ status: NodeStatus = "todo"
126
+ lane_id: Optional[str] = None
127
+ task_id: Optional[str] = None
128
+ parent_topic_id: Optional[str] = None
129
+ position: Position = Field(default_factory=Position)
130
+ attached_files: List[FileReference] = Field(default_factory=list)
131
+ sources: List[FileSource] = Field(default_factory=list)
132
+ ai_context_settings: Dict[str, Any] = Field(
133
+ default_factory=lambda: {"include_neighbors": True, "strip_bodies": True}
134
+ )
135
+ updated_at: str = Field(default_factory=utc_now_iso)
136
+
137
+
138
+ class WorkspaceModel(BaseModel):
139
+ version: str = "2.0"
140
+ id: str = "default_workspace"
141
+ name: str = "CodeGraphy Workspace"
142
+ root_path: Optional[str] = None
143
+ lanes: List[TopicLane] = Field(
144
+ default_factory=lambda: [
145
+ TopicLane(id="lane_plan", title="Planning", order=0, color="#a855f7"),
146
+ TopicLane(id="lane_dev", title="Development", order=1, color="#38bdf8"),
147
+ TopicLane(id="lane_test", title="Testing", order=2, color="#eab308"),
148
+ TopicLane(id="lane_done", title="Done", order=3, color="#22c55e"),
149
+ ]
150
+ )
151
+ topics: List[Topic] = Field(default_factory=list)
152
+ edges: List[EdgeData] = Field(default_factory=list)
153
+ tasks: List[TaskItem] = Field(default_factory=list)
154
+ file_sources: List[FileSource] = Field(default_factory=list)
155
+ updated_at: str = Field(default_factory=utc_now_iso)
156
+
157
+ def get_topic_progress(self, topic_id: str) -> Tuple[int, int, int]:
158
+ children = [t for t in self.topics if t.parent_topic_id == topic_id]
159
+ if not children:
160
+ t = next((x for x in self.topics if x.id == topic_id), None)
161
+ pct = 100 if t and t.status == "done" else (50 if t and t.status == "in_progress" else 0)
162
+ return 0, (1 if pct == 100 else 0), pct
163
+ total = len(children)
164
+ done = sum(1 for c in children if c.status == "done")
165
+ pct = int((done / total) * 100) if total else 0
166
+ return total, done, pct
167
+
168
+
169
+ class ScannedEdgePreview(BaseModel):
170
+ source_topic_id: str
171
+ target_topic_id: str
172
+ source_file: str
173
+ target_module: str
174
+ line_number: int = 1
175
+
176
+
177
+ class ScannedTopicPreview(BaseModel):
178
+ id: str
179
+ label: str
180
+ category: str = "module"
181
+ files: List[str] = Field(default_factory=list)
182
+ incoming_count: int = 0
183
+ outgoing_count: int = 0
184
+
185
+
186
+ class ScanPreviewRequest(BaseModel):
187
+ root_path: str
188
+ ignore_dirs: List[str] = Field(
189
+ default_factory=lambda: [
190
+ ".git", ".venv", "venv", "node_modules", "__pycache__",
191
+ "dist", "build", "release", ".codegraphy", ".archflow",
192
+ ".idea", ".vscode", "coverage"
193
+ ]
194
+ )
195
+
196
+
197
+ class ScanPreviewResponse(BaseModel):
198
+ root_path: str
199
+ project_name: str
200
+ total_files: int
201
+ topics: List[ScannedTopicPreview] = Field(default_factory=list)
202
+ edges: List[ScannedEdgePreview] = Field(default_factory=list)
203
+ unresolved_imports: List[str] = Field(default_factory=list)
204
+ ignored_dirs_triggered: List[str] = Field(default_factory=list)