fix: recover territorial demarcation QA and prepare retained geometry restoration
This commit is contained in:
@@ -318,4 +318,7 @@ Situação comum observada: `publication_status=draft`, `validation_status=valid
|
||||
|
||||
### Referência da entrada única de gestão — QA, 22/10/2026
|
||||
|
||||
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
|
||||
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
|
||||
|
||||
|
||||
AUD-GEO-02/03: Recuperação da demarcação territorial — QA, 22/10/2026. 408/581 limites recuperados em QA; cadeia territorial guardada/reaberta no portal. Homologacao ainda sem reposicao; consultar a nova secao no manual existente. Mantidas 173 correspondencias pendentes e ausencia de geometrias proprias das localidades. Origem: pedido expresso da Tempo de recuperar funcionalidade; enquadramento e mensagens sao escolhas tecnicas. Nao encerra o criterio de reposicao no destino.
|
||||
|
||||
@@ -2254,4 +2254,34 @@ Verificação nova: quatro testes, 99 asserções, com transacções revertidas
|
||||
|
||||
No navegador, login e MFA normais da conta sintética de equipa; entrada → legislação; entrada → nova oportunidade → guardar preparação incompleta → recarregar, com título recuperado e ficha QA 241. Não foi repetido o workflow integral de revisão/publicação de oportunidades já existente; a prova nova cobre a entrada e a gravação/recuperação. O ensaio documental completo foi HTTP transaccional, não uma prova visual de cada clique. A conta QA tem autorizações explicitamente sintéticas; não comprova as designações das contas reais. Não transportar fixtures, configurações QA ou concessões de acesso.
|
||||
|
||||
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
|
||||
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
|
||||
|
||||
|
||||
## Recuperação da demarcação territorial — QA, 22/10/2026
|
||||
|
||||
Retomada expressa da Tempo como recuperação de funcionalidade anteriormente demonstrada, nos requisitos existentes AUD-GEO-02/03 e na experiência de cadastro. Não é criação de outra cartografia. O incremento de divulgação promocional permanece na sua branch independente.
|
||||
|
||||
**Causa comprovada:** leitura SSH autenticada, em transacção READ ONLY, da release `8286d750`/base `teste_investidor`: zero linhas em `geo_geometries`, incluindo eliminadas. Cadastro preservado: 11 províncias, 148 distritos, 437 postos e 1324 localidades. Directório dos originais privados ausente; importador desabilitado. Serviço de consulta, controlador e correspondências iguais por SHA256 ao código QA. Hash do cadastro (IDs, nomes, pais, nível, origem/códigos): `9de4181c42055bc6ef11b56333b64dd463d0ebe16bb2d1ea3238fc79e69024f7`, igual no QA e destino. Não se confirmou recusa de acesso nem perda de associação: faltam os próprios dados.
|
||||
|
||||
**Histórico:** as provas de 09–10/10 em `docs/investmoz/v2/auditoria-portal/correspondencias-cartograficas.md` registam 361→393→408 correspondências em bases QA anteriores. Os originais e recibos privados permanecem disponíveis. Os transportes de 19–20/10 deste manual excluíram expressamente geometrias; a disponibilização Git também exclui dados e ficheiros privados (`deploy/homologacao/README.md`). Em 22/10 a leitura de Cabo Delgado já encontrara zero linhas; esta passagem conferiu a tabela inteira. Prova-se uma diferença de composição entre QA cartográfico e homologação. Não foi localizado recibo da instalação anterior destes limites na mesma base; não se atribui data a uma suposta eliminação nem se descarta a observação anterior no mesmo endereço.
|
||||
|
||||
**Recuperação executada em QA:** o clone actual também tinha zero geometrias. Reutilizados seis originais INE/UNOCHA de 07/06/2019 (três GeoJSON e três metadados), hashes preservados e correspondências v2 existentes. O serviço `TerritoryGeometryImport` inseriu 11 províncias, 133 distritos e 264 postos, internos, com recibos e auditoria. Todos válidos/SRID4326. Nos exemplos, a consulta pública continua sem devolver limites internos. ClamAV real, assinaturas de 22/10: três originais limpos. ADM3, 26773796 bytes, excedeu o stream de 25 MiB e passou pelo scanner de ficheiro GIS com orçamento de 40 MiB nesse processo; tentativa inconclusiva preservada. Sem alterar limites globais ou configuração do daemon.
|
||||
|
||||
O mapa enquadra o limite seleccionado; se não existir, enquadra o nível superior disponível mais próximo e informa que é contexto. A lista distingue selecção de contexto superior. Fonte, edição e actualidade jurídica indeterminada preservadas. Limite administrativo não representa parcela nem autorização de investimento.
|
||||
|
||||
**Prova de interface:** oportunidade sintética QA268 criada no portal. Cabo Delgado → ANCUABE → posto ANCUABE → ANCUABE-SEDE; limites e aproximação progressiva nos três primeiros níveis. Recurso de contexto/principal guardado; após recarregar e abrir “Ver / editar / restaurar”, os quatro níveis foram recuperados. A localidade sem limite próprio apresenta aviso dos níveis superiores. A fixture QA255 anterior tinha versão fechada/payload sintético mínimo incompatível com gravação do contrato; não foi usada como prova de persistência nem corrigida silenciosamente. Uma navegação durante a recompilação QA conservava referência a asset removido; recarregar resolveu o carregamento, sem alteração no destino.
|
||||
|
||||
**Cobertura:** 408/581 elementos do exemplar aceite: 11/11 províncias, 133/159 distritos, 264/411 postos. Mantêm-se 26+147 correspondências pendentes, com nomes nas prévias e no documento de correspondências existente. O denominador do cadastro é diferente (148 distritos, 437 postos). Este exemplar não contém geometrias próprias de localidades ou parcelas; não atribuir limites superiores às 1324 localidades.
|
||||
|
||||
**Reposição preparada, não aplicada na homologação:** `outputs/investmoz-territory-recovery-20261022/demarcation/retained-geometry-restoration.zip`, SHA256 `53a5d8aa2be57603c928279cbd14561741dd2ec4b44b138fbc785380de6f951d`, contém seis originais e manifesto. Não contém dump de substituição, actores QA ou limites sintéticos. Procedimento existente:
|
||||
|
||||
1. Rever e integrar pelo Git o ajuste visual; conferir revisão efectivamente servida.
|
||||
2. Colocar originais no armazenamento privado partilhado, conservando hashes. Executar scanner actual ligado aos originais. Conferir backup/restauro e prova de activação da revisão/alvo, como exige o importador já existente; disponibilizar a configuração do importador.
|
||||
3. Gestor já autorizado, com MFA, abre `/management/territory-geometry-import`: prévia e aplicação de províncias, depois distritos, depois postos, com motivo/confirmar. Reconciliar prévia fresca no destino; não executar hashes antigos do QA. Recibos e reversão dos lotes novos existentes preservados.
|
||||
4. Conferir a cadeia territorial no formulário do destino e a recuperação após guardar, mantendo lacunas explícitas.
|
||||
|
||||
O staging dos originais, backup/restauro e configuração inicial ainda exigem operação técnica. Depois da reposição, seleccionar, guardar e recuperar a localização/mapa usam a interface. Esta passagem só leu o servidor remoto: sem alterar dados, permissões, camadas ou configuração. Não declarar a reposição no destino concluída.
|
||||
|
||||
Origem das restrições: decisões expressas da Tempo e requisitos anteriores do importador para preservação/backup e autenticação. Enquadramento e mensagens são escolhas de implementação, sem novo papel ou aprovador. Fonte oficial aceite reutilizada sem exigir nova validação da credibilidade.
|
||||
|
||||
Provas na pasta desta recuperação: inventário dos seis ficheiros, prévias/recibos por nível, `verification.json`, `homologation-readonly.json` e capturas. TypeScript, build QA e sintaxe PHP verificados; provas editoriais anteriores não repetidas.
|
||||
|
||||
@@ -19,6 +19,8 @@ export type InvestmentMapFeature = {
|
||||
};
|
||||
};
|
||||
export type InvestmentMapArea = {
|
||||
selected_territory?: boolean;
|
||||
location_type?: string;
|
||||
purpose?: string;
|
||||
geometry: GeoJsonObject;
|
||||
title: string;
|
||||
@@ -74,6 +76,13 @@ export default function InteractiveInvestmentMap({
|
||||
.addTo(instance);
|
||||
L.control.scale({ imperial: false }).addTo(instance);
|
||||
const bounds: [number, number][] = [];
|
||||
const hasSelectedBoundary = areas.some((area) => area.selected_territory);
|
||||
const administrativeLevel: Record<string, number> = {
|
||||
province: 1, district: 2, admin_post: 3, locality: 4,
|
||||
};
|
||||
const closestContextLevel = contextOnly
|
||||
? Math.max(0, ...areas.map((area) => administrativeLevel[area.location_type ?? ''] ?? 0))
|
||||
: 0;
|
||||
for (const area of areas) {
|
||||
const label = document.createElement('p');
|
||||
label.textContent = `${area.title} · ${area.purpose === 'administrative_boundary' ? 'Limite administrativo da edição indicada; não representa um local de investimento' : 'Polígono cadastrado, limites oficiais por verificar'} · Fonte: ${area.source_reference ?? 'Não indicada'} · Data: ${area.source_date ?? 'Não documentada'}`;
|
||||
@@ -92,7 +101,10 @@ export default function InteractiveInvestmentMap({
|
||||
.bindPopup(label)
|
||||
.addTo(instance);
|
||||
const extent = layer.getBounds();
|
||||
if (extent.isValid())
|
||||
const focusBoundary = hasSelectedBoundary
|
||||
? area.selected_territory
|
||||
: !closestContextLevel || administrativeLevel[area.location_type ?? ''] === closestContextLevel;
|
||||
if (extent.isValid() && focusBoundary)
|
||||
bounds.push(
|
||||
[extent.getSouth(), extent.getWest()],
|
||||
[extent.getNorth(), extent.getEast()],
|
||||
|
||||
@@ -71,6 +71,12 @@ export default function TerritoryGeometryPreview({
|
||||
? 'Limite administrativo disponível para este território.'
|
||||
: 'Sem limite administrativo acessível para este território neste perfil.'}
|
||||
</p>
|
||||
{!context.selected_geometry_available && context.areas.length > 0 && (
|
||||
<p role="note">
|
||||
O mapa mostra apenas limites dos níveis superiores como contexto.
|
||||
Estes limites não representam o território seleccionado.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
{context.context_geometry_count} limites disponíveis na
|
||||
hierarquia selecionada. São contexto administrativo, não
|
||||
@@ -93,7 +99,7 @@ export default function TerritoryGeometryPreview({
|
||||
<ul>
|
||||
{context.areas.map((area, index) => (
|
||||
<li key={index}>
|
||||
{area.title} · Fonte:{' '}
|
||||
{area.title} · {area.selected_territory ? 'Território seleccionado' : 'Contexto de nível superior'} · Fonte:{' '}
|
||||
{area.source_reference ?? 'Não indicada'} ·
|
||||
Edição: {area.source_date ?? 'Não indicada'}
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
// Rehearse recovery with the existing importer. Never accepts an active database.
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
$app = require dirname(__DIR__).'/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$target = Illuminate\Support\Facades\DB::selectOne('select current_database() as database,inet_server_port() as port');
|
||||
throw_unless($target->database === 'simulator_fiscal_delta_20261021_test' && (int) $target->port === 55441
|
||||
&& config('database.connections.pgsql.host') === '127.0.0.1', RuntimeException::class, 'Isolated QA only.');
|
||||
$app->detectEnvironment(fn () => 'testing');
|
||||
config(['geometry-import.enabled' => true, 'portal.managers' => ['qa-content-promoter@example.test'],
|
||||
'services.malware.scanner' => 'clamd', 'services.malware.socket' => null,
|
||||
'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
||||
$actor = App\Models\User::where('email', 'qa-content-promoter@example.test')->firstOrFail();
|
||||
$directory = config('geometry-import.directory');
|
||||
$out = storage_path('app/private/territory-demarcation-recovery');
|
||||
Illuminate\Support\Facades\File::ensureDirectoryExists($out, 0700);
|
||||
$manifest = [];
|
||||
foreach (glob($directory.'/moz-adm*-ine-*') as $file) {
|
||||
$manifest[] = ['file' => basename($file), 'bytes' => filesize($file), 'sha256' => hash_file('sha256', $file)];
|
||||
}
|
||||
file_put_contents($out.'/source-inventory.json', json_encode($manifest, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
$scanner = app(App\Services\Malware\MalwareScanner::class);
|
||||
$health = $scanner->health();
|
||||
throw_unless($health['available'], RuntimeException::class, 'Current scanner unavailable.');
|
||||
$scans = [];
|
||||
foreach ([1, 2, 3] as $level) {
|
||||
$file = $directory.'/moz-adm'.$level.'-ine-20190607.geojson';
|
||||
// Scope the stream budget to this retained GIS original in this process only.
|
||||
config(['services.malware.max_file_bytes' => filesize($file), 'services.malware.max_stream_bytes' => filesize($file)]);
|
||||
$scan = $scanner->scan($file, filesize($file), storage_path('app/private'));
|
||||
$scan['signatures_at'] = $scan['signatures_at']?->format(DATE_ATOM);
|
||||
if ($level === 3 && $scan['reason_code'] === 'scanner_decompression_limit') {
|
||||
$binary = getenv('GIS_CLAMSCAN_BINARY');
|
||||
$database = getenv('GIS_CLAMSCAN_DATABASE');
|
||||
throw_unless(is_file($binary ?: '') && is_dir($database ?: ''), RuntimeException::class, 'GIS scanner paths required.');
|
||||
$process = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--max-filesize=40M', '--max-scansize=100M', '--no-summary', $file]);
|
||||
$process->setTimeout(120)->mustRun();
|
||||
throw_unless(str_ends_with(trim($process->getOutput()), ': OK'), RuntimeException::class, 'GIS original scan incomplete.');
|
||||
$version = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--version']);
|
||||
$version->mustRun();
|
||||
$parts = explode('/', trim($version->getOutput()));
|
||||
$signatureTime = strtotime(end($parts));
|
||||
throw_unless($signatureTime && time() - $signatureTime < 48 * 3600, RuntimeException::class, 'Current GIS signatures required.');
|
||||
$scan = ['result' => 'clean', 'engine' => 'ClamAV', 'engine_version' => trim($version->getOutput()),
|
||||
'signatures_at' => gmdate(DATE_ATOM, $signatureTime), 'scope' => 'retained GIS original only',
|
||||
'original_sha256' => hash_file('sha256', $file), 'stream_attempt' => $scan, 'exit_code' => $process->getExitCode()];
|
||||
}
|
||||
$scans[] = ['file' => basename($file), 'sha256' => hash_file('sha256', $file), 'scan' => $scan];
|
||||
echo json_encode(['level' => $level, 'scan' => $scan]).PHP_EOL;
|
||||
throw_unless($scan['result'] === 'clean', RuntimeException::class, 'Original scan incomplete.');
|
||||
}
|
||||
file_put_contents($directory.'/geometry-source-scans.json', json_encode(['captured_at' => gmdate('c'), 'files' => $scans], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
$service = app(App\Services\TerritoryGeometryImport::class);
|
||||
foreach ([1, 2, 3] as $level) {
|
||||
$plan = $service->preview($actor, $level);
|
||||
file_put_contents($out.'/preview-'.$level.'.json', json_encode($plan, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['level' => $level, 'resolved' => $plan['resolved'], 'pending' => $plan['pending']]).PHP_EOL;
|
||||
if (! in_array('--apply', $argv, true)) break; // Lower levels require imported parent correspondences.
|
||||
$result = $service->apply($actor, ['level' => $level, 'hash' => $plan['hash'], 'idempotency_key' => (string) Illuminate\Support\Str::uuid(),
|
||||
'reason' => 'Recuperação QA da demarcação anteriormente demonstrada, com originais e correspondências aceites.', 'confirm' => true]);
|
||||
file_put_contents($out.'/result-'.$level.'.json', json_encode($result, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['level' => $level, 'inserted' => $result['inserted'], 'reused' => $result['reused']]).PHP_EOL;
|
||||
}
|
||||
Reference in New Issue
Block a user