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,552 @@
1
+ """Runner de migrações versionadas (Blueprint §6).
2
+
3
+ Cada migração é ``(version, name, sql)``; aplicação é sequencial,
4
+ transacional e idempotente (versões aplicadas são puladas). O checksum do
5
+ SQL aplicado é gravado em ``schema_migrations`` e validado em execuções
6
+ posteriores — drift levanta ``MigrationError``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import sqlite3
13
+ from collections.abc import Iterator
14
+ from dataclasses import dataclass
15
+
16
+
17
+ class MigrationError(Exception):
18
+ """Migração inválida ou drift de schema detectado."""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Migration:
23
+ version: int
24
+ name: str
25
+ sql: str
26
+
27
+
28
+ _SCHEMA_V1 = """
29
+ CREATE TABLE repositories (
30
+ id INTEGER PRIMARY KEY,
31
+ origin TEXT NOT NULL DEFAULT '',
32
+ root_handle TEXT NOT NULL,
33
+ git_provider TEXT NOT NULL DEFAULT '',
34
+ default_branch TEXT NOT NULL DEFAULT '',
35
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
36
+ );
37
+
38
+ CREATE TABLE snapshots (
39
+ id INTEGER PRIMARY KEY,
40
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
41
+ git_ref TEXT NOT NULL DEFAULT '',
42
+ commit_sha TEXT,
43
+ worktree_digest TEXT NOT NULL,
44
+ analyzer_versions TEXT NOT NULL DEFAULT '{}',
45
+ parent_snapshot_id INTEGER REFERENCES snapshots(id),
46
+ status TEXT NOT NULL CHECK (status IN ('BUILDING','READY','FAILED','STALE')),
47
+ error TEXT,
48
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
49
+ );
50
+
51
+ CREATE TABLE source_files (
52
+ id INTEGER PRIMARY KEY,
53
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
54
+ relative_path TEXT NOT NULL,
55
+ language TEXT NOT NULL,
56
+ sha256 TEXT,
57
+ size_bytes INTEGER NOT NULL DEFAULT 0,
58
+ ignored INTEGER NOT NULL DEFAULT 0,
59
+ UNIQUE (repository_id, relative_path)
60
+ );
61
+
62
+ CREATE TABLE file_inventory (
63
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
64
+ source_file_id INTEGER NOT NULL REFERENCES source_files(id),
65
+ sha256 TEXT,
66
+ size_bytes INTEGER NOT NULL DEFAULT 0,
67
+ ignored INTEGER NOT NULL DEFAULT 0,
68
+ PRIMARY KEY (snapshot_id, source_file_id)
69
+ );
70
+
71
+ CREATE TABLE symbols (
72
+ id INTEGER PRIMARY KEY,
73
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
74
+ language TEXT NOT NULL,
75
+ qualified_name TEXT NOT NULL,
76
+ kind TEXT NOT NULL,
77
+ UNIQUE (repository_id, language, qualified_name, kind)
78
+ );
79
+
80
+ CREATE TABLE symbol_occurrences (
81
+ id INTEGER PRIMARY KEY,
82
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
83
+ logical_symbol_id INTEGER NOT NULL REFERENCES symbols(id),
84
+ source_file_id INTEGER NOT NULL REFERENCES source_files(id),
85
+ span_start INTEGER,
86
+ span_end INTEGER,
87
+ line INTEGER,
88
+ col INTEGER,
89
+ content_hash TEXT,
90
+ parse_status TEXT NOT NULL DEFAULT 'parsed'
91
+ );
92
+
93
+ CREATE TABLE relations (
94
+ id INTEGER PRIMARY KEY,
95
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
96
+ from_id INTEGER NOT NULL,
97
+ to_id INTEGER,
98
+ to_unresolved TEXT,
99
+ type TEXT NOT NULL CHECK (type IN (
100
+ 'CONTAINS','DEFINES','IMPORTS','CALLS','IMPLEMENTS',
101
+ 'EXTENDS','REFERENCES','CONFIGURES','TESTS')),
102
+ resolution_status TEXT NOT NULL CHECK (resolution_status IN (
103
+ 'resolved','ambiguous','unresolved','external')),
104
+ confidence_class TEXT NOT NULL CHECK (confidence_class IN (
105
+ 'resolved','syntactic','heuristic','inferred')),
106
+ resolver TEXT NOT NULL,
107
+ evidence_ids TEXT NOT NULL DEFAULT '[]'
108
+ );
109
+
110
+ CREATE TABLE evidence (
111
+ id INTEGER PRIMARY KEY,
112
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
113
+ source_file_id INTEGER NOT NULL REFERENCES source_files(id),
114
+ path TEXT NOT NULL,
115
+ span TEXT,
116
+ content_hash TEXT NOT NULL,
117
+ extractor TEXT NOT NULL,
118
+ extractor_version TEXT NOT NULL,
119
+ notes TEXT NOT NULL DEFAULT ''
120
+ );
121
+
122
+ CREATE TABLE index_jobs (
123
+ id INTEGER PRIMARY KEY,
124
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
125
+ snapshot_id INTEGER REFERENCES snapshots(id),
126
+ status TEXT NOT NULL CHECK (status IN ('running','success','failed','noop')),
127
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
128
+ finished_at TEXT,
129
+ counts TEXT NOT NULL DEFAULT '{}',
130
+ failures TEXT NOT NULL DEFAULT '[]',
131
+ manifest_hash TEXT
132
+ );
133
+
134
+ CREATE INDEX idx_snapshots_repo_status ON snapshots (repository_id, status);
135
+ CREATE INDEX idx_file_inventory_snapshot ON file_inventory (snapshot_id);
136
+ CREATE INDEX idx_source_files_repo_path ON source_files (repository_id, relative_path);
137
+ CREATE INDEX idx_occurrences_snap_file ON symbol_occurrences (snapshot_id, source_file_id);
138
+ CREATE INDEX idx_relations_from ON relations (from_id, type);
139
+ CREATE INDEX idx_relations_to ON relations (to_id, type);
140
+ """
141
+
142
+ _SCHEMA_V2_FTS = """
143
+ CREATE VIRTUAL TABLE fts_symbols USING fts5(
144
+ qualified_name, language, kind, content=''
145
+ );
146
+ """
147
+
148
+ _SCHEMA_V3_INDEXES = """
149
+ CREATE INDEX IF NOT EXISTS idx_relations_snapshot ON relations (snapshot_id, type);
150
+ CREATE INDEX IF NOT EXISTS idx_occurrences_snapshot_symbol
151
+ ON symbol_occurrences (snapshot_id, logical_symbol_id);
152
+ CREATE INDEX IF NOT EXISTS idx_evidence_snapshot ON evidence (snapshot_id);
153
+ """
154
+
155
+ _SCHEMA_V4_CACHE = """
156
+ CREATE TABLE parse_result_cache (
157
+ id INTEGER PRIMARY KEY,
158
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
159
+ path TEXT NOT NULL,
160
+ content_sha256 TEXT NOT NULL,
161
+ language TEXT NOT NULL,
162
+ parser_version TEXT NOT NULL,
163
+ grammar_version TEXT NOT NULL,
164
+ extractor_version TEXT NOT NULL,
165
+ resolver_version TEXT NOT NULL,
166
+ builder_version TEXT NOT NULL,
167
+ config_digest TEXT NOT NULL,
168
+ payload TEXT NOT NULL,
169
+ payload_sha256 TEXT NOT NULL,
170
+ last_used_snapshot_id INTEGER,
171
+ hit_count INTEGER NOT NULL DEFAULT 0,
172
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
173
+ UNIQUE (repository_id, path, content_sha256, language,
174
+ parser_version, grammar_version, extractor_version,
175
+ resolver_version, builder_version, config_digest)
176
+ );
177
+ CREATE INDEX idx_cache_repo_path ON parse_result_cache (repository_id, path);
178
+ """
179
+
180
+ _SCHEMA_V5_IDENTITY = """
181
+ ALTER TABLE repositories ADD COLUMN identity_key TEXT NOT NULL DEFAULT '';
182
+ UPDATE repositories SET identity_key = 'path:' || root_handle
183
+ WHERE identity_key = '';
184
+ CREATE UNIQUE INDEX uq_repositories_identity ON repositories (identity_key);
185
+ ALTER TABLE snapshots ADD COLUMN dirty INTEGER NOT NULL DEFAULT 0;
186
+ """
187
+
188
+ _SCHEMA_V6_EVIDENCE = """
189
+ ALTER TABLE evidence ADD COLUMN resolver_id TEXT NOT NULL DEFAULT '';
190
+ ALTER TABLE evidence ADD COLUMN resolver_version TEXT NOT NULL DEFAULT '';
191
+ ALTER TABLE evidence ADD COLUMN evidence_tier INTEGER NOT NULL DEFAULT 0;
192
+ ALTER TABLE evidence ADD COLUMN index_hash TEXT NOT NULL DEFAULT '';
193
+ ALTER TABLE evidence ADD COLUMN validation_status TEXT NOT NULL DEFAULT '';
194
+ """
195
+
196
+ _SCHEMA_V7_FTS_OCC = """
197
+ CREATE VIRTUAL TABLE fts_occurrences USING fts5(
198
+ snapshot_id UNINDEXED,
199
+ name,
200
+ terms,
201
+ signature,
202
+ docstring,
203
+ path,
204
+ language UNINDEXED,
205
+ kind UNINDEXED
206
+ );
207
+ """
208
+
209
+ _SCHEMA_V8_VECTORS = """
210
+ CREATE TABLE vector_embeddings (
211
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
212
+ occurrence_id INTEGER NOT NULL REFERENCES symbol_occurrences(id) ON DELETE CASCADE,
213
+ symbol_key TEXT NOT NULL,
214
+ model_id TEXT NOT NULL,
215
+ model_version TEXT NOT NULL,
216
+ dim INTEGER NOT NULL,
217
+ content_hash TEXT NOT NULL,
218
+ vector BLOB NOT NULL,
219
+ PRIMARY KEY (snapshot_id, occurrence_id, model_id)
220
+ );
221
+ CREATE INDEX idx_vector_embeddings_snap ON vector_embeddings(snapshot_id, model_id);
222
+ CREATE TABLE embedding_cache (
223
+ model_id TEXT NOT NULL,
224
+ model_version TEXT NOT NULL,
225
+ content_hash TEXT NOT NULL,
226
+ dim INTEGER NOT NULL,
227
+ vector BLOB NOT NULL,
228
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
229
+ PRIMARY KEY (model_id, model_version, content_hash)
230
+ );
231
+ """
232
+
233
+
234
+ _SCHEMA_V9_DOMAINS = """
235
+ -- Federação de domínios (lf-0110 / ADR-0010): tabelas dedicadas porque
236
+ -- (a) relations.type tem CHECK que não aceita o vocabulário de domínio e
237
+ -- SQLite não altera CHECK sem rebuild; (b) a identidade de entidades de
238
+ -- domínio inclui catalog/namespace/environment, inexpressável no UNIQUE
239
+ -- de symbols (repo, language, qualified_name, kind). Aditiva: bancos
240
+ -- antigos abrem e ganham as três tabelas vazias.
241
+
242
+ CREATE TABLE domain_entities (
243
+ id INTEGER PRIMARY KEY,
244
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
245
+ domain TEXT NOT NULL,
246
+ kind TEXT NOT NULL,
247
+ identity_key TEXT NOT NULL,
248
+ qualified_name TEXT NOT NULL,
249
+ name TEXT NOT NULL DEFAULT '',
250
+ catalog TEXT NOT NULL DEFAULT '',
251
+ namespace TEXT NOT NULL DEFAULT '',
252
+ environment TEXT NOT NULL DEFAULT '',
253
+ attributes TEXT NOT NULL DEFAULT '{}',
254
+ UNIQUE (repository_id, identity_key)
255
+ );
256
+
257
+ CREATE TABLE domain_entity_occurrences (
258
+ id INTEGER PRIMARY KEY,
259
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
260
+ entity_id INTEGER NOT NULL REFERENCES domain_entities(id),
261
+ source_file_id INTEGER REFERENCES source_files(id),
262
+ span_start INTEGER,
263
+ span_end INTEGER,
264
+ line INTEGER,
265
+ col INTEGER,
266
+ content_hash TEXT,
267
+ attributes TEXT NOT NULL DEFAULT '{}',
268
+ extraction_status TEXT NOT NULL DEFAULT 'extracted'
269
+ );
270
+
271
+ CREATE TABLE domain_relations (
272
+ id INTEGER PRIMARY KEY,
273
+ snapshot_id INTEGER NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE,
274
+ from_entity_occ_id INTEGER REFERENCES domain_entity_occurrences(id),
275
+ from_symbol_occ_id INTEGER REFERENCES symbol_occurrences(id),
276
+ to_entity_occ_id INTEGER REFERENCES domain_entity_occurrences(id),
277
+ to_symbol_occ_id INTEGER REFERENCES symbol_occurrences(id),
278
+ to_unresolved TEXT,
279
+ type TEXT NOT NULL CHECK (type IN (
280
+ 'CONTAINS','DEFINES','IMPORTS','CALLS','IMPLEMENTS',
281
+ 'EXTENDS','REFERENCES','CONFIGURES','TESTS',
282
+ 'READS_FROM','WRITES_TO','TRANSFORMS','DERIVES_FROM',
283
+ 'DEPENDS_ON','CALLS_ENDPOINT','IMPLEMENTS_CONTRACT',
284
+ 'PROVISIONS','USES_RESOURCE','ASSUMES_ROLE','TRIGGERS','DEPLOYS')),
285
+ resolution_status TEXT NOT NULL CHECK (resolution_status IN (
286
+ 'resolved','ambiguous','unresolved','external')),
287
+ confidence_class TEXT NOT NULL CHECK (confidence_class IN (
288
+ 'resolved','syntactic','heuristic','inferred')),
289
+ resolver TEXT NOT NULL,
290
+ evidence_ids TEXT NOT NULL DEFAULT '[]',
291
+ CHECK (from_entity_occ_id IS NOT NULL OR from_symbol_occ_id IS NOT NULL),
292
+ CHECK (to_entity_occ_id IS NOT NULL OR to_symbol_occ_id IS NOT NULL
293
+ OR to_unresolved IS NOT NULL)
294
+ );
295
+
296
+ CREATE INDEX idx_domain_entities_repo
297
+ ON domain_entities (repository_id, domain, kind);
298
+ CREATE INDEX idx_domain_occ_snapshot
299
+ ON domain_entity_occurrences (snapshot_id, entity_id);
300
+ CREATE INDEX idx_domain_occ_entity
301
+ ON domain_entity_occurrences (entity_id);
302
+ CREATE INDEX idx_domain_relations_from
303
+ ON domain_relations (from_entity_occ_id, type);
304
+ CREATE INDEX idx_domain_relations_to
305
+ ON domain_relations (to_entity_occ_id, type);
306
+ CREATE INDEX idx_domain_relations_snap
307
+ ON domain_relations (snapshot_id, type);
308
+ """
309
+
310
+
311
+ _SCHEMA_V10_FEDERATION = """
312
+ -- Workspaces federados (lf-0118 / Cycle 7 Phase 10): agrupa >=2
313
+ -- repositórios já indexados numa "view" multi-repo. Estritamente
314
+ -- aditiva — bancos antigos abrem e ganham as duas tabelas vazias.
315
+ --
316
+ -- ``federation_members.pinned_snapshot_id`` é NULLABLE e carrega
317
+ -- ``ON DELETE SET NULL``: quando o GC de retenção remove um snapshot
318
+ -- STALE que estava pinado, o pin é limpo e o membro volta a seguir o
319
+ -- READY mais recente — nunca FK dangling nem ponteiro para dados
320
+ -- removidos. ``repository_id`` não tem cascade: repositórios não são
321
+ -- removidos por nenhum caminho hoje (só snapshots viram GC).
322
+
323
+ CREATE TABLE federation_workspaces (
324
+ id INTEGER PRIMARY KEY,
325
+ name TEXT NOT NULL UNIQUE,
326
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
327
+ );
328
+
329
+ CREATE TABLE federation_members (
330
+ id INTEGER PRIMARY KEY,
331
+ workspace_id INTEGER NOT NULL
332
+ REFERENCES federation_workspaces(id) ON DELETE CASCADE,
333
+ repository_id INTEGER NOT NULL REFERENCES repositories(id),
334
+ pinned_snapshot_id INTEGER REFERENCES snapshots(id) ON DELETE SET NULL,
335
+ added_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
336
+ UNIQUE (workspace_id, repository_id)
337
+ );
338
+
339
+ CREATE INDEX idx_federation_members_ws
340
+ ON federation_members (workspace_id);
341
+ CREATE INDEX idx_federation_members_repo
342
+ ON federation_members (repository_id);
343
+ """
344
+
345
+
346
+ _SCHEMA_V11_DOMAIN_REL_INDEXES = """
347
+ -- lf-0132 (Cycle 8 Phase 6): índices compostos nos quatro endpoints de
348
+ -- ``domain_relations``. O ego (``from_/to_entity_occ_id IN (...)``) e o
349
+ -- BFS de linhagem filtram por endpoint dentro do snapshot; os índices
350
+ -- single-column de v9 lideram pela coluna de endpoint e não cobrem o
351
+ -- predicado ``snapshot_id = ?``, então o planner preferia o range-scan
352
+ -- de ``idx_domain_relations_snap`` (~O(snapshot)) mesmo para INs
353
+ -- pequenos. ``(snapshot_id, endpoint)`` dá probes diretos nos INs de um
354
+ -- termo e habilita MULTI-INDEX OR nas egos **quando há estatísticas**.
355
+ --
356
+ -- ``ANALYZE`` escopo-domínio popula ``sqlite_stat1`` em bancos já
357
+ -- povoados — sem stats o planner mantém o scan de snapshot até para os
358
+ -- INs literais do ego e os probes PK de ``deo.id IN (json_each)`` na
359
+ -- hidratação de endpoints. ANALYZE só escreve a tabela de sistema
360
+ -- ``sqlite_stat1``: nenhuma tabela de aplicação muda de forma. O
361
+ -- refresh contínuo fica em ``Store.publish_snapshot`` (mesma tx da
362
+ -- publicação — stats commitam com os dados que descrevem).
363
+ --
364
+ -- Estritamente aditiva: bancos antigos abrem e ganham os quatro
365
+ -- índices + stats; nada é dropado (os índices single-column de v9
366
+ -- permanecem — ``(endpoint, type)`` cobre formas diferentes).
367
+
368
+ CREATE INDEX IF NOT EXISTS idx_domain_relations_snap_from_ent
369
+ ON domain_relations (snapshot_id, from_entity_occ_id);
370
+ CREATE INDEX IF NOT EXISTS idx_domain_relations_snap_to_ent
371
+ ON domain_relations (snapshot_id, to_entity_occ_id);
372
+ CREATE INDEX IF NOT EXISTS idx_domain_relations_snap_from_sym
373
+ ON domain_relations (snapshot_id, from_symbol_occ_id);
374
+ CREATE INDEX IF NOT EXISTS idx_domain_relations_snap_to_sym
375
+ ON domain_relations (snapshot_id, to_symbol_occ_id);
376
+
377
+ ANALYZE domain_relations;
378
+ ANALYZE domain_entity_occurrences;
379
+ ANALYZE domain_entities;
380
+ """
381
+
382
+
383
+ _SCHEMA_V12_DOMAIN_FTS = """
384
+ -- lf-0133 (Cycle 8 Phase 7): FTS5 dedicado a entidades de domínio.
385
+ --
386
+ -- Standalone, mesmo desenho de ``fts_occurrences`` (v7): rowid = id da
387
+ -- ``domain_entity_occurrences`` — join direto para âncora snapshot/
388
+ -- entidade. ``terms`` carrega identificadores pré-tokenizados
389
+ -- (``search/tokenizer.index_terms``) — snake_case/camelCase/dotted
390
+ -- recuperáveis por fragmento, enquanto ``name``/``qualified_name``
391
+ -- guardam a forma exata. ``snapshot_id``/``entity_id``/``domain``/
392
+ -- ``kind`` são UNINDEXED: filtros de escopo, nunca alvos de MATCH.
393
+ --
394
+ -- ``content=`` externo não serve: as colunas de match moram em
395
+ -- ``domain_entities``/``source_files``, e cascades de FK não disparam
396
+ -- triggers em SQLite — a limpeza é manual em
397
+ -- ``Store.gc_stale_snapshots`` (mesmo mecanismo de fts_occurrences).
398
+ --
399
+ -- Migração separada da v11 por duas razões: (a) FTS5 é opcional de
400
+ -- build — um gate na v11 também removeria os índices dos builds sem
401
+ -- módulo; (b) o checksum por-versão exige SQL determinístico — variantes
402
+ -- condicionais na mesma versão gerariam drift entre builds. Bancos sem
403
+ -- FTS5 ficam na v11 com índices e LIKE bounded intactos.
404
+ --
405
+ -- Populada em ``persist_domain_result``: somente snapshots **novos**
406
+ -- ganham rows — snapshots pré-v12 seguem no LIKE honesto do
407
+ -- ``DomainEntitySearch`` (fallback declarado). Backfill in-place por
408
+ -- snapshot existe em ``Store.rebuild_domain_fts``; um ``code-atlas
409
+ -- reindex`` de superfície é o equivalente operacional (nova snapshot
410
+ -- já sai coberta pelo persist).
411
+
412
+ CREATE VIRTUAL TABLE domain_fts USING fts5(
413
+ snapshot_id UNINDEXED,
414
+ entity_id UNINDEXED,
415
+ name,
416
+ qualified_name,
417
+ terms,
418
+ path,
419
+ domain UNINDEXED,
420
+ kind UNINDEXED
421
+ );
422
+ """
423
+
424
+
425
+ def migrations(*, fts5: bool) -> list[Migration]:
426
+ """Lista de migrações; v2/v7/v12 (FTS5) só existem quando a build suporta."""
427
+ m = [Migration(1, "core schema (blueprint §6)", _SCHEMA_V1)]
428
+ if fts5:
429
+ m.append(Migration(2, "fts_symbols virtual table", _SCHEMA_V2_FTS))
430
+ m.append(Migration(3, "snapshot-scoped indexes", _SCHEMA_V3_INDEXES))
431
+ m.append(Migration(4, "parse result cache (lf-0020)", _SCHEMA_V4_CACHE))
432
+ m.append(Migration(5, "logical repo identity + dirty flag (lf-0021)", _SCHEMA_V5_IDENTITY))
433
+ m.append(Migration(6, "evidence resolver provenance (lf-0035)", _SCHEMA_V6_EVIDENCE))
434
+ if fts5:
435
+ m.append(Migration(
436
+ 7, "fts_occurrences snapshot-scoped lexical index (lf-0036)",
437
+ _SCHEMA_V7_FTS_OCC,
438
+ ))
439
+ m.append(Migration(
440
+ 8, "vector embeddings + content cache (lf-0039)",
441
+ _SCHEMA_V8_VECTORS,
442
+ ))
443
+ m.append(Migration(
444
+ 9, "domain entities + relations (lf-0110)",
445
+ _SCHEMA_V9_DOMAINS,
446
+ ))
447
+ m.append(Migration(
448
+ 10, "federation workspaces + members (lf-0118)",
449
+ _SCHEMA_V10_FEDERATION,
450
+ ))
451
+ m.append(Migration(
452
+ 11, "domain_relations endpoint indexes + stats (lf-0132)",
453
+ _SCHEMA_V11_DOMAIN_REL_INDEXES,
454
+ ))
455
+ if fts5:
456
+ m.append(Migration(
457
+ 12, "domain_fts dedicated FTS5 (lf-0133)",
458
+ _SCHEMA_V12_DOMAIN_FTS,
459
+ ))
460
+ return m
461
+
462
+
463
+ def _checksum(sql: str) -> str:
464
+ return hashlib.sha256(sql.encode("utf-8")).hexdigest()
465
+
466
+
467
+ def _split_statements(sql: str) -> Iterator[str]:
468
+ """Divide o script em statements completos via sqlite3.complete_statement.
469
+
470
+ Necessário porque ``executescript()`` faz COMMIT implícito antes de
471
+ rodar — sairia da transação do ``with conn:`` e deixaria DDL parcial
472
+ aplicada sem registro em ``schema_migrations`` (Phase 0 / P1.3).
473
+ """
474
+ buf: list[str] = []
475
+ for line in sql.splitlines(keepends=True):
476
+ buf.append(line)
477
+ chunk = "".join(buf)
478
+ if sqlite3.complete_statement(chunk):
479
+ stmt = chunk.strip()
480
+ if stmt:
481
+ yield stmt
482
+ buf.clear()
483
+
484
+
485
+ def migrate(conn: sqlite3.Connection, *, fts5: bool | None = None) -> int:
486
+ """Aplica migrações pendentes; retorna o schema_version resultante."""
487
+ if fts5 is None:
488
+ from code_atlas.storage.db import fts5_available
489
+
490
+ fts5 = fts5_available(conn)
491
+ conn.execute(
492
+ """
493
+ CREATE TABLE IF NOT EXISTS schema_migrations (
494
+ version INTEGER PRIMARY KEY,
495
+ name TEXT NOT NULL,
496
+ checksum TEXT NOT NULL,
497
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
498
+ )
499
+ """
500
+ )
501
+ applied = {
502
+ row["version"]: row["checksum"]
503
+ for row in conn.execute("SELECT version, checksum FROM schema_migrations")
504
+ }
505
+ # lf-0155 fail-safe: ``schema_migrations`` com versão fora do
506
+ # catálogo completo = banco de build mais nova — migrar seria
507
+ # silencioso e inseguro; recusa honesta, nunca segue adiante.
508
+ # O catálogo fts5=True é o superset (v2/v7/v12 existem sempre) —
509
+ # um banco v12 num build sem FTS5 é caminho projetado (fallback
510
+ # LIKE), não versão desconhecida.
511
+ catalog = migrations(fts5=True)
512
+ by_version = {m.version: m for m in catalog}
513
+ unknown = sorted(v for v in applied if v not in by_version)
514
+ if unknown:
515
+ raise MigrationError(
516
+ f"schema_migrations has unknown versions {unknown} — "
517
+ "database was written by a newer build; upgrade code-atlas")
518
+ applicable = {m.version for m in migrations(fts5=fts5)}
519
+ for mig in catalog:
520
+ digest = _checksum(mig.sql)
521
+ if mig.version in applied:
522
+ if applied[mig.version] != digest:
523
+ raise MigrationError(
524
+ f"migration v{mig.version} checksum drift "
525
+ f"(applied {applied[mig.version][:12]} != current {digest[:12]})"
526
+ )
527
+ continue
528
+ if mig.version not in applicable:
529
+ continue # migração FTS5-gated — não aplicável nesta build
530
+ try:
531
+ with conn:
532
+ # sqlite3 legacy só inicia transação implícita em DML —
533
+ # BEGIN explícito garante que DDL também seja atômica.
534
+ conn.execute("BEGIN")
535
+ for stmt in _split_statements(mig.sql):
536
+ conn.execute(stmt)
537
+ conn.execute(
538
+ "INSERT INTO schema_migrations (version, name, checksum) VALUES (?, ?, ?)",
539
+ (mig.version, mig.name, digest),
540
+ )
541
+ except sqlite3.Error as exc:
542
+ raise MigrationError(f"migration v{mig.version} failed: {exc}") from exc
543
+ return schema_version(conn)
544
+
545
+
546
+ def schema_version(conn: sqlite3.Connection) -> int:
547
+ """Maior versão aplicada; 0 se nenhuma."""
548
+ try:
549
+ row = conn.execute("SELECT MAX(version) AS v FROM schema_migrations").fetchone()
550
+ except sqlite3.Error:
551
+ return 0
552
+ return int(row["v"] or 0)