braynix-dev-mcp 2.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.
- app.py +60 -0
- brain/__init__.py +7 -0
- brain/dependency.py +292 -0
- brain/guardian.py +124 -0
- brain/handoff_engine.py +235 -0
- brain/orchestrator.py +406 -0
- brain/phase_manager.py +285 -0
- brain/prd_manager.py +151 -0
- brain/project_readiness.py +1071 -0
- brain/requirements.py +300 -0
- brain/sync_engine.py +554 -0
- brain/todo_engine.py +355 -0
- braynix_dev_mcp-2.2.0.dist-info/METADATA +451 -0
- braynix_dev_mcp-2.2.0.dist-info/RECORD +33 -0
- braynix_dev_mcp-2.2.0.dist-info/WHEEL +5 -0
- braynix_dev_mcp-2.2.0.dist-info/entry_points.txt +3 -0
- braynix_dev_mcp-2.2.0.dist-info/licenses/LICENSE +21 -0
- braynix_dev_mcp-2.2.0.dist-info/top_level.txt +9 -0
- cli.py +503 -0
- context.py +409 -0
- prompts/__init__.py +3 -0
- prompts/definitions.py +136 -0
- resources/__init__.py +3 -0
- resources/definitions.py +35 -0
- security.py +136 -0
- server.py +322 -0
- tools/__init__.py +6 -0
- tools/context_mgmt.py +257 -0
- tools/desktop_ops.py +347 -0
- tools/file_ops.py +413 -0
- tools/git_ops.py +169 -0
- tools/search.py +122 -0
- tools/terminal.py +108 -0
app.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared application state for dev-mcp.
|
|
3
|
+
|
|
4
|
+
This module creates the FastMCP instance and holds global state that is
|
|
5
|
+
shared across all tool, prompt, and resource modules.
|
|
6
|
+
|
|
7
|
+
All tool/prompt/resource modules import `mcp` from here to register their
|
|
8
|
+
decorators in-place. `server.py` then imports those modules to trigger
|
|
9
|
+
registration, and finally calls ``mcp.run()``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
from context import ProjectContext
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# FastMCP instance
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
mcp = FastMCP(
|
|
26
|
+
"dev-mcp",
|
|
27
|
+
instructions=(
|
|
28
|
+
"Project management MCP server for building, editing, and customizing "
|
|
29
|
+
"projects with AI. Maintains a persistent project context document that "
|
|
30
|
+
"captures tech stack, architecture, conventions, key files, active "
|
|
31
|
+
"tasks, goals, and architectural decisions. Provides prebuilt prompt "
|
|
32
|
+
"templates for common operations."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Shared global state
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
# Lazy-initialized project context
|
|
41
|
+
_project_context: ProjectContext | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_context(project_root: str | Path | None = None) -> ProjectContext:
|
|
45
|
+
"""Get (or create) the global project context instance."""
|
|
46
|
+
global _project_context
|
|
47
|
+
from security import validate_path
|
|
48
|
+
|
|
49
|
+
target_root = validate_path(project_root) if project_root else Path.cwd().resolve()
|
|
50
|
+
|
|
51
|
+
if _project_context is None or _project_context._root != target_root:
|
|
52
|
+
_project_context = ProjectContext(target_root)
|
|
53
|
+
|
|
54
|
+
return _project_context
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_context(ctx: ProjectContext) -> None:
|
|
58
|
+
"""Set the global project context (used by init_context tool)."""
|
|
59
|
+
global _project_context
|
|
60
|
+
_project_context = ctx
|
brain/__init__.py
ADDED
brain/dependency.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency Graph and Impact Analysis engine for Project Brain.
|
|
3
|
+
|
|
4
|
+
Scans imports across codebase to map connections and analyze edit impact.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import json
|
|
13
|
+
from app import mcp
|
|
14
|
+
from security import validate_path
|
|
15
|
+
|
|
16
|
+
IGNORE_DIRS = {
|
|
17
|
+
".git",
|
|
18
|
+
"node_modules",
|
|
19
|
+
".venv",
|
|
20
|
+
"venv",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
"dist",
|
|
23
|
+
"build",
|
|
24
|
+
".project_brain",
|
|
25
|
+
".agents",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_js_ts_import(source_file: Path, import_str: str, root: Path) -> Path | None:
|
|
30
|
+
"""Resolve JS/TS relative imports to actual file paths."""
|
|
31
|
+
if not import_str.startswith("."):
|
|
32
|
+
return None # External package
|
|
33
|
+
|
|
34
|
+
dirpath = source_file.parent
|
|
35
|
+
base_target = (dirpath / import_str).resolve()
|
|
36
|
+
|
|
37
|
+
# Try direct file resolutions
|
|
38
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".json"):
|
|
39
|
+
candidate = base_target.with_suffix(ext)
|
|
40
|
+
if candidate.is_file():
|
|
41
|
+
return candidate.relative_to(root)
|
|
42
|
+
|
|
43
|
+
# Try directory resolutions (index files)
|
|
44
|
+
if base_target.is_dir():
|
|
45
|
+
for index_name in ("index.ts", "index.tsx", "index.js", "index.jsx"):
|
|
46
|
+
candidate = base_target / index_name
|
|
47
|
+
if candidate.is_file():
|
|
48
|
+
return candidate.relative_to(root)
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_python_import(source_file: Path, import_str: str, root: Path) -> Path | None:
|
|
54
|
+
"""Resolve Python absolute or relative imports to file paths."""
|
|
55
|
+
# Handle relative imports e.g., 'from .app import mcp' or 'from ..utils import X'
|
|
56
|
+
if import_str.startswith("."):
|
|
57
|
+
dots_count = len(import_str) - len(import_str.lstrip("."))
|
|
58
|
+
target_parts = import_str.lstrip(".").split(".")
|
|
59
|
+
|
|
60
|
+
dirpath = source_file.parent
|
|
61
|
+
for _ in range(dots_count - 1):
|
|
62
|
+
dirpath = dirpath.parent
|
|
63
|
+
|
|
64
|
+
base_target = dirpath
|
|
65
|
+
if target_parts and target_parts[0]:
|
|
66
|
+
base_target = base_target.joinpath(*target_parts)
|
|
67
|
+
|
|
68
|
+
candidate = base_target.with_suffix(".py")
|
|
69
|
+
if candidate.is_file():
|
|
70
|
+
return candidate.relative_to(root)
|
|
71
|
+
|
|
72
|
+
# Check folder __init__.py
|
|
73
|
+
candidate_init = base_target / "__init__.py"
|
|
74
|
+
if candidate_init.is_file():
|
|
75
|
+
return candidate_init.relative_to(root)
|
|
76
|
+
|
|
77
|
+
# Handle absolute imports relative to project root e.g., 'brain.todo_engine'
|
|
78
|
+
parts = import_str.split(".")
|
|
79
|
+
candidate = root.joinpath(*parts).with_suffix(".py")
|
|
80
|
+
if candidate.is_file():
|
|
81
|
+
return candidate.relative_to(root)
|
|
82
|
+
|
|
83
|
+
candidate_init = root.joinpath(*parts) / "__init__.py"
|
|
84
|
+
if candidate_init.is_file():
|
|
85
|
+
return candidate_init.relative_to(root)
|
|
86
|
+
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# In-memory mtime cache to avoid re-reading and re-parsing unchanged files
|
|
91
|
+
# Key: canonical file path, Value: (mtime, list_of_raw_imports)
|
|
92
|
+
_FILE_PARSE_CACHE: dict[str, tuple[float, list[str]]] = {}
|
|
93
|
+
|
|
94
|
+
# Graph-level cache: key is root string, value is (cache_timestamp, dependencies, dependents)
|
|
95
|
+
_ROOT_GRAPH_CACHE: dict[str, tuple[float, dict[str, list[str]], dict[str, list[str]]]] = {}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_graph(root: Path, force_refresh: bool = False) -> tuple[dict[str, list[str]], dict[str, list[str]]]:
|
|
99
|
+
"""Scan root files and build adjacency lists: dependencies (outgoing) and dependents (incoming).
|
|
100
|
+
|
|
101
|
+
Uses mtime-based in-memory caching to eliminate disk reads and regex overhead on unchanged files.
|
|
102
|
+
"""
|
|
103
|
+
dependencies: dict[str, list[str]] = {}
|
|
104
|
+
dependents: dict[str, list[str]] = {}
|
|
105
|
+
|
|
106
|
+
js_import_re = re.compile(r'\b(?:import\s+.*\s+from\s+[\'"]([^\'"]+)[\'"]|require\([\'"]([^\'"]+)[\'"]\))')
|
|
107
|
+
py_import_re = re.compile(r'\b(?:import\s+([\w\.]+)|from\s+([\w\.]+)\s+import)')
|
|
108
|
+
|
|
109
|
+
all_files: list[tuple[Path, str, str, float]] = []
|
|
110
|
+
|
|
111
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
112
|
+
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS and not d.startswith(".")]
|
|
113
|
+
|
|
114
|
+
for name in filenames:
|
|
115
|
+
file_path = Path(dirpath) / name
|
|
116
|
+
suffix = file_path.suffix
|
|
117
|
+
if suffix not in (".py", ".ts", ".tsx", ".js", ".jsx"):
|
|
118
|
+
continue
|
|
119
|
+
rel = str(file_path.relative_to(root))
|
|
120
|
+
try:
|
|
121
|
+
mtime = file_path.stat().st_mtime
|
|
122
|
+
except OSError:
|
|
123
|
+
continue
|
|
124
|
+
all_files.append((file_path, rel, suffix, mtime))
|
|
125
|
+
dependencies[rel] = []
|
|
126
|
+
dependents[rel] = []
|
|
127
|
+
|
|
128
|
+
# Parse each file using mtime cache
|
|
129
|
+
for file_path, rel, suffix, mtime in all_files:
|
|
130
|
+
canonical_key = str(file_path.resolve())
|
|
131
|
+
cached_entry = _FILE_PARSE_CACHE.get(canonical_key)
|
|
132
|
+
|
|
133
|
+
if cached_entry is not None and cached_entry[0] == mtime and not force_refresh:
|
|
134
|
+
raw_imports = cached_entry[1]
|
|
135
|
+
else:
|
|
136
|
+
try:
|
|
137
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
raw_imports = []
|
|
142
|
+
if suffix in (".js", ".jsx", ".ts", ".tsx"):
|
|
143
|
+
for match in js_import_re.finditer(content):
|
|
144
|
+
imp = match.group(1) or match.group(2)
|
|
145
|
+
if imp:
|
|
146
|
+
raw_imports.append(imp)
|
|
147
|
+
elif suffix == ".py":
|
|
148
|
+
for match in py_import_re.finditer(content):
|
|
149
|
+
imp = match.group(1) or match.group(2)
|
|
150
|
+
if imp:
|
|
151
|
+
raw_imports.append(imp)
|
|
152
|
+
|
|
153
|
+
_FILE_PARSE_CACHE[canonical_key] = (mtime, raw_imports)
|
|
154
|
+
|
|
155
|
+
# Resolve imports to relative paths
|
|
156
|
+
targets: list[str] = []
|
|
157
|
+
if suffix in (".js", ".jsx", ".ts", ".tsx"):
|
|
158
|
+
for imp in raw_imports:
|
|
159
|
+
resolved = _resolve_js_ts_import(file_path, imp, root)
|
|
160
|
+
if resolved:
|
|
161
|
+
targets.append(str(resolved))
|
|
162
|
+
elif suffix == ".py":
|
|
163
|
+
for imp in raw_imports:
|
|
164
|
+
resolved = _resolve_python_import(file_path, imp, root)
|
|
165
|
+
if resolved:
|
|
166
|
+
targets.append(str(resolved))
|
|
167
|
+
|
|
168
|
+
# Save unique edges
|
|
169
|
+
for t in set(targets):
|
|
170
|
+
if t != rel and t in dependencies: # ensure it exists in project
|
|
171
|
+
dependencies[rel].append(t)
|
|
172
|
+
if rel not in dependents[t]:
|
|
173
|
+
dependents[t].append(rel)
|
|
174
|
+
|
|
175
|
+
return dependencies, dependents
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@mcp.tool(
|
|
179
|
+
name="build_dependency_graph",
|
|
180
|
+
description="Scan files and construct a complete mapping of dependencies and references in the project.",
|
|
181
|
+
)
|
|
182
|
+
def build_dependency_graph(path: str = ".") -> str:
|
|
183
|
+
"""Scan codebase imports and return dependency graph JSON."""
|
|
184
|
+
root = validate_path(path)
|
|
185
|
+
deps, deps_in = _get_graph(root)
|
|
186
|
+
|
|
187
|
+
total_edges = sum(len(v) for v in deps.values())
|
|
188
|
+
summary = {
|
|
189
|
+
"total_files": len(deps),
|
|
190
|
+
"total_connections": total_edges,
|
|
191
|
+
"graph": deps
|
|
192
|
+
}
|
|
193
|
+
return json.dumps(summary, indent=2)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@mcp.tool(
|
|
197
|
+
name="impact_analysis",
|
|
198
|
+
description="Calculate direct and transitive downstream files affected if a given file is modified.",
|
|
199
|
+
)
|
|
200
|
+
def impact_analysis(file_path: str, path: str = ".") -> str:
|
|
201
|
+
"""Find all files that depend on the target file, directly or transitively (BFS traversal)."""
|
|
202
|
+
root = validate_path(path)
|
|
203
|
+
target_path = Path(file_path)
|
|
204
|
+
|
|
205
|
+
# Try to resolve relative to root or as is
|
|
206
|
+
resolved_target = target_path
|
|
207
|
+
if not resolved_target.is_file():
|
|
208
|
+
resolved_target = root / target_path
|
|
209
|
+
|
|
210
|
+
if not resolved_target.is_file():
|
|
211
|
+
return f"File '{file_path}' not found."
|
|
212
|
+
|
|
213
|
+
rel_target = str(resolved_target.relative_to(root))
|
|
214
|
+
|
|
215
|
+
dependencies, dependents = _get_graph(root)
|
|
216
|
+
|
|
217
|
+
if rel_target not in dependents:
|
|
218
|
+
return f"File '{rel_target}' is not tracked in the dependency graph (or has no suffix dependencies)."
|
|
219
|
+
|
|
220
|
+
# Traverse using BFS to find direct and transitive dependents
|
|
221
|
+
queue = [rel_target]
|
|
222
|
+
visited = set()
|
|
223
|
+
direct = dependents[rel_target]
|
|
224
|
+
transitive = []
|
|
225
|
+
|
|
226
|
+
while queue:
|
|
227
|
+
current = queue.pop(0)
|
|
228
|
+
for parent in dependents.get(current, []):
|
|
229
|
+
if parent not in visited and parent != rel_target:
|
|
230
|
+
visited.add(parent)
|
|
231
|
+
queue.append(parent)
|
|
232
|
+
if parent not in direct:
|
|
233
|
+
transitive.append(parent)
|
|
234
|
+
|
|
235
|
+
total_affected = len(direct) + len(transitive)
|
|
236
|
+
|
|
237
|
+
# Assess Blast Radius Risk Level
|
|
238
|
+
if total_affected == 0:
|
|
239
|
+
risk_badge = "๐ข LOW RISK (Isolated)"
|
|
240
|
+
elif total_affected <= 3:
|
|
241
|
+
risk_badge = "๐ก MEDIUM RISK (Manageable)"
|
|
242
|
+
else:
|
|
243
|
+
risk_badge = "๐ด HIGH RISK (Critical Surface Area)"
|
|
244
|
+
|
|
245
|
+
# Identify affected test files vs source files
|
|
246
|
+
all_affected = sorted(list(set(direct + transitive)))
|
|
247
|
+
test_files = [f for f in all_affected if "test" in f.lower() or "spec" in f.lower()]
|
|
248
|
+
source_files = [f for f in all_affected if f not in test_files]
|
|
249
|
+
|
|
250
|
+
report = [
|
|
251
|
+
f"# ๐จ Downstream Impact Analysis: `{rel_target}`",
|
|
252
|
+
f"**Blast Radius Assessment:** {risk_badge}",
|
|
253
|
+
f"- **Direct Dependents (1st level):** {len(direct)} files",
|
|
254
|
+
f"- **Transitive Dependents (recursive):** {len(transitive)} files",
|
|
255
|
+
f"- **Total Affected Files:** {total_affected} files",
|
|
256
|
+
""
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
if direct:
|
|
260
|
+
report.append("### ๐ Direct Impact (Must update immediately when modifying exports)")
|
|
261
|
+
for d in sorted(direct):
|
|
262
|
+
icon = "๐งช" if d in test_files else "๐"
|
|
263
|
+
report.append(f"- {icon} `{d}`")
|
|
264
|
+
report.append("")
|
|
265
|
+
|
|
266
|
+
if transitive:
|
|
267
|
+
report.append("### ๐ช๏ธ Transitive Impact (Verify against regressions)")
|
|
268
|
+
for t in sorted(transitive):
|
|
269
|
+
icon = "๐งช" if t in test_files else "๐"
|
|
270
|
+
report.append(f"- {icon} `{t}`")
|
|
271
|
+
report.append("")
|
|
272
|
+
|
|
273
|
+
if test_files:
|
|
274
|
+
report.append("### ๐งช Affected Test Suites")
|
|
275
|
+
report.append("Run these test files to verify refactoring integrity:")
|
|
276
|
+
for tf in sorted(test_files):
|
|
277
|
+
report.append(f"- `{tf}`")
|
|
278
|
+
report.append("")
|
|
279
|
+
|
|
280
|
+
# Blast-Radius-First Refactoring Playbook
|
|
281
|
+
if total_affected > 0:
|
|
282
|
+
report.append("### ๐ Blast-Radius-First Refactoring Playbook")
|
|
283
|
+
report.append("1. **Contract Lock:** Review existing exports/signatures in `" + rel_target + "`.")
|
|
284
|
+
report.append("2. **Core Refactor:** Implement changes in `" + rel_target + "`.")
|
|
285
|
+
report.append(f"3. **Adapt Direct Callers:** Update the {len(direct)} direct dependent files.")
|
|
286
|
+
if test_files:
|
|
287
|
+
report.append(f"4. **Regression Run:** Execute tests on: " + ", ".join([f"`{t}`" for t in test_files[:4]]) + ("..." if len(test_files) > 4 else ""))
|
|
288
|
+
report.append("5. **Verify Transitive:** Validate that no downstream runtime contracts were broken.")
|
|
289
|
+
else:
|
|
290
|
+
report.append("โ
**Isolation verified:** This file has no downstream dependents. It is safe to refactor without breaking other project modules.")
|
|
291
|
+
|
|
292
|
+
return "\n".join(report)
|
brain/guardian.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Architecture Guardian for Project Brain.
|
|
3
|
+
|
|
4
|
+
Validates project files against allowed and forbidden technology stacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from app import mcp, load_context as get_context
|
|
13
|
+
from security import validate_path
|
|
14
|
+
|
|
15
|
+
# Standard directories to skip when scanning
|
|
16
|
+
IGNORE_DIRS = {
|
|
17
|
+
".git",
|
|
18
|
+
"node_modules",
|
|
19
|
+
".venv",
|
|
20
|
+
"venv",
|
|
21
|
+
"__pycache__",
|
|
22
|
+
"dist",
|
|
23
|
+
"build",
|
|
24
|
+
".project_brain",
|
|
25
|
+
".agents",
|
|
26
|
+
"brain",
|
|
27
|
+
"tools",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Standard file extensions to scan
|
|
31
|
+
SCAN_EXTENSIONS = {
|
|
32
|
+
".py",
|
|
33
|
+
".ts",
|
|
34
|
+
".tsx",
|
|
35
|
+
".js",
|
|
36
|
+
".jsx",
|
|
37
|
+
".go",
|
|
38
|
+
".rs",
|
|
39
|
+
".java",
|
|
40
|
+
".cs",
|
|
41
|
+
".cpp",
|
|
42
|
+
".h",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool(
|
|
47
|
+
name="validate_architecture",
|
|
48
|
+
description="Scan the codebase and validate imports/technologies against the project's architectural guidelines and forbidden tools.",
|
|
49
|
+
)
|
|
50
|
+
def validate_architecture(path: str = ".") -> str:
|
|
51
|
+
"""Validate imports in codebase files against forbidden and required lists in project context."""
|
|
52
|
+
root = validate_path(path)
|
|
53
|
+
ctx = get_context(root)
|
|
54
|
+
|
|
55
|
+
rules = ctx.data.get("architecture", {}).get("rules", {})
|
|
56
|
+
forbidden = rules.get("forbidden_technologies", [])
|
|
57
|
+
required = rules.get("required_technologies", [])
|
|
58
|
+
|
|
59
|
+
if not forbidden and not required:
|
|
60
|
+
# Provide sensible defaults if none are defined
|
|
61
|
+
forbidden = ["redux", "firebase"]
|
|
62
|
+
required = []
|
|
63
|
+
|
|
64
|
+
violations = []
|
|
65
|
+
scanned_files_count = 0
|
|
66
|
+
|
|
67
|
+
# Compile regexes for each forbidden tech
|
|
68
|
+
forbidden_patterns = {}
|
|
69
|
+
for tech in forbidden:
|
|
70
|
+
# Match imports: import * from 'tech', require('tech'), import tech, from tech import
|
|
71
|
+
js_import = re.compile(r'\b(?:import\s+.*\s+from\s+[\'"]' + re.escape(tech) + r'[\'"]|require\([\'"]' + re.escape(tech) + r'[\'"]\))', re.IGNORECASE)
|
|
72
|
+
py_import = re.compile(r'\b(?:import\s+' + re.escape(tech) + r'\b|from\s+' + re.escape(tech) + r'\s+import)', re.IGNORECASE)
|
|
73
|
+
forbidden_patterns[tech] = (js_import, py_import)
|
|
74
|
+
|
|
75
|
+
# Traverse directories
|
|
76
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
77
|
+
# Exclude directories in-place
|
|
78
|
+
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS and not d.startswith(".")]
|
|
79
|
+
|
|
80
|
+
for name in filenames:
|
|
81
|
+
file_path = Path(dirpath) / name
|
|
82
|
+
if file_path.suffix not in SCAN_EXTENSIONS:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
scanned_files_count += 1
|
|
86
|
+
rel_path = file_path.relative_to(root)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
content_lines = file_path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
90
|
+
for line_idx, line in enumerate(content_lines, 1):
|
|
91
|
+
# Check for forbidden imports
|
|
92
|
+
for tech, (js_pat, py_pat) in forbidden_patterns.items():
|
|
93
|
+
if js_pat.search(line) or py_pat.search(line):
|
|
94
|
+
violations.append({
|
|
95
|
+
"file": str(rel_path),
|
|
96
|
+
"line": line_idx,
|
|
97
|
+
"tech": tech,
|
|
98
|
+
"content": line.strip()
|
|
99
|
+
})
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Compile report
|
|
104
|
+
report = [
|
|
105
|
+
"# ๐ก๏ธ Architecture Guardian Report",
|
|
106
|
+
f"**Scanned Files:** {scanned_files_count}",
|
|
107
|
+
f"**Forbidden Technologies Checked:** {', '.join(forbidden) if forbidden else 'None'}",
|
|
108
|
+
""
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
if violations:
|
|
112
|
+
report.append("## ๐จ Violations Detected!")
|
|
113
|
+
report.append("The following files imported or referenced forbidden libraries:")
|
|
114
|
+
report.append("")
|
|
115
|
+
for v in violations:
|
|
116
|
+
report.append(f"- **{v['file']}** (Line {v['line']}): Found `{v['tech']}`")
|
|
117
|
+
report.append(f" `{v['content']}`")
|
|
118
|
+
report.append("")
|
|
119
|
+
report.append("โ ๏ธ **Recommendation:** Replace forbidden imports with the approved stack (e.g. use Supabase instead of Firebase, or Zustand/Recoil instead of Redux).")
|
|
120
|
+
else:
|
|
121
|
+
report.append("## โ
Verification Passed")
|
|
122
|
+
report.append("No architectural guidelines violations were found. All imports align with rules.")
|
|
123
|
+
|
|
124
|
+
return "\n".join(report)
|