, omits: array, hidden: bool}> */ private const PLAN_COPY = array( License::PRO => array( // Grows with each Pro feature; pushed to Freemius by AF-19.8. 'description' => 'An AI evaluator that approves routine, reversible content edits your rules send it and holds the rest for you, ledger CSV export for the evidence clients ask for, a digest email when something needs a person, and exposure-scan history showing what changed since the last scan', 'claims' => array( 'ai_evaluator', 'ledger_export', 'scheduled_digest', 'scan_history' ), 'omits' => array(), 'hidden' => false, ), License::AGENCY => array( // Un-hidden by AF-22.5; pushed to Freemius by AF-19.8. 'description' => 'Multisite network control - locked network rules, a network kill switch, network-wide activity and agents created across sites - plus webhook routing that sends chosen events from chosen agents to several destinations, with retries and a delivery log, and starter policy packs for publisher, store and membership sites', 'claims' => array( 'multisite', 'webhook_routing', 'policy_templates' ), 'omits' => array(), 'hidden' => false, ), ); /** * What stays free whatever else is gated (ADR-14 §3), keyed by slug. * * Not feature gates: nothing calls adaf_feature_available() for these. * They are data so a test can hold the promise - no FEATURES key may * reuse one at a paid tier - and so the generated tier docs list them. * * @var array */ private const FREE_PROMISES = array( 'firewall_core' => 'The firewall core: agent identity and token scopes, every interception door, the policy engine, rule builder and dry-run, the rate limiter, the approval queue and drift guard, undo, the hash-chained ledger with retention and WP-CLI, the kill switch, and policy import/export', 'alarms' => 'Every alarm: anomaly email, held-action and notable-denial alerts, the ability-drift digest, and the shadow-automation guard', 'webhook' => 'One webhook destination for alarm events, with its signing secret, address guard and test-send button', 'exposure_scan' => 'The exposure scan and its printable client report, including the agency name on it', 'webhook_filters' => 'The public adaf_webhook_destinations and adaf_webhook_payload filters', ); /** * The feature matrix. * * @var array */ private const FEATURES = array( 'ai_evaluator' => array( 'min_tier' => License::PRO, 'label' => 'AI Evaluator', 'desc' => 'Reviews holds an EVALUATE_AI rule made, through the AI provider configured in WordPress (7.0+): approves routine, reversible content changes and holds everything else for a person; never denies (AF-E21)', 'readme' => 'An AI evaluator that reviews the actions your rules send to it, approves routine content edits a person could undo, and holds everything else for you', 'enforced' => true, 'stripped' => true, ), 'ledger_export' => array( 'min_tier' => License::PRO, 'label' => 'Ledger CSV export', 'desc' => 'Download the filtered ledger as CSV, with chain hashes for verification (AF-20.1)', 'readme' => 'Ledger CSV export, with the chain hashes that prove nothing was altered, for the evidence clients and auditors ask for', 'enforced' => true, 'stripped' => true, ), 'scheduled_digest' => array( 'min_tier' => License::PRO, 'label' => 'Scheduled digest', 'desc' => 'Opt-in daily email when something needs a person, a weekly all-clear otherwise, to the recipients you choose (AF-20.3)', 'readme' => 'A digest email when something needs a person, and a weekly all-clear when nothing does, to the recipients you choose', 'enforced' => true, 'stripped' => true, ), 'scan_history' => array( 'min_tier' => License::PRO, 'label' => 'Exposure-scan history', 'desc' => 'The exposure report shows what is new, resolved and changed since the last scan; the last 50 distinct runs are kept (AF-20.4)', 'readme' => 'Exposure-scan history: each scan shows what is new, resolved and changed since the last one', 'enforced' => true, 'stripped' => true, ), 'multisite' => array( 'min_tier' => License::AGENCY, 'label' => 'Multisite', 'desc' => 'Network-level management on a multisite network: locked network rules, a network kill switch, network activity, and agents created on many sites at once (AF-22.2, ADR-16)', 'readme' => 'Network rules every site must follow, one switch that stops agents across the network, network-wide activity, and agents created on many sites at once, each with its own token', 'enforced' => true, 'stripped' => true, ), 'webhook_routing' => array( 'min_tier' => License::AGENCY, 'label' => 'Webhook routing rules', 'desc' => 'Route events by agent, decision and type to several destinations, with a delivery log (AF-22.3)', 'readme' => 'Webhook routing: send chosen events from chosen agents to several destinations, including Microsoft Teams, with retries and a delivery log', 'enforced' => true, 'stripped' => true, ), 'policy_templates' => array( 'min_tier' => License::AGENCY, 'label' => 'Policy template library', 'desc' => 'Starter policy packs per site shape (AF-22.4)', 'readme' => 'Starter policy packs for publisher, store and membership sites, previewed before they apply and removable as a group', 'enforced' => true, 'stripped' => true, ), ); /** * The full feature matrix. * * @return array */ public static function all(): array { return self::FEATURES; } /** * Copy and visibility per paid plan. * * @return array, omits: array, hidden: bool}> */ public static function plan_copy(): array { return self::PLAN_COPY; } /** * What stays free whatever else is gated, slug => description. * * @return array */ public static function free_promises(): array { return self::FREE_PROMISES; } /** * Minimum tier for a feature, or null if the key is not declared. * * @param string $feature Feature key. */ public static function min_tier( string $feature ): ?string { return self::FEATURES[ $feature ]['min_tier'] ?? null; } /** * Rank of a tier in the ladder. An unknown tier ranks as free. * * @param string $tier Tier identifier. */ public static function tier_rank( string $tier ): int { $rank = array_search( $tier, self::TIER_ORDER, true ); return false === $rank ? 0 : (int) $rank; } }