codecortex-context-engine 0.1.0a1__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 (90) hide show
  1. codecortex/__init__.py +3 -0
  2. codecortex/architecture/__init__.py +23 -0
  3. codecortex/architecture/drift.py +182 -0
  4. codecortex/architecture/inference.py +160 -0
  5. codecortex/backends/__init__.py +35 -0
  6. codecortex/backends/base.py +38 -0
  7. codecortex/backends/context.py +118 -0
  8. codecortex/backends/contracts.py +65 -0
  9. codecortex/backends/factory.py +60 -0
  10. codecortex/backends/graph.py +107 -0
  11. codecortex/backends/manager.py +303 -0
  12. codecortex/backends/mcp_client.py +196 -0
  13. codecortex/backends/pool.py +128 -0
  14. codecortex/backends/spec.py +83 -0
  15. codecortex/backends/symbols.py +189 -0
  16. codecortex/benchmark.py +211 -0
  17. codecortex/cli.py +409 -0
  18. codecortex/config.py +27 -0
  19. codecortex/context/__init__.py +13 -0
  20. codecortex/context/budget.py +62 -0
  21. codecortex/context/integrated.py +60 -0
  22. codecortex/context/pipeline.py +223 -0
  23. codecortex/core/__init__.py +1 -0
  24. codecortex/core/contracts.py +45 -0
  25. codecortex/core/errors.py +17 -0
  26. codecortex/core/models.py +69 -0
  27. codecortex/dashboard.py +259 -0
  28. codecortex/editing.py +41 -0
  29. codecortex/engines/__init__.py +5 -0
  30. codecortex/engines/builtin/__init__.py +5 -0
  31. codecortex/engines/builtin/factory.py +26 -0
  32. codecortex/engines/builtin/memory.py +35 -0
  33. codecortex/engines/builtin/repository.py +73 -0
  34. codecortex/engines/builtin/symbols.py +89 -0
  35. codecortex/engines/builtin/validation.py +54 -0
  36. codecortex/engines/registry.py +26 -0
  37. codecortex/entrypoint.py +266 -0
  38. codecortex/evaluation/__init__.py +55 -0
  39. codecortex/evaluation/external.py +265 -0
  40. codecortex/evaluation/production.py +670 -0
  41. codecortex/evaluation/regression.py +188 -0
  42. codecortex/gateway.py +38 -0
  43. codecortex/git_intelligence.py +252 -0
  44. codecortex/indexing/__init__.py +6 -0
  45. codecortex/indexing/graph.py +78 -0
  46. codecortex/indexing/impact.py +127 -0
  47. codecortex/indexing/incremental.py +163 -0
  48. codecortex/indexing/incremental_graph.py +189 -0
  49. codecortex/indexing/indexer.py +172 -0
  50. codecortex/indexing/relationships.py +179 -0
  51. codecortex/indexing/resolution.py +88 -0
  52. codecortex/integrations/__init__.py +5 -0
  53. codecortex/integrations/agents.py +235 -0
  54. codecortex/interfaces/__init__.py +1 -0
  55. codecortex/interfaces/mcp_bridge.py +66 -0
  56. codecortex/languages/__init__.py +5 -0
  57. codecortex/languages/native.py +166 -0
  58. codecortex/languages/registry.py +232 -0
  59. codecortex/mcp/__init__.py +5 -0
  60. codecortex/mcp/extended.py +114 -0
  61. codecortex/mcp/server.py +473 -0
  62. codecortex/memory/__init__.py +11 -0
  63. codecortex/memory/json_store.py +52 -0
  64. codecortex/memory/knowledge.py +193 -0
  65. codecortex/memory/team_store.py +193 -0
  66. codecortex/orchestrator.py +153 -0
  67. codecortex/pr_intelligence.py +214 -0
  68. codecortex/retrieval/__init__.py +16 -0
  69. codecortex/retrieval/hybrid.py +67 -0
  70. codecortex/retrieval/index.py +135 -0
  71. codecortex/retrieval/providers.py +67 -0
  72. codecortex/retrieval/repository.py +94 -0
  73. codecortex/router/__init__.py +5 -0
  74. codecortex/router/router.py +79 -0
  75. codecortex/runtime.py +69 -0
  76. codecortex/setup.py +100 -0
  77. codecortex/symbols/__init__.py +5 -0
  78. codecortex/symbols/providers.py +192 -0
  79. codecortex/telemetry/__init__.py +5 -0
  80. codecortex/telemetry/collector.py +43 -0
  81. codecortex/tracing/__init__.py +9 -0
  82. codecortex/tracing/task_trace.py +235 -0
  83. codecortex/workspace/__init__.py +9 -0
  84. codecortex/workspace/federation.py +173 -0
  85. codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
  86. codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
  87. codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
  88. codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
  89. codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  90. codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,223 @@
