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,355 @@
|
|
|
1
|
+
"""Modelo do workspace federado (lf-0118, Cycle 7 Phase 10).
|
|
2
|
+
|
|
3
|
+
Um ``FederationWorkspace`` agrupa >=2 ``repository_id`` já indexados.
|
|
4
|
+
Cada ``FederationMember`` pode carregar um ``pinned_snapshot_id``
|
|
5
|
+
explícito; quando o pin está vazio a view resolve o READY mais recente
|
|
6
|
+
**naquele momento** — registrado na view, nunca presumido "mesmo tempo"
|
|
7
|
+
entre repositórios.
|
|
8
|
+
|
|
9
|
+
``FederatedView`` é a materialização ``{repo_id: snapshot_id}``:
|
|
10
|
+
``view_id`` é a **chave de cache** da view (lf-0134) — hash sha256 do
|
|
11
|
+
contrato completo de identidade: membership (``repo_id`` +
|
|
12
|
+
``identity_key``), o snapshot resolvido de cada membro (pin/None), o
|
|
13
|
+
digest das ``analyzer_versions`` que produziram os dados daquele
|
|
14
|
+
snapshot (adapters/resolvers de índice), a versão do resolver
|
|
15
|
+
federado e o ``schema_version`` do storage. Qualquer componente que
|
|
16
|
+
mude invalida a view — caches chaveados em ``view_id`` nunca servem
|
|
17
|
+
rows de uma identidade diferente.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FederationError(Exception):
|
|
29
|
+
"""Erro tipado da camada de federação."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkspaceNotFoundError(FederationError):
|
|
33
|
+
"""Workspace inexistente (por id ou nome)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class WorkspaceExistsError(FederationError):
|
|
37
|
+
"""Nome de workspace já registrado (UNIQUE)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WorkspaceSizeError(FederationError):
|
|
41
|
+
"""Federação exige >=2 membros — views não existem abaixo disso."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MemberNotFoundError(FederationError):
|
|
45
|
+
"""Repositório não é membro do workspace."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RepositoryNotFoundError(FederationError):
|
|
49
|
+
"""Repo id/path não corresponde a repositório indexado."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SnapshotNotFoundError(FederationError):
|
|
53
|
+
"""Snapshot inexistente (ou fora do repositório do membro)."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SnapshotNotReadyError(FederationError):
|
|
57
|
+
"""Snapshot existe mas não é legível (BUILDING/FAILED)."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InvalidPinError(FederationError):
|
|
61
|
+
"""Pin malformado ou fora do escopo do workspace."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InvalidKeyError(FederationError):
|
|
65
|
+
"""Chave de identidade federada malformada."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
MIN_FEDERATION_MEMBERS = 2
|
|
69
|
+
MAX_GRAPH_DEPTH = 8
|
|
70
|
+
DEFAULT_GRAPH_LIMIT = 500
|
|
71
|
+
MAX_GRAPH_LIMIT = 5000
|
|
72
|
+
|
|
73
|
+
FEDERATION_RESOLVER_ID = "federation-view"
|
|
74
|
+
|
|
75
|
+
# Versão do contrato hasheado em ``view_id``. Bump quando o payload da
|
|
76
|
+
# identidade muda — views computadas por outro formato nunca colidem,
|
|
77
|
+
# o que torna a invalidação de caches externos chaveados em ``view_id``
|
|
78
|
+
# honesta por construção (lf-0134: formato 1 era só os pares
|
|
79
|
+
# ``(repo_id, snapshot_id)`` do Cycle 7 — sem identidade do repo,
|
|
80
|
+
# versões de adapter/resolver nem schema).
|
|
81
|
+
VIEW_ID_FORMAT = 2
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class FederationWorkspace:
|
|
86
|
+
"""Agrupamento nomeado de repositórios (row de ``federation_workspaces``)."""
|
|
87
|
+
|
|
88
|
+
id: int
|
|
89
|
+
name: str
|
|
90
|
+
created_at: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class FederationMember:
|
|
95
|
+
"""Membro de um workspace (row de ``federation_members``).
|
|
96
|
+
|
|
97
|
+
``pinned_snapshot_id=None`` = membro segue o READY mais recente —
|
|
98
|
+
registrado na view a cada resolução.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
id: int
|
|
102
|
+
workspace_id: int
|
|
103
|
+
repository_id: int
|
|
104
|
+
pinned_snapshot_id: int | None
|
|
105
|
+
added_at: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class ViewMember:
|
|
110
|
+
"""Resolução de um membro dentro de uma ``FederatedView``.
|
|
111
|
+
|
|
112
|
+
``resolution`` declara a origem do snapshot escolhido — honesta por
|
|
113
|
+
construção: ``pin-override`` (request), ``pinned`` (persistido),
|
|
114
|
+
``latest-ready`` (default) ou ``no-ready-snapshot`` (ausente).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
repository_id: int
|
|
118
|
+
snapshot_id: int | None
|
|
119
|
+
pinned: bool
|
|
120
|
+
resolution: str # pin-override | pinned | latest-ready | no-ready-snapshot
|
|
121
|
+
snapshot_status: str = "" # status atual do snapshot escolhido
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class FederatedView:
|
|
126
|
+
"""Mapa ``{repository_id: snapshot_id}`` materializado — read-only.
|
|
127
|
+
|
|
128
|
+
``view_id`` é ``fedview:<sha256>`` do payload de identidade
|
|
129
|
+
completo (ver ``compute_view_id``) cobrindo TODOS os membros —
|
|
130
|
+
membros sem snapshot resolvido entram como ``null`` na posição de
|
|
131
|
+
snapshot: a ausência também faz parte da identidade da view.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
workspace_id: int
|
|
135
|
+
workspace_name: str
|
|
136
|
+
view_id: str
|
|
137
|
+
members: tuple[ViewMember, ...]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def snapshot_map(self) -> dict[int, int]:
|
|
141
|
+
"""``{repository_id: snapshot_id}`` dos membros resolvidos."""
|
|
142
|
+
return {
|
|
143
|
+
m.repository_id: m.snapshot_id
|
|
144
|
+
for m in self.members
|
|
145
|
+
if m.snapshot_id is not None
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict[str, object]:
|
|
149
|
+
return {
|
|
150
|
+
"workspace_id": self.workspace_id,
|
|
151
|
+
"workspace": self.workspace_name,
|
|
152
|
+
"view_id": self.view_id,
|
|
153
|
+
"members": [
|
|
154
|
+
{
|
|
155
|
+
"repository_id": m.repository_id,
|
|
156
|
+
"snapshot_id": m.snapshot_id,
|
|
157
|
+
"pinned": m.pinned,
|
|
158
|
+
"resolution": m.resolution,
|
|
159
|
+
"snapshot_status": m.snapshot_status,
|
|
160
|
+
}
|
|
161
|
+
for m in self.members
|
|
162
|
+
],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def compute_view_id(
|
|
167
|
+
pairs: Mapping[int, int | None],
|
|
168
|
+
*,
|
|
169
|
+
identities: Mapping[int, str] | None = None,
|
|
170
|
+
member_versions: Mapping[int, str] | None = None,
|
|
171
|
+
schema: int = 0,
|
|
172
|
+
) -> str:
|
|
173
|
+
"""Hash determinístico da identidade completa da view — a cache key.
|
|
174
|
+
|
|
175
|
+
Componentes (lf-0134, audit de cache):
|
|
176
|
+
|
|
177
|
+
- **membership**: ``repo_id`` de cada membro (chaves de ``pairs``)
|
|
178
|
+
— add/remove de membro muda a lista;
|
|
179
|
+
- **identidade do repo**: ``identities[repo_id]`` = ``identity_key``
|
|
180
|
+
lógica — um re-bind de identidade sob o mesmo ``repo_id`` invalida;
|
|
181
|
+
- **snapshot pins**: ``pairs[repo_id]`` (``None`` declarado) —
|
|
182
|
+
pin/override/READY novo muda a posição resolvida;
|
|
183
|
+
- **versões de resolver/adapter**: ``member_versions[repo_id]`` =
|
|
184
|
+
digest das ``analyzer_versions`` do snapshot resolvido (a versão
|
|
185
|
+
dos adapters/resolvers que produziram os dados) + o próprio
|
|
186
|
+
``FEDERATION_RESOLVER_ID``/``VIEW_ID_FORMAT`` do caminho de
|
|
187
|
+
leitura federado;
|
|
188
|
+
- **schema**: ``schema`` = ``schema_version`` do storage.
|
|
189
|
+
|
|
190
|
+
Ordenação por ``repo_id`` — mesma resolução, mesmo ``view_id``,
|
|
191
|
+
qualquer ordem de inserção. Componentes não fornecidos entram como
|
|
192
|
+
vazio/0 — degradação declarada, nunca fabricada.
|
|
193
|
+
"""
|
|
194
|
+
ids = dict(identities or {})
|
|
195
|
+
vers = dict(member_versions or {})
|
|
196
|
+
ordered = sorted(pairs.items(), key=lambda kv: int(kv[0]))
|
|
197
|
+
members = [
|
|
198
|
+
[
|
|
199
|
+
int(r),
|
|
200
|
+
str(ids.get(int(r), "")),
|
|
201
|
+
(None if s is None else int(s)),
|
|
202
|
+
str(vers.get(int(r), "")),
|
|
203
|
+
]
|
|
204
|
+
for r, s in ordered
|
|
205
|
+
]
|
|
206
|
+
payload = {
|
|
207
|
+
"format": VIEW_ID_FORMAT,
|
|
208
|
+
"resolver": FEDERATION_RESOLVER_ID,
|
|
209
|
+
"schema": int(schema),
|
|
210
|
+
"members": members,
|
|
211
|
+
}
|
|
212
|
+
digest = hashlib.sha256(
|
|
213
|
+
json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
214
|
+
).hexdigest()
|
|
215
|
+
return f"fedview:{digest}"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class SharedOccurrence:
|
|
220
|
+
"""Ocorrência de entidade de domínio vista pela federação."""
|
|
221
|
+
|
|
222
|
+
repository_id: int
|
|
223
|
+
snapshot_id: int
|
|
224
|
+
entity_id: int
|
|
225
|
+
occurrence_id: int
|
|
226
|
+
domain: str
|
|
227
|
+
kind: str
|
|
228
|
+
identity_key: str # chave lógica completa (repo-scoped)
|
|
229
|
+
shared_key: str # chave sem o segmento repository (join federado)
|
|
230
|
+
qualified_name: str
|
|
231
|
+
name: str
|
|
232
|
+
catalog: str
|
|
233
|
+
namespace: str
|
|
234
|
+
environment: str
|
|
235
|
+
path: str
|
|
236
|
+
line: int | None
|
|
237
|
+
col: int | None
|
|
238
|
+
extraction_status: str
|
|
239
|
+
|
|
240
|
+
def to_dict(self) -> dict[str, object]:
|
|
241
|
+
return {
|
|
242
|
+
"repository_id": self.repository_id,
|
|
243
|
+
"snapshot_id": self.snapshot_id,
|
|
244
|
+
"entity_id": self.entity_id,
|
|
245
|
+
"occurrence_id": self.occurrence_id,
|
|
246
|
+
"domain": self.domain,
|
|
247
|
+
"kind": self.kind,
|
|
248
|
+
"identity_key": self.identity_key,
|
|
249
|
+
"shared_key": self.shared_key,
|
|
250
|
+
"qualified_name": self.qualified_name,
|
|
251
|
+
"name": self.name,
|
|
252
|
+
"catalog": self.catalog,
|
|
253
|
+
"namespace": self.namespace,
|
|
254
|
+
"environment": self.environment,
|
|
255
|
+
"path": self.path,
|
|
256
|
+
"line": self.line,
|
|
257
|
+
"col": self.col,
|
|
258
|
+
"extraction_status": self.extraction_status,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class FederatedEdge:
|
|
264
|
+
"""Aresta do grafo federado.
|
|
265
|
+
|
|
266
|
+
``kind`` = ``intra`` (row real de ``domain_relations`` dentro de um
|
|
267
|
+
snapshot-membro) ou ``shared_entity`` (aresta computada: mesma
|
|
268
|
+
identidade compartilhada observada em repositórios distintos).
|
|
269
|
+
Arestas ``shared_entity`` são derivadas da view — nunca persistidas:
|
|
270
|
+
``computed=True`` declara a proveniência honesta.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
kind: str # intra | shared_entity
|
|
274
|
+
from_entity_id: int
|
|
275
|
+
to_entity_id: int
|
|
276
|
+
from_repo: int
|
|
277
|
+
to_repo: int
|
|
278
|
+
relation_type: str
|
|
279
|
+
resolution_status: str
|
|
280
|
+
confidence_class: str
|
|
281
|
+
resolver: str
|
|
282
|
+
relation_id: int | None = None
|
|
283
|
+
computed: bool = False
|
|
284
|
+
|
|
285
|
+
def to_dict(self) -> dict[str, object]:
|
|
286
|
+
return {
|
|
287
|
+
"kind": self.kind,
|
|
288
|
+
"relation_id": self.relation_id,
|
|
289
|
+
"from_entity_id": self.from_entity_id,
|
|
290
|
+
"to_entity_id": self.to_entity_id,
|
|
291
|
+
"from_repository_id": self.from_repo,
|
|
292
|
+
"to_repository_id": self.to_repo,
|
|
293
|
+
"type": self.relation_type,
|
|
294
|
+
"resolution_status": self.resolution_status,
|
|
295
|
+
"confidence_class": self.confidence_class,
|
|
296
|
+
"resolver": self.resolver,
|
|
297
|
+
"computed": self.computed,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass(frozen=True)
|
|
302
|
+
class EntityNode:
|
|
303
|
+
"""Nó do grafo federado — ``(repository_id, entity)`` observado."""
|
|
304
|
+
|
|
305
|
+
entity_id: int
|
|
306
|
+
repository_id: int
|
|
307
|
+
snapshot_id: int
|
|
308
|
+
domain: str
|
|
309
|
+
kind: str
|
|
310
|
+
identity_key: str
|
|
311
|
+
shared_key: str
|
|
312
|
+
qualified_name: str
|
|
313
|
+
name: str
|
|
314
|
+
paths: tuple[str, ...] = ()
|
|
315
|
+
|
|
316
|
+
def to_dict(self) -> dict[str, object]:
|
|
317
|
+
return {
|
|
318
|
+
"entity_id": self.entity_id,
|
|
319
|
+
"repository_id": self.repository_id,
|
|
320
|
+
"snapshot_id": self.snapshot_id,
|
|
321
|
+
"domain": self.domain,
|
|
322
|
+
"kind": self.kind,
|
|
323
|
+
"identity_key": self.identity_key,
|
|
324
|
+
"shared_key": self.shared_key,
|
|
325
|
+
"qualified_name": self.qualified_name,
|
|
326
|
+
"name": self.name,
|
|
327
|
+
"paths": list(self.paths),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True)
|
|
332
|
+
class FederatedGraph:
|
|
333
|
+
"""Resultado do traversal federado — nós/arestas + flags honestas."""
|
|
334
|
+
|
|
335
|
+
view: FederatedView
|
|
336
|
+
nodes: tuple[EntityNode, ...]
|
|
337
|
+
edges: tuple[FederatedEdge, ...]
|
|
338
|
+
depth: int
|
|
339
|
+
seeds: int
|
|
340
|
+
truncated: bool
|
|
341
|
+
stats: Mapping[str, int] = field(default_factory=dict)
|
|
342
|
+
|
|
343
|
+
def to_dict(self) -> dict[str, object]:
|
|
344
|
+
out = self.view.to_dict()
|
|
345
|
+
out.update(
|
|
346
|
+
{
|
|
347
|
+
"nodes": [n.to_dict() for n in self.nodes],
|
|
348
|
+
"edges": [e.to_dict() for e in self.edges],
|
|
349
|
+
"depth": self.depth,
|
|
350
|
+
"seed_entities": self.seeds,
|
|
351
|
+
"truncated": self.truncated,
|
|
352
|
+
"stats": dict(self.stats),
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
return out
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Persistência das tabelas de federação (lf-0118, schema v10).
|
|
2
|
+
|
|
3
|
+
Mesma disciplina de ``storage.store``: somente queries parametrizadas,
|
|
4
|
+
SQL constante, nenhuma concatenação. ``FederationStore`` cobre apenas as
|
|
5
|
+
tabelas v10 — leituras de ``repositories``/``snapshots``/domínio passam
|
|
6
|
+
pelo ``Store`` compartilhado (uma conexão, dois repositórios).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
|
|
15
|
+
from code_atlas.federation.model import (
|
|
16
|
+
FederationMember,
|
|
17
|
+
FederationWorkspace,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FederationStore:
|
|
22
|
+
"""CRUD mínimo de ``federation_workspaces``/``federation_members``."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, conn: sqlite3.Connection) -> None:
|
|
25
|
+
self._conn = conn
|
|
26
|
+
|
|
27
|
+
# --- workspaces -------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def create_workspace(self, name: str) -> int:
|
|
30
|
+
cur = self._conn.execute(
|
|
31
|
+
"INSERT INTO federation_workspaces (name) VALUES (?)", (name,)
|
|
32
|
+
)
|
|
33
|
+
return int(cur.lastrowid) # type: ignore[arg-type]
|
|
34
|
+
|
|
35
|
+
def workspace_by_id(self, workspace_id: int) -> FederationWorkspace | None:
|
|
36
|
+
row = self._conn.execute(
|
|
37
|
+
"SELECT id, name, created_at FROM federation_workspaces"
|
|
38
|
+
" WHERE id = ?",
|
|
39
|
+
(workspace_id,),
|
|
40
|
+
).fetchone()
|
|
41
|
+
return _ws_from_row(row) if row is not None else None
|
|
42
|
+
|
|
43
|
+
def workspace_by_name(self, name: str) -> FederationWorkspace | None:
|
|
44
|
+
row = self._conn.execute(
|
|
45
|
+
"SELECT id, name, created_at FROM federation_workspaces"
|
|
46
|
+
" WHERE name = ?",
|
|
47
|
+
(name,),
|
|
48
|
+
).fetchone()
|
|
49
|
+
return _ws_from_row(row) if row is not None else None
|
|
50
|
+
|
|
51
|
+
def list_workspaces(self) -> list[FederationWorkspace]:
|
|
52
|
+
rows = self._conn.execute(
|
|
53
|
+
"SELECT id, name, created_at FROM federation_workspaces"
|
|
54
|
+
" ORDER BY name"
|
|
55
|
+
).fetchall()
|
|
56
|
+
return [_ws_from_row(r) for r in rows]
|
|
57
|
+
|
|
58
|
+
def member_count(self, workspace_id: int) -> int:
|
|
59
|
+
row = self._conn.execute(
|
|
60
|
+
"SELECT COUNT(*) AS c FROM federation_members WHERE workspace_id = ?",
|
|
61
|
+
(workspace_id,),
|
|
62
|
+
).fetchone()
|
|
63
|
+
return int(row["c"])
|
|
64
|
+
|
|
65
|
+
# --- members ----------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def add_member(
|
|
68
|
+
self, workspace_id: int, repository_id: int
|
|
69
|
+
) -> int:
|
|
70
|
+
"""Registra repo no workspace (``INSERT OR IGNORE`` — idempotente).
|
|
71
|
+
|
|
72
|
+
Retorna a row id do membro (existente ou criada).
|
|
73
|
+
"""
|
|
74
|
+
self._conn.execute(
|
|
75
|
+
"INSERT OR IGNORE INTO federation_members"
|
|
76
|
+
" (workspace_id, repository_id) VALUES (?, ?)",
|
|
77
|
+
(workspace_id, repository_id),
|
|
78
|
+
)
|
|
79
|
+
row = self._conn.execute(
|
|
80
|
+
"SELECT id FROM federation_members"
|
|
81
|
+
" WHERE workspace_id = ? AND repository_id = ?",
|
|
82
|
+
(workspace_id, repository_id),
|
|
83
|
+
).fetchone()
|
|
84
|
+
assert row is not None # UNIQUE + INSERT OR IGNORE garante a linha
|
|
85
|
+
return int(row["id"])
|
|
86
|
+
|
|
87
|
+
def remove_member(self, workspace_id: int, repository_id: int) -> bool:
|
|
88
|
+
cur = self._conn.execute(
|
|
89
|
+
"DELETE FROM federation_members"
|
|
90
|
+
" WHERE workspace_id = ? AND repository_id = ?",
|
|
91
|
+
(workspace_id, repository_id),
|
|
92
|
+
)
|
|
93
|
+
return int(cur.rowcount or 0) > 0
|
|
94
|
+
|
|
95
|
+
def member_for(
|
|
96
|
+
self, workspace_id: int, repository_id: int
|
|
97
|
+
) -> FederationMember | None:
|
|
98
|
+
row = self._conn.execute(
|
|
99
|
+
"SELECT id, workspace_id, repository_id, pinned_snapshot_id,"
|
|
100
|
+
" added_at FROM federation_members"
|
|
101
|
+
" WHERE workspace_id = ? AND repository_id = ?",
|
|
102
|
+
(workspace_id, repository_id),
|
|
103
|
+
).fetchone()
|
|
104
|
+
return _member_from_row(row) if row is not None else None
|
|
105
|
+
|
|
106
|
+
def list_members(self, workspace_id: int) -> list[FederationMember]:
|
|
107
|
+
rows = self._conn.execute(
|
|
108
|
+
"SELECT id, workspace_id, repository_id, pinned_snapshot_id,"
|
|
109
|
+
" added_at FROM federation_members"
|
|
110
|
+
" WHERE workspace_id = ? ORDER BY repository_id",
|
|
111
|
+
(workspace_id,),
|
|
112
|
+
).fetchall()
|
|
113
|
+
return [_member_from_row(r) for r in rows]
|
|
114
|
+
|
|
115
|
+
def set_pinned_snapshot(
|
|
116
|
+
self, workspace_id: int, repository_id: int, snapshot_id: int | None
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Grava (ou limpa, ``None``) o pin do membro — validação é do engine."""
|
|
119
|
+
self._conn.execute(
|
|
120
|
+
"UPDATE federation_members SET pinned_snapshot_id = ?"
|
|
121
|
+
" WHERE workspace_id = ? AND repository_id = ?",
|
|
122
|
+
(snapshot_id, workspace_id, repository_id),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def repo_identities(
|
|
126
|
+
self, repository_ids: Iterable[int]
|
|
127
|
+
) -> dict[int, str]:
|
|
128
|
+
"""``{repository_id: identity_key}`` — identidade lógica do membro.
|
|
129
|
+
|
|
130
|
+
Componente "repo identity" da cache key da view (lf-0134): um
|
|
131
|
+
re-bind de ``identity_key`` sob o mesmo ``repo_id`` muda a
|
|
132
|
+
identidade resolvida mesmo com snapshots inalterados.
|
|
133
|
+
"""
|
|
134
|
+
ids = sorted({int(r) for r in repository_ids})
|
|
135
|
+
if not ids:
|
|
136
|
+
return {}
|
|
137
|
+
# SQL constante (auditoria AST): IN variável via json_each —
|
|
138
|
+
# mesmo padrão de hidratação em lote do resto do código.
|
|
139
|
+
rows = self._conn.execute(
|
|
140
|
+
"SELECT id, identity_key FROM repositories"
|
|
141
|
+
" WHERE id IN (SELECT value FROM json_each(?))",
|
|
142
|
+
(json.dumps(ids),),
|
|
143
|
+
).fetchall()
|
|
144
|
+
return {int(r["id"]): str(r["identity_key"]) for r in rows}
|
|
145
|
+
|
|
146
|
+
# --- leitura federada ------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def snapshot_entity_occurrences(
|
|
149
|
+
self, snapshot_id: int
|
|
150
|
+
) -> list[sqlite3.Row]:
|
|
151
|
+
"""Ocorrências de entidade de domínio de UM snapshot membro.
|
|
152
|
+
|
|
153
|
+
Mesma forma de ``Store.domain_occurrences_snapshot`` com
|
|
154
|
+
``line``/``col`` incluídos — a leitura federada nunca mistura
|
|
155
|
+
snapshots: o ``WHERE`` é sempre o ``snapshot_id`` resolvido para
|
|
156
|
+
aquele repo-membro.
|
|
157
|
+
"""
|
|
158
|
+
return self._conn.execute(
|
|
159
|
+
"""
|
|
160
|
+
SELECT deo.id AS occ_id, deo.entity_id, deo.line, deo.col,
|
|
161
|
+
deo.extraction_status,
|
|
162
|
+
de.domain, de.kind, de.identity_key, de.qualified_name,
|
|
163
|
+
de.name, de.catalog, de.namespace, de.environment,
|
|
164
|
+
sf.relative_path AS source_path
|
|
165
|
+
FROM domain_entity_occurrences deo
|
|
166
|
+
JOIN domain_entities de ON de.id = deo.entity_id
|
|
167
|
+
LEFT JOIN source_files sf ON sf.id = deo.source_file_id
|
|
168
|
+
WHERE deo.snapshot_id = ?
|
|
169
|
+
ORDER BY deo.id
|
|
170
|
+
""",
|
|
171
|
+
(snapshot_id,),
|
|
172
|
+
).fetchall()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _ws_from_row(row: sqlite3.Row) -> FederationWorkspace:
|
|
176
|
+
return FederationWorkspace(
|
|
177
|
+
id=int(row["id"]),
|
|
178
|
+
name=str(row["name"]),
|
|
179
|
+
created_at=str(row["created_at"]),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _member_from_row(row: sqlite3.Row) -> FederationMember:
|
|
184
|
+
return FederationMember(
|
|
185
|
+
id=int(row["id"]),
|
|
186
|
+
workspace_id=int(row["workspace_id"]),
|
|
187
|
+
repository_id=int(row["repository_id"]),
|
|
188
|
+
pinned_snapshot_id=(
|
|
189
|
+
None
|
|
190
|
+
if row["pinned_snapshot_id"] is None
|
|
191
|
+
else int(row["pinned_snapshot_id"])
|
|
192
|
+
),
|
|
193
|
+
added_at=str(row["added_at"]),
|
|
194
|
+
)
|
code_atlas/gitbin.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Único módulo autorizado a invocar subprocess no pacote (audit lf-0005).
|
|
2
|
+
|
|
3
|
+
Toda chamada externa do Code Atlas passa por aqui: argv sem shell, somente o
|
|
4
|
+
binário `git` resolvido via ``shutil.which``. Nenhum outro módulo de ``src/``
|
|
5
|
+
pode importar ``subprocess`` — o teste de confinamento reprova.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
GIT_TIMEOUT_S = 30.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class GitResult:
|
|
20
|
+
"""Resultado de uma invocação `git` (rc -1 = falha de execução/timeout)."""
|
|
21
|
+
|
|
22
|
+
returncode: int
|
|
23
|
+
stdout: bytes
|
|
24
|
+
stderr: bytes = b""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def git_binary() -> str | None:
|
|
28
|
+
"""Caminho do binário `git` ou None se ausente."""
|
|
29
|
+
return shutil.which("git")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_git(args: list[str], cwd: Path | None = None) -> GitResult:
|
|
33
|
+
"""Invoca `git` com argv seguro; nunca levanta por timeout/OS error."""
|
|
34
|
+
binary = git_binary()
|
|
35
|
+
if binary is None:
|
|
36
|
+
return GitResult(returncode=-1, stdout=b"", stderr=b"git absent")
|
|
37
|
+
argv = [binary, *args] if cwd is None else [binary, "-C", str(cwd), *args]
|
|
38
|
+
try:
|
|
39
|
+
out = subprocess.run(argv, capture_output=True, timeout=GIT_TIMEOUT_S, check=False)
|
|
40
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
41
|
+
return GitResult(returncode=-1, stdout=b"", stderr=str(exc).encode())
|
|
42
|
+
return GitResult(returncode=out.returncode, stdout=out.stdout, stderr=out.stderr)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def git_version() -> str:
|
|
46
|
+
"""`git --version` ou string vazia em ausência/falha."""
|
|
47
|
+
out = run_git(["--version"])
|
|
48
|
+
return out.stdout.decode("utf-8", "replace").strip()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Grafo canônico de código (Cycle 2 Phase 4).
|
|
2
|
+
|
|
3
|
+
O builder é puro e determinístico: recebe ``ParseResult``s + inventário e
|
|
4
|
+
produz ``GraphBuildResult`` — persistência é responsabilidade do storage
|
|
5
|
+
(lf-0011). Nenhum caminho aqui toca SQLite ou tree_sitter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from code_atlas.graph.builder import GraphBuilder
|
|
9
|
+
from code_atlas.graph.identity import edge_key, node_id
|
|
10
|
+
from code_atlas.graph.queries import GraphView
|
|
11
|
+
from code_atlas.graph.validation import ValidationError, validate_graph
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"GraphBuilder",
|
|
15
|
+
"GraphView",
|
|
16
|
+
"ValidationError",
|
|
17
|
+
"edge_key",
|
|
18
|
+
"node_id",
|
|
19
|
+
"validate_graph",
|
|
20
|
+
]
|