'FAQ', 'quick_answer' => 'Quick Answer', 'howto' => 'HowTo']; /** @return array */ public static function create(int $post_id, string $type, string $question, string $answer, array $steps, int $actor_id): array { if (!current_user_can(self::CAPABILITY)) return self::fail('denied', 'You do not have permission to propose this record.'); if (!self::eligible($post_id)) return self::fail('not_applicable', 'Only an eligible published post or page can receive a controlled answerable record.'); $type = sanitize_key($type); $question = self::text($question, 300); $answer = self::text($answer, 3000); $steps = self::steps($steps); if (!isset(self::TYPES[$type])) return self::fail('invalid_type', 'Choose FAQ, Quick Answer, or HowTo.'); if ($question === '' || $answer === '') return self::fail('needs_input', 'A human-authored question and answer are required.'); if ($type === 'howto' && count($steps) < 2) return self::fail('needs_input', 'HowTo needs at least two ordered, named steps.'); $current = self::records_for_post($post_id); $record = ['id'=>wp_generate_uuid4(),'siteId'=>get_current_blog_id(),'postId'=>$post_id,'type'=>$type,'question'=>$question,'answer'=>$answer,'steps'=>$steps,'visibility'=>'internal_pending','eligibility'=>'eligible','state'=>'READY_FOR_REVIEW','version'=>1,'fingerprint'=>self::fingerprint($type,$question,$answer,$steps),'baseFingerprint'=>self::fingerprint_records($current),'createdBy'=>$actor_id,'createdAt'=>gmdate('c'),'approval'=>null,'snapshot'=>null,'history'=>[['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_id): array { return self::transition($id, 'READY_FOR_REVIEW', 'APPROVED', $actor_id, 'approved'); } /** @return array */ public static function reject(string $id, int $actor_id): array { return self::transition($id, 'READY_FOR_REVIEW', 'REJECTED', $actor_id, 'rejected'); } /** @return array */ public static function apply(string $id, int $actor_id): array { $record=self::find($id); if (!$record) return self::fail('not_found','The controlled record was not found.'); if (!current_user_can(self::CAPABILITY)) return self::fail('denied','You do not have permission to apply this record.'); if (($record['state']??'')!=='APPROVED') return self::fail('invalid_state','Named approval is required before applying.'); $post=(int)$record['postId']; if (!self::eligible($post)) return self::fail('not_applicable','The target is no longer eligible.'); $current=self::records_for_post($post); if (!hash_equals((string)$record['baseFingerprint'],self::fingerprint_records($current))) return self::update($record,'CONFLICT',$actor_id,'conflict'); $record['snapshot']=$current; $current[] = self::approved_output($record); update_post_meta($post,self::META_KEY,$current); $record['state']='APPLIED'; $record['visibility']='local_approved'; return self::update($record,'APPLIED',$actor_id,'applied'); } /** @return array */ public static function verify(string $id, int $actor_id): array { $record=self::find($id); if (!$record || ($record['state']??'')!=='APPLIED') return self::fail('invalid_state','Apply the approved record before verifying it.'); $found=false; foreach(self::records_for_post((int)$record['postId']) as $item) if (($item['id']??'')===$id && ($item['fingerprint']??'')===$record['fingerprint']) $found=true; return self::update($record,$found?'VERIFIED':'CONFLICT',$actor_id,$found?'verified':'conflict'); } /** @return array */ public static function restore(string $id, int $actor_id): array { $record=self::find($id); if (!$record || !in_array($record['state']??'', ['APPLIED','VERIFIED'],true)) return self::fail('invalid_state','Only an applied local record can be restored.'); if (!current_user_can(self::CAPABILITY)) return self::fail('denied','You do not have permission to restore this record.'); update_post_meta((int)$record['postId'],self::META_KEY,is_array($record['snapshot'])?$record['snapshot']:[]); return self::update($record,'RESTORED',$actor_id,'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 records_for_post(int $id): array { $v=get_post_meta($id,self::META_KEY,true); return is_array($v)?array_values($v):[]; } /** @return list> */ public static function schema_preview(int $post_id): array { $out=[]; foreach(self::records_for_post($post_id) as $r) { if (($r['state']??'')==='APPROVED' && self::eligible($post_id)) $out[]=self::schema($r); } return $out; } public static function is_eligible(int $post_id): bool { return self::eligible($post_id); } /** * Render a native post-editor view over the same local record store used * by the Content workspace. No record is created or changed here without * the existing capability + nonce guarded admin-post handler. * * @param WP_Post $post */ public static function render_editor_surface($post): void { if (!$post instanceof WP_Post) return; if (!self::eligible((int) $post->ID)) { echo '

' . esc_html__('Not applicable: publish this non-password-protected page or post before creating a controlled record.', 'agentic-bridge') . '

'; return; } $records = array_values(array_filter(self::all(), static fn($record): bool => (int) ($record['postId'] ?? 0) === (int) $post->ID)); $preview = self::schema_preview((int) $post->ID); echo '

' . esc_html__('Add only human-authored FAQ, Quick Answer, or HowTo records. Nothing is generated, published, or written to an SEO plugin.', 'agentic-bridge') . '

'; echo '
'; wp_nonce_field('thedotdev_agentic_bridge_controlled_answer'); echo ''; echo '

'; echo '

'; echo '

'; echo '

'; echo '

'; echo '

' . esc_html__('Lifecycle', 'agentic-bridge') . '

'; if ($records === []) { echo '

' . esc_html__('No controlled records yet.', 'agentic-bridge') . '

'; } foreach ($records as $record) self::render_editor_record($record); echo '

' . esc_html__('Schema preview', 'agentic-bridge') . '

'; if ($preview === []) { echo '

' . esc_html__('No approved local records are available for preview.', 'agentic-bridge') . '

'; return; } echo '
' . esc_html((string) wp_json_encode($preview, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '
'; } /** @param array $record */ private static function render_editor_record(array $record): void { $id = (string) ($record['id'] ?? ''); $state = (string) ($record['state'] ?? 'UNKNOWN'); echo '

' . esc_html((string) ($record['question'] ?? '')) . '
' . esc_html__('State:', 'agentic-bridge') . ' ' . esc_html(self::human_state($state)) . '

'; $actions = match ($state) { 'READY_FOR_REVIEW' => ['approve' => __('Approve', 'agentic-bridge'), 'reject' => __('Reject', 'agentic-bridge')], 'APPROVED' => ['apply' => __('Apply approved record', 'agentic-bridge')], 'APPLIED' => ['verify' => __('Verify applied record', 'agentic-bridge'), 'restore' => __('Restore previous local records', 'agentic-bridge')], 'VERIFIED' => ['restore' => __('Restore previous local records', 'agentic-bridge')], default => [], }; foreach ($actions as $operation => $label) { echo '
'; wp_nonce_field('thedotdev_agentic_bridge_controlled_answer'); echo '
'; } echo '
'; } private static function human_state(string $state): string { return match ($state) { '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|null */ private static function find(string $id): ?array { foreach(self::all() as $r) if (($r['id']??'')===$id && (int)($r['siteId']??0)===get_current_blog_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 controlled record 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['history'][]=['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===''; } private static function text(string $v,int $max): string { $v=trim(wp_strip_all_tags($v)); return strlen($v)<=$max?$v:''; } /** @return list */ private static function steps(array $steps): array { $out=[]; foreach($steps as $s){$s=self::text((string)$s,500);if($s!=='')$out[]=$s;} return array_values(array_slice($out,0,20)); } private static function fingerprint(string $t,string $q,string $a,array $s): string { return hash('sha256',wp_json_encode([$t,$q,$a,$s])); } private static function fingerprint_records(array $v): string { return hash('sha256',wp_json_encode($v)); } /** @return array */ private static function approved_output(array $r): array { return ['id'=>$r['id'],'type'=>$r['type'],'question'=>$r['question'],'answer'=>$r['answer'],'steps'=>$r['steps'],'fingerprint'=>$r['fingerprint'],'state'=>'APPROVED']; } /** @return array */ private static function schema(array $r): array { return match($r['type']) { 'faq'=>['@type'=>'FAQPage','mainEntity'=>[['@type'=>'Question','name'=>$r['question'],'acceptedAnswer'=>['@type'=>'Answer','text'=>$r['answer']]]]], 'quick_answer'=>['@type'=>'Question','name'=>$r['question'],'acceptedAnswer'=>['@type'=>'Answer','text'=>$r['answer']]], default=>['@type'=>'HowTo','name'=>$r['question'],'step'=>array_map(static fn($s)=>['@type'=>'HowToStep','name'=>$s],$r['steps'])] }; } /** @return array */ private static function fail(string $code,string $message): array{return ['ok'=>false,'code'=>$code,'message'=>$message];} }