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,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)."""
|