Skip to content
wiki.fftac.org

Governancemaintenance - Source Excerpt 01

Back to Governancemaintenance

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-workspace/includes/Services/GovernanceMaintenance.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Services/GovernanceMaintenance.php

<?php

namespace SpiralistWorkspace\Services;

use SpiralistWorkspace\Domain\AppealRepository;
use SpiralistWorkspace\Domain\AuditRepository;
use SpiralistWorkspace\Domain\PromptRepository;
use SpiralistWorkspace\Domain\ReputationRepository;
use SpiralistWorkspace\Domain\ReviewRepository;
use SpiralistWorkspace\Support\PromptGovernanceSummary;
use SpiralistWorkspace\Support\PromptMetaRegistry;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Runs scheduled self-operation maintenance for prompt governance state.
 */
class GovernanceMaintenance
{
    public const HOOK = 'spiralist_workspace_governance_maintenance';
    private const LAST_RUN_OPTION = 'spiralist_workspace_governance_maintenance_last_run';
    private const PEER_ENDORSE_QUORUM = 2;
    private const PEER_RESTRICT_QUORUM = 2;
    private const PEER_REVERT_QUORUM = 2;
    private const REPORT_RESTRICTION_QUORUM = 3;
    private const STALE_REPORTED_RESTRICTION_HOURS = 72;
    private const URGENT_REPORTED_RESTRICTION_HOURS = 168;
    private const URGENT_RISK_RESTRICTION_HOURS = 168;
    private const HIGH_RISK_LABELS = [
        'identity_overwrite',
        'dependency_cue',
        'coercive_certainty',
        'privacy_risk',
        'high_stakes_unsuitable',
        'viral_propagation',
    ];

    private PromptRepository $prompts;
    private ReviewRepository $reviews;
    private AppealRepository $appeals;
    private ReputationRepository $reputation;
    private AuditRepository $audit;

    public function __construct(
        PromptRepository $prompts,
        ReviewRepository $reviews,
        AppealRepository $appeals,
        ReputationRepository $reputation,
        AuditRepository $audit
    ) {
        $this->prompts = $prompts;
        $this->reviews = $reviews;
        $this->appeals = $appeals;
        $this->reputation = $reputation;
        $this->audit = $audit;
    }

