Skip to content

Commit 01da005

Browse files
Make cleanup blocker size accounting explicit (#843)
* fix: make cleanup blocker sizes explicit * Fix cleanup size accounting lint * Fix cleanup blocker byte alignment --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent c1ff2dd commit 01da005

5 files changed

Lines changed: 338 additions & 26 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,14 +2086,61 @@ private function render_cleanup_plan_blockers( array $blockers ): void {
20862086
$rows[] = array(
20872087
'reason' => (string) $reason,
20882088
'count' => (int) ( $bucket['count'] ?? 0 ),
2089-
'bytes' => $this->format_bytes($bucket['size_bytes'] ?? 0),
2089+
'bytes' => $this->format_cleanup_size_accounting($bucket),
20902090
'repos' => implode(', ', array_slice($repos, 0, 5)),
2091-
'examples' => implode(', ', array_slice(array_map('strval', (array) ( $bucket['examples'] ?? array() )), 0, 5)),
2091+
'examples' => implode(', ', array_slice(array_map(fn( $row ) => $this->format_cleanup_blocker_example($row), (array) ( $bucket['examples'] ?? array() )), 0, 5)),
20922092
);
20932093
}
20942094
$this->format_items($rows, array( 'reason', 'count', 'bytes', 'repos', 'examples' ), array( 'format' => 'table' ), 'reason');
20952095
}
20962096

