option('write'); if ($write && ! (bool) $this->option('confirm-write')) { $this->error('Explicit write confirmation required. Re-run with --write --confirm-write.'); return self::FAILURE; } $workspaceOption = $this->option('workspace'); if ($workspaceOption !== null && ! is_numeric($workspaceOption)) { $this->error('The --workspace option must be a numeric workspace id.'); return self::FAILURE; } $workspaceId = is_numeric($workspaceOption) ? (int) $workspaceOption : null; $scan = $this->scanCandidates( chunkSize: max(1, (int) $this->option('chunk')), workspaceId: $workspaceId, ); $mode = $write ? 'commit' : 'preview'; $candidateCount = count($scan['candidates']); $invalidCount = count($scan['invalid']); $this->info(sprintf('Mode: %s', $mode)); $this->info('Scope surface: baseline_profiles_only'); $this->info(sprintf('Candidate count: %d', $candidateCount)); foreach ($scan['candidates'] as $candidate) { $this->line(sprintf(' - %s', $candidate['summary'])); } if ($invalidCount > 0) { $this->warn(sprintf('Invalid rows: %d', $invalidCount)); foreach ($scan['invalid'] as $invalidRow) { $this->warn(sprintf(' - %s', $invalidRow)); } } if ($candidateCount === 0) { $this->info('No baseline profile scope rows require backfill.'); $this->info('Rewritten count: 0'); $this->info('Audit logged: no'); return self::SUCCESS; } if ($write && $invalidCount > 0) { $this->error('Backfill aborted because invalid scope rows were detected. Resolve them before committing.'); return self::FAILURE; } if (! $write) { $this->info('Rewritten count: 0'); $this->info('Audit logged: no'); return self::SUCCESS; } $rewrittenCount = 0; $auditLogged = false; foreach ($scan['candidates'] as $candidate) { $profile = BaselineProfile::query() ->with('workspace') ->find($candidate['id']); if (! $profile instanceof BaselineProfile) { continue; } $before = $profile->rawScopeJsonb(); $after = $profile->canonicalScopeJsonb(); if (! $profile->rewriteScopeToCanonicalV2()) { continue; } $workspace = $profile->workspace; if ($workspace instanceof Workspace) { $auditLogger->log( workspace: $workspace, action: AuditActionId::BaselineProfileScopeBackfilled, context: [ 'metadata' => [ 'source' => 'tenantpilot:baseline-scope-v2:backfill', 'workspace_id' => (int) $profile->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'mode' => 'commit', 'before_scope' => $before, 'after_scope' => $after, ], ], resourceType: 'baseline_profile', resourceId: (string) $profile->getKey(), targetLabel: (string) $profile->name, summary: sprintf('Baseline profile "%s" scope backfilled to canonical V2.', (string) $profile->name), ); $auditLogged = true; } $rewrittenCount++; } $this->info(sprintf('Rewritten count: %d', $rewrittenCount)); $this->info(sprintf('Audit logged: %s', $auditLogged ? 'yes' : 'no')); return self::SUCCESS; } /** * @return array{candidates: list, invalid: list} */ private function scanCandidates(int $chunkSize, ?int $workspaceId = null): array { $candidates = []; $invalid = []; BaselineProfile::query() ->when( $workspaceId !== null, fn ($query) => $query->where('workspace_id', $workspaceId), ) ->orderBy('id') ->chunkById($chunkSize, function ($profiles) use (&$candidates, &$invalid): void { foreach ($profiles as $profile) { if (! $profile instanceof BaselineProfile) { continue; } try { if (! $profile->requiresScopeSaveForward()) { continue; } $candidates[] = [ 'id' => (int) $profile->getKey(), 'summary' => $this->candidateSummary($profile), ]; } catch (InvalidArgumentException $exception) { $invalid[] = sprintf( '#%d "%s": %s', (int) $profile->getKey(), (string) $profile->name, $exception->getMessage(), ); } } }); return [ 'candidates' => $candidates, 'invalid' => $invalid, ]; } private function candidateSummary(BaselineProfile $profile): string { $groupSummary = collect($profile->normalizedScope()->summaryGroups()) ->map(function (array $group): string { return $group['group_label'].': '.implode(', ', $group['selected_subject_types']); }) ->implode('; '); return sprintf( '#%d workspace=%d "%s" => %s', (int) $profile->getKey(), (int) $profile->workspace_id, (string) $profile->name, $groupSummary, ); } }