Skip to content

Commit affb80b

Browse files
fix: compact bounded cleanup JSON output (#741)
* fix: compact bounded cleanup JSON output * fix: satisfy cleanup output lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent b28b919 commit affb80b

2 files changed

Lines changed: 250 additions & 1 deletion

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 233 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6385,7 +6385,8 @@ private function flatten_artifact_cleanup_rows( array $rows ): array {
63856385
private function render_worktree_bounded_cleanup_eligible_apply_result( array $result, array $assoc_args ): void {
63866386
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
63876387
if ( 'json' === $format ) {
6388-
$json = wp_json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
6388+
$report = ! empty($assoc_args['verbose']) ? $result : $this->compact_worktree_bounded_cleanup_eligible_apply_json($result);
6389+
$json = wp_json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
63896390
WP_CLI::log(false === $json ? '{}' : $json);
63906391
return;
63916392
}
@@ -6512,6 +6513,237 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
65126513
}
65136514
}
65146515

6516+
/**
6517+
* Compact bounded cleanup JSON for chat/operator output.
6518+
*
6519+
* @param array<string,mixed> $result Full bounded apply result.
6520+
* @return array<string,mixed>
6521+
*/
6522+
private function compact_worktree_bounded_cleanup_eligible_apply_json( array $result ): array {
6523+
$summary = (array) ( $result['summary'] ?? array() );
6524+
$candidates = (array) ( $result['candidates'] ?? array() );
6525+
$removed = (array) ( $result['removed'] ?? array() );
6526+
$skipped = (array) ( $result['skipped'] ?? array() );
6527+
$buckets = $this->build_cleanup_blocker_buckets($skipped);
6528+
$actions = $this->build_cleanup_next_actions($buckets, (array) ( $summary['skipped_next_commands'] ?? array() ));
6529+
6530+
$compact_summary = array_merge(
6531+
$summary,
6532+
array(
6533+
'processed' => (int) ( $summary['processed'] ?? $summary['inspected'] ?? count($candidates) ),
6534+
'would_remove' => (int) ( $summary['would_remove'] ?? ( ! empty($result['dry_run']) ? count($candidates) : 0 ) ),
6535+
'removed' => (int) ( $summary['removed'] ?? count($removed) ),
6536+
'skipped' => max( (int) ( $summary['skipped'] ?? 0 ), count($skipped) ),
6537+
'bytes_reclaimed' => (int) ( $summary['bytes_reclaimed'] ?? 0 ),
6538+
'skipped_by_reason' => array_map(fn( $bucket ) => (int) ( $bucket['count'] ?? 0 ), $buckets),
6539+
'blocker_bucket_count' => count($buckets),
6540+
)
6541+
);
6542+
6543+
$report = array(
6544+
'success' => (bool) ( $result['success'] ?? true ),
6545+
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
6546+
'dry_run' => ! empty($result['dry_run']),
6547+
'destructive' => ! empty($result['destructive']),
6548+
'workspace_path' => $result['workspace_path'] ?? null,
6549+
'generated_at' => $result['generated_at'] ?? null,
6550+
'summary' => $compact_summary,
6551+
'blocker_buckets' => $buckets,
6552+
'next_actions' => $actions,
6553+
'candidates' => $this->compact_cleanup_rows($candidates, 25),
6554+
'removed' => $this->compact_cleanup_rows($removed, 25),
6555+
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
6556+
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
6557+
);
6558+
6559+
if ( ! empty($result['job_backed']) ) {
6560+
$report['job_backed'] = true;
6561+
}
6562+
6563+
return array_filter($report, fn( $value ) => null !== $value);
6564+
}
6565+
6566+
/**
6567+
* Build skipped blocker buckets with bounded examples.
6568+
*
6569+
* @param array<int,array<string,mixed>> $rows Skipped rows.
6570+
* @return array<string,array<string,mixed>>
6571+
*/
6572+
private function build_cleanup_blocker_buckets( array $rows ): array {
6573+
$buckets = array();
6574+
foreach ( $rows as $row ) {
6575+
$reason_code = (string) ( $row['reason_code'] ?? 'unknown' );
6576+
if ( ! isset($buckets[ $reason_code ]) ) {
6577+
$buckets[ $reason_code ] = array(
6578+
'count' => 0,
6579+
'examples' => array(),
6580+
'reason' => (string) ( $row['reason'] ?? '' ),
6581+
);
6582+
}
6583+
++$buckets[ $reason_code ]['count'];
6584+
if ( count($buckets[ $reason_code ]['examples']) < 3 ) {
6585+
$buckets[ $reason_code ]['examples'][] = $this->compact_cleanup_row($row);
6586+
}
6587+
}
6588+
6589+
ksort($buckets);
6590+
return $buckets;
6591+
}
6592+
6593+
/**
6594+
* Build compact next actions for unresolved blocker classes.
6595+
*
6596+
* @param array<string,array<string,mixed>> $buckets Blocker buckets.
6597+
* @param array<int,array<string,mixed>> $commands Existing cleanup command hints.
6598+
* @return array<int,array<string,mixed>>
6599+
*/
6600+
private function build_cleanup_next_actions( array $buckets, array $commands ): array {
6601+
$by_reason = array();
6602+
foreach ( $commands as $command ) {
6603+
$reason_code = (string) ( $command['reason_code'] ?? '' );
6604+
if ( '' !== $reason_code ) {
6605+
$by_reason[ $reason_code ] = $command;
6606+
}
6607+
}
6608+
6609+
$defaults = array(
6610+
'active_no_signal' => array(
6611+
'command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=25 --offset=0 --format=json',
6612+
'destructive' => false,
6613+
),
6614+
'needs_metadata_reconcile' => array(
6615+
'command' => 'studio wp datamachine-code workspace worktree reconcile-metadata --dry-run --limit=25 --offset=0 --until-budget=30s --format=json',
6616+
'destructive' => false,
6617+
),
6618+
'lifecycle_reconciliation_candidate' => array(
6619+
'command' => 'studio wp datamachine-code workspace worktree cleanup --dry-run --format=json',
6620+
'destructive' => false,
6621+
),
6622+
'dirty_worktree' => array(
6623+
'command' => 'git -C <worktree-path> status --short --branch --untracked-files=normal',
6624+
'destructive' => false,
6625+
),
6626+
'unpushed_commits' => array(
6627+
'command' => 'git -C <worktree-path> log --oneline --decorate @{u}..HEAD',
6628+
'destructive' => false,
6629+
),
6630+
'stale_worktree_marker' => array(
6631+
'command' => 'git -C <primary-path> worktree prune --dry-run --verbose',
6632+
'destructive' => false,
6633+
),
6634+
'primary_missing' => array(
6635+
'command' => 'studio wp datamachine-code workspace show <repo>',
6636+
'destructive' => false,
6637+
),
6638+
'submodule_worktree' => array(
6639+
'command' => 'git -C <worktree-path> submodule status --recursive',
6640+
'destructive' => false,
6641+
),
6642+
'remove_timeout' => array(
6643+
'command' => 'studio wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --limit=25 --remove-timeout=<seconds>',
6644+
'destructive' => true,
6645+
),
6646+
);
6647+
6648+
$actions = array();
6649+
foreach ( $buckets as $reason_code => $bucket ) {
6650+
$hint = $by_reason[ $reason_code ] ?? $defaults[ $reason_code ] ?? array(
6651+
'command' => 'Re-run with --verbose --format=json and inspect this reason_code before retrying cleanup.',
6652+
'destructive' => false,
6653+
);
6654+
$actions[] = array(
6655+
'reason_code' => $reason_code,
6656+
'count' => (int) ( $bucket['count'] ?? 0 ),
6657+
'command' => (string) ( $hint['command'] ?? '' ),
6658+
'alternative' => (string) ( $hint['alternative'] ?? '' ),
6659+
'destructive' => ! empty($hint['destructive']),
6660+
);
6661+
}
6662+
6663+
return $actions;
6664+
}
6665+
6666+
/**
6667+
* Compact a bounded cleanup continuation without full handle lists.
6668+
*
6669+
* @param array<string,mixed> $continuation Continuation payload.
6670+
* @return array<string,mixed>
6671+
*/
6672+
private function compact_cleanup_continuation( array $continuation ): array {
6673+
if ( empty($continuation) ) {
6674+
return array();
6675+
}
6676+
6677+
$handles = array_values(array_filter(array_map('strval', (array) ( $continuation['remaining_handles'] ?? array() ))));
6678+
unset($continuation['remaining_handles']);
6679+
if ( ! isset($continuation['remaining_total']) && isset($continuation['total']) ) {
6680+
$continuation['remaining_total'] = (int) $continuation['total'];
6681+
}
6682+
$continuation['remaining_handles_count'] = count($handles);
6683+
$continuation['remaining_handles_examples'] = array_slice($handles, 0, 10);
6684+
if ( count($handles) > 10 ) {
6685+
$continuation['remaining_handles_truncated'] = true;
6686+
}
6687+
6688+
return $continuation;
6689+
}
6690+
6691+
/**
6692+
* Compact evidence while removing full skipped handle lists.
6693+
*
6694+
* @param array<string,mixed> $evidence Evidence payload.
6695+
* @param array<int,array<string,mixed>> $skipped Skipped rows.
6696+
* @return array<string,mixed>
6697+
*/
6698+
private function compact_cleanup_evidence( array $evidence, array $skipped ): array {
6699+
$skipped_handles = array_values(array_filter(array_map(fn( $row ) => (string) ( $row['handle'] ?? '' ), $skipped)));
6700+
unset($evidence['skipped_handles']);
6701+
$evidence['skipped_handles_count'] = count($skipped_handles);
6702+
$evidence['skipped_handles_examples'] = array_slice($skipped_handles, 0, 10);
6703+
if ( count($skipped_handles) > 10 ) {
6704+
$evidence['skipped_handles_truncated'] = true;
6705+
}
6706+
$evidence['full_detail_hint'] = 'Re-run with --verbose --format=json for full skipped rows and handle lists.';
6707+
6708+
return $evidence;
6709+
}
6710+
6711+
/**
6712+
* Compact cleanup rows to the fields operators need first.
6713+
*
6714+
* @param array<int,array<string,mixed>> $rows Rows to compact.
6715+
* @param int $limit Maximum rows.
6716+
* @return array<int,array<string,mixed>>
6717+
*/
6718+
private function compact_cleanup_rows( array $rows, int $limit ): array {
6719+
return array_map(fn( $row ) => $this->compact_cleanup_row( (array) $row ), array_slice($rows, 0, $limit));
6720+
}
6721+
6722+
/**
6723+
* Compact one cleanup row.
6724+
*
6725+
* @param array<string,mixed> $row Cleanup row.
6726+
* @return array<string,mixed>
6727+
*/
6728+
private function compact_cleanup_row( array $row ): array {
6729+
$compact = array(
6730+
'handle' => $row['handle'] ?? null,
6731+
'repo' => $row['repo'] ?? null,
6732+
'branch' => $row['branch'] ?? null,
6733+
'reason_code' => $row['reason_code'] ?? $row['signal'] ?? null,
6734+
'reason' => isset($row['reason']) ? $this->shorten_cleanup_reason( (string) $row['reason'] ) : null,
6735+
'path' => $row['path'] ?? null,
6736+
);
6737+
6738+
foreach ( array( 'size_bytes', 'artifact_size_bytes', 'dirty', 'unpushed', 'created_at', 'liveness', 'pr_url' ) as $field ) {
6739+
if ( array_key_exists($field, $row) ) {
6740+
$compact[ $field ] = $row[ $field ];
6741+
}
6742+
}
6743+
6744+
return array_filter($compact, fn( $value ) => null !== $value && '' !== $value && array() !== $value);
6745+
}
6746+
65156747
private function render_worktree_emergency_cleanup_result( array $result, array $assoc_args ): void {
65166748
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
65176749
if ( 'json' === $format ) {

tests/smoke-worktree-cleanup-cli.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,11 +1896,28 @@ public function execute( array $input ): array
18961896
WP_CLI::$successes = array();
18971897
$command->worktree(array( 'bounded-cleanup-eligible-apply' ), array( 'limit' => 25, 'remove-timeout' => 120, 'format' => 'json' ));
18981898
datamachine_code_cleanup_assert(120 === (int) ( $bounded_apply_ability->last_input['remove_timeout'] ?? 0 ), 'bounded cleanup apply forwards remove-timeout');
1899+
$bounded_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1900+
datamachine_code_cleanup_assert(JSON_ERROR_NONE === json_last_error(), 'bounded cleanup compact JSON parses cleanly');
1901+
datamachine_code_cleanup_assert(32 === (int) ( $bounded_json['summary']['skipped'] ?? 0 ), 'bounded cleanup compact JSON reports full skipped count');
1902+
datamachine_code_cleanup_assert(! isset($bounded_json['skipped']), 'bounded cleanup compact JSON omits full skipped rows by default');
1903+
datamachine_code_cleanup_assert(15 === (int) ( $bounded_json['blocker_buckets']['active_no_signal']['count'] ?? 0 ), 'bounded cleanup compact JSON buckets active/no-signal blockers');
1904+
datamachine_code_cleanup_assert('repo@blocked-0' === (string) ( $bounded_json['blocker_buckets']['active_no_signal']['examples'][0]['handle'] ?? '' ), 'bounded cleanup compact JSON includes bounded blocker examples');
1905+
datamachine_code_cleanup_assert(str_contains((string) wp_json_encode($bounded_json['next_actions'] ?? array()), 'active-no-signal-report'), 'bounded cleanup compact JSON includes actionable next actions');
1906+
datamachine_code_cleanup_assert(! isset($bounded_json['continuation']['remaining_handles']), 'bounded cleanup compact JSON omits full remaining handle lists');
1907+
datamachine_code_cleanup_assert(32 === (int) ( $bounded_json['continuation']['remaining_handles_count'] ?? 0 ), 'bounded cleanup compact JSON keeps remaining handle count');
1908+
datamachine_code_cleanup_assert(10 === count($bounded_json['evidence']['skipped_handles_examples'] ?? array()), 'bounded cleanup compact JSON keeps bounded skipped handle examples');
18991909

19001910
WP_CLI::$logs = array();
19011911
WP_CLI::$successes = array();
19021912
$command->worktree(array( 'bounded-cleanup-eligible-apply' ), array( 'limit' => 25, 'verbose' => true ));
19031913
datamachine_code_cleanup_assert(in_array('table:32:handle,reason_code,reason', WP_CLI::$logs, true), 'bounded cleanup apply verbose output lists skipped rows');
1914+
1915+
WP_CLI::$logs = array();
1916+
WP_CLI::$successes = array();
1917+
$command->worktree(array( 'bounded-cleanup-eligible-apply' ), array( 'limit' => 25, 'verbose' => true, 'format' => 'json' ));
1918+
$bounded_verbose_json = json_decode(WP_CLI::$logs[0] ?? '', true);
1919+
datamachine_code_cleanup_assert(32 === count($bounded_verbose_json['skipped'] ?? array()), 'bounded cleanup verbose JSON preserves full skipped rows');
1920+
datamachine_code_cleanup_assert(32 === count($bounded_verbose_json['pagination']['remaining_handles'] ?? array()), 'bounded cleanup verbose JSON preserves full remaining handles');
19041921
$bounded_apply_ability->extra_skipped = 0;
19051922

19061923
echo "\n[8a] active/no-signal apply forwards bounded continuation flags\n";

0 commit comments

Comments
 (0)