Skip to content

Commit 8993286

Browse files
Add active no-signal triage preview (#784)
* Add active no-signal triage preview * fix: satisfy active no-signal lint * fix: align active no-signal output arrays --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent d0d2a18 commit 8993286

5 files changed

Lines changed: 282 additions & 39 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6025,6 +6025,8 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
60256025
}
60266026
}
60276027

6028+
$this->render_active_no_signal_triage_preview( (array) ( $result['active_no_signal_triage'] ?? array() ) );
6029+
60286030
WP_CLI::log('');
60296031
$remaining = (int) ( $continuation['remaining_total'] ?? 0 );
60306032
if ( $remaining > 0 ) {
@@ -6074,19 +6076,20 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
60746076
);
60756077

60766078
$report = array(
6077-
'success' => (bool) ( $result['success'] ?? true ),
6078-
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
6079-
'dry_run' => ! empty($result['dry_run']),
6080-
'destructive' => ! empty($result['destructive']),
6081-
'workspace_path' => $result['workspace_path'] ?? null,
6082-
'generated_at' => $result['generated_at'] ?? null,
6083-
'summary' => $compact_summary,
6084-
'blocker_buckets' => $buckets,
6085-
'next_actions' => $actions,
6086-
'candidates' => $this->compact_cleanup_rows($candidates, 25),
6087-
'removed' => $this->compact_cleanup_rows($removed, 25),
6088-
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
6089-
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
6079+
'success' => (bool) ( $result['success'] ?? true ),
6080+
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
6081+
'dry_run' => ! empty($result['dry_run']),
6082+
'destructive' => ! empty($result['destructive']),
6083+
'workspace_path' => $result['workspace_path'] ?? null,
6084+
'generated_at' => $result['generated_at'] ?? null,
6085+
'summary' => $compact_summary,
6086+
'blocker_buckets' => $buckets,
6087+
'next_actions' => $actions,
6088+
'active_no_signal_triage' => (array) ( $result['active_no_signal_triage'] ?? array() ),
6089+
'candidates' => $this->compact_cleanup_rows($candidates, 25),
6090+
'removed' => $this->compact_cleanup_rows($removed, 25),
6091+
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
6092+
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
60906093
);
60916094

60926095
if ( ! empty($result['job_backed']) ) {
@@ -6096,6 +6099,55 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
60966099
return array_filter($report, fn( $value ) => null !== $value);
60976100
}
60986101

