diff --git a/inc/Workspace/WorkspaceWorktreeCleanupEngine.php b/inc/Workspace/WorkspaceWorktreeCleanupEngine.php index 58905a9..37a39c0 100644 --- a/inc/Workspace/WorkspaceWorktreeCleanupEngine.php +++ b/inc/Workspace/WorkspaceWorktreeCleanupEngine.php @@ -1057,18 +1057,20 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() ) return $this->schedule_bounded_cleanup_eligible_chunks($batch, $deferred, $force, $source, $started_at, $continuation, $include_repaired_metadata, $remove_timeout_seconds, $discard_unpushed); } - $processed = 0; - $removed = array(); - $skipped = $inventory_skipped; - $bytes_reclaimed = 0; - $timeout_handles = array(); - $discarded_unpushed = array(); + $processed = 0; + $processed_candidates = array(); + $removed = array(); + $skipped = $inventory_skipped; + $bytes_reclaimed = 0; + $timeout_handles = array(); + $discarded_unpushed = array(); foreach ( $batch as $candidate ) { ++$processed; $revalidated = $this->revalidate_bounded_cleanup_eligible_candidate($candidate, $force, false, $discard_unpushed); if ( isset($revalidated['skipped']) ) { - $skipped[] = $revalidated['skipped']; + $processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($candidate, 'skipped', $revalidated['skipped']); + $skipped[] = $revalidated['skipped']; continue; } @@ -1107,16 +1109,17 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) { ); if ( is_wp_error($remove) ) { - $skip = $this->build_worktree_remove_failure_skip($candidate, $remove, $remove_timeout_seconds); - $skipped[] = $skip; + $skip = $this->build_worktree_remove_failure_skip($candidate, $remove, $remove_timeout_seconds); + $processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($validated, 'skipped', $skip); + $skipped[] = $skip; if ( 'remove_timeout' === (string) ( $skip['reason_code'] ?? '' ) ) { $timeout_handles[] = (string) ( $skip['handle'] ?? '' ); } continue; } - $unpushed_count = (int) ( $validated['unpushed'] ?? 0 ); - $removed_row = array_merge( + $unpushed_count = (int) ( $validated['unpushed'] ?? 0 ); + $removed_row = array_merge( array( 'handle' => (string) ( $candidate['handle'] ?? '' ), 'repo' => $repo, @@ -1130,7 +1133,8 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) { ), is_array($candidate['metadata'] ?? null) ? array( 'metadata' => $candidate['metadata'] ) : array() ); - $removed[] = $removed_row; + $removed[] = $removed_row; + $processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($validated, 'removed', $removed_row); if ( $discard_unpushed && $unpushed_count > 0 ) { $discarded_unpushed[] = array( 'handle' => (string) ( $candidate['handle'] ?? '' ), @@ -1160,7 +1164,8 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) { 'destructive' => true, 'workspace_path' => $this->workspace_path, 'generated_at' => gmdate('c'), - 'candidates' => $batch, + 'planned_candidates' => $batch, + 'candidates' => $processed_candidates, 'removed' => $removed, 'skipped' => $skipped, 'summary' => array( @@ -1186,6 +1191,31 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) { ); } + /** + * Attach final revalidation/removal outcome to a processed bounded cleanup row. + * + * @param array $candidate Planned or revalidated candidate row. + * @param string $action Final action: removed or skipped. + * @param array $outcome Fresh removal or blocker row. + * @return array + */ + private function build_bounded_cleanup_processed_candidate( array $candidate, string $action, array $outcome ): array { + $row = $candidate; + foreach ( array( 'dirty', 'unpushed', 'path', 'size_bytes' ) as $field ) { + if ( array_key_exists($field, $outcome) ) { + $row[ $field ] = $outcome[ $field ]; + } + } + + $row['final_action'] = $action; + $row['final_reason_code'] = (string) ( $outcome['reason_code'] ?? $action ); + if ( isset($outcome['reason']) ) { + $row['final_reason'] = (string) $outcome['reason']; + } + + return $row; + } + /** * Apply DB-backed cleanup rows without rebuilding a full workspace scan. * @@ -1500,6 +1530,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate 'reason_code' => 'dirty_worktree', 'reason' => sprintf('working tree dirty (%d entries) — bounded cleanup-eligible apply refuses to override; rerun with force=true after review', $dirty_count), 'dirty' => $dirty_count, + 'unpushed' => (int) $unpushed, ), ); } @@ -1513,6 +1544,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate 'path' => $wt_path, 'reason_code' => 'unpushed_commits', 'reason' => sprintf('%d unpushed commit(s) — bounded cleanup-eligible apply refuses to remove without discard_unpushed=true', $unpushed), + 'dirty' => $dirty_count, 'unpushed' => $unpushed, ), ); @@ -1522,6 +1554,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate $candidate, array( 'path' => $real_path, + 'dirty' => $dirty_count, 'unpushed' => (int) $unpushed, ) ); diff --git a/tests/bounded-cleanup-processed-candidates.php b/tests/bounded-cleanup-processed-candidates.php new file mode 100644 index 0000000..09aa907 --- /dev/null +++ b/tests/bounded-cleanup-processed-candidates.php @@ -0,0 +1,59 @@ +build_bounded_cleanup_processed_candidate($candidate, $action, $outcome); + } +} + +$harness = new BoundedCleanupProcessedCandidateHarness(); + +$candidate = array( + 'handle' => 'repo@stale-cleanup-row', + 'repo' => 'repo', + 'branch' => 'stale-cleanup-row', + 'path' => '/tmp/repo@stale-cleanup-row', + 'reason_code' => 'cleanup_eligible', + 'dirty' => 0, +); + +$processed = $harness->processed( + $candidate, + 'skipped', + array( + 'handle' => 'repo@stale-cleanup-row', + 'repo' => 'repo', + 'branch' => 'stale-cleanup-row', + 'path' => '/tmp/repo@stale-cleanup-row', + 'reason_code' => 'dirty_worktree', + 'reason' => 'working tree dirty (2 entries)', + 'dirty' => 2, + 'unpushed' => 1, + ) +); + +bounded_cleanup_processed_candidates_assert_same(2, $processed['dirty'], 'processed candidate carries fresh dirty count'); +bounded_cleanup_processed_candidates_assert_same(1, $processed['unpushed'], 'processed candidate carries fresh unpushed count'); +bounded_cleanup_processed_candidates_assert_same('skipped', $processed['final_action'], 'processed candidate records final action'); +bounded_cleanup_processed_candidates_assert_same('dirty_worktree', $processed['final_reason_code'], 'processed candidate records blocker bucket'); +bounded_cleanup_processed_candidates_assert_same('cleanup_eligible', $processed['reason_code'], 'planned reason remains available separately'); + +echo "bounded-cleanup-processed-candidates: ok\n";