*/ private const FIELD_KEYS = [ 'inclusionMode', 'inheritDefaults', 'intent', 'allowedActions', 'blockedClaims', 'customBlockedClaims', 'agentSummary', 'relationshipLabel', 'relationshipType', ]; /** * Normalize the exact editor payload before it enters the lifecycle. * * @param array $input * @return array{ok: bool, values?: array, code?: string, message?: string} */ public static function normalize_submitted_values(array $input): array { foreach (array_keys($input) as $key) { if (!in_array((string) $key, self::FIELD_KEYS, true)) { return self::fail('unknown_field', __('The proposed settings contain an unsupported field.', 'agentic-bridge')); } } $required_scalars = [ 'inclusionMode', 'inheritDefaults', 'intent', 'agentSummary', 'relationshipLabel', 'relationshipType', ]; foreach ($required_scalars as $required) { if (!array_key_exists($required, $input) || !is_string($input[$required])) { return self::fail('malformed_field', __('The proposed settings payload is incomplete or malformed.', 'agentic-bridge')); } } $allowed_actions = self::normalize_id_list($input['allowedActions'] ?? []); $blocked_claims = self::normalize_id_list($input['blockedClaims'] ?? []); if ($allowed_actions === null || $blocked_claims === null) { return self::fail('malformed_field', __('The proposed settings payload is incomplete or malformed.', 'agentic-bridge')); } $custom_claims = self::normalize_custom_claims($input['customBlockedClaims'] ?? ''); if ($custom_claims === null) { return self::fail('malformed_field', __('The proposed settings payload is incomplete or malformed.', 'agentic-bridge')); } if (!in_array($input['inheritDefaults'], ['0', '1'], true)) { return self::fail('malformed_field', __('The proposed settings payload is incomplete or malformed.', 'agentic-bridge')); } return [ 'ok' => true, 'values' => [ 'inclusionMode' => sanitize_key($input['inclusionMode']), 'inheritDefaults' => $input['inheritDefaults'] === '1', 'intent' => sanitize_key($input['intent']), 'allowedActions' => $allowed_actions, 'blockedClaims' => $blocked_claims, 'customBlockedClaims' => $custom_claims, 'agentSummary' => sanitize_text_field($input['agentSummary']), 'relationshipLabel' => sanitize_text_field($input['relationshipLabel']), 'relationshipType' => sanitize_key($input['relationshipType']), ], ]; } /** * @param array $record * @return list */ public static function changed_fields(array $record): array { $before = is_array($record['before'] ?? null) ? $record['before'] : []; $after = is_array($record['after'] ?? null) ? $record['after'] : []; $changes = []; foreach (self::FIELD_KEYS as $key) { if (($before[$key] ?? null) === ($after[$key] ?? null)) { continue; } $changes[] = [ 'field' => self::field_label($key), 'before' => self::display_value($key, $before[$key] ?? null), 'after' => self::display_value($key, $after[$key] ?? null), ]; } return $changes; } /** @param WP_Post $post */ public static function render_editor($post): void { if (!$post instanceof WP_Post) { return; } echo '
'; echo '

' . esc_html__('Review and approve changes to Agentic Bridge settings for this page. Nothing changes until an administrator approves and applies a proposal. Page content and SEO-plugin fields are never edited here.', 'agentic-bridge') . '

'; if (!self::eligible((int) $post->ID)) { echo '

' . esc_html__('Not available: publish a non-password-protected page or post before using local readiness changes.', 'agentic-bridge') . '

'; return; } if (!current_user_can(TheDotDevAgenticBridge_Content_Readiness_Fix_Lifecycle::CAPABILITY)) { echo '

' . esc_html__('An administrator with site-management permission can propose and approve these changes.', 'agentic-bridge') . '

'; return; } self::render_quick_actions($post); self::render_explicit_form($post); echo '
'; echo '

' . esc_html__('Change history', 'agentic-bridge') . '

'; echo '

' . esc_html__('Each proposal keeps its original settings, named decision, application result, local verification and restore events.', 'agentic-bridge') . '

'; $records = TheDotDevAgenticBridge_Content_Readiness_Fix_Lifecycle::for_post((int) $post->ID); if ($records === []) { echo '

' . esc_html__('No local Agent Ready changes have been proposed for this page.', 'agentic-bridge') . '

'; } else { foreach (array_reverse($records) as $record) { self::render_record($record); } } echo '
'; } /** @param mixed $value @return list|null */ private static function normalize_id_list(mixed $value): ?array { if (!is_array($value)) { return null; } $items = []; foreach ($value as $item) { if (!is_string($item)) { return null; } $clean = sanitize_key($item); if ($clean !== '') { $items[] = $clean; } } return array_values(array_unique($items)); } /** @param mixed $value @return list|null */ private static function normalize_custom_claims(mixed $value): ?array { if (!is_string($value) && !is_array($value)) { return null; } $items = is_array($value) ? $value : preg_split('/\r?\n/', $value); if (!is_array($items)) { return []; } $claims = []; foreach ($items as $item) { if (!is_string($item)) { return null; } $clean = trim(sanitize_text_field($item)); if ($clean !== '') { $claims[] = $clean; } } return array_values(array_unique($claims)); } private static function render_quick_actions(WP_Post $post): void { $workspace_url = TheDotDevAgenticBridge_Content_Agent_Ready_Editor::content_workspace_url((int) $post->ID); echo '
'; echo '

' . esc_html__('Quick proposals', 'agentic-bridge') . '

'; echo '

' . esc_html__('Create focused include, exclude, or allow-read proposals in Agentic Bridge. You will still review them before approval and application.', 'agentic-bridge') . '

'; echo '
'; TheDotDevAgenticBridge_Content_Agent_Ready_Editor::render_workspace_link( __('Open content workspace', 'agentic-bridge'), $workspace_url, ['class' => 'button button-primary'] ); echo '
'; } private static function render_explicit_form(WP_Post $post): void { $workspace_url = TheDotDevAgenticBridge_Content_Agent_Ready_Editor::content_workspace_url((int) $post->ID); echo '
'; echo '' . esc_html__('Propose detailed Agent Ready settings', 'agentic-bridge') . ''; echo '
'; echo '

' . esc_html__('Detailed readiness proposals are edited in Agentic Bridge so this WordPress editor save stays compatible with SEO plugins and Rank Math.', 'agentic-bridge') . '

'; TheDotDevAgenticBridge_Content_Agent_Ready_Editor::render_workspace_link( __('Propose settings in Agentic Bridge', 'agentic-bridge'), $workspace_url, ['class' => 'button button-primary'] ); echo '
'; } /** @param array $record */ private static function render_record(array $record): void { $state = (string) ($record['state'] ?? 'UNKNOWN'); $changes = self::changed_fields($record); echo '
'; echo '

' . esc_html(TheDotDevAgenticBridge_Content_Readiness_Fix_Lifecycle::label((string) ($record['change'] ?? ''))) . '

'; echo '

' . esc_html(sprintf( /* translators: 1: administrator user ID, 2: UTC timestamp */ __('Proposed by administrator #%1$d at %2$s UTC.', 'agentic-bridge'), (int) ($record['createdBy'] ?? 0), self::display_time((string) ($record['createdAt'] ?? '')), )) . '

'; echo '' . esc_html(TheDotDevAgenticBridge_Content_Readiness_Fix_Lifecycle::state_label($state)) . '
'; if (is_array($record['approval'] ?? null)) { echo '

' . esc_html(sprintf( /* translators: 1: administrator user ID, 2: UTC timestamp */ __('Named approval: administrator #%1$d at %2$s UTC.', 'agentic-bridge'), (int) ($record['approval']['actorId'] ?? 0), self::display_time((string) ($record['approval']['at'] ?? '')), )) . '

'; } if ($changes !== []) { echo '
'; echo '
' . esc_html__('Setting', 'agentic-bridge') . '' . esc_html__('Before', 'agentic-bridge') . '' . esc_html__('Proposed', 'agentic-bridge') . '
'; foreach ($changes as $change) { echo '
' . esc_html($change['field']) . '' . esc_html($change['before']) . '' . esc_html($change['after']) . '
'; } echo '
'; } echo '
' . esc_html__('Activity details', 'agentic-bridge') . '
    '; foreach (is_array($record['audit'] ?? null) ? $record['audit'] : [] as $event) { if (!is_array($event)) { continue; } echo '
  1. ' . esc_html(sprintf( /* translators: 1: activity name, 2: administrator user ID, 3: UTC timestamp */ __('%1$s by administrator #%2$d at %3$s UTC', 'agentic-bridge'), self::event_label((string) ($event['event'] ?? '')), (int) ($event['actorId'] ?? 0), self::display_time((string) ($event['at'] ?? '')), )) . '
  2. '; } echo '
'; $actions = match ($state) { 'READY_FOR_REVIEW' => ['approve' => __('Approve', 'agentic-bridge'), 'reject' => __('Do not approve', 'agentic-bridge')], 'APPROVED' => ['apply' => __('Apply approved change', 'agentic-bridge')], 'APPLIED' => ['verify' => __('Verify locally', 'agentic-bridge'), 'restore' => __('Restore previous settings', 'agentic-bridge')], 'VERIFIED' => ['restore' => __('Restore previous settings', 'agentic-bridge')], default => [], }; if ($actions !== []) { $workspace_url = TheDotDevAgenticBridge_Content_Agent_Ready_Editor::content_workspace_url((int) ($record['postId'] ?? 0)); echo '
'; echo '

' . esc_html__('Approval and apply actions run in Agentic Bridge, not inside this editor.', 'agentic-bridge') . '

'; TheDotDevAgenticBridge_Content_Agent_Ready_Editor::render_workspace_link( __('Manage in Agentic Bridge', 'agentic-bridge'), $workspace_url, ['class' => 'button'] ); echo '' . esc_html(implode(', ', array_values($actions))) . ''; echo '
'; } echo '
'; } private static function eligible(int $post_id): bool { $post = get_post($post_id); return $post instanceof WP_Post && in_array($post->post_type, ['post', 'page'], true) && $post->post_status === 'publish' && $post->post_password === ''; } private static function field_label(string $key): string { return match ($key) { 'inclusionMode' => __('Artifact visibility', 'agentic-bridge'), 'inheritDefaults' => __('Use reviewed site defaults', 'agentic-bridge'), 'intent' => __('Page purpose', 'agentic-bridge'), 'allowedActions' => __('Allowed uses', 'agentic-bridge'), 'blockedClaims' => __('Claims to withhold', 'agentic-bridge'), 'customBlockedClaims' => __('Other claims to withhold', 'agentic-bridge'), 'agentSummary' => __('Agent-facing summary', 'agentic-bridge'), 'relationshipLabel' => __('Related offering name', 'agentic-bridge'), 'relationshipType' => __('Related offering type', 'agentic-bridge'), default => $key, }; } private static function display_value(string $key, mixed $value): string { if ($key === 'inheritDefaults') { return !empty($value) ? __('Yes', 'agentic-bridge') : __('No', 'agentic-bridge'); } if ($key === 'inclusionMode' && is_string($value)) { return TheDotDevAgenticBridge_Content_Metadata::inclusion_mode_label($value); } if ($key === 'intent' && is_string($value)) { return TheDotDevAgenticBridge_Content_Metadata::intent_label($value); } if ($key === 'relationshipType' && is_string($value)) { return match ($value) { 'service' => __('Service', 'agentic-bridge'), 'product' => __('Product', 'agentic-bridge'), default => __('Not set', 'agentic-bridge'), }; } if (is_array($value)) { $labels = []; foreach ($value as $item) { if (!is_string($item)) { continue; } $labels[] = match ($key) { 'allowedActions' => TheDotDevAgenticBridge_Content_Metadata::action_label($item), 'blockedClaims' => TheDotDevAgenticBridge_Content_Metadata::blocked_claim_label($item), default => $item, }; } return $labels === [] ? __('None', 'agentic-bridge') : implode(', ', $labels); } if (!is_scalar($value) || (string) $value === '') { return __('Not set', 'agentic-bridge'); } return (string) $value; } private static function event_label(string $event): string { return match ($event) { 'created' => __('Proposal created', 'agentic-bridge'), 'approved' => __('Approval recorded', 'agentic-bridge'), 'rejected' => __('Proposal not approved', 'agentic-bridge'), 'applied' => __('Approved settings applied', 'agentic-bridge'), 'verified' => __('Applied settings verified', 'agentic-bridge'), 'restored' => __('Previous settings restored', 'agentic-bridge'), 'conflict' => __('Conflict detected', 'agentic-bridge'), 'restore_conflict' => __('Restore conflict detected', 'agentic-bridge'), default => __('Activity recorded', 'agentic-bridge'), }; } private static function display_time(string $value): string { $timestamp = strtotime($value); return $timestamp === false ? __('Unknown time', 'agentic-bridge') : gmdate('Y-m-d H:i', $timestamp); } /** @return array{ok: false, code: string, message: string} */ private static function fail(string $code, string $message): array { return ['ok' => false, 'code' => $code, 'message' => $message]; } }