Confirm active opportunity classifications with read-only audit

This commit is contained in:
2026-10-09 07:38:01 +02:00
parent ae789ce024
commit ebfc188240
3 changed files with 34 additions and 1 deletions
+10
View File
@@ -197,3 +197,13 @@ Plano condicional de exemplos: classificação explícita pela auditoria existen
[Relatório e procedimento](revisao-origem-oportunidades.md). Página `/management/opportunity-origin`: seis itens, decisões real/exemplo/indeterminado, fundamento obrigatório, autor/data e pré-visualização antes de confirmação individual. Nenhuma classificação inferida por ausência de prova ou coincidência com seeder; associações a projetos/clusters permanecem separadas.
**QA:** 58 testes/917 asserções passaram; percurso no navegador com separação/reversão, API/mapa e consulta autorizada. Backup/restauro de 288 tabelas/cinco ficheiros; comparação de dez tabelas após ensaio demonstrou preservação. **Ativo:** leitura com zero decisões de classificação; sem escritas nesta entrega. Prova preparada válida até 10/10/2026 02:38:49 locais, aplicação exclusivamente autenticada após pré-visualização. Cobertura documental, questões jurídicas, ClamD e primeiro pedido real mantêm estados próprios.
### Confirmação ativa das classificações — 9 de outubro
Leitura `READ ONLY` após a ação autenticada: **IDs 1–6 classificados como `real`**, pela conta **13**, entre **02:48:05 e 02:51:00 locais**. Seis eventos `origin_classification_decided` registam estado anterior indeterminado, fundamento na decisão do Conselho de Ministros, autor, data e `relationships_changed=false`. Trata-se da decisão do responsável registada no portal; não de classificação inferida pelo seeder.
Todos mantêm estado editorial `published` e inclusão no filtro público do catálogo/API/mapa; nenhum foi separado como exemplo. A leitura confirma elegibilidade pelo filtro implementado, não a existência de coordenadas ou de um ponto no mapa para cada item.
**Preservação:** hashes das dez tabelas comparadas com o backup coincidem, incluindo IDs/dados das seis oportunidades, pedidos, recibos, mensagens e relações. Os **118 eventos históricos anteriores** permanecem presentes e inalterados; foram acrescentados seis eventos de classificação. Os dois eventos do ensaio QA são excluídos da comparação histórica, pois não pertencem à base ativa.
**Associações separadas:** zero relações oportunidade–projeto e oportunidade–cluster continuam presentes, tal como antes. As **28 localizações de projetos e 13 relações cluster–projeto** permanecem iguais ao backup. Classificar origem não aplica as correspondências ambíguas. Evidência privada: `opportunity-origin-active-confirmation.json`; leitor `tools/opportunity-origin-active-confirmation.php`. Nenhuma escrita ativa nesta confirmação. As restantes pendências independentes mantêm-se.
@@ -35,3 +35,9 @@ Provas privadas: `opportunity-origin-confirmation.json`, `opportunity-origin-bac
Na leitura protegida desta entrega há **zero decisões de classificação** na base ativa. Nenhum dos seis foi classificado, retirado do catálogo ou associado automaticamente. A página está preparada para revisão individual pelo responsável, começando pela pré-visualização. Não foi enviado contacto externo.
Continuam separadas: decisões sobre realidade dos investimentos e correspondências ambíguas; diferença de cobertura do inventário; questões jurídicas específicas; ClamD real; fontes institucionais pendentes específicas; primeiro pedido real no Balcão. Estes testes não concluem nenhuma dessas pendências.
## Confirmação posterior por leitura
Em 9 de outubro, após ação autenticada da conta 13: os seis IDs estão classificados como `real`, com seis eventos entre 02:48:05 e 02:51:00 locais e fundamento registado na decisão do Conselho de Ministros. Todos continuam publicados e incluídos no filtro público; não foram criados exemplos. O estado anterior de zero decisões acima descreve a entrega anterior à ação do utilizador.
Comparação protegida: dez tabelas iguais ao backup e 118 eventos históricos preservados, sem alterações nem ausências; seis eventos novos de classificação. Eventos exclusivamente QA excluídos da comparação. IDs, relações e interações preservados. As relações oportunidade–projeto/cluster continuam vazias; 28 localizações de projetos e 13 relações cluster–projeto permanecem preservadas. Evidência privada `opportunity-origin-active-confirmation.json`. Nenhuma escrita ativa efetuada na confirmação.
@@ -13,6 +13,23 @@ foreach ($records as &$record) {
}
unset($record);
$checks = [];
$restored = $backup['restored_target'];
if (! str_ends_with($restored['database'], '_test') || (int) $restored['port'] !== 55441) {
throw new RuntimeException('Invalid isolated comparison target');
}
$baseline = new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$restored['database'], $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$baseline->exec('BEGIN TRANSACTION READ ONLY');
$oldEvents = $baseline->query("SELECT id,row_to_json(t)::text AS row FROM audit_events t WHERE source_system IS DISTINCT FROM 'opportunity-origin-review' ORDER BY id")->fetchAll(PDO::FETCH_KEY_PAIR);
$currentEvents = $pdo->query('SELECT id,row_to_json(t)::text AS row FROM audit_events t ORDER BY id')->fetchAll(PDO::FETCH_KEY_PAIR);
$missing = $changed = 0;
foreach ($oldEvents as $id => $row) {
if (! isset($currentEvents[$id])) {
$missing++;
} elseif ($currentEvents[$id] !== $row) {
$changed++;
}
}
$baseline->exec('ROLLBACK');
foreach (['opportunities','opportunity_projects','opportunity_clusters','opportunity_favourites','interest_requests','interest_submission_receipts','investment_messages','tasks','project_locations','cluster_projects'] as $table) {
$key = '"public"."'.$table.'"';
$rows = $pdo->query('SELECT row_to_json(t)::text AS row FROM '.$key.' t ORDER BY row_to_json(t)::text')->fetchAll(PDO::FETCH_COLUMN);
@@ -20,6 +37,6 @@ foreach (['opportunities','opportunity_projects','opportunity_clusters','opportu
$checks[$table] = ['matches_backup' => $hash === ($backup['table_hashes'][$key] ?? null), 'hash' => $hash, 'rows' => count($rows)];
}
$pdo->exec('ROLLBACK');
$proof = ['captured_at' => gmdate('c'), 'identity' => $identity, 'records' => $records, 'audit' => $events, 'preservation' => $checks, 'active_writes' => 0];
$proof = ['captured_at' => gmdate('c'), 'identity' => $identity, 'records' => $records, 'audit' => $events, 'preservation' => $checks, 'historical_audit' => ['baseline_events' => count($oldEvents), 'missing' => $missing, 'changed' => $changed, 'additional_events' => count($currentEvents) - count($oldEvents)], 'active_writes' => 0];
file_put_contents($directory.'/opportunity-origin-active-confirmation.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;