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,45 @@
1
+ """Progress reporting protocol for pipeline phases.
2
+
3
+ Core modules call ``reporter.update(phase, current, total)`` without any
4
+ dependency on display libraries. The CLI layer provides a concrete
5
+ ``IngestReporter`` implementation (a live Rich table); tests can use a null
6
+ or recording stub that satisfies the same protocol.
7
+
8
+ Adding a new phase
9
+ ------------------
10
+ 1. Add its name to the ``Phase`` literal.
11
+ 2. Call ``reporter.update("<new_phase>", current, total)`` in the relevant
12
+ core module.
13
+ 3. Wire it up in ``compass_cli.progress.IngestReporter``.
14
+
15
+ Nothing else needs to change.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Literal, Protocol
21
+
22
+ Phase = Literal[
23
+ "discovery",
24
+ "parse",
25
+ "symbol_table",
26
+ "resolution",
27
+ "validation",
28
+ "resolve",
29
+ "write_nodes",
30
+ "write_edges",
31
+ ]
32
+
33
+
34
+ class ProgressReporter(Protocol):
35
+ """Minimal interface for reporting progress across pipeline phases."""
36
+
37
+ def update(self, phase: Phase, current: int, total: int) -> None:
38
+ """Report progress for a pipeline phase.
39
+
40
+ Args:
41
+ phase: The pipeline phase being reported (e.g. ``"parse"``).
42
+ current: Number of items completed so far.
43
+ total: Total number of items for this phase.
44
+ """
45
+ ...
compass_core/py.typed ADDED
File without changes
@@ -0,0 +1 @@
1
+ """Query-layer contract: typed vocabularies and result models."""
@@ -0,0 +1,92 @@
1
+ """Typed Cypher queries: each statement lives next to the decoder for its rows.
2
+
3
+ A ``Query`` is defined once, at module level, with its full Cypher text. Running it
4
+ returns typed values instead of raw rows, so no caller indexes a row by position, and
5
+ the ``$parameters`` a statement uses are checked against the ones supplied.
6
+
7
+ User text only ever reaches Cypher as a ``$parameter``. Statements may be composed
8
+ from the column lists in ``query/rows.py`` when the module is imported, never per call.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Callable, Mapping
15
+ from dataclasses import dataclass, field
16
+ from typing import Generic, TypeVar
17
+
18
+ from compass_core.graph.db import GraphDB, QueryParams, Row, as_rows
19
+
20
+ T = TypeVar("T")
21
+
22
+ _PARAMETER = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)")
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Query(Generic[T]):
27
+ """A Cypher statement and the decoder for one of its result rows.
28
+
29
+ Attributes:
30
+ cypher: The full statement. User input appears only as ``$name`` parameters.
31
+ decode: Turns one result row into a typed value.
32
+ parameters: Names of the ``$parameters`` the statement uses, derived from
33
+ ``cypher``.
34
+
35
+ Example:
36
+ ``FILE_PATHS = Query("MATCH (f:File) RETURN f.path", lambda row: text_value(row[0],
37
+ "path"))`` makes ``FILE_PATHS.run(db)`` return ``["app/auth.py", ...]``.
38
+ """
39
+
40
+ cypher: str
41
+ decode: Callable[[Row], T]
42
+ parameters: frozenset[str] = field(init=False)
43
+
44
+ def __post_init__(self) -> None:
45
+ object.__setattr__(self, "parameters", frozenset(_PARAMETER.findall(self.cypher)))
46
+
47
+ def run(self, db: GraphDB, params: Mapping[str, object] | None = None) -> list[T]:
48
+ """Execute the statement and decode every row.
49
+
50
+ Args:
51
+ db: Open graph database.
52
+ params: A value for every ``$parameter`` of the statement, and nothing else.
53
+
54
+ Returns:
55
+ One decoded value per result row, in result order.
56
+
57
+ Raises:
58
+ ValueError: ``params`` does not name exactly the statement's parameters. This
59
+ is a bug in the query layer, never caused by user input.
60
+ IndexIncompatibleError: A row does not have the shape the decoder expects.
61
+
62
+ Example:
63
+ ``ENTRY_POINTS.run(db, {"limit": 5})`` returns up to five file symbols.
64
+ """
65
+ supplied: QueryParams = dict(params or {})
66
+ if supplied.keys() != self.parameters:
67
+ raise ValueError(
68
+ f"query expects parameters {sorted(self.parameters)}, got {sorted(supplied)}"
69
+ )
70
+ result = db.execute(self.cypher, supplied) if supplied else db.execute(self.cypher)
71
+ rows = as_rows(result)
72
+ return [self.decode(row) for row in rows]
73
+
74
+ def first(self, db: GraphDB, params: Mapping[str, object] | None = None) -> T | None:
75
+ """Like ``run`` but return only the first decoded row, or ``None`` when empty.
76
+
77
+ Args:
78
+ db: Open graph database.
79
+ params: A value for every ``$parameter`` of the statement, and nothing else.
80
+
81
+ Returns:
82
+ The first decoded value, or ``None`` if the statement returned no rows.
83
+
84
+ Raises:
85
+ ValueError: ``params`` does not name exactly the statement's parameters.
86
+ IndexIncompatibleError: The row does not have the shape the decoder expects.
87
+
88
+ Example:
89
+ ``FUNCTION_COUNT.first(db)`` returns ``412`` on a repo with 412 functions.
90
+ """
91
+ found = self.run(db, params)
92
+ return found[0] if found else None
@@ -0,0 +1,241 @@
1
+ """Node detail: everything the graph knows about one file, class, function or method.
2
+
3
+ The reference is resolved with ``references.resolve`` (a public id such as
4
+ ``app/auth/permissions.py::Policy`` or ``file::app/auth/permissions.py`` resolves under the
5
+ exact-id rule). The resolved symbol is then enriched with its docstring and source text and
6
+ with the neighbours that make sense for its kind:
7
+
8
+ * class: base classes, subclasses and methods;
9
+ * function / method: callees and callers;
10
+ * file: the classes and functions it defines, the files it imports and the files that import it,
11
+ and its text, read from the working tree (the index does not store file contents);
12
+ * external: nothing beyond the symbol.
13
+
14
+ User text only ever reaches Cypher as a ``$parameter``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import replace
20
+ from pathlib import Path
21
+
22
+ from compass_core.contract.models import SymbolRef
23
+ from compass_core.contract.results import Lookup, NodeDetail
24
+ from compass_core.graph.db import GraphDB, QueryParams, Row
25
+ from compass_core.index.locate import IndexLocation
26
+ from compass_core.query.cypher import Query
27
+ from compass_core.query.references import ResolvedReference, resolve
28
+ from compass_core.query.rows import (
29
+ CLASS_FIELDS,
30
+ FILE_FIELDS,
31
+ FUNCTION_FIELDS,
32
+ class_from_row,
33
+ columns,
34
+ file_from_row,
35
+ function_from_row,
36
+ )
37
+ from compass_core.query.status import index_status
38
+ from compass_core.shared_types import SymbolKind
39
+
40
+ MAX_FILE_BYTES = 512 * 1024
41
+ """Largest file whose text is returned; bigger files are described without source."""
42
+ MAX_RELATED = 200
43
+ """Most neighbours returned per relationship; the result is flagged ``truncated`` past it."""
44
+
45
+
46
+ def _is_definition(variable: str) -> str:
47
+ """``WHERE`` condition selecting the definition ``$id`` (a primary-key match).
48
+
49
+ Used only to compose the constants below, at import.
50
+ """
51
+ return f"{variable}.id = $id"
52
+
53
+
54
+ def _text_pair(row: Row) -> tuple[str | None, str | None]:
55
+ return _text_or_none(row[0]), _text_or_none(row[1])
56
+
57
+
58
+ _CLASS = columns("r", CLASS_FIELDS)
59
+ _FUNCTION = columns("r", FUNCTION_FIELDS)
60
+
61
+ _BASES: Query[SymbolRef] = Query(
62
+ f"MATCH (c:Class)-[:INHERITS]->(r:Class) WHERE {_is_definition('c')} RETURN DISTINCT {_CLASS}",
63
+ class_from_row,
64
+ )
65
+ _SUBCLASSES: Query[SymbolRef] = Query(
66
+ f"MATCH (r:Class)-[:INHERITS]->(c:Class) WHERE {_is_definition('c')} RETURN DISTINCT {_CLASS}",
67
+ class_from_row,
68
+ )
69
+ _METHODS: Query[SymbolRef] = Query(
70
+ f"MATCH (c:Class)-[:CONTAINS]->(r:Function) WHERE {_is_definition('c')} "
71
+ f"RETURN DISTINCT {_FUNCTION}",
72
+ function_from_row,
73
+ )
74
+ _CALLEES: Query[SymbolRef] = Query(
75
+ f"MATCH (f:Function)-[:CALLS]->(r:Function) WHERE {_is_definition('f')} "
76
+ f"RETURN DISTINCT {_FUNCTION}",
77
+ function_from_row,
78
+ )
79
+ _CALLERS: Query[SymbolRef] = Query(
80
+ f"MATCH (r:Function)-[:CALLS]->(f:Function) WHERE {_is_definition('f')} "
81
+ f"RETURN DISTINCT {_FUNCTION}",
82
+ function_from_row,
83
+ )
84
+ _FILE_CLASSES: Query[SymbolRef] = Query(
85
+ f"MATCH (f:File)-[:CONTAINS]->(r:Class) WHERE f.path = $path RETURN DISTINCT {_CLASS}",
86
+ class_from_row,
87
+ )
88
+ _FILE_FUNCTIONS: Query[SymbolRef] = Query(
89
+ f"MATCH (f:File)-[:CONTAINS]->(r:Function) WHERE f.path = $path RETURN DISTINCT {_FUNCTION}",
90
+ function_from_row,
91
+ )
92
+ _IMPORTS: Query[SymbolRef] = Query(
93
+ f"MATCH (f:File)-[:IMPORTS]->(r:File) WHERE f.path = $path "
94
+ f"RETURN DISTINCT {columns('r', FILE_FIELDS)}",
95
+ file_from_row,
96
+ )
97
+ _IMPORTED_BY: Query[SymbolRef] = Query(
98
+ f"MATCH (r:File)-[:IMPORTS]->(f:File) WHERE f.path = $path "
99
+ f"RETURN DISTINCT {columns('r', FILE_FIELDS)}",
100
+ file_from_row,
101
+ )
102
+ _CLASS_TEXT: Query[tuple[str | None, str | None]] = Query(
103
+ f"MATCH (n:Class) WHERE {_is_definition('n')} RETURN n.docstring, n.source",
104
+ _text_pair,
105
+ )
106
+ _FUNCTION_TEXT: Query[tuple[str | None, str | None]] = Query(
107
+ f"MATCH (n:Function) WHERE {_is_definition('n')} RETURN n.docstring, n.source",
108
+ _text_pair,
109
+ )
110
+
111
+
112
+ def _definition_params(symbol: SymbolRef) -> QueryParams:
113
+ return {"id": symbol.id}
114
+
115
+
116
+ def _text_or_none(value: object) -> str | None:
117
+ """Docstring/source column: ``None`` or empty means absent."""
118
+ return value if isinstance(value, str) and value.strip() else None
119
+
120
+
121
+ def _ordered(symbols: list[SymbolRef]) -> list[SymbolRef]:
122
+ """Stable display order: by file, line, then id."""
123
+ return sorted(symbols, key=lambda s: (s.file_path or "", s.line_start or 0, s.id))
124
+
125
+
126
+ def _class_relations(
127
+ db: GraphDB, params: QueryParams
128
+ ) -> tuple[list[SymbolRef], list[SymbolRef], list[SymbolRef]]:
129
+ return (
130
+ _BASES.run(db, params),
131
+ _SUBCLASSES.run(db, params),
132
+ _METHODS.run(db, params),
133
+ )
134
+
135
+
136
+ def _function_relations(
137
+ db: GraphDB, params: QueryParams
138
+ ) -> tuple[list[SymbolRef], list[SymbolRef]]:
139
+ return _CALLEES.run(db, params), _CALLERS.run(db, params)
140
+
141
+
142
+ def _file_relations(
143
+ db: GraphDB, symbol: SymbolRef
144
+ ) -> tuple[list[SymbolRef], list[SymbolRef], list[SymbolRef]]:
145
+ params: QueryParams = {"path": symbol.file_path}
146
+ members = _FILE_CLASSES.run(db, params) + _FILE_FUNCTIONS.run(db, params)
147
+ return members, _IMPORTS.run(db, params), _IMPORTED_BY.run(db, params)
148
+
149
+
150
+ def _cap(symbols: list[SymbolRef]) -> tuple[tuple[SymbolRef, ...], bool]:
151
+ ordered = _ordered(symbols)
152
+ return tuple(ordered[:MAX_RELATED]), len(ordered) > MAX_RELATED
153
+
154
+
155
+ def _docstring_and_source(db: GraphDB, symbol: SymbolRef) -> tuple[str | None, str | None]:
156
+ if symbol.kind not in (SymbolKind.CLASS, SymbolKind.FUNCTION, SymbolKind.METHOD):
157
+ return None, None
158
+ query = _CLASS_TEXT if symbol.kind is SymbolKind.CLASS else _FUNCTION_TEXT
159
+ return query.first(db, _definition_params(symbol)) or (None, None)
160
+
161
+
162
+ def _file_source(symbol: SymbolRef, root: Path) -> str | None:
163
+ """Text of a file under ``root``, or ``None`` if it is gone, unreadable, binary-sized or outside.
164
+
165
+ The text is what is on disk now, which can differ from what was indexed; the index status in
166
+ the result says whether files changed since.
167
+ """
168
+ if not symbol.file_path:
169
+ return None
170
+ path = (root / symbol.file_path).resolve()
171
+ try:
172
+ if (
173
+ not path.is_relative_to(root)
174
+ or not path.is_file()
175
+ or path.stat().st_size > MAX_FILE_BYTES
176
+ ):
177
+ return None
178
+ return _text_or_none(path.read_text(encoding="utf-8", errors="replace"))
179
+ except OSError:
180
+ return None
181
+
182
+
183
+ def describe(db: GraphDB, location: IndexLocation, ref: str) -> Lookup[NodeDetail]:
184
+ """Describe one node of the graph.
185
+
186
+ Args:
187
+ db: Open graph database.
188
+ location: Index location, used to build the result's ``IndexStatus``.
189
+ ref: Reference to the node; a public id from the graph endpoint always resolves.
190
+
191
+ Returns:
192
+ A ``NodeDetail``; an ``AmbiguousReference`` or ``SymbolNotFound`` when ``ref`` does
193
+ not resolve to exactly one node.
194
+
195
+ Raises:
196
+ IndexIncompatibleError: A stored row or the index metadata is unreadable.
197
+
198
+ Example:
199
+ ``describe(db, loc, "app/models.py::User")`` returns a ``NodeDetail`` whose ``symbol``
200
+ has the class source, ``bases`` is empty and ``members`` lists ``User.save``.
201
+ """
202
+ status = index_status(location, db) # first: rejects an incompatible index
203
+ root = location.repo_root.resolve()
204
+ resolved = resolve(db, ref, repo_root=root, allow_file=True)
205
+ if not isinstance(resolved, ResolvedReference):
206
+ return resolved
207
+
208
+ symbol = resolved.symbol
209
+ docstring, source = _docstring_and_source(db, symbol)
210
+ if symbol.kind is SymbolKind.FILE:
211
+ source = _file_source(symbol, root)
212
+ bases: list[SymbolRef] = []
213
+ subclasses: list[SymbolRef] = []
214
+ members: list[SymbolRef] = []
215
+ callees: list[SymbolRef] = []
216
+ callers: list[SymbolRef] = []
217
+ imports: list[SymbolRef] = []
218
+ imported_by: list[SymbolRef] = []
219
+
220
+ if symbol.kind is SymbolKind.CLASS:
221
+ bases, subclasses, members = _class_relations(db, _definition_params(symbol))
222
+ elif symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
223
+ callees, callers = _function_relations(db, _definition_params(symbol))
224
+ elif symbol.kind is SymbolKind.FILE:
225
+ members, imports, imported_by = _file_relations(db, symbol)
226
+
227
+ capped = [_cap(x) for x in (bases, subclasses, members, callees, callers, imports, imported_by)]
228
+ return NodeDetail(
229
+ index=status,
230
+ caveats=(),
231
+ symbol=replace(symbol, source=source),
232
+ docstring=docstring,
233
+ bases=capped[0][0],
234
+ subclasses=capped[1][0],
235
+ members=capped[2][0],
236
+ callees=capped[3][0],
237
+ callers=capped[4][0],
238
+ imports=capped[5][0],
239
+ imported_by=capped[6][0],
240
+ truncated=any(truncated for _, truncated in capped),
241
+ )
@@ -0,0 +1,127 @@
1
+ """The whole graph as nodes and edges, for adapters that draw it.
2
+
3
+ Unlike the other operations this returns no ``ResultHeader``: a map answers no question
4
+ about the index, so it carries neither an ``IndexStatus`` nor caveats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from compass_core.contract.results import GraphMap, MapEdge, MapNode
12
+ from compass_core.contract.types import SymbolId
13
+ from compass_core.graph.db import GraphDB, Row
14
+ from compass_core.query.cypher import Query
15
+ from compass_core.query.rows import (
16
+ is_method_flag,
17
+ public_file_id,
18
+ text_value,
19
+ )
20
+ from compass_core.shared_types import EdgeType, SymbolKind
21
+
22
+
23
+ @dataclass(frozen=True, slots=True, kw_only=True)
24
+ class _StoredDefinition:
25
+ """A class or function as stored: id, name and path.
26
+
27
+ Attributes:
28
+ id: Public id.
29
+ name: Unqualified name.
30
+ kind: ``CLASS``, ``FUNCTION`` or ``METHOD``.
31
+ path: Stored ``file_path``.
32
+
33
+ Example:
34
+ ``class User`` in ``app/models.py`` gives ``_StoredDefinition(id="app/models.py::User",
35
+ name="User", kind=SymbolKind.CLASS, path="app/models.py")``.
36
+ """
37
+
38
+ id: str
39
+ name: str
40
+ kind: SymbolKind
41
+ path: str
42
+
43
+
44
+ def _definition(row: Row, kind: SymbolKind) -> _StoredDefinition:
45
+ return _StoredDefinition(
46
+ id=text_value(row[0], "id"),
47
+ name=text_value(row[1], "name"),
48
+ kind=kind,
49
+ path=text_value(row[2], "file_path"),
50
+ )
51
+
52
+
53
+ def _edge_query(cypher: str, edge_type: EdgeType) -> Query[MapEdge]:
54
+ return Query(
55
+ cypher,
56
+ lambda row: MapEdge(
57
+ source=text_value(row[0], "source"), target=text_value(row[1], "target"), type=edge_type
58
+ ),
59
+ )
60
+
61
+
62
+ _FILE_PATHS: Query[str] = Query(
63
+ "MATCH (f:File) RETURN f.path", lambda row: text_value(row[0], "path")
64
+ )
65
+ _CLASSES: Query[_StoredDefinition] = Query(
66
+ "MATCH (c:Class) RETURN c.id, c.name, c.file_path",
67
+ lambda row: _definition(row, SymbolKind.CLASS),
68
+ )
69
+ _FUNCTIONS: Query[_StoredDefinition] = Query(
70
+ "MATCH (fn:Function) RETURN fn.id, fn.name, fn.file_path, fn.is_method",
71
+ lambda row: _definition(
72
+ row, SymbolKind.METHOD if is_method_flag(row[3]) else SymbolKind.FUNCTION
73
+ ),
74
+ )
75
+ _EDGES: tuple[Query[MapEdge], ...] = (
76
+ _edge_query("MATCH (a:File)-[:CONTAINS]->(b:Class) RETURN a.path, b.id", EdgeType.CONTAINS),
77
+ _edge_query("MATCH (a:File)-[:CONTAINS]->(b:Function) RETURN a.path, b.id", EdgeType.CONTAINS),
78
+ _edge_query("MATCH (a:Class)-[:CONTAINS]->(b:Function) RETURN a.id, b.id", EdgeType.CONTAINS),
79
+ _edge_query("MATCH (a:File)-[:IMPORTS]->(b:File) RETURN a.path, b.path", EdgeType.IMPORTS),
80
+ _edge_query("MATCH (a:Class)-[:INHERITS]->(b:Class) RETURN a.id, b.id", EdgeType.INHERITS),
81
+ _edge_query("MATCH (a:Function)-[:CALLS]->(b:Function) RETURN a.id, b.id", EdgeType.CALLS),
82
+ )
83
+
84
+
85
+ def _file_nodes(db: GraphDB) -> list[MapNode]:
86
+ return [
87
+ MapNode(
88
+ id=path,
89
+ label=path.split("/")[-1],
90
+ kind=SymbolKind.FILE,
91
+ file_path=path,
92
+ public_id=public_file_id(path),
93
+ )
94
+ for path in _FILE_PATHS.run(db)
95
+ ]
96
+
97
+
98
+ def _definition_nodes(db: GraphDB) -> list[MapNode]:
99
+ return [
100
+ MapNode(id=d.id, label=d.name, kind=d.kind, file_path=d.path, public_id=SymbolId(d.id))
101
+ for d in (*_CLASSES.run(db), *_FUNCTIONS.run(db))
102
+ ]
103
+
104
+
105
+ def _edges(db: GraphDB) -> tuple[MapEdge, ...]:
106
+ edges = (edge for query in _EDGES for edge in query.run(db))
107
+ return tuple(dict.fromkeys(edges))
108
+
109
+
110
+ def graph_map(db: GraphDB) -> GraphMap:
111
+ """Read every node and edge of an index.
112
+
113
+ Args:
114
+ db: Open, read-only graph database.
115
+
116
+ Returns:
117
+ The map: files, then classes, then functions and methods, and the distinct
118
+ edges between them.
119
+
120
+ Raises:
121
+ IndexIncompatibleError: A stored column has an unexpected type.
122
+
123
+ Example:
124
+ ``graph_map(db).nodes[0].public_id`` is ``"file::app/auth.py"`` when
125
+ ``app/auth.py`` is the first indexed file.
126
+ """
127
+ return GraphMap(nodes=(*_file_nodes(db), *_definition_nodes(db)), edges=_edges(db))