system-design 0.1.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.
Files changed (46) hide show
  1. system_design/__init__.py +0 -0
  2. system_design/ai/__init__.py +0 -0
  3. system_design/ai/analyzer.py +276 -0
  4. system_design/ai/prompts.py +83 -0
  5. system_design/cli/__init__.py +0 -0
  6. system_design/cli/commands/__init__.py +0 -0
  7. system_design/cli/commands/ai_analyze.py +106 -0
  8. system_design/cli/commands/analyze.py +98 -0
  9. system_design/cli/main.py +79 -0
  10. system_design/core/__init__.py +0 -0
  11. system_design/core/config.py +26 -0
  12. system_design/core/evidence.py +47 -0
  13. system_design/core/logging.py +22 -0
  14. system_design/discovery/__init__.py +0 -0
  15. system_design/discovery/doc_detector.py +67 -0
  16. system_design/discovery/framework_detector.py +130 -0
  17. system_design/discovery/language_detector.py +54 -0
  18. system_design/discovery/models.py +135 -0
  19. system_design/discovery/package_detector.py +64 -0
  20. system_design/discovery/scanner.py +158 -0
  21. system_design/domain/__init__.py +0 -0
  22. system_design/graph/__init__.py +0 -0
  23. system_design/graph/builder.py +193 -0
  24. system_design/graph/schema.py +77 -0
  25. system_design/graph/serializer.py +25 -0
  26. system_design/graph/taxonomy.py +77 -0
  27. system_design/inference/__init__.py +0 -0
  28. system_design/inference/api_detector.py +112 -0
  29. system_design/inference/database_detector.py +124 -0
  30. system_design/inference/dependency_detector.py +178 -0
  31. system_design/inference/engine.py +95 -0
  32. system_design/inference/event_detector.py +144 -0
  33. system_design/inference/service_detector.py +163 -0
  34. system_design/infrastructure/__init__.py +0 -0
  35. system_design/plugins/__init__.py +0 -0
  36. system_design/plugins/base.py +36 -0
  37. system_design/plugins/registry.py +31 -0
  38. system_design/structural/__init__.py +0 -0
  39. system_design/visualization/__init__.py +1 -0
  40. system_design/visualization/threejs_generator.py +777 -0
  41. system_design-0.1.0.dist-info/METADATA +205 -0
  42. system_design-0.1.0.dist-info/RECORD +46 -0
  43. system_design-0.1.0.dist-info/WHEEL +5 -0
  44. system_design-0.1.0.dist-info/entry_points.txt +2 -0
  45. system_design-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. system_design-0.1.0.dist-info/top_level.txt +1 -0
