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
codecortex/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Architecture inference and drift analysis."""
|
|
2
|
+
|
|
3
|
+
from codecortex.architecture.drift import (
|
|
4
|
+
ArchitectureDriftDetector,
|
|
5
|
+
ArchitectureDriftReport,
|
|
6
|
+
ArchitectureFingerprint,
|
|
7
|
+
DriftFinding,
|
|
8
|
+
)
|
|
9
|
+
from codecortex.architecture.inference import (
|
|
10
|
+
ArchitectureHypothesis,
|
|
11
|
+
ArchitectureInferenceEngine,
|
|
12
|
+
ArchitectureReport,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ArchitectureDriftDetector",
|
|
17
|
+
"ArchitectureDriftReport",
|
|
18
|
+
"ArchitectureFingerprint",
|
|
19
|
+
"ArchitectureHypothesis",
|
|
20
|
+
"ArchitectureInferenceEngine",
|
|
21
|
+
"ArchitectureReport",
|
|
22
|
+
"DriftFinding",
|
|
23
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Architecture fingerprinting and drift detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
|
|
10
|
+
from codecortex.architecture.inference import ArchitectureInferenceEngine
|
|
11
|
+
from codecortex.indexing.graph import ProjectGraph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ArchitectureFingerprint:
|
|
16
|
+
version: int
|
|
17
|
+
pattern: str | None
|
|
18
|
+
confidence: float
|
|
19
|
+
dependency_counts: dict[str, int]
|
|
20
|
+
zone_file_counts: dict[str, int]
|
|
21
|
+
unresolved_ratio: float
|
|
22
|
+
|
|
23
|
+
def save(self, path: Path) -> None:
|
|
24
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
temp = path.with_suffix(path.suffix + ".tmp")
|
|
26
|
+
temp.write_text(json.dumps(asdict(self), indent=2, ensure_ascii=False), encoding="utf-8")
|
|
27
|
+
temp.replace(path)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def load(cls, path: Path) -> ArchitectureFingerprint | None:
|
|
31
|
+
try:
|
|
32
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
return cls(
|
|
34
|
+
version=int(payload["version"]),
|
|
35
|
+
pattern=str(payload["pattern"]) if payload.get("pattern") else None,
|
|
36
|
+
confidence=float(payload["confidence"]),
|
|
37
|
+
dependency_counts={str(k): int(v) for k, v in payload["dependency_counts"].items()},
|
|
38
|
+
zone_file_counts={str(k): int(v) for k, v in payload["zone_file_counts"].items()},
|
|
39
|
+
unresolved_ratio=float(payload["unresolved_ratio"]),
|
|
40
|
+
)
|
|
41
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class DriftFinding:
|
|
47
|
+
kind: str
|
|
48
|
+
severity: str
|
|
49
|
+
score: float
|
|
50
|
+
message: str
|
|
51
|
+
evidence: tuple[str, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ArchitectureDriftReport:
|
|
56
|
+
drifted: bool
|
|
57
|
+
score: float
|
|
58
|
+
findings: tuple[DriftFinding, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ArchitectureDriftDetector:
|
|
62
|
+
VERSION = 1
|
|
63
|
+
|
|
64
|
+
def fingerprint(self, graph: ProjectGraph) -> ArchitectureFingerprint:
|
|
65
|
+
report = ArchitectureInferenceEngine().analyze(graph)
|
|
66
|
+
node_map = {node.id: node for node in graph.nodes}
|
|
67
|
+
dependencies: Counter[str] = Counter()
|
|
68
|
+
zones: Counter[str] = Counter()
|
|
69
|
+
unresolved = 0
|
|
70
|
+
relevant_edges = 0
|
|
71
|
+
for node in graph.nodes:
|
|
72
|
+
if node.kind == "file" and node.path:
|
|
73
|
+
zones[self._zone(node.path)] += 1
|
|
74
|
+
for edge in graph.edges:
|
|
75
|
+
if edge.kind not in {"calls", "imports", "inherits", "implements"}:
|
|
76
|
+
continue
|
|
77
|
+
relevant_edges += 1
|
|
78
|
+
source = node_map.get(edge.source)
|
|
79
|
+
target = node_map.get(edge.target)
|
|
80
|
+
if target is None or target.kind in {"reference", "module"}:
|
|
81
|
+
unresolved += 1
|
|
82
|
+
source_zone = self._zone(source.path) if source and source.path else "external"
|
|
83
|
+
target_zone = self._zone(target.path) if target and target.path else "external"
|
|
84
|
+
dependencies[f"{source_zone}->{target_zone}:{edge.kind}"] += 1
|
|
85
|
+
primary = report.primary
|
|
86
|
+
return ArchitectureFingerprint(
|
|
87
|
+
version=self.VERSION,
|
|
88
|
+
pattern=primary.name if primary else None,
|
|
89
|
+
confidence=primary.confidence if primary else 0.0,
|
|
90
|
+
dependency_counts=dict(sorted(dependencies.items())),
|
|
91
|
+
zone_file_counts=dict(sorted(zones.items())),
|
|
92
|
+
unresolved_ratio=unresolved / max(1, relevant_edges),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def compare(
|
|
96
|
+
self,
|
|
97
|
+
baseline: ArchitectureFingerprint,
|
|
98
|
+
current: ArchitectureFingerprint,
|
|
99
|
+
dependency_growth_limit: float = 0.75,
|
|
100
|
+
) -> ArchitectureDriftReport:
|
|
101
|
+
findings: list[DriftFinding] = []
|
|
102
|
+
if baseline.pattern and current.pattern and baseline.pattern != current.pattern:
|
|
103
|
+
findings.append(
|
|
104
|
+
DriftFinding(
|
|
105
|
+
"primary-pattern-change",
|
|
106
|
+
"high",
|
|
107
|
+
0.90,
|
|
108
|
+
f"Primary architecture changed from {baseline.pattern} to {current.pattern}.",
|
|
109
|
+
(f"baseline={baseline.confidence:.2f}", f"current={current.confidence:.2f}"),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
elif baseline.confidence - current.confidence >= 0.20:
|
|
113
|
+
findings.append(
|
|
114
|
+
DriftFinding(
|
|
115
|
+
"pattern-confidence-drop",
|
|
116
|
+
"medium",
|
|
117
|
+
0.55,
|
|
118
|
+
"Architecture pattern confidence dropped materially.",
|
|
119
|
+
(f"{baseline.confidence:.2f}->{current.confidence:.2f}",),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for signature, count in current.dependency_counts.items():
|
|
124
|
+
before = baseline.dependency_counts.get(signature, 0)
|
|
125
|
+
if before == 0 and count > 0:
|
|
126
|
+
severity = "high" if any(kind in signature for kind in (":calls", ":inherits")) else "medium"
|
|
127
|
+
findings.append(
|
|
128
|
+
DriftFinding(
|
|
129
|
+
"new-dependency-direction",
|
|
130
|
+
severity,
|
|
131
|
+
0.70 if severity == "high" else 0.45,
|
|
132
|
+
f"New dependency direction detected: {signature}.",
|
|
133
|
+
(f"count={count}",),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
growth = (count - before) / max(1, before)
|
|
138
|
+
if count - before >= 2 and growth > dependency_growth_limit:
|
|
139
|
+
findings.append(
|
|
140
|
+
DriftFinding(
|
|
141
|
+
"dependency-growth",
|
|
142
|
+
"medium",
|
|
143
|
+
min(0.70, 0.35 + growth * 0.20),
|
|
144
|
+
f"Dependency volume increased sharply: {signature}.",
|
|
145
|
+
(f"{before}->{count}", f"growth={growth:.0%}"),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
unresolved_delta = current.unresolved_ratio - baseline.unresolved_ratio
|
|
150
|
+
if unresolved_delta >= 0.12:
|
|
151
|
+
findings.append(
|
|
152
|
+
DriftFinding(
|
|
153
|
+
"resolution-quality-drop",
|
|
154
|
+
"medium",
|
|
155
|
+
min(0.65, 0.40 + unresolved_delta),
|
|
156
|
+
"Unresolved dependency ratio increased.",
|
|
157
|
+
(
|
|
158
|
+
f"{baseline.unresolved_ratio:.2%}->{current.unresolved_ratio:.2%}",
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
score = 1.0
|
|
164
|
+
for finding in findings:
|
|
165
|
+
score *= 1.0 - min(0.95, finding.score)
|
|
166
|
+
aggregate = round(1.0 - score, 4)
|
|
167
|
+
findings.sort(key=lambda item: (-item.score, item.kind))
|
|
168
|
+
return ArchitectureDriftReport(
|
|
169
|
+
drifted=bool(findings),
|
|
170
|
+
score=aggregate,
|
|
171
|
+
findings=tuple(findings),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _zone(path: str) -> str:
|
|
176
|
+
parts = PurePosixPath(path).parts
|
|
177
|
+
ignored = {"src", "lib", "app", "source"}
|
|
178
|
+
for part in parts[:-1]:
|
|
179
|
+
normalized = part.lower().replace("-", "_")
|
|
180
|
+
if normalized not in ignored:
|
|
181
|
+
return normalized
|
|
182
|
+
return "root"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Evidence-based architecture pattern inference with calibrated confidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
|
|
8
|
+
from codecortex.indexing.graph import ProjectGraph
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ArchitectureHypothesis:
|
|
13
|
+
name: str
|
|
14
|
+
confidence: float
|
|
15
|
+
evidence: tuple[str, ...]
|
|
16
|
+
missing_signals: tuple[str, ...]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class ArchitectureReport:
|
|
21
|
+
primary: ArchitectureHypothesis | None
|
|
22
|
+
alternatives: tuple[ArchitectureHypothesis, ...]
|
|
23
|
+
analyzed_files: int
|
|
24
|
+
analyzed_symbols: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class _Pattern:
|
|
29
|
+
name: str
|
|
30
|
+
groups: tuple[tuple[str, tuple[str, ...], float], ...]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ArchitectureInferenceEngine:
|
|
34
|
+
"""Infer architecture from path vocabulary, symbols, and graph relations.
|
|
35
|
+
|
|
36
|
+
Confidence is evidence-derived rather than a binary label. A pattern only reaches
|
|
37
|
+
high confidence when independent signal groups are present across the repository.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_PATTERNS = (
|
|
41
|
+
_Pattern(
|
|
42
|
+
"hexagonal",
|
|
43
|
+
(
|
|
44
|
+
("domain", ("domain", "entities", "aggregate"), 0.24),
|
|
45
|
+
("ports", ("ports", "port", "interfaces"), 0.24),
|
|
46
|
+
("adapters", ("adapters", "adapter"), 0.22),
|
|
47
|
+
("application", ("application", "usecases", "use_cases"), 0.18),
|
|
48
|
+
("infrastructure", ("infrastructure", "infra"), 0.12),
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
_Pattern(
|
|
52
|
+
"clean-architecture",
|
|
53
|
+
(
|
|
54
|
+
("domain", ("domain", "entities"), 0.25),
|
|
55
|
+
("use-cases", ("usecases", "use_cases", "application"), 0.25),
|
|
56
|
+
("interfaces", ("interfaces", "presenters", "controllers"), 0.20),
|
|
57
|
+
("infrastructure", ("infrastructure", "frameworks", "drivers"), 0.20),
|
|
58
|
+
("dependency-boundaries", ("repository", "gateway"), 0.10),
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
_Pattern(
|
|
62
|
+
"layered",
|
|
63
|
+
(
|
|
64
|
+
("presentation", ("controllers", "controller", "api", "routes"), 0.22),
|
|
65
|
+
("service", ("services", "service"), 0.24),
|
|
66
|
+
("persistence", ("repositories", "repository", "dao"), 0.24),
|
|
67
|
+
("model", ("models", "model", "entities"), 0.16),
|
|
68
|
+
("configuration", ("config", "configuration"), 0.14),
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
_Pattern(
|
|
72
|
+
"mvc",
|
|
73
|
+
(
|
|
74
|
+
("models", ("models", "model"), 0.34),
|
|
75
|
+
("views", ("views", "templates", "view"), 0.33),
|
|
76
|
+
("controllers", ("controllers", "controller"), 0.33),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
_Pattern(
|
|
80
|
+
"service-repository",
|
|
81
|
+
(
|
|
82
|
+
("services", ("services", "service"), 0.38),
|
|
83
|
+
("repositories", ("repositories", "repository"), 0.38),
|
|
84
|
+
("models", ("models", "entities", "model"), 0.14),
|
|
85
|
+
("contracts", ("protocol", "interface", "contracts"), 0.10),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
_Pattern(
|
|
89
|
+
"monorepo",
|
|
90
|
+
(
|
|
91
|
+
("apps", ("apps", "applications"), 0.30),
|
|
92
|
+
("packages", ("packages", "libs", "libraries"), 0.30),
|
|
93
|
+
("workspace", ("workspace", "workspaces"), 0.15),
|
|
94
|
+
("shared", ("shared", "common"), 0.15),
|
|
95
|
+
("tooling", ("tools", "tooling"), 0.10),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
_Pattern(
|
|
99
|
+
"plugin-oriented",
|
|
100
|
+
(
|
|
101
|
+
("plugins", ("plugins", "plugin", "extensions"), 0.42),
|
|
102
|
+
("adapters", ("adapters", "providers"), 0.22),
|
|
103
|
+
("registry", ("registry", "registries"), 0.18),
|
|
104
|
+
("hooks", ("hooks", "hook"), 0.18),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def analyze(self, graph: ProjectGraph, threshold: float = 0.20) -> ArchitectureReport:
|
|
110
|
+
corpus = self._corpus(graph)
|
|
111
|
+
hypotheses = [self._score(pattern, corpus) for pattern in self._PATTERNS]
|
|
112
|
+
hypotheses = [item for item in hypotheses if item.confidence >= threshold]
|
|
113
|
+
hypotheses.sort(key=lambda item: (-item.confidence, item.name))
|
|
114
|
+
return ArchitectureReport(
|
|
115
|
+
primary=hypotheses[0] if hypotheses else None,
|
|
116
|
+
alternatives=tuple(hypotheses[1:4]),
|
|
117
|
+
analyzed_files=sum(1 for node in graph.nodes if node.kind == "file"),
|
|
118
|
+
analyzed_symbols=sum(
|
|
119
|
+
1 for node in graph.nodes if node.kind not in {"file", "module", "reference"}
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _corpus(graph: ProjectGraph) -> set[str]:
|
|
125
|
+
terms: set[str] = set()
|
|
126
|
+
for node in graph.nodes:
|
|
127
|
+
if node.path:
|
|
128
|
+
path = PurePosixPath(node.path)
|
|
129
|
+
for part in path.parts:
|
|
130
|
+
stem = PurePosixPath(part).stem.lower().replace("-", "_")
|
|
131
|
+
terms.add(stem)
|
|
132
|
+
terms.update(piece for piece in stem.split("_") if piece)
|
|
133
|
+
name = node.name.lower().replace("-", "_")
|
|
134
|
+
terms.add(name)
|
|
135
|
+
terms.update(piece for piece in name.split("_") if piece)
|
|
136
|
+
return terms
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _score(pattern: _Pattern, corpus: set[str]) -> ArchitectureHypothesis:
|
|
140
|
+
confidence = 0.0
|
|
141
|
+
evidence: list[str] = []
|
|
142
|
+
missing: list[str] = []
|
|
143
|
+
matched_groups = 0
|
|
144
|
+
for label, aliases, weight in pattern.groups:
|
|
145
|
+
matches = sorted(alias for alias in aliases if alias in corpus)
|
|
146
|
+
if matches:
|
|
147
|
+
confidence += weight
|
|
148
|
+
matched_groups += 1
|
|
149
|
+
evidence.append(f"{label}: {', '.join(matches[:3])}")
|
|
150
|
+
else:
|
|
151
|
+
missing.append(label)
|
|
152
|
+
# Independent groups matter more than repeated vocabulary from one layer.
|
|
153
|
+
diversity = matched_groups / max(1, len(pattern.groups))
|
|
154
|
+
confidence *= 0.70 + 0.30 * diversity
|
|
155
|
+
return ArchitectureHypothesis(
|
|
156
|
+
name=pattern.name,
|
|
157
|
+
confidence=round(min(1.0, confidence), 4),
|
|
158
|
+
evidence=tuple(evidence),
|
|
159
|
+
missing_signals=tuple(missing),
|
|
160
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Stable optional backend boundary."""
|
|
2
|
+
|
|
3
|
+
from codecortex.backends.context import ContextBackendAdapter
|
|
4
|
+
from codecortex.backends.contracts import (
|
|
5
|
+
BackendCompatibilityError,
|
|
6
|
+
BackendStatus,
|
|
7
|
+
ContextIntelligence,
|
|
8
|
+
GraphIntelligence,
|
|
9
|
+
ManagedBackend,
|
|
10
|
+
SymbolIntelligence,
|
|
11
|
+
)
|
|
12
|
+
from codecortex.backends.graph import GraphBackendAdapter
|
|
13
|
+
from codecortex.backends.manager import BackendManager, BackendProcessError, ProcessResult
|
|
14
|
+
from codecortex.backends.mcp_client import MCPError, MCPStdioClient
|
|
15
|
+
from codecortex.backends.spec import BACKENDS, BackendSpec
|
|
16
|
+
from codecortex.backends.symbols import SymbolBackendAdapter
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BACKENDS",
|
|
20
|
+
"BackendCompatibilityError",
|
|
21
|
+
"BackendManager",
|
|
22
|
+
"BackendProcessError",
|
|
23
|
+
"BackendSpec",
|
|
24
|
+
"BackendStatus",
|
|
25
|
+
"ContextBackendAdapter",
|
|
26
|
+
"ContextIntelligence",
|
|
27
|
+
"GraphBackendAdapter",
|
|
28
|
+
"GraphIntelligence",
|
|
29
|
+
"MCPError",
|
|
30
|
+
"MCPStdioClient",
|
|
31
|
+
"ManagedBackend",
|
|
32
|
+
"ProcessResult",
|
|
33
|
+
"SymbolBackendAdapter",
|
|
34
|
+
"SymbolIntelligence",
|
|
35
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Common adapter behavior and compatibility reporting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from codecortex.backends.contracts import BackendCompatibilityError, BackendStatus
|
|
6
|
+
from codecortex.backends.manager import BackendManager
|
|
7
|
+
from codecortex.backends.spec import BackendSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ManagedAdapterMixin:
|
|
11
|
+
contract_version = 1
|
|
12
|
+
manager: BackendManager
|
|
13
|
+
spec: BackendSpec
|
|
14
|
+
|
|
15
|
+
def status(self) -> BackendStatus:
|
|
16
|
+
installed = self.manager.is_installed(self.spec)
|
|
17
|
+
healthy = self.manager.probe(self.spec, provision=False) if installed else False
|
|
18
|
+
return BackendStatus(
|
|
19
|
+
key=self.spec.key,
|
|
20
|
+
installed=installed,
|
|
21
|
+
healthy=healthy,
|
|
22
|
+
revision=self.spec.revision,
|
|
23
|
+
contract_version=self.contract_version,
|
|
24
|
+
capabilities=self.spec.capabilities,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def require_tools(catalog: list[dict[str, object]], required: set[str]) -> None:
|
|
29
|
+
available = {
|
|
30
|
+
str(item.get("name"))
|
|
31
|
+
for item in catalog
|
|
32
|
+
if isinstance(item.get("name"), str)
|
|
33
|
+
}
|
|
34
|
+
missing = sorted(required - available)
|
|
35
|
+
if missing:
|
|
36
|
+
raise BackendCompatibilityError(
|
|
37
|
+
"backend tool contract mismatch; missing: " + ", ".join(missing)
|
|
38
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Configurable context-processing backend adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from codecortex.backends.base import ManagedAdapterMixin
|
|
12
|
+
from codecortex.backends.manager import BackendManager
|
|
13
|
+
from codecortex.backends.mcp_client import MCPStdioClient
|
|
14
|
+
from codecortex.backends.pool import BackendSessionPool
|
|
15
|
+
from codecortex.backends.spec import BACKENDS
|
|
16
|
+
from codecortex.core.contracts import Engine
|
|
17
|
+
from codecortex.core.models import AgentRequest, Capability, ContextChunk, EngineResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContextBackendAdapter(ManagedAdapterMixin, Engine):
|
|
21
|
+
capability = Capability.CONTEXT
|
|
22
|
+
required_tools = {"context_compress", "context_retrieve", "context_stats"}
|
|
23
|
+
|
|
24
|
+
def __init__(self, project_root: Path, manager: BackendManager | None = None) -> None:
|
|
25
|
+
self.project_root = project_root.resolve()
|
|
26
|
+
self.manager = manager or BackendManager()
|
|
27
|
+
self.spec = BACKENDS["context"]
|
|
28
|
+
self.pool = BackendSessionPool(self.manager)
|
|
29
|
+
|
|
30
|
+
async def health(self) -> bool:
|
|
31
|
+
return await asyncio.to_thread(self.manager.probe, self.spec, False)
|
|
32
|
+
|
|
33
|
+
def server_args(self) -> tuple[str, ...]:
|
|
34
|
+
return ("mcp", "serve", "--transport", "stdio")
|
|
35
|
+
|
|
36
|
+
def _env(self) -> dict[str, str]:
|
|
37
|
+
workspace = self.project_root / ".codecortex" / "context-backend"
|
|
38
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return {"CODECORTEX_CONTEXT_WORKSPACE_DIR": str(workspace)}
|
|
40
|
+
|
|
41
|
+
def _client(self) -> MCPStdioClient:
|
|
42
|
+
return MCPStdioClient(
|
|
43
|
+
self.manager,
|
|
44
|
+
self.spec,
|
|
45
|
+
self.server_args(),
|
|
46
|
+
cwd=self.project_root,
|
|
47
|
+
env=self._env(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def tools(self) -> list[dict[str, Any]]:
|
|
51
|
+
tools = self.pool.tools(
|
|
52
|
+
self.spec,
|
|
53
|
+
self.server_args(),
|
|
54
|
+
cwd=self.project_root,
|
|
55
|
+
env=self._env(),
|
|
56
|
+
)
|
|
57
|
+
self.require_tools(tools, self.required_tools)
|
|
58
|
+
return tools
|
|
59
|
+
|
|
60
|
+
def compress(self, content: str) -> dict[str, Any]:
|
|
61
|
+
return self.call("context_compress", {"content": content})
|
|
62
|
+
|
|
63
|
+
def compress_batch(self, contents: Sequence[str]) -> list[dict[str, Any]]:
|
|
64
|
+
return [self.compress(content) for content in contents]
|
|
65
|
+
|
|
66
|
+
def retrieve(self, hash_key: str) -> dict[str, Any]:
|
|
67
|
+
return self.call("context_retrieve", {"hash": hash_key})
|
|
68
|
+
|
|
69
|
+
def stats(self) -> dict[str, Any]:
|
|
70
|
+
return self.call("context_stats", {})
|
|
71
|
+
|
|
72
|
+
def call(self, tool: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
|
73
|
+
return self.pool.call_tool(
|
|
74
|
+
self.spec,
|
|
75
|
+
self.server_args(),
|
|
76
|
+
tool,
|
|
77
|
+
arguments,
|
|
78
|
+
cwd=self.project_root,
|
|
79
|
+
env=self._env(),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def execute(self, request: AgentRequest) -> EngineResult:
|
|
83
|
+
return await asyncio.to_thread(self._execute_sync, request)
|
|
84
|
+
|
|
85
|
+
def _execute_sync(self, request: AgentRequest) -> EngineResult:
|
|
86
|
+
tool = request.metadata.get("context_tool")
|
|
87
|
+
arguments = request.metadata.get("context_arguments")
|
|
88
|
+
payload = (
|
|
89
|
+
self.call(tool, dict(arguments) if isinstance(arguments, Mapping) else {})
|
|
90
|
+
if isinstance(tool, str)
|
|
91
|
+
else self.compress(request.query)
|
|
92
|
+
)
|
|
93
|
+
tool = tool if isinstance(tool, str) else "context_compress"
|
|
94
|
+
content = MCPStdioClient.content_text(payload) or json.dumps(payload, ensure_ascii=False)
|
|
95
|
+
metadata: dict[str, Any] = {
|
|
96
|
+
"backend": self.spec.key,
|
|
97
|
+
"revision": self.spec.revision,
|
|
98
|
+
"tool": tool,
|
|
99
|
+
}
|
|
100
|
+
structured = payload.get("structuredContent")
|
|
101
|
+
if isinstance(structured, dict):
|
|
102
|
+
metadata["compression"] = structured
|
|
103
|
+
return EngineResult(
|
|
104
|
+
capability=self.capability,
|
|
105
|
+
content=content,
|
|
106
|
+
chunks=[
|
|
107
|
+
ContextChunk(
|
|
108
|
+
source=f"context:{tool}",
|
|
109
|
+
content=content,
|
|
110
|
+
tokens=max(1, len(content) // 4),
|
|
111
|
+
relevance=1.0,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
if content
|
|
116
|
+
else [],
|
|
117
|
+
metadata=metadata,
|
|
118
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Stable CodeCortex-side contracts for optional mature backends.
|
|
2
|
+
|
|
3
|
+
These interfaces are the compatibility boundary. CodeCortex code outside this
|
|
4
|
+
package must not import backend implementation modules or internal APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from codecortex.backends.spec import BackendSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class BackendStatus:
|
|
18
|
+
key: str
|
|
19
|
+
installed: bool
|
|
20
|
+
healthy: bool
|
|
21
|
+
revision: str
|
|
22
|
+
contract_version: int
|
|
23
|
+
capabilities: tuple[str, ...]
|
|
24
|
+
detail: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BackendCompatibilityError(RuntimeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class ManagedBackend(Protocol):
|
|
33
|
+
spec: BackendSpec
|
|
34
|
+
contract_version: int
|
|
35
|
+
|
|
36
|
+
async def health(self) -> bool: ...
|
|
37
|
+
|
|
38
|
+
def status(self) -> BackendStatus: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class GraphIntelligence(ManagedBackend, Protocol):
|
|
43
|
+
def build(self) -> dict[str, Any]: ...
|
|
44
|
+
|
|
45
|
+
def query(self, query: str) -> str: ...
|
|
46
|
+
|
|
47
|
+
def explain(self, node: str) -> str: ...
|
|
48
|
+
|
|
49
|
+
def path(self, source: str, target: str) -> str: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@runtime_checkable
|
|
53
|
+
class SymbolIntelligence(ManagedBackend, Protocol):
|
|
54
|
+
def tools(self) -> list[dict[str, Any]]: ...
|
|
55
|
+
|
|
56
|
+
def call(self, tool: str, arguments: Mapping[str, Any]) -> dict[str, Any]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@runtime_checkable
|
|
60
|
+
class ContextIntelligence(ManagedBackend, Protocol):
|
|
61
|
+
def compress(self, content: str) -> dict[str, Any]: ...
|
|
62
|
+
|
|
63
|
+
def retrieve(self, hash_key: str) -> dict[str, Any]: ...
|
|
64
|
+
|
|
65
|
+
def stats(self) -> dict[str, Any]: ...
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Assembly of CodeCortex-owned intelligence with optional configured adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from codecortex.backends.context import ContextBackendAdapter
|
|
9
|
+
from codecortex.backends.graph import GraphBackendAdapter
|
|
10
|
+
from codecortex.backends.manager import BackendManager
|
|
11
|
+
from codecortex.backends.spec import BACKENDS
|
|
12
|
+
from codecortex.backends.symbols import SymbolBackendAdapter
|
|
13
|
+
from codecortex.config import CortexConfig
|
|
14
|
+
from codecortex.context import IntegratedContextProcessor
|
|
15
|
+
from codecortex.core.contracts import ContextProcessor, MemoryStore
|
|
16
|
+
from codecortex.engines import EngineRegistry
|
|
17
|
+
from codecortex.engines.builtin import build_default_registry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class BackendStack:
|
|
22
|
+
registry: EngineRegistry
|
|
23
|
+
context_processor: ContextProcessor
|
|
24
|
+
manager: BackendManager
|
|
25
|
+
active: tuple[str, ...]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_backend_stack(config: CortexConfig, memory_store: MemoryStore) -> BackendStack:
|
|
29
|
+
registry = build_default_registry(config, memory_store=memory_store)
|
|
30
|
+
manager = BackendManager()
|
|
31
|
+
mode = os.getenv("CODECORTEX_BACKENDS", "builtin").strip().lower()
|
|
32
|
+
if mode == "mature":
|
|
33
|
+
mode = "external"
|
|
34
|
+
if mode not in {"auto", "builtin", "external"}:
|
|
35
|
+
mode = "builtin"
|
|
36
|
+
|
|
37
|
+
active: list[str] = []
|
|
38
|
+
context_backend: ContextBackendAdapter | None = None
|
|
39
|
+
if mode != "builtin":
|
|
40
|
+
graph = GraphBackendAdapter(config.project_root, manager)
|
|
41
|
+
symbols = SymbolBackendAdapter(config.project_root, manager)
|
|
42
|
+
context = ContextBackendAdapter(config.project_root, manager)
|
|
43
|
+
for key, adapter in (("graph", graph), ("symbols", symbols)):
|
|
44
|
+
spec = BACKENDS[key]
|
|
45
|
+
if spec.configured and (mode == "external" or manager.is_installed(spec)):
|
|
46
|
+
registry.register(adapter)
|
|
47
|
+
active.append(key)
|
|
48
|
+
context_spec = BACKENDS["context"]
|
|
49
|
+
if context_spec.configured and (
|
|
50
|
+
mode == "external" or manager.is_installed(context_spec)
|
|
51
|
+
):
|
|
52
|
+
context_backend = context
|
|
53
|
+
active.append("context")
|
|
54
|
+
|
|
55
|
+
return BackendStack(
|
|
56
|
+
registry=registry,
|
|
57
|
+
context_processor=IntegratedContextProcessor(context_backend),
|
|
58
|
+
manager=manager,
|
|
59
|
+
active=tuple(active),
|
|
60
|
+
)
|