Skip to content

Commit 8a53583

Browse files
committed
feat(v3): launch self-healing CI and deep architecture mapping systems
1 parent e39ce97 commit 8a53583

4 files changed

Lines changed: 280 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,34 @@ jobs:
5050
run: |
5151
pytest --cov=devguardian --cov-report term-missing
5252
53+
repair:
54+
name: 🩹 Self-Healing Repair
55+
runs-on: ubuntu-latest
56+
# Run only if lint/test fails AND we are on the main branch
57+
if: failure() && github.ref == 'refs/heads/main'
58+
needs: [lint, test]
59+
steps:
60+
- uses: actions/checkout@v4
61+
with:
62+
fetch-depth: 0 # needed for push
63+
token: ${{ secrets.GITHUB_TOKEN }}
64+
- name: Set up Python 3.10
65+
uses: actions/setup-python@v5
66+
with:
67+
python-version: "3.10"
68+
- name: Install dependencies
69+
run: |
70+
python -m pip install --upgrade pip
71+
pip install uv
72+
uv pip install --system -e .
73+
- name: Run AI Repair
74+
env:
75+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
76+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77+
run: |
78+
uv pip install --system pytest ruff pytest-cov langchain-google-genai langgraph
79+
python devguardian/tools/self_healing.py
80+
5381
build:
5482
runs-on: ubuntu-latest
5583
needs: [lint, test]

