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,670 @@
|
|
|
1
|
+
"""Production benchmark harness for real, revision-pinned repositories.
|
|
2
|
+
|
|
3
|
+
The harness intentionally separates measurements from claims. It never fills missing
|
|
4
|
+
metrics with synthetic values: unavailable file-read counts, provider token usage, and
|
|
5
|
+
costs remain ``None`` unless the strategy can observe them directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
17
|
+
from dataclasses import asdict, dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from time import perf_counter
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
from codecortex.backends import (
|
|
23
|
+
BackendManager,
|
|
24
|
+
ContextBackendAdapter,
|
|
25
|
+
GraphBackendAdapter,
|
|
26
|
+
SymbolBackendAdapter,
|
|
27
|
+
)
|
|
28
|
+
from codecortex.backends.mcp_client import MCPStdioClient
|
|
29
|
+
|
|
30
|
+
_TEXT_SUFFIXES = {
|
|
31
|
+
".py",
|
|
32
|
+
".pyi",
|
|
33
|
+
".js",
|
|
34
|
+
".jsx",
|
|
35
|
+
".ts",
|
|
36
|
+
".tsx",
|
|
37
|
+
".go",
|
|
38
|
+
".rs",
|
|
39
|
+
".java",
|
|
40
|
+
".c",
|
|
41
|
+
".h",
|
|
42
|
+
".cc",
|
|
43
|
+
".cpp",
|
|
44
|
+
".hpp",
|
|
45
|
+
".cs",
|
|
46
|
+
".php",
|
|
47
|
+
".rb",
|
|
48
|
+
".kt",
|
|
49
|
+
".kts",
|
|
50
|
+
".scala",
|
|
51
|
+
".swift",
|
|
52
|
+
".vue",
|
|
53
|
+
".svelte",
|
|
54
|
+
".md",
|
|
55
|
+
".toml",
|
|
56
|
+
".yaml",
|
|
57
|
+
".yml",
|
|
58
|
+
".json",
|
|
59
|
+
}
|
|
60
|
+
_EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build", "target"}
|
|
61
|
+
_PATH_RE = re.compile(
|
|
62
|
+
r"(?:^|[\s\[(`'\"])([A-Za-z0-9_.@+-]+(?:/[A-Za-z0-9_.@+-]+)+\.[A-Za-z0-9]+)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
ScenarioName = Literal["vanilla", "graph", "symbols", "context", "full"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class RepositorySpec:
|
|
70
|
+
name: str
|
|
71
|
+
url: str
|
|
72
|
+
revision: str
|
|
73
|
+
cases: tuple[BenchmarkCaseSpec, ...]
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, value: Mapping[str, Any]) -> RepositorySpec:
|
|
77
|
+
return cls(
|
|
78
|
+
name=str(value["name"]),
|
|
79
|
+
url=str(value["url"]),
|
|
80
|
+
revision=str(value["revision"]),
|
|
81
|
+
cases=tuple(BenchmarkCaseSpec.from_dict(item) for item in value.get("cases", [])),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class BenchmarkCaseSpec:
|
|
87
|
+
id: str
|
|
88
|
+
query: str
|
|
89
|
+
expected_paths: tuple[str, ...] = ()
|
|
90
|
+
expected_symbols: tuple[str, ...] = ()
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, value: Mapping[str, Any]) -> BenchmarkCaseSpec:
|
|
94
|
+
return cls(
|
|
95
|
+
id=str(value["id"]),
|
|
96
|
+
query=str(value["query"]),
|
|
97
|
+
expected_paths=tuple(str(item) for item in value.get("expected_paths", [])),
|
|
98
|
+
expected_symbols=tuple(str(item) for item in value.get("expected_symbols", [])),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class ObservedMetrics:
|
|
104
|
+
wall_time_ms: float
|
|
105
|
+
context_chars: int
|
|
106
|
+
estimated_context_tokens: int
|
|
107
|
+
files_read: int | None
|
|
108
|
+
files_surfaced: int
|
|
109
|
+
tool_calls: int
|
|
110
|
+
input_tokens: int | None = None
|
|
111
|
+
output_tokens: int | None = None
|
|
112
|
+
cost_usd: float | None = None
|
|
113
|
+
cost_source: str | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class ScenarioResult:
|
|
118
|
+
repository: str
|
|
119
|
+
revision: str
|
|
120
|
+
case_id: str
|
|
121
|
+
scenario: ScenarioName
|
|
122
|
+
status: Literal["ok", "skipped", "error"]
|
|
123
|
+
success: bool | None
|
|
124
|
+
path_recall: float | None
|
|
125
|
+
symbol_recall: float | None
|
|
126
|
+
metrics: ObservedMetrics | None
|
|
127
|
+
error: str | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class SetupMeasurement:
|
|
132
|
+
repository: str
|
|
133
|
+
scenario: ScenarioName
|
|
134
|
+
wall_time_ms: float
|
|
135
|
+
status: Literal["ok", "skipped", "error"]
|
|
136
|
+
detail: str = ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(slots=True)
|
|
140
|
+
class ProductionBenchmarkReport:
|
|
141
|
+
repositories: list[dict[str, str]] = field(default_factory=list)
|
|
142
|
+
setup: list[SetupMeasurement] = field(default_factory=list)
|
|
143
|
+
results: list[ScenarioResult] = field(default_factory=list)
|
|
144
|
+
|
|
145
|
+
def summary(self) -> dict[str, dict[str, float | int | None]]:
|
|
146
|
+
grouped: dict[str, list[ScenarioResult]] = {}
|
|
147
|
+
for result in self.results:
|
|
148
|
+
grouped.setdefault(result.scenario, []).append(result)
|
|
149
|
+
output: dict[str, dict[str, float | int | None]] = {}
|
|
150
|
+
for scenario, rows in grouped.items():
|
|
151
|
+
completed = [row for row in rows if row.status == "ok" and row.metrics is not None]
|
|
152
|
+
scored = [row for row in completed if row.success is not None]
|
|
153
|
+
output[scenario] = {
|
|
154
|
+
"cases": len(rows),
|
|
155
|
+
"completed": len(completed),
|
|
156
|
+
"success_rate": (
|
|
157
|
+
sum(bool(row.success) for row in scored) / len(scored) if scored else None
|
|
158
|
+
),
|
|
159
|
+
"avg_wall_time_ms": _average(
|
|
160
|
+
row.metrics.wall_time_ms for row in completed if row.metrics
|
|
161
|
+
),
|
|
162
|
+
"avg_estimated_context_tokens": _average(
|
|
163
|
+
row.metrics.estimated_context_tokens for row in completed if row.metrics
|
|
164
|
+
),
|
|
165
|
+
"avg_files_read": _average(
|
|
166
|
+
row.metrics.files_read
|
|
167
|
+
for row in completed
|
|
168
|
+
if row.metrics and row.metrics.files_read is not None
|
|
169
|
+
),
|
|
170
|
+
"avg_files_surfaced": _average(
|
|
171
|
+
row.metrics.files_surfaced for row in completed if row.metrics
|
|
172
|
+
),
|
|
173
|
+
"avg_tool_calls": _average(
|
|
174
|
+
row.metrics.tool_calls for row in completed if row.metrics
|
|
175
|
+
),
|
|
176
|
+
"avg_cost_usd": _average(
|
|
177
|
+
row.metrics.cost_usd
|
|
178
|
+
for row in completed
|
|
179
|
+
if row.metrics and row.metrics.cost_usd is not None
|
|
180
|
+
),
|
|
181
|
+
}
|
|
182
|
+
return output
|
|
183
|
+
|
|
184
|
+
def save(self, output: Path) -> None:
|
|
185
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
payload = {
|
|
187
|
+
"schema_version": 1,
|
|
188
|
+
"measurement_policy": {
|
|
189
|
+
"estimated_context_tokens": "UTF-8 text length divided by four; explicitly estimated",
|
|
190
|
+
"missing_metrics": "null; never synthesized",
|
|
191
|
+
"cost": "reported only when an instrumented agent/provider supplies a measured value",
|
|
192
|
+
},
|
|
193
|
+
"repositories": self.repositories,
|
|
194
|
+
"setup": [asdict(item) for item in self.setup],
|
|
195
|
+
"summary": self.summary(),
|
|
196
|
+
"results": [asdict(item) for item in self.results],
|
|
197
|
+
}
|
|
198
|
+
output.write_text(
|
|
199
|
+
json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
|
|
200
|
+
encoding="utf-8",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True, slots=True)
|
|
205
|
+
class RetrievalObservation:
|
|
206
|
+
text: str
|
|
207
|
+
files_read: int | None
|
|
208
|
+
tool_calls: int
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class RepositoryCheckout:
|
|
212
|
+
"""Materialize a public repository at an exact immutable revision."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, cache_root: Path) -> None:
|
|
215
|
+
self.cache_root = cache_root.resolve()
|
|
216
|
+
|
|
217
|
+
def ensure(self, spec: RepositorySpec) -> Path:
|
|
218
|
+
target = self.cache_root / _safe_name(spec.name) / spec.revision[:12]
|
|
219
|
+
marker = target / ".codecortex-benchmark-revision"
|
|
220
|
+
if marker.exists() and marker.read_text(encoding="utf-8").strip() == spec.revision:
|
|
221
|
+
return target
|
|
222
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
if target.exists():
|
|
224
|
+
_remove_tree(target)
|
|
225
|
+
target.mkdir(parents=True)
|
|
226
|
+
_git(target, "init")
|
|
227
|
+
_git(target, "remote", "add", "origin", spec.url)
|
|
228
|
+
_git(target, "fetch", "--depth", "1", "origin", spec.revision, timeout=600)
|
|
229
|
+
_git(target, "checkout", "--detach", "FETCH_HEAD")
|
|
230
|
+
resolved = _git(target, "rev-parse", "HEAD").strip()
|
|
231
|
+
if resolved != spec.revision:
|
|
232
|
+
raise RuntimeError(
|
|
233
|
+
f"revision mismatch for {spec.name}: expected {spec.revision}, got {resolved}"
|
|
234
|
+
)
|
|
235
|
+
marker.write_text(spec.revision + "\n", encoding="utf-8")
|
|
236
|
+
return target
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ProductionBenchmarkRunner:
|
|
240
|
+
"""Compare a lexical baseline, individual mature engines, and the integrated stack."""
|
|
241
|
+
|
|
242
|
+
scenarios: tuple[ScenarioName, ...] = ("vanilla", "graph", "symbols", "context", "full")
|
|
243
|
+
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
specs: Sequence[RepositorySpec],
|
|
247
|
+
*,
|
|
248
|
+
workspace: Path,
|
|
249
|
+
backend_manager: BackendManager | None = None,
|
|
250
|
+
provision_backends: bool = False,
|
|
251
|
+
result_limit: int = 30,
|
|
252
|
+
) -> None:
|
|
253
|
+
self.specs = tuple(specs)
|
|
254
|
+
self.workspace = workspace.resolve()
|
|
255
|
+
self.checkout = RepositoryCheckout(self.workspace / "repositories")
|
|
256
|
+
self.manager = backend_manager or BackendManager(timeout_seconds=900)
|
|
257
|
+
self.provision_backends = provision_backends
|
|
258
|
+
self.result_limit = result_limit
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def load(
|
|
262
|
+
cls,
|
|
263
|
+
spec_path: Path,
|
|
264
|
+
*,
|
|
265
|
+
workspace: Path,
|
|
266
|
+
backend_manager: BackendManager | None = None,
|
|
267
|
+
provision_backends: bool = False,
|
|
268
|
+
) -> ProductionBenchmarkRunner:
|
|
269
|
+
payload = json.loads(spec_path.read_text(encoding="utf-8"))
|
|
270
|
+
repos = [RepositorySpec.from_dict(item) for item in payload["repositories"]]
|
|
271
|
+
return cls(
|
|
272
|
+
repos,
|
|
273
|
+
workspace=workspace,
|
|
274
|
+
backend_manager=backend_manager,
|
|
275
|
+
provision_backends=provision_backends,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def run(
|
|
279
|
+
self,
|
|
280
|
+
scenarios: Sequence[ScenarioName] | None = None,
|
|
281
|
+
) -> ProductionBenchmarkReport:
|
|
282
|
+
selected = tuple(scenarios or self.scenarios)
|
|
283
|
+
report = ProductionBenchmarkReport()
|
|
284
|
+
for spec in self.specs:
|
|
285
|
+
root = self.checkout.ensure(spec)
|
|
286
|
+
report.repositories.append(
|
|
287
|
+
{"name": spec.name, "url": spec.url, "revision": spec.revision}
|
|
288
|
+
)
|
|
289
|
+
graph = GraphBackendAdapter(root, self.manager)
|
|
290
|
+
symbols = SymbolBackendAdapter(root, self.manager)
|
|
291
|
+
context = ContextBackendAdapter(root, self.manager)
|
|
292
|
+
availability = self._prepare(
|
|
293
|
+
spec,
|
|
294
|
+
root,
|
|
295
|
+
selected,
|
|
296
|
+
graph,
|
|
297
|
+
symbols,
|
|
298
|
+
context,
|
|
299
|
+
report,
|
|
300
|
+
)
|
|
301
|
+
for case in spec.cases:
|
|
302
|
+
baseline: RetrievalObservation | None = None
|
|
303
|
+
for scenario in selected:
|
|
304
|
+
if scenario == "vanilla":
|
|
305
|
+
baseline = self._lexical(root, case)
|
|
306
|
+
report.results.append(
|
|
307
|
+
self._measure(spec, case, scenario, lambda b=baseline: b)
|
|
308
|
+
)
|
|
309
|
+
continue
|
|
310
|
+
if not availability.get(scenario, False):
|
|
311
|
+
report.results.append(
|
|
312
|
+
ScenarioResult(
|
|
313
|
+
repository=spec.name,
|
|
314
|
+
revision=spec.revision,
|
|
315
|
+
case_id=case.id,
|
|
316
|
+
scenario=scenario,
|
|
317
|
+
status="skipped",
|
|
318
|
+
success=None,
|
|
319
|
+
path_recall=None,
|
|
320
|
+
symbol_recall=None,
|
|
321
|
+
metrics=None,
|
|
322
|
+
error="required backend is not provisioned/healthy",
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
continue
|
|
326
|
+
if baseline is None and scenario == "context":
|
|
327
|
+
baseline = self._lexical(root, case)
|
|
328
|
+
operation = self._operation(
|
|
329
|
+
scenario,
|
|
330
|
+
case,
|
|
331
|
+
root,
|
|
332
|
+
graph,
|
|
333
|
+
symbols,
|
|
334
|
+
context,
|
|
335
|
+
baseline,
|
|
336
|
+
)
|
|
337
|
+
report.results.append(self._measure(spec, case, scenario, operation))
|
|
338
|
+
return report
|
|
339
|
+
|
|
340
|
+
def _prepare(
|
|
341
|
+
self,
|
|
342
|
+
spec: RepositorySpec,
|
|
343
|
+
root: Path,
|
|
344
|
+
selected: Sequence[ScenarioName],
|
|
345
|
+
graph: GraphBackendAdapter,
|
|
346
|
+
symbols: SymbolBackendAdapter,
|
|
347
|
+
context: ContextBackendAdapter,
|
|
348
|
+
report: ProductionBenchmarkReport,
|
|
349
|
+
) -> dict[ScenarioName, bool]:
|
|
350
|
+
availability: dict[ScenarioName, bool] = {
|
|
351
|
+
"vanilla": True,
|
|
352
|
+
"graph": False,
|
|
353
|
+
"symbols": False,
|
|
354
|
+
"context": False,
|
|
355
|
+
"full": False,
|
|
356
|
+
}
|
|
357
|
+
adapters = {"graph": graph, "symbols": symbols, "context": context}
|
|
358
|
+
for key, adapter in adapters.items():
|
|
359
|
+
needed = key in selected or "full" in selected
|
|
360
|
+
if not needed:
|
|
361
|
+
continue
|
|
362
|
+
started = perf_counter()
|
|
363
|
+
try:
|
|
364
|
+
if self.provision_backends:
|
|
365
|
+
self.manager.ensure(adapter.spec)
|
|
366
|
+
installed = self.manager.is_installed(adapter.spec)
|
|
367
|
+
if not installed:
|
|
368
|
+
status: Literal["ok", "skipped", "error"] = "skipped"
|
|
369
|
+
detail = "not installed"
|
|
370
|
+
healthy = False
|
|
371
|
+
else:
|
|
372
|
+
healthy = self.manager.probe(adapter.spec, provision=False)
|
|
373
|
+
status = "ok" if healthy else "error"
|
|
374
|
+
detail = "healthy" if healthy else "health probe failed"
|
|
375
|
+
if healthy and key == "graph":
|
|
376
|
+
graph.build()
|
|
377
|
+
elif healthy and key in {"symbols", "context"}:
|
|
378
|
+
adapter.require_tools(adapter.tools(), adapter.required_tools)
|
|
379
|
+
availability[key] = healthy # type: ignore[literal-required]
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
status = "error"
|
|
382
|
+
detail = f"{type(exc).__name__}: {exc}"
|
|
383
|
+
availability[key] = False # type: ignore[literal-required]
|
|
384
|
+
report.setup.append(
|
|
385
|
+
SetupMeasurement(
|
|
386
|
+
repository=spec.name,
|
|
387
|
+
scenario=key, # type: ignore[arg-type]
|
|
388
|
+
wall_time_ms=(perf_counter() - started) * 1000,
|
|
389
|
+
status=status,
|
|
390
|
+
detail=detail,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
availability["full"] = (
|
|
394
|
+
availability["graph"] and availability["symbols"] and availability["context"]
|
|
395
|
+
)
|
|
396
|
+
return availability
|
|
397
|
+
|
|
398
|
+
def _operation(
|
|
399
|
+
self,
|
|
400
|
+
scenario: ScenarioName,
|
|
401
|
+
case: BenchmarkCaseSpec,
|
|
402
|
+
root: Path,
|
|
403
|
+
graph: GraphBackendAdapter,
|
|
404
|
+
symbols: SymbolBackendAdapter,
|
|
405
|
+
context: ContextBackendAdapter,
|
|
406
|
+
baseline: RetrievalObservation | None,
|
|
407
|
+
):
|
|
408
|
+
if scenario == "graph":
|
|
409
|
+
return lambda: RetrievalObservation(graph.query(case.query), None, 1)
|
|
410
|
+
if scenario == "symbols":
|
|
411
|
+
return lambda: self._symbol_observation(symbols, case)
|
|
412
|
+
if scenario == "context":
|
|
413
|
+
assert baseline is not None
|
|
414
|
+
return lambda: self._context_observation(context, baseline)
|
|
415
|
+
if scenario == "full":
|
|
416
|
+
return lambda: self._full_observation(graph, symbols, context, case)
|
|
417
|
+
raise ValueError(scenario)
|
|
418
|
+
|
|
419
|
+
def _symbol_observation(
|
|
420
|
+
self,
|
|
421
|
+
symbols: SymbolBackendAdapter,
|
|
422
|
+
case: BenchmarkCaseSpec,
|
|
423
|
+
) -> RetrievalObservation:
|
|
424
|
+
target = case.expected_symbols[0] if case.expected_symbols else case.query
|
|
425
|
+
payload = symbols.call(
|
|
426
|
+
"find_symbol",
|
|
427
|
+
{"name_path_pattern": target, "include_body": True, "depth": 1},
|
|
428
|
+
)
|
|
429
|
+
return RetrievalObservation(
|
|
430
|
+
MCPStdioClient.content_text(payload) or json.dumps(payload),
|
|
431
|
+
None,
|
|
432
|
+
1,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
def _context_observation(
|
|
436
|
+
self,
|
|
437
|
+
context: ContextBackendAdapter,
|
|
438
|
+
baseline: RetrievalObservation,
|
|
439
|
+
) -> RetrievalObservation:
|
|
440
|
+
payload = context.compress(baseline.text)
|
|
441
|
+
text = MCPStdioClient.content_text(payload) or json.dumps(payload)
|
|
442
|
+
return RetrievalObservation(text, baseline.files_read, baseline.tool_calls + 1)
|
|
443
|
+
|
|
444
|
+
def _full_observation(
|
|
445
|
+
self,
|
|
446
|
+
graph: GraphBackendAdapter,
|
|
447
|
+
symbols: SymbolBackendAdapter,
|
|
448
|
+
context: ContextBackendAdapter,
|
|
449
|
+
case: BenchmarkCaseSpec,
|
|
450
|
+
) -> RetrievalObservation:
|
|
451
|
+
graph_text = graph.query(case.query)
|
|
452
|
+
symbol = self._symbol_observation(symbols, case)
|
|
453
|
+
combined = f"[graph]\n{graph_text}\n\n[symbols]\n{symbol.text}"
|
|
454
|
+
payload = context.compress(combined)
|
|
455
|
+
compressed = MCPStdioClient.content_text(payload) or json.dumps(payload)
|
|
456
|
+
return RetrievalObservation(compressed, None, 3)
|
|
457
|
+
|
|
458
|
+
def _lexical(
|
|
459
|
+
self,
|
|
460
|
+
root: Path,
|
|
461
|
+
case: BenchmarkCaseSpec,
|
|
462
|
+
) -> RetrievalObservation:
|
|
463
|
+
terms = {
|
|
464
|
+
term.lower()
|
|
465
|
+
for term in re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}", case.query)
|
|
466
|
+
}
|
|
467
|
+
scored: list[tuple[int, str, str]] = []
|
|
468
|
+
files_read = 0
|
|
469
|
+
for path in root.rglob("*"):
|
|
470
|
+
if not path.is_file() or path.suffix.lower() not in _TEXT_SUFFIXES:
|
|
471
|
+
continue
|
|
472
|
+
relative = path.relative_to(root)
|
|
473
|
+
if any(part in _EXCLUDED for part in relative.parts):
|
|
474
|
+
continue
|
|
475
|
+
try:
|
|
476
|
+
text = path.read_text(encoding="utf-8")
|
|
477
|
+
except (OSError, UnicodeDecodeError):
|
|
478
|
+
continue
|
|
479
|
+
files_read += 1
|
|
480
|
+
lowered = text.lower()
|
|
481
|
+
path_text = relative.as_posix().lower()
|
|
482
|
+
score = sum(
|
|
483
|
+
lowered.count(term) + (5 if term in path_text else 0)
|
|
484
|
+
for term in terms
|
|
485
|
+
)
|
|
486
|
+
if score:
|
|
487
|
+
scored.append((score, relative.as_posix(), text[:8000]))
|
|
488
|
+
scored.sort(key=lambda row: (-row[0], row[1]))
|
|
489
|
+
selected = scored[: self.result_limit]
|
|
490
|
+
text = "\n\n".join(f"[{path}]\n{content}" for _, path, content in selected)
|
|
491
|
+
return RetrievalObservation(text, files_read, 1)
|
|
492
|
+
|
|
493
|
+
def _measure(
|
|
494
|
+
self,
|
|
495
|
+
spec: RepositorySpec,
|
|
496
|
+
case: BenchmarkCaseSpec,
|
|
497
|
+
scenario: ScenarioName,
|
|
498
|
+
operation,
|
|
499
|
+
) -> ScenarioResult:
|
|
500
|
+
started = perf_counter()
|
|
501
|
+
try:
|
|
502
|
+
observation: RetrievalObservation = operation()
|
|
503
|
+
duration = (perf_counter() - started) * 1000
|
|
504
|
+
path_recall = _evidence_recall(case.expected_paths, observation.text)
|
|
505
|
+
symbol_recall = _evidence_recall(case.expected_symbols, observation.text)
|
|
506
|
+
success = (not case.expected_paths or path_recall > 0) and (
|
|
507
|
+
not case.expected_symbols or symbol_recall > 0
|
|
508
|
+
)
|
|
509
|
+
surfaced = len(_extract_paths(observation.text))
|
|
510
|
+
return ScenarioResult(
|
|
511
|
+
repository=spec.name,
|
|
512
|
+
revision=spec.revision,
|
|
513
|
+
case_id=case.id,
|
|
514
|
+
scenario=scenario,
|
|
515
|
+
status="ok",
|
|
516
|
+
success=success,
|
|
517
|
+
path_recall=path_recall,
|
|
518
|
+
symbol_recall=symbol_recall,
|
|
519
|
+
metrics=ObservedMetrics(
|
|
520
|
+
wall_time_ms=duration,
|
|
521
|
+
context_chars=len(observation.text),
|
|
522
|
+
estimated_context_tokens=max(0, len(observation.text) // 4),
|
|
523
|
+
files_read=observation.files_read,
|
|
524
|
+
files_surfaced=surfaced,
|
|
525
|
+
tool_calls=observation.tool_calls,
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
except Exception as exc:
|
|
529
|
+
return ScenarioResult(
|
|
530
|
+
repository=spec.name,
|
|
531
|
+
revision=spec.revision,
|
|
532
|
+
case_id=case.id,
|
|
533
|
+
scenario=scenario,
|
|
534
|
+
status="error",
|
|
535
|
+
success=None,
|
|
536
|
+
path_recall=None,
|
|
537
|
+
symbol_recall=None,
|
|
538
|
+
metrics=None,
|
|
539
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@dataclass(frozen=True, slots=True)
|
|
544
|
+
class AgentProtocolResult:
|
|
545
|
+
answer: str
|
|
546
|
+
files_read: int | None = None
|
|
547
|
+
tool_calls: int | None = None
|
|
548
|
+
input_tokens: int | None = None
|
|
549
|
+
output_tokens: int | None = None
|
|
550
|
+
cost_usd: float | None = None
|
|
551
|
+
cost_source: str | None = None
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class InstrumentedAgentRunner:
|
|
555
|
+
"""Run an actual coding agent through a JSON protocol without inventing usage metrics.
|
|
556
|
+
|
|
557
|
+
The configured command receives one JSON request on stdin and must emit one JSON object
|
|
558
|
+
on stdout. Metrics omitted by the agent remain null in the benchmark result.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
def __init__(self, command: str, *, timeout_seconds: float = 900.0) -> None:
|
|
562
|
+
self.argv = tuple(shlex.split(command))
|
|
563
|
+
if not self.argv:
|
|
564
|
+
raise ValueError("agent command is empty")
|
|
565
|
+
self.timeout_seconds = timeout_seconds
|
|
566
|
+
|
|
567
|
+
def run(
|
|
568
|
+
self,
|
|
569
|
+
*,
|
|
570
|
+
scenario: ScenarioName,
|
|
571
|
+
repository: Path,
|
|
572
|
+
case: BenchmarkCaseSpec,
|
|
573
|
+
environment: Mapping[str, str] | None = None,
|
|
574
|
+
) -> AgentProtocolResult:
|
|
575
|
+
request = {
|
|
576
|
+
"schema_version": 1,
|
|
577
|
+
"scenario": scenario,
|
|
578
|
+
"repository": str(repository),
|
|
579
|
+
"case": asdict(case),
|
|
580
|
+
}
|
|
581
|
+
process = subprocess.run(
|
|
582
|
+
self.argv,
|
|
583
|
+
cwd=repository,
|
|
584
|
+
env={**os.environ, **dict(environment or {})},
|
|
585
|
+
input=json.dumps(request),
|
|
586
|
+
text=True,
|
|
587
|
+
capture_output=True,
|
|
588
|
+
timeout=self.timeout_seconds,
|
|
589
|
+
check=False,
|
|
590
|
+
)
|
|
591
|
+
if process.returncode != 0:
|
|
592
|
+
raise RuntimeError(
|
|
593
|
+
process.stderr.strip() or f"agent exited with {process.returncode}"
|
|
594
|
+
)
|
|
595
|
+
payload = json.loads(process.stdout)
|
|
596
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("answer"), str):
|
|
597
|
+
raise ValueError("agent must return a JSON object containing string field 'answer'")
|
|
598
|
+
return AgentProtocolResult(
|
|
599
|
+
answer=payload["answer"],
|
|
600
|
+
files_read=_optional_int(payload.get("files_read")),
|
|
601
|
+
tool_calls=_optional_int(payload.get("tool_calls")),
|
|
602
|
+
input_tokens=_optional_int(payload.get("input_tokens")),
|
|
603
|
+
output_tokens=_optional_int(payload.get("output_tokens")),
|
|
604
|
+
cost_usd=_optional_float(payload.get("cost_usd")),
|
|
605
|
+
cost_source=(
|
|
606
|
+
str(payload["cost_source"])
|
|
607
|
+
if payload.get("cost_source") is not None
|
|
608
|
+
else None
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def load_repository_specs(path: Path) -> tuple[RepositorySpec, ...]:
|
|
614
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
615
|
+
return tuple(RepositorySpec.from_dict(item) for item in payload["repositories"])
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _evidence_recall(expected: Sequence[str], text: str) -> float:
|
|
619
|
+
if not expected:
|
|
620
|
+
return 1.0
|
|
621
|
+
lowered = text.lower()
|
|
622
|
+
return sum(item.lower() in lowered for item in expected) / len(expected)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _extract_paths(text: str) -> set[str]:
|
|
626
|
+
return {match.group(1) for match in _PATH_RE.finditer(text)}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _average(values: Iterable[int | float | None]) -> float | None:
|
|
630
|
+
rows = [float(value) for value in values if value is not None]
|
|
631
|
+
return sum(rows) / len(rows) if rows else None
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _optional_int(value: Any) -> int | None:
|
|
635
|
+
return int(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _optional_float(value: Any) -> float | None:
|
|
639
|
+
return (
|
|
640
|
+
float(value)
|
|
641
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
642
|
+
else None
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _safe_name(value: str) -> str:
|
|
647
|
+
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-") or "repository"
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _remove_tree(path: Path) -> None:
|
|
651
|
+
import shutil
|
|
652
|
+
|
|
653
|
+
shutil.rmtree(path, ignore_errors=False)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _git(root: Path, *args: str, timeout: float = 120.0) -> str:
|
|
657
|
+
process = subprocess.run(
|
|
658
|
+
["git", "-C", str(root), *args],
|
|
659
|
+
text=True,
|
|
660
|
+
capture_output=True,
|
|
661
|
+
timeout=timeout,
|
|
662
|
+
check=False,
|
|
663
|
+
)
|
|
664
|
+
if process.returncode != 0:
|
|
665
|
+
raise RuntimeError(process.stderr.strip() or process.stdout.strip())
|
|
666
|
+
return process.stdout
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def temporary_benchmark_workspace() -> tempfile.TemporaryDirectory[str]:
|
|
670
|
+
return tempfile.TemporaryDirectory(prefix="codecortex-benchmark-")
|