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.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. 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
+ )