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,38 @@
|
|
|
1
|
+
"""Contratos de domínio do Code Atlas (modelos, enums, ports).
|
|
2
|
+
|
|
3
|
+
O domínio não importa CLI, web, MCP, UI ou bancos externos.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from code_atlas.contracts.enums import (
|
|
7
|
+
ConfidenceClass,
|
|
8
|
+
EdgeKind,
|
|
9
|
+
NodeKind,
|
|
10
|
+
ResolutionStatus,
|
|
11
|
+
SnapshotStatus,
|
|
12
|
+
)
|
|
13
|
+
from code_atlas.contracts.models import (
|
|
14
|
+
DomainSourceFile,
|
|
15
|
+
Evidence,
|
|
16
|
+
IndexJob,
|
|
17
|
+
LogicalSymbol,
|
|
18
|
+
Relation,
|
|
19
|
+
Repository,
|
|
20
|
+
Snapshot,
|
|
21
|
+
SymbolOccurrence,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ConfidenceClass",
|
|
26
|
+
"DomainSourceFile",
|
|
27
|
+
"EdgeKind",
|
|
28
|
+
"Evidence",
|
|
29
|
+
"IndexJob",
|
|
30
|
+
"LogicalSymbol",
|
|
31
|
+
"NodeKind",
|
|
32
|
+
"Relation",
|
|
33
|
+
"Repository",
|
|
34
|
+
"ResolutionStatus",
|
|
35
|
+
"Snapshot",
|
|
36
|
+
"SnapshotStatus",
|
|
37
|
+
"SymbolOccurrence",
|
|
38
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Vocabulário canônico do domínio (Blueprint §5).
|
|
2
|
+
|
|
3
|
+
Valores devem casar exatamente com o Blueprint — não renomear nem estender
|
|
4
|
+
sem ADR. Valores futuros documentados (READS_FROM etc.) ficam de fora até
|
|
5
|
+
existir produtor real.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NodeKind(StrEnum):
|
|
14
|
+
"""Tipos de nó do grafo (Blueprint §5 "Tipos iniciais")."""
|
|
15
|
+
|
|
16
|
+
REPOSITORY = "repository"
|
|
17
|
+
PACKAGE = "package"
|
|
18
|
+
MODULE = "module"
|
|
19
|
+
FILE = "file"
|
|
20
|
+
CLASS = "class"
|
|
21
|
+
INTERFACE = "interface"
|
|
22
|
+
FUNCTION = "function"
|
|
23
|
+
METHOD = "method"
|
|
24
|
+
SYMBOL = "symbol"
|
|
25
|
+
CONFIG = "config"
|
|
26
|
+
UNKNOWN = "unknown"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EdgeKind(StrEnum):
|
|
30
|
+
"""Vocabulário de arestas (Blueprint §5)."""
|
|
31
|
+
|
|
32
|
+
CONTAINS = "CONTAINS"
|
|
33
|
+
DEFINES = "DEFINES"
|
|
34
|
+
IMPORTS = "IMPORTS"
|
|
35
|
+
CALLS = "CALLS"
|
|
36
|
+
IMPLEMENTS = "IMPLEMENTS"
|
|
37
|
+
EXTENDS = "EXTENDS"
|
|
38
|
+
REFERENCES = "REFERENCES"
|
|
39
|
+
CONFIGURES = "CONFIGURES"
|
|
40
|
+
TESTS = "TESTS"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfidenceClass(StrEnum):
|
|
44
|
+
"""Classe de confiança da evidência — não é probabilidade (Blueprint §5)."""
|
|
45
|
+
|
|
46
|
+
RESOLVED = "resolved"
|
|
47
|
+
SYNTACTIC = "syntactic"
|
|
48
|
+
HEURISTIC = "heuristic"
|
|
49
|
+
INFERRED = "inferred"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ResolutionStatus(StrEnum):
|
|
53
|
+
"""Status de resolução de um alvo de relação (Blueprint §5)."""
|
|
54
|
+
|
|
55
|
+
RESOLVED = "resolved"
|
|
56
|
+
AMBIGUOUS = "ambiguous"
|
|
57
|
+
UNRESOLVED = "unresolved"
|
|
58
|
+
EXTERNAL = "external"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SnapshotStatus(StrEnum):
|
|
62
|
+
"""Estados do ciclo de vida de um snapshot (Blueprint §5)."""
|
|
63
|
+
|
|
64
|
+
BUILDING = "BUILDING"
|
|
65
|
+
READY = "READY"
|
|
66
|
+
FAILED = "FAILED"
|
|
67
|
+
STALE = "STALE"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Modelos de domínio do Code Atlas (Blueprint §5).
|
|
2
|
+
|
|
3
|
+
Dataclasses stdlib, imutáveis onde possível. Campos de enum aceitam o valor
|
|
4
|
+
string ou o enum e são validados em ``__post_init__`` — valores fora do
|
|
5
|
+
vocabulário levantam ``ValueError``. ``id=None`` significa "a persistir".
|
|
6
|
+
|
|
7
|
+
Identidade lógica (``LogicalSymbol``, ``SourceFile``) é separada da
|
|
8
|
+
ocorrência por snapshot (``SymbolOccurrence``, ``file_inventory``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from code_atlas.contracts.enums import (
|
|
17
|
+
ConfidenceClass,
|
|
18
|
+
EdgeKind,
|
|
19
|
+
NodeKind,
|
|
20
|
+
ResolutionStatus,
|
|
21
|
+
SnapshotStatus,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _coerce(enum_cls: type, value: object, field_name: str) -> object:
|
|
26
|
+
try:
|
|
27
|
+
return enum_cls(value)
|
|
28
|
+
except ValueError as exc:
|
|
29
|
+
raise ValueError(f"invalid {field_name}: {value!r}") from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Repository:
|
|
34
|
+
"""Repositório indexado; `id` independe do pathname local."""
|
|
35
|
+
|
|
36
|
+
root_handle: str
|
|
37
|
+
id: int | None = None
|
|
38
|
+
origin: str = ""
|
|
39
|
+
git_provider: str = ""
|
|
40
|
+
default_branch: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Snapshot:
|
|
45
|
+
"""Ocorrência imutável de um estado do repositório."""
|
|
46
|
+
|
|
47
|
+
repository_id: int
|
|
48
|
+
worktree_digest: str
|
|
49
|
+
id: int | None = None
|
|
50
|
+
git_ref: str = ""
|
|
51
|
+
commit_sha: str | None = None
|
|
52
|
+
dirty: bool = False # lf-0021: conteúdo diverge do commit_sha
|
|
53
|
+
analyzer_versions: Mapping[str, str] = field(default_factory=dict)
|
|
54
|
+
parent_snapshot_id: int | None = None
|
|
55
|
+
status: SnapshotStatus = SnapshotStatus.BUILDING
|
|
56
|
+
created_at: str = ""
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
object.__setattr__(
|
|
60
|
+
self, "status", _coerce(SnapshotStatus, self.status, "status")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class LogicalSymbol:
|
|
66
|
+
"""Símbolo com identidade lógica estável por repositório."""
|
|
67
|
+
|
|
68
|
+
repository_id: int
|
|
69
|
+
language: str
|
|
70
|
+
qualified_name: str
|
|
71
|
+
kind: NodeKind
|
|
72
|
+
id: int | None = None
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
object.__setattr__(self, "kind", _coerce(NodeKind, self.kind, "kind"))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class DomainSourceFile:
|
|
80
|
+
"""Arquivo-fonte com identidade por (repo, relative_path)."""
|
|
81
|
+
|
|
82
|
+
repository_id: int
|
|
83
|
+
relative_path: str
|
|
84
|
+
language: str
|
|
85
|
+
id: int | None = None
|
|
86
|
+
sha256: str | None = None
|
|
87
|
+
size_bytes: int = 0
|
|
88
|
+
ignored: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class SymbolOccurrence:
|
|
93
|
+
"""Ocorrência de um símbolo num snapshot (span/hash são da ocorrência)."""
|
|
94
|
+
|
|
95
|
+
snapshot_id: int
|
|
96
|
+
logical_symbol_id: int
|
|
97
|
+
source_file_id: int
|
|
98
|
+
id: int | None = None
|
|
99
|
+
span_start: int | None = None
|
|
100
|
+
span_end: int | None = None
|
|
101
|
+
line: int | None = None
|
|
102
|
+
col: int | None = None
|
|
103
|
+
content_hash: str | None = None
|
|
104
|
+
parse_status: str = "parsed"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Relation:
|
|
109
|
+
"""Aresta tipada com proveniência obrigatória (Blueprint ADR-012)."""
|
|
110
|
+
|
|
111
|
+
snapshot_id: int
|
|
112
|
+
from_id: int
|
|
113
|
+
relation_type: EdgeKind
|
|
114
|
+
resolver: str
|
|
115
|
+
confidence_class: ConfidenceClass
|
|
116
|
+
resolution_status: ResolutionStatus
|
|
117
|
+
id: int | None = None
|
|
118
|
+
to_id: int | None = None
|
|
119
|
+
to_unresolved: str | None = None
|
|
120
|
+
evidence_ids: tuple[int, ...] = ()
|
|
121
|
+
|
|
122
|
+
def __post_init__(self) -> None:
|
|
123
|
+
object.__setattr__(
|
|
124
|
+
self, "relation_type", _coerce(EdgeKind, self.relation_type, "relation_type")
|
|
125
|
+
)
|
|
126
|
+
object.__setattr__(
|
|
127
|
+
self,
|
|
128
|
+
"confidence_class",
|
|
129
|
+
_coerce(ConfidenceClass, self.confidence_class, "confidence_class"),
|
|
130
|
+
)
|
|
131
|
+
object.__setattr__(
|
|
132
|
+
self,
|
|
133
|
+
"resolution_status",
|
|
134
|
+
_coerce(ResolutionStatus, self.resolution_status, "resolution_status"),
|
|
135
|
+
)
|
|
136
|
+
if self.to_id is None and not self.to_unresolved:
|
|
137
|
+
raise ValueError("relation requires to_id or to_unresolved target")
|
|
138
|
+
if not self.resolver:
|
|
139
|
+
raise ValueError("relation requires resolver provenance")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class Evidence:
|
|
144
|
+
"""Trecho observável que sustenta um fato (extrator + versão + hash)."""
|
|
145
|
+
|
|
146
|
+
snapshot_id: int
|
|
147
|
+
source_file_id: int
|
|
148
|
+
path: str
|
|
149
|
+
content_hash: str
|
|
150
|
+
extractor: str
|
|
151
|
+
extractor_version: str
|
|
152
|
+
id: int | None = None
|
|
153
|
+
span: str | None = None
|
|
154
|
+
notes: str = ""
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
if not self.content_hash:
|
|
158
|
+
raise ValueError("evidence requires content_hash")
|
|
159
|
+
if not self.extractor or not self.extractor_version:
|
|
160
|
+
raise ValueError("evidence requires extractor + extractor_version")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class IndexJob:
|
|
165
|
+
"""Registro de uma tentativa de indexação (diagnóstico)."""
|
|
166
|
+
|
|
167
|
+
repository_id: int
|
|
168
|
+
status: str # running | success | failed | noop
|
|
169
|
+
id: int | None = None
|
|
170
|
+
snapshot_id: int | None = None
|
|
171
|
+
started_at: str = ""
|
|
172
|
+
finished_at: str = ""
|
|
173
|
+
counts: Mapping[str, int] = field(default_factory=dict)
|
|
174
|
+
failures: tuple[str, ...] = ()
|
|
175
|
+
manifest_hash: str = ""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Ports hexagonais do domínio (Blueprint §3).
|
|
2
|
+
|
|
3
|
+
Interfaces ``Protocol`` — apenas assinaturas. Implementações reais do
|
|
4
|
+
Cycle 1: ``RepositoryScanner`` (discovery.scan), ``GraphStore`` parcial
|
|
5
|
+
(storage), ``IndexCoordinator`` (indexing). As demais são contrato para
|
|
6
|
+
ciclos futuros e não têm implementação placebo.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
from code_atlas.contracts.enums import ResolutionStatus
|
|
16
|
+
from code_atlas.contracts.models import Snapshot, SymbolOccurrence
|
|
17
|
+
from code_atlas.discovery.models import ScanOptions, ScanResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RepositoryScanner(Protocol):
|
|
21
|
+
"""Produz o inventário de arquivos de uma raiz (implementado: discovery)."""
|
|
22
|
+
|
|
23
|
+
def scan(
|
|
24
|
+
self, root: Path, options: ScanOptions | None = None
|
|
25
|
+
) -> ScanResult: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParserAdapter(Protocol):
|
|
29
|
+
"""Adaptador AST (Tree-sitter, Cycle 2) com capability matrix declarada."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str: ...
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def version(self) -> str: ...
|
|
36
|
+
|
|
37
|
+
def languages(self) -> frozenset[str]: ...
|
|
38
|
+
|
|
39
|
+
def capabilities(self) -> frozenset[str]:
|
|
40
|
+
"""Ex.: ``symbols``, ``imports``, ``calls``, ``docstrings``."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SymbolResolver(Protocol):
|
|
45
|
+
"""Resolve ocorrências para alvos canônicos (SCIP/LSP, Cycle 3)."""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str: ...
|
|
49
|
+
|
|
50
|
+
def resolve(self, occurrence: SymbolOccurrence) -> ResolutionStatus: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GraphStore(Protocol):
|
|
54
|
+
"""Persistência do grafo tipado (implementado parcialmente: storage)."""
|
|
55
|
+
|
|
56
|
+
def latest_ready_snapshot(self, repository_id: int) -> Snapshot | None: ...
|
|
57
|
+
|
|
58
|
+
def snapshot_by_id(self, snapshot_id: int) -> Snapshot | None: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SearchPort(Protocol):
|
|
62
|
+
"""Busca lexical/estrutural (FTS5, Cycle 4)."""
|
|
63
|
+
|
|
64
|
+
def search(self, query: str, limit: int = 20) -> Sequence[object]: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class IndexCoordinator(Protocol):
|
|
68
|
+
"""Orquestra scan → snapshot → publicação atômica (implementado: indexing)."""
|
|
69
|
+
|
|
70
|
+
def index(self, root: Path) -> Snapshot: ...
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Discovery Git-aware: inventário de arquivos, fingerprints e manifest."""
|
|
2
|
+
|
|
3
|
+
from code_atlas.discovery.models import (
|
|
4
|
+
GitInfo,
|
|
5
|
+
ScanMode,
|
|
6
|
+
ScanOptions,
|
|
7
|
+
ScanResult,
|
|
8
|
+
ScanWarning,
|
|
9
|
+
SourceFile,
|
|
10
|
+
WarningKind,
|
|
11
|
+
)
|
|
12
|
+
from code_atlas.discovery.scanner import probe_git, scan
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GitInfo",
|
|
16
|
+
"ScanMode",
|
|
17
|
+
"ScanOptions",
|
|
18
|
+
"ScanResult",
|
|
19
|
+
"ScanWarning",
|
|
20
|
+
"SourceFile",
|
|
21
|
+
"WarningKind",
|
|
22
|
+
"probe_git",
|
|
23
|
+
"scan",
|
|
24
|
+
]
|
|
25
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Parser mínimo de `.gitignore` (fallback sem git).
|
|
2
|
+
|
|
3
|
+
Best-effort: suporta `*`, `?`, `[...]`, `**`, âncora `/`, negação `!` e
|
|
4
|
+
padrões de diretório com `/` final. Escopo é o diretório do `.gitignore`.
|
|
5
|
+
Semântica: a última regra que casa decide (ordem do arquivo, raiz → folha).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _translate(pattern: str) -> str:
|
|
16
|
+
"""Converte um glob gitignore em regex sobre path relativo POSIX."""
|
|
17
|
+
out = []
|
|
18
|
+
i = 0
|
|
19
|
+
while i < len(pattern):
|
|
20
|
+
c = pattern[i]
|
|
21
|
+
if c == "*":
|
|
22
|
+
if pattern[i : i + 2] == "**":
|
|
23
|
+
if pattern[i : i + 3] == "**/":
|
|
24
|
+
out.append("(?:.*/)?")
|
|
25
|
+
i += 3
|
|
26
|
+
continue
|
|
27
|
+
out.append(".*")
|
|
28
|
+
i += 2
|
|
29
|
+
continue
|
|
30
|
+
out.append("[^/]*")
|
|
31
|
+
elif c == "?":
|
|
32
|
+
out.append("[^/]")
|
|
33
|
+
elif c == "[":
|
|
34
|
+
j = pattern.find("]", i + 1)
|
|
35
|
+
if j == -1:
|
|
36
|
+
out.append(re.escape(c))
|
|
37
|
+
else:
|
|
38
|
+
out.append(pattern[i : j + 1])
|
|
39
|
+
i = j
|
|
40
|
+
else:
|
|
41
|
+
out.append(re.escape(c))
|
|
42
|
+
i += 1
|
|
43
|
+
return "".join(out)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _Rule:
|
|
48
|
+
regex: re.Pattern[str]
|
|
49
|
+
negate: bool
|
|
50
|
+
dir_only: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GitIgnoreScope:
|
|
54
|
+
"""Regras de um único `.gitignore`, aplicáveis a paths relativos a seu dir."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, base: str, rules: list[_Rule]) -> None:
|
|
57
|
+
self.base = base # dir relativo POSIX onde o .gitignore vive ("" = raiz)
|
|
58
|
+
self.rules = rules
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def parse(cls, base: str, text: str) -> GitIgnoreScope:
|
|
62
|
+
rules: list[_Rule] = []
|
|
63
|
+
for raw in text.splitlines():
|
|
64
|
+
line = raw.strip()
|
|
65
|
+
if not line or line.startswith("#"):
|
|
66
|
+
continue
|
|
67
|
+
negate = line.startswith("!")
|
|
68
|
+
if negate:
|
|
69
|
+
line = line[1:]
|
|
70
|
+
dir_only = line.endswith("/")
|
|
71
|
+
line = line.rstrip("/")
|
|
72
|
+
anchored = line.startswith("/")
|
|
73
|
+
line = line.lstrip("/")
|
|
74
|
+
if not line:
|
|
75
|
+
continue
|
|
76
|
+
if "/" not in line and not anchored:
|
|
77
|
+
# Sem barra: casa basename em qualquer profundidade do escopo.
|
|
78
|
+
regex = re.compile(rf"(?:^|/){_translate(line)}$")
|
|
79
|
+
else:
|
|
80
|
+
regex = re.compile(rf"^{_translate(line)}$")
|
|
81
|
+
rules.append(_Rule(regex=regex, negate=negate, dir_only=dir_only))
|
|
82
|
+
return cls(base=base, rules=rules)
|
|
83
|
+
|
|
84
|
+
def matches(self, path: str, is_dir: bool) -> bool | None:
|
|
85
|
+
"""True/False se decidido por este escopo; None se nenhuma regra casa."""
|
|
86
|
+
rel = path[len(self.base) :].lstrip("/") if self.base else path
|
|
87
|
+
if not rel:
|
|
88
|
+
return None
|
|
89
|
+
decision: bool | None = None
|
|
90
|
+
for rule in self.rules:
|
|
91
|
+
if rule.dir_only and not is_dir:
|
|
92
|
+
continue
|
|
93
|
+
if rule.regex.match(rel):
|
|
94
|
+
decision = not rule.negate # último match vence
|
|
95
|
+
return decision
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class GitIgnoreStack:
|
|
99
|
+
"""Pilha de escopos `.gitignore`; a decisão mais profunda e tardia vence."""
|
|
100
|
+
|
|
101
|
+
def __init__(self) -> None:
|
|
102
|
+
self._scopes: list[GitIgnoreScope] = []
|
|
103
|
+
|
|
104
|
+
def add_file(self, base_dir: str, path: Path) -> None:
|
|
105
|
+
try:
|
|
106
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
107
|
+
except OSError:
|
|
108
|
+
return
|
|
109
|
+
self._scopes.append(GitIgnoreScope.parse(base_dir, text))
|
|
110
|
+
|
|
111
|
+
def is_ignored(self, path: str, is_dir: bool) -> bool:
|
|
112
|
+
"""True se ignorado; o escopo mais profundo que decide vence."""
|
|
113
|
+
for scope in reversed(self._scopes):
|
|
114
|
+
if scope.base and not path.startswith(scope.base + "/"):
|
|
115
|
+
continue
|
|
116
|
+
decision = scope.matches(path, is_dir)
|
|
117
|
+
if decision is not None:
|
|
118
|
+
return decision
|
|
119
|
+
return False
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Identidade lógica do repositório (lf-0021).
|
|
2
|
+
|
|
3
|
+
``repository_id`` é derivado de identidade **lógica**, nunca só do path:
|
|
4
|
+
|
|
5
|
+
1. ``git-remote:<remote.origin.url>`` — repositório com remote;
|
|
6
|
+
2. ``git-root:<sha do commit raiz>`` — repo git sem remote;
|
|
7
|
+
3. ``path:<root resolvido>`` — último recurso (sem git).
|
|
8
|
+
|
|
9
|
+
Realocar o diretório físico mantém a identidade (``root_handle`` passa a
|
|
10
|
+
ser campo de diagnóstico atualizado a cada index). Dois checkouts do
|
|
11
|
+
mesmo remote **compartilham** identidade — snapshots de ambos vivem sob
|
|
12
|
+
o mesmo repo e se distinguem por ``worktree_digest``/revisão.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from code_atlas.discovery.models import GitInfo
|
|
20
|
+
from code_atlas.discovery.scanner import probe_git
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def identity_key(git: GitInfo, root: Path) -> str:
|
|
24
|
+
"""Chave de identidade estável do repositório.
|
|
25
|
+
|
|
26
|
+
lf-0058: raiz **abaixo** do toplevel git recebe sufixo
|
|
27
|
+
``@<caminho relativo>`` — dois projetos num mesmo monorepo não
|
|
28
|
+
colidem mais (antes compartilhavam o remote → mesmo repo).
|
|
29
|
+
Raiz == toplevel mantém o formato legado (retrocompatível);
|
|
30
|
+
realocar o diretório preserva identidade pois o relpath é estável.
|
|
31
|
+
"""
|
|
32
|
+
base = ""
|
|
33
|
+
if git.remote_url:
|
|
34
|
+
base = f"git-remote:{git.remote_url}"
|
|
35
|
+
elif git.root_commit:
|
|
36
|
+
base = f"git-root:{git.root_commit}"
|
|
37
|
+
else:
|
|
38
|
+
return f"path:{Path(root).resolve()}"
|
|
39
|
+
if git.toplevel:
|
|
40
|
+
try:
|
|
41
|
+
rel = Path(root).resolve().relative_to(
|
|
42
|
+
Path(git.toplevel).resolve())
|
|
43
|
+
except ValueError:
|
|
44
|
+
rel = None
|
|
45
|
+
if rel is not None and str(rel) not in ("", "."):
|
|
46
|
+
return f"{base}@{rel.as_posix()}"
|
|
47
|
+
return base
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def identity_key_for_root(root: Path) -> str:
|
|
51
|
+
"""Identidade sem scan completo — usado por ``init`` (probe apenas)."""
|
|
52
|
+
return identity_key(probe_git(Path(root)), Path(root))
|