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,359 @@
|
|
|
1
|
+
"""Contratos canônicos de evidência + validador (lf-0051).
|
|
2
|
+
|
|
3
|
+
Toda referência carrega identidade suficiente para resolver sem
|
|
4
|
+
ambiguidade: repository_id + snapshot_id + ids de domínio. O
|
|
5
|
+
``EvidenceValidator`` revalida contra SQLite — cross-repo/cross-
|
|
6
|
+
snapshot/spans inválidos falham com ``EvidenceValidationResult``
|
|
7
|
+
tipado, nunca silencioso.
|
|
8
|
+
|
|
9
|
+
Kinds de evidência diferenciados:
|
|
10
|
+
- ``lexical-match`` — casou via FTS/bm25 (matched_fields)
|
|
11
|
+
- ``syntactic-occurrence`` — ocorrência sintática persistida
|
|
12
|
+
- ``graph-relation`` — aresta do grafo (relation_id)
|
|
13
|
+
- ``resolver-resolution`` — resolução externa (SCIP/LSP)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sqlite3
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
EVIDENCE_KINDS = frozenset({
|
|
22
|
+
"lexical-match", "syntactic-occurrence", "graph-relation",
|
|
23
|
+
"resolver-resolution",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SnapshotRef:
|
|
29
|
+
repository_id: int
|
|
30
|
+
snapshot_id: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class SymbolRef:
|
|
35
|
+
repository_id: int
|
|
36
|
+
symbol_id: int
|
|
37
|
+
qualified_name: str
|
|
38
|
+
kind: str
|
|
39
|
+
language: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class OccurrenceRef:
|
|
44
|
+
repository_id: int
|
|
45
|
+
snapshot_id: int
|
|
46
|
+
occurrence_id: int
|
|
47
|
+
symbol_id: int
|
|
48
|
+
qualified_name: str
|
|
49
|
+
source_path: str
|
|
50
|
+
span_start: int | None
|
|
51
|
+
span_end: int | None
|
|
52
|
+
content_hash: str | None
|
|
53
|
+
parse_status: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class RelationRef:
|
|
58
|
+
repository_id: int
|
|
59
|
+
snapshot_id: int
|
|
60
|
+
relation_id: int
|
|
61
|
+
type: str
|
|
62
|
+
from_occurrence: int
|
|
63
|
+
to_occurrence: int | None
|
|
64
|
+
to_unresolved: str | None
|
|
65
|
+
resolution_status: str
|
|
66
|
+
confidence_class: str
|
|
67
|
+
resolver: str | None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class SourceFileRef:
|
|
72
|
+
repository_id: int
|
|
73
|
+
snapshot_id: int
|
|
74
|
+
file_id: int
|
|
75
|
+
relative_path: str
|
|
76
|
+
content_hash: str | None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class EvidenceRef:
|
|
81
|
+
"""Uma evidência tipada apontando para um ref canônico."""
|
|
82
|
+
|
|
83
|
+
kind: str # EVIDENCE_KINDS
|
|
84
|
+
ref: OccurrenceRef | RelationRef | SourceFileRef | SymbolRef
|
|
85
|
+
origin_strategy: str # lexical|structural|graph|vector|hybrid
|
|
86
|
+
detail: str = ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class EvidenceValidationResult:
|
|
91
|
+
ok: bool
|
|
92
|
+
code: str # "" | not-found | cross-repo | cross-snapshot
|
|
93
|
+
# | bad-span | metadata-mismatch
|
|
94
|
+
message: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
_OK = EvidenceValidationResult(True, "", "")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _mismatch(field: str) -> EvidenceValidationResult:
|
|
101
|
+
"""Metadado declarado contradiz o persistido (lf-0058).
|
|
102
|
+
|
|
103
|
+
``None`` na ref = "não assertado" → não validado. Declarado e
|
|
104
|
+
divergente → rejeitado, mesmo que o id exista."""
|
|
105
|
+
return EvidenceValidationResult(
|
|
106
|
+
False, "metadata-mismatch", f"campo '{field}' diverge")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class EvidenceValidator:
|
|
110
|
+
"""Revalida refs contra o banco — sem fabricar evidência."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, conn: sqlite3.Connection) -> None:
|
|
113
|
+
self._conn = conn
|
|
114
|
+
conn.row_factory = sqlite3.Row
|
|
115
|
+
|
|
116
|
+
def _repo_of_snapshot(self, snapshot_id: int) -> int | None:
|
|
117
|
+
row = self._conn.execute(
|
|
118
|
+
"SELECT repository_id FROM snapshots WHERE id = ?",
|
|
119
|
+
(snapshot_id,)).fetchone()
|
|
120
|
+
return int(row["repository_id"]) if row else None
|
|
121
|
+
|
|
122
|
+
def validate_occurrence(
|
|
123
|
+
self, ref: OccurrenceRef
|
|
124
|
+
) -> EvidenceValidationResult:
|
|
125
|
+
repo = self._repo_of_snapshot(ref.snapshot_id)
|
|
126
|
+
if repo is None:
|
|
127
|
+
return EvidenceValidationResult(
|
|
128
|
+
False, "not-found", "snapshot inexistente")
|
|
129
|
+
if repo != ref.repository_id:
|
|
130
|
+
return EvidenceValidationResult(
|
|
131
|
+
False, "cross-repo",
|
|
132
|
+
f"occurrence em repo {repo}, ref declara "
|
|
133
|
+
f"{ref.repository_id}")
|
|
134
|
+
row = self._conn.execute(
|
|
135
|
+
"SELECT o.id, o.logical_symbol_id, o.span_start, o.span_end,"
|
|
136
|
+
" o.content_hash, o.parse_status, f.relative_path,"
|
|
137
|
+
" s.qualified_name FROM symbol_occurrences o"
|
|
138
|
+
" JOIN source_files f ON f.id = o.source_file_id"
|
|
139
|
+
" JOIN symbols s ON s.id = o.logical_symbol_id"
|
|
140
|
+
" WHERE o.id = ? AND o.snapshot_id = ?",
|
|
141
|
+
(ref.occurrence_id, ref.snapshot_id)).fetchone()
|
|
142
|
+
if row is None:
|
|
143
|
+
return EvidenceValidationResult(
|
|
144
|
+
False, "not-found",
|
|
145
|
+
f"occurrence {ref.occurrence_id} ausente no snapshot")
|
|
146
|
+
if row["relative_path"] != ref.source_path:
|
|
147
|
+
return EvidenceValidationResult(
|
|
148
|
+
False, "cross-snapshot",
|
|
149
|
+
"occurrence existe mas em outro arquivo")
|
|
150
|
+
return self._check_occurrence_metadata(ref, row)
|
|
151
|
+
|
|
152
|
+
def _check_occurrence_metadata(
|
|
153
|
+
self, ref: OccurrenceRef, row: sqlite3.Row
|
|
154
|
+
) -> EvidenceValidationResult:
|
|
155
|
+
"""lf-0058: metadados declarados são revalidados — id existente
|
|
156
|
+
não basta (adulteração de qname/símbolo/span/hash/status)."""
|
|
157
|
+
if (ref.symbol_id
|
|
158
|
+
and int(row["logical_symbol_id"]) != ref.symbol_id):
|
|
159
|
+
return _mismatch("symbol_id")
|
|
160
|
+
if (ref.qualified_name
|
|
161
|
+
and str(row["qualified_name"]) != ref.qualified_name):
|
|
162
|
+
return _mismatch("qualified_name")
|
|
163
|
+
for col, span in (("span_start", ref.span_start),
|
|
164
|
+
("span_end", ref.span_end)):
|
|
165
|
+
if span is not None and row[col] != span:
|
|
166
|
+
return EvidenceValidationResult(
|
|
167
|
+
False, "bad-span", f"{col} diverge do persistido")
|
|
168
|
+
if (ref.content_hash is not None
|
|
169
|
+
and row["content_hash"] != ref.content_hash):
|
|
170
|
+
return _mismatch("content_hash")
|
|
171
|
+
if (ref.parse_status
|
|
172
|
+
and str(row["parse_status"]) != ref.parse_status):
|
|
173
|
+
return _mismatch("parse_status")
|
|
174
|
+
return _OK
|
|
175
|
+
|
|
176
|
+
def validate_relation(
|
|
177
|
+
self, ref: RelationRef
|
|
178
|
+
) -> EvidenceValidationResult:
|
|
179
|
+
repo = self._repo_of_snapshot(ref.snapshot_id)
|
|
180
|
+
if repo is None or repo != ref.repository_id:
|
|
181
|
+
return EvidenceValidationResult(
|
|
182
|
+
False, "cross-repo", "relação fora do repo/snapshot")
|
|
183
|
+
row = self._conn.execute(
|
|
184
|
+
"SELECT id, from_id, to_id, to_unresolved, type,"
|
|
185
|
+
" resolution_status, confidence_class, resolver"
|
|
186
|
+
" FROM relations WHERE id = ? AND snapshot_id = ?",
|
|
187
|
+
(ref.relation_id, ref.snapshot_id)).fetchone()
|
|
188
|
+
if row is None:
|
|
189
|
+
return EvidenceValidationResult(
|
|
190
|
+
False, "not-found",
|
|
191
|
+
f"relation {ref.relation_id} ausente")
|
|
192
|
+
if row["from_id"] != ref.from_occurrence:
|
|
193
|
+
return EvidenceValidationResult(
|
|
194
|
+
False, "bad-span", "origem da relação diverge")
|
|
195
|
+
# lf-0058: endpoints/tipo/status/resolver/confiança declarados
|
|
196
|
+
# são revalidados — aresta existe ≠ metadados corretos.
|
|
197
|
+
if (ref.to_occurrence is not None
|
|
198
|
+
and row["to_id"] != ref.to_occurrence):
|
|
199
|
+
return _mismatch("to_occurrence")
|
|
200
|
+
if (ref.to_unresolved is not None
|
|
201
|
+
and row["to_unresolved"] != ref.to_unresolved):
|
|
202
|
+
return _mismatch("to_unresolved")
|
|
203
|
+
if ref.type and str(row["type"]) != ref.type:
|
|
204
|
+
return _mismatch("type")
|
|
205
|
+
if (ref.resolution_status
|
|
206
|
+
and str(row["resolution_status"]) != ref.resolution_status):
|
|
207
|
+
return _mismatch("resolution_status")
|
|
208
|
+
if (ref.confidence_class
|
|
209
|
+
and str(row["confidence_class"]) != ref.confidence_class):
|
|
210
|
+
return _mismatch("confidence_class")
|
|
211
|
+
if (ref.resolver is not None
|
|
212
|
+
and row["resolver"] != ref.resolver):
|
|
213
|
+
return _mismatch("resolver")
|
|
214
|
+
return _OK
|
|
215
|
+
|
|
216
|
+
def validate_symbol(
|
|
217
|
+
self, ref: SymbolRef, snapshot_id: int
|
|
218
|
+
) -> EvidenceValidationResult:
|
|
219
|
+
"""Símbolo é repo-level: valida repo + identidade persistida."""
|
|
220
|
+
repo = self._repo_of_snapshot(snapshot_id)
|
|
221
|
+
if repo is None or repo != ref.repository_id:
|
|
222
|
+
return EvidenceValidationResult(
|
|
223
|
+
False, "cross-repo", "símbolo fora do repo")
|
|
224
|
+
row = self._conn.execute(
|
|
225
|
+
"SELECT qualified_name, kind, language"
|
|
226
|
+
" FROM symbols WHERE id = ?",
|
|
227
|
+
(ref.symbol_id,)).fetchone()
|
|
228
|
+
if row is None:
|
|
229
|
+
return EvidenceValidationResult(
|
|
230
|
+
False, "not-found",
|
|
231
|
+
f"symbol {ref.symbol_id} ausente")
|
|
232
|
+
if ref.qualified_name and row["qualified_name"] != ref.qualified_name:
|
|
233
|
+
return _mismatch("qualified_name")
|
|
234
|
+
if ref.kind and str(row["kind"]) != ref.kind:
|
|
235
|
+
return _mismatch("kind")
|
|
236
|
+
if ref.language and str(row["language"]) != ref.language:
|
|
237
|
+
return _mismatch("language")
|
|
238
|
+
return _OK
|
|
239
|
+
|
|
240
|
+
def validate_source_file(
|
|
241
|
+
self, ref: SourceFileRef
|
|
242
|
+
) -> EvidenceValidationResult:
|
|
243
|
+
repo = self._repo_of_snapshot(ref.snapshot_id)
|
|
244
|
+
if repo is None or repo != ref.repository_id:
|
|
245
|
+
return EvidenceValidationResult(
|
|
246
|
+
False, "cross-repo", "arquivo fora do repo/snapshot")
|
|
247
|
+
row = self._conn.execute(
|
|
248
|
+
"SELECT relative_path, content_hash FROM source_files"
|
|
249
|
+
" WHERE id = ? AND snapshot_id = ?",
|
|
250
|
+
(ref.file_id, ref.snapshot_id)).fetchone()
|
|
251
|
+
if row is None:
|
|
252
|
+
return EvidenceValidationResult(
|
|
253
|
+
False, "not-found",
|
|
254
|
+
f"source_file {ref.file_id} ausente")
|
|
255
|
+
if (ref.relative_path
|
|
256
|
+
and str(row["relative_path"]) != ref.relative_path):
|
|
257
|
+
return _mismatch("relative_path")
|
|
258
|
+
if (ref.content_hash is not None
|
|
259
|
+
and row["content_hash"] != ref.content_hash):
|
|
260
|
+
return _mismatch("content_hash")
|
|
261
|
+
return _OK
|
|
262
|
+
|
|
263
|
+
def validate_snapshot(
|
|
264
|
+
self, ref: SnapshotRef
|
|
265
|
+
) -> EvidenceValidationResult:
|
|
266
|
+
repo = self._repo_of_snapshot(ref.snapshot_id)
|
|
267
|
+
if repo is None:
|
|
268
|
+
return EvidenceValidationResult(
|
|
269
|
+
False, "not-found", "snapshot inexistente")
|
|
270
|
+
if repo != ref.repository_id:
|
|
271
|
+
return EvidenceValidationResult(
|
|
272
|
+
False, "cross-repo",
|
|
273
|
+
f"snapshot pertence ao repo {repo}, "
|
|
274
|
+
f"ref declara {ref.repository_id}")
|
|
275
|
+
return _OK
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class EvidenceGrounder:
|
|
279
|
+
"""Materializa ``EvidenceRef``s tipadas a partir de um SearchHit —
|
|
280
|
+
rastreabilidade hit → ocorrência → origem (lf-0051)."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, conn: sqlite3.Connection) -> None:
|
|
283
|
+
self._conn = conn
|
|
284
|
+
conn.row_factory = sqlite3.Row
|
|
285
|
+
|
|
286
|
+
def occurrence_ref(
|
|
287
|
+
self, repository_id: int, snapshot_id: int, occurrence_id: int,
|
|
288
|
+
) -> OccurrenceRef | None:
|
|
289
|
+
row = self._conn.execute(
|
|
290
|
+
"SELECT o.id, o.logical_symbol_id, o.span_start, o.span_end,"
|
|
291
|
+
" o.content_hash, o.parse_status, f.relative_path,"
|
|
292
|
+
" s.qualified_name FROM symbol_occurrences o"
|
|
293
|
+
" JOIN source_files f ON f.id = o.source_file_id"
|
|
294
|
+
" JOIN symbols s ON s.id = o.logical_symbol_id"
|
|
295
|
+
" WHERE o.id = ? AND o.snapshot_id = ?",
|
|
296
|
+
(occurrence_id, snapshot_id)).fetchone()
|
|
297
|
+
if row is None:
|
|
298
|
+
return None
|
|
299
|
+
# lf-0056: repository_id do caller deve bater com o dono REAL
|
|
300
|
+
# do snapshot — sem essa checagem uma ref cross-repo recebia
|
|
301
|
+
# identidade falsificada.
|
|
302
|
+
srow = self._conn.execute(
|
|
303
|
+
"SELECT repository_id FROM snapshots WHERE id = ?",
|
|
304
|
+
(snapshot_id,)).fetchone()
|
|
305
|
+
if srow is None or int(srow["repository_id"]) != repository_id:
|
|
306
|
+
return None
|
|
307
|
+
return OccurrenceRef(
|
|
308
|
+
repository_id=repository_id, snapshot_id=snapshot_id,
|
|
309
|
+
occurrence_id=occurrence_id,
|
|
310
|
+
symbol_id=int(row["logical_symbol_id"]),
|
|
311
|
+
qualified_name=str(row["qualified_name"]),
|
|
312
|
+
source_path=str(row["relative_path"]),
|
|
313
|
+
span_start=row["span_start"], span_end=row["span_end"],
|
|
314
|
+
content_hash=row["content_hash"],
|
|
315
|
+
parse_status=str(row["parse_status"]))
|
|
316
|
+
|
|
317
|
+
def ground_hit(
|
|
318
|
+
self,
|
|
319
|
+
repository_id: int,
|
|
320
|
+
snapshot_id: int,
|
|
321
|
+
hit: object,
|
|
322
|
+
) -> tuple[EvidenceRef, ...]:
|
|
323
|
+
"""Refs tipadas para um hit: ocorrência + relations + resolver.
|
|
324
|
+
|
|
325
|
+
Sem evidência persistida → tupla vazia (ausência identificada,
|
|
326
|
+
nunca fabricada)."""
|
|
327
|
+
occ_id = int(getattr(hit, "occurrence_id", 0) or 0)
|
|
328
|
+
strategy = str(getattr(hit, "strategy", ""))
|
|
329
|
+
out: list[EvidenceRef] = []
|
|
330
|
+
occ = self.occurrence_ref(repository_id, snapshot_id, occ_id)
|
|
331
|
+
if occ is not None:
|
|
332
|
+
kind = ("lexical-match" if strategy == "lexical"
|
|
333
|
+
else "syntactic-occurrence")
|
|
334
|
+
out.append(EvidenceRef(kind=kind, ref=occ,
|
|
335
|
+
origin_strategy=strategy))
|
|
336
|
+
rows = self._conn.execute(
|
|
337
|
+
"SELECT id, from_id, to_id, to_unresolved, type,"
|
|
338
|
+
" resolution_status, confidence_class, resolver"
|
|
339
|
+
" FROM relations WHERE snapshot_id = ? AND ("
|
|
340
|
+
" from_id = ? OR to_id = ?) ORDER BY id LIMIT 50",
|
|
341
|
+
(snapshot_id, occ_id, occ_id)).fetchall()
|
|
342
|
+
for r in rows:
|
|
343
|
+
out.append(EvidenceRef(
|
|
344
|
+
kind="graph-relation",
|
|
345
|
+
ref=RelationRef(
|
|
346
|
+
repository_id=repository_id,
|
|
347
|
+
snapshot_id=snapshot_id,
|
|
348
|
+
relation_id=int(r["id"]), type=str(r["type"]),
|
|
349
|
+
from_occurrence=int(r["from_id"]),
|
|
350
|
+
to_occurrence=r["to_id"],
|
|
351
|
+
to_unresolved=r["to_unresolved"],
|
|
352
|
+
resolution_status=str(r["resolution_status"]),
|
|
353
|
+
confidence_class=str(r["confidence_class"]),
|
|
354
|
+
resolver=r["resolver"]),
|
|
355
|
+
origin_strategy="graph" if strategy == "graph"
|
|
356
|
+
else strategy,
|
|
357
|
+
detail="outgoing" if int(r["from_id"]) == occ_id
|
|
358
|
+
else "incoming"))
|
|
359
|
+
return tuple(out)
|