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,19 @@
1
+ """Logging estruturado e redaction (sem telemetria, sem egress)."""
2
+
3
+ from code_atlas.observability.logging import (
4
+ JsonFormatter,
5
+ RedactionFilter,
6
+ get_logger,
7
+ log_if_slow,
8
+ slow_ms,
9
+ timed,
10
+ )
11
+
12
+ __all__ = [
13
+ "JsonFormatter",
14
+ "RedactionFilter",
15
+ "get_logger",
16
+ "log_if_slow",
17
+ "slow_ms",
18
+ "timed",
19
+ ]
@@ -0,0 +1,199 @@
1
+ """Logging estruturado com redaction (Blueprint §9, ADR-014).
2
+
3
+ Sem telemetria, sem egress — logs vão apenas para stderr local.
4
+ Formato: texto simples por padrão, JSON-lines com ``CODE_ATLAS_LOG_FORMAT=json``.
5
+ Nível: ``CODE_ATLAS_LOG_LEVEL`` (default WARNING).
6
+
7
+ O filtro de redaction mascara: (a) caminhos absolutos dentro de $HOME,
8
+ (b) padrões de segredo (tokens, chaves, assignments sensíveis).
9
+
10
+ Slow-query guard (lf-0141, Cycle 8 Phase 15): ``CODE_ATLAS_SLOW_MS``
11
+ define um teto de latência em milissegundos; chamadas instrumentadas
12
+ que o excedem emitem ``DEBUG`` via :func:`log_if_slow` / :func:`timed`.
13
+ Ausente, inválido ou ``<=0`` = guard desligado com custo zero (um
14
+ ``os.environ.get`` por chamada, nenhum clock extra além do já medido).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import functools
20
+ import json
21
+ import logging
22
+ import os
23
+ import re
24
+ import time
25
+ from collections.abc import Callable
26
+ from pathlib import Path
27
+ from typing import ParamSpec, TypeVar
28
+
29
+ ENV_FORMAT = "CODE_ATLAS_LOG_FORMAT"
30
+ ENV_LEVEL = "CODE_ATLAS_LOG_LEVEL"
31
+ ENV_SLOW_MS = "CODE_ATLAS_SLOW_MS"
32
+
33
+ _TOKEN_RE = re.compile(
34
+ r"(?i)(api[_-]?key|token|secret|password|passwd|authorization|bearer|"
35
+ r"aws[_-]?access|private[_-]?key|credential)\s*[:=]\s*\S+"
36
+ )
37
+ _LONG_SECRET_RE = re.compile(r"\b[A-Za-z0-9+/=_\-]{40,}\b")
38
+ _PEM_RE = re.compile(r"-----BEGIN [A-Z ]+PRIVATE KEY-----.*?-----END [A-Z ]+PRIVATE KEY-----",
39
+ re.DOTALL)
40
+ # Controles C0/C1/DEL → escape ``\xNN`` (lf-0153, cycle-8.1 Phase 10):
41
+ # linha de request hostil (``GET /\x1b[31m``) ou filename com ``\n``
42
+ # podiam forjar linhas/sequências ANSI no stderr — logs são single-line
43
+ # por contrato, então TODO caractere de controle vira escape visível.
44
+ _CONTROL_RE = re.compile(r"[\x00-\x1f\x7f-\x9f]")
45
+
46
+
47
+ def escape_control(text: str) -> str:
48
+ """Caracteres de controle viram ``\\xNN`` — anti log-injection."""
49
+ return _CONTROL_RE.sub(lambda m: f"\\x{ord(m.group(0)):02x}", text)
50
+
51
+
52
+ def _redact_text(text: str, home: str) -> str:
53
+ """Aplica as regras de mascaramento a uma string."""
54
+ text = escape_control(text)
55
+ if home and home in text:
56
+ text = text.replace(home, "~")
57
+ text = _PEM_RE.sub("<REDACTED:pem>", text)
58
+ text = _TOKEN_RE.sub(lambda m: m.group(1) + "=<REDACTED>", text)
59
+ # Sequências longas tipo chave/token só mascaram se parecerem segredo.
60
+ text = _LONG_SECRET_RE.sub(lambda m: "<REDACTED>", text)
61
+ return text
62
+
63
+
64
+ class RedactionFilter(logging.Filter):
65
+ """Remove home paths e segredos de todos os records emitidos."""
66
+
67
+ def __init__(self, home: str | None = None) -> None:
68
+ super().__init__()
69
+ self._home = home or str(Path.home())
70
+
71
+ def filter(self, record: logging.LogRecord) -> bool:
72
+ if isinstance(record.msg, str):
73
+ record.msg = _redact_text(record.msg, self._home)
74
+ if record.args:
75
+ record.args = tuple(
76
+ _redact_text(str(a), self._home) if isinstance(a, (str, Path)) else a
77
+ for a in record.args
78
+ ) if isinstance(record.args, tuple) else record.args
79
+ if record.exc_info and record.exc_text is None:
80
+ # lf-0153: traceback text contorna msg/args — pré-renderiza
81
+ # escapado para que o Formatter não emita controles crus
82
+ # (filename ``a\nb.py`` numa exceção forjaria linhas).
83
+ record.exc_text = escape_control(
84
+ logging.Formatter().formatException(record.exc_info))
85
+ return True
86
+
87
+
88
+ class JsonFormatter(logging.Formatter):
89
+ """Serializa records como JSON-lines (uma linha por evento)."""
90
+
91
+ def format(self, record: logging.LogRecord) -> str:
92
+ payload = {
93
+ "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
94
+ "level": record.levelname,
95
+ "logger": record.name,
96
+ "msg": record.getMessage(),
97
+ }
98
+ if record.exc_info:
99
+ payload["exc"] = self.formatException(record.exc_info)
100
+ return json.dumps(payload, ensure_ascii=False)
101
+
102
+
103
+ def get_logger(name: str) -> logging.Logger:
104
+ """Logger configurado com redaction; idempotente por nome."""
105
+ logger = logging.getLogger(name)
106
+ if getattr(logger, "_atlas_configured", False):
107
+ return logger
108
+ logger.setLevel(os.environ.get(ENV_LEVEL, "WARNING").upper())
109
+ handler = logging.StreamHandler()
110
+ handler.addFilter(RedactionFilter())
111
+ if os.environ.get(ENV_FORMAT, "").lower() == "json":
112
+ handler.setFormatter(JsonFormatter())
113
+ else:
114
+ handler.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
115
+ logger.addHandler(handler)
116
+ logger.propagate = False
117
+ logger._atlas_configured = True # type: ignore[attr-defined]
118
+ return logger
119
+
120
+
121
+ # --- slow-query guard (lf-0141) -----------------------------------------
122
+ #
123
+ # Guard opt-in por env: ``CODE_ATLAS_SLOW_MS`` é o teto em ms. As
124
+ # fronteiras instrumentadas (despacho HTTP em ``api.handlers.handle``,
125
+ # ``SearchEngine.search_scoped``) medem com ``time.monotonic`` — o
126
+ # guard só decide se o valor já medido vira uma linha DEBUG.
127
+
128
+ def slow_ms(env: dict[str, str] | None = None) -> float | None:
129
+ """Teto do slow-query guard em ms; ``None`` = desligado.
130
+
131
+ ``CODE_ATLAS_SLOW_MS`` ausente/vazio, não-numérico ou ``<= 0``
132
+ desliga o guard — opt-in explícito, default de custo zero.
133
+ """
134
+ raw = (env if env is not None else os.environ).get(ENV_SLOW_MS, "")
135
+ if not raw:
136
+ return None
137
+ try:
138
+ ms = float(raw)
139
+ except ValueError:
140
+ return None
141
+ return ms if ms > 0 else None
142
+
143
+
144
+ def log_if_slow(
145
+ logger: logging.Logger, label: str, t0: float, detail: str = ""
146
+ ) -> None:
147
+ """Emite ``DEBUG`` quando ``time.monotonic() - t0`` excede o teto.
148
+
149
+ ``detail`` é contexto da chamada (rota, estratégia) — passa pelo
150
+ ``RedactionFilter`` do logger como qualquer mensagem.
151
+ """
152
+ ms = slow_ms()
153
+ if ms is None:
154
+ return
155
+ elapsed = (time.monotonic() - t0) * 1000
156
+ # Windows: clock monotônico tem resolução ~15.6ms — elapsed=0 pode
157
+ # ser "instantâneo" ou "menor que um tick". Somar a resolução torna
158
+ # o guard conservador: só garante não-slow quando mesmo o pior caso
159
+ # dentro da granularidade ficaria abaixo do teto.
160
+ res_ms = time.get_clock_info("monotonic").resolution * 1000
161
+ if elapsed + res_ms > ms:
162
+ suffix = f" {detail}" if detail else ""
163
+ logger.debug("slow %s elapsed_ms=%.1f threshold_ms=%.0f%s",
164
+ label, elapsed, ms, suffix)
165
+
166
+
167
+ _P = ParamSpec("_P")
168
+ _R = TypeVar("_R")
169
+
170
+
171
+ def timed(label: str) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
172
+ """Decorator do slow-query guard — mede a chamada e delega a
173
+ ``log_if_slow``. O logger é ``code_atlas.<label>`` (mesmo filtro
174
+ de redaction de qualquer ``get_logger``)."""
175
+ logger = get_logger(f"code_atlas.{label}")
176
+
177
+ def deco(fn: Callable[_P, _R]) -> Callable[_P, _R]:
178
+ @functools.wraps(fn)
179
+ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
180
+ t0 = time.monotonic()
181
+ try:
182
+ return fn(*args, **kwargs)
183
+ finally:
184
+ log_if_slow(logger, label, t0)
185
+
186
+ return wrapper
187
+
188
+ return deco
189
+
190
+
191
+ __all__ = [
192
+ "JsonFormatter",
193
+ "RedactionFilter",
194
+ "escape_control",
195
+ "get_logger",
196
+ "log_if_slow",
197
+ "slow_ms",
198
+ "timed",
199
+ ]
@@ -0,0 +1,43 @@
1
+ """Subsistema de parsing por adaptadores (Cycle 2 — Phases 1+2).
2
+
3
+ Boundary: apenas ``parsers/`` pode importar ``tree_sitter``. Código de
4
+ domínio (contracts, discovery, storage, graph) consome os contratos
5
+ tipados daqui sem conhecer a implementação concreta.
6
+ """
7
+
8
+ from code_atlas.parsers.contracts import (
9
+ EvidenceRecord,
10
+ ExtractedReference,
11
+ ExtractedRelation,
12
+ ExtractedSymbol,
13
+ GraphBuildResult,
14
+ GraphEdge,
15
+ GraphNode,
16
+ ParseDiagnostic,
17
+ ParserAdapter,
18
+ ParserCapability,
19
+ ParseRequest,
20
+ ParseResult,
21
+ SourceSpan,
22
+ )
23
+ from code_atlas.parsers.engine import ParseEngine, ParseLimits
24
+ from code_atlas.parsers.registry import ParserRegistry
25
+
26
+ __all__ = [
27
+ "EvidenceRecord",
28
+ "ExtractedReference",
29
+ "ExtractedRelation",
30
+ "ExtractedSymbol",
31
+ "GraphBuildResult",
32
+ "GraphEdge",
33
+ "GraphNode",
34
+ "ParseDiagnostic",
35
+ "ParseEngine",
36
+ "ParseLimits",
37
+ "ParseRequest",
38
+ "ParseResult",
39
+ "ParserAdapter",
40
+ "ParserCapability",
41
+ "ParserRegistry",
42
+ "SourceSpan",
43
+ ]
@@ -0,0 +1,209 @@
1
+ """Contratos tipados do subsistema de parsing (Cycle 2 Phase 1).
2
+
3
+ Separação dura: parsing sintático ≠ resolução semântica. Tudo que um
4
+ adaptador produz é sintático — resolução de alvos é responsabilidade do
5
+ graph builder e de resolvers futuros (SCIP/LSP, Cycle 3).
6
+
7
+ Convenções de span: ``line`` é 1-based (humanos/editores), ``col`` é a
8
+ coluna em bytes 0-based do Tree-sitter; offsets em bytes sobre UTF-8.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Protocol
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class SourceSpan:
19
+ """Localização de um nó no arquivo-fonte."""
20
+
21
+ start_byte: int
22
+ end_byte: int
23
+ start_line: int # 1-based
24
+ start_col: int # byte offset 0-based
25
+ end_line: int
26
+ end_col: int
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ParseDiagnostic:
31
+ """Problema estruturado registrado durante parsing/extração."""
32
+
33
+ code: str # ex.: "error-node", "missing-node", "file-too-large", "timeout"
34
+ severity: str # "error" | "warning" | "info"
35
+ message: str
36
+ span: SourceSpan | None = None
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ParserCapability:
41
+ """Capacidades declaradas de um adaptador (capability matrix)."""
42
+
43
+ symbols: bool = True
44
+ signatures: bool = True
45
+ docstrings: bool = True
46
+ imports: bool = True
47
+ calls: bool = True
48
+ decorators: bool = False
49
+ inheritance: bool = True
50
+ interfaces: bool = False
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ParseRequest:
55
+ """Entrada de parsing: bytes UTF-8 + metadados. Nunca executa o alvo."""
56
+
57
+ path: str # rel POSIX dentro do repositório
58
+ language: str # id resolvido pelo registry
59
+ content: bytes
60
+ timeout_us: int = 2_000_000
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class ExtractedSymbol:
65
+ """Símbolo estrutural extraído — ligação lógica fica para o grafo."""
66
+
67
+ language: str
68
+ kind: str # NodeKind string: module|class|interface|function|method|symbol
69
+ name: str
70
+ qualified_name: str # module.scope.name — nunca usa nº de linha
71
+ span: SourceSpan
72
+ name_span: SourceSpan | None = None
73
+ scope_path: tuple[str, ...] = () # nomes dos escopos léxicos externos
74
+ signature: str | None = None
75
+ params: tuple[str, ...] = () # nomes dos parâmetros, em ordem
76
+ docstring: str | None = None
77
+ decorators: tuple[str, ...] = ()
78
+ bases: tuple[str, ...] = () # extends declarado
79
+ implements: tuple[str, ...] = () # implements declarado (TS)
80
+ modifiers: tuple[str, ...] = () # async|export|default|static|abstract...
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ExtractedReference:
85
+ """Menção sintática a um nome — alvo textual preservado."""
86
+
87
+ name: str # texto do alvo (ex.: "foo", "pkg.mod", "obj.method")
88
+ context: str # call|import|import-member|extends|implements|decorator|type-ref|jsx
89
+ span: SourceSpan
90
+ scope_path: tuple[str, ...] = ()
91
+ target: str = "" # import-member: "modulo.membro" do símbolo importado
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class ExtractedRelation:
96
+ """Relação sintática candidata extraída pelo adaptador."""
97
+
98
+ type: str # EdgeKind string
99
+ from_name: str # qualified name da origem
100
+ to_name: str # alvo textual — resolução é do graph builder
101
+ span: SourceSpan
102
+ confidence_class: str = "syntactic"
103
+ resolution_status: str = "unresolved"
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class ParseResult:
108
+ """Saída de um arquivo parseado — real, sem mocks."""
109
+
110
+ path: str
111
+ language: str
112
+ parser: str # id do adaptador (ex.: "tree-sitter-python")
113
+ parser_version: str
114
+ grammar_version: str
115
+ content_hash: str # sha256 do conteúdo — casa com a evidência
116
+ has_error: bool = False
117
+ error_node_count: int = 0
118
+ missing_node_count: int = 0
119
+ node_count: int = 0
120
+ diagnostics: tuple[ParseDiagnostic, ...] = ()
121
+ symbols: tuple[ExtractedSymbol, ...] = ()
122
+ references: tuple[ExtractedReference, ...] = ()
123
+ relations: tuple[ExtractedRelation, ...] = ()
124
+ supported: bool = True
125
+
126
+
127
+ class ParserAdapter(Protocol):
128
+ """Adaptador concreto de linguagem (implementado por Tree-sitter)."""
129
+
130
+ language: str
131
+ extensions: tuple[str, ...]
132
+ capabilities: ParserCapability
133
+
134
+ @property
135
+ def adapter_id(self) -> str:
136
+ """Ex.: ``tree-sitter-python`` — entra em analyzer_versions."""
137
+ ...
138
+
139
+ @property
140
+ def parser_version(self) -> str: ...
141
+
142
+ @property
143
+ def grammar_version(self) -> str: ...
144
+
145
+ def parse(self, request: ParseRequest) -> ParseResult: ...
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class EvidenceRecord:
150
+ """Proveniência de uma relação/nó persistido (Blueprint §5)."""
151
+
152
+ path: str # arquivo de origem
153
+ span: SourceSpan | None
154
+ content_hash: str
155
+ extractor: str # adapter_id
156
+ extractor_version: str # "parserX.Y+grammarZ.W"
157
+ notes: str = ""
158
+ # Proveniência de resolvedor (lf-0035) — opcionais; "" quando a
159
+ # evidência veio do parser e não de um resolvedor semântico.
160
+ resolver_id: str = ""
161
+ resolver_version: str = ""
162
+ evidence_tier: int = 0 # EvidenceTier (0 = não-resolver)
163
+ index_hash: str = "" # sha do índice externo (ex.: SCIP), se houver
164
+ validation_status: str = "" # ex.: "confirmed", "conflict"
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class GraphNode:
169
+ """Nó tipado do grafo canônico (saída do builder)."""
170
+
171
+ node_id: str # identidade determinística — nunca nº de linha
172
+ kind: str # NodeKind
173
+ name: str
174
+ qualified_name: str
175
+ language: str = ""
176
+ path: str = "" # rel path (file/module/package nodes)
177
+ span: SourceSpan | None = None
178
+ content_hash: str = ""
179
+ signature: str | None = None
180
+ params: tuple[str, ...] = ()
181
+ docstring: str | None = None
182
+ modifiers: tuple[str, ...] = ()
183
+
184
+
185
+ @dataclass(frozen=True)
186
+ class GraphEdge:
187
+ """Aresta tipada com proveniência completa."""
188
+
189
+ type: str # EdgeKind
190
+ from_id: str # GraphNode.node_id
191
+ to_id: str | None = None # None quando alvo não resolveu para nó
192
+ to_text: str = "" # alvo textual preservado (unresolved/ambiguous)
193
+ resolution_status: str = "unresolved"
194
+ confidence_class: str = "syntactic"
195
+ resolver: str = "" # id do resolver/extrator
196
+ evidence_index: tuple[int, ...] = () # índices em GraphBuildResult.evidence
197
+ span: SourceSpan | None = None
198
+ source_path: str = "" # arquivo de origem da aresta (lf-0027)
199
+
200
+
201
+ @dataclass(frozen=True)
202
+ class GraphBuildResult:
203
+ """Saída determinística do graph builder (hand-off para storage)."""
204
+
205
+ nodes: tuple[GraphNode, ...] = ()
206
+ edges: tuple[GraphEdge, ...] = ()
207
+ evidence: tuple[EvidenceRecord, ...] = ()
208
+ diagnostics: tuple[ParseDiagnostic, ...] = ()
209
+ stats: dict[str, int] = field(default_factory=dict)
@@ -0,0 +1,125 @@
1
+ """Motor de parsing: orquestra requests, limites e isolamento de falhas.
2
+
3
+ Política de erro (Phase 2): falha por arquivo é registrada em
4
+ ``ParseResult.diagnostics`` e nunca derruba o batch. Erros recuperáveis
5
+ (file-too-large, unsupported, timeout, parse-error) viram resultado com
6
+ ``supported``/``has_error`` marcados; erro fatal = bug no adaptador —
7
+ isolado por arquivo como diagnóstico ``adapter-crash``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ from collections.abc import Iterable
14
+ from dataclasses import dataclass
15
+
16
+ from code_atlas.parsers.contracts import (
17
+ ParseDiagnostic,
18
+ ParseRequest,
19
+ ParseResult,
20
+ SourceSpan,
21
+ )
22
+ from code_atlas.parsers.registry import ParserRegistry
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ParseLimits:
27
+ """Limites por arquivo — proteção contra conteúdo hostil."""
28
+
29
+ max_file_size: int = 1 << 20 # 1 MiB
30
+ timeout_us: int = 2_000_000
31
+ max_diagnostics: int = 50
32
+
33
+
34
+ def _empty_result(
35
+ path: str,
36
+ language: str,
37
+ content: bytes,
38
+ diagnostic: ParseDiagnostic,
39
+ ) -> ParseResult:
40
+ return ParseResult(
41
+ path=path,
42
+ language=language,
43
+ parser="",
44
+ parser_version="",
45
+ grammar_version="",
46
+ content_hash=hashlib.sha256(content).hexdigest(),
47
+ diagnostics=(diagnostic,),
48
+ supported=False,
49
+ )
50
+
51
+
52
+ class ParseEngine:
53
+ """Orquestra ParseRequests via registry. Sem estado mutável exposto."""
54
+
55
+ def __init__(
56
+ self, registry: ParserRegistry | None = None, limits: ParseLimits | None = None
57
+ ) -> None:
58
+ self.registry = registry or ParserRegistry.default()
59
+ self.limits = limits or ParseLimits()
60
+
61
+ def parse(self, rel_path: str, content: bytes) -> ParseResult:
62
+ """Parse de um arquivo; falhas viram diagnósticos, não exceções."""
63
+ adapter = self.registry.for_path(rel_path)
64
+ if adapter is None:
65
+ return _empty_result(
66
+ rel_path,
67
+ "",
68
+ content,
69
+ ParseDiagnostic(
70
+ code="unsupported-language",
71
+ severity="info",
72
+ message="no parser adapter for this path",
73
+ ),
74
+ )
75
+ if len(content) > self.limits.max_file_size:
76
+ return _empty_result(
77
+ rel_path,
78
+ adapter.language,
79
+ content,
80
+ ParseDiagnostic(
81
+ code="file-too-large",
82
+ severity="warning",
83
+ message=f"{len(content)} bytes exceeds limit {self.limits.max_file_size}",
84
+ ),
85
+ )
86
+ request = ParseRequest(
87
+ path=rel_path,
88
+ language=adapter.language,
89
+ content=content,
90
+ timeout_us=self.limits.timeout_us,
91
+ )
92
+ try:
93
+ result = adapter.parse(request)
94
+ except Exception as exc:
95
+ return _empty_result(
96
+ rel_path,
97
+ adapter.language,
98
+ content,
99
+ ParseDiagnostic(
100
+ code="adapter-crash",
101
+ severity="error",
102
+ message=f"{type(exc).__name__}: {exc}",
103
+ ),
104
+ )
105
+ if len(result.diagnostics) > self.limits.max_diagnostics:
106
+ result = ParseResult(
107
+ **{
108
+ **result.__dict__,
109
+ "diagnostics": result.diagnostics[: self.limits.max_diagnostics],
110
+ }
111
+ )
112
+ return result
113
+
114
+ def parse_batch(
115
+ self, items: Iterable[tuple[str, bytes]]
116
+ ) -> list[ParseResult]:
117
+ """Parse de lote — isolamento garantido por arquivo."""
118
+ return [self.parse(path, content) for path, content in items]
119
+
120
+
121
+ def span_of_point(
122
+ start_byte: int, end_byte: int, line: int, col: int, end_line: int, end_col: int
123
+ ) -> SourceSpan:
124
+ """Helper de construção de span para testes."""
125
+ return SourceSpan(start_byte, end_byte, line, col, end_line, end_col)
@@ -0,0 +1 @@
1
+ """Adaptadores concretos de linguagem (Tree-sitter)."""