2097+
/**
2098+
* Format blocker size accounting without hiding skipped/unknown probes as 0 B.
2099+
*
2100+
* @param array<string,mixed> $bucket Blocker bucket.
2101+
* @return string
2102+
*/
2103+
private function format_cleanup_size_accounting( array $bucket ): string {
2104+
$accounting = (array) ( $bucket['size_accounting'] ?? array() );
2105+
$known = (int) ( $accounting['known_count'] ?? 0 );
2106+
$known_zero = (int) ( $accounting['known_zero_count'] ?? 0 );
2107+
$skipped = (int) ( $accounting['skipped_count'] ?? 0 );
2108+
$unknown = (int) ( $accounting['unknown_count'] ?? 0 );
2109+
$bytes = array_key_exists('known_bytes', $accounting) ? (int) $accounting['known_bytes'] : (int) ( $bucket['size_bytes'] ?? 0 );
2110+
2111+
$parts = array();
2112+
if ( $known > 0 ) {
2113+
$parts[] = sprintf('%s known', $this->format_bytes($bytes));
2114+
}
2115+
if ( $known_zero > 0 ) {
2116+
$parts[] = sprintf('%d true zero', $known_zero);
2117+
}
2118+
if ( $skipped > 0 ) {
2119+
$parts[] = sprintf('%d skipped', $skipped);
2120+
}
2121+
if ( $unknown > 0 ) {
2122+
$parts[] = sprintf('%d unknown', $unknown);
2123+
}
2124+
2125+
return array() === $parts ? $this->format_bytes($bucket['size_bytes'] ?? null) : implode('; ', $parts);
2126+
}
2127+
2128+
/**
2129+
* Format a compact blocker example that may carry size status metadata.
2130+
*
2131+
* @param mixed $row Example row.
2132+
* @return string
2133+
*/
2134+
private function format_cleanup_blocker_example( mixed $row ): string {
2135+
if ( ! is_array($row) ) {
2136+
return (string) $row;
2137+
}
2138+
2139+
$handle = (string) ( $row['handle'] ?? $row['path'] ?? '' );
2140+
$status = (string) ( $row['size_status'] ?? '' );
2141+
return '' === $status ? $handle : sprintf('%s (%s)', $handle, $status);
2142+
}
2143+
20972144
/**
20982145
* Render directly executable recommended cleanup commands.
20992146
*

inc/Cli/WorkspaceCompactOutput.php

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ public static function hygiene_report( array $report ): array {
9595
'cleanup' => array(
9696
'blocker_probe_source' => $cleanup['blocker_probe_source'] ?? null,
9797
'blocker_counts' => $cleanup['blocker_counts'] ?? null,
98-
'summary' => (array) ( $cleanup['summary'] ?? array() ),
99-
'biggest_candidates' => self::compact_rows( (array) ( $cleanup['biggest_candidates'] ?? array() ) ),
98+
'summary' => (array) ( $cleanup['summary'] ?? array() ),
99+
'biggest_candidates' => self::compact_rows( (array) ( $cleanup['biggest_candidates'] ?? array() ) ),
100100
),
101101
'size' => array(
102102
'mode' => $size['mode'] ?? null,
@@ -172,8 +172,9 @@ private static function blocker_buckets( array $rows, array $counts = array() ):
172172
$buckets = array();
173173
foreach ( $counts as $reason => $count ) {
174174
$buckets[ (string) $reason ] = array(
175-
'count' => (int) $count,
176-
'examples' => array(),
175+
'count' => (int) $count,
176+
'size_accounting' => self::empty_size_accounting(),
177+
'examples' => array(),
177178
);
178179
}
179180
foreach ( $rows as $row ) {
@@ -182,12 +183,14 @@ private static function blocker_buckets( array $rows, array $counts = array() ):
182183
}
183184
$reason = (string) ( $row['reason_code'] ?? $row['reason'] ?? 'unknown' );
184185
$buckets[ $reason ] ??= array(
185-
'count' => 0,
186-
'examples' => array(),
186+
'count' => 0,
187+
'size_accounting' => self::empty_size_accounting(),
188+
'examples' => array(),
187189
);
188190
if ( ! isset( $counts[ $reason ] ) ) {
189191
++$buckets[ $reason ]['count'];
190192
}
193+
$buckets[ $reason ]['size_accounting'] = self::add_row_size_accounting( (array) $buckets[ $reason ]['size_accounting'], $row );
191194
if ( count( $buckets[ $reason ]['examples'] ) < 3 ) {
192195
$buckets[ $reason ]['examples'][] = self::compact_row( $row );
193196
}
@@ -313,9 +316,67 @@ private static function compact_row( array $row ): array {
313316
$compact[ $field ] = $row[ $field ];
314317
}
315318
}
319+
foreach ( array( 'size_status', 'size_accounting', 'fields_skipped' ) as $field ) {
320+
if ( array_key_exists( $field, $row ) ) {
321+
$compact[ $field ] = $row[ $field ];
322+
}
323+
}
316324
return self::filter_empty( $compact );
317325
}
318326

327+
private static function empty_size_accounting(): array {
328+
return array(
329+
'known_bytes' => 0,
330+
'known_count' => 0,
331+
'known_zero_count' => 0,
332+
'skipped_count' => 0,
333+
'unknown_count' => 0,
334+
);
335+
}
336+
337+
private static function add_row_size_accounting( array $accounting, array $row ): array {
338+
$accounting = array_merge( self::empty_size_accounting(), array_map( 'intval', $accounting ) );
339+
$status = self::row_size_status( $row );
340+
if ( 'known' === $status || 'known_zero' === $status ) {
341+
++$accounting['known_count'];
342+
$accounting['known_bytes'] += self::row_known_bytes( $row );
343+
if ( 'known_zero' === $status ) {
344+
++$accounting['known_zero_count'];
345+
}
346+
} elseif ( 'skipped' === $status ) {
347+
++$accounting['skipped_count'];
348+
} else {
349+
++$accounting['unknown_count'];
350+
}
351+
352+
return $accounting;
353+
}
354+
355+
private static function row_size_status( array $row ): string {
356+
if ( isset( $row['size_accounting'] ) && is_array( $row['size_accounting'] ) && isset( $row['size_accounting']['status'] ) ) {
357+
return (string) $row['size_accounting']['status'];
358+
}
359+
foreach ( array( 'artifact_size_bytes', 'size_bytes', 'bytes_reclaimed' ) as $field ) {
360+
if ( array_key_exists( $field, $row ) && is_numeric( $row[ $field ] ) ) {
361+
return 0 === max( 0, (int) $row[ $field ] ) ? 'known_zero' : 'known';
362+
}
363+
}
364+
$skipped = array_map( 'strval', (array) ( $row['fields_skipped'] ?? array() ) );
365+
return array_intersect( $skipped, array( 'disk', 'size', 'sizes' ) ) ? 'skipped' : 'unknown';
366+
}
367+
368+
private static function row_known_bytes( array $row ): int {
369+
if ( isset( $row['size_accounting'] ) && is_array( $row['size_accounting'] ) && isset( $row['size_accounting']['bytes'] ) && is_numeric( $row['size_accounting']['bytes'] ) ) {
370+
return max( 0, (int) $row['size_accounting']['bytes'] );
371+
}
372+
foreach ( array( 'artifact_size_bytes', 'size_bytes', 'bytes_reclaimed' ) as $field ) {
373+
if ( array_key_exists( $field, $row ) && is_numeric( $row[ $field ] ) ) {
374+
return max( 0, (int) $row[ $field ] );
375+
}
376+
}
377+
return 0;
378+
}
379+
319380
private static function filter_empty( array $data ): array {
320381
return array_filter( $data, static fn( $value ) => null !== $value && '' !== $value && array() !== $value );
321382
}

inc/Workspace/WorkspaceCleanupPlan.php

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,13 @@ private function prepare_cleanup_plan_blocked_rows( string $type, array $rows ):
271271
if ( ! is_array($row) ) {
272272
continue;
273273
}
274-
$row['row_type'] = $type . '_blocked';
275-
$row['safety_class'] = 'blocked';
276-
$row['row_id'] = $this->stable_cleanup_row_id($type . '_blocked', $row);
277-
$result[] = $row;
274+
$accounting = $this->cleanup_plan_row_size_accounting($row);
275+
$row['size_status'] = $accounting['status'];
276+
$row['size_accounting'] = $accounting;
277+
$row['row_type'] = $type . '_blocked';
278+
$row['safety_class'] = 'blocked';
279+
$row['row_id'] = $this->stable_cleanup_row_id($type . '_blocked', $row);
280+
$result[] = $row;
278281
}
279282
return $result;
280283
}
@@ -485,7 +488,60 @@ private function build_cleanup_plan_continuation( array $artifact_plan, array $w
485488
* @return int
486489
*/
487490
private function cleanup_plan_reclaimable_bytes( array $row ): int {
488-
return max(0, (int) ( $row['artifact_size_bytes'] ?? $row['size_bytes'] ?? 0 ));
491+
$accounting = $this->cleanup_plan_row_size_accounting($row);
492+
return null === $accounting['bytes'] ? 0 : max(0, (int) $accounting['bytes']);
493+
}
494+
495+
/**
496+
* Return explicit size accounting for a cleanup row.
497+
*
498+
* @param array<string,mixed> $row Cleanup row.
499+
* @return array<string,mixed>
500+
*/
501+
private function cleanup_plan_row_size_accounting( array $row ): array {
502+
foreach ( array( 'artifact_size_bytes', 'size_bytes', 'bytes_reclaimed' ) as $field ) {
503+
if ( array_key_exists($field, $row) && is_numeric($row[ $field ]) ) {
504+
$bytes = max(0, (int) $row[ $field ]);
505+
return array(
506+
'status' => 0 === $bytes ? 'known_zero' : 'known',
507+
'bytes' => $bytes,
508+
'source' => $field,
509+
);
510+
}
511+
}
512+
513+
$artifact_bytes = 0;
514+
$artifact_known = false;
515+
foreach ( (array) ( $row['artifacts'] ?? array() ) as $artifact ) {
516+
if ( is_array($artifact) && array_key_exists('size_bytes', $artifact) && is_numeric($artifact['size_bytes']) ) {
517+
$artifact_known = true;
518+
$artifact_bytes += max(0, (int) $artifact['size_bytes']);
519+
}
520+
}
521+
if ( $artifact_known ) {
522+
return array(
523+
'status' => 0 === $artifact_bytes ? 'known_zero' : 'known',
524+
'bytes' => $artifact_bytes,
525+
'source' => 'artifacts.size_bytes',
526+
);
527+
}
528+
529+
$skipped = array_map('strval', (array) ( $row['fields_skipped'] ?? array() ));
530+
if ( array_intersect($skipped, array( 'disk', 'size', 'sizes' )) ) {
531+
return array(
532+
'status' => 'skipped',
533+
'bytes' => null,
534+
'source' => null,
535+
'reason' => 'disk_size_probe_skipped',
536+
);
537+
}
538+
539+
return array(
540+
'status' => 'unknown',
541+
'bytes' => null,
542+
'source' => null,
543+
'reason' => 'size_not_reported',
544+
);
489545
}
490546

