diff --git a/inc/Abilities/WorkspaceAbilities.php b/inc/Abilities/WorkspaceAbilities.php index 26958580..d36253a8 100644 --- a/inc/Abilities/WorkspaceAbilities.php +++ b/inc/Abilities/WorkspaceAbilities.php @@ -4689,9 +4689,9 @@ public static function worktreeAbandonedCleanup( array $input ): array|\WP_Error public static function worktreeCleanupArtifacts( array $input ): array|\WP_Error { $workspace = new Workspace(); $opts = array( - 'dry_run' => ! empty($input['dry_run']), - 'force' => ! empty($input['force']), - 'allow_active_artifact_cleanup' => ! empty($input['allow_active_artifact_cleanup']), + 'dry_run' => ! empty($input['dry_run']), + 'force' => ! empty($input['force']), + 'allow_active_artifact_cleanup' => ! empty($input['allow_active_artifact_cleanup']), 'allow_unavailable_process_probe' => ! empty($input['allow_unavailable_process_probe']), ); if ( isset($input['apply_plan']) && is_array($input['apply_plan']) ) { diff --git a/inc/Cleanup/CleanupRemainingWorkSummary.php b/inc/Cleanup/CleanupRemainingWorkSummary.php index c0abfa47..7b362fb7 100644 --- a/inc/Cleanup/CleanupRemainingWorkSummary.php +++ b/inc/Cleanup/CleanupRemainingWorkSummary.php @@ -47,7 +47,7 @@ public static function from_items( array $items ): array { } if ( 'artifact_cleanup' === $type && 'applied' !== $status ) { - $remaining = (array) ( $evidence['partial']['remaining_artifacts'] ?? array() ); + $remaining = (array) ( $evidence['partial']['remaining_artifacts'] ?? array() ); $summary['remaining_reclaimable_artifact_bytes'] += 'partial' === $status ? array_sum(array_map(fn( $artifact ) => max(0, (int) ( $artifact['size_bytes'] ?? 0 )), $remaining)) : self::row_bytes($row, array( 'artifact_size_bytes', 'size_bytes' )); diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index b691937f..575acd42 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -4325,9 +4325,9 @@ public function worktree( array $args, array $assoc_args ): void { break; case 'cleanup-artifacts': - $input['dry_run'] = ! empty($assoc_args['dry-run']); - $input['force'] = ! empty($assoc_args['force']); - $input['allow_active_artifact_cleanup'] = ! empty($assoc_args['allow-active-artifact-cleanup']); + $input['dry_run'] = ! empty($assoc_args['dry-run']); + $input['force'] = ! empty($assoc_args['force']); + $input['allow_active_artifact_cleanup'] = ! empty($assoc_args['allow-active-artifact-cleanup']); $input['allow_unavailable_process_probe'] = ! empty($assoc_args['allow-unavailable-process-probe']); if ( isset($assoc_args['limit']) ) { $input['limit'] = (int) $assoc_args['limit']; diff --git a/inc/Storage/SqliteBusyRetry.php b/inc/Storage/SqliteBusyRetry.php index 75210d55..cadca3e1 100644 --- a/inc/Storage/SqliteBusyRetry.php +++ b/inc/Storage/SqliteBusyRetry.php @@ -11,9 +11,9 @@ final class SqliteBusyRetry { - private const DEFAULT_MAX_WAIT_MS = 1000; + private const DEFAULT_MAX_WAIT_MS = 1000; private const DEFAULT_INITIAL_WAIT_MS = 25; - private const DEFAULT_MAX_DELAY_MS = 250; + private const DEFAULT_MAX_DELAY_MS = 250; /** * Retry only a SQLite write which reports a transient busy/locked failure. @@ -44,10 +44,11 @@ public static function run( string $operation_name, callable $operation ): mixed throw $error; } $busy_message = $error->getMessage(); - $result = false; + $result = false; } - if ( false !== $result || ! self::is_busy_error($busy_message ?: (string) ( $wpdb->last_error ?? '' )) ) { + $last_error = '' !== $busy_message ? $busy_message : (string) ( $wpdb->last_error ?? '' ); + if ( false !== $result || ! self::is_busy_error($last_error) ) { return $result; } @@ -72,7 +73,7 @@ public static function run( string $operation_name, callable $operation ): mixed $delay_ms = min($max_delay_ms, $initial_wait_ms * ( 2 ** ( $attempts - 1 ) )); // Spread competing CLI processes without extending the configured budget. $jitter_ms = $delay_ms > 1 ? random_int(0, max(1, (int) floor($delay_ms / 4))) : 0; - usleep((int) min($delay_ms + $jitter_ms, max(1, $max_wait_ms - $elapsed_ms)) * 1000); + usleep( (int) min($delay_ms + $jitter_ms, max(1, $max_wait_ms - $elapsed_ms)) * 1000); } while ( true ); } @@ -84,10 +85,11 @@ public static function is_sqlite( mixed $database ): bool { return false; } - $signals = array(get_class($database)); + $signals = array( get_class($database) ); if ( method_exists($database, 'db_server_info') ) { try { $signals[] = (string) $database->db_server_info(); + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- A failed optional capability probe is intentionally ignored. } catch ( \Throwable ) { // A failed capability probe must not alter normal database behavior. } @@ -112,8 +114,8 @@ private static function is_busy_error( string $message ): bool { return str_contains($message, 'database is locked') || str_contains($message, 'database is busy') || str_contains($message, 'sqlite_busy') || str_contains($message, 'sqlite_locked'); } - private static function filtered_positive_int( string $hook, int $default ): int { - $value = function_exists('apply_filters') ? (int) apply_filters($hook, $default) : $default; + private static function filtered_positive_int( string $hook, int $default_value ): int { + $value = function_exists('apply_filters') ? (int) apply_filters($hook, $default_value) : $default_value; return max(1, $value); } } diff --git a/inc/Storage/WorktreeInventoryRepository.php b/inc/Storage/WorktreeInventoryRepository.php index 1283866b..efe5ceaf 100644 --- a/inc/Storage/WorktreeInventoryRepository.php +++ b/inc/Storage/WorktreeInventoryRepository.php @@ -173,7 +173,7 @@ public function last_error(): ?\WP_Error { private function mutation_failure_error( string $operation ): \WP_Error { global $wpdb; - $database_error = trim((string) ( $wpdb->last_error ?? '' )); + $database_error = trim( (string) ( $wpdb->last_error ?? '' )); $database_error = SecretRedactor::redact($database_error); $database_error = substr($database_error, 0, 512); @@ -232,7 +232,7 @@ public function mark_missing( string $handle ): bool { return false; } - $data = array( + $data = array( 'missing_path' => 1, 'last_probe_at' => current_time('mysql', true), 'last_probe_status' => 'missing_path', diff --git a/inc/Support/ProcessPathProbe.php b/inc/Support/ProcessPathProbe.php index c46e70cc..ae55efe3 100644 --- a/inc/Support/ProcessPathProbe.php +++ b/inc/Support/ProcessPathProbe.php @@ -1,4 +1,5 @@ scanner)(); + return ( $this->scanner )(); } } @@ -40,7 +41,11 @@ public function snapshot(): array { return array( 'status' => 'unsupported', 'records' => array(), - 'diagnostics' => array( 'reason' => 'process_path_probe_unsupported', 'platform' => $this->platform, 'remediation' => 'Use a supported host process-path provider before artifact cleanup.' ), + 'diagnostics' => array( + 'reason' => 'process_path_probe_unsupported', + 'platform' => $this->platform, + 'remediation' => 'Use a supported host process-path provider before artifact cleanup.', + ), ); } } @@ -53,17 +58,25 @@ public function __construct(private $runner = null) {} public function snapshot(): array { $argv = array( 'lsof', '-n', '-P', '-Fpcfn0' ); if ( is_callable($this->runner) ) { - $result = ($this->runner)($argv); + $result = ( $this->runner )($argv); } else { $command = CommandSpec::from_argv($argv); - $result = $command instanceof \WP_Error ? $command : ProcessRunner::run($command, array( 'timeout_seconds' => 2, 'output_cap_bytes' => 1048576, 'error_as_result' => true )); + $result = $command instanceof \WP_Error ? $command : ProcessRunner::run($command, array( + 'timeout_seconds' => 2, + 'output_cap_bytes' => 1048576, + 'error_as_result' => true, + )); } if ( $result instanceof \WP_Error || ! is_array($result) || empty($result['success']) ) { $data = $result instanceof \WP_Error ? (array) $result->get_error_data() : (array) $result; return array( 'status' => 'uncertain', 'records' => array(), - 'diagnostics' => array( 'reason' => ! empty($data['timeout']) ? 'process_path_probe_timeout' : 'process_path_probe_failed', 'provider' => 'lsof', 'details' => $data ), + 'diagnostics' => array( + 'reason' => ! empty($data['timeout']) ? 'process_path_probe_timeout' : 'process_path_probe_failed', + 'provider' => 'lsof', + 'details' => $data, + ), ); } @@ -71,7 +84,7 @@ public function snapshot(): array { $pid = 0; $command = ''; $fd = ''; - foreach ( explode("\0", (string) ($result['output'] ?? '')) as $field ) { + foreach ( explode("\0", (string) ( $result['output'] ?? '' )) as $field ) { $field = ltrim($field, "\n"); if ( '' === $field ) { continue; @@ -97,7 +110,10 @@ public function snapshot(): array { return array( 'status' => 'available', 'records' => $records, - 'diagnostics' => array( 'provider' => 'lsof', 'path_records' => count($records) ), + 'diagnostics' => array( + 'provider' => 'lsof', + 'path_records' => count($records), + ), ); } } diff --git a/inc/Support/ProcessRunner.php b/inc/Support/ProcessRunner.php index 758b3c6e..556766c2 100644 --- a/inc/Support/ProcessRunner.php +++ b/inc/Support/ProcessRunner.php @@ -94,6 +94,7 @@ private static function run_via_exec( string $command, array $options, int $outp } /** + * @param string|list $command * @param array $options * @param callable|null $on_output * @param array|null $env @@ -146,7 +147,7 @@ private static function run_via_proc_open( string|array $command, array $options } if ( null !== $deadline && microtime(true) >= $deadline ) { - $remaining = self::terminate_timed_out_process($process, $pipes, $output, $stdout, $stderr, (int) ( $status['pid'] ?? 0 ), $uses_process_group); + $remaining = self::terminate_timed_out_process($process, $pipes, $output, $stdout, $stderr, (int) $status['pid'], $uses_process_group); return self::error( $options, sprintf('Process command timed out after %d second(s).', $timeout_seconds), @@ -250,10 +251,10 @@ private static function terminate_timed_out_process( $process, array $pipes, str if ( self::is_windows() && $pid > 0 ) { // taskkill's /T removes descendants that inherited the process pipes. $taskkill = self::terminate_windows_process_tree($pid); - $cleanup = array( - 'containment' => 'windows_process_tree', - 'verified' => $taskkill['success'], - 'attempts' => 1, + $cleanup = array( + 'containment' => 'windows_process_tree', + 'verified' => $taskkill['success'], + 'attempts' => 1, 'taskkill_exit_code' => $taskkill['exit_code'], ); } elseif ( $uses_process_group && $pid > 0 && function_exists('posix_kill') ) { @@ -296,9 +297,9 @@ private static function terminate_timed_out_process( $process, array $pipes, str proc_close($process); return array( - 'output' => $output, - 'stdout' => $stdout, - 'stderr' => $stderr, + 'output' => $output, + 'stdout' => $stdout, + 'stderr' => $stderr, 'cleanup' => $cleanup, ); } @@ -307,19 +308,28 @@ private static function terminate_timed_out_process( $process, array $pipes, str * Put timed POSIX commands in a session of their own so descendants cannot * keep the command's pipes alive after the timeout owner exits. * - * @param string|array $command Command to execute. - * @return array{command: string|array, uses_process_group: bool} + * @param string|list $command Command to execute. + * @return array{command: string|list, uses_process_group: bool} */ private static function command_with_process_group( string|array $command, int $timeout_seconds ): array { if ( $timeout_seconds <= 0 || self::is_windows() || null === self::setsid_command() ) { - return array( 'command' => $command, 'uses_process_group' => false ); + return array( + 'command' => $command, + 'uses_process_group' => false, + ); } if ( is_array($command) ) { - return array( 'command' => array_merge(array( self::setsid_command() ), $command), 'uses_process_group' => true ); + return array( + 'command' => array_merge(array( self::setsid_command() ), $command), + 'uses_process_group' => true, + ); } - return array( 'command' => array( self::setsid_command(), 'sh', '-c', $command ), 'uses_process_group' => true ); + return array( + 'command' => array( self::setsid_command(), 'sh', '-c', $command ), + 'uses_process_group' => true, + ); } private static function setsid_command(): ?string { @@ -344,7 +354,7 @@ private static function terminate_windows_process_tree( int $pid ): array { $output = array(); $exit_code = 1; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec - @exec(sprintf('taskkill /PID %d /T /F', $pid), $output, $exit_code); + exec(sprintf('taskkill /PID %d /T /F', $pid), $output, $exit_code); return array( 'success' => 0 === $exit_code, diff --git a/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php b/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php index dc04b780..0a06c46c 100644 --- a/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php +++ b/inc/Tasks/WorkspaceDiskEmergencyCleanupTask.php @@ -16,9 +16,9 @@ class WorkspaceDiskEmergencyCleanupTask extends SystemTask { - private const MAX_DURABLE_REASONS = 10; + private const MAX_DURABLE_REASONS = 10; private const MAX_DURABLE_REASON_BYTES = 120; - private const MAX_DURABLE_JOB_IDS = 25; + private const MAX_DURABLE_JOB_IDS = 25; /** * PluginSettings key that gates threshold-triggered emergency cleanup. */ @@ -95,6 +95,22 @@ public function executeTask( int $jobId, array $params ): void { $workspace = new Workspace(); $result = $workspace->workspace_disk_emergency_cleanup(array_merge($opts, array( 'dry_run' => true ))); + if ( $result instanceof \WP_Error ) { + do_action( + 'datamachine_log', + 'error', + 'Workspace disk emergency cleanup failed', + array( + 'task' => $this->getTaskType(), + 'jobId' => $jobId, + 'error' => $result->get_error_message(), + 'code' => $result->get_error_code(), + ) + ); + $this->failJob($jobId, $result->get_error_message()); + return; + } + if ( ! empty($result['triggered']) && empty($opts['dry_run']) && ! empty($result['selected_artifact_count']) ) { $result = $this->schedule_artifact_cleanup_chunks($jobId, $result, $params); } @@ -115,9 +131,9 @@ public function executeTask( int $jobId, array $params ): void { return; } - $budget = (array) ( $result['disk_budget'] ?? array() ); - $summary = (array) ( $result['scheduled_summary'] ?? array() ); - $message = ! empty($result['triggered']) + $budget = (array) ( $result['disk_budget'] ?? array() ); + $summary = (array) ( $result['scheduled_summary'] ?? array() ); + $message = ! empty($result['triggered']) ? sprintf( 'Workspace disk emergency cleanup triggered (%s): scheduled %d artifact chunk(s) covering %d artifact row(s); action_required=%s.', implode(',', (array) ( $budget['trigger_reasons'] ?? array() )), @@ -153,34 +169,34 @@ public function executeTask( int $jobId, array $params ): void { * @return array */ private function compact_durable_report( array $result ): array { - $budget = (array) ( $result['disk_budget'] ?? array() ); - $summary = (array) ( $result['scheduled_summary'] ?? array() ); + $budget = (array) ( $result['disk_budget'] ?? array() ); + $summary = (array) ( $result['scheduled_summary'] ?? array() ); $selection = (array) ( $result['inode_recovery_plan'] ?? array() ); - $capacity = (array) ( $result['capacity_evidence'] ?? array() ); + $capacity = (array) ( $result['capacity_evidence'] ?? array() ); return array( - 'success' => ! empty($result['success']), - 'triggered' => ! empty($result['triggered']), - 'skipped' => ! empty($result['skipped']), - 'dry_run' => ! empty($result['dry_run']), - 'generated_at' => (string) ( $result['generated_at'] ?? '' ), - 'trigger_reasons' => $this->compact_reasons($budget['trigger_reasons'] ?? array()), - 'selected_artifact_count' => (int) ( $result['selected_artifact_count'] ?? 0 ), - 'selected_worktree_count' => (int) ( $result['selected_worktree_count'] ?? 0 ), - 'scheduled' => array( - 'chunks' => (int) ( $summary['scheduled_chunks'] ?? 0 ), + 'success' => ! empty($result['success']), + 'triggered' => ! empty($result['triggered']), + 'skipped' => ! empty($result['skipped']), + 'dry_run' => ! empty($result['dry_run']), + 'generated_at' => (string) ( $result['generated_at'] ?? '' ), + 'trigger_reasons' => $this->compact_reasons($budget['trigger_reasons'] ?? array()), + 'selected_artifact_count' => (int) ( $result['selected_artifact_count'] ?? 0 ), + 'selected_worktree_count' => (int) ( $result['selected_worktree_count'] ?? 0 ), + 'scheduled' => array( + 'chunks' => (int) ( $summary['scheduled_chunks'] ?? 0 ), 'artifact_rows' => (int) ( $summary['scheduled_artifact_rows'] ?? 0 ), 'batch_job_id' => (int) ( $summary['batch_job_id'] ?? 0 ), 'direct_job_ids' => array_slice(array_map('intval', (array) ( $summary['direct_job_ids'] ?? array() )), 0, self::MAX_DURABLE_JOB_IDS), ), - 'measured_recovery' => array( - 'target_bytes' => (int) ( $budget['target_recovery_bytes'] ?? 0 ), - 'target_inodes' => (int) ( $budget['target_recovery_inodes'] ?? 0 ), - 'planned_bytes' => (int) ( $selection['planned_measured_recovery_bytes'] ?? 0 ), - 'planned_inodes' => (int) ( $selection['planned_measured_recovery_inodes'] ?? 0 ), - 'target_met' => ! empty($selection['target_met']), - 'reclaimed_bytes' => is_numeric($capacity['reclaimed_bytes'] ?? null) ? (int) $capacity['reclaimed_bytes'] : null, - 'reclaimed_inodes' => is_numeric($capacity['reclaimed_inodes'] ?? null) ? (int) $capacity['reclaimed_inodes'] : null, + 'measured_recovery' => array( + 'target_bytes' => (int) ( $budget['target_recovery_bytes'] ?? 0 ), + 'target_inodes' => (int) ( $budget['target_recovery_inodes'] ?? 0 ), + 'planned_bytes' => (int) ( $selection['planned_measured_recovery_bytes'] ?? 0 ), + 'planned_inodes' => (int) ( $selection['planned_measured_recovery_inodes'] ?? 0 ), + 'target_met' => ! empty($selection['target_met']), + 'reclaimed_bytes' => is_numeric($capacity['reclaimed_bytes'] ?? null) ? (int) $capacity['reclaimed_bytes'] : null, + 'reclaimed_inodes' => is_numeric($capacity['reclaimed_inodes'] ?? null) ? (int) $capacity['reclaimed_inodes'] : null, ), 'action_required' => ! empty($result['action_required']), 'action_required_reasons' => $this->compact_reasons($result['action_required_reasons'] ?? array()), @@ -194,8 +210,8 @@ private function compact_durable_report( array $result ): array { */ private function compact_reasons( mixed $reasons ): array { $compact = array(); - foreach ( array_slice((array) $reasons, 0, self::MAX_DURABLE_REASONS) as $reason ) { - $compact[] = substr((string) $reason, 0, self::MAX_DURABLE_REASON_BYTES); + foreach ( array_slice( (array) $reasons, 0, self::MAX_DURABLE_REASONS) as $reason ) { + $compact[] = substr( (string) $reason, 0, self::MAX_DURABLE_REASON_BYTES); } return $compact; } @@ -205,7 +221,7 @@ private function compact_reasons( mixed $reasons ): array { * @return array */ private function compact_capacity_evidence( array $capacity ): array { - $fields = array( 'filesystem_total_bytes', 'filesystem_free_bytes', 'filesystem_free_inodes', 'workspace_allocated_bytes' ); + $fields = array( 'filesystem_total_bytes', 'filesystem_free_bytes', 'filesystem_free_inodes', 'workspace_allocated_bytes' ); $compact = array( 'reclaimed_bytes' => is_numeric($capacity['reclaimed_bytes'] ?? null) ? (int) $capacity['reclaimed_bytes'] : null, 'reclaimed_inodes' => is_numeric($capacity['reclaimed_inodes'] ?? null) ? (int) $capacity['reclaimed_inodes'] : null, diff --git a/inc/Tasks/WorkspaceSafeCleanupTask.php b/inc/Tasks/WorkspaceSafeCleanupTask.php index c6961167..a84a8a4b 100644 --- a/inc/Tasks/WorkspaceSafeCleanupTask.php +++ b/inc/Tasks/WorkspaceSafeCleanupTask.php @@ -78,7 +78,7 @@ public function executeTask( int $jobId, array $params ): void { return; } - $input = array( + $input = array( 'source' => (string) ( $params['source'] ?? 'system_task' ), 'limit' => isset($params['limit']) ? (int) $params['limit'] : 25, 'passes' => isset($params['passes']) ? (int) $params['passes'] : 5, @@ -91,12 +91,21 @@ public function executeTask( int $jobId, array $params ): void { $result = $this->run_safe_cleanup($input); if ( $result instanceof \WP_Error ) { - do_action('datamachine_log', 'error', 'Safe workspace cleanup failed', array( 'task' => $this->getTaskType(), 'jobId' => $jobId, 'error' => $result->get_error_message(), 'code' => $result->get_error_code() )); + do_action('datamachine_log', 'error', 'Safe workspace cleanup failed', array( + 'task' => $this->getTaskType(), + 'jobId' => $jobId, + 'error' => $result->get_error_message(), + 'code' => $result->get_error_code(), + )); $this->failJob($jobId, $result->get_error_message()); return; } - do_action('datamachine_log', 'info', 'Safe workspace cleanup completed.', array( 'task' => $this->getTaskType(), 'jobId' => $jobId, 'result' => $result )); + do_action('datamachine_log', 'info', 'Safe workspace cleanup completed.', array( + 'task' => $this->getTaskType(), + 'jobId' => $jobId, + 'result' => $result, + )); $this->completeJob($jobId, $result); } diff --git a/inc/Workspace/CleanupRunService.php b/inc/Workspace/CleanupRunService.php index bbdf0dca..56d9a77e 100644 --- a/inc/Workspace/CleanupRunService.php +++ b/inc/Workspace/CleanupRunService.php @@ -111,8 +111,8 @@ public function apply( string $run_id, array $opts = array() ): array|\WP_Error return new \WP_Error('cleanup_run_not_found', sprintf('Cleanup run not found: %s', $run_id), array( 'status' => 404 )); } - $limit = $this->apply_limit($opts); - $policy = (array) ( $run['policy'] ?? array() ); + $limit = $this->apply_limit($opts); + $policy = (array) ( $run['policy'] ?? array() ); $allow_active_cleanup = ! empty($policy['allow_active_artifact_cleanup']); $requested_active = ! empty($opts['allow_active_artifact_cleanup']); if ( $allow_active_cleanup !== $requested_active ) { @@ -159,12 +159,12 @@ public function apply( string $run_id, array $opts = array() ): array|\WP_Error } $results['artifact_cleanup'] = $this->workspace->worktree_cleanup_artifacts( array( - 'apply_plan' => array( 'candidates' => array_map(fn( $item ) => $item['evidence'], $artifact_batch) ), + 'apply_plan' => array( 'candidates' => array_map(fn( $item ) => $item['evidence'], $artifact_batch) ), // The reviewed run policy can force only reconstructable artifact deletion. - 'force' => $force_artifact_cleanup, + 'force' => $force_artifact_cleanup, 'allow_active_artifact_cleanup' => $allow_active_cleanup, - 'older_than' => (string) ( $policy['artifact_age_filter']['older_than'] ?? '' ), - 'limit' => count($artifact_batch), + 'older_than' => (string) ( $policy['artifact_age_filter']['older_than'] ?? '' ), + 'limit' => count($artifact_batch), ) ); if ( $results['artifact_cleanup'] instanceof \WP_Error ) { @@ -413,13 +413,13 @@ public function until_empty( array $opts = array() ): array|\WP_Error { $plan = $this->plan( array( - 'mode' => 'artifacts', - 'include_artifacts' => true, - 'include_worktrees' => false, - 'include_resolvers' => false, - 'force_artifact_cleanup' => ! empty($opts['force']), + 'mode' => 'artifacts', + 'include_artifacts' => true, + 'include_worktrees' => false, + 'include_resolvers' => false, + 'force_artifact_cleanup' => ! empty($opts['force']), 'allow_active_artifact_cleanup' => ! empty($opts['allow_active_artifact_cleanup']), - 'worktree_older_than' => isset($opts['older_than']) ? trim( (string) $opts['older_than']) : '', + 'worktree_older_than' => isset($opts['older_than']) ? trim( (string) $opts['older_than']) : '', ) ); if ( $plan instanceof \WP_Error ) { @@ -456,9 +456,9 @@ public function until_empty( array $opts = array() ): array|\WP_Error { $apply = $this->apply( $run_id, array( - 'force' => ! empty($opts['force']), + 'force' => ! empty($opts['force']), 'allow_active_artifact_cleanup' => ! empty($opts['allow_active_artifact_cleanup']), - 'limit' => $limit, + 'limit' => $limit, ) ); if ( $apply instanceof \WP_Error ) { diff --git a/inc/Workspace/RemoteWorkspaceBackend.php b/inc/Workspace/RemoteWorkspaceBackend.php index 9e26ee04..8fbb7227 100644 --- a/inc/Workspace/RemoteWorkspaceBackend.php +++ b/inc/Workspace/RemoteWorkspaceBackend.php @@ -634,8 +634,8 @@ public function materialization_context( string $handle ): array|\WP_Error { $state = $this->state(); $repo_name = (string) ( $context['repo_name'] ?? '' ); - $repo = (string) ( $context['repo'] ?? '' ); - $url = (string) ( $state['repos'][ $repo_name ]['url'] ?? GitHubRemote::cloneUrl($repo) ); + $repo = (string) ( $context['repo'] ?? '' ); + $url = (string) ( $state['repos'][ $repo_name ]['url'] ?? GitHubRemote::cloneUrl($repo) ); return array( 'handle' => (string) ( $context['handle'] ?? $handle ), diff --git a/inc/Workspace/WorkspaceArtifactCleanup.php b/inc/Workspace/WorkspaceArtifactCleanup.php index 0a7ad47c..40e826f2 100644 --- a/inc/Workspace/WorkspaceArtifactCleanup.php +++ b/inc/Workspace/WorkspaceArtifactCleanup.php @@ -46,21 +46,21 @@ trait WorkspaceArtifactCleanup { * @return array|\WP_Error */ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_Error { - $dry_run = ! empty($opts['dry_run']); - $force = ! empty($opts['force']); - $allow_active = ! empty($opts['allow_active_artifact_cleanup']); + $dry_run = ! empty($opts['dry_run']); + $force = ! empty($opts['force']); + $allow_active = ! empty($opts['allow_active_artifact_cleanup']); $allow_unavailable_process_probe = ! empty($opts['allow_unavailable_process_probe']); - $apply_plan = isset($opts['apply_plan']) && is_array($opts['apply_plan']) ? $opts['apply_plan'] : null; - $older_than = isset($opts['older_than']) ? trim( (string) $opts['older_than']) : ''; + $apply_plan = isset($opts['apply_plan']) && is_array($opts['apply_plan']) ? $opts['apply_plan'] : null; + $older_than = isset($opts['older_than']) ? trim( (string) $opts['older_than']) : ''; if ( '' === $older_than && null !== $apply_plan && is_array($apply_plan['age_filter'] ?? null) ) { $older_than = trim( (string) ( $apply_plan['age_filter']['older_than'] ?? '' )); } $opts['older_than'] = $older_than; - $exhaustive = ! empty($opts['exhaustive']); - $full_workspace = ! empty($opts['full_workspace']); - $sort = isset($opts['sort']) ? strtolower(trim( (string) $opts['sort'])) : ''; - $limit = isset($opts['limit']) ? (int) $opts['limit'] : self::ARTIFACT_CLEANUP_DEFAULT_LIMIT; - $offset = isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0; + $exhaustive = ! empty($opts['exhaustive']); + $full_workspace = ! empty($opts['full_workspace']); + $sort = isset($opts['sort']) ? strtolower(trim( (string) $opts['sort'])) : ''; + $limit = isset($opts['limit']) ? (int) $opts['limit'] : self::ARTIFACT_CLEANUP_DEFAULT_LIMIT; + $offset = isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0; if ( $limit < 0 ) { return new \WP_Error('invalid_artifact_cleanup_limit', 'Artifact cleanup --limit must be greater than 0. Use --exhaustive for an unbounded full artifact audit.', array( 'status' => 400 )); } @@ -113,13 +113,13 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E $plan = $this->build_worktree_artifact_cleanup_plan( $force, array( - 'allow_active_artifact_cleanup' => $allow_active, + 'allow_active_artifact_cleanup' => $allow_active, 'allow_unavailable_process_probe' => $allow_unavailable_process_probe, - 'limit' => $plan_limit, - 'offset' => $rank_by_size ? 0 : $offset, - 'only_handles' => $only_handles, - 'safety_probes' => $safety_probes, - 'older_than' => $older_than, + 'limit' => $plan_limit, + 'offset' => $rank_by_size ? 0 : $offset, + 'only_handles' => $only_handles, + 'safety_probes' => $safety_probes, + 'older_than' => $older_than, ) ); if ( $plan instanceof \WP_Error ) { @@ -204,7 +204,7 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E $partial = array(); foreach ( $candidates as $candidate ) { $removed_artifacts = array(); - $artifacts = array_values((array) ( $candidate['artifacts'] ?? array() )); + $artifacts = array_values( (array) ( $candidate['artifacts'] ?? array() )); $failed = false; $process_guard = $this->active_artifact_process_protection( (string) ( $candidate['path'] ?? '' ), $artifacts, true); if ( null !== $process_guard ) { @@ -367,19 +367,19 @@ private function build_artifact_cleanup_preview_command( array $opts ): string { * @param bool $force Whether to allow dirty/unpushed worktrees. * @param array $opts Options: `limit` (0 = unbounded internal exhaustive mode), `offset`, * `only_handles` (array|null), `safety_probes`, `older_than`. - * @return array{candidates: array, skipped: array, pagination: ?array}|\WP_Error + * @return array{candidates: array, skipped: array, pagination: ?array, age_filter: ?array}|\WP_Error */ private function build_worktree_artifact_cleanup_plan( bool $force, array $opts = array() ): array|\WP_Error { - $limit = isset($opts['limit']) ? (int) $opts['limit'] : 0; - $offset = isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0; - $allow_active = ! empty($opts['allow_active_artifact_cleanup']); + $limit = isset($opts['limit']) ? (int) $opts['limit'] : 0; + $offset = isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0; + $allow_active = ! empty($opts['allow_active_artifact_cleanup']); $allow_unavailable_process_probe = ! empty($opts['allow_unavailable_process_probe']); - $only_handles = isset($opts['only_handles']) && is_array($opts['only_handles']) + $only_handles = isset($opts['only_handles']) && is_array($opts['only_handles']) ? array_values(array_filter(array_map('strval', $opts['only_handles']), fn( $h ) => '' !== $h)) : null; - $safety_probes = ! empty($opts['safety_probes']); - $older_than = isset($opts['older_than']) ? trim( (string) $opts['older_than']) : ''; - $age_filter = null; + $safety_probes = ! empty($opts['safety_probes']); + $older_than = isset($opts['older_than']) ? trim( (string) $opts['older_than']) : ''; + $age_filter = null; if ( '' !== $older_than ) { $duration_seconds = $this->parse_worktree_cleanup_duration($older_than); if ( $duration_seconds instanceof \WP_Error ) { @@ -525,7 +525,7 @@ private function build_worktree_artifact_cleanup_plan( bool $force, array $opts $process_protection = $this->active_artifact_process_protection($wt_path, $artifacts); if ( null !== $process_protection ) { - $is_probe_unavailable = str_starts_with((string) ($process_protection['reason_code'] ?? ''), 'active_process_probe_'); + $is_probe_unavailable = str_starts_with( (string) ( $process_protection['reason_code'] ?? '' ), 'active_process_probe_'); if ( ( $is_probe_unavailable && ! $allow_unavailable_process_probe ) || ( ! $is_probe_unavailable && ! $allow_active ) ) { $skipped[] = array_merge($base_row, $process_protection, array( 'artifacts' => $artifacts )); continue; @@ -721,8 +721,8 @@ private function active_artifact_process_protection( string $worktree_path, arra 'reason_code' => 'active_build', 'protecting_reason' => 'active_build', 'reason' => 'active process cwd or open file intersects the worktree artifacts; leaving reconstructable artifacts in place', - 'process_probe' => $probe, - 'process_evidence' => $evidence, + 'process_probe' => $probe, + 'process_evidence' => $evidence, ); } @@ -735,7 +735,7 @@ private function active_artifact_process_protection( string $worktree_path, arra 'reason_code' => 'uncertain' === $status ? 'active_process_probe_uncertain' : 'active_process_probe_unavailable', 'protecting_reason' => 'active_process_probe_' . $status, 'reason' => 'active process use could not be authoritatively excluded; safe cleanup is failing closed', - 'process_probe' => $probe, + 'process_probe' => $probe, ); } @@ -768,7 +768,7 @@ protected function detect_active_artifact_processes( string $worktree_path, arra $snapshot = $this->artifact_process_path_records($fresh); $records = (array) ( $snapshot['records'] ?? array() ); - $matches = array(); + $matches = array(); foreach ( $records as $record ) { $path = rtrim( (string) ( $record['path'] ?? '' ), '/'); foreach ( $roots as $root ) { @@ -813,7 +813,10 @@ private function artifact_procfs_process_path_records(): array { return array( 'status' => 'unavailable', 'records' => array(), - 'diagnostics' => array( 'reason' => 'process_filesystem_unavailable', 'path' => $proc_root ), + 'diagnostics' => array( + 'reason' => 'process_filesystem_unavailable', + 'path' => $proc_root, + ), ); } @@ -823,7 +826,10 @@ private function artifact_procfs_process_path_records(): array { return array( 'status' => 'unavailable', 'records' => array(), - 'diagnostics' => array( 'reason' => 'process_filesystem_unreadable', 'path' => $proc_root ), + 'diagnostics' => array( + 'reason' => 'process_filesystem_unreadable', + 'path' => $proc_root, + ), ); } $scanned = 0; @@ -832,7 +838,10 @@ private function artifact_procfs_process_path_records(): array { $unknown_namespaces = array(); $self_mount_namespace = @readlink($proc_root . '/self/ns/mnt'); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Procfs namespace links may be unavailable on non-Linux hosts. if ( ! is_string($self_mount_namespace) || '' === $self_mount_namespace ) { - $unknown_namespaces[] = array( 'pid' => getmypid(), 'scope' => 'self' ); + $unknown_namespaces[] = array( + 'pid' => getmypid(), + 'scope' => 'self', + ); } foreach ( $entries as $entry ) { if ( ! ctype_digit( (string) $entry ) || getmypid() === (int) $entry ) { @@ -843,10 +852,16 @@ private function artifact_procfs_process_path_records(): array { $mount_namespace = @readlink($proc . '/ns/mnt'); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Processes can exit between procfs enumeration and readlink. if ( ! is_string($mount_namespace) || '' === $mount_namespace ) { if ( is_dir($proc) ) { - $unknown_namespaces[] = array( 'pid' => (int) $entry, 'scope' => 'process' ); + $unknown_namespaces[] = array( + 'pid' => (int) $entry, + 'scope' => 'process', + ); } } elseif ( is_string($self_mount_namespace) && $mount_namespace !== $self_mount_namespace ) { - $foreign_namespaces[] = array( 'pid' => (int) $entry, 'mount_namespace' => $mount_namespace ); + $foreign_namespaces[] = array( + 'pid' => (int) $entry, + 'mount_namespace' => $mount_namespace, + ); } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Processes can exit between procfs enumeration and ownership lookup. $owner_uid = @fileowner($proc); @@ -863,14 +878,20 @@ private function artifact_procfs_process_path_records(): array { 'path' => preg_replace('/ \(deleted\)$/', '', $cwd), ); } elseif ( is_dir($proc) ) { - $unreadable[] = array( 'pid' => (int) $entry, 'resource' => 'cwd' ); + $unreadable[] = array( + 'pid' => (int) $entry, + 'resource' => 'cwd', + ); } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Processes can exit between procfs enumeration and scandir. $fd_entries = @scandir($proc . '/fd'); if ( false === $fd_entries ) { if ( is_dir($proc) ) { - $unreadable[] = array( 'pid' => (int) $entry, 'resource' => 'fd' ); + $unreadable[] = array( + 'pid' => (int) $entry, + 'resource' => 'fd', + ); } continue; } @@ -951,12 +972,12 @@ private function build_partial_artifact_cleanup_row( array $candidate, array $re return array_merge( $candidate, array( - 'reason_code' => 'partial_artifact_cleanup', - 'reason' => 'one or more artifacts were removed before a later artifact became blocked', - 'artifacts' => $removed_artifacts, + 'reason_code' => 'partial_artifact_cleanup', + 'reason' => 'one or more artifacts were removed before a later artifact became blocked', + 'artifacts' => $removed_artifacts, 'remaining_artifacts' => $remaining, - 'bytes_reclaimed' => $bytes, - 'blocker' => $blocker, + 'bytes_reclaimed' => $bytes, + 'blocker' => $blocker, ) ); } @@ -969,7 +990,7 @@ private function build_partial_artifact_cleanup_row( array $candidate, array $re */ private function observe_artifact_reclamation_rows( array $rows ): array { foreach ( $rows as &$row ) { - $observations = array(); + $observations = array(); $durable_bytes = 0; $rebuilt_bytes = 0; foreach ( (array) ( $row['artifacts'] ?? array() ) as $artifact ) { @@ -1072,19 +1093,19 @@ private function build_worktree_artifact_cleanup_summary( array $candidates, arr arsort($artifact_by_repo); return array( - 'would_remove_worktrees' => count($candidates), - 'would_remove_artifacts' => $would_count, - 'removed_worktrees' => count($removed), + 'would_remove_worktrees' => count($candidates), + 'would_remove_artifacts' => $would_count, + 'removed_worktrees' => count($removed), 'partially_removed_worktrees' => count($partial), - 'removed_artifacts' => $removed_count, - 'skipped' => count($skipped), - 'skipped_by_reason' => $skipped_by_reason, - 'artifact_count' => 0 === $removed_count ? $would_count : $removed_count, - 'artifact_size_bytes' => 0 === $removed_count ? $would_bytes : $removed_bytes, - 'removed_size_bytes' => $removed_bytes, - 'durable_reclaimed_bytes' => $durable_bytes, - 'rebuilt_artifact_bytes' => $rebuilt_bytes, - 'artifact_size_by_repo' => $artifact_by_repo, + 'removed_artifacts' => $removed_count, + 'skipped' => count($skipped), + 'skipped_by_reason' => $skipped_by_reason, + 'artifact_count' => 0 === $removed_count ? $would_count : $removed_count, + 'artifact_size_bytes' => 0 === $removed_count ? $would_bytes : $removed_bytes, + 'removed_size_bytes' => $removed_bytes, + 'durable_reclaimed_bytes' => $durable_bytes, + 'rebuilt_artifact_bytes' => $rebuilt_bytes, + 'artifact_size_by_repo' => $artifact_by_repo, ); } diff --git a/inc/Workspace/WorkspaceCleanupPlan.php b/inc/Workspace/WorkspaceCleanupPlan.php index 4850f322..c671eab8 100644 --- a/inc/Workspace/WorkspaceCleanupPlan.php +++ b/inc/Workspace/WorkspaceCleanupPlan.php @@ -36,21 +36,21 @@ trait WorkspaceCleanupPlan { public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error { $mode = (string) ( $opts['mode'] ?? 'cleanup_plan' ); $inputs = array( - 'mode' => $mode, - 'force_artifact_cleanup' => ! empty($opts['force_artifact_cleanup']), + 'mode' => $mode, + 'force_artifact_cleanup' => ! empty($opts['force_artifact_cleanup']), 'allow_active_artifact_cleanup' => ! empty($opts['allow_active_artifact_cleanup']), - 'include_artifacts' => array_key_exists('include_artifacts', $opts) ? (bool) $opts['include_artifacts'] : true, - 'include_worktrees' => array_key_exists('include_worktrees', $opts) ? (bool) $opts['include_worktrees'] : true, - 'include_resolvers' => ! empty($opts['include_resolvers']), - 'top_n' => isset($opts['top_n']) ? max(1, min(50, (int) $opts['top_n'])) : 10, - 'limit' => isset($opts['limit']) ? max(1, (int) $opts['limit']) : self::CLEANUP_PLAN_DEFAULT_LIMIT, - 'offset' => isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0, - 'until_budget' => isset($opts['until_budget']) && '' !== trim( (string) $opts['until_budget']) ? trim( (string) $opts['until_budget']) : self::CLEANUP_PLAN_DEFAULT_BUDGET, - 'full_workspace' => ! empty($opts['full_workspace']), - 'worktree_older_than' => isset($opts['worktree_older_than']) ? trim( (string) $opts['worktree_older_than']) : '', - 'worktree_sort' => isset($opts['worktree_sort']) && '' !== trim( (string) $opts['worktree_sort']) ? trim( (string) $opts['worktree_sort']) : '', - 'artifact_sort' => isset($opts['artifact_sort']) && '' !== trim( (string) $opts['artifact_sort']) ? trim( (string) $opts['artifact_sort']) : ( 'artifacts' === $mode ? 'size' : '' ), - 'worktree_stale_only' => ! empty($opts['worktree_stale_only']), + 'include_artifacts' => array_key_exists('include_artifacts', $opts) ? (bool) $opts['include_artifacts'] : true, + 'include_worktrees' => array_key_exists('include_worktrees', $opts) ? (bool) $opts['include_worktrees'] : true, + 'include_resolvers' => ! empty($opts['include_resolvers']), + 'top_n' => isset($opts['top_n']) ? max(1, min(50, (int) $opts['top_n'])) : 10, + 'limit' => isset($opts['limit']) ? max(1, (int) $opts['limit']) : self::CLEANUP_PLAN_DEFAULT_LIMIT, + 'offset' => isset($opts['offset']) ? max(0, (int) $opts['offset']) : 0, + 'until_budget' => isset($opts['until_budget']) && '' !== trim( (string) $opts['until_budget']) ? trim( (string) $opts['until_budget']) : self::CLEANUP_PLAN_DEFAULT_BUDGET, + 'full_workspace' => ! empty($opts['full_workspace']), + 'worktree_older_than' => isset($opts['worktree_older_than']) ? trim( (string) $opts['worktree_older_than']) : '', + 'worktree_sort' => isset($opts['worktree_sort']) && '' !== trim( (string) $opts['worktree_sort']) ? trim( (string) $opts['worktree_sort']) : '', + 'artifact_sort' => isset($opts['artifact_sort']) && '' !== trim( (string) $opts['artifact_sort']) ? trim( (string) $opts['artifact_sort']) : ( 'artifacts' === $mode ? 'size' : '' ), + 'worktree_stale_only' => ! empty($opts['worktree_stale_only']), ); $artifact_plan = array( @@ -61,14 +61,14 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error if ( $inputs['include_artifacts'] ) { $artifact_plan = $this->worktree_cleanup_artifacts( array( - 'dry_run' => true, - 'force' => $inputs['force_artifact_cleanup'], + 'dry_run' => true, + 'force' => $inputs['force_artifact_cleanup'], 'allow_active_artifact_cleanup' => $inputs['allow_active_artifact_cleanup'], - 'full_workspace' => $inputs['full_workspace'], - 'limit' => $inputs['limit'], - 'offset' => $inputs['offset'], - 'sort' => $inputs['artifact_sort'], - 'older_than' => $inputs['worktree_older_than'], + 'full_workspace' => $inputs['full_workspace'], + 'limit' => $inputs['limit'], + 'offset' => $inputs['offset'], + 'sort' => $inputs['artifact_sort'], + 'older_than' => $inputs['worktree_older_than'], ) ); if ( $artifact_plan instanceof \WP_Error ) { @@ -135,14 +135,14 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error 'workspace_path' => $this->workspace_path, 'inputs' => $inputs, 'safety_policy' => array( - 'applies_inline' => false, - 'force_artifact_cleanup' => $inputs['force_artifact_cleanup'], + 'applies_inline' => false, + 'force_artifact_cleanup' => $inputs['force_artifact_cleanup'], 'allow_active_artifact_cleanup' => $inputs['allow_active_artifact_cleanup'], - 'artifact_age_filter' => is_array($artifact_plan['age_filter'] ?? null) ? $artifact_plan['age_filter'] : null, - 'artifact_cleanup' => 'apply-plan must freshly revalidate the reviewed age gate, profile-derived paths, authoritative liveness before each artifact, and active process use once per row immediately before mutation', - 'worktree_removal' => 'apply-plan must re-run dirty, unpushed, identity, lifecycle, containment, and primary protections before deletion', - 'resolver' => 'resolver rows may gather merge signals but cannot delete worktrees', - 'destructive_rows_need_review' => true, + 'artifact_age_filter' => is_array($artifact_plan['age_filter'] ?? null) ? $artifact_plan['age_filter'] : null, + 'artifact_cleanup' => 'apply-plan must freshly revalidate the reviewed age gate, profile-derived paths, authoritative liveness before each artifact, and active process use once per row immediately before mutation', + 'worktree_removal' => 'apply-plan must re-run dirty, unpushed, identity, lifecycle, containment, and primary protections before deletion', + 'resolver' => 'resolver rows may gather merge signals but cannot delete worktrees', + 'destructive_rows_need_review' => true, ), 'plans' => array( 'artifact_cleanup' => $artifact_plan, diff --git a/inc/Workspace/WorkspaceCoreUtilities.php b/inc/Workspace/WorkspaceCoreUtilities.php index e4aafb8c..5d9aadfe 100644 --- a/inc/Workspace/WorkspaceCoreUtilities.php +++ b/inc/Workspace/WorkspaceCoreUtilities.php @@ -692,7 +692,7 @@ private function build_primary_freshness_report( string $repo_path, string $hand ); } - return $this->build_primary_freshness_report_from_status_output(( string ) ( $status_result['output'] ?? '' ), $handle); + return $this->build_primary_freshness_report_from_status_output( (string) ( $status_result['output'] ?? '' ), $handle); } /** diff --git a/inc/Workspace/WorkspaceEmergencyCandidateSelector.php b/inc/Workspace/WorkspaceEmergencyCandidateSelector.php index 781e9731..2e02fec5 100644 --- a/inc/Workspace/WorkspaceEmergencyCandidateSelector.php +++ b/inc/Workspace/WorkspaceEmergencyCandidateSelector.php @@ -63,7 +63,11 @@ static function ( array $candidate ) use ( $bytes_field ): array { /** @return array>|null */ private static function find_viable_set( array $rows, int $target_bytes, int $target_inodes, int $limit ): ?array { $states = array( - array( 'selected' => array(), 'bytes' => 0, 'inodes' => 0 ), + array( + 'selected' => array(), + 'bytes' => 0, + 'inodes' => 0, + ), ); foreach ( array_slice($rows, 0, self::MAX_SEARCH_CANDIDATES) as $row ) { $next_states = $states; @@ -87,7 +91,8 @@ private static function find_viable_set( array $rows, int $target_bytes, int $ta static function ( array $left, array $right ) use ( $target_bytes, $target_inodes ): int { $left_score = min($target_bytes, $left['bytes']) + min($target_inodes, $left['inodes']); $right_score = min($target_bytes, $right['bytes']) + min($target_inodes, $right['inodes']); - return $right_score <=> $left_score ?: count($left['selected']) <=> count($right['selected']); + $score_order = $right_score <=> $left_score; + return 0 !== $score_order ? $score_order : count($left['selected']) <=> count($right['selected']); } ); $states = array_slice($next_states, 0, self::MAX_SEARCH_STATES); diff --git a/inc/Workspace/WorkspaceGitIdentityPolicy.php b/inc/Workspace/WorkspaceGitIdentityPolicy.php index b9939b88..9c81d06b 100644 --- a/inc/Workspace/WorkspaceGitIdentityPolicy.php +++ b/inc/Workspace/WorkspaceGitIdentityPolicy.php @@ -82,7 +82,7 @@ private function repository_git_identity_policy( string $repo_path ): array|null return null; } - $remote = trim((string) ($remote_result['output'] ?? '')); + $remote = trim( (string) ( $remote_result['output'] ?? '' )); $descriptor = GitHubRemote::descriptor($remote); if ( null === $descriptor || ! function_exists('apply_filters') ) { return null; @@ -96,13 +96,16 @@ private function repository_git_identity_policy( string $repo_path ): array|null return new \WP_Error('invalid_git_identity_policy', 'Git identity policy must return an array with non-empty name and email values.', array( 'status' => 500 )); } - $name = trim((string) ($identity['name'] ?? '')); - $email = trim((string) ($identity['email'] ?? '')); + $name = trim( (string) ( $identity['name'] ?? '' )); + $email = trim( (string) ( $identity['email'] ?? '' )); if ( '' === $name || '' === $email ) { return new \WP_Error('invalid_git_identity_policy', 'Git identity policy must provide non-empty name and email values.', array( 'status' => 500 )); } - return array( 'name' => $name, 'email' => $email ); + return array( + 'name' => $name, + 'email' => $email, + ); } /** @@ -113,6 +116,6 @@ private function git_config_value( array|\WP_Error $result ): string { return ''; } - return trim((string) ($result['output'] ?? '')); + return trim( (string) ( $result['output'] ?? '' )); } } diff --git a/inc/Workspace/WorkspaceHygieneReport.php b/inc/Workspace/WorkspaceHygieneReport.php index 13243b1f..4dd0402b 100644 --- a/inc/Workspace/WorkspaceHygieneReport.php +++ b/inc/Workspace/WorkspaceHygieneReport.php @@ -313,8 +313,8 @@ public function workspace_disk_emergency_cleanup( array $opts = array() ): array if ( $allow_worktree_deletion && $human_approved_deletion ) { $worktree_selection = WorkspaceEmergencyCandidateSelector::select( $worktree_candidates, - max(0, $target_bytes - $selection['planned_measured_recovery_bytes']), - max(0, $target_inodes - $selection['planned_measured_recovery_inodes']), + (int) max(0, $target_bytes - $selection['planned_measured_recovery_bytes']), + (int) max(0, $target_inodes - $selection['planned_measured_recovery_inodes']), $artifact_chunk_size, 'size_bytes' ); diff --git a/inc/Workspace/WorkspaceLockStore.php b/inc/Workspace/WorkspaceLockStore.php index e5101620..f725a38b 100644 --- a/inc/Workspace/WorkspaceLockStore.php +++ b/inc/Workspace/WorkspaceLockStore.php @@ -133,11 +133,12 @@ public static function status(): array { // specifically while a writer may be stalled. SQLite contention is bounded // by the same primitive used for lock ownership writes. global $wpdb; - $table = self::table_name(); - $now = gmdate('Y-m-d H:i:s'); + $table = self::table_name(); + $now = gmdate('Y-m-d H:i:s'); $result = \DataMachineCode\Storage\SqliteBusyRetry::run( 'workspace_lock_status', static function () use ( $wpdb, $table, $now ): array|false { + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table name is generated by self::table_name(); values remain prepared. $active = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE status = %s AND expires_at >= %s", 'active', $now)); if ( false === $active ) { return false; @@ -145,10 +146,13 @@ static function () use ( $wpdb, $table, $now ): array|false { return array( 'active' => (int) $active, 'active_keys' => self::lock_keys_for_status('active', false), + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table name is generated by self::table_name(); values remain prepared. 'stale' => (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE status = %s AND expires_at < %s", 'active', $now)), 'stale_keys' => self::lock_keys_for_status('active', true), 'locks' => array_merge(self::lock_rows_for_status('active', false), self::lock_rows_for_status('active', true)), + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table name is generated by self::table_name(); values remain prepared. 'released' => (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE status = %s", 'released')), + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Internal table name is generated by self::table_name(). 'total' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"), ); } @@ -156,16 +160,16 @@ static function () use ( $wpdb, $table, $now ): array|false { if ( is_wp_error($result) ) { $error_data = (array) $result->get_error_data(); return array( - 'available' => false, - 'state' => 'contended', - 'error' => $result->get_error_message(), - 'error_code' => $result->get_error_code(), + 'available' => false, + 'state' => 'contended', + 'error' => $result->get_error_message(), + 'error_code' => $result->get_error_code(), 'retry_after_seconds' => (int) ( $error_data['retry_after_seconds'] ?? 1 ), - 'table' => self::table_name(), - 'active' => 0, - 'stale' => 0, - 'released' => 0, - 'total' => 0, + 'table' => self::table_name(), + 'active' => 0, + 'stale' => 0, + 'released' => 0, + 'total' => 0, ); } if ( false === $result ) { @@ -182,8 +186,8 @@ static function () use ( $wpdb, $table, $now ): array|false { } return array( - 'available' => true, - 'table' => $table, + 'available' => true, + 'table' => $table, ...$result, ); } diff --git a/inc/Workspace/WorkspaceMutationLock.php b/inc/Workspace/WorkspaceMutationLock.php index 832ff6f8..ed49ef99 100644 --- a/inc/Workspace/WorkspaceMutationLock.php +++ b/inc/Workspace/WorkspaceMutationLock.php @@ -32,8 +32,8 @@ final class WorkspaceMutationLock { private string $request_path; private function __construct( $handle, int $lock_id = 0, string $request_path = '' ) { - $this->handle = $handle; - $this->lock_id = $lock_id; + $this->handle = $handle; + $this->lock_id = $lock_id; $this->request_path = $request_path; } @@ -99,10 +99,10 @@ public static function acquire( string $workspace_path, string $repo, int $timeo } } - $lock_path = $lock_dir . '/worktree-' . $repo . '.lock'; + $lock_path = $lock_dir . '/worktree-' . $repo . '.lock'; $request_path = self::record_request($lock_dir, $repo, $lock_path); $request_id = '' === $request_path ? '' : basename($request_path, '.json'); - $handle = fopen($lock_path, 'c'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + $handle = fopen($lock_path, 'c'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( false === $handle ) { self::remove_request($request_path); return new \WP_Error( @@ -259,7 +259,7 @@ private static function admission_error( \WP_Error $error, string $repo, string private static function record_request( string $lock_dir, string $repo, string $lock_path ): string { $request_dir = $lock_dir . '/requests'; - if ( ! is_dir($request_dir) && ! @mkdir($request_dir, 0755, true) && ! is_dir($request_dir) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir + if ( ! is_dir($request_dir) && ! @mkdir($request_dir, 0755, true) && ! is_dir($request_dir) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir,WordPress.PHP.NoSilencedErrors.Discouraged -- Atomic local lock setup handles the suppressed race by rechecking the directory. return ''; } $request_id = bin2hex(random_bytes(12)); @@ -279,7 +279,7 @@ private static function update_request( string $path, string $state ): void { if ( '' === $path || ! is_file($path) ) { return; } - $data = json_decode((string) file_get_contents($path), true); + $data = json_decode( (string) file_get_contents($path), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads a local lock request file, not a remote URL. if ( ! is_array($data) ) { return; } @@ -290,7 +290,7 @@ private static function update_request( string $path, string $state ): void { private static function write_request( string $path, array $data ): void { if ( '' !== $path ) { - $json = function_exists('wp_json_encode') ? wp_json_encode($data) : json_encode($data); + $json = function_exists('wp_json_encode') ? wp_json_encode($data) : json_encode($data); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- This lock primitive also runs outside WordPress bootstrap. $temporary = $path . '.' . bin2hex(random_bytes(6)) . '.tmp'; if ( false !== file_put_contents($temporary, false === $json ? '{}' : (string) $json, LOCK_EX) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents rename($temporary, $path); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename @@ -306,10 +306,11 @@ private static function remove_request( string $path ): void { /** @return array> */ private static function queued_requests( string $workspace_path ): array { - $files = glob(rtrim($workspace_path, '/') . '/.locks/requests/*.json') ?: array(); + $files = glob(rtrim($workspace_path, '/') . '/.locks/requests/*.json'); + $files = false !== $files ? $files : array(); $rows = array(); foreach ( array_slice($files, 0, 25) as $file ) { - $row = json_decode((string) file_get_contents($file), true); + $row = json_decode( (string) file_get_contents($file), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads a local lock request file, not a remote URL. if ( is_array($row) && ! self::request_owner_exited($row) ) { $rows[] = $row; } elseif ( is_array($row) ) { @@ -321,7 +322,7 @@ private static function queued_requests( string $workspace_path ): array { private static function request_owner_exited( array $request ): bool { $pid = (int) ( $request['pid'] ?? 0 ); - return $pid > 0 && function_exists('posix_kill') && ! @posix_kill($pid, 0); + return $pid > 0 && function_exists('posix_kill') && ! @posix_kill($pid, 0); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Probe failure means the request owner is unavailable. } /** @@ -608,7 +609,7 @@ private static function filesystem_lock_entry( string $file ): array { if ( ! flock($handle, LOCK_EX | LOCK_NB) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_flock $entry['state'] = 'active'; $entry['reason'] = 'filesystem_flock_held'; - $entry['owner_evidence'] = self::owner_evidence_for_lock($lock_key, $scope); + $entry['owner_evidence'] = self::owner_evidence_for_lock(); $entry['recovery_command'] = 'wp datamachine-code workspace worktree locks --format=json'; $entry['safe_to_prune'] = false; $entry['operator_guidance'] = 'An active OS flock cannot be safely pruned. Inspect the owner evidence or running DMC/WP-CLI processes and retry after the holder exits.'; @@ -632,7 +633,7 @@ private static function filesystem_lock_entry( string $file ): array { /** * @return array */ - private static function owner_evidence_for_lock( string $lock_key, string $scope ): array { + private static function owner_evidence_for_lock(): array { return array( 'source' => 'filesystem_only', 'message' => 'Database owner lookup is intentionally deferred so lock inspection remains available during database contention.', diff --git a/inc/Workspace/WorkspaceSafeCleanupOrchestrator.php b/inc/Workspace/WorkspaceSafeCleanupOrchestrator.php index fe93a585..1b84129d 100644 --- a/inc/Workspace/WorkspaceSafeCleanupOrchestrator.php +++ b/inc/Workspace/WorkspaceSafeCleanupOrchestrator.php @@ -71,16 +71,16 @@ public function run( array $input ): array|\WP_Error { } $result = array( - 'success' => true, - 'mode' => 'safe_workspace_cleanup', - 'applied' => ! $dry_run, - 'destructive' => ! $dry_run, - 'limit' => $limit, - 'passes' => $passes, - 'cycles' => $cycles, - 'generated_at' => gmdate('c'), - 'steps' => array(), - 'summary' => array( + 'success' => true, + 'mode' => 'safe_workspace_cleanup', + 'applied' => ! $dry_run, + 'destructive' => ! $dry_run, + 'limit' => $limit, + 'passes' => $passes, + 'cycles' => $cycles, + 'generated_at' => gmdate('c'), + 'steps' => array(), + 'summary' => array( 'cycles' => 0, 'planned' => 0, 'applied_rows' => 0, @@ -94,9 +94,9 @@ public function run( array $input ): array|\WP_Error { 'blocker_count' => 0, 'blockers_by_reason' => array(), ), - 'blockers' => array(), + 'blockers' => array(), 'blockers_by_stage' => array(), - 'evidence' => array( + 'evidence' => array( 'safety' => $dry_run ? 'Preview only. Uses DMC safe classifiers/removals and stale lock pruning in dry-run mode.' : 'Applies only DMC safe classifiers/removals, refuses force and unpushed discard, and prunes stale DMC locks.', @@ -138,17 +138,17 @@ public function run( array $input ): array|\WP_Error { 'limit' => $limit, ) : array( - 'mode' => 'artifacts', - 'force' => false, - 'limit' => $limit, - 'max_passes' => $passes, + 'mode' => 'artifacts', + 'force' => false, + 'limit' => $limit, + 'max_passes' => $passes, ); $artifacts = $this->execute_ability($artifact_cleanup, $artifact_input); if ( is_wp_error($artifacts) ) { return $artifacts; } - $result['steps']['artifact_cleanup'] = $this->summarize_artifact_step($artifacts, $dry_run); - $result['blockers_by_stage']['artifact_cleanup'] = (array) ( $result['steps']['artifact_cleanup']['blockers'] ?? array() ); + $result['steps']['artifact_cleanup'] = $this->summarize_artifact_step($artifacts, $dry_run); + $result['blockers_by_stage']['artifact_cleanup'] = (array) ( $result['steps']['artifact_cleanup']['blockers'] ?? array() ); $this->accumulate_artifact_step($result, $result['steps']['artifact_cleanup']); $this->checkpoint_progress($run_id, $result, 'applying'); @@ -172,18 +172,18 @@ public function run( array $input ): array|\WP_Error { if ( is_wp_error($eligible) ) { return $eligible; } - $result['steps'][ 'cleanup_eligible_' . $cycle ] = $this->summarize_cleanup_step($eligible); + $result['steps'][ 'cleanup_eligible_' . $cycle ] = $this->summarize_cleanup_step($eligible); $result['blockers_by_stage'][ 'cleanup_eligible_' . $cycle ] = (array) ( $result['steps'][ 'cleanup_eligible_' . $cycle ]['blockers'] ?? array() ); - $cycle_progress += $this->accumulate_cleanup_step($result, $eligible); + $cycle_progress += $this->accumulate_cleanup_step($result, $eligible); $this->checkpoint_progress($run_id, $result, 'applying'); $active = $this->execute_ability($active_no_signal, $common); if ( is_wp_error($active) ) { return $active; } - $result['steps'][ 'active_no_signal_' . $cycle ] = $this->summarize_cleanup_step($active); + $result['steps'][ 'active_no_signal_' . $cycle ] = $this->summarize_cleanup_step($active); $result['blockers_by_stage'][ 'active_no_signal_' . $cycle ] = (array) ( $result['steps'][ 'active_no_signal_' . $cycle ]['blockers'] ?? array() ); - $cycle_progress += $this->accumulate_cleanup_step($result, $active); + $cycle_progress += $this->accumulate_cleanup_step($result, $active); if ( is_array($active['continuation'] ?? null) && ! empty($active['continuation']['next_command']) ) { $result['continuation']['next_command'] = (string) $active['continuation']['next_command']; $result['continuation']['reason'] = (string) ( $active['continuation']['reason'] ?? 'active_no_signal_page_incomplete' ); @@ -203,9 +203,9 @@ public function run( array $input ): array|\WP_Error { $result['summary']['lock_files_removed'] += (int) ( $result['steps']['lock_prune_end']['removed_count'] ?? 0 ); $this->checkpoint_progress($run_id, $result, 'applying'); - $result['blockers'] = $this->compact_blockers($result['blockers']); - $result['summary']['blocker_count'] = array_sum(array_map(static fn( array $row ): int => (int) ( $row['count'] ?? 0 ), $result['blockers'])); - $result['summary']['blockers_by_reason'] = array_column($result['blockers'], 'count', 'reason_code'); + $result['blockers'] = $this->compact_blockers($result['blockers']); + $result['summary']['blocker_count'] = array_sum(array_map(static fn( array $row ): int => (int) ( $row['count'] ?? 0 ), $result['blockers'])); + $result['summary']['blockers_by_reason'] = array_column($result['blockers'], 'count', 'reason_code'); $result['summary']['blocker_count_scope'] = 'maximum_observed_per_reason_across_stages'; if ( ! $dry_run && $result['summary']['blocker_count'] > 0 ) { $result['state'] = 'complete_with_blockers'; @@ -352,10 +352,10 @@ private function summarize_cleanup_step( array $step ): array { /** @return array */ private function summarize_artifact_step( array $step, bool $dry_run ): array { - $rows = (array) ( $step['rows']['artifact_cleanup'] ?? array() ); - $blocked = (array) ( $step['blocked']['artifact_cleanup'] ?? array() ); - $passes = (array) ( $step['passes'] ?? array() ); - $planned = $dry_run ? count($rows) : array_sum(array_map(static fn( array $pass ): int => (int) ( $pass['planned_rows'] ?? 0 ), $passes)); + $rows = (array) ( $step['rows']['artifact_cleanup'] ?? array() ); + $blocked = (array) ( $step['blocked']['artifact_cleanup'] ?? array() ); + $passes = (array) ( $step['passes'] ?? array() ); + $planned = $dry_run ? count($rows) : array_sum(array_map(static fn( array $pass ): int => (int) ( $pass['planned_rows'] ?? 0 ), $passes)); $blockers = array(); if ( $dry_run ) { foreach ( $blocked as $row ) { @@ -383,13 +383,13 @@ private function summarize_artifact_step( array $step, bool $dry_run ): array { /** @param array $step */ private function accumulate_artifact_step( array &$result, array $step ): void { - $result['summary']['planned'] += (int) ( $step['planned'] ?? 0 ); - $result['summary']['applied_rows'] += (int) ( $step['applied_rows'] ?? 0 ); - $result['summary']['skipped_rows'] += (int) ( $step['skipped_rows'] ?? 0 ); + $result['summary']['planned'] += (int) ( $step['planned'] ?? 0 ); + $result['summary']['applied_rows'] += (int) ( $step['applied_rows'] ?? 0 ); + $result['summary']['skipped_rows'] += (int) ( $step['skipped_rows'] ?? 0 ); $result['summary']['would_reclaim_bytes'] += (int) ( $step['would_reclaim'] ?? 0 ); - $result['summary']['removed'] += (int) ( $step['applied_rows'] ?? 0 ); - $result['summary']['would_remove'] += (int) ( $step['planned'] ?? 0 ); - $result['summary']['bytes_reclaimed'] += (int) ( $step['bytes_reclaimed'] ?? 0 ); + $result['summary']['removed'] += (int) ( $step['applied_rows'] ?? 0 ); + $result['summary']['would_remove'] += (int) ( $step['planned'] ?? 0 ); + $result['summary']['bytes_reclaimed'] += (int) ( $step['bytes_reclaimed'] ?? 0 ); foreach ( (array) ( $step['blockers'] ?? array() ) as $reason => $count ) { $result['blockers'][] = array( 'reason_code' => (string) $reason, @@ -453,8 +453,8 @@ private function summarize_lock_step( array $step ): array { private function compact_blockers( array $rows ): array { $blockers = array(); foreach ( $rows as $row ) { - $reason = (string) ( $row['reason_code'] ?? 'unknown' ); - $blockers[ $reason ] ??= array( + $reason = (string) ( $row['reason_code'] ?? 'unknown' ); + $blockers[ $reason ] ??= array( 'reason_code' => $reason, 'count' => 0, ); diff --git a/inc/Workspace/WorkspaceTargetInspector.php b/inc/Workspace/WorkspaceTargetInspector.php index 1021d9a6..ee415d18 100644 --- a/inc/Workspace/WorkspaceTargetInspector.php +++ b/inc/Workspace/WorkspaceTargetInspector.php @@ -81,7 +81,7 @@ public static function inspect( string $path, string $handle ): array|\WP_Error ); } - $decoded = json_decode(( string ) ( $result['stdout'] ?? '' ), true); + $decoded = json_decode( (string) ( $result['stdout'] ?? '' ), true); if ( ! is_array($decoded) ) { return new \WP_Error( 'workspace_target_lookup_invalid_response', diff --git a/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php b/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php index c1387ac9..62268565 100644 --- a/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php +++ b/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php @@ -269,9 +269,9 @@ private function build_bounded_cleanup_eligible_apply_hint( int $limit, string $ * @return array */ private function build_inventory_cleanup_no_signal_skip( array $base_row, array $wt, array $metadata ): array { - $liveness = (string) ( $wt['liveness'] ?? WorktreeContextInjector::LIVENESS_UNKNOWN ); - $liveness_reason = (string) ( $wt['liveness_reason'] ?? '' ); - $state = isset($metadata['lifecycle_state']) ? WorktreeContextInjector::normalize_state( (string) $metadata['lifecycle_state']) : null; + $liveness = (string) ( $wt['liveness'] ?? WorktreeContextInjector::LIVENESS_UNKNOWN ); + $liveness_reason = (string) ( $wt['liveness_reason'] ?? '' ); + $state = isset($metadata['lifecycle_state']) ? WorktreeContextInjector::normalize_state( (string) $metadata['lifecycle_state']) : null; if ( WorktreeCleanupCandidateClassifier::needs_lifecycle_reconciliation($metadata, $liveness) ) { return array_merge( $base_row, array( diff --git a/inc/Workspace/WorkspaceWorktreeLifecycle.php b/inc/Workspace/WorkspaceWorktreeLifecycle.php index 532c732b..9a50dc5f 100644 --- a/inc/Workspace/WorkspaceWorktreeLifecycle.php +++ b/inc/Workspace/WorkspaceWorktreeLifecycle.php @@ -315,7 +315,11 @@ static function ( $row ): string { return new \WP_Error( 'worktree_rebase_cleanup_failed', 'Rebase failed and its cleanup could not be verified; refusing bootstrap until the worktree is repaired.', - array( 'status' => 500, 'path' => $wt_path, 'rebase' => $response ) + array( + 'status' => 500, + 'path' => $wt_path, + 'rebase' => $response, + ) ); } @@ -620,7 +624,7 @@ private function worktree_add_locked( } } - $lifecycle_metadata = WorktreeContextInjector::build_lifecycle_metadata( + $lifecycle_metadata = WorktreeContextInjector::build_lifecycle_metadata( array( 'handle' => $wt_handle, 'path' => $wt_path, @@ -638,7 +642,7 @@ private function worktree_add_locked( 'inject_context' => $inject_context, 'bootstrap' => $bootstrap, ); - $metadata_stored = WorktreeContextInjector::store_lifecycle_metadata( $wt_handle, $lifecycle_metadata ); + $metadata_stored = WorktreeContextInjector::store_lifecycle_metadata( $wt_handle, $lifecycle_metadata ); if ( is_wp_error( $metadata_stored ) ) { $this->rollback_rejected_worktree( $primary_path, $wt_path, $branch, $created_branch ); return $metadata_stored; @@ -682,9 +686,14 @@ private function worktree_add_locked( * Reuse an exact managed handle only when its persisted contract proves it is * safe. This intentionally does not search for equivalent task candidates or * recycle terminal worktrees; those need explicit caller policy. + * + * @return array{success: bool, handle: string, path: string, branch: string, slug: string, created_branch: bool, message: string, disk_budget?: array, context_injected?: bool, context_files?: string[], context_skip_reason?: string, bootstrap?: array, fetch_failed?: bool, fetch_error?: string, stale_commits_behind?: int, upstream?: string, base_stale_commits_behind?: int, base_upstream?: string, default_branch_commits_behind?: int, default_branch_ref?: string, gate_threshold?: int, rebase_attempted?: bool, rebase_succeeded?: bool, rebase_error?: string, rebase_target?: string}|\WP_Error */ private function reuse_existing_worktree( string $handle, string $branch, ?string $from, bool $inject_context, bool $bootstrap, array $task ): array|\WP_Error { - $inspection = $this->worktree_get($handle, array( 'include_status' => true, 'include_disk' => false )); + $inspection = $this->worktree_get($handle, array( + 'include_status' => true, + 'include_disk' => false, + )); if ( is_wp_error($inspection) || empty($inspection['worktrees'][0]) ) { return $this->worktree_reuse_refused($handle, 'inspection_failed', array( 'error_code' => is_wp_error($inspection) ? $inspection->get_error_code() : 'worktree_not_found', @@ -702,7 +711,7 @@ private function reuse_existing_worktree( string $handle, string $branch, ?strin 'task' => $existing['task'] ?? null, 'metadata' => $existing['metadata'] ?? null, ); - if ( $branch !== ( $existing['branch'] ?? null ) ) { + if ( ( $existing['branch'] ?? null ) !== $branch ) { return $this->worktree_reuse_refused($handle, 'branch_mismatch', $evidence + array( 'requested_branch' => $branch )); } if ( (int) ( $existing['dirty'] ?? 0 ) > 0 ) { @@ -721,16 +730,25 @@ private function reuse_existing_worktree( string $handle, string $branch, ?strin return $this->worktree_reuse_refused($handle, 'reuse_contract_missing', $evidence); } $requested_base = null !== $from && '' !== trim($from) ? trim($from) : ( $contract['base_ref'] ?? null ); - if ( $requested_base !== ( $contract['base_ref'] ?? null ) ) { - return $this->worktree_reuse_refused($handle, 'base_mismatch', $evidence + array( 'requested_base_ref' => $requested_base, 'stored_base_ref' => $contract['base_ref'] ?? null )); + if ( ( $contract['base_ref'] ?? null ) !== $requested_base ) { + return $this->worktree_reuse_refused($handle, 'base_mismatch', $evidence + array( + 'requested_base_ref' => $requested_base, + 'stored_base_ref' => $contract['base_ref'] ?? null, + )); } - if ( $inject_context !== (bool) ( $contract['inject_context'] ?? null ) || $bootstrap !== (bool) ( $contract['bootstrap'] ?? null ) ) { + if ( (bool) ( $contract['inject_context'] ?? null ) !== $inject_context || (bool) ( $contract['bootstrap'] ?? null ) !== $bootstrap ) { return $this->worktree_reuse_refused($handle, 'runtime_incompatible', $evidence + array( - 'requested_runtime' => array( 'inject_context' => $inject_context, 'bootstrap' => $bootstrap ), - 'stored_runtime' => array( 'inject_context' => $contract['inject_context'] ?? null, 'bootstrap' => $contract['bootstrap'] ?? null ), + 'requested_runtime' => array( + 'inject_context' => $inject_context, + 'bootstrap' => $bootstrap, + ), + 'stored_runtime' => array( + 'inject_context' => $contract['inject_context'] ?? null, + 'bootstrap' => $contract['bootstrap'] ?? null, + ), )); } - if ( $this->worktree_reuse_task_identity($task) !== $this->worktree_reuse_task_identity((array) ($existing['task'] ?? array())) ) { + if ( $this->worktree_reuse_task_identity($task) !== $this->worktree_reuse_task_identity( (array) ( $existing['task'] ?? array() )) ) { return $this->worktree_reuse_refused($handle, 'task_mismatch', $evidence + array( 'requested_task' => $task )); } @@ -742,7 +760,10 @@ private function reuse_existing_worktree( string $handle, string $branch, ?strin 'slug' => $this->slugify_branch($branch), 'created_branch' => false, 'reused' => true, - 'reuse' => array( 'status' => 'accepted', 'reason_code' => 'exact_compatible_handle' ) + $evidence, + 'reuse' => array( + 'status' => 'accepted', + 'reason_code' => 'exact_compatible_handle', + ) + $evidence, 'metadata' => $metadata, 'message' => sprintf('Reused clean compatible worktree "%s" at %s.', $handle, $existing['path']), ); @@ -753,7 +774,13 @@ private function worktree_reuse_refused( string $handle, string $reason_code, ar return new \WP_Error( 'worktree_reuse_refused', sprintf('Refusing to reuse worktree "%s": %s.', $handle, str_replace('_', ' ', $reason_code)), - array( 'status' => 409, 'reuse' => array( 'status' => 'refused', 'reason_code' => $reason_code ) + $evidence ) + array( + 'status' => 409, + 'reuse' => array( + 'status' => 'refused', + 'reason_code' => $reason_code, + ) + $evidence, + ) ); } @@ -888,7 +915,7 @@ public function worktree_get( string $handle_or_path, array $opts = array() ): a } } - if ( ! is_array($parsed) || '' === $parsed['dir_name'] || ! is_dir($path) || ! file_exists($path . '/.git') ) { + if ( ! is_array($parsed) || '' === $parsed['dir_name'] || false === $path || ! is_dir($path) || ! file_exists($path . '/.git') ) { $not_found_handle = is_array($parsed) ? $parsed['dir_name'] : $target; return new \WP_Error( 'worktree_not_found', diff --git a/inc/Workspace/WorktreeContextInjector.php b/inc/Workspace/WorktreeContextInjector.php index 9b6002f0..56dd56a9 100644 --- a/inc/Workspace/WorktreeContextInjector.php +++ b/inc/Workspace/WorktreeContextInjector.php @@ -1496,8 +1496,12 @@ public static function resolve_task_metadata( array $args = array() ): ?array { $task_url = trim($env_task_url); } } - $task_url_parts = '' !== $task_url ? parse_url($task_url) : false; - if ( is_array($task_url_parts) && isset($task_url_parts['host'], $task_url_parts['scheme']) && in_array(strtolower((string) $task_url_parts['scheme']), array( 'http', 'https' ), true) ) { + $task_url_parts = false; + if ( '' !== $task_url ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- This class also runs in standalone bootstrap contexts without WordPress loaded. + $task_url_parts = function_exists('wp_parse_url') ? wp_parse_url($task_url) : parse_url($task_url); + } + if ( is_array($task_url_parts) && isset($task_url_parts['host'], $task_url_parts['scheme']) && in_array(strtolower( (string) $task_url_parts['scheme']), array( 'http', 'https' ), true) ) { $task['task_url'] = $task_url; } @@ -1568,8 +1572,10 @@ public static function get_metadata_fresh( string $handle ): ?array { return $inventory_metadata; } - $inventory_seen = strtotime( (string) ( $inventory_metadata['last_seen_at'] ?? '' ) ) ?: 0; - $option_seen = strtotime( (string) ( $option_metadata['last_seen_at'] ?? '' ) ) ?: 0; + $inventory_seen = strtotime( (string) ( $inventory_metadata['last_seen_at'] ?? '' ) ); + $option_seen = strtotime( (string) ( $option_metadata['last_seen_at'] ?? '' ) ); + $inventory_seen = false !== $inventory_seen ? $inventory_seen : 0; + $option_seen = false !== $option_seen ? $option_seen : 0; return $option_seen > $inventory_seen ? array_merge($inventory_metadata, $option_metadata) : array_merge($option_metadata, $inventory_metadata); diff --git a/inc/Workspace/workspace-target-probe.php b/inc/Workspace/workspace-target-probe.php index 65b74a90..543fba79 100644 --- a/inc/Workspace/workspace-target-probe.php +++ b/inc/Workspace/workspace-target-probe.php @@ -1,4 +1,5 @@ false ), JSON_UNESCAPED_SLASHES)); + fwrite(STDOUT, (string) json_encode(array( 'exists' => false ), JSON_UNESCAPED_SLASHES)); exit(0); } @@ -52,7 +53,7 @@ fwrite( STDOUT, - json_encode( + (string) json_encode( array( 'exists' => true, 'branch' => '' !== (string) $branch ? $branch : null, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..25787882 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,505 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function method_exists\(\) with WP_Ability and ''execute'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/AgentsApi/WorkspaceExecutorAdapter.php + + - + message: '#^Call to function method_exists\(\) with WP_Error and ''get_error_code'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/AgentsApi/WorkspaceExecutorAdapter.php + + - + message: '#^Call to function method_exists\(\) with WP_Error and ''get_error_message'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/AgentsApi/WorkspaceExecutorAdapter.php + + - + message: '#^Parameter \#3 \$error_type of static method DataMachineCode\\AgentsApi\\WorkspaceExecutorAdapter\:\:error_result\(\) expects string, int\|string given\.$#' + identifier: argument.type + count: 1 + path: inc/AgentsApi/WorkspaceExecutorAdapter.php + + - + message: '#^Cannot access offset ''configured'' on array\|WP_Error\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: inc/Cli/Commands/GitHubCommand.php + + - + message: '#^Static method DataMachineCode\\Environment\:\:evaluate_shell_capability\(\) is unused\.$#' + identifier: method.unused + count: 1 + path: inc/Environment.php + + - + message: '#^Cannot access offset ''issues'' on array\|WP_Error\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: inc/Handlers/GitHub/GitHub.php + + - + message: '#^Cannot access offset ''pulls'' on array\|WP_Error\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: inc/Handlers/GitHub/GitHub.php + + - + message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Runtime/ActiveWorkspaceProjector.php + + - + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Runtime/ActiveWorkspaceProjector.php + + - + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Runtime/WordPressRuntimeInspector.php + + - + message: '#^Call to function method_exists\(\) with WP_Theme and ''get'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 4 + path: inc/Runtime/WordPressRuntimeInspector.php + + - + message: '#^PHPDoc tag @param references unknown parameter\: \$input$#' + identifier: parameter.notFound + count: 1 + path: inc/SourceInventory/WorkspaceSourceInventory.php + + - + message: '#^Cannot call method get_charset_collate\(\) on class\-string\|object\.$#' + identifier: method.nonObject + count: 1 + path: inc/Storage/CleanupSchema.php + + - + message: '#^Parameter \#1 \$hook_name of function apply_filters expects non\-empty\-string, string given\.$#' + identifier: argument.type + count: 1 + path: inc/Storage/SqliteBusyRetry.php + + - + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue + count: 2 + path: inc/Support/CommandSpec.php + + - + message: '#^Strict comparison using \=\=\= between '''' and non\-empty\-string will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 2 + path: inc/Support/GitHubRemote.php + + - + message: '#^Call to function method_exists\(\) with WP_Error and ''get_error_data'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Support/GitRunner.php + + - + message: '#^Offset ''output'' on array\{success\: true, output\: string\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Support/GitRunner.php + + - + message: '#^Parameter \#3 \$depth of function json_encode expects int\<1, max\>, int given\.$#' + identifier: argument.type + count: 1 + path: inc/Support/JsonCodec.php + + - + message: '#^Parameter \#3 \$depth of function wp_json_encode expects int\<1, max\>, int given\.$#' + identifier: argument.type + count: 1 + path: inc/Support/JsonCodec.php + + - + message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Support/RunArtifactPrSectionRenderer.php + + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: inc/Support/RunArtifactPrSectionRenderer.php + + - + message: '#^Offset 0 on non\-empty\-list\<\(int\|string\)\> on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Support/SystemTaskDrainability.php + + - + message: '#^Strict comparison using \=\=\= between false and int will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: inc/Support/SystemTaskDrainability.php + + - + message: '#^Argument of an invalid type list\\|false supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 11 + path: inc/Workspace/Workspace.php + + - + message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Call to function is_array\(\) with array\ will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 3 + path: inc/Workspace/Workspace.php + + - + message: '#^Call to function method_exists\(\) with WP_Error and ''get_error_code'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Cannot call method get_error_message\(\) on array\\|WP_Error\.$#' + identifier: method.nonObject + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Method DataMachineCode\\Workspace\\Workspace\:\:artifact_process_path_records\(\) should return array\{status\: string, records\: array\\>, diagnostics\: array\\} but returns array\\.$#' + identifier: return.type + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Negated boolean expression is always true\.$#' + identifier: booleanNot.alwaysTrue + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''artifact_size_bytes'' on non\-empty\-array on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''bytes'' might not exist on array\{success\: true, bytes\?\: int, reason\?\: string, timeout_seconds\?\: int, cleanup\?\: array\\}\.$#' + identifier: offsetAccess.notFound + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''count'' on array\{count\: int\<1, max\>, size_bytes\: int\<0, max\>, size_accounting\: array\, repos\: non\-empty\-array\\>, examples\: list\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''diagnostics'' on array\{status\: string, records\: array\\>, diagnostics\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''elapsed_ms'' on non\-empty\-array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 3 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''entry_count'' might not exist on array\{success\: bool, entry_count\?\: int, reason\?\: string, timeout_seconds\?\: int, cleanup\?\: array\\}\.$#' + identifier: offsetAccess.notFound + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''entry_count'' might not exist on array\{success\: true, entry_count\?\: int, reason\?\: string, timeout_seconds\?\: int, cleanup\?\: array\\}\.$#' + identifier: offsetAccess.notFound + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''entry_count_status'' on non\-empty\-array on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''evidence'' on array\{status\: string, evidence\: array\\>, diagnostics\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''fields_skipped'' on array\{success\: bool, worktrees\: array, fields_skipped\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''is_worktree'' on non\-empty\-array\ in empty\(\) always exists and is not falsy\.$#' + identifier: empty.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''liveness'' on array\{liveness\: string, reason\: string, heartbeat_age_seconds\: int\|null, last_seen_at\: string\|null\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''message'' might not exist on array\{valid\: false, real_path\?\: string, message\?\: string\}\.$#' + identifier: offsetAccess.notFound + count: 3 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''real_path'' might not exist on array\{valid\: true, real_path\?\: string, message\?\: string\}\.$#' + identifier: offsetAccess.notFound + count: 3 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''reason'' on array\{handle\: string, path\: non\-falsy\-string, reason\: ''total_timeout''\}\|array\{handle\: string, path\: non\-falsy\-string, reason\: string, timeout_seconds\: int\|null, cleanup\: array\\|null\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''records'' on array\{status\: string, records\: array\\>, diagnostics\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''repo'' on array\{repo\: string, branch_slug\: string\|null, is_worktree\: true, dir_name\?\: string\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''row_id'' on non\-empty\-array\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 3 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''signal'' on array\{signal\: string, reason\: string, finalized_state\?\: string, pr_url\?\: string\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''signal'' on array\{signal\: string, reason\: string, pr_url\?\: string\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''size_bytes'' on array\{count\: int\<1, max\>, size_bytes\: int\<0, max\>, size_accounting\: array\, repos\: non\-empty\-array\\>, examples\: list\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''size_bytes'' on array\{path\: string, handle\: string, repo\: string, category\: string, row_type\: ''artifact_cleanup''\|''worktree_removal'', safety_class\: string, size_bytes\: int\<0, max\>\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''status'' on array\{status\: string, evidence\: array\\>, diagnostics\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''status'' on array\{status\: string, records\: array\\>, diagnostics\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Offset ''worktrees'' on array\{success\: bool, worktrees\: array, fields_skipped\: array\\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 8 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#1 \$array \(array\\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' + identifier: arrayFilter.same + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#1 \$array \(list\\>\) of array_values is already a list, call has no effect\.$#' + identifier: arrayValues.list + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#1 \$array of function uasort contains unresolvable type\.$#' + identifier: argument.unresolvableType + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#2 \$callback of function uasort contains unresolvable type\.$#' + identifier: argument.unresolvableType + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#2 \$default_ref of method DataMachineCode\\Workspace\\Workspace\:\:worktree_behind_default_branch_error\(\) expects string, string\|WP_Error given\.$#' + identifier: argument.type + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, int\<1, max\>\|WP_Error given\.$#' + identifier: argument.type + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#3 \$upstream of static method DataMachineCode\\Workspace\\WorktreeStalenessProbe\:\:behind_count\(\) expects string, string\|WP_Error given\.$#' + identifier: argument.type + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Parameter \#7 \$scope of method DataMachineCode\\Workspace\\Workspace\:\:build_worktree_metadata_reconciliation_remaining_blockers\(\) expects array\{argument\: string, repo\: string, handle\: string\|null\}\|null, array\\|null given\.$#' + identifier: argument.type + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \!\=\= between '''' and '''' will always evaluate to false\.$#' + identifier: notIdentical.alwaysFalse + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \!\=\= between '''' and non\-empty\-string will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \!\=\= between mixed and \*NEVER\* will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#' + identifier: notIdentical.alwaysFalse + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \!\=\= between null and string will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \=\=\= between '''' and non\-empty\-string will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 2 + path: inc/Workspace/Workspace.php + + - + message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Variable \$unpushed in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable + count: 1 + path: inc/Workspace/Workspace.php + + - + message: '#^Cannot access offset ''output'' on array\{success\: true, output\: string\}\|WP_Error\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 7 + path: inc/Workspace/WorkspaceDiff.php + + - + message: '#^Offset ''message'' might not exist on array\{valid\: false, real_path\?\: string, message\?\: string\}\.$#' + identifier: offsetAccess.notFound + count: 1 + path: inc/Workspace/WorkspaceDiff.php + + - + message: '#^Offset ''real_path'' might not exist on array\{valid\: true, real_path\?\: string, message\?\: string\}\.$#' + identifier: offsetAccess.notFound + count: 1 + path: inc/Workspace/WorkspaceDiff.php + + - + message: '#^Offset 0 on non\-empty\-list\ on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: inc/Workspace/WorkspaceHandle.php + + - + message: '#^Cannot call method get_charset_collate\(\) on class\-string\|object\.$#' + identifier: method.nonObject + count: 1 + path: inc/Workspace/WorkspaceLockStore.php + + - + message: '#^Parameter \#1 \$callback of function call_user_func_array expects callable\(\)\: mixed, array\{mixed, ''prepare''\} given\.$#' + identifier: argument.type + count: 2 + path: inc/Workspace/WorkspaceLockStore.php + + - + message: '#^Call to function method_exists\(\) with WP_Error and ''get_error_code'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: inc/Workspace/WorktreeStalenessProbe.php diff --git a/phpstan.neon b/phpstan.neon index 9298004a..c923a741 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ parameters: scanFiles: - stubs/phpstan/datamachine-base-command.php - stubs/phpstan/datamachine-plugin-settings.php + - stubs/phpstan/datamachine-runtime.php - stubs/phpstan/datamachine-system-task.php ignoreErrors: # The inventory repository intentionally guards $wpdb with isset()/method_exists() diff --git a/stubs/phpstan/datamachine-runtime.php b/stubs/phpstan/datamachine-runtime.php new file mode 100644 index 00000000..9e5bf989 --- /dev/null +++ b/stubs/phpstan/datamachine-runtime.php @@ -0,0 +1,126 @@ +handler_type = $handler_type; + } + + abstract protected function executeFetch( array $config, ExecutionContext $context ): array; + + protected function applyTimeframeFilter( int $timestamp, string $timeframe_limit ): bool { + return true; + } + + public function applyKeywordSearch( string $text, string $search ): bool { + return true; + } + + public function applyExcludeKeywords( string $text, string $exclude_keywords ): bool { + return true; + } + } + + abstract class FetchHandlerSettings extends SettingsHandler { + public static function get_common_fields(): array { + return array(); + } + } +} + +namespace DataMachine\Core\Steps\Publish\Handlers { + + abstract class PublishHandler { + protected string $handler_type; + + public function __construct( string $handler_type ) { + $this->handler_type = $handler_type; + } + + protected function successResponse( array $data ): array { + return $data; + } + + protected function errorResponse( string $error_message, ?array $context = null, string $severity = 'warning' ): array { + return array(); + } + + protected function log( string $level, string $message, array $context = array() ): void { + } + } +} + +namespace DataMachine\Core\Steps\Upsert\Handlers { + + abstract class UpsertHandler { + abstract protected function executeUpsert( array $parameters, array $handler_config ): array; + + protected function errorResponse( string $message, array $context = array(), string $level = 'error' ): array { + return array(); + } + } +} + +namespace DataMachine\Engine\AI\Tools { + + abstract class BaseTool { + protected function registerTool( string $tool_name, array|callable $tool_definition, array $modes = array(), array $meta = array() ): void { + } + + protected function buildErrorResponse( string $error, string $tool_name ): array { + return array(); + } + } +} + +namespace AgentsAPI\AI\Tools { + + interface WP_Agent_Tool_Executor { + public function executeWP_Agent_Tool_Call( array $tool_call, array $tool_definition, array $context = array() ): array; + } +}