feat(kb-mail): fundament — pgvector spine, koperta, archiwum, pakiet domeny
- services/kb-postgres: pgvector/pgvector:pg16 na SOLARIA (:5433), named
volume, init/001_envelope.sql (CREATE EXTENSION vector + zamrożona tabela
envelope: id/source/ts/geo/raw_ref/entities), service.yaml, healthcheck,
README z poprawnym mechanizmem deploy (deploy-node.sh składa dwa -f)
- hosts/solaria/runtime/kb-postgres/docker-compose.override.yml: mem_limit 4g
- inventory/topology.yaml + hosts/solaria/services.yaml: kb-postgres wpisany
- packages/kb-mail: nowa konwencja shared lib (pip install /repo/packages/<lib>/)
envelope.py — @dataclass Envelope, walidacja tz-aware ts
db.py — insert_envelope / get_envelope (asyncpg, ON CONFLICT DO NOTHING)
archive.py — save_eml append-only (asyncio.to_thread, FileExistsError na dup)
tests: 15 unit pass + 5 integration (@pytest.mark.integration, wymaga KB_TEST_DSN)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 21:50:49 +02:00
|
|
|
"""Sanity tests for SQL migration files — no DB or network required."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.parent.parent
|
|
|
|
|
INIT_DIR = REPO_ROOT / "services" / "kb-postgres" / "init"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_exists():
|
|
|
|
|
assert (INIT_DIR / "001_envelope.sql").exists(), \
|
|
|
|
|
f"Expected {INIT_DIR / '001_envelope.sql'} to exist"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_enables_vector_extension():
|
|
|
|
|
sql = (INIT_DIR / "001_envelope.sql").read_text()
|
|
|
|
|
assert "CREATE EXTENSION" in sql
|
|
|
|
|
assert "vector" in sql.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_creates_envelope_table():
|
|
|
|
|
sql = (INIT_DIR / "001_envelope.sql").read_text()
|
|
|
|
|
assert "CREATE TABLE" in sql
|
|
|
|
|
assert "envelope" in sql.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_has_all_frozen_columns():
|
|
|
|
|
sql = (INIT_DIR / "001_envelope.sql").read_text()
|
|
|
|
|
frozen_columns = ("id", "source", "ts", "geo", "raw_ref", "entities")
|
|
|
|
|
for col in frozen_columns:
|
|
|
|
|
assert col in sql, f"Frozen column '{col}' missing from 001_envelope.sql"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_geo_is_nullable():
|
|
|
|
|
sql = (INIT_DIR / "001_envelope.sql").read_text()
|
|
|
|
|
# geo must NOT have NOT NULL constraint
|
|
|
|
|
lines = [l for l in sql.splitlines() if "geo" in l.lower()]
|
|
|
|
|
assert lines, "No 'geo' line found in migration SQL"
|
|
|
|
|
for line in lines:
|
|
|
|
|
assert "NOT NULL" not in line.upper(), \
|
|
|
|
|
f"geo must be nullable but found: {line.strip()}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_001_entities_has_default():
|
|
|
|
|
sql = (INIT_DIR / "001_envelope.sql").read_text()
|
|
|
|
|
lines = [l for l in sql.splitlines() if "entities" in l.lower()]
|
|
|
|
|
assert any("DEFAULT" in l.upper() for l in lines), \
|
|
|
|
|
"entities column must have a DEFAULT '[]'"
|
feat(kb-postgres): migration 002_chunks.sql — document_chunk table
Krok 1 planu docs/kb/modules/05-faza2-plan.md §3/§6 (chunk-level embeddings,
1:N do envelope). Addytywna — 001_envelope.sql nietknięta (zweryfikowane
\d envelope po migracji: identyczny schemat + FK jako "Referenced by").
Schemat wg rekomendacji recon (§2 decyzja 1+2): osobna tabela (nie kolumna
w envelope, bo N-wartościowy chunking jest obowiązkowy przy dokumentach
>8k tokenów), embedding VECTOR(1024) pod bge-m3 (dense), HNSW cosine index,
kolumna `model` do trywialnego re-indexu przy zmianie modelu (kb-00 zasada
#1: indeks odtwarzalny). Idempotentna (CREATE TABLE/INDEX IF NOT EXISTS,
zweryfikowane podwójnym uruchomieniem na kb-postgres@PIHA — drugi run same
NOTICE "already exists, skipping").
Zastosowana na żywej bazie: ssh piha docker exec kb-postgres psql, po
potwierdzeniu SQL przez Oskara. \dt + \d document_chunk + \d envelope
zweryfikowane po migracji.
Testy: dopisane sanity-testy 002 do packages/kb-mail/tests/test_migration.py
(wzorzec 001 — statyczne assercje na treści SQL, bez DB), 13/13 zielone.
Co NIE jest częścią tego kroku (§3 planu, odłożone): entity/entity_link
(graf encji) — szkic na przyszłość, nie blokuje domknięcia modułu 5.
Co dalej (plan §6, poza zakresem tego kroku): ollama pull bge-m3 na SOLARII,
token API Paperless, jobs/gmail-header-backfill/, adapter Paperless→koperta,
chunking+embed job.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 21:08:41 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_exists():
|
|
|
|
|
assert (INIT_DIR / "002_chunks.sql").exists(), \
|
|
|
|
|
f"Expected {INIT_DIR / '002_chunks.sql'} to exist"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_does_not_touch_envelope():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
|
|
|
|
assert "ALTER TABLE envelope" not in sql
|
|
|
|
|
assert "DROP TABLE envelope" not in sql.upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_creates_document_chunk_table():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
|
|
|
|
assert "CREATE TABLE IF NOT EXISTS document_chunk" in sql
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_references_envelope_with_cascade():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
|
|
|
|
assert "REFERENCES envelope(id) ON DELETE CASCADE" in sql
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_embedding_dimension_matches_bge_m3():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:12:24 +02:00
|
|
|
# bge-m3 dense embedding dimension is 1024 (kb/phases/kb-m5-faza2.md §1.4)
|
feat(kb-postgres): migration 002_chunks.sql — document_chunk table
Krok 1 planu docs/kb/modules/05-faza2-plan.md §3/§6 (chunk-level embeddings,
1:N do envelope). Addytywna — 001_envelope.sql nietknięta (zweryfikowane
\d envelope po migracji: identyczny schemat + FK jako "Referenced by").
Schemat wg rekomendacji recon (§2 decyzja 1+2): osobna tabela (nie kolumna
w envelope, bo N-wartościowy chunking jest obowiązkowy przy dokumentach
>8k tokenów), embedding VECTOR(1024) pod bge-m3 (dense), HNSW cosine index,
kolumna `model` do trywialnego re-indexu przy zmianie modelu (kb-00 zasada
#1: indeks odtwarzalny). Idempotentna (CREATE TABLE/INDEX IF NOT EXISTS,
zweryfikowane podwójnym uruchomieniem na kb-postgres@PIHA — drugi run same
NOTICE "already exists, skipping").
Zastosowana na żywej bazie: ssh piha docker exec kb-postgres psql, po
potwierdzeniu SQL przez Oskara. \dt + \d document_chunk + \d envelope
zweryfikowane po migracji.
Testy: dopisane sanity-testy 002 do packages/kb-mail/tests/test_migration.py
(wzorzec 001 — statyczne assercje na treści SQL, bez DB), 13/13 zielone.
Co NIE jest częścią tego kroku (§3 planu, odłożone): entity/entity_link
(graf encji) — szkic na przyszłość, nie blokuje domknięcia modułu 5.
Co dalej (plan §6, poza zakresem tego kroku): ollama pull bge-m3 na SOLARII,
token API Paperless, jobs/gmail-header-backfill/, adapter Paperless→koperta,
chunking+embed job.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 21:08:41 +02:00
|
|
|
assert "VECTOR(1024)" in sql.upper()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_has_unique_envelope_chunk_index():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
|
|
|
|
assert "UNIQUE (envelope_id, chunk_index)" in sql
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_migration_002_uses_idempotent_ddl():
|
|
|
|
|
sql = (INIT_DIR / "002_chunks.sql").read_text()
|
|
|
|
|
assert "CREATE TABLE IF NOT EXISTS" in sql
|
|
|
|
|
for line in sql.splitlines():
|
|
|
|
|
if line.strip().upper().startswith("CREATE INDEX"):
|
|
|
|
|
assert "IF NOT EXISTS" in line.upper(), \
|
|
|
|
|
f"CREATE INDEX must be idempotent: {line.strip()}"
|