Skip to content

Commit 0a48d5d

Browse files
authored
Merge pull request #820 from Extra-Chill/issue-817-backlog-summary
Summarize active no-signal drain backlog
2 parents 9250153 + 8152ed5 commit 0a48d5d

2 files changed

Lines changed: 163 additions & 2 deletions

File tree

inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class WorkspaceAbandonedCleanupOrchestrator {
2222
/** @var callable */
2323
private $clock;
2424

25+
private ?object $active_no_signal_report_ability = null;
26+
2527
/**
2628
* @param callable|null $ability_resolver Optional resolver receiving an ability name.
2729
* @param callable|null $clock Optional clock returning microtime-style seconds.
@@ -69,7 +71,7 @@ public function run( array $input ): array|\WP_Error {
6971
$deadline = $this->now() + $budget_seconds;
7072
}
7173

72-
$abilities = $this->resolve_required_abilities();
74+
$abilities = $this->resolve_required_abilities($active_no_signal_drain);
7375
if ( is_wp_error($abilities) ) {
7476
return $abilities;
7577
}
@@ -217,7 +219,7 @@ private function stage_order(): array {
217219
}
218220

219221
/** @return array<string,mixed>|\WP_Error */
220-
private function resolve_required_abilities(): array|\WP_Error {
222+
private function resolve_required_abilities( bool $active_no_signal_drain = false ): array|\WP_Error {
221223
$required = array(
222224
'reconcile_metadata' => 'datamachine-code/workspace-worktree-reconcile-metadata',
223225
'finalized' => 'datamachine-code/workspace-worktree-active-no-signal-finalized-apply',
@@ -227,6 +229,9 @@ private function resolve_required_abilities(): array|\WP_Error {
227229
'bounded_apply' => 'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply',
228230
'prune' => 'datamachine-code/workspace-worktree-prune',
229231
);
232+
if ( $active_no_signal_drain ) {
233+
$required['active_no_signal_report'] = 'datamachine-code/workspace-worktree-active-no-signal-report';
234+
}
230235

231236
$abilities = array();
232237
foreach ( $required as $key => $ability_name ) {
@@ -236,6 +241,7 @@ private function resolve_required_abilities(): array|\WP_Error {
236241
}
237242
$abilities[ $key ] = $ability;
238243
}
244+
$this->active_no_signal_report_ability = $abilities['active_no_signal_report'] ?? null;
239245

240246
return $abilities;
241247
}
@@ -334,12 +340,122 @@ private function finalize_result( array $result, bool $apply, bool $force, int $
334340
if ( empty($result['continuation']) && ! $force && ! $active_no_signal_drain ) {
335341
$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 : '');
336342
}
343+
if ( $active_no_signal_drain && empty($result['continuation']) && empty($result['evidence']['budget_exhausted']) ) {
344+
$this->append_active_no_signal_backlog_summary($result, min($limit, 25));
345+
}
337346

338347
$result['evidence']['elapsed_ms'] = (int) round(( $this->now() - $started_at ) * 1000);
339348

340349
return $result;
341350
}
342351

352+
private function append_active_no_signal_backlog_summary( array &$result, int $limit ): void {
353+
if ( null === $this->active_no_signal_report_ability ) {
354+
return;
355+
}
356+
357+
$limit = max(1, $limit);
358+
$report = $this->execute_ability(
359+
$this->active_no_signal_report_ability,
360+
array(
361+
'limit' => $limit,
362+
'offset' => 0,
363+
'until_budget' => '15s',
364+
)
365+
);
366+
if ( is_wp_error($report) ) {
367+
$result['remaining_active_no_signal_backlog'] = array(
368+
'available' => false,
369+
'reason' => (string) $report->get_error_code(),
370+
'message' => $report->get_error_message(),
371+
'next_commands' => array(
372+
sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --format=json', $limit),
373+
),
374+
);
375+
return;
376+
}
377+
378+
$result['remaining_active_no_signal_backlog'] = $this->build_active_no_signal_backlog_summary($report, $limit);
379+
foreach ( (array) ( $result['remaining_active_no_signal_backlog']['next_commands'] ?? array() ) as $command ) {
380+
$result['next_commands'][] = (string) $command;
381+
}
382+
$result['next_commands'] = array_values(array_unique(array_filter(array_map('strval', (array) $result['next_commands']))));
383+
}
384+
385+
/** @return array<string,mixed> */
386+
private function build_active_no_signal_backlog_summary( array $report, int $limit ): array {
387+
$rows = (array) ( $report['rows'] ?? array() );
388+
$summary = (array) ( $report['summary'] ?? array() );
389+
$pagination = (array) ( $report['pagination'] ?? array() );
390+
$total = (int) ( $summary['total_active_no_signal'] ?? $pagination['total'] ?? 0 );
391+
$sampled = (int) ( $summary['inspected'] ?? count($rows) );
392+
$buckets = array();
393+
394+
foreach ( (array) ( $summary['by_suggested_action'] ?? array() ) as $reason => $count ) {
395+
$buckets[ (string) $reason ] = array(
396+
'count' => (int) $count,
397+
'examples' => array(),
398+
);
399+
}
400+
401+
foreach ( $rows as $row ) {
402+
if ( ! is_array($row) ) {
403+
continue;
404+
}
405+
$reason = (string) ( $row['suggested_action'] ?? 'insufficient_signal' );
406+
$buckets[ $reason ] ??= array(
407+
'count' => 0,
408+
'examples' => array(),
409+
);
410+
if ( ! isset($summary['by_suggested_action'][ $reason ]) ) {
411+
++$buckets[ $reason ]['count'];
412+
}
413+
if ( count($buckets[ $reason ]['examples']) < 3 ) {
414+
$buckets[ $reason ]['examples'][] = $this->active_no_signal_backlog_example($row);
415+
}
416+
}
417+
ksort($buckets);
418+
419+
$commands = array(
420+
sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --format=json', $limit),
421+
sprintf('studio wp datamachine-code workspace worktree active-no-signal-report --limit=%d --offset=0 --verbose --format=json', $limit),
422+
);
423+
if ( ! empty($pagination['next_command']) ) {
424+
$commands[] = (string) $pagination['next_command'];
425+
}
426+
427+
return array(
428+
'available' => true,
429+
'total_active_no_signal' => $total,
430+
'sampled' => $sampled,
431+
'unreviewed_count' => max(0, $total - $sampled),
432+
'by_actionable_reason' => $buckets,
433+
'counts_scope' => 'bounded_post_drain_sample_only',
434+
'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.',
435+
'pagination' => $pagination,
436+
'next_commands' => array_values(array_unique(array_filter($commands))),
437+
);
438+
}
439+
440+
/** @return array<string,mixed> */
441+
private function active_no_signal_backlog_example( array $row ): array {
442+
$example = array(
443+
'handle' => (string) ( $row['handle'] ?? '' ),
444+
);
445+
foreach ( array( 'repo', 'branch', 'path', 'reason' ) as $field ) {
446+
if ( isset($row[ $field ]) && '' !== (string) $row[ $field ] ) {
447+
$example[ $field ] = (string) $row[ $field ];
448+
}
449+
}
450+
if ( isset($row['dirty']) ) {
451+
$example['dirty'] = (int) $row['dirty'];
452+
}
453+
if ( isset($row['unpushed']) ) {
454+
$example['unpushed'] = (int) $row['unpushed'];
455+
}
456+
return $example;
457+
}
458+
343459
private function stage_incomplete( array $step ): bool {
344460
$pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() );
345461
if ( empty($pagination) || ! empty($pagination['complete']) || ! isset($pagination['next_offset']) ) {

tests/smoke-abandoned-cleanup-orchestrator.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public function __construct( string $code = '', string $message = '', array $dat
1818
$this->message = $message;
1919
$this->data = $data;
2020
}
21+
22+
public function get_error_code(): string {
23+
return $this->code;
24+
}
25+
26+
public function get_error_message(): string {
27+
return $this->message;
28+
}
2129
}
2230
}
2331

@@ -57,6 +65,7 @@ public function execute( array $input ): array {
5765
'summary' => $this->summary,
5866
'pagination' => $this->pagination,
5967
'skipped' => $this->skipped,
68+
'rows' => $this->skipped,
6069
);
6170
}
6271
}
@@ -82,6 +91,7 @@ public function execute( array $input ): array {
8291
'summary' => array(),
8392
'pagination' => array( 'complete' => true ),
8493
'skipped' => array(),
94+
'rows' => array(),
8595
),
8696
$response
8797
);
@@ -171,6 +181,34 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void {
171181
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
172182
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
173183
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
184+
'datamachine-code/workspace-worktree-active-no-signal-report' => new AbandonedCleanupFakeAbility(
185+
'active_no_signal_report',
186+
array(
187+
'total_active_no_signal' => 5,
188+
'inspected' => 2,
189+
'by_suggested_action' => array(
190+
'inspect_unpushed_or_dirty' => 1,
191+
'insufficient_signal' => 1,
192+
),
193+
),
194+
array( 'complete' => false, 'total' => 5, 'next_command' => 'studio wp datamachine-code workspace worktree active-no-signal-report --limit=10 --offset=2 --format=json' ),
195+
array(
196+
array(
197+
'handle' => 'repo@dirty',
198+
'repo' => 'repo',
199+
'branch' => 'dirty',
200+
'suggested_action' => 'inspect_unpushed_or_dirty',
201+
'dirty' => 1,
202+
'unpushed' => 0,
203+
),
204+
array(
205+
'handle' => 'repo@unknown',
206+
'repo' => 'repo',
207+
'branch' => 'unknown',
208+
'suggested_action' => 'insufficient_signal',
209+
),
210+
)
211+
),
174212
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 1, 'removed' => 1 ), array( 'complete' => true )),
175213
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
176214
);
@@ -184,6 +222,12 @@ function abandoned_cleanup_assert( bool $condition, string $label ): void {
184222
abandoned_cleanup_assert(0 === count($active_abilities['datamachine-code/workspace-worktree-reconcile-metadata']->calls), 'active/no-signal drain skips reconcile metadata');
185223
abandoned_cleanup_assert(1 === $active_result['summary']['marked_cleanup_eligible'], 'active/no-signal drain counts metadata promotions');
186224
abandoned_cleanup_assert(2 === $active_result['summary']['removed'], 'active/no-signal drain removes bounded eligible rows before and after classification');
225+
abandoned_cleanup_assert(5 === $active_result['remaining_active_no_signal_backlog']['total_active_no_signal'], 'active/no-signal drain summarizes remaining backlog total');
226+
abandoned_cleanup_assert(2 === $active_result['remaining_active_no_signal_backlog']['sampled'], 'active/no-signal drain summarizes sampled backlog rows');
227+
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');
228+
abandoned_cleanup_assert(3 === $active_result['remaining_active_no_signal_backlog']['unreviewed_count'], 'active/no-signal drain reports unreviewed backlog count');
229+
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');
230+
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');
187231

188232
$force_result = $orchestrator->run(array( 'active_no_signal_drain' => true, 'apply' => true, 'force' => true ));
189233
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 {
200244
'datamachine-code/workspace-worktree-active-no-signal-equivalent-clean-apply' => new AbandonedCleanupFakeAbility('equivalent_clean', array(), array( 'complete' => true )),
201245
'datamachine-code/workspace-worktree-active-no-signal-merged-apply' => new AbandonedCleanupFakeAbility('merged', array(), array( 'complete' => true )),
202246
'datamachine-code/workspace-worktree-active-no-signal-remote-clean-apply' => new AbandonedCleanupFakeAbility('remote_clean', array(), array( 'complete' => true )),
247+
'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 )),
203248
'datamachine-code/workspace-worktree-bounded-cleanup-eligible-apply' => new AbandonedCleanupFakeAbility('bounded', array( 'processed' => 0, 'removed' => 0 ), array( 'complete' => true )),
204249
'datamachine-code/workspace-worktree-prune' => new AbandonedCleanupFakeAbility('prune'),
205250
);

0 commit comments

Comments
 (0)