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.
- codecortex/__init__.py +3 -0
- codecortex/architecture/__init__.py +23 -0
- codecortex/architecture/drift.py +182 -0
- codecortex/architecture/inference.py +160 -0
- codecortex/backends/__init__.py +35 -0
- codecortex/backends/base.py +38 -0
- codecortex/backends/context.py +118 -0
- codecortex/backends/contracts.py +65 -0
- codecortex/backends/factory.py +60 -0
- codecortex/backends/graph.py +107 -0
- codecortex/backends/manager.py +303 -0
- codecortex/backends/mcp_client.py +196 -0
- codecortex/backends/pool.py +128 -0
- codecortex/backends/spec.py +83 -0
- codecortex/backends/symbols.py +189 -0
- codecortex/benchmark.py +211 -0
- codecortex/cli.py +409 -0
- codecortex/config.py +27 -0
- codecortex/context/__init__.py +13 -0
- codecortex/context/budget.py +62 -0
- codecortex/context/integrated.py +60 -0
- codecortex/context/pipeline.py +223 -0
- codecortex/core/__init__.py +1 -0
- codecortex/core/contracts.py +45 -0
- codecortex/core/errors.py +17 -0
- codecortex/core/models.py +69 -0
- codecortex/dashboard.py +259 -0
- codecortex/editing.py +41 -0
- codecortex/engines/__init__.py +5 -0
- codecortex/engines/builtin/__init__.py +5 -0
- codecortex/engines/builtin/factory.py +26 -0
- codecortex/engines/builtin/memory.py +35 -0
- codecortex/engines/builtin/repository.py +73 -0
- codecortex/engines/builtin/symbols.py +89 -0
- codecortex/engines/builtin/validation.py +54 -0
- codecortex/engines/registry.py +26 -0
- codecortex/entrypoint.py +266 -0
- codecortex/evaluation/__init__.py +55 -0
- codecortex/evaluation/external.py +265 -0
- codecortex/evaluation/production.py +670 -0
- codecortex/evaluation/regression.py +188 -0
- codecortex/gateway.py +38 -0
- codecortex/git_intelligence.py +252 -0
- codecortex/indexing/__init__.py +6 -0
- codecortex/indexing/graph.py +78 -0
- codecortex/indexing/impact.py +127 -0
- codecortex/indexing/incremental.py +163 -0
- codecortex/indexing/incremental_graph.py +189 -0
- codecortex/indexing/indexer.py +172 -0
- codecortex/indexing/relationships.py +179 -0
- codecortex/indexing/resolution.py +88 -0
- codecortex/integrations/__init__.py +5 -0
- codecortex/integrations/agents.py +235 -0
- codecortex/interfaces/__init__.py +1 -0
- codecortex/interfaces/mcp_bridge.py +66 -0
- codecortex/languages/__init__.py +5 -0
- codecortex/languages/native.py +166 -0
- codecortex/languages/registry.py +232 -0
- codecortex/mcp/__init__.py +5 -0
- codecortex/mcp/extended.py +114 -0
- codecortex/mcp/server.py +473 -0
- codecortex/memory/__init__.py +11 -0
- codecortex/memory/json_store.py +52 -0
- codecortex/memory/knowledge.py +193 -0
- codecortex/memory/team_store.py +193 -0
- codecortex/orchestrator.py +153 -0
- codecortex/pr_intelligence.py +214 -0
- codecortex/retrieval/__init__.py +16 -0
- codecortex/retrieval/hybrid.py +67 -0
- codecortex/retrieval/index.py +135 -0
- codecortex/retrieval/providers.py +67 -0
- codecortex/retrieval/repository.py +94 -0
- codecortex/router/__init__.py +5 -0
- codecortex/router/router.py +79 -0
- codecortex/runtime.py +69 -0
- codecortex/setup.py +100 -0
- codecortex/symbols/__init__.py +5 -0
- codecortex/symbols/providers.py +192 -0
- codecortex/telemetry/__init__.py +5 -0
- codecortex/telemetry/collector.py +43 -0
- codecortex/tracing/__init__.py +9 -0
- codecortex/tracing/task_trace.py +235 -0
- codecortex/workspace/__init__.py +9 -0
- codecortex/workspace/federation.py +173 -0
- codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
- codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
- codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
- codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- 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)
|
codecortex/dashboard.py
ADDED
|
@@ -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("&", "&").replace("<", "<").replace(">", ">")
|
|
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=>({{'&':'&','<':'<','>':'>','"':'"',"'":'''}}[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)
|