Skip to content

Commit 2d00bf9

Browse files
Add active no-signal triage preview
1 parent 478b13a commit 2d00bf9

5 files changed

Lines changed: 243 additions & 0 deletions

File tree

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5998,6 +5998,8 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
59985998
}
59995999
}
60006000

6001+
$this->render_active_no_signal_triage_preview((array) ( $result['active_no_signal_triage'] ?? array() ));
6002+
60016003
WP_CLI::log('');
60026004
$remaining = (int) ( $continuation['remaining_total'] ?? 0 );
60036005
if ( $remaining > 0 ) {
@@ -6056,6 +6058,7 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
60566058
'summary' => $compact_summary,
60576059
'blocker_buckets' => $buckets,
60586060
'next_actions' => $actions,
6061+
'active_no_signal_triage' => (array) ( $result['active_no_signal_triage'] ?? array() ),
60596062
'candidates' => $this->compact_cleanup_rows($candidates, 25),
60606063
'removed' => $this->compact_cleanup_rows($removed, 25),
60616064
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
@@ -6069,6 +6072,55 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
60696072
return array_filter($report, fn( $value ) => null !== $value);
60706073
}
60716074

6075+
/**
6076+
* Render concise active/no-signal triage preview from bounded cleanup output.
6077+
*
6078+
* @param array<string,mixed> $preview Triage preview payload.
6079+
* @return void
6080+
*/
6081+
private function render_active_no_signal_triage_preview( array $preview ): void {
6082+
$total = (int) ( $preview['total'] ?? 0 );
6083+
if ( $total <= 0 ) {
6084+
return;
6085+
}
6086+
6087+
WP_CLI::log('');
6088+
WP_CLI::log(sprintf('Active/no-signal triage preview: %d unresolved active worktree(s).', $total));
6089+
$summary_rows = array();
6090+
foreach ( (array) ( $preview['by_age'] ?? array() ) as $bucket => $count ) {
6091+
if ( (int) $count > 0 ) {
6092+
$summary_rows[] = array(
6093+
'dimension' => 'age',
6094+
'bucket' => (string) $bucket,
6095+
'count' => (int) $count,
6096+
);
6097+
}
6098+
}
6099+
foreach ( (array) ( $preview['by_liveness'] ?? array() ) as $bucket => $count ) {
6100+
$summary_rows[] = array(
6101+
'dimension' => 'liveness',
6102+
'bucket' => (string) $bucket,
6103+
'count' => (int) $count,
6104+
);
6105+
}
6106+
foreach ( (array) ( $preview['by_repo'] ?? array() ) as $bucket => $count ) {
6107+
$summary_rows[] = array(
6108+
'dimension' => 'repo',
6109+
'bucket' => (string) $bucket,
6110+
'count' => (int) $count,
6111+
);
6112+
}
6113+
$this->format_items($summary_rows, array( 'dimension', 'bucket', 'count' ), array( 'format' => 'table' ), 'dimension');
6114+
6115+
WP_CLI::log('Non-destructive next commands:');
6116+
foreach ( (array) ( $preview['commands'] ?? array() ) as $label => $command ) {
6117+
WP_CLI::log(sprintf(' %s: %s', (string) $label, (string) $command));
6118+
}
6119+
if ( ! empty($preview['safety']) ) {
6120+
WP_CLI::log('Safety: ' . (string) $preview['safety']);
6121+
}
6122+
}
6123+
60726124
/**
60736125
* Build skipped blocker buckets with bounded examples.
60746126
*

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,7 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
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(
@@ -1040,6 +1041,7 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
10401041
'limit' => $limit,
10411042
),
10421043
'continuation' => $continuation,
1044+
'active_no_signal_triage' => $active_no_signal_triage,
10431045
'evidence' => array(
10441046
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
10451047
'inventory_total' => count($all_candidates),
@@ -1170,6 +1172,7 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
11701172
'discarded_unpushed' => count($discarded_unpushed),
11711173
),
11721174
'continuation' => $continuation,
1175+
'active_no_signal_triage' => $active_no_signal_triage,
11731176
'evidence' => array(
11741177
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
11751178
'inventory_total' => count($all_candidates),
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 ( ! is_array($row) || ! 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)