$subjects * @param array{max_items_per_run: int, max_concurrency: int, max_retries: int} $budgets * @return array{ * stats: array{requested: int, succeeded: int, skipped: int, failed: int, throttled: int}, * gaps: array, * resume_token: ?string * } */ public function capture( Tenant $tenant, array $subjects, PolicyVersionCapturePurpose $purpose, array $budgets, ?string $resumeToken = null, ?int $operationRunId = null, ?int $baselineProfileId = null, ?string $createdBy = null, ): array { $maxItemsPerRun = max(0, (int) ($budgets['max_items_per_run'] ?? 0)); $offset = 0; if (is_string($resumeToken) && $resumeToken !== '') { $state = BaselineEvidenceResumeToken::decode($resumeToken) ?? []; $offset = is_numeric($state['offset'] ?? null) ? max(0, (int) $state['offset']) : 0; } $remaining = array_slice($subjects, $offset); $batch = $maxItemsPerRun > 0 ? array_slice($remaining, 0, $maxItemsPerRun) : []; $stats = [ 'requested' => count($batch), 'succeeded' => 0, 'skipped' => 0, 'failed' => 0, 'throttled' => 0, ]; /** @var array $gaps */ $gaps = []; foreach ($batch as $subject) { $policyType = trim((string) ($subject['policy_type'] ?? '')); $externalId = trim((string) ($subject['subject_external_id'] ?? '')); if ($policyType === '' || $externalId === '') { $gaps['invalid_subject'] = ($gaps['invalid_subject'] ?? 0) + 1; $stats['failed']++; continue; } $policy = Policy::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('policy_type', $policyType) ->where('external_id', $externalId) ->first(); if (! $policy instanceof Policy) { $gaps['policy_not_found'] = ($gaps['policy_not_found'] ?? 0) + 1; $stats['failed']++; continue; } $result = $this->captureOrchestrator->capture( policy: $policy, tenant: $tenant, includeAssignments: true, includeScopeTags: true, createdBy: $createdBy, metadata: [ 'capture_source' => 'baseline_evidence', ], capturePurpose: $purpose, operationRunId: $operationRunId, baselineProfileId: $baselineProfileId, ); if (is_array($result) && array_key_exists('failure', $result)) { $gaps['capture_failed'] = ($gaps['capture_failed'] ?? 0) + 1; $stats['failed']++; continue; } $stats['succeeded']++; } $processed = $offset + count($batch); $resumeTokenOut = null; if ($processed < count($subjects)) { $resumeTokenOut = BaselineEvidenceResumeToken::encode([ 'offset' => $processed, ]); } ksort($gaps); return [ 'stats' => $stats, 'gaps' => $gaps, 'resume_token' => $resumeTokenOut, ]; } }