mnemo-dev 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.
@@ -0,0 +1,103 @@
1
+ """Code Health Score — track complexity hotspots, churn, and metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from ..config import IGNORE_DIRS
9
+
10
+
11
+ def _should_ignore(path: Path) -> bool:
12
+ return any(part in IGNORE_DIRS for part in path.parts)
13
+
14
+
15
+ def _count_methods(content: str) -> int:
16
+ """Count methods in a C# file."""
17
+ return len(re.findall(r'(public|private|protected|internal)\s+(static\s+)?(async\s+)?\S+\s+\w+\s*\(', content))
18
+
19
+
20
+ def _estimate_complexity(content: str) -> int:
21
+ """Rough cyclomatic complexity estimate based on branching keywords."""
22
+ keywords = ["if ", "else ", "switch ", "case ", "for ", "foreach ", "while ", "catch ", "&&", "||", "??", "?.", "=>"]
23
+ return sum(content.count(k) for k in keywords)
24
+
25
+
26
+ def _get_git_churn(repo_root: Path, filepath: Path) -> int:
27
+ """Get number of commits that touched this file."""
28
+ try:
29
+ from git import Repo
30
+ repo = Repo(repo_root)
31
+ rel = str(filepath.relative_to(repo_root))
32
+ commits = list(repo.iter_commits(paths=rel, max_count=100))
33
+ return len(commits)
34
+ except Exception:
35
+ return 0
36
+
37
+
38
+ def calculate_health(repo_root: Path) -> str:
39
+ """Calculate code health metrics for the project."""
40
+ lines = ["# Code Health Report\n"]
41
+
42
+ hotspots: list[tuple[str, int, int, int]] = [] # (file, complexity, methods, lines)
43
+
44
+ for cs_file in repo_root.rglob("*.cs"):
45
+ if _should_ignore(cs_file) or "Test" in str(cs_file):
46
+ continue
47
+ try:
48
+ content = cs_file.read_text(errors="replace")
49
+ except (OSError, PermissionError):
50
+ continue
51
+
52
+ if len(content) < 100:
53
+ continue
54
+
55
+ rel = str(cs_file.relative_to(repo_root))
56
+ complexity = _estimate_complexity(content)
57
+ methods = _count_methods(content)
58
+ line_count = content.count("\n")
59
+
60
+ hotspots.append((rel, complexity, methods, line_count))
61
+
62
+ if not hotspots:
63
+ return "No source files found for health analysis."
64
+
65
+ # Sort by complexity (highest first)
66
+ hotspots.sort(key=lambda x: -x[1])
67
+
68
+ # Overall stats
69
+ total_files = len(hotspots)
70
+ total_lines = sum(h[3] for h in hotspots)
71
+ total_methods = sum(h[2] for h in hotspots)
72
+ avg_complexity = sum(h[1] for h in hotspots) / total_files
73
+
74
+ lines.append("## Summary")
75
+ lines.append(f"- **Files:** {total_files}")
76
+ lines.append(f"- **Total lines:** {total_lines:,}")
77
+ lines.append(f"- **Total methods:** {total_methods}")
78
+ lines.append(f"- **Avg complexity/file:** {avg_complexity:.1f}")
79
+ lines.append("")
80
+
81
+ # Complexity hotspots
82
+ lines.append("## Complexity Hotspots (top 10)")
83
+ lines.append("Files with highest branching complexity — candidates for refactoring:")
84
+ lines.append("")
85
+ for rel, complexity, methods, line_count in hotspots[:10]:
86
+ lines.append(f"- **{rel}** — complexity: {complexity}, methods: {methods}, lines: {line_count}")
87
+ lines.append("")
88
+
89
+ # Large files
90
+ large = sorted(hotspots, key=lambda x: -x[3])[:10]
91
+ lines.append("## Largest Files (top 10)")
92
+ for rel, complexity, methods, line_count in large:
93
+ lines.append(f"- **{rel}** — {line_count} lines, {methods} methods")
94
+ lines.append("")
95
+
96
+ # Files with many methods (god classes)
97
+ many_methods = sorted(hotspots, key=lambda x: -x[2])[:5]
98
+ lines.append("## Potential God Classes (most methods)")
99
+ for rel, complexity, methods, line_count in many_methods:
100
+ if methods > 10:
101
+ lines.append(f"- **{rel}** — {methods} methods")
102
+
103
+ return "\n".join(lines)
@@ -0,0 +1,90 @@
1
+ """Incident memory - store production incidents, root cause, fix, and prevention."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from ..storage import Collections, get_storage
9
+
10
+
11
+ def _load_incidents(repo_root: Path) -> list[dict]:
12
+ data = get_storage(repo_root).read_collection(Collections.INCIDENTS)
13
+ return data if isinstance(data, list) else []
14
+
15
+
16
+ def _save_incidents(repo_root: Path, incidents: list[dict]) -> None:
17
+ get_storage(repo_root).write_collection(Collections.INCIDENTS, incidents)
18
+
19
+
20
+ def add_incident(
21
+ repo_root: Path,
22
+ title: str,
23
+ what_happened: str,
24
+ root_cause: str,
25
+ fix: str,
26
+ prevention: str = "",
27
+ severity: str = "medium",
28
+ services: list[str] | None = None,
29
+ ) -> dict:
30
+ """Store a production incident."""
31
+ incidents = _load_incidents(repo_root)
32
+ entry = {
33
+ "id": len(incidents) + 1,
34
+ "timestamp": time.time(),
35
+ "title": title,
36
+ "what_happened": what_happened,
37
+ "root_cause": root_cause,
38
+ "fix": fix,
39
+ "prevention": prevention,
40
+ "severity": severity,
41
+ "services": services or [],
42
+ }
43
+ incidents.append(entry)
44
+ _save_incidents(repo_root, incidents)
45
+ return entry
46
+
47
+
48
+ def search_incidents(repo_root: Path, query: str) -> str:
49
+ """Search incident history."""
50
+ incidents = _load_incidents(repo_root)
51
+ query_lower = query.lower()
52
+
53
+ matches = [
54
+ incident
55
+ for incident in incidents
56
+ if query_lower in incident.get("title", "").lower()
57
+ or query_lower in incident.get("root_cause", "").lower()
58
+ or query_lower in incident.get("what_happened", "").lower()
59
+ or any(query_lower in service.lower() for service in incident.get("services", []))
60
+ ]
61
+
62
+ if not matches:
63
+ return f"No incidents matching '{query}'."
64
+
65
+ lines = [f"# Incidents matching '{query}'\n"]
66
+ for incident in matches:
67
+ severity = f"[{incident['severity']}]" if incident.get("severity") else ""
68
+ lines.append(f"## {incident['title']} {severity}")
69
+ lines.append(f"**What happened:** {incident['what_happened']}")
70
+ lines.append(f"**Root cause:** {incident['root_cause']}")
71
+ lines.append(f"**Fix:** {incident['fix']}")
72
+ if incident.get("prevention"):
73
+ lines.append(f"**Prevention:** {incident['prevention']}")
74
+ if incident.get("services"):
75
+ lines.append(f"**Services affected:** {', '.join(incident['services'])}")
76
+ lines.append("")
77
+ return "\n".join(lines)
78
+
79
+
80
+ def format_incidents(repo_root: Path) -> str:
81
+ """Format all incidents as markdown."""
82
+ incidents = _load_incidents(repo_root)
83
+ if not incidents:
84
+ return "No incidents recorded."
85
+
86
+ lines = ["# Incident History\n"]
87
+ for incident in incidents:
88
+ severity = f"[{incident['severity']}]" if incident.get("severity") else ""
89
+ lines.append(f"- {severity} **{incident['title']}** - {incident['root_cause'][:80]}")
90
+ return "\n".join(lines)
mnemo/init.py ADDED
@@ -0,0 +1,167 @@
1
+ """Initialize .mnemo in a repository and configure MCP clients."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .clients import (
6
+ CLIENTS,
7
+ DEFAULT_CLIENT,
8
+ ClientTarget,
9
+ context_path,
10
+ resolve_clients,
11
+ setup_mcp_config,
12
+ )
13
+ from .config import mnemo_path
14
+ from .memory import save_context
15
+ from .repo_map import save_repo_map
16
+
17
+
18
+ MNEMO_RULE_HEADER = """\
19
+ You have access to Mnemo - a persistent memory system for this project.
20
+ All project context, decisions, and chat history is below. Use it to answer questions without re-reading files.
21
+
22
+ AT THE START OF EVERY CHAT:
23
+ - Call `mnemo_recall` to get the latest context. The embedded context below may be stale.
24
+
25
+ ANSWERING QUESTIONS:
26
+ - If the recalled memory already contains the answer, USE IT DIRECTLY. Do not re-read files or re-run lookups for information already in memory.
27
+ - Only call `mnemo_lookup` or read files when memory does not have enough detail to answer.
28
+
29
+ SAVING MEMORY:
30
+ - Call `mnemo_remember` AFTER any of these happen in the conversation:
31
+ - You made a code change that affects behavior (theme change, config change, new feature, refactor)
32
+ - A bug was found and fixed
33
+ - A design or architecture decision was made
34
+ - The user stated a preference or convention
35
+ - A TODO or follow-up was identified
36
+ - You learned something non-obvious about the codebase
37
+ - Call `mnemo_remember` when the context window is getting long to summarize progress so far.
38
+ - Call `mnemo_remember` when the user explicitly asks to remember something.
39
+ - Do NOT save trivial things like "read a file" or "answered a question with no new insight".
40
+ - When in doubt, SAVE. It is better to remember too much than to forget something useful.
41
+ - RULE: If you called `mnemo_lookup`, `mnemo_similar`, or `mnemo_who_touched` AND produced a summary or analysis from the results, you MUST call `mnemo_remember` with a concise summary before ending your response.
42
+
43
+ AVAILABLE TOOLS:
44
+ - `mnemo_lookup` - get method-level details for a file or folder
45
+ - `mnemo_similar` - find similar implementations to follow as patterns
46
+ - `mnemo_intelligence` - architecture graph, patterns, dependencies
47
+ - `mnemo_discover_apis` - discover all API endpoints
48
+ - `mnemo_search_api` - search for a specific endpoint
49
+ - `mnemo_knowledge` - search team knowledge base
50
+ - `mnemo_decide` - record a decision
51
+ - `mnemo_context` - save project metadata
52
+ - `mnemo_map` - refresh code map after changes
53
+ - `mnemo_cross_search` - search across ALL linked repos (use when code might live in a sibling service)
54
+ - `mnemo_cross_impact` - cross-repo impact analysis (what breaks in other repos if you change something here)
55
+ - `mnemo_links` - show linked repos
56
+
57
+ CROSS-REPO AWARENESS:
58
+ - This repo has linked sibling repos. Use `mnemo_links` to see them.
59
+ - ALWAYS call `mnemo_cross_search` BEFORE using grep or reading files when:
60
+ - The user asks about code that does not exist in this repo
61
+ - The user mentions a service, project, or module name that is not a folder in this repo
62
+ - `mnemo_lookup` or `mnemo_similar` returned no results
63
+ - If the user asks "what breaks if I change X", use `mnemo_cross_impact` for full cross-repo analysis.
64
+ - NEVER fall back to grep for code in other repos. Use `mnemo_cross_search` instead.
65
+
66
+ ---
67
+
68
+ """
69
+
70
+
71
+ def _build_rule_with_context(repo_root: Path) -> str:
72
+ """Build a client context file with embedded Mnemo context."""
73
+ from .memory import recall
74
+
75
+ context = recall(repo_root)
76
+ return MNEMO_RULE_HEADER + context if context else MNEMO_RULE_HEADER
77
+
78
+
79
+ def _install_context_file(repo_root: Path, target: ClientTarget) -> Path | None:
80
+ """Install or refresh the repo-local context file for a client."""
81
+ path = context_path(repo_root, target)
82
+ if path is None:
83
+ return None
84
+ path.parent.mkdir(parents=True, exist_ok=True)
85
+ path.write_text(_build_rule_with_context(repo_root), encoding="utf-8")
86
+ return path
87
+
88
+
89
+ def refresh_context_files(repo_root: Path) -> None:
90
+ """Refresh all known Mnemo client context files that already exist."""
91
+ for target in CLIENTS.values():
92
+ path = context_path(repo_root, target)
93
+ if path and path.exists():
94
+ path.write_text(_build_rule_with_context(repo_root), encoding="utf-8")
95
+
96
+
97
+ def _ensure_gitignore(repo_root: Path) -> None:
98
+ """Ensure local Mnemo data is ignored by git."""
99
+ gitignore = repo_root / ".gitignore"
100
+ if gitignore.exists():
101
+ content = gitignore.read_text(encoding="utf-8")
102
+ if ".mnemo" not in content:
103
+ gitignore.write_text(content.rstrip() + "\n.mnemo/\n", encoding="utf-8")
104
+ else:
105
+ gitignore.write_text(".mnemo/\n", encoding="utf-8")
106
+
107
+
108
+ def init(repo_root: Path, client: str = DEFAULT_CLIENT) -> str:
109
+ """Create .mnemo/, generate repo map, install context files, and configure MCP."""
110
+ import os
111
+ os.environ["MNEMO_AUTO_INSTALL"] = "1"
112
+
113
+ targets = resolve_clients(client)
114
+ base = mnemo_path(repo_root)
115
+ base.mkdir(exist_ok=True)
116
+
117
+ save_repo_map(repo_root)
118
+
119
+ from .knowledge import init_knowledge
120
+
121
+ init_knowledge(repo_root)
122
+
123
+ from .intelligence import detect_patterns
124
+
125
+ patterns = detect_patterns(repo_root)
126
+ context_data = {
127
+ "repo_root": str(repo_root),
128
+ "initialized": True,
129
+ }
130
+ if patterns:
131
+ context_data["patterns"] = patterns
132
+ save_context(repo_root, context_data)
133
+
134
+ _ensure_gitignore(repo_root)
135
+
136
+ context_results: list[tuple[ClientTarget, Path | None]] = []
137
+ config_results: list[tuple[ClientTarget, bool]] = []
138
+ for target in targets:
139
+ context_results.append((target, _install_context_file(repo_root, target)))
140
+ config_results.append((target, setup_mcp_config(target)))
141
+
142
+ lines = [
143
+ "Mnemo initialized",
144
+ f"- .mnemo/ created at {base}",
145
+ "- Repo map generated",
146
+ "- Knowledge base directory ready",
147
+ ]
148
+
149
+ for target, path in context_results:
150
+ if path:
151
+ rel = path.relative_to(repo_root)
152
+ lines.append(f"- {target.display_name} {target.context_label} installed at {rel}")
153
+
154
+ for target, changed in config_results:
155
+ if target.mcp_config_path:
156
+ state = "configured" if changed else "already configured"
157
+ lines.append(f"- {target.display_name} MCP {state} at {target.mcp_config_path}")
158
+
159
+ if client == DEFAULT_CLIENT:
160
+ lines.append("")
161
+ lines.append("Amazon Q will now recall project memory at the start of every new chat.")
162
+ else:
163
+ client_names = ", ".join(target.display_name for target in targets)
164
+ lines.append("")
165
+ lines.append(f"{client_names} will now have access to Mnemo project memory.")
166
+
167
+ return "\n".join(lines)
@@ -0,0 +1,323 @@
1
+ """Code Intelligence — architecture graph, dependencies, patterns, ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ..config import IGNORE_DIRS, mnemo_path
11
+ from ..retrieval import semantic_query
12
+ from ..sprint import get_current_task
13
+
14
+
15
+ def _should_ignore(path: Path) -> bool:
16
+ return any(part in IGNORE_DIRS for part in path.parts)
17
+
18
+
19
+ # --- Architecture Graph (service-to-service calls) ---
20
+
21
+ def detect_service_calls(repo_root: Path) -> dict[str, list[str]]:
22
+ """Detect HTTP client calls between services by scanning for HttpClient, base URLs, etc."""
23
+ graph: dict[str, set[str]] = {}
24
+
25
+ for cs_file in repo_root.rglob("*.cs"):
26
+ if _should_ignore(cs_file):
27
+ continue
28
+ try:
29
+ content = cs_file.read_text(errors="replace")
30
+ except (OSError, PermissionError):
31
+ continue
32
+
33
+ # Identify which service this file belongs to
34
+ parts = cs_file.relative_to(repo_root).parts
35
+ service = parts[0] if len(parts) > 1 else "root"
36
+ if service not in graph:
37
+ graph[service] = set()
38
+
39
+ # Detect outbound HTTP calls
40
+ urls = re.findall(r'["\']https?://[^"\']+["\']', content)
41
+
42
+ for url in urls:
43
+ # Try to identify target service from URL
44
+ for known_svc in ["eligibility", "isauth", "providersearch", "servicereview", "auditlog", "mockdb"]:
45
+ if known_svc in url.lower():
46
+ graph[service].add(known_svc)
47
+
48
+ return {k: sorted(v) for k, v in graph.items() if v}
49
+
50
+
51
+ # --- Dependency Map ---
52
+
53
+ def detect_dependencies(repo_root: Path) -> dict[str, list[str]]:
54
+ """Parse .csproj, package.json, requirements.txt for dependencies."""
55
+ deps: dict[str, list[str]] = {}
56
+
57
+ # .csproj files (NuGet)
58
+ for csproj in repo_root.rglob("*.csproj"):
59
+ if _should_ignore(csproj):
60
+ continue
61
+ try:
62
+ content = csproj.read_text(errors="replace")
63
+ except (OSError, PermissionError):
64
+ continue
65
+ packages = re.findall(r'<PackageReference\s+Include="([^"]+)"(?:\s+Version="([^"]*)")?', content)
66
+ if packages:
67
+ name = csproj.stem
68
+ deps[name] = [f"{pkg} {ver}".strip() for pkg, ver in packages]
69
+
70
+ # package.json
71
+ for pkg_json in repo_root.rglob("package.json"):
72
+ if _should_ignore(pkg_json):
73
+ continue
74
+ try:
75
+ data = json.loads(pkg_json.read_text())
76
+ except (OSError, json.JSONDecodeError):
77
+ continue
78
+ all_deps = {}
79
+ all_deps.update(data.get("dependencies", {}))
80
+ all_deps.update(data.get("devDependencies", {}))
81
+ if all_deps:
82
+ name = data.get("name", pkg_json.parent.name)
83
+ deps[name] = [f"{k} {v}" for k, v in all_deps.items()]
84
+
85
+ return deps
86
+
87
+
88
+ # --- Pattern Detection ---
89
+
90
+ def detect_patterns(repo_root: Path) -> list[str]:
91
+ """Detect common code patterns and conventions."""
92
+ patterns: list[str] = []
93
+
94
+ # Check for common .NET patterns
95
+ controllers = list(repo_root.rglob("*Controller.cs"))
96
+ if controllers:
97
+ # Check what they inherit from
98
+ for c in controllers[:3]:
99
+ try:
100
+ content = c.read_text(errors="replace")
101
+ if "ControllerBase" in content:
102
+ patterns.append("Controllers inherit from ControllerBase (API controllers, no views)")
103
+ break
104
+ elif "Controller" in content:
105
+ patterns.append("Controllers inherit from Controller (MVC with views)")
106
+ break
107
+ except (OSError, PermissionError):
108
+ continue
109
+
110
+ # Check for repository pattern
111
+ repos = list(repo_root.rglob("*Repository.cs"))
112
+ interfaces = list(repo_root.rglob("I*Repository.cs"))
113
+ if repos and interfaces:
114
+ patterns.append(f"Repository pattern with interfaces ({len(interfaces)} interfaces, {len(repos)} implementations)")
115
+
116
+ # Check for handler/strategy pattern
117
+ handlers = list(repo_root.rglob("*Handler.cs"))
118
+ if len(handlers) > 2:
119
+ patterns.append(f"Strategy/Handler pattern ({len(handlers)} handlers found)")
120
+
121
+ # Check for DI registration
122
+ for f in repo_root.rglob("Program.cs"):
123
+ if _should_ignore(f):
124
+ continue
125
+ try:
126
+ content = f.read_text(errors="replace")
127
+ if "AddScoped" in content or "AddTransient" in content or "AddSingleton" in content:
128
+ patterns.append("Dependency injection via built-in .NET DI container")
129
+ break
130
+ except (OSError, PermissionError):
131
+ continue
132
+
133
+ # Check for test patterns
134
+ test_files = list(repo_root.rglob("*Tests.cs"))
135
+ if test_files:
136
+ try:
137
+ sample = test_files[0].read_text(errors="replace")
138
+ if "xUnit" in sample or "[Fact]" in sample:
139
+ patterns.append("Testing with xUnit ([Fact], [Theory])")
140
+ elif "[Test]" in sample:
141
+ patterns.append("Testing with NUnit")
142
+ if "Moq" in sample or "Mock<" in sample:
143
+ patterns.append("Mocking with Moq")
144
+ if "FluentAssertions" in sample:
145
+ patterns.append("Assertions with FluentAssertions")
146
+ except (OSError, PermissionError):
147
+ pass
148
+
149
+ # Check for CosmosDB
150
+ cosmos_files = [f for f in repo_root.rglob("*.cs") if not _should_ignore(f)]
151
+ for f in cosmos_files[:50]:
152
+ try:
153
+ if "CosmosClient" in f.read_text(errors="replace"):
154
+ patterns.append("Data layer: Azure CosmosDB")
155
+ break
156
+ except (OSError, PermissionError):
157
+ continue
158
+
159
+ return patterns
160
+
161
+
162
+ def classify_architecture(repo_root: Path) -> list[dict[str, Any]]:
163
+ """Classify high-level architecture styles with evidence."""
164
+ findings: list[dict[str, Any]] = []
165
+ paths = [p for p in repo_root.rglob("*") if not _should_ignore(p)]
166
+ names = [str(p).lower() for p in paths]
167
+ text_blob = " ".join(names)
168
+
169
+ checks = [
170
+ ("Clean Architecture", ["domain", "application", "infrastructure", "presentation"]),
171
+ ("CQRS", ["command", "query", "handler"]),
172
+ ("Hexagonal", ["ports", "adapters"]),
173
+ ("Event-Driven", ["event", "consumer", "producer"]),
174
+ ("Microservices", ["docker-compose", "helm", "service", "gateway"]),
175
+ ]
176
+ for arch, signals in checks:
177
+ matched = [signal for signal in signals if signal in text_blob]
178
+ if matched:
179
+ findings.append(
180
+ {
181
+ "name": arch,
182
+ "confidence": round(len(matched) / len(signals), 2),
183
+ "evidence": matched,
184
+ }
185
+ )
186
+ return sorted(findings, key=lambda item: item["confidence"], reverse=True)
187
+
188
+
189
+ # --- Similar Implementations ---
190
+
191
+ def find_similar(repo_root: Path, query: str) -> list[dict[str, str]]:
192
+ """Find files with similar naming patterns to help implement new features."""
193
+ query_lower = query.lower()
194
+ semantic_hits = semantic_query(repo_root, "code", query, limit=20)
195
+ if semantic_hits:
196
+ results = []
197
+ for hit in semantic_hits:
198
+ meta = hit.get("metadata", {})
199
+ results.append(
200
+ {
201
+ "file": str(meta.get("path", "")),
202
+ "class": str(meta.get("symbol", "")),
203
+ "content": hit.get("content", "")[:300],
204
+ }
205
+ )
206
+ return results
207
+
208
+ similar: list[dict[str, str]] = []
209
+
210
+ # Find files matching the pattern
211
+ for cs_file in repo_root.rglob("*.cs"):
212
+ if _should_ignore(cs_file):
213
+ continue
214
+ name = cs_file.stem.lower()
215
+ # Match by suffix pattern (e.g. "Handler" finds all *Handler.cs)
216
+ if query_lower in name:
217
+ rel = str(cs_file.relative_to(repo_root))
218
+ try:
219
+ content = cs_file.read_text(errors="replace")
220
+ # Get class declaration line
221
+ match = re.search(r'(public\s+class\s+\w+[^{]*)', content)
222
+ sig = match.group(1).strip() if match else ""
223
+ similar.append({"file": rel, "class": sig})
224
+ except (OSError, PermissionError):
225
+ similar.append({"file": rel, "class": ""})
226
+
227
+ return similar[:20]
228
+
229
+
230
+ def context_for_active_task(repo_root: Path, query: str = "") -> str:
231
+ """Retrieve semantic context using the active task as a relevance hint."""
232
+ active_task_markdown = get_current_task(repo_root)
233
+ if active_task_markdown.startswith("No active task"):
234
+ return active_task_markdown
235
+
236
+ search_query = query.strip() or active_task_markdown
237
+ hits = semantic_query(repo_root, "code", search_query, limit=10)
238
+ if not hits:
239
+ return "No semantic context found for the current task."
240
+
241
+ lines = ["# Context for Active Task", "", "## Active Task", active_task_markdown, "", "## Relevant Code"]
242
+ for hit in hits:
243
+ meta = hit.get("metadata", {})
244
+ lines.append(f"- `{meta.get('path', '')}` :: `{meta.get('symbol', '')}`")
245
+ return "\n".join(lines)
246
+
247
+
248
+ # --- Ownership (git blame stats) ---
249
+
250
+ def detect_ownership(repo_root: Path) -> dict[str, str]:
251
+ """Detect code ownership from CODEOWNERS or git stats."""
252
+ owners: dict[str, str] = {}
253
+
254
+ # Check CODEOWNERS
255
+ for codeowners_path in [
256
+ repo_root / "CODEOWNERS",
257
+ repo_root / ".github" / "CODEOWNERS",
258
+ repo_root / "docs" / "CODEOWNERS",
259
+ ]:
260
+ if codeowners_path.exists():
261
+ try:
262
+ for line in codeowners_path.read_text().splitlines():
263
+ line = line.strip()
264
+ if line and not line.startswith("#"):
265
+ parts = line.split()
266
+ if len(parts) >= 2:
267
+ owners[parts[0]] = " ".join(parts[1:])
268
+ except (OSError, PermissionError):
269
+ pass
270
+ break
271
+
272
+ return owners
273
+
274
+
275
+ # --- Main intelligence report ---
276
+
277
+ def generate_intelligence(repo_root: Path) -> str:
278
+ """Generate a full code intelligence report as markdown."""
279
+ lines = ["# Code Intelligence\n"]
280
+
281
+ # Patterns
282
+ patterns = detect_patterns(repo_root)
283
+ if patterns:
284
+ lines.append("## Patterns & Conventions")
285
+ for p in patterns:
286
+ lines.append(f"- {p}")
287
+ lines.append("")
288
+
289
+ # Architecture
290
+ graph = detect_service_calls(repo_root)
291
+ if graph:
292
+ lines.append("## Service Architecture")
293
+ for svc, targets in sorted(graph.items()):
294
+ lines.append(f"- **{svc}** → {', '.join(targets)}")
295
+ lines.append("")
296
+
297
+ # Dependencies
298
+ deps = detect_dependencies(repo_root)
299
+ if deps:
300
+ lines.append("## Dependencies")
301
+ for project, pkgs in sorted(deps.items()):
302
+ lines.append(f"**{project}:** {', '.join(pkgs[:10])}")
303
+ if len(pkgs) > 10:
304
+ lines.append(f" ... and {len(pkgs) - 10} more")
305
+ lines.append("")
306
+
307
+ # Ownership
308
+ owners = detect_ownership(repo_root)
309
+ if owners:
310
+ lines.append("## Code Ownership")
311
+ for path, owner in owners.items():
312
+ lines.append(f"- `{path}` → {owner}")
313
+ lines.append("")
314
+
315
+ architectures = classify_architecture(repo_root)
316
+ if architectures:
317
+ lines.append("## Architecture Classification")
318
+ for arch in architectures:
319
+ evidence = ", ".join(arch["evidence"])
320
+ lines.append(f"- **{arch['name']}** (confidence {arch['confidence']}) — evidence: {evidence}")
321
+ lines.append("")
322
+
323
+ return "\n".join(lines)