devguardian/server.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ async def _run_sync(func, *args, **kwargs):
3737
from devguardian.tools.tdd import test_and_fix
3838
from devguardian.tools.github_review import review_pull_request
3939
from devguardian.tools.infra import dockerize, generate_ci, generate_gitignore
40+
from devguardian.tools.architect import generate_architecture_map, generate_technical_docs
4041
from devguardian.tools.mass_refactor import mass_refactor
4142
from devguardian.tools.git_ops import (
4243
git_status,
@@ -273,11 +274,36 @@ async def list_tools() -> list[types.Tool]:
273274
"type": "object",
274275
"properties": {
275276
"project_path": {"type": "string"},
276-
"instruction": {"type": "string", "description": "What to change across the whole codebase."},
277+
"instruction": {
278+
"type": "string",
279+
"description": "What to change across the whole codebase.",
280+
},
277281
},
278282
"required": ["project_path", "instruction"],
279283
},
280284
),
285+
types.Tool(
286+
name="generate_architecture_map",
287+
description="Generates a Mermaid.js diagram of the project's internal dependencies.",
288+
inputSchema={
289+
"type": "object",
290+
"properties": {
291+
"project_path": {"type": "string"},
292+
},
293+
"required": ["project_path"],
294+
},
295+
),
296+
types.Tool(
297+
name="generate_technical_docs",
298+
description="Generates a high-density, professional architecture summary of the project.",
299+
inputSchema={
300+
"type": "object",
301+
"properties": {
302+
"project_path": {"type": "string"},
303+
},
304+
"required": ["project_path"],
305+
},
306+
),
281307
# ── Git ───────────────────────────────────────────────────────────────
282308
types.Tool(
283309
name="git_status",
@@ -477,6 +503,10 @@ def text(result) -> list[types.TextContent]:
477503
return text(await _run_sync(generate_gitignore, **arguments))
478504
elif name == "mass_refactor":
479505
return text(await _run_sync(mass_refactor, **arguments))
506+
elif name == "generate_architecture_map":
507+
return text(await _run_sync(generate_architecture_map, **arguments))
508+
elif name == "generate_technical_docs":
509+
return text(await _run_sync(generate_technical_docs, **arguments))
480510

481511
# ── Git ───────────────────────────────────────────────────────────────
482512
elif name == "git_status":

devguardian/tools/architect.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""
2+
DevGuardian Architect — Structural Awareness & Diagramming
3+
==========================================================
4+
Analyzes project imports and file structure to generate:
5+
1. Mermaid.js Flowcharts
6+
2. Architecture Summaries
7+
"""
8+
9+
import os
10+
import re
11+
from pathlib import Path
12+
from devguardian.utils.file_reader import list_project_files
13+
14+
def _extract_internal_imports(file_path: Path, project_root: Path) -> list[str]:
15+
"""Finds all internal imports (e.g., from devguardian.utils...) in a file."""
16+
try:
17+
content = file_path.read_text(encoding="utf-8", errors="replace")
18+
except:
19+
return []
20+
21+
# Matches: from devguardian.utils... import ... OR import devguardian...
22+
patterns = [
23+
r"^\s*from\s+(devguardian[\w.]+)\s+import",
24+
r"^\s*import\s+(devguardian[\w.]+)",
25+
]
26+
27+
internal_imports = []
28+
for pat in patterns:
29+
for match in re.findall(pat, content, re.MULTILINE):
30+
# Clean up: e.g. devguardian.utils.memory -> devguardian/utils/memory.py (abstractly)
31+
internal_imports.append(match)
32+
33+
return list(set(internal_imports))
34+
35+
def generate_architecture_map(project_path: str) -> str:
36+
"""
37+
Generates a Mermaid.js diagram representing the internal dependencies.
38+
"""
39+
root = Path(project_path)
40+
all_files = list_project_files(project_path, max_files=200)
41+
42+
# Map nodes: file_path -> simple_name
43+
# Example: devguardian/tools/infra.py -> tools.infra
44+
nodes = {}
45+
edges = []
46+
47+
for f in all_files:
48+
fp = Path(f)
49+
rel_path = fp.relative_to(root)
50+
51+
# We only care about .py files for the dependency map
52+
if fp.suffix != ".py" or "__pycache__" in str(rel_path):
53+
continue
54+
55+
# Clean name for node: devguardian/utils/security.py -> utils.security
56+
node_id = str(rel_path).replace(os.sep, ".").replace(".py", "")
57+
if node_id.startswith("devguardian."):
58+
node_id = node_id.replace("devguardian.", "", 1)
59+
60+
nodes[node_id] = node_id
61+
62+
# Find who this file imports
63+
imports = _extract_internal_imports(fp, root)
64+
for imp in imports:
65+
# devguardian.utils.security -> utils.security
66+
target_id = imp.replace("devguardian.", "", 1) if imp.startswith("devguardian.") else imp
67+
edges.append((node_id, target_id))
68+
69+
# Build Mermaid syntax
70+
mermaid = ["graph TD", " subgraph DevGuardian_Core"]
71+
72+
# Deduplicate edges and filter to only existing internal nodes
73+
seen_edges = set()
74+
for source, target in edges:
75+
if source in nodes and target in nodes and source != target:
76+
edge = f" {source.replace('.', '_')} --> {target.replace('.', '_')}"
77+
if edge not in seen_edges:
78+
mermaid.append(edge)
79+
seen_edges.add(edge)
80+
81+
mermaid.append(" end")
82+
83+
# Add human-readable labels to make it look premium
84+
for nid in nodes:
85+
clean_id = nid.replace(".", "_")
86+
mermaid.append(f" {clean_id}[\"{nid}\"]")
87+
88+
return "\n".join(mermaid)
89+
90+
def generate_technical_docs(project_path: str) -> str:
91+
"""
92+
Generates a technical README snippet explaining the architecture.
93+
"""
94+
from devguardian.utils.gemini_client import ask_gemini
95+
from devguardian.utils.file_reader import build_project_context
96+
97+
ctx = build_project_context(project_path)
98+
prompt = (
99+
f"{ctx}\n\n"
100+
"Based on the project structure and context above, write a Deep Architecture Document. "
101+
"Explain: \n"
102+
"1. The Core Engine (How server.py orchestrates things)\n"
103+
"2. The Tool Matrix (What each tool in /tools does)\n"
104+
"3. The Agent Intelligence (How the swarm works)\n"
105+
"4. The Data Flow (How information moves from tools to agents)\n"
106+
"\nFormat in professional markdown with icons."
107+
)
108+
109+
system = "You are a lead software architect. Provide a high-density, professional architecture summary."
110+
return ask_gemini(prompt, system_instruction=system)

devguardian/tools/self_healing.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
DevGuardian Self-Healing CI — The Repair Engine
3+
================================================
4+
Automatically detects and fixes CI failures (Tests/Linting).
5+
When a CI run fails, this script:
6+
1. Identifies the failing file or test
7+
2. Calls the Swarm to fix it
8+
3. Verifies the fix locally
9+
4. Pushes the repair commit back to GitHub
10+
"""
11+
12+
import os
13+
import subprocess
14+
from pathlib import Path
15+
from devguardian.agents.swarm import run_swarm
16+
17+
def run_git_command(args: list[str], cwd: str) -> str:
18+
"""Helper to run git commands in a subprocess."""
19+
try:
20+
res = subprocess.run(
21+
["git"] + args,
22+
cwd=cwd,
23+
capture_output=True,
24+
text=True,
25+
encoding="utf-8",
26+
errors="replace",
27+
)
28+
return res.stdout.strip()
29+
except Exception as e:
30+
return f"Git error: {e}"
31+
32+
def detect_failure_context() -> dict:
33+
"""
34+
Tries to find which file recently failed by running ruff and pytest locally.
35+
In CI, it can also parse logs (if available).
36+
"""
37+
context = {"failed_files": [], "errors": ""}
38+
39+
# Check linting first
40+
lint_res = subprocess.run(["uv", "run", "ruff", "check", "."], capture_output=True, text=True)
41+
if lint_res.returncode != 0:
42+
context["type"] = "lint"
43+
context["errors"] += lint_res.stdout
44+
# Extract files from ruff output (best effort)
45+
files = re.findall(r"(?:[\w./\\]+):[\d:]+", lint_res.stdout)
46+
context["failed_files"].extend([f.split(":")[0] for f in files])
47+
48+
# Check tests
49+
test_res = subprocess.run(["uv", "run", "pytest", "-v", "--tb=short"], capture_output=True, text=True)
50+
if test_res.returncode != 0:
51+
context["type"] = "test"
52+
context["errors"] += test_res.stdout
53+
# Extract failing test files from pytest output (simplified)
54+
test_files = re.findall(r"FAILED\s+([\w./\\]+)::", test_res.stdout)
55+
context["failed_files"].extend(test_files)
56+
57+
return context
58+
59+
async def repair_ci_failure(project_path: str):
60+
"""Entry point for the self-healing process."""
61+
print("🩹 Starting DevGuardian Self-Healing Process...")
62+
63+
root = Path(project_path)
64+
failure = detect_failure_context()
65+
66+
if not failure["failed_files"]:
67+
print("✅ No local failures detected. All checks passed!")
68+
return
69+
70+
for target_file in set(failure["failed_files"]):
71+
target_abs = root / target_file
72+
if not target_abs.exists():
73+
continue
74+
75+
print(f"🛠️ Attempting to repair: {target_file}")
76+
77+
# Task for the swarm:
78+
task_desc = (
79+
f"Fix the following error in `{target_file}`:\n\n"
80+
f"```\n{failure['errors'][:2000]}\n```\n\n"
81+
"Ensure the file follows project style and passes architecture rules."
82+
)
83+
84+
# Use our elite Swarm v3
85+
report = await run_swarm(task_desc, project_path)
86+
87+
# Extract final code (best effort from the report markdown)
88+
if "## Final Code" in report:
89+
new_code = report.split("## Final Code")[1].split("## ")[0].strip()
90+
# Clean up potential markdown fences
91+
new_code = "\n".join([l for l in new_code.splitlines() if not l.strip().startswith("```")]).strip()
92+
93+
# overwrite the file
94+
target_abs.write_text(new_code, encoding="utf-8")
95+
print(f"✅ Successfully updated {target_file}")
96+
97+
# Final wrap-up: Commit fix if in CI environment
98+
if os.getenv("GITHUB_ACTIONS") == "true":
99+
print("🚀 In GitHub environment — pushing repair commit...")
100+
run_git_command(["config", "user.name", "devguardian-bot"], project_path)
101+
run_git_command(["config", "user.email", "bot@devguardian.ai"], project_path)
102+
run_git_command(["add", "."], project_path)
103+
run_git_command(["commit", "-m", "🩹 chore(ci): auto-repair lint/test failures via DevGuardian"], project_path)
104+
run_git_command(["push", "origin", os.getenv("GITHUB_REF_NAME", "main")], project_path)
105+
else:
106+
print("💡 Local run complete. Review the changes in your working tree.")
107+
108+
if __name__ == "__main__":
109+
import asyncio
110+
import re
111+
asyncio.run(repair_ci_failure(os.getcwd()))

0 commit comments

Comments
 (0)