|string|int|float|bool|null> $findings * @param array{dedupeMode?: string, initialRejected?: list} $options * @return array{ * schemaVersion: string, * prepared: list, * rejected: list * } */ public static function prepare(array $findings, array $options = []): array { $prepared = []; $rejected = self::coerce_initial_rejected($options['initialRejected'] ?? []); $dedupe_mode = self::normalize_dedupe_mode($options['dedupeMode'] ?? self::DEDUPE_BY_FINDING_KEY); foreach ($findings as $finding) { $finding_key = self::extract_finding_key($finding); if ($finding_key === null) { $rejected[] = [ 'findingKey' => self::MALFORMED_FINDING_KEY, 'state' => 'observed', 'reason' => 'malformed_finding', ]; continue; } $resolver = TheDotDevAgenticBridge_Resolver_Registry::classify_finding($finding_key); if ($resolver === null) { $rejected[] = [ 'findingKey' => $finding_key, 'state' => 'observed', 'reason' => 'unknown_finding_key', ]; continue; } $resolver_id = isset($resolver['resolverId']) && is_string($resolver['resolverId']) ? $resolver['resolverId'] : ''; if ($resolver_id === '' || !TheDotDevAgenticBridge_Resolver_Registry::is_supported_resolver($resolver_id)) { $rejected[] = [ 'findingKey' => $finding_key, 'state' => 'observed', 'reason' => 'unknown_resolver_id', ]; continue; } $coverage = TheDotDevAgenticBridge_Resolver_Registry::coverage_for_finding($finding_key); $prepared[] = [ 'findingKey' => $finding_key, 'resolverId' => $resolver_id, 'ownership' => (string) ($resolver['ownership'] ?? ''), 'riskClass' => (string) ($resolver['riskClass'] ?? ''), 'capability' => (string) ($resolver['capability'] ?? ''), 'previewRequired' => (bool) ($resolver['previewRequired'] ?? false), 'approvalRequired' => (bool) ($resolver['explicitApprovalRequired'] ?? false), 'rollbackSupported' => (bool) ($resolver['rollbackSupported'] ?? false), 'localCheckRequired' => (bool) ($resolver['localCheckRequired'] ?? false), 'receivingSideRecheckRequired' => (($resolver['browsegradeRecheckState'] ?? 'not_started') !== 'verified'), 'outcome' => (string) ($coverage['outcome'] ?? 'guided'), 'applyAvailable' => (bool) ($coverage['applyAvailable'] ?? false), 'reason' => (string) ($coverage['reason'] ?? ''), 'state' => 'prepared', ]; } return [ 'schemaVersion' => '1.0', 'prepared' => self::dedupe_prepared($prepared, $dedupe_mode), 'rejected' => $rejected, ]; } /** * @param mixed $report * @param array{dedupeMode?: string, initialRejected?: list} $options * @return array{ * schemaVersion: string, * prepared: list, * rejected: list * } */ public static function prepare_from_browsegrade_report(mixed $report, array $options = []): array { $adapted = self::adapt_browsegrade_report($report); $initial_rejected = self::coerce_initial_rejected($options['initialRejected'] ?? []); $options['initialRejected'] = array_merge($initial_rejected, $adapted['rejected']); return self::prepare($adapted['findingKeys'], $options); } /** * @param mixed $report * @return array{ * findingKeys: list, * rejected: list * } */ public static function adapt_browsegrade_report(mixed $report): array { $findings = self::extract_report_findings_list($report); if ($findings === null) { return [ 'findingKeys' => [], 'rejected' => [[ 'findingKey' => self::MALFORMED_FINDING_KEY, 'state' => 'observed', 'reason' => 'malformed_finding', ]], ]; } $finding_keys = []; $rejected = []; foreach ($findings as $finding) { $finding_key = self::extract_finding_key($finding); if ($finding_key === null) { $rejected[] = [ 'findingKey' => self::MALFORMED_FINDING_KEY, 'state' => 'observed', 'reason' => 'malformed_finding', ]; continue; } $finding_keys[] = $finding_key; } return [ 'findingKeys' => $finding_keys, 'rejected' => $rejected, ]; } /** * @param array|string|int|float|bool|null $finding */ private static function extract_finding_key(array|string|int|float|bool|null $finding): ?string { if (is_string($finding)) { $finding_key = trim($finding); return $finding_key !== '' ? $finding_key : null; } if (!is_array($finding)) { return null; } foreach (['findingKey', 'key', 'id', 'checkId'] as $field) { if (!array_key_exists($field, $finding) || !is_string($finding[$field])) { continue; } $finding_key = trim($finding[$field]); if ($finding_key !== '') { return $finding_key; } } return null; } /** * @param mixed $report * @return list|string|int|float|bool|null>|null */ private static function extract_report_findings_list(mixed $report): ?array { if (is_array($report) && TheDotDevAgenticBridge_Compat::array_is_list($report)) { return $report; } if (!is_array($report)) { return null; } foreach (['findings', 'items', 'checks', 'issues'] as $field) { if (!array_key_exists($field, $report) || !is_array($report[$field])) { continue; } if (!TheDotDevAgenticBridge_Compat::array_is_list($report[$field])) { continue; } return $report[$field]; } return null; } /** * @param string $mode */ private static function normalize_dedupe_mode(string $mode): string { if ($mode === self::DEDUPE_NONE || $mode === self::DEDUPE_BY_RESOLVER_ID) { return $mode; } return self::DEDUPE_BY_FINDING_KEY; } /** * @param list $prepared * @return list */ private static function dedupe_prepared(array $prepared, string $mode): array { if ($mode === self::DEDUPE_NONE) { return $prepared; } $seen = []; $output = []; foreach ($prepared as $intent) { $key = $mode === self::DEDUPE_BY_RESOLVER_ID ? $intent['resolverId'] : $intent['findingKey']; if (isset($seen[$key])) { continue; } $seen[$key] = true; $output[] = $intent; } return $output; } /** * @param mixed $initial_rejected * @return list */ private static function coerce_initial_rejected(mixed $initial_rejected): array { if (!is_array($initial_rejected)) { return []; } $output = []; foreach ($initial_rejected as $item) { if (!is_array($item)) { continue; } $finding_key = isset($item['findingKey']) && is_string($item['findingKey']) ? $item['findingKey'] : self::MALFORMED_FINDING_KEY; $state = isset($item['state']) && is_string($item['state']) ? $item['state'] : 'observed'; $reason = isset($item['reason']) && is_string($item['reason']) ? $item['reason'] : 'malformed_finding'; $output[] = [ 'findingKey' => $finding_key, 'state' => $state, 'reason' => $reason, ]; } return $output; } }