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,96 @@
1
+ """EmbeddingCache + build de embeddings por snapshot (lf-0039).
2
+
3
+ Cache por ``content_hash`` (sha256 do texto de indexação): rebuild só
4
+ quando conteúdo OU modelo mudam — PK ``(model_id, model_version,
5
+ content_hash)`` torna o versionamento estrutural, não convenção.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import sqlite3
12
+
13
+ from code_atlas.search.vector.contracts import (
14
+ EmbeddingModelInfo,
15
+ EmbeddingProvider,
16
+ )
17
+ from code_atlas.search.vector.index import pack_vector, unpack_vector
18
+
19
+
20
+ def content_hash(text: str) -> str:
21
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
22
+
23
+
24
+ class SqliteEmbeddingCache:
25
+ """Cache persistente por conteúdo+modelo (tabela embedding_cache)."""
26
+
27
+ _GET = (
28
+ "SELECT vector FROM embedding_cache "
29
+ "WHERE model_id = ? AND model_version = ? AND content_hash = ?"
30
+ )
31
+ _PUT = (
32
+ "INSERT OR REPLACE INTO embedding_cache "
33
+ "(model_id, model_version, content_hash, dim, vector) "
34
+ "VALUES (?, ?, ?, ?, ?)"
35
+ )
36
+
37
+ def __init__(self, conn: sqlite3.Connection) -> None:
38
+ self._conn = conn
39
+
40
+ def get(
41
+ self, model: EmbeddingModelInfo, chash: str
42
+ ) -> list[float] | None:
43
+ row = self._conn.execute(
44
+ self._GET, (model.model_id, model.version, chash),
45
+ ).fetchone()
46
+ return unpack_vector(row["vector"]) if row else None
47
+
48
+ def put(
49
+ self, model: EmbeddingModelInfo, chash: str, vector: list[float]
50
+ ) -> None:
51
+ self._conn.execute(
52
+ self._PUT,
53
+ (model.model_id, model.version, chash, len(vector),
54
+ pack_vector(vector)),
55
+ )
56
+
57
+ def commit(self) -> None:
58
+ """Persiste as writes pendentes do cache (lote, não por linha)."""
59
+ self._conn.commit()
60
+
61
+
62
+ def embed_texts_cached(
63
+ provider: EmbeddingProvider,
64
+ cache: SqliteEmbeddingCache,
65
+ texts: list[str],
66
+ ) -> tuple[list[list[float]], int, int]:
67
+ """Embed com cache: retorna (vetores, hits, misses) — misses sobem
68
+ em lote único ao provider."""
69
+ model = provider.info()
70
+ out: list[list[float] | None] = [None] * len(texts)
71
+ miss_idx: list[int] = []
72
+ hits = 0
73
+ for i, text in enumerate(texts):
74
+ vec = cache.get(model, content_hash(text))
75
+ if vec is None:
76
+ miss_idx.append(i)
77
+ else:
78
+ out[i] = vec
79
+ hits += 1
80
+ if miss_idx:
81
+ fresh = provider.embed([texts[i] for i in miss_idx])
82
+ for i, vec in zip(miss_idx, fresh, strict=True):
83
+ cache.put(model, content_hash(texts[i]), vec)
84
+ out[i] = vec
85
+ # Cache de embeddings é append-only e semanticamente
86
+ # independente do estado transacional do snapshot — commita o
87
+ # lote para que CLI/processos one-shot não percam o cache no
88
+ # rollback do close (lf-0142 finding).
89
+ cache.commit()
90
+ return [v if v is not None else [] for v in out], hits, len(miss_idx)
91
+
92
+
93
+ def index_text_for(symbol_key: str, signature: str, docstring: str) -> str:
94
+ """Texto de indexação: fronteiras sintáticas (nome+signature+doc),
95
+ nunca chunk fixo."""
96
+ return "\n".join(p for p in (symbol_key, signature, docstring) if p)
@@ -0,0 +1,169 @@
1
+ """VectorIndex persistido — sqlite-vec (opt-in) ou bruteforce (lf-0039).
2
+
3
+ Backend escolhido por capability: ``sqlite_vec`` importável → vec0
4
+ (KNN nativo); senão → bruteforce stdlib sobre BLOB float32 da mesma
5
+ tabela ``vector_embeddings``. Ambos filtram por ``snapshot_id`` — o
6
+ isolamento por snapshot é estrutural, nunca filtro pós-query.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import sqlite3
13
+ import struct
14
+
15
+ from code_atlas.search.vector.contracts import VectorSearchResult
16
+
17
+ _UPSERT = (
18
+ "INSERT OR REPLACE INTO vector_embeddings "
19
+ "(snapshot_id, occurrence_id, symbol_key, model_id, model_version,"
20
+ " dim, content_hash, vector) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
21
+ )
22
+ _DELETE_SNAP = "DELETE FROM vector_embeddings WHERE snapshot_id = ?"
23
+
24
+
25
+ def pack_vector(vec: list[float]) -> bytes:
26
+ """float32 little-endian — 4 bytes/dim, round-trip estável."""
27
+ return struct.pack(f"<{len(vec)}f", *vec)
28
+
29
+
30
+ def unpack_vector(blob: bytes) -> list[float]:
31
+ n = len(blob) // 4
32
+ return list(struct.unpack(f"<{n}f", blob))
33
+
34
+
35
+ def _cosine(a: list[float], b: list[float]) -> float:
36
+ dot = na = nb = 0.0
37
+ for x, y in zip(a, b, strict=True):
38
+ dot += x * y
39
+ na += x * x
40
+ nb += y * y
41
+ if na == 0.0 or nb == 0.0:
42
+ return 0.0
43
+ return max(0.0, min(1.0, dot / math.sqrt(na * nb)))
44
+
45
+
46
+ class BruteForceIndex:
47
+ """Scan O(n·dim) com filtro de snapshot — honesto, sem deps."""
48
+
49
+ def __init__(
50
+ self, conn: sqlite3.Connection, model_id: str, model_version: str
51
+ ) -> None:
52
+ self._conn = conn
53
+ self._model_id = model_id
54
+ self._model_version = model_version
55
+
56
+ def upsert(
57
+ self, snapshot_id: int, occurrence_id: int,
58
+ symbol_key: str, vector: list[float],
59
+ content_hash: str = "",
60
+ ) -> None:
61
+ self._conn.execute(
62
+ _UPSERT,
63
+ (snapshot_id, occurrence_id, symbol_key, self._model_id,
64
+ self._model_version, len(vector), content_hash,
65
+ pack_vector(vector)),
66
+ )
67
+
68
+ def search(
69
+ self, snapshot_id: int, query: list[float], limit: int
70
+ ) -> list[VectorSearchResult]:
71
+ rows = self._conn.execute(
72
+ "SELECT occurrence_id, symbol_key, vector "
73
+ "FROM vector_embeddings "
74
+ "WHERE snapshot_id = ? AND model_id = ? AND model_version = ?",
75
+ (snapshot_id, self._model_id, self._model_version),
76
+ ).fetchall()
77
+ scored = [
78
+ VectorSearchResult(
79
+ symbol_key=r["symbol_key"],
80
+ occurrence_id=int(r["occurrence_id"]),
81
+ score=_cosine(query, unpack_vector(r["vector"])),
82
+ model_id=self._model_id,
83
+ )
84
+ for r in rows
85
+ ]
86
+ scored.sort(key=lambda r: (-r.score, r.occurrence_id))
87
+ return scored[: max(1, limit)]
88
+
89
+ def delete_snapshot(self, snapshot_id: int) -> None:
90
+ self._conn.execute(_DELETE_SNAP, (snapshot_id,))
91
+
92
+
93
+ def sqlite_vec_available() -> bool:
94
+ """sqlite-vec instalado? (extra opt-in ``[vector]``)"""
95
+ try:
96
+ import sqlite_vec # type: ignore[import-not-found] # noqa: F401
97
+ except ImportError:
98
+ return False
99
+ return True
100
+
101
+
102
+ class SqliteVecIndex(BruteForceIndex):
103
+ """Backend nativo via sqlite-vec quando o extra está instalado.
104
+
105
+ Mesma tabela de persistência (interoperável com bruteforce); só a
106
+ busca usa a virtual table vec0 — fallback ao scan se a extensão
107
+ falhar em runtime.
108
+ """
109
+
110
+ def __init__(
111
+ self, conn: sqlite3.Connection, model_id: str,
112
+ model_version: str, dim: int
113
+ ) -> None:
114
+ super().__init__(conn, model_id, model_version)
115
+ self._dim = dim
116
+ import sqlite_vec
117
+
118
+ sqlite_vec.load(conn)
119
+ # SQL nomeado — auditoria AST exige literal/nome, nunca expr.
120
+ create_vec_sql = (
121
+ "CREATE VIRTUAL TABLE IF NOT EXISTS vec_symbols USING vec0("
122
+ "snapshot_id INTEGER, occurrence_id INTEGER, "
123
+ f"embedding FLOAT[{dim}])"
124
+ )
125
+ conn.execute(create_vec_sql)
126
+
127
+ def upsert(
128
+ self, snapshot_id: int, occurrence_id: int,
129
+ symbol_key: str, vector: list[float],
130
+ content_hash: str = "",
131
+ ) -> None:
132
+ super().upsert(
133
+ snapshot_id, occurrence_id, symbol_key, vector, content_hash)
134
+ self._conn.execute(
135
+ "DELETE FROM vec_symbols WHERE occurrence_id = ?",
136
+ (occurrence_id,))
137
+ self._conn.execute(
138
+ "INSERT INTO vec_symbols "
139
+ "(snapshot_id, occurrence_id, embedding) VALUES (?, ?, ?)",
140
+ (snapshot_id, occurrence_id, pack_vector(vector)),
141
+ )
142
+
143
+ def search(
144
+ self, snapshot_id: int, query: list[float], limit: int
145
+ ) -> list[VectorSearchResult]:
146
+ try:
147
+ rows = self._conn.execute(
148
+ "SELECT occurrence_id, distance FROM vec_symbols "
149
+ "WHERE embedding MATCH ? AND k = ? "
150
+ "AND snapshot_id = ? ORDER BY distance",
151
+ (pack_vector(query), max(1, limit), snapshot_id),
152
+ ).fetchall()
153
+ except sqlite3.Error:
154
+ return super().search(snapshot_id, query, limit)
155
+ return [
156
+ VectorSearchResult(
157
+ symbol_key="", # preenchido na fusão via occurrence
158
+ occurrence_id=int(r["occurrence_id"]),
159
+ score=max(0.0, 1.0 - float(r["distance"])),
160
+ model_id=self._model_id,
161
+ )
162
+ for r in rows
163
+ ]
164
+
165
+ def delete_snapshot(self, snapshot_id: int) -> None:
166
+ super().delete_snapshot(snapshot_id)
167
+ self._conn.execute(
168
+ "DELETE FROM vec_symbols WHERE snapshot_id = ?",
169
+ (snapshot_id,))
@@ -0,0 +1,50 @@
1
+ """Provider de embeddings local — baseline naïve stdlib (lf-0039).
2
+
3
+ ``NaiveEmbeddingProvider``: hashing de tokens com sinal (feature
4
+ hashing) → vetor L2-normalizado. Determinístico, offline, zero deps.
5
+
6
+ Baseline HONESTO: captura sobreposição lexical (útil para RRF), NÃO
7
+ semântica real — sinônimos ("order"/"pedido") não aproximam. Trocável
8
+ via seam ``EmbeddingProvider`` (provider externo só opt-in explícito;
9
+ default nunca faz egress).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import math
16
+ import re
17
+
18
+ from code_atlas.search.vector.contracts import EmbeddingModelInfo
19
+
20
+ _TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
21
+
22
+
23
+ class NaiveEmbeddingProvider:
24
+ """Feature hashing: sha256(token) → (dim, sinal); soma e normaliza."""
25
+
26
+ def __init__(self, dim: int = 256) -> None:
27
+ if dim <= 0:
28
+ raise ValueError("dim deve ser > 0")
29
+ self._dim = dim
30
+ self._info = EmbeddingModelInfo(
31
+ model_id="naive-hash", version="v1", dim=dim,
32
+ detail="feature hashing de tokens (baseline não-semântico)")
33
+
34
+ def info(self) -> EmbeddingModelInfo:
35
+ return self._info
36
+
37
+ def embed(self, texts: list[str]) -> list[list[float]]:
38
+ return [self._embed_one(t) for t in texts]
39
+
40
+ def _embed_one(self, text: str) -> list[float]:
41
+ vec = [0.0] * self._dim
42
+ for tok in _TOKEN_RE.findall(text.lower()):
43
+ h = hashlib.sha256(tok.encode("utf-8")).digest()
44
+ bucket = int.from_bytes(h[:4], "big") % self._dim
45
+ sign = 1.0 if h[4] & 1 else -1.0
46
+ vec[bucket] += sign
47
+ norm = math.sqrt(sum(v * v for v in vec))
48
+ if norm == 0.0:
49
+ return vec
50
+ return [v / norm for v in vec]
@@ -0,0 +1,14 @@
1
+ """Persistência SQLite (WAL, migrações, snapshots)."""
2
+
3
+ from code_atlas.storage.db import connect, fts5_available
4
+ from code_atlas.storage.migrations import MigrationError, migrate, schema_version
5
+ from code_atlas.storage.store import Store
6
+
7
+ __all__ = [
8
+ "MigrationError",
9
+ "Store",
10
+ "connect",
11
+ "fts5_available",
12
+ "migrate",
13
+ "schema_version",
14
+ ]
@@ -0,0 +1,47 @@
1
+ """Fábrica de conexões SQLite (Blueprint ADR-006).
2
+
3
+ Pragmas obrigatórios: WAL, foreign_keys=ON, busy_timeout, synchronous=NORMAL.
4
+ Todos os caminhos de escrita usam transações explícitas; escritor único por
5
+ workspace é coordenado pelo IndexCoordinator via ``BEGIN IMMEDIATE``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ from pathlib import Path
12
+
13
+ BUSY_TIMEOUT_MS = 5000
14
+
15
+
16
+ def connect(path: Path | str, *, read_only: bool = False) -> sqlite3.Connection:
17
+ """Abre conexão configurada para o workspace Atlas.
18
+
19
+ ``read_only=True`` usa URI ``mode=ro`` para leitores concorrentes que
20
+ nunca devem escrever nem criar o arquivo.
21
+ """
22
+ if read_only:
23
+ uri = f"file:{Path(path).as_posix()}?mode=ro"
24
+ conn = sqlite3.connect(uri, uri=True, timeout=BUSY_TIMEOUT_MS / 1000)
25
+ else:
26
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
27
+ conn = sqlite3.connect(str(path), timeout=BUSY_TIMEOUT_MS / 1000)
28
+ conn.execute("PRAGMA foreign_keys = ON")
29
+ # timeout= do connect() já aplica busy_timeout; PRAGMA não aceita bind.
30
+ if not read_only:
31
+ conn.execute("PRAGMA journal_mode = WAL")
32
+ conn.execute("PRAGMA synchronous = NORMAL")
33
+ # 64MiB de page cache — barateia a manutenção de índices/FKs
34
+ # durante persist de snapshots grandes (lf-0023, medido ~4%).
35
+ conn.execute("PRAGMA cache_size = -64000")
36
+ conn.row_factory = sqlite3.Row
37
+ return conn
38
+
39
+
40
+ def fts5_available(conn: sqlite3.Connection) -> bool:
41
+ """True se a build do sqlite suporta FTS5."""
42
+ try:
43
+ conn.execute("CREATE VIRTUAL TABLE temp._fts_probe USING fts5(x)")
44
+ conn.execute("DROP TABLE temp._fts_probe")
45
+ return True
46
+ except sqlite3.Error:
47
+ return False