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,57 @@
|
|
|
1
|
+
"""Evidência de fatos de domínio (lf-0110, Cycle 7).
|
|
2
|
+
|
|
3
|
+
Nenhum tipo novo: evidência de domínio **é** ``EvidenceRecord`` — a mesma
|
|
4
|
+
tabela ``evidence``, o mesmo envelope (extractor + versão + content_hash +
|
|
5
|
+
span). O que muda é a convenção de identificação do produtor:
|
|
6
|
+
|
|
7
|
+
- ``extractor`` = ``domain:<domain_id>`` (ex.: ``domain:sql``);
|
|
8
|
+
- ``extractor_version`` = ``adapter_version`` do adapter;
|
|
9
|
+
- ``resolver_id``/``resolver_version``/``evidence_tier`` preenchidos só
|
|
10
|
+
quando a evidência veio de um resolvedor (mesma semântica de lf-0035);
|
|
11
|
+
- ``content_hash`` = sha256 do artefato observado — casa 1:1 com o
|
|
12
|
+
arquivo do inventário (evidence-first: nenhum fato sem trecho real).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from code_atlas.parsers.contracts import EvidenceRecord, SourceSpan
|
|
18
|
+
|
|
19
|
+
# Alias documentado — evidência de domínio reutiliza o envelope canônico.
|
|
20
|
+
DomainEvidence = EvidenceRecord
|
|
21
|
+
|
|
22
|
+
DOMAIN_EXTRACTOR_PREFIX = "domain:"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def domain_extractor_id(domain_id: str) -> str:
|
|
26
|
+
"""``extractor`` canônico de um adapter de domínio."""
|
|
27
|
+
return f"{DOMAIN_EXTRACTOR_PREFIX}{domain_id}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def evidence_record(
|
|
31
|
+
*,
|
|
32
|
+
adapter_domain: str,
|
|
33
|
+
adapter_version: str,
|
|
34
|
+
path: str,
|
|
35
|
+
content_hash: str,
|
|
36
|
+
span: SourceSpan | None = None,
|
|
37
|
+
notes: str = "",
|
|
38
|
+
resolver_id: str = "",
|
|
39
|
+
resolver_version: str = "",
|
|
40
|
+
evidence_tier: int = 0,
|
|
41
|
+
index_hash: str = "",
|
|
42
|
+
validation_status: str = "",
|
|
43
|
+
) -> EvidenceRecord:
|
|
44
|
+
"""Fábrica tipada de ``EvidenceRecord`` com convenção de domínio."""
|
|
45
|
+
return EvidenceRecord(
|
|
46
|
+
path=path,
|
|
47
|
+
span=span,
|
|
48
|
+
content_hash=content_hash,
|
|
49
|
+
extractor=domain_extractor_id(adapter_domain),
|
|
50
|
+
extractor_version=adapter_version,
|
|
51
|
+
notes=notes,
|
|
52
|
+
resolver_id=resolver_id,
|
|
53
|
+
resolver_version=resolver_version,
|
|
54
|
+
evidence_tier=evidence_tier,
|
|
55
|
+
index_hash=index_hash,
|
|
56
|
+
validation_status=validation_status,
|
|
57
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Identidade canônica de entidades de domínio (lf-0110, Cycle 7).
|
|
2
|
+
|
|
3
|
+
Regra inegociável: duas entidades com o mesmo nome em escopos distintos
|
|
4
|
+
NÃO podem colidir — duas tabelas ``orders`` em catálogos diferentes, dois
|
|
5
|
+
``aws_s3_bucket.logs`` em contas/regiões distintas têm identidades
|
|
6
|
+
diferentes. A chave canônica é posicional e **escapada por segmento**
|
|
7
|
+
(percent-encoding), portanto separadores dentro de um componente
|
|
8
|
+
(``:``, ``/``, ``.``) nunca se confundem com o separador estrutural.
|
|
9
|
+
|
|
10
|
+
Componentes, em ordem fixa:
|
|
11
|
+
|
|
12
|
+
1. ``domain`` — id do domínio do adapter (``sql``, ``terraform``, …);
|
|
13
|
+
2. ``repository`` — ``identity_key`` lógica do repositório (lf-0021);
|
|
14
|
+
3. ``root`` — sub-raiz dentro do repo/monorepo (``""`` = raiz);
|
|
15
|
+
4. ``catalog`` — catálogo/conta/região/projeto que qualifica o
|
|
16
|
+
namespace (ex.: ``glue:123456789012:us-east-1``, ``aws:…:eu-west-1``);
|
|
17
|
+
5. ``namespace`` — prefixo intermediário (schema, database, module);
|
|
18
|
+
6. ``environment`` — qualificador opcional (``dev``/``prod``); ``""``
|
|
19
|
+
quando o adapter não distingue ambientes;
|
|
20
|
+
7. ``qualified_name`` — nome qualificado definido pelo adapter;
|
|
21
|
+
8. ``snapshot`` — escopo opcional de revisão (commit/digest); ``""``
|
|
22
|
+
para identidade **lógica** (vale por repositório, como ``symbols``).
|
|
23
|
+
|
|
24
|
+
``identity_key`` persistida usa sempre a forma lógica (``snapshot=""``);
|
|
25
|
+
a ocorrência por snapshot é quem carrega o vínculo temporal — mesmo
|
|
26
|
+
desenho de ``symbols``/``symbol_occurrences``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
_SCHEME = "dom"
|
|
34
|
+
_SEP = "/"
|
|
35
|
+
|
|
36
|
+
# Percent-encoding próprio: ``urllib.parse`` é proibido fora de ``api/``
|
|
37
|
+
# (auditoria de rede lf-0086). Safe-set = RFC 3986 unreserved.
|
|
38
|
+
_SAFE = frozenset(
|
|
39
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-~"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _enc(part: str) -> str:
|
|
44
|
+
"""Escapa um segmento — todo byte fora do safe-set vira ``%XX``.
|
|
45
|
+
|
|
46
|
+
Cobre ``:``, ``/``, ``.``, espaço e UTF-8 multibyte: separadores
|
|
47
|
+
estruturais nunca se confundem com conteúdo do componente.
|
|
48
|
+
"""
|
|
49
|
+
out: list[str] = []
|
|
50
|
+
for b in part.encode("utf-8"):
|
|
51
|
+
ch = chr(b)
|
|
52
|
+
out.append(ch if ch in _SAFE else f"%{b:02X}")
|
|
53
|
+
return "".join(out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _dec(part: str) -> str:
|
|
57
|
+
"""Inverso de ``_enc`` — ``%XX`` malformado é literal, nunca crash."""
|
|
58
|
+
out = bytearray()
|
|
59
|
+
i, n = 0, len(part)
|
|
60
|
+
while i < n:
|
|
61
|
+
ch = part[i]
|
|
62
|
+
if ch == "%" and i + 3 <= n:
|
|
63
|
+
try:
|
|
64
|
+
out.append(int(part[i + 1:i + 3], 16))
|
|
65
|
+
i += 3
|
|
66
|
+
continue
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass
|
|
69
|
+
out.extend(ch.encode("utf-8"))
|
|
70
|
+
i += 1
|
|
71
|
+
return out.decode("utf-8")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class DomainIdentity:
|
|
76
|
+
"""Identidade canônica completa de uma entidade de domínio.
|
|
77
|
+
|
|
78
|
+
Campos são ``strip()``ados em ``__post_init__``; capitalização é
|
|
79
|
+
**preservada** (política de case é do adapter — identificadores SQL
|
|
80
|
+
quoted são case-sensitive e adapters não devem fundir nomes reais
|
|
81
|
+
distintos).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
domain: str # id do domínio produtor (adapter.domain_id)
|
|
85
|
+
repository: str # identity_key lógica do repositório (lf-0021)
|
|
86
|
+
qualified_name: str # nome qualificado definido pelo adapter
|
|
87
|
+
root: str = "" # sub-raiz no monorepo; "" = raiz do repo
|
|
88
|
+
catalog: str = "" # catálogo/conta/região (qualificador superior)
|
|
89
|
+
namespace: str = "" # schema/database/módulo (qualificador inferior)
|
|
90
|
+
environment: str = "" # qualificador opcional de ambiente
|
|
91
|
+
snapshot: str = "" # escopo de revisão; "" = identidade lógica
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
for f in ("domain", "repository", "qualified_name", "root",
|
|
95
|
+
"catalog", "namespace", "environment", "snapshot"):
|
|
96
|
+
object.__setattr__(self, f, getattr(self, f).strip())
|
|
97
|
+
for f in ("domain", "repository", "qualified_name"):
|
|
98
|
+
if not getattr(self, f):
|
|
99
|
+
raise ValueError(f"domain identity requires {f}")
|
|
100
|
+
|
|
101
|
+
def _segments(self, *, include_snapshot: bool) -> tuple[str, ...]:
|
|
102
|
+
seg = (
|
|
103
|
+
self.domain, self.repository, self.root, self.catalog,
|
|
104
|
+
self.namespace, self.environment, self.qualified_name,
|
|
105
|
+
)
|
|
106
|
+
if include_snapshot:
|
|
107
|
+
return (*seg, self.snapshot)
|
|
108
|
+
return seg
|
|
109
|
+
|
|
110
|
+
def canonical_key(self) -> str:
|
|
111
|
+
"""Chave canônica completa (inclui ``snapshot`` quando presente).
|
|
112
|
+
|
|
113
|
+
Formato posicional ``dom:<seg>/<seg>/…`` com cada segmento
|
|
114
|
+
percent-encodado — colisão só se **todos** os componentes forem
|
|
115
|
+
idênticos.
|
|
116
|
+
"""
|
|
117
|
+
segs = "/".join(_enc(s) for s in self._segments(include_snapshot=True))
|
|
118
|
+
return f"{_SCHEME}:{segs}"
|
|
119
|
+
|
|
120
|
+
def logical_key(self) -> str:
|
|
121
|
+
"""Chave lógica (sem ``snapshot``) — identidade por repositório."""
|
|
122
|
+
segs = "/".join(_enc(s) for s in self._segments(include_snapshot=False))
|
|
123
|
+
return f"{_SCHEME}:{segs}"
|
|
124
|
+
|
|
125
|
+
def logical(self) -> DomainIdentity:
|
|
126
|
+
"""Identidade lógica equivalente (``snapshot=""``)."""
|
|
127
|
+
if not self.snapshot:
|
|
128
|
+
return self
|
|
129
|
+
return DomainIdentity(
|
|
130
|
+
domain=self.domain, repository=self.repository,
|
|
131
|
+
qualified_name=self.qualified_name, root=self.root,
|
|
132
|
+
catalog=self.catalog, namespace=self.namespace,
|
|
133
|
+
environment=self.environment,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def display(self) -> str:
|
|
137
|
+
"""Forma legível para humanos (não usada como chave)."""
|
|
138
|
+
head = ".".join(p for p in (self.catalog, self.namespace) if p)
|
|
139
|
+
qname = f"{head}.{self.qualified_name}" if head else self.qualified_name
|
|
140
|
+
env = f"@{self.environment}" if self.environment else ""
|
|
141
|
+
return f"{self.domain}:{qname}{env}"
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def parse_key(key: str) -> tuple[str, ...] | None:
|
|
145
|
+
"""Decodifica os segmentos de uma chave ``dom:…``; None se inválida."""
|
|
146
|
+
if not key.startswith(f"{_SCHEME}:"):
|
|
147
|
+
return None
|
|
148
|
+
body = key[len(_SCHEME) + 1:]
|
|
149
|
+
return tuple(_dec(seg) for seg in body.split(_SEP))
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Modelo unificado de entidades/relações de domínio (lf-0110, Cycle 7).
|
|
2
|
+
|
|
3
|
+
Vocabulário fechado do prompt Cycle 7 (Phases 1+2): adapters extraem
|
|
4
|
+
**somente** estes kinds — qualquer extensão exige spec + ADR. ``TESTS`` e
|
|
5
|
+
``REFERENCES`` são compartilhados com ``EdgeKind`` (mesmo valor semântico).
|
|
6
|
+
|
|
7
|
+
``REPOSITORY``/``PROJECT`` aqui são entidades de **domínio** (ex.: um repo
|
|
8
|
+
federado referenciado por um workflow CI/CD), distintos de
|
|
9
|
+
``NodeKind.REPOSITORY`` — nó do grafo de código já coberto pelo modelo de
|
|
10
|
+
símbolos. Camadas diferentes, tabelas diferentes.
|
|
11
|
+
|
|
12
|
+
Persistência (ADR-0010): entidades de domínio NÃO reutilizam a tabela
|
|
13
|
+
``symbols`` — a identidade delas inclui ``catalog``/``namespace``/
|
|
14
|
+
``environment``, que o UNIQUE de ``symbols`` (repo, language, qname, kind)
|
|
15
|
+
não consegue expressar (duas tabelas ``orders`` de catálogos distintos
|
|
16
|
+
colidiriam). Schema dedicado na migração v9.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from enum import StrEnum
|
|
24
|
+
|
|
25
|
+
from code_atlas.contracts.enums import ConfidenceClass, EdgeKind, ResolutionStatus
|
|
26
|
+
from code_atlas.contracts.models import _coerce
|
|
27
|
+
from code_atlas.domains.identity import DomainIdentity
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DomainEntityKind(StrEnum):
|
|
31
|
+
"""Tipos de entidade de domínio (prompt Cycle 7 — vocabulário fechado)."""
|
|
32
|
+
|
|
33
|
+
DATA_TABLE = "data_table"
|
|
34
|
+
DATA_COLUMN = "data_column"
|
|
35
|
+
DATA_VIEW = "data_view"
|
|
36
|
+
DATA_TRANSFORMATION = "data_transformation"
|
|
37
|
+
DATA_PIPELINE = "data_pipeline"
|
|
38
|
+
SPARK_JOB = "spark_job"
|
|
39
|
+
GLUE_JOB = "glue_job"
|
|
40
|
+
DATA_CATALOG = "data_catalog"
|
|
41
|
+
ICEBERG_TABLE = "iceberg_table"
|
|
42
|
+
API_ENDPOINT = "api_endpoint"
|
|
43
|
+
API_SERVICE = "api_service"
|
|
44
|
+
API_CONTRACT = "api_contract"
|
|
45
|
+
TERRAFORM_RESOURCE = "terraform_resource"
|
|
46
|
+
TERRAFORM_MODULE = "terraform_module"
|
|
47
|
+
IAM_ROLE = "iam_role"
|
|
48
|
+
CICD_WORKFLOW = "cicd_workflow"
|
|
49
|
+
CICD_JOB = "cicd_job"
|
|
50
|
+
REPOSITORY = "repository" # entidade de federação (lf-0118), ≠ NodeKind
|
|
51
|
+
PROJECT = "project"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DomainRelationKind(StrEnum):
|
|
55
|
+
"""Relações de domínio (prompt Cycle 7 — vocabulário fechado)."""
|
|
56
|
+
|
|
57
|
+
READS_FROM = "READS_FROM"
|
|
58
|
+
WRITES_TO = "WRITES_TO"
|
|
59
|
+
TRANSFORMS = "TRANSFORMS"
|
|
60
|
+
DERIVES_FROM = "DERIVES_FROM"
|
|
61
|
+
DEPENDS_ON = "DEPENDS_ON"
|
|
62
|
+
CALLS_ENDPOINT = "CALLS_ENDPOINT"
|
|
63
|
+
IMPLEMENTS_CONTRACT = "IMPLEMENTS_CONTRACT"
|
|
64
|
+
PROVISIONS = "PROVISIONS"
|
|
65
|
+
USES_RESOURCE = "USES_RESOURCE"
|
|
66
|
+
ASSUMES_ROLE = "ASSUMES_ROLE"
|
|
67
|
+
TRIGGERS = "TRIGGERS"
|
|
68
|
+
DEPLOYS = "DEPLOYS"
|
|
69
|
+
TESTS = "TESTS" # valor compartilhado com EdgeKind.TESTS
|
|
70
|
+
REFERENCES = "REFERENCES" # valor compartilhado com EdgeKind.REFERENCES
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Vocabulário aceito em domain_relations.type: as relações de domínio mais
|
|
74
|
+
# o vocabulário estrutural (CONTAINS módulo→recurso, IMPORTS de tf module…).
|
|
75
|
+
DOMAIN_RELATION_TYPES: frozenset[str] = frozenset(
|
|
76
|
+
{str(k) for k in DomainRelationKind} | {str(k) for k in EdgeKind}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class DomainEntity:
|
|
82
|
+
"""Entidade lógica de domínio — estável por repositório.
|
|
83
|
+
|
|
84
|
+
``id`` é a row id em ``domain_entities`` (``None`` = a persistir);
|
|
85
|
+
``attributes`` carrega observáveis específicos do domínio (ddl
|
|
86
|
+
resumido, tipo de recurso, provider) como strings — nada de blobs
|
|
87
|
+
arbitrários nem segredos.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
identity: DomainIdentity
|
|
91
|
+
kind: DomainEntityKind
|
|
92
|
+
name: str
|
|
93
|
+
attributes: Mapping[str, str] = field(default_factory=dict)
|
|
94
|
+
id: int | None = None
|
|
95
|
+
|
|
96
|
+
def __post_init__(self) -> None:
|
|
97
|
+
object.__setattr__(
|
|
98
|
+
self, "kind", _coerce(DomainEntityKind, self.kind, "kind")
|
|
99
|
+
)
|
|
100
|
+
if not self.name.strip():
|
|
101
|
+
raise ValueError("domain entity requires name")
|
|
102
|
+
object.__setattr__(
|
|
103
|
+
self,
|
|
104
|
+
"attributes",
|
|
105
|
+
{str(k): str(v) for k, v in self.attributes.items()},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def domain(self) -> str:
|
|
110
|
+
return self.identity.domain
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def qualified_name(self) -> str:
|
|
114
|
+
return self.identity.qualified_name
|
|
115
|
+
|
|
116
|
+
def identity_key(self) -> str:
|
|
117
|
+
"""Chave lógica persistida em ``domain_entities.identity_key``."""
|
|
118
|
+
return self.identity.logical_key()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class DomainEntityOccurrence:
|
|
123
|
+
"""Ocorrência de uma entidade num snapshot (span/hash da ocorrência).
|
|
124
|
+
|
|
125
|
+
``source_file_id=None`` é permitido apenas para entidades sintetizadas
|
|
126
|
+
por derivação declarada (ex.: catálogo agregado) — o caso normal é
|
|
127
|
+
evidence-first: entidade observada num arquivo do inventário.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
snapshot_id: int
|
|
131
|
+
entity_id: int
|
|
132
|
+
source_file_id: int | None
|
|
133
|
+
id: int | None = None
|
|
134
|
+
span_start: int | None = None
|
|
135
|
+
span_end: int | None = None
|
|
136
|
+
line: int | None = None
|
|
137
|
+
col: int | None = None
|
|
138
|
+
content_hash: str | None = None
|
|
139
|
+
attributes: Mapping[str, str] = field(default_factory=dict)
|
|
140
|
+
extraction_status: str = "extracted" # extracted | partial | failed
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class DomainRelation:
|
|
145
|
+
"""Aresta de domínio com proveniência obrigatória (espelha ``Relation``).
|
|
146
|
+
|
|
147
|
+
Endpoints podem ser ocorrências de entidade de domínio OU de símbolo
|
|
148
|
+
de código — a ponte código↔domínio (lf-0117/0120) precisa das duas
|
|
149
|
+
direções sem nova migração. Exige-se ≥1 endpoint de origem e ≥1 alvo
|
|
150
|
+
(ou ``to_unresolved`` honesto).
|
|
151
|
+
|
|
152
|
+
Invariante inegociável (spec): evidência ``inferred`` NUNCA é
|
|
153
|
+
promovida a ``resolved``.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
snapshot_id: int
|
|
157
|
+
relation_type: str
|
|
158
|
+
resolver: str
|
|
159
|
+
confidence_class: ConfidenceClass
|
|
160
|
+
resolution_status: ResolutionStatus
|
|
161
|
+
id: int | None = None
|
|
162
|
+
from_entity_occ_id: int | None = None
|
|
163
|
+
from_symbol_occ_id: int | None = None
|
|
164
|
+
to_entity_occ_id: int | None = None
|
|
165
|
+
to_symbol_occ_id: int | None = None
|
|
166
|
+
to_unresolved: str | None = None
|
|
167
|
+
evidence_ids: tuple[int, ...] = ()
|
|
168
|
+
|
|
169
|
+
def __post_init__(self) -> None:
|
|
170
|
+
t = str(self.relation_type)
|
|
171
|
+
if t not in DOMAIN_RELATION_TYPES:
|
|
172
|
+
raise ValueError(f"invalid relation_type: {self.relation_type!r}")
|
|
173
|
+
object.__setattr__(self, "relation_type", t)
|
|
174
|
+
object.__setattr__(
|
|
175
|
+
self,
|
|
176
|
+
"confidence_class",
|
|
177
|
+
_coerce(ConfidenceClass, self.confidence_class, "confidence_class"),
|
|
178
|
+
)
|
|
179
|
+
object.__setattr__(
|
|
180
|
+
self,
|
|
181
|
+
"resolution_status",
|
|
182
|
+
_coerce(ResolutionStatus, self.resolution_status, "resolution_status"),
|
|
183
|
+
)
|
|
184
|
+
if self.from_entity_occ_id is None and self.from_symbol_occ_id is None:
|
|
185
|
+
raise ValueError("domain relation requires a from endpoint")
|
|
186
|
+
if (
|
|
187
|
+
self.to_entity_occ_id is None
|
|
188
|
+
and self.to_symbol_occ_id is None
|
|
189
|
+
and not self.to_unresolved
|
|
190
|
+
):
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"domain relation requires to endpoint or to_unresolved"
|
|
193
|
+
)
|
|
194
|
+
if not self.resolver:
|
|
195
|
+
raise ValueError("domain relation requires resolver provenance")
|
|
196
|
+
if (
|
|
197
|
+
self.confidence_class is ConfidenceClass.INFERRED
|
|
198
|
+
and self.resolution_status is ResolutionStatus.RESOLVED
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"inferred evidence can never be reported resolved"
|
|
202
|
+
)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Registro de adaptadores de domínio (lf-0110, Cycle 7).
|
|
2
|
+
|
|
3
|
+
**Registro explícito** (mesmo desenho de ``ResolverRegistry``, ADR-0010):
|
|
4
|
+
nenhum adapter é ativado por padrão, nenhum código de terceiros é
|
|
5
|
+
carregado por varredura automática. ``discover_entrypoints`` existe como
|
|
6
|
+
mecanismo **opt-in** — só roda quando o caller invoca explicitamente.
|
|
7
|
+
|
|
8
|
+
Semântica honesta de ausência: domínio não registrado NÃO é erro de
|
|
9
|
+
extração — adapters são opcionais (spec). ``get`` devolve ``None``;
|
|
10
|
+
``require`` levanta exceção tipada para quem precisa do domínio.
|
|
11
|
+
Adapters desabilitados produzem **nada**: ``enabled()`` os exclui e
|
|
12
|
+
``require`` recusa.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from code_atlas.domains.contracts import DomainAdapter
|
|
22
|
+
|
|
23
|
+
ENTRYPOINT_GROUP = "code_atlas.domain_adapters"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AdapterRegistryError(Exception):
|
|
27
|
+
"""Falha de registro/lookup de adapter de domínio."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnknownDomainError(AdapterRegistryError):
|
|
31
|
+
"""Domínio requerido não está registrado."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AdapterDisabledError(AdapterRegistryError):
|
|
35
|
+
"""Domínio registrado mas desabilitado — não produz extração."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DuplicateAdapterError(AdapterRegistryError):
|
|
39
|
+
"""Par (domain_id, adapter_version) já registrado."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _version_key(version: str) -> tuple[tuple[int, ...], str]:
|
|
43
|
+
"""Chave de ordenação semver-ish: segmentos numéricos + tiebreak lexical."""
|
|
44
|
+
nums = tuple(int(p) for p in re.findall(r"\d+", version))
|
|
45
|
+
return (nums, version)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_adapter(adapter: DomainAdapter) -> None:
|
|
49
|
+
"""Validação mínima do contrato no momento do registro."""
|
|
50
|
+
if not isinstance(adapter, DomainAdapter):
|
|
51
|
+
raise AdapterRegistryError(
|
|
52
|
+
f"object does not satisfy DomainAdapter: {adapter!r}"
|
|
53
|
+
)
|
|
54
|
+
if not adapter.domain_id.strip():
|
|
55
|
+
raise AdapterRegistryError("adapter requires non-empty domain_id")
|
|
56
|
+
if not adapter.adapter_version.strip():
|
|
57
|
+
raise AdapterRegistryError(
|
|
58
|
+
f"adapter {adapter.domain_id!r} requires non-empty version"
|
|
59
|
+
)
|
|
60
|
+
adapter.entity_kinds() # materializa — valida early
|
|
61
|
+
adapter.relation_kinds()
|
|
62
|
+
adapter.confidence_model()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DomainRegistry:
|
|
66
|
+
"""Registro explícito de adapters de domínio com versionamento.
|
|
67
|
+
|
|
68
|
+
``register`` nunca substitui silenciosamente: mesmo ``domain_id`` com
|
|
69
|
+
versões distintas convivem (rastreabilidade de upgrade); mesma versão
|
|
70
|
+
registrada duas vezes é ``DuplicateAdapterError``.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self._adapters: dict[str, dict[str, DomainAdapter]] = {}
|
|
75
|
+
self._disabled: set[str] = set()
|
|
76
|
+
self._entrypoint_errors: list[str] = []
|
|
77
|
+
|
|
78
|
+
# --- registro ----------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def register(self, adapter: DomainAdapter, *, enabled: bool = True) -> None:
|
|
81
|
+
"""Registra ``adapter``; ``enabled=False`` registra desabilitado."""
|
|
82
|
+
_check_adapter(adapter)
|
|
83
|
+
versions = self._adapters.setdefault(adapter.domain_id, {})
|
|
84
|
+
if adapter.adapter_version in versions:
|
|
85
|
+
raise DuplicateAdapterError(
|
|
86
|
+
f"adapter already registered: "
|
|
87
|
+
f"{adapter.domain_id}@{adapter.adapter_version}"
|
|
88
|
+
)
|
|
89
|
+
versions[adapter.adapter_version] = adapter
|
|
90
|
+
if not enabled:
|
|
91
|
+
self._disabled.add(adapter.domain_id)
|
|
92
|
+
|
|
93
|
+
def unregister(self, domain_id: str, version: str | None = None) -> bool:
|
|
94
|
+
"""Remove ``version`` (ou todas quando ``None``). True se removeu."""
|
|
95
|
+
versions = self._adapters.get(domain_id)
|
|
96
|
+
if not versions:
|
|
97
|
+
return False
|
|
98
|
+
if version is None:
|
|
99
|
+
del self._adapters[domain_id]
|
|
100
|
+
self._disabled.discard(domain_id)
|
|
101
|
+
return True
|
|
102
|
+
if version not in versions:
|
|
103
|
+
return False
|
|
104
|
+
del versions[version]
|
|
105
|
+
if not versions:
|
|
106
|
+
del self._adapters[domain_id]
|
|
107
|
+
self._disabled.discard(domain_id)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def discover_entrypoints(
|
|
111
|
+
self, group: str = ENTRYPOINT_GROUP
|
|
112
|
+
) -> int:
|
|
113
|
+
"""Carrega adapters declarados via ``entry_points`` instalados.
|
|
114
|
+
|
|
115
|
+
**Opt-in explícito** — nunca chamado automaticamente (ADR-0010):
|
|
116
|
+
importar código de terceiros é decisão do caller, não do
|
|
117
|
+
registry. Cada entry point deve resolver para um
|
|
118
|
+
``DomainAdapter`` ou uma fábrica ``() -> DomainAdapter``.
|
|
119
|
+
Falhas são registradas em ``entrypoint_errors`` e não abortam
|
|
120
|
+
os demais. Retorna quantos adapters foram registrados.
|
|
121
|
+
"""
|
|
122
|
+
loaded = 0
|
|
123
|
+
for ep in importlib.metadata.entry_points(group=group):
|
|
124
|
+
try:
|
|
125
|
+
target = ep.load()
|
|
126
|
+
adapter: DomainAdapter = (
|
|
127
|
+
target() if callable(target) else target
|
|
128
|
+
)
|
|
129
|
+
self.register(adapter)
|
|
130
|
+
except Exception as exc: # diagnóstico honesto, não crash
|
|
131
|
+
self._entrypoint_errors.append(f"{ep.name}: {exc}")
|
|
132
|
+
continue
|
|
133
|
+
loaded += 1
|
|
134
|
+
return loaded
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def entrypoint_errors(self) -> tuple[str, ...]:
|
|
138
|
+
return tuple(self._entrypoint_errors)
|
|
139
|
+
|
|
140
|
+
# --- enable / disable --------------------------------------------
|
|
141
|
+
|
|
142
|
+
def enable(self, domain_id: str) -> None:
|
|
143
|
+
if domain_id not in self._adapters:
|
|
144
|
+
raise UnknownDomainError(f"domain not registered: {domain_id!r}")
|
|
145
|
+
self._disabled.discard(domain_id)
|
|
146
|
+
|
|
147
|
+
def disable(self, domain_id: str) -> None:
|
|
148
|
+
if domain_id not in self._adapters:
|
|
149
|
+
raise UnknownDomainError(f"domain not registered: {domain_id!r}")
|
|
150
|
+
self._disabled.add(domain_id)
|
|
151
|
+
|
|
152
|
+
def is_enabled(self, domain_id: str) -> bool:
|
|
153
|
+
return (
|
|
154
|
+
domain_id in self._adapters and domain_id not in self._disabled
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# --- lookup --------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def _pick(self, domain_id: str, version: str | None) -> DomainAdapter | None:
|
|
160
|
+
versions = self._adapters.get(domain_id)
|
|
161
|
+
if not versions:
|
|
162
|
+
return None
|
|
163
|
+
if version is not None:
|
|
164
|
+
return versions.get(version)
|
|
165
|
+
if len(versions) == 1:
|
|
166
|
+
return next(iter(versions.values()))
|
|
167
|
+
# versão mais alta por ordenação semver-ish — determinístico
|
|
168
|
+
return versions[max(versions, key=_version_key)]
|
|
169
|
+
|
|
170
|
+
def get(
|
|
171
|
+
self, domain_id: str, version: str | None = None
|
|
172
|
+
) -> DomainAdapter | None:
|
|
173
|
+
"""Adapter registrado (habilitado ou não); ``None`` se ausente.
|
|
174
|
+
|
|
175
|
+
``version=None`` retorna a maior versão registrada.
|
|
176
|
+
"""
|
|
177
|
+
return self._pick(domain_id, version)
|
|
178
|
+
|
|
179
|
+
def require(
|
|
180
|
+
self, domain_id: str, version: str | None = None
|
|
181
|
+
) -> DomainAdapter:
|
|
182
|
+
"""Adapter **habilitado** ou exceção tipada e honesta.
|
|
183
|
+
|
|
184
|
+
- não registrado → ``UnknownDomainError``;
|
|
185
|
+
- registrado mas desabilitado → ``AdapterDisabledError``;
|
|
186
|
+
- versão inexistente → ``UnknownDomainError`` com detalhe.
|
|
187
|
+
"""
|
|
188
|
+
versions = self._adapters.get(domain_id)
|
|
189
|
+
if not versions:
|
|
190
|
+
raise UnknownDomainError(f"domain not registered: {domain_id!r}")
|
|
191
|
+
if domain_id in self._disabled:
|
|
192
|
+
raise AdapterDisabledError(f"domain disabled: {domain_id!r}")
|
|
193
|
+
adapter = self._pick(domain_id, version)
|
|
194
|
+
if adapter is None:
|
|
195
|
+
raise UnknownDomainError(
|
|
196
|
+
f"version not registered: {domain_id!r}@{version!r}"
|
|
197
|
+
)
|
|
198
|
+
return adapter
|
|
199
|
+
|
|
200
|
+
# --- inventário ------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def versions(self, domain_id: str) -> tuple[str, ...]:
|
|
203
|
+
"""Versões registradas do domínio, ordenadas."""
|
|
204
|
+
versions = self._adapters.get(domain_id, {})
|
|
205
|
+
return tuple(sorted(versions, key=_version_key))
|
|
206
|
+
|
|
207
|
+
def domains(self) -> tuple[str, ...]:
|
|
208
|
+
return tuple(sorted(self._adapters))
|
|
209
|
+
|
|
210
|
+
def adapters(self) -> tuple[DomainAdapter, ...]:
|
|
211
|
+
"""Todos os adapters registrados (habilitados e desabilitados)."""
|
|
212
|
+
return tuple(
|
|
213
|
+
a for versions in self._adapters.values() for a in versions.values()
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def enabled(self) -> tuple[DomainAdapter, ...]:
|
|
217
|
+
"""Adapters habilitados — maior versão por domínio.
|
|
218
|
+
|
|
219
|
+
É o conjunto de extração: domínio desabilitado ou não registrado
|
|
220
|
+
produz exatamente zero entidades/relações.
|
|
221
|
+
"""
|
|
222
|
+
out: list[DomainAdapter] = []
|
|
223
|
+
for domain_id in sorted(self._adapters):
|
|
224
|
+
if domain_id in self._disabled:
|
|
225
|
+
continue
|
|
226
|
+
picked = self._pick(domain_id, None)
|
|
227
|
+
if picked is not None:
|
|
228
|
+
out.append(picked)
|
|
229
|
+
return tuple(out)
|
|
230
|
+
|
|
231
|
+
def discovery_candidates(self, paths: list[str]) -> dict[str, list[str]]:
|
|
232
|
+
"""Mapa ``domain_id → paths`` suportados pelos adapters habilitados.
|
|
233
|
+
|
|
234
|
+
Filtro puro por ``ArtifactSupport`` — não executa adapters.
|
|
235
|
+
"""
|
|
236
|
+
out: dict[str, list[str]] = {}
|
|
237
|
+
for adapter in self.enabled():
|
|
238
|
+
support = adapter.supported_artifacts()
|
|
239
|
+
out[adapter.domain_id] = [p for p in paths if support.matches(p)]
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def default_registry(
|
|
244
|
+
factories: tuple[Callable[[], DomainAdapter], ...] = (),
|
|
245
|
+
) -> DomainRegistry:
|
|
246
|
+
"""Constrói um registry populado por fábricas explícitas do caller."""
|
|
247
|
+
reg = DomainRegistry()
|
|
248
|
+
for f in factories:
|
|
249
|
+
reg.register(f())
|
|
250
|
+
return reg
|