archilens 0.2.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.
- archilens/__init__.py +10 -0
- archilens/__main__.py +3 -0
- archilens/ai/__init__.py +441 -0
- archilens/analyzers/__init__.py +6 -0
- archilens/analyzers/dependencies.py +462 -0
- archilens/analyzers/diff.py +241 -0
- archilens/analyzers/discovery.py +233 -0
- archilens/analyzers/evolution.py +453 -0
- archilens/analyzers/git_utils.py +109 -0
- archilens/analyzers/treesitter.py +419 -0
- archilens/cli.py +476 -0
- archilens/config.py +224 -0
- archilens/engine.py +254 -0
- archilens/generators/__init__.py +5 -0
- archilens/generators/base.py +153 -0
- archilens/generators/d2.py +260 -0
- archilens/generators/mermaid.py +387 -0
- archilens/generators/plantuml.py +292 -0
- archilens/mcp/__init__.py +1 -0
- archilens/mcp/cache.py +87 -0
- archilens/mcp/server.py +434 -0
- archilens/models/__init__.py +225 -0
- archilens/utils/__init__.py +1 -0
- archilens/viewer/__init__.py +1 -0
- archilens/viewer/app.py +944 -0
- archilens-0.2.0.dist-info/METADATA +488 -0
- archilens-0.2.0.dist-info/RECORD +30 -0
- archilens-0.2.0.dist-info/WHEEL +4 -0
- archilens-0.2.0.dist-info/entry_points.txt +2 -0
- archilens-0.2.0.dist-info/licenses/LICENSE +15 -0
archilens/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ArchiLens - AI-powered layered architecture & process flow visualization
|
|
3
|
+
for Git repositories.
|
|
4
|
+
|
|
5
|
+
Provides hierarchical drill-down from system context to process flows,
|
|
6
|
+
with AI-augmented analysis, architecture drift detection, and
|
|
7
|
+
business capability mapping.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
archilens/__main__.py
ADDED
archilens/ai/__init__.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI-powered analysis using LLMs for capabilities beyond static analysis.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Process flow inference from controller/handler code
|
|
6
|
+
- Natural-language annotations for diagram nodes
|
|
7
|
+
- Business capability mapping suggestions
|
|
8
|
+
- Architectural pattern detection
|
|
9
|
+
- Module responsibility summaries
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from archilens.config import AIConfig
|
|
21
|
+
from archilens.models import (
|
|
22
|
+
ArchPattern,
|
|
23
|
+
ArchSnapshot,
|
|
24
|
+
FlowStep,
|
|
25
|
+
ProcessFlow,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Tool-use schemas (Anthropic structured output)
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
_FLOW_TOOL: dict[str, Any] = {
|
|
36
|
+
"name": "record_flow",
|
|
37
|
+
"description": "Record the extracted process flow with all its steps.",
|
|
38
|
+
"input_schema": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"flow_name": {"type": "string", "description": "Descriptive name, e.g. 'Create Order Flow'"},
|
|
42
|
+
"trigger": {"type": "string", "description": "What initiates this flow, e.g. 'POST /api/orders'"},
|
|
43
|
+
"description": {"type": "string", "description": "One-sentence summary"},
|
|
44
|
+
"steps": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"order": {"type": "integer"},
|
|
50
|
+
"actor": {"type": "string"},
|
|
51
|
+
"action": {"type": "string"},
|
|
52
|
+
"target": {"type": "string"},
|
|
53
|
+
"data": {"type": "string"},
|
|
54
|
+
"is_async": {"type": "boolean"},
|
|
55
|
+
"condition": {"type": "string"},
|
|
56
|
+
},
|
|
57
|
+
"required": ["order", "actor", "action", "target"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
"required": ["flow_name", "trigger", "steps"],
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_SUMMARY_TOOL: dict[str, Any] = {
|
|
66
|
+
"name": "record_summary",
|
|
67
|
+
"description": "Record a concise module summary.",
|
|
68
|
+
"input_schema": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"summary": {"type": "string", "description": "One sentence describing what this module does"},
|
|
72
|
+
"responsibility": {"type": "string", "description": "Primary responsibility in 3-5 words"},
|
|
73
|
+
"suggested_capability": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Business capability this maps to (e.g. 'Order Management', 'Authentication')",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"required": ["summary", "responsibility"],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_PATTERN_TOOL: dict[str, Any] = {
|
|
83
|
+
"name": "record_patterns",
|
|
84
|
+
"description": "Record detected architectural patterns.",
|
|
85
|
+
"input_schema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"patterns": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"enum": [
|
|
93
|
+
"mvc", "hexagonal", "layered", "microservices",
|
|
94
|
+
"event_driven", "cqrs", "repository", "factory",
|
|
95
|
+
"observer", "middleware_chain", "saga", "clean_architecture",
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
"description": "List of detected pattern names",
|
|
99
|
+
},
|
|
100
|
+
"reasoning": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": {"type": "string"},
|
|
103
|
+
"description": "Brief explanation per detected pattern",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
"required": ["patterns"],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Prompt templates
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
PROCESS_FLOW_PROMPT = """You are an expert software architect analyzing source code to extract runtime process flows.
|
|
116
|
+
|
|
117
|
+
Given the following source code from an entry point (controller/handler/route), identify the end-to-end request lifecycle.
|
|
118
|
+
|
|
119
|
+
**File:** {file_path}
|
|
120
|
+
**Entry Point Type:** {entry_type}
|
|
121
|
+
|
|
122
|
+
```{language}
|
|
123
|
+
{code}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Additional context - files that this entry point imports or calls:
|
|
127
|
+
{dependency_context}
|
|
128
|
+
|
|
129
|
+
Analyze this code and use the record_flow tool to capture the complete process flow."""
|
|
130
|
+
|
|
131
|
+
MODULE_SUMMARY_PROMPT = """You are an expert software architect. Given the following module information, provide a concise summary.
|
|
132
|
+
|
|
133
|
+
**Module:** {module_name}
|
|
134
|
+
**Files:**
|
|
135
|
+
{file_list}
|
|
136
|
+
|
|
137
|
+
**Key classes/functions found:**
|
|
138
|
+
{symbols}
|
|
139
|
+
|
|
140
|
+
**Dependencies (imports from other modules):**
|
|
141
|
+
{dependencies}
|
|
142
|
+
|
|
143
|
+
Use the record_summary tool to record a one-sentence summary of this module's responsibility."""
|
|
144
|
+
|
|
145
|
+
PATTERN_DETECTION_PROMPT = """You are an expert software architect. Given the following architecture overview, identify which architectural patterns are present.
|
|
146
|
+
|
|
147
|
+
**Project:** {project_name}
|
|
148
|
+
**Modules and their relationships:**
|
|
149
|
+
{module_graph}
|
|
150
|
+
|
|
151
|
+
**Directory structure:**
|
|
152
|
+
{directory_structure}
|
|
153
|
+
|
|
154
|
+
Use the record_patterns tool to record all detected patterns."""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# AI Client abstraction
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class AIClient:
|
|
163
|
+
"""
|
|
164
|
+
Abstraction over LLM providers.
|
|
165
|
+
|
|
166
|
+
Supports Anthropic (Claude), OpenAI, Ollama, and any
|
|
167
|
+
OpenAI-compatible endpoint via LiteLLM.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, config: AIConfig):
|
|
171
|
+
self.config = config
|
|
172
|
+
self._client: Any = None
|
|
173
|
+
|
|
174
|
+
def _get_client(self) -> Any:
|
|
175
|
+
"""Lazy-initialize the LLM client."""
|
|
176
|
+
if self._client is not None:
|
|
177
|
+
return self._client
|
|
178
|
+
|
|
179
|
+
if self.config.provider == "anthropic":
|
|
180
|
+
try:
|
|
181
|
+
import anthropic
|
|
182
|
+
|
|
183
|
+
self._client = anthropic.Anthropic()
|
|
184
|
+
return self._client
|
|
185
|
+
except ImportError as exc:
|
|
186
|
+
raise RuntimeError("anthropic package not installed. Run: pip install anthropic") from exc
|
|
187
|
+
else:
|
|
188
|
+
try:
|
|
189
|
+
import litellm
|
|
190
|
+
|
|
191
|
+
self._client = litellm
|
|
192
|
+
return self._client
|
|
193
|
+
except ImportError as exc:
|
|
194
|
+
raise RuntimeError("litellm package not installed. Run: pip install litellm") from exc
|
|
195
|
+
|
|
196
|
+
def complete(self, prompt: str, max_tokens: int = 4096) -> str:
|
|
197
|
+
"""Send a prompt and return the response text."""
|
|
198
|
+
client = self._get_client()
|
|
199
|
+
|
|
200
|
+
if self.config.provider == "anthropic":
|
|
201
|
+
response = client.messages.create(
|
|
202
|
+
model=self.config.model,
|
|
203
|
+
max_tokens=max_tokens,
|
|
204
|
+
messages=[{"role": "user", "content": prompt}],
|
|
205
|
+
)
|
|
206
|
+
return response.content[0].text
|
|
207
|
+
else:
|
|
208
|
+
response = client.completion(
|
|
209
|
+
model=self.config.model,
|
|
210
|
+
messages=[{"role": "user", "content": prompt}],
|
|
211
|
+
max_tokens=max_tokens,
|
|
212
|
+
)
|
|
213
|
+
return response.choices[0].message.content
|
|
214
|
+
|
|
215
|
+
def complete_with_tool(
|
|
216
|
+
self,
|
|
217
|
+
prompt: str,
|
|
218
|
+
tool: dict[str, Any],
|
|
219
|
+
max_tokens: int = 1024,
|
|
220
|
+
) -> dict[str, Any] | None:
|
|
221
|
+
"""
|
|
222
|
+
Use tool use (structured output) to get deterministic JSON.
|
|
223
|
+
|
|
224
|
+
For Anthropic: uses tool_choice={"type":"tool"} for a guaranteed
|
|
225
|
+
structured response — no fence-stripping or JSON hunting needed.
|
|
226
|
+
For other providers: falls back to text completion + JSON parsing.
|
|
227
|
+
|
|
228
|
+
Returns the tool input dict, or None on failure.
|
|
229
|
+
"""
|
|
230
|
+
client = self._get_client()
|
|
231
|
+
|
|
232
|
+
if self.config.provider == "anthropic":
|
|
233
|
+
try:
|
|
234
|
+
response = client.messages.create(
|
|
235
|
+
model=self.config.model,
|
|
236
|
+
max_tokens=max_tokens,
|
|
237
|
+
tools=[{
|
|
238
|
+
"name": tool["name"],
|
|
239
|
+
"description": tool.get("description", ""),
|
|
240
|
+
"input_schema": tool["input_schema"],
|
|
241
|
+
}],
|
|
242
|
+
tool_choice={"type": "tool", "name": tool["name"]},
|
|
243
|
+
messages=[{"role": "user", "content": prompt}],
|
|
244
|
+
)
|
|
245
|
+
for block in response.content:
|
|
246
|
+
if hasattr(block, "input"):
|
|
247
|
+
return block.input # type: ignore[return-value]
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
logger.warning("Tool use failed, falling back to text: %s", exc)
|
|
250
|
+
|
|
251
|
+
# Fallback: plain text + JSON parsing (non-Anthropic or on tool-use error)
|
|
252
|
+
text = self.complete(prompt, max_tokens)
|
|
253
|
+
return _parse_json_response(text)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------------------
|
|
257
|
+
# AI Analysis Functions
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def infer_process_flows(
|
|
262
|
+
entry_point_files: list[dict[str, str]],
|
|
263
|
+
ai_client: AIClient,
|
|
264
|
+
repo_path: Path,
|
|
265
|
+
) -> list[ProcessFlow]:
|
|
266
|
+
"""
|
|
267
|
+
Use AI to infer process flows from entry point source code.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
entry_point_files: List of dicts with keys: path, language, entry_type, dependencies
|
|
271
|
+
ai_client: Configured AI client
|
|
272
|
+
repo_path: Repository root path
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
List of ProcessFlow objects
|
|
276
|
+
"""
|
|
277
|
+
flows: list[ProcessFlow] = []
|
|
278
|
+
|
|
279
|
+
for ep in entry_point_files:
|
|
280
|
+
file_path = repo_path / ep["path"]
|
|
281
|
+
if not file_path.exists():
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
code = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
286
|
+
except OSError:
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
if len(code) > 15000:
|
|
290
|
+
code = code[:15000] + "\n# ... (truncated)"
|
|
291
|
+
|
|
292
|
+
prompt = PROCESS_FLOW_PROMPT.format(
|
|
293
|
+
file_path=ep["path"],
|
|
294
|
+
entry_type=ep.get("entry_type", "http_handler"),
|
|
295
|
+
language=ep.get("language", "python"),
|
|
296
|
+
code=code,
|
|
297
|
+
dependency_context=ep.get("dependencies", "No additional context available."),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
flow_data = ai_client.complete_with_tool(prompt, _FLOW_TOOL, max_tokens=2048)
|
|
302
|
+
|
|
303
|
+
if flow_data:
|
|
304
|
+
flow = ProcessFlow(
|
|
305
|
+
id=f"flow:{ep['path']}",
|
|
306
|
+
name=flow_data.get("flow_name", f"Flow: {ep['path']}"),
|
|
307
|
+
description=flow_data.get("description", ""),
|
|
308
|
+
trigger=flow_data.get("trigger", ""),
|
|
309
|
+
entry_point=ep["path"],
|
|
310
|
+
steps=[
|
|
311
|
+
FlowStep(
|
|
312
|
+
order=s.get("order", i),
|
|
313
|
+
actor=s.get("actor", "Unknown"),
|
|
314
|
+
action=s.get("action", ""),
|
|
315
|
+
target=s.get("target", ""),
|
|
316
|
+
data=s.get("data") or None,
|
|
317
|
+
is_async=s.get("is_async", False),
|
|
318
|
+
condition=s.get("condition") or None,
|
|
319
|
+
)
|
|
320
|
+
for i, s in enumerate(flow_data.get("steps", []))
|
|
321
|
+
],
|
|
322
|
+
)
|
|
323
|
+
flows.append(flow)
|
|
324
|
+
logger.info("Inferred flow: %s (%d steps)", flow.name, len(flow.steps))
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
logger.warning("AI flow inference failed for %s: %s", ep["path"], exc)
|
|
327
|
+
|
|
328
|
+
return flows
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def generate_module_summaries(
|
|
332
|
+
snapshot: ArchSnapshot,
|
|
333
|
+
ai_client: AIClient,
|
|
334
|
+
max_workers: int = 5,
|
|
335
|
+
) -> dict[str, dict[str, str]]:
|
|
336
|
+
"""
|
|
337
|
+
Generate AI-powered summaries for each module, running up to *max_workers*
|
|
338
|
+
completions in parallel via ThreadPoolExecutor.
|
|
339
|
+
|
|
340
|
+
Returns dict: module_id -> {"summary": ..., "responsibility": ..., "suggested_capability": ...}
|
|
341
|
+
"""
|
|
342
|
+
summaries: dict[str, dict[str, str]] = {}
|
|
343
|
+
module_nodes = snapshot.get_module_nodes()
|
|
344
|
+
|
|
345
|
+
def _summarize_one(node) -> tuple[str, dict[str, str] | None]:
|
|
346
|
+
children = snapshot.get_children(node.id)
|
|
347
|
+
symbols = ", ".join(c.name for c in children[:20])
|
|
348
|
+
deps = [e.target.replace("module:", "") for e in snapshot.edges if e.source == node.id]
|
|
349
|
+
|
|
350
|
+
prompt = MODULE_SUMMARY_PROMPT.format(
|
|
351
|
+
module_name=node.name,
|
|
352
|
+
file_list=node.file_path or "N/A",
|
|
353
|
+
symbols=symbols or "N/A",
|
|
354
|
+
dependencies=", ".join(deps) if deps else "None",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
result = ai_client.complete_with_tool(prompt, _SUMMARY_TOOL, max_tokens=500)
|
|
359
|
+
return node.id, result
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
logger.warning("AI summary failed for %s: %s", node.name, exc)
|
|
362
|
+
return node.id, None
|
|
363
|
+
|
|
364
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
365
|
+
futures = {pool.submit(_summarize_one, node): node for node in module_nodes}
|
|
366
|
+
for future in as_completed(futures):
|
|
367
|
+
try:
|
|
368
|
+
node_id, result = future.result()
|
|
369
|
+
if result:
|
|
370
|
+
summaries[node_id] = result
|
|
371
|
+
# Write ai_summary back onto the node in-place
|
|
372
|
+
for node in module_nodes:
|
|
373
|
+
if node.id == node_id:
|
|
374
|
+
node.ai_summary = result.get("summary", "")
|
|
375
|
+
break
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
logger.warning("Summary future failed: %s", exc)
|
|
378
|
+
|
|
379
|
+
return summaries
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def detect_patterns(
|
|
383
|
+
snapshot: ArchSnapshot,
|
|
384
|
+
directory_tree: str,
|
|
385
|
+
ai_client: AIClient,
|
|
386
|
+
) -> list[ArchPattern]:
|
|
387
|
+
"""Use AI to detect architectural patterns in the codebase."""
|
|
388
|
+
module_graph_lines: list[str] = []
|
|
389
|
+
for node in snapshot.get_module_nodes():
|
|
390
|
+
deps = [e.target.replace("module:", "") for e in snapshot.edges if e.source == node.id]
|
|
391
|
+
dep_str = " -> " + ", ".join(deps) if deps else " (no dependencies)"
|
|
392
|
+
module_graph_lines.append(f" {node.name}{dep_str}")
|
|
393
|
+
|
|
394
|
+
prompt = PATTERN_DETECTION_PROMPT.format(
|
|
395
|
+
project_name=snapshot.project_name,
|
|
396
|
+
module_graph="\n".join(module_graph_lines),
|
|
397
|
+
directory_structure=directory_tree[:3000],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
result = ai_client.complete_with_tool(prompt, _PATTERN_TOOL, max_tokens=1000)
|
|
402
|
+
if result and "patterns" in result:
|
|
403
|
+
valid_values = {e.value for e in ArchPattern}
|
|
404
|
+
return [ArchPattern(p) for p in result["patterns"] if p in valid_values]
|
|
405
|
+
except Exception as exc:
|
|
406
|
+
logger.warning("AI pattern detection failed: %s", exc)
|
|
407
|
+
|
|
408
|
+
return []
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ---------------------------------------------------------------------------
|
|
412
|
+
# Helpers
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _parse_json_response(text: str) -> dict[str, Any] | None:
|
|
417
|
+
"""
|
|
418
|
+
Parse JSON from an LLM response for non-Anthropic providers that don't
|
|
419
|
+
support tool use. Handles markdown code fences and partial wrapping.
|
|
420
|
+
"""
|
|
421
|
+
text = text.strip()
|
|
422
|
+
if text.startswith("```"):
|
|
423
|
+
lines = text.split("\n")
|
|
424
|
+
text = "\n".join(lines[1:])
|
|
425
|
+
if text.endswith("```"):
|
|
426
|
+
text = text[:-3]
|
|
427
|
+
text = text.strip()
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
return json.loads(text)
|
|
431
|
+
except json.JSONDecodeError:
|
|
432
|
+
start = text.find("{")
|
|
433
|
+
end = text.rfind("}") + 1
|
|
434
|
+
if start >= 0 and end > start:
|
|
435
|
+
try:
|
|
436
|
+
return json.loads(text[start:end])
|
|
437
|
+
except json.JSONDecodeError:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
logger.warning("Failed to parse JSON from AI response")
|
|
441
|
+
return None
|