compass-code 0.1.0__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.
- compass_cli/__init__.py +0 -0
- compass_cli/commands/__init__.py +0 -0
- compass_cli/commands/ingest.py +310 -0
- compass_cli/commands/mcp.py +25 -0
- compass_cli/commands/query.py +181 -0
- compass_cli/commands/serve.py +123 -0
- compass_cli/commands/setup.py +171 -0
- compass_cli/display.py +231 -0
- compass_cli/errors.py +30 -0
- compass_cli/main.py +54 -0
- compass_cli/progress.py +191 -0
- compass_cli/result_views.py +389 -0
- compass_cli/types.py +41 -0
- compass_code-0.1.0.dist-info/METADATA +72 -0
- compass_code-0.1.0.dist-info/RECORD +137 -0
- compass_code-0.1.0.dist-info/WHEEL +4 -0
- compass_code-0.1.0.dist-info/entry_points.txt +2 -0
- compass_core/__init__.py +1 -0
- compass_core/contract/__init__.py +6 -0
- compass_core/contract/errors.py +116 -0
- compass_core/contract/models.py +204 -0
- compass_core/contract/params.py +99 -0
- compass_core/contract/results.py +370 -0
- compass_core/contract/serialize.py +59 -0
- compass_core/contract/types.py +76 -0
- compass_core/errors.py +148 -0
- compass_core/graph/__init__.py +0 -0
- compass_core/graph/db.py +205 -0
- compass_core/graph/schema.py +165 -0
- compass_core/graph/stored_ids.py +133 -0
- compass_core/graph/writer.py +482 -0
- compass_core/index/__init__.py +1 -0
- compass_core/index/freshness.py +55 -0
- compass_core/index/locate.py +155 -0
- compass_core/index/metadata.py +107 -0
- compass_core/ir/__init__.py +0 -0
- compass_core/ir/graph.py +65 -0
- compass_core/ir/models.py +75 -0
- compass_core/languages/__init__.py +1 -0
- compass_core/languages/base.py +26 -0
- compass_core/languages/python/__init__.py +6 -0
- compass_core/languages/python/extractor.py +670 -0
- compass_core/languages/python/import_resolver.py +107 -0
- compass_core/languages/registry.py +89 -0
- compass_core/logger.py +42 -0
- compass_core/phases/__init__.py +0 -0
- compass_core/phases/base.py +58 -0
- compass_core/phases/discovery.py +167 -0
- compass_core/phases/parse.py +80 -0
- compass_core/phases/persistence.py +33 -0
- compass_core/phases/pipeline.py +152 -0
- compass_core/phases/resolution.py +572 -0
- compass_core/phases/symbol_table.py +36 -0
- compass_core/phases/validation.py +76 -0
- compass_core/progress.py +45 -0
- compass_core/py.typed +0 -0
- compass_core/query/__init__.py +1 -0
- compass_core/query/cypher.py +92 -0
- compass_core/query/detail.py +241 -0
- compass_core/query/graph_map.py +127 -0
- compass_core/query/impact.py +273 -0
- compass_core/query/impact_walk.py +352 -0
- compass_core/query/overview.py +221 -0
- compass_core/query/references.py +235 -0
- compass_core/query/rows.py +378 -0
- compass_core/query/search.py +183 -0
- compass_core/query/status.py +49 -0
- compass_core/query/walk_queries.py +202 -0
- compass_core/render.py +408 -0
- compass_core/settings.py +61 -0
- compass_core/shared_types.py +78 -0
- compass_mcp/__init__.py +0 -0
- compass_mcp/guide.py +45 -0
- compass_mcp/py.typed +0 -0
- compass_mcp/server.py +261 -0
- compass_server/__init__.py +5 -0
- compass_server/errors.py +84 -0
- compass_server/main.py +42 -0
- compass_server/py.typed +0 -0
- compass_server/routers/__init__.py +0 -0
- compass_server/routers/query.py +182 -0
- compass_server/settings.py +37 -0
- compass_server/static/404.html +1 -0
- compass_server/static/_next/static/chunks/1629039a.eed2a06658ec4f9c.js +1 -0
- compass_server/static/_next/static/chunks/1ff282bf-bb7c483245264fc9.js +1 -0
- compass_server/static/_next/static/chunks/223.f881a22f446259bf.js +508 -0
- compass_server/static/_next/static/chunks/51fa5c19.6396973222c9d6fd.js +485 -0
- compass_server/static/_next/static/chunks/556.443c1682b6b20128.js +1 -0
- compass_server/static/_next/static/chunks/684-1aa0170f59baf77e.js +1 -0
- compass_server/static/_next/static/chunks/92e71660.3a42d4f70d017951.js +342 -0
- compass_server/static/_next/static/chunks/937-8692224bb6d1b605.js +1 -0
- compass_server/static/_next/static/chunks/app/_not-found/page-2c96ddf295eb6a86.js +1 -0
- compass_server/static/_next/static/chunks/app/layout-4bd0ba4a7d18aff9.js +1 -0
- compass_server/static/_next/static/chunks/app/page-5376a0a98bbd1faa.js +1 -0
- compass_server/static/_next/static/chunks/framework-96dcd958cfe08bd4.js +1 -0
- compass_server/static/_next/static/chunks/main-6dee8e626b16731a.js +1 -0
- compass_server/static/_next/static/chunks/main-app-460e8bc670818718.js +1 -0
- compass_server/static/_next/static/chunks/pages/_app-638a3e7363f92b80.js +1 -0
- compass_server/static/_next/static/chunks/pages/_error-6a72dbbb45cae743.js +1 -0
- compass_server/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- compass_server/static/_next/static/chunks/webpack-597057bf48ce4111.js +1 -0
- compass_server/static/_next/static/css/1b0fbfd8dd653f37.css +3 -0
- compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_buildManifest.js +1 -0
- compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_ssgManifest.js +1 -0
- compass_server/static/_next/static/media/0086f8992871c45b-s.woff2 +0 -0
- compass_server/static/_next/static/media/034d78ad42e9620c-s.woff2 +0 -0
- compass_server/static/_next/static/media/03b436aa846269de-s.woff2 +0 -0
- compass_server/static/_next/static/media/0484562807a97172-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/08aedeceaf1dcd57-s.woff2 +0 -0
- compass_server/static/_next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
- compass_server/static/_next/static/media/29a4aea02fdee119-s.woff2 +0 -0
- compass_server/static/_next/static/media/29e7bbdce9332268-s.woff2 +0 -0
- compass_server/static/_next/static/media/36008747766f78c6-s.woff2 +0 -0
- compass_server/static/_next/static/media/39969fcf98a3026e-s.woff2 +0 -0
- compass_server/static/_next/static/media/4de1fea1a954a5b6-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/5c0c2bcbaa4149ca-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/67957d42bae0796d-s.woff2 +0 -0
- compass_server/static/_next/static/media/6d38f9d4e0f4772b-s.woff2 +0 -0
- compass_server/static/_next/static/media/6d664cce900333ee-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
- compass_server/static/_next/static/media/8888a3826f4a3af4-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
- compass_server/static/_next/static/media/a1386beebedccca4-s.woff2 +0 -0
- compass_server/static/_next/static/media/ab00a911ac2adf48-s.woff2 +0 -0
- compass_server/static/_next/static/media/b957ea75a84b6ea7-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/c3bc380753a8436c-s.woff2 +0 -0
- compass_server/static/_next/static/media/cce080f35d014443-s.woff2 +0 -0
- compass_server/static/_next/static/media/db911767852bc875-s.woff2 +0 -0
- compass_server/static/_next/static/media/eafabf029ad39a43-s.p.woff2 +0 -0
- compass_server/static/_next/static/media/f10b8e9d91f3edcb-s.woff2 +0 -0
- compass_server/static/_next/static/media/f911b923c6adde36-s.woff2 +0 -0
- compass_server/static/_next/static/media/fe0777f1195381cb-s.woff2 +0 -0
- compass_server/static/index.html +1 -0
- compass_server/static/index.txt +25 -0
- compass_server/web_bundle.py +27 -0
- compass_server/web_limits.py +48 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Map raw graph rows to public query models.
|
|
2
|
+
|
|
3
|
+
This is, with ``graph/db.py``, the only module allowed to touch untyped (``Any``)
|
|
4
|
+
driver values. Every column is validated here so the rest of the query layer works
|
|
5
|
+
with typed models, and a schema mismatch surfaces as ``IndexIncompatibleError``.
|
|
6
|
+
|
|
7
|
+
The graph stores exactly what the public models carry: class and function ids are
|
|
8
|
+
public ids (``app/models.py::User.save``), ``file_path`` values are repo-relative POSIX
|
|
9
|
+
paths, and ``qualified_name`` is stored (``app.models.User.save``). Files and external
|
|
10
|
+
functions get their public ids here: ``file::<path>`` and ``ext::<full_ref>``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from pathlib import PurePosixPath
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from compass_core.contract.models import SymbolRef
|
|
20
|
+
from compass_core.contract.types import QualifiedName, RepoPath, SymbolId
|
|
21
|
+
from compass_core.errors import IndexIncompatibleError
|
|
22
|
+
from compass_core.shared_types import CallType, SymbolKind
|
|
23
|
+
|
|
24
|
+
_SEP = "::"
|
|
25
|
+
_CONFIDENCE_DIGITS = 6
|
|
26
|
+
|
|
27
|
+
FUNCTION_FIELDS = (
|
|
28
|
+
"id",
|
|
29
|
+
"name",
|
|
30
|
+
"qualified_name",
|
|
31
|
+
"file_path",
|
|
32
|
+
"line_start",
|
|
33
|
+
"line_end",
|
|
34
|
+
"signature",
|
|
35
|
+
"is_method",
|
|
36
|
+
)
|
|
37
|
+
"""``Function`` properties read by ``function_from_row``, in column order."""
|
|
38
|
+
CLASS_FIELDS = ("id", "name", "qualified_name", "file_path", "line_start", "line_end")
|
|
39
|
+
"""``Class`` properties read by ``class_from_row``, in column order."""
|
|
40
|
+
FILE_FIELDS = ("path", "module_name")
|
|
41
|
+
"""``File`` properties read by ``file_from_row``, in column order."""
|
|
42
|
+
FUNCTION_WIDTH = len(FUNCTION_FIELDS)
|
|
43
|
+
CLASS_WIDTH = len(CLASS_FIELDS)
|
|
44
|
+
FILE_WIDTH = len(FILE_FIELDS)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def columns(variable: str, fields: Sequence[str]) -> str:
|
|
48
|
+
"""Render a ``RETURN`` column list for one node variable.
|
|
49
|
+
|
|
50
|
+
Used only to compose module-level ``Query`` constants, so the column order a query
|
|
51
|
+
returns and the order its decoder reads are the same list.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
variable: Cypher variable bound to the node, e.g. ``"n"``.
|
|
55
|
+
fields: Property names, e.g. ``FUNCTION_FIELDS``.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The comma-separated column list.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
``columns("a", FILE_FIELDS)`` returns ``"a.path, a.module_name"``.
|
|
62
|
+
"""
|
|
63
|
+
return ", ".join(f"{variable}.{name}" for name in fields)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _text(value: Any, column: str) -> str:
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
raise IndexIncompatibleError(
|
|
69
|
+
f"unexpected column type for {column}: expected str, got {type(value).__name__}"
|
|
70
|
+
)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _line(value: Any, column: str) -> int | None:
|
|
75
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
76
|
+
raise IndexIncompatibleError(
|
|
77
|
+
f"unexpected column type for {column}: expected int, got {type(value).__name__}"
|
|
78
|
+
)
|
|
79
|
+
return value if value > 0 else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _expect_width(row: Sequence[Any], width: int, what: str) -> None:
|
|
83
|
+
if len(row) < width:
|
|
84
|
+
raise IndexIncompatibleError(
|
|
85
|
+
f"unexpected column count for {what}: expected {width}, got {len(row)}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def public_file_id(path: str) -> SymbolId:
|
|
90
|
+
"""Build the public id of a file from its stored path.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Repo-relative POSIX path, as stored.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
``file::<path>``.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
``public_file_id("app/auth.py")`` returns ``"file::app/auth.py"``.
|
|
100
|
+
"""
|
|
101
|
+
return SymbolId(f"file{_SEP}{path}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _definition(row: Sequence[Any], kind: SymbolKind, signature: str | None) -> SymbolRef:
|
|
105
|
+
return SymbolRef(
|
|
106
|
+
id=SymbolId(_text(row[0], "id")),
|
|
107
|
+
kind=kind,
|
|
108
|
+
name=_text(row[1], "name"),
|
|
109
|
+
qualified_name=QualifiedName(_text(row[2], "qualified_name")),
|
|
110
|
+
file_path=RepoPath(_text(row[3], "file_path")),
|
|
111
|
+
line_start=_line(row[4], "line_start"),
|
|
112
|
+
line_end=_line(row[5], "line_end"),
|
|
113
|
+
signature=signature,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def symbol_from_row(row: Sequence[Any], kind: SymbolKind) -> SymbolRef:
|
|
118
|
+
"""Build a function, method or class ``SymbolRef`` from a graph row.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
row: Columns ``[id, name, qualified_name, file_path, line_start, line_end,
|
|
122
|
+
signature]``; extra trailing columns are ignored. A stored line of ``0``
|
|
123
|
+
means "unknown".
|
|
124
|
+
kind: Kind to assign (``FUNCTION``, ``METHOD`` or ``CLASS``); a class gets no
|
|
125
|
+
signature.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The symbol.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
IndexIncompatibleError: A column is missing or has an unexpected type.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
``["app/auth.py::parse", "parse", "app.auth.parse", "app/auth.py", 6, 7,
|
|
135
|
+
"def parse(raw):"]`` becomes ``SymbolRef(id="app/auth.py::parse",
|
|
136
|
+
qualified_name="app.auth.parse", line_start=6, ...)``.
|
|
137
|
+
"""
|
|
138
|
+
_expect_width(row, 7, "symbol row")
|
|
139
|
+
signature = _text(row[6], "signature") or None
|
|
140
|
+
return _definition(row, kind, None if kind is SymbolKind.CLASS else signature)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def file_from_row(row: Sequence[Any]) -> SymbolRef:
|
|
144
|
+
"""Build a ``FILE`` ``SymbolRef`` from a row of ``FILE_FIELDS`` columns.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
row: Columns ``[path, module_name]``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The file symbol with id ``file::<path>``.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
IndexIncompatibleError: A column is missing or has an unexpected type.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
``["app/auth.py", "app.auth"]`` becomes ``SymbolRef(id="file::app/auth.py",
|
|
157
|
+
name="auth.py", qualified_name="app.auth", ...)``.
|
|
158
|
+
"""
|
|
159
|
+
_expect_width(row, FILE_WIDTH, "file row")
|
|
160
|
+
path = _text(row[0], "path")
|
|
161
|
+
return SymbolRef(
|
|
162
|
+
id=public_file_id(path),
|
|
163
|
+
kind=SymbolKind.FILE,
|
|
164
|
+
name=PurePosixPath(path).name,
|
|
165
|
+
qualified_name=QualifiedName(_text(row[1], "module_name")),
|
|
166
|
+
file_path=RepoPath(path),
|
|
167
|
+
line_start=None,
|
|
168
|
+
line_end=None,
|
|
169
|
+
signature=None,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def external_from_row(row: Sequence[Any]) -> SymbolRef:
|
|
174
|
+
"""Build an ``EXTERNAL`` ``SymbolRef`` from a ``[full_ref, name]`` row.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
row: Columns ``[full_ref, name]``.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The external symbol with id ``ext::<full_ref>``.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
IndexIncompatibleError: A column is missing or has an unexpected type.
|
|
184
|
+
"""
|
|
185
|
+
_expect_width(row, 2, "external row")
|
|
186
|
+
full_ref = _text(row[0], "full_ref")
|
|
187
|
+
return SymbolRef(
|
|
188
|
+
id=SymbolId(f"ext{_SEP}{full_ref}"),
|
|
189
|
+
kind=SymbolKind.EXTERNAL,
|
|
190
|
+
name=_text(row[1], "name"),
|
|
191
|
+
qualified_name=QualifiedName(full_ref),
|
|
192
|
+
file_path=None,
|
|
193
|
+
line_start=None,
|
|
194
|
+
line_end=None,
|
|
195
|
+
signature=None,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def is_method_flag(value: Any) -> bool:
|
|
200
|
+
"""Validate a ``Function.is_method`` column.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
value: Raw column value.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The flag as a ``bool``.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
IndexIncompatibleError: The value is not a boolean.
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(value, bool):
|
|
212
|
+
raise IndexIncompatibleError(
|
|
213
|
+
f"unexpected column type for is_method: expected bool, got {type(value).__name__}"
|
|
214
|
+
)
|
|
215
|
+
return value
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def count_value(value: Any) -> int:
|
|
219
|
+
"""Validate a count column.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
value: Raw column value.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The count as an ``int``.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
IndexIncompatibleError: The value is not an integer.
|
|
229
|
+
"""
|
|
230
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
231
|
+
raise IndexIncompatibleError(
|
|
232
|
+
f"unexpected column type for count: expected int, got {type(value).__name__}"
|
|
233
|
+
)
|
|
234
|
+
return value
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def function_from_row(row: Sequence[Any]) -> SymbolRef:
|
|
238
|
+
"""Build a function or method ``SymbolRef`` from a row of ``FUNCTION_FIELDS`` columns.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
row: Columns in ``FUNCTION_FIELDS`` order.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The symbol, with kind ``METHOD`` when ``is_method`` is true, else ``FUNCTION``.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
IndexIncompatibleError: A column is missing or has an unexpected type.
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
``["app/models.py::User.save", "save", "app.models.User.save", "app/models.py", 6,
|
|
251
|
+
7, "def save(self):", True]`` becomes a ``METHOD`` with id
|
|
252
|
+
``"app/models.py::User.save"``.
|
|
253
|
+
"""
|
|
254
|
+
_expect_width(row, FUNCTION_WIDTH, "function row")
|
|
255
|
+
kind = SymbolKind.METHOD if is_method_flag(row[7]) else SymbolKind.FUNCTION
|
|
256
|
+
return symbol_from_row(row, kind)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def class_from_row(row: Sequence[Any]) -> SymbolRef:
|
|
260
|
+
"""Build a ``CLASS`` ``SymbolRef`` from a row of ``CLASS_FIELDS`` columns.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
row: Columns in ``CLASS_FIELDS`` order; extra trailing columns are ignored.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The class symbol; classes have no signature.
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
IndexIncompatibleError: A column is missing or has an unexpected type.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
``["app/models.py::User", "User", "app.models.User", "app/models.py", 4, 9]``
|
|
273
|
+
becomes ``SymbolRef(id="app/models.py::User", kind=SymbolKind.CLASS, ...)``.
|
|
274
|
+
"""
|
|
275
|
+
_expect_width(row, CLASS_WIDTH, "class row")
|
|
276
|
+
return _definition(row, SymbolKind.CLASS, None)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def text_value(value: Any, column: str) -> str:
|
|
280
|
+
"""Validate a string column.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
value: Raw column value.
|
|
284
|
+
column: Column name, for the error message.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The value as a ``str``.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
IndexIncompatibleError: The value is not a string.
|
|
291
|
+
"""
|
|
292
|
+
return _text(value, column)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def text_list_value(value: Any, column: str) -> tuple[str, ...]:
|
|
296
|
+
"""Validate a ``STRING[]`` column such as ``Function.raw_calls``.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
value: Raw column value; a stored ``NULL`` reads as an empty tuple.
|
|
300
|
+
column: Column name, for the error message.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
The strings, in stored order.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
IndexIncompatibleError: The value is not a list of strings.
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
``text_list_value(["parse"], "raw_calls")`` returns ``("parse",)``.
|
|
310
|
+
"""
|
|
311
|
+
if value is None:
|
|
312
|
+
return ()
|
|
313
|
+
if not isinstance(value, list):
|
|
314
|
+
raise IndexIncompatibleError(
|
|
315
|
+
f"unexpected column type for {column}: expected list, got {type(value).__name__}"
|
|
316
|
+
)
|
|
317
|
+
return tuple(_text(item, column) for item in value)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def line_value(value: Any, column: str) -> int | None:
|
|
321
|
+
"""Validate a 1-indexed line column; a stored ``0`` (unknown) becomes ``None``.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
value: Raw column value.
|
|
325
|
+
column: Column name, for the error message.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
The 1-indexed line, or ``None`` when unknown.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
IndexIncompatibleError: The value is not an integer.
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
``line_value(0, "line")`` returns ``None``; ``line_value(4, "line")`` returns ``4``.
|
|
335
|
+
"""
|
|
336
|
+
return _line(value, column)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def confidence_value(value: Any) -> float:
|
|
340
|
+
"""Validate a ``CALLS.confidence`` column.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
value: Raw column value.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
The confidence as a ``float`` in ``[0, 1]``, rounded to 6 decimals: the column
|
|
347
|
+
is float32, so a written ``0.9`` reads back as ``0.8999999761581421``.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
IndexIncompatibleError: The value is not a number in ``[0, 1]``.
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
``confidence_value(0.800000011920929)`` returns ``0.8``.
|
|
354
|
+
"""
|
|
355
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= 1:
|
|
356
|
+
raise IndexIncompatibleError(f"unexpected value for confidence: {value!r}")
|
|
357
|
+
return round(float(value), _CONFIDENCE_DIGITS)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def call_type_value(value: Any) -> CallType:
|
|
361
|
+
"""Validate a ``CALLS.call_type`` column.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
value: Raw column value.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
The call type.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
IndexIncompatibleError: The value is not one of the ``CallType`` values.
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
``call_type_value("MRO")`` returns ``CallType.MRO``.
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
return CallType(_text(value, "call_type"))
|
|
377
|
+
except ValueError as exc:
|
|
378
|
+
raise IndexIncompatibleError(f"unexpected value for call_type: {value!r}") from exc
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Symbol search: files, classes, functions and methods by name or path.
|
|
2
|
+
|
|
3
|
+
Matching is case-insensitive substring matching done in the graph with
|
|
4
|
+
``lower(x) CONTAINS lower($q)``; the user text only ever reaches Cypher as a
|
|
5
|
+
``$parameter``, so ``%``, ``_``, quotes and backslashes are literal. Each candidate is
|
|
6
|
+
then classified (see ``SearchMatch``) and ranked by match class, fan-in, then name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from compass_core.contract.models import IndexStatus, SymbolRef
|
|
14
|
+
from compass_core.contract.params import SearchParams
|
|
15
|
+
from compass_core.contract.results import SearchHit, SearchResult
|
|
16
|
+
from compass_core.contract.types import SearchMatch
|
|
17
|
+
from compass_core.graph.db import GraphDB, Row
|
|
18
|
+
from compass_core.index.locate import IndexLocation
|
|
19
|
+
from compass_core.query.cypher import Query
|
|
20
|
+
from compass_core.query.rows import (
|
|
21
|
+
CLASS_FIELDS,
|
|
22
|
+
FILE_FIELDS,
|
|
23
|
+
FILE_WIDTH,
|
|
24
|
+
FUNCTION_FIELDS,
|
|
25
|
+
FUNCTION_WIDTH,
|
|
26
|
+
class_from_row,
|
|
27
|
+
columns,
|
|
28
|
+
count_value,
|
|
29
|
+
file_from_row,
|
|
30
|
+
function_from_row,
|
|
31
|
+
)
|
|
32
|
+
from compass_core.query.status import index_status
|
|
33
|
+
from compass_core.shared_types import SymbolKind
|
|
34
|
+
|
|
35
|
+
_RANK = {match: i for i, match in enumerate(SearchMatch)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _with_fan_in(
|
|
39
|
+
decode: Callable[[Row], SymbolRef], width: int
|
|
40
|
+
) -> Callable[[Row], tuple[SymbolRef, int]]:
|
|
41
|
+
"""Decoder for rows of symbol columns followed by one count column."""
|
|
42
|
+
return lambda row: (decode(row), count_value(row[width]))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Candidates are fetched by the last dotted segment of the query ("User.save" -> "save"),
|
|
46
|
+
# then classified in Python against the full qualified name.
|
|
47
|
+
_FUNCTIONS_NAMED: Query[tuple[SymbolRef, int]] = Query(
|
|
48
|
+
f"""
|
|
49
|
+
MATCH (n:Function) WHERE lower(n.name) CONTAINS lower($last)
|
|
50
|
+
OPTIONAL MATCH (c:Function)-[:CALLS]->(n)
|
|
51
|
+
RETURN {columns("n", FUNCTION_FIELDS)}, count(c)
|
|
52
|
+
""",
|
|
53
|
+
_with_fan_in(function_from_row, FUNCTION_WIDTH),
|
|
54
|
+
)
|
|
55
|
+
_CLASSES_NAMED: Query[SymbolRef] = Query(
|
|
56
|
+
f"""
|
|
57
|
+
MATCH (n:Class) WHERE lower(n.name) CONTAINS lower($last)
|
|
58
|
+
RETURN {columns("n", CLASS_FIELDS)}
|
|
59
|
+
""",
|
|
60
|
+
class_from_row,
|
|
61
|
+
)
|
|
62
|
+
_FILES_WITH_PATH: Query[tuple[SymbolRef, int]] = Query(
|
|
63
|
+
f"""
|
|
64
|
+
MATCH (n:File) WHERE lower(n.path) CONTAINS lower($q)
|
|
65
|
+
OPTIONAL MATCH (i:File)-[:IMPORTS]->(n)
|
|
66
|
+
RETURN {columns("n", FILE_FIELDS)}, count(i)
|
|
67
|
+
""",
|
|
68
|
+
_with_fan_in(file_from_row, FILE_WIDTH),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _classify_definition(symbol: SymbolRef, query: str) -> SearchMatch | None:
|
|
73
|
+
"""Match class of a function/class for a lower-cased ``query``, or ``None``."""
|
|
74
|
+
name = symbol.name.lower()
|
|
75
|
+
qualified = symbol.qualified_name.lower()
|
|
76
|
+
if name == query or qualified == query:
|
|
77
|
+
return SearchMatch.EXACT
|
|
78
|
+
if qualified.endswith("." + query):
|
|
79
|
+
return SearchMatch.QUALIFIED_SUFFIX
|
|
80
|
+
if name.startswith(query):
|
|
81
|
+
return SearchMatch.PREFIX
|
|
82
|
+
if query in name:
|
|
83
|
+
return SearchMatch.SUBSTRING
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _classify_file(symbol: SymbolRef, query: str) -> SearchMatch | None:
|
|
88
|
+
"""Match class of a file for a lower-cased ``query``, or ``None``."""
|
|
89
|
+
path = (symbol.file_path or "").lower()
|
|
90
|
+
name = symbol.name.lower()
|
|
91
|
+
if path == query or name == query:
|
|
92
|
+
return SearchMatch.EXACT
|
|
93
|
+
if path.endswith("/" + query):
|
|
94
|
+
return SearchMatch.QUALIFIED_SUFFIX
|
|
95
|
+
if path.startswith(query) or name.startswith(query):
|
|
96
|
+
return SearchMatch.PREFIX
|
|
97
|
+
if query in path:
|
|
98
|
+
return SearchMatch.SUBSTRING
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _definition_hits(db: GraphDB, kind: SymbolKind | None, text: str) -> list[SearchHit]:
|
|
103
|
+
params = {"last": text.rsplit(".", 1)[-1]}
|
|
104
|
+
lowered = text.lower()
|
|
105
|
+
hits: list[SearchHit] = []
|
|
106
|
+
|
|
107
|
+
def add(symbol: SymbolRef, fan_in: int) -> None:
|
|
108
|
+
match = _classify_definition(symbol, lowered)
|
|
109
|
+
if match is not None:
|
|
110
|
+
hits.append(SearchHit(symbol=symbol, match=match, fan_in=fan_in))
|
|
111
|
+
|
|
112
|
+
if kind in (None, SymbolKind.CLASS):
|
|
113
|
+
for symbol in _CLASSES_NAMED.run(db, params):
|
|
114
|
+
add(symbol, 0)
|
|
115
|
+
if kind in (None, SymbolKind.FUNCTION, SymbolKind.METHOD):
|
|
116
|
+
for symbol, fan_in in _FUNCTIONS_NAMED.run(db, params):
|
|
117
|
+
if kind in (None, symbol.kind):
|
|
118
|
+
add(symbol, fan_in)
|
|
119
|
+
return hits
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _file_hits(db: GraphDB, text: str) -> list[SearchHit]:
|
|
123
|
+
lowered = text.lower()
|
|
124
|
+
hits: list[SearchHit] = []
|
|
125
|
+
for symbol, fan_in in _FILES_WITH_PATH.run(db, {"q": text}):
|
|
126
|
+
match = _classify_file(symbol, lowered)
|
|
127
|
+
if match is not None:
|
|
128
|
+
hits.append(SearchHit(symbol=symbol, match=match, fan_in=fan_in))
|
|
129
|
+
return hits
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def find_symbols(db: GraphDB, location: IndexLocation, params: SearchParams) -> SearchResult:
|
|
133
|
+
"""Search files, classes, functions and methods.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
db: Open graph database.
|
|
137
|
+
location: Index location, used to build the result's ``IndexStatus``. (Adds
|
|
138
|
+
``location`` to the parent spec's signature; recorded ruling.)
|
|
139
|
+
params: Query text, optional kind filter and limit.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Hits best first: ``EXACT`` > ``QUALIFIED_SUFFIX`` > ``PREFIX`` > ``SUBSTRING``,
|
|
143
|
+
ties broken by fan-in (incoming ``CALLS`` for functions, incoming ``IMPORTS``
|
|
144
|
+
for files, 0 for classes) then name. ``total`` counts matches before the limit.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
IndexIncompatibleError: A stored row or the index metadata is unreadable.
|
|
148
|
+
|
|
149
|
+
Note:
|
|
150
|
+
Cost is O(matches of the last dotted segment of the query) because an exact
|
|
151
|
+
``total`` is required. Qualified-prefix queries such as ``auth.decode`` are not
|
|
152
|
+
supported: dotted queries match only exactly or as a suffix of the qualified
|
|
153
|
+
name. A query whose last dotted segment is empty (``"."``, ``"a."``) matches
|
|
154
|
+
nothing. ``index_status`` stats every indexed file per call; callers may cache.
|
|
155
|
+
Fan-in counts edges (not distinct callers), as in ``references``.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
``find_symbols(db, loc, SearchParams(query="User.save"))`` returns
|
|
159
|
+
``app/models.py::User.save`` first with ``SearchMatch.QUALIFIED_SUFFIX``.
|
|
160
|
+
"""
|
|
161
|
+
status = index_status(location, db) # first: rejects an incompatible index
|
|
162
|
+
text = params.query.strip()
|
|
163
|
+
hits: list[SearchHit] = []
|
|
164
|
+
if text.endswith("."):
|
|
165
|
+
# Empty last segment: a CONTAINS '' scan would match everything.
|
|
166
|
+
return _result(status, params, hits)
|
|
167
|
+
if params.kind in (None, SymbolKind.FILE):
|
|
168
|
+
hits += _file_hits(db, text)
|
|
169
|
+
hits += _definition_hits(db, params.kind, text)
|
|
170
|
+
return _result(status, params, hits)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _result(status: IndexStatus, params: SearchParams, hits: list[SearchHit]) -> SearchResult:
|
|
174
|
+
hits.sort(key=lambda h: (_RANK[h.match], -h.fan_in, h.symbol.name, h.symbol.id))
|
|
175
|
+
return SearchResult(
|
|
176
|
+
index=status,
|
|
177
|
+
caveats=(),
|
|
178
|
+
query=params.query,
|
|
179
|
+
kind=params.kind,
|
|
180
|
+
hits=tuple(hits[: params.limit]),
|
|
181
|
+
total=len(hits),
|
|
182
|
+
truncated=len(hits) > params.limit,
|
|
183
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Index status for query results: metadata plus a count of stale files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from compass_core.contract.models import IndexStatus
|
|
6
|
+
from compass_core.contract.types import RepoPath
|
|
7
|
+
from compass_core.graph.db import GraphDB
|
|
8
|
+
from compass_core.index.freshness import count_stale
|
|
9
|
+
from compass_core.index.locate import IndexLocation
|
|
10
|
+
from compass_core.index.metadata import load_metadata
|
|
11
|
+
from compass_core.query.cypher import Query
|
|
12
|
+
from compass_core.query.rows import text_value
|
|
13
|
+
|
|
14
|
+
_FILE_PATHS: Query[RepoPath] = Query(
|
|
15
|
+
"MATCH (f:File) RETURN f.path", lambda row: RepoPath(text_value(row[0], "path"))
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def index_status(location: IndexLocation, db: GraphDB) -> IndexStatus:
|
|
20
|
+
"""Describe the index and count its stale files.
|
|
21
|
+
|
|
22
|
+
The indexed paths come from the graph; the stale check itself is
|
|
23
|
+
``index.freshness.count_stale`` (missing, or modified after the ingest).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
location: Where the index lives.
|
|
27
|
+
db: Open graph database of that index.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The status, with ``new_file_count`` left ``None`` (only ``status`` computes it).
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
IndexIncompatibleError: The metadata or a stored row is unreadable.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
After ``touch app/auth.py`` post-ingest, ``index_status(loc, db).stale_file_count``
|
|
37
|
+
is ``1``.
|
|
38
|
+
"""
|
|
39
|
+
meta = load_metadata(location.metadata_path)
|
|
40
|
+
repo_root = location.repo_root.resolve()
|
|
41
|
+
paths = _FILE_PATHS.run(db)
|
|
42
|
+
return IndexStatus(
|
|
43
|
+
repo_root=repo_root,
|
|
44
|
+
indexed_at=meta.indexed_at,
|
|
45
|
+
indexed_commit=meta.indexed_commit,
|
|
46
|
+
schema_version=meta.schema_version,
|
|
47
|
+
stale_file_count=count_stale(repo_root, paths, int(meta.indexed_at.timestamp())),
|
|
48
|
+
new_file_count=None,
|
|
49
|
+
)
|