'Include this page in Agent Ready files', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Action identifier, not a WP_Query argument. 'exclude' => 'Exclude this page from Agent Ready files', 'allow_read' => 'Allow agents to read this page', 'set_fields' => 'Update Agent Ready settings', ]; /** @return array */ public static function create(int $post_id, string $change, int $actor_id, array $values = []): array { if (!current_user_can(self::CAPABILITY)) return self::fail('denied', 'You do not have permission to propose this local change.'); if (!self::eligible($post_id)) return self::fail('not_applicable', 'Only an eligible published post or page can receive a local readiness change.'); $change = sanitize_key($change); if ($change !== 'set_fields' && !isset(self::CHANGES[$change])) return self::fail('unsupported', 'This local readiness change is not supported.'); $raw_before = TheDotDevAgenticBridge_Content_Metadata::raw_record($post_id); $before = self::comparable(self::config($post_id)); $after = $change === 'set_fields' ? self::proposal_fields($before, $values) : self::proposal($before, $change); if ($after === null) return self::fail('needs_input', 'Provide explicit valid Agentic Bridge values before proposing this change.'); $validation = TheDotDevAgenticBridge_Content_Metadata::validate_content_config($after); if (empty($validation['ok'])) return self::fail('needs_input', 'Provide explicit valid Agentic Bridge values before proposing this change.'); $after = is_array($validation['config'] ?? null) ? self::comparable($validation['config']) : $after; if ($before === $after) return self::fail('not_applicable', 'This local readiness setting is already in place.'); $record = ['id'=>wp_generate_uuid4(),'postId'=>$post_id,'change'=>$change,'values'=>$values,'ownership'=>'agentic_bridge','state'=>'READY_FOR_REVIEW','before'=>$before,'rawBefore'=>$raw_before,'after'=>$after,'baseFingerprint'=>self::fingerprint($before),'version'=>1,'createdBy'=>$actor_id,'createdAt'=>gmdate('c'),'approval'=>null,'audit'=>[['event'=>'created','at'=>gmdate('c'),'actorId'=>$actor_id]]]; self::save($record); return ['ok'=>true,'record'=>$record]; } /** @return array */ public static function approve(string $id,int $actor): array { return self::transition($id,'READY_FOR_REVIEW','APPROVED',$actor,'approved'); } /** @return array */ public static function reject(string $id,int $actor): array { return self::transition($id,'READY_FOR_REVIEW','REJECTED',$actor,'rejected'); } /** @return array */ public static function apply(string $id,int $actor): array { $r=self::find($id); if(!$r)return self::fail('not_found','The local readiness change was not found.'); if(!current_user_can(self::CAPABILITY))return self::fail('denied','You do not have permission to apply this local change.'); if(($r['state']??'')!=='APPROVED')return self::fail('approval_required','Named approval is required before applying.'); $post=(int)$r['postId']; if(!self::eligible($post))return self::fail('not_applicable','The target is no longer eligible.'); if(!hash_equals((string)$r['baseFingerprint'],self::fingerprint(self::comparable(self::config($post)))))return self::update($r,'CONFLICT',$actor,'conflict'); $saved=TheDotDevAgenticBridge_Content_Metadata::save_content_config($post,is_array($r['after']??null)?$r['after']:[]); if(empty($saved['ok']))return self::fail('apply_failed','The local readiness setting could not be saved.'); return self::update($r,'APPLIED',$actor,'applied'); } /** @return array */ public static function verify(string $id,int $actor): array { $r=self::find($id); if(!$r||($r['state']??'')!=='APPLIED')return self::fail('invalid_state','Apply the approved local change before verifying it.'); $matches=self::comparable(self::config((int)$r['postId']))===($r['after']??[]); return self::update($r,$matches ? 'VERIFIED':'CONFLICT',$actor,$matches ? 'verified':'conflict'); } /** @return array */ public static function restore(string $id,int $actor): array { $r=self::find($id); if(!$r||!in_array($r['state']??'', ['APPLIED','VERIFIED'],true))return self::fail('invalid_state','Only an applied local change can be restored.'); if(!current_user_can(self::CAPABILITY))return self::fail('denied','You do not have permission to restore this local change.'); if(!hash_equals(self::fingerprint(is_array($r['after']??null)?$r['after']:[]),self::fingerprint(self::comparable(self::config((int)$r['postId'])))))return self::update($r,'CONFLICT',$actor,'restore_conflict'); $saved=TheDotDevAgenticBridge_Content_Metadata::restore_raw_record((int)$r['postId'],is_array($r['rawBefore']??null)?$r['rawBefore']:['present'=>true,'value'=>$r['before']??[]]); if(empty($saved['ok']))return self::fail('restore_failed','The previous local setting could not be restored.'); return self::update($r,'RESTORED',$actor,'restored'); } /** @return list> */ public static function all(): array { $v=get_option(self::OPTION_KEY,[]);return is_array($v)?array_values($v):[]; } /** @return list> */ public static function for_post(int $post): array { return array_values(array_filter(self::all(),static fn($r):bool=>(int)($r['postId']??0)===$post)); } /** @return array> */ public static function resolver_catalog(): array { return [ 'include' => ['label' => self::label('include'), 'ownership' => 'agentic_bridge', 'reversible' => true, 'writes' => 'Agentic Bridge inclusion setting only'], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Action identifier, not a WP_Query argument. 'exclude' => ['label' => self::label('exclude'), 'ownership' => 'agentic_bridge', 'reversible' => true, 'writes' => 'Agentic Bridge inclusion setting only'], 'allow_read' => ['label' => self::label('allow_read'), 'ownership' => 'agentic_bridge', 'reversible' => true, 'writes' => 'Agentic Bridge allowed-action setting only'], ]; } public static function label(string $change): string { return self::CHANGES[$change]??__('Unsupported local change','agentic-bridge'); } public static function state_label(string $state): string { return self::state($state); } /** @param WP_Post $post */ public static function render_editor($post): void { TheDotDevAgenticBridge_Content_Readiness_Review_UI::render_editor($post); } private static function render_explicit_form(WP_Post $post): void { $config=self::config((int)$post->ID); echo '

'.esc_html__('Propose explicit local metadata values','agentic-bridge').'

'.esc_html__('Enter only the fields you intend to change. Empty fields mean Needs input and are not applied.','agentic-bridge').'

';wp_nonce_field('thedotdev_agentic_bridge_content_readiness_fix'); echo '

'; } /** @param array $r */ private static function render_record(array $r): void { $state=(string)($r['state']??'UNKNOWN');echo '

'.esc_html(self::label((string)($r['change']??''))).'
'.esc_html__('State:','agentic-bridge').' '.esc_html(self::state($state)).'

'; $actions=match($state){'READY_FOR_REVIEW'=>['approve'=>__('Approve','agentic-bridge'),'reject'=>__('Reject','agentic-bridge')],'APPROVED'=>['apply'=>__('Apply approved change','agentic-bridge')],'APPLIED'=>['verify'=>__('Verify locally','agentic-bridge'),'restore'=>__('Restore previous local setting','agentic-bridge')],'VERIFIED'=>['restore'=>__('Restore previous local setting','agentic-bridge')],default=>[]};foreach($actions as $op=>$label){echo '
';wp_nonce_field('thedotdev_agentic_bridge_content_readiness_fix');echo '
'; }echo '
'; } /** @return array|null */ private static function find(string $id): ?array { foreach(self::all() as $r)if(($r['id']??'')===$id)return $r;return null; } /** @return array */ private static function transition(string $id,string $from,string $to,int $actor,string $event):array{$r=self::find($id);if(!$r)return self::fail('not_found','The local readiness change was not found.');if(!current_user_can(self::CAPABILITY))return self::fail('denied','You do not have permission for this decision.');if(($r['state']??'')!==$from)return self::fail('invalid_state','That decision is no longer available.');if($to==='APPROVED')$r['approval']=['actorId'=>$actor,'at'=>gmdate('c'),'explicit'=>true];return self::update($r,$to,$actor,$event);} /** @return array */ private static function update(array $r,string $state,int $actor,string $event):array{$r['state']=$state;$r['version']=(int)($r['version']??0)+1;$r['audit'][]=['event'=>$event,'at'=>gmdate('c'),'actorId'=>$actor];self::save($r);return ['ok'=>$state!=='CONFLICT','record'=>$r];} private static function save(array $r):void{$all=self::all();foreach($all as $i=>$old)if(($old['id']??'')===($r['id']??'')){$all[$i]=$r;update_option(self::OPTION_KEY,$all,false);return;}$all[]=$r;update_option(self::OPTION_KEY,$all,false);} private static function eligible(int $id):bool{$p=get_post($id);return $p instanceof WP_Post&&in_array($p->post_type,['post','page'],true)&&$p->post_status==='publish'&&$p->post_password==='';} /** @return array */ private static function config(int $id):array{$p=get_post($id);return $p instanceof WP_Post?TheDotDevAgenticBridge_Content_Metadata::effective_config_for_post($p):[];} /** @return array */ private static function comparable(array $config):array{unset($config['updatedAt'],$config['sourceModifiedAt']);return $config;} /** @return array */ private static function proposal(array $config,string $change):array{if($change==='include'){$config['inclusionMode']='include';$config['inheritDefaults']=false;}elseif($change==='exclude'){$config['inclusionMode']='exclude';$config['inheritDefaults']=false;}elseif($change==='allow_read'){ $actions=is_array($config['allowedActions']??null)?$config['allowedActions']:[];if(!in_array('read_content',$actions,true))$actions[]='read_content';$config['allowedActions']=$actions;}return $config;} private static function fingerprint(array $value):string{$json=wp_json_encode($value);return hash('sha256',is_string($json)?$json:'');} private static function proposal_fields(array $config,array $values): ?array { $allowed=['inclusionMode','inheritDefaults','intent','allowedActions','blockedClaims','customBlockedClaims','agentSummary','relationshipLabel','relationshipType']; if($values===[])return null; foreach($values as $key=>$value){if(!in_array((string)$key,$allowed,true))return null;$config[(string)$key]=$value;} return $config; } private static function state(string $value):string{return match($value){'READY_FOR_REVIEW'=>__('Ready for named review','agentic-bridge'),'APPROVED'=>__('Approved','agentic-bridge'),'APPLIED'=>__('Applied locally','agentic-bridge'),'VERIFIED'=>__('Verified locally','agentic-bridge'),'REJECTED'=>__('Not approved','agentic-bridge'),'CONFLICT'=>__('Needs attention','agentic-bridge'),'RESTORED'=>__('Restored','agentic-bridge'),default=>__('Unknown','agentic-bridge')};} /** @return array */ private static function fail(string $code,string $message):array{return ['ok'=>false,'code'=>$code,'message'=>$message];} }