diff --git a/inc/Bundle/WorkspacePreloadArtifact.php b/inc/Bundle/WorkspacePreloadArtifact.php index 4f4b754..8533350 100644 --- a/inc/Bundle/WorkspacePreloadArtifact.php +++ b/inc/Bundle/WorkspacePreloadArtifact.php @@ -174,7 +174,7 @@ private function clone_repository( array $repository ): array|\WP_Error { $input['full'] = $repository['full']; } - $callback = $this->clone_callback ?: array( WorkspaceAbilities::class, 'cloneRepo' ); + $callback = $this->clone_callback ? $this->clone_callback : array( WorkspaceAbilities::class, 'cloneRepo' ); $result = call_user_func( $callback, $input ); if ( is_wp_error( $result ) && 'repo_exists' === $result->get_error_code() ) { diff --git a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php index 05ede7b..0c94cfc 100644 --- a/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php +++ b/inc/Cleanup/DataMachineJobCleanupRunEvidenceStore.php @@ -158,10 +158,10 @@ private function aggregate_cleanup_child_jobs( array $child_jobs ): array { $summary['artifact_cleanup']['freed_human'] = $this->format_bytes( $summary['artifact_cleanup']['bytes_reclaimed'] ); $summary['cleanup_items']['freed_human'] = $this->format_bytes( $summary['cleanup_items']['bytes_reclaimed'] ); - $summary['children']['batch_job_ids'] = array_values( array_unique( $summary['children']['batch_job_ids'] ) ); - $summary['children']['chunk_job_ids'] = array_values( array_unique( $summary['children']['chunk_job_ids'] ) ); - $summary['children']['job_ids'] = array_values( array_unique( $summary['children']['job_ids'] ) ); - $summary['children']['running'] = (int) $summary['children']['processing']; + $summary['children']['batch_job_ids'] = array_values( array_unique( $summary['children']['batch_job_ids'] ) ); + $summary['children']['chunk_job_ids'] = array_values( array_unique( $summary['children']['chunk_job_ids'] ) ); + $summary['children']['job_ids'] = array_values( array_unique( $summary['children']['job_ids'] ) ); + $summary['children']['running'] = (int) $summary['children']['processing']; return $summary; } @@ -440,11 +440,12 @@ private function cleanup_run_job_id( string $run_id ): int { * @return string */ private function format_bytes( int $bytes ): string { - $bytes = max( 0, $bytes ); - $units = array( 'B', 'KiB', 'MiB', 'GiB', 'TiB' ); - $value = (float) $bytes; - $unit = 0; - while ( $value >= 1024 && $unit < count( $units ) - 1 ) { + $bytes = max( 0, $bytes ); + $units = array( 'B', 'KiB', 'MiB', 'GiB', 'TiB' ); + $max_unit = count( $units ) - 1; + $value = (float) $bytes; + $unit = 0; + while ( $value >= 1024 && $unit < $max_unit ) { $value /= 1024; ++$unit; } diff --git a/inc/Handlers/GitHub/GitHub.php b/inc/Handlers/GitHub/GitHub.php index c3e3c42..85878a3 100644 --- a/inc/Handlers/GitHub/GitHub.php +++ b/inc/Handlers/GitHub/GitHub.php @@ -394,7 +394,7 @@ private function fetchActionsArtifactItems( array $config, ExecutionContext $con return array(); } - $payload = $json_files[ $json_file ]; + $payload = $json_files[ $json_file ]; $items_payload = $this->selectArtifactItems( $payload, (string) ( $config['items_path'] ?? '' ) ); if ( empty( $items_payload ) ) { $context->log( 'info', sprintf( 'GitHub: Artifact JSON file %s contained no items.', $json_file ) ); @@ -414,7 +414,7 @@ private function fetchActionsArtifactItems( array $config, ExecutionContext $con $head_sha, $artifact_name, $json_file, - hash( 'sha256', wp_json_encode( $item, JSON_UNESCAPED_SLASHES ) ?: (string) $index ) + hash( 'sha256', wp_json_encode( $item, JSON_UNESCAPED_SLASHES ) ?: (string) $index ) // phpcs:ignore Universal.Operators.DisallowShortTernary.Found -- Avoids double-encoding side effect. ); $title = (string) ( $item['title'] ?? $item['selector'] ?? $item['kind'] ?? '' ); @@ -426,18 +426,18 @@ private function fetchActionsArtifactItems( array $config, ExecutionContext $con 'title' => $title, 'content' => wp_json_encode( $item, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), 'metadata' => array( - 'source_type' => 'github_actions_artifact', - 'item_identifier' => $item_identifier, - 'original_id' => $item_identifier, - 'dedup_key' => $item_identifier, - 'original_title' => $title, - 'github_repo' => $repo, - 'github_type' => 'actions_artifact_items', - 'github_head_sha' => $head_sha, - 'artifact_name' => $artifact_name, - 'artifact_json_file' => $json_file, + 'source_type' => 'github_actions_artifact', + 'item_identifier' => $item_identifier, + 'original_id' => $item_identifier, + 'dedup_key' => $item_identifier, + 'original_title' => $title, + 'github_repo' => $repo, + 'github_type' => 'actions_artifact_items', + 'github_head_sha' => $head_sha, + 'artifact_name' => $artifact_name, + 'artifact_json_file' => $json_file, 'artifact_item_index' => (int) $index, - 'artifact_item' => $item, + 'artifact_item' => $item, ), ); } @@ -579,7 +579,7 @@ private function fetchFiles( array $config, ExecutionContext $context, string $r $context->log( 'debug', sprintf( 'GitHub: Skipped %s — no file content returned.', $file['path'] ) ); continue; } - $guid = sprintf( 'github_%s_files_%s', $repo, $file['sha'] ); + $guid = sprintf( 'github_%s_files_%s', $repo, $file['sha'] ); $eligible_items[] = array( 'title' => $file_data['path'], @@ -619,10 +619,10 @@ private function fetchFiles( array $config, ExecutionContext $context, string $r * @return array DataPacket-compatible array or empty on no data. */ private function fetchIssuesOrPulls( array $config, ExecutionContext $context, string $repo, string $data_source ): array { - $state = $config['state'] ?? 'open'; - $labels = $config['labels'] ?? ''; - $issue_number = (int) ( $config['issue_number'] ?? 0 ); - $pull_number = (int) ( $config['pull_number'] ?? 0 ); + $state = $config['state'] ?? 'open'; + $labels = $config['labels'] ?? ''; + $issue_number = (int) ( $config['issue_number'] ?? 0 ); + $pull_number = (int) ( $config['pull_number'] ?? 0 ); // Targeted single-item fetch by issue_number / pull_number. if ( $issue_number > 0 || $pull_number > 0 ) { @@ -659,11 +659,11 @@ private function fetchIssuesOrPulls( array $config, ExecutionContext $context, s $context->log( 'info', sprintf( 'GitHub: Found %d %s.', count( $items ), $data_source ) ); - $search = $config['search'] ?? ''; - $exclude_keywords = $config['exclude_keywords'] ?? ''; - $exclude_labels_raw = $config['exclude_labels'] ?? ''; - $timeframe_limit = $config['timeframe_limit'] ?? 'all_time'; - $eligible_items = array(); + $search = $config['search'] ?? ''; + $exclude_keywords = $config['exclude_keywords'] ?? ''; + $exclude_labels_raw = $config['exclude_labels'] ?? ''; + $timeframe_limit = $config['timeframe_limit'] ?? 'all_time'; + $eligible_items = array(); $exclude_labels = array(); if ( ! empty( $exclude_labels_raw ) ) { @@ -691,7 +691,7 @@ private function fetchIssuesOrPulls( array $config, ExecutionContext $context, s static fn( $label ) => strtolower( (string) $label ), (array) $item['labels'] ); - $hit = array_values( array_intersect( $item_labels_lower, $exclude_labels ) ); + $hit = array_values( array_intersect( $item_labels_lower, $exclude_labels ) ); if ( ! empty( $hit ) ) { $context->log( 'debug', sprintf( 'GitHub: skipping #%d — excluded by label(s): %s', @@ -802,8 +802,8 @@ private function fetchSingleIssueOrPull( $context->log( 'info', 'GitHub: targeted fetch ignores list filters: ' . implode( ', ', $ignored_fields ) ); } - $number = $issue_number > 0 ? $issue_number : $pull_number; - $expected_state = $config['state'] ?? 'open'; + $number = $issue_number > 0 ? $issue_number : $pull_number; + $expected_state = $config['state'] ?? 'open'; if ( 'pulls' === $data_source ) { $result = GitHubAbilities::getPull( array( diff --git a/inc/Runtime/WordPressRuntimeInspector.php b/inc/Runtime/WordPressRuntimeInspector.php index 7a64551..924bbae 100644 --- a/inc/Runtime/WordPressRuntimeInspector.php +++ b/inc/Runtime/WordPressRuntimeInspector.php @@ -135,10 +135,14 @@ public function read( array $input ): array|\WP_Error { $size = (int) filesize( $resolved['real_path'] ); if ( $size > $max_size ) { - return new \WP_Error( 'datamachine_runtime_file_too_large', 'File exceeds the requested max_size.', array( 'path' => $resolved['relative_path'], 'size' => $size, 'max_size' => $max_size ) ); + return new \WP_Error( 'datamachine_runtime_file_too_large', 'File exceeds the requested max_size.', array( + 'path' => $resolved['relative_path'], + 'size' => $size, + 'max_size' => $max_size, + ) ); } - $sample = @file_get_contents( $resolved['real_path'], false, null, 0, min( $size, 8192 ) ); + $sample = @file_get_contents( $resolved['real_path'], false, null, 0, min( $size, 8192 ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,WordPress.PHP.NoSilencedErrors.Discouraged -- Path is validated by resolveReadablePath(). if ( false === $sample ) { return new \WP_Error( 'datamachine_runtime_unreadable', 'File is not readable.' ); } @@ -147,7 +151,7 @@ public function read( array $input ): array|\WP_Error { return new \WP_Error( 'datamachine_runtime_binary_file', 'Binary file reading is denied.', array( 'path' => $resolved['relative_path'] ) ); } - $lines = @file( $resolved['real_path'], FILE_IGNORE_NEW_LINES ); + $lines = @file( $resolved['real_path'], FILE_IGNORE_NEW_LINES ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file,WordPress.PHP.NoSilencedErrors.Discouraged -- Path is validated by resolveReadablePath(). if ( false === $lines ) { return new \WP_Error( 'datamachine_runtime_unreadable', 'File is not readable.' ); } diff --git a/inc/Storage/CleanupRunRepository.php b/inc/Storage/CleanupRunRepository.php index 0891c20..82ff331 100644 --- a/inc/Storage/CleanupRunRepository.php +++ b/inc/Storage/CleanupRunRepository.php @@ -104,7 +104,9 @@ public function add_items( string $run_id, array $items ): int|\WP_Error { public function get_run( string $run_id ): ?array { global $wpdb; + // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . CleanupSchema::runs_table() . ' WHERE run_id = %s', $run_id ), ARRAY_A ); + // phpcs:enable WordPress.DB.PreparedSQL return is_array( $row ) ? $this->decode_run( $row ) : null; } @@ -117,7 +119,9 @@ public function get_run( string $run_id ): ?array { public function get_items( string $run_id ): array { global $wpdb; + // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . CleanupSchema::items_table() . ' WHERE run_id = %s ORDER BY id ASC', $run_id ), ARRAY_A ); + // phpcs:enable WordPress.DB.PreparedSQL return array_map( fn( $row ) => $this->decode_item( (array) $row ), is_array( $rows ) ? $rows : array() ); } diff --git a/inc/Storage/WorktreeInventoryRepository.php b/inc/Storage/WorktreeInventoryRepository.php index cc84a7b..d49621d 100644 --- a/inc/Storage/WorktreeInventoryRepository.php +++ b/inc/Storage/WorktreeInventoryRepository.php @@ -166,7 +166,9 @@ public function list( ?string $repo = null ): array { $table = self::table_name(); if ( null !== $repo && '' !== trim( $repo ) && method_exists( $wpdb, 'prepare' ) ) { + // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE repo = %s ORDER BY handle ASC", $repo ); + // phpcs:enable WordPress.DB.PreparedSQL } else { $sql = "SELECT * FROM {$table} ORDER BY handle ASC"; } @@ -253,7 +255,7 @@ private function normalize_row( array $row ): array { * @return array */ private function decode_row( array $row ): array { - $decoded = isset( $row['metadata'] ) && is_string( $row['metadata'] ) ? json_decode( $row['metadata'], true ) : null; + $decoded = isset( $row['metadata'] ) && is_string( $row['metadata'] ) ? json_decode( $row['metadata'], true ) : null; $row['metadata'] = is_array( $decoded ) ? $decoded : null; foreach ( array( 'id', 'is_primary', 'dirty_count', 'unpushed_count', 'artifact_count', 'artifact_size_bytes', 'size_bytes', 'missing_path' ) as $key ) { if ( isset( $row[ $key ] ) ) { diff --git a/inc/Workspace/CleanupRunService.php b/inc/Workspace/CleanupRunService.php index 6726cb0..55d4439 100644 --- a/inc/Workspace/CleanupRunService.php +++ b/inc/Workspace/CleanupRunService.php @@ -18,7 +18,7 @@ public function __construct( private ?Workspace $workspace = null ) { $this->repository ??= new CleanupRunRepository(); - $this->workspace ??= new Workspace(); + $this->workspace ??= new Workspace(); } /** @@ -55,9 +55,9 @@ public function plan( array $opts = array() ): array|\WP_Error { $plan['run_id'] = $run_id; $plan['cleanup_storage'] = array( - 'type' => 'database', - 'item_count' => $inserted, - 'plan_id' => $plan['plan_id'] ?? null, + 'type' => 'database', + 'item_count' => $inserted, + 'plan_id' => $plan['plan_id'] ?? null, 'escape_hatch' => 'filesystem apply-plan import remains available on lower-level worktree commands only', ); @@ -77,7 +77,10 @@ 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 ) ); } - $this->repository->update_run( $run_id, array( 'status' => 'applying', 'started_at' => gmdate( 'Y-m-d H:i:s' ) ) ); + $this->repository->update_run( $run_id, array( + 'status' => 'applying', + 'started_at' => gmdate( 'Y-m-d H:i:s' ), + ) ); $items = $this->repository->get_items( $run_id ); $artifact_rows = $this->pending_rows_of_type( $items, 'artifact_cleanup' ); @@ -105,7 +108,11 @@ public function apply( string $run_id, array $opts = array() ): array|\WP_Error $this->record_apply_result( $worktree_rows, $results['worktree_removal'], 'removed' ); } - $this->repository->update_run( $run_id, array( 'status' => 'completed', 'completed_at' => gmdate( 'Y-m-d H:i:s' ), 'summary' => $this->status( $run_id )['summary'] ?? array() ) ); + $this->repository->update_run( $run_id, array( + 'status' => 'completed', + 'completed_at' => gmdate( 'Y-m-d H:i:s' ), + 'summary' => $this->status( $run_id )['summary'] ?? array(), + ) ); return array( 'success' => true, @@ -138,8 +145,8 @@ public function status( string $run_id ): array|\WP_Error { 'pending_or_failed' => 0, ); foreach ( $items as $item ) { - $status = (string) ( $item['status'] ?? 'unknown' ); - $type = (string) ( $item['item_type'] ?? 'unknown' ); + $status = (string) ( $item['status'] ?? 'unknown' ); + $type = (string) ( $item['item_type'] ?? 'unknown' ); $summary['items_by_status'][ $status ] = ( $summary['items_by_status'][ $status ] ?? 0 ) + 1; $summary['items_by_type'][ $type ] = ( $summary['items_by_type'][ $type ] ?? 0 ) + 1; $summary['bytes_reclaimed'] += max( 0, (int) ( $item['bytes_reclaimed'] ?? 0 ) ); @@ -190,7 +197,10 @@ public function cancel( string $run_id ): array|\WP_Error { $this->repository->update_item( (int) $item['id'], array( 'status' => 'cancelled' ) ); } } - $this->repository->update_run( $run_id, array( 'status' => 'cancelled', 'completed_at' => gmdate( 'Y-m-d H:i:s' ) ) ); + $this->repository->update_run( $run_id, array( + 'status' => 'cancelled', + 'completed_at' => gmdate( 'Y-m-d H:i:s' ), + ) ); return $this->status( $run_id ); } @@ -227,13 +237,17 @@ private function plan_items( array $plan ): array { } private function pending_rows_of_type( array $items, string $type ): array { - return array_values( array_filter( $items, fn( $item ) => $type === (string) ( $item['item_type'] ?? '' ) && in_array( (string) ( $item['status'] ?? '' ), array( 'pending', 'failed' ), true ) ) ); + return array_values( array_filter( $items, fn( $item ) => (string) ( $item['item_type'] ?? '' ) === $type && in_array( (string) ( $item['status'] ?? '' ), array( 'pending', 'failed' ), true ) ) ); } private function record_apply_result( array $items, mixed $result, string $applied_key ): void { if ( $result instanceof \WP_Error ) { foreach ( $items as $item ) { - $this->repository->update_item( (int) $item['id'], array( 'status' => 'failed', 'reason_code' => $result->get_error_code(), 'reason' => $result->get_error_message() ) ); + $this->repository->update_item( (int) $item['id'], array( + 'status' => 'failed', + 'reason_code' => $result->get_error_code(), + 'reason' => $result->get_error_message(), + ) ); } return; } diff --git a/inc/Workspace/WorkspaceArtifactCleanup.php b/inc/Workspace/WorkspaceArtifactCleanup.php index f7c2bb0..c2d62fe 100644 --- a/inc/Workspace/WorkspaceArtifactCleanup.php +++ b/inc/Workspace/WorkspaceArtifactCleanup.php @@ -137,7 +137,7 @@ public function worktree_cleanup_artifacts( array $opts = array() ): array|\WP_E 'reason' => sprintf( 'failed to remove artifact %s: %s', (string) ( $artifact['path'] ?? '' ), $remove->get_error_message() ), 'artifacts' => array( $artifact ), ); - $failed = true; + $failed = true; break; } @@ -417,7 +417,7 @@ private function build_worktree_artifact_cleanup_summary( array $candidates, arr foreach ( $candidates as $row ) { $repo = (string) ( $row['repo'] ?? 'unknown' ); foreach ( (array) ( $row['artifacts'] ?? array() ) as $artifact ) { - $bytes = (int) ( is_array( $artifact ) ? ( $artifact['size_bytes'] ?? 0 ) : 0 ); + $bytes = (int) ( is_array( $artifact ) ? ( $artifact['size_bytes'] ?? 0 ) : 0 ); $would_bytes += max( 0, $bytes ); ++$would_count; $artifact_by_repo[ $repo ] = ( $artifact_by_repo[ $repo ] ?? 0 ) + max( 0, $bytes ); @@ -522,7 +522,7 @@ private function scope_worktree_artifact_cleanup_to_plan( array $planned_candida } } - $skip = $complete ? array( + $skip = $complete ? array( 'handle' => $handle, 'repo' => (string) ( $plan_row['repo'] ?? '' ), 'branch' => (string) ( $plan_row['branch'] ?? '' ), @@ -674,5 +674,4 @@ private function is_active_studio_symlink_target( string $worktree_path ): bool return false; } - } diff --git a/inc/Workspace/WorkspaceCleanupPlan.php b/inc/Workspace/WorkspaceCleanupPlan.php index 72c62fd..2aa1bb4 100644 --- a/inc/Workspace/WorkspaceCleanupPlan.php +++ b/inc/Workspace/WorkspaceCleanupPlan.php @@ -78,8 +78,8 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error 'resolver' => $inputs['include_resolvers'] ? $this->build_cleanup_plan_resolver_rows( (array) ( $worktree_plan['skipped'] ?? array() ) ) : array(), ); - $summary = $this->build_cleanup_plan_summary( $rows ); - $plan = array( + $summary = $this->build_cleanup_plan_summary( $rows ); + $plan = array( 'success' => true, 'mode' => 'cleanup_plan', 'generated_at' => gmdate( 'c' ), @@ -87,9 +87,9 @@ public function workspace_cleanup_plan( array $opts = array() ): array|\WP_Error 'inputs' => $inputs, 'safety_policy' => array( 'applies_inline' => false, - 'artifact_cleanup' => 'apply-plan must revalidate profile-derived artifact paths before deletion', - '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', + 'artifact_cleanup' => 'apply-plan must revalidate profile-derived artifact paths before deletion', + '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( @@ -126,7 +126,7 @@ public function workspace_cleanup_plan_chunks( array $opts = array() ): array|\W $plan_id = (string) ( $plan['plan_id'] ?? '' ); if ( '' === $plan_id ) { - $plan_id = $this->stable_cleanup_hash( array( 'rows' => $this->cleanup_row_ids( (array) ( $plan['rows'] ?? array() ) ) ), 'cleanup-plan' ); + $plan_id = $this->stable_cleanup_hash( array( 'rows' => $this->cleanup_row_ids( (array) ( $plan['rows'] ?? array() ) ) ), 'cleanup-plan' ); $plan['plan_id'] = $plan_id; } @@ -152,17 +152,17 @@ public function workspace_cleanup_plan_chunks( array $opts = array() ): array|\W ++$index; $row_ids = array_map( fn( $row ) => (string) ( $row['row_id'] ?? '' ), $chunk_rows ); $chunks[] = array( - 'chunk_id' => $this->stable_cleanup_hash( array( $plan_id, $type, $safety_class, $index, $row_ids ), 'cleanup-chunk' ), - 'plan_id' => $plan_id, - 'type' => (string) $type, - 'safety_class' => $safety_class, - 'index' => $index, - 'chunk_size' => count( $chunk_rows ), - 'max_rows' => $chunk_size, - 'row_ids' => $row_ids, - 'rows' => $chunk_rows, - 'idempotency' => array( - 'key' => $this->stable_cleanup_hash( array( $plan_id, $row_ids ), 'cleanup-idempotency' ), + 'chunk_id' => $this->stable_cleanup_hash( array( $plan_id, $type, $safety_class, $index, $row_ids ), 'cleanup-chunk' ), + 'plan_id' => $plan_id, + 'type' => (string) $type, + 'safety_class' => $safety_class, + 'index' => $index, + 'chunk_size' => count( $chunk_rows ), + 'max_rows' => $chunk_size, + 'row_ids' => $row_ids, + 'rows' => $chunk_rows, + 'idempotency' => array( + 'key' => $this->stable_cleanup_hash( array( $plan_id, $row_ids ), 'cleanup-idempotency' ), 'revalidate_before_apply' => true, ), 'workspace_path' => $plan['workspace_path'] ?? $this->workspace_path, @@ -233,7 +233,7 @@ private function build_cleanup_plan_resolver_rows( array $skipped ): array { default => 'workspace worktree cleanup --dry-run --skip-github --format=json', }; - $resolver = array( + $resolver = array( 'handle' => (string) ( $row['handle'] ?? '' ), 'repo' => (string) ( $row['repo'] ?? '' ), 'branch' => (string) ( $row['branch'] ?? '' ), @@ -270,7 +270,7 @@ private function build_cleanup_plan_summary( array $rows ): array { $byte_totals[ $type ] = 0; $total_rows += $counts[ $type ]; foreach ( (array) $typed_rows as $row ) { - $bytes = max( 0, (int) ( $row['artifact_size_bytes'] ?? $row['size_bytes'] ?? 0 ) ); + $bytes = max( 0, (int) ( $row['artifact_size_bytes'] ?? $row['size_bytes'] ?? 0 ) ); $byte_totals[ $type ] += $bytes; $total_bytes += $bytes; } @@ -298,11 +298,11 @@ private function build_cleanup_chunk_summary( array $chunks, array $plan ): arra $chunks_by_safety = array(); $rows_by_type = array(); foreach ( $chunks as $chunk ) { - $type = (string) ( $chunk['type'] ?? 'unknown' ); - $class = (string) ( $chunk['safety_class'] ?? 'unknown' ); - $chunks_by_type[ $type ] = ( $chunks_by_type[ $type ] ?? 0 ) + 1; - $chunks_by_safety[ $class ] = ( $chunks_by_safety[ $class ] ?? 0 ) + 1; - $rows_by_type[ $type ] = ( $rows_by_type[ $type ] ?? 0 ) + (int) ( $chunk['chunk_size'] ?? 0 ); + $type = (string) ( $chunk['type'] ?? 'unknown' ); + $class = (string) ( $chunk['safety_class'] ?? 'unknown' ); + $chunks_by_type[ $type ] = ( $chunks_by_type[ $type ] ?? 0 ) + 1; + $chunks_by_safety[ $class ] = ( $chunks_by_safety[ $class ] ?? 0 ) + 1; + $rows_by_type[ $type ] = ( $rows_by_type[ $type ] ?? 0 ) + (int) ( $chunk['chunk_size'] ?? 0 ); } ksort( $chunks_by_type ); ksort( $chunks_by_safety ); @@ -395,5 +395,4 @@ private function ksort_recursive( mixed &$value ): void { ksort( $value ); } } - } diff --git a/inc/Workspace/WorkspaceHygieneReport.php b/inc/Workspace/WorkspaceHygieneReport.php index cbd9bba..80b0115 100644 --- a/inc/Workspace/WorkspaceHygieneReport.php +++ b/inc/Workspace/WorkspaceHygieneReport.php @@ -207,38 +207,38 @@ public function workspace_retention_cleanup( array $opts = array() ): array|\WP_ * @return array|\WP_Error */ public function workspace_disk_emergency_cleanup( array $opts = array() ): array|\WP_Error { - $dry_run = ! empty( $opts['dry_run'] ); - $artifact_chunk_size = isset( $opts['artifact_chunk_size'] ) && is_numeric( $opts['artifact_chunk_size'] ) ? max( 1, (int) $opts['artifact_chunk_size'] ) : 10; - $allow_worktree_deletion = ! empty( $opts['allow_worktree_deletion'] ); - $human_approved_deletion = ! empty( $opts['human_approved_worktree_deletion'] ); - $force_worktree_deletion = ! empty( $opts['force'] ) && $human_approved_deletion; - $thresholds = isset( $opts['thresholds'] ) && is_array( $opts['thresholds'] ) ? $opts['thresholds'] : WorktreeDiskBudget::thresholds( 'workspace', 'emergency-cleanup' ); - $budget = WorktreeDiskBudget::inspect( $this->workspace_path, $thresholds ); + $dry_run = ! empty( $opts['dry_run'] ); + $artifact_chunk_size = isset( $opts['artifact_chunk_size'] ) && is_numeric( $opts['artifact_chunk_size'] ) ? max( 1, (int) $opts['artifact_chunk_size'] ) : 10; + $allow_worktree_deletion = ! empty( $opts['allow_worktree_deletion'] ); + $human_approved_deletion = ! empty( $opts['human_approved_worktree_deletion'] ); + $force_worktree_deletion = ! empty( $opts['force'] ) && $human_approved_deletion; + $thresholds = isset( $opts['thresholds'] ) && is_array( $opts['thresholds'] ) ? $opts['thresholds'] : WorktreeDiskBudget::thresholds( 'workspace', 'emergency-cleanup' ); + $budget = WorktreeDiskBudget::inspect( $this->workspace_path, $thresholds ); $plan = $this->worktree_emergency_cleanup( array( 'dry_run' => true ) ); if ( $plan instanceof \WP_Error ) { return $plan; } - $artifact_candidates = (array) ( $plan['artifact_candidates'] ?? array() ); - $worktree_candidates = (array) ( $plan['worktree_candidates'] ?? array() ); - $top_artifact_offenders = $this->summarize_top_worktree_rows( $artifact_candidates, 'artifact_size_bytes' ); + $artifact_candidates = (array) ( $plan['artifact_candidates'] ?? array() ); + $worktree_candidates = (array) ( $plan['worktree_candidates'] ?? array() ); + $top_artifact_offenders = $this->summarize_top_worktree_rows( $artifact_candidates, 'artifact_size_bytes' ); $budget['top_artifact_offenders'] = $top_artifact_offenders; $triggered = ! empty( $budget['emergency_triggered'] ); if ( ! $triggered ) { return array( - 'success' => true, - 'triggered' => false, - 'skipped' => true, - 'reason' => 'disk thresholds not crossed', - 'dry_run' => $dry_run, - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'disk_budget' => $budget, - 'emergency_plan' => $plan, - 'action_required' => false, - 'applied' => null, + 'success' => true, + 'triggered' => false, + 'skipped' => true, + 'reason' => 'disk thresholds not crossed', + 'dry_run' => $dry_run, + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'disk_budget' => $budget, + 'emergency_plan' => $plan, + 'action_required' => false, + 'applied' => null, ); } @@ -296,11 +296,11 @@ public function workspace_disk_emergency_cleanup( array $opts = array() ): array 'action_required' => array() !== $blocked_reasons || ( array() === $selected_artifacts && array() !== $worktree_candidates && array() === $selected_worktrees ), 'action_required_reasons' => $blocked_reasons, 'policy' => array( - 'artifact_first' => true, - 'allow_worktree_deletion' => $allow_worktree_deletion, - 'human_approved_worktree_deletion' => $human_approved_deletion, - 'force_requires_human_approval' => true, - 'force_worktree_deletion_applied' => $force_worktree_deletion, + 'artifact_first' => true, + 'allow_worktree_deletion' => $allow_worktree_deletion, + 'human_approved_worktree_deletion' => $human_approved_deletion, + 'force_requires_human_approval' => true, + 'force_worktree_deletion_applied' => $force_worktree_deletion, ), ); } @@ -346,29 +346,29 @@ private function build_workspace_inventory_rows(): array { $task_view = is_array( $metadata ) && is_array( $metadata['origin_task'] ?? null ) ? $metadata['origin_task'] : null; $row = array( - 'handle' => $parsed['dir_name'], - 'repo' => $parsed['repo'], - 'kind' => $kind, - 'is_worktree' => $is_worktree, - 'is_primary' => 'primary' === $kind, - 'external' => false, - 'branch_slug' => $parsed['branch_slug'], - 'branch' => is_array( $metadata ) && ! empty( $metadata['branch'] ) ? (string) $metadata['branch'] : (string) ( $parsed['branch_slug'] ?? '' ), - 'path' => $path, - 'dirty' => 0, - 'created_at' => is_array( $metadata ) ? ( $metadata['created_at'] ?? null ) : null, - 'lifecycle_state' => is_array( $metadata ) ? ( $metadata['lifecycle_state'] ?? null ) : null, - 'pr_url' => is_array( $metadata ) ? ( $metadata['pr_url'] ?? null ) : null, - 'pr_number' => is_array( $metadata ) ? ( $metadata['pr_number'] ?? null ) : null, - 'last_seen_at' => is_array( $metadata ) ? ( $metadata['last_seen_at'] ?? null ) : null, - 'liveness' => $liveness['liveness'], - 'liveness_reason' => $liveness['reason'], + 'handle' => $parsed['dir_name'], + 'repo' => $parsed['repo'], + 'kind' => $kind, + 'is_worktree' => $is_worktree, + 'is_primary' => 'primary' === $kind, + 'external' => false, + 'branch_slug' => $parsed['branch_slug'], + 'branch' => is_array( $metadata ) && ! empty( $metadata['branch'] ) ? (string) $metadata['branch'] : (string) ( $parsed['branch_slug'] ?? '' ), + 'path' => $path, + 'dirty' => 0, + 'created_at' => is_array( $metadata ) ? ( $metadata['created_at'] ?? null ) : null, + 'lifecycle_state' => is_array( $metadata ) ? ( $metadata['lifecycle_state'] ?? null ) : null, + 'pr_url' => is_array( $metadata ) ? ( $metadata['pr_url'] ?? null ) : null, + 'pr_number' => is_array( $metadata ) ? ( $metadata['pr_number'] ?? null ) : null, + 'last_seen_at' => is_array( $metadata ) ? ( $metadata['last_seen_at'] ?? null ) : null, + 'liveness' => $liveness['liveness'], + 'liveness_reason' => $liveness['reason'], 'heartbeat_age_seconds' => $liveness['heartbeat_age_seconds'], - 'owner' => $owner, - 'session' => $session_view, - 'task' => $task_view, - 'missing_metadata' => $is_worktree && ! is_array( $metadata ), - 'metadata' => $metadata, + 'owner' => $owner, + 'session' => $session_view, + 'task' => $task_view, + 'missing_metadata' => $is_worktree && ! is_array( $metadata ), + 'metadata' => $metadata, ); $base_branch_warning = $this->base_branch_worktree_warning( $row ); @@ -525,24 +525,24 @@ private function build_workspace_disk_report(): array { */ private function summarize_workspace_worktrees( array $worktrees, ?array $cleanup ): array { $summary = array( - 'total' => count( $worktrees ), - 'primaries' => 0, - 'worktrees' => 0, - 'artifacts' => 0, - 'external' => 0, - 'dirty' => 0, - 'protected_dirty' => 0, - 'protected_unpushed' => 0, - 'missing_metadata' => 0, + 'total' => count( $worktrees ), + 'primaries' => 0, + 'worktrees' => 0, + 'artifacts' => 0, + 'external' => 0, + 'dirty' => 0, + 'protected_dirty' => 0, + 'protected_unpushed' => 0, + 'missing_metadata' => 0, 'base_branch_worktree_count' => 0, - 'base_branch_worktrees' => array(), - 'by_liveness' => array( + 'base_branch_worktrees' => array(), + 'by_liveness' => array( WorktreeContextInjector::LIVENESS_LIVE => 0, WorktreeContextInjector::LIVENESS_STOPPED => 0, WorktreeContextInjector::LIVENESS_STALE => 0, WorktreeContextInjector::LIVENESS_UNKNOWN => 0, ), - 'duplicate_task_groups' => 0, + 'duplicate_task_groups' => 0, ); foreach ( $worktrees as $row ) { @@ -580,7 +580,7 @@ private function summarize_workspace_worktrees( array $worktrees, ?array $cleanu } } - $duplicates = WorktreeContextInjector::find_duplicate_task_ownership( $worktrees ); + $duplicates = WorktreeContextInjector::find_duplicate_task_ownership( $worktrees ); $summary['duplicate_task_groups'] = count( $duplicates ); $summary['duplicates'] = $duplicates; @@ -665,8 +665,8 @@ private function cleanup_storage_status(): array { return $status; } - $runs_table = $wpdb->prefix . 'datamachine_code_cleanup_runs'; - $items_table = $wpdb->prefix . 'datamachine_code_cleanup_items'; + $runs_table = $wpdb->prefix . 'datamachine_code_cleanup_runs'; + $items_table = $wpdb->prefix . 'datamachine_code_cleanup_items'; $status['cleanup_runs_available'] = $this->database_table_exists( $runs_table ); $status['cleanup_items_available'] = $this->database_table_exists( $items_table ); if ( $status['cleanup_runs_available'] && $status['cleanup_items_available'] ) { @@ -894,5 +894,4 @@ private function format_bytes( int $bytes ): string { return sprintf( $index > 0 ? '%.1f %s' : '%.0f %s', $value, $units[ $index ] ); } - } diff --git a/inc/Workspace/WorkspaceLockStore.php b/inc/Workspace/WorkspaceLockStore.php index f40eaa7..ee53b00 100644 --- a/inc/Workspace/WorkspaceLockStore.php +++ b/inc/Workspace/WorkspaceLockStore.php @@ -66,7 +66,10 @@ public static function register_acquired( array $args ): int|\WP_Error { return new \WP_Error( 'workspace_lock_db_insert_failed', 'Failed to record workspace lock ownership in the database.', - array( 'status' => 500, 'wpdb_error' => (string) $wpdb->last_error ) + array( + 'status' => 500, + 'wpdb_error' => (string) $wpdb->last_error, + ) ); } @@ -128,6 +131,7 @@ public static function status(): array { $table = self::table_name(); $now = gmdate( 'Y-m-d H:i:s' ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, not user input. return array( 'available' => true, 'table' => $table, @@ -136,6 +140,7 @@ public static function status(): array { 'released' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", 'released' ) ), 'total' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ), ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** @@ -147,31 +152,33 @@ public static function prune_expired(): array { $status = self::status(); if ( empty( $status['available'] ) ) { return array( - 'available' => false, + 'available' => false, 'active_marked_stale' => 0, - 'released_deleted' => 0, - 'before' => $status, - 'after' => $status, + 'released_deleted' => 0, + 'before' => $status, + 'after' => $status, ); } 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' ); $released_cutoff = gmdate( 'Y-m-d H:i:s', time() - self::released_ttl_seconds() ); + // phpcs:disable WordPress.DB.PreparedSQL -- Table name from $wpdb->prefix, not user input. $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s WHERE status = %s AND expires_at < %s", 'stale', 'active', $now ) ); $marked = (int) $wpdb->rows_affected; $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE status IN (%s, %s) AND COALESCE(released_at, expires_at) < %s", 'released', 'stale', $released_cutoff ) ); + // phpcs:enable WordPress.DB.PreparedSQL $deleted = (int) $wpdb->rows_affected; return array( - 'available' => true, + 'available' => true, 'active_marked_stale' => $marked, - 'released_deleted' => $deleted, - 'before' => $status, - 'after' => self::status(), + 'released_deleted' => $deleted, + 'before' => $status, + 'after' => self::status(), ); } diff --git a/inc/Workspace/WorkspaceMetadataReconciliation.php b/inc/Workspace/WorkspaceMetadataReconciliation.php index 042816f..37387d6 100644 --- a/inc/Workspace/WorkspaceMetadataReconciliation.php +++ b/inc/Workspace/WorkspaceMetadataReconciliation.php @@ -25,7 +25,7 @@ trait WorkspaceMetadataReconciliation { * @return array|\WP_Error */ public function worktree_reconcile_metadata( array $opts = array() ): array|\WP_Error { - $started_at = microtime( true ); + $started_at = microtime( true ); $dry_run = ! empty( $opts['dry_run'] ); $apply = ! empty( $opts['apply'] ); $via_jobs = ! empty( $opts['via_jobs'] ); @@ -80,7 +80,7 @@ public function worktree_reconcile_metadata( array $opts = array() ): array|\WP_ return $listing; } - $all_worktrees = array_values( array_filter( + $all_worktrees = array_values( array_filter( (array) ( $listing['worktrees'] ?? array() ), fn( $wt ) => empty( $wt['is_primary'] ) ) ); @@ -101,14 +101,14 @@ public function worktree_reconcile_metadata( array $opts = array() ): array|\WP_ break; } $row_started = microtime( true ); - $proposal = $this->build_worktree_metadata_reconciliation_row( $wt, $github_cache, $fetched ); - $elapsed_ms = (int) round( ( microtime( true ) - $row_started ) * 1000 ); + $proposal = $this->build_worktree_metadata_reconciliation_row( $wt, $github_cache, $fetched ); + $elapsed_ms = (int) round( ( microtime( true ) - $row_started ) * 1000 ); if ( isset( $proposal['proposal'] ) ) { $proposal['proposal']['elapsed_ms'] = $elapsed_ms; - $proposals[] = $proposal['proposal']; + $proposals[] = $proposal['proposal']; } elseif ( isset( $proposal['skip'] ) ) { $proposal['skip']['elapsed_ms'] = $elapsed_ms; - $skipped[] = $proposal['skip']; + $skipped[] = $proposal['skip']; } } @@ -122,23 +122,23 @@ public function worktree_reconcile_metadata( array $opts = array() ): array|\WP_ } $plan = array( - 'success' => true, - 'dry_run' => $dry_run, - 'applied' => false, - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'proposals' => $proposals, - 'written' => array(), - 'skipped' => $skipped, - 'still_unsafe' => $classified_skips['still_unsafe'], + 'success' => true, + 'dry_run' => $dry_run, + 'applied' => false, + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'proposals' => $proposals, + 'written' => array(), + 'skipped' => $skipped, + 'still_unsafe' => $classified_skips['still_unsafe'], 'external_worktrees' => $classified_skips['external_worktrees'], - 'summary' => $this->build_worktree_metadata_reconciliation_summary( $paged ? count( $page_worktrees ) : count( (array) ( $listing['worktrees'] ?? array() ) ), $proposals, array(), $skipped ), + 'summary' => $this->build_worktree_metadata_reconciliation_summary( $paged ? count( $page_worktrees ) : count( (array) ( $listing['worktrees'] ?? array() ) ), $proposals, array(), $skipped ), ); if ( null !== $pagination ) { $plan['pagination'] = $pagination; $plan['evidence'] = array( - 'scope' => 'paginated metadata reconciliation dry-run', - 'note' => 'Only this page ran per-worktree dirty, unpushed, merge-signal, and GitHub probes. Run the next_offset page until complete for full inventory review.', + 'scope' => 'paginated metadata reconciliation dry-run', + 'note' => 'Only this page ran per-worktree dirty, unpushed, merge-signal, and GitHub probes. Run the next_offset page until complete for full inventory review.', 'fields_skipped_by_listing' => (array) ( $listing['fields_skipped'] ?? array() ), ); if ( null !== $budget_context ) { @@ -240,20 +240,20 @@ private function drain_worktree_metadata_reconciliation_budget( int $limit, int } return array( - 'success' => true, - 'dry_run' => false, - 'applied' => true, - 'direct_apply' => true, - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'proposals' => $proposals, - 'written' => $written, - 'skipped' => $skipped, - 'still_unsafe' => $classified_skips['still_unsafe'], + 'success' => true, + 'dry_run' => false, + 'applied' => true, + 'direct_apply' => true, + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'proposals' => $proposals, + 'written' => $written, + 'skipped' => $skipped, + 'still_unsafe' => $classified_skips['still_unsafe'], 'external_worktrees' => $classified_skips['external_worktrees'], - 'summary' => $this->build_worktree_metadata_reconciliation_summary( $scanned, $proposals, $written, $skipped ), - 'pagination' => $pagination, - 'evidence' => array_filter( + 'summary' => $this->build_worktree_metadata_reconciliation_summary( $scanned, $proposals, $written, $skipped ), + 'pagination' => $pagination, + 'evidence' => array_filter( array( 'scope' => 'time-budgeted metadata reconciliation direct apply', 'apply_source' => 'direct_apply', @@ -300,15 +300,15 @@ private function schedule_worktree_metadata_reconciliation_pages( array $first_p if ( array() === $items ) { return array( - 'success' => true, - 'dry_run' => false, - 'applied' => false, - 'job_backed' => true, - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'proposals' => array(), - 'written' => array(), - 'skipped' => array(), + 'success' => true, + 'dry_run' => false, + 'applied' => false, + 'job_backed' => true, + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'proposals' => array(), + 'written' => array(), + 'skipped' => array(), 'still_unsafe' => array(), 'external_worktrees' => array(), 'summary' => array( @@ -319,8 +319,8 @@ private function schedule_worktree_metadata_reconciliation_pages( array $first_p 'scheduled_jobs' => 0, 'limit' => $limit, ), - 'pagination' => $pagination, - 'evidence' => array( + 'pagination' => $pagination, + 'evidence' => array( 'elapsed_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ), 'note' => 'No metadata reconciliation pages eligible for scheduling.', 'source' => $source, @@ -339,15 +339,15 @@ private function schedule_worktree_metadata_reconciliation_pages( array $first_p } return array( - 'success' => true, - 'dry_run' => false, - 'applied' => false, - 'job_backed' => true, - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'proposals' => array_values( (array) ( $first_page['proposals'] ?? array() ) ), - 'written' => array(), - 'skipped' => array(), + 'success' => true, + 'dry_run' => false, + 'applied' => false, + 'job_backed' => true, + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'proposals' => array_values( (array) ( $first_page['proposals'] ?? array() ) ), + 'written' => array(), + 'skipped' => array(), 'still_unsafe' => array_values( (array) ( $first_page['still_unsafe'] ?? array() ) ), 'external_worktrees' => array_values( (array) ( $first_page['external_worktrees'] ?? array() ) ), 'summary' => array_merge( @@ -358,8 +358,8 @@ private function schedule_worktree_metadata_reconciliation_pages( array $first_p 'limit' => $limit, ) ), - 'pagination' => $pagination, - 'evidence' => array( + 'pagination' => $pagination, + 'evidence' => array( 'elapsed_ms' => (int) round( ( microtime( true ) - $started_at ) * 1000 ), 'scope' => 'job-backed metadata reconciliation apply', 'page_offsets' => array_column( $items, 'offset' ), @@ -534,7 +534,7 @@ private function build_worktree_metadata_reconciliation_row( array $wt, array &$ ); } } - $dirty = (int) $dirty; + $dirty = (int) $dirty; $unpushed = $this->count_unpushed_commits( $path ); if ( is_wp_error( $unpushed ) ) { return array( @@ -828,7 +828,7 @@ private function detect_stored_pr_merged_signal( array $metadata, array &$github if ( array_key_exists( $cache_key, $github_cache ) ) { $pr = $github_cache[ $cache_key ]; } else { - $pr = $this->fetch_github_pull_request( $pr_repo, $pr_number ); + $pr = $this->fetch_github_pull_request( $pr_repo, $pr_number ); $github_cache[ $cache_key ] = $pr; } @@ -895,8 +895,8 @@ private function set_reconciled_metadata_field( array &$metadata, array &$source if ( null === $value || '' === (string) $value ) { return; } - $metadata[ $field ] = $value; - $source_map[ $field ] = $source; + $metadata[ $field ] = $value; + $source_map[ $field ] = $source; } /** @@ -971,8 +971,8 @@ private function apply_worktree_metadata_reconciliation_plan( array $plan ): arr $metadata['lifecycle_state'] = $state; if ( WorktreeContextInjector::STATE_CLEANUP_ELIGIBLE === $state ) { - $path = (string) ( $current['path'] ?? '' ); - $dirty = ! empty( $plan['direct_apply'] ) ? $this->probe_worktree_dirty_count( $path, self::CLEANUP_GIT_PROBE_TIMEOUT ) : (int) ( $current['dirty'] ?? 0 ); + $path = (string) ( $current['path'] ?? '' ); + $dirty = ! empty( $plan['direct_apply'] ) ? $this->probe_worktree_dirty_count( $path, self::CLEANUP_GIT_PROBE_TIMEOUT ) : (int) ( $current['dirty'] ?? 0 ); if ( is_wp_error( $dirty ) ) { $skipped[] = $this->build_reconcile_apply_skip( $row, $current, 'probe_timeout', 'dirty-state probe failed - refusing cleanup_eligible metadata write: ' . $dirty->get_error_message() ); continue; @@ -1008,18 +1008,18 @@ private function apply_worktree_metadata_reconciliation_plan( array $plan ): arr $inspected = isset( $plan['summary']['inspected'] ) ? (int) $plan['summary']['inspected'] : count( (array) ( $listing['worktrees'] ?? array() ) ); $result = array( - 'success' => true, - 'dry_run' => false, - 'applied' => true, - 'direct_apply' => ! empty( $plan['direct_apply'] ), - 'generated_at' => gmdate( 'c' ), - 'workspace_path' => $this->workspace_path, - 'proposals' => $planned, - 'written' => $written, - 'skipped' => $skipped, - 'still_unsafe' => $classified_skips['still_unsafe'], + 'success' => true, + 'dry_run' => false, + 'applied' => true, + 'direct_apply' => ! empty( $plan['direct_apply'] ), + 'generated_at' => gmdate( 'c' ), + 'workspace_path' => $this->workspace_path, + 'proposals' => $planned, + 'written' => $written, + 'skipped' => $skipped, + 'still_unsafe' => $classified_skips['still_unsafe'], 'external_worktrees' => $classified_skips['external_worktrees'], - 'summary' => $this->build_worktree_metadata_reconciliation_summary( $inspected, $planned, $written, $skipped ), + 'summary' => $this->build_worktree_metadata_reconciliation_summary( $inspected, $planned, $written, $skipped ), ); if ( isset( $plan['pagination'] ) && is_array( $plan['pagination'] ) ) { @@ -1059,7 +1059,7 @@ private function classify_worktree_metadata_reconciliation_skips( array $skipped } return array( - 'still_unsafe' => $still_unsafe, + 'still_unsafe' => $still_unsafe, 'external_worktrees' => $external_worktrees, ); } @@ -1210,5 +1210,4 @@ private function summarize_slow_worktree_rows( array $rows ): array { array_slice( $timed, 0, 10 ) ); } - } diff --git a/inc/Workspace/WorkspaceMutationLock.php b/inc/Workspace/WorkspaceMutationLock.php index 96f1f10..5585b41 100644 --- a/inc/Workspace/WorkspaceMutationLock.php +++ b/inc/Workspace/WorkspaceMutationLock.php @@ -189,10 +189,10 @@ public static function status( string $workspace_path ): array { * @return array */ public static function prune_stale( string $workspace_path, bool $dry_run = false ): array { - $before = self::status( $workspace_path ); - $db_pruned = $dry_run ? array( 'dry_run' => true ) : WorkspaceLockStore::prune_expired(); - $fs_pruned = self::prune_stale_filesystem_locks( $workspace_path, $dry_run ); - $after = self::status( $workspace_path ); + $before = self::status( $workspace_path ); + $db_pruned = $dry_run ? array( 'dry_run' => true ) : WorkspaceLockStore::prune_expired(); + $fs_pruned = self::prune_stale_filesystem_locks( $workspace_path, $dry_run ); + $after = self::status( $workspace_path ); return array( 'dry_run' => $dry_run, @@ -232,7 +232,7 @@ private static function filesystem_status( string $workspace_path ): array { } if ( ! flock( $handle, LOCK_EX | LOCK_NB ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_flock - $active++; + ++$active; fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose continue; } @@ -241,9 +241,9 @@ private static function filesystem_status( string $workspace_path ): array { fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose $mtime = filemtime( $file ); if ( false !== $mtime && $mtime < $cutoff ) { - $stale++; + ++$stale; } else { - $recent++; + ++$recent; } } @@ -271,18 +271,27 @@ private static function prune_stale_filesystem_locks( string $workspace_path, bo foreach ( $files as $file ) { $mtime = filemtime( $file ); if ( false === $mtime || $mtime >= $cutoff ) { - $skipped[] = array( 'path' => $file, 'reason' => 'not_stale' ); + $skipped[] = array( + 'path' => $file, + 'reason' => 'not_stale', + ); continue; } $handle = fopen( $file, 'c' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if ( false === $handle ) { - $skipped[] = array( 'path' => $file, 'reason' => 'open_failed' ); + $skipped[] = array( + 'path' => $file, + 'reason' => 'open_failed', + ); continue; } if ( ! flock( $handle, LOCK_EX | LOCK_NB ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_flock - $skipped[] = array( 'path' => $file, 'reason' => 'active' ); + $skipped[] = array( + 'path' => $file, + 'reason' => 'active', + ); fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose continue; } diff --git a/inc/Workspace/WorkspaceRepositoryLifecycle.php b/inc/Workspace/WorkspaceRepositoryLifecycle.php index a44d50d..4ef0262 100644 --- a/inc/Workspace/WorkspaceRepositoryLifecycle.php +++ b/inc/Workspace/WorkspaceRepositoryLifecycle.php @@ -230,7 +230,7 @@ private function run_clone_command( string $command, ?callable $progress_callbac stream_set_blocking( $pipes[1], false ); stream_set_blocking( $pipes[2], false ); - $output = ''; + $output = ''; $exit_code = null; while ( true ) { $chunk = (string) stream_get_contents( $pipes[1] ) . (string) stream_get_contents( $pipes[2] ); @@ -257,7 +257,7 @@ private function run_clone_command( string $command, ?callable $progress_callbac if ( null === $exit_code ) { $exit_code = $close_code; } - $output = trim( str_replace( "\r", "\n", $output ) ); + $output = trim( str_replace( "\r", "\n", $output ) ); if ( 0 !== $exit_code ) { return new \WP_Error( 'clone_failed', @@ -559,5 +559,4 @@ public function show_repo( string $handle ): array|\WP_Error { 'dirty' => (int) $status, ); } - } diff --git a/inc/Workspace/WorkspaceWorktreeEmergencyCleanup.php b/inc/Workspace/WorkspaceWorktreeEmergencyCleanup.php index 67f199c..796db66 100644 --- a/inc/Workspace/WorkspaceWorktreeEmergencyCleanup.php +++ b/inc/Workspace/WorkspaceWorktreeEmergencyCleanup.php @@ -390,18 +390,17 @@ private function build_worktree_emergency_cleanup_summary( array $artifact_candi ksort( $skipped_by_reason ); return array( - 'would_remove_artifacts' => $artifact_count, - 'would_remove_worktrees' => count( $worktree_candidates ), - 'removed_artifacts' => $removed_count, - 'removed_worktrees' => count( $removed_worktrees ), - 'skipped' => count( $skipped ), - 'skipped_by_reason' => $skipped_by_reason, - 'artifact_size_bytes' => 0 === $removed_count ? $artifact_bytes : $removed_bytes, - 'removed_artifact_bytes' => $removed_bytes, - 'worktree_size_bytes' => array_sum( array_map( fn( $row ) => max( 0, (int) ( $row['size_bytes'] ?? 0 ) ), $worktree_candidates ) ), - 'top_artifacts_by_size' => $this->summarize_top_worktree_rows( $artifact_candidates, 'artifact_size_bytes' ), - 'top_worktrees_by_age' => $this->summarize_top_worktree_rows( $worktree_candidates, 'age_days' ), + 'would_remove_artifacts' => $artifact_count, + 'would_remove_worktrees' => count( $worktree_candidates ), + 'removed_artifacts' => $removed_count, + 'removed_worktrees' => count( $removed_worktrees ), + 'skipped' => count( $skipped ), + 'skipped_by_reason' => $skipped_by_reason, + 'artifact_size_bytes' => 0 === $removed_count ? $artifact_bytes : $removed_bytes, + 'removed_artifact_bytes' => $removed_bytes, + 'worktree_size_bytes' => array_sum( array_map( fn( $row ) => max( 0, (int) ( $row['size_bytes'] ?? 0 ) ), $worktree_candidates ) ), + 'top_artifacts_by_size' => $this->summarize_top_worktree_rows( $artifact_candidates, 'artifact_size_bytes' ), + 'top_worktrees_by_age' => $this->summarize_top_worktree_rows( $worktree_candidates, 'age_days' ), ); } - } diff --git a/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php b/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php index da28858..63f65db 100644 --- a/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php +++ b/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php @@ -51,13 +51,13 @@ private function worktree_cleanup_inventory_only( string $older_than, string $so continue; } - $handle = (string) ( $wt['handle'] ?? '?' ); - $repo = (string) ( $wt['repo'] ?? '' ); - $branch = (string) ( $wt['branch_slug'] ?? '' ); - $path = (string) ( $wt['path'] ?? '' ); - $metadata = $wt['metadata'] ?? null; - $created_at = $wt['created_at'] ?? null; - $base_row = array( + $handle = (string) ( $wt['handle'] ?? '?' ); + $repo = (string) ( $wt['repo'] ?? '' ); + $branch = (string) ( $wt['branch_slug'] ?? '' ); + $path = (string) ( $wt['path'] ?? '' ); + $metadata = $wt['metadata'] ?? null; + $created_at = $wt['created_at'] ?? null; + $base_row = array( 'handle' => $handle, 'repo' => $repo, 'branch' => $branch, @@ -253,10 +253,10 @@ 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; - $has_pr_context = ! empty( $metadata['pr_url'] ) || ! empty( $metadata['pr_number'] ) || ! empty( $metadata['pr_ref'] ); + $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; + $has_pr_context = ! empty( $metadata['pr_url'] ) || ! empty( $metadata['pr_number'] ) || ! empty( $metadata['pr_ref'] ); $has_task_context = is_array( $metadata['origin_task'] ?? null ) && ! empty( $metadata['origin_task']['task_url'] ); if ( WorktreeContextInjector::LIVENESS_LIVE !== $liveness && ( $has_pr_context || $has_task_context ) ) { @@ -282,5 +282,4 @@ private function build_inventory_cleanup_no_signal_skip( array $base_row, array 'liveness_reason' => $liveness_reason, ) ); } - } diff --git a/inc/Workspace/WorkspaceWorktreeLifecycle.php b/inc/Workspace/WorkspaceWorktreeLifecycle.php index 979533e..3695461 100644 --- a/inc/Workspace/WorkspaceWorktreeLifecycle.php +++ b/inc/Workspace/WorkspaceWorktreeLifecycle.php @@ -469,7 +469,7 @@ public function cleanup_merged_pr_worktree( string $github_repo, string $branch, continue; } - if ( $branch !== (string) ( $wt['branch'] ?? '' ) ) { + if ( (string) ( $wt['branch'] ?? '' ) !== $branch ) { continue; } @@ -820,18 +820,18 @@ public function worktree_list( ?string $repo = null, ?string $state = null, arra } } - $duplicates = WorktreeContextInjector::find_duplicate_task_ownership( $worktrees ); - $base_branch_worktrees = array_values( array_filter( array_map( + $duplicates = WorktreeContextInjector::find_duplicate_task_ownership( $worktrees ); + $base_branch_worktrees = array_values( array_filter( array_map( fn( $row ) => $row['base_branch_warning'] ?? null, $worktrees ) ) ); return array( - 'success' => true, - 'worktrees' => $worktrees, - 'duplicates' => $duplicates, - 'base_branch_worktrees' => $base_branch_worktrees, - 'fields_skipped' => $skipped_groups, + 'success' => true, + 'worktrees' => $worktrees, + 'duplicates' => $duplicates, + 'base_branch_worktrees' => $base_branch_worktrees, + 'fields_skipped' => $skipped_groups, ); } @@ -856,12 +856,12 @@ private function base_branch_worktree_warning( array $row ): ?array { } return array( - 'handle' => (string) ( $row['handle'] ?? '' ), - 'repo' => (string) ( $row['repo'] ?? '' ), - 'branch' => $branch, - 'path' => (string) ( $row['path'] ?? '' ), - 'reason_code' => 'base_branch_checked_out_in_worktree', - 'message' => sprintf( 'Worktree %s has base branch %s checked out; gh pr merge --delete-branch may merge remotely but fail local cleanup.', (string) ( $row['handle'] ?? '' ), $branch ), + 'handle' => (string) ( $row['handle'] ?? '' ), + 'repo' => (string) ( $row['repo'] ?? '' ), + 'branch' => $branch, + 'path' => (string) ( $row['path'] ?? '' ), + 'reason_code' => 'base_branch_checked_out_in_worktree', + 'message' => sprintf( 'Worktree %s has base branch %s checked out; gh pr merge --delete-branch may merge remotely but fail local cleanup.', (string) ( $row['handle'] ?? '' ), $branch ), ); } @@ -1082,8 +1082,8 @@ public function worktree_prune(): array|\WP_Error { } return array( - 'success' => true, - 'pruned' => $pruned, + 'success' => true, + 'pruned' => $pruned, 'inventory' => $refresh, ); } @@ -1248,7 +1248,8 @@ private function resolve_worktree_branch_from_head_file( string $wt_path ): ?str $gitdir = null; if ( is_file( $git_pointer ) ) { - $pointer = @file_get_contents( $git_pointer ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading .git pointer file in a controlled worktree. + $pointer = @file_get_contents( $git_pointer ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( false === $pointer ) { return null; } @@ -1273,7 +1274,8 @@ private function resolve_worktree_branch_from_head_file( string $wt_path ): ?str return null; } - $head = @file_get_contents( $head_file ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading .git HEAD file in a controlled worktree. + $head = @file_get_contents( $head_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( false === $head ) { return null; } @@ -1309,5 +1311,4 @@ private function parse_worktree_block( string $block ): ?array { } return ( '' === $out['path'] ) ? null : $out; } - } diff --git a/inc/Workspace/WorktreeContextInjector.php b/inc/Workspace/WorktreeContextInjector.php index 3a30551..29994ee 100644 --- a/inc/Workspace/WorktreeContextInjector.php +++ b/inc/Workspace/WorktreeContextInjector.php @@ -76,11 +76,11 @@ class WorktreeContextInjector { /** * Lifecycle states stored on worktree metadata records. */ - public const STATE_ACTIVE = 'active'; - public const STATE_PR_OPENED = 'pr_opened'; - public const STATE_MERGED = 'merged'; - public const STATE_CLOSED = 'closed'; - public const STATE_ABANDONED = 'abandoned'; + public const STATE_ACTIVE = 'active'; + public const STATE_PR_OPENED = 'pr_opened'; + public const STATE_MERGED = 'merged'; + public const STATE_CLOSED = 'closed'; + public const STATE_ABANDONED = 'abandoned'; public const STATE_CLEANUP_ELIGIBLE = 'cleanup_eligible'; /** @@ -102,9 +102,9 @@ class WorktreeContextInjector { * from {@see self::VALID_STATES} so a worktree can be in `active` lifecycle * but `stale` liveness when its session heartbeat has lapsed. */ - public const LIVENESS_LIVE = 'live'; + public const LIVENESS_LIVE = 'live'; public const LIVENESS_STOPPED = 'stopped'; - public const LIVENESS_STALE = 'stale'; + public const LIVENESS_STALE = 'stale'; public const LIVENESS_UNKNOWN = 'unknown'; /** @@ -360,9 +360,9 @@ public static function find_duplicate_task_ownership( array $worktrees ): array $keys = self::extract_task_keys( $row, $metadata ); foreach ( $keys as $kind => $key ) { - $bucket_key = $kind . '|' . $key; - $buckets[ $bucket_key ]['kind'] = $kind; - $buckets[ $bucket_key ]['key'] = $key; + $bucket_key = $kind . '|' . $key; + $buckets[ $bucket_key ]['kind'] = $kind; + $buckets[ $bucket_key ]['key'] = $key; $buckets[ $bucket_key ]['handles'][] = $handle; } } @@ -660,7 +660,7 @@ private static function runtime_signatures(): array { private static function extract_task_keys( array $row, array $metadata ): array { $keys = array(); - $task = is_array( $metadata['origin_task'] ?? null ) ? $metadata['origin_task'] : array(); + $task = is_array( $metadata['origin_task'] ?? null ) ? $metadata['origin_task'] : array(); $task_url = trim( (string) ( $task['task_url'] ?? '' ) ); if ( '' !== $task_url ) { $keys['task_url'] = strtolower( $task_url ); @@ -1074,7 +1074,7 @@ private static function project_site_agents_md( string $worktree_path, array $pa return self::project_site_agents_md_via_opencode_config( $worktree_path, $source ); } - $marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_AGENTS_MARKER_PATH; + $marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_AGENTS_MARKER_PATH; $marker_dir = dirname( $marker ); if ( ! is_dir( $marker_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir @@ -1118,13 +1118,14 @@ private static function project_site_agents_md( string $worktree_path, array $pa * @return string[]|\WP_Error Absolute written paths, or WP_Error on failure. */ private static function project_site_agents_md_via_opencode_config( string $worktree_path, string $source ): array|\WP_Error { - $config = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_PATH; - $marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_MARKER_PATH; + $config = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_PATH; + $marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_MARKER_PATH; $written = array(); $config_exists = is_file( $config ); - $previous = $config_exists ? (string) file_get_contents( $config ) : ''; - $data = array( + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree. + $previous = $config_exists ? (string) file_get_contents( $config ) : ''; + $data = array( '$schema' => 'https://opencode.ai/config.json', 'instructions' => array(), ); @@ -1200,10 +1201,11 @@ private static function project_site_agents_md_via_opencode_config( string $work * @return array{success: bool, removed: string[]} */ public static function uninject( string $worktree_path ): array { - $removed = array(); - $projected_agents = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_AGENTS_PATH; + $removed = array(); + $projected_agents = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_AGENTS_PATH; $projection_marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_AGENTS_MARKER_PATH; - $marked_source = is_file( $projection_marker ) ? trim( (string) file_get_contents( $projection_marker ) ) : ''; + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree. + $marked_source = is_file( $projection_marker ) ? trim( (string) file_get_contents( $projection_marker ) ) : ''; if ( is_link( $projected_agents ) && '' !== $marked_source && @@ -1222,6 +1224,7 @@ public static function uninject( string $worktree_path ): array { $opencode_config = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_PATH; $opencode_marker = rtrim( $worktree_path, '/' ) . '/' . self::PROJECTED_OPENCODE_CONFIG_MARKER_PATH; if ( is_file( $opencode_marker ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Path is a marker file within a controlled worktree. $marker = json_decode( (string) file_get_contents( $opencode_marker ), true ); if ( is_array( $marker ) && ! empty( $marker['existed'] ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents @@ -1269,7 +1272,7 @@ public static function store_lifecycle_metadata( string $handle, array $metadata $all = array(); } - $existing = isset( $all[ $handle ] ) && is_array( $all[ $handle ] ) ? $all[ $handle ] : array(); + $existing = isset( $all[ $handle ] ) && is_array( $all[ $handle ] ) ? $all[ $handle ] : array(); if ( empty( $existing ) ) { $existing = self::get_inventory_metadata( $handle ) ?? array(); } @@ -1408,7 +1411,7 @@ private static function resolve_origin_session(): ?array { return null; } - $session = array( + $session = array( 'primary_id' => null, 'ids' => $ids, ); diff --git a/tests/smoke-github-fetch-by-number.php b/tests/smoke-github-fetch-by-number.php index 7c51880..1f8e8fe 100644 --- a/tests/smoke-github-fetch-by-number.php +++ b/tests/smoke-github-fetch-by-number.php @@ -26,8 +26,8 @@ }; // Handler config reads. -$assert( false !== strpos( $handler, "\$issue_number = (int) ( \$config['issue_number'] ?? 0 )" ), 'handler reads issue_number from config' ); -$assert( false !== strpos( $handler, "\$pull_number = (int) ( \$config['pull_number'] ?? 0 )" ), 'handler reads pull_number from config' ); +$assert( false !== strpos( $handler, "\$issue_number = (int) ( \$config['issue_number'] ?? 0 )" ), 'handler reads issue_number from config' ); +$assert( false !== strpos( $handler, "\$pull_number = (int) ( \$config['pull_number'] ?? 0 )" ), 'handler reads pull_number from config' ); // Targeted-fetch dispatch. $assert( false !== strpos( $handler, 'fetchSingleIssueOrPull' ), 'handler dispatches to fetchSingleIssueOrPull when a number is set' );