*/ private const SUPPORTED = [ 'artifact-publication' => ['priority' => 10, 'action' => 'preview_only'], 'llms-route' => ['priority' => 20, 'action' => 'open_manual_check'], 'agent-map-route' => ['priority' => 21, 'action' => 'open_manual_check'], 'llms-validity' => ['priority' => 30, 'action' => 'review'], 'agent-map-format' => ['priority' => 31, 'action' => 'review'], 'physical-llms-conflict' => ['priority' => 40, 'action' => 'review'], 'artifact-freshness' => ['priority' => 50, 'action' => 'preview_only'], 'schema-evidence' => ['priority' => 60, 'action' => 'review'], 'manifest-alignment' => ['priority' => 70, 'action' => 'review'], 'seo-owner' => ['priority' => 80, 'action' => 'defer_to_owner'], ]; /** @var array */ private const STATUS_ORDER = [ 'confirmed_issue' => 0, 'review_needed' => 1, 'unknown' => 2, 'not_applicable' => 3, 'ok' => 4, ]; /** @var list */ private const TRUSTED_SOURCES = [ 'local_contract', 'local_fingerprint', 'local_manifest', 'local_plugin_inventory', 'local_schema', 'local_storage', 'same_origin_scan', ]; /** @var array */ private const READINESS_MAP = [ 'disc-llms-route' => ['id' => 'llms-route', 'source' => 'same_origin_scan'], 'disc-agent-map-route' => ['id' => 'agent-map-route', 'source' => 'same_origin_scan'], 'disc-llms-valid' => ['id' => 'llms-validity', 'source' => 'same_origin_scan'], 'und-agent-map-format' => ['id' => 'agent-map-format', 'source' => 'same_origin_scan'], 'saf-physical-llms-conflict' => ['id' => 'physical-llms-conflict', 'source' => 'local_storage'], 'saf-artifact-stale' => ['id' => 'artifact-freshness', 'source' => 'local_fingerprint'], 'und-home-schema' => ['id' => 'schema-evidence', 'source' => 'local_schema'], 'und-artifact-manifest' => ['id' => 'manifest-alignment', 'source' => 'local_manifest'], 'saf-content-policy' => ['id' => 'manifest-alignment', 'source' => 'local_manifest'], ]; /** * Adapts an existing readiness result; it never starts or saves a scan. * * @param array $result * @param array{artifactsPublished?: bool, seoOwner?: string} $context * @return array */ public static function assemble_from_readiness_result(array $result, array $context = []): array { $findings = $result['findings'] ?? null; if (!is_array($findings) || !TheDotDevAgenticBridge_Compat::array_is_list($findings)) { return self::assemble([null]); } $scan_time = isset($result['scannedAt']) && is_string($result['scannedAt']) ? $result['scannedAt'] : ''; $observations = []; foreach ($findings as $finding) { if (!is_array($finding) || !isset($finding['id'], $finding['status']) || !is_string($finding['id']) || !is_string($finding['status'])) { $observations[] = null; continue; } $mapping = self::READINESS_MAP[$finding['id']] ?? null; if ($mapping === null) { continue; } $checked_at = isset($finding['lastChecked']) && is_string($finding['lastChecked']) ? $finding['lastChecked'] : $scan_time; $observations[] = [ 'id' => $mapping['id'], 'status' => $finding['status'], 'source' => $mapping['source'], 'checkedAt' => $checked_at, 'scope' => 'readiness_result', 'evidenceCode' => sanitize_key($finding['id'] . '_' . $finding['status']), ]; } if (array_key_exists('artifactsPublished', $context)) { if (!is_bool($context['artifactsPublished'])) { $observations[] = null; } else { $observations[] = [ 'id' => 'artifact-publication', 'status' => $context['artifactsPublished'] ? 'published' : 'unpublished', 'source' => 'local_storage', 'checkedAt' => $scan_time, 'scope' => 'plugin_artifacts', 'evidenceCode' => $context['artifactsPublished'] ? 'published_snapshot_present' : 'published_snapshot_absent', ]; } } if (array_key_exists('seoOwner', $context)) { if (!is_string($context['seoOwner'])) { $observations[] = null; } else { $observations[] = [ 'id' => 'seo-owner', 'status' => 'review_needed', 'source' => 'local_plugin_inventory', 'checkedAt' => $scan_time, 'scope' => 'seo_ownership', 'evidenceCode' => sanitize_key($context['seoOwner']), ]; } } return self::assemble($observations); } /** * @param list|string|int|float|bool|null> $observations * @return array */ public static function assemble(array $observations): array { $items_by_id = []; $rejected = 0; $bounded = array_slice($observations, 0, self::MAX_OBSERVATIONS); foreach ($bounded as $observation) { if (!is_array($observation)) { $rejected++; continue; } if ( !isset($observation['id'], $observation['status']) || !is_string($observation['id']) || !is_string($observation['status']) || (isset($observation['source']) && !is_string($observation['source'])) || (isset($observation['checkedAt']) && !is_string($observation['checkedAt'])) || (isset($observation['scope']) && !is_string($observation['scope'])) || (isset($observation['evidenceCode']) && !is_string($observation['evidenceCode'])) ) { $rejected++; continue; } $id = sanitize_key($observation['id']); if (!isset(self::SUPPORTED[$id])) { $rejected++; continue; } $status = self::normalize_status($observation['status']); $evidence_code = sanitize_key($observation['evidenceCode'] ?? ''); if ($id === 'seo-owner') { $status = self::seo_owner_status($evidence_code); } $item = self::item($id, $status, $observation, $evidence_code); if (!isset($items_by_id[$id]) || self::is_more_important($item, $items_by_id[$id])) { $items_by_id[$id] = $item; } } if ($rejected > 0) { $items_by_id['artifact-input-validation'] = [ 'id' => 'artifact-input-validation', 'status' => 'unknown', 'priority' => 5, 'source' => 'local_contract', 'checkedAt' => '', 'scope' => 'artifact_command', 'evidenceCode' => 'malformed_or_unsupported_observation', 'limitation' => 'One or more observations could not be classified safely.', 'nextSafeAction' => 'review', 'previewRequired' => true, 'approvalRequired' => true, 'automaticChangeAllowed' => false, ]; } if (count($observations) > self::MAX_OBSERVATIONS) { $items_by_id['artifact-input-truncation'] = [ 'id' => 'artifact-input-truncation', 'status' => 'unknown', 'priority' => 6, 'source' => 'local_contract', 'checkedAt' => '', 'scope' => 'artifact_command', 'evidenceCode' => 'observation_limit_reached', 'limitation' => 'Additional observations were not processed because the local safety bound was reached.', 'nextSafeAction' => 'review', 'previewRequired' => true, 'approvalRequired' => true, 'automaticChangeAllowed' => false, ]; } $items = array_values($items_by_id); usort($items, static function (array $left, array $right): int { $priority = ((int) $left['priority']) <=> ((int) $right['priority']); return $priority !== 0 ? $priority : strcmp((string) $left['id'], (string) $right['id']); }); $items = array_slice($items, 0, self::MAX_ITEMS); $counts = array_fill_keys(array_keys(self::STATUS_ORDER), 0); foreach ($items as $item) { $counts[(string) $item['status']]++; } return [ 'version' => self::VERSION, 'mode' => 'read_only_proposals', 'networkRequests' => 0, 'writesPerformed' => 0, 'observationsReceived' => count($observations), 'observationsProcessed' => count($bounded), 'rejectedObservations' => $rejected, 'truncated' => count($observations) > self::MAX_OBSERVATIONS, 'counts' => $counts, 'items' => $items, 'limitation' => 'Proposal metadata only; no route, artifact, schema, or SEO change was made.', ]; } /** @param array $observation @return array */ private static function item(string $id, string $status, array $observation, string $evidence_code): array { $source = self::bound_key($observation['source'] ?? '', 64); if (!in_array($source, self::TRUSTED_SOURCES, true)) { $source = 'unknown'; $status = $status === 'not_applicable' ? $status : 'unknown'; } return [ 'id' => $id, 'status' => $status, 'priority' => self::SUPPORTED[$id]['priority'], 'source' => $source, 'checkedAt' => self::normalize_checked_at((string) ($observation['checkedAt'] ?? '')), 'scope' => self::bound_key($observation['scope'] ?? 'local_site', 64), 'evidenceCode' => $evidence_code !== '' ? self::bound($evidence_code, 80) : 'unspecified', 'limitation' => self::limitation_for_status($status), 'nextSafeAction' => self::SUPPORTED[$id]['action'], 'previewRequired' => true, 'approvalRequired' => true, 'automaticChangeAllowed' => false, ]; } private static function normalize_status(string $status): string { return match (sanitize_key($status)) { 'pass', 'ok', 'present', 'valid', 'published', 'aligned', 'fresh', 'no_conflict' => 'ok', 'fail', 'missing', 'malformed', 'conflict', 'stale', 'divergent', 'ineligible', 'unpublished' => 'confirmed_issue', 'warning', 'mixed', 'unverified', 'review_needed' => 'review_needed', 'not_applicable' => 'not_applicable', default => 'unknown', }; } private static function seo_owner_status(string $evidence_code): string { return match ($evidence_code) { 'none' => 'not_applicable', 'yoast', 'rankmath', 'rank_math', 'aioseo', 'multiple' => 'review_needed', default => 'unknown', }; } /** @param array $candidate @param array $current */ private static function is_more_important(array $candidate, array $current): bool { $candidate_order = self::STATUS_ORDER[(string) $candidate['status']]; $current_order = self::STATUS_ORDER[(string) $current['status']]; if ($candidate_order !== $current_order) { return $candidate_order < $current_order; } $candidate_key = (string) $candidate['source'] . '|' . (string) $candidate['evidenceCode']; $current_key = (string) $current['source'] . '|' . (string) $current['evidenceCode']; return strcmp($candidate_key, $current_key) < 0; } private static function normalize_checked_at(string $value): string { $value = trim($value); return preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/', $value) === 1 ? self::bound($value, 40) : ''; } private static function limitation_for_status(string $status): string { return match ($status) { 'confirmed_issue' => 'The supplied local evidence reports an issue; it has not been changed automatically.', 'review_needed' => 'The supplied local evidence requires named human review.', 'not_applicable' => 'The supplied local evidence marks this item not applicable.', 'ok' => 'Observed local evidence only; no outcome, indexing, or citation claim is made.', default => 'Evidence is incomplete or could not be classified safely.', }; } private static function bound(string $value, int $limit): string { $clean = sanitize_text_field($value); return function_exists('mb_substr') ? mb_substr($clean, 0, $limit) : substr($clean, 0, $limit); } private static function bound_key(string $value, int $limit): string { $clean = sanitize_key($value); return function_exists('mb_substr') ? mb_substr($clean, 0, $limit) : substr($clean, 0, $limit); } }