1
+ """Query-aware context ranking, graph expansion, caching, and budgeting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ from codecortex.context.budget import BudgetContextProcessor
12
+ from codecortex.core.models import ContextChunk
13
+ from codecortex.indexing.graph import ProjectGraph
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ContextMetrics:
18
+ raw_tokens: int
19
+ final_tokens: int
20
+ tokens_saved: int
21
+ reduction: float
22
+ candidates: int
23
+ selected: int
24
+ cache_hit: bool
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ContextResult:
29
+ chunks: tuple[ContextChunk, ...]
30
+ metrics: ContextMetrics
31
+
32
+
33
+ class ContextCache:
34
+ VERSION = 1
35
+
36
+ def __init__(self, path: Path, ttl_seconds: int = 600, max_entries: int = 128) -> None:
37
+ self.path = path
38
+ self.ttl_seconds = ttl_seconds
39
+ self.max_entries = max_entries
40
+
41
+ @staticmethod
42
+ def key(query: str, budget: int, fingerprints: list[str]) -> str:
43
+ payload = "\n".join([query.strip().lower(), str(budget), *fingerprints])
44
+ return hashlib.blake2b(payload.encode("utf-8"), digest_size=20).hexdigest()
45
+
46
+ def _load(self) -> dict[str, dict[str, object]]:
47
+ try:
48
+ payload = json.loads(self.path.read_text(encoding="utf-8"))
49
+ except (OSError, json.JSONDecodeError):
50
+ return {}
51
+ if payload.get("version") != self.VERSION:
52
+ return {}
53
+ return dict(payload.get("entries", {}))
54
+
55
+ def get(self, key: str) -> list[ContextChunk] | None:
56
+ entries = self._load()
57
+ item = entries.get(key)
58
+ if not item:
59
+ return None
60
+ created = float(item.get("created", 0))
61
+ if time.time() - created > self.ttl_seconds:
62
+ return None
63
+ try:
64
+ return [ContextChunk.model_validate(chunk) for chunk in item["chunks"]]
65
+ except (KeyError, TypeError, ValueError):
66
+ return None
67
+
68
+ def put(self, key: str, chunks: list[ContextChunk]) -> None:
69
+ entries = self._load()
70
+ entries[key] = {
71
+ "created": time.time(),
72
+ "chunks": [chunk.model_dump(mode="json") for chunk in chunks],
73
+ }
74
+ ordered = sorted(
75
+ entries.items(),
76
+ key=lambda pair: float(pair[1].get("created", 0)),
77
+ reverse=True,
78
+ )[: self.max_entries]
79
+ self.path.parent.mkdir(parents=True, exist_ok=True)
80
+ temp = self.path.with_suffix(".tmp")
81
+ temp.write_text(
82
+ json.dumps(
83
+ {"version": self.VERSION, "entries": dict(ordered)},
84
+ ensure_ascii=False,
85
+ ),
86
+ encoding="utf-8",
87
+ )
88
+ temp.replace(self.path)
89
+
90
+
91
+ class ContextPipeline:
92
+ def __init__(
93
+ self,
94
+ root: Path,
95
+ graph: ProjectGraph | None = None,
96
+ cache_ttl_seconds: int = 600,
97
+ ) -> None:
98
+ self.root = root.resolve()
99
+ self.graph = graph
100
+ self.budget = BudgetContextProcessor()
101
+ self.cache = ContextCache(
102
+ self.root / ".codecortex" / "cache" / "context.json",
103
+ ttl_seconds=cache_ttl_seconds,
104
+ )
105
+
106
+ @staticmethod
107
+ def _terms(text: str) -> set[str]:
108
+ return {
109
+ word.strip(".,:;()[]{}<>\"'`_-+").lower()
110
+ for word in text.split()
111
+ if len(word.strip()) > 2
112
+ }
113
+
114
+ @staticmethod
115
+ def _fingerprint(chunk: ContextChunk) -> str:
116
+ normalized = " ".join(chunk.content.lower().split())
117
+ return hashlib.blake2b(normalized.encode("utf-8"), digest_size=12).hexdigest()
118
+
119
+ def _rank(self, query: str, chunks: list[ContextChunk]) -> list[ContextChunk]:
120
+ query_terms = self._terms(query)
121
+ ranked: list[ContextChunk] = []
122
+ for chunk in chunks:
123
+ terms = self._terms(chunk.content)
124
+ overlap = len(query_terms & terms) / max(1, len(query_terms))
125
+ path = str(chunk.metadata.get("path", "")).lower()
126
+ path_boost = 0.12 if any(term in path for term in query_terms) else 0.0
127
+ relevance = min(1.0, chunk.relevance * 0.58 + overlap * 0.42 + path_boost)
128
+ ranked.append(chunk.model_copy(update={"relevance": relevance}))
129
+ return sorted(ranked, key=lambda item: (item.relevance, -item.tokens), reverse=True)
130
+
131
+ def _graph_chunks(self, query: str, limit: int = 20) -> list[ContextChunk]:
132
+ if self.graph is None:
133
+ return []
134
+ nodes = self.graph.search(query, limit=8)
135
+ if not nodes:
136
+ return []
137
+ node_ids = {node.id for node in nodes}
138
+ lines: list[str] = []
139
+ for edge in self.graph.edges:
140
+ if edge.source in node_ids or edge.target in node_ids:
141
+ source = next((node for node in self.graph.nodes if node.id == edge.source), None)
142
+ target = next((node for node in self.graph.nodes if node.id == edge.target), None)
143
+ if source and target:
144
+ lines.append(f"{source.name} --{edge.kind}--> {target.name}")
145
+ if len(lines) >= limit:
146
+ break
147
+ if not lines:
148
+ return []
149
+ content = "\n".join(lines)
150
+ return [
151
+ ContextChunk(
152
+ source="knowledge-graph",
153
+ content=content,
154
+ tokens=max(1, len(content) // 4),
155
+ relevance=0.88,
156
+ metadata={"expanded_nodes": [node.name for node in nodes]},
157
+ )
158
+ ]
159
+
160
+ @staticmethod
161
+ def _near_deduplicate(chunks: list[ContextChunk]) -> list[ContextChunk]:
162
+ selected: list[ContextChunk] = []
163
+ term_sets: list[set[str]] = []
164
+ for chunk in chunks:
165
+ terms = ContextPipeline._terms(chunk.content)
166
+ duplicate = False
167
+ for existing in term_sets:
168
+ union = terms | existing
169
+ similarity = len(terms & existing) / max(1, len(union))
170
+ if similarity >= 0.90:
171
+ duplicate = True
172
+ break
173
+ if not duplicate:
174
+ selected.append(chunk)
175
+ term_sets.append(terms)
176
+ return selected
177
+
178
+ async def prepare(
179
+ self,
180
+ query: str,
181
+ chunks: list[ContextChunk],
182
+ budget: int,
183
+ ) -> ContextResult:
184
+ candidates = [*chunks, *self._graph_chunks(query)]
185
+ raw_tokens = sum(chunk.tokens for chunk in candidates)
186
+ fingerprints = [self._fingerprint(chunk) for chunk in candidates]
187
+ cache_key = self.cache.key(query, budget, fingerprints)
188
+ cached = self.cache.get(cache_key)
189
+ if cached is not None:
190
+ final_tokens = sum(chunk.tokens for chunk in cached)
191
+ return ContextResult(
192
+ tuple(cached),
193
+ self._metrics(raw_tokens, final_tokens, len(candidates), len(cached), True),
194
+ )
195
+ ranked = self._rank(query, candidates)
196
+ unique = self._near_deduplicate(ranked)
197
+ fitted = await self.budget.fit(unique, budget)
198
+ self.cache.put(cache_key, fitted)
199
+ final_tokens = sum(chunk.tokens for chunk in fitted)
200
+ return ContextResult(
201
+ tuple(fitted),
202
+ self._metrics(raw_tokens, final_tokens, len(candidates), len(fitted), False),
203
+ )
204
+
205
+ @staticmethod
206
+ def _metrics(
207
+ raw_tokens: int,
208
+ final_tokens: int,
209
+ candidates: int,
210
+ selected: int,
211
+ cache_hit: bool,
212
+ ) -> ContextMetrics:
213
+ saved = max(0, raw_tokens - final_tokens)
214
+ reduction = saved / raw_tokens if raw_tokens else 0.0
215
+ return ContextMetrics(
216
+ raw_tokens=raw_tokens,
217
+ final_tokens=final_tokens,
218
+ tokens_saved=saved,
219
+ reduction=reduction,
220
+ candidates=candidates,
221
+ selected=selected,
222
+ cache_hit=cache_hit,
223
+ )
@@ -0,0 +1 @@
1
+ """Core contracts and shared domain models."""
@@ -0,0 +1,45 @@
1
+ """Engine contracts used by the orchestrator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from codecortex.core.models import AgentRequest, Capability, ContextChunk, EngineResult
8
+
9
+
10
+ class Engine(ABC):
11
+ """Base interface for every pluggable CodeCortex engine."""
12
+
13
+ capability: Capability
14
+
15
+ @abstractmethod
16
+ async def health(self) -> bool:
17
+ """Return True when the engine can serve requests."""
18
+
19
+ @abstractmethod
20
+ async def execute(self, request: AgentRequest) -> EngineResult:
21
+ """Execute one request against the engine."""
22
+
23
+
24
+ class ContextProcessor(ABC):
25
+ """Contract for context ranking and budget enforcement."""
26
+
27
+ @abstractmethod
28
+ async def fit(self, chunks: list[ContextChunk], budget: int) -> list[ContextChunk]:
29
+ """Return the most useful context that fits inside the token budget."""
30
+
31
+
32
+ class MemoryStore(ABC):
33
+ """Contract for project-scoped persistent memory."""
34
+
35
+ @abstractmethod
36
+ async def put(self, namespace: str, key: str, value: str) -> None:
37
+ """Persist one memory value."""
38
+
39
+ @abstractmethod
40
+ async def get(self, namespace: str, key: str) -> str | None:
41
+ """Load one memory value."""
42
+
43
+ @abstractmethod
44
+ async def search(self, namespace: str, query: str, limit: int = 10) -> list[str]:
45
+ """Return relevant memory values."""
@@ -0,0 +1,17 @@
1
+ """Domain errors used across CodeCortex."""
2
+
3
+
4
+ class CodeCortexError(Exception):
5
+ """Base exception for CodeCortex."""
6
+
7
+
8
+ class EngineUnavailableError(CodeCortexError):
9
+ """Raised when a requested engine is not available."""
10
+
11
+
12
+ class RoutingError(CodeCortexError):
13
+ """Raised when a request cannot be routed safely."""
14
+
15
+
16
+ class ContextBudgetExceededError(CodeCortexError):
17
+ """Raised when a hard context budget cannot be satisfied."""
@@ -0,0 +1,69 @@
1
+ """Shared domain models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class Capability(StrEnum):
12
+ REPOSITORY = "repository"
13
+ SYMBOLS = "symbols"
14
+ CONTEXT = "context"
15
+ MEMORY = "memory"
16
+ VALIDATION = "validation"
17
+
18
+
19
+ class RequestKind(StrEnum):
20
+ EXPLAIN = "explain"
21
+ LOCATE = "locate"
22
+ DEBUG = "debug"
23
+ REFACTOR = "refactor"
24
+ CHANGE = "change"
25
+ REVIEW = "review"
26
+ UNKNOWN = "unknown"
27
+
28
+
29
+ class AgentRequest(BaseModel):
30
+ query: str = Field(min_length=1)
31
+ project_root: str = "."
32
+ kind: RequestKind = RequestKind.UNKNOWN
33
+ metadata: dict[str, Any] = Field(default_factory=dict)
34
+
35
+
36
+ class RouteScore(BaseModel):
37
+ capability: Capability
38
+ score: float = Field(ge=0.0, le=1.0)
39
+ reason: str
40
+
41
+
42
+ class RoutePlan(BaseModel):
43
+ request_kind: RequestKind
44
+ scores: list[RouteScore]
45
+ selected: list[Capability]
46
+ context_budget: int = Field(default=32_000, gt=0)
47
+
48
+
49
+ class ContextChunk(BaseModel):
50
+ source: str
51
+ content: str
52
+ tokens: int = Field(ge=0)
53
+ relevance: float = Field(default=0.5, ge=0.0, le=1.0)
54
+ metadata: dict[str, Any] = Field(default_factory=dict)
55
+
56
+
57
+ class EngineResult(BaseModel):
58
+ capability: Capability
59
+ content: str = ""
60
+ chunks: list[ContextChunk] = Field(default_factory=list)
61
+ metadata: dict[str, Any] = Field(default_factory=dict)
62
+
63
+
64
+ class ExecutionResult(BaseModel):
65
+ request: AgentRequest
66
+ plan: RoutePlan
67
+ results: list[EngineResult] = Field(default_factory=list)
68
+ context_tokens: int = 0
69
+ metadata: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,259 @@
1
+ """Read-only local observability dashboard for CodeCortex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import re
8
+ from collections import Counter, defaultdict
9
+ from dataclasses import asdict
10
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
+ from typing import Any
12
+ from urllib.parse import parse_qs, urlparse
13
+
14
+ from codecortex.architecture import ArchitectureDriftDetector, ArchitectureFingerprint
15
+ from codecortex.evaluation import BenchmarkHistory
16
+ from codecortex.indexing.incremental_graph import IncrementalGraphIndex
17
+ from codecortex.pr_intelligence import PRIntelligence
18
+ from codecortex.runtime import CortexRuntime
19
+ from codecortex.tracing import TaskTraceRecorder
20
+
21
+ _SAFE_REF = re.compile(r"^[A-Za-z0-9._/@+-]{1,200}$")
22
+
23
+
24
+ def _read_events(runtime: CortexRuntime, limit: int = 10_000) -> list[dict[str, Any]]:
25
+ path = runtime.config.state_dir / "runtime" / "events.jsonl"
26
+ try:
27
+ lines = path.read_text(encoding="utf-8").splitlines()[-limit:]
28
+ except OSError:
29
+ return []
30
+ events: list[dict[str, Any]] = []
31
+ for line in lines:
32
+ try:
33
+ payload = json.loads(line)
34
+ except json.JSONDecodeError:
35
+ continue
36
+ if isinstance(payload, dict):
37
+ events.append(payload)
38
+ return events
39
+
40
+
41
+ def _event_stats(events: list[dict[str, Any]]) -> dict[str, Any]:
42
+ counts: Counter[str] = Counter()
43
+ routes: Counter[str] = Counter()
44
+ token_saved = 0
45
+ engine_ms: defaultdict[str, float] = defaultdict(float)
46
+ engine_calls: Counter[str] = Counter()
47
+ for event in events:
48
+ name = str(event.get("name", "unknown"))
49
+ attrs = event.get("attributes") if isinstance(event.get("attributes"), dict) else {}
50
+ counts[name] += 1
51
+ if name == "route.created":
52
+ routes[str(attrs.get("kind", "unknown"))] += 1
53
+ elif name == "context.fitted":
54
+ token_saved += int(attrs.get("saved", 0) or 0)
55
+ elif name == "engine.executed":
56
+ capability = str(attrs.get("capability", "unknown"))
57
+ engine_calls[capability] += 1
58
+ engine_ms[capability] += float(attrs.get("duration_ms", 0.0) or 0.0)
59
+ engine_latency = {
60
+ key: round(engine_ms[key] / count, 2)
61
+ for key, count in engine_calls.items()
62
+ if count
63
+ }
64
+ return {
65
+ "counts": dict(counts),
66
+ "routes": dict(routes),
67
+ "context_tokens_saved": token_saved,
68
+ "engine_avg_latency_ms": engine_latency,
69
+ }
70
+
71
+
72
+ def _recent_traces(runtime: CortexRuntime, limit: int = 12) -> list[dict[str, Any]]:
73
+ recorder = TaskTraceRecorder(runtime.config.state_dir / "runtime" / "traces.jsonl")
74
+ spans = recorder.read(limit=2_000)
75
+ trace_ids: list[str] = []
76
+ for span in reversed(spans):
77
+ if span.trace_id not in trace_ids:
78
+ trace_ids.append(span.trace_id)
79
+ if len(trace_ids) >= limit:
80
+ break
81
+ result: list[dict[str, Any]] = []
82
+ for trace_id in trace_ids:
83
+ try:
84
+ result.append(asdict(recorder.summarize(trace_id)))
85
+ except (KeyError, ValueError):
86
+ continue
87
+ return result
88
+
89
+
90
+ def _benchmark_history(runtime: CortexRuntime, limit: int = 12) -> list[dict[str, Any]]:
91
+ history = BenchmarkHistory(runtime.config.state_dir / "benchmarks" / "history.json").load()
92
+ return [asdict(item) for item in history[-limit:]]
93
+
94
+
95
+ def _architecture_drift(runtime: CortexRuntime, graph: Any) -> dict[str, Any]:
96
+ detector = ArchitectureDriftDetector()
97
+ current = detector.fingerprint(graph)
98
+ target = runtime.config.state_dir / "architecture" / "baseline.json"
99
+ baseline = ArchitectureFingerprint.load(target)
100
+ if baseline is None:
101
+ return {"status": "no-baseline", "current": asdict(current)}
102
+ report = detector.compare(baseline, current)
103
+ return {"status": "compared", "report": asdict(report)}
104
+
105
+
106
+ async def _overview(runtime: CortexRuntime) -> dict[str, Any]:
107
+ graph, index_stats = await asyncio.to_thread(
108
+ IncrementalGraphIndex(runtime.config.project_root).refresh
109
+ )
110
+ health = await runtime.gateway.health()
111
+ events = _read_events(runtime)
112
+ stats = _event_stats(events)
113
+ degree: Counter[str] = Counter()
114
+ for edge in graph.edges:
115
+ degree[edge.source] += 1
116
+ degree[edge.target] += 1
117
+ by_id = {node.id: node for node in graph.nodes}
118
+ hot_nodes = [
119
+ {
120
+ "id": node_id,
121
+ "name": by_id[node_id].name if node_id in by_id else node_id,
122
+ "path": by_id[node_id].path if node_id in by_id else None,
123
+ "degree": count,
124
+ }
125
+ for node_id, count in degree.most_common(12)
126
+ ]
127
+ return {
128
+ "project": str(runtime.config.project_root),
129
+ "active_backends": list(runtime.active_backends),
130
+ "health": health,
131
+ "index": {
132
+ "tracked": index_stats.index.tracked,
133
+ "files_reparsed": index_stats.files_reparsed,
134
+ "full_rebuild": index_stats.full_rebuild,
135
+ "duration_ms": index_stats.index.duration_ms,
136
+ },
137
+ "graph": {
138
+ "nodes": len(graph.nodes),
139
+ "edges": len(graph.edges),
140
+ "counts": graph.counts(),
141
+ "hot_nodes": hot_nodes,
142
+ },
143
+ "runtime": stats,
144
+ "traces": _recent_traces(runtime),
145
+ "benchmarks": _benchmark_history(runtime),
146
+ "architecture": _architecture_drift(runtime, graph),
147
+ }
148
+
149
+
150
+ def _html(project: str) -> str:
151
+ escaped = (
152
+ project.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
153
+ )
154
+ return f"""<!doctype html>
155
+ <html lang="en">
156
+ <head>
157
+ <meta charset="utf-8">
158
+ <meta name="viewport" content="width=device-width,initial-scale=1">
159
+ <meta name="color-scheme" content="dark">
160
+ <title>CodeCortex Observatory</title>
161
+ <style>
162
+ :root{{--bg:#090b0f;--panel:#11151b;--line:#252c36;--muted:#8b96a5;--text:#f4f7fb;--accent:#8ce0c8;--warn:#f3c969}}
163
+ *{{box-sizing:border-box}}body{{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif}}
164
+ main{{max-width:1280px;margin:auto;padding:32px}}header{{display:flex;justify-content:space-between;gap:24px;align-items:end;margin-bottom:24px}}
165
+ h1{{font-size:34px;margin:0}}.muted{{color:var(--muted)}}code{{font-family:ui-monospace,SFMono-Regular,monospace}}
166
+ .grid{{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}}.card,.panel{{border:1px solid var(--line);background:var(--panel);border-radius:14px;padding:16px}}
167
+ .metric{{font-size:28px;font-weight:700;margin-top:5px}}.wide{{grid-column:span 2}}.full{{grid-column:1/-1}}
168
+ section{{margin-top:12px}}h2{{font-size:16px;margin:0 0 12px}}table{{width:100%;border-collapse:collapse}}th,td{{text-align:left;border-bottom:1px solid var(--line);padding:8px 4px}}th{{color:var(--muted);font-weight:500}}
169
+ .pill{{display:inline-block;border:1px solid var(--line);border-radius:999px;padding:2px 8px;margin:2px}}.ok{{color:var(--accent)}}.bad{{color:var(--warn)}}
170
+ .bar{{height:7px;background:#202630;border-radius:9px;overflow:hidden;margin-top:5px}}.bar>i{{display:block;height:100%;background:var(--accent)}}
171
+ @media(max-width:850px){{.grid{{grid-template-columns:1fr 1fr}}.wide{{grid-column:1/-1}}}}@media(max-width:520px){{main{{padding:18px}}.grid{{grid-template-columns:1fr}}.wide{{grid-column:auto}}header{{display:block}}}}
172
+ </style>
173
+ </head><body><main>
174
+ <header><div><h1>CodeCortex Observatory</h1><div class="muted"><code>{escaped}</code></div></div><div id="updated" class="muted">Loading…</div></header>
175
+ <div id="metrics" class="grid"></div>
176
+ <section class="grid">
177
+ <div class="panel wide"><h2>Backend health</h2><div id="health"></div></div>
178
+ <div class="panel wide"><h2>Routing distribution</h2><div id="routes"></div></div>
179
+ <div class="panel wide"><h2>Hot graph nodes</h2><table><thead><tr><th>Node</th><th>Path</th><th>Degree</th></tr></thead><tbody id="hot"></tbody></table></div>
180
+ <div class="panel wide"><h2>Recent traces</h2><table><thead><tr><th>Trace</th><th>Spans</th><th>ms</th><th>Tokens</th><th>Errors</th></tr></thead><tbody id="traces"></tbody></table></div>
181
+ <div class="panel wide"><h2>Engine latency</h2><div id="latency"></div></div>
182
+ <div class="panel wide"><h2>Architecture drift</h2><pre id="drift" class="muted"></pre></div>
183
+ <div class="panel full"><h2>Benchmark history</h2><table><thead><tr><th>Time</th><th>Commit</th><th>Strategies</th></tr></thead><tbody id="bench"></tbody></table></div>
184
+ </section>
185
+ <script>
186
+ const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}}[c]));
187
+ function bars(target,obj){{const max=Math.max(1,...Object.values(obj||{{}}));document.getElementById(target).innerHTML=Object.entries(obj||{{}}).sort((a,b)=>b[1]-a[1]).map(([k,v])=>`<div>${{esc(k)}} <span class="muted">${{Number(v).toFixed(2)}}</span><div class="bar"><i style="width:${{Math.max(2,100*v/max)}}%"></i></div></div>`).join('')||'<span class="muted">No data yet.</span>'}}
188
+ async function load(){{const d=await fetch('/api/overview',{{cache:'no-store'}}).then(r=>r.json());const c=d.runtime.counts||{{}};const cards=[['Files',d.index.tracked],['Graph nodes',d.graph.nodes],['Graph edges',d.graph.edges],['Tokens saved',d.runtime.context_tokens_saved],['Routes',c['route.created']||0],['Engine calls',c['engine.executed']||0],['MCP calls',c['mcp.tool.called']||0],['Reparsed',d.index.files_reparsed]];document.getElementById('metrics').innerHTML=cards.map(([k,v])=>`<div class="card"><div class="muted">${{esc(k)}}</div><div class="metric">${{Number(v||0).toLocaleString()}}</div></div>`).join('');document.getElementById('health').innerHTML=Object.entries(d.health||{{}}).map(([k,v])=>`<span class="pill ${{v?'ok':'bad'}}">${{esc(k)}} · ${{v?'ready':'unavailable'}}</span>`).join('');bars('routes',d.runtime.routes);bars('latency',d.runtime.engine_avg_latency_ms);document.getElementById('hot').innerHTML=(d.graph.hot_nodes||[]).map(x=>`<tr><td>${{esc(x.name)}}</td><td class="muted">${{esc(x.path||'')}}</td><td>${{x.degree}}</td></tr>`).join('');document.getElementById('traces').innerHTML=(d.traces||[]).map(x=>`<tr><td><code>${{esc(x.trace_id.slice(0,10))}}</code></td><td>${{x.spans}}</td><td>${{Number(x.duration_ms).toFixed(1)}}</td><td>${{x.context_tokens}}</td><td>${{x.errors}}</td></tr>`).join('');document.getElementById('drift').textContent=JSON.stringify(d.architecture,null,2);document.getElementById('bench').innerHTML=(d.benchmarks||[]).slice().reverse().map(x=>`<tr><td>${{esc(x.created_at)}}</td><td><code>${{esc((x.commit||'').slice(0,10))}}</code></td><td>${{esc(Object.keys(x.metrics||{{}}).join(', '))}}</td></tr>`).join('');document.getElementById('updated').textContent='Updated '+new Date().toLocaleTimeString();}}
189
+ load().catch(e=>document.getElementById('updated').textContent='Dashboard error: '+e);setInterval(load,15000);
190
+ </script></main></body></html>"""
191
+
192
+
193
+ def run_dashboard(runtime: CortexRuntime, host: str = "127.0.0.1", port: int = 7331) -> None:
194
+ class Handler(BaseHTTPRequestHandler):
195
+ def _send(self, status: int, content_type: str, body: bytes) -> None:
196
+ self.send_response(status)
197
+ self.send_header("Content-Type", content_type)
198
+ self.send_header("Content-Length", str(len(body)))
199
+ self.send_header("Cache-Control", "no-store")
200
+ self.send_header("X-Content-Type-Options", "nosniff")
201
+ self.send_header("X-Frame-Options", "DENY")
202
+ self.send_header("Referrer-Policy", "no-referrer")
203
+ self.send_header(
204
+ "Content-Security-Policy",
205
+ "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'",
206
+ )
207
+ self.end_headers()
208
+ self.wfile.write(body)
209
+
210
+ def _json(self, status: int, payload: Any) -> None:
211
+ body = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8")
212
+ self._send(status, "application/json; charset=utf-8", body)
213
+
214
+ def do_GET(self) -> None: # noqa: N802
215
+ parsed = urlparse(self.path)
216
+ if parsed.path == "/":
217
+ self._send(
218
+ 200,
219
+ "text/html; charset=utf-8",
220
+ _html(str(runtime.config.project_root)).encode("utf-8"),
221
+ )
222
+ return
223
+ if parsed.path == "/api/overview":
224
+ self._json(200, asyncio.run(_overview(runtime)))
225
+ return
226
+ if parsed.path == "/api/health":
227
+ self._json(200, asyncio.run(runtime.gateway.health()))
228
+ return
229
+ if parsed.path == "/api/traces":
230
+ self._json(200, {"traces": _recent_traces(runtime, 50)})
231
+ return
232
+ if parsed.path == "/api/benchmarks":
233
+ self._json(200, {"benchmarks": _benchmark_history(runtime, 50)})
234
+ return
235
+ if parsed.path == "/api/pr-risk":
236
+ query = parse_qs(parsed.query)
237
+ base = (query.get("base") or [""])[0]
238
+ head = (query.get("head") or ["HEAD"])[0]
239
+ if not _SAFE_REF.fullmatch(base) or not _SAFE_REF.fullmatch(head):
240
+ self._json(400, {"error": "invalid git ref"})
241
+ return
242
+ try:
243
+ graph = IncrementalGraphIndex(runtime.config.project_root).refresh()[0]
244
+ report = PRIntelligence(runtime.config.project_root, graph).analyze(base, head)
245
+ except Exception as exc:
246
+ self._json(422, {"error": f"{type(exc).__name__}: {exc}"})
247
+ return
248
+ self._json(200, asdict(report))
249
+ return
250
+ self._json(404, {"error": "not found"})
251
+
252
+ def log_message(self, format: str, *args: object) -> None:
253
+ del format, args
254
+
255
+ server = ThreadingHTTPServer((host, port), Handler)
256
+ try:
257
+ server.serve_forever()
258
+ finally:
259
+ server.server_close()
codecortex/editing.py ADDED
@@ -0,0 +1,41 @@
1
+ """Public guarded editing service used by CLI and MCP surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from codecortex.backends.symbols import SymbolBackendAdapter
10
+ from codecortex.core.models import Capability
11
+ from codecortex.runtime import CortexRuntime
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class EditService:
16
+ runtime: CortexRuntime
17
+
18
+ @property
19
+ def root(self) -> Path:
20
+ return self.runtime.config.project_root
21
+
22
+ def backend(self) -> SymbolBackendAdapter:
23
+ engine = self.runtime.registry.get(Capability.SYMBOLS)
24
+ if not isinstance(engine, SymbolBackendAdapter):
25
+ raise RuntimeError(
26
+ "semantic editing requires the mature symbol backend; "
27
+ "run `cortex backend install symbols`"
28
+ )
29
+ return engine
30
+
31
+ def rename(self, path: str, name_path: str, new_name: str) -> dict[str, Any]:
32
+ return self.backend().rename_symbol(name_path, path, new_name)
33
+
34
+ def replace(self, path: str, name_path: str, body: str) -> dict[str, Any]:
35
+ return self.backend().replace_symbol_body(name_path, path, body)
36
+
37
+ def insert_before(self, path: str, name_path: str, body: str) -> dict[str, Any]:
38
+ return self.backend().insert_before_symbol(name_path, path, body)
39
+
40
+ def insert_after(self, path: str, name_path: str, body: str) -> dict[str, Any]:
41
+ return self.backend().insert_after_symbol(name_path, path, body)
@@ -0,0 +1,5 @@
1
+ """Engine registry and built-in engine implementations."""
2
+
3
+ from codecortex.engines.registry import EngineRegistry
4
+
5
+ __all__ = ["EngineRegistry"]
@@ -0,0 +1,5 @@
1
+ """Built-in local engines."""
2
+
3
+ from codecortex.engines.builtin.factory import build_default_registry
4
+
5
+ __all__ = ["build_default_registry"]