Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 66 additions & 14 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3635,7 +3635,7 @@ public function worktree( array $args, array $assoc_args ): void {

$input = array();
$input_builder = (string) ( $operation_config['input_builder'] ?? '' );
if ( '' !== $input_builder && method_exists($this, $input_builder) ) {
if ( '' !== $input_builder ) {
$input = $this->{$input_builder}($operation, $assoc_args);
}

Expand Down Expand Up @@ -5998,6 +5998,8 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
}
}

$this->render_active_no_signal_triage_preview( (array) ( $result['active_no_signal_triage'] ?? array() ) );

WP_CLI::log('');
$remaining = (int) ( $continuation['remaining_total'] ?? 0 );
if ( $remaining > 0 ) {
Expand Down Expand Up @@ -6047,19 +6049,20 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
);

$report = array(
'success' => (bool) ( $result['success'] ?? true ),
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
'dry_run' => ! empty($result['dry_run']),
'destructive' => ! empty($result['destructive']),
'workspace_path' => $result['workspace_path'] ?? null,
'generated_at' => $result['generated_at'] ?? null,
'summary' => $compact_summary,
'blocker_buckets' => $buckets,
'next_actions' => $actions,
'candidates' => $this->compact_cleanup_rows($candidates, 25),
'removed' => $this->compact_cleanup_rows($removed, 25),
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
'success' => (bool) ( $result['success'] ?? true ),
'mode' => (string) ( $result['mode'] ?? 'bounded_cleanup_eligible_apply' ),
'dry_run' => ! empty($result['dry_run']),
'destructive' => ! empty($result['destructive']),
'workspace_path' => $result['workspace_path'] ?? null,
'generated_at' => $result['generated_at'] ?? null,
'summary' => $compact_summary,
'blocker_buckets' => $buckets,
'next_actions' => $actions,
'active_no_signal_triage' => (array) ( $result['active_no_signal_triage'] ?? array() ),
'candidates' => $this->compact_cleanup_rows($candidates, 25),
'removed' => $this->compact_cleanup_rows($removed, 25),
'continuation' => $this->compact_cleanup_continuation( (array) ( $result['continuation'] ?? $result['pagination'] ?? array() ) ),
'evidence' => $this->compact_cleanup_evidence( (array) ( $result['evidence'] ?? array() ), $skipped ),
);

