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,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
|
+
]
|
code_atlas/storage/db.py
ADDED
|
@@ -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
|