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,268 @@
|
|
|
1
|
+
"""Planner de invalidação dependency-aware (lf-0019).
|
|
2
|
+
|
|
3
|
+
Consome o ``ChangeSet`` + arestas IMPORTS do snapshot anterior e produz
|
|
4
|
+
um ``IndexPlan`` inspecionável **antes** de qualquer execução.
|
|
5
|
+
|
|
6
|
+
Ações por arquivo:
|
|
7
|
+
|
|
8
|
+
- ``reparse`` — conteúdo/presença mudou (added/modified/renamed ou
|
|
9
|
+
analyzer bumped); símbolos e relações precisam ser refeitos.
|
|
10
|
+
- ``revalidate-relations`` — fonte inalterada mas um módulo do qual ela
|
|
11
|
+
depende (direta ou transitivamente) mudou; relações precisam ser
|
|
12
|
+
reavaliadas mesmo sem re-leitura conceitual do conteúdo.
|
|
13
|
+
- ``reuse`` — inalterada e não afetada: ocorrências e relações copiadas
|
|
14
|
+
do snapshot anterior.
|
|
15
|
+
- ``drop`` — deletada.
|
|
16
|
+
|
|
17
|
+
Postura conservadora: informação de dependência desconhecida ou
|
|
18
|
+
incompleta → reavaliar. Ciclos são atravessados com conjunto de
|
|
19
|
+
visitados. Nenhuma relação pode ficar *stale* após mudança de
|
|
20
|
+
dependência — invalidação é superconjunto do necessário, nunca sub.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import posixpath
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from enum import StrEnum
|
|
29
|
+
|
|
30
|
+
from code_atlas.indexing.change_detector import ChangeKind, ChangeSet
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PlanAction(StrEnum):
|
|
34
|
+
REPARSE = "reparse"
|
|
35
|
+
REVALIDATE_RELATIONS = "revalidate-relations"
|
|
36
|
+
REUSE = "reuse"
|
|
37
|
+
DROP = "drop"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class PlannedFile:
|
|
42
|
+
path: str
|
|
43
|
+
action: PlanAction
|
|
44
|
+
reason: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class IndexPlan:
|
|
49
|
+
"""Plano de indexação serializável e consultável (lf-0019)."""
|
|
50
|
+
|
|
51
|
+
files: tuple[PlannedFile, ...]
|
|
52
|
+
incremental: bool = True
|
|
53
|
+
|
|
54
|
+
def action_for(self, path: str) -> PlanAction | None:
|
|
55
|
+
for f in self.files:
|
|
56
|
+
if f.path == path:
|
|
57
|
+
return f.action
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def paths_for(self, action: PlanAction) -> tuple[str, ...]:
|
|
61
|
+
return tuple(f.path for f in self.files if f.action == action)
|
|
62
|
+
|
|
63
|
+
def counts(self) -> dict[str, int]:
|
|
64
|
+
out: dict[str, int] = {}
|
|
65
|
+
for f in self.files:
|
|
66
|
+
out[str(f.action)] = out.get(str(f.action), 0) + 1
|
|
67
|
+
return dict(sorted(out.items()))
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, object]:
|
|
70
|
+
ordered = sorted(self.files, key=lambda f: f.path)
|
|
71
|
+
return {
|
|
72
|
+
"incremental": self.incremental,
|
|
73
|
+
"counts": self.counts(),
|
|
74
|
+
"files": [
|
|
75
|
+
{"path": f.path, "action": str(f.action), "reason": f.reason}
|
|
76
|
+
for f in ordered
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def to_json(self) -> str:
|
|
81
|
+
return json.dumps(self.to_dict(), sort_keys=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_TS_EXTS = (".ts", ".tsx", ".d.ts", ".mts", ".cts", "/index.ts", "/index.tsx")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _candidate_paths(importer: str, text: str) -> set[str]:
|
|
88
|
+
"""Caminhos de módulo que o texto de import poderia designar.
|
|
89
|
+
|
|
90
|
+
Cobre python absoluto/relativo e TS relativo, com todos os prefixos
|
|
91
|
+
(``pkg.util.helper`` pode ser satisfeito por ``pkg/util.py``).
|
|
92
|
+
"""
|
|
93
|
+
cands: set[str] = set()
|
|
94
|
+
|
|
95
|
+
def py_prefixed(dotted: str) -> None:
|
|
96
|
+
segs = [s for s in dotted.split(".") if s]
|
|
97
|
+
for i in range(len(segs), 0, -1):
|
|
98
|
+
base = "/".join(segs[:i])
|
|
99
|
+
cands.add(base + ".py")
|
|
100
|
+
cands.add(base + "/__init__.py")
|
|
101
|
+
|
|
102
|
+
if text.startswith(("./", "../")):
|
|
103
|
+
base = posixpath.normpath(
|
|
104
|
+
posixpath.join(posixpath.dirname(importer), text)
|
|
105
|
+
)
|
|
106
|
+
for ext in _TS_EXTS:
|
|
107
|
+
cands.add(base + ext)
|
|
108
|
+
cands.add(base)
|
|
109
|
+
# `./util.member` — último segmento pode ser membro, não path
|
|
110
|
+
parent, _, _leaf = base.rpartition("/")
|
|
111
|
+
if parent:
|
|
112
|
+
for ext in _TS_EXTS:
|
|
113
|
+
cands.add(parent + ext)
|
|
114
|
+
elif text.startswith("."):
|
|
115
|
+
dots = len(text) - len(text.lstrip("."))
|
|
116
|
+
rest = text.lstrip(".")
|
|
117
|
+
dir_parts = importer.split("/")[:-1]
|
|
118
|
+
keep = dir_parts[: max(len(dir_parts) - (dots - 1), 0)]
|
|
119
|
+
if rest:
|
|
120
|
+
py_prefixed(".".join([*keep, *rest.split(".")]))
|
|
121
|
+
else:
|
|
122
|
+
py_prefixed(".".join(keep))
|
|
123
|
+
else:
|
|
124
|
+
py_prefixed(text)
|
|
125
|
+
return cands
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class InvalidationPlanner:
|
|
129
|
+
"""Gera ``IndexPlan`` a partir do ChangeSet e do grafo anterior."""
|
|
130
|
+
|
|
131
|
+
def plan(
|
|
132
|
+
self,
|
|
133
|
+
changeset: ChangeSet,
|
|
134
|
+
prior_import_edges: list[tuple[str, str | None, str, str]],
|
|
135
|
+
prior_parse_status: dict[str, str] | None = None,
|
|
136
|
+
) -> IndexPlan:
|
|
137
|
+
"""``prior_import_edges``: linhas de ``Store.module_import_edges``
|
|
138
|
+
(importer, imported_path|None, to_text, resolution_status).
|
|
139
|
+
``prior_parse_status``: path → parse_status do snapshot anterior;
|
|
140
|
+
arquivos com parse incompleto têm dependências desconhecidas.
|
|
141
|
+
"""
|
|
142
|
+
actions, affected = self._primary_actions(changeset)
|
|
143
|
+
self._propagate_dependents(changeset, prior_import_edges,
|
|
144
|
+
actions, affected)
|
|
145
|
+
self._revalidate_unresolved(changeset, prior_import_edges,
|
|
146
|
+
actions, affected)
|
|
147
|
+
self._revalidate_incomplete(changeset, prior_parse_status or {},
|
|
148
|
+
actions, affected)
|
|
149
|
+
self._revalidate_resolver_bump(changeset, actions)
|
|
150
|
+
return IndexPlan(
|
|
151
|
+
files=tuple(sorted(actions.values(), key=lambda f: f.path))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _primary_actions(
|
|
155
|
+
self, changeset: ChangeSet
|
|
156
|
+
) -> tuple[dict[str, PlannedFile], set[str]]:
|
|
157
|
+
"""1. ações primárias: DROP/REUSE/REPARSE + conjunto afetado."""
|
|
158
|
+
actions: dict[str, PlannedFile] = {}
|
|
159
|
+
affected: set[str] = set()
|
|
160
|
+
for c in changeset.changes:
|
|
161
|
+
if c.kind == ChangeKind.DELETED:
|
|
162
|
+
actions[c.path] = PlannedFile(
|
|
163
|
+
c.path, PlanAction.DROP, f"deleted: {c.reason}"
|
|
164
|
+
)
|
|
165
|
+
affected.add(c.path)
|
|
166
|
+
elif c.kind == ChangeKind.UNCHANGED:
|
|
167
|
+
actions[c.path] = PlannedFile(
|
|
168
|
+
c.path, PlanAction.REUSE, "unchanged and unaffected"
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
actions[c.path] = PlannedFile(
|
|
172
|
+
c.path, PlanAction.REPARSE, f"{c.kind}: {c.reason}"
|
|
173
|
+
)
|
|
174
|
+
affected.add(c.path)
|
|
175
|
+
return actions, affected
|
|
176
|
+
|
|
177
|
+
def _propagate_dependents(
|
|
178
|
+
self,
|
|
179
|
+
changeset: ChangeSet,
|
|
180
|
+
prior_import_edges: list[tuple[str, str | None, str, str]],
|
|
181
|
+
actions: dict[str, PlannedFile],
|
|
182
|
+
affected: set[str],
|
|
183
|
+
) -> None:
|
|
184
|
+
"""2. fechamento transitivo de dependentes via arestas IMPORTS
|
|
185
|
+
resolvidas — cobre cadeias de re-export e ciclos (visitados)."""
|
|
186
|
+
del changeset
|
|
187
|
+
imported_to_importers: dict[str, set[str]] = {}
|
|
188
|
+
for importer, imported, _text, _status in prior_import_edges:
|
|
189
|
+
if imported:
|
|
190
|
+
imported_to_importers.setdefault(imported, set()).add(importer)
|
|
191
|
+
|
|
192
|
+
frontier = set(affected)
|
|
193
|
+
seen = set(affected)
|
|
194
|
+
while frontier:
|
|
195
|
+
target = frontier.pop()
|
|
196
|
+
for importer in sorted(imported_to_importers.get(target, ())):
|
|
197
|
+
if importer in seen:
|
|
198
|
+
continue
|
|
199
|
+
seen.add(importer)
|
|
200
|
+
cur = actions.get(importer)
|
|
201
|
+
if cur is not None and cur.action in (
|
|
202
|
+
PlanAction.REPARSE, PlanAction.DROP,
|
|
203
|
+
):
|
|
204
|
+
continue # já coberto por ação mais forte
|
|
205
|
+
actions[importer] = PlannedFile(
|
|
206
|
+
importer, PlanAction.REVALIDATE_RELATIONS,
|
|
207
|
+
f"imports changed module {target}",
|
|
208
|
+
)
|
|
209
|
+
frontier.add(importer)
|
|
210
|
+
|
|
211
|
+
def _revalidate_unresolved(
|
|
212
|
+
self,
|
|
213
|
+
changeset: ChangeSet,
|
|
214
|
+
prior_import_edges: list[tuple[str, str | None, str, str]],
|
|
215
|
+
actions: dict[str, PlannedFile],
|
|
216
|
+
affected: set[str],
|
|
217
|
+
) -> None:
|
|
218
|
+
"""3. imports sem alvo persistido: texto → candidatos; mudança
|
|
219
|
+
num candidato pode mudar o resultado da resolução."""
|
|
220
|
+
del changeset
|
|
221
|
+
for importer, imported, text, _status in prior_import_edges:
|
|
222
|
+
if imported or not text:
|
|
223
|
+
continue
|
|
224
|
+
hits = _candidate_paths(importer, text) & affected
|
|
225
|
+
if hits:
|
|
226
|
+
hit = sorted(hits)[0]
|
|
227
|
+
cur = actions.get(importer)
|
|
228
|
+
if cur is None or cur.action == PlanAction.REUSE:
|
|
229
|
+
actions[importer] = PlannedFile(
|
|
230
|
+
importer, PlanAction.REVALIDATE_RELATIONS,
|
|
231
|
+
f"unresolved import '{text}' may now match {hit}",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _revalidate_incomplete(
|
|
235
|
+
self,
|
|
236
|
+
changeset: ChangeSet,
|
|
237
|
+
prior_status: dict[str, str],
|
|
238
|
+
actions: dict[str, PlannedFile],
|
|
239
|
+
affected: set[str],
|
|
240
|
+
) -> None:
|
|
241
|
+
"""4. dependências incompletas → conservador: parse parcial/falho
|
|
242
|
+
no snapshot anterior é reavaliado quando existe mudança real."""
|
|
243
|
+
if not affected:
|
|
244
|
+
return
|
|
245
|
+
for c in changeset.changes:
|
|
246
|
+
if c.kind != ChangeKind.UNCHANGED:
|
|
247
|
+
continue
|
|
248
|
+
st = prior_status.get(c.path)
|
|
249
|
+
if st in ("parse-failed", "parsed-with-errors", "unparsed"):
|
|
250
|
+
actions[c.path] = PlannedFile(
|
|
251
|
+
c.path, PlanAction.REVALIDATE_RELATIONS,
|
|
252
|
+
f"incomplete dependency info (previous {st})",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _revalidate_resolver_bump(
|
|
256
|
+
self, changeset: ChangeSet, actions: dict[str, PlannedFile]
|
|
257
|
+
) -> None:
|
|
258
|
+
"""5. bump de resolver → reavalia relações dos reaproveitados."""
|
|
259
|
+
if not changeset.resolver_changes:
|
|
260
|
+
return
|
|
261
|
+
for c in changeset.changes:
|
|
262
|
+
cur = actions.get(c.path)
|
|
263
|
+
if cur is not None and cur.action == PlanAction.REUSE:
|
|
264
|
+
actions[c.path] = PlannedFile(
|
|
265
|
+
c.path, PlanAction.REVALIDATE_RELATIONS,
|
|
266
|
+
"resolver version changed "
|
|
267
|
+
f"({', '.join(changeset.resolver_changes)})",
|
|
268
|
+
)
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Persistência do GraphBuildResult nas tabelas do Blueprint §6.
|
|
2
|
+
|
|
3
|
+
Projeção: nós com ``path`` real (file/module/symbol kinds) viram
|
|
4
|
+
``symbols`` + ``symbol_occurrences``; ``package``/``repository`` são
|
|
5
|
+
deriváveis dos paths e não ganham ocorrência. Arestas persistem quando
|
|
6
|
+
``from_id`` tem ocorrência mapeada — CONTAINS estruturais entre
|
|
7
|
+
repo/package/file são deriváveis via ``file_inventory`` + paths.
|
|
8
|
+
|
|
9
|
+
Decisão registrada (lf-0011): relações são entre ocorrências, conforme
|
|
10
|
+
``cross_snapshot_relations`` exige endpoints no mesmo snapshot.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
|
|
18
|
+
from code_atlas.contracts.enums import NodeKind
|
|
19
|
+
from code_atlas.domains.contracts import (
|
|
20
|
+
DomainExtractionResult,
|
|
21
|
+
ExtractedDomainEntity,
|
|
22
|
+
ExtractedDomainRelation,
|
|
23
|
+
)
|
|
24
|
+
from code_atlas.domains.model import DomainEntity
|
|
25
|
+
from code_atlas.parsers.contracts import GraphBuildResult, SourceSpan
|
|
26
|
+
from code_atlas.search.tokenizer import index_terms
|
|
27
|
+
from code_atlas.storage.store import Store
|
|
28
|
+
|
|
29
|
+
# Docstring truncada no índice FTS — não copiamos o repo integral.
|
|
30
|
+
_DOCSTRING_FTS_MAX = 4000
|
|
31
|
+
|
|
32
|
+
# Kinds que ganham símbolo+ocorrência persistida.
|
|
33
|
+
_PERSISTED_KINDS = frozenset(
|
|
34
|
+
{
|
|
35
|
+
NodeKind.FILE, NodeKind.MODULE, NodeKind.CLASS, NodeKind.INTERFACE,
|
|
36
|
+
NodeKind.FUNCTION, NodeKind.METHOD, NodeKind.SYMBOL, NodeKind.CONFIG,
|
|
37
|
+
NodeKind.UNKNOWN,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _span_json(span: SourceSpan | None) -> str | None:
|
|
43
|
+
if span is None:
|
|
44
|
+
return None
|
|
45
|
+
return json.dumps(
|
|
46
|
+
[
|
|
47
|
+
span.start_byte, span.end_byte,
|
|
48
|
+
span.start_line, span.start_col, span.end_line, span.end_col,
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _persist_evidence(
|
|
54
|
+
store: Store,
|
|
55
|
+
snapshot_id: int,
|
|
56
|
+
file_ids: Mapping[str, int],
|
|
57
|
+
graph: GraphBuildResult | DomainExtractionResult,
|
|
58
|
+
) -> dict[int, int]:
|
|
59
|
+
"""Evidências → rows; retorna mapa índice-em-graph.evidence → row_id.
|
|
60
|
+
|
|
61
|
+
Mapa explícito (lf-0015): evidências filtradas (path fora do
|
|
62
|
+
inventário) NÃO podem deslocar os índices das demais — o modelo
|
|
63
|
+
posicional compactado corrompia silenciosamente a associação
|
|
64
|
+
evidence↔relation.
|
|
65
|
+
"""
|
|
66
|
+
evi_row_ids: dict[int, int] = {}
|
|
67
|
+
rows: list[tuple[object, ...]] = []
|
|
68
|
+
order: list[int] = [] # índice em graph.evidence alinhado a `rows`
|
|
69
|
+
for i, rec in enumerate(graph.evidence):
|
|
70
|
+
fid = file_ids.get(rec.path)
|
|
71
|
+
if fid is None:
|
|
72
|
+
continue
|
|
73
|
+
rows.append(
|
|
74
|
+
(snapshot_id, fid, rec.path, _span_json(rec.span),
|
|
75
|
+
rec.content_hash, rec.extractor, rec.extractor_version,
|
|
76
|
+
rec.notes, rec.resolver_id, rec.resolver_version,
|
|
77
|
+
rec.evidence_tier, rec.index_hash, rec.validation_status)
|
|
78
|
+
)
|
|
79
|
+
order.append(i)
|
|
80
|
+
for idx, rid in zip(order, store.insert_evidence_batch(rows), strict=True):
|
|
81
|
+
evi_row_ids[idx] = rid
|
|
82
|
+
return evi_row_ids
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def persist_graph(
|
|
86
|
+
store: Store,
|
|
87
|
+
repo_id: int,
|
|
88
|
+
snapshot_id: int,
|
|
89
|
+
file_ids: Mapping[str, int],
|
|
90
|
+
graph: GraphBuildResult,
|
|
91
|
+
*,
|
|
92
|
+
fts5: bool,
|
|
93
|
+
file_status: Mapping[str, str] | None = None,
|
|
94
|
+
) -> dict[str, int]:
|
|
95
|
+
"""Grava evidências, símbolos, ocorrências e relações — na transação
|
|
96
|
+
corrente do caller (atomicidade é de quem chama).
|
|
97
|
+
|
|
98
|
+
``file_status`` mapeia path → parse_status da ocorrência
|
|
99
|
+
(``parsed`` | ``parsed-with-errors`` | ``parse-failed``).
|
|
100
|
+
"""
|
|
101
|
+
counts = {
|
|
102
|
+
"evidence": 0, "symbols_new": 0, "occurrences": 0,
|
|
103
|
+
"relations": 0, "edges_skipped": 0,
|
|
104
|
+
}
|
|
105
|
+
status = file_status or {}
|
|
106
|
+
|
|
107
|
+
# 1. evidências primeiro — relations referenciam ids de evidência.
|
|
108
|
+
evi_row_ids = _persist_evidence(store, snapshot_id, file_ids, graph)
|
|
109
|
+
counts["evidence"] = len(evi_row_ids)
|
|
110
|
+
|
|
111
|
+
# 2. nós → símbolo + ocorrência (id do nó → id da ocorrência).
|
|
112
|
+
# lf-0023: dedup em duas fases + multi-row INSERT…RETURNING — o
|
|
113
|
+
# per-row upsert/insert era o custo dominante (~145k round-trips
|
|
114
|
+
# num corpus de 5k arquivos).
|
|
115
|
+
persisted = [
|
|
116
|
+
n for n in graph.nodes
|
|
117
|
+
if n.kind in _PERSISTED_KINDS and n.path in file_ids
|
|
118
|
+
]
|
|
119
|
+
sym_ids, created = store.ensure_symbols(
|
|
120
|
+
repo_id,
|
|
121
|
+
[(n.language, n.qualified_name, str(n.kind)) for n in persisted],
|
|
122
|
+
)
|
|
123
|
+
counts["symbols_new"] = len(created)
|
|
124
|
+
if fts5 and created:
|
|
125
|
+
store.insert_symbols_batch(
|
|
126
|
+
(sym_ids[k], k[1], k[0], k[2]) for k in sorted(created)
|
|
127
|
+
)
|
|
128
|
+
node_occ: dict[str, int] = {}
|
|
129
|
+
occ_rows: list[tuple[object, ...]] = []
|
|
130
|
+
occ_nodes: list[str] = [] # node_id alinhado a occ_rows
|
|
131
|
+
for n in persisted:
|
|
132
|
+
span = n.span
|
|
133
|
+
occ_rows.append(
|
|
134
|
+
(
|
|
135
|
+
snapshot_id, sym_ids[(n.language, n.qualified_name, str(n.kind))],
|
|
136
|
+
file_ids[n.path],
|
|
137
|
+
span.start_byte if span else None,
|
|
138
|
+
span.end_byte if span else None,
|
|
139
|
+
span.start_line if span else None,
|
|
140
|
+
span.start_col if span else None,
|
|
141
|
+
n.content_hash,
|
|
142
|
+
status.get(n.path, "parsed"),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
occ_nodes.append(n.node_id)
|
|
146
|
+
for nid, rid in zip(
|
|
147
|
+
occ_nodes, store.insert_occurrences_batch(occ_rows), strict=True
|
|
148
|
+
):
|
|
149
|
+
node_occ[nid] = rid
|
|
150
|
+
counts["occurrences"] = len(node_occ)
|
|
151
|
+
|
|
152
|
+
# 2b. FTS por-ocorrência (lf-0036) — snapshot-scoped; rowid = id da
|
|
153
|
+
# ocorrência. ``terms`` recebe identificadores pré-tokenizados;
|
|
154
|
+
# docstring truncada (não copiamos o repo integral).
|
|
155
|
+
if fts5 and node_occ:
|
|
156
|
+
node_by_id = {n.node_id: n for n in persisted}
|
|
157
|
+
fts_rows = [
|
|
158
|
+
(
|
|
159
|
+
occ_id,
|
|
160
|
+
snapshot_id,
|
|
161
|
+
(n := node_by_id[nid]).name
|
|
162
|
+
or n.qualified_name.rsplit(".", 1)[-1],
|
|
163
|
+
index_terms(
|
|
164
|
+
n.name, n.qualified_name, n.path
|
|
165
|
+
),
|
|
166
|
+
n.signature or "",
|
|
167
|
+
(n.docstring or "")[:_DOCSTRING_FTS_MAX],
|
|
168
|
+
n.path,
|
|
169
|
+
n.language,
|
|
170
|
+
str(n.kind),
|
|
171
|
+
)
|
|
172
|
+
for nid, occ_id in node_occ.items()
|
|
173
|
+
]
|
|
174
|
+
store.insert_fts_occurrences_batch(fts_rows)
|
|
175
|
+
|
|
176
|
+
# 3. relações — somente endpoints dentro do snapshot
|
|
177
|
+
rel_rows: list[tuple[object, ...]] = []
|
|
178
|
+
for e in graph.edges:
|
|
179
|
+
frm = node_occ.get(e.from_id)
|
|
180
|
+
if frm is None:
|
|
181
|
+
counts["edges_skipped"] += 1
|
|
182
|
+
continue
|
|
183
|
+
to = node_occ.get(e.to_id) if e.to_id is not None else None
|
|
184
|
+
if e.to_id is not None and to is None:
|
|
185
|
+
counts["edges_skipped"] += 1 # alvo estrutural não persistido
|
|
186
|
+
continue
|
|
187
|
+
ev_ids = json.dumps([evi_row_ids[i] for i in e.evidence_index
|
|
188
|
+
if i in evi_row_ids])
|
|
189
|
+
rel_rows.append(
|
|
190
|
+
(
|
|
191
|
+
snapshot_id, frm, to, e.to_text or None, str(e.type),
|
|
192
|
+
e.resolution_status, e.confidence_class, e.resolver, ev_ids,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
store.insert_relations(rel_rows)
|
|
196
|
+
counts["relations"] = len(rel_rows)
|
|
197
|
+
return counts
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# --- persistência de domínio (lf-0110, schema v9 / ADR-0010) -----------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _domain_occ_for_identity(
|
|
204
|
+
store: Store,
|
|
205
|
+
repo_id: int,
|
|
206
|
+
snapshot_id: int,
|
|
207
|
+
identity_key: str,
|
|
208
|
+
occ_by_key: dict[str, int],
|
|
209
|
+
) -> int | None:
|
|
210
|
+
"""Ocorrência de uma entidade de domínio neste snapshot.
|
|
211
|
+
|
|
212
|
+
Primeiro olha as ocorrências inseridas nesta persist; depois cai para
|
|
213
|
+
ocorrências já gravadas no snapshot (entidade extraída por outro
|
|
214
|
+
adapter — base da resolução cross-domain de lf-0117). Determinístico:
|
|
215
|
+
sempre o menor id disponível.
|
|
216
|
+
"""
|
|
217
|
+
if identity_key in occ_by_key:
|
|
218
|
+
return occ_by_key[identity_key]
|
|
219
|
+
entity_id = store.domain_entity_id_for(repo_id, identity_key)
|
|
220
|
+
if entity_id is None:
|
|
221
|
+
return None
|
|
222
|
+
occs = store.domain_occurrences_for(snapshot_id, entity_id)
|
|
223
|
+
return occs[0] if occs else None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _resolve_domain_target(
|
|
227
|
+
store: Store,
|
|
228
|
+
repo_id: int,
|
|
229
|
+
snapshot_id: int,
|
|
230
|
+
rel: ExtractedDomainRelation,
|
|
231
|
+
occ_by_key: dict[str, int],
|
|
232
|
+
) -> tuple[int | None, int | None, str | None, str]:
|
|
233
|
+
"""Resolve o alvo da relação → (to_entity_occ, to_symbol_occ, unresolved, status).
|
|
234
|
+
|
|
235
|
+
- ``to_identity`` vinculada a ocorrência do snapshot → ``resolved``;
|
|
236
|
+
identidade conhecida fora do snapshot → ``external``; desconhecida
|
|
237
|
+
→ ``unresolved`` com a chave canônica preservada como texto.
|
|
238
|
+
- ``to_symbol``: 1 match → ``resolved``; >1 → ``ambiguous``;
|
|
239
|
+
0 → ``unresolved``.
|
|
240
|
+
- ``to_unresolved`` textual → ``unresolved``, salvo quando o adapter
|
|
241
|
+
declarou ``external`` explicitamente (alvo fora do snapshot).
|
|
242
|
+
"""
|
|
243
|
+
status_override = rel.resolution.status if rel.resolution else None
|
|
244
|
+
if rel.to_identity is not None:
|
|
245
|
+
occ = _domain_occ_for_identity(
|
|
246
|
+
store, repo_id, snapshot_id, rel.to_identity.logical_key(),
|
|
247
|
+
occ_by_key,
|
|
248
|
+
)
|
|
249
|
+
if occ is not None:
|
|
250
|
+
return occ, None, None, "resolved"
|
|
251
|
+
text = rel.to_identity.display()
|
|
252
|
+
status = (
|
|
253
|
+
"external" if status_override == "external" else "unresolved"
|
|
254
|
+
)
|
|
255
|
+
return None, None, text, status
|
|
256
|
+
if rel.to_symbol:
|
|
257
|
+
occs = store.symbol_occurrences_for_name(snapshot_id, rel.to_symbol)
|
|
258
|
+
if len(occs) == 1:
|
|
259
|
+
return None, occs[0], None, "resolved"
|
|
260
|
+
if len(occs) > 1:
|
|
261
|
+
return None, None, rel.to_symbol, "ambiguous"
|
|
262
|
+
return None, None, rel.to_symbol, "unresolved"
|
|
263
|
+
status = "external" if status_override == "external" else "unresolved"
|
|
264
|
+
return None, None, rel.to_unresolved or None, status
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def persist_domain_result(
|
|
268
|
+
store: Store,
|
|
269
|
+
repo_id: int,
|
|
270
|
+
snapshot_id: int,
|
|
271
|
+
file_ids: Mapping[str, int],
|
|
272
|
+
result: DomainExtractionResult,
|
|
273
|
+
) -> dict[str, int]:
|
|
274
|
+
"""Grava um ``DomainExtractionResult`` nas tabelas v9 — na transação
|
|
275
|
+
do caller, mesmo desenho de ``persist_graph``.
|
|
276
|
+
|
|
277
|
+
Entidades são dedupadas por ``identity_key`` lógica (estáveis por
|
|
278
|
+
repositório); ocorrências ligam entidade↔snapshot↔arquivo; relações
|
|
279
|
+
só persistem com origem vinculada a uma ocorrência do snapshot —
|
|
280
|
+
endpoint ausente conta em ``edges_skipped`` (nunca dangling).
|
|
281
|
+
"""
|
|
282
|
+
counts = {
|
|
283
|
+
"evidence": 0, "entities": 0, "occurrences": 0,
|
|
284
|
+
"relations": 0, "edges_skipped": 0,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# 1. evidências — mesma tabela/envelope do grafo de código.
|
|
288
|
+
evi_row_ids = _persist_evidence(store, snapshot_id, file_ids, result)
|
|
289
|
+
counts["evidence"] = len(evi_row_ids)
|
|
290
|
+
|
|
291
|
+
# 2. entidades lógicas (dedup por identity_key) + ocorrências.
|
|
292
|
+
entity_rows = [
|
|
293
|
+
DomainEntity(
|
|
294
|
+
identity=e.identity, kind=e.kind, name=e.name,
|
|
295
|
+
attributes=e.attributes,
|
|
296
|
+
)
|
|
297
|
+
for e in result.entities
|
|
298
|
+
]
|
|
299
|
+
ent_ids = store.ensure_domain_entities(repo_id, entity_rows)
|
|
300
|
+
counts["entities"] = len({e.identity.logical_key() for e in result.entities})
|
|
301
|
+
|
|
302
|
+
occ_rows: list[tuple[object, ...]] = []
|
|
303
|
+
occ_keys: list[str] = [] # logical_key alinhado a occ_rows
|
|
304
|
+
seen_occ: set[str] = set() # uma ocorrência por identidade/snapshot
|
|
305
|
+
for e in result.entities:
|
|
306
|
+
key = e.identity.logical_key()
|
|
307
|
+
if key in seen_occ:
|
|
308
|
+
continue
|
|
309
|
+
seen_occ.add(key)
|
|
310
|
+
entity_id = ent_ids.get(key)
|
|
311
|
+
if entity_id is None:
|
|
312
|
+
continue
|
|
313
|
+
span = e.span
|
|
314
|
+
occ_rows.append(
|
|
315
|
+
(
|
|
316
|
+
snapshot_id, entity_id, file_ids.get(e.path),
|
|
317
|
+
span.start_byte if span else None,
|
|
318
|
+
span.end_byte if span else None,
|
|
319
|
+
span.start_line if span else None,
|
|
320
|
+
span.start_col if span else None,
|
|
321
|
+
e.content_hash or None, "{}", "extracted",
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
occ_keys.append(key)
|
|
325
|
+
occ_by_key: dict[str, int] = {}
|
|
326
|
+
for key, rid in zip(
|
|
327
|
+
occ_keys, store.insert_domain_occurrences_batch(occ_rows),
|
|
328
|
+
strict=True,
|
|
329
|
+
):
|
|
330
|
+
occ_by_key[key] = rid
|
|
331
|
+
counts["occurrences"] = len(occ_by_key)
|
|
332
|
+
|
|
333
|
+
# 2b. FTS de domínio (lf-0133, schema v12) — rowid = id da
|
|
334
|
+
# ocorrência, mesmo desenho de ``fts_occurrences``. ``terms``
|
|
335
|
+
# pré-tokenizado cobre snake/camel/dotted por fragmento; ``path``
|
|
336
|
+
# só entra quando a ocorrência ancorou num arquivo do inventário —
|
|
337
|
+
# mesma semântica do LIKE sobre ``sf.relative_path`` (LEFT JOIN
|
|
338
|
+
# NULL nunca casava). Snapshots anteriores à v12 não são
|
|
339
|
+
# retroalimentados aqui — ``Store.rebuild_domain_fts`` é o backfill.
|
|
340
|
+
if occ_by_key and store.domain_fts_available():
|
|
341
|
+
# mesma regra de ``seen_occ``: primeira entidade por
|
|
342
|
+
# logical_key — a que gerou a ocorrência persistida.
|
|
343
|
+
entity_by_key: dict[str, ExtractedDomainEntity] = {}
|
|
344
|
+
for e in result.entities:
|
|
345
|
+
entity_by_key.setdefault(e.identity.logical_key(), e)
|
|
346
|
+
fts_rows: list[tuple[object, ...]] = []
|
|
347
|
+
for key, occ_id in sorted(occ_by_key.items(), key=lambda kv: kv[1]):
|
|
348
|
+
e = entity_by_key[key]
|
|
349
|
+
path = e.path if e.path in file_ids else ""
|
|
350
|
+
fts_rows.append(
|
|
351
|
+
(
|
|
352
|
+
occ_id, snapshot_id, ent_ids[key],
|
|
353
|
+
e.name or "", e.identity.qualified_name,
|
|
354
|
+
index_terms(
|
|
355
|
+
e.name or "", e.identity.qualified_name, path
|
|
356
|
+
),
|
|
357
|
+
path, e.identity.domain, str(e.kind),
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
store.insert_domain_fts_batch(fts_rows)
|
|
361
|
+
|
|
362
|
+
# 3. relações — endpoints podem ser entidade OU símbolo de código.
|
|
363
|
+
rel_rows: list[tuple[object, ...]] = []
|
|
364
|
+
for r in result.relations:
|
|
365
|
+
row = _domain_relation_row(
|
|
366
|
+
store, repo_id, snapshot_id, result, r, occ_by_key, evi_row_ids
|
|
367
|
+
)
|
|
368
|
+
if row is None:
|
|
369
|
+
counts["edges_skipped"] += 1 # origem sem âncora no snapshot
|
|
370
|
+
continue
|
|
371
|
+
rel_rows.append(row)
|
|
372
|
+
store.insert_domain_relations(rel_rows)
|
|
373
|
+
counts["relations"] = len(rel_rows)
|
|
374
|
+
return counts
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _domain_from_endpoint(
|
|
378
|
+
store: Store,
|
|
379
|
+
repo_id: int,
|
|
380
|
+
snapshot_id: int,
|
|
381
|
+
rel: ExtractedDomainRelation,
|
|
382
|
+
occ_by_key: dict[str, int],
|
|
383
|
+
) -> tuple[int | None, int | None]:
|
|
384
|
+
"""Endpoint de origem → (entity_occ_id, symbol_occ_id); (None, None)
|
|
385
|
+
quando nenhum âncora existe no snapshot — caller conta como skip."""
|
|
386
|
+
if rel.from_identity is not None:
|
|
387
|
+
occ = _domain_occ_for_identity(
|
|
388
|
+
store, repo_id, snapshot_id, rel.from_identity.logical_key(),
|
|
389
|
+
occ_by_key,
|
|
390
|
+
)
|
|
391
|
+
if occ is not None:
|
|
392
|
+
return occ, None
|
|
393
|
+
if rel.from_symbol:
|
|
394
|
+
occs = store.symbol_occurrences_for_name(snapshot_id, rel.from_symbol)
|
|
395
|
+
if len(occs) == 1:
|
|
396
|
+
return None, occs[0]
|
|
397
|
+
return None, None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _domain_relation_row(
|
|
401
|
+
store: Store,
|
|
402
|
+
repo_id: int,
|
|
403
|
+
snapshot_id: int,
|
|
404
|
+
result: DomainExtractionResult,
|
|
405
|
+
rel: ExtractedDomainRelation,
|
|
406
|
+
occ_by_key: dict[str, int],
|
|
407
|
+
evi_row_ids: dict[int, int],
|
|
408
|
+
) -> tuple[object, ...] | None:
|
|
409
|
+
"""Monta a row de ``domain_relations``; ``None`` = origem sem âncora."""
|
|
410
|
+
frm_ent, frm_sym = _domain_from_endpoint(
|
|
411
|
+
store, repo_id, snapshot_id, rel, occ_by_key
|
|
412
|
+
)
|
|
413
|
+
if frm_ent is None and frm_sym is None:
|
|
414
|
+
return None
|
|
415
|
+
to_ent, to_sym, to_text, status = _resolve_domain_target(
|
|
416
|
+
store, repo_id, snapshot_id, rel, occ_by_key
|
|
417
|
+
)
|
|
418
|
+
confidence = (
|
|
419
|
+
str(rel.resolution.confidence_class)
|
|
420
|
+
if rel.resolution is not None
|
|
421
|
+
else "syntactic"
|
|
422
|
+
)
|
|
423
|
+
if confidence == "inferred" and status == "resolved":
|
|
424
|
+
raise ValueError("inferred evidence can never be reported resolved")
|
|
425
|
+
ev_ids = json.dumps(
|
|
426
|
+
[evi_row_ids[i] for i in rel.evidence if i in evi_row_ids]
|
|
427
|
+
)
|
|
428
|
+
resolver = (
|
|
429
|
+
rel.resolution.resolver
|
|
430
|
+
if rel.resolution is not None
|
|
431
|
+
else f"domain:{result.domain_id}"
|
|
432
|
+
)
|
|
433
|
+
return (
|
|
434
|
+
snapshot_id, frm_ent, frm_sym, to_ent, to_sym, to_text,
|
|
435
|
+
rel.relation_type, status, confidence, resolver, ev_ids,
|
|
436
|
+
)
|