= $len) break; $start += ($size - $overlap); } return $chunks; } $sources = require APP_PATH . '/config/nis2_sources.php'; $isoKeys = ['iso_27001_2022','iso_27002_2022','iso_27017_2015','iso_27018_2019']; // Costruisce un documento di testo per ciascuno standard ISO: descrizione fonte // + elenco dei controlli (codice, titolo IT/EN, tema) presi dal DB curato. $standardMap = [ 'iso_27001_2022' => 'iso27001', 'iso_27017_2015' => 'iso27017', 'iso_27018_2019' => 'iso27018', ]; logln('=== Ingest fonti ISO 27000 nella KB (scope SYSTEM) ==='); if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.'); $embed = null; $vector = null; if (!$dryRun) { $embed = new EmbedService(); $vector = new VectorService(); $vector->ensureCollection($embed->dims); } $totalChunks = 0; $done = 0; foreach ($isoKeys as $key) { if (!isset($sources[$key])) { logln("SKIP {$key}: non nel registry"); continue; } $src = $sources[$key]; $body = "FONTE: {$src['citation']}\nAUTORITA: {$src['authority']}\n\n{$src['full']}\n\n"; // Catalogo controlli (se questo standard ha una mappatura DB) if (isset($standardMap[$key])) { $std = $standardMap[$key]; try { $rows = Database::fetchAll( 'SELECT control_code, theme, title_it, title_en, iso27002_ref, condition_tag FROM iso27001_annex_controls WHERE standard = ? ORDER BY sort_order', [$std] ); } catch (Throwable $e) { logln(" ERRORE lettura iso27001_annex_controls (migration 038 applicata?): " . $e->getMessage()); $rows = []; } if ($rows) { $body .= "CATALOGO CONTROLLI ({$src['short']}):\n"; foreach ($rows as $r) { $cond = $r['condition_tag'] ? " [condizionale: {$r['condition_tag']}]" : ''; $ref = $r['iso27002_ref'] ? " ({$r['iso27002_ref']})" : ''; $body .= "- {$r['control_code']} - {$r['title_it']} / {$r['title_en']} [tema: {$r['theme']}]{$ref}{$cond}\n"; } } } $body = preg_replace('/[ \t]+/', ' ', $body); $chunks = chunkText($body, 2000, 200); logln("Fonte: {$src['short']} -> " . count($chunks) . ' chunk'); $totalChunks += count($chunks); if ($dryRun) { $done++; continue; } // Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte try { $vector->deleteByFilter(['must' => [ ['key' => 'scope', 'match' => ['value' => 'SYSTEM']], ['key' => 'source', 'match' => ['value' => $src['citation']]], ]]); } catch (Exception $e) { logln(' (warning) delete precedente: ' . $e->getMessage()); } $docUuid = uuid(); $points = []; foreach ($chunks as $i => $chunk) { $vec = null; for ($try = 1; $try <= 5; $try++) { try { $vec = $embed->embed($chunk); break; } catch (Throwable $e) { if ($try === 5) { logln(" ERRORE embed chunk {$i}: " . $e->getMessage()); throw $e; } sleep($try); } } $points[] = [ 'id' => uuid(), 'vector' => $vec, 'payload' => [ 'doc_uuid' => $docUuid, 'title' => $src['short'] . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''), 'chunk' => $chunk, 'entity_type' => 'standard_iso', 'source' => $src['citation'], 'lang' => 'it', 'scope' => 'SYSTEM', 'consulting_firm_id' => null, 'organization_id' => null, 'shared_with_orgs' => [], 'uploaded_by' => 0, ], ]; } foreach (array_chunk($points, 64) as $batch) { $vector->upsertBatch($batch); } try { $stmt = Database::getInstance()->prepare( "INSERT INTO kb_uploaded_documents (qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status) VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'standard_iso', ?, 'it', ?, '[]', 'ready')" ); $stmt->execute([$docUuid, $src['short'], $src['citation'], count($chunks)]); } catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); } logln(" OK indicizzato (doc_uuid={$docUuid})"); $done++; } logln("=== Completato: {$done} fonti ISO, {$totalChunks} chunk totali ===");