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.
Files changed (137) hide show
  1. compass_cli/__init__.py +0 -0
  2. compass_cli/commands/__init__.py +0 -0
  3. compass_cli/commands/ingest.py +310 -0
  4. compass_cli/commands/mcp.py +25 -0
  5. compass_cli/commands/query.py +181 -0
  6. compass_cli/commands/serve.py +123 -0
  7. compass_cli/commands/setup.py +171 -0
  8. compass_cli/display.py +231 -0
  9. compass_cli/errors.py +30 -0
  10. compass_cli/main.py +54 -0
  11. compass_cli/progress.py +191 -0
  12. compass_cli/result_views.py +389 -0
  13. compass_cli/types.py +41 -0
  14. compass_code-0.1.0.dist-info/METADATA +72 -0
  15. compass_code-0.1.0.dist-info/RECORD +137 -0
  16. compass_code-0.1.0.dist-info/WHEEL +4 -0
  17. compass_code-0.1.0.dist-info/entry_points.txt +2 -0
  18. compass_core/__init__.py +1 -0
  19. compass_core/contract/__init__.py +6 -0
  20. compass_core/contract/errors.py +116 -0
  21. compass_core/contract/models.py +204 -0
  22. compass_core/contract/params.py +99 -0
  23. compass_core/contract/results.py +370 -0
  24. compass_core/contract/serialize.py +59 -0
  25. compass_core/contract/types.py +76 -0
  26. compass_core/errors.py +148 -0
  27. compass_core/graph/__init__.py +0 -0
  28. compass_core/graph/db.py +205 -0
  29. compass_core/graph/schema.py +165 -0
  30. compass_core/graph/stored_ids.py +133 -0
  31. compass_core/graph/writer.py +482 -0
  32. compass_core/index/__init__.py +1 -0
  33. compass_core/index/freshness.py +55 -0
  34. compass_core/index/locate.py +155 -0
  35. compass_core/index/metadata.py +107 -0
  36. compass_core/ir/__init__.py +0 -0
  37. compass_core/ir/graph.py +65 -0
  38. compass_core/ir/models.py +75 -0
  39. compass_core/languages/__init__.py +1 -0
  40. compass_core/languages/base.py +26 -0
  41. compass_core/languages/python/__init__.py +6 -0
  42. compass_core/languages/python/extractor.py +670 -0
  43. compass_core/languages/python/import_resolver.py +107 -0
  44. compass_core/languages/registry.py +89 -0
  45. compass_core/logger.py +42 -0
  46. compass_core/phases/__init__.py +0 -0
  47. compass_core/phases/base.py +58 -0
  48. compass_core/phases/discovery.py +167 -0
  49. compass_core/phases/parse.py +80 -0
  50. compass_core/phases/persistence.py +33 -0
  51. compass_core/phases/pipeline.py +152 -0
  52. compass_core/phases/resolution.py +572 -0
  53. compass_core/phases/symbol_table.py +36 -0
  54. compass_core/phases/validation.py +76 -0
  55. compass_core/progress.py +45 -0
  56. compass_core/py.typed +0 -0
  57. compass_core/query/__init__.py +1 -0
  58. compass_core/query/cypher.py +92 -0
  59. compass_core/query/detail.py +241 -0
  60. compass_core/query/graph_map.py +127 -0
  61. compass_core/query/impact.py +273 -0
  62. compass_core/query/impact_walk.py +352 -0
  63. compass_core/query/overview.py +221 -0
  64. compass_core/query/references.py +235 -0
  65. compass_core/query/rows.py +378 -0
  66. compass_core/query/search.py +183 -0
  67. compass_core/query/status.py +49 -0
  68. compass_core/query/walk_queries.py +202 -0
  69. compass_core/render.py +408 -0
  70. compass_core/settings.py +61 -0
  71. compass_core/shared_types.py +78 -0
  72. compass_mcp/__init__.py +0 -0
  73. compass_mcp/guide.py +45 -0
  74. compass_mcp/py.typed +0 -0
  75. compass_mcp/server.py +261 -0
  76. compass_server/__init__.py +5 -0
  77. compass_server/errors.py +84 -0
  78. compass_server/main.py +42 -0
  79. compass_server/py.typed +0 -0
  80. compass_server/routers/__init__.py +0 -0
  81. compass_server/routers/query.py +182 -0
  82. compass_server/settings.py +37 -0
  83. compass_server/static/404.html +1 -0
  84. compass_server/static/_next/static/chunks/1629039a.eed2a06658ec4f9c.js +1 -0
  85. compass_server/static/_next/static/chunks/1ff282bf-bb7c483245264fc9.js +1 -0
  86. compass_server/static/_next/static/chunks/223.f881a22f446259bf.js +508 -0
  87. compass_server/static/_next/static/chunks/51fa5c19.6396973222c9d6fd.js +485 -0
  88. compass_server/static/_next/static/chunks/556.443c1682b6b20128.js +1 -0
  89. compass_server/static/_next/static/chunks/684-1aa0170f59baf77e.js +1 -0
  90. compass_server/static/_next/static/chunks/92e71660.3a42d4f70d017951.js +342 -0
  91. compass_server/static/_next/static/chunks/937-8692224bb6d1b605.js +1 -0
  92. compass_server/static/_next/static/chunks/app/_not-found/page-2c96ddf295eb6a86.js +1 -0
  93. compass_server/static/_next/static/chunks/app/layout-4bd0ba4a7d18aff9.js +1 -0
  94. compass_server/static/_next/static/chunks/app/page-5376a0a98bbd1faa.js +1 -0
  95. compass_server/static/_next/static/chunks/framework-96dcd958cfe08bd4.js +1 -0
  96. compass_server/static/_next/static/chunks/main-6dee8e626b16731a.js +1 -0
  97. compass_server/static/_next/static/chunks/main-app-460e8bc670818718.js +1 -0
  98. compass_server/static/_next/static/chunks/pages/_app-638a3e7363f92b80.js +1 -0
  99. compass_server/static/_next/static/chunks/pages/_error-6a72dbbb45cae743.js +1 -0
  100. compass_server/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  101. compass_server/static/_next/static/chunks/webpack-597057bf48ce4111.js +1 -0
  102. compass_server/static/_next/static/css/1b0fbfd8dd653f37.css +3 -0
  103. compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_buildManifest.js +1 -0
  104. compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_ssgManifest.js +1 -0
  105. compass_server/static/_next/static/media/0086f8992871c45b-s.woff2 +0 -0
  106. compass_server/static/_next/static/media/034d78ad42e9620c-s.woff2 +0 -0
  107. compass_server/static/_next/static/media/03b436aa846269de-s.woff2 +0 -0
  108. compass_server/static/_next/static/media/0484562807a97172-s.p.woff2 +0 -0
  109. compass_server/static/_next/static/media/08aedeceaf1dcd57-s.woff2 +0 -0
  110. compass_server/static/_next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
  111. compass_server/static/_next/static/media/29a4aea02fdee119-s.woff2 +0 -0
  112. compass_server/static/_next/static/media/29e7bbdce9332268-s.woff2 +0 -0
  113. compass_server/static/_next/static/media/36008747766f78c6-s.woff2 +0 -0
  114. compass_server/static/_next/static/media/39969fcf98a3026e-s.woff2 +0 -0
  115. compass_server/static/_next/static/media/4de1fea1a954a5b6-s.p.woff2 +0 -0
  116. compass_server/static/_next/static/media/5c0c2bcbaa4149ca-s.p.woff2 +0 -0
  117. compass_server/static/_next/static/media/67957d42bae0796d-s.woff2 +0 -0
  118. compass_server/static/_next/static/media/6d38f9d4e0f4772b-s.woff2 +0 -0
  119. compass_server/static/_next/static/media/6d664cce900333ee-s.p.woff2 +0 -0
  120. compass_server/static/_next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
  121. compass_server/static/_next/static/media/8888a3826f4a3af4-s.p.woff2 +0 -0
  122. compass_server/static/_next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
  123. compass_server/static/_next/static/media/a1386beebedccca4-s.woff2 +0 -0
  124. compass_server/static/_next/static/media/ab00a911ac2adf48-s.woff2 +0 -0
  125. compass_server/static/_next/static/media/b957ea75a84b6ea7-s.p.woff2 +0 -0
  126. compass_server/static/_next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
  127. compass_server/static/_next/static/media/c3bc380753a8436c-s.woff2 +0 -0
  128. compass_server/static/_next/static/media/cce080f35d014443-s.woff2 +0 -0
  129. compass_server/static/_next/static/media/db911767852bc875-s.woff2 +0 -0
  130. compass_server/static/_next/static/media/eafabf029ad39a43-s.p.woff2 +0 -0
  131. compass_server/static/_next/static/media/f10b8e9d91f3edcb-s.woff2 +0 -0
  132. compass_server/static/_next/static/media/f911b923c6adde36-s.woff2 +0 -0
  133. compass_server/static/_next/static/media/fe0777f1195381cb-s.woff2 +0 -0
  134. compass_server/static/index.html +1 -0
  135. compass_server/static/index.txt +25 -0
  136. compass_server/web_bundle.py +27 -0
  137. 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
+ )