phase a audit

This commit is contained in:
team2
2026-02-22 13:51:45 +01:00
parent 5656a10930
commit b3e9110dd1
14 changed files with 222 additions and 463 deletions

79
PHASE_A_AUDIT.md Normal file
View File

@@ -0,0 +1,79 @@
# Technische Projektdokumentation
## RAG-System Phase A Abschluss
**Projekt:** KI-RAG System
**Architekturstand:** Phase A abgeschlossen
**Datum:** Februar 2026
**Status:** Verbindliche Referenzdokumentation
---
# 1. Zielsetzung von Phase A
Phase A hatte das Ziel, das bestehende Retrieval-Augmented-Generation-System strukturell zu stabilisieren und produktionsreif zu machen.
Im Fokus standen:
- Speicherstabilität (Streaming statt RAM-Load)
- Deterministische Indexierung
- Strikte Trennung von Domain (PHP) und Runtime (Python)
- Zentrale Konfigurationssteuerung
- Drift-Sicherheit des Vector-Index
Phase A beinhaltete **keine funktionale Erweiterung**, sondern ausschließlich strukturelle und architektonische Stabilisierung.
---
# 2. Architekturprinzipien
Das System folgt folgenden verbindlichen Grundprinzipien:
1. **NDJSON ist Single Source of Truth**
Alle Vektoren werden deterministisch aus `index.ndjson` erzeugt.
2. **Full Rebuild statt inkrementeller Mutation**
Der FAISS-Index wird bei Änderungen vollständig neu aufgebaut.
3. **Streaming statt Full-RAM-Load**
Keine vollständigen JSON-Arrays im Speicher.
4. **Runtime und Domain sind strikt getrennt**
PHP enthält Orchestrierung und Governance, Python enthält Vektor-Runtime.
5. **Atomare Dateioperationen**
Schreibvorgänge erfolgen über `.tmp` + `rename()`.
6. **Konfigurationszentrierung**
Alle Pfade und Script-Referenzen sind über `services.yaml` parametriert.
---
# 3. Umsetzung Phase A
## A1 Streaming-Architektur
### Problem
RAM-basierte JSON-Verarbeitung hätte bei steigender Chunk-Zahl zu Speicherproblemen geführt.
### Umsetzung
- Einführung von NDJSON als persistentes Format
- Streaming-Verarbeitung in:
- `ChunkManager::streamAll()`
- `ChunkManager::countAllChunks()`
- `ChunkManager::compactByDocument()`
- `ChunkManager::rewriteAll()`
- Entfernung von `iterator_to_array` im IngestFlow
### Ergebnis
- Speicherverbrauch unabhängig von Chunk-Anzahl
- Stabil bis mindestens 120.000 Chunks
---
## A2 Strukturtrennung Runtime / Domain
### Umsetzung
Python-Runtime wurde vollständig aus `src/` entfernt und ausgelagert nach:

0
PHASE_B_AUDIT.md Normal file
View File

View File

