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,116 @@
|
|
|
1
|
+
"""Typed error envelope and the per-code mapping used by every adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
|
|
9
|
+
from compass_core.errors import (
|
|
10
|
+
ErrorCode,
|
|
11
|
+
IndexBusyError,
|
|
12
|
+
IndexNotFoundError,
|
|
13
|
+
InvalidArgumentError,
|
|
14
|
+
QueryError,
|
|
15
|
+
QueryTimeoutError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
20
|
+
class ErrorMapping:
|
|
21
|
+
"""How one error code surfaces in each adapter.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
exit_code: CLI process exit code.
|
|
25
|
+
http_status: HTTP status code.
|
|
26
|
+
mcp_is_error: Whether MCP reports a tool error (else a structured result).
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
``ErrorMapping(exit_code=2, http_status=422, mcp_is_error=True)``
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
exit_code: int
|
|
33
|
+
http_status: int
|
|
34
|
+
mcp_is_error: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
38
|
+
class ErrorDetail:
|
|
39
|
+
"""One key/value fact attached to an error.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
key: Detail name, e.g. ``"argument"``.
|
|
43
|
+
value: Detail value rendered as text.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
``ErrorDetail(key="retry_after_s", value="5")``
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
key: str
|
|
50
|
+
value: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
54
|
+
class ErrorResponse:
|
|
55
|
+
"""The single envelope adapters render for any failure.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
code: Machine-readable failure code.
|
|
59
|
+
message: One sentence plus the fix.
|
|
60
|
+
details: Typed context for the failure.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
``InvalidArgumentError(argument="depth", reason="too big")`` becomes
|
|
64
|
+
``ErrorResponse(code=INVALID_ARGUMENT, message="Invalid depth: too big",
|
|
65
|
+
details=(ErrorDetail(key="argument", value="depth"),
|
|
66
|
+
ErrorDetail(key="reason", value="too big")))``
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
code: ErrorCode
|
|
70
|
+
message: str
|
|
71
|
+
details: tuple[ErrorDetail, ...]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
ERROR_MAPPINGS: Mapping[ErrorCode, ErrorMapping] = MappingProxyType(
|
|
75
|
+
{
|
|
76
|
+
ErrorCode.INVALID_ARGUMENT: ErrorMapping(exit_code=2, http_status=422, mcp_is_error=True),
|
|
77
|
+
ErrorCode.NOT_FOUND: ErrorMapping(exit_code=3, http_status=404, mcp_is_error=False),
|
|
78
|
+
ErrorCode.AMBIGUOUS: ErrorMapping(exit_code=4, http_status=409, mcp_is_error=False),
|
|
79
|
+
ErrorCode.INDEX_NOT_FOUND: ErrorMapping(exit_code=5, http_status=503, mcp_is_error=True),
|
|
80
|
+
ErrorCode.INDEX_BUSY: ErrorMapping(exit_code=6, http_status=503, mcp_is_error=True),
|
|
81
|
+
ErrorCode.INDEX_INCOMPATIBLE: ErrorMapping(exit_code=7, http_status=503, mcp_is_error=True),
|
|
82
|
+
ErrorCode.QUERY_TIMEOUT: ErrorMapping(exit_code=8, http_status=504, mcp_is_error=True),
|
|
83
|
+
ErrorCode.INTERNAL: ErrorMapping(exit_code=1, http_status=500, mcp_is_error=True),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _details(exc: QueryError) -> tuple[ErrorDetail, ...]:
|
|
89
|
+
if isinstance(exc, InvalidArgumentError):
|
|
90
|
+
return (
|
|
91
|
+
ErrorDetail(key="argument", value=exc.argument),
|
|
92
|
+
ErrorDetail(key="reason", value=exc.reason),
|
|
93
|
+
)
|
|
94
|
+
if isinstance(exc, IndexNotFoundError):
|
|
95
|
+
return (ErrorDetail(key="searched_from", value=str(exc.searched_from)),)
|
|
96
|
+
if isinstance(exc, IndexBusyError):
|
|
97
|
+
return (ErrorDetail(key="retry_after_s", value=str(exc.retry_after_s)),)
|
|
98
|
+
if isinstance(exc, QueryTimeoutError):
|
|
99
|
+
return (ErrorDetail(key="timeout_ms", value=str(exc.timeout_ms)),)
|
|
100
|
+
return ()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def to_error_response(exc: QueryError) -> ErrorResponse:
|
|
104
|
+
"""Build the error envelope for a query-layer exception.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
exc: The exception raised by the query layer.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
An ``ErrorResponse`` carrying the exception's code, message and typed details.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
``to_error_response(IndexBusyError(retry_after_s=5))`` returns an
|
|
114
|
+
``INDEX_BUSY`` response with ``ErrorDetail(key="retry_after_s", value="5")``.
|
|
115
|
+
"""
|
|
116
|
+
return ErrorResponse(code=exc.code, message=str(exc), details=_details(exc))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Building-block models shared by every query result.
|
|
2
|
+
|
|
3
|
+
All models are plain frozen dataclasses with no I/O or database dependencies.
|
|
4
|
+
Invariants are checked on construction; a violation raises ``ValueError``
|
|
5
|
+
because it is a bug in the query layer, never caused by user input.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from compass_core.contract.types import CaveatCode, QualifiedName, RepoPath, SymbolId, ViaKind
|
|
15
|
+
from compass_core.shared_types import CallType, EdgeType, SymbolKind
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
19
|
+
class SymbolRef:
|
|
20
|
+
"""A reference to one graph node (file, class, function, method or external).
|
|
21
|
+
|
|
22
|
+
Public ids are ``<repo-relative POSIX path>::<[Class.]name>`` for classes and
|
|
23
|
+
functions, ``file::<repo-relative path>`` for files and ``ext::<full_ref>`` for
|
|
24
|
+
externals.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
id: Public symbol id.
|
|
28
|
+
kind: Kind of node.
|
|
29
|
+
name: Short name, e.g. ``"write"``.
|
|
30
|
+
qualified_name: Dotted qualified name.
|
|
31
|
+
file_path: Repo-relative POSIX path; ``None`` only for ``EXTERNAL``.
|
|
32
|
+
line_start: 1-indexed first line; ``None`` for ``FILE`` and ``EXTERNAL``.
|
|
33
|
+
line_end: 1-indexed last line, inclusive; ``None`` when ``line_start`` is.
|
|
34
|
+
signature: Signature text; ``FUNCTION`` and ``METHOD`` only.
|
|
35
|
+
source: Source text; set only when the caller asked for source.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
``def f(): ...`` on lines 1-2 of ``a.py`` becomes::
|
|
39
|
+
|
|
40
|
+
SymbolRef(id=SymbolId("a.py::f"), kind=SymbolKind.FUNCTION, name="f",
|
|
41
|
+
qualified_name=QualifiedName("a.f"), file_path=RepoPath("a.py"),
|
|
42
|
+
line_start=1, line_end=2, signature="def f()")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
id: SymbolId
|
|
46
|
+
kind: SymbolKind
|
|
47
|
+
name: str
|
|
48
|
+
qualified_name: QualifiedName
|
|
49
|
+
file_path: RepoPath | None
|
|
50
|
+
line_start: int | None
|
|
51
|
+
line_end: int | None
|
|
52
|
+
signature: str | None
|
|
53
|
+
source: str | None = None
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
if self.line_start is not None and self.line_start < 1:
|
|
57
|
+
raise ValueError(f"line_start must be >= 1 (1-indexed), got {self.line_start}")
|
|
58
|
+
if self.line_end is not None and self.line_end < 1:
|
|
59
|
+
raise ValueError(f"line_end must be >= 1 (1-indexed), got {self.line_end}")
|
|
60
|
+
if (
|
|
61
|
+
self.line_start is not None
|
|
62
|
+
and self.line_end is not None
|
|
63
|
+
and self.line_end < self.line_start
|
|
64
|
+
):
|
|
65
|
+
raise ValueError(f"line_end {self.line_end} is before line_start {self.line_start}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
69
|
+
class EdgeRef:
|
|
70
|
+
"""A directed graph edge.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
source_id: Id of the edge's source node.
|
|
74
|
+
target_id: Id of the edge's target node.
|
|
75
|
+
type: Edge type.
|
|
76
|
+
confidence: Resolution confidence in ``[0, 1]``; set iff ``type`` is ``CALLS``.
|
|
77
|
+
call_type: Resolution type; set iff ``type`` is ``CALLS``.
|
|
78
|
+
line: 1-indexed call site for ``CALLS`` / ``CALLS_EXTERN``; ``None`` if unknown.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
``a`` calls ``b`` at line 3 through a direct import::
|
|
82
|
+
|
|
83
|
+
EdgeRef(source_id=SymbolId("a"), target_id=SymbolId("b"), type=EdgeType.CALLS,
|
|
84
|
+
confidence=1.0, call_type=CallType.DIRECT_IMPORT, line=3)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
source_id: SymbolId
|
|
88
|
+
target_id: SymbolId
|
|
89
|
+
type: EdgeType
|
|
90
|
+
confidence: float | None
|
|
91
|
+
call_type: CallType | None
|
|
92
|
+
line: int | None
|
|
93
|
+
|
|
94
|
+
def __post_init__(self) -> None:
|
|
95
|
+
is_calls = self.type is EdgeType.CALLS
|
|
96
|
+
if is_calls != (self.confidence is not None):
|
|
97
|
+
raise ValueError("confidence must be set if and only if type is CALLS")
|
|
98
|
+
if is_calls != (self.call_type is not None):
|
|
99
|
+
raise ValueError("call_type must be set if and only if type is CALLS")
|
|
100
|
+
if self.confidence is not None and not 0.0 <= self.confidence <= 1.0:
|
|
101
|
+
raise ValueError(f"confidence must be in [0, 1], got {self.confidence}")
|
|
102
|
+
if self.line is not None and self.line < 1:
|
|
103
|
+
raise ValueError(f"line must be >= 1 (1-indexed), got {self.line}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
107
|
+
class Via:
|
|
108
|
+
"""Why an item is in an answer, e.g. "calls write".
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
kind: Relationship to ``symbol``.
|
|
112
|
+
symbol: The node the relationship points at.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
``g`` is reached because it calls ``f``::
|
|
116
|
+
|
|
117
|
+
Via(kind=ViaKind.CALLS, symbol=<SymbolRef for f>)
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
kind: ViaKind
|
|
121
|
+
symbol: SymbolRef
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
125
|
+
class ResultItem:
|
|
126
|
+
"""One symbol in a traversal answer.
|
|
127
|
+
|
|
128
|
+
Attributes:
|
|
129
|
+
symbol: The symbol reached.
|
|
130
|
+
distance: Hops from the target; 0 is the target itself.
|
|
131
|
+
via: Why the item is present; ``None`` iff ``distance == 0``.
|
|
132
|
+
min_confidence: Lowest ``CALLS`` confidence on the path; ``None`` when the
|
|
133
|
+
path has no ``CALLS`` edge.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
A direct caller of the target::
|
|
137
|
+
|
|
138
|
+
ResultItem(symbol=<caller>, distance=1,
|
|
139
|
+
via=Via(kind=ViaKind.CALLS, symbol=<target>), min_confidence=1.0)
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
symbol: SymbolRef
|
|
143
|
+
distance: int
|
|
144
|
+
via: Via | None
|
|
145
|
+
min_confidence: float | None
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
if self.distance < 0:
|
|
149
|
+
raise ValueError(f"distance must be >= 0, got {self.distance}")
|
|
150
|
+
if (self.distance == 0) != (self.via is None):
|
|
151
|
+
raise ValueError("via must be None if and only if distance == 0")
|
|
152
|
+
if self.min_confidence is not None and not 0.0 <= self.min_confidence <= 1.0:
|
|
153
|
+
raise ValueError(f"min_confidence must be in [0, 1], got {self.min_confidence}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
157
|
+
class Caveat:
|
|
158
|
+
"""A reason an answer may be incomplete.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
code: Machine-readable caveat code.
|
|
162
|
+
message: Rendered sentence for humans.
|
|
163
|
+
count: The number in the message, for machine use.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
``Caveat(code=CaveatCode.UNRESOLVED_CALLS, message="4 unresolved calls in 3
|
|
167
|
+
affected functions; there may be more callers", count=4)``
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
code: CaveatCode
|
|
171
|
+
message: str
|
|
172
|
+
count: int | None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
176
|
+
class IndexStatus:
|
|
177
|
+
"""Freshness and identity of the index that produced an answer.
|
|
178
|
+
|
|
179
|
+
Attributes:
|
|
180
|
+
repo_root: Absolute path of the indexed repository.
|
|
181
|
+
indexed_at: Timezone-aware UTC time of the last ingest.
|
|
182
|
+
indexed_commit: Git commit at ingest; ``None`` when not a git checkout.
|
|
183
|
+
schema_version: Graph schema version of the index.
|
|
184
|
+
stale_file_count: Indexed files changed or deleted since ``indexed_at``.
|
|
185
|
+
new_file_count: Unindexed files; computed by ``status`` only.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
``IndexStatus(repo_root=Path("/repo"), indexed_at=datetime(2026, 1, 2,
|
|
189
|
+
tzinfo=UTC), indexed_commit=None, schema_version=2, stale_file_count=0,
|
|
190
|
+
new_file_count=None)``
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
repo_root: Path
|
|
194
|
+
indexed_at: datetime
|
|
195
|
+
indexed_commit: str | None
|
|
196
|
+
schema_version: int
|
|
197
|
+
stale_file_count: int
|
|
198
|
+
new_file_count: int | None
|
|
199
|
+
|
|
200
|
+
def __post_init__(self) -> None:
|
|
201
|
+
if self.indexed_at.tzinfo is None or self.indexed_at.utcoffset() is None:
|
|
202
|
+
raise ValueError("indexed_at must be timezone-aware")
|
|
203
|
+
if not self.repo_root.is_absolute():
|
|
204
|
+
raise ValueError(f"repo_root must be absolute, got {self.repo_root}")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Parameter objects for query operations.
|
|
2
|
+
|
|
3
|
+
Bounds are module constants so the CLI, MCP server and API can all read them.
|
|
4
|
+
``__post_init__`` also enforces them and raises ``InvalidArgumentError``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from compass_core.errors import InvalidArgumentError
|
|
12
|
+
from compass_core.shared_types import SymbolKind
|
|
13
|
+
|
|
14
|
+
MAX_DEPTH = 10
|
|
15
|
+
"""Largest allowed traversal depth."""
|
|
16
|
+
|
|
17
|
+
MAX_LIMIT = 500
|
|
18
|
+
"""Largest allowed result limit."""
|
|
19
|
+
|
|
20
|
+
MAX_TEXT_LENGTH = 512
|
|
21
|
+
"""Longest allowed search query or symbol reference, in characters."""
|
|
22
|
+
|
|
23
|
+
DEFAULT_LIMIT = 50
|
|
24
|
+
"""Result limit when the caller gives none."""
|
|
25
|
+
|
|
26
|
+
DEFAULT_IMPACT_DEPTH = 3
|
|
27
|
+
"""Impact traversal depth when the caller gives none."""
|
|
28
|
+
|
|
29
|
+
DEFAULT_MIN_CONFIDENCE = 0.0
|
|
30
|
+
"""Lowest ``CALLS`` confidence traversed when the caller gives none: every edge."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_int(value: object) -> bool:
|
|
34
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_limit(limit: int) -> None:
|
|
38
|
+
if not _is_int(limit) or not 1 <= limit <= MAX_LIMIT:
|
|
39
|
+
raise InvalidArgumentError(argument="limit", reason=f"must be between 1 and {MAX_LIMIT}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check_depth(depth: int) -> None:
|
|
43
|
+
if not _is_int(depth) or not 1 <= depth <= MAX_DEPTH:
|
|
44
|
+
raise InvalidArgumentError(argument="depth", reason=f"must be between 1 and {MAX_DEPTH}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _check_confidence(min_confidence: float) -> None:
|
|
48
|
+
if (
|
|
49
|
+
isinstance(min_confidence, bool)
|
|
50
|
+
or not isinstance(min_confidence, (int, float))
|
|
51
|
+
or not 0.0 <= min_confidence <= 1.0
|
|
52
|
+
):
|
|
53
|
+
raise InvalidArgumentError(argument="min_confidence", reason="must be between 0 and 1")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
57
|
+
class SearchParams:
|
|
58
|
+
"""Parameters for symbol search.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
query: Non-empty search text.
|
|
62
|
+
kind: Restrict hits to one symbol kind; ``None`` for all.
|
|
63
|
+
limit: Maximum hits, ``1..MAX_LIMIT``.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
``SearchParams(query="write", kind=SymbolKind.FUNCTION, limit=20)``
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
query: str
|
|
70
|
+
kind: SymbolKind | None = None
|
|
71
|
+
limit: int = DEFAULT_LIMIT
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
if not isinstance(self.query, str) or not self.query.strip():
|
|
75
|
+
raise InvalidArgumentError(argument="query", reason="must not be empty")
|
|
76
|
+
_check_limit(self.limit)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
80
|
+
class ImpactParams:
|
|
81
|
+
"""Parameters for impact analysis.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
depth: Maximum hops to traverse, ``1..MAX_DEPTH``.
|
|
85
|
+
min_confidence: Ignore CALLS edges below this confidence, ``0..1``.
|
|
86
|
+
limit: Maximum affected symbols returned, ``1..MAX_LIMIT``.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
``ImpactParams(depth=3, min_confidence=0.5, limit=100)``
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
depth: int = DEFAULT_IMPACT_DEPTH
|
|
93
|
+
min_confidence: float = DEFAULT_MIN_CONFIDENCE
|
|
94
|
+
limit: int = DEFAULT_LIMIT
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
_check_depth(self.depth)
|
|
98
|
+
_check_confidence(self.min_confidence)
|
|
99
|
+
_check_limit(self.limit)
|