[KB] #384 — ingest KB/RAG 'Archivio Policy unificato' (scope SYSTEM)

Indicizza in Qdrant nis2_kb (SYSTEM) la conoscenza prodotto sulle nuove funzioni Policy:
unificazione Policy/Procedura, collegamento M:N con Misure e Requisiti, allegati, matrice RACI
(con la nota fonti-certe: R+A per approvare = regola di prodotto, non obbligo NIS2; ISO distingue
policy/procedura come best practice). Script self-contained sotto application/cli (montata in
nis2-app), doc inline, idempotente per source. Eseguito host-side: 2 chunk, doc_uuid 1df7926a,
tracking kb_uploaded_documents id=17. Verifica RAG: searchForUser ritorna il doc come 1° risultato
(score 0.63-0.67) su 3 query (Policy=Procedura, RACI, allegati/link). Chiude il residuo KB di #384.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-19 07:06:39 +02:00
co-authored by Claude Opus 4.8
parent 726a2a80ef
commit fb2b7ecc2f
+157
View File
@@ -0,0 +1,157 @@
<?php
/**
* Ingest KB (RAG) — Archivio Policy unificato (ticket #384).
* ----------------------------------------------------------------------------
* Indicizza nella collection Qdrant `nis2_kb` (scope SYSTEM) la conoscenza
* prodotto sulle nuove funzioni Policy (#384): unificazione Policy/Procedura,
* collegamento M:N con Misure e Requisiti, allegati, matrice RACI. Cosi'
* l'assistente AI (ARIA) e AIService::askWithRag() possono rispondere e citare.
*
* Self-contained: il testo del documento e' INLINE (niente file esterno), e lo
* script vive sotto application/cli/ (montata in nis2-app) → si esegue diretto:
* docker exec nis2-app php /var/www/nis2-agile/application/cli/ingest_kb_policy_384.php
* docker exec nis2-app php .../ingest_kb_policy_384.php --dry-run # statistiche, no upsert
*
* Idempotente: cancella i chunk SYSTEM di questa stessa `source` prima del re-upsert.
* Richiede Qdrant (172.21.0.5) + Voyage → eseguire SU HETZNER (nis2-app), non dal devenv.
* ============================================================================
*/
if (PHP_SAPI !== 'cli') { fwrite(STDERR, "Solo CLI\n"); exit(1); }
define('APP_PATH', dirname(__DIR__)); // .../application
define('BASE_PATH', dirname(APP_PATH)); // project root
require_once APP_PATH . '/config/env.php';
require_once APP_PATH . '/config/config.php';
require_once APP_PATH . '/config/database.php';
require_once APP_PATH . '/services/EmbedService.php';
require_once APP_PATH . '/services/VectorService.php';
$opts = getopt('', ['dry-run']);
$dryRun = isset($opts['dry-run']);
$SOURCE = 'NIS2 Agile — Archivio Policy unificato (#384)';
$TITLE = 'Policy unificate: collegamento Misure e Requisiti, allegati, matrice RACI';
$DOC = <<<'MD'
DOCUMENTO: Archivio Policy unificato in NIS2 Agile (funzioni introdotte dal ticket #384).
USO: aiutare l'utente a capire le Policy, il loro collegamento con Misure e Requisiti, gli allegati e la matrice RACI.
## Policy e Procedure: un unico archivio
In NIS2 Agile i termini "Policy" e "Procedura" indicano lo stesso oggetto: l'archivio e' unico ed e' gestito dalla pagina "Policy". Una policy puo' rappresentare sia una politica di alto livello sia una procedura operativa.
Nota di metodo (fonti certe): questa unificazione e' una scelta di prodotto. Nessuna norma NIS2 — Direttiva (UE) 2022/2555 e D.Lgs. 138/2024 art. 24 (misure di gestione del rischio) — impone di tenere separate policy e procedure. Lo standard ISO/IEC 27001:2022 (cl. 5.2) e la ISO/IEC 27002 distinguono la "policy" (dichiarazione di intenti di alto livello) dalla "procedura" (documento operativo): e' una buona pratica, NON un obbligo di legge.
## Codici Policy
Ogni policy ha un codice nella forma "Policy.NN" (in precedenza "Proc.NN"). Il codice identifica la policy nell'elenco e nei collegamenti.
## Collegamento con Misure e Requisiti (relazione molti-a-molti)
Le pagine "Policy" e "Misure e Requisiti" sono collegate con una relazione molti-a-molti:
- un requisito NIS2 puo' essere coperto da piu' policy;
- una policy puo' coprire piu' requisiti NIS2.
Nella pagina "Misure e Requisiti" la colonna "Policy (default)" elenca tutte le policy collegate a ciascun requisito. Nella pagina "Policy" la colonna "Requisito NIS2" elenca tutti i requisiti collegati a ciascuna policy; il codice del requisito e' la concatenazione di MISURA + REQUISITO (esempio: "DE.CM-01 comma 1").
I codici mostrati sono link cliccabili: da una policy si apre il requisito collegato, e dal requisito si apre la policy collegata (navigazione bidirezionale tra le due pagine).
## Allegati alle Policy
Nel dettaglio di ogni policy e' presente la sezione "Allegati": si possono caricare file (PDF, immagini PNG/JPG/GIF/WEBP, documenti Office DOC/DOCX/XLS/XLSX/PPT/PPTX/ODT/ODS, txt, csv, zip), fino a 10 MB ciascuno. Per sicurezza non sono ammessi file html, svg o js (prevenzione XSS). Ogni allegato e' registrato con autore e data e puo' essere scaricato.
## Matrice RACI sulla Policy
Ogni policy ha una "Matrice RACI" per assegnare le responsabilita' ai ruoli definiti nell'Organigramma del prodotto. I quattro ruoli RACI sono:
- R = Responsible: chi esegue concretamente l'attivita';
- A = Accountable: chi ne risponde e la approva (unico responsabile finale);
- C = Consulted: chi viene consultato;
- I = Informed: chi viene informato.
Regola di approvazione: per APPROVARE una policy occorre aver assegnato almeno una R e almeno una A; in stato di bozza l'assegnazione e' libera e non vincolante. Questa e' una regola di governance interna del prodotto, NON un obbligo normativo NIS2: serve a garantire che ogni policy approvata abbia un esecutore e un responsabile chiari.
## Finestra di modifica Policy
Nella finestra "Modifica Policy" le etichette dei campi compaiono sopra i rispettivi campi (allineamento corretto, conforme al design system AGID/Bootstrap Italia).
MD;
function logln(string $m): void { echo '[' . date('Y-m-d H:i:s') . "] $m\n"; }
function chunkText(string $text, int $size = 2000, int $overlap = 200): array
{
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
$chunks = []; $len = mb_strlen($text, 'UTF-8'); $start = 0;
while ($start < $len) {
$take = min($size, $len - $start);
$piece = mb_substr($text, $start, $take, 'UTF-8');
if (trim($piece) !== '') $chunks[] = $piece;
if ($start + $take >= $len) break;
$start += ($size - $overlap);
}
return $chunks;
}
function uuid(): string
{
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
}
logln('=== Ingest KB "Archivio Policy unificato (#384)" (scope SYSTEM) ===');
if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.');
$text = preg_replace('/[ \t]+/', ' ', $DOC);
$text = preg_replace('/\n{3,}/', "\n\n", trim($text));
if (strlen($text) < 200) { logln('ERRORE: testo troppo breve.'); exit(1); }
$header = "DOCUMENTO: Funzioni Policy di NIS2 Agile (unificazione Policy/Procedura, collegamento Misure e Requisiti, allegati, matrice RACI).\n"
. "USO: rispondere alle domande dell'utente su Policy, requisiti collegati, allegati e RACI.\n\n";
$chunks = chunkText($header . $text, 2000, 200);
logln(' testo: ' . strlen($text) . ' char -> ' . count($chunks) . ' chunk');
if ($dryRun) { logln('=== DRY-RUN completato ==='); exit(0); }
$embed = new EmbedService();
$vector = new VectorService();
$vector->ensureCollection($embed->dims);
// Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte
try {
$vector->deleteByFilter(['must' => [
['key' => 'scope', 'match' => ['value' => 'SYSTEM']],
['key' => 'source', 'match' => ['value' => $SOURCE]],
]]);
} 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} dopo 5 tentativi: " . $e->getMessage()); throw $e; }
logln(" retry embed chunk {$i} (tentativo {$try})"); sleep($try);
}
}
$points[] = [
'id' => uuid(),
'vector' => $vec,
'payload' => [
'doc_uuid' => $docUuid,
'title' => $TITLE . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''),
'chunk' => $chunk,
'entity_type' => 'guida_prodotto',
'source' => $SOURCE,
'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); }
// Tracking MySQL (best-effort)
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, ?, 'guida_prodotto', ?, 'it', ?, '[]', 'ready')"
);
$stmt->execute([$docUuid, $TITLE, $SOURCE, count($chunks)]);
} catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); }
logln("=== OK indicizzato: doc_uuid={$docUuid}, " . count($chunks) . " chunk ===");