diff --git a/inc/Tasks/WorkspaceHygieneReportTask.php b/inc/Tasks/WorkspaceHygieneReportTask.php index e2cd08b..6d54026 100644 --- a/inc/Tasks/WorkspaceHygieneReportTask.php +++ b/inc/Tasks/WorkspaceHygieneReportTask.php @@ -112,17 +112,19 @@ public function executeTask( int $jobId, array $params ): void { $this->failJob($jobId, $result->get_error_message()); return; } - $worktrees = (array) ( $result['worktrees'] ?? array() ); - $cleanup = (array) ( $result['cleanup']['summary'] ?? array() ); + $worktrees = (array) ( $result['worktrees'] ?? array() ); + $cleanup = (array) ( $result['cleanup']['summary'] ?? array() ); + $fast_counts = (array) ( $result['fast_stats']['counts'] ?? array() ); + $inventory_cleanup_candidates = (int) ( $cleanup['inventory_cleanup_candidate_count'] ?? $fast_counts['cleanup_eligible_unprobed_count'] ?? 0 ); do_action( 'datamachine_log', 'info', sprintf( - 'Workspace hygiene report: %s used, %s free, %d worktree(s), %d cleanup candidate(s).', + 'Workspace hygiene report: %s used, %s free, %d worktree(s), %d inventory cleanup candidate(s) pending fresh safety probes.', $result['size']['total_human'] ?? 'unknown size', $result['disk']['free_human'] ?? 'unknown disk', (int) ( $worktrees['worktrees'] ?? 0 ), - (int) ( $cleanup['would_remove'] ?? 0 ) + $inventory_cleanup_candidates ), array( 'task' => $this->getTaskType(), diff --git a/inc/Workspace/WorkspaceWorktreeCleanupEngine.php b/inc/Workspace/WorkspaceWorktreeCleanupEngine.php index 88b9a75..cd32f7f 100644 --- a/inc/Workspace/WorkspaceWorktreeCleanupEngine.php +++ b/inc/Workspace/WorkspaceWorktreeCleanupEngine.php @@ -1842,22 +1842,25 @@ private function build_worktree_cleanup_summary( arsort($size_by_repo); arsort($artifact_by_repo); - $summary = array( - 'would_remove' => count($candidates), - 'removed' => count($removed), - 'skipped' => count($skipped), - 'skipped_by_reason' => $skipped_by_reason, - 'skipped_next_commands' => $this->worktree_cleanup_skipped_next_commands($skipped_by_reason), - 'cleanup_buckets' => $this->worktree_cleanup_buckets(count($candidates), $candidates_by_signal, $skipped_by_reason, $candidate_bucket), - 'candidates_by_signal' => $candidates_by_signal, - 'stale_reasons' => $stale_reasons, - 'liveness' => $liveness, - 'total_size_bytes' => $total_size_bytes, - 'artifact_size_bytes' => $total_artifact_bytes, - 'size_by_repo' => $size_by_repo, - 'artifact_size_by_repo' => $artifact_by_repo, - 'top_by_size' => $this->summarize_top_worktree_rows($all_rows, 'size_bytes'), - 'top_by_age' => $this->summarize_top_worktree_rows($all_rows, 'age_days'), + $candidate_count = count($candidates); + $summary = array( + 'would_remove' => $candidate_count, + 'inventory_cleanup_candidate_count' => WorktreeCleanupClassifier::BUCKET_CLEANUP_ELIGIBLE_UNPROBED === $candidate_bucket ? $candidate_count : 0, + 'fresh_safe_removable_count' => WorktreeCleanupClassifier::BUCKET_SAFE_TO_REMOVE_NOW === $candidate_bucket ? $candidate_count : 0, + 'removed' => count($removed), + 'skipped' => count($skipped), + 'skipped_by_reason' => $skipped_by_reason, + 'skipped_next_commands' => $this->worktree_cleanup_skipped_next_commands($skipped_by_reason), + 'cleanup_buckets' => $this->worktree_cleanup_buckets($candidate_count, $candidates_by_signal, $skipped_by_reason, $candidate_bucket), + 'candidates_by_signal' => $candidates_by_signal, + 'stale_reasons' => $stale_reasons, + 'liveness' => $liveness, + 'total_size_bytes' => $total_size_bytes, + 'artifact_size_bytes' => $total_artifact_bytes, + 'size_by_repo' => $size_by_repo, + 'artifact_size_by_repo' => $artifact_by_repo, + 'top_by_size' => $this->summarize_top_worktree_rows($all_rows, 'size_bytes'), + 'top_by_age' => $this->summarize_top_worktree_rows($all_rows, 'age_days'), ); if ( null !== $age_filter ) { diff --git a/tests/workspace-compact-output.php b/tests/workspace-compact-output.php index 58a2bc2..541644f 100644 --- a/tests/workspace-compact-output.php +++ b/tests/workspace-compact-output.php @@ -200,9 +200,11 @@ function compact_output_large_rows( int $count ): array { ), ), 'summary' => array( - 'would_remove' => 40, - 'artifact_size_bytes' => 654321, - 'cleanup_buckets' => array( + 'would_remove' => 40, + 'inventory_cleanup_candidate_count' => 40, + 'fresh_safe_removable_count' => 0, + 'artifact_size_bytes' => 654321, + 'cleanup_buckets' => array( 'cleanup_eligible_pending_revalidation' => 40, 'safe_to_remove_now' => 0, ), @@ -221,6 +223,8 @@ function compact_output_large_rows( int $count ): array { compact_output_assert(40 === ( $hygiene['worktrees']['worktrees'] ?? null ), 'Compact hygiene output must preserve worktree counts.'); compact_output_assert(40 === ( $hygiene['fast_stats']['counts']['cleanup_eligible_unprobed_count'] ?? null ), 'Compact hygiene output must label cheap cleanup candidates as unprobed.'); compact_output_assert(! isset($hygiene['fast_stats']['counts']['safe_removable_count']), 'Compact hygiene output must not expose misleading safe_removable_count for cheap inventory.'); +compact_output_assert(40 === ( $hygiene['cleanup']['summary']['inventory_cleanup_candidate_count'] ?? null ), 'Compact cleanup summary must expose inventory cleanup candidates separately.'); +compact_output_assert(0 === ( $hygiene['cleanup']['summary']['fresh_safe_removable_count'] ?? null ), 'Compact cleanup summary must not mark inventory cleanup candidates as fresh safe removals.'); compact_output_assert(20 === ( $hygiene['fast_stats']['counts']['inventory_known_dirty_count'] ?? null ), 'Compact hygiene output must preserve inventory-known dirty counts.'); compact_output_assert(3 === ( $hygiene['fast_stats']['counts']['inventory_known_blocker_count'] ?? null ), 'Compact hygiene output must preserve inventory-known blocker counts.'); compact_output_assert(0 === ( $hygiene['fast_stats']['counts']['fresh_probed_blocker_count'] ?? null ), 'Compact hygiene output must distinguish fresh-probed blocker counts.'); diff --git a/tests/worktree-cleanup-candidate-classifier.php b/tests/worktree-cleanup-candidate-classifier.php index bed636b..066e8ac 100644 --- a/tests/worktree-cleanup-candidate-classifier.php +++ b/tests/worktree-cleanup-candidate-classifier.php @@ -10,6 +10,7 @@ require_once dirname(__DIR__) . '/inc/Workspace/WorktreeCleanupSignal.php'; require_once dirname(__DIR__) . '/inc/Workspace/WorktreeCleanupClassifier.php'; require_once dirname(__DIR__) . '/inc/Workspace/WorktreeCleanupCandidateClassifier.php'; +require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceWorktreeCleanupEngine.php'; use DataMachineCode\Workspace\WorktreeAgeFilter; use DataMachineCode\Workspace\WorktreeCleanupClassifier; @@ -100,4 +101,29 @@ function (): array { $probed_buckets = WorktreeCleanupClassifier::buckets(2, array(), array()); worktree_cleanup_candidate_assert_same(2, $probed_buckets['safe_to_remove_now'], 'probed cleanup candidates keep the safe-to-remove bucket'); +$engine = new class { + use DataMachineCode\Workspace\WorkspaceWorktreeCleanupEngine; + + public function summary( array $candidates, string $bucket ): array { + $method = new ReflectionMethod($this, 'build_worktree_cleanup_summary'); + return $method->invoke($this, $candidates, array(), array(), null, $bucket); + } + + private function worktree_cleanup_skipped_next_commands( array $skipped_by_reason ): array { + return array(); + } + + private function summarize_top_worktree_rows( array $rows, string $field ): array { + return array(); + } +}; + +$inventory_summary = $engine->summary(array( array( 'signal' => 'cleanup_eligible' ) ), WorktreeCleanupClassifier::BUCKET_CLEANUP_ELIGIBLE_UNPROBED); +worktree_cleanup_candidate_assert_same(1, $inventory_summary['inventory_cleanup_candidate_count'], 'inventory summary exposes cheap cleanup candidates separately'); +worktree_cleanup_candidate_assert_same(0, $inventory_summary['fresh_safe_removable_count'], 'inventory summary does not label unprobed candidates fresh safe'); + +$fresh_summary = $engine->summary(array( array( 'signal' => 'github-merged-pr' ) ), WorktreeCleanupClassifier::BUCKET_SAFE_TO_REMOVE_NOW); +worktree_cleanup_candidate_assert_same(0, $fresh_summary['inventory_cleanup_candidate_count'], 'fresh summary does not count inventory-only candidates'); +worktree_cleanup_candidate_assert_same(1, $fresh_summary['fresh_safe_removable_count'], 'fresh summary exposes freshly probed safe removals'); + echo "worktree-cleanup-candidate-classifier: ok\n";