From 306a6dd3e14673b1b35f272aef2c9dd0782aa307 Mon Sep 17 00:00:00 2001 From: "homeboy-ci[bot]" <266378653+homeboy-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:19:59 -0400 Subject: [PATCH] Summarize active no-signal drain backlog --- .../WorkspaceAbandonedCleanupOrchestrator.php | 120 +++++++++++++++++- .../smoke-abandoned-cleanup-orchestrator.php | 45 +++++++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php b/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php index 669c5bb..b20c17e 100644 --- a/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php +++ b/inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php @@ -22,6 +22,8 @@ class WorkspaceAbandonedCleanupOrchestrator { /** @var callable */ private $clock; + private ?object $active_no_signal_report_ability = null; + /** * @param callable|null $ability_resolver Optional resolver receiving an ability name. * @param callable|null $clock Optional clock returning microtime-style seconds. @@ -69,7 +71,7 @@ public function run( array $input ): array|\WP_Error { $deadline = $this->now() + $budget_seconds; } - $abilities = $this->resolve_required_abilities(); + $abilities = $this->resolve_required_abilities($active_no_signal_drain); if ( is_wp_error($abilities) ) { return $abilities; } @@ -217,7 +219,7 @@ private function stage_order(): array { } /** @return array|\WP_Error */ - private function resolve_required_abilities(): array|\WP_Error { + private function resolve_required_abilities( bool $active_no_signal_drain = false ): array|\WP_Error { $required = array( 'reconcile_metadata' => 'datamachine-code/workspace-worktree-reconcile-metadata', 'finalized' => 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply', @@ -227,6 +229,9 @@ private function resolve_required_abilities(): array|\WP_Error { 'bounded_apply' => 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply', 'prune' => 'datamachine-code/workspace-worktree-prune', ); + if ( $active_no_signal_drain ) { + $required['active_no_signal_report'] = 'datamachine-code/workspace-worktree-active-no-signal-report'; + } $abilities = array(); foreach ( $required as $key => $ability_name ) { @@ -236,6 +241,7 @@ private function resolve_required_abilities(): array|\WP_Error { } $abilities[ $key ] = $ability; } + $this->active_no_signal_report_ability = $abilities['active_no_signal_report'] ?? null; return $abilities; } @@ -334,12 +340,122 @@ private function finalize_result( array $result, bool $apply, bool $force, int $ if ( empty($result['continuation']) && ! $force && ! $active_no_signal_drain ) { $result['next_commands'][] = sprintf('studio wp datamachine-code workspace worktree abandoned --apply --force --limit=%d --passes=%d%s --format=json', $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : ''); } + if ( $active_no_signal_drain && empty($result['continuation']) && empty($result['evidence']['budget_exhausted']) ) { + $this->append_active_no_signal_backlog_summary($result, min($limit, 25)); + } $result['evidence']['elapsed_ms'] = (int) round(( $this->now() - $started_at ) * 1000); return $result; } + private function append_active_no_signal_backlog_summary( array &$result, int $limit ): void { + if ( null === $this->active_no_signal_report_ability ) { + return; + } + + $limit = max(1, $limit); + $report = $this->execute_ability( + $this->active_no_signal_report_ability, + array( + 'limit' => $limit, + 'offset' => 0, + 'until_budget' => '15s', + ) + ); + if ( is_wp_error($report) ) { + $result['remaining_active_no_signal_backlog'] = array( + 'available' => false, + 'reason' => (string) $report->get_error_code(), + 'message' => $report->get_error_message(), + 'next_commands' => array( + sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --format=json', $limit), + ), + ); + return; + } + + $result['remaining_active_no_signal_backlog'] = $this->build_active_no_signal_backlog_summary($report, $limit); + foreach ( (array) ( $result['remaining_active_no_signal_backlog']['next_commands'] ?? array() ) as $command ) { + $result['next_commands'][] = (string) $command; + } + $result['next_commands'] = array_values(array_unique(array_filter(array_map('strval', (array) $result['next_commands'])))); + } + + /** @return array */ + private function build_active_no_signal_backlog_summary( array $report, int $limit ): array { + $rows = (array) ( $report['rows'] ?? array() ); + $summary = (array) ( $report['summary'] ?? array() ); + $pagination = (array) ( $report['pagination'] ?? array() ); + $total = (int) ( $summary['total_active_no_signal'] ?? $pagination['total'] ?? 0 ); + $sampled = (int) ( $summary['inspected'] ?? count($rows) ); + $buckets = array(); + + foreach ( (array) ( $summary['by_suggested_action'] ?? array() ) as $reason => $count ) { + $buckets[ (string) $reason ] = array( + 'count' => (int) $count, + 'examples' => array(), + ); + } + + foreach ( $rows as $row ) { + if ( ! is_array($row) ) { + continue; + } + $reason = (string) ( $row['suggested_action'] ?? 'insufficient_signal' ); + $buckets[ $reason ] ??= array( + 'count' => 0, + 'examples' => array(), + ); + if ( ! isset($summary['by_suggested_action'][ $reason ]) ) { + ++$buckets[ $reason ]['count']; + } + if ( count($buckets[ $reason ]['examples']) < 3 ) { + $buckets[ $reason ]['examples'][] = $this->active_no_signal_backlog_example($row); + } + } + ksort($buckets); + + $commands = array( + sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --format=json', $limit), + sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --verbose --format=json', $limit), + ); + if ( ! empty($pagination['next_command']) ) { + $commands[] = (string) $pagination['next_command']; + } + + return array( + 'available' => true, + 'total_active_no_signal' => $total, + 'sampled' => $sampled, + 'unreviewed_count' => max(0, $total - $sampled), + 'by_actionable_reason' => $buckets, + 'counts_scope' => 'bounded_post_drain_sample_only', + 'limitation' => 'Counts by actionable reason cover only this bounded post-drain sample; active-no-signal report has pagination but no safe bucket filter, so full per-bucket totals are not scanned by default.', + 'pagination' => $pagination, + 'next_commands' => array_values(array_unique(array_filter($commands))), + ); + } + + /** @return array */ + private function active_no_signal_backlog_example( array $row ): array { + $example = array( + 'handle' => (string) ( $row['handle'] ?? '' ), + ); + foreach ( array( 'repo', 'branch', 'path', 'reason' ) as $field ) { + if ( isset($row[ $field ]) && '' !== (string) $row[ $field ] ) { + $example[ $field ] = (string) $row[ $field ]; + } + } + if ( isset($row['dirty']) ) { + $example['dirty'] = (int) $row['dirty']; + } + if ( isset($row['unpushed']) ) { + $example['unpushed'] = (int) $row['unpushed']; + } + return $example; + } + private function stage_incomplete( array $step ): bool { $pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() ); if ( empty($pagination) || ! empty($pagination['complete']) || ! isset($pagination['next_offset']) ) { diff --git a/tests/smoke-abandoned-cleanup-orchestrator.php b/tests/smoke-abandoned-cleanup-orchestrator.php index 1372ec9..b80cbdb 100644 --- a/tests/smoke-abandoned-cleanup-orchestrator.php +++ b/tests/smoke-abandoned-cleanup-orchestrator.php @@ -18,6 +18,14 @@ public function __construct( string $code = '', string $message = '', array $dat $this->message = $message; $this->data = $data; } + + public function get_error_code(): string { + return $this->code; + } + + public function get_error_message(): string { + return $this->message; + } } } @@ -57,6 +65,7 @@ public function execute( array $input ): array { 'summary' => $this->summary, 'pagination' => $this->pagination, 'skipped' => $this->skipped, + 'rows' => $this->skipped, ); } } @@ -82,6 +91,7 @@ public function execute( array $input ): array { 'summary' => array(), 'pagination' => array( 'complete' => true ), 'skipped' => array(), + 'rows' => array(), ), $response ); @@ -171,6 +181,34 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void { 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )), 'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )), 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility( + 'active_no_signal_report', + array( + 'total_active_no_signal' => 5, + 'inspected' => 2, + 'by_suggested_action' => array( + 'inspect_unpushed_or_dirty' => 1, + 'insufficient_signal' => 1, + ), + ), + array( 'complete' => false, 'total' => 5, 'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=10 --offset=2 --format=json' ), + array( + array( + 'handle' => 'repo@dirty', + 'repo' => 'repo', + 'branch' => 'dirty', + 'suggested_action' => 'inspect_unpushed_or_dirty', + 'dirty' => 1, + 'unpushed' => 0, + ), + array( + 'handle' => 'repo@unknown', + 'repo' => 'repo', + 'branch' => 'unknown', + 'suggested_action' => 'insufficient_signal', + ), + ) + ), 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 1, 'removed' => 1 ), array( 'complete' => true )), 'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'), ); @@ -184,6 +222,12 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void { abandoned_cleanup_assert(0 === count($active_abilities['datamachine-code/workspace-worktree-reconcile-metadata']->calls), 'active/no-signal drain skips reconcile metadata'); abandoned_cleanup_assert(1 === $active_result['summary']['marked_cleanup_eligible'], 'active/no-signal drain counts metadata promotions'); abandoned_cleanup_assert(2 === $active_result['summary']['removed'], 'active/no-signal drain removes bounded eligible rows before and after classification'); +abandoned_cleanup_assert(5 === $active_result['remaining_active_no_signal_backlog']['total_active_no_signal'], 'active/no-signal drain summarizes remaining backlog total'); +abandoned_cleanup_assert(2 === $active_result['remaining_active_no_signal_backlog']['sampled'], 'active/no-signal drain summarizes sampled backlog rows'); +abandoned_cleanup_assert(1 === $active_result['remaining_active_no_signal_backlog']['by_actionable_reason']['inspect_unpushed_or_dirty']['count'], 'active/no-signal drain groups backlog by actionable reason'); +abandoned_cleanup_assert(3 === $active_result['remaining_active_no_signal_backlog']['unreviewed_count'], 'active/no-signal drain reports unreviewed backlog count'); +abandoned_cleanup_assert('bounded_post_drain_sample_only' === $active_result['remaining_active_no_signal_backlog']['counts_scope'], 'active/no-signal drain documents bounded counts scope'); +abandoned_cleanup_assert(in_array('studio wp datamachine-code workspace worktree active-no-signal-report --limit=10 --offset=2 --format=json', $active_result['next_commands'], true), 'active/no-signal drain includes next report page command'); $force_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'force' => true )); abandoned_cleanup_assert(is_wp_error($force_result), 'active/no-signal drain refuses force'); @@ -200,6 +244,7 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void { 'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )), 'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )), 'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )), + 'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility('active_no_signal_report', array( 'total_active_no_signal' => 0, 'inspected' => 0, 'by_suggested_action' => array() ), array( 'complete' => true, 'total' => 0 )), 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )), 'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'), );