File without changes
File without changes
@@ -0,0 +1,276 @@
1
+ """
2
+ system-design AI Architecture Analyzer
3
+ =============================
4
+ Uses Claude to analyze a repository inventory + static graph and produce
5
+ a rich structured architecture description for 3D rendering.
6
+
7
+ Requires: ANTHROPIC_API_KEY environment variable.
8
+ Falls back gracefully to static inference if API key not set.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ from pathlib import Path
16
+
17
+ from system_design.ai.prompts import SYSTEM_PROMPT, USER_PROMPT_TEMPLATE
18
+ from system_design.core.logging import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+ _MAX_NODES_IN_PROMPT = 60 # truncate to keep token count manageable
23
+ _MAX_EDGES_IN_PROMPT = 80
24
+
25
+
26
+ def _summarize_nodes(nodes: list[dict]) -> str:
27
+ lines = []
28
+ for n in nodes[:_MAX_NODES_IN_PROMPT]:
29
+ lines.append(f" - [{n.get('type','?')}] id={n.get('id','?')} name={n.get('name','?')}")
30
+ if len(nodes) > _MAX_NODES_IN_PROMPT:
31
+ lines.append(f" ... and {len(nodes) - _MAX_NODES_IN_PROMPT} more")
32
+ return "\n".join(lines)
33
+
34
+
35
+ def _summarize_edges(edges: list[dict]) -> str:
36
+ lines = []
37
+ for e in edges[:_MAX_EDGES_IN_PROMPT]:
38
+ lines.append(
39
+ f" - {e.get('source_id','?')} --[{e.get('type','?')}]--> {e.get('target_id','?')}"
40
+ )
41
+ if len(edges) > _MAX_EDGES_IN_PROMPT:
42
+ lines.append(f" ... and {len(edges) - _MAX_EDGES_IN_PROMPT} more")
43
+ return "\n".join(lines)
44
+
45
+
46
+ def _build_prompt(inventory: dict, graph: dict) -> str:
47
+ nodes = graph.get("nodes", [])
48
+ edges = graph.get("edges", [])
49
+ inv = inventory
50
+
51
+ return USER_PROMPT_TEMPLATE.format(
52
+ repo_name = inv.get("repository_name", "unknown"),
53
+ primary_language= inv.get("primary_language", "unknown"),
54
+ languages = ", ".join(
55
+ f"{l['language']}({l['file_count']} files)"
56
+ for l in inv.get("languages", [])
57
+ ) or "none detected",
58
+ frameworks = ", ".join(
59
+ f['framework'] for f in inv.get("frameworks", [])
60
+ ) or "none detected",
61
+ package_managers= ", ".join(
62
+ p['manager'] for p in inv.get("package_managers", [])
63
+ ) or "none",
64
+ infrastructure = ", ".join(
65
+ i['type'] for i in inv.get("infrastructure", [])
66
+ ) or "none detected",
67
+ has_tests = inv.get("has_tests", False),
68
+ has_ci = inv.get("has_ci", False),
69
+ total_files = inv.get("total_files", 0),
70
+ node_count = len(nodes),
71
+ nodes_summary = _summarize_nodes(nodes),
72
+ edge_count = len(edges),
73
+ edges_summary = _summarize_edges(edges),
74
+ )
75
+
76
+
77
+ def _extract_json(text: str) -> dict:
78
+ """Extract JSON from Claude's response (handles markdown code blocks)."""
79
+ # Try direct parse first
80
+ try:
81
+ return json.loads(text.strip())
82
+ except json.JSONDecodeError:
83
+ pass
84
+
85
+ # Try extracting from ```json ... ``` block
86
+ m = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
87
+ if m:
88
+ try:
89
+ return json.loads(m.group(1))
90
+ except json.JSONDecodeError:
91
+ pass
92
+
93
+ # Find the first { ... } block
94
+ m = re.search(r"\{.*\}", text, re.DOTALL)
95
+ if m:
96
+ try:
97
+ return json.loads(m.group(0))
98
+ except json.JSONDecodeError:
99
+ pass
100
+
101
+ raise ValueError("Could not extract valid JSON from Claude response")
102
+
103
+
104
+ def _static_fallback(inventory: dict, graph: dict) -> dict:
105
+ """Generate a minimal architecture analysis without Claude."""
106
+ nodes = graph.get("nodes", [])
107
+ edges = graph.get("edges", [])
108
+ repo = inventory.get("repository_name", "system")
109
+
110
+ LAYER_MAP = {
111
+ "frontend": "frontend", "backend": "service", "monolith": "service",
112
+ "service": "service", "gateway": "gateway", "worker": "service",
113
+ "database": "data", "cache": "data",
114
+ "queue": "messaging", "topic": "messaging", "event": "messaging",
115
+ "rest_api": "gateway", "graphql_api": "gateway", "grpc_api": "gateway",
116
+ "webhook": "gateway", "cluster": "infrastructure", "namespace": "infrastructure",
117
+ "pod": "infrastructure", "external_system": "external",
118
+ "domain": "service", "repository": "service", "team": "external",
119
+ }
120
+ SHAPE_MAP = {
121
+ "frontend": "cuboid", "backend": "cube", "monolith": "cube",
122
+ "service": "cube", "gateway": "cuboid", "worker": "cuboid",
123
+ "database": "cylinder", "cache": "cylinder",
124
+ "queue": "torus", "topic": "torus", "event": "octahedron",
125
+ "rest_api": "panel", "graphql_api": "panel", "grpc_api": "panel",
126
+ "cluster": "cuboid", "external_system": "sphere", "domain": "cuboid",
127
+ }
128
+ WIRELESS_TYPES = {"PUBLISHES", "CONSUMES", "INVOKES"}
129
+
130
+ components = []
131
+ for n in nodes:
132
+ ntype = n.get("type", "unknown")
133
+ components.append({
134
+ "node_id": n.get("id", ""),
135
+ "display_name": n.get("name", n.get("id", "")),
136
+ "description": f"A {ntype} component in {repo}",
137
+ "capabilities": [],
138
+ "layer": LAYER_MAP.get(ntype, "service"),
139
+ "shape": SHAPE_MAP.get(ntype, "cube"),
140
+ "importance": 3,
141
+ })
142
+
143
+ connections = []
144
+ for e in edges:
145
+ etype = e.get("type", "CALLS")
146
+ connections.append({
147
+ "source_id": e.get("source_id", ""),
148
+ "target_id": e.get("target_id", ""),
149
+ "edge_type": etype,
150
+ "wire_type": "wireless" if etype in WIRELESS_TYPES else "wired",
151
+ "label": etype.lower().replace("_", " "),
152
+ })
153
+
154
+ return {
155
+ "system_description": f"Software system: {repo}",
156
+ "system_type": "unknown",
157
+ "components": components,
158
+ "connections": connections,
159
+ "domains": [],
160
+ }
161
+
162
+
163
+ def analyze_with_claude(
164
+ inventory: dict,
165
+ graph: dict,
166
+ model: str = "claude-opus-4-6",
167
+ ) -> dict:
168
+ """
169
+ Send repository inventory + graph to Claude and return structured
170
+ architecture analysis.
171
+
172
+ Returns a dict with keys: system_description, system_type,
173
+ components, connections, domains.
174
+
175
+ If ANTHROPIC_API_KEY is not set, falls back to static analysis.
176
+ """
177
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
178
+ if not api_key:
179
+ logger.warning(
180
+ "ANTHROPIC_API_KEY not set — using static fallback analysis. "
181
+ "Set the key to enable AI-powered architecture analysis."
182
+ )
183
+ return _static_fallback(inventory, graph)
184
+
185
+ try:
186
+ import anthropic
187
+ except ImportError:
188
+ logger.warning("anthropic package not installed. Run: pip install anthropic")
189
+ return _static_fallback(inventory, graph)
190
+
191
+ logger.info(f"Sending repository to Claude ({model}) for architecture analysis...")
192
+
193
+ prompt = _build_prompt(inventory, graph)
194
+ client = anthropic.Anthropic(api_key=api_key)
195
+
196
+ try:
197
+ response = client.messages.create(
198
+ model=model,
199
+ max_tokens=4096,
200
+ system=SYSTEM_PROMPT,
201
+ messages=[{"role": "user", "content": prompt}],
202
+ )
203
+ raw = response.content[0].text
204
+ result = _extract_json(raw)
205
+ logger.info(
206
+ f"AI analysis complete: {len(result.get('components', []))} components, "
207
+ f"{len(result.get('connections', []))} connections, "
208
+ f"{len(result.get('domains', []))} domains"
209
+ )
210
+ return result
211
+ except Exception as e:
212
+ logger.warning(f"Claude API call failed ({e}) — using static fallback")
213
+ return _static_fallback(inventory, graph)
214
+
215
+
216
+ def save_analysis(analysis: dict, output_dir: Path) -> dict[str, Path]:
217
+ """Save AI analysis as separate output files."""
218
+ output_dir.mkdir(parents=True, exist_ok=True)
219
+ outputs: dict[str, Path] = {}
220
+
221
+ # architecture.json (enriched graph)
222
+ arch_path = output_dir / "architecture.json"
223
+ arch_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
224
+ outputs["architecture"] = arch_path
225
+
226
+ # service-map.json
227
+ services = [
228
+ c for c in analysis.get("components", [])
229
+ if c.get("layer") in {"service", "gateway", "frontend"}
230
+ ]
231
+ sm_path = output_dir / "service-map.json"
232
+ sm_path.write_text(json.dumps({
233
+ "services": services,
234
+ "connections": [
235
+ c for c in analysis.get("connections", [])
236
+ if c.get("edge_type") in {"CALLS", "DEPENDS_ON"}
237
+ ],
238
+ }, indent=2), encoding="utf-8")
239
+ outputs["service_map"] = sm_path
240
+
241
+ # domain-map.json
242
+ dm_path = output_dir / "domain-map.json"
243
+ dm_path.write_text(json.dumps({
244
+ "domains": analysis.get("domains", []),
245
+ }, indent=2), encoding="utf-8")
246
+ outputs["domain_map"] = dm_path
247
+
248
+ # architecture.md
249
+ md_lines = [
250
+ f"# {analysis.get('system_description', 'Architecture')}",
251
+ f"\n**System Type:** {analysis.get('system_type', 'unknown')}\n",
252
+ "\n## Components\n",
253
+ ]
254
+ for c in analysis.get("components", []):
255
+ md_lines.append(f"### {c['display_name']} `[{c.get('layer','?')}]`")
256
+ md_lines.append(f"{c.get('description', '')}")
257
+ caps = c.get("capabilities", [])
258
+ if caps:
259
+ md_lines.append("\n**Capabilities:**")
260
+ for cap in caps:
261
+ md_lines.append(f"- {cap}")
262
+ md_lines.append("")
263
+
264
+ if analysis.get("domains"):
265
+ md_lines.append("\n## Domains\n")
266
+ for d in analysis["domains"]:
267
+ md_lines.append(f"### {d['name']}")
268
+ md_lines.append(d.get("description", ""))
269
+ md_lines.append("")
270
+
271
+ md_path = output_dir / "architecture.md"
272
+ md_path.write_text("\n".join(md_lines), encoding="utf-8")
273
+ outputs["architecture_md"] = md_path
274
+
275
+ logger.info(f"Saved analysis artifacts to {output_dir}")
276
+ return outputs
@@ -0,0 +1,83 @@
1
+ """Prompt templates for system-design AI architecture analysis."""
2
+
3
+ SYSTEM_PROMPT = """You are a senior software architect with deep expertise in system design, microservices, distributed systems, and cloud-native architecture.
4
+
5
+ Your job is to analyze a software repository and produce a precise, structured architectural description that will be used to render an interactive 3D system design diagram.
6
+
7
+ Rules:
8
+ - Be concrete and specific. Use actual service names from the codebase.
9
+ - Infer capabilities from the code evidence provided.
10
+ - Classify every component into a layer: frontend | gateway | service | data | messaging | infrastructure | external
11
+ - Classify every connection as: wired (synchronous HTTP/gRPC/function call) OR wireless (async event/queue/pub-sub)
12
+ - Return ONLY valid JSON. No markdown, no explanation outside the JSON."""
13
+
14
+ USER_PROMPT_TEMPLATE = """Analyze this software repository and produce a structured system design description.
15
+
16
+ === REPOSITORY INVENTORY ===
17
+ Name: {repo_name}
18
+ Primary Language: {primary_language}
19
+ Languages detected: {languages}
20
+ Frameworks detected: {frameworks}
21
+ Package managers: {package_managers}
22
+ Infrastructure: {infrastructure}
23
+ Has tests: {has_tests}
24
+ Has CI: {has_ci}
25
+ Total files: {total_files}
26
+
27
+ === ARCHITECTURE GRAPH (from static analysis) ===
28
+ Nodes ({node_count}):
29
+ {nodes_summary}
30
+
31
+ Edges ({edge_count}):
32
+ {edges_summary}
33
+
34
+ === TASK ===
35
+ Return a JSON object with this exact schema:
36
+
37
+ {{
38
+ "system_description": "One clear sentence describing what this system does",
39
+ "system_type": "monolith | microservices | monorepo | library | cli | data-pipeline | unknown",
40
+ "components": [
41
+ {{
42
+ "node_id": "<matches a node id from the graph above>",
43
+ "display_name": "<human-readable name>",
44
+ "description": "<what this component does, 1-2 sentences>",
45
+ "capabilities": ["<specific capability>", "<specific capability>"],
46
+ "layer": "frontend | gateway | service | data | messaging | infrastructure | external",
47
+ "shape": "cube | cuboid | cylinder | torus | octahedron | sphere | panel",
48
+ "importance": 1-5
49
+ }}
50
+ ],
51
+ "connections": [
52
+ {{
53
+ "source_id": "<node id>",
54
+ "target_id": "<node id>",
55
+ "edge_type": "CALLS | READS | WRITES | PUBLISHES | CONSUMES | DEPENDS_ON",
56
+ "wire_type": "wired | wireless",
57
+ "label": "<short label like 'REST API', 'event', 'SQL query'>"
58
+ }}
59
+ ],
60
+ "domains": [
61
+ {{
62
+ "name": "<domain name, e.g. Commerce, Identity, Billing>",
63
+ "description": "<what business area>",
64
+ "node_ids": ["<node ids that belong to this domain>"]
65
+ }}
66
+ ]
67
+ }}
68
+
69
+ For components, choose shapes based on their type:
70
+ - service/backend/monolith → cube or cuboid
71
+ - database/storage → cylinder
72
+ - queue/message broker → torus
73
+ - event → octahedron
74
+ - frontend/UI → cuboid (wide, flat)
75
+ - gateway/load balancer → cuboid (wide)
76
+ - external system → sphere
77
+ - infrastructure/cluster → cuboid (tall)
78
+
79
+ For connections:
80
+ - HTTP calls, database queries, function calls → wired
81
+ - Events, pub/sub, queues, webhooks → wireless
82
+
83
+ Include ALL nodes from the graph in components. Add connections that make architectural sense even if not explicitly in the edge list — infer from the codebase context."""
File without changes
File without changes
@@ -0,0 +1,106 @@
1
+ """system-design ai-analyze — AI-powered architecture analysis using Claude."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def ai_analyze_command(
14
+ repo_path: Path,
15
+ output_dir: Path,
16
+ model: str,
17
+ verbose: bool,
18
+ ) -> None:
19
+ """
20
+ Full pipeline:
21
+ 1. Static scan → repository_inventory.json + architecture_graph.json
22
+ 2. Claude AI → architecture.json + architecture.md + service-map.json + domain-map.json
23
+ 3. 3D render → architecture.html
24
+ """
25
+ from system_design.core.config import ZEAConfig
26
+ from system_design.core.logging import setup_logging
27
+ from system_design.discovery.scanner import save_inventory, scan_repository
28
+ from system_design.graph.builder import AKGBuilder
29
+ from system_design.graph.serializer import save_graph
30
+ from system_design.inference.engine import InferenceEngine
31
+ from system_design.ai.analyzer import analyze_with_claude, save_analysis
32
+ from system_design.visualization.threejs_generator import generate_html
33
+
34
+ setup_logging(verbose)
35
+ if not repo_path.exists():
36
+ console.print(f"[red]Error:[/red] Path not found: {repo_path}")
37
+ raise typer.Exit(1)
38
+
39
+ config = ZEAConfig(output_dir=output_dir, verbose=verbose)
40
+ console.print(f"\n[bold blue]system-design[/bold blue] AI Analysis — [cyan]{repo_path.resolve().name}[/cyan]\n")
41
+
42
+ # ── Stage 1: Static discovery ──────────────────────────────────────────────
43
+ with console.status("[bold]Stage 1/4:[/bold] Scanning repository…"):
44
+ inventory = scan_repository(repo_path, config)
45
+ save_inventory(inventory, config.output_path("repository_inventory.json"))
46
+
47
+ console.print(
48
+ f" [green]✓[/green] Discovery: [b]{len(inventory.languages)}[/b] languages, "
49
+ f"[b]{len(inventory.frameworks)}[/b] frameworks, "
50
+ f"[b]{inventory.total_files}[/b] files"
51
+ )
52
+
53
+ # ── Stage 2: Static graph ──────────────────────────────────────────────────
54
+ with console.status("[bold]Stage 2/4:[/bold] Building knowledge graph…"):
55
+ builder = AKGBuilder(inventory)
56
+ akg = builder.build()
57
+ engine = InferenceEngine(akg, repo_path)
58
+ akg = engine.run()
59
+ save_graph(akg, config.output_path("architecture_graph.json"))
60
+
61
+ console.print(
62
+ f" [green]✓[/green] Graph: [b]{akg.node_count}[/b] nodes, [b]{akg.edge_count}[/b] edges"
63
+ )
64
+
65
+ # ── Stage 3: AI analysis ───────────────────────────────────────────────────
66
+ with console.status("[bold]Stage 3/4:[/bold] AI architecture analysis (Claude)…"):
67
+ inventory_dict = json.loads(
68
+ config.output_path("repository_inventory.json").read_text()
69
+ )
70
+ graph_dict = json.loads(
71
+ config.output_path("architecture_graph.json").read_text()
72
+ )
73
+ analysis = analyze_with_claude(inventory_dict, graph_dict, model=model)
74
+ analysis["_repo_name"] = inventory.repository_name
75
+ outputs = save_analysis(analysis, output_dir)
76
+
77
+ console.print(
78
+ f" [green]✓[/green] AI analysis: [b]{len(analysis.get('components',[]))}[/b] components, "
79
+ f"[b]{len(analysis.get('connections',[]))}[/b] connections, "
80
+ f"[b]{len(analysis.get('domains',[]))}[/b] domains"
81
+ )
82
+
83
+ # ── Stage 4: 3D render ─────────────────────────────────────────────────────
84
+ with console.status("[bold]Stage 4/4:[/bold] Rendering 3D system design…"):
85
+ html_path = config.output_path("architecture.html")
86
+ generate_html(analysis, html_path)
87
+
88
+ console.print(f" [green]✓[/green] 3D system design rendered\n")
89
+
90
+ # ── Summary ────────────────────────────────────────────────────────────────
91
+ console.print(f"[bold green]✓[/bold green] Complete. Artifacts in [cyan]{output_dir}[/cyan]:\n")
92
+ files = [
93
+ ("repository_inventory.json", "Static discovery"),
94
+ ("architecture_graph.json", "Knowledge graph"),
95
+ ("architecture.json", "AI architecture analysis"),
96
+ ("architecture.md", "System design document"),
97
+ ("service-map.json", "Service dependency map"),
98
+ ("domain-map.json", "Domain map"),
99
+ ("architecture.html", "3D interactive system design"),
100
+ ]
101
+ for fname, desc in files:
102
+ p = output_dir / fname
103
+ if p.exists():
104
+ console.print(f" [dim]{fname:35}[/dim] {desc}")
105
+
106
+ console.print(f"\n [bold]Open:[/bold] {output_dir / 'architecture.html'}\n")
@@ -0,0 +1,98 @@
1
+ """system-design analyze — full pipeline: Discovery → Graph → Inference."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ # Default output directory
11
+ _DEFAULT_OUTPUT = Path(".system-design")
12
+
13
+ from system_design.core.config import ZEAConfig
14
+ from system_design.core.logging import setup_logging
15
+ from system_design.discovery.scanner import save_inventory, scan_repository
16
+ from system_design.graph.builder import AKGBuilder
17
+ from system_design.graph.serializer import save_graph
18
+ from system_design.inference.engine import InferenceEngine
19
+
20
+ console = Console()
21
+
22
+
23
+ def analyze_command(
24
+ repo_path: Path,
25
+ output_dir: Path = Path(".system-design"),
26
+ verbose: bool = False,
27
+ skip_inference: bool = False,
28
+ ) -> None:
29
+ """Analyze a repository and generate the Architecture Knowledge Graph."""
30
+ setup_logging(verbose)
31
+
32
+ if not repo_path.exists():
33
+ console.print(f"[red]Error:[/red] Path does not exist: {repo_path}")
34
+ raise typer.Exit(1)
35
+
36
+ config = ZEAConfig(output_dir=output_dir, verbose=verbose)
37
+
38
+ # --- Stage 1: Repository Discovery ---
39
+ console.print(f"\n[bold blue]system-design[/bold blue] Analyzing [cyan]{repo_path.resolve().name}[/cyan]\n")
40
+
41
+ with console.status("[bold green]Stage 1/3:[/bold green] Repository Discovery..."):
42
+ inventory = scan_repository(repo_path, config)
43
+ inv_path = config.output_path("repository_inventory.json")
44
+ save_inventory(inventory, inv_path)
45
+
46
+ _print_inventory_summary(inventory)
47
+
48
+ # --- Stage 2: Build AKG skeleton ---
49
+ with console.status("[bold green]Stage 2/3:[/bold green] Building Knowledge Graph..."):
50
+ builder = AKGBuilder(inventory)
51
+ akg = builder.build()
52
+
53
+ # --- Stage 3: Architecture Inference ---
54
+ if not skip_inference:
55
+ with console.status("[bold green]Stage 3/3:[/bold green] Architecture Inference..."):
56
+ engine = InferenceEngine(akg, repo_path)
57
+ akg = engine.run()
58
+
59
+ graph_path = config.output_path("architecture_graph.json")
60
+ save_graph(akg, graph_path)
61
+
62
+ # --- Summary ---
63
+ console.print(f"\n[bold green]✓[/bold green] Analysis complete\n")
64
+ console.print(f" [dim]repository_inventory.json[/dim] → {inv_path}")
65
+ console.print(f" [dim]architecture_graph.json[/dim] → {graph_path}")
66
+ console.print(f"\n Nodes: [bold]{akg.node_count}[/bold] Edges: [bold]{akg.edge_count}[/bold]\n")
67
+
68
+
69
+ def _print_inventory_summary(inventory: object) -> None:
70
+ from system_design.discovery.models import RepositoryInventory
71
+
72
+ if not isinstance(inventory, RepositoryInventory):
73
+ return
74
+
75
+ table = Table(title="Repository Inventory", show_header=True, header_style="bold cyan")
76
+ table.add_column("Property", style="dim")
77
+ table.add_column("Value")
78
+
79
+ table.add_row("Name", inventory.repository_name)
80
+ table.add_row("Primary Language", inventory.primary_language)
81
+ table.add_row("Total Files", str(inventory.total_files))
82
+ table.add_row(
83
+ "Languages",
84
+ ", ".join(f"{ls.language}({ls.file_count})" for ls in inventory.languages[:5]),
85
+ )
86
+ table.add_row(
87
+ "Frameworks",
88
+ ", ".join(f.framework for f in inventory.frameworks) or "None detected",
89
+ )
90
+ table.add_row(
91
+ "Package Managers",
92
+ ", ".join(p.manager for p in inventory.package_managers) or "None detected",
93
+ )
94
+ table.add_row("Has Tests", "✓" if inventory.has_tests else "✗")
95
+ table.add_row("Has CI", "✓" if inventory.has_ci else "✗")
96
+
97
+ console.print(table)
98
+ console.print()
@@ -0,0 +1,79 @@
1
+ """system-design CLI entrypoint."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from system_design.cli.commands.analyze import analyze_command
8
+
9
+ app = typer.Typer(
10
+ name="system-design",
11
+ help="system-design — Architecture Intelligence Engine",
12
+ add_completion=False,
13
+ rich_markup_mode="rich",
14
+ )
15
+
16
+ console = Console()
17
+
18
+
19
+ @app.command("analyze")
20
+ def analyze(
21
+ repo_path: str = typer.Argument(..., help="Path to repository"),
22
+ output_dir: str = typer.Option(".system-design", "--output", "-o"),
23
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
24
+ skip_inference: bool = typer.Option(False, "--no-inference"),
25
+ ) -> None:
26
+ """Analyze a repository: Discovery → Graph → Inference."""
27
+ from pathlib import Path
28
+ analyze_command(
29
+ repo_path=Path(repo_path),
30
+ output_dir=Path(output_dir),
31
+ verbose=verbose,
32
+ skip_inference=skip_inference,
33
+ )
34
+
35
+
36
+ @app.command("visualize")
37
+ def visualize(
38
+ graph_path: str = typer.Argument(..., help="Path to architecture_graph.json"),
39
+ output: str = typer.Option("architecture.html", "--output", "-o", help="Output HTML file path"),
40
+ ) -> None:
41
+ """Generate a standalone 3D interactive architecture visualisation."""
42
+ from pathlib import Path
43
+ from system_design.visualization.threejs_generator import generate_html_from_file
44
+
45
+ src = Path(graph_path)
46
+ if not src.exists():
47
+ console.print(f"[bold red]Error:[/bold red] File not found: {graph_path}")
48
+ raise typer.Exit(code=1)
49
+
50
+ out = generate_html_from_file(src, Path(output))
51
+ console.print(f"[bold green]✓[/bold green] 3D visualisation written to [bold]{out}[/bold]")
52
+
53
+
54
+ @app.command("ai-analyze")
55
+ def ai_analyze(
56
+ repo_path: str = typer.Argument(..., help="Path to repository"),
57
+ output_dir: str = typer.Option(".system-design", "--output", "-o"),
58
+ model: str = typer.Option("claude-opus-4-6", "--model", "-m", help="Claude model to use"),
59
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
60
+ ) -> None:
61
+ """Full AI pipeline: Scan → Claude Analysis → 3D System Design."""
62
+ from pathlib import Path
63
+ from system_design.cli.commands.ai_analyze import ai_analyze_command
64
+ ai_analyze_command(
65
+ repo_path=Path(repo_path),
66
+ output_dir=Path(output_dir),
67
+ model=model,
68
+ verbose=verbose,
69
+ )
70
+
71
+
72
+ @app.command("version")
73
+ def version() -> None:
74
+ """Show version."""
75
+ console.print("[bold blue]system-design[/bold blue] v0.1.0 — Architecture Intelligence Engine")
76
+
77
+
78
+ if __name__ == "__main__":
79
+ app()
File without changes
@@ -0,0 +1,26 @@
1
+ """system-design core configuration."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ZEAConfig(BaseModel):
9
+ """Global system-design configuration."""
10
+
11
+ output_dir: Path = Field(default=Path(".system-design"), description="Output directory for artifacts")
12
+ verbose: bool = False
13
+ max_file_size_kb: int = 512 # Skip files larger than this
14
+ exclude_dirs: list[str] = Field(
15
+ default_factory=lambda: [
16
+ ".git", ".hg", ".svn",
17
+ "node_modules", "__pycache__", ".venv", "venv", "env",
18
+ ".tox", "dist", "build", ".eggs", "*.egg-info",
19
+ ".mypy_cache", ".ruff_cache", ".pytest_cache",
20
+ "coverage", ".coverage",
21
+ ]
22
+ )
23
+
24
+ def output_path(self, filename: str) -> Path:
25
+ self.output_dir.mkdir(parents=True, exist_ok=True)
26
+ return self.output_dir / filename