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,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