6102+
/**
6103+
* Render concise active/no-signal triage preview from bounded cleanup output.
6104+
*
6105+
* @param array<string,mixed> $preview Triage preview payload.
6106+
* @return void
6107+
*/
6108+
private function render_active_no_signal_triage_preview( array $preview ): void {
6109+
$total = (int) ( $preview['total'] ?? 0 );
6110+
if ( $total <= 0 ) {
6111+
return;
6112+
}
6113+
6114+
WP_CLI::log('');
6115+
WP_CLI::log(sprintf('Active/no-signal triage preview: %d unresolved active worktree(s).', $total));
6116+
$summary_rows = array();
6117+
foreach ( (array) ( $preview['by_age'] ?? array() ) as $bucket => $count ) {
6118+
if ( (int) $count > 0 ) {
6119+
$summary_rows[] = array(
6120+
'dimension' => 'age',
6121+
'bucket' => (string) $bucket,
6122+
'count' => (int) $count,
6123+
);
6124+
}
6125+
}
6126+
foreach ( (array) ( $preview['by_liveness'] ?? array() ) as $bucket => $count ) {
6127+
$summary_rows[] = array(
6128+
'dimension' => 'liveness',
6129+
'bucket' => (string) $bucket,
6130+
'count' => (int) $count,
6131+
);
6132+
}
6133+
foreach ( (array) ( $preview['by_repo'] ?? array() ) as $bucket => $count ) {
6134+
$summary_rows[] = array(
6135+
'dimension' => 'repo',
6136+
'bucket' => (string) $bucket,
6137+
'count' => (int) $count,
6138+
);
6139+
}
6140+
$this->format_items($summary_rows, array( 'dimension', 'bucket', 'count' ), array( 'format' => 'table' ), 'dimension');
6141+
6142+
WP_CLI::log('Non-destructive next commands:');
6143+
foreach ( (array) ( $preview['commands'] ?? array() ) as $label => $command ) {
6144+
WP_CLI::log(sprintf(' %s: %s', (string) $label, (string) $command));
6145+
}
6146+
if ( ! empty($preview['safety']) ) {
6147+
WP_CLI::log('Safety: ' . (string) $preview['safety']);
6148+
}
6149+
}
6150+
60996151
/**
61006152
* Build skipped blocker buckets with bounded examples.
61016153
*

inc/Workspace/Workspace.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
defined('ABSPATH') || exit;
1616

1717
require_once __DIR__ . '/WorkspaceCoreUtilities.php';
18+
require_once __DIR__ . '/WorktreeActiveNoSignalTriagePreview.php';
1819
require_once __DIR__ . '/WorkspaceActiveNoSignalCleanup.php';
1920
require_once __DIR__ . '/WorkspaceArtifactCleanup.php';
2021
require_once __DIR__ . '/WorkspaceCleanupPlan.php';

inc/Workspace/WorkspaceWorktreeCleanupEngine.php

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ private function discover_broken_orphan_worktree_markers( array $listed_worktree
697697
}
698698

699699
$parsed = $this->parse_handle($entry);
700-
if ( empty($parsed['is_worktree']) || '' === (string) ( $parsed['repo'] ?? '' ) ) {
700+
if ( empty($parsed['is_worktree']) || '' === (string) $parsed['repo'] ) {
701701
continue;
702702
}
703703

@@ -1012,35 +1012,37 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
10121012
$batch = array_slice($all_candidates, 0, $limit);
10131013
$deferred = array_slice($all_candidates, $limit);
10141014

1015-
$continuation = array(
1015+
$continuation = array(
10161016
'remaining_total' => count($deferred),
10171017
'remaining_handles' => array_values(array_filter(array_map(fn( $row ) => is_array($row) ? (string) ( $row['handle'] ?? '' ) : '', $deferred))),
10181018
'next_call_hint' => count($deferred) > 0 ? sprintf('Run the bounded cleanup-eligible apply again to drain the next %d candidate(s).', min($limit, count($deferred))) : null,
10191019
'inventory_skipped' => count($inventory_skipped),
10201020
'limit_applied' => $limit,
10211021
'remove_timeout' => $remove_timeout_seconds,
10221022
);
1023+
$active_no_signal_triage = WorktreeActiveNoSignalTriagePreview::build($inventory_skipped, min($limit, 25));
10231024

10241025
if ( $dry_run ) {
10251026
return array(
1026-
'success' => true,
1027-
'mode' => 'bounded_cleanup_eligible_apply',
1028-
'dry_run' => true,
1029-
'destructive' => false,
1030-
'workspace_path' => $this->workspace_path,
1031-
'generated_at' => gmdate('c'),
1032-
'candidates' => $batch,
1033-
'removed' => array(),
1034-
'skipped' => $inventory_skipped,
1035-
'summary' => array(
1027+
'success' => true,
1028+
'mode' => 'bounded_cleanup_eligible_apply',
1029+
'dry_run' => true,
1030+
'destructive' => false,
1031+
'workspace_path' => $this->workspace_path,
1032+
'generated_at' => gmdate('c'),
1033+
'candidates' => $batch,
1034+
'removed' => array(),
1035+
'skipped' => $inventory_skipped,
1036+
'summary' => array(
10361037
'processed' => count($batch),
10371038
'removed' => 0,
10381039
'skipped' => count($inventory_skipped),
10391040
'bytes_reclaimed' => 0,
10401041
'limit' => $limit,
10411042
),
1042-
'continuation' => $continuation,
1043-
'evidence' => array(
1043+
'continuation' => $continuation,
1044+
'active_no_signal_triage' => $active_no_signal_triage,
1045+
'evidence' => array(
10441046
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
10451047
'inventory_total' => count($all_candidates),
10461048
'planned_handles' => array_values(array_filter(array_map(fn( $row ) => is_array($row) ? (string) ( $row['handle'] ?? '' ) : '', $batch))),
@@ -1152,25 +1154,26 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
11521154
$this->worktree_prune();
11531155

11541156
return array(
1155-
'success' => true,
1156-
'mode' => 'bounded_cleanup_eligible_apply',
1157-
'dry_run' => false,
1158-
'destructive' => true,
1159-
'workspace_path' => $this->workspace_path,
1160-
'generated_at' => gmdate('c'),
1161-
'candidates' => $batch,
1162-
'removed' => $removed,
1163-
'skipped' => $skipped,
1164-
'summary' => array(
1157+
'success' => true,
1158+
'mode' => 'bounded_cleanup_eligible_apply',
1159+
'dry_run' => false,
1160+
'destructive' => true,
1161+
'workspace_path' => $this->workspace_path,
1162+
'generated_at' => gmdate('c'),
1163+
'candidates' => $batch,
1164+
'removed' => $removed,
1165+
'skipped' => $skipped,
1166+
'summary' => array(
11651167
'processed' => $processed,
11661168
'removed' => count($removed),
11671169
'skipped' => count($skipped),
11681170
'bytes_reclaimed' => $bytes_reclaimed,
11691171
'limit' => $limit,
11701172
'discarded_unpushed' => count($discarded_unpushed),
11711173
),
1172-
'continuation' => $continuation,
1173-
'evidence' => array(
1174+
'continuation' => $continuation,
1175+
'active_no_signal_triage' => $active_no_signal_triage,
1176+
'evidence' => array(
11741177
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
11751178
'inventory_total' => count($all_candidates),
11761179
'removed_handles' => array_values(array_filter(array_map(fn( $row ) => (string) $row['handle'], $removed))),
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
/**
3+
* Concise active/no-signal triage preview for bounded cleanup output.
4+
*
5+
* @package DataMachineCode\Workspace
6+
*/
7+
8+
namespace DataMachineCode\Workspace;
9+
10+
defined('ABSPATH') || exit;
11+
12+
final class WorktreeActiveNoSignalTriagePreview {
13+
14+
private const ACTIVE_REASON_CODES = array(
15+
'active_no_signal',
16+
'no_inventory_cleanup_signal',
17+
'lifecycle_reconciliation_candidate',
18+
);
19+
20+
/**
21+
* Build a bounded operator preview for unresolved active/no-signal rows.
22+
*
23+
* @param array<int,array<string,mixed>> $rows Skipped cleanup rows.
24+
* @param int $limit Suggested command page size.
25+
* @param int $now Current timestamp for age buckets.
26+
* @return array<string,mixed>
27+
*/
28+
public static function build( array $rows, int $limit = 25, ?int $now = null ): array {
29+
$now = $now ?? time();
30+
$limit = max(1, min(200, $limit));
31+
$preview = array(
32+
'total' => 0,
33+
'by_age' => array(
34+
'lt_1d' => 0,
35+
'1_7d' => 0,
36+
'7_30d' => 0,
37+
'gte_30d' => 0,
38+
'unknown' => 0,
39+
),
40+
'by_liveness' => array(),
41+
'by_repo' => array(),
42+
'commands' => self::commands($limit),
43+
'safety' => 'Commands classify active/no-signal rows into cleanup_eligible metadata only; they do not remove worktrees or branches.',
44+
);
45+
46+
foreach ( $rows as $row ) {
47+
if ( ! in_array( (string) ( $row['reason_code'] ?? '' ), self::ACTIVE_REASON_CODES, true ) ) {
48+
continue;
49+
}
50+
51+
++$preview['total'];
52+
++$preview['by_age'][ self::age_bucket($row['created_at'] ?? null, $now) ];
53+
54+
$liveness = (string) ( $row['liveness'] ?? 'unknown' );
55+
if ( '' === $liveness ) {
56+
$liveness = 'unknown';
57+
}
58+
$preview['by_liveness'][ $liveness ] = (int) ( $preview['by_liveness'][ $liveness ] ?? 0 ) + 1;
59+
60+
$repo = (string) ( $row['repo'] ?? 'unknown' );
61+
if ( '' === $repo ) {
62+
$repo = 'unknown';
63+
}
64+
$preview['by_repo'][ $repo ] = (int) ( $preview['by_repo'][ $repo ] ?? 0 ) + 1;
65+
}
66+
67+
arsort($preview['by_liveness']);
68+
arsort($preview['by_repo']);
69+
$preview['by_repo'] = array_slice($preview['by_repo'], 0, 10, true);
70+
71+
return $preview;
72+
}
73+
74+
/**
75+
* @param mixed $created_at Created-at value from inventory metadata.
76+
*/
77+
private static function age_bucket( mixed $created_at, int $now ): string {
78+
if ( ! is_string($created_at) || '' === trim($created_at) ) {
79+
return 'unknown';
80+
}
81+
82+
$created = strtotime($created_at);
83+
if ( false === $created ) {
84+
return 'unknown';
85+
}
86+
87+
$age = max(0, $now - $created);
88+
$day = 86400;
89+
if ( $age < $day ) {
90+
return 'lt_1d';
91+
}
92+
if ( $age < 7 * $day ) {
93+
return '1_7d';
94+
}
95+
if ( $age < 30 * $day ) {
96+
return '7_30d';
97+
}
98+
99+
return 'gte_30d';
100+
}
101+
102+
/**
103+
* @return array<string,string>
104+
*/
105+
private static function commands( int $limit ): array {
106+
$base = sprintf('--limit=%d --offset=0 --until-budget=60s --format=json', $limit);
107+
108+
return array(
109+
'report' => 'studio wp datamachine-code workspace worktree active-no-signal-report ' . $base,
110+
'equivalent_clean_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-equivalent-clean-apply --dry-run ' . $base,
111+
'equivalent_clean_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-equivalent-clean-apply ' . $base,
112+
'merged_to_default_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-merged-apply --dry-run ' . $base,
113+
'merged_to_default_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-merged-apply ' . $base,
114+
'remote_clean_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run ' . $base,
115+
'remote_clean_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply ' . $base,
116+
);
117+
}
118+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
if ( ! defined('ABSPATH') ) {
6+
define('ABSPATH', __DIR__ . '/fixtures/');
7+
}
8+
9+
require_once dirname(__DIR__) . '/inc/Workspace/WorktreeActiveNoSignalTriagePreview.php';
10+
11+
use DataMachineCode\Workspace\WorktreeActiveNoSignalTriagePreview;
12+
13+
function active_no_signal_triage_assert_same( mixed $expected, mixed $actual, string $message ): void {
14+
if ( $expected !== $actual ) {
15+
throw new RuntimeException(sprintf('%s Expected %s, got %s.', $message, var_export($expected, true), var_export($actual, true)));
16+
}
17+
}
18+
19+
$now = strtotime('2026-06-17T00:00:00+00:00');
20+
$preview = WorktreeActiveNoSignalTriagePreview::build(
21+
array(
22+
array(
23+
'reason_code' => 'active_no_signal',
24+
'repo' => 'repo-a',
25+
'liveness' => 'stale',
26+
'created_at' => '2026-06-16T12:00:00+00:00',
27+
),
28+
array(
29+
'reason_code' => 'lifecycle_reconciliation_candidate',
30+
'repo' => 'repo-a',
31+
'liveness' => 'unknown',
32+
'created_at' => '2026-06-01T00:00:00+00:00',
33+
),
34+
array(
35+
'reason_code' => 'active_no_signal',
36+
'repo' => 'repo-b',
37+
'liveness' => '',
38+
'created_at' => 'not-a-date',
39+
),
40+
array(
41+
'reason_code' => 'dirty_worktree',
42+
'repo' => 'repo-b',
43+
'liveness' => 'stale',
44+
'created_at' => '2026-06-16T12:00:00+00:00',
45+
),
46+
),
47+
10,
48+
$now
49+
);
50+
51+
active_no_signal_triage_assert_same(3, $preview['total'], 'active/no-signal total excludes non-active blockers');
52+
active_no_signal_triage_assert_same(1, $preview['by_age']['lt_1d'], 'recent rows are counted');
53+
active_no_signal_triage_assert_same(1, $preview['by_age']['7_30d'], 'older rows are counted');
54+
active_no_signal_triage_assert_same(1, $preview['by_age']['unknown'], 'invalid dates are counted as unknown age');
55+
active_no_signal_triage_assert_same(1, $preview['by_liveness']['stale'], 'stale liveness is counted');
56+
active_no_signal_triage_assert_same(2, $preview['by_liveness']['unknown'], 'blank liveness is normalized to unknown');
57+
active_no_signal_triage_assert_same(2, $preview['by_repo']['repo-a'], 'repo counts are aggregated');
58+
active_no_signal_triage_assert_same(
59+
'studio wp datamachine-code workspace worktree active-no-signal-equivalent-clean-apply --dry-run --limit=10 --offset=0 --until-budget=60s --format=json',
60+
$preview['commands']['equivalent_clean_dry_run'],
61+
'equivalent-clean dry-run command is exact and non-destructive'
62+
);
63+
active_no_signal_triage_assert_same(
64+
'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --limit=10 --offset=0 --until-budget=60s --format=json',
65+
$preview['commands']['remote_clean_apply'],
66+
'remote-clean metadata apply command is exact'
67+
);
68+
69+
echo "worktree-active-no-signal-triage-preview: ok\n";

0 commit comments

Comments
 (0)