Skip to content

Commit 1e7e382

Browse files
Add cleanup evidence summary output
1 parent 3c74d8b commit 1e7e382

2 files changed

Lines changed: 260 additions & 1 deletion

File tree

inc/Cli/Commands/WorkspaceCommand.php

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

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');
@@ -1532,6 +1533,29 @@ public function execute( array $input ): array
15321533
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');
15331534
datamachine_code_cleanup_assert(4 === count($evidence_json['evidence']['child_jobs'] ?? array()), 'cleanup evidence emits descendant child jobs');
15341535

1536+
WP_CLI::$logs = array();
1537+
WP_CLI::$successes = array();
1538+
$command->cleanup(array( 'evidence', 'cleanup-run-123' ), array( 'summary' => true, 'format' => 'json' ));
1539+
$summary_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1540+
datamachine_code_cleanup_assert(! isset($summary_json['evidence']), 'cleanup evidence --summary omits full nested evidence');
1541+
datamachine_code_cleanup_assert('cleanup-run-123' === ( $summary_json['run_id'] ?? '' ), 'cleanup evidence --summary keeps run id');
1542+
datamachine_code_cleanup_assert(4 === (int) ( $summary_json['cleanup_counts']['planned'] ?? 0 ), 'cleanup evidence --summary reports planned cleanup rows');
1543+
datamachine_code_cleanup_assert(4096 === (int) ( $summary_json['cleanup_counts']['bytes_reclaimed'] ?? 0 ), 'cleanup evidence --summary reports reclaimed bytes');
1544+
datamachine_code_cleanup_assert(10240 === (int) ( $summary_json['artifact_cleanup']['remaining_reclaimable_artifact_bytes'] ?? 0 ), 'cleanup evidence --summary reports remaining reclaimable artifact bytes');
1545+
datamachine_code_cleanup_assert(1 === (int) ( $summary_json['skipped_by_reason']['dirty_worktree']['count'] ?? 0 ), 'cleanup evidence --summary groups skipped reasons');
1546+
datamachine_code_cleanup_assert('repo@dirty' === (string) ( $summary_json['top_blocked_examples'][0]['handle'] ?? '' ), 'cleanup evidence --summary includes largest blocked example');
1547+
datamachine_code_cleanup_assert('dirty_worktree' === (string) ( $summary_json['top_blocked_examples'][0]['reason'] ?? '' ), 'cleanup evidence --summary includes example reason');
1548+
datamachine_code_cleanup_assert(8192 === (int) ( $summary_json['top_blocked_examples'][0]['size_bytes'] ?? 0 ), 'cleanup evidence --summary includes example size');
1549+
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');
1550+
1551+
WP_CLI::$logs = array();
1552+
WP_CLI::$successes = array();
1553+
$command->cleanup(array( 'evidence', 'cleanup-run-123' ), array( 'summary' => true ));
1554+
datamachine_code_cleanup_assert(in_array('Cleanup operator summary:', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints operator heading');
1555+
datamachine_code_cleanup_assert(in_array('table:6:metric,value', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints compact metric table');
1556+
datamachine_code_cleanup_assert(in_array('Top blocked examples:', WP_CLI::$logs, true), 'cleanup evidence --summary human output prints blocked examples');
1557+
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');
1558+
15351559
WP_CLI::$logs = array();
15361560
WP_CLI::$successes = array();
15371561
$command->cleanup(array( 'resume', 'cleanup-run-123' ), array( 'force' => true, 'format' => 'json' ));

0 commit comments

Comments
 (0)