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,107 @@
|
|
|
1
|
+
"""Federação de domínios (Cycle 7): adapters, identidade canônica,
|
|
2
|
+
modelo unificado de entidades/relações e registro explícito.
|
|
3
|
+
|
|
4
|
+
Camada separada do grafo de símbolos: ``domains`` observa artefatos
|
|
5
|
+
não-código (SQL, Terraform, OpenAPI, CI/CD) com o mesmo rigor de
|
|
6
|
+
evidência e snapshots do restante do Atlas (ADR-0010).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from code_atlas.domains.contracts import (
|
|
10
|
+
DOMAIN_CONTRACT_VERSION,
|
|
11
|
+
ArtifactSupport,
|
|
12
|
+
ConfidenceModel,
|
|
13
|
+
DiscoveredArtifact,
|
|
14
|
+
DiscoveryRule,
|
|
15
|
+
DomainAdapter,
|
|
16
|
+
DomainDiagnostic,
|
|
17
|
+
DomainExtractionRequest,
|
|
18
|
+
DomainExtractionResult,
|
|
19
|
+
ExtractedDomainEntity,
|
|
20
|
+
ExtractedDomainRelation,
|
|
21
|
+
confidence_rank,
|
|
22
|
+
)
|
|
23
|
+
from code_atlas.domains.crossresolve import (
|
|
24
|
+
CROSS_RESOLVER_ID,
|
|
25
|
+
CROSS_RESOLVER_VERSION,
|
|
26
|
+
CrossDiagnostic,
|
|
27
|
+
CrossDomainResolver,
|
|
28
|
+
CrossEdge,
|
|
29
|
+
CrossResolutionPlan,
|
|
30
|
+
CrossUpdate,
|
|
31
|
+
)
|
|
32
|
+
from code_atlas.domains.evidence import (
|
|
33
|
+
DOMAIN_EXTRACTOR_PREFIX,
|
|
34
|
+
DomainEvidence,
|
|
35
|
+
domain_extractor_id,
|
|
36
|
+
evidence_record,
|
|
37
|
+
)
|
|
38
|
+
from code_atlas.domains.identity import DomainIdentity
|
|
39
|
+
from code_atlas.domains.model import (
|
|
40
|
+
DOMAIN_RELATION_TYPES,
|
|
41
|
+
DomainEntity,
|
|
42
|
+
DomainEntityKind,
|
|
43
|
+
DomainEntityOccurrence,
|
|
44
|
+
DomainRelation,
|
|
45
|
+
DomainRelationKind,
|
|
46
|
+
)
|
|
47
|
+
from code_atlas.domains.registry import (
|
|
48
|
+
ENTRYPOINT_GROUP,
|
|
49
|
+
AdapterDisabledError,
|
|
50
|
+
AdapterRegistryError,
|
|
51
|
+
DomainRegistry,
|
|
52
|
+
DuplicateAdapterError,
|
|
53
|
+
UnknownDomainError,
|
|
54
|
+
default_registry,
|
|
55
|
+
)
|
|
56
|
+
from code_atlas.domains.resolution import DomainResolution
|
|
57
|
+
from code_atlas.domains.summarize import (
|
|
58
|
+
SUMMARIZE_VERSION,
|
|
59
|
+
LineageFact,
|
|
60
|
+
summarize_lineage,
|
|
61
|
+
)
|
|
62
|
+
from code_atlas.domains.validation import validate_extraction
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"CROSS_RESOLVER_ID",
|
|
66
|
+
"CROSS_RESOLVER_VERSION",
|
|
67
|
+
"DOMAIN_CONTRACT_VERSION",
|
|
68
|
+
"DOMAIN_EXTRACTOR_PREFIX",
|
|
69
|
+
"DOMAIN_RELATION_TYPES",
|
|
70
|
+
"ENTRYPOINT_GROUP",
|
|
71
|
+
"SUMMARIZE_VERSION",
|
|
72
|
+
"AdapterDisabledError",
|
|
73
|
+
"AdapterRegistryError",
|
|
74
|
+
"ArtifactSupport",
|
|
75
|
+
"ConfidenceModel",
|
|
76
|
+
"CrossDiagnostic",
|
|
77
|
+
"CrossDomainResolver",
|
|
78
|
+
"CrossEdge",
|
|
79
|
+
"CrossResolutionPlan",
|
|
80
|
+
"CrossUpdate",
|
|
81
|
+
"DiscoveredArtifact",
|
|
82
|
+
"DiscoveryRule",
|
|
83
|
+
"DomainAdapter",
|
|
84
|
+
"DomainDiagnostic",
|
|
85
|
+
"DomainEntity",
|
|
86
|
+
"DomainEntityKind",
|
|
87
|
+
"DomainEntityOccurrence",
|
|
88
|
+
"DomainEvidence",
|
|
89
|
+
"DomainExtractionRequest",
|
|
90
|
+
"DomainExtractionResult",
|
|
91
|
+
"DomainIdentity",
|
|
92
|
+
"DomainRegistry",
|
|
93
|
+
"DomainRelation",
|
|
94
|
+
"DomainRelationKind",
|
|
95
|
+
"DomainResolution",
|
|
96
|
+
"DuplicateAdapterError",
|
|
97
|
+
"ExtractedDomainEntity",
|
|
98
|
+
"ExtractedDomainRelation",
|
|
99
|
+
"LineageFact",
|
|
100
|
+
"UnknownDomainError",
|
|
101
|
+
"confidence_rank",
|
|
102
|
+
"default_registry",
|
|
103
|
+
"domain_extractor_id",
|
|
104
|
+
"evidence_record",
|
|
105
|
+
"summarize_lineage",
|
|
106
|
+
"validate_extraction",
|
|
107
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Adapter de domínio ``api`` (lf-0114, Cycle 7 Phase 6).
|
|
2
|
+
|
|
3
|
+
Extrai ``APIContract``/``APIService``/``APIEndpoint`` de specs
|
|
4
|
+
OpenAPI/Swagger e de rotas declaradas em código (FastAPI, Express),
|
|
5
|
+
mais relações ``CALLS_ENDPOINT`` de consumidores HTTP literais.
|
|
6
|
+
|
|
7
|
+
Fronteiras preservadas: nenhum import de ``tree_sitter`` (AST via
|
|
8
|
+
``ParseEngine``), nenhum ``urllib`` (split de URL local), nenhuma
|
|
9
|
+
dependência nova (parser YAML de subconjunto próprio, ``json`` stdlib),
|
|
10
|
+
nenhuma execução de código alvo nem acesso a rede.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from code_atlas.domains.api.adapter import ADAPTER_VERSION, ApiContractAdapter
|
|
14
|
+
from code_atlas.domains.api.yaml_subset import (
|
|
15
|
+
YamlDocument,
|
|
16
|
+
YamlIssue,
|
|
17
|
+
YamlValue,
|
|
18
|
+
parse_yaml_subset,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ADAPTER_VERSION",
|
|
23
|
+
"ApiContractAdapter",
|
|
24
|
+
"YamlDocument",
|
|
25
|
+
"YamlIssue",
|
|
26
|
+
"YamlValue",
|
|
27
|
+
"parse_yaml_subset",
|
|
28
|
+
]
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""``ApiContractAdapter`` — adapter de domínio para APIs/contratos (lf-0114).
|
|
2
|
+
|
|
3
|
+
Phase 6 do Cycle 7. Fontes observadas (somente leitura, nunca execução):
|
|
4
|
+
|
|
5
|
+
- **OpenAPI 3.x / Swagger 2.0** (``.yaml``/``.yml``/``.json``): parser
|
|
6
|
+
YAML de subconjunto delimitado local (``yaml_subset`` — PyYAML não é
|
|
7
|
+
dependência aprovada) + ``json`` stdlib → ``APIContract`` +
|
|
8
|
+
``APIService`` + ``APIEndpoint`` (identidade ``METHOD base_path+path``)
|
|
9
|
+
+ ``REFERENCES`` para ``$ref`` de request/response.
|
|
10
|
+
- **FastAPI** (``.py``): decorators ``@app.get``/``@router.*`` extraídos
|
|
11
|
+
de artefatos AST reais via ``ParseEngine`` (Tree-sitter atrás da
|
|
12
|
+
fronteira ``parsers/``) → ``APIService``/``APIEndpoint`` +
|
|
13
|
+
``CONTAINS``/``DEFINES`` (ponte handler→endpoint) — ``syntactic``.
|
|
14
|
+
- **Express** (``.ts``/``.tsx`` via AST; ``.js``/``.mjs``/``.cjs`` via
|
|
15
|
+
regex-static por linha): literais ``app.get``/``router.post`` → mesmo
|
|
16
|
+
esquema, confiança ``heuristic`` declarada.
|
|
17
|
+
- **Consumidores**: ``requests.*``/``httpx.*``/``urlopen``/``fetch``/
|
|
18
|
+
``axios.*`` com URL literal → ``CALLS_ENDPOINT`` ``unresolved``/
|
|
19
|
+
``external`` + ``candidates`` (a resolução literal↔contrato é Phase 9);
|
|
20
|
+
URLs dinâmicas → marcador ``dynamic:`` nunca resolvido.
|
|
21
|
+
- **API Gateway em IaC**: fora de escopo — só ``REFERENCES`` triviais
|
|
22
|
+
quando visíveis diretamente (não implementado neste slice).
|
|
23
|
+
|
|
24
|
+
Leitura de artefatos: o contrato ``DomainAdapter`` proíbe ler disco fora
|
|
25
|
+
do request — o adapter lê ``workspace_root / artifact.path`` apenas para
|
|
26
|
+
artefatos listados em ``request.artifacts`` (o coordinator escolhe o
|
|
27
|
+
recorte; ``workspace_root`` é injeção do caller, default ``.``).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import hashlib
|
|
33
|
+
import json
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from code_atlas.contracts.enums import ConfidenceClass, EdgeKind
|
|
38
|
+
from code_atlas.discovery.scanner import (
|
|
39
|
+
artifact_path_confined,
|
|
40
|
+
read_file_verified,
|
|
41
|
+
)
|
|
42
|
+
from code_atlas.domains.api import common, consumers, openapi, routes
|
|
43
|
+
from code_atlas.domains.api.emit import ADAPTER_DOMAIN, Emit
|
|
44
|
+
from code_atlas.domains.api.yaml_subset import parse_yaml_subset
|
|
45
|
+
from code_atlas.domains.contracts import (
|
|
46
|
+
ArtifactSupport,
|
|
47
|
+
ConfidenceModel,
|
|
48
|
+
DiscoveryRule,
|
|
49
|
+
DomainAdapter,
|
|
50
|
+
DomainExtractionRequest,
|
|
51
|
+
DomainExtractionResult,
|
|
52
|
+
)
|
|
53
|
+
from code_atlas.domains.model import DomainEntityKind, DomainRelationKind
|
|
54
|
+
from code_atlas.parsers.contracts import ParseResult
|
|
55
|
+
from code_atlas.parsers.engine import ParseEngine
|
|
56
|
+
|
|
57
|
+
ADAPTER_VERSION = "0.1.0"
|
|
58
|
+
|
|
59
|
+
_SPEC_EXTS = frozenset({".yaml", ".yml", ".json"})
|
|
60
|
+
_PY_EXTS = frozenset({".py", ".pyi"})
|
|
61
|
+
_TS_EXTS = frozenset({".ts", ".tsx"})
|
|
62
|
+
_JS_EXTS = frozenset({".js", ".jsx", ".mjs", ".cjs"})
|
|
63
|
+
_MAX_ARTIFACT = 4 << 20 # 4 MiB — guarda contra artefatos hostis
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ApiContractAdapter:
|
|
67
|
+
"""Adapter ``api``: contratos OpenAPI + rotas/consumidores em código."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
workspace_root: str | Path = ".",
|
|
72
|
+
*,
|
|
73
|
+
engine: ParseEngine | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._root = Path(workspace_root)
|
|
76
|
+
self._engine = engine or ParseEngine()
|
|
77
|
+
|
|
78
|
+
# -- contrato ------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def domain_id(self) -> str:
|
|
82
|
+
return ADAPTER_DOMAIN
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def adapter_version(self) -> str:
|
|
86
|
+
return ADAPTER_VERSION
|
|
87
|
+
|
|
88
|
+
def supported_artifacts(self) -> ArtifactSupport:
|
|
89
|
+
return ArtifactSupport(
|
|
90
|
+
extensions=_SPEC_EXTS | _PY_EXTS | _TS_EXTS | _JS_EXTS,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def entity_kinds(self) -> frozenset[DomainEntityKind]:
|
|
94
|
+
return frozenset(
|
|
95
|
+
{
|
|
96
|
+
DomainEntityKind.API_CONTRACT,
|
|
97
|
+
DomainEntityKind.API_SERVICE,
|
|
98
|
+
DomainEntityKind.API_ENDPOINT,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def relation_kinds(self) -> frozenset[str]:
|
|
103
|
+
return frozenset(
|
|
104
|
+
{
|
|
105
|
+
str(EdgeKind.CONTAINS),
|
|
106
|
+
str(EdgeKind.DEFINES),
|
|
107
|
+
str(DomainRelationKind.CALLS_ENDPOINT),
|
|
108
|
+
str(EdgeKind.REFERENCES),
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def confidence_model(self) -> ConfidenceModel:
|
|
113
|
+
# Extração local evidence-first: teto syntactic; Express/.js
|
|
114
|
+
# declaram heuristic; nada inferred é emitido.
|
|
115
|
+
return ConfidenceModel(
|
|
116
|
+
ceiling=ConfidenceClass.SYNTACTIC,
|
|
117
|
+
default=ConfidenceClass.SYNTACTIC,
|
|
118
|
+
allows_inferred=False,
|
|
119
|
+
notes="openapi/fastapi=syntactic; express+regex=heuristic; "
|
|
120
|
+
"consumer calls literal=syntactic, dynamic=heuristic",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def discovery_rules(self) -> tuple[DiscoveryRule, ...]:
|
|
124
|
+
return (
|
|
125
|
+
DiscoveryRule(
|
|
126
|
+
glob="**/openapi.yaml", artifact_kind="openapi-spec",
|
|
127
|
+
notes="canonical OpenAPI filename",
|
|
128
|
+
),
|
|
129
|
+
DiscoveryRule(
|
|
130
|
+
glob="**/openapi.json", artifact_kind="openapi-spec",
|
|
131
|
+
notes="canonical OpenAPI filename (JSON)",
|
|
132
|
+
),
|
|
133
|
+
DiscoveryRule(
|
|
134
|
+
glob="**/*.yaml", artifact_kind="maybe-openapi",
|
|
135
|
+
notes="content-checked for openapi/swagger keys",
|
|
136
|
+
),
|
|
137
|
+
DiscoveryRule(
|
|
138
|
+
glob="**/*.json", artifact_kind="maybe-openapi",
|
|
139
|
+
notes="content-checked for openapi/swagger keys",
|
|
140
|
+
),
|
|
141
|
+
DiscoveryRule(
|
|
142
|
+
glob="**/*.py", artifact_kind="http-routes",
|
|
143
|
+
notes="FastAPI decorators + python HTTP clients",
|
|
144
|
+
),
|
|
145
|
+
DiscoveryRule(
|
|
146
|
+
glob="**/*.ts", artifact_kind="http-routes",
|
|
147
|
+
notes="Express routes + fetch/axios consumers",
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# -- extract ---------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def extract(
|
|
154
|
+
self, request: DomainExtractionRequest
|
|
155
|
+
) -> DomainExtractionResult:
|
|
156
|
+
sink = Emit(request, self.adapter_version)
|
|
157
|
+
# Dois passes: consumidores rodam depois de specs+rotas para que
|
|
158
|
+
# ``candidates`` de CALLS_ENDPOINT vejam TODOS os endpoints do
|
|
159
|
+
# request — independente da ordem alfabética dos artefatos.
|
|
160
|
+
deferred: list[Callable[[], None]] = []
|
|
161
|
+
artifacts = sorted(request.artifacts, key=lambda a: a.path)
|
|
162
|
+
for art in artifacts:
|
|
163
|
+
ext = Path(art.path).suffix.lower()
|
|
164
|
+
try:
|
|
165
|
+
self._extract_one(sink, art.path, art.sha256, ext, deferred)
|
|
166
|
+
except Exception as exc: # isolamento por arquivo — nunca crash
|
|
167
|
+
sink.diag(
|
|
168
|
+
"adapter-crash", "error",
|
|
169
|
+
f"{type(exc).__name__}: {exc}", art.path,
|
|
170
|
+
)
|
|
171
|
+
for fn in deferred:
|
|
172
|
+
try:
|
|
173
|
+
fn()
|
|
174
|
+
except Exception as exc: # mesma isolação por arquivo
|
|
175
|
+
sink.diag(
|
|
176
|
+
"adapter-crash", "error",
|
|
177
|
+
f"deferred consumer pass: {type(exc).__name__}: {exc}",
|
|
178
|
+
)
|
|
179
|
+
return sink.result()
|
|
180
|
+
|
|
181
|
+
# -- por artefato ------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def _read(self, sink: Emit, path: str, sha256: str) -> tuple[str, str] | None:
|
|
184
|
+
"""Lê ``workspace_root/path``; devolve (texto, sha256) ou None."""
|
|
185
|
+
# path do request é hostil — absoluto, ``..`` e symlink trocado
|
|
186
|
+
# nunca saem da raiz do workspace (lf-c8 F7).
|
|
187
|
+
root = self._root.resolve()
|
|
188
|
+
fs = artifact_path_confined(path, root)
|
|
189
|
+
if fs is None:
|
|
190
|
+
sink.diag(
|
|
191
|
+
"artifact-outside-root", "error",
|
|
192
|
+
f"artifact path escapes repository root: {path}", path,
|
|
193
|
+
)
|
|
194
|
+
return None
|
|
195
|
+
raw = read_file_verified(fs, root)
|
|
196
|
+
if raw is None:
|
|
197
|
+
sink.diag(
|
|
198
|
+
"unreadable-artifact", "error",
|
|
199
|
+
"cannot read artifact: unsafe or vanished", path,
|
|
200
|
+
)
|
|
201
|
+
return None
|
|
202
|
+
if len(raw) > _MAX_ARTIFACT:
|
|
203
|
+
sink.diag(
|
|
204
|
+
"artifact-too-large", "error",
|
|
205
|
+
f"{len(raw)} bytes exceeds {_MAX_ARTIFACT}", path,
|
|
206
|
+
)
|
|
207
|
+
return None
|
|
208
|
+
real = hashlib.sha256(raw).hexdigest()
|
|
209
|
+
if sha256 and sha256 != real:
|
|
210
|
+
sink.diag(
|
|
211
|
+
"artifact-hash-mismatch", "warning",
|
|
212
|
+
f"inventory sha256 {sha256[:12]}… != computed {real[:12]}…",
|
|
213
|
+
path,
|
|
214
|
+
)
|
|
215
|
+
return raw.decode("utf-8", "replace"), real
|
|
216
|
+
|
|
217
|
+
def _extract_one(
|
|
218
|
+
self,
|
|
219
|
+
sink: Emit,
|
|
220
|
+
path: str,
|
|
221
|
+
sha256: str,
|
|
222
|
+
ext: str,
|
|
223
|
+
deferred: list[Callable[[], None]],
|
|
224
|
+
) -> None:
|
|
225
|
+
got = self._read(sink, path, sha256)
|
|
226
|
+
if got is None:
|
|
227
|
+
return
|
|
228
|
+
text, content_hash = got
|
|
229
|
+
offsets = common.byte_offsets(text)
|
|
230
|
+
if ext in (".yaml", ".yml"):
|
|
231
|
+
self._extract_yaml(sink, path, text, offsets, content_hash)
|
|
232
|
+
elif ext == ".json":
|
|
233
|
+
self._extract_json(sink, path, text, offsets, content_hash)
|
|
234
|
+
elif ext in _PY_EXTS:
|
|
235
|
+
self._extract_py(sink, path, text, content_hash, deferred)
|
|
236
|
+
elif ext in _TS_EXTS:
|
|
237
|
+
self._extract_ts(sink, path, text, content_hash, deferred)
|
|
238
|
+
elif ext in _JS_EXTS:
|
|
239
|
+
self._extract_js(sink, path, text, content_hash, deferred)
|
|
240
|
+
else:
|
|
241
|
+
sink.diag(
|
|
242
|
+
"unsupported-artifact", "info",
|
|
243
|
+
f"extension {ext!r} not handled", path,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# -- specs -----------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def _extract_yaml(
|
|
249
|
+
self, sink: Emit, path: str, text: str,
|
|
250
|
+
offsets: list[int], content_hash: str,
|
|
251
|
+
) -> None:
|
|
252
|
+
doc = parse_yaml_subset(text)
|
|
253
|
+
for issue in doc.issues:
|
|
254
|
+
sink.diag(
|
|
255
|
+
issue.code, issue.severity,
|
|
256
|
+
f"line {issue.line}: {issue.message}" if issue.line
|
|
257
|
+
else issue.message,
|
|
258
|
+
path,
|
|
259
|
+
)
|
|
260
|
+
flavor = openapi.spec_flavor(doc.value)
|
|
261
|
+
if flavor is None:
|
|
262
|
+
if any(i.severity == "error" for i in doc.issues):
|
|
263
|
+
sink.diag(
|
|
264
|
+
"malformed-spec", "error",
|
|
265
|
+
"YAML did not parse into an OpenAPI/Swagger document",
|
|
266
|
+
path,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
sink.diag(
|
|
270
|
+
"not-openapi-artifact", "info",
|
|
271
|
+
"YAML is not an OpenAPI/Swagger document — skipped",
|
|
272
|
+
path,
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
openapi.extract_spec(
|
|
276
|
+
sink=sink, path=path, fmt="yaml", doc=doc.value,
|
|
277
|
+
flavor=flavor,
|
|
278
|
+
key_line=openapi.make_key_line(doc.key_lines),
|
|
279
|
+
text=text, offsets=offsets, content_hash=content_hash,
|
|
280
|
+
)
|
|
281
|
+
sink.bump("openapi_specs")
|
|
282
|
+
|
|
283
|
+
def _extract_json(
|
|
284
|
+
self, sink: Emit, path: str, text: str,
|
|
285
|
+
offsets: list[int], content_hash: str,
|
|
286
|
+
) -> None:
|
|
287
|
+
try:
|
|
288
|
+
doc = json.loads(text)
|
|
289
|
+
except json.JSONDecodeError as exc:
|
|
290
|
+
if '"openapi"' in text or '"swagger"' in text:
|
|
291
|
+
sink.diag(
|
|
292
|
+
"malformed-spec", "error",
|
|
293
|
+
f"JSON looks like a spec but does not parse: {exc}",
|
|
294
|
+
path,
|
|
295
|
+
)
|
|
296
|
+
else:
|
|
297
|
+
sink.diag(
|
|
298
|
+
"not-openapi-artifact", "info",
|
|
299
|
+
"JSON is not an OpenAPI/Swagger document — skipped",
|
|
300
|
+
path,
|
|
301
|
+
)
|
|
302
|
+
return
|
|
303
|
+
flavor = openapi.spec_flavor(doc)
|
|
304
|
+
if flavor is None:
|
|
305
|
+
sink.diag(
|
|
306
|
+
"not-openapi-artifact", "info",
|
|
307
|
+
"JSON is not an OpenAPI/Swagger document — skipped",
|
|
308
|
+
path,
|
|
309
|
+
)
|
|
310
|
+
return
|
|
311
|
+
openapi.extract_spec(
|
|
312
|
+
sink=sink, path=path, fmt="json", doc=doc,
|
|
313
|
+
flavor=flavor,
|
|
314
|
+
key_line=openapi.make_json_key_line(text),
|
|
315
|
+
text=text, offsets=offsets, content_hash=content_hash,
|
|
316
|
+
)
|
|
317
|
+
sink.bump("openapi_specs")
|
|
318
|
+
|
|
319
|
+
# -- código ------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
def _parse_result(
|
|
322
|
+
self, sink: Emit, path: str, text: str
|
|
323
|
+
) -> ParseResult | None:
|
|
324
|
+
result = self._engine.parse(path, text.encode("utf-8"))
|
|
325
|
+
for d in result.diagnostics:
|
|
326
|
+
sink.diag(d.code, d.severity, d.message, path)
|
|
327
|
+
if not result.supported:
|
|
328
|
+
return None
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
def _extract_py(
|
|
332
|
+
self,
|
|
333
|
+
sink: Emit,
|
|
334
|
+
path: str,
|
|
335
|
+
text: str,
|
|
336
|
+
content_hash: str,
|
|
337
|
+
deferred: list[Callable[[], None]],
|
|
338
|
+
) -> None:
|
|
339
|
+
result = self._parse_result(sink, path, text)
|
|
340
|
+
if result is None:
|
|
341
|
+
return
|
|
342
|
+
module = common.module_name(path, common.PY_MODULE_SUFFIXES)
|
|
343
|
+
routes.extract_py_routes(
|
|
344
|
+
sink=sink, path=path, text=text, result=result,
|
|
345
|
+
module=module, content_hash=content_hash,
|
|
346
|
+
)
|
|
347
|
+
deferred.append(
|
|
348
|
+
lambda: consumers.extract_consumers(
|
|
349
|
+
sink=sink, path=path, text=text, result=result,
|
|
350
|
+
module=module, content_hash=content_hash,
|
|
351
|
+
table=consumers.PY_URL_ARG, lang="py",
|
|
352
|
+
confidence=ConfidenceClass.SYNTACTIC,
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _extract_ts(
|
|
357
|
+
self,
|
|
358
|
+
sink: Emit,
|
|
359
|
+
path: str,
|
|
360
|
+
text: str,
|
|
361
|
+
content_hash: str,
|
|
362
|
+
deferred: list[Callable[[], None]],
|
|
363
|
+
) -> None:
|
|
364
|
+
result = self._parse_result(sink, path, text)
|
|
365
|
+
if result is None:
|
|
366
|
+
return
|
|
367
|
+
module = common.module_name(path, common.TS_MODULE_SUFFIXES)
|
|
368
|
+
routes.extract_ts_routes(
|
|
369
|
+
sink=sink, path=path, text=text, result=result,
|
|
370
|
+
module=module, content_hash=content_hash,
|
|
371
|
+
)
|
|
372
|
+
deferred.append(
|
|
373
|
+
lambda: consumers.extract_consumers(
|
|
374
|
+
sink=sink, path=path, text=text, result=result,
|
|
375
|
+
module=module, content_hash=content_hash,
|
|
376
|
+
table=consumers.TS_URL_ARG, lang="ts",
|
|
377
|
+
confidence=ConfidenceClass.SYNTACTIC,
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def _extract_js(
|
|
382
|
+
self,
|
|
383
|
+
sink: Emit,
|
|
384
|
+
path: str,
|
|
385
|
+
text: str,
|
|
386
|
+
content_hash: str,
|
|
387
|
+
deferred: list[Callable[[], None]],
|
|
388
|
+
) -> None:
|
|
389
|
+
module = common.module_name(path, common.TS_MODULE_SUFFIXES)
|
|
390
|
+
routes.extract_js_routes(
|
|
391
|
+
sink=sink, path=path, text=text,
|
|
392
|
+
module=module, content_hash=content_hash,
|
|
393
|
+
)
|
|
394
|
+
deferred.append(
|
|
395
|
+
lambda: consumers.extract_js_consumers(
|
|
396
|
+
sink=sink, path=path, text=text,
|
|
397
|
+
module=module, content_hash=content_hash,
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def satisfies_protocol() -> DomainAdapter:
|
|
403
|
+
"""Smoke check para tests: a instância satisfaz ``DomainAdapter``."""
|
|
404
|
+
adapter: DomainAdapter = ApiContractAdapter()
|
|
405
|
+
return adapter
|