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,411 @@
|
|
|
1
|
+
"""Sink de extração do adapter ``domains.aws`` (lf-0113, Cycle 7 Ph.5).
|
|
2
|
+
|
|
3
|
+
Acumulador determinístico da saída de ``extract`` — mesmo desenho de
|
|
4
|
+
``domains.api.emit.Emit``/``domains.terraform`` ``_Builder``:
|
|
5
|
+
|
|
6
|
+
- fábrica de ``DomainIdentity`` com ``catalog`` (coordenadas AWS
|
|
7
|
+
explícitas via ``common.aws_catalog`` — nunca por convenção);
|
|
8
|
+
- evidência ``domain:aws-data`` dedup por ``(path, span)``;
|
|
9
|
+
- entidades dedup por ``logical_key`` com **merge de evidência** (a
|
|
10
|
+
mesma tabela ``db.t`` lida por dois jobs é a mesma entidade lógica);
|
|
11
|
+
- relações dedup por ``(tipo, origem, alvo)`` com ``DomainResolution``
|
|
12
|
+
tipada — ``resolved`` só quando o alvo é entidade deste resultado,
|
|
13
|
+
``external`` para recursos fora do snapshot (ARNs, catálogos
|
|
14
|
+
referenciados mas não declarados), ``unresolved`` para texto
|
|
15
|
+
preservado/dinâmico;
|
|
16
|
+
- registro de catálogos declarados (``spark.sql.catalog.<n>``,
|
|
17
|
+
``catalog_id`` de Glue) resolvendo ``REFERENCES`` de catálogo ao fim
|
|
18
|
+
do request — nomes não declarados viram ``external``, nunca
|
|
19
|
+
fabricados.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
from code_atlas.contracts.enums import ConfidenceClass, ResolutionStatus
|
|
27
|
+
from code_atlas.domains.contracts import (
|
|
28
|
+
DomainDiagnostic,
|
|
29
|
+
DomainExtractionRequest,
|
|
30
|
+
DomainExtractionResult,
|
|
31
|
+
ExtractedDomainEntity,
|
|
32
|
+
ExtractedDomainRelation,
|
|
33
|
+
)
|
|
34
|
+
from code_atlas.domains.evidence import domain_extractor_id, evidence_record
|
|
35
|
+
from code_atlas.domains.identity import DomainIdentity
|
|
36
|
+
from code_atlas.domains.model import DomainEntityKind
|
|
37
|
+
from code_atlas.domains.resolution import DomainResolution
|
|
38
|
+
from code_atlas.parsers.contracts import EvidenceRecord, SourceSpan
|
|
39
|
+
|
|
40
|
+
ADAPTER_DOMAIN = "aws-data"
|
|
41
|
+
RESOLVER_ID = domain_extractor_id(ADAPTER_DOMAIN)
|
|
42
|
+
|
|
43
|
+
_DIAG_CAP = 120 # diagnósticos por request — depois vira resumo
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class PendingCatalogRef:
|
|
48
|
+
"""Referência a catálogo nomeado — resolvida ao fim do request."""
|
|
49
|
+
|
|
50
|
+
from_id: DomainIdentity
|
|
51
|
+
catalog: str # segmento ``catalog`` escrito no artefato
|
|
52
|
+
path: str
|
|
53
|
+
span: SourceSpan | None
|
|
54
|
+
evi: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Sink:
|
|
58
|
+
"""Acumulador da saída de ``AwsDataAdapter.extract``."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, request: DomainExtractionRequest, adapter_version: str) -> None:
|
|
61
|
+
self._request = request
|
|
62
|
+
self._version = adapter_version
|
|
63
|
+
self.entities: dict[str, ExtractedDomainEntity] = {}
|
|
64
|
+
self.relations: dict[tuple[str, str, str], ExtractedDomainRelation] = {}
|
|
65
|
+
self.evidence: list[EvidenceRecord] = []
|
|
66
|
+
self._evi_index: dict[tuple[str, tuple[int, ...] | None], int] = {}
|
|
67
|
+
self.diagnostics: list[DomainDiagnostic] = []
|
|
68
|
+
self._truncated_diags = 0
|
|
69
|
+
self.stats: dict[str, int] = {}
|
|
70
|
+
# catálogos declarados no request: segmento ``catalog`` → id
|
|
71
|
+
self.catalogs: dict[str, DomainIdentity] = {}
|
|
72
|
+
self.pending_cat_refs: list[PendingCatalogRef] = []
|
|
73
|
+
# nome declarado → identidade (lookup p/ TRIGGERS→job etc.)
|
|
74
|
+
self.by_name: dict[tuple[str, str], DomainIdentity] = {}
|
|
75
|
+
|
|
76
|
+
# -- fábricas -----------------------------------------------------
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def repository(self) -> str:
|
|
80
|
+
return self._request.repository
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def root(self) -> str:
|
|
84
|
+
return self._request.root
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def environment(self) -> str:
|
|
88
|
+
return self._request.environment
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def version(self) -> str:
|
|
92
|
+
return self._version
|
|
93
|
+
|
|
94
|
+
def ident(
|
|
95
|
+
self,
|
|
96
|
+
qualified_name: str,
|
|
97
|
+
*,
|
|
98
|
+
catalog: str = "",
|
|
99
|
+
namespace: str = "",
|
|
100
|
+
) -> DomainIdentity:
|
|
101
|
+
req = self._request
|
|
102
|
+
return DomainIdentity(
|
|
103
|
+
domain=ADAPTER_DOMAIN,
|
|
104
|
+
repository=req.repository,
|
|
105
|
+
qualified_name=qualified_name,
|
|
106
|
+
root=req.root,
|
|
107
|
+
catalog=catalog,
|
|
108
|
+
namespace=namespace,
|
|
109
|
+
environment=req.environment,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def evi(
|
|
113
|
+
self,
|
|
114
|
+
path: str,
|
|
115
|
+
content_hash: str,
|
|
116
|
+
span: SourceSpan | None = None,
|
|
117
|
+
notes: str = "",
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Evidência dedup por ``(path, span)`` → índice no envelope."""
|
|
120
|
+
key = (
|
|
121
|
+
path,
|
|
122
|
+
None
|
|
123
|
+
if span is None
|
|
124
|
+
else (
|
|
125
|
+
span.start_byte,
|
|
126
|
+
span.end_byte,
|
|
127
|
+
span.start_line,
|
|
128
|
+
span.start_col,
|
|
129
|
+
span.end_line,
|
|
130
|
+
span.end_col,
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
idx = self._evi_index.get(key)
|
|
134
|
+
if idx is not None:
|
|
135
|
+
return idx
|
|
136
|
+
idx = len(self.evidence)
|
|
137
|
+
self.evidence.append(
|
|
138
|
+
evidence_record(
|
|
139
|
+
adapter_domain=ADAPTER_DOMAIN,
|
|
140
|
+
adapter_version=self._version,
|
|
141
|
+
path=path,
|
|
142
|
+
content_hash=content_hash,
|
|
143
|
+
span=span,
|
|
144
|
+
notes=notes,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
self._evi_index[key] = idx
|
|
148
|
+
return idx
|
|
149
|
+
|
|
150
|
+
def diag(
|
|
151
|
+
self,
|
|
152
|
+
code: str,
|
|
153
|
+
severity: str,
|
|
154
|
+
message: str,
|
|
155
|
+
path: str = "",
|
|
156
|
+
) -> None:
|
|
157
|
+
if len(self.diagnostics) >= _DIAG_CAP:
|
|
158
|
+
self._truncated_diags += 1
|
|
159
|
+
return
|
|
160
|
+
self.diagnostics.append(
|
|
161
|
+
DomainDiagnostic(
|
|
162
|
+
code=code,
|
|
163
|
+
severity=severity,
|
|
164
|
+
message=message,
|
|
165
|
+
path=path,
|
|
166
|
+
adapter_id=ADAPTER_DOMAIN,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def bump(self, stat: str, n: int = 1) -> None:
|
|
171
|
+
self.stats[stat] = self.stats.get(stat, 0) + n
|
|
172
|
+
|
|
173
|
+
# -- entidades ------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def entity(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
kind: DomainEntityKind,
|
|
179
|
+
name: str,
|
|
180
|
+
qualified_name: str,
|
|
181
|
+
catalog: str = "",
|
|
182
|
+
namespace: str = "",
|
|
183
|
+
path: str = "",
|
|
184
|
+
span: SourceSpan | None = None,
|
|
185
|
+
content_hash: str = "",
|
|
186
|
+
attributes: dict[str, str] | None = None,
|
|
187
|
+
evidence: tuple[int, ...] = (),
|
|
188
|
+
) -> DomainIdentity:
|
|
189
|
+
"""Registra entidade dedup por ``logical_key``; merge de evidência.
|
|
190
|
+
|
|
191
|
+
Em reemissão, evidência acumula e os primeiros atributos vencem
|
|
192
|
+
(determinístico); kinds conflitantes para a mesma identidade
|
|
193
|
+
viram ``conflicting-kind`` — nunca merge silencioso de tipo.
|
|
194
|
+
"""
|
|
195
|
+
ident = self.ident(qualified_name, catalog=catalog, namespace=namespace)
|
|
196
|
+
return self.entity_for(
|
|
197
|
+
ident,
|
|
198
|
+
kind=kind,
|
|
199
|
+
name=name,
|
|
200
|
+
path=path,
|
|
201
|
+
span=span,
|
|
202
|
+
content_hash=content_hash,
|
|
203
|
+
attributes=attributes,
|
|
204
|
+
evidence=evidence,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def entity_for(
|
|
208
|
+
self,
|
|
209
|
+
ident: DomainIdentity,
|
|
210
|
+
*,
|
|
211
|
+
kind: DomainEntityKind,
|
|
212
|
+
name: str,
|
|
213
|
+
path: str = "",
|
|
214
|
+
span: SourceSpan | None = None,
|
|
215
|
+
content_hash: str = "",
|
|
216
|
+
attributes: dict[str, str] | None = None,
|
|
217
|
+
evidence: tuple[int, ...] = (),
|
|
218
|
+
) -> DomainIdentity:
|
|
219
|
+
"""Mesma dedup de ``entity`` mas com identidade pronta."""
|
|
220
|
+
key = ident.logical_key()
|
|
221
|
+
prev = self.entities.get(key)
|
|
222
|
+
if prev is not None:
|
|
223
|
+
merged_ev = tuple(sorted({*prev.evidence, *evidence}))
|
|
224
|
+
if prev.kind is not kind:
|
|
225
|
+
self.diag(
|
|
226
|
+
"conflicting-kind",
|
|
227
|
+
"warning",
|
|
228
|
+
f"{ident.display()} emitted as both "
|
|
229
|
+
f"{prev.kind.value} and {kind.value} — first kept",
|
|
230
|
+
path or prev.path,
|
|
231
|
+
)
|
|
232
|
+
merged_attrs = dict(prev.attributes)
|
|
233
|
+
merged_attrs.update(
|
|
234
|
+
{k: v for k, v in (attributes or {}).items() if k not in merged_attrs}
|
|
235
|
+
)
|
|
236
|
+
self.entities[key] = ExtractedDomainEntity(
|
|
237
|
+
identity=prev.identity,
|
|
238
|
+
kind=prev.kind,
|
|
239
|
+
name=prev.name,
|
|
240
|
+
path=prev.path,
|
|
241
|
+
span=prev.span,
|
|
242
|
+
content_hash=prev.content_hash or content_hash,
|
|
243
|
+
attributes=merged_attrs,
|
|
244
|
+
evidence=merged_ev,
|
|
245
|
+
)
|
|
246
|
+
return ident
|
|
247
|
+
self.entities[key] = ExtractedDomainEntity(
|
|
248
|
+
identity=ident,
|
|
249
|
+
kind=kind,
|
|
250
|
+
name=name,
|
|
251
|
+
path=path,
|
|
252
|
+
span=span,
|
|
253
|
+
content_hash=content_hash,
|
|
254
|
+
attributes=attributes or {},
|
|
255
|
+
evidence=evidence,
|
|
256
|
+
)
|
|
257
|
+
self.by_name.setdefault((kind.value, name), ident)
|
|
258
|
+
self.bump(f"entities_{kind.value}")
|
|
259
|
+
return ident
|
|
260
|
+
|
|
261
|
+
# -- relações ------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
def resolution(
|
|
264
|
+
self,
|
|
265
|
+
status: ResolutionStatus,
|
|
266
|
+
confidence: ConfidenceClass = ConfidenceClass.SYNTACTIC,
|
|
267
|
+
*,
|
|
268
|
+
target: DomainIdentity | None = None,
|
|
269
|
+
unresolved_text: str = "",
|
|
270
|
+
) -> DomainResolution:
|
|
271
|
+
return DomainResolution(
|
|
272
|
+
status=status,
|
|
273
|
+
confidence_class=confidence,
|
|
274
|
+
resolver=RESOLVER_ID,
|
|
275
|
+
target=target,
|
|
276
|
+
unresolved_text=unresolved_text,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def rel(
|
|
280
|
+
self,
|
|
281
|
+
relation_type: str,
|
|
282
|
+
path: str,
|
|
283
|
+
from_identity: DomainIdentity,
|
|
284
|
+
*,
|
|
285
|
+
to_identity: DomainIdentity | None = None,
|
|
286
|
+
to_unresolved: str = "",
|
|
287
|
+
status: ResolutionStatus | None = None,
|
|
288
|
+
confidence: ConfidenceClass = ConfidenceClass.SYNTACTIC,
|
|
289
|
+
span: SourceSpan | None = None,
|
|
290
|
+
evidence: tuple[int, ...] = (),
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Relação com estado honesto; dedup por (tipo, origem, alvo).
|
|
293
|
+
|
|
294
|
+
``status=None`` → ``resolved`` quando ``to_identity`` é dado,
|
|
295
|
+
``unresolved`` para alvo textual. ``external`` explícito para
|
|
296
|
+
alvos fora do snapshot (ARNs, catálogos não declarados).
|
|
297
|
+
"""
|
|
298
|
+
if status is None:
|
|
299
|
+
status = (
|
|
300
|
+
ResolutionStatus.RESOLVED
|
|
301
|
+
if to_identity is not None
|
|
302
|
+
else ResolutionStatus.UNRESOLVED
|
|
303
|
+
)
|
|
304
|
+
to_key = to_identity.logical_key() if to_identity is not None else f"text:{to_unresolved}"
|
|
305
|
+
key = (relation_type, from_identity.logical_key(), to_key)
|
|
306
|
+
prev = self.relations.get(key)
|
|
307
|
+
if prev is not None:
|
|
308
|
+
merged_ev = tuple(sorted({*prev.evidence, *evidence}))
|
|
309
|
+
self.relations[key] = ExtractedDomainRelation(
|
|
310
|
+
relation_type=prev.relation_type,
|
|
311
|
+
path=prev.path,
|
|
312
|
+
from_identity=prev.from_identity,
|
|
313
|
+
to_identity=prev.to_identity,
|
|
314
|
+
to_unresolved=prev.to_unresolved,
|
|
315
|
+
resolution=prev.resolution,
|
|
316
|
+
span=prev.span,
|
|
317
|
+
evidence=merged_ev,
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
res = self.resolution(
|
|
321
|
+
status,
|
|
322
|
+
confidence,
|
|
323
|
+
target=to_identity if status is not ResolutionStatus.UNRESOLVED else None,
|
|
324
|
+
unresolved_text=(to_unresolved if status is not ResolutionStatus.RESOLVED else ""),
|
|
325
|
+
)
|
|
326
|
+
self.relations[key] = ExtractedDomainRelation(
|
|
327
|
+
relation_type=relation_type,
|
|
328
|
+
path=path,
|
|
329
|
+
from_identity=from_identity,
|
|
330
|
+
to_identity=to_identity,
|
|
331
|
+
to_unresolved=(to_unresolved if status is not ResolutionStatus.RESOLVED else ""),
|
|
332
|
+
resolution=res,
|
|
333
|
+
span=span,
|
|
334
|
+
evidence=evidence,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# -- catálogos -------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
def register_catalog(self, catalog_segment: str, ident: DomainIdentity) -> None:
|
|
340
|
+
"""Catálogo declarado (conf/catalog_id) → lookup para REFERENCES."""
|
|
341
|
+
if catalog_segment:
|
|
342
|
+
self.catalogs.setdefault(catalog_segment, ident)
|
|
343
|
+
|
|
344
|
+
def catalog_ref(
|
|
345
|
+
self,
|
|
346
|
+
from_id: DomainIdentity,
|
|
347
|
+
catalog_segment: str,
|
|
348
|
+
path: str,
|
|
349
|
+
span: SourceSpan | None,
|
|
350
|
+
evi: int,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Adia ``REFERENCES`` a catálogo nomeado até ``finish()``.
|
|
353
|
+
|
|
354
|
+
O catálogo pode ser declarado por outro artefato do request —
|
|
355
|
+
resolução só é honesta com o conjunto completo.
|
|
356
|
+
"""
|
|
357
|
+
if catalog_segment:
|
|
358
|
+
self.pending_cat_refs.append(
|
|
359
|
+
PendingCatalogRef(from_id, catalog_segment, path, span, evi)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# -- saída ----------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def finish(self) -> DomainExtractionResult:
|
|
365
|
+
"""Resolve referências pendentes e monta o envelope final."""
|
|
366
|
+
for ref in self.pending_cat_refs:
|
|
367
|
+
target = self.catalogs.get(ref.catalog)
|
|
368
|
+
if target is not None:
|
|
369
|
+
self.rel(
|
|
370
|
+
"REFERENCES",
|
|
371
|
+
ref.path,
|
|
372
|
+
ref.from_id,
|
|
373
|
+
to_identity=target,
|
|
374
|
+
span=ref.span,
|
|
375
|
+
evidence=(ref.evi,),
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
self.rel(
|
|
379
|
+
"REFERENCES",
|
|
380
|
+
ref.path,
|
|
381
|
+
ref.from_id,
|
|
382
|
+
to_unresolved=f"catalog:{ref.catalog}",
|
|
383
|
+
status=ResolutionStatus.EXTERNAL,
|
|
384
|
+
span=ref.span,
|
|
385
|
+
evidence=(ref.evi,),
|
|
386
|
+
)
|
|
387
|
+
self.pending_cat_refs.clear()
|
|
388
|
+
if self._truncated_diags:
|
|
389
|
+
self.diagnostics.append(
|
|
390
|
+
DomainDiagnostic(
|
|
391
|
+
code="diagnostics-truncated",
|
|
392
|
+
severity="info",
|
|
393
|
+
message=(
|
|
394
|
+
f"{self._truncated_diags} further diagnostics suppressed (cap {_DIAG_CAP})"
|
|
395
|
+
),
|
|
396
|
+
adapter_id=ADAPTER_DOMAIN,
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
stats = dict(self.stats)
|
|
400
|
+
stats["entities"] = len(self.entities)
|
|
401
|
+
stats["relations"] = len(self.relations)
|
|
402
|
+
stats["diagnostics"] = len(self.diagnostics)
|
|
403
|
+
return DomainExtractionResult(
|
|
404
|
+
domain_id=ADAPTER_DOMAIN,
|
|
405
|
+
adapter_version=self._version,
|
|
406
|
+
entities=tuple(self.entities.values()),
|
|
407
|
+
relations=tuple(self.relations.values()),
|
|
408
|
+
evidence=tuple(self.evidence),
|
|
409
|
+
diagnostics=tuple(self.diagnostics),
|
|
410
|
+
stats=stats,
|
|
411
|
+
)
|