code-atlas-forge 1.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 (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,216 @@
1
+ """Decoder protobuf wire-format mínimo para índices SCIP (lf-0017).
2
+
3
+ Decisão (grill): decoder stdlib próprio — zero dependências. Implementa o
4
+ wire format completo do Protocol Buffers: varints (incl. >32 bits),
5
+ zigzag, fixed32/64, campos length-delimited, packed repeated varints e
6
+ skip de campos desconhecidos. Erros carregam o field-path para diagnóstico.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import struct
12
+ from typing import Any
13
+
14
+ WIRE_VARINT = 0
15
+ WIRE_FIXED64 = 1
16
+ WIRE_LEN = 2
17
+ WIRE_SGROUP = 3 # deprecated — rejeitado explicitamente
18
+ WIRE_EGROUP = 4
19
+ WIRE_FIXED32 = 5
20
+
21
+
22
+ class DecodeError(ValueError):
23
+ """Bytes malformados/truncados — sempre com field-path."""
24
+
25
+
26
+ def _zigzag(v: int) -> int:
27
+ return (v >> 1) ^ -(v & 1)
28
+
29
+
30
+ class Reader:
31
+ """Cursor de leitura sobre um buffer protobuf."""
32
+
33
+ __slots__ = ("_buf", "_end", "_path", "_pos")
34
+
35
+ def __init__(self, buf: bytes, path: str = "") -> None:
36
+ self._buf = buf
37
+ self._pos = 0
38
+ self._end = len(buf)
39
+ self._path = path
40
+
41
+ def eof(self) -> bool:
42
+ return self._pos >= self._end
43
+
44
+ def _err(self, msg: str) -> DecodeError:
45
+ return DecodeError(f"{self._path or '<root>'}: {msg}")
46
+
47
+ def varint(self) -> int:
48
+ """LEB128 — até 10 bytes; truncamento/overflow → erro."""
49
+ result = 0
50
+ shift = 0
51
+ for _ in range(10):
52
+ if self._pos >= self._end:
53
+ raise self._err("truncated varint")
54
+ b = self._buf[self._pos]
55
+ self._pos += 1
56
+ result |= (b & 0x7F) << shift
57
+ if not (b & 0x80):
58
+ return result
59
+ shift += 7
60
+ raise self._err("varint overflow (>10 bytes)")
61
+
62
+ def tag(self) -> tuple[int, int]:
63
+ """(field_number, wire_type); field 0 e tipos inválidos → erro."""
64
+ v = self.varint()
65
+ field_no, wire = v >> 3, v & 0x7
66
+ if field_no == 0:
67
+ raise self._err("field number 0")
68
+ if wire in (WIRE_SGROUP, WIRE_EGROUP):
69
+ raise self._err(f"deprecated group wire type {wire}")
70
+ if wire > WIRE_FIXED32:
71
+ raise self._err(f"invalid wire type {wire}")
72
+ return field_no, wire
73
+
74
+ def _take(self, n: int) -> bytes:
75
+ if n < 0 or self._pos + n > self._end:
76
+ raise self._err(f"truncated field (need {n} bytes)")
77
+ out = self._buf[self._pos : self._pos + n]
78
+ self._pos += n
79
+ return out
80
+
81
+ def fixed32(self) -> int:
82
+ v: int = struct.unpack("<I", self._take(4))[0]
83
+ return v
84
+
85
+ def fixed64(self) -> int:
86
+ v: int = struct.unpack("<Q", self._take(8))[0]
87
+ return v
88
+
89
+ def sfixed32(self) -> int:
90
+ v: int = struct.unpack("<i", self._take(4))[0]
91
+ return v
92
+
93
+ def sfixed64(self) -> int:
94
+ v: int = struct.unpack("<q", self._take(8))[0]
95
+ return v
96
+
97
+ def sint(self) -> int:
98
+ return _zigzag(self.varint())
99
+
100
+ def int32(self) -> int:
101
+ v = self.varint()
102
+ # int32 negativos viajam como varint de 10 bytes (complemento 64)
103
+ if v >= 1 << 63:
104
+ v -= 1 << 64
105
+ elif v >= 1 << 31:
106
+ v -= 1 << 32
107
+ return v
108
+
109
+ def bool_(self) -> bool:
110
+ return self.varint() != 0
111
+
112
+ def double(self) -> float:
113
+ v: float = struct.unpack("<d", self._take(8))[0]
114
+ return v
115
+
116
+ def float_(self) -> float:
117
+ v: float = struct.unpack("<f", self._take(4))[0]
118
+ return v
119
+
120
+ def bytes_(self) -> bytes:
121
+ return self._take(self.varint())
122
+
123
+ def string(self) -> str:
124
+ try:
125
+ return self.bytes_().decode("utf-8")
126
+ except UnicodeDecodeError as exc:
127
+ raise self._err(f"invalid utf-8 string: {exc}") from exc
128
+
129
+ def packed_int32(self) -> list[int]:
130
+ """Campo repeated int32 packed (wire LEN)."""
131
+ data = self.bytes_()
132
+ sub = Reader(data, path=f"{self._path}<packed>")
133
+ out: list[int] = []
134
+ while not sub.eof():
135
+ out.append(sub.int32())
136
+ return out
137
+
138
+ def packed_enum(self) -> list[int]:
139
+ data = self.bytes_()
140
+ sub = Reader(data, path=f"{self._path}<packed>")
141
+ out: list[int] = []
142
+ while not sub.eof():
143
+ out.append(sub.varint())
144
+ return out
145
+
146
+ def sub(self, path: str) -> Reader:
147
+ """Sub-reader para um campo length-delimited (mensagem)."""
148
+ return Reader(self.bytes_(), path=path)
149
+
150
+ def skip(self, wire: int) -> None:
151
+ """Consome um campo de tipo desconhecido."""
152
+ if wire == WIRE_VARINT:
153
+ self.varint()
154
+ elif wire == WIRE_FIXED64:
155
+ self._take(8)
156
+ elif wire == WIRE_LEN:
157
+ self._take(self.varint())
158
+ elif wire == WIRE_FIXED32:
159
+ self._take(4)
160
+ else: # pragma: no cover — tag() já rejeita
161
+ raise self._err(f"cannot skip wire type {wire}")
162
+
163
+
164
+ def decode_message(buf: bytes, spec: dict[int, tuple[str, Any]],
165
+ path: str = "") -> dict[str, Any]:
166
+ """Decodifica uma mensagem conforme `spec` {field_no: (name, kind)}.
167
+
168
+ `kind` é um string-dispatch: "varint","int32","sint","bool","fixed32",
169
+ "fixed64","sfixed32","sfixed64","double","float","bytes","string",
170
+ "packed_int32","packed_enum", uma tupla (spec, "message") para
171
+ mensagem aninhada, ou ("repeated", inner_kind) para campos repetidos.
172
+ Campos não mapeados são skipados preservando o wire type.
173
+ """
174
+ r = Reader(buf, path=path)
175
+ out: dict[str, Any] = {}
176
+ while not r.eof():
177
+ field_no, wire = r.tag()
178
+ entry = spec.get(field_no)
179
+ if entry is None:
180
+ r.skip(wire)
181
+ continue
182
+ name, kind = entry
183
+ val = _read_field(r, kind, wire, f"{path}.{name}" if path else name)
184
+ if _is_repeated(kind):
185
+ out.setdefault(name, []).extend(val)
186
+ else:
187
+ out[name] = val
188
+ return out
189
+
190
+
191
+ def _is_repeated(kind: Any) -> bool:
192
+ return isinstance(kind, tuple) and bool(kind) and kind[0] == "repeated"
193
+
194
+
195
+ def _read_field(r: Reader, kind: Any, wire: int, path: str) -> Any:
196
+ if isinstance(kind, tuple) and kind and kind[0] == "repeated":
197
+ inner = kind[1]
198
+ # packed só para escalares varint — len-delimited acumula valores
199
+ if wire == WIRE_LEN and inner in ("int32", "packed_int32"):
200
+ return r.packed_int32()
201
+ return [_read_field(r, inner, wire, path)]
202
+ if isinstance(kind, tuple) and len(kind) == 2 and kind[1] == "message":
203
+ return decode_message(r.bytes_(), kind[0], path=path)
204
+
205
+ readers: dict[str, Any] = {
206
+ "varint": r.varint, "int32": r.int32, "sint": r.sint,
207
+ "bool": r.bool_, "fixed32": r.fixed32, "fixed64": r.fixed64,
208
+ "sfixed32": r.sfixed32, "sfixed64": r.sfixed64,
209
+ "double": r.double, "float": r.float_, "bytes": r.bytes_,
210
+ "string": r.string,
211
+ "packed_int32": r.packed_int32, "packed_enum": r.packed_enum,
212
+ }
213
+ fn = readers.get(kind)
214
+ if fn is None:
215
+ raise DecodeError(f"{path}: unknown field kind {kind!r}")
216
+ return fn()
@@ -0,0 +1,248 @@
1
+ """Importador de índices SCIP — bytes protobuf → modelo tipado (lf-0017).
2
+
3
+ Cobertura do schema (scip.proto oficial):
4
+ - Index{metadata, documents, external_symbols}
5
+ - Metadata{version, tool_info, project_root, text_document_encoding}
6
+ - Document{language, relative_path, occurrences, symbols, position_encoding}
7
+ - Occurrence{range(deprec.), single_line_range, multi_line_range, symbol,
8
+ symbol_roles, override_documentation}
9
+ - SymbolInformation{symbol, documentation, relationships, kind}
10
+ - Relationship{symbol, is_reference, is_implementation, is_type_definition,
11
+ is_definition}
12
+ Campos não listados são skipados preservando wire type (forward-compat).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ from code_atlas.resolvers.scip.decoder import DecodeError, decode_message
21
+
22
+ SpecMap = dict[int, tuple[str, Any]]
23
+
24
+ ROLE_DEFINITION = 0x1
25
+ ROLE_IMPORT = 0x2
26
+ ROLE_WRITE_ACCESS = 0x4
27
+ ROLE_READ_ACCESS = 0x8
28
+ ROLE_GENERATED = 0x10
29
+ ROLE_TEST = 0x20
30
+ ROLE_FORWARD = 0x1000000
31
+
32
+ TOOL_INFO_SPEC: SpecMap = {
33
+ 1: ("name", "string"),
34
+ 2: ("version", "string"),
35
+ 3: ("arguments", ("repeated", "string")),
36
+ }
37
+ METADATA_SPEC: SpecMap = {
38
+ 1: ("version", "varint"),
39
+ 2: ("tool_info", (TOOL_INFO_SPEC, "message")),
40
+ 3: ("project_root", "string"),
41
+ 4: ("text_document_encoding", "varint"),
42
+ }
43
+ SLR_SPEC: SpecMap = {
44
+ 1: ("line", "int32"),
45
+ 2: ("start_character", "int32"),
46
+ 3: ("end_character", "int32"),
47
+ }
48
+ MLR_SPEC: SpecMap = {
49
+ 1: ("start_line", "int32"),
50
+ 2: ("start_character", "int32"),
51
+ 3: ("end_line", "int32"),
52
+ 4: ("end_character", "int32"),
53
+ }
54
+ OCCURRENCE_SPEC: SpecMap = {
55
+ 1: ("range", ("repeated", "int32")),
56
+ 2: ("symbol", "string"),
57
+ 3: ("symbol_roles", "int32"),
58
+ 4: ("override_documentation", ("repeated", "string")),
59
+ 5: ("syntax_kind", "varint"),
60
+ 6: ("diagnostics", ("repeated", ("repeated", "string"))), # skip-ish
61
+ 7: ("enclosing_range", ("repeated", "int32")),
62
+ 8: ("single_line_range", (SLR_SPEC, "message")),
63
+ 9: ("multi_line_range", (MLR_SPEC, "message")),
64
+ }
65
+ RELATIONSHIP_SPEC: SpecMap = {
66
+ 1: ("symbol", "string"),
67
+ 2: ("is_reference", "bool"),
68
+ 3: ("is_implementation", "bool"),
69
+ 4: ("is_type_definition", "bool"),
70
+ 5: ("is_definition", "bool"),
71
+ }
72
+ SYMBOL_INFO_SPEC: SpecMap = {
73
+ 1: ("symbol", "string"),
74
+ 3: ("documentation", ("repeated", "string")),
75
+ 4: ("relationships", ("repeated", (RELATIONSHIP_SPEC, "message"))),
76
+ 5: ("kind", "varint"),
77
+ 6: ("display_name", "string"),
78
+ 7: ("signature_documentation", ("repeated", "string")),
79
+ 9: ("enclosing_symbol", "string"),
80
+ }
81
+ DOCUMENT_SPEC: SpecMap = {
82
+ 1: ("relative_path", "string"),
83
+ 2: ("occurrences", ("repeated", (OCCURRENCE_SPEC, "message"))),
84
+ 3: ("symbols", ("repeated", (SYMBOL_INFO_SPEC, "message"))),
85
+ 4: ("language", "string"),
86
+ 5: ("text", "string"),
87
+ 6: ("position_encoding", "varint"),
88
+ }
89
+ INDEX_SPEC: SpecMap = {
90
+ 1: ("metadata", (METADATA_SPEC, "message")),
91
+ 2: ("documents", ("repeated", (DOCUMENT_SPEC, "message"))),
92
+ 3: ("external_symbols", ("repeated", (SYMBOL_INFO_SPEC, "message"))),
93
+ }
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class ScipRange:
98
+ """Range normalizado (sempre multilinha: start/end line+char)."""
99
+
100
+ start_line: int
101
+ start_char: int
102
+ end_line: int
103
+ end_char: int
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class ScipOccurrence:
108
+ symbol: str
109
+ roles: int
110
+ range: ScipRange | None
111
+
112
+ @property
113
+ def is_definition(self) -> bool:
114
+ return bool(self.roles & ROLE_DEFINITION)
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class ScipRelationship:
119
+ symbol: str
120
+ is_reference: bool = False
121
+ is_implementation: bool = False
122
+ is_type_definition: bool = False
123
+ is_definition: bool = False
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class ScipSymbolInfo:
128
+ symbol: str
129
+ kind: int = 0
130
+ documentation: tuple[str, ...] = ()
131
+ relationships: tuple[ScipRelationship, ...] = ()
132
+ enclosing_symbol: str = ""
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class ScipDocument:
137
+ relative_path: str
138
+ language: str
139
+ occurrences: tuple[ScipOccurrence, ...] = ()
140
+ symbols: tuple[ScipSymbolInfo, ...] = ()
141
+ position_encoding: int = 0
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class ScipIndex:
146
+ """Índice SCIP decodificado — modelo interno do Atlas."""
147
+
148
+ project_root: str = ""
149
+ tool_name: str = ""
150
+ tool_version: str = ""
151
+ text_document_encoding: int = 0
152
+ documents: tuple[ScipDocument, ...] = ()
153
+ external_symbols: tuple[ScipSymbolInfo, ...] = ()
154
+ raw_metadata: dict[str, Any] = field(default_factory=dict)
155
+
156
+
157
+ def _to_range(msg: dict[str, Any]) -> ScipRange | None:
158
+ slr = msg.get("single_line_range")
159
+ if slr is not None:
160
+ return ScipRange(
161
+ start_line=slr.get("line", 0),
162
+ start_char=slr.get("start_character", 0),
163
+ end_line=slr.get("line", 0),
164
+ end_char=slr.get("end_character", 0),
165
+ )
166
+ mlr = msg.get("multi_line_range")
167
+ if mlr is not None:
168
+ return ScipRange(
169
+ start_line=mlr.get("start_line", 0),
170
+ start_char=mlr.get("start_character", 0),
171
+ end_line=mlr.get("end_line", 0),
172
+ end_char=mlr.get("end_character", 0),
173
+ )
174
+ rng = msg.get("range") or []
175
+ if len(rng) >= 4:
176
+ return ScipRange(rng[0], rng[1], rng[2], rng[3])
177
+ if len(rng) == 3:
178
+ return ScipRange(rng[0], rng[1], rng[0], rng[2])
179
+ return None
180
+
181
+
182
+ def _to_relationship(msg: dict[str, Any]) -> ScipRelationship:
183
+ return ScipRelationship(
184
+ symbol=msg.get("symbol", ""),
185
+ is_reference=bool(msg.get("is_reference", False)),
186
+ is_implementation=bool(msg.get("is_implementation", False)),
187
+ is_type_definition=bool(msg.get("is_type_definition", False)),
188
+ is_definition=bool(msg.get("is_definition", False)),
189
+ )
190
+
191
+
192
+ def _to_symbol_info(msg: dict[str, Any]) -> ScipSymbolInfo:
193
+ return ScipSymbolInfo(
194
+ symbol=msg.get("symbol", ""),
195
+ kind=msg.get("kind", 0),
196
+ documentation=tuple(msg.get("documentation", [])),
197
+ relationships=tuple(
198
+ _to_relationship(r) for r in msg.get("relationships", [])),
199
+ enclosing_symbol=msg.get("enclosing_symbol", ""),
200
+ )
201
+
202
+
203
+ def _to_document(msg: dict[str, Any]) -> ScipDocument:
204
+ return ScipDocument(
205
+ relative_path=msg.get("relative_path", ""),
206
+ language=msg.get("language", ""),
207
+ occurrences=tuple(
208
+ ScipOccurrence(
209
+ symbol=o.get("symbol", ""),
210
+ roles=o.get("symbol_roles", 0),
211
+ range=_to_range(o),
212
+ )
213
+ for o in msg.get("occurrences", [])
214
+ ),
215
+ symbols=tuple(_to_symbol_info(s) for s in msg.get("symbols", [])),
216
+ position_encoding=msg.get("position_encoding", 0),
217
+ )
218
+
219
+
220
+ def parse_index(data: bytes) -> ScipIndex:
221
+ """Decodifica bytes protobuf de um .scip → ScipIndex tipado."""
222
+ msg = decode_message(data, INDEX_SPEC, path="Index")
223
+ meta = msg.get("metadata") or {}
224
+ tool = meta.get("tool_info") or {}
225
+ docs = [_to_document(d) for d in msg.get("documents", [])]
226
+ return ScipIndex(
227
+ project_root=meta.get("project_root", ""),
228
+ tool_name=tool.get("name", ""),
229
+ tool_version=tool.get("version", ""),
230
+ text_document_encoding=meta.get("text_document_encoding", 0),
231
+ documents=tuple(docs),
232
+ external_symbols=tuple(
233
+ _to_symbol_info(s) for s in msg.get("external_symbols", [])),
234
+ raw_metadata=meta,
235
+ )
236
+
237
+
238
+ __all__ = [
239
+ "ROLE_DEFINITION",
240
+ "DecodeError",
241
+ "ScipDocument",
242
+ "ScipIndex",
243
+ "ScipOccurrence",
244
+ "ScipRange",
245
+ "ScipRelationship",
246
+ "ScipSymbolInfo",
247
+ "parse_index",
248
+ ]
@@ -0,0 +1,90 @@
1
+ """Carregamento opt-in de índices SCIP do disco (lf-0017).
2
+
3
+ Política do indexer externo:
4
+ - Entrada é **somente um caminho de arquivo** — o Atlas nunca executa
5
+ o indexer nem qualquer comando.
6
+ - Cap de tamanho configurável (default ``MAX_INDEX_BYTES``).
7
+ - Conteúdo é tratado como untrusted: decodificação via wire-reader
8
+ próprio + validação estrutural + binding de revisão.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from code_atlas.resolvers.scip.decoder import DecodeError
17
+ from code_atlas.resolvers.scip.importer import ScipIndex, parse_index
18
+ from code_atlas.resolvers.scip.validation import (
19
+ MAX_INDEX_BYTES,
20
+ project_root_matches,
21
+ validate_index,
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LoadOutcome:
27
+ """Resultado honesto do load — nunca levanta por conteúdo ruim."""
28
+
29
+ index: ScipIndex | None
30
+ errors: tuple[str, ...] = ()
31
+ warnings: tuple[str, ...] = ()
32
+
33
+ @property
34
+ def ok(self) -> bool:
35
+ return self.index is not None and not self.errors
36
+
37
+
38
+ def load_index(
39
+ path: Path,
40
+ *,
41
+ workspace_root: Path | None = None,
42
+ max_bytes: int = MAX_INDEX_BYTES,
43
+ allow_revision_mismatch: bool = False,
44
+ ) -> LoadOutcome:
45
+ """Lê e valida um arquivo ``.scip``.
46
+
47
+ - Arquivo inexistente/ilegível, oversize ou bytes malformados →
48
+ ``errors`` populadas (nunca raise para input externo).
49
+ - ``workspace_root`` informado ativa o binding de revisão:
50
+ ``project_root`` divergente rejeita a associação salvo
51
+ ``allow_revision_mismatch=True`` (opt-in explícito do usuário).
52
+ """
53
+ try:
54
+ size = path.stat().st_size
55
+ except OSError as exc:
56
+ return LoadOutcome(None, errors=(f"cannot stat {path}: {exc}",))
57
+ if size > max_bytes:
58
+ return LoadOutcome(
59
+ None, errors=(f"index too large: {size} > {max_bytes}",))
60
+ try:
61
+ data = path.read_bytes()
62
+ except OSError as exc:
63
+ return LoadOutcome(None, errors=(f"cannot read {path}: {exc}",))
64
+ try:
65
+ index = parse_index(data)
66
+ except DecodeError as exc:
67
+ return LoadOutcome(None, errors=(f"decode error: {exc}",))
68
+
69
+ res = validate_index(index, size)
70
+ if not res.ok:
71
+ return LoadOutcome(None, errors=res.errors, warnings=res.warnings)
72
+
73
+ errors: list[str] = []
74
+ warnings = list(res.warnings)
75
+ if workspace_root is not None and not project_root_matches(
76
+ index, workspace_root
77
+ ):
78
+ msg = (
79
+ f"project_root {index.project_root!r} does not match "
80
+ f"workspace {workspace_root}"
81
+ )
82
+ if allow_revision_mismatch:
83
+ warnings.append(f"{msg} (allowed by flag)")
84
+ else:
85
+ errors.append(msg)
86
+ return LoadOutcome(
87
+ index if not errors else None,
88
+ errors=tuple(errors),
89
+ warnings=tuple(warnings),
90
+ )