make(Kernel::class)->bootstrap(); $identity = portalQaAssertTarget($target); config(['portal.managers' => ['opportunity-manager@example.invalid', 'geo-manager-qa@example.invalid']]); $directory = storage_path('app/private/investmoz-v2/portal-audit'); $stamp = gmdate('Ymd_His'); set_exception_handler(function (Throwable $error) use ($directory, $stamp): void { file_put_contents($directory.'/legacy-sector-import-error-private-'.$stamp.'.json', json_encode(['type' => get_class($error), 'message' => $error->getMessage()], JSON_THROW_ON_ERROR)); fwrite(STDERR, "QA sector operation failed; inspect private diagnostic.\n"); exit(1); }); $mode = $argv[1] ?? 'preview'; throw_unless(in_array($mode, ['preview', 'apply', 'verify', 'replay'], true), RuntimeException::class, 'Unknown operation'); $manifestName = 'legacy-setores-20261010/manifest.json'; $staging = storage_path('app/private/document-import-staging'); $manifest = json_decode(file_get_contents($staging.'/'.$manifestName), true, 512, JSON_THROW_ON_ERROR); throw_unless(count($manifest['documents']) === 22, RuntimeException::class, 'Expected original 22-item sector lot'); $actorFile = json_decode(file_get_contents($directory.'/resource-http-actors-private.json'), true, 512, JSON_THROW_ON_ERROR); $actor = User::findOrFail($actorFile['manager']['id']); throw_unless($actor->can('manage-portal'), RuntimeException::class, 'Authorized existing library manager required'); function sectorCounts(): array { $result = []; foreach (['documents', 'portal_resources', 'audit_events', 'document_malware_scans', 'opportunities', 'desk_cases', 'users', 'jobs'] as $table) { if (Schema::hasTable($table)) { $result[$table] = DB::table($table)->count(); } } return $result; } $before = sectorCounts(); $exit = null; if (in_array($mode, ['preview', 'apply', 'replay'], true)) { $args = ['manifest' => $manifestName]; if ($mode !== 'preview') { $health = app(MalwareScanner::class)->health(); throw_unless($health['available'] ?? false, RuntimeException::class, 'Real ClamD not healthy; import refused'); // A fresh read-only preview is mandatory before each application, including repetition. $previewExit = Artisan::call('documents:import-batch', $args); $preview = Artisan::output(); file_put_contents($directory.'/legacy-sectors-pre-apply-'.$stamp.'.txt', $preview); throw_unless($previewExit === 0 && ! str_contains($preview, 'review required') && ! str_contains($preview, 'not imported'), RuntimeException::class, 'Preview has unresolved conflicts'); $args += ['--apply' => true, '--actor' => (string) $actor->id]; config(['queue.default' => 'sync']); // Actual configured ClamD scanner; no fake scan result. } $exit = Artisan::call('documents:import-batch', $args); $output = Artisan::output(); file_put_contents($directory.'/legacy-sectors-import-'.$mode.'-'.$stamp.'.txt', $output); throw_unless($exit === 0 && ! str_contains($output, 'review required') && ! str_contains($output, 'not imported'), RuntimeException::class, 'Import/preview failed'); } $rows = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id') ->whereIn('d.external_id', array_column($manifest['documents'], 'external_id')) ->orderBy('r.id')->get(['r.id', 'r.document_id', 'r.title', 'r.source_url', 'r.metadata_version', 'r.published', 'd.external_id', 'd.sha256', 'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.status', 'd.file_name', 'd.storage_key', 'd.storage_encryption', 'd.data_classification']); $checks = []; foreach ($rows as $row) { $record = collect($manifest['documents'])->firstWhere('external_id', $row->external_id); $path = $staging.'/'.$record['relative_path']; $bytes = Storage::disk('local')->get($row->storage_key); if ($row->storage_encryption === 'laravel') { $bytes = Crypt::decryptString($bytes); } $checks[] = ['resource_id' => $row->id, 'source_hash_equal' => hash_equals(trim($row->sha256), hash_file('sha256', $path)), 'stored_original_equal' => hash_equals(trim($row->sha256), hash('sha256', $bytes)), 'clean_scan_for_hash' => $row->malware_scan_status === 'clean' && hash_equals(trim($row->sha256), (string) $row->malware_scanned_sha256)]; unset($bytes); } $after = sectorCounts(); if ($mode === 'preview') { throw_unless($before === $after, RuntimeException::class, 'Preview unexpectedly changed database'); } if ($mode !== 'preview') { throw_unless($rows->count() === 22 && collect($checks)->every(fn ($c) => $c['source_hash_equal'] && $c['stored_original_equal'] && $c['clean_scan_for_hash']), RuntimeException::class, 'Original/hash/scan checks incomplete'); if ($mode === 'apply') { throw_unless($rows->every(fn ($r) => ! $r->published && $r->data_classification === 'internal'), RuntimeException::class, 'Importer must not publish'); } if ($mode === 'replay') { throw_unless($before === $after, RuntimeException::class, 'Repeat import is not idempotent'); } } $proof = ['mode' => $mode, 'captured_at' => gmdate('c'), 'target' => $identity, 'manifest_sha256' => hash_file('sha256', $staging.'/'.$manifestName), 'expected_items' => 22, 'resources' => $rows, 'checks' => $checks, 'before' => $before, 'after' => $after, 'command_exit' => $exit, 'scanner_class' => get_class(app(MalwareScanner::class)), 'active_writes' => 0, 'publication_is_separate' => true]; $path = $directory.'/legacy-sectors-'.$mode.'-'.$stamp.'.json'; file_put_contents($path, json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); file_put_contents($directory.'/legacy-sectors-'.$mode.'-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); echo json_encode(['mode' => $mode, 'expected_items' => 22, 'resources_found' => $rows->count(), 'before' => $before, 'after' => $after, 'command_exit' => $exit, 'target' => $identity, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;