[FIX] Gap Analysis ACN: bug da review avversariale (4 agenti)

#1 CRITICO aiAnalyze: askWithRag ritorna ['answer','sources','rag_used'], non
   una stringa. Ora estrae 'answer' (ai_summary) e salva 'sources' in
   ai_recommendations. Prima salvava il JSON intero in ai_summary.
#2 ALTO corpus RAG: acn_requirements.json aveva 188/203 testi TRONCATI alla
   prima riga PDF (es. GV.PO-01#1: 84 char invece di 838). Rigenerato dai testi
   INTEGRALI di acn_measures.json (87+116, zero troncamenti). Ri-ingest Qdrant.
#3 MEDIO catalog(): org non classificata dava entity_level=null + warning PHP
   $totals[null] + TypeError frontend. Ora 422 ENTITY_LEVEL_REQUIRED come create().
#4 MEDIO guida cap-5 GV.RR-04: "figure chiave dell'organigramma" era errato e
   auto-contraddittorio -> "personale autorizzato + amministratori di sistema,
   valutazione esperienza/capacita/affidabilita" (allineato testo ACN).
#5 BASSI: openAcn try/catch (no unhandled rejection su Riprendi); badge
   importante/essenziale IT/EN; overall_score=null (non 0.0) se tutti N/A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-01 12:18:25 +02:00
co-authored by Claude Opus 4.8
parent 09f51795c1
commit d6924a30c1
4 changed files with 1667 additions and 1646 deletions
@@ -47,6 +47,15 @@ class AcnAssessmentController extends BaseController
{
$this->requireOrgAccess();
$level = $this->resolveEntityLevel();
if ($level === null) {
// Coerente con create(): senza classificazione non c'e perimetro.
// Il frontend intercetta ENTITY_LEVEL_REQUIRED e mostra l'invito a classificare.
$this->jsonError(
'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
422,
'ENTITY_LEVEL_REQUIRED'
);
}
$grouped = $this->groupedRequirements($level, []);
$totals = $this->datasetTotals();
@@ -385,18 +394,23 @@ class AcnAssessmentController extends BaseController
'organization_id' => $this->getCurrentOrgId(),
'consulting_firm_id' => $this->currentUser['consulting_firm_id'] ?? null,
];
$answer = $ai->askWithRag($question, $userContext);
// askWithRag ritorna ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]:
// estraiamo il testo, NON salviamo l'intero array.
$result = $ai->askWithRag($question, $userContext);
$answer = is_array($result) ? (string) ($result['answer'] ?? '') : (string) $result;
$sources = (is_array($result) && isset($result['sources'])) ? $result['sources'] : [];
} catch (Throwable $e) {
error_log('[AcnAssessment] aiAnalyze fallita: ' . $e->getMessage());
$this->jsonError('Analisi AI temporaneamente non disponibile. Riprova piu tardi.', 503, 'AI_UNAVAILABLE');
}
Database::update('acn_assessments', [
'ai_summary' => is_string($answer) ? $answer : json_encode($answer, JSON_UNESCAPED_UNICODE),
'ai_summary' => $answer,
'ai_recommendations' => json_encode($sources, JSON_UNESCAPED_UNICODE),
], 'id = ?', [$id]);
$this->logAudit('acn_assessment_ai_analyzed', 'acn_assessment', $id, ['gaps' => count($gaps)]);
$this->jsonSuccess(['ai_summary' => $answer], 'Analisi AI completata');
$this->jsonSuccess(['ai_summary' => $answer, 'sources' => $sources], 'Analisi AI completata');
}
// ════════════════════════ HELPER ════════════════════════
@@ -580,7 +594,9 @@ class AcnAssessmentController extends BaseController
$cnt++;
}
$overall = $cnt > 0 ? round($sum / $cnt, 2) : 0.0;
// Se nessun requisito e applicabile (tutti not_applicable) il punteggio
// non e definito: null, non 0.0 (che significherebbe "0% conforme").
$overall = $cnt > 0 ? round($sum / $cnt, 2) : null;
$funcScores = [];
foreach ($byFunc as $fc => $d) {
$funcScores[$fc] = $d['cnt'] > 0 ? round($d['sum'] / $d['cnt'], 2) : 0.0;