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,128 @@
1
+ """Warm process/session pool for long-lived isolated MCP backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import threading
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from codecortex.backends.manager import BackendManager
13
+ from codecortex.backends.mcp_client import MCPStdioClient
14
+ from codecortex.backends.spec import BackendSpec
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class _Session:
19
+ client: MCPStdioClient
20
+ lock: threading.RLock
21
+
22
+
23
+ class BackendSessionPool:
24
+ """Keep MCP backend processes warm and serialize traffic per process.
25
+
26
+ A failed request invalidates the process and retries once with a clean session. This
27
+ removes repeated process startup/initialize latency while keeping recovery bounded.
28
+ """
29
+
30
+ def __init__(self, manager: BackendManager) -> None:
31
+ self.manager = manager
32
+ self._sessions: dict[tuple[object, ...], _Session] = {}
33
+ self._lock = threading.RLock()
34
+ atexit.register(self.close_all)
35
+
36
+ @staticmethod
37
+ def _key(
38
+ spec: BackendSpec,
39
+ server_args: Sequence[str],
40
+ cwd: Path | None,
41
+ env: Mapping[str, str] | None,
42
+ ) -> tuple[object, ...]:
43
+ return (
44
+ spec.key,
45
+ spec.revision,
46
+ tuple(server_args),
47
+ str(cwd.resolve()) if cwd else None,
48
+ tuple(sorted((env or {}).items())),
49
+ )
50
+
51
+ def _session(
52
+ self,
53
+ spec: BackendSpec,
54
+ server_args: Sequence[str],
55
+ *,
56
+ cwd: Path | None = None,
57
+ env: Mapping[str, str] | None = None,
58
+ ) -> tuple[tuple[object, ...], _Session]:
59
+ key = self._key(spec, server_args, cwd, env)
60
+ with self._lock:
61
+ session = self._sessions.get(key)
62
+ if session is None:
63
+ client = MCPStdioClient(
64
+ self.manager,
65
+ spec,
66
+ server_args,
67
+ cwd=cwd,
68
+ env=env,
69
+ )
70
+ client.start()
71
+ session = _Session(client=client, lock=threading.RLock())
72
+ self._sessions[key] = session
73
+ return key, session
74
+
75
+ def invalidate(self, key: tuple[object, ...]) -> None:
76
+ with self._lock:
77
+ session = self._sessions.pop(key, None)
78
+ if session is not None:
79
+ session.client.close()
80
+
81
+ def call_tool(
82
+ self,
83
+ spec: BackendSpec,
84
+ server_args: Sequence[str],
85
+ name: str,
86
+ arguments: Mapping[str, Any] | None = None,
87
+ *,
88
+ cwd: Path | None = None,
89
+ env: Mapping[str, str] | None = None,
90
+ ) -> dict[str, Any]:
91
+ last: Exception | None = None
92
+ for attempt in range(2):
93
+ key, session = self._session(spec, server_args, cwd=cwd, env=env)
94
+ try:
95
+ with session.lock:
96
+ return session.client.call_tool(name, arguments)
97
+ except Exception as exc:
98
+ last = exc
99
+ self.invalidate(key)
100
+ if attempt:
101
+ raise
102
+ assert last is not None
103
+ raise last
104
+
105
+ def tools(
106
+ self,
107
+ spec: BackendSpec,
108
+ server_args: Sequence[str],
109
+ *,
110
+ cwd: Path | None = None,
111
+ env: Mapping[str, str] | None = None,
112
+ ) -> list[dict[str, Any]]:
113
+ key, session = self._session(spec, server_args, cwd=cwd, env=env)
114
+ try:
115
+ with session.lock:
116
+ return session.client.tools()
117
+ except Exception:
118
+ self.invalidate(key)
119
+ key, session = self._session(spec, server_args, cwd=cwd, env=env)
120
+ with session.lock:
121
+ return session.client.tools()
122
+
123
+ def close_all(self) -> None:
124
+ with self._lock:
125
+ sessions = list(self._sessions.values())
126
+ self._sessions.clear()
127
+ for session in sessions:
128
+ session.client.close()
@@ -0,0 +1,83 @@
1
+ """Configuration-driven optional backend specifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class BackendSpec:
11
+ key: str
12
+ capabilities: tuple[str, ...]
13
+ package: str = ""
14
+ source_url: str = ""
15
+ revision: str = ""
16
+ command: str = ""
17
+ license_id: str = ""
18
+ extras: tuple[str, ...] = ()
19
+ python: str = "3.13"
20
+ vendor_path: str | None = None
21
+
22
+ @property
23
+ def configured(self) -> bool:
24
+ return bool(
25
+ self.package
26
+ and self.source_url
27
+ and len(self.revision) == 40
28
+ and self.command
29
+ )
30
+
31
+ @property
32
+ def source_requirement(self) -> str:
33
+ if not self.configured:
34
+ raise RuntimeError(f"backend {self.key!r} is not configured")
35
+ if not self.extras:
36
+ return f"git+{self.source_url}@{self.revision}"
37
+ extras = ",".join(self.extras)
38
+ return f"{self.package}[{extras}] @ git+{self.source_url}@{self.revision}"
39
+
40
+
41
+ def _external_spec(
42
+ key: str,
43
+ capabilities: tuple[str, ...],
44
+ *,
45
+ default_extras: tuple[str, ...] = (),
46
+ ) -> BackendSpec:
47
+ prefix = f"CODECORTEX_{key.upper()}_BACKEND"
48
+ extras_value = os.getenv(f"{prefix}_EXTRAS", "").strip()
49
+ extras = (
50
+ tuple(part.strip() for part in extras_value.split(",") if part.strip())
51
+ if extras_value
52
+ else default_extras
53
+ )
54
+ vendor_path = os.getenv(f"{prefix}_LOCAL_PATH", "").strip() or None
55
+ return BackendSpec(
56
+ key=key,
57
+ capabilities=capabilities,
58
+ package=os.getenv(f"{prefix}_PACKAGE", "").strip(),
59
+ source_url=os.getenv(f"{prefix}_SOURCE_URL", "").strip(),
60
+ revision=os.getenv(f"{prefix}_REVISION", "").strip(),
61
+ command=os.getenv(f"{prefix}_COMMAND", "").strip(),
62
+ license_id=os.getenv(f"{prefix}_LICENSE", "").strip(),
63
+ extras=extras,
64
+ python=os.getenv(f"{prefix}_PYTHON", "3.13").strip() or "3.13",
65
+ vendor_path=vendor_path,
66
+ )
67
+
68
+
69
+ BACKENDS: dict[str, BackendSpec] = {
70
+ "graph": _external_spec(
71
+ "graph",
72
+ ("ast", "graph", "query", "path", "explain", "incremental"),
73
+ ),
74
+ "symbols": _external_spec(
75
+ "symbols",
76
+ ("lsp", "symbols", "references", "diagnostics", "editing", "refactor"),
77
+ ),
78
+ "context": _external_spec(
79
+ "context",
80
+ ("compression", "routing", "reversible", "memory", "proxy", "mcp"),
81
+ default_extras=("mcp", "code", "memory", "relevance"),
82
+ ),
83
+ }
@@ -0,0 +1,189 @@
1
+ """IDE-grade semantic symbol backend adapter with guarded edits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import Mapping
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, RequestKind
18
+
19
+
20
+ class SymbolBackendAdapter(ManagedAdapterMixin, Engine):
21
+ capability = Capability.SYMBOLS
22
+ required_tools = {"find_symbol", "find_referencing_symbols"}
23
+ editing_tools = {
24
+ "rename_symbol",
25
+ "replace_symbol_body",
26
+ "insert_before_symbol",
27
+ "insert_after_symbol",
28
+ }
29
+
30
+ def __init__(self, project_root: Path, manager: BackendManager | None = None) -> None:
31
+ self.project_root = project_root.resolve()
32
+ self.manager = manager or BackendManager()
33
+ self.spec = BACKENDS["symbols"]
34
+ self.pool = BackendSessionPool(self.manager)
35
+
36
+ async def health(self) -> bool:
37
+ return await asyncio.to_thread(self.manager.probe, self.spec, False)
38
+
39
+ def server_args(self) -> tuple[str, ...]:
40
+ return (
41
+ "start-mcp-server",
42
+ "--transport",
43
+ "stdio",
44
+ "--project",
45
+ str(self.project_root),
46
+ "--enable-web-dashboard",
47
+ "false",
48
+ "--open-web-dashboard",
49
+ "false",
50
+ )
51
+
52
+ def tools(self) -> list[dict[str, Any]]:
53
+ tools = self.pool.tools(
54
+ self.spec,
55
+ self.server_args(),
56
+ cwd=self.project_root,
57
+ )
58
+ self.require_tools(tools, self.required_tools)
59
+ return tools
60
+
61
+ def call(self, tool: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
62
+ return self.pool.call_tool(
63
+ self.spec,
64
+ self.server_args(),
65
+ tool,
66
+ arguments,
67
+ cwd=self.project_root,
68
+ )
69
+
70
+ def _relative_path(self, value: str) -> str:
71
+ candidate = (self.project_root / value).resolve()
72
+ try:
73
+ relative = candidate.relative_to(self.project_root)
74
+ except ValueError as exc:
75
+ raise ValueError("symbol edits must stay inside the project root") from exc
76
+ if not candidate.exists():
77
+ raise ValueError(f"path does not exist: {value}")
78
+ if not candidate.is_file():
79
+ raise ValueError(f"expected a file path: {value}")
80
+ return relative.as_posix()
81
+
82
+ def _require_edit_tool(self, name: str) -> None:
83
+ available = {str(item.get("name")) for item in self.tools()}
84
+ if name not in available:
85
+ raise RuntimeError(f"symbol backend does not expose required edit tool: {name}")
86
+
87
+ def preflight_symbol(self, name_path: str, relative_path: str) -> dict[str, Any]:
88
+ relative_path = self._relative_path(relative_path)
89
+ return self.call(
90
+ "find_symbol",
91
+ {
92
+ "name_path_pattern": name_path,
93
+ "relative_path": relative_path,
94
+ "include_body": True,
95
+ "max_matches": 2,
96
+ },
97
+ )
98
+
99
+ def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> dict[str, Any]:
100
+ relative_path = self._relative_path(relative_path)
101
+ if not new_name.strip():
102
+ raise ValueError("new_name cannot be empty")
103
+ self._require_edit_tool("rename_symbol")
104
+ self.preflight_symbol(name_path, relative_path)
105
+ return self.call(
106
+ "rename_symbol",
107
+ {"name_path": name_path, "relative_path": relative_path, "new_name": new_name},
108
+ )
109
+
110
+ def replace_symbol_body(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
111
+ relative_path = self._relative_path(relative_path)
112
+ if not body.strip():
113
+ raise ValueError("replacement body cannot be empty")
114
+ self._require_edit_tool("replace_symbol_body")
115
+ self.preflight_symbol(name_path, relative_path)
116
+ return self.call(
117
+ "replace_symbol_body",
118
+ {"name_path": name_path, "relative_path": relative_path, "body": body},
119
+ )
120
+
121
+ def insert_before_symbol(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
122
+ relative_path = self._relative_path(relative_path)
123
+ self._require_edit_tool("insert_before_symbol")
124
+ self.preflight_symbol(name_path, relative_path)
125
+ return self.call(
126
+ "insert_before_symbol",
127
+ {"name_path": name_path, "relative_path": relative_path, "body": body},
128
+ )
129
+
130
+ def insert_after_symbol(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
131
+ relative_path = self._relative_path(relative_path)
132
+ self._require_edit_tool("insert_after_symbol")
133
+ self.preflight_symbol(name_path, relative_path)
134
+ return self.call(
135
+ "insert_after_symbol",
136
+ {"name_path": name_path, "relative_path": relative_path, "body": body},
137
+ )
138
+
139
+ async def execute(self, request: AgentRequest) -> EngineResult:
140
+ return await asyncio.to_thread(self._execute_sync, request)
141
+
142
+ def _execute_sync(self, request: AgentRequest) -> EngineResult:
143
+ explicit_tool = request.metadata.get("symbol_tool")
144
+ explicit_args = request.metadata.get("symbol_arguments")
145
+ if isinstance(explicit_tool, str):
146
+ arguments = dict(explicit_args) if isinstance(explicit_args, Mapping) else {}
147
+ result = self.call(explicit_tool, arguments)
148
+ tool = explicit_tool
149
+ else:
150
+ tool, arguments = self._plan(request)
151
+ result = self.call(tool, arguments)
152
+ content = MCPStdioClient.content_text(result) or json.dumps(result, ensure_ascii=False)
153
+ return EngineResult(
154
+ capability=self.capability,
155
+ content=content,
156
+ chunks=[
157
+ ContextChunk(
158
+ source=f"symbol:{tool}",
159
+ content=content,
160
+ tokens=max(1, len(content) // 4),
161
+ relevance=0.98,
162
+ metadata={"backend": self.spec.key, "tool": tool},
163
+ )
164
+ ]
165
+ if content
166
+ else [],
167
+ metadata={"backend": self.spec.key, "revision": self.spec.revision, "tool": tool},
168
+ )
169
+
170
+ @staticmethod
171
+ def _plan(request: AgentRequest) -> tuple[str, dict[str, Any]]:
172
+ relative_path = request.metadata.get("relative_path")
173
+ if request.kind in {RequestKind.REFACTOR, RequestKind.CHANGE}:
174
+ return "find_symbol", {
175
+ "name_path_pattern": request.query,
176
+ "include_body": True,
177
+ **({"relative_path": relative_path} if isinstance(relative_path, str) else {}),
178
+ }
179
+ if request.metadata.get("references") and isinstance(relative_path, str):
180
+ return "find_referencing_symbols", {
181
+ "name_path": request.query,
182
+ "relative_path": relative_path,
183
+ }
184
+ return "find_symbol", {
185
+ "name_path_pattern": request.query,
186
+ "include_body": request.kind in {RequestKind.DEBUG, RequestKind.REVIEW},
187
+ "depth": 1,
188
+ **({"relative_path": relative_path} if isinstance(relative_path, str) else {}),
189
+ }
@@ -0,0 +1,211 @@
1
+ """Reproducible repository intelligence benchmark harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass
7
+ from pathlib import Path
8
+ from time import perf_counter
9
+ from typing import Protocol
10
+
11
+ from codecortex.indexing.indexer import ProjectIndexer
12
+
13
+ _TEXT_SUFFIXES = {
14
+ ".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".rs", ".java",
15
+ ".c", ".h", ".cc", ".cpp", ".hpp", ".cs", ".php", ".rb",
16
+ ".md", ".toml", ".yaml", ".yml", ".json",
17
+ }
18
+ _EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build"}
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class BenchmarkCase:
23
+ id: str
24
+ query: str
25
+ expected_paths: tuple[str, ...] = ()
26
+ expected_symbols: tuple[str, ...] = ()
27
+
28
+ @classmethod
29
+ def from_dict(cls, value: dict[str, object]) -> BenchmarkCase:
30
+ return cls(
31
+ id=str(value["id"]),
32
+ query=str(value["query"]),
33
+ expected_paths=tuple(str(item) for item in value.get("expected_paths", [])),
34
+ expected_symbols=tuple(str(item) for item in value.get("expected_symbols", [])),
35
+ )
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class StrategyResult:
40
+ strategy: str
41
+ case_id: str
42
+ duration_ms: float
43
+ context_tokens: int
44
+ files_read: int
45
+ tool_calls: int
46
+ path_recall: float
47
+ symbol_recall: float
48
+ success: bool
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class BenchmarkReport:
53
+ results: tuple[StrategyResult, ...]
54
+
55
+ def summary(self) -> dict[str, dict[str, float]]:
56
+ grouped: dict[str, list[StrategyResult]] = {}
57
+ for result in self.results:
58
+ grouped.setdefault(result.strategy, []).append(result)
59
+ summary: dict[str, dict[str, float]] = {}
60
+ for name, rows in grouped.items():
61
+ count = max(1, len(rows))
62
+ summary[name] = {
63
+ "cases": float(len(rows)),
64
+ "success_rate": sum(row.success for row in rows) / count,
65
+ "avg_duration_ms": sum(row.duration_ms for row in rows) / count,
66
+ "avg_context_tokens": sum(row.context_tokens for row in rows) / count,
67
+ "avg_files_read": sum(row.files_read for row in rows) / count,
68
+ "avg_tool_calls": sum(row.tool_calls for row in rows) / count,
69
+ "avg_path_recall": sum(row.path_recall for row in rows) / count,
70
+ "avg_symbol_recall": sum(row.symbol_recall for row in rows) / count,
71
+ }
72
+ return summary
73
+
74
+ def save(self, output: Path) -> None:
75
+ output.parent.mkdir(parents=True, exist_ok=True)
76
+ payload = {
77
+ "summary": self.summary(),
78
+ "results": [asdict(result) for result in self.results],
79
+ }
80
+ output.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
81
+
82
+
83
+ class BenchmarkStrategy(Protocol):
84
+ name: str
85
+
86
+ def run(self, case: BenchmarkCase) -> StrategyResult: ...
87
+
88
+
89
+ def _recall(expected: tuple[str, ...], actual: set[str]) -> float:
90
+ if not expected:
91
+ return 1.0
92
+ normalized = {item.lower() for item in actual}
93
+ hits = sum(
94
+ 1
95
+ for item in expected
96
+ if item.lower() in normalized
97
+ or any(item.lower() in candidate for candidate in normalized)
98
+ )
99
+ return hits / len(expected)
100
+
101
+
102
+ def _success(case: BenchmarkCase, path_recall: float, symbol_recall: float) -> bool:
103
+ paths_ok = not case.expected_paths or path_recall > 0
104
+ symbols_ok = not case.expected_symbols or symbol_recall > 0
105
+ return paths_ok and symbols_ok
106
+
107
+
108
+ class FullTextBaseline:
109
+ """Simple full-repository lexical baseline used for measured comparisons."""
110
+
111
+ name = "full_text_baseline"
112
+
113
+ def __init__(self, root: Path, max_files: int = 10_000, result_limit: int = 50) -> None:
114
+ self.root = root.resolve()
115
+ self.max_files = max_files
116
+ self.result_limit = result_limit
117
+
118
+ def run(self, case: BenchmarkCase) -> StrategyResult:
119
+ started = perf_counter()
120
+ terms = {term.lower() for term in case.query.split() if len(term) > 2}
121
+ scored: list[tuple[int, str, str]] = []
122
+ files_read = 0
123
+ for path in self.root.rglob("*"):
124
+ if files_read >= self.max_files:
125
+ break
126
+ if not path.is_file() or path.suffix.lower() not in _TEXT_SUFFIXES:
127
+ continue
128
+ relative = path.relative_to(self.root)
129
+ if any(part in _EXCLUDED for part in relative.parts):
130
+ continue
131
+ try:
132
+ text = path.read_text(encoding="utf-8")
133
+ except (OSError, UnicodeDecodeError):
134
+ continue
135
+ files_read += 1
136
+ lowered = text.lower()
137
+ path_text = relative.as_posix().lower()
138
+ score = sum(lowered.count(term) + (3 if term in path_text else 0) for term in terms)
139
+ if score:
140
+ scored.append((score, relative.as_posix(), text[:4000]))
141
+ scored.sort(key=lambda item: (-item[0], item[1]))
142
+ selected = scored[: self.result_limit]
143
+ paths = {path for _, path, _ in selected}
144
+ symbols: set[str] = set()
145
+ context = "\n".join(f"[{path}]\n{text}" for _, path, text in selected)
146
+ path_recall = _recall(case.expected_paths, paths)
147
+ symbol_recall = _recall(case.expected_symbols, symbols)
148
+ return StrategyResult(
149
+ strategy=self.name,
150
+ case_id=case.id,
151
+ duration_ms=(perf_counter() - started) * 1000,
152
+ context_tokens=max(0, len(context) // 4),
153
+ files_read=files_read,
154
+ tool_calls=1,
155
+ path_recall=path_recall,
156
+ symbol_recall=symbol_recall,
157
+ success=_success(case, path_recall, symbol_recall),
158
+ )
159
+
160
+
161
+ class CodeCortexGraphStrategy:
162
+ name = "codecortex_graph"
163
+
164
+ def __init__(self, root: Path, result_limit: int = 50) -> None:
165
+ self.root = root.resolve()
166
+ self.result_limit = result_limit
167
+ self.graph = ProjectIndexer(self.root).build()
168
+
169
+ def run(self, case: BenchmarkCase) -> StrategyResult:
170
+ started = perf_counter()
171
+ matches = self.graph.search(case.query, self.result_limit)
172
+ paths = {node.path for node in matches if node.path}
173
+ symbols = {node.name for node in matches if node.kind not in {"file", "module", "reference"}}
174
+ lines = [
175
+ f"{node.kind} {node.name} {node.path or ''}:{node.line or ''}"
176
+ for node in matches
177
+ ]
178
+ context = "\n".join(lines)
179
+ path_recall = _recall(case.expected_paths, {path for path in paths if path})
180
+ symbol_recall = _recall(case.expected_symbols, symbols)
181
+ return StrategyResult(
182
+ strategy=self.name,
183
+ case_id=case.id,
184
+ duration_ms=(perf_counter() - started) * 1000,
185
+ context_tokens=max(0, len(context) // 4),
186
+ files_read=len({path for path in paths if path}),
187
+ tool_calls=1,
188
+ path_recall=path_recall,
189
+ symbol_recall=symbol_recall,
190
+ success=_success(case, path_recall, symbol_recall),
191
+ )
192
+
193
+
194
+ class BenchmarkSuite:
195
+ def __init__(self, cases: list[BenchmarkCase], strategies: list[BenchmarkStrategy]) -> None:
196
+ self.cases = cases
197
+ self.strategies = strategies
198
+
199
+ @classmethod
200
+ def load(cls, path: Path, strategies: list[BenchmarkStrategy]) -> BenchmarkSuite:
201
+ payload = json.loads(path.read_text(encoding="utf-8"))
202
+ cases = [BenchmarkCase.from_dict(item) for item in payload["cases"]]
203
+ return cls(cases, strategies)
204
+
205
+ def run(self) -> BenchmarkReport:
206
+ results = tuple(
207
+ strategy.run(case)
208
+ for case in self.cases
209
+ for strategy in self.strategies
210
+ )
211
+ return BenchmarkReport(results)