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.
- code_atlas/__init__.py +6 -0
- code_atlas/__main__.py +6 -0
- code_atlas/api/__init__.py +5 -0
- code_atlas/api/handlers.py +1514 -0
- code_atlas/api/server.py +394 -0
- code_atlas/cli.py +2084 -0
- code_atlas/config.py +149 -0
- code_atlas/context/__init__.py +66 -0
- code_atlas/context/contracts.py +277 -0
- code_atlas/context/domain.py +760 -0
- code_atlas/context/engine.py +442 -0
- code_atlas/context/expand.py +133 -0
- code_atlas/context/renderers.py +71 -0
- code_atlas/context/tokenizer.py +37 -0
- code_atlas/contracts/__init__.py +38 -0
- code_atlas/contracts/enums.py +67 -0
- code_atlas/contracts/models.py +175 -0
- code_atlas/contracts/ports.py +70 -0
- code_atlas/discovery/__init__.py +25 -0
- code_atlas/discovery/gitignore.py +119 -0
- code_atlas/discovery/identity.py +52 -0
- code_atlas/discovery/models.py +204 -0
- code_atlas/discovery/scanner.py +495 -0
- code_atlas/domains/__init__.py +107 -0
- code_atlas/domains/api/__init__.py +28 -0
- code_atlas/domains/api/adapter.py +405 -0
- code_atlas/domains/api/common.py +308 -0
- code_atlas/domains/api/consumers.py +224 -0
- code_atlas/domains/api/emit.py +231 -0
- code_atlas/domains/api/openapi.py +398 -0
- code_atlas/domains/api/routes.py +582 -0
- code_atlas/domains/api/yaml_subset.py +683 -0
- code_atlas/domains/aws/__init__.py +28 -0
- code_atlas/domains/aws/adapter.py +382 -0
- code_atlas/domains/aws/common.py +261 -0
- code_atlas/domains/aws/configs.py +1164 -0
- code_atlas/domains/aws/emit.py +411 -0
- code_atlas/domains/aws/pyfiles.py +2220 -0
- code_atlas/domains/aws/sqlbridge.py +448 -0
- code_atlas/domains/cicd/__init__.py +50 -0
- code_atlas/domains/cicd/adapter.py +762 -0
- code_atlas/domains/cicd/classify.py +136 -0
- code_atlas/domains/cicd/workflow.py +504 -0
- code_atlas/domains/cicd/yamlmini.py +946 -0
- code_atlas/domains/cli_surface.py +739 -0
- code_atlas/domains/contracts.py +274 -0
- code_atlas/domains/crossresolve.py +1208 -0
- code_atlas/domains/evidence.py +57 -0
- code_atlas/domains/identity.py +149 -0
- code_atlas/domains/model.py +202 -0
- code_atlas/domains/registry.py +250 -0
- code_atlas/domains/resolution.py +65 -0
- code_atlas/domains/spark/__init__.py +31 -0
- code_atlas/domains/spark/adapter.py +146 -0
- code_atlas/domains/spark/emit.py +344 -0
- code_atlas/domains/spark/extract.py +108 -0
- code_atlas/domains/spark/sqllit.py +192 -0
- code_atlas/domains/spark/walk.py +1943 -0
- code_atlas/domains/sql/__init__.py +30 -0
- code_atlas/domains/sql/adapter.py +382 -0
- code_atlas/domains/sql/extract.py +1271 -0
- code_atlas/domains/sql/nodes.py +264 -0
- code_atlas/domains/sql/parser.py +1510 -0
- code_atlas/domains/sql/tokens.py +318 -0
- code_atlas/domains/summarize.py +126 -0
- code_atlas/domains/terraform/__init__.py +20 -0
- code_atlas/domains/terraform/adapter.py +127 -0
- code_atlas/domains/terraform/catalog.py +43 -0
- code_atlas/domains/terraform/extract.py +1277 -0
- code_atlas/domains/terraform/hcl.py +538 -0
- code_atlas/domains/terraform/refs.py +181 -0
- code_atlas/domains/terraform/tfjson.py +163 -0
- code_atlas/domains/terraform/tfvars.py +98 -0
- code_atlas/domains/validation.py +120 -0
- code_atlas/env_override.py +27 -0
- code_atlas/eval/__init__.py +26 -0
- code_atlas/eval/domain_eval.py +681 -0
- code_atlas/eval/ground_truth.py +688 -0
- code_atlas/eval/impact_truth.py +758 -0
- code_atlas/eval/metrics.py +178 -0
- code_atlas/eval/pack_value.py +610 -0
- code_atlas/federation/__init__.py +72 -0
- code_atlas/federation/federation.py +676 -0
- code_atlas/federation/model.py +355 -0
- code_atlas/federation/store.py +194 -0
- code_atlas/gitbin.py +48 -0
- code_atlas/graph/__init__.py +20 -0
- code_atlas/graph/builder.py +378 -0
- code_atlas/graph/identity.py +54 -0
- code_atlas/graph/queries.py +47 -0
- code_atlas/graph/validation.py +92 -0
- code_atlas/impact/__init__.py +37 -0
- code_atlas/impact/blast_radius.py +171 -0
- code_atlas/impact/contract_analyzer.py +104 -0
- code_atlas/impact/contracts.py +315 -0
- code_atlas/impact/diff_reader.py +142 -0
- code_atlas/impact/domain_traversal.py +768 -0
- code_atlas/impact/engine.py +392 -0
- code_atlas/impact/packs.py +128 -0
- code_atlas/impact/report.py +136 -0
- code_atlas/impact/revision_binding.py +220 -0
- code_atlas/impact/risk_classifier.py +83 -0
- code_atlas/impact/snapshot_resolver.py +136 -0
- code_atlas/impact/symbol_diff.py +210 -0
- code_atlas/impact/test_selector.py +166 -0
- code_atlas/indexing/__init__.py +5 -0
- code_atlas/indexing/cache.py +396 -0
- code_atlas/indexing/change_detector.py +278 -0
- code_atlas/indexing/coordinator.py +1032 -0
- code_atlas/indexing/dependency_planner.py +268 -0
- code_atlas/indexing/persist.py +436 -0
- code_atlas/indexing/reuse.py +85 -0
- code_atlas/install/__init__.py +7 -0
- code_atlas/install/cli.py +168 -0
- code_atlas/install/lifecycle.py +286 -0
- code_atlas/mcp.py +674 -0
- code_atlas/observability/__init__.py +19 -0
- code_atlas/observability/logging.py +199 -0
- code_atlas/parsers/__init__.py +43 -0
- code_atlas/parsers/contracts.py +209 -0
- code_atlas/parsers/engine.py +125 -0
- code_atlas/parsers/languages/__init__.py +1 -0
- code_atlas/parsers/languages/python.py +391 -0
- code_atlas/parsers/languages/tsx.py +28 -0
- code_atlas/parsers/languages/typescript.py +909 -0
- code_atlas/parsers/registry.py +51 -0
- code_atlas/parsers/tree_sitter_runtime.py +118 -0
- code_atlas/resolvers/__init__.py +33 -0
- code_atlas/resolvers/bootstrap.py +246 -0
- code_atlas/resolvers/capabilities.py +33 -0
- code_atlas/resolvers/contracts.py +133 -0
- code_atlas/resolvers/coordinator.py +96 -0
- code_atlas/resolvers/enrichment.py +318 -0
- code_atlas/resolvers/lsp/__init__.py +26 -0
- code_atlas/resolvers/lsp/adapter.py +256 -0
- code_atlas/resolvers/lsp/lifecycle.py +251 -0
- code_atlas/resolvers/lsp/spawn.py +92 -0
- code_atlas/resolvers/lsp/transport.py +240 -0
- code_atlas/resolvers/registry.py +33 -0
- code_atlas/resolvers/scip/__init__.py +49 -0
- code_atlas/resolvers/scip/decoder.py +216 -0
- code_atlas/resolvers/scip/importer.py +248 -0
- code_atlas/resolvers/scip/loader.py +90 -0
- code_atlas/resolvers/scip/mapper.py +271 -0
- code_atlas/resolvers/scip/provenance.py +211 -0
- code_atlas/resolvers/scip/resolver.py +289 -0
- code_atlas/resolvers/scip/validation.py +101 -0
- code_atlas/resolvers/syntactic/__init__.py +8 -0
- code_atlas/resolvers/syntactic/resolver.py +254 -0
- code_atlas/search/__init__.py +32 -0
- code_atlas/search/contracts.py +176 -0
- code_atlas/search/domain.py +561 -0
- code_atlas/search/engine.py +530 -0
- code_atlas/search/federated.py +276 -0
- code_atlas/search/graph.py +282 -0
- code_atlas/search/hybrid.py +151 -0
- code_atlas/search/intent.py +48 -0
- code_atlas/search/lexical.py +176 -0
- code_atlas/search/refs.py +359 -0
- code_atlas/search/structural.py +505 -0
- code_atlas/search/tokenizer.py +43 -0
- code_atlas/search/vector/__init__.py +35 -0
- code_atlas/search/vector/contracts.py +76 -0
- code_atlas/search/vector/embeddings.py +96 -0
- code_atlas/search/vector/index.py +169 -0
- code_atlas/search/vector/provider.py +50 -0
- code_atlas/storage/__init__.py +14 -0
- code_atlas/storage/db.py +47 -0
- code_atlas/storage/migrations.py +552 -0
- code_atlas/storage/store.py +1045 -0
- code_atlas/ui/__init__.py +29 -0
- code_atlas/ui/i18n.py +143 -0
- code_atlas/ui/kit.py +689 -0
- code_atlas/ui/wizard.py +317 -0
- code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
- code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
- code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
- code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
- code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
- code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
- 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
|
+
)
|