Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion inc/Cleanup/CleanupRemainingWorkSummary.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ private static function empty_summary(): array {
'applied_by_type' => array(),
'skipped_by_reason' => array(),
'blocked_resolvers_by_reason' => array(),
'total_bytes_reclaimed' => 0,
'remaining_reclaimable_artifact_bytes' => 0,
'remaining_safely_removable_worktrees' => 0,
'remaining_safe_candidates' => 0,
'protected_unpushed_candidates' => 0,
'recommended_commands' => array(),
'next_commands' => array(),
);
}

Expand Down Expand Up @@ -170,10 +174,47 @@ private static function finalize( array $summary ): array {
ksort($summary['applied_by_type']);
ksort($summary['skipped_by_reason']);
ksort($summary['blocked_resolvers_by_reason']);
$summary['recommended_commands'] = self::recommended_commands($summary);
$summary['total_bytes_reclaimed'] = self::total_applied_bytes( (array) $summary['applied_by_type']);
$summary['remaining_safe_candidates'] = (int) ( $summary['remaining_safely_removable_worktrees'] ?? 0 );
$summary['protected_unpushed_candidates'] = self::reason_count($summary, 'unpushed_commits');
$summary['recommended_commands'] = self::recommended_commands($summary);
$summary['next_commands'] = self::next_commands( (array) $summary['recommended_commands']);
return $summary;
}

private static function total_applied_bytes( array $types ): int {
$total = 0;
foreach ( $types as $row ) {
$total += max(0, (int) ( is_array($row) ? ( $row['bytes_reclaimed'] ?? 0 ) : 0 ));
}
return $total;
}

private static function reason_count( array $summary, string $reason ): int {
$total = 0;
foreach ( array( 'skipped_by_reason', 'blocked_resolvers_by_reason' ) as $bucket ) {
$row = (array) ( $summary[ $bucket ][ $reason ] ?? array() );
$total += max(0, (int) ( $row['count'] ?? 0 ));
}
return $total;
}

private static function next_commands( array $commands ): array {
$next = array();
foreach ( $commands as $row ) {
if ( ! is_array($row) ) {
continue;
}
foreach ( array( 'command', 'apply', 'alternative' ) as $field ) {
$value = (string) ( $row[ $field ] ?? '' );
if ( '' !== $value ) {
$next[] = $value;
}
}
}
return array_values(array_unique($next));
}