@@ -1,368 +0,0 @@
# TECHNICAL_ARCHITECTURE.md
mitho AI Agent Enterprise Hybrid RAG System
Stand: Februar 2026
---
# 1. Zielsetzung
Diese Dokumentation beschreibt die vollständige technische Architektur des
mitho AI Agent RAG-Systems.
Ziele:
- Deterministisches Retrieval
- Governance-stabile Index-Struktur
- Versionierte Wissensbasis
- Reproduzierbare Builds
- Keine inkrementellen Vektor-Mutationen
- Job-basierte, asynchrone Ingest-Architektur
- Skalierbarkeit >200k Chunks
---
# 2. Architektur-Übersicht
Systemebenen:
1. Admin Layer (Symfony)
2. Ingest Layer
3. Storage Layer (NDJSON)
4. Vector Layer (FAISS)
5. Retrieval Layer (Hybrid)
6. Application Layer (LLM + SSE)
---
# 3. Core Architekturprinzipien
## 3.1 Determinismus
Gleiche Eingabe + gleiche Konfiguration → identisches Ergebnis.
- index.ndjson ist deterministisch
- FAISS wird vollständig neu gebaut
- Retrieval ist stabil reproduzierbar
- Keine impliziten Indexveränderungen
## 3.2 Governance
- Genau eine aktive Version pro Dokument
- Keine Inline-Rebuilds im HTTP-Request
- Strukturänderungen erzwingen Global Reindex
- Locking verhindert Parallel-Ingest
## 3.3 Skalierbarkeit
- NDJSON statt JSON-Array
- Streaming-Append
- Streaming-Compaction
- Kein RAM-Full-Load
---
# 4. Domain Model
## 4.1 Document
Primäre Wissenseinheit.
Eigenschaften:
- id (UUID)
- title
- createdAt
- createdBy
- archived (bool)
- currentVersion (ManyToOne → DocumentVersion)
## 4.2 DocumentVersion
Immutable Inhaltsversion.
Eigenschaften:
- id (UUID)
- document (ManyToOne)
- versionNumber
- filePath
- checksum
- active (bool)
- ingestStatus (PENDING, RUNNING, INDEXED, FAILED)
Regel:
Nur eine Version pro Dokument darf active = true sein.
---
# 5. Ingest Architektur
## 5.1 Grundsatz
Kein Ingest läuft synchron im HTTP-Request.
Jede Indexänderung läuft über:
IngestJob → CLI Runner → IngestOrchestrator → IngestFlow
---
## 5.2 IngestJob
Entity mit:
- id
- type
- status
- documentId
- documentVersionId
- startedAt
- finishedAt
- errorMessage
Job-Typen:
DOCUMENT_VERSION_ACTIVATE
DOCUMENT
GLOBAL_REINDEX
Status:
QUEUED
RUNNING
COMPLETED
FAILED
ABORTED
---
## 5.3 Job-Execution
Start:
php bin/console mto:agent:ingest:run <jobId>
Execution-Flow:
1. LockService.acquire()
2. Job → RUNNING
3. IngestFlow ausführen
4. Job → COMPLETED / FAILED
5. LockService.release()
---
# 6. IngestFlow Details
## 6.1 Local Ingest (Version aktivieren oder neue Datei)
Ablauf:
1. Extract (PDF/Text)
2. Normalize
3. Chunk deterministisch
4. Streaming Compaction:
- Entferne alle Chunks mit document_id
5. Append neue Chunks
6. Full FAISS Rebuild
Wichtig:
Es werden immer alle alten Chunks des Dokuments entfernt.
---
## 6.2 Global Reindex
Wird ausgelöst bei:
- embedding_model Änderung
- embedding_dimension Änderung
- chunk_size Änderung
- scoring_version Änderung
- index_format Änderung
Ablauf:
1. Alle aktiven Versionen extrahieren
2. Komplettes index.ndjson neu schreiben
3. FAISS neu bauen
4. index_version++
---
# 7. Storage Layer
## 7.1 index.ndjson
Single Source of Truth.
Eigenschaften:
- 1 JSON-Objekt pro Zeile
- Streaming-lesbar
- Append-only
- Compaction by document_id
Beispiel:
{
"chunk_id": "...",
"document_id": "...",
"document_version_id": "...",
"text": "...",
"meta": {...}
}
---
## 7.2 index_meta.json
Enthält:
- index_version
- embedding_model
- embedding_dimension
- chunk_size
- chunk_overlap
- scoring_version
- index_format
- vector_backend
Bei strukturellem Mismatch:
→ Global Reindex zwingend.
---
# 8. Vector Layer (FAISS)
## 8.1 Build
vector_ingest.py:
- stream index.ndjson
- Embeddings berechnen
- FAISS IndexFlatIP bauen
- vector.index schreiben
- vector_meta.json schreiben
Immer vollständiger Rebuild.
Keine Partial Updates.
## 8.2 Search
vector_search.py:
Input:
- Query
- Top-K
Output:
- chunk_id
- score
---
# 9. Hybrid Retrieval
Ablauf:
1. Keyword Retrieval (NDJSON Keyword Search)
2. Vector Retrieval (FAISS)
3. Score Fusion
4. Chunk Lookup
5. Context Builder
6. Prompt Builder
7. LLM
Keyword bleibt Primärsignal.
Vector ergänzt semantische Nähe.
---
# 10. Locking
LockService schützt:
- NDJSON Mutationen
- FAISS Rebuild
- Global Reindex
- Aktivierungs-Parallelität
Keine zwei Ingest-Jobs gleichzeitig erlaubt.
---
# 11. Admin Flows
## 11.1 Neue Datei hochladen
1. Document + Version 1 erstellen
2. Version aktiv
3. DOCUMENT_VERSION_ACTIVATE Job anlegen
4. Async Runner starten
5. Redirect auf Job-Seite
## 11.2 Version aktivieren
1. DB-Status ändern
2. IngestStatus → PENDING
3. DOCUMENT_VERSION_ACTIVATE Job anlegen
4. Async Runner starten
## 11.3 Manuelles Ingest
1. DOCUMENT Job anlegen
2. Async Runner starten
---
# 12. Error Handling
Typische Fehler:
- exec deaktiviert → Async Start nicht möglich
- Lock aktiv → paralleler Ingest blockiert
- index_meta mismatch → Reindex erforderlich
- Vector-Dateien fehlen → Rebuild ausführen
---
# 13. Security & Stability
- Keine User-Uploads direkt ins Retrieval
- Kein Self-Learning
- Kein unkontrollierter Index-Mutate
- Strict Separation:
- Admin Layer
- Ingest Layer
- Retrieval Layer
- LLM Layer
---
# 14. System Guarantees
Dieses System garantiert:
- Reproduzierbarkeit
- Drift-Freiheit
- Auditierbarkeit
- Versionierte Wissensbasis
- Deterministische Retrieval-Ergebnisse
- Enterprise-taugliche Skalierbarkeit
---
# 15. Zusammenfassung
Das mitho AI Agent System ist eine:
- deterministische Hybrid-RAG Engine
- NDJSON-basierte Wissensplattform
- Full-Rebuild-FAISS Architektur
- Job-basierte Ingest-Pipeline
- Versionierte, governance-stabile Wissensstruktur
Keine Inline-Rebuilds.
Keine inkrementellen Vektor-Updates.
Keine impliziten Strukturänderungen.
Alles läuft kontrolliert über das Job-System.

