*/ private const ISSUE_STATUSES = ['fail', 'warning']; /** @var list */ private const VALID_RECHECK_SOURCES = ['manual_full', 'manual_targeted', 'hosted_full']; public static function local_blog_id(): int { return (int) get_current_blog_id(); } public static function local_site_id(): string { return 'local-' . self::local_blog_id(); } /** @return array */ public static function local_scope(): array { return [ 'namespace' => 'readiness', 'blogId' => self::local_blog_id(), 'organizationId' => '', 'siteId' => self::local_site_id(), 'subjectType' => 'readiness_check', 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'checkVersion' => self::DEFAULT_CHECK_VERSION, ]; } /** @param array $record */ public static function identity_key(array $record): string { return implode('|', [ (string) ($record['namespace'] ?? ''), (string) ($record['blogId'] ?? ''), (string) ($record['organizationId'] ?? ''), (string) ($record['siteId'] ?? ''), (string) ($record['subjectType'] ?? ''), (string) ($record['subjectId'] ?? ''), (string) ($record['checkId'] ?? ''), (string) ($record['methodologyVersion'] ?? ''), (string) ($record['checkVersion'] ?? ''), ]); } /** @param array $record */ public static function logical_identity_key(array $record): string { return implode('|', [ (string) ($record['namespace'] ?? ''), (string) ($record['blogId'] ?? ''), (string) ($record['organizationId'] ?? ''), (string) ($record['siteId'] ?? ''), (string) ($record['subjectType'] ?? ''), (string) ($record['subjectId'] ?? ''), (string) ($record['checkId'] ?? ''), ]); } /** @param array $scope @param array $record */ public static function matches_logical_scope(array $scope, array $record): bool { $expected = array_merge($scope, [ 'subjectId' => (string) ($record['subjectId'] ?? $record['checkId'] ?? ''), 'checkId' => (string) ($record['checkId'] ?? ''), ]); return self::logical_identity_key($expected) === self::logical_identity_key($record); } /** @param array $scope @param array $record */ public static function matches_scope(array $scope, array $record): bool { $expected = array_merge($scope, [ 'subjectId' => (string) ($record['subjectId'] ?? $record['checkId'] ?? ''), 'checkId' => (string) ($record['checkId'] ?? ''), ]); return self::identity_key($expected) === self::identity_key($record); } /** @return array */ public static function build_subject_deps(): array { $settings = TheDotDevAgenticBridge_Storage::get(); $content = TheDotDevAgenticBridge_Content_Metadata::artifact_eligible_content_items(); $content_projection = []; foreach ($content as $item) { if (!is_array($item)) { continue; } $content_projection[] = [ 'id' => (int) ($item['id'] ?? 0), 'modifiedAt' => (string) ($item['modifiedAt'] ?? ''), 'type' => (string) ($item['type'] ?? ''), ]; } usort( $content_projection, static fn(array $a, array $b): int => ($a['id'] ?? 0) <=> ($b['id'] ?? 0), ); $excluded_ids = TheDotDevAgenticBridge_Content_Policy::excluded_content_ids(); sort($excluded_ids); $metadata_store = TheDotDevAgenticBridge_Content_Metadata::normalized_store(); $metadata_exclusions = []; foreach ($metadata_store['by_content_id'] ?? [] as $content_id => $config) { if (!is_array($config)) { continue; } if (($config['inclusionMode'] ?? '') === TheDotDevAgenticBridge_Content_Metadata::INCLUSION_EXCLUDE) { $metadata_exclusions[] = (string) $content_id; } } sort($metadata_exclusions); $destination = ''; $api_base_url = (string) ($settings['api_base_url'] ?? ''); if ($api_base_url !== '') { $normalized = TheDotDevAgenticBridge_Connector::normalize_destination($api_base_url); if (is_array($normalized)) { $destination = (string) ($normalized['origin'] ?? '') . '|' . (string) ($normalized['base_url'] ?? ''); } } return [ 'blogId' => self::local_blog_id(), 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'checkVersion' => self::DEFAULT_CHECK_VERSION, 'blogPublic' => (string) get_option('blog_public', '1'), 'searchVisibility' => (string) ($settings['search_visibility'] ?? ''), 'artifactsPublished' => TheDotDevAgenticBridge_Storage::artifacts_published(), 'killSwitch' => TheDotDevAgenticBridge_Storage::kill_switch_enabled(), 'connectionStatus' => (string) ($settings['connection_status'] ?? 'disconnected'), 'destination' => $destination, 'siteId' => self::local_site_id(), 'contentProjection' => $content_projection, 'excludedContentIds' => $excluded_ids, 'metadataExclusions' => $metadata_exclusions, ]; } /** @param array $evidence */ public static function fingerprint_evidence(array $evidence): string { return self::sha256_canonical(TheDotDevAgenticBridge_Readiness_Methodology::bound_evidence($evidence)); } /** @param array $subject_deps */ public static function fingerprint_subject(array $subject_deps): string { return self::sha256_canonical($subject_deps); } /** * @param array $scan * @return array */ public static function apply_scan_staleness(array $scan): array { $subject_deps = self::build_subject_deps(); $current_fingerprint = self::fingerprint_subject($subject_deps); $scope = self::local_scope(); $findings = is_array($scan['findings'] ?? null) ? $scan['findings'] : []; $verifications = is_array($scan['verifications'] ?? null) ? $scan['verifications'] : []; $history = is_array($scan['verificationHistory'] ?? null) ? $scan['verificationHistory'] : []; $observations = is_array($scan['observations'] ?? null) ? $scan['observations'] : []; $presentation_by_check = []; $changed = false; [$verifications, $history, $version_drift_checks] = self::demote_version_drifted_current_verifications( $verifications, $history, $scope, ); foreach ($version_drift_checks as $check_id) { $presentation_by_check[$check_id] = 'stale'; $changed = true; } foreach ($verifications as $record) { if (!is_array($record) || empty($record['isCurrent'])) { continue; } if (!self::matches_logical_scope($scope, $record)) { continue; } $check_id = (string) ($record['checkId'] ?? ''); $check_scope = array_merge($scope, [ 'checkId' => $check_id, 'subjectId' => (string) ($record['subjectId'] ?? $check_id), ]); if ((string) ($record['subjectFingerprint'] ?? '') === $current_fingerprint && (string) ($record['methodologyVersion'] ?? '') === TheDotDevAgenticBridge_Readiness_Methodology::VERSION && (string) ($record['checkVersion'] ?? '') === self::DEFAULT_CHECK_VERSION && (int) ($record['blogId'] ?? -1) === self::local_blog_id()) { $presentation_by_check[$check_id] = 'verified'; continue; } $verifications = self::mark_verification_non_current_by_logical_key($verifications, $check_scope); $demoted = $record; $demoted['isCurrent'] = false; $history = self::append_history($history, [$demoted]); $presentation_by_check[$check_id] = 'stale'; $changed = true; } foreach ($findings as $index => $finding) { if (!is_array($finding)) { continue; } $check_id = (string) ($finding['id'] ?? ''); if (!isset($presentation_by_check[$check_id])) { continue; } $next_state = $presentation_by_check[$check_id]; if ((string) ($finding['presentationState'] ?? '') === $next_state) { continue; } $next = $finding; $next['presentationState'] = $next_state; $next['verificationScheduled'] = false; if ($next_state !== 'verified') { unset($next['verifiedAt'], $next['verificationRef']); } $findings[$index] = $next; $changed = true; } if (!$changed) { return $scan; } return array_merge($scan, [ 'findings' => $findings, 'verifications' => self::bound_current_verifications($verifications, $scope), 'verificationHistory' => $history, 'observations' => self::filter_scoped_observations($observations, $scope), ]); } /** * @param list> $findings * @param array|null $prior_scan * @return array{observations: list>, verifications: list>, verificationHistory: list>, findings: list>} */ public static function reconcile_scan(array $findings, string $recheck_source, ?array $prior_scan = null): array { self::assert_valid_recheck_source($recheck_source); $subject_deps = self::build_subject_deps(); $scope = self::local_scope(); $prior_verifications = is_array($prior_scan['verifications'] ?? null) ? $prior_scan['verifications'] : []; $prior_history = is_array($prior_scan['verificationHistory'] ?? null) ? $prior_scan['verificationHistory'] : []; [$prior_verifications, $prior_history] = self::demote_version_drifted_current_verifications( $prior_verifications, $prior_history, $scope, ); if ($prior_scan !== null) { $prior_scan['verifications'] = $prior_verifications; $prior_scan['verificationHistory'] = $prior_history; } $context = self::context_from_scan($prior_scan, $scope); $enriched = []; foreach ($findings as $finding) { if (!is_array($finding)) { continue; } $check_scope = array_merge($scope, [ 'checkId' => (string) ($finding['id'] ?? ''), 'subjectId' => (string) ($finding['id'] ?? ''), ]); $result = self::reconcile_finding($finding, $recheck_source, $subject_deps, $context, $check_scope); $context = $result['context']; $enriched[] = $result['finding']; } return [ 'observations' => $context['observations'], 'verifications' => self::bound_current_verifications($context['verifications'], $scope), 'verificationHistory' => $context['history'], 'findings' => $enriched, ]; } /** * @param array $prior_scan * @param array $target_finding * @param list> $all_findings * @return array{observations: list>, verifications: list>, verificationHistory: list>, findings: list>} */ public static function reconcile_targeted_recheck(array $prior_scan, array $target_finding, array $all_findings): array { $subject_deps = self::build_subject_deps(); $scope = self::local_scope(); $stale_scan = self::apply_scan_staleness($prior_scan); $context = self::context_from_scan($stale_scan, $scope); $target_id = (string) ($target_finding['id'] ?? ''); $check_scope = array_merge($scope, [ 'checkId' => $target_id, 'subjectId' => $target_id, ]); $result = self::reconcile_finding( $target_finding, 'manual_targeted', $subject_deps, $context, $check_scope, ); $context = $result['context']; $presentation_by_check = []; foreach ($context['verifications'] as $record) { if (is_array($record) && !empty($record['isCurrent'])) { $presentation_by_check[(string) $record['checkId']] = 'verified'; } } $presentation_by_check[$target_id] = (string) $result['finding']['presentationState']; $enriched = []; foreach ($all_findings as $finding) { if (!is_array($finding)) { continue; } $check_id = (string) ($finding['id'] ?? ''); if ($check_id === $target_id) { $enriched[] = $result['finding']; continue; } $next = $finding; $status = (string) ($next['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN); if (isset($presentation_by_check[$check_id])) { $next['presentationState'] = $presentation_by_check[$check_id]; } elseif (in_array($status, [TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN], true)) { $next['presentationState'] = 'pending_recheck'; } elseif (($next['presentationState'] ?? '') === 'verified') { $next['presentationState'] = 'stale'; unset($next['verifiedAt'], $next['verificationRef']); } $next['verificationScheduled'] = false; $enriched[] = $next; } return [ 'observations' => $context['observations'], 'verifications' => self::bound_current_verifications($context['verifications'], $scope), 'verificationHistory' => $context['history'], 'findings' => $enriched, ]; } /** * @param array $scan * @return array */ public static function sanitize_scan_scope(array $scan): array { $scope = self::local_scope(); $verifications = is_array($scan['verifications'] ?? null) ? $scan['verifications'] : []; $history = is_array($scan['verificationHistory'] ?? null) ? $scan['verificationHistory'] : []; [$verifications, $history] = self::demote_version_drifted_current_verifications( $verifications, $history, $scope, ); $scan = array_merge($scan, [ 'verifications' => $verifications, 'verificationHistory' => $history, ]); $context = self::context_from_scan($scan, $scope); return array_merge($scan, [ 'observations' => $context['observations'], 'verifications' => self::bound_current_verifications($context['verifications'], $scope), 'verificationHistory' => $context['history'], ]); } /** * @param array $finding * @param array $scope * @param array{observations: list>, verifications: list>, history: list>} $context * @return array{finding: array, context: array{observations: list>, verifications: list>, history: list>}} */ public static function reconcile_finding( array $finding, string $recheck_source, array $subject_deps, array $context, array $scope, ): array { $check_id = (string) ($finding['id'] ?? ''); $observation = self::build_observation( $check_id, (string) ($finding['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN), is_array($finding['evidence'] ?? null) ? $finding['evidence'] : [], $subject_deps, $recheck_source, TheDotDevAgenticBridge_Readiness_Clock::iso(), ); $reconciled = self::reconcile_observation($observation, $context, $scope); $finding['presentationState'] = $reconciled['presentationState']; $finding['recheckSource'] = $recheck_source; $finding['verificationScheduled'] = false; if ($reconciled['currentVerification'] !== null) { $finding['verifiedAt'] = (string) ($reconciled['currentVerification']['verifiedAt'] ?? ''); $finding['verificationRef'] = (string) ($reconciled['currentVerification']['observationRef'] ?? ''); } else { unset($finding['verifiedAt'], $finding['verificationRef']); } return [ 'finding' => $finding, 'context' => $reconciled['context'], ]; } /** * @param array|null $scan * @param array $scope * @return array{observations: list>, verifications: list>, history: list>} */ public static function context_from_scan(?array $scan, array $scope): array { $observations = is_array($scan['observations'] ?? null) ? $scan['observations'] : []; $verifications = is_array($scan['verifications'] ?? null) ? $scan['verifications'] : []; $history = is_array($scan['verificationHistory'] ?? null) ? $scan['verificationHistory'] : []; return [ 'observations' => self::filter_scoped_observations($observations, $scope), 'verifications' => self::filter_scoped_verifications($verifications, $scope), 'history' => self::filter_valid_history($history, $scope), ]; } /** @param mixed $record */ public static function is_valid_observation(mixed $record): bool { if (!is_array($record)) { return false; } $required = [ 'schemaVersion', 'namespace', 'observationRef', 'checkId', 'checkVersion', 'methodologyVersion', 'siteId', 'blogId', 'subjectType', 'subjectId', 'status', 'observedAt', 'recheckSource', 'evidenceFingerprint', 'subjectFingerprint', ]; foreach ($required as $key) { if (!array_key_exists($key, $record)) { return false; } } if ($record['schemaVersion'] !== self::SCHEMA_VERSION || $record['namespace'] !== 'readiness') { return false; } if (!is_int($record['blogId'] ?? null) || (int) $record['blogId'] < 0) { return false; } if ((int) $record['blogId'] !== self::local_blog_id()) { return false; } $site_id = (string) ($record['siteId'] ?? ''); if ($site_id === '' || strlen($site_id) > 128 || $site_id !== self::local_site_id()) { return false; } if (($record['subjectType'] ?? '') !== 'readiness_check') { return false; } $status = (string) ($record['status'] ?? ''); if (!in_array($status, [ TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE, ], true)) { return false; } $check_id = (string) ($record['checkId'] ?? ''); $check_version = (string) ($record['checkVersion'] ?? ''); $methodology_version = (string) ($record['methodologyVersion'] ?? ''); if ($check_id === '' || strlen($check_id) > 64) { return false; } if ($check_version === '' || strlen($check_version) > 32) { return false; } if ($methodology_version === '' || strlen($methodology_version) > 32) { return false; } if (strlen((string) ($record['subjectId'] ?? '')) === 0 || strlen((string) $record['subjectId']) > 64) { return false; } if (!in_array($record['recheckSource'], self::VALID_RECHECK_SOURCES, true)) { return false; } if (!self::is_uuid((string) $record['observationRef'])) { return false; } if (!self::is_sha256((string) $record['evidenceFingerprint']) || !self::is_sha256((string) $record['subjectFingerprint'])) { return false; } if (!self::is_iso_datetime((string) $record['observedAt'])) { return false; } if (array_key_exists('evidence', $record) && $record['evidence'] !== null) { if (!is_array($record['evidence'])) { return false; } $bounded = TheDotDevAgenticBridge_Readiness_Methodology::bound_evidence($record['evidence']); if ($bounded !== $record['evidence']) { return false; } } return true; } /** @param mixed $record */ public static function is_valid_verification(mixed $record): bool { if (!is_array($record) || ($record['kind'] ?? '') !== 'verification') { return false; } return self::is_valid_observation($record) && ($record['state'] ?? '') === 'verified' && ($record['presentationState'] ?? '') === 'verified' && self::is_uuid((string) ($record['priorObservationRef'] ?? '')) && isset($record['isCurrent']) && self::is_iso_datetime((string) ($record['verifiedAt'] ?? '')); } /** * @param array $evidence * @return array */ private static function build_observation( string $check_id, string $status, array $evidence, array $subject_deps, string $recheck_source, string $observed_at, ): array { $bounded_evidence = TheDotDevAgenticBridge_Readiness_Methodology::bound_evidence($evidence); return [ 'schemaVersion' => self::SCHEMA_VERSION, 'namespace' => 'readiness', 'observationRef' => self::new_uuid(), 'checkId' => $check_id, 'checkVersion' => self::DEFAULT_CHECK_VERSION, 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'siteId' => self::local_site_id(), 'blogId' => self::local_blog_id(), 'subjectType' => 'readiness_check', 'subjectId' => $check_id, 'status' => TheDotDevAgenticBridge_Readiness_Methodology::normalize_status($status), 'observedAt' => $observed_at, 'recheckSource' => $recheck_source, 'evidenceFingerprint' => self::fingerprint_evidence($bounded_evidence), 'subjectFingerprint' => self::fingerprint_subject($subject_deps), 'evidence' => $bounded_evidence, ]; } /** * @param array $observation * @param array{observations: list>, verifications: list>, history: list>} $context * @param array $scope * @return array{presentationState: string, currentVerification: array|null, context: array{observations: list>, verifications: list>, history: list>}} */ private static function reconcile_observation(array $observation, array $context, array $scope): array { [$verifications, $history, $version_drift_checks] = self::demote_version_drifted_current_verifications( $context['verifications'], $context['history'], $scope, ); $observations = $context['observations']; $presentation = in_array((string) ($observation['checkId'] ?? ''), $version_drift_checks, true) ? 'stale' : 'observed'; $prior_observation = self::find_latest_observation($observations, $scope); $current_verification = self::find_current_verification($verifications, $scope); if ( $current_verification !== null && ($current_verification['subjectFingerprint'] ?? '') !== ($observation['subjectFingerprint'] ?? '') ) { $verifications = self::mark_verification_non_current($verifications, $scope); $history = self::append_history($history, [$current_verification]); $current_verification = null; $presentation = 'stale'; } $status = (string) ($observation['status'] ?? TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN); if (in_array($status, [TheDotDevAgenticBridge_Readiness_Methodology::STATUS_FAIL, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_WARNING, TheDotDevAgenticBridge_Readiness_Methodology::STATUS_UNKNOWN], true)) { if ($current_verification !== null) { $verifications = self::mark_verification_non_current($verifications, $scope); $history = self::append_history($history, [$current_verification]); $current_verification = null; } $history = self::append_history($history, [$observation]); $observations = self::upsert_observation($observations, $observation); return [ 'presentationState' => 'pending_recheck', 'currentVerification' => null, 'context' => [ 'observations' => $observations, 'verifications' => $verifications, 'history' => $history, ], ]; } if ($status === TheDotDevAgenticBridge_Readiness_Methodology::STATUS_NOT_APPLICABLE) { if ($current_verification !== null) { $verifications = self::mark_verification_non_current($verifications, $scope); $history = self::append_history($history, [$current_verification]); $current_verification = null; } $history = self::append_history($history, [$observation]); $observations = self::upsert_observation($observations, $observation); return [ 'presentationState' => 'observed', 'currentVerification' => null, 'context' => [ 'observations' => $observations, 'verifications' => $verifications, 'history' => $history, ], ]; } if ($status === TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS) { $prior_had_issue = $prior_observation !== null && in_array((string) ($prior_observation['status'] ?? ''), self::ISSUE_STATUSES, true); if ($prior_had_issue && $prior_observation !== null) { $verified = self::create_verification($observation, (string) $prior_observation['observationRef']); $verifications = self::mark_verification_non_current($verifications, $scope); $verifications[] = $verified; $current_verification = $verified; $presentation = 'verified'; $history = self::append_history($history, [$observation, $verified]); } else { $presentation = $presentation === 'stale' ? 'stale' : 'passing'; $history = self::append_history($history, [$observation]); } $observations = self::upsert_observation($observations, $observation); return [ 'presentationState' => $presentation, 'currentVerification' => $current_verification, 'context' => [ 'observations' => $observations, 'verifications' => $verifications, 'history' => $history, ], ]; } $history = self::append_history($history, [$observation]); $observations = self::upsert_observation($observations, $observation); return [ 'presentationState' => $presentation, 'currentVerification' => $current_verification, 'context' => [ 'observations' => $observations, 'verifications' => $verifications, 'history' => $history, ], ]; } /** @param array $observation */ private static function create_verification(array $observation, string $prior_ref): array { return [ 'schemaVersion' => self::SCHEMA_VERSION, 'namespace' => 'readiness', 'kind' => 'verification', 'state' => 'verified', 'presentationState' => 'verified', 'observationRef' => (string) $observation['observationRef'], 'checkId' => (string) $observation['checkId'], 'checkVersion' => (string) $observation['checkVersion'], 'methodologyVersion' => (string) $observation['methodologyVersion'], 'siteId' => (string) $observation['siteId'], 'blogId' => (int) $observation['blogId'], 'subjectType' => 'readiness_check', 'subjectId' => (string) $observation['subjectId'], 'status' => TheDotDevAgenticBridge_Readiness_Methodology::STATUS_PASS, 'observedAt' => (string) $observation['observedAt'], 'verifiedAt' => (string) $observation['observedAt'], 'recheckSource' => (string) $observation['recheckSource'], 'evidenceFingerprint' => (string) $observation['evidenceFingerprint'], 'subjectFingerprint' => (string) $observation['subjectFingerprint'], 'priorObservationRef' => $prior_ref, 'isCurrent' => true, ]; } /** * @param list> $observations * @return array|null */ private static function find_latest_observation(array $observations, array $scope): ?array { $key = self::identity_key(array_merge($scope, [ 'subjectId' => (string) ($scope['checkId'] ?? ''), 'checkId' => (string) ($scope['checkId'] ?? ''), ])); $latest = null; foreach ($observations as $observation) { if (!is_array($observation) || self::identity_key($observation) !== $key) { continue; } if ($latest === null || (string) $observation['observedAt'] > (string) $latest['observedAt']) { $latest = $observation; } } return $latest; } /** * @param list> $verifications * @return array|null */ private static function find_current_verification(array $verifications, array $scope): ?array { $key = self::identity_key(array_merge($scope, [ 'subjectId' => (string) ($scope['checkId'] ?? ''), 'checkId' => (string) ($scope['checkId'] ?? ''), ])); foreach ($verifications as $record) { if (!is_array($record)) { continue; } if (self::identity_key($record) === $key && !empty($record['isCurrent'])) { return $record; } } return null; } /** * @param list> $verifications * @return list> */ private static function mark_verification_non_current(array $verifications, array $scope): array { $key = self::identity_key(array_merge($scope, [ 'subjectId' => (string) ($scope['checkId'] ?? ''), 'checkId' => (string) ($scope['checkId'] ?? ''), ])); $out = []; foreach ($verifications as $record) { if (!is_array($record)) { continue; } if (self::identity_key($record) === $key && !empty($record['isCurrent'])) { $record['isCurrent'] = false; } $out[] = $record; } return $out; } /** * @param list> $verifications * @return list> */ private static function mark_verification_non_current_by_logical_key(array $verifications, array $scope): array { $logical_key = self::logical_identity_key(array_merge($scope, [ 'subjectId' => (string) ($scope['subjectId'] ?? $scope['checkId'] ?? ''), 'checkId' => (string) ($scope['checkId'] ?? ''), ])); $out = []; foreach ($verifications as $record) { if (!is_array($record)) { continue; } if (self::logical_identity_key($record) === $logical_key && !empty($record['isCurrent'])) { $record['isCurrent'] = false; } $out[] = $record; } return $out; } /** * @param list> $verifications * @param list> $history * @param array $scope * @return array{0: list>, 1: list>, 2: list} */ private static function demote_version_drifted_current_verifications( array $verifications, array $history, array $scope, ): array { $demoted_checks = []; $next_verifications = $verifications; foreach ($verifications as $record) { if (!is_array($record) || empty($record['isCurrent'])) { continue; } if (!self::matches_logical_scope($scope, $record)) { continue; } $check_scope = array_merge($scope, [ 'checkId' => (string) ($record['checkId'] ?? ''), 'subjectId' => (string) ($record['subjectId'] ?? $record['checkId'] ?? ''), 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'checkVersion' => self::DEFAULT_CHECK_VERSION, ]); if (self::identity_key($record) === self::identity_key($check_scope)) { continue; } $next_verifications = self::mark_verification_non_current_by_logical_key( $next_verifications, $check_scope, ); $demoted = $record; $demoted['isCurrent'] = false; $history = self::append_history($history, [$demoted]); $demoted_checks[] = (string) ($record['checkId'] ?? ''); } return [$next_verifications, $history, array_values(array_unique($demoted_checks))]; } /** * @param list> $observations * @return list> */ private static function upsert_observation(array $observations, array $observation): array { $key = self::identity_key($observation); $kept = []; foreach ($observations as $item) { if (!is_array($item) || self::identity_key($item) !== $key) { $kept[] = $item; } } $kept[] = $observation; return $kept; } /** * @param list> $verifications * @return list> */ private static function bound_current_verifications(array $verifications, ?array $scope = null): array { $current = []; foreach ($verifications as $record) { if (!is_array($record) || empty($record['isCurrent'])) { continue; } if ($scope !== null) { $expected_key = self::identity_key(array_merge($scope, [ 'subjectId' => (string) ($record['subjectId'] ?? $record['checkId'] ?? ''), 'checkId' => (string) ($record['checkId'] ?? ''), 'methodologyVersion' => TheDotDevAgenticBridge_Readiness_Methodology::VERSION, 'checkVersion' => self::DEFAULT_CHECK_VERSION, ])); if (self::identity_key($record) !== $expected_key) { continue; } } $current[] = $record; } return array_slice($current, 0, self::MAX_CURRENT_VERIFICATIONS); } /** * @param list> $history * @param list> $entries * @return list> */ private static function append_history(array $history, array $entries): array { return array_slice(array_merge($entries, $history), 0, self::MAX_HISTORY); } /** * @param list $entries * @param array $scope * @return list> */ private static function filter_scoped_observations(array $entries, array $scope): array { $out = []; foreach ($entries as $entry) { if (self::is_valid_observation($entry) && self::matches_scope($scope, $entry)) { $out[] = $entry; } } return $out; } /** * @param list $entries * @param array $scope * @return list> */ private static function filter_scoped_verifications(array $entries, array $scope): array { $out = []; foreach ($entries as $entry) { if (self::is_valid_verification($entry) && self::matches_scope($scope, $entry)) { $out[] = $entry; } } return $out; } /** * @param list $entries * @param array $scope * @return list> */ private static function filter_valid_history(array $entries, array $scope): array { $out = []; foreach ($entries as $entry) { if ((self::is_valid_observation($entry) || self::is_valid_verification($entry)) && self::matches_scope($scope, $entry)) { $out[] = $entry; } } return array_slice($out, 0, self::MAX_HISTORY); } private static function assert_valid_recheck_source(string $source): void { if (!in_array($source, self::VALID_RECHECK_SOURCES, true)) { throw new InvalidArgumentException('Invalid recheck source: ' . sanitize_key($source)); } } private static function is_uuid(string $value): bool { return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1; } private static function is_sha256(string $value): bool { return preg_match('/^[a-f0-9]{64}$/', $value) === 1; } private static function is_iso_datetime(string $value): bool { return preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/', $value, ) === 1; } /** @param array $value */ public static function sha256_canonical(array $value): string { return hash('sha256', self::canonical_json($value)); } public static function sha256_canonical_value(mixed $value): string { return hash('sha256', self::canonical_json_value($value)); } public static function canonical_json_value(mixed $value): string { if ($value instanceof stdClass && (array) $value === []) { return '{}'; } if (!is_array($value)) { $value = json_decode(json_encode($value), true); } if (!is_array($value)) { return '{}'; } return self::canonical_json($value); } /** @param array $value */ public static function canonical_json(array $value): string { $encoded = wp_json_encode(self::canonicalize_value($value), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); return is_string($encoded) ? $encoded : '{}'; } private static function canonicalize_value(mixed $value): mixed { if ($value === null) { return $value; } if (is_bool($value)) { return $value; } if (is_int($value)) { return $value; } if (is_float($value)) { throw new InvalidArgumentException('Float values are not permitted in canonical evidence'); } if (is_string($value)) { return $value; } if (!is_array($value)) { return (string) $value; } if (array_is_list($value)) { $normalized = array_map(static fn(mixed $item): mixed => self::canonicalize_value($item), $value); if ($normalized !== [] && self::all_strings($normalized)) { sort($normalized); return $normalized; } if ($normalized !== [] && self::all_ints($normalized)) { sort($normalized); return $normalized; } return $normalized; } $keys = array_keys($value); sort($keys); $out = []; foreach ($keys as $key) { $next = $value[$key]; if ($next === null) { continue; } $out[(string) $key] = self::canonicalize_value($next); } return $out; } /** @param list $items */ private static function all_strings(array $items): bool { foreach ($items as $item) { if (!is_string($item)) { return false; } } return true; } /** @param list $items */ private static function all_ints(array $items): bool { foreach ($items as $item) { if (!is_int($item)) { return false; } } return true; } private static function new_uuid(): string { if (function_exists('wp_generate_uuid4')) { return wp_generate_uuid4(); } return 'obs-' . gmdate('YmdHis', TheDotDevAgenticBridge_Readiness_Clock::now()) . '-' . wp_rand(1000, 9999); } }