private static function recommended_commands( array $summary ): array {
$commands = array();
if ( (int) $summary['remaining_reclaimable_artifact_bytes'] > 0 ) {
Expand Down
64 changes: 43 additions & 21 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,7 @@ private function render_cleanup_operator_summary( array $summary ): void {
WP_CLI::log('Cleanup operator summary:');
$cleanup_counts = (array) ( $summary['cleanup_counts'] ?? array() );
$artifacts = (array) ( $summary['artifact_cleanup'] ?? array() );
$remaining_safe = (int) ( $summary['remaining_safe_candidates'] ?? $summary['remaining_safely_removable_worktrees'] ?? 0 );
$this->format_items(
array(
array(
Expand All @@ -1399,6 +1400,14 @@ private function render_cleanup_operator_summary( array $summary ): void {
'metric' => 'bytes_reclaimed',
'value' => $this->format_bytes($cleanup_counts['bytes_reclaimed'] ?? 0),
),
array(
'metric' => 'remaining_safe_candidates',
'value' => $remaining_safe,
),
array(
'metric' => 'protected_unpushed_candidates',
'value' => (int) ( $summary['protected_unpushed_candidates'] ?? 0 ),
),
array(
'metric' => 'remaining_reclaimable_artifacts',
'value' => $this->format_bytes($artifacts['remaining_reclaimable_artifact_bytes'] ?? 0),
Expand Down Expand Up @@ -1459,24 +1468,28 @@ private function build_cleanup_operator_summary( array $result ): array {

return array_filter(
array(
'success' => (bool) ( $result['success'] ?? false ),
'run_id' => (string) ( $result['run_id'] ?? '' ),
'job_id' => isset($result['job_id']) ? (int) $result['job_id'] : null,
'mode' => (string) ( $result['mode'] ?? $result['evidence']['engine_data']['cleanup_run']['mode'] ?? '' ),
'state' => (string) ( $result['state'] ?? '' ),
'status' => (string) ( $result['status'] ?? '' ),
'parent_status' => (string) ( $result['parent_status'] ?? '' ),
'created_at' => (string) ( $result['created_at'] ?? '' ),
'completed_at' => (string) ( $result['completed_at'] ?? $result['parent_completed_at'] ?? '' ),
'cleanup_counts' => array(
'success' => (bool) ( $result['success'] ?? false ),
'run_id' => (string) ( $result['run_id'] ?? '' ),
'job_id' => isset($result['job_id']) ? (int) $result['job_id'] : null,
'mode' => (string) ( $result['mode'] ?? $result['evidence']['engine_data']['cleanup_run']['mode'] ?? '' ),
'state' => (string) ( $result['state'] ?? '' ),
'status' => (string) ( $result['status'] ?? '' ),
'parent_status' => (string) ( $result['parent_status'] ?? '' ),
'created_at' => (string) ( $result['created_at'] ?? '' ),
'completed_at' => (string) ( $result['completed_at'] ?? $result['parent_completed_at'] ?? '' ),
'cleanup_counts' => array(
'planned' => (int) ( $cleanup_items['planned_rows'] ?? 0 ),
'applied' => (int) ( $cleanup_items['applied_rows'] ?? 0 ),
'skipped' => (int) ( $cleanup_items['skipped_rows'] ?? 0 ),
'failed' => (int) ( $cleanup_items['failed_rows'] ?? 0 ),
'bytes_reclaimed' => (int) ( $cleanup_items['bytes_reclaimed'] ?? 0 ),
'freed_human' => (string) ( $cleanup_items['freed_human'] ?? $this->format_bytes($cleanup_items['bytes_reclaimed'] ?? 0) ),
),
'artifact_cleanup' => array(
'total_bytes_reclaimed' => (int) ( $remaining['total_bytes_reclaimed'] ?? $cleanup_items['bytes_reclaimed'] ?? 0 ),
'total_reclaimed_human' => $this->format_bytes($remaining['total_bytes_reclaimed'] ?? $cleanup_items['bytes_reclaimed'] ?? 0),
'remaining_safe_candidates' => (int) ( $remaining['remaining_safe_candidates'] ?? $remaining['remaining_safely_removable_worktrees'] ?? 0 ),
'protected_unpushed_candidates' => (int) ( $remaining['protected_unpushed_candidates'] ?? 0 ),
'artifact_cleanup' => array(
'planned' => (int) ( $artifacts['planned_rows'] ?? 0 ),
'applied' => (int) ( $artifacts['applied_rows'] ?? 0 ),
'skipped' => (int) ( $artifacts['skipped_rows'] ?? 0 ),
Expand All @@ -1485,13 +1498,14 @@ private function build_cleanup_operator_summary( array $result ): array {
'remaining_reclaimable_artifact_bytes' => (int) ( $remaining['remaining_reclaimable_artifact_bytes'] ?? $artifacts['remaining_reclaimable_artifact_bytes'] ?? 0 ),
'remaining_reclaimable_human' => $this->format_bytes($remaining['remaining_reclaimable_artifact_bytes'] ?? $artifacts['remaining_reclaimable_artifact_bytes'] ?? 0),
),
'children' => $this->build_cleanup_operator_child_summary( (array) ( $result['children'] ?? $result['evidence']['children'] ?? array() ) ),
'by_type' => (array) ( $cleanup_items['by_type'] ?? array() ),
'skipped_by_reason' => (array) ( $remaining['skipped_by_reason'] ?? $cleanup_items['skipped_examples_by_reason'] ?? array() ),
'failed_by_reason' => (array) ( $cleanup_items['failed_by_reason'] ?? $artifacts['failed_by_reason'] ?? array() ),
'top_blocked_examples' => $this->cleanup_operator_blocked_examples($result),
'recommended_commands' => (array) ( $remaining['recommended_commands'] ?? array() ),
'locks' => (array) ( $result['locks'] ?? array() ),
'children' => $this->build_cleanup_operator_child_summary( (array) ( $result['children'] ?? $result['evidence']['children'] ?? array() ) ),
'by_type' => (array) ( $cleanup_items['by_type'] ?? array() ),
'skipped_by_reason' => (array) ( $remaining['skipped_by_reason'] ?? $cleanup_items['skipped_examples_by_reason'] ?? array() ),
'failed_by_reason' => (array) ( $cleanup_items['failed_by_reason'] ?? $artifacts['failed_by_reason'] ?? array() ),
'top_blocked_examples' => $this->cleanup_operator_blocked_examples($result),
'recommended_commands' => (array) ( $remaining['recommended_commands'] ?? array() ),
'next_commands' => (array) ( $remaining['next_commands'] ?? array() ),
'locks' => (array) ( $result['locks'] ?? array() ),
),
fn( $value ) => null !== $value && array() !== $value && '' !== $value
);
Expand Down Expand Up @@ -1673,13 +1687,21 @@ private function render_cleanup_remaining_work_summary( array $summary ): void {
WP_CLI::log('Remaining work summary:');
$this->format_items(
array(
array(
'metric' => 'total_bytes_reclaimed',
'value' => $this->format_bytes($summary['total_bytes_reclaimed'] ?? 0),
),
array(
'metric' => 'remaining_reclaimable_artifact_bytes',
'value' => $this->format_bytes($summary['remaining_reclaimable_artifact_bytes'] ?? 0),
),
array(
'metric' => 'remaining_safely_removable_worktrees',
'value' => (int) ( $summary['remaining_safely_removable_worktrees'] ?? 0 ),
'metric' => 'remaining_safe_candidates',
'value' => (int) ( $summary['remaining_safe_candidates'] ?? $summary['remaining_safely_removable_worktrees'] ?? 0 ),
),
array(
'metric' => 'protected_unpushed_candidates',
'value' => (int) ( $summary['protected_unpushed_candidates'] ?? 0 ),
),
),
array( 'metric', 'value' ),
Expand Down Expand Up @@ -3635,7 +3657,7 @@ public function worktree( array $args, array $assoc_args ): void {

$input = array();
$input_builder = (string) ( $operation_config['input_builder'] ?? '' );
if ( '' !== $input_builder && method_exists($this, $input_builder) ) {
if ( '' !== $input_builder ) {
$input = $this->{$input_builder}($operation, $assoc_args);
}

Expand Down
19 changes: 11 additions & 8 deletions inc/Workspace/CleanupRunService.php
Original file line number Diff line number Diff line change
Expand Up @@ -607,17 +607,20 @@ private function run_progress( array $run, array $items, array $summary ): array
private function remaining_work_summary( string $run_id, array $items, array $progress ): array {
$summary = CleanupRemainingWorkSummary::from_items($items);
if ( ! empty($progress['resumable']) ) {
$resume_command = array(
'bucket' => 'current_run_resume',
'command' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
'apply' => sprintf('studio wp datamachine-code workspace cleanup resume %s --limit=%d', $run_id, self::DEFAULT_APPLY_LIMIT),
'destructive' => false,
'apply_destructive' => true,
'why' => 'Resume the reviewed DB-backed cleanup run from persisted pending/failed/applying rows.',
);
array_unshift(
$summary['recommended_commands'],
array(
'bucket' => 'current_run_resume',
'command' => sprintf('studio wp datamachine-code workspace cleanup status %s --format=json', $run_id),
'apply' => sprintf('studio wp datamachine-code workspace cleanup resume %s --limit=%d', $run_id, self::DEFAULT_APPLY_LIMIT),
'destructive' => false,
'apply_destructive' => true,
'why' => 'Resume the reviewed DB-backed cleanup run from persisted pending/failed/applying rows.',
)
$resume_command
);
array_unshift($summary['next_commands'], (string) $resume_command['command'], (string) $resume_command['apply']);
$summary['next_commands'] = array_values(array_unique($summary['next_commands']));
}

return $summary;
Expand Down
67 changes: 67 additions & 0 deletions tests/cleanup-remaining-work-summary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* Standalone coverage for cleanup remaining-work summary rendering fields.
*/

declare(strict_types=1);

if ( ! defined('ABSPATH') ) {
define('ABSPATH', __DIR__ . '/fixtures/');
}

require_once dirname(__DIR__) . '/inc/Cleanup/CleanupRemainingWorkSummary.php';

use DataMachineCode\Cleanup\CleanupRemainingWorkSummary;

function cleanup_summary_assert_same( mixed $expected, mixed $actual, string $message ): void {
if ( $expected !== $actual ) {
throw new RuntimeException(sprintf("%s\nExpected: %s\nActual: %s", $message, var_export($expected, true), var_export($actual, true)));
}
}

$summary = CleanupRemainingWorkSummary::from_items(
array(
array(
'item_type' => 'artifact_cleanup',
'status' => 'applied',
'bytes_reclaimed' => 2048,
),
array(
'item_type' => 'worktree_removal',
'status' => 'applied',
'bytes_reclaimed' => 1024,
),
array(
'item_type' => 'worktree_removal',
'status' => 'pending',
'handle' => 'repo@safe-candidate',
),
array(
'item_type' => 'worktree_removal',
'status' => 'skipped',
'handle' => 'repo@unpushed',
'reason_code' => 'unpushed_commits',
),
array(
'item_type' => 'artifact_cleanup',
'status' => 'pending',
'evidence' => array(
'handle' => 'repo@artifact',
'artifact_size_bytes' => 4096,
),
),
)
);

cleanup_summary_assert_same(3072, $summary['total_bytes_reclaimed'] ?? null, 'Total reclaimed bytes should sum applied rows across types.');
cleanup_summary_assert_same(1, $summary['remaining_safe_candidates'] ?? null, 'Remaining safe candidates should mirror safely removable worktrees.');
cleanup_summary_assert_same(1, $summary['protected_unpushed_candidates'] ?? null, 'Protected unpushed candidates should be counted explicitly.');
cleanup_summary_assert_same(4096, $summary['remaining_reclaimable_artifact_bytes'] ?? null, 'Remaining reclaimable artifact bytes should stay visible.');

$next_commands = (array) ( $summary['next_commands'] ?? array() );
cleanup_summary_assert_same(true, in_array('studio wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --limit=25', $next_commands, true), 'Safe worktree review command should be flattened into next_commands.');
cleanup_summary_assert_same(true, in_array('studio wp datamachine-code workspace cleanup run --mode=retention', $next_commands, true), 'Safe worktree apply command should be flattened into next_commands.');
cleanup_summary_assert_same(true, in_array('git -C <worktree-path> log --oneline --decorate @{u}..HEAD', $next_commands, true), 'Unpushed inspection command should be flattened into next_commands.');
cleanup_summary_assert_same(true, in_array('studio wp datamachine-code workspace cleanup run --mode=retention --dry-run --only=unpushed_commits --verbose --format=json', $next_commands, true), 'Unpushed focused review command should be flattened into next_commands.');

echo "cleanup remaining work summary test passed.\n";
Loading