Skip to content

Commit 369f6b9

Browse files
authored
Merge pull request #813 from Extra-Chill/fix/cleanup-compact-operator-summaries-809
Compact worktree cleanup operator summaries
2 parents f5e037a + cfc6881 commit 369f6b9

3 files changed

Lines changed: 80 additions & 236 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 16 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -5346,6 +5346,9 @@ private function render_worktree_metadata_reconciliation_result( array $result,
53465346
private function render_worktree_active_no_signal_report_result( array $result, array $assoc_args ): void {
53475347
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
53485348
if ( 'json' === $format ) {
5349+
if ( empty($assoc_args['verbose']) ) {
5350+
$result = WorkspaceCompactOutput::cleanup_result($result);
5351+
}
53495352
$this->renderer()->json($result);
53505353
return;
53515354
}
@@ -5436,6 +5439,9 @@ private function render_worktree_active_no_signal_report_result( array $result,
54365439
private function render_worktree_active_no_signal_finalized_apply_result( array $result, array $assoc_args ): void {
54375440
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
54385441
if ( 'json' === $format ) {
5442+
if ( empty($assoc_args['verbose']) ) {
5443+
$result = WorkspaceCompactOutput::cleanup_result($result);
5444+
}
54395445
$this->renderer()->json($result);
54405446
return;
54415447
}
@@ -5526,6 +5532,9 @@ private function render_worktree_active_no_signal_finalized_apply_result( array
55265532
private function render_worktree_active_no_signal_equivalent_clean_apply_result( array $result, array $assoc_args ): void {
55275533
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
55285534
if ( 'json' === $format ) {
5535+
if ( empty($assoc_args['verbose']) ) {
5536+
$result = WorkspaceCompactOutput::cleanup_result($result);
5537+
}
55295538
$this->renderer()->json($result);
55305539
return;
55315540
}
@@ -5616,6 +5625,9 @@ private function render_worktree_active_no_signal_equivalent_clean_apply_result(
56165625
private function render_worktree_active_no_signal_merged_apply_result( array $result, array $assoc_args ): void {
56175626
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
56185627
if ( 'json' === $format ) {
5628+
if ( empty($assoc_args['verbose']) ) {
5629+
$result = WorkspaceCompactOutput::cleanup_result($result);
5630+
}
56195631
$this->renderer()->json($result);
56205632
return;
56215633
}
@@ -5706,6 +5718,9 @@ private function render_worktree_active_no_signal_merged_apply_result( array $re
57065718
private function render_worktree_active_no_signal_remote_clean_apply_result( array $result, array $assoc_args ): void {
57075719
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
57085720
if ( 'json' === $format ) {
5721+
if ( empty($assoc_args['verbose']) ) {
5722+
$result = WorkspaceCompactOutput::cleanup_result($result);
5723+
}
57095724
$this->renderer()->json($result);
57105725
return;
57115726
}
@@ -5945,7 +5960,7 @@ private function flatten_artifact_cleanup_rows( array $rows ): array {
59455960
private function render_worktree_bounded_cleanup_eligible_apply_result( array $result, array $assoc_args ): void {
59465961
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
59475962
if ( 'json' === $format ) {
5948-
$report = ! empty($assoc_args['verbose']) ? $result : $this->compact_worktree_bounded_cleanup_eligible_apply_json($result);
5963+
$report = ! empty($assoc_args['verbose']) ? $result : WorkspaceCompactOutput::cleanup_result($result);
59495964
$this->renderer()->json($report);
59505965
return;
59515966
}
@@ -6183,57 +6198,6 @@ private function render_worktree_cleanup_eligible_drain_result( array $result, a
61836198
}
61846199
}
61856200

6186-
/**
6187-
* Compact bounded cleanup JSON for chat/operator output.
6188-
*
6189-
* @param array<string,mixed> $result Full bounded apply result.
6190-
* @return array<string,mixed>
6191-
*/
6192-
private function compact_worktree_bounded_cleanup_eligible_apply_json( array $result ): array {
6193-
$summary = (array) ( $result['summary'] ?? array() );
6194-
$candidates = (array) ( $result['candidates'] ?? array() );
6195-
$removed = (array) ( $result['removed'] ?? array() );
6196-
$skipped = (array) ( $result['skipped'] ?? array() );
6197-
$buckets = $this->build_cleanup_blocker_buckets($skipped);
6198-
$actions = $this->build_cleanup_next_actions($buckets, (array) ( $summary['skipped_next_commands'] ?? array() ));
6199-
6200-
$compact_summary = array_merge(
6201-
$summary,
6202-
array(
6203-
'processed' => (int) ( $summary['processed'] ?? $summary['inspected'] ?? count($candidates) ),
6204-
'would_remove' => (int) ( $summary['would_remove'] ?? ( ! empty($result['dry_run']) ? count($candidates) : 0 ) ),
6205-
'removed' => (int) ( $summary['removed'] ?? count($removed) ),
6206-
'skipped' => max( (int) ( $summary['skipped'] ?? 0 ), count($skipped) ),
6207-
'bytes_reclaimed' => (int) ( $summary['bytes_reclaimed'] ?? 0 ),
6208-
'skipped_by_reason' => array_map(fn( $bucket ) => (int) ( $bucket['count'] ?? 0 ), $buckets),
6209-
'blocker_bucket_count' => count($buckets),
6210-
)
6211-
);
6212-
6213-
$report = array(
6214-
'success' => (bool) ( $result['success'] ?? true ),
6215-
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
6216-
'dry_run' => ! empty($result['dry_run']),
6217-
'destructive' => ! empty($result['destructive']),
6218-
'workspace_path' => $result['workspace_path'] ?? null,
6219-
'generated_at' => $result['generated_at'] ?? null,
6220-
'summary' => $compact_summary,
6221-
'blocker_buckets' => $buckets,
6222-
'next_actions' => $actions,
6223-
'active_no_signal_triage' => (array) ( $result['active_no_signal_triage'] ?? array() ),
6224-
'candidates' => $this->compact_cleanup_rows($candidates, 25),
6225-
'removed' => $this->compact_cleanup_rows($removed, 25),
6226-
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
6227-
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
6228-
);
6229-
6230-
if ( ! empty($result['job_backed']) ) {
6231-
$report['job_backed'] = true;
6232-
}
6233-
6234-
return array_filter($report, fn( $value ) => null !== $value);
6235-
}
6236-
62376201
/**
62386202
* Render concise active/no-signal triage preview from bounded cleanup output.
62396203
*
@@ -6283,187 +6247,6 @@ private function render_active_no_signal_triage_preview( array $preview ): void
62836247
}
62846248
}
62856249

6286-
/**
6287-
* Build skipped blocker buckets with bounded examples.
6288-
*
6289-
* @param array<int,array<string,mixed>> $rows Skipped rows.
6290-
* @return array<string,array<string,mixed>>
6291-
*/
6292-
private function build_cleanup_blocker_buckets( array $rows ): array {
6293-
$buckets = array();
6294-
foreach ( $rows as $row ) {
6295-
$reason_code = (string) ( $row['reason_code'] ?? 'unknown' );
6296-
if ( ! isset($buckets[ $reason_code ]) ) {
6297-
$buckets[ $reason_code ] = array(
6298-
'count' => 0,
6299-
'examples' => array(),
6300-
'reason' => (string) ( $row['reason'] ?? '' ),
6301-
);
6302-
}
6303-
++$buckets[ $reason_code ]['count'];
6304-
if ( count($buckets[ $reason_code ]['examples']) < 3 ) {
6305-
$buckets[ $reason_code ]['examples'][] = $this->compact_cleanup_row($row);
6306-
}
6307-
}
6308-
6309-
ksort($buckets);
6310-
return $buckets;
6311-
}
6312-
6313-
/**
6314-
* Build compact next actions for unresolved blocker classes.
6315-
*
6316-
* @param array<string,array<string,mixed>> $buckets Blocker buckets.
6317-
* @param array<int,array<string,mixed>> $commands Existing cleanup command hints.
6318-
* @return array<int,array<string,mixed>>
6319-
*/
6320-
private function build_cleanup_next_actions( array $buckets, array $commands ): array {
6321-
$by_reason = array();
6322-
foreach ( $commands as $command ) {
6323-
$reason_code = (string) ( $command['reason_code'] ?? '' );
6324-
if ( '' !== $reason_code ) {
6325-
$by_reason[ $reason_code ] = $command;
6326-
}
6327-
}
6328-
6329-
$defaults = array(
6330-
'active_no_signal' => array(
6331-
'command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=25 --offset=0 --format=json',
6332-
'destructive' => false,
6333-
),
6334-
'needs_metadata_reconcile' => array(
6335-
'command' => 'studio wp datamachine-code workspace worktree reconcile-metadata --dry-run --limit=25 --offset=0 --until-budget=30s --format=json',
6336-
'destructive' => false,
6337-
),
6338-
'lifecycle_reconciliation_candidate' => array(
6339-
'command' => 'studio wp datamachine-code workspace worktree cleanup --dry-run --format=json',
6340-
'destructive' => false,
6341-
),
6342-
'dirty_worktree' => array(
6343-
'command' => 'git -C <worktree-path> status --short --branch --untracked-files=normal',
6344-
'destructive' => false,
6345-
),
6346-
'unpushed_commits' => array(
6347-
'command' => 'git -C <worktree-path> log --oneline --decorate @{u}..HEAD',
6348-
'destructive' => false,
6349-
),
6350-
'stale_worktree_marker' => array(
6351-
'command' => 'git -C <primary-path> worktree prune --dry-run --verbose',
6352-
'destructive' => false,
6353-
),
6354-
'primary_missing' => array(
6355-
'command' => 'studio wp datamachine-code workspace show <repo>',
6356-
'destructive' => false,
6357-
),
6358-
'submodule_worktree' => array(
6359-
'command' => 'git -C <worktree-path> submodule status --recursive',
6360-
'destructive' => false,
6361-
),
6362-
'remove_timeout' => array(
6363-
'command' => 'studio wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --limit=25 --remove-timeout=<seconds>',
6364-
'destructive' => true,
6365-
),
6366-
);
6367-
6368-
$actions = array();
6369-
foreach ( $buckets as $reason_code => $bucket ) {
6370-
$hint = $by_reason[ $reason_code ] ?? $defaults[ $reason_code ] ?? array(
6371-
'command' => 'Re-run with --verbose --format=json and inspect this reason_code before retrying cleanup.',
6372-
'destructive' => false,
6373-
);
6374-
$actions[] = array(
6375-
'reason_code' => $reason_code,
6376-
'count' => (int) ( $bucket['count'] ?? 0 ),
6377-
'command' => (string) ( $hint['command'] ?? '' ),
6378-
'alternative' => (string) ( $hint['alternative'] ?? '' ),
6379-
'destructive' => ! empty($hint['destructive']),
6380-
);
6381-
}
6382-
6383-
return $actions;
6384-
}
6385-
6386-
/**
6387-
* Compact a bounded cleanup continuation without full handle lists.
6388-
*
6389-
* @param array<string,mixed> $continuation Continuation payload.
6390-
* @return array<string,mixed>
6391-
*/
6392-
private function compact_cleanup_continuation( array $continuation ): array {
6393-
if ( empty($continuation) ) {
6394-
return array();
6395-
}
6396-
6397-
$handles = array_values(array_filter(array_map('strval', (array) ( $continuation['remaining_handles'] ?? array() ))));
6398-
unset($continuation['remaining_handles']);
6399-
if ( ! isset($continuation['remaining_total']) && isset($continuation['total']) ) {
6400-
$continuation['remaining_total'] = (int) $continuation['total'];
6401-
}
6402-
$continuation['remaining_handles_count'] = count($handles);
6403-
$continuation['remaining_handles_examples'] = array_slice($handles, 0, 10);
6404-
if ( count($handles) > 10 ) {
6405-
$continuation['remaining_handles_truncated'] = true;
6406-
}
6407-
6408-
return $continuation;
6409-
}
6410-
6411-
/**
6412-
* Compact evidence while removing full skipped handle lists.
6413-
*
6414-
* @param array<string,mixed> $evidence Evidence payload.
6415-
* @param array<int,array<string,mixed>> $skipped Skipped rows.
6416-
* @return array<string,mixed>
6417-
*/
6418-
private function compact_cleanup_evidence( array $evidence, array $skipped ): array {
6419-
$skipped_handles = array_values(array_filter(array_map(fn( $row ) => (string) ( $row['handle'] ?? '' ), $skipped)));
6420-
unset($evidence['skipped_handles']);
6421-
$evidence['skipped_handles_count'] = count($skipped_handles);
6422-
$evidence['skipped_handles_examples'] = array_slice($skipped_handles, 0, 10);
6423-
if ( count($skipped_handles) > 10 ) {
6424-
$evidence['skipped_handles_truncated'] = true;
6425-
}
6426-
$evidence['full_detail_hint'] = 'Re-run with --verbose --format=json for full skipped rows and handle lists.';
6427-
6428-
return $evidence;
6429-
}
6430-
6431-
/**
6432-
* Compact cleanup rows to the fields operators need first.
6433-
*
6434-
* @param array<int,array<string,mixed>> $rows Rows to compact.
6435-
* @param int $limit Maximum rows.
6436-
* @return array<int,array<string,mixed>>
6437-
*/
6438-
private function compact_cleanup_rows( array $rows, int $limit ): array {
6439-
return array_map(fn( $row ) => $this->compact_cleanup_row( (array) $row ), array_slice($rows, 0, $limit));
6440-
}
6441-
6442-
/**
6443-
* Compact one cleanup row.
6444-
*
6445-
* @param array<string,mixed> $row Cleanup row.
6446-
* @return array<string,mixed>
6447-
*/
6448-
private function compact_cleanup_row( array $row ): array {
6449-
$compact = array(
6450-
'handle' => $row['handle'] ?? null,
6451-
'repo' => $row['repo'] ?? null,
6452-
'branch' => $row['branch'] ?? null,
6453-
'reason_code' => $row['reason_code'] ?? $row['signal'] ?? null,
6454-
'reason' => isset($row['reason']) ? $this->shorten_cleanup_reason( (string) $row['reason'] ) : null,
6455-
'path' => $row['path'] ?? null,
6456-
);
6457-
6458-
foreach ( array( 'size_bytes', 'artifact_size_bytes', 'dirty', 'unpushed', 'created_at', 'liveness', 'pr_url' ) as $field ) {
6459-
if ( array_key_exists($field, $row) ) {
6460-
$compact[ $field ] = $row[ $field ];
6461-
}
6462-
}
6463-
6464-
return array_filter($compact, fn( $value ) => null !== $value && '' !== $value && array() !== $value);
6465-
}
6466-
64676250
private function render_worktree_emergency_cleanup_result( array $result, array $assoc_args ): void {
64686251
$format = isset($assoc_args['format']) ? (string) $assoc_args['format'] : 'table';
64696252
if ( 'json' === $format ) {

inc/Cli/WorkspaceCompactOutput.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class WorkspaceCompactOutput {
1515

1616
public static function cleanup_result( array $result ): array {
1717
$summary = (array) ( $result['summary'] ?? array() );
18-
$candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? array() );
19-
$removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? array() );
18+
$candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? $result['worktree_candidates'] ?? $result['rows'] ?? $result['planned'] ?? array() );
19+
$removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? $result['written'] ?? array() );
2020
$skipped = (array) ( $result['skipped'] ?? array() );
2121

2222
return self::filter_empty(
@@ -144,7 +144,7 @@ public static function lock_result( array $result ): array {
144144

145145
private static function row_counts( array $result ): array {
146146
$counts = array();
147-
foreach ( array( 'candidates', 'artifact_candidates', 'worktree_candidates', 'removed', 'removed_artifacts', 'removed_worktrees', 'skipped', 'written', 'proposals', 'pass_results' ) as $key ) {
147+
foreach ( array( 'candidates', 'artifact_candidates', 'worktree_candidates', 'rows', 'planned', 'removed', 'removed_artifacts', 'removed_worktrees', 'written', 'skipped', 'proposals', 'pass_results' ) as $key ) {
148148
if ( isset( $result[ $key ] ) && is_array( $result[ $key ] ) ) {
149149
$counts[ $key ] = count( $result[ $key ] );
150150
}
@@ -204,6 +204,14 @@ private static function next_commands( array $result, array $summary ): array {
204204
$commands[] = (string) $summary[ $field ];
205205
}
206206
}
207+
foreach ( array( 'pagination', 'continuation' ) as $bucket ) {
208+
if ( ! empty($result[ $bucket ]['next_command']) ) {
209+
$commands[] = (string) $result[ $bucket ]['next_command'];
210+
}
211+
if ( ! empty($summary[ $bucket ]['next_command']) ) {
212+
$commands[] = (string) $summary[ $bucket ]['next_command'];
213+
}
214+
}
207215
$deduped = array();
208216
$seen = array();
209217
foreach ( $commands as $command ) {

tests/workspace-compact-output.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,59 @@ function compact_output_large_rows( int $count ): array {
7171
compact_output_assert(count((array) ( $cleanup['samples']['skipped'] ?? array() )) <= 5, 'Compact cleanup output must sample skipped rows.');
7272
compact_output_assert(! empty($cleanup['next_commands']), 'Compact cleanup output must preserve next commands.');
7373

74+
$active_report = WorkspaceCompactOutput::cleanup_result(
75+
array(
76+
'success' => true,
77+
'mode' => 'active_no_signal_report',
78+
'rows' => $large_rows,
79+
'summary' => array(
80+
'total_active_no_signal' => 40,
81+
'inspected' => 40,
82+
'by_suggested_action' => array( 'remote_tracking_clean' => 40 ),
83+
),
84+
'pagination' => array(
85+
'total' => 80,
86+
'offset' => 0,
87+
'limit' => 40,
88+
'next_offset' => 40,
89+
'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=40 --offset=40 --format=json',
90+
),
91+
)
92+
);
93+
94+
compact_output_assert(! isset($active_report['rows']), 'Compact active/no-signal report must omit full rows array.');
95+
compact_output_assert(40 === ( $active_report['row_counts']['rows'] ?? null ), 'Compact active/no-signal report must preserve row count.');
96+
compact_output_assert(40 === ( $active_report['pagination']['next_offset'] ?? null ), 'Compact active/no-signal report must preserve pagination.');
97+
compact_output_assert(in_array('studio wp datamachine-code workspace worktree active-no-signal-report --limit=40 --offset=40 --format=json', (array) ( $active_report['next_commands'] ?? array() ), true), 'Compact active/no-signal report must expose next page command.');
98+
99+
$active_apply = WorkspaceCompactOutput::cleanup_result(
100+
array(
101+
'success' => true,
102+
'mode' => 'active_no_signal_remote_clean_apply',
103+
'dry_run' => true,
104+
'planned' => $large_rows,
105+
'written' => $large_rows,
106+
'skipped' => $large_rows,
107+
'summary' => array(
108+
'inspected' => 40,
109+
'planned' => 40,
110+
'written' => 40,
111+
'skipped' => 40,
112+
'skipped_by_reason' => array( 'not_remote_tracking_clean' => 40 ),
113+
),
114+
'pagination' => array(
115+
'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run --limit=40 --offset=40 --format=json',
116+
),
117+
)
118+
);
119+
120+
compact_output_assert(! isset($active_apply['planned']), 'Compact active/no-signal apply must omit full planned array.');
121+
compact_output_assert(! isset($active_apply['written']), 'Compact active/no-signal apply must omit full written array.');
122+
compact_output_assert(40 === ( $active_apply['row_counts']['planned'] ?? null ), 'Compact active/no-signal apply must preserve planned count.');
123+
compact_output_assert(40 === ( $active_apply['row_counts']['written'] ?? null ), 'Compact active/no-signal apply must preserve written count.');
124+
compact_output_assert(40 === ( $active_apply['blockers']['not_remote_tracking_clean']['count'] ?? null ), 'Compact active/no-signal apply must preserve blocker counts from summary.');
125+
compact_output_assert(in_array('studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run --limit=40 --offset=40 --format=json', (array) ( $active_apply['next_commands'] ?? array() ), true), 'Compact active/no-signal apply must expose next page command.');
126+
74127
$locks = WorkspaceCompactOutput::lock_result(
75128
array(
76129
'active' => 2,

0 commit comments

Comments
 (0)