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,214 @@
1
+ """Pull-request change intelligence from Git diffs and the repository graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ from codecortex.indexing.graph import GraphNode, ProjectGraph
11
+ from codecortex.indexing.impact import ImpactAnalyzer
12
+ from codecortex.languages import LanguageRegistry
13
+
14
+ _HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(?P<start>\d+)(?:,(?P<count>\d+))? @@")
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class LineRange:
19
+ start: int
20
+ end: int
21
+
22
+ def contains(self, line: int | None) -> bool:
23
+ return line is not None and self.start <= line <= self.end
24
+
25
+ def overlaps(self, other: LineRange) -> bool:
26
+ return self.start <= other.end and other.start <= self.end
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class PRFileChange:
31
+ path: str
32
+ status: str
33
+ additions: int
34
+ deletions: int
35
+ changed_ranges: tuple[LineRange, ...]
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ChangedSymbol:
40
+ node: GraphNode
41
+ direct_change: bool
42
+ impact_risk: float
43
+ affected_nodes: int
44
+ affected_tests: int
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class PRReport:
49
+ base_ref: str
50
+ head_ref: str
51
+ files: tuple[PRFileChange, ...]
52
+ symbols: tuple[ChangedSymbol, ...]
53
+ affected_tests: tuple[str, ...]
54
+ risk_score: float
55
+ risk_level: str
56
+
57
+
58
+ class PRIntelligence:
59
+ def __init__(self, root: Path, graph: ProjectGraph, timeout_seconds: float = 15.0) -> None:
60
+ self.root = root.resolve()
61
+ self.graph = graph
62
+ self.timeout_seconds = timeout_seconds
63
+ self.languages = LanguageRegistry()
64
+
65
+ def _git(self, *args: str) -> str:
66
+ try:
67
+ result = subprocess.run(
68
+ ["git", "-C", str(self.root), *args],
69
+ check=False,
70
+ capture_output=True,
71
+ text=True,
72
+ encoding="utf-8",
73
+ errors="replace",
74
+ timeout=self.timeout_seconds,
75
+ )
76
+ except (OSError, subprocess.TimeoutExpired):
77
+ return ""
78
+ return result.stdout if result.returncode == 0 else ""
79
+
80
+ def analyze(self, base_ref: str, head_ref: str = "HEAD") -> PRReport:
81
+ files = self._file_changes(base_ref, head_ref)
82
+ changed_by_path = {change.path: change for change in files}
83
+ symbols: list[ChangedSymbol] = []
84
+ tests: set[str] = set()
85
+ analyzer = ImpactAnalyzer(self.graph)
86
+ ranges_by_path: dict[str, dict[tuple[str, int], LineRange]] = {}
87
+
88
+ for node in self.graph.nodes:
89
+ if not node.path or node.path not in changed_by_path:
90
+ continue
91
+ if node.kind in {"file", "module", "reference"} or node.line is None:
92
+ continue
93
+ change = changed_by_path[node.path]
94
+ symbol_ranges = ranges_by_path.setdefault(
95
+ node.path,
96
+ self._symbol_ranges(node.path),
97
+ )
98
+ symbol_range = symbol_ranges.get(
99
+ (node.name, node.line),
100
+ LineRange(node.line, node.line),
101
+ )
102
+ direct = not change.changed_ranges or any(
103
+ symbol_range.overlaps(changed) for changed in change.changed_ranges
104
+ )
105
+ if not direct:
106
+ continue
107
+ try:
108
+ impact = analyzer.analyze(node.name)
109
+ affected_nodes = len(impact.direct) + len(impact.indirect)
110
+ affected_tests = len(impact.affected_tests)
111
+ tests.update(
112
+ item.node.path or item.node.name for item in impact.affected_tests
113
+ )
114
+ risk = impact.risk_score
115
+ except ValueError:
116
+ affected_nodes = 0
117
+ affected_tests = 0
118
+ risk = 0.0
119
+ symbols.append(
120
+ ChangedSymbol(
121
+ node=node,
122
+ direct_change=True,
123
+ impact_risk=risk,
124
+ affected_nodes=affected_nodes,
125
+ affected_tests=affected_tests,
126
+ )
127
+ )
128
+
129
+ churn = sum(item.additions + item.deletions for item in files)
130
+ max_impact = max((item.impact_risk for item in symbols), default=0.0)
131
+ breadth = min(1.0, len(files) / 20)
132
+ churn_score = min(1.0, churn / 600)
133
+ risk_score = min(1.0, max_impact * 0.55 + breadth * 0.25 + churn_score * 0.20)
134
+ if risk_score >= 0.70:
135
+ level = "high"
136
+ elif risk_score >= 0.40:
137
+ level = "medium"
138
+ else:
139
+ level = "low"
140
+ symbols.sort(key=lambda item: (-item.impact_risk, item.node.path or "", item.node.line or 0))
141
+ return PRReport(
142
+ base_ref=base_ref,
143
+ head_ref=head_ref,
144
+ files=tuple(files),
145
+ symbols=tuple(symbols),
146
+ affected_tests=tuple(sorted(tests)),
147
+ risk_score=round(risk_score, 4),
148
+ risk_level=level,
149
+ )
150
+
151
+ def _symbol_ranges(self, relative: str) -> dict[tuple[str, int], LineRange]:
152
+ path = self.root / relative
153
+ try:
154
+ source = path.read_text(encoding="utf-8")
155
+ except (OSError, UnicodeDecodeError):
156
+ return {}
157
+ units = sorted(
158
+ self.languages.parse(path, source),
159
+ key=lambda item: (item.line, item.name),
160
+ )
161
+ total_lines = max(1, len(source.splitlines()))
162
+ ranges: dict[tuple[str, int], LineRange] = {}
163
+ for index, unit in enumerate(units):
164
+ if unit.end_line is not None:
165
+ end = unit.end_line
166
+ else:
167
+ next_line = units[index + 1].line if index + 1 < len(units) else total_lines + 1
168
+ end = max(unit.line, next_line - 1)
169
+ ranges[(unit.name, unit.line)] = LineRange(unit.line, end)
170
+ return ranges
171
+
172
+ def _file_changes(self, base_ref: str, head_ref: str) -> list[PRFileChange]:
173
+ range_ref = f"{base_ref}...{head_ref}"
174
+ numstat = self._git("diff", "--numstat", range_ref)
175
+ statuses = self._git("diff", "--name-status", range_ref)
176
+ patch = self._git("diff", "--unified=0", "--no-color", range_ref)
177
+
178
+ stats: dict[str, tuple[int, int]] = {}
179
+ for line in numstat.splitlines():
180
+ parts = line.split("\t")
181
+ if len(parts) >= 3:
182
+ additions = int(parts[0]) if parts[0].isdigit() else 0
183
+ deletions = int(parts[1]) if parts[1].isdigit() else 0
184
+ stats[parts[-1]] = (additions, deletions)
185
+ status_map: dict[str, str] = {}
186
+ for line in statuses.splitlines():
187
+ parts = line.split("\t")
188
+ if len(parts) >= 2:
189
+ status_map[parts[-1]] = parts[0]
190
+ ranges: dict[str, list[LineRange]] = {}
191
+ current_path: str | None = None
192
+ for line in patch.splitlines():
193
+ if line.startswith("+++ b/"):
194
+ current_path = line[6:]
195
+ ranges.setdefault(current_path, [])
196
+ continue
197
+ match = _HUNK.match(line)
198
+ if current_path and match:
199
+ start = int(match.group("start"))
200
+ count = int(match.group("count") or "1")
201
+ if count > 0:
202
+ ranges[current_path].append(LineRange(start, start + count - 1))
203
+
204
+ paths = sorted(set(stats) | set(status_map) | set(ranges))
205
+ return [
206
+ PRFileChange(
207
+ path=path,
208
+ status=status_map.get(path, "M"),
209
+ additions=stats.get(path, (0, 0))[0],
210
+ deletions=stats.get(path, (0, 0))[1],
211
+ changed_ranges=tuple(ranges.get(path, [])),
212
+ )
213
+ for path in paths
214
+ ]
@@ -0,0 +1,16 @@
1
+ """Semantic and hybrid retrieval."""
2
+
3
+ from codecortex.retrieval.hybrid import HybridRetriever, RetrievalHit
4
+ from codecortex.retrieval.index import SemanticDocument, SemanticIndex
5
+ from codecortex.retrieval.providers import EmbeddingProvider, FeatureHashEmbeddingProvider
6
+ from codecortex.retrieval.repository import RepositorySemanticIndex
7
+
8
+ __all__ = [
9
+ "EmbeddingProvider",
10
+ "FeatureHashEmbeddingProvider",
11
+ "HybridRetriever",
12
+ "RepositorySemanticIndex",
13
+ "RetrievalHit",
14
+ "SemanticDocument",
15
+ "SemanticIndex",
16
+ ]
@@ -0,0 +1,67 @@
1
+ """Hybrid retrieval combining vector, lexical, and structural priors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+ from codecortex.retrieval.index import SemanticDocument, SemanticIndex
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class RetrievalHit:
13
+ document: SemanticDocument
14
+ score: float
15
+ vector_score: float
16
+ lexical_score: float
17
+ structural_score: float
18
+
19
+
20
+ class HybridRetriever:
21
+ def __init__(
22
+ self,
23
+ index: SemanticIndex,
24
+ vector_weight: float = 0.60,
25
+ lexical_weight: float = 0.30,
26
+ structural_weight: float = 0.10,
27
+ ) -> None:
28
+ total = vector_weight + lexical_weight + structural_weight
29
+ if total <= 0:
30
+ raise ValueError("retrieval weights must sum to a positive value")
31
+ self.index = index
32
+ self.vector_weight = vector_weight / total
33
+ self.lexical_weight = lexical_weight / total
34
+ self.structural_weight = structural_weight / total
35
+
36
+ def search(self, query: str, limit: int = 20) -> list[RetrievalHit]:
37
+ candidates = self.index.search(query, limit=max(limit * 4, 40))
38
+ query_terms = {term.lower() for term in re.findall(r"[A-Za-z_][\w.-]*", query)}
39
+ hits: list[RetrievalHit] = []
40
+ for match in candidates:
41
+ text_terms = {term.lower() for term in re.findall(r"[A-Za-z_][\w.-]*", match.document.text)}
42
+ lexical = len(query_terms & text_terms) / max(1, len(query_terms))
43
+ metadata = match.document.metadata
44
+ structural = 0.0
45
+ path = str(metadata.get("path", "")).lower()
46
+ symbol = str(metadata.get("symbol", "")).lower()
47
+ if any(term in path for term in query_terms):
48
+ structural += 0.45
49
+ if any(term == symbol for term in query_terms):
50
+ structural += 0.55
51
+ vector_score = (match.score + 1.0) / 2.0
52
+ score = (
53
+ self.vector_weight * vector_score
54
+ + self.lexical_weight * lexical
55
+ + self.structural_weight * min(1.0, structural)
56
+ )
57
+ hits.append(
58
+ RetrievalHit(
59
+ document=match.document,
60
+ score=score,
61
+ vector_score=vector_score,
62
+ lexical_score=lexical,
63
+ structural_score=min(1.0, structural),
64
+ )
65
+ )
66
+ hits.sort(key=lambda item: (-item.score, item.document.id))
67
+ return hits[:limit]
@@ -0,0 +1,135 @@
1
+ """Persistent semantic vector index."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from dataclasses import asdict, dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from codecortex.retrieval.providers import EmbeddingProvider
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class SemanticDocument:
16
+ id: str
17
+ text: str
18
+ metadata: dict[str, Any] = field(default_factory=dict)
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class SemanticMatch:
23
+ document: SemanticDocument
24
+ score: float
25
+
26
+
27
+ class SemanticIndex:
28
+ VERSION = 1
29
+
30
+ def __init__(self, provider: EmbeddingProvider, path: Path | None = None) -> None:
31
+ self.provider = provider
32
+ self.path = path
33
+ self._documents: dict[str, SemanticDocument] = {}
34
+ self._vectors: dict[str, list[float]] = {}
35
+ if path and path.exists():
36
+ self.load()
37
+
38
+ @property
39
+ def document_ids(self) -> set[str]:
40
+ return set(self._documents)
41
+
42
+ def upsert(self, documents: list[SemanticDocument]) -> None:
43
+ if not documents:
44
+ return
45
+ vectors = self.provider.embed([document.text for document in documents])
46
+ for document, vector in zip(documents, vectors, strict=True):
47
+ self._documents[document.id] = document
48
+ self._vectors[document.id] = vector
49
+ if self.path:
50
+ self.save()
51
+
52
+ def replace(self, documents: list[SemanticDocument]) -> None:
53
+ self._documents.clear()
54
+ self._vectors.clear()
55
+ self.upsert(documents)
56
+ if not documents and self.path:
57
+ self.save()
58
+
59
+ def delete(self, ids: set[str]) -> None:
60
+ for document_id in ids:
61
+ self._documents.pop(document_id, None)
62
+ self._vectors.pop(document_id, None)
63
+ if self.path:
64
+ self.save()
65
+
66
+ def search(self, query: str, limit: int = 20, min_score: float = -1.0) -> list[SemanticMatch]:
67
+ if not self._documents:
68
+ return []
69
+ query_vector = self.provider.embed([query])[0]
70
+ ranked: list[tuple[float, str]] = []
71
+ for document_id, vector in self._vectors.items():
72
+ score = self._cosine(query_vector, vector)
73
+ if score >= min_score:
74
+ ranked.append((score, document_id))
75
+ ranked.sort(key=lambda item: (-item[0], item[1]))
76
+ return [
77
+ SemanticMatch(document=self._documents[document_id], score=score)
78
+ for score, document_id in ranked[:limit]
79
+ ]
80
+
81
+ def save(self) -> None:
82
+ if self.path is None:
83
+ return
84
+ self.path.parent.mkdir(parents=True, exist_ok=True)
85
+ payload = {
86
+ "version": self.VERSION,
87
+ "provider": self.provider.name,
88
+ "dimensions": self.provider.dimensions,
89
+ "documents": {
90
+ document_id: asdict(document)
91
+ for document_id, document in sorted(self._documents.items())
92
+ },
93
+ "vectors": self._vectors,
94
+ }
95
+ temp = self.path.with_suffix(self.path.suffix + ".tmp")
96
+ temp.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
97
+ temp.replace(self.path)
98
+
99
+ def load(self) -> None:
100
+ if self.path is None:
101
+ return
102
+ try:
103
+ payload = json.loads(self.path.read_text(encoding="utf-8"))
104
+ except (OSError, json.JSONDecodeError):
105
+ return
106
+ if payload.get("version") != self.VERSION:
107
+ return
108
+ if payload.get("provider") != self.provider.name:
109
+ return
110
+ if int(payload.get("dimensions", -1)) != self.provider.dimensions:
111
+ return
112
+ documents = payload.get("documents", {})
113
+ vectors = payload.get("vectors", {})
114
+ self._documents = {
115
+ document_id: SemanticDocument(
116
+ id=str(value["id"]),
117
+ text=str(value["text"]),
118
+ metadata=dict(value.get("metadata", {})),
119
+ )
120
+ for document_id, value in documents.items()
121
+ }
122
+ self._vectors = {
123
+ document_id: [float(value) for value in vector]
124
+ for document_id, vector in vectors.items()
125
+ if document_id in self._documents
126
+ }
127
+
128
+ @staticmethod
129
+ def _cosine(left: list[float], right: list[float]) -> float:
130
+ if len(left) != len(right):
131
+ return -1.0
132
+ dot = sum(a * b for a, b in zip(left, right, strict=True))
133
+ left_norm = math.sqrt(sum(value * value for value in left)) or 1.0
134
+ right_norm = math.sqrt(sum(value * value for value in right)) or 1.0
135
+ return dot / (left_norm * right_norm)
@@ -0,0 +1,67 @@
1
+ """Embedding provider contracts and local/optional implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import math
7
+ import re
8
+ from typing import Protocol
9
+
10
+ _TOKEN = re.compile(r"[A-Za-z_][A-Za-z0-9_.$:/-]*")
11
+
12
+
13
+ class EmbeddingProvider(Protocol):
14
+ name: str
15
+ dimensions: int
16
+
17
+ def embed(self, texts: list[str]) -> list[list[float]]: ...
18
+
19
+
20
+ class FeatureHashEmbeddingProvider:
21
+ """Deterministic zero-network fallback suitable for local code retrieval."""
22
+
23
+ name = "feature-hash-v1"
24
+
25
+ def __init__(self, dimensions: int = 384) -> None:
26
+ if dimensions < 64:
27
+ raise ValueError("dimensions must be >= 64")
28
+ self.dimensions = dimensions
29
+
30
+ def embed(self, texts: list[str]) -> list[list[float]]:
31
+ return [self._embed_one(text) for text in texts]
32
+
33
+ def _embed_one(self, text: str) -> list[float]:
34
+ vector = [0.0] * self.dimensions
35
+ tokens = [token.lower() for token in _TOKEN.findall(text)]
36
+ features = tokens + [
37
+ f"{left}::{right}"
38
+ for left, right in zip(tokens, tokens[1:], strict=False)
39
+ ]
40
+ for feature in features:
41
+ digest = hashlib.blake2b(feature.encode("utf-8"), digest_size=8).digest()
42
+ raw = int.from_bytes(digest, "little")
43
+ index = raw % self.dimensions
44
+ sign = -1.0 if raw & 1 else 1.0
45
+ vector[index] += sign
46
+ norm = math.sqrt(sum(value * value for value in vector)) or 1.0
47
+ return [value / norm for value in vector]
48
+
49
+
50
+ class SentenceTransformerEmbeddingProvider:
51
+ """Optional local neural provider loaded only when the semantic extra is installed."""
52
+
53
+ def __init__(self, model: str = "sentence-transformers/all-MiniLM-L6-v2") -> None:
54
+ try:
55
+ from sentence_transformers import SentenceTransformer
56
+ except ImportError as exc:
57
+ raise RuntimeError(
58
+ "Install CodeCortex with the 'semantic' extra to use neural embeddings."
59
+ ) from exc
60
+ self._model = SentenceTransformer(model)
61
+ self.name = f"sentence-transformer:{model}"
62
+ probe = self._model.encode(["codecortex"], normalize_embeddings=True)
63
+ self.dimensions = int(len(probe[0]))
64
+
65
+ def embed(self, texts: list[str]) -> list[list[float]]:
66
+ vectors = self._model.encode(texts, normalize_embeddings=True)
67
+ return [[float(value) for value in row] for row in vectors]
@@ -0,0 +1,94 @@
1
+ """Repository-to-semantic-index ingestion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from codecortex.indexing.graph import ProjectGraph
8
+ from codecortex.indexing.incremental_graph import IncrementalGraphIndex
9
+ from codecortex.retrieval.hybrid import HybridRetriever, RetrievalHit
10
+ from codecortex.retrieval.index import SemanticDocument, SemanticIndex
11
+ from codecortex.retrieval.providers import EmbeddingProvider, FeatureHashEmbeddingProvider
12
+
13
+
14
+ class RepositorySemanticIndex:
15
+ def __init__(
16
+ self,
17
+ root: Path,
18
+ provider: EmbeddingProvider | None = None,
19
+ index_path: Path | None = None,
20
+ max_snippet_chars: int = 3_000,
21
+ ) -> None:
22
+ self.root = root.resolve()
23
+ self.provider = provider or FeatureHashEmbeddingProvider()
24
+ self.index_path = index_path or self.root / ".codecortex" / "index" / "semantic.json"
25
+ self.index = SemanticIndex(self.provider, self.index_path)
26
+ self.max_snippet_chars = max_snippet_chars
27
+
28
+ def refresh(self, graph: ProjectGraph | None = None) -> int:
29
+ graph = graph or IncrementalGraphIndex(self.root).refresh()[0]
30
+ documents = [self._document(node, graph) for node in graph.nodes]
31
+ filtered = [document for document in documents if document is not None]
32
+ self.index.replace(filtered)
33
+ return len(filtered)
34
+
35
+ def search(self, query: str, limit: int = 20) -> list[RetrievalHit]:
36
+ if not self.index.document_ids:
37
+ self.refresh()
38
+ return HybridRetriever(self.index).search(query, limit)
39
+
40
+ def _document(self, node, graph: ProjectGraph) -> SemanticDocument | None:
41
+ if node.kind in {"module", "reference"}:
42
+ return None
43
+ metadata = {
44
+ "path": node.path or "",
45
+ "symbol": node.name if node.kind != "file" else "",
46
+ "kind": node.kind,
47
+ "line": node.line or 0,
48
+ }
49
+ structural = self._structural_context(node.id, graph)
50
+ if node.path:
51
+ path = self.root / node.path
52
+ snippet = self._snippet(path, node.line)
53
+ else:
54
+ snippet = ""
55
+ text = "\n".join(
56
+ part
57
+ for part in (
58
+ f"{node.kind} {node.name}",
59
+ f"path {node.path}" if node.path else "",
60
+ structural,
61
+ snippet,
62
+ )
63
+ if part
64
+ )
65
+ return SemanticDocument(id=node.id, text=text, metadata=metadata)
66
+
67
+ def _snippet(self, path: Path, line: int | None) -> str:
68
+ try:
69
+ source = path.read_text(encoding="utf-8")
70
+ except (OSError, UnicodeDecodeError):
71
+ return ""
72
+ if line is None:
73
+ return source[: self.max_snippet_chars]
74
+ lines = source.splitlines()
75
+ start = max(0, line - 8)
76
+ end = min(len(lines), line + 20)
77
+ return "\n".join(lines[start:end])[: self.max_snippet_chars]
78
+
79
+ @staticmethod
80
+ def _structural_context(node_id: str, graph: ProjectGraph) -> str:
81
+ relations: list[str] = []
82
+ node_map = {node.id: node for node in graph.nodes}
83
+ for edge in graph.edges:
84
+ if edge.source == node_id:
85
+ target = node_map.get(edge.target)
86
+ if target:
87
+ relations.append(f"{edge.kind} {target.name}")
88
+ elif edge.target == node_id:
89
+ source = node_map.get(edge.source)
90
+ if source:
91
+ relations.append(f"used-by {source.name} via {edge.kind}")
92
+ if len(relations) >= 20:
93
+ break
94
+ return "\n".join(relations)
@@ -0,0 +1,5 @@
1
+ """Adaptive request routing."""
2
+
3
+ from codecortex.router.router import AdaptiveRouter
4
+
5
+ __all__ = ["AdaptiveRouter"]
@@ -0,0 +1,79 @@
1
+ """Deterministic adaptive router used by the orchestration core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codecortex.core.models import AgentRequest, Capability, RequestKind, RoutePlan, RouteScore
6
+
7
+
8
+ class AdaptiveRouter:
9
+ """Classify requests and produce an observable capability plan."""
10
+
11
+ _kind_terms: dict[RequestKind, tuple[str, ...]] = {
12
+ RequestKind.LOCATE: ("find", "where", "locate", "symbol", "definition", "reference"),
13
+ RequestKind.DEBUG: ("bug", "debug", "error", "fail", "crash", "race", "trace"),
14
+ RequestKind.REFACTOR: ("refactor", "rename", "extract", "move", "restructure"),
15
+ RequestKind.CHANGE: ("change", "edit", "implement", "add", "remove", "fix"),
16
+ RequestKind.REVIEW: ("review", "risk", "regression", "security", "inspect"),
17
+ RequestKind.EXPLAIN: ("explain", "why", "how", "architecture", "understand"),
18
+ }
19
+
20
+ def __init__(self, default_budget: int = 32_000) -> None:
21
+ self.default_budget = default_budget
22
+
23
+ def route(self, request: AgentRequest) -> RoutePlan:
24
+ text = request.query.lower()
25
+ kind = request.kind if request.kind != RequestKind.UNKNOWN else self._classify(text)
26
+
27
+ scores = {
28
+ Capability.REPOSITORY: 0.35,
29
+ Capability.SYMBOLS: 0.30,
30
+ Capability.CONTEXT: 0.45,
31
+ Capability.MEMORY: 0.20,
32
+ Capability.VALIDATION: 0.10,
33
+ }
34
+
35
+ if kind in {RequestKind.EXPLAIN, RequestKind.DEBUG, RequestKind.REVIEW}:
36
+ scores[Capability.REPOSITORY] += 0.45
37
+ if kind in {RequestKind.LOCATE, RequestKind.DEBUG, RequestKind.REFACTOR, RequestKind.CHANGE}:
38
+ scores[Capability.SYMBOLS] += 0.50
39
+ if kind in {RequestKind.DEBUG, RequestKind.REFACTOR, RequestKind.CHANGE, RequestKind.REVIEW}:
40
+ scores[Capability.VALIDATION] += 0.55
41
+ if any(term in text for term in ("history", "previous", "decision", "remember", "again")):
42
+ scores[Capability.MEMORY] += 0.65
43
+ if any(term in text for term in ("large", "logs", "context", "tokens", "many files")):
44
+ scores[Capability.CONTEXT] += 0.35
45
+
46
+ ranked = sorted(scores.items(), key=lambda item: item[1], reverse=True)
47
+ route_scores = [
48
+ RouteScore(
49
+ capability=capability,
50
+ score=min(score, 1.0),
51
+ reason=self._reason(capability, kind),
52
+ )
53
+ for capability, score in ranked
54
+ ]
55
+ selected = [item.capability for item in route_scores if item.score >= 0.50]
56
+ if not selected:
57
+ selected = [Capability.REPOSITORY]
58
+
59
+ return RoutePlan(
60
+ request_kind=kind,
61
+ scores=route_scores,
62
+ selected=selected,
63
+ context_budget=self.default_budget,
64
+ )
65
+
66
+ def _classify(self, text: str) -> RequestKind:
67
+ matches: list[tuple[int, RequestKind]] = []
68
+ for kind, terms in self._kind_terms.items():
69
+ count = sum(1 for term in terms if term in text)
70
+ if count:
71
+ matches.append((count, kind))
72
+ if not matches:
73
+ return RequestKind.EXPLAIN
74
+ matches.sort(key=lambda item: item[0], reverse=True)
75
+ return matches[0][1]
76
+
77
+ @staticmethod
78
+ def _reason(capability: Capability, kind: RequestKind) -> str:
79
+ return f"{capability.value} is relevant to a {kind.value} request"