Files
Investidor-V2/tools/portal-segmentation-import-qa.php
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

108 lines
7.9 KiB
PHP

<?php
// Preserve the exact available exemplar in the existing library, QA only.
use App\Models\User;
use App\Services\Malware\MalwareScanner;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
$workspace = dirname(__DIR__);
$private = $workspace.'/storage/app/private/investmoz-v2/portal-audit';
$directory = $private.'/segmentation-reconciliation-20261011';
$plan = json_decode(file_get_contents($private.'/portal-stability-latest.json'), true, 512, JSON_THROW_ON_ERROR);
$source = json_decode(file_get_contents($directory.'/source-provenance.json'), true, 512, JSON_THROW_ON_ERROR);
putenv('STABILITY_QA_MODE=consumers');
$app = (static fn (string $file) => require $file)($plan['source'].'/tools/portal-stability-runtime.php');
$mode = $argv[1] ?? 'preview';
throw_unless(in_array($mode, ['preview', 'apply', 'replay'], true), RuntimeException::class, 'Unsupported QA action.');
$target = DB::selectOne('select current_database() as database,inet_server_port() as port');
throw_unless($target->database === 'stability_consumers_20261011_084534_test' && (int) $target->port === 55441 && app()->environment('testing'), RuntimeException::class, 'Exact isolated QA required.');
$actor = User::findOrFail(17);
throw_unless($actor->can('manage-portal'), RuntimeException::class, 'Existing authorized QA library manager required.');
throw_unless(hash_file('sha256', $source['retained']) === $source['sha256'], RuntimeException::class, 'Original source bytes changed.');
$staging = storage_path('app/private/document-import-staging/segmentation-20261011');
if (! is_dir($staging)) {
mkdir($staging, 0700, true);
}
$original = $staging.'/'.$source['sha256'].'.docx';
if (! is_file($original)) {
copy($source['retained'], $original);
}
throw_unless(hash_file('sha256', $original) === $source['sha256'], RuntimeException::class, 'Staged copy differs.');
$externalId = 'investmoz-segmentation-source-'.$source['sha256'];
$identityStatement = $source['exact_requested_exemplar_confirmed']
? 'Identidade binária com segmentar processos v1(3).docx confirmada pelo responsável em 11/10/2026; v1.docx é nome local do mesmo exemplar.'
: 'O nome pedido v1(3) ainda não foi confirmado; não declarar identidade entre exemplares.';
$manifest = ['documents' => [[
'external_id' => $externalId, 'relative_path' => 'segmentation-20261011/'.$source['sha256'].'.docx',
'file_name' => $source['observed_filename'], 'title' => 'Portal do Investidor — InvesteMoz: segmentar processos v1',
'issuer' => (string) $source['version_properties']['creator'], 'category' => 'portal_specifications',
'topic' => 'segmentation_reconciliation', 'legislation' => false, 'document_type' => 'guide', 'language' => 'pt',
'version_label' => 'v1; propriedade OOXML revision '.$source['version_properties']['revision'].'; SHA256 '.substr($source['sha256'], 0, 12),
'source_reference' => 'Exemplar observado: '.$source['source'].'; SHA256 '.$source['sha256'].'; propriedades OOXML: criação '.$source['version_properties']['created'].', alteração '.$source['version_properties']['modified'].'. Autoria/datas são propriedades do original; data de emissão não identificada. '.$identityStatement,
'description' => 'Especificação de programação incorporada privadamente em QA. Referências por secção/OOXML na auditoria existente. Não é diploma oficial, não ativa regra ou cálculo e não publica conteúdo. Preservar versões anteriores, cinco fases/19 etapas e decisões pendentes.',
]]];
file_put_contents($staging.'/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
$counts = static function (): array {
$result = [];
foreach (['documents', 'portal_resources', 'portal_resource_revisions', 'document_malware_scans', 'audit_events', 'opportunities', 'organizations', 'memberships', 'users'] as $table) {
$result[$table] = DB::table($table)->count();
}
return $result;
};
$before = $counts();
$args = ['manifest' => 'segmentation-20261011/manifest.json'];
$previewExit = Artisan::call('documents:import-batch', $args);
$preview = Artisan::output();
file_put_contents($directory.'/library-preview-'.$mode.'.txt', $preview);
throw_unless($previewExit === 0 && ! str_contains($preview, 'review required') && ! str_contains($preview, 'not imported'), RuntimeException::class, 'Preview failed or identity/version conflict.');
throw_unless($counts() === $before, RuntimeException::class, 'Preview mutated database.');
$health = app(MalwareScanner::class)->health();
if ($mode !== 'preview') {
throw_unless($health['available'] ?? false, RuntimeException::class, 'Real ClamD not healthy.');
config(['queue.default' => 'sync']);
$exit = Artisan::call('documents:import-batch', $args + ['--apply' => true, '--actor' => '17']);
$output = Artisan::output();
file_put_contents($directory.'/library-apply-'.$mode.'.txt', $output);
throw_unless($exit === 0 && ! str_contains($output, 'review required') && ! str_contains($output, 'not imported'), RuntimeException::class, 'QA import failed.');
}
$documents = DB::table('documents')->where('sha256', $source['sha256'])->whereNull('deleted_at')->get();
$retrieval = [];
foreach ($documents as $document) {
$resource = DB::table('portal_resources')->where('document_id', $document->id)->first();
$bytes = Storage::disk('local')->get($document->storage_key);
if ($document->storage_encryption === 'laravel') {
$bytes = Crypt::decryptString($bytes);
}
$retrieval[] = ['document_id' => $document->id, 'resource_id' => $resource?->id,
'sha256' => trim($document->sha256), 'original_bytes_recovered' => hash_equals($source['sha256'], hash('sha256', $bytes)),
'clean_scan_for_same_hash' => $document->malware_scan_status === 'clean' && hash_equals($source['sha256'], (string) $document->malware_scanned_sha256),
'private' => $document->data_classification === 'internal' && ! $resource?->published,
'resource_metadata_version' => $resource?->metadata_version, 'file_name' => $document->file_name,
'audit_count' => DB::table('audit_events')->where('entity_type', 'document')->where('entity_id', $document->id)->where('action', 'private_document_batch_imported')->count(),
'historical_revisions' => DB::table('portal_resource_revisions')->where('resource_id', $resource?->id)->count(),
];
unset($bytes);
}
$after = $counts();
if ($mode !== 'preview') {
throw_unless(count($retrieval) === 1 && $retrieval[0]['original_bytes_recovered'] && $retrieval[0]['clean_scan_for_same_hash'] && $retrieval[0]['private'] && $retrieval[0]['audit_count'] === 1, RuntimeException::class, 'Retrieval/scan/private/audit incomplete.');
}
if ($mode === 'replay') {
throw_unless($before === $after, RuntimeException::class, 'Repeat created duplicates.');
}
foreach (['opportunities', 'organizations', 'memberships', 'users'] as $table) {
throw_unless($before[$table] === $after[$table], RuntimeException::class, 'Unrelated identities or opportunity data changed.');
}
$proof = ['result' => 'passed', 'mode' => $mode, 'at' => gmdate('c'), 'target' => $target,
'requested_exemplar_identity_pending' => ! $source['exact_requested_exemplar_confirmed'],
'source_sha256' => $source['sha256'], 'manifest_sha256' => hash_file('sha256', $staging.'/manifest.json'),
'preview_read_only' => true, 'real_scanner' => get_class(app(MalwareScanner::class)), 'health' => $health,
'before' => $before, 'after' => $after, 'retrieval' => $retrieval,
'publication_separate' => true, 'rule_activation' => false, 'active_writes' => 0,
];
file_put_contents($directory.'/library-'.$mode.'-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
echo json_encode(['result' => 'passed', 'mode' => $mode, 'target' => $target, 'source_sha256' => $source['sha256'], 'retrieval' => $retrieval, 'active_writes' => 0], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR).PHP_EOL;