491547
/**
@@ -630,27 +686,51 @@ private function cleanup_plan_blockers( array $artifact_plan, array $worktree_pl
630686
if ( '' === $repo ) {
631687
$repo = 'unknown';
632688
}
633-
$bytes = max(0, (int) ( $row['artifact_size_bytes'] ?? $row['size_bytes'] ?? 0 ));
689+
$accounting = $this->cleanup_plan_row_size_accounting($row);
690+
$bytes = null === $accounting['bytes'] ? 0 : max(0, (int) $accounting['bytes']);
691+
$status = (string) $accounting['status'];
634692
$blockers[ $reason ] ??= array(
635-
'count' => 0,
636-
'size_bytes' => 0,
637-
'repos' => array(),
638-
'examples' => array(),
693+
'count' => 0,
694+
'size_bytes' => 0,
695+
'size_accounting' => array(
696+
'known_bytes' => 0,
697+
'known_count' => 0,
698+
'known_zero_count' => 0,
699+
'skipped_count' => 0,
700+
'unknown_count' => 0,
701+
),
702+
'repos' => array(),
703+
'examples' => array(),
639704
);
640705
$blockers[ $reason ]['count'] = (int) $blockers[ $reason ]['count'] + 1;
641706
$blockers[ $reason ]['size_bytes'] += $bytes;
707+
$blockers[ $reason ]['size_accounting'] = $this->cleanup_plan_add_size_accounting($blockers[ $reason ]['size_accounting'], $accounting);
642708
$blockers[ $reason ]['repos'][ $repo ] ??= array(
643-
'count' => 0,
644-
'size_bytes' => 0,
645-
'examples' => array(),
709+
'count' => 0,
710+
'size_bytes' => 0,
711+
'size_accounting' => array(
712+
'known_bytes' => 0,
713+
'known_count' => 0,
714+
'known_zero_count' => 0,
715+
'skipped_count' => 0,
716+
'unknown_count' => 0,
717+
),
718+
'examples' => array(),
646719
);
647-
$blockers[ $reason ]['repos'][ $repo ]['count'] = (int) $blockers[ $reason ]['repos'][ $repo ]['count'] + 1;
648-
$blockers[ $reason ]['repos'][ $repo ]['size_bytes'] += $bytes;
720+
$blockers[ $reason ]['repos'][ $repo ]['count'] = (int) $blockers[ $reason ]['repos'][ $repo ]['count'] + 1;
721+
$blockers[ $reason ]['repos'][ $repo ]['size_bytes'] += $bytes;
722+
$blockers[ $reason ]['repos'][ $repo ]['size_accounting'] = $this->cleanup_plan_add_size_accounting($blockers[ $reason ]['repos'][ $repo ]['size_accounting'], $accounting);
649723
if ( count($blockers[ $reason ]['examples']) < 5 ) {
650-
$blockers[ $reason ]['examples'][] = (string) ( $row['handle'] ?? $row['path'] ?? '' );
724+
$blockers[ $reason ]['examples'][] = array(
725+
'handle' => (string) ( $row['handle'] ?? $row['path'] ?? '' ),
726+
'size_status' => $status,
727+
);
651728
}
652729
if ( count($blockers[ $reason ]['repos'][ $repo ]['examples']) < 3 ) {
653-
$blockers[ $reason ]['repos'][ $repo ]['examples'][] = (string) ( $row['handle'] ?? $row['path'] ?? '' );
730+
$blockers[ $reason ]['repos'][ $repo ]['examples'][] = array(
731+
'handle' => (string) ( $row['handle'] ?? $row['path'] ?? '' ),
732+
'size_status' => $status,
733+
);
654734
}
655735
}
656736
}
@@ -676,6 +756,41 @@ private function cleanup_plan_blockers( array $artifact_plan, array $worktree_pl
676756
return $blockers;
677757
}
678758

759+
/**
760+
* Add one row's explicit size accounting into a blocker bucket.
761+
*
762+
* @param array<string,int> $bucket Existing bucket accounting.
763+
* @param array<string,mixed> $accounting Row accounting.
764+
* @return array<string,int>
765+
*/
766+
private function cleanup_plan_add_size_accounting( array $bucket, array $accounting ): array {
767+
$bucket = array_merge(
768+
array(
769+
'known_bytes' => 0,
770+
'known_count' => 0,
771+
'known_zero_count' => 0,
772+
'skipped_count' => 0,
773+
'unknown_count' => 0,
774+
),
775+
array_map('intval', $bucket)
776+
);
777+
778+
$status = (string) ( $accounting['status'] ?? 'unknown' );
779+
if ( 'known' === $status || 'known_zero' === $status ) {
780+
++$bucket['known_count'];
781+
$bucket['known_bytes'] += max(0, (int) ( $accounting['bytes'] ?? 0 ));
782+
if ( 'known_zero' === $status ) {
783+
++$bucket['known_zero_count'];
784+
}
785+
} elseif ( 'skipped' === $status ) {
786+
++$bucket['skipped_count'];
787+
} else {
788+
++$bucket['unknown_count'];
789+
}
790+
791+
return $bucket;
792+
}
793+
679794
/**
680795
* Build directly executable cleanup recommendations with risk labels.
681796
*
@@ -684,6 +799,12 @@ private function cleanup_plan_blockers( array $artifact_plan, array $worktree_pl
684799
*/
685800
private function cleanup_plan_recommended_commands( array $inputs ): array {
686801
$commands = array(
802+
array(
803+
'label' => 'bounded_size_aware_review',
804+
'risk' => 'none',
805+
'command' => sprintf('studio wp datamachine-code workspace hygiene --include-sizes --size-limit=%d --format=json', max(1, (int) ( $inputs['limit'] ?? self::CLEANUP_PLAN_DEFAULT_LIMIT ))),
806+
'when' => 'cleanup blockers or cheap inventory rows have skipped or unknown size accounting under disk pressure',
807+
),
687808
array(
688809
'label' => 'apply_reviewed_plan',
689810
'risk' => 'reviewed_destructive',

0 commit comments

Comments
 (0)