if ( ! empty($result['job_backed']) ) {
Expand All @@ -6069,6 +6072,55 @@ private function compact_worktree_bounded_cleanup_eligible_apply_json( array $re
return array_filter($report, fn( $value ) => null !== $value);
}

/**
* Render concise active/no-signal triage preview from bounded cleanup output.
*
* @param array<string,mixed> $preview Triage preview payload.
* @return void
*/
private function render_active_no_signal_triage_preview( array $preview ): void {
$total = (int) ( $preview['total'] ?? 0 );
if ( $total <= 0 ) {
return;
}

WP_CLI::log('');
WP_CLI::log(sprintf('Active/no-signal triage preview: %d unresolved active worktree(s).', $total));
$summary_rows = array();
foreach ( (array) ( $preview['by_age'] ?? array() ) as $bucket => $count ) {
if ( (int) $count > 0 ) {
$summary_rows[] = array(
'dimension' => 'age',
'bucket' => (string) $bucket,
'count' => (int) $count,
);
}
}
foreach ( (array) ( $preview['by_liveness'] ?? array() ) as $bucket => $count ) {
$summary_rows[] = array(
'dimension' => 'liveness',
'bucket' => (string) $bucket,
'count' => (int) $count,
);
}
foreach ( (array) ( $preview['by_repo'] ?? array() ) as $bucket => $count ) {
$summary_rows[] = array(
'dimension' => 'repo',
'bucket' => (string) $bucket,
'count' => (int) $count,
);
}
$this->format_items($summary_rows, array( 'dimension', 'bucket', 'count' ), array( 'format' => 'table' ), 'dimension');

WP_CLI::log('Non-destructive next commands:');
foreach ( (array) ( $preview['commands'] ?? array() ) as $label => $command ) {
WP_CLI::log(sprintf(' %s: %s', (string) $label, (string) $command));
}
if ( ! empty($preview['safety']) ) {
WP_CLI::log('Safety: ' . (string) $preview['safety']);
}
}

/**
* Build skipped blocker buckets with bounded examples.
*
Expand Down
1 change: 1 addition & 0 deletions inc/Workspace/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
defined('ABSPATH') || exit;

require_once __DIR__ . '/WorkspaceCoreUtilities.php';
require_once __DIR__ . '/WorktreeActiveNoSignalTriagePreview.php';
require_once __DIR__ . '/WorkspaceActiveNoSignalCleanup.php';
require_once __DIR__ . '/WorkspaceArtifactCleanup.php';
require_once __DIR__ . '/WorkspaceCleanupPlan.php';
Expand Down
55 changes: 29 additions & 26 deletions inc/Workspace/WorkspaceWorktreeCleanupEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ private function discover_broken_orphan_worktree_markers( array $listed_worktree
}

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

Expand Down Expand Up @@ -1012,35 +1012,37 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
$batch = array_slice($all_candidates, 0, $limit);
$deferred = array_slice($all_candidates, $limit);

$continuation = array(
$continuation = array(
'remaining_total' => count($deferred),
'remaining_handles' => array_values(array_filter(array_map(fn( $row ) => is_array($row) ? (string) ( $row['handle'] ?? '' ) : '', $deferred))),
'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,
'inventory_skipped' => count($inventory_skipped),
'limit_applied' => $limit,
'remove_timeout' => $remove_timeout_seconds,
);
$active_no_signal_triage = WorktreeActiveNoSignalTriagePreview::build($inventory_skipped, min($limit, 25));

if ( $dry_run ) {
return array(
'success' => true,
'mode' => 'bounded_cleanup_eligible_apply',
'dry_run' => true,
'destructive' => false,
'workspace_path' => $this->workspace_path,
'generated_at' => gmdate('c'),
'candidates' => $batch,
'removed' => array(),
'skipped' => $inventory_skipped,
'summary' => array(
'success' => true,
'mode' => 'bounded_cleanup_eligible_apply',
'dry_run' => true,
'destructive' => false,
'workspace_path' => $this->workspace_path,
'generated_at' => gmdate('c'),
'candidates' => $batch,
'removed' => array(),
'skipped' => $inventory_skipped,
'summary' => array(
'processed' => count($batch),
'removed' => 0,
'skipped' => count($inventory_skipped),
'bytes_reclaimed' => 0,
'limit' => $limit,
),
'continuation' => $continuation,
'evidence' => array(
'continuation' => $continuation,
'active_no_signal_triage' => $active_no_signal_triage,
'evidence' => array(
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
'inventory_total' => count($all_candidates),
'planned_handles' => array_values(array_filter(array_map(fn( $row ) => is_array($row) ? (string) ( $row['handle'] ?? '' ) : '', $batch))),
Expand Down Expand Up @@ -1152,25 +1154,26 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
$this->worktree_prune();

return array(
'success' => true,
'mode' => 'bounded_cleanup_eligible_apply',
'dry_run' => false,
'destructive' => true,
'workspace_path' => $this->workspace_path,
'generated_at' => gmdate('c'),
'candidates' => $batch,
'removed' => $removed,
'skipped' => $skipped,
'summary' => array(
'success' => true,
'mode' => 'bounded_cleanup_eligible_apply',
'dry_run' => false,
'destructive' => true,
'workspace_path' => $this->workspace_path,
'generated_at' => gmdate('c'),
'candidates' => $batch,
'removed' => $removed,
'skipped' => $skipped,
'summary' => array(
'processed' => $processed,
'removed' => count($removed),
'skipped' => count($skipped),
'bytes_reclaimed' => $bytes_reclaimed,
'limit' => $limit,
'discarded_unpushed' => count($discarded_unpushed),
),
'continuation' => $continuation,
'evidence' => array(
'continuation' => $continuation,
'active_no_signal_triage' => $active_no_signal_triage,
'evidence' => array(
'elapsed_ms' => (int) round(( microtime(true) - $started_at ) * 1000),
'inventory_total' => count($all_candidates),
'removed_handles' => array_values(array_filter(array_map(fn( $row ) => (string) $row['handle'], $removed))),
Expand Down
118 changes: 118 additions & 0 deletions inc/Workspace/WorktreeActiveNoSignalTriagePreview.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* Concise active/no-signal triage preview for bounded cleanup output.
*
* @package DataMachineCode\Workspace
*/

namespace DataMachineCode\Workspace;

defined('ABSPATH') || exit;

final class WorktreeActiveNoSignalTriagePreview {

private const ACTIVE_REASON_CODES = array(
'active_no_signal',
'no_inventory_cleanup_signal',
'lifecycle_reconciliation_candidate',
);

/**
* Build a bounded operator preview for unresolved active/no-signal rows.
*
* @param array<int,array<string,mixed>> $rows Skipped cleanup rows.
* @param int $limit Suggested command page size.
* @param int $now Current timestamp for age buckets.
* @return array<string,mixed>
*/
public static function build( array $rows, int $limit = 25, ?int $now = null ): array {
$now = $now ?? time();
$limit = max(1, min(200, $limit));
$preview = array(
'total' => 0,
'by_age' => array(
'lt_1d' => 0,
'1_7d' => 0,
'7_30d' => 0,
'gte_30d' => 0,
'unknown' => 0,
),
'by_liveness' => array(),
'by_repo' => array(),
'commands' => self::commands($limit),
'safety' => 'Commands classify active/no-signal rows into cleanup_eligible metadata only; they do not remove worktrees or branches.',
);

foreach ( $rows as $row ) {
if ( ! in_array( (string) ( $row['reason_code'] ?? '' ), self::ACTIVE_REASON_CODES, true ) ) {
continue;
}

++$preview['total'];
++$preview['by_age'][ self::age_bucket($row['created_at'] ?? null, $now) ];

$liveness = (string) ( $row['liveness'] ?? 'unknown' );
if ( '' === $liveness ) {
$liveness = 'unknown';
}
$preview['by_liveness'][ $liveness ] = (int) ( $preview['by_liveness'][ $liveness ] ?? 0 ) + 1;

$repo = (string) ( $row['repo'] ?? 'unknown' );
if ( '' === $repo ) {
$repo = 'unknown';
}
$preview['by_repo'][ $repo ] = (int) ( $preview['by_repo'][ $repo ] ?? 0 ) + 1;
}

arsort($preview['by_liveness']);
arsort($preview['by_repo']);
$preview['by_repo'] = array_slice($preview['by_repo'], 0, 10, true);

return $preview;
}

/**
* @param mixed $created_at Created-at value from inventory metadata.
*/
private static function age_bucket( mixed $created_at, int $now ): string {
if ( ! is_string($created_at) || '' === trim($created_at) ) {
return 'unknown';
}

$created = strtotime($created_at);
if ( false === $created ) {
return 'unknown';
}

$age = max(0, $now - $created);
$day = 86400;
if ( $age < $day ) {
return 'lt_1d';
}
if ( $age < 7 * $day ) {
return '1_7d';
}
if ( $age < 30 * $day ) {
return '7_30d';
}

return 'gte_30d';
}

/**
* @return array<string,string>
*/
private static function commands( int $limit ): array {
$base = sprintf('--limit=%d --offset=0 --until-budget=60s --format=json', $limit);

return array(
'report' => 'studio wp datamachine-code workspace worktree active-no-signal-report ' . $base,
'equivalent_clean_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-equivalent-clean-apply --dry-run ' . $base,
'equivalent_clean_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-equivalent-clean-apply ' . $base,
'merged_to_default_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-merged-apply --dry-run ' . $base,
'merged_to_default_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-merged-apply ' . $base,
'remote_clean_dry_run' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply --dry-run ' . $base,
'remote_clean_apply' => 'studio wp datamachine-code workspace worktree active-no-signal-remote-clean-apply ' . $base,
);
}
}
Loading
Loading