View File

@@ -30,9 +30,14 @@ parameters:
mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index' mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index'
mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json' mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json'
# Tag vector scripts (in src/Vector) # ------------------------------------------------------------
mto.vector.ingest_tags_script: '%mto.root%/src/Vector/vector_ingest_tags.py' # Vector Script Directory (A2)
mto.vector.search_tags_script: '%mto.root%/src/Vector/vector_search_tags.py' # ------------------------------------------------------------
mto.vector.script_dir: '%mto.root%/python/vector'
# Tag vector scripts
mto.vector.ingest_tags_script: '%mto.vector.script_dir%/vector_ingest_tags.py'
mto.vector.search_tags_script: '%mto.vector.script_dir%/vector_search_tags.py'
# Lock for tag rebuild jobs # Lock for tag rebuild jobs
mto.tags.rebuild_lock: '%mto.knowledge.root%/locks/tag_rebuild.lock' mto.tags.rebuild_lock: '%mto.knowledge.root%/locks/tag_rebuild.lock'
@@ -53,10 +58,9 @@ parameters:
# Python / Vector Runtime # Python / Vector Runtime
# ------------------------------------------------------------ # ------------------------------------------------------------
mto.vector.python_bin: '/var/www/html/.venv/bin/python3' mto.vector.python_bin: '/var/www/html/.venv/bin/python3'
mto.vector.ingest_script: '%mto.root%/src/Vector/vector_ingest.py' mto.vector.ingest_script: '%mto.vector.script_dir%/vector_ingest.py'
mto.vector.search_script: '%mto.root%/src/Vector/vector_search.py' mto.vector.search_script: '%mto.vector.script_dir%/vector_search.py'
mto.vector.timeout: 600 mto.vector.timeout: 600
mto.vector.service_url: 'http://127.0.0.1:8090' mto.vector.service_url: 'http://127.0.0.1:8090'
@@ -177,7 +181,7 @@ services:
$tagsNdjsonPath: '%mto.knowledge.tags_ndjson%' $tagsNdjsonPath: '%mto.knowledge.tags_ndjson%'
# ------------------------------------------------------------ # ------------------------------------------------------------
# Tags Vector (Builder + Search) ✅ HIER IST DER FIX # Tags Vector
# ------------------------------------------------------------ # ------------------------------------------------------------
App\Tag\TagVectorIndexBuilder: App\Tag\TagVectorIndexBuilder:
@@ -221,6 +225,6 @@ services:
App\Vector\VectorIndexHealthService: App\Vector\VectorIndexHealthService:
arguments: arguments:
$indexNdjsonPath: '%kernel.project_dir%/var/knowledge/index.ndjson' $indexNdjsonPath: '%mto.knowledge.ndjson%'
$vectorIndexPath: '%kernel.project_dir%/var/knowledge/vector.index' $vectorIndexPath: '%mto.knowledge.vector_index%'
$vectorMetaPath: '%kernel.project_dir%/var/knowledge/vector.index.meta.json' $vectorMetaPath: '%mto.knowledge.vector_index_meta%'

