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,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)