Skip to content

Commit cdc84c2

Browse files
authored
Merge pull request #732 from Extra-Chill/fix/issue-729-cleanup-summary
Add cleanup evidence summary output
2 parents e17a13e + bd44528 commit cdc84c2

2 files changed

Lines changed: 278 additions & 1 deletion

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,10 @@ public function adopt_repo( array $args, array $assoc_args ): void {
633633
* [--verbose]
634634
* : Include full diagnostic child job ID lists in task-backed cleanup status output.
635635
*
636+
* [--summary]
637+
* : For `status` and `evidence`, print compact operator-focused cleanup counts,
638+
* blockers, examples, and next commands instead of full nested evidence.
639+
*
636640
* [--drain]
637641
* : For `cleanup run`, drain the queued parent job, drain active child cleanup
638642
* jobs discovered from cleanup status, then print verified bytes reclaimed.
@@ -695,6 +699,9 @@ public function adopt_repo( array $args, array $assoc_args ): void {
695699
* # Print recorded evidence / engine data
696700
* wp datamachine-code workspace cleanup evidence cleanup-run-123 --format=json
697701
*
702+
* # Print compact evidence summary for chat/operator follow-up
703+
* wp datamachine-code workspace cleanup evidence cleanup-run-123 --summary
704+
*
698705
* @subcommand cleanup
699706
*/
700707
public function cleanup( array $args, array $assoc_args ): void {
@@ -1276,6 +1283,9 @@ private function cleanup_run_control_job_ids( string $operation, int $job_id ):
12761283
private function render_cleanup_control_result( array $result, array $assoc_args ): void {
12771284
$result = $this->attach_current_workspace_lock_status($result);
12781285
$format = (string) ( $assoc_args['format'] ?? 'table' );
1286+
if ( ! empty($assoc_args['summary']) ) {
1287+
$result = $this->build_cleanup_operator_summary($result);
1288+
}
12791289
if ( 'json' === $format ) {
12801290
WP_CLI::log( (string) wp_json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
12811291
return;
@@ -1291,6 +1301,10 @@ private function render_cleanup_control_result( array $result, array $assoc_args
12911301
WP_CLI::log(sprintf('%s: %s', ucfirst(str_replace('_', ' ', $key)), (string) $result[ $key ]));
12921302
}
12931303
}
1304+
if ( ! empty($assoc_args['summary']) ) {
1305+
$this->render_cleanup_operator_summary($result);
1306+
return;
1307+
}
12941308
if ( ! empty($result['progress']) && is_array($result['progress']) ) {
12951309
$this->render_cleanup_progress_summary( (array) $result['progress']);
12961310
}
@@ -1311,6 +1325,245 @@ private function render_cleanup_control_result( array $result, array $assoc_args
13111325
}
13121326
}
13131327

1328+
/**
1329+
* Render compact cleanup status/evidence summary tables.
1330+
*
1331+
* @param array<string,mixed> $summary Compact cleanup summary.
1332+
* @return void
1333+
*/
1334+
private function render_cleanup_operator_summary( array $summary ): void {
1335+
WP_CLI::log('');
1336+
WP_CLI::log('Cleanup operator summary:');
1337+
$cleanup_counts = (array) ( $summary['cleanup_counts'] ?? array() );
1338+
$artifacts = (array) ( $summary['artifact_cleanup'] ?? array() );
1339+
$this->format_items(
1340+
array(
1341+
array(
1342+
'metric' => 'planned_rows',
1343+
'value' => (int) ( $cleanup_counts['planned'] ?? 0 ),
1344+
),
1345+
array(
1346+
'metric' => 'applied_rows',
1347+
'value' => (int) ( $cleanup_counts['applied'] ?? 0 ),
1348+
),
1349+
array(
1350+
'metric' => 'skipped_rows',
1351+
'value' => (int) ( $cleanup_counts['skipped'] ?? 0 ),
1352+
),
1353+
array(
1354+
'metric' => 'failed_rows',
1355+
'value' => (int) ( $cleanup_counts['failed'] ?? 0 ),
1356+
),
1357+
array(
1358+
'metric' => 'bytes_reclaimed',
1359+
'value' => $this->format_bytes($cleanup_counts['bytes_reclaimed'] ?? 0),
1360+
),
1361+
array(
1362+
'metric' => 'remaining_reclaimable_artifacts',
1363+
'value' => $this->format_bytes($artifacts['remaining_reclaimable_artifact_bytes'] ?? 0),
1364+
),
1365+
),
1366+
array( 'metric', 'value' ),
1367+
array( 'format' => 'table' ),
1368+
'metric'
1369+
);
1370+
1371+
$this->render_cleanup_summary_reason_rows('Skipped rows by reason:', (array) ( $summary['skipped_by_reason'] ?? array() ));
1372+
$this->render_cleanup_summary_reason_rows('Failed rows by reason:', (array) ( $summary['failed_by_reason'] ?? array() ));
1373+
1374+
$examples = (array) ( $summary['top_blocked_examples'] ?? array() );
1375+
if ( array() !== $examples ) {
1376+
WP_CLI::log('');
1377+
WP_CLI::log('Top blocked examples:');
1378+
$rows = array_map(
1379+
fn( $row ) => array(
1380+
'size' => $this->format_bytes(is_array($row) ? ( $row['size_bytes'] ?? 0 ) : 0),
1381+
'reason' => is_array($row) ? (string) ( $row['reason'] ?? '' ) : '',
1382+
'handle' => is_array($row) ? (string) ( $row['handle'] ?? '' ) : '',
1383+
'artifact_path' => is_array($row) ? (string) ( $row['artifact_path'] ?? '' ) : '',
1384+
'path' => is_array($row) ? (string) ( $row['path'] ?? '' ) : '',
1385+
),
1386+
array_slice($examples, 0, 10)
1387+
);
1388+
$this->format_items($rows, array( 'size', 'reason', 'handle', 'artifact_path', 'path' ), array( 'format' => 'table' ), 'size');
1389+
}
1390+
1391+
$commands = (array) ( $summary['recommended_commands'] ?? array() );
1392+
if ( array() !== $commands ) {
1393+
WP_CLI::log('');
1394+
WP_CLI::log('Recommended next commands:');
1395+
$rows = array_map(
1396+
fn( $row ) => array(
1397+
'bucket' => is_array($row) ? (string) ( $row['bucket'] ?? '' ) : '',
1398+
'review_command' => is_array($row) ? (string) ( $row['command'] ?? '' ) : '',
1399+
'apply_command' => is_array($row) ? (string) ( $row['apply'] ?? '' ) : '',
1400+
'apply_destructive' => is_array($row) && ! empty($row['apply_destructive']) ? 'yes' : 'no',
1401+
),
1402+
array_slice($commands, 0, 10)
1403+
);
1404+
$this->format_items($rows, array( 'bucket', 'review_command', 'apply_command', 'apply_destructive' ), array( 'format' => 'table' ), 'bucket');
1405+
}
1406+
}
1407+
1408+
/**
1409+
* Build compact cleanup status/evidence output for chat/operator workflows.
1410+
*
1411+
* @param array<string,mixed> $result Cleanup status/evidence result.
1412+
* @return array<string,mixed>
1413+
*/
1414+
private function build_cleanup_operator_summary( array $result ): array {
1415+
$cleanup_items = (array) ( $result['cleanup_items'] ?? $result['evidence']['cleanup_items'] ?? array() );
1416+
$artifacts = (array) ( $result['artifact_cleanup'] ?? $result['evidence']['artifact_cleanup'] ?? array() );
1417+
$remaining = (array) ( $result['remaining_work_summary'] ?? array() );
1418+
1419+
return array_filter(
1420+
array(
1421+
'success' => (bool) ( $result['success'] ?? false ),
1422+
'run_id' => (string) ( $result['run_id'] ?? '' ),
1423+
'job_id' => isset($result['job_id']) ? (int) $result['job_id'] : null,
1424+
'mode' => (string) ( $result['mode'] ?? $result['evidence']['engine_data']['cleanup_run']['mode'] ?? '' ),
1425+
'state' => (string) ( $result['state'] ?? '' ),
1426+
'status' => (string) ( $result['status'] ?? '' ),
1427+
'parent_status' => (string) ( $result['parent_status'] ?? '' ),
1428+
'created_at' => (string) ( $result['created_at'] ?? '' ),
1429+
'completed_at' => (string) ( $result['completed_at'] ?? $result['parent_completed_at'] ?? '' ),
1430+
'cleanup_counts' => array(
1431+
'planned' => (int) ( $cleanup_items['planned_rows'] ?? 0 ),
1432+
'applied' => (int) ( $cleanup_items['applied_rows'] ?? 0 ),
1433+
'skipped' => (int) ( $cleanup_items['skipped_rows'] ?? 0 ),
1434+
'failed' => (int) ( $cleanup_items['failed_rows'] ?? 0 ),
1435+
'bytes_reclaimed' => (int) ( $cleanup_items['bytes_reclaimed'] ?? 0 ),
1436+
'freed_human' => (string) ( $cleanup_items['freed_human'] ?? $this->format_bytes($cleanup_items['bytes_reclaimed'] ?? 0) ),
1437+
),
1438+
'artifact_cleanup' => array(
1439+
'planned' => (int) ( $artifacts['planned_rows'] ?? 0 ),
1440+
'applied' => (int) ( $artifacts['applied_rows'] ?? 0 ),
1441+
'skipped' => (int) ( $artifacts['skipped_rows'] ?? 0 ),
1442+
'failed' => (int) ( $artifacts['failed_rows'] ?? 0 ),
1443+
'bytes_reclaimed' => (int) ( $artifacts['bytes_reclaimed'] ?? 0 ),
1444+
'remaining_reclaimable_artifact_bytes' => (int) ( $remaining['remaining_reclaimable_artifact_bytes'] ?? $artifacts['remaining_reclaimable_artifact_bytes'] ?? 0 ),
1445+
'remaining_reclaimable_human' => $this->format_bytes($remaining['remaining_reclaimable_artifact_bytes'] ?? $artifacts['remaining_reclaimable_artifact_bytes'] ?? 0),
1446+
),
1447+
'children' => $this->build_cleanup_operator_child_summary( (array) ( $result['children'] ?? $result['evidence']['children'] ?? array() ) ),
1448+
'by_type' => (array) ( $cleanup_items['by_type'] ?? array() ),
1449+
'skipped_by_reason' => (array) ( $remaining['skipped_by_reason'] ?? $cleanup_items['skipped_examples_by_reason'] ?? array() ),
1450+
'failed_by_reason' => (array) ( $cleanup_items['failed_by_reason'] ?? $artifacts['failed_by_reason'] ?? array() ),
1451+
'top_blocked_examples' => $this->cleanup_operator_blocked_examples($result),
1452+
'recommended_commands' => (array) ( $remaining['recommended_commands'] ?? array() ),
1453+
'locks' => (array) ( $result['locks'] ?? array() ),
1454+
),
1455+
fn( $value ) => null !== $value && array() !== $value && '' !== $value
1456+
);
1457+
}
1458+
1459+
/**
1460+
* Summarize child cleanup jobs without unbounded ID lists.
1461+
*
1462+
* @param array<string,mixed> $children Child job aggregate.
1463+
* @return array<string,mixed>
1464+
*/
1465+
private function build_cleanup_operator_child_summary( array $children ): array {
1466+
return array(
1467+
'total' => (int) ( $children['total'] ?? 0 ),
1468+
'running' => (int) ( $children['running'] ?? 0 ),
1469+
'completed' => (int) ( $children['completed'] ?? 0 ),
1470+
'failed' => (int) ( $children['failed'] ?? 0 ),
1471+
'skipped' => (int) ( $children['skipped'] ?? 0 ),
1472+
'statuses' => (array) ( $children['statuses'] ?? array() ),
1473+
'batch_jobs' => isset($children['batch_total']) ? (int) $children['batch_total'] : count( (array) ( $children['batch_job_ids'] ?? array() ) ),
1474+
'chunk_jobs' => isset($children['chunk_total']) ? (int) $children['chunk_total'] : count( (array) ( $children['chunk_job_ids'] ?? array() ) ),
1475+
);
1476+
}
1477+
1478+
/**
1479+
* Extract largest blocked cleanup examples from compact summaries and full evidence when available.
1480+
*
1481+
* @param array<string,mixed> $result Cleanup status/evidence result.
1482+
* @return array<int,array<string,mixed>>
1483+
*/
1484+
private function cleanup_operator_blocked_examples( array $result ): array {
1485+
$examples = array();
1486+
foreach ( (array) ( $result['remaining_work_summary']['skipped_by_reason'] ?? array() ) as $reason => $bucket ) {
1487+
foreach ( (array) ( is_array($bucket) ? ( $bucket['examples'] ?? array() ) : array() ) as $row ) {
1488+
if ( is_array($row) ) {
1489+
$examples[] = $this->cleanup_operator_example_row($row, (string) $reason);
1490+
}
1491+
}
1492+
}
1493+
1494+
foreach ( (array) ( $result['evidence']['child_jobs'] ?? array() ) as $job ) {
1495+
$engine_data = (array) ( is_array($job) ? ( $job['engine_data'] ?? array() ) : array() );
1496+
foreach ( array( 'skipped', 'failed' ) as $bucket ) {
1497+
foreach ( (array) ( $engine_data[ $bucket ] ?? array() ) as $row ) {
1498+
if ( is_array($row) ) {
1499+
$examples[] = $this->cleanup_operator_example_row($row, (string) ( $row['reason_code'] ?? $bucket ));
1500+
}
1501+
}
1502+
}
1503+
}
1504+
1505+
usort($examples, fn( $a, $b ) => (int) ( $b['size_bytes'] ?? 0 ) <=> (int) ( $a['size_bytes'] ?? 0 ));
1506+
$seen = array();
1507+
$deduped = array_values(array_filter(
1508+
$examples,
1509+
function ( array $row ) use ( &$seen ): bool {
1510+
$key = (string) ( $row['handle'] ?? '' ) . '|' . (string) ( $row['reason'] ?? '' ) . '|' . (string) ( $row['path'] ?? '' );
1511+
if ( isset($seen[ $key ]) ) {
1512+
return false;
1513+
}
1514+
$seen[ $key ] = true;
1515+
return true;
1516+
}
1517+
));
1518+
return array_slice($deduped, 0, 10);
1519+
}
1520+
1521+
/**
1522+
* Normalize one blocked cleanup example row for compact output.
1523+
*
1524+
* @param array<string,mixed> $row Cleanup row.
1525+
* @param string $reason Fallback reason code.
1526+
* @return array<string,mixed>
1527+
*/
1528+
private function cleanup_operator_example_row( array $row, string $reason ): array {
1529+
$artifact_path = (string) ( $row['artifact_path'] ?? '' );
1530+
$artifacts = (array) ( $row['artifacts'] ?? array() );
1531+
if ( '' === $artifact_path && isset($artifacts[0]) && is_array($artifacts[0]) ) {
1532+
$artifact_path = (string) ( $artifacts[0]['path'] ?? '' );
1533+
}
1534+
1535+
return array_filter(
1536+
array(
1537+
'handle' => (string) ( $row['handle'] ?? '' ),
1538+
'reason' => (string) ( $row['reason_code'] ?? $row['reason'] ?? $reason ),
1539+
'path' => (string) ( $row['path'] ?? '' ),
1540+
'artifact_path' => $artifact_path,
1541+
'size_bytes' => $this->cleanup_operator_row_bytes($row),
1542+
'size' => $this->format_bytes($this->cleanup_operator_row_bytes($row)),
1543+
),
1544+
fn( $value ) => '' !== $value && 0 !== $value
1545+
);
1546+
}
1547+
1548+
/**
1549+
* Return best-known reclaimable bytes for one cleanup row.
1550+
*
1551+
* @param array<string,mixed> $row Cleanup row.
1552+
* @return int
1553+
*/
1554+
private function cleanup_operator_row_bytes( array $row ): int {
1555+
foreach ( array( 'artifact_size_bytes', 'size_bytes', 'bytes_reclaimed' ) as $field ) {
1556+
if ( isset($row[ $field ]) ) {
1557+
return max(0, (int) $row[ $field ]);
1558+
}
1559+
}
1560+
$total = 0;
1561+
foreach ( (array) ( $row['artifacts'] ?? array() ) as $artifact ) {
1562+
$total += max(0, (int) ( is_array($artifact) ? ( $artifact['size_bytes'] ?? 0 ) : 0 ));
1563+
}
1564+
return $total;
1565+
}
1566+
13141567
/**
13151568
* Attach live workspace lock status to cleanup triage surfaces when available.
13161569
*

tests/smoke-worktree-cleanup-cli.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1225,7 +1225,8 @@ public function execute( array $input ): array
12251225
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, 'Control task-backed workspace cleanup runs.'), 'workspace cleanup command documents task-backed controller surface');
12261226
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '<plan|apply|until-empty|run|status|resume|cancel|evidence>'), 'workspace cleanup synopsis exposes DB-backed and task-backed cleanup operations');
12271227
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--dry-run]'), 'task-backed cleanup synopsis keeps synchronous dry-run review');
1228-
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--drain]'), 'task-backed cleanup synopsis exposes drain option');
1228+
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--drain]'), 'task-backed cleanup synopsis exposes drain option');
1229+
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--summary]'), 'task-backed cleanup synopsis exposes compact summary option');
12291230
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, '[--max-passes=<count>]'), 'workspace cleanup synopsis exposes until-empty pass budget');
12301231
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, 'workspace cleanup until-empty --mode=artifacts'), 'workspace cleanup examples include artifact until-empty loop');
12311232
datamachine_code_cleanup_assert(str_contains($cleanup_doc_comment, 'stale-worktrees'), 'workspace cleanup synopsis exposes stale-worktrees destructive profile');
@@ -1539,6 +1540,29 @@ public function execute( array $input ): array
15391540
datamachine_code_cleanup_assert(1 === (int) ( $evidence_json['evidence']['cleanup_items']['failed_by_reason']['apply_failed'] ?? 0 ), 'cleanup evidence reconstructs failed cleanup item reasons from job rows');
15401541
datamachine_code_cleanup_assert(4 === count($evidence_json['evidence']['child_jobs'] ?? array()), 'cleanup evidence emits descendant child jobs');
15411542

1543+
WP_CLI::$logs = array();
1544+
WP_CLI::$successes = array();
1545+
$command->cleanup(array( 'evidence', 'cleanup-run-123' ), array( 'summary' => true, 'format' => 'json' ));
1546+
$summary_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1547+
datamachine_code_cleanup_assert(! isset($summary_json['evidence']), 'cleanup evidence --summary omits full nested evidence');
1548+
datamachine_code_cleanup_assert('cleanup-run-123' === ( $summary_json['run_id'] ?? '' ), 'cleanup evidence --summary keeps run id');
1549+
datamachine_code_cleanup_assert(4 === (int) ( $summary_json['cleanup_counts']['planned'] ?? 0 ), 'cleanup evidence --summary reports planned cleanup rows');
1550+
datamachine_code_cleanup_assert(4096 === (int) ( $summary_json['cleanup_counts']['bytes_reclaimed'] ?? 0 ), 'cleanup evidence --summary reports reclaimed bytes');
1551+
datamachine_code_cleanup_assert(10240 === (int) ( $summary_json['artifact_cleanup']['remaining_reclaimable_artifact_bytes'] ?? 0 ), 'cleanup evidence --summary reports remaining reclaimable artifact bytes');
1552+
datamachine_code_cleanup_assert(1 === (int) ( $summary_json['skipped_by_reason']['dirty_worktree']['count'] ?? 0 ), 'cleanup evidence --summary groups skipped reasons');
1553+
datamachine_code_cleanup_assert('repo@dirty' === (string) ( $summary_json['top_blocked_examples'][0]['handle'] ?? '' ), 'cleanup evidence --summary includes largest blocked example');
1554+
datamachine_code_cleanup_assert('dirty_worktree' === (string) ( $summary_json['top_blocked_examples'][0]['reason'] ?? '' ), 'cleanup evidence --summary includes example reason');
1555+
datamachine_code_cleanup_assert(8192 === (int) ( $summary_json['top_blocked_examples'][0]['size_bytes'] ?? 0 ), 'cleanup evidence --summary includes example size');
1556+
datamachine_code_cleanup_assert(str_contains((string) wp_json_encode($summary_json['recommended_commands'] ?? array()), 'workspace cleanup run --mode=artifacts'), 'cleanup evidence --summary includes recommended commands');
1557+
1558+
WP_CLI::$logs = array();
1559+
WP_CLI::$successes = array();
1560+
$command->cleanup(array( 'evidence', 'cleanup-run-123' ), array( 'summary' => true ));
1561+
datamachine_code_cleanup_assert(in_array('Cleanup operator summary:', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints operator heading');
1562+
datamachine_code_cleanup_assert(in_array('table:6:metric,value', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints compact metric table');
1563+
datamachine_code_cleanup_assert(in_array('Top blocked examples:', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints blocked examples');
1564+
datamachine_code_cleanup_assert(array() !== array_filter(WP_CLI::$logs, fn( $log ) => str_contains((string) $log, ':size,reason,handle,artifact_path,path')), 'cleanup evidence --summary human output prints blocked example table');
1565+
15421566
WP_CLI::$logs = array();
15431567
WP_CLI::$successes = array();
15441568
$command->cleanup(array( 'resume', 'cleanup-run-123' ), array( 'force' => true, 'format' => 'json' ));

0 commit comments

Comments
 (0)