View File

@@ -181,7 +181,7 @@ def start_service(host: str, port: int) -> Dict:
cmd = [ cmd = [
str(UVICORN_BIN), str(UVICORN_BIN),
"src.Vector.vector_service:app", "python.vector.vector_service:app",
"--host", host, "--host", host,
"--port", str(port), "--port", str(port),
] ]

View File

@@ -33,7 +33,7 @@ final class VectorControlCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$cmd = ['.venv/bin/python', 'src/Vector/vector_control.py']; $cmd = ['.venv/bin/python', 'python/vector/vector_control.py'];
if ($input->getOption('install')) { if ($input->getOption('install')) {
$cmd[] = '--install'; $cmd[] = '--install';

View File

@@ -29,7 +29,7 @@ final readonly class IngestFlow
) {} ) {}
// ========================================================= // =========================================================
// DOCUMENT INGEST // DOCUMENT INGEST (STREAMING SAFE)
// ========================================================= // =========================================================
public function ingestDocumentVersion(DocumentVersion $version): void public function ingestDocumentVersion(DocumentVersion $version): void
@@ -45,12 +45,34 @@ final readonly class IngestFlow
$existing = $this->chunkManager->countAllChunks(); $existing = $this->chunkManager->countAllChunks();
$records = iterator_to_array( $incoming = 0;
$this->knowledgeIngestService->buildChunkRecords($version), $generator = $this->knowledgeIngestService->buildChunkRecords($version);
false
); $wrappedGenerator = (function () use ($generator, $existing, &$incoming) {
foreach ($generator as $record) {
$incoming++;
$total = $existing + $incoming;
if ($total >= self::CHUNK_LIMIT_WARN) {
// Nur einmal warnen
if ($incoming === 1 || $total === self::CHUNK_LIMIT_WARN) {
// Logging erfolgt außerhalb des Streams final
}
}
if ($total > self::CHUNK_LIMIT_HARD) {
throw new \RuntimeException('Chunk limit exceeded.');
}
yield $record;
}
})();
$this->chunkManager->appendChunks($wrappedGenerator);
$incoming = count($records);
$total = $existing + $incoming; $total = $existing + $incoming;
if ($total >= self::CHUNK_LIMIT_WARN) { if ($total >= self::CHUNK_LIMIT_WARN) {
@@ -61,12 +83,6 @@ final readonly class IngestFlow
]); ]);
} }
if ($total > self::CHUNK_LIMIT_HARD) {
throw new \RuntimeException('Chunk limit exceeded.');
}
$this->chunkManager->appendChunks($records);
$this->rebuildIndex(false); $this->rebuildIndex(false);
$version->setIngestStatus(DocumentVersion::INGEST_INDEXED); $version->setIngestStatus(DocumentVersion::INGEST_INDEXED);
@@ -81,13 +97,11 @@ final readonly class IngestFlow
} }
// ========================================================= // =========================================================
// GLOBAL REINDEX // GLOBAL REINDEX (STREAMING SAFE)
// ========================================================= // =========================================================
public function globalReindex(): void public function globalReindex(): void
{ {
// 1⃣ Prüfen ob aktive Dokumente existieren
$activeDocuments = $this->em $activeDocuments = $this->em
->getRepository(Document::class) ->getRepository(Document::class)
->createQueryBuilder('d') ->createQueryBuilder('d')
@@ -102,22 +116,40 @@ final readonly class IngestFlow
); );
} }
// 2⃣ ChunkRecords erzeugen $incoming = 0;
$records = iterator_to_array(
$this->knowledgeIngestService->buildAllActiveChunkRecords(),
false
);
if (empty($records)) { $generator = $this->knowledgeIngestService->buildAllActiveChunkRecords();
$wrappedGenerator = (function () use ($generator, &$incoming) {
foreach ($generator as $record) {
$incoming++;
yield $record;
}
})();
// Prüfen ob überhaupt etwas kommt (ohne alles in RAM zu ziehen)
$peekIterator = $wrappedGenerator instanceof \Iterator
? $wrappedGenerator
: (function () use ($wrappedGenerator) {
foreach ($wrappedGenerator as $item) {
yield $item;
}
})();
if (!$peekIterator->valid()) {
$peekIterator->rewind();
}
if (!$peekIterator->valid()) {
throw new \RuntimeException( throw new \RuntimeException(
'Global Reindex abgebrochen: Es wurden keine Chunks erzeugt. Bitte prüfen Sie die Dokumente.' 'Global Reindex abgebrochen: Es wurden keine Chunks erzeugt.'
); );
} }
// 3⃣ Rewrite NDJSON $this->chunkManager->rewriteAll($peekIterator);
$this->chunkManager->rewriteAll($records);
// 4⃣ Rebuild Vector Index
$this->rebuildIndex(true); $this->rebuildIndex(true);
} }
@@ -137,18 +169,14 @@ final readonly class IngestFlow
throw new \RuntimeException('Document not found.'); throw new \RuntimeException('Document not found.');
} }
// Chunks entfernen
$this->chunkManager->compactByDocument($documentId); $this->chunkManager->compactByDocument($documentId);
// Dokument aus DB entfernen
$this->em->remove($document); $this->em->remove($document);
$this->em->flush(); $this->em->flush();
// 4⃣ Reindex nur wenn sinnvoll
$this->rebuildIndex(false); $this->rebuildIndex(false);
} }
// ========================================================= // =========================================================
// CENTRAL REBUILD // CENTRAL REBUILD
// ========================================================= // =========================================================

