code-atlas-forge 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. code_atlas/__init__.py +6 -0
  2. code_atlas/__main__.py +6 -0
  3. code_atlas/api/__init__.py +5 -0
  4. code_atlas/api/handlers.py +1514 -0
  5. code_atlas/api/server.py +394 -0
  6. code_atlas/cli.py +2084 -0
  7. code_atlas/config.py +149 -0
  8. code_atlas/context/__init__.py +66 -0
  9. code_atlas/context/contracts.py +277 -0
  10. code_atlas/context/domain.py +760 -0
  11. code_atlas/context/engine.py +442 -0
  12. code_atlas/context/expand.py +133 -0
  13. code_atlas/context/renderers.py +71 -0
  14. code_atlas/context/tokenizer.py +37 -0
  15. code_atlas/contracts/__init__.py +38 -0
  16. code_atlas/contracts/enums.py +67 -0
  17. code_atlas/contracts/models.py +175 -0
  18. code_atlas/contracts/ports.py +70 -0
  19. code_atlas/discovery/__init__.py +25 -0
  20. code_atlas/discovery/gitignore.py +119 -0
  21. code_atlas/discovery/identity.py +52 -0
  22. code_atlas/discovery/models.py +204 -0
  23. code_atlas/discovery/scanner.py +495 -0
  24. code_atlas/domains/__init__.py +107 -0
  25. code_atlas/domains/api/__init__.py +28 -0
  26. code_atlas/domains/api/adapter.py +405 -0
  27. code_atlas/domains/api/common.py +308 -0
  28. code_atlas/domains/api/consumers.py +224 -0
  29. code_atlas/domains/api/emit.py +231 -0
  30. code_atlas/domains/api/openapi.py +398 -0
  31. code_atlas/domains/api/routes.py +582 -0
  32. code_atlas/domains/api/yaml_subset.py +683 -0
  33. code_atlas/domains/aws/__init__.py +28 -0
  34. code_atlas/domains/aws/adapter.py +382 -0
  35. code_atlas/domains/aws/common.py +261 -0
  36. code_atlas/domains/aws/configs.py +1164 -0
  37. code_atlas/domains/aws/emit.py +411 -0
  38. code_atlas/domains/aws/pyfiles.py +2220 -0
  39. code_atlas/domains/aws/sqlbridge.py +448 -0
  40. code_atlas/domains/cicd/__init__.py +50 -0
  41. code_atlas/domains/cicd/adapter.py +762 -0
  42. code_atlas/domains/cicd/classify.py +136 -0
  43. code_atlas/domains/cicd/workflow.py +504 -0
  44. code_atlas/domains/cicd/yamlmini.py +946 -0
  45. code_atlas/domains/cli_surface.py +739 -0
  46. code_atlas/domains/contracts.py +274 -0
  47. code_atlas/domains/crossresolve.py +1208 -0
  48. code_atlas/domains/evidence.py +57 -0
  49. code_atlas/domains/identity.py +149 -0
  50. code_atlas/domains/model.py +202 -0
  51. code_atlas/domains/registry.py +250 -0
  52. code_atlas/domains/resolution.py +65 -0
  53. code_atlas/domains/spark/__init__.py +31 -0
  54. code_atlas/domains/spark/adapter.py +146 -0
  55. code_atlas/domains/spark/emit.py +344 -0
  56. code_atlas/domains/spark/extract.py +108 -0
  57. code_atlas/domains/spark/sqllit.py +192 -0
  58. code_atlas/domains/spark/walk.py +1943 -0
  59. code_atlas/domains/sql/__init__.py +30 -0
  60. code_atlas/domains/sql/adapter.py +382 -0
  61. code_atlas/domains/sql/extract.py +1271 -0
  62. code_atlas/domains/sql/nodes.py +264 -0
  63. code_atlas/domains/sql/parser.py +1510 -0
  64. code_atlas/domains/sql/tokens.py +318 -0
  65. code_atlas/domains/summarize.py +126 -0
  66. code_atlas/domains/terraform/__init__.py +20 -0
  67. code_atlas/domains/terraform/adapter.py +127 -0
  68. code_atlas/domains/terraform/catalog.py +43 -0
  69. code_atlas/domains/terraform/extract.py +1277 -0
  70. code_atlas/domains/terraform/hcl.py +538 -0
  71. code_atlas/domains/terraform/refs.py +181 -0
  72. code_atlas/domains/terraform/tfjson.py +163 -0
  73. code_atlas/domains/terraform/tfvars.py +98 -0
  74. code_atlas/domains/validation.py +120 -0
  75. code_atlas/env_override.py +27 -0
  76. code_atlas/eval/__init__.py +26 -0
  77. code_atlas/eval/domain_eval.py +681 -0
  78. code_atlas/eval/ground_truth.py +688 -0
  79. code_atlas/eval/impact_truth.py +758 -0
  80. code_atlas/eval/metrics.py +178 -0
  81. code_atlas/eval/pack_value.py +610 -0
  82. code_atlas/federation/__init__.py +72 -0
  83. code_atlas/federation/federation.py +676 -0
  84. code_atlas/federation/model.py +355 -0
  85. code_atlas/federation/store.py +194 -0
  86. code_atlas/gitbin.py +48 -0
  87. code_atlas/graph/__init__.py +20 -0
  88. code_atlas/graph/builder.py +378 -0
  89. code_atlas/graph/identity.py +54 -0
  90. code_atlas/graph/queries.py +47 -0
  91. code_atlas/graph/validation.py +92 -0
  92. code_atlas/impact/__init__.py +37 -0
  93. code_atlas/impact/blast_radius.py +171 -0
  94. code_atlas/impact/contract_analyzer.py +104 -0
  95. code_atlas/impact/contracts.py +315 -0
  96. code_atlas/impact/diff_reader.py +142 -0
  97. code_atlas/impact/domain_traversal.py +768 -0
  98. code_atlas/impact/engine.py +392 -0
  99. code_atlas/impact/packs.py +128 -0
  100. code_atlas/impact/report.py +136 -0
  101. code_atlas/impact/revision_binding.py +220 -0
  102. code_atlas/impact/risk_classifier.py +83 -0
  103. code_atlas/impact/snapshot_resolver.py +136 -0
  104. code_atlas/impact/symbol_diff.py +210 -0
  105. code_atlas/impact/test_selector.py +166 -0
  106. code_atlas/indexing/__init__.py +5 -0
  107. code_atlas/indexing/cache.py +396 -0
  108. code_atlas/indexing/change_detector.py +278 -0
  109. code_atlas/indexing/coordinator.py +1032 -0
  110. code_atlas/indexing/dependency_planner.py +268 -0
  111. code_atlas/indexing/persist.py +436 -0
  112. code_atlas/indexing/reuse.py +85 -0
  113. code_atlas/install/__init__.py +7 -0
  114. code_atlas/install/cli.py +168 -0
  115. code_atlas/install/lifecycle.py +286 -0
  116. code_atlas/mcp.py +674 -0
  117. code_atlas/observability/__init__.py +19 -0
  118. code_atlas/observability/logging.py +199 -0
  119. code_atlas/parsers/__init__.py +43 -0
  120. code_atlas/parsers/contracts.py +209 -0
  121. code_atlas/parsers/engine.py +125 -0
  122. code_atlas/parsers/languages/__init__.py +1 -0
  123. code_atlas/parsers/languages/python.py +391 -0
  124. code_atlas/parsers/languages/tsx.py +28 -0
  125. code_atlas/parsers/languages/typescript.py +909 -0
  126. code_atlas/parsers/registry.py +51 -0
  127. code_atlas/parsers/tree_sitter_runtime.py +118 -0
  128. code_atlas/resolvers/__init__.py +33 -0
  129. code_atlas/resolvers/bootstrap.py +246 -0
  130. code_atlas/resolvers/capabilities.py +33 -0
  131. code_atlas/resolvers/contracts.py +133 -0
  132. code_atlas/resolvers/coordinator.py +96 -0
  133. code_atlas/resolvers/enrichment.py +318 -0
  134. code_atlas/resolvers/lsp/__init__.py +26 -0
  135. code_atlas/resolvers/lsp/adapter.py +256 -0
  136. code_atlas/resolvers/lsp/lifecycle.py +251 -0
  137. code_atlas/resolvers/lsp/spawn.py +92 -0
  138. code_atlas/resolvers/lsp/transport.py +240 -0
  139. code_atlas/resolvers/registry.py +33 -0
  140. code_atlas/resolvers/scip/__init__.py +49 -0
  141. code_atlas/resolvers/scip/decoder.py +216 -0
  142. code_atlas/resolvers/scip/importer.py +248 -0
  143. code_atlas/resolvers/scip/loader.py +90 -0
  144. code_atlas/resolvers/scip/mapper.py +271 -0
  145. code_atlas/resolvers/scip/provenance.py +211 -0
  146. code_atlas/resolvers/scip/resolver.py +289 -0
  147. code_atlas/resolvers/scip/validation.py +101 -0
  148. code_atlas/resolvers/syntactic/__init__.py +8 -0
  149. code_atlas/resolvers/syntactic/resolver.py +254 -0
  150. code_atlas/search/__init__.py +32 -0
  151. code_atlas/search/contracts.py +176 -0
  152. code_atlas/search/domain.py +561 -0
  153. code_atlas/search/engine.py +530 -0
  154. code_atlas/search/federated.py +276 -0
  155. code_atlas/search/graph.py +282 -0
  156. code_atlas/search/hybrid.py +151 -0
  157. code_atlas/search/intent.py +48 -0
  158. code_atlas/search/lexical.py +176 -0
  159. code_atlas/search/refs.py +359 -0
  160. code_atlas/search/structural.py +505 -0
  161. code_atlas/search/tokenizer.py +43 -0
  162. code_atlas/search/vector/__init__.py +35 -0
  163. code_atlas/search/vector/contracts.py +76 -0
  164. code_atlas/search/vector/embeddings.py +96 -0
  165. code_atlas/search/vector/index.py +169 -0
  166. code_atlas/search/vector/provider.py +50 -0
  167. code_atlas/storage/__init__.py +14 -0
  168. code_atlas/storage/db.py +47 -0
  169. code_atlas/storage/migrations.py +552 -0
  170. code_atlas/storage/store.py +1045 -0
  171. code_atlas/ui/__init__.py +29 -0
  172. code_atlas/ui/i18n.py +143 -0
  173. code_atlas/ui/kit.py +689 -0
  174. code_atlas/ui/wizard.py +317 -0
  175. code_atlas_forge-1.1.0.dist-info/METADATA +246 -0
  176. code_atlas_forge-1.1.0.dist-info/RECORD +181 -0
  177. code_atlas_forge-1.1.0.dist-info/WHEEL +5 -0
  178. code_atlas_forge-1.1.0.dist-info/entry_points.txt +2 -0
  179. code_atlas_forge-1.1.0.dist-info/licenses/LICENSE +202 -0
  180. code_atlas_forge-1.1.0.dist-info/licenses/NOTICE +17 -0
  181. code_atlas_forge-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ ]