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,370 @@
1
+ """Result types returned by query operations.
2
+
3
+ Every successful result inherits ``ResultHeader`` so adapters read ``index`` and
4
+ ``caveats`` flat. ``operation`` plus ``outcome`` form a discriminated union.
5
+ Reference outcomes (ambiguous / not found) are values, not exceptions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from dataclasses import dataclass
12
+ from typing import Literal, TypeAlias, TypeVar
13
+
14
+ from compass_core.contract.models import Caveat, EdgeRef, IndexStatus, ResultItem, SymbolRef
15
+ from compass_core.contract.types import (
16
+ MatchRule,
17
+ Operation,
18
+ Outcome,
19
+ RepoPath,
20
+ SearchMatch,
21
+ SymbolId,
22
+ )
23
+ from compass_core.shared_types import EdgeType, SymbolKind
24
+
25
+
26
+ @dataclass(frozen=True, slots=True, kw_only=True)
27
+ class ResultHeader:
28
+ """Fields common to every successful result.
29
+
30
+ Attributes:
31
+ outcome: Always ``Outcome.OK`` for a successful result.
32
+ index: Status of the index that produced the answer.
33
+ caveats: Reasons the answer may be incomplete.
34
+
35
+ Example:
36
+ Subclasses are built as ``SearchResult(index=status, caveats=(), ...)``.
37
+ """
38
+
39
+ outcome: Literal[Outcome.OK] = Outcome.OK
40
+ index: IndexStatus
41
+ caveats: tuple[Caveat, ...]
42
+
43
+
44
+ @dataclass(frozen=True, slots=True, kw_only=True)
45
+ class SearchHit:
46
+ """One search match.
47
+
48
+ Attributes:
49
+ symbol: The matching symbol.
50
+ match: How it matched the query.
51
+ fan_in: Number of incoming edges, used for ranking.
52
+
53
+ Example:
54
+ ``SearchHit(symbol=<f>, match=SearchMatch.EXACT, fan_in=2)``
55
+ """
56
+
57
+ symbol: SymbolRef
58
+ match: SearchMatch
59
+ fan_in: int
60
+
61
+
62
+ @dataclass(frozen=True, slots=True, kw_only=True)
63
+ class SearchResult(ResultHeader):
64
+ """Result of a symbol search.
65
+
66
+ Attributes:
67
+ operation: Always ``Operation.FIND``.
68
+ query: The search text.
69
+ kind: Kind filter that was applied, if any.
70
+ hits: Matching symbols, best first.
71
+ total: Number of matches before truncation.
72
+ truncated: Whether ``hits`` was cut at the limit.
73
+
74
+ Example:
75
+ ``find "write"`` yields ``SearchResult(query="write", hits=(...), total=3,
76
+ truncated=False, ...)``.
77
+ """
78
+
79
+ operation: Literal[Operation.FIND] = Operation.FIND
80
+ query: str
81
+ kind: SymbolKind | None
82
+ hits: tuple[SearchHit, ...]
83
+ total: int
84
+ truncated: bool
85
+
86
+
87
+ @dataclass(frozen=True, slots=True, kw_only=True)
88
+ class ImpactGroup:
89
+ """Affected symbols at one distance within one file.
90
+
91
+ Attributes:
92
+ distance: Hops from the target.
93
+ file_path: Repo-relative path of the file holding the items.
94
+ items: Affected symbols in that file at that distance.
95
+
96
+ Example:
97
+ ``ImpactGroup(distance=1, file_path=RepoPath("a.py"), items=(<item>,))``
98
+ """
99
+
100
+ distance: int
101
+ file_path: RepoPath
102
+ items: tuple[ResultItem, ...]
103
+
104
+
105
+ @dataclass(frozen=True, slots=True, kw_only=True)
106
+ class ImpactResult(ResultHeader):
107
+ """Result of an impact analysis.
108
+
109
+ Attributes:
110
+ operation: Always ``Operation.IMPACT``.
111
+ target: The analysed file or symbol.
112
+ matched_by: Resolution rule that matched the reference.
113
+ depth: Depth that was traversed.
114
+ groups: Affected symbols grouped by distance and file.
115
+ affected_file_count: Number of distinct affected files.
116
+ edges: Edges connecting the affected symbols.
117
+ total: Number of affected symbols before truncation.
118
+ truncated: Whether the output was cut at the limit.
119
+
120
+ Example:
121
+ ``impact write`` yields ``ImpactResult(target=<write>, depth=3,
122
+ groups=(ImpactGroup(...),), affected_file_count=6, total=14, ...)``.
123
+ """
124
+
125
+ operation: Literal[Operation.IMPACT] = Operation.IMPACT
126
+ target: SymbolRef
127
+ matched_by: MatchRule
128
+ depth: int
129
+ groups: tuple[ImpactGroup, ...]
130
+ affected_file_count: int
131
+ edges: tuple[EdgeRef, ...]
132
+ total: int
133
+ truncated: bool
134
+
135
+
136
+ @dataclass(frozen=True, slots=True, kw_only=True)
137
+ class RankedSymbol:
138
+ """A symbol with a count used for ranking.
139
+
140
+ Attributes:
141
+ symbol: The symbol.
142
+ count: Ranking count (callers, importers, or affected symbols).
143
+
144
+ Example:
145
+ ``RankedSymbol(symbol=<write>, count=14)``
146
+ """
147
+
148
+ symbol: SymbolRef
149
+ count: int
150
+
151
+
152
+ @dataclass(frozen=True, slots=True, kw_only=True)
153
+ class LibraryUsage:
154
+ """How heavily an external library is called.
155
+
156
+ Attributes:
157
+ library: Library name, e.g. ``"pydantic"``.
158
+ call_count: Number of call sites into the library.
159
+
160
+ Example:
161
+ ``LibraryUsage(library="pydantic", call_count=12)``
162
+ """
163
+
164
+ library: str
165
+ call_count: int
166
+
167
+
168
+ @dataclass(frozen=True, slots=True, kw_only=True)
169
+ class IndexCounts:
170
+ """Node and edge totals of the index.
171
+
172
+ Attributes:
173
+ files: Number of file nodes.
174
+ classes: Number of class nodes.
175
+ functions: Number of function and method nodes.
176
+ edges_by_type: Edge count per edge type.
177
+
178
+ Example:
179
+ ``IndexCounts(files=10, classes=2, functions=40,
180
+ edges_by_type={EdgeType.CALLS: 55})``
181
+ """
182
+
183
+ files: int
184
+ classes: int
185
+ functions: int
186
+ edges_by_type: Mapping[EdgeType, int]
187
+
188
+
189
+ @dataclass(frozen=True, slots=True, kw_only=True)
190
+ class CallResolution:
191
+ """How many call sites in the index were tied to a definition.
192
+
193
+ Attributes:
194
+ total: Call sites recorded in function and method bodies.
195
+ resolved: Call sites whose called name matches a function the caller has a
196
+ ``CALLS`` or ``CALLS_EXTERN`` edge to.
197
+
198
+ Example:
199
+ ``CallResolution(total=193, resolved=171)`` means 22 calls are unresolved.
200
+ """
201
+
202
+ total: int
203
+ resolved: int
204
+
205
+
206
+ @dataclass(frozen=True, slots=True, kw_only=True)
207
+ class OverviewResult(ResultHeader):
208
+ """Repository overview.
209
+
210
+ Attributes:
211
+ operation: Always ``Operation.OVERVIEW``.
212
+ counts: Node and edge totals.
213
+ call_resolution: Share of call sites the indexer resolved.
214
+ entry_points: Symbols that are entry points.
215
+ most_imported_files: Files ranked by number of importers.
216
+ most_called_functions: Functions ranked by number of callers.
217
+ riskiest_to_change: Symbols ranked by number of affected symbols.
218
+ top_external_libraries: External libraries ranked by call count.
219
+
220
+ Example:
221
+ ``OverviewResult(counts=IndexCounts(...), entry_points=(), ...,
222
+ riskiest_to_change=(RankedSymbol(symbol=<write>, count=14),), ...)``
223
+ """
224
+
225
+ operation: Literal[Operation.OVERVIEW] = Operation.OVERVIEW
226
+ counts: IndexCounts
227
+ call_resolution: CallResolution
228
+ entry_points: tuple[SymbolRef, ...]
229
+ most_imported_files: tuple[RankedSymbol, ...]
230
+ most_called_functions: tuple[RankedSymbol, ...]
231
+ riskiest_to_change: tuple[RankedSymbol, ...]
232
+ top_external_libraries: tuple[LibraryUsage, ...]
233
+
234
+
235
+ @dataclass(frozen=True, slots=True, kw_only=True)
236
+ class NodeDetail(ResultHeader):
237
+ """Everything known about one node, for a details panel.
238
+
239
+ Only the relationships that apply to the node's kind are filled; the rest are empty.
240
+
241
+ Attributes:
242
+ operation: Always ``Operation.DESCRIBE``.
243
+ symbol: The node; ``symbol.source`` holds its source text for classes and functions.
244
+ docstring: Extracted docstring, or ``None`` when there is none.
245
+ bases: Classes this class inherits from, within the repository.
246
+ subclasses: Classes that inherit from this class.
247
+ members: Methods of a class, or the classes and functions a file defines.
248
+ callees: Functions this function calls.
249
+ callers: Functions that call this function.
250
+ imports: Files this file imports.
251
+ imported_by: Files that import this file.
252
+ truncated: Whether any relationship was cut at the per-relationship limit.
253
+
254
+ Example:
255
+ ``describe(db, loc, "app/auth.py::Policy")`` yields ``NodeDetail(symbol=<Policy>,
256
+ docstring="Declarative mapping...", bases=(), subclasses=(), members=(<Policy.allows>,),
257
+ ...)``.
258
+ """
259
+
260
+ operation: Literal[Operation.DESCRIBE] = Operation.DESCRIBE
261
+ symbol: SymbolRef
262
+ docstring: str | None
263
+ bases: tuple[SymbolRef, ...]
264
+ subclasses: tuple[SymbolRef, ...]
265
+ members: tuple[SymbolRef, ...]
266
+ callees: tuple[SymbolRef, ...]
267
+ callers: tuple[SymbolRef, ...]
268
+ imports: tuple[SymbolRef, ...]
269
+ imported_by: tuple[SymbolRef, ...]
270
+ truncated: bool
271
+
272
+
273
+ @dataclass(frozen=True, slots=True, kw_only=True)
274
+ class AmbiguousReference:
275
+ """A reference that matched several symbols.
276
+
277
+ Attributes:
278
+ outcome: Always ``Outcome.AMBIGUOUS``.
279
+ reference: What the user typed.
280
+ candidates: At least two matches, ordered by rule then fan-in.
281
+
282
+ Example:
283
+ ``AmbiguousReference(reference="run", candidates=(<a.run>, <b.run>))``
284
+ """
285
+
286
+ outcome: Literal[Outcome.AMBIGUOUS] = Outcome.AMBIGUOUS
287
+ reference: str
288
+ candidates: tuple[SymbolRef, ...]
289
+
290
+
291
+ @dataclass(frozen=True, slots=True, kw_only=True)
292
+ class SymbolNotFound:
293
+ """A reference that matched nothing.
294
+
295
+ Attributes:
296
+ outcome: Always ``Outcome.NOT_FOUND``.
297
+ reference: What the user typed.
298
+ suggestions: Up to five case-insensitive substring matches.
299
+
300
+ Example:
301
+ ``SymbolNotFound(reference="wrte", suggestions=(<write>,))``
302
+ """
303
+
304
+ outcome: Literal[Outcome.NOT_FOUND] = Outcome.NOT_FOUND
305
+ reference: str
306
+ suggestions: tuple[SymbolRef, ...]
307
+
308
+
309
+ @dataclass(frozen=True, slots=True, kw_only=True)
310
+ class MapNode:
311
+ """One file, class, function or method in the map.
312
+
313
+ Attributes:
314
+ id: What ``MapEdge.source`` and ``MapEdge.target`` refer to: the public id of a
315
+ class or function, the path of a file.
316
+ label: Short display name.
317
+ kind: Kind of node; never ``EXTERNAL``.
318
+ file_path: Repo-relative path.
319
+ public_id: Public id, accepted wherever a query takes a reference.
320
+
321
+ Example:
322
+ ``def f(): ...`` in ``a.py`` becomes ``MapNode(id="a.py::f", label="f",
323
+ kind=SymbolKind.FUNCTION, file_path="a.py", public_id=SymbolId("a.py::f"))``.
324
+ """
325
+
326
+ id: str
327
+ label: str
328
+ kind: SymbolKind
329
+ file_path: str
330
+ public_id: SymbolId
331
+
332
+
333
+ @dataclass(frozen=True, slots=True, kw_only=True)
334
+ class MapEdge:
335
+ """One relationship between two ``MapNode`` values.
336
+
337
+ Attributes:
338
+ source: ``MapNode.id`` the edge leaves.
339
+ target: ``MapNode.id`` the edge enters.
340
+ type: Kind of edge; never ``CALLS_EXTERN``, whose targets are not in the map.
341
+
342
+ Example:
343
+ ``a.py`` importing ``b.py`` becomes ``MapEdge(source="a.py", target="b.py",
344
+ type=EdgeType.IMPORTS)``.
345
+ """
346
+
347
+ source: str
348
+ target: str
349
+ type: EdgeType
350
+
351
+
352
+ @dataclass(frozen=True, slots=True, kw_only=True)
353
+ class GraphMap:
354
+ """Every indexed node and the deduplicated edges between them.
355
+
356
+ Attributes:
357
+ nodes: Files, then classes, then functions and methods.
358
+ edges: Distinct ``(source, target, type)`` edges, grouped by type.
359
+
360
+ Example:
361
+ A repo with one file holding one function has two nodes and one ``CONTAINS``
362
+ edge.
363
+ """
364
+
365
+ nodes: tuple[MapNode, ...]
366
+ edges: tuple[MapEdge, ...]
367
+
368
+
369
+ R = TypeVar("R")
370
+ Lookup: TypeAlias = R | AmbiguousReference | SymbolNotFound
@@ -0,0 +1,59 @@
1
+ """JSON serialization of query results via cached Pydantic ``TypeAdapter``s.
2
+
3
+ Models stay plain dataclasses; Pydantic is used only here, at the adapter edge.
4
+ Enums serialize as their value, ``datetime`` as ISO-8601, ``Path`` as a string and
5
+ tuples / frozensets as arrays.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pydantic import TypeAdapter
11
+
12
+ from compass_core.contract.errors import ErrorResponse
13
+ from compass_core.contract.models import IndexStatus
14
+ from compass_core.contract.results import (
15
+ AmbiguousReference,
16
+ ImpactResult,
17
+ NodeDetail,
18
+ OverviewResult,
19
+ SearchResult,
20
+ SymbolNotFound,
21
+ )
22
+
23
+ Serializable = (
24
+ SearchResult
25
+ | ImpactResult
26
+ | OverviewResult
27
+ | NodeDetail
28
+ | AmbiguousReference
29
+ | SymbolNotFound
30
+ | IndexStatus
31
+ | ErrorResponse
32
+ )
33
+
34
+
35
+ _ADAPTERS: dict[type[object], TypeAdapter[object]] = {}
36
+
37
+
38
+ def _adapter_for(obj: Serializable) -> TypeAdapter[object]:
39
+ tp: type[object] = type(obj)
40
+ adapter = _ADAPTERS.get(tp)
41
+ if adapter is None:
42
+ adapter = _ADAPTERS[tp] = TypeAdapter(tp)
43
+ return adapter
44
+
45
+
46
+ def to_json(obj: Serializable) -> str:
47
+ """Serialize a query result, reference outcome or error envelope to JSON.
48
+
49
+ Args:
50
+ obj: The value to serialize.
51
+
52
+ Returns:
53
+ A compact JSON string.
54
+
55
+ Example:
56
+ ``to_json(SymbolNotFound(reference="x", suggestions=()))`` returns
57
+ ``'{"outcome":"not_found","reference":"x","suggestions":[]}'``.
58
+ """
59
+ return _adapter_for(obj).dump_json(obj).decode()
@@ -0,0 +1,76 @@
1
+ """Closed vocabularies and identifier types of the query contract.
2
+
3
+ Enums are ``StrEnum`` so they serialize as their string value. Identifiers are
4
+ ``NewType`` over ``str`` so a file path cannot be passed where a symbol id is expected.
5
+ Vocabulary shared with parsing and persistence lives in ``compass_core/shared_types.py``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import StrEnum
11
+ from typing import NewType
12
+
13
+ SymbolId = NewType("SymbolId", str)
14
+ """Public node id, e.g. ``"packages/core/src/compass_core/graph/writer.py::write"``."""
15
+
16
+ RepoPath = NewType("RepoPath", str)
17
+ """Repo-relative POSIX path, e.g. ``"packages/core/src/compass_core/graph/writer.py"``."""
18
+
19
+ QualifiedName = NewType("QualifiedName", str)
20
+ """Dotted qualified name, e.g. ``"compass_core.graph.writer.write"``."""
21
+
22
+
23
+ class ViaKind(StrEnum):
24
+ """Relationship through which an impact item was reached from the item before it.
25
+
26
+ Only values the query layer produces are listed; add one with the operation that
27
+ produces it.
28
+ """
29
+
30
+ CALLS = "CALLS"
31
+ IMPORTS = "IMPORTS"
32
+ INHERITS = "INHERITS"
33
+
34
+
35
+ class MatchRule(StrEnum):
36
+ """Symbol resolution rules, in the order they are tried."""
37
+
38
+ EXACT_ID = "EXACT_ID"
39
+ FILE_PATH = "FILE_PATH"
40
+ QUALIFIED_NAME = "QUALIFIED_NAME"
41
+ PATH_AND_NAME = "PATH_AND_NAME"
42
+ CLASS_MEMBER = "CLASS_MEMBER"
43
+ BARE_NAME = "BARE_NAME"
44
+
45
+
46
+ class SearchMatch(StrEnum):
47
+ """How a search hit matched the query."""
48
+
49
+ EXACT = "EXACT"
50
+ QUALIFIED_SUFFIX = "QUALIFIED_SUFFIX"
51
+ PREFIX = "PREFIX"
52
+ SUBSTRING = "SUBSTRING"
53
+
54
+
55
+ class Operation(StrEnum):
56
+ """Query operations that return a ``ResultHeader`` result (the JSON discriminator)."""
57
+
58
+ FIND = "FIND"
59
+ DESCRIBE = "DESCRIBE"
60
+ IMPACT = "IMPACT"
61
+ OVERVIEW = "OVERVIEW"
62
+
63
+
64
+ class Outcome(StrEnum):
65
+ """JSON discriminator for a query result."""
66
+
67
+ OK = "ok"
68
+ AMBIGUOUS = "ambiguous"
69
+ NOT_FOUND = "not_found"
70
+
71
+
72
+ class CaveatCode(StrEnum):
73
+ """Reasons an answer may be incomplete; only codes the query layer emits."""
74
+
75
+ STALE_INDEX = "STALE_INDEX"
76
+ UNRESOLVED_CALLS = "UNRESOLVED_CALLS"
compass_core/errors.py ADDED
@@ -0,0 +1,148 @@
1
+ """Exception hierarchy for compass_core.
2
+
3
+ Reserved for genuinely unexpected/unrecoverable failures (DB connection
4
+ lost, malformed grammar binary, config missing) that propagate up to
5
+ phases/pipeline.py and are caught once at the top level. Expected failures
6
+ (an unresolved call site, a degraded parse) are modeled as typed result
7
+ objects elsewhere, never raised as one of these.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+ from typing import ClassVar
15
+
16
+
17
+ class CompassError(Exception):
18
+ """Base exception for compass_core. Carries enough context to debug without re-running."""
19
+
20
+ def __init__(
21
+ self,
22
+ message: str,
23
+ *,
24
+ file_path: str | None = None,
25
+ language: str | None = None,
26
+ phase_name: str | None = None,
27
+ ) -> None:
28
+ super().__init__(message)
29
+ self.file_path = file_path
30
+ self.language = language
31
+ self.phase_name = phase_name
32
+
33
+
34
+ class DiscoveryError(CompassError):
35
+ """Phase 0 failure — repo walk or manifest construction failed unrecoverably."""
36
+
37
+
38
+ class ParseError(CompassError):
39
+ """Phase 1 failure. Only raised for unrecoverable extractor setup failures
40
+ (e.g. grammar binary missing) — per-file parse failures are captured as a
41
+ `degraded` flag on ParsedFile, not raised."""
42
+
43
+
44
+ class UnsupportedLanguageError(ParseError):
45
+ """Raised by languages/registry.py when no extractor is registered for a file's language."""
46
+
47
+
48
+ class ResolutionError(CompassError):
49
+ """Phase 3 failure — unrecoverable failure building the symbol table or resolving edges."""
50
+
51
+
52
+ class ValidationError(CompassError):
53
+ """Phase 4 failure — the ingest run failed a regression gate check."""
54
+
55
+
56
+ class PersistenceError(CompassError):
57
+ """Phase 5 failure — DB connection lost or write failed unrecoverably mid-batch."""
58
+
59
+
60
+ class ErrorCode(StrEnum):
61
+ """Machine-readable failure codes shared by the API, CLI and MCP adapters."""
62
+
63
+ INVALID_ARGUMENT = "INVALID_ARGUMENT"
64
+ NOT_FOUND = "NOT_FOUND"
65
+ AMBIGUOUS = "AMBIGUOUS"
66
+ INDEX_NOT_FOUND = "INDEX_NOT_FOUND"
67
+ INDEX_BUSY = "INDEX_BUSY"
68
+ INDEX_INCOMPATIBLE = "INDEX_INCOMPATIBLE"
69
+ QUERY_TIMEOUT = "QUERY_TIMEOUT"
70
+ INTERNAL = "INTERNAL"
71
+
72
+
73
+ class QueryError(CompassError):
74
+ """Base class for failures raised by the query layer.
75
+
76
+ Attributes:
77
+ code: Machine-readable failure code; every direct subclass declares a
78
+ distinct value.
79
+ """
80
+
81
+ code: ClassVar[ErrorCode] = ErrorCode.INTERNAL
82
+
83
+
84
+ class InvalidArgumentError(QueryError):
85
+ """A caller-supplied argument was missing, malformed or out of range.
86
+
87
+ Attributes:
88
+ argument: Name of the offending argument.
89
+ reason: Human-readable explanation of why it was rejected.
90
+
91
+ Example:
92
+ >>> InvalidArgumentError(argument="depth", reason="must be between 1 and 10")
93
+ """
94
+
95
+ code: ClassVar[ErrorCode] = ErrorCode.INVALID_ARGUMENT
96
+
97
+ def __init__(self, *, argument: str, reason: str) -> None:
98
+ super().__init__(f"Invalid {argument}: {reason}")
99
+ self.argument = argument
100
+ self.reason = reason
101
+
102
+
103
+ class IndexNotFoundError(QueryError):
104
+ """No ``.compass/graph.ladybug`` was found walking up from ``searched_from``.
105
+
106
+ Attributes:
107
+ searched_from: Directory the upward search started from.
108
+ """
109
+
110
+ code: ClassVar[ErrorCode] = ErrorCode.INDEX_NOT_FOUND
111
+
112
+ def __init__(self, *, searched_from: Path) -> None:
113
+ super().__init__(f"No Compass index found from {searched_from}. Run `compass ingest`.")
114
+ self.searched_from = searched_from
115
+
116
+
117
+ class IndexBusyError(QueryError):
118
+ """The index is locked by another process (typically an ingest rebuild).
119
+
120
+ Attributes:
121
+ retry_after_s: Suggested number of seconds to wait before retrying.
122
+ """
123
+
124
+ code: ClassVar[ErrorCode] = ErrorCode.INDEX_BUSY
125
+
126
+ def __init__(self, *, retry_after_s: int) -> None:
127
+ super().__init__(f"The index is being rebuilt; retry in {retry_after_s}s.")
128
+ self.retry_after_s = retry_after_s
129
+
130
+
131
+ class IndexIncompatibleError(QueryError):
132
+ """The index or its metadata is unreadable or from an unsupported version."""
133
+
134
+ code: ClassVar[ErrorCode] = ErrorCode.INDEX_INCOMPATIBLE
135
+
136
+
137
+ class QueryTimeoutError(QueryError):
138
+ """A query ran longer than the configured timeout and was interrupted.
139
+
140
+ Attributes:
141
+ timeout_ms: The timeout, in milliseconds, that was exceeded.
142
+ """
143
+
144
+ code: ClassVar[ErrorCode] = ErrorCode.QUERY_TIMEOUT
145
+
146
+ def __init__(self, *, timeout_ms: int) -> None:
147
+ super().__init__(f"Query exceeded {timeout_ms} ms.")
148
+ self.timeout_ms = timeout_ms
File without changes