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,30 @@
|
|
|
1
|
+
"""Adapter de domínio SQL (lf-0111, Cycle 7 Phase 3).
|
|
2
|
+
|
|
3
|
+
Linhagem estática ANSI/Athena(Trino)/Spark em stdlib puro — tokenizer
|
|
4
|
+
(``tokens``) + parser recursivo-descendente (``parser``) + extração para
|
|
5
|
+
o grafo unificado de domínio (``extract``). Nenhuma execução de SQL,
|
|
6
|
+
nenhuma conexão externa, nenhuma dependência nova.
|
|
7
|
+
|
|
8
|
+
Estados honestos: ``SELECT *`` limita linhagem de coluna declaradamente;
|
|
9
|
+
aliases/expressões não-resolvíveis viram ``ambiguous``/``unresolved`` —
|
|
10
|
+
nunca ``resolved``; ``${}``/``{{}}``/``EXEC`` são ``dynamic`` —
|
|
11
|
+
diagnosticados, nunca fabricados.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from code_atlas.domains.sql.adapter import (
|
|
15
|
+
ADAPTER_VERSION,
|
|
16
|
+
DOMAIN_ID,
|
|
17
|
+
SqlDomainAdapter,
|
|
18
|
+
looks_like_sql,
|
|
19
|
+
)
|
|
20
|
+
from code_atlas.domains.sql.parser import ParsedFile, detect_dialect, parse_file
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ADAPTER_VERSION",
|
|
24
|
+
"DOMAIN_ID",
|
|
25
|
+
"ParsedFile",
|
|
26
|
+
"SqlDomainAdapter",
|
|
27
|
+
"detect_dialect",
|
|
28
|
+
"looks_like_sql",
|
|
29
|
+
"parse_file",
|
|
30
|
+
]
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""``SqlDomainAdapter`` — adapter de domínio SQL (lf-0111, Cycle 7 Ph.3).
|
|
2
|
+
|
|
3
|
+
Linhagem estática ANSI/Athena(Trino)/Spark sobre artefatos ``.sql``:
|
|
4
|
+
|
|
5
|
+
- tokenizer + parser stdlib puros (``tokens.py``/``parser.py``) —
|
|
6
|
+
**zero dependências**, nenhum ``sqlparse``;
|
|
7
|
+
- extração por statement em ``extract.py``: ``DataTable``/``DataView``
|
|
8
|
+
com identidade catalog/schema/name-qualificada, ``data_column`` para
|
|
9
|
+
linhagem de coluna e ``data_transformation`` (1 por statement) como
|
|
10
|
+
âncora honesta de ``READS_FROM``/``WRITES_TO``/``TRANSFORMS``;
|
|
11
|
+
- ``DERIVES_FROM`` tabela→tabela colapsa CTEs/subqueries/views;
|
|
12
|
+
``DERIVES_FROM`` coluna→coluna só quando a expressão resolve
|
|
13
|
+
honestamente (alias único, sem star, sem fonte opaca);
|
|
14
|
+
- estados honestos: ``SELECT *``, aliases não-resolvíveis, SQL
|
|
15
|
+
dinâmico (``${}``/``{{}}``/``EXEC``) e fontes opacas viram
|
|
16
|
+
``DomainDiagnostic`` ou ``to_unresolved`` — nunca ``resolved``;
|
|
17
|
+
- dialeto é **rótulo** de discovery (``detect_dialect``), declarado em
|
|
18
|
+
``discovery_rules`` — não é gate de extração.
|
|
19
|
+
|
|
20
|
+
Leitura de artefatos (mesmo desenho de ``ApiContractAdapter``/
|
|
21
|
+
``TerraformAdapter``): ``base_path`` resolve os paths relativos de
|
|
22
|
+
``DiscoveredArtifact``; ``None`` trata os paths como estão. O adapter
|
|
23
|
+
nunca lê fora do request, nunca executa SQL/jobs e nunca abre rede.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import re
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from code_atlas.contracts.enums import ConfidenceClass
|
|
33
|
+
from code_atlas.discovery.scanner import (
|
|
34
|
+
artifact_path_confined,
|
|
35
|
+
read_file_verified,
|
|
36
|
+
)
|
|
37
|
+
from code_atlas.domains.contracts import (
|
|
38
|
+
ArtifactSupport,
|
|
39
|
+
ConfidenceModel,
|
|
40
|
+
DiscoveryRule,
|
|
41
|
+
DomainDiagnostic,
|
|
42
|
+
DomainExtractionRequest,
|
|
43
|
+
DomainExtractionResult,
|
|
44
|
+
ExtractedDomainEntity,
|
|
45
|
+
ExtractedDomainRelation,
|
|
46
|
+
)
|
|
47
|
+
from code_atlas.domains.model import DomainEntityKind, DomainRelationKind
|
|
48
|
+
from code_atlas.domains.sql.extract import FileExtraction, extract_parsed
|
|
49
|
+
from code_atlas.domains.sql.parser import parse_file
|
|
50
|
+
from code_atlas.parsers.contracts import EvidenceRecord
|
|
51
|
+
|
|
52
|
+
DOMAIN_ID = "sql"
|
|
53
|
+
ADAPTER_VERSION = "0.1.0"
|
|
54
|
+
|
|
55
|
+
_MAX_ARTIFACT = 4 << 20 # 4 MiB — mesmo guarda-rail do adapter api
|
|
56
|
+
|
|
57
|
+
# Content-sniffing honesto: um artefato listado no request cuja extensão
|
|
58
|
+
# não é ``.sql`` só é extraído quando o conteúdo realmente parece SQL.
|
|
59
|
+
# Extensão ``.sql`` sem conteúdo SQL vira diagnóstico — nunca claim falso.
|
|
60
|
+
_SNIFF_RE = re.compile(
|
|
61
|
+
r"^\s*(?:--[^\n]*\n|/\*.*?\*/\s*)*"
|
|
62
|
+
r"(?:WITH|SELECT|INSERT|UPDATE|DELETE|MERGE|CREATE|USE|TRUNCATE|"
|
|
63
|
+
r"DROP|ALTER|PREPARE|EXECUTE|EXPLAIN|GRANT)\b",
|
|
64
|
+
re.IGNORECASE | re.DOTALL,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def looks_like_sql(text: str) -> bool:
|
|
69
|
+
"""Heurística de sniffing — keyword inicial após comentários."""
|
|
70
|
+
return bool(_SNIFF_RE.match(text[:8192]))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SqlDomainAdapter:
|
|
74
|
+
"""Adapter ``sql`` — linhagem estática ANSI/Athena/Spark.
|
|
75
|
+
|
|
76
|
+
``base_path``: raiz on-disk que resolve os paths relativos de
|
|
77
|
+
``DiscoveredArtifact``; ``None`` trata os paths como estão
|
|
78
|
+
(absolutos ou relativos ao cwd do caller).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, base_path: str | Path | None = None) -> None:
|
|
82
|
+
self._base = Path(base_path) if base_path is not None else None
|
|
83
|
+
|
|
84
|
+
# -- contrato ------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def domain_id(self) -> str:
|
|
88
|
+
return DOMAIN_ID
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def adapter_version(self) -> str:
|
|
92
|
+
return ADAPTER_VERSION
|
|
93
|
+
|
|
94
|
+
def supported_artifacts(self) -> ArtifactSupport:
|
|
95
|
+
return ArtifactSupport(
|
|
96
|
+
extensions=frozenset({".sql"}),
|
|
97
|
+
globs=("*.sql.j2",),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def entity_kinds(self) -> frozenset[DomainEntityKind]:
|
|
101
|
+
return frozenset(
|
|
102
|
+
{
|
|
103
|
+
DomainEntityKind.DATA_TABLE,
|
|
104
|
+
DomainEntityKind.DATA_VIEW,
|
|
105
|
+
DomainEntityKind.DATA_COLUMN,
|
|
106
|
+
DomainEntityKind.DATA_TRANSFORMATION,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def relation_kinds(self) -> frozenset[str]:
|
|
111
|
+
return frozenset(
|
|
112
|
+
{
|
|
113
|
+
str(DomainRelationKind.READS_FROM),
|
|
114
|
+
str(DomainRelationKind.WRITES_TO),
|
|
115
|
+
str(DomainRelationKind.TRANSFORMS),
|
|
116
|
+
str(DomainRelationKind.DERIVES_FROM),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def confidence_model(self) -> ConfidenceModel:
|
|
121
|
+
# Extração estritamente evidence-first: o que o texto SQL não
|
|
122
|
+
# estabelece sai como diagnóstico/unresolved — nunca promovido.
|
|
123
|
+
return ConfidenceModel(
|
|
124
|
+
ceiling=ConfidenceClass.SYNTACTIC,
|
|
125
|
+
default=ConfidenceClass.SYNTACTIC,
|
|
126
|
+
allows_inferred=False,
|
|
127
|
+
notes="static SQL lineage; star/ambiguous/dynamic are "
|
|
128
|
+
"diagnosed, never resolved",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def discovery_rules(self) -> tuple[DiscoveryRule, ...]:
|
|
132
|
+
return (
|
|
133
|
+
DiscoveryRule(
|
|
134
|
+
glob="*.sql",
|
|
135
|
+
artifact_kind="sql-script",
|
|
136
|
+
notes=(
|
|
137
|
+
"ANSI/Athena(Trino)/Spark SQL scripts — dialect is "
|
|
138
|
+
"content-sniffed (spark|athena|ansi label), not a gate"
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
DiscoveryRule(
|
|
142
|
+
glob="*.sql.j2",
|
|
143
|
+
artifact_kind="sql-template",
|
|
144
|
+
notes=(
|
|
145
|
+
"templated SQL — parsed best-effort; dynamic markers "
|
|
146
|
+
"produce diagnostics, never fabricated relations"
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# -- extract ---------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def extract(
|
|
154
|
+
self, request: DomainExtractionRequest
|
|
155
|
+
) -> DomainExtractionResult:
|
|
156
|
+
try:
|
|
157
|
+
return _extract(request, self.adapter_version, self._base)
|
|
158
|
+
except Exception as exc: # última linha de defesa — nunca levanta
|
|
159
|
+
return DomainExtractionResult(
|
|
160
|
+
domain_id=self.domain_id,
|
|
161
|
+
adapter_version=self.adapter_version,
|
|
162
|
+
diagnostics=(
|
|
163
|
+
DomainDiagnostic(
|
|
164
|
+
code="adapter-failure",
|
|
165
|
+
severity="error",
|
|
166
|
+
message=f"sql adapter failure: {exc!r}",
|
|
167
|
+
adapter_id=self.domain_id,
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# -- por artefato ----------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _read(
|
|
175
|
+
self, path: str, sha256: str, diags: list[DomainDiagnostic]
|
|
176
|
+
) -> tuple[str, str] | None:
|
|
177
|
+
"""Lê ``base_path/path``; devolve (texto, sha256) ou None."""
|
|
178
|
+
if self._base is not None:
|
|
179
|
+
base = self._base.resolve()
|
|
180
|
+
fs = artifact_path_confined(path, base)
|
|
181
|
+
if fs is None:
|
|
182
|
+
diags.append(
|
|
183
|
+
DomainDiagnostic(
|
|
184
|
+
code="artifact-outside-root", severity="error",
|
|
185
|
+
message=f"artifact path escapes repository root: {path}",
|
|
186
|
+
path=path, adapter_id=DOMAIN_ID,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return None
|
|
190
|
+
raw = read_file_verified(fs, base)
|
|
191
|
+
if raw is None:
|
|
192
|
+
diags.append(
|
|
193
|
+
DomainDiagnostic(
|
|
194
|
+
code="unreadable-artifact", severity="error",
|
|
195
|
+
message="cannot read artifact: unsafe or vanished",
|
|
196
|
+
path=path, adapter_id=DOMAIN_ID,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return None
|
|
200
|
+
else:
|
|
201
|
+
fs = Path(path)
|
|
202
|
+
try:
|
|
203
|
+
raw = fs.read_bytes()
|
|
204
|
+
except OSError as exc:
|
|
205
|
+
diags.append(
|
|
206
|
+
DomainDiagnostic(
|
|
207
|
+
code="unreadable-artifact", severity="error",
|
|
208
|
+
message=f"cannot read artifact: {exc}",
|
|
209
|
+
path=path, adapter_id=DOMAIN_ID,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
if len(raw) > _MAX_ARTIFACT:
|
|
214
|
+
diags.append(
|
|
215
|
+
DomainDiagnostic(
|
|
216
|
+
code="artifact-too-large", severity="error",
|
|
217
|
+
message=f"{len(raw)} bytes exceeds {_MAX_ARTIFACT}",
|
|
218
|
+
path=path, adapter_id=DOMAIN_ID,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
return None
|
|
222
|
+
real = hashlib.sha256(raw).hexdigest()
|
|
223
|
+
if sha256 and sha256 != real:
|
|
224
|
+
diags.append(
|
|
225
|
+
DomainDiagnostic(
|
|
226
|
+
code="artifact-hash-mismatch", severity="warning",
|
|
227
|
+
message=(
|
|
228
|
+
f"inventory sha256 {sha256[:12]}… != "
|
|
229
|
+
f"computed {real[:12]}…"
|
|
230
|
+
),
|
|
231
|
+
path=path, adapter_id=DOMAIN_ID,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return raw.decode("utf-8", "replace"), real
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _extract(
|
|
238
|
+
request: DomainExtractionRequest,
|
|
239
|
+
adapter_version: str,
|
|
240
|
+
base: Path | None,
|
|
241
|
+
) -> DomainExtractionResult:
|
|
242
|
+
"""Extrai o request inteiro — merge determinístico por artefato."""
|
|
243
|
+
entities: dict[str, ExtractedDomainEntity] = {}
|
|
244
|
+
relations: list[ExtractedDomainRelation] = []
|
|
245
|
+
evidence: list[EvidenceRecord] = []
|
|
246
|
+
diagnostics: list[DomainDiagnostic] = []
|
|
247
|
+
stats: dict[str, int] = {}
|
|
248
|
+
|
|
249
|
+
adapter = SqlDomainAdapter(base)
|
|
250
|
+
for art in sorted(request.artifacts, key=lambda a: a.path):
|
|
251
|
+
got = adapter._read(art.path, art.sha256, diagnostics)
|
|
252
|
+
if got is None:
|
|
253
|
+
continue
|
|
254
|
+
text, content_hash = got
|
|
255
|
+
ext = Path(art.path).suffix.lower()
|
|
256
|
+
if ext != ".sql" and not looks_like_sql(text):
|
|
257
|
+
diagnostics.append(
|
|
258
|
+
DomainDiagnostic(
|
|
259
|
+
code="not-sql-artifact", severity="info",
|
|
260
|
+
message="content does not look like SQL — skipped",
|
|
261
|
+
path=art.path, adapter_id=DOMAIN_ID,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
parsed = parse_file(text)
|
|
267
|
+
fx = extract_parsed(
|
|
268
|
+
parsed,
|
|
269
|
+
repository=request.repository,
|
|
270
|
+
root=request.root,
|
|
271
|
+
environment=request.environment,
|
|
272
|
+
path=art.path,
|
|
273
|
+
sha=content_hash,
|
|
274
|
+
adapter_version=adapter_version,
|
|
275
|
+
)
|
|
276
|
+
except Exception as exc: # isolamento por arquivo — nunca crash
|
|
277
|
+
diagnostics.append(
|
|
278
|
+
DomainDiagnostic(
|
|
279
|
+
code="adapter-crash", severity="error",
|
|
280
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
281
|
+
path=art.path, adapter_id=DOMAIN_ID,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
continue
|
|
285
|
+
if (
|
|
286
|
+
not fx.entities and not fx.relations
|
|
287
|
+
and not parsed.statements and not looks_like_sql(text)
|
|
288
|
+
):
|
|
289
|
+
diagnostics.append(
|
|
290
|
+
DomainDiagnostic(
|
|
291
|
+
code="not-sql-artifact", severity="info",
|
|
292
|
+
message=".sql artifact with no SQL statements",
|
|
293
|
+
path=art.path, adapter_id=DOMAIN_ID,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
continue
|
|
297
|
+
_merge_file(
|
|
298
|
+
fx, entities=entities, relations=relations,
|
|
299
|
+
evidence=evidence, diagnostics=diagnostics, stats=stats,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return DomainExtractionResult(
|
|
303
|
+
domain_id=DOMAIN_ID,
|
|
304
|
+
adapter_version=adapter_version,
|
|
305
|
+
entities=tuple(entities.values()),
|
|
306
|
+
relations=tuple(relations),
|
|
307
|
+
evidence=tuple(evidence),
|
|
308
|
+
diagnostics=tuple(diagnostics),
|
|
309
|
+
stats=stats,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _merge_file(
|
|
314
|
+
fx: FileExtraction,
|
|
315
|
+
*,
|
|
316
|
+
entities: dict[str, ExtractedDomainEntity],
|
|
317
|
+
relations: list[ExtractedDomainRelation],
|
|
318
|
+
evidence: list[EvidenceRecord],
|
|
319
|
+
diagnostics: list[DomainDiagnostic],
|
|
320
|
+
stats: dict[str, int],
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Funde a extração de um artefato no resultado do request.
|
|
323
|
+
|
|
324
|
+
Índices de evidência são relativos ao arquivo — remapeados para o
|
|
325
|
+
envelope global. Entidades dedupam por ``logical_key``; declaração
|
|
326
|
+
(``declared=create``) vence referência no merge de atributos e a
|
|
327
|
+
evidência acumula — mesma política do dedup intra-arquivo.
|
|
328
|
+
"""
|
|
329
|
+
base = len(evidence)
|
|
330
|
+
evidence.extend(fx.evidence)
|
|
331
|
+
|
|
332
|
+
def remap(evi: tuple[int, ...]) -> tuple[int, ...]:
|
|
333
|
+
return tuple(i + base for i in evi)
|
|
334
|
+
|
|
335
|
+
for e in fx.entities:
|
|
336
|
+
key = e.identity.logical_key()
|
|
337
|
+
prev = entities.get(key)
|
|
338
|
+
if prev is None:
|
|
339
|
+
entities[key] = ExtractedDomainEntity(
|
|
340
|
+
identity=e.identity, kind=e.kind, name=e.name,
|
|
341
|
+
path=e.path, span=e.span, content_hash=e.content_hash,
|
|
342
|
+
attributes=e.attributes, evidence=remap(e.evidence),
|
|
343
|
+
)
|
|
344
|
+
continue
|
|
345
|
+
merged_ev = tuple(sorted({*prev.evidence, *remap(e.evidence)}))
|
|
346
|
+
declared_wins = (
|
|
347
|
+
e.attributes.get("declared") == "create"
|
|
348
|
+
and prev.attributes.get("declared") != "create"
|
|
349
|
+
)
|
|
350
|
+
if declared_wins:
|
|
351
|
+
entities[key] = ExtractedDomainEntity(
|
|
352
|
+
identity=e.identity, kind=e.kind, name=e.name,
|
|
353
|
+
path=e.path, span=e.span, content_hash=e.content_hash,
|
|
354
|
+
attributes=e.attributes, evidence=merged_ev,
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
entities[key] = ExtractedDomainEntity(
|
|
358
|
+
identity=prev.identity, kind=prev.kind, name=prev.name,
|
|
359
|
+
path=prev.path, span=prev.span,
|
|
360
|
+
content_hash=prev.content_hash,
|
|
361
|
+
attributes=prev.attributes, evidence=merged_ev,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
relations.extend(
|
|
365
|
+
ExtractedDomainRelation(
|
|
366
|
+
relation_type=r.relation_type, path=r.path,
|
|
367
|
+
from_identity=r.from_identity, from_symbol=r.from_symbol,
|
|
368
|
+
to_identity=r.to_identity, to_symbol=r.to_symbol,
|
|
369
|
+
to_unresolved=r.to_unresolved, resolution=r.resolution,
|
|
370
|
+
span=r.span, evidence=remap(r.evidence),
|
|
371
|
+
)
|
|
372
|
+
for r in fx.relations
|
|
373
|
+
)
|
|
374
|
+
diagnostics.extend(fx.diagnostics)
|
|
375
|
+
for k, v in fx.stats.items():
|
|
376
|
+
stats[k] = stats.get(k, 0) + v
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def satisfies_protocol() -> SqlDomainAdapter:
|
|
380
|
+
"""Smoke check para tests: a instância satisfaz ``DomainAdapter``."""
|
|
381
|
+
adapter: SqlDomainAdapter = SqlDomainAdapter()
|
|
382
|
+
return adapter
|