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,271 @@
|
|
|
1
|
+
"""Correlação explícita SCIP ↔ Atlas (lf-0017).
|
|
2
|
+
|
|
3
|
+
Símbolos SCIP NÃO são ids internos do Atlas — este módulo faz a ponte:
|
|
4
|
+
|
|
5
|
+
- ``parse_symbol``: string scip → partes (scheme, package, descriptors).
|
|
6
|
+
Gramática oficial (scip.proto):
|
|
7
|
+
``<scheme> ' ' <manager> ' ' <name> ' ' <version> ' ' <descriptors>``
|
|
8
|
+
ou ``local <id>`` para símbolos confinados ao documento.
|
|
9
|
+
- ``symbol_to_qname``: descriptors → qname estilo Atlas
|
|
10
|
+
(``ns.Class.method``), preservando sufixos de descriptor.
|
|
11
|
+
- ``ScipCorrelator``: resolve occurrence→(path → símbolo Atlas), usando os
|
|
12
|
+
``ScopeUniverse``/qnames montados pelo builder — associação por qname,
|
|
13
|
+
nunca por id posicional.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from code_atlas.resolvers.syntactic.resolver import ScopeUniverse
|
|
22
|
+
|
|
23
|
+
# sufixo de descriptor → separador qname
|
|
24
|
+
_SUFFIX_SEP = {
|
|
25
|
+
1: "/", # Namespace
|
|
26
|
+
2: "#", # Type
|
|
27
|
+
3: ".", # Term
|
|
28
|
+
4: ".", # Method (com disambiguator)
|
|
29
|
+
5: "[]", # TypeParameter
|
|
30
|
+
6: "()", # Parameter
|
|
31
|
+
7: ":", # Meta
|
|
32
|
+
8: ".", # Local
|
|
33
|
+
9: "!", # Macro
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Descriptor:
|
|
39
|
+
name: str
|
|
40
|
+
disambiguator: str
|
|
41
|
+
suffix: int # Descriptor.Suffix
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ScipSymbol:
|
|
46
|
+
"""Símbolo SCIP decomposto — nunca usado como id Atlas direto."""
|
|
47
|
+
|
|
48
|
+
scheme: str
|
|
49
|
+
manager: str
|
|
50
|
+
package_name: str
|
|
51
|
+
package_version: str
|
|
52
|
+
descriptors: tuple[Descriptor, ...]
|
|
53
|
+
local_id: str = "" # preenchido só para 'local N'
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_local(self) -> bool:
|
|
57
|
+
return bool(self.local_id)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def is_external(self) -> bool:
|
|
61
|
+
"""Símbolo de pacote externo (manager/pacote preenchidos e
|
|
62
|
+
não-locais podem ser locais-do-repo também — externo de fato é
|
|
63
|
+
decidido pelo correlator via presença no universo)."""
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_SYMBOL_RE = re.compile(
|
|
68
|
+
r"^(?P<scheme>\S+)\s+(?P<manager>\S+)\s+(?P<pkg>\S+)\s+"
|
|
69
|
+
r"(?P<ver>\S+)\s+(?P<desc>.+)$"
|
|
70
|
+
)
|
|
71
|
+
_LOCAL_RE = re.compile(r"^local\s+(?P<id>\S+)$")
|
|
72
|
+
|
|
73
|
+
# um descriptor termina em: '/', '#', '.', ':', '!', '].', ').', '[]'
|
|
74
|
+
_DESC_TOKEN_RE = re.compile(
|
|
75
|
+
r"(?P<name>`(?:[^`]|``)*`|[^\s/.#!:\[\]()`]+|`[^`]*`)"
|
|
76
|
+
r"(?P<disambig>(?:\([^)]*\))?)"
|
|
77
|
+
r"(?P<suffix>/|#|\.|:|!|\]|\))"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_symbol(raw: str) -> ScipSymbol | None:
|
|
82
|
+
"""String scip → ScipSymbol; None quando malformado."""
|
|
83
|
+
raw = raw.strip()
|
|
84
|
+
if not raw:
|
|
85
|
+
return None
|
|
86
|
+
m = _LOCAL_RE.match(raw)
|
|
87
|
+
if m:
|
|
88
|
+
return ScipSymbol(
|
|
89
|
+
scheme="", manager="", package_name="", package_version="",
|
|
90
|
+
descriptors=(), local_id=m.group("id"),
|
|
91
|
+
)
|
|
92
|
+
m = _SYMBOL_RE.match(raw)
|
|
93
|
+
if not m:
|
|
94
|
+
return None
|
|
95
|
+
descriptors = _parse_descriptors(m.group("desc"))
|
|
96
|
+
if descriptors is None:
|
|
97
|
+
return None
|
|
98
|
+
return ScipSymbol(
|
|
99
|
+
scheme=m.group("scheme"), manager=m.group("manager"),
|
|
100
|
+
package_name=m.group("pkg"), package_version=m.group("ver"),
|
|
101
|
+
descriptors=tuple(descriptors),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_descriptors(text: str) -> list[Descriptor] | None:
|
|
106
|
+
"""Separa a cadeia de descriptors em tokens (name+disambig+suffix)."""
|
|
107
|
+
out: list[Descriptor] = []
|
|
108
|
+
i = 0
|
|
109
|
+
n = len(text)
|
|
110
|
+
while i < n:
|
|
111
|
+
parsed = _read_name(text, i)
|
|
112
|
+
if parsed is None:
|
|
113
|
+
return None
|
|
114
|
+
name, i = parsed
|
|
115
|
+
disambig = ""
|
|
116
|
+
if i < n and text[i] == "(": # method: name(disambig).
|
|
117
|
+
j = text.find(")", i)
|
|
118
|
+
if j == -1:
|
|
119
|
+
return None
|
|
120
|
+
disambig = text[i + 1 : j]
|
|
121
|
+
i = j # consome ')' abaixo
|
|
122
|
+
if i >= n:
|
|
123
|
+
return None
|
|
124
|
+
end = _read_suffix(text, i)
|
|
125
|
+
if end is None:
|
|
126
|
+
return None
|
|
127
|
+
suffix, i = end
|
|
128
|
+
if not name:
|
|
129
|
+
return None
|
|
130
|
+
clean = name[1:-1] if name.startswith("`") and name.endswith("`") \
|
|
131
|
+
else name
|
|
132
|
+
out.append(Descriptor(name=clean, disambiguator=disambig,
|
|
133
|
+
suffix=suffix))
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _read_name(text: str, i: int) -> tuple[str, int] | None:
|
|
138
|
+
"""Lê um name (identifier ou `escaped`) a partir de i → (name, next)."""
|
|
139
|
+
n = len(text)
|
|
140
|
+
if text[i] == "`": # escaped identifier — `` = escape
|
|
141
|
+
j = i + 1
|
|
142
|
+
while j < n:
|
|
143
|
+
if text[j] == "`":
|
|
144
|
+
if j + 1 < n and text[j + 1] == "`":
|
|
145
|
+
j += 2
|
|
146
|
+
continue
|
|
147
|
+
break
|
|
148
|
+
j += 1
|
|
149
|
+
if j >= n:
|
|
150
|
+
return None # backtick não fechado
|
|
151
|
+
return text[i : j + 1], j + 1
|
|
152
|
+
j = i
|
|
153
|
+
while j < n and text[j] not in "/#.:(![`":
|
|
154
|
+
j += 1
|
|
155
|
+
return text[i:j], j
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _read_suffix(text: str, i: int) -> tuple[int, int] | None:
|
|
159
|
+
"""Lê o terminador de descriptor a partir de i → (suffix, next)."""
|
|
160
|
+
ch = text[i]
|
|
161
|
+
if ch == ")": # fecha method descriptor (pode vir seguido de '.')
|
|
162
|
+
i += 1
|
|
163
|
+
if i < len(text) and text[i] == ".":
|
|
164
|
+
i += 1
|
|
165
|
+
return 4, i
|
|
166
|
+
if ch == "[":
|
|
167
|
+
j = text.find("]", i)
|
|
168
|
+
if j == -1:
|
|
169
|
+
return None
|
|
170
|
+
return 5, j + 1 # ']' termina o descriptor type-param
|
|
171
|
+
suffix = {"/": 1, "#": 2, ".": 3, ":": 7, "!": 9}.get(ch)
|
|
172
|
+
if suffix is None:
|
|
173
|
+
return None
|
|
174
|
+
return suffix, i + 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def symbol_to_qname(sym: ScipSymbol) -> str:
|
|
178
|
+
"""Converte descriptors scip → qname estilo Atlas (pkg.Class.method).
|
|
179
|
+
|
|
180
|
+
Namespace e Type/Terms são concatenados com '.'; Method recebe '()';
|
|
181
|
+
TypeParameter/Parameter são sufixados ao termo anterior.
|
|
182
|
+
"""
|
|
183
|
+
if sym.is_local:
|
|
184
|
+
return f"local:{sym.local_id}"
|
|
185
|
+
parts: list[str] = []
|
|
186
|
+
for d in sym.descriptors:
|
|
187
|
+
if d.suffix in (5, 6): # type-param / parameter — suffixo inline
|
|
188
|
+
if parts:
|
|
189
|
+
parts[-1] += f"[{d.name}]" if d.suffix == 5 else f"({d.name})"
|
|
190
|
+
continue
|
|
191
|
+
name = d.name
|
|
192
|
+
if d.suffix == 4: # method
|
|
193
|
+
name = f"{name}({d.disambiguator})" if d.disambiguator else name
|
|
194
|
+
parts.append(name)
|
|
195
|
+
return ".".join(parts)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def symbol_parts(sym: ScipSymbol) -> tuple[str, tuple[str, ...]]:
|
|
199
|
+
"""Decompõe descriptors em (doc_path candidato, nomes de membros).
|
|
200
|
+
|
|
201
|
+
Indexadores SCIP usam o path do documento como descriptor Namespace
|
|
202
|
+
(ex.: `` `pkg/util.py`/helper(). ``). Membros = descriptors
|
|
203
|
+
não-namespace, com disambiguator/parens removidos.
|
|
204
|
+
"""
|
|
205
|
+
if sym.is_local:
|
|
206
|
+
return "", ()
|
|
207
|
+
ns_parts = [d.name for d in sym.descriptors if d.suffix == 1]
|
|
208
|
+
members = tuple(
|
|
209
|
+
d.name for d in sym.descriptors if d.suffix != 1
|
|
210
|
+
)
|
|
211
|
+
doc = "/".join(ns_parts)
|
|
212
|
+
return doc, members
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ScipCorrelator:
|
|
216
|
+
"""Mapa occurrence→nó Atlas dentro de um ``ScopeUniverse``.
|
|
217
|
+
|
|
218
|
+
Associação por (path do documento → módulo ctx) + (qname do símbolo →
|
|
219
|
+
``qname_nid``); nunca assume que o id scip é o id interno.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, universe: ScopeUniverse) -> None:
|
|
223
|
+
self._u = universe
|
|
224
|
+
|
|
225
|
+
def module_for_document(self, relative_path: str) -> str | None:
|
|
226
|
+
"""Document scip → nid do módulo Atlas com mesmo path."""
|
|
227
|
+
return self._u.by_path.get(relative_path)
|
|
228
|
+
|
|
229
|
+
def node_for_symbol(self, raw: str, path: str) -> str | None:
|
|
230
|
+
"""String scip → nid de símbolo no snapshot (ou None).
|
|
231
|
+
|
|
232
|
+
Estratégia: (1) doc-path do namespace → ctx do módulo + membro
|
|
233
|
+
final; (2) leaf global como fallback; (3) qname direto. Só
|
|
234
|
+
retorna nid quando a identidade é única.
|
|
235
|
+
"""
|
|
236
|
+
sym = parse_symbol(raw)
|
|
237
|
+
if sym is None or sym.is_local:
|
|
238
|
+
return None
|
|
239
|
+
doc, members = symbol_parts(sym)
|
|
240
|
+
hits = self._match(sym, doc, members, path)
|
|
241
|
+
return hits[0] if len(hits) == 1 else None
|
|
242
|
+
|
|
243
|
+
def _match(
|
|
244
|
+
self,
|
|
245
|
+
sym: ScipSymbol,
|
|
246
|
+
doc: str,
|
|
247
|
+
members: tuple[str, ...],
|
|
248
|
+
path: str,
|
|
249
|
+
) -> list[str]:
|
|
250
|
+
if doc:
|
|
251
|
+
ctx = self._u.ctx_for_path(doc)
|
|
252
|
+
if ctx is not None:
|
|
253
|
+
if not members:
|
|
254
|
+
return [ctx.nid]
|
|
255
|
+
hits = sorted(set(ctx.defines.get(members[-1], [])))
|
|
256
|
+
if hits:
|
|
257
|
+
return hits
|
|
258
|
+
if members:
|
|
259
|
+
hits = sorted(set(self._u.leaf.get(members[-1], [])))
|
|
260
|
+
if hits:
|
|
261
|
+
return hits
|
|
262
|
+
qname = symbol_to_qname(sym)
|
|
263
|
+
return sorted(set(self._u.qname_nid.get(qname, [])))
|
|
264
|
+
|
|
265
|
+
def candidates_for_symbol(self, raw: str) -> tuple[str, ...]:
|
|
266
|
+
sym = parse_symbol(raw)
|
|
267
|
+
if sym is None or sym.is_local:
|
|
268
|
+
return ()
|
|
269
|
+
doc, members = symbol_parts(sym)
|
|
270
|
+
hits = self._match(sym, doc, members, "")
|
|
271
|
+
return tuple(sorted(set(hits)))
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Contrato de proveniência + revision safety do índice SCIP (lf-0028).
|
|
2
|
+
|
|
3
|
+
``project_root`` bater não basta: um índice pode ser de outro repo com
|
|
4
|
+
o mesmo path, estar velho (arquivos modificados/removidos depois da
|
|
5
|
+
indexação) ou simplesmente incompleto. ``IndexProvenance`` é o modelo
|
|
6
|
+
interno — não presume que o .scip contenha todos os campos; o que não
|
|
7
|
+
pode ser provado fica explicitamente marcado.
|
|
8
|
+
|
|
9
|
+
``validation_status``:
|
|
10
|
+
- ``current``: todas as verificações passaram.
|
|
11
|
+
- ``degraded``: índice utilizável, mas evidência de revisão
|
|
12
|
+
insuficiente — relações derivadas NUNCA são promovidas como
|
|
13
|
+
comprovadas (confidence capado em ``heuristic`` no resolver).
|
|
14
|
+
- ``rejected``: incompatível — o resolver não produz resolução
|
|
15
|
+
nenhuma (índice incompatível nunca contamina snapshot READY).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from code_atlas.resolvers.scip.importer import ScipDocument, ScipIndex
|
|
26
|
+
from code_atlas.resolvers.scip.validation import project_root_matches
|
|
27
|
+
|
|
28
|
+
# position_encoding válido no mapper: UTF-32 (3) ou não-declarado (0
|
|
29
|
+
# — protobuf enum zero/default); outros encodings tornam spans
|
|
30
|
+
# não-confiáveis → degraded (o mapper não re-decodifica).
|
|
31
|
+
TRUSTED_ENCODINGS = frozenset({0, 3})
|
|
32
|
+
|
|
33
|
+
STATUS_CURRENT = "current"
|
|
34
|
+
STATUS_DEGRADED = "degraded"
|
|
35
|
+
STATUS_REJECTED = "rejected"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class IndexProvenance:
|
|
40
|
+
"""Proveniência de um índice externo — inspecionável e serializável."""
|
|
41
|
+
|
|
42
|
+
index_format: str = "scip"
|
|
43
|
+
index_sha256: str = "" # sha256 dos bytes do arquivo
|
|
44
|
+
producer_name: str = "" # metadata.tool_info.name
|
|
45
|
+
producer_version: str = "" # metadata.tool_info.version
|
|
46
|
+
repository_identity: str = "" # metadata.project_root (URI)
|
|
47
|
+
source_revision: str = "" # revisão do repo indexado, se provável
|
|
48
|
+
worktree_digest: str = "" # digest dos arquivos indexados vistos
|
|
49
|
+
indexed_at: str = "" # mtime do arquivo .scip (evidência host)
|
|
50
|
+
validation_status: str = STATUS_REJECTED
|
|
51
|
+
validation_evidence: tuple[str, ...] = ()
|
|
52
|
+
|
|
53
|
+
def summary(self) -> str:
|
|
54
|
+
"""Resumo one-line para evidence notes / diagnósticos."""
|
|
55
|
+
return (
|
|
56
|
+
f"scip-provenance status={self.validation_status} "
|
|
57
|
+
f"sha256={self.index_sha256[:12]} "
|
|
58
|
+
f"producer={self.producer_name}@{self.producer_version} "
|
|
59
|
+
f"revision={self.source_revision or 'unknown'}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class RevisionAssessment:
|
|
65
|
+
"""Resultado do assessment: proveniência + erros + avisos."""
|
|
66
|
+
|
|
67
|
+
provenance: IndexProvenance
|
|
68
|
+
errors: tuple[str, ...] = ()
|
|
69
|
+
warnings: tuple[str, ...] = ()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def assess_revision(
|
|
73
|
+
index: ScipIndex,
|
|
74
|
+
index_path: Path,
|
|
75
|
+
workspace_root: Path,
|
|
76
|
+
*,
|
|
77
|
+
raw_bytes: bytes | None = None,
|
|
78
|
+
) -> RevisionAssessment:
|
|
79
|
+
"""Verifica o índice contra o worktree atual.
|
|
80
|
+
|
|
81
|
+
Sinais sem executar nada do repo: identidade (``project_root`` ↔
|
|
82
|
+
workspace), staleness, paths, spans vs fim de arquivo,
|
|
83
|
+
``position_encoding``, completude de metadata, símbolos duplicados.
|
|
84
|
+
"""
|
|
85
|
+
if raw_bytes is None:
|
|
86
|
+
try:
|
|
87
|
+
raw_bytes = index_path.read_bytes()
|
|
88
|
+
except OSError as exc:
|
|
89
|
+
return RevisionAssessment(
|
|
90
|
+
IndexProvenance(validation_status=STATUS_REJECTED),
|
|
91
|
+
errors=(f"cannot read index for provenance: {exc}",),
|
|
92
|
+
)
|
|
93
|
+
sha = hashlib.sha256(raw_bytes).hexdigest()
|
|
94
|
+
index_mtime, indexed_at = _index_stamp(index_path)
|
|
95
|
+
base = dict(
|
|
96
|
+
index_sha256=sha,
|
|
97
|
+
producer_name=index.tool_name,
|
|
98
|
+
producer_version=index.tool_version,
|
|
99
|
+
repository_identity=index.project_root,
|
|
100
|
+
indexed_at=indexed_at,
|
|
101
|
+
)
|
|
102
|
+
if not project_root_matches(index, workspace_root):
|
|
103
|
+
return RevisionAssessment(
|
|
104
|
+
IndexProvenance(
|
|
105
|
+
**base,
|
|
106
|
+
validation_status=STATUS_REJECTED,
|
|
107
|
+
validation_evidence=("project_root mismatch",),
|
|
108
|
+
),
|
|
109
|
+
errors=(
|
|
110
|
+
f"index project_root {index.project_root!r} does not "
|
|
111
|
+
f"identify workspace {workspace_root}",
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
degraded, digest = _check_docs(
|
|
115
|
+
index, Path(workspace_root).resolve(), index_mtime)
|
|
116
|
+
degraded += _check_completeness(index, index_mtime)
|
|
117
|
+
status = STATUS_DEGRADED if degraded else STATUS_CURRENT
|
|
118
|
+
return RevisionAssessment(
|
|
119
|
+
IndexProvenance(
|
|
120
|
+
**base,
|
|
121
|
+
source_revision="", # scip metadata não prova commit
|
|
122
|
+
worktree_digest=digest,
|
|
123
|
+
validation_status=status,
|
|
124
|
+
validation_evidence=(
|
|
125
|
+
"project_root matches workspace", *degraded),
|
|
126
|
+
),
|
|
127
|
+
warnings=tuple(degraded)
|
|
128
|
+
+ (() if index_mtime else ("index mtime unavailable",)),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _index_stamp(index_path: Path) -> tuple[float, str]:
|
|
133
|
+
try:
|
|
134
|
+
mtime = index_path.stat().st_mtime
|
|
135
|
+
except OSError:
|
|
136
|
+
return 0.0, ""
|
|
137
|
+
return mtime, datetime.fromtimestamp(
|
|
138
|
+
mtime, tz=UTC).isoformat()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_docs(
|
|
142
|
+
index: ScipIndex, root: Path, index_mtime: float
|
|
143
|
+
) -> tuple[list[str], str]:
|
|
144
|
+
"""staleness + paths + spans + encoding + símbolos duplicados."""
|
|
145
|
+
degraded: list[str] = []
|
|
146
|
+
defined_in: dict[str, str] = {}
|
|
147
|
+
digest_parts: list[str] = []
|
|
148
|
+
for doc in index.documents:
|
|
149
|
+
p = doc.relative_path
|
|
150
|
+
if "\\" in p or "//" in p or p.startswith("./"):
|
|
151
|
+
degraded.append(f"non-normalized path: {p!r}")
|
|
152
|
+
if doc.position_encoding not in TRUSTED_ENCODINGS:
|
|
153
|
+
degraded.append(f"doc {p}: position_encoding="
|
|
154
|
+
f"{doc.position_encoding} untrusted")
|
|
155
|
+
for s in doc.symbols:
|
|
156
|
+
prev = defined_in.get(s.symbol)
|
|
157
|
+
if prev is not None and prev != p:
|
|
158
|
+
degraded.append(
|
|
159
|
+
f"symbol {s.symbol!r} defined in {prev} and {p}")
|
|
160
|
+
else:
|
|
161
|
+
defined_in[s.symbol] = p
|
|
162
|
+
degraded += _check_doc_file(doc, root / p, index_mtime,
|
|
163
|
+
digest_parts)
|
|
164
|
+
digest = hashlib.sha256(
|
|
165
|
+
"\n".join(sorted(digest_parts)).encode()).hexdigest()
|
|
166
|
+
return degraded, digest
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _check_doc_file(
|
|
170
|
+
doc: ScipDocument, target: Path, index_mtime: float,
|
|
171
|
+
digest_parts: list[str],
|
|
172
|
+
) -> list[str]:
|
|
173
|
+
"""staleness e spans de um documento contra o arquivo atual."""
|
|
174
|
+
p = doc.relative_path
|
|
175
|
+
if not target.is_file():
|
|
176
|
+
digest_parts.append(f"{p}:absent")
|
|
177
|
+
return [f"indexed file missing: {p}"]
|
|
178
|
+
try:
|
|
179
|
+
fst = target.stat()
|
|
180
|
+
except OSError:
|
|
181
|
+
return [f"cannot stat {p}"]
|
|
182
|
+
digest_parts.append(f"{p}:{fst.st_size}")
|
|
183
|
+
out: list[str] = []
|
|
184
|
+
if index_mtime and fst.st_mtime > index_mtime:
|
|
185
|
+
out.append(f"file newer than index: {p}")
|
|
186
|
+
if doc.occurrences:
|
|
187
|
+
try:
|
|
188
|
+
n_lines = len(
|
|
189
|
+
target.read_text(encoding="utf-8").splitlines())
|
|
190
|
+
except (OSError, UnicodeDecodeError):
|
|
191
|
+
n_lines = -1
|
|
192
|
+
if n_lines >= 0:
|
|
193
|
+
max_line = max(
|
|
194
|
+
(o.range.start_line if o.range else 0)
|
|
195
|
+
for o in doc.occurrences)
|
|
196
|
+
if max_line > n_lines:
|
|
197
|
+
out.append(f"doc {p}: occurrence span line {max_line} "
|
|
198
|
+
f"beyond file end {n_lines}")
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_completeness(index: ScipIndex, index_mtime: float) -> list[str]:
|
|
203
|
+
"""índice válido ≠ índice completo."""
|
|
204
|
+
out: list[str] = []
|
|
205
|
+
if not index.tool_version:
|
|
206
|
+
out.append("metadata.tool_info.version missing")
|
|
207
|
+
if not index.documents:
|
|
208
|
+
out.append("index has no documents")
|
|
209
|
+
if not index_mtime:
|
|
210
|
+
out.append("index timestamp unknown")
|
|
211
|
+
return out
|