has_budget()` at your own item * boundary and return `stopped_on_budget()` with work left rather than * running to completion regardless. A run that overshoots gets killed * mid-delete by the host. * 3. RE-CHECK ACTIVITY IMMEDIATELY BEFORE REMOVING, not when you built the * candidate list. The engine cannot do this for you — only the feature that * produced the data knows what "still in use" means for it. * 4. DO NOT THROW FOR ONE BAD ITEM. Collect per-item failures into * `TaskResult::fail()` so one failure does not abort the whole run. */ interface CleanupTask { /** * Self-description. See {@see TaskDescriptor::from_array()} for the fields * and their validation. * * Translate the label HERE, not at registration: this is called lazily, once * the textdomain is loaded. A `__()` call during `plugins_loaded` returns the * untranslated string forever. * * @return array */ public function descriptor(): array; /** * What this task WOULD remove. MUST NOT mutate anything. * * The engine calls this to populate the confirmation dialog, so it runs on * an ordinary page load — keep it cheap, or report a bounded figure. */ public function estimate( Context $context ): TaskResult; /** * Perform the removal. * * MUST honour `$context->dry_run` by behaving exactly as `estimate()`. The * engine forces that flag on an unactivated site and on an unconfirmed * manual request, and it does not re-check afterwards — a task that ignores * it deletes data the user never approved. */ public function run( Context $context ): TaskResult; }