108 lines
7.9 KiB
PHP
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;
|