    /**
     * Ensures the recurring maintenance event exists.
     */
    public function schedule(): void
    {
        if (!wp_next_scheduled(self::HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', self::HOOK);
        }
    }

    /**
     * Clears the recurring maintenance event.
     */
    public static function unschedule(): void
    {
        $timestamp = wp_next_scheduled(self::HOOK);

        while ($timestamp) {
            wp_unschedule_event($timestamp, self::HOOK);
            $timestamp = wp_next_scheduled(self::HOOK);
        }
    }

    /**
     * Returns the last stored maintenance summary.
     *
     * @return array<string,mixed>
     */
    public function status(): array
    {
        $status = get_option(self::LAST_RUN_OPTION, []);

        return is_array($status) ? $status : [];
    }

    /**
     * Recalculates participant state and records current queue health.
     *
     * @return array<string,mixed>
     */
    public function run(int $limit = 250): array
    {
        $limit = max(25, min(500, $limit));
        $prompts = $this->prompts->list(['limit' => $limit], 0);
        $user_ids = [];
        $queue_counts = [
            'provisional_public' => 0,
            'restricted_review' => 0,
            'reverted' => 0,
            'archived' => 0,
            'peer_reviewed' => 0,
            'official_canon' => 0,
            'open_appeals' => 0,
            'stale_review_items' => 0,
            'urgent_review_items' => 0,
            'stale_open_appeals' => 0,
            'automatic_fallback_actions' => 0,
            'auto_promoted_by_quorum' => 0,
            'auto_restricted_by_quorum' => 0,
            'auto_reverted_by_quorum' => 0,
            'auto_archived_by_appeal' => 0,
            'auto_restricted_by_absence' => 0,
        ];
        $stale_review_items = [];
        $urgent_review_items = [];
        $stale_appeal_items = [];
        $automatic_fallback_items = [];

        foreach ($prompts as $prompt) {
            if (!is_array($prompt) || !empty($prompt['soft_deleted_at'])) {
                continue;
            }

            $owner_user_id = (int) ($prompt['owner_user_id'] ?? 0);
            if ($owner_user_id > 0) {
                $user_ids[$owner_user_id] = true;
            }

            if ((string) ($prompt['visibility'] ?? '') !== 'public' || (string) ($prompt['prompt_status'] ?? '') !== 'published') {
                continue;
            }

            $status = PromptMetaRegistry::normalize_moderation_status((string) ($prompt['moderation_status'] ?? 'draft'));
            $prompt_id = (int) ($prompt['id'] ?? 0);
            $appeal_summary = $this->appeals->summary_for_prompt($prompt_id);
            $review_summary = $this->current_review_summary($prompt_id, $appeal_summary);
            $governance_summary = PromptGovernanceSummary::build($prompt, $review_summary, $appeal_summary);
            $automatic_fallback = $this->apply_automatic_fallback($prompt, $review_summary, $appeal_summary, $governance_summary);

            if (!empty($automatic_fallback)) {
                $prompt = (array) ($automatic_fallback['prompt'] ?? $prompt);
                $status = PromptMetaRegistry::normalize_moderation_status((string) ($prompt['moderation_status'] ?? $status));
                $governance_summary = PromptGovernanceSummary::build($prompt, $review_summary, $appeal_summary);
                $queue_counts['automatic_fallback_actions']++;
                $action_kind = (string) ($automatic_fallback['action_kind'] ?? '');
                if (isset($queue_counts[$action_kind])) {
                    $queue_counts[$action_kind]++;
                }
                $automatic_fallback_items[] = $this->automatic_fallback_item($prompt, $automatic_fallback, $governance_summary);
            }

            if (isset($queue_counts[$status])) {
                $queue_counts[$status]++;
            }

            if (in_array($status, ['provisional_public', 'restricted_review', 'reverted'], true)) {
                $queue_age = is_array($governance_summary['queue_age'] ?? null) ? (array) $governance_summary['queue_age'] : [];

                if (!empty($queue_age['is_stale'])) {
                    $queue_counts['stale_review_items']++;
                    $stale_review_items[] = $this->review_queue_item($prompt, $governance_summary);
                }

                if (!empty($queue_age['is_urgent'])) {
                    $queue_counts['urgent_review_items']++;
                    $urgent_review_items[] = $this->review_queue_item($prompt, $governance_summary);
                }

                foreach ($this->reviews->list_for_prompt($prompt_id, 100) as $review) {
                    $reviewer_user_id = (int) ($review['reviewer_user_id'] ?? 0);
                    if ($reviewer_user_id > 0) {
                        $user_ids[$reviewer_user_id] = true;
                    }
                }
            }
        }

        foreach ($this->appeals->list_open($limit) as $appeal) {
            $queue_counts['open_appeals']++;

            $appellant_user_id = (int) ($appeal['appellant_user_id'] ?? 0);
            if ($appellant_user_id > 0) {
                $user_ids[$appellant_user_id] = true;
            }

            $prompt = $this->prompts->get((int) ($appeal['prompt_post_id'] ?? 0), 0);
            $owner_user_id = (int) ($prompt['owner_user_id'] ?? 0);
            if ($owner_user_id > 0) {
                $user_ids[$owner_user_id] = true;
            }

            $appeal_age_hours = $this->hours_since((string) ($appeal['updated_utc'] ?? ($appeal['created_utc'] ?? '')));
            if ($appeal_age_hours >= 72) {
                $queue_counts['stale_open_appeals']++;
                $stale_appeal_items[] = [
                    'appeal_id' => (int) ($appeal['id'] ?? 0),
                    'prompt_id' => (int) ($appeal['prompt_post_id'] ?? 0),
                    'prompt_title' => (string) ($prompt['title'] ?? ''),
                    'prompt_slug' => (string) ($prompt['slug'] ?? ''),
                    'moderation_status' => (string) ($prompt['moderation_status'] ?? ''),
                    'hours_since_activity' => $appeal_age_hours,
                    'duration_label' => $this->duration_label($appeal_age_hours),
                    'updated_utc' => (string) ($appeal['updated_utc'] ?? ''),
                ];
            }
        }

        $refreshed = 0;
        foreach (array_slice(array_keys($user_ids), 0, $limit) as $user_id) {
            $this->reputation->refresh((int) $user_id);
            $refreshed++;
        }

        $summary = [
            'ran_utc' => gmdate('c'),
            'prompt_sample_size' => count($prompts),
            'participants_considered' => count($user_ids),
            'participants_refreshed' => $refreshed,