oxide-agent 0.1.1__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.
- oxide/__init__.py +57 -0
- oxide/__main__.py +3 -0
- oxide/api.py +468 -0
- oxide/capture.py +231 -0
- oxide/cli.py +177 -0
- oxide/core/__init__.py +1 -0
- oxide/core/capsule.py +221 -0
- oxide/core/indexing.py +262 -0
- oxide/core/languages/__init__.py +0 -0
- oxide/core/languages/c.py +372 -0
- oxide/core/languages/c_resolve.py +170 -0
- oxide/core/languages/cpp.py +474 -0
- oxide/core/languages/cpp_facts.py +109 -0
- oxide/core/languages/cpp_macros.py +92 -0
- oxide/core/languages/cpp_resolve.py +312 -0
- oxide/core/languages/go.py +375 -0
- oxide/core/languages/go_resolve.py +239 -0
- oxide/core/languages/java.py +392 -0
- oxide/core/languages/java_facts.py +128 -0
- oxide/core/languages/java_resolve.py +423 -0
- oxide/core/languages/nesting.py +42 -0
- oxide/core/languages/python.py +487 -0
- oxide/core/languages/python_dispatch.py +173 -0
- oxide/core/languages/python_resolve.py +484 -0
- oxide/core/languages/rust.py +427 -0
- oxide/core/languages/rust_cargo.py +325 -0
- oxide/core/languages/rust_facts.py +251 -0
- oxide/core/languages/rust_macros.py +74 -0
- oxide/core/languages/rust_resolve.py +446 -0
- oxide/core/languages/tsconfig.py +227 -0
- oxide/core/languages/typescript.py +397 -0
- oxide/core/languages/typescript_resolve.py +447 -0
- oxide/core/languages/typescript_syntax.py +447 -0
- oxide/core/navigation.py +130 -0
- oxide/core/ordering.py +85 -0
- oxide/core/packing.py +356 -0
- oxide/core/retrieval.py +181 -0
- oxide/core/routing.py +123 -0
- oxide/core/structural.py +119 -0
- oxide/jev.py +295 -0
- oxide/mcp_server.py +230 -0
- oxide/models.py +338 -0
- oxide/py.typed +0 -0
- oxide/storage/__init__.py +1 -0
- oxide/storage/fts.py +107 -0
- oxide/storage/ladybug.py +441 -0
- oxide/storage/store.py +378 -0
- oxide_agent-0.1.1.dist-info/METADATA +144 -0
- oxide_agent-0.1.1.dist-info/RECORD +52 -0
- oxide_agent-0.1.1.dist-info/WHEEL +4 -0
- oxide_agent-0.1.1.dist-info/entry_points.txt +3 -0
- oxide_agent-0.1.1.dist-info/licenses/LICENSE +21 -0
oxide/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""OXIDE v2: a structural context engine for agents (docs/spec/SPEC.md, ADR-0011)."""
|
|
2
|
+
|
|
3
|
+
from oxide.api import Repository, index_repository, open_repository
|
|
4
|
+
from oxide.models import (
|
|
5
|
+
ChannelStatus,
|
|
6
|
+
ContextBundle,
|
|
7
|
+
ContextItem,
|
|
8
|
+
Entity,
|
|
9
|
+
EntryPoint,
|
|
10
|
+
FileInfo,
|
|
11
|
+
HintIssue,
|
|
12
|
+
Impact,
|
|
13
|
+
ImpactHop,
|
|
14
|
+
IndexReport,
|
|
15
|
+
ItemEntity,
|
|
16
|
+
JudgeStatus,
|
|
17
|
+
Omission,
|
|
18
|
+
OxideError,
|
|
19
|
+
Relation,
|
|
20
|
+
RouteSummary,
|
|
21
|
+
SearchResult,
|
|
22
|
+
Seed,
|
|
23
|
+
Skip,
|
|
24
|
+
Snapshot,
|
|
25
|
+
SourceRef,
|
|
26
|
+
StructuralMatch,
|
|
27
|
+
StructuralResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ChannelStatus",
|
|
32
|
+
"ContextBundle",
|
|
33
|
+
"ContextItem",
|
|
34
|
+
"Entity",
|
|
35
|
+
"EntryPoint",
|
|
36
|
+
"FileInfo",
|
|
37
|
+
"HintIssue",
|
|
38
|
+
"Impact",
|
|
39
|
+
"ImpactHop",
|
|
40
|
+
"IndexReport",
|
|
41
|
+
"ItemEntity",
|
|
42
|
+
"JudgeStatus",
|
|
43
|
+
"Omission",
|
|
44
|
+
"OxideError",
|
|
45
|
+
"Relation",
|
|
46
|
+
"Repository",
|
|
47
|
+
"RouteSummary",
|
|
48
|
+
"SearchResult",
|
|
49
|
+
"Seed",
|
|
50
|
+
"Skip",
|
|
51
|
+
"Snapshot",
|
|
52
|
+
"SourceRef",
|
|
53
|
+
"StructuralMatch",
|
|
54
|
+
"StructuralResult",
|
|
55
|
+
"index_repository",
|
|
56
|
+
"open_repository",
|
|
57
|
+
]
|
oxide/__main__.py
ADDED
oxide/api.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""The public Python API: the stable product boundary of OXIDE v2 (ADR-0011).
|
|
2
|
+
|
|
3
|
+
``index_repository -> search -> build_context`` over persisted repository state::
|
|
4
|
+
|
|
5
|
+
with oxide.open_repository("path/to/repo") as repo:
|
|
6
|
+
repo.index()
|
|
7
|
+
hits = repo.search("where failed requests are retried")
|
|
8
|
+
bundle = repo.build_context("where failed requests are retried", budget=1024)
|
|
9
|
+
print(bundle.payload)
|
|
10
|
+
|
|
11
|
+
Every result names the snapshot it was read from. A ``Repository`` owns its store
|
|
12
|
+
exclusively (a second owner, in any process, gets ``OwnershipConflict``) and is not
|
|
13
|
+
thread-safe.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import gc
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from types import TracebackType
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
from oxide import jev
|
|
27
|
+
from oxide.capture import capture
|
|
28
|
+
from oxide.core import navigation, ordering, packing, retrieval, routing, structural
|
|
29
|
+
from oxide.core.indexing import components, derive, source_dialect, validate
|
|
30
|
+
from oxide.core.retrieval import Hints
|
|
31
|
+
from oxide.models import (
|
|
32
|
+
Closed,
|
|
33
|
+
ContextBundle,
|
|
34
|
+
Entity,
|
|
35
|
+
EntryPoint,
|
|
36
|
+
FileInfo,
|
|
37
|
+
Impact,
|
|
38
|
+
ImpactHop,
|
|
39
|
+
IndexReport,
|
|
40
|
+
InvalidArgument,
|
|
41
|
+
InvalidPath,
|
|
42
|
+
JudgeStatus,
|
|
43
|
+
Relation,
|
|
44
|
+
RouteSummary,
|
|
45
|
+
SearchResult,
|
|
46
|
+
Snapshot,
|
|
47
|
+
StructuralResult,
|
|
48
|
+
)
|
|
49
|
+
from oxide.storage.fts import fts_location, fts_unavailable, prepare_fts
|
|
50
|
+
from oxide.storage.ladybug import View, default_data_dir, storage_components
|
|
51
|
+
from oxide.storage.store import Store
|
|
52
|
+
|
|
53
|
+
# Upper bounds of caller-chosen sizes, checked before any query is built.
|
|
54
|
+
MAX_LIMIT = 1000
|
|
55
|
+
MAX_BUDGET = 1 << 20
|
|
56
|
+
MAX_DEPTH = 8
|
|
57
|
+
_MAX_OFFSET = (1 << 63) - 1 # LadybugDB INT64
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _count(name: str, value: object, high: int) -> int:
|
|
61
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= high:
|
|
62
|
+
raise InvalidArgument(f"{name} must be an integer in [0, {high}], got {value!r}")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _texts(name: str, values: Iterable[str]) -> tuple[str, ...]:
|
|
67
|
+
try:
|
|
68
|
+
out = () if isinstance(values, str) else tuple(values)
|
|
69
|
+
except TypeError:
|
|
70
|
+
out = None
|
|
71
|
+
if out is None or isinstance(values, str) or not all(isinstance(v, str) for v in out):
|
|
72
|
+
raise InvalidArgument(f"{name} must be an iterable of strings, got {values!r}")
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _hints(
|
|
77
|
+
query: str,
|
|
78
|
+
paths: Iterable[str],
|
|
79
|
+
symbols: Iterable[str],
|
|
80
|
+
selection: tuple[str, int, int] | None,
|
|
81
|
+
changed: Iterable[str],
|
|
82
|
+
) -> Hints:
|
|
83
|
+
if not isinstance(query, str):
|
|
84
|
+
raise InvalidArgument(f"query must be a string, got {query!r}")
|
|
85
|
+
if selection is not None:
|
|
86
|
+
if not (isinstance(selection, tuple) and len(selection) == 3):
|
|
87
|
+
raise InvalidArgument(f"selection must be (path, start, end), got {selection!r}")
|
|
88
|
+
path, start, end = selection
|
|
89
|
+
_texts("selection path", [path])
|
|
90
|
+
if _count("selection start", start, _MAX_OFFSET) > _count(
|
|
91
|
+
"selection end", end, _MAX_OFFSET
|
|
92
|
+
):
|
|
93
|
+
raise InvalidArgument(f"selection start {start} is after its end {end}")
|
|
94
|
+
return Hints(
|
|
95
|
+
selection, _texts("paths", paths), _texts("symbols", symbols), _texts("changed", changed)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Repository:
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
path: str | os.PathLike[str],
|
|
103
|
+
data_dir: str | os.PathLike[str] | None = None,
|
|
104
|
+
create: bool = True,
|
|
105
|
+
) -> None:
|
|
106
|
+
try:
|
|
107
|
+
root = Path(path).resolve(strict=True)
|
|
108
|
+
except OSError as error:
|
|
109
|
+
raise InvalidPath(f"{path}: {error.strerror}") from error
|
|
110
|
+
if not root.is_dir():
|
|
111
|
+
raise InvalidPath(f"{root}: not a directory")
|
|
112
|
+
self.root = root
|
|
113
|
+
data = Path(data_dir or default_data_dir()).absolute()
|
|
114
|
+
self._store: Store | None = Store(root, data, create)
|
|
115
|
+
|
|
116
|
+
def __enter__(self) -> Repository:
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def __exit__(
|
|
120
|
+
self,
|
|
121
|
+
kind: type[BaseException] | None,
|
|
122
|
+
error: BaseException | None,
|
|
123
|
+
trace: TracebackType | None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.close()
|
|
126
|
+
|
|
127
|
+
def close(self) -> None:
|
|
128
|
+
if self._store is not None:
|
|
129
|
+
self._store.close()
|
|
130
|
+
self._store = None
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def store(self) -> Store:
|
|
134
|
+
if self._store is None:
|
|
135
|
+
raise Closed(f"{self.root}: repository is closed")
|
|
136
|
+
return self._store
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def repo_id(self) -> str:
|
|
140
|
+
return self.store.repo_id
|
|
141
|
+
|
|
142
|
+
def index(self, *, progress: Callable[[str], None] | None = None) -> IndexReport:
|
|
143
|
+
"""Captures the worktree and publishes a new generation, or reports ``unchanged``
|
|
144
|
+
when the capture and derivation equal the current one. Supported source
|
|
145
|
+
files whose bytes the current generation already parsed reuse its facts;
|
|
146
|
+
resolution and publication always cover the whole capture, so the result equals
|
|
147
|
+
a full rebuild. ``progress`` receives a line as each stage starts."""
|
|
148
|
+
started = time.perf_counter()
|
|
149
|
+
report = progress or (lambda line: None)
|
|
150
|
+
store = self.store
|
|
151
|
+
report(f"capturing {self.root}")
|
|
152
|
+
# The data location is never source, wherever it sits (compared by identity).
|
|
153
|
+
snapshot = capture(self.root, exclude=(store.data, store.data / "repos"))
|
|
154
|
+
lexical_unavailable = fts_unavailable(fts_location())
|
|
155
|
+
parts = components(storage_components(lexical_unavailable is None))
|
|
156
|
+
built = not store.is_current(snapshot.snapshot_id, parts)
|
|
157
|
+
previous: Snapshot | None = None
|
|
158
|
+
changes: tuple[tuple[str, ...], ...] = ((), (), ())
|
|
159
|
+
parsed = reused = 0
|
|
160
|
+
if built:
|
|
161
|
+
old = store.previous()
|
|
162
|
+
before = {f.path: f.digest for f in old.files.values()} if old else {}
|
|
163
|
+
after = {f.path: f.digest for f in snapshot.files}
|
|
164
|
+
changes = (
|
|
165
|
+
tuple(p for p in after if p not in before),
|
|
166
|
+
tuple(p for p in after if p in before and before[p] != after[p]),
|
|
167
|
+
tuple(sorted(p for p in before if p not in after)),
|
|
168
|
+
)
|
|
169
|
+
previous = old.snapshot if old else None
|
|
170
|
+
cached = old.facts() if old else {}
|
|
171
|
+
sources = [
|
|
172
|
+
(lang, f.digest)
|
|
173
|
+
for f in snapshot.files
|
|
174
|
+
if (lang := source_dialect(f.path, f.data)) is not None
|
|
175
|
+
]
|
|
176
|
+
hits = sum(isinstance(s := cached.get(lang), dict) and d in s for lang, d in sources)
|
|
177
|
+
report(
|
|
178
|
+
f"deriving {len(after)} files: parsing {len(sources) - hits} source files, "
|
|
179
|
+
f"reusing {hits}"
|
|
180
|
+
)
|
|
181
|
+
# A build allocates millions of objects that hold no cycles; the cyclic GC only
|
|
182
|
+
# re-scans them (~8% of a build, indexing-throughput at 013e791). The switch
|
|
183
|
+
# is process-global, so the caller's state is restored on every exit.
|
|
184
|
+
enabled = gc.isenabled()
|
|
185
|
+
gc.disable()
|
|
186
|
+
try:
|
|
187
|
+
derived = derive(snapshot, store.repo_id, cached)
|
|
188
|
+
reused, parsed = derived.reused, len(sources) - derived.reused
|
|
189
|
+
if reused < hits:
|
|
190
|
+
report(f"{hits - reused} cached parse facts were unusable and parsed again")
|
|
191
|
+
validate(derived)
|
|
192
|
+
report(
|
|
193
|
+
f"publishing {len(derived.entities)} entities, {len(derived.edges)} relations"
|
|
194
|
+
)
|
|
195
|
+
store.publish(snapshot, derived, parts, lexical_unavailable)
|
|
196
|
+
finally:
|
|
197
|
+
if enabled:
|
|
198
|
+
gc.enable()
|
|
199
|
+
view = store.view()
|
|
200
|
+
files = view.files.values()
|
|
201
|
+
return IndexReport(
|
|
202
|
+
snapshot=view.snapshot,
|
|
203
|
+
status="built" if built else "unchanged",
|
|
204
|
+
files=len(view.files),
|
|
205
|
+
entities=view.counts[0],
|
|
206
|
+
relations=view.counts[1],
|
|
207
|
+
partial=tuple(f.path for f in files if f.coverage == "partial"),
|
|
208
|
+
unsupported=sum(f.coverage == "unsupported" for f in files),
|
|
209
|
+
skipped=snapshot.skipped,
|
|
210
|
+
case_collisions=snapshot.case_collisions,
|
|
211
|
+
lexical=view.lexical_unavailable or "available",
|
|
212
|
+
seconds=time.perf_counter() - started,
|
|
213
|
+
previous=previous,
|
|
214
|
+
added=changes[0],
|
|
215
|
+
modified=changes[1],
|
|
216
|
+
deleted=changes[2],
|
|
217
|
+
parsed=parsed,
|
|
218
|
+
reused=reused,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _view(self) -> View:
|
|
222
|
+
"""The current generation; its compatibility is checked before it is opened."""
|
|
223
|
+
return self.store.view()
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def snapshot(self) -> Snapshot:
|
|
227
|
+
return self._view().snapshot
|
|
228
|
+
|
|
229
|
+
def files(self) -> list[FileInfo]:
|
|
230
|
+
return list(self._view().files.values())
|
|
231
|
+
|
|
232
|
+
def entity(self, key: str) -> Entity | None:
|
|
233
|
+
return self._view().entities([key]).get(key)
|
|
234
|
+
|
|
235
|
+
def relations(self, key: str) -> list[Relation]:
|
|
236
|
+
return self._view().relations(key)
|
|
237
|
+
|
|
238
|
+
def impact(
|
|
239
|
+
self,
|
|
240
|
+
key: str,
|
|
241
|
+
*,
|
|
242
|
+
direction: Literal["upstream", "downstream"] = "upstream",
|
|
243
|
+
depth: int = 3,
|
|
244
|
+
limit: int = 200,
|
|
245
|
+
) -> Impact:
|
|
246
|
+
"""Bounded dependents (``upstream``) or dependencies (``downstream``) of the entity
|
|
247
|
+
``key``, nearest first, each with its proven depth. Read-only; never ranked or
|
|
248
|
+
packed into context."""
|
|
249
|
+
if direction not in ("upstream", "downstream"):
|
|
250
|
+
raise InvalidArgument(f"direction must be upstream or downstream, got {direction!r}")
|
|
251
|
+
_texts("key", [key])
|
|
252
|
+
depth = _count("depth", depth, MAX_DEPTH)
|
|
253
|
+
limit = _count("limit", limit, MAX_LIMIT)
|
|
254
|
+
view = self._view()
|
|
255
|
+
root = view.entities([key]).get(key)
|
|
256
|
+
if root is None:
|
|
257
|
+
raise InvalidArgument(f"no entity {key!r} in this snapshot")
|
|
258
|
+
found = navigation.walk(view, key, direction, depth, limit)
|
|
259
|
+
known = view.entities(h.key for h in found.hops)
|
|
260
|
+
names = [e.name for e in view.entities(found.roots).values() if e.category == "symbol"]
|
|
261
|
+
return Impact(
|
|
262
|
+
view.snapshot,
|
|
263
|
+
root,
|
|
264
|
+
direction,
|
|
265
|
+
found.roots,
|
|
266
|
+
tuple(
|
|
267
|
+
ImpactHop(known[h.key], h.depth, h.proven_depth, h.via, h.cyclic)
|
|
268
|
+
for h in found.hops
|
|
269
|
+
),
|
|
270
|
+
view.mentions(names, ("calls", "references")) if direction == "upstream" else 0,
|
|
271
|
+
found.stopped_by,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def search(
|
|
275
|
+
self,
|
|
276
|
+
query: str,
|
|
277
|
+
*,
|
|
278
|
+
limit: int = retrieval.LEXICAL_LIMIT,
|
|
279
|
+
paths: Iterable[str] = (),
|
|
280
|
+
symbols: Iterable[str] = (),
|
|
281
|
+
selection: tuple[str, int, int] | None = None,
|
|
282
|
+
changed: Iterable[str] = (),
|
|
283
|
+
) -> SearchResult:
|
|
284
|
+
"""Entry-point candidates for inspection; never the final context."""
|
|
285
|
+
hints = _hints(query, paths, symbols, selection, changed)
|
|
286
|
+
limit = _count("limit", limit, MAX_LIMIT)
|
|
287
|
+
view = self._view()
|
|
288
|
+
found = retrieval.retrieve(view, query, hints, lexical_limit=limit)
|
|
289
|
+
entities = view.entities(e.key for e in found.entries)
|
|
290
|
+
entries = tuple(
|
|
291
|
+
EntryPoint(entities[e.key], e.rank, e.score, tuple(e.seeds)) for e in found.entries
|
|
292
|
+
)
|
|
293
|
+
return SearchResult(view.snapshot, query, entries, found.lexical, tuple(found.issues))
|
|
294
|
+
|
|
295
|
+
def structural_search(
|
|
296
|
+
self,
|
|
297
|
+
rule: str | Mapping[str, Any],
|
|
298
|
+
*,
|
|
299
|
+
language: str,
|
|
300
|
+
paths: Iterable[str] = (),
|
|
301
|
+
limit: int = 100,
|
|
302
|
+
) -> StructuralResult:
|
|
303
|
+
"""Nodes of the published snapshot an ast-grep ``rule`` matches: a pattern string
|
|
304
|
+
or a rule config (``{"rule": ..., "constraints": ...}``). Explicit and read-only;
|
|
305
|
+
never ranked or packed. Needs the ``structural`` extra."""
|
|
306
|
+
if language not in structural.GRAMMARS:
|
|
307
|
+
raise InvalidArgument(f"language must be python or typescript, got {language!r}")
|
|
308
|
+
if not (isinstance(rule, Mapping) or (isinstance(rule, str) and rule.strip())):
|
|
309
|
+
raise InvalidArgument(f"rule must be a pattern string or a rule config, got {rule!r}")
|
|
310
|
+
scope = _texts("paths", paths)
|
|
311
|
+
return structural.search(
|
|
312
|
+
self._view(), rule, language, scope, _count("limit", limit, MAX_LIMIT)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def build_context(
|
|
316
|
+
self,
|
|
317
|
+
query: str,
|
|
318
|
+
budget: int,
|
|
319
|
+
*,
|
|
320
|
+
paths: Iterable[str] = (),
|
|
321
|
+
symbols: Iterable[str] = (),
|
|
322
|
+
selection: tuple[str, int, int] | None = None,
|
|
323
|
+
changed: Iterable[str] = (),
|
|
324
|
+
) -> ContextBundle:
|
|
325
|
+
"""A bounded, source-backed context bundle: ``used <= budget`` units. Ordered by
|
|
326
|
+
the optional judge only when this repository authorized it (``judge``)."""
|
|
327
|
+
by = jev.configured(self)
|
|
328
|
+
return self._context(query, budget, paths, symbols, selection, changed, by)
|
|
329
|
+
|
|
330
|
+
def judge(self, enable: bool | None = None) -> JudgeStatus:
|
|
331
|
+
"""The optional judge's status for this repository (ADR-0009). ``True`` authorizes
|
|
332
|
+
``build_context`` to send this repository's source to it, ``False`` revokes that.
|
|
333
|
+
The authorization lives in this repository's store, never in the repository."""
|
|
334
|
+
if enable is not None and not isinstance(enable, bool):
|
|
335
|
+
raise InvalidArgument(f"enable must be True, False or None, got {enable!r}")
|
|
336
|
+
if enable is not None:
|
|
337
|
+
(jev.authorize if enable else jev.revoke)(self)
|
|
338
|
+
return jev.status(self)
|
|
339
|
+
|
|
340
|
+
def _context(
|
|
341
|
+
self,
|
|
342
|
+
query: str,
|
|
343
|
+
budget: int,
|
|
344
|
+
paths: Iterable[str],
|
|
345
|
+
symbols: Iterable[str],
|
|
346
|
+
selection: tuple[str, int, int] | None,
|
|
347
|
+
changed: Iterable[str],
|
|
348
|
+
by: ordering.Order | None,
|
|
349
|
+
) -> ContextBundle:
|
|
350
|
+
"""``build_context``, optionally ordered ``by`` a judge. Internal (ADR-0009 § 5)."""
|
|
351
|
+
hints = _hints(query, paths, symbols, selection, changed)
|
|
352
|
+
budget = _count("budget", budget, MAX_BUDGET)
|
|
353
|
+
view = self._view()
|
|
354
|
+
found = retrieval.retrieve(view, query, hints)
|
|
355
|
+
entries = {e.key: e for e in found.entries}
|
|
356
|
+
route = routing.route(view, list(entries))
|
|
357
|
+
routed = {c.key: c for c in route.candidates}
|
|
358
|
+
later = sorted((k for k in routed if k not in entries), key=lambda k: (routed[k].depth, k))
|
|
359
|
+
order = [k for k in entries if k in routed] + later
|
|
360
|
+
beyond = max(0, len(order) - packing.GRAPH_NODES)
|
|
361
|
+
order = order[: packing.GRAPH_NODES]
|
|
362
|
+
known = view.entities([*order, *(a for k in order for a in packing.ancestors(k))])
|
|
363
|
+
hinted = {k[5:] for k, e in entries.items() if any(s.kind == "path" for s in e.seeds)}
|
|
364
|
+
pool = [
|
|
365
|
+
packing.Candidate(
|
|
366
|
+
known[k],
|
|
367
|
+
routed[k].depth,
|
|
368
|
+
entries[k].rank if k in entries else None,
|
|
369
|
+
bool(entries[k].seeds) if k in entries else False,
|
|
370
|
+
(source := known[k].source) is not None and source.path in hinted,
|
|
371
|
+
entries[k].score if k in entries else None,
|
|
372
|
+
tuple(f"{s.kind}:{s.hint}" for s in entries[k].seeds) if k in entries else (),
|
|
373
|
+
routed[k].origins,
|
|
374
|
+
)
|
|
375
|
+
for k in order
|
|
376
|
+
if k in known
|
|
377
|
+
]
|
|
378
|
+
# Module-level views need the top-level declarations of every parsed file.
|
|
379
|
+
files = [
|
|
380
|
+
c.entity.key
|
|
381
|
+
for c in pool
|
|
382
|
+
if c.entity.category == "file"
|
|
383
|
+
and c.entity.source is not None
|
|
384
|
+
and view.files[c.entity.source.path].coverage != "unsupported"
|
|
385
|
+
]
|
|
386
|
+
children = view.adjacency(files, kinds=("contains",))
|
|
387
|
+
tops = view.entities(
|
|
388
|
+
b for k in files for kind, b, *_ in children[k][0] if kind == "contains"
|
|
389
|
+
)
|
|
390
|
+
declarations = {
|
|
391
|
+
k: [
|
|
392
|
+
(s.start, s.end)
|
|
393
|
+
for kind, b, *_ in children[k][0]
|
|
394
|
+
if kind == "contains" and b in tops and (s := tops[b].source) is not None
|
|
395
|
+
]
|
|
396
|
+
for k in files
|
|
397
|
+
}
|
|
398
|
+
judge = by(query, view.snapshot.derivation_id, view.source) if by else None
|
|
399
|
+
items, omissions = packing.pack(pool, known, budget, view.source, declarations, judge)
|
|
400
|
+
degradations = []
|
|
401
|
+
if found.lexical.state != "ran":
|
|
402
|
+
degradations.append(f"lexical channel {found.lexical.state}: {found.lexical.detail}")
|
|
403
|
+
if route.stopped_by is not None:
|
|
404
|
+
degradations.append(f"routing stopped by its {route.stopped_by} bound")
|
|
405
|
+
if beyond:
|
|
406
|
+
degradations.append(f"{beyond} routed candidates beyond the graph node limit")
|
|
407
|
+
incomplete = {f.path for f in view.files.values() if f.coverage != "complete"}
|
|
408
|
+
used_paths = {c.entity.source.path for c in pool if c.entity.source is not None}
|
|
409
|
+
degradations += [f"incomplete parse: {path}" for path in sorted(used_paths & incomplete)]
|
|
410
|
+
if judge is not None and judge.outcome not in (None, "judged"):
|
|
411
|
+
degradations.append(f"judge {judge.identity} fell back: {judge.outcome}")
|
|
412
|
+
summary = RouteSummary(
|
|
413
|
+
route.root_fallback,
|
|
414
|
+
route.regions,
|
|
415
|
+
len(route.candidates),
|
|
416
|
+
route.deferred,
|
|
417
|
+
route.fanout_truncations,
|
|
418
|
+
route.stopped_by,
|
|
419
|
+
)
|
|
420
|
+
versions = {
|
|
421
|
+
"retrieval": retrieval.RETRIEVAL_VERSION,
|
|
422
|
+
"router": routing.ROUTER_VERSION,
|
|
423
|
+
"selector": packing.SELECTOR_VERSION,
|
|
424
|
+
"payload": packing.PAYLOAD_VERSION,
|
|
425
|
+
"derivation": view.snapshot.derivation_id,
|
|
426
|
+
}
|
|
427
|
+
if judge is not None and judge.outcome == "judged":
|
|
428
|
+
versions |= judge.versions
|
|
429
|
+
return ContextBundle(
|
|
430
|
+
snapshot=view.snapshot,
|
|
431
|
+
query=query,
|
|
432
|
+
budget=budget,
|
|
433
|
+
used=sum(i.units for i in items),
|
|
434
|
+
units=packing.UNITS_VERSION,
|
|
435
|
+
items=tuple(items),
|
|
436
|
+
omissions=tuple(omissions),
|
|
437
|
+
lexical=found.lexical,
|
|
438
|
+
hints=tuple(found.issues),
|
|
439
|
+
route=summary,
|
|
440
|
+
degradations=tuple(degradations),
|
|
441
|
+
versions=versions,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def open_repository(
|
|
446
|
+
path: str | os.PathLike[str],
|
|
447
|
+
*,
|
|
448
|
+
data_dir: str | os.PathLike[str] | None = None,
|
|
449
|
+
create: bool = True,
|
|
450
|
+
) -> Repository:
|
|
451
|
+
"""Opens (and exclusively owns) the store of the repository at ``path``. Nothing is
|
|
452
|
+
indexed until ``index()``; reads before that raise ``NotIndexed``. With
|
|
453
|
+
``create=False`` (read-only clients) a repository without a store raises
|
|
454
|
+
``NotIndexed`` here, and nothing is written."""
|
|
455
|
+
return Repository(path, data_dir, create)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def index_repository(
|
|
459
|
+
path: str | os.PathLike[str], *, data_dir: str | os.PathLike[str] | None = None
|
|
460
|
+
) -> IndexReport:
|
|
461
|
+
with Repository(path, data_dir) as repo:
|
|
462
|
+
return repo.index()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def prepare() -> Path:
|
|
466
|
+
"""Fetches and verifies the pinned FTS extension (``oxide prepare``); returns its
|
|
467
|
+
path. The only network access OXIDE makes, and only when called."""
|
|
468
|
+
return prepare_fts()
|