View File

@@ -23,13 +23,9 @@ final class ChunkManager
} }
// ============================================================ // ============================================================
// COUNT (für Guardrails / Limits) // COUNT (Streaming, robust)
// ============================================================ // ============================================================
/**
* Zählt Datensätze (NDJSON-Zeilen) im index.ndjson streaming-basiert.
* Leere / kaputte Zeilen werden ignoriert.
*/
public function countAllChunks(): int public function countAllChunks(): int
{ {
if (!is_file($this->indexPath)) { if (!is_file($this->indexPath)) {
@@ -42,6 +38,7 @@ final class ChunkManager
} }
$count = 0; $count = 0;
try { try {
while (($line = fgets($handle)) !== false) { while (($line = fgets($handle)) !== false) {
$line = trim($line); $line = trim($line);
@@ -49,7 +46,6 @@ final class ChunkManager
continue; continue;
} }
// NDJSON besteht aus JSON-Objekten; wir zählen nur valide Arrays.
$data = json_decode($line, true); $data = json_decode($line, true);
if (is_array($data)) { if (is_array($data)) {
$count++; $count++;
@@ -63,7 +59,7 @@ final class ChunkManager
} }
// ============================================================ // ============================================================
// APPEND // APPEND (Streaming + Exception Safe)
// ============================================================ // ============================================================
/** /**
@@ -82,27 +78,34 @@ final class ChunkManager
throw new \RuntimeException('Unable to open index.ndjson for append'); throw new \RuntimeException('Unable to open index.ndjson for append');
} }
foreach ($records as $record) { try {
$json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); foreach ($records as $record) {
if ($json === false) { $json = json_encode(
fclose($handle); $record,
throw new \RuntimeException('Unable to encode chunk record'); JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ($json === false) {
throw new \RuntimeException('Unable to encode chunk record');
}
if (fwrite($handle, $json . PHP_EOL) === false) {
throw new \RuntimeException('Unable to write chunk to index');
}
} }
} finally {
fwrite($handle, $json . PHP_EOL); fclose($handle);
} }
fclose($handle);
} }
// ============================================================ // ============================================================
// COMPACTION Entfernt alle Chunks eines Dokuments // COMPACTION (Streaming + Safe Handles)
// ============================================================ // ============================================================
public function compactByDocument(Uuid $documentId): void public function compactByDocument(Uuid $documentId): void
{ {
if (!is_file($this->indexPath)) { if (!is_file($this->indexPath)) {
return; // nichts zu kompaktieren return;
} }
$tmpPath = $this->indexPath . '.tmp'; $tmpPath = $this->indexPath . '.tmp';
@@ -116,32 +119,36 @@ final class ChunkManager
$docIdString = $documentId->toRfc4122(); $docIdString = $documentId->toRfc4122();
while (($line = fgets($in)) !== false) { try {
$line = trim($line); while (($line = fgets($in)) !== false) {
if ($line === '') { $line = trim($line);
continue; if ($line === '') {
} continue;
}
$data = json_decode($line, true); $data = json_decode($line, true);
if (!is_array($data)) { if (!is_array($data)) {
continue; // skip corrupted line continue;
} }
if (($data['document_id'] ?? null) === $docIdString) { if (($data['document_id'] ?? null) === $docIdString) {
continue; // skip this document's chunks continue;
} }
fwrite($out, $line . PHP_EOL); if (fwrite($out, $line . PHP_EOL) === false) {
throw new \RuntimeException('Unable to write compacted chunk');
}
}
} finally {
fclose($in);
fclose($out);
} }
fclose($in);
fclose($out);
$this->atomicSwitch($tmpPath, $this->indexPath); $this->atomicSwitch($tmpPath, $this->indexPath);
} }
// ============================================================ // ============================================================
// FULL REWRITE (Global Reindex) // FULL REWRITE (Streaming + Atomic)
// ============================================================ // ============================================================
/** /**
@@ -162,23 +169,30 @@ final class ChunkManager
throw new \RuntimeException('Unable to open temp index file'); throw new \RuntimeException('Unable to open temp index file');
} }
foreach ($records as $record) { try {
$json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); foreach ($records as $record) {
if ($json === false) { $json = json_encode(
fclose($handle); $record,
throw new \RuntimeException('Unable to encode chunk record'); JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
if ($json === false) {
throw new \RuntimeException('Unable to encode chunk record');
}
if (fwrite($handle, $json . PHP_EOL) === false) {
throw new \RuntimeException('Unable to write chunk during rewrite');
}
} }
} finally {
fwrite($handle, $json . PHP_EOL); fclose($handle);
} }
fclose($handle);
$this->atomicSwitch($tmpPath, $this->indexPath); $this->atomicSwitch($tmpPath, $this->indexPath);
} }
// ============================================================ // ============================================================
// STREAM READ (für FAISS rebuild) // STREAM READ (FAISS rebuild safe)
// ============================================================ // ============================================================
/** /**

View File

@@ -49,9 +49,8 @@ final class VectorIndexBuilder
// -------------------------------------------- // --------------------------------------------
// 🔵 FALL: NDJSON ist leer → kein Vector Index // 🔵 FALL: NDJSON ist leer → kein Vector Index
// -------------------------------------------- // --------------------------------------------
if (filesize($this->indexNdjsonPath) === 0) { if (!is_file($this->indexNdjsonPath) || filesize($this->indexNdjsonPath) === 0) {
// Alten Index entfernen
@unlink($this->vectorIndexPath); @unlink($this->vectorIndexPath);
@unlink($this->vectorMetaPath); @unlink($this->vectorMetaPath);
@@ -63,7 +62,7 @@ final class VectorIndexBuilder
); );
} }
return; // WICHTIG: kein Python, kein tmp, kein Fehler return;
} }
// -------------------------------------------- // --------------------------------------------
@@ -79,7 +78,6 @@ final class VectorIndexBuilder
$tmpVectorIndexPath = $this->vectorIndexPath . '.tmp'; $tmpVectorIndexPath = $this->vectorIndexPath . '.tmp';
// Clean leftovers
@unlink($tmpVectorIndexPath); @unlink($tmpVectorIndexPath);
@unlink($this->vectorMetaPath); @unlink($this->vectorMetaPath);
@@ -108,11 +106,15 @@ final class VectorIndexBuilder
private function assertPreconditions(): void private function assertPreconditions(): void
{ {
if (!is_file($this->scriptPath)) { if (!is_file($this->scriptPath)) {
throw new \RuntimeException('vector_ingest.py not found at: ' . $this->scriptPath); throw new \RuntimeException(
'Vector build script not found at: ' . $this->scriptPath
);
} }
if (!is_file($this->indexNdjsonPath)) { if (!is_file($this->indexNdjsonPath)) {
throw new \RuntimeException('index.ndjson not found at: ' . $this->indexNdjsonPath); throw new \RuntimeException(
'index.ndjson not found at: ' . $this->indexNdjsonPath
);
} }
} }