Skip to content

Commit be13f15

Browse files
Merge remote-tracking branch 'origin/main' into issue/822-hygiene-reporting
# Conflicts: # tests/workspace-compact-output.php
2 parents dfe290d + f6c7105 commit be13f15

6 files changed

Lines changed: 234 additions & 33 deletions

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4766,12 +4766,28 @@ private function render_workspace_hygiene_report( array $report, array $assoc_ar
47664766
'value' => (string) ( $worktrees['artifacts'] ?? 0 ),
47674767
),
47684768
array(
4769-
'metric' => 'dirty_protected',
4770-
'value' => (string) ( $worktrees['protected_dirty'] ?? 0 ),
4769+
'metric' => 'inventory_known_dirty',
4770+
'value' => (string) ( $worktrees['inventory_known_dirty'] ?? 0 ),
47714771
),
47724772
array(
4773-
'metric' => 'unpushed_protected',
4774-
'value' => (string) ( $worktrees['protected_unpushed'] ?? 0 ),
4773+
'metric' => 'inventory_known_dirty_blockers',
4774+
'value' => (string) ( $worktrees['protected_dirty_inventory_known'] ?? 0 ),
4775+
),
4776+
array(
4777+
'metric' => 'inventory_known_unpushed_blockers',
4778+
'value' => (string) ( $worktrees['protected_unpushed_inventory_known'] ?? 0 ),
4779+
),
4780+
array(
4781+
'metric' => 'fresh_probed_dirty_blockers',
4782+
'value' => (string) ( $worktrees['protected_dirty_fresh_probed'] ?? 0 ),
4783+
),
4784+
array(
4785+
'metric' => 'fresh_probed_unpushed_blockers',
4786+
'value' => (string) ( $worktrees['protected_unpushed_fresh_probed'] ?? 0 ),
4787+
),
4788+
array(
4789+
'metric' => 'blocker_probe_source',
4790+
'value' => (string) ( $worktrees['protected_count_probe_source'] ?? 'none' ),
47754791
),
47764792
array(
47774793
'metric' => 'missing_metadata',

inc/Cli/WorkspaceCompactOutput.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ class WorkspaceCompactOutput {
1515

1616
public static function cleanup_result( array $result ): array {
1717
$summary = (array) ( $result['summary'] ?? array() );
18-
$candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? $result['worktree_candidates'] ?? $result['rows'] ?? $result['planned'] ?? array() );
19-
$removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? $result['written'] ?? array() );
18+
$candidates = (array) ( $result['candidates'] ?? $result['artifact_candidates'] ?? $result['worktree_candidates'] ?? $result['rows'] ?? array() );
19+
$planned = (array) ( $result['planned'] ?? array() );
20+
$written = (array) ( $result['written'] ?? array() );
21+
$removed = (array) ( $result['removed'] ?? $result['removed_worktrees'] ?? $result['removed_artifacts'] ?? array() );
2022
$skipped = (array) ( $result['skipped'] ?? array() );
2123

2224
return self::filter_empty(
@@ -33,6 +35,8 @@ public static function cleanup_result( array $result ): array {
3335
'bytes' => self::byte_summary( $summary ),
3436
'samples' => array(
3537
'candidates' => self::compact_rows( $candidates ),
38+
'planned' => self::compact_rows( $planned ),
39+
'written' => self::compact_rows( $written ),
3640
'removed' => self::compact_rows( $removed ),
3741
'skipped' => self::compact_rows( $skipped ),
3842
),
@@ -89,6 +93,8 @@ public static function hygiene_report( array $report ): array {
8993
'worktree_status_mode' => $report['worktree_status_mode'] ?? null,
9094
'locks' => isset( $report['locks'] ) ? self::lock_result( (array) $report['locks'] ) : null,
9195
'cleanup' => array(
96+
'blocker_probe_source' => $cleanup['blocker_probe_source'] ?? null,
97+
'blocker_counts' => $cleanup['blocker_counts'] ?? null,
9298
'summary' => (array) ( $cleanup['summary'] ?? array() ),
9399
'biggest_candidates' => self::compact_rows( (array) ( $cleanup['biggest_candidates'] ?? array() ) ),
94100
),

inc/Workspace/WorkspaceAbandonedCleanupOrchestrator.php

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ public function run( array $input ): array|\WP_Error {
142142

143143
if ( $this->budget_expired($deadline) ) {
144144
$result['evidence']['budget_exhausted'] = true;
145+
$result['continuation'] = $this->build_budget_continuation($step_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_before_stage');
146+
$result['next_commands'][] = (string) $result['continuation']['next_command'];
145147
break 2;
146148
}
147149

@@ -182,13 +184,23 @@ public function run( array $input ): array|\WP_Error {
182184
if ( is_wp_error($bounded) ) {
183185
return $bounded;
184186
}
187+
if ( $this->budget_expired($deadline) ) {
188+
$result['evidence']['budget_exhausted'] = true;
189+
$result['continuation'] = $this->build_budget_continuation($default_stage, 0, $limit, $passes, $force, $until_budget, $active_no_signal_drain, 'budget_exhausted_after_bounded_apply');
190+
$result['next_commands'][] = (string) $result['continuation']['next_command'];
191+
break;
192+
}
185193

186194
$removed_or_would = (int) ( $bounded['summary']['removed'] ?? 0 ) + (int) ( $bounded['summary']['would_remove'] ?? 0 );
187195
if ( 0 === $pass_marked && 0 === $removed_or_would ) {
188196
break;
189197
}
190198
}
191199

200+
if ( ! empty($result['continuation']) ) {
201+
return $this->finalize_result($result, $apply, $force, $limit, $passes, $until_budget, $started_at, $active_no_signal_drain);
202+
}
203+
192204
if ( $apply ) {
193205
$prune = $this->execute_ability($abilities['prune'], array());
194206
if ( is_wp_error($prune) ) {
@@ -507,8 +519,7 @@ private function run_bounded_apply( object $ability, array &$result, bool $apply
507519
private function build_continuation( string $stage, array $step, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain = false ): array {
508520
$pagination = (array) ( $step['pagination'] ?? $step['continuation'] ?? array() );
509521
$next_offset = isset($pagination['next_offset']) ? max(0, (int) $pagination['next_offset']) : 0;
510-
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
511-
$command = sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $next_offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
522+
$command = $this->build_continuation_command($stage, $next_offset, $limit, $passes, $force, $until_budget, $active_no_signal_drain);
512523
$current = (int) ( $pagination['offset'] ?? 0 );
513524
$mutated = (int) ( $step['summary']['written'] ?? 0 ) + (int) ( $step['summary']['removed'] ?? 0 );
514525
$restart = ! empty($pagination['partial']) && $next_offset <= $current && $mutated > 0;
@@ -520,13 +531,42 @@ private function build_continuation( string $stage, array $step, int $limit, int
520531
'pagination' => $pagination,
521532
);
522533
if ( $restart ) {
534+
$written = (int) ( $step['summary']['written'] ?? 0 );
535+
$removed = (int) ( $step['summary']['removed'] ?? 0 );
536+
523537
$continuation['candidate_set_changed_restart_required'] = true;
524538
$continuation['reason'] = 'candidate_set_changed_restart_required';
539+
$continuation['reason_description'] = 'The previous cleanup pass changed the candidate set, so the next safe continuation intentionally restarts this stage from offset 0.';
540+
$continuation['progress_delta'] = array(
541+
'written' => $written,
542+
'removed' => $removed,
543+
'total_mutations' => $written + $removed,
544+
'previous_offset' => $current,
545+
'restart_offset' => $next_offset,
546+
'candidate_set_now' => 'changed',
547+
);
548+
$continuation['next_command_label'] = 'Restart this stage from offset 0 because the cleanup candidate set changed.';
525549
}
526550

527551
return $continuation;
528552
}
529553

554+
private function build_budget_continuation( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain, string $reason ): array {
555+
return array(
556+
'stage' => $stage,
557+
'offset' => max(0, $offset),
558+
'next_command' => $this->build_continuation_command($stage, max(0, $offset), $limit, $passes, $force, $until_budget, $active_no_signal_drain),
559+
'reason' => $reason,
560+
'budget_exhausted' => true,
561+
'hint' => 'Budget expired after safe progress. Re-run next_command to continue the drain from the next safe boundary.',
562+
);
563+
}
564+
565+
private function build_continuation_command( string $stage, int $offset, int $limit, int $passes, bool $force, string $until_budget, bool $active_no_signal_drain ): string {
566+
$operation = $active_no_signal_drain ? 'active-no-signal-drain' : 'abandoned';
567+
return sprintf('studio wp datamachine-code workspace worktree %s --apply%s --stage=%s --offset=%d --limit=%d --passes=%d%s --format=json', $operation, $force ? ' --force' : '', $stage, $offset, $limit, $passes, '' !== $until_budget ? ' --until-budget=' . $until_budget : '');
568+
}
569+
530570
private function drain_pages( object $ability, array $base_input, bool $apply, ?float $deadline = null ): array|\WP_Error {
531571
$pages = array();
532572
$summary = array();

inc/Workspace/WorkspaceHygieneReport.php

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -577,27 +577,33 @@ private function build_workspace_disk_report(): array {
577577
*/
578578
private function summarize_workspace_worktrees( array $worktrees, ?array $cleanup ): array {
579579
$summary = array(
580-
'total' => count($worktrees),
581-
'primaries' => 0,
582-
'worktrees' => 0,
583-
'artifacts' => 0,
584-
'external' => 0,
585-
'dirty' => 0,
586-
'protected_dirty' => 0,
587-
'protected_unpushed' => 0,
588-
'missing_metadata' => 0,
589-
'stale_primaries' => 0,
590-
'primary_freshness_by_status' => array(),
591-
'primary_freshness_attention' => array(),
592-
'base_branch_worktree_count' => 0,
593-
'base_branch_worktrees' => array(),
594-
'by_liveness' => array(
580+
'total' => count($worktrees),
581+
'primaries' => 0,
582+
'worktrees' => 0,
583+
'artifacts' => 0,
584+
'external' => 0,
585+
'dirty' => 0,
586+
'inventory_known_dirty' => 0,
587+
'protected_dirty' => 0,
588+
'protected_unpushed' => 0,
589+
'protected_dirty_inventory_known' => 0,
590+
'protected_unpushed_inventory_known' => 0,
591+
'protected_dirty_fresh_probed' => 0,
592+
'protected_unpushed_fresh_probed' => 0,
593+
'protected_count_probe_source' => 'none',
594+
'missing_metadata' => 0,
595+
'stale_primaries' => 0,
596+
'primary_freshness_by_status' => array(),
597+
'primary_freshness_attention' => array(),
598+
'base_branch_worktree_count' => 0,
599+
'base_branch_worktrees' => array(),
600+
'by_liveness' => array(
595601
WorktreeContextInjector::LIVENESS_LIVE => 0,
596602
WorktreeContextInjector::LIVENESS_STOPPED => 0,
597603
WorktreeContextInjector::LIVENESS_STALE => 0,
598604
WorktreeContextInjector::LIVENESS_UNKNOWN => 0,
599605
),
600-
'duplicate_task_groups' => 0,
606+
'duplicate_task_groups' => 0,
601607
);
602608

603609
foreach ( $worktrees as $row ) {
@@ -632,6 +638,7 @@ private function summarize_workspace_worktrees( array $worktrees, ?array $cleanu
632638

633639
if ( (int) ( $row['dirty'] ?? 0 ) > 0 ) {
634640
++$summary['dirty'];
641+
++$summary['inventory_known_dirty'];
635642
}
636643

637644
if ( ! empty($row['missing_metadata']) ) {
@@ -657,11 +664,23 @@ private function summarize_workspace_worktrees( array $worktrees, ?array $cleanu
657664
$summary['duplicates'] = $duplicates;
658665

659666
if ( null !== $cleanup ) {
660-
$by_reason = (array) ( $cleanup['summary']['skipped_by_reason'] ?? array() );
661-
$summary['protected_dirty'] = (int) ( $by_reason['dirty_worktree'] ?? 0 );
662-
$summary['protected_unpushed'] = (int) ( $by_reason['unpushed_commits'] ?? 0 );
663-
$summary['missing_metadata'] = (int) ( $by_reason['missing_metadata'] ?? 0 );
664-
$summary['external'] = max($summary['external'], (int) ( $by_reason['external_worktree'] ?? 0 ));
667+
$by_reason = (array) ( $cleanup['summary']['skipped_by_reason'] ?? array() );
668+
$dirty_blockers = (int) ( $by_reason['dirty_worktree'] ?? 0 );
669+
$unpushed_blockers = (int) ( $by_reason['unpushed_commits'] ?? 0 );
670+
$summary['protected_dirty'] = $dirty_blockers;
671+
$summary['protected_unpushed'] = $unpushed_blockers;
672+
$summary['missing_metadata'] = (int) ( $by_reason['missing_metadata'] ?? 0 );
673+
$summary['external'] = max($summary['external'], (int) ( $by_reason['external_worktree'] ?? 0 ));
674+
675+
$probe_source = ! empty($cleanup['inventory_only']) ? 'inventory_known' : 'fresh_probe';
676+
$summary['protected_count_probe_source'] = $probe_source;
677+
if ( 'fresh_probe' === $probe_source ) {
678+
$summary['protected_dirty_fresh_probed'] = $dirty_blockers;
679+
$summary['protected_unpushed_fresh_probed'] = $unpushed_blockers;
680+
} else {
681+
$summary['protected_dirty_inventory_known'] = $dirty_blockers;
682+
$summary['protected_unpushed_inventory_known'] = $unpushed_blockers;
683+
}
665684
}
666685

667686
return $summary;
@@ -702,15 +721,32 @@ private function summarize_workspace_cleanup( ?array $cleanup, ?array $error, ar
702721
}
703722
unset($candidate);
704723
usort($candidates, fn( $a, $b ) => (int) ( $b['size_bytes'] ?? 0 ) <=> (int) ( $a['size_bytes'] ?? 0 ));
724+
$summary = (array) ( $cleanup['summary'] ?? array() );
725+
$skipped_reason = (array) ( $summary['skipped_by_reason'] ?? array() );
726+
$probe_source = ! empty($cleanup['inventory_only']) ? 'inventory_known' : 'fresh_probe';
727+
$empty_blockers = array(
728+
'dirty_worktree' => 0,
729+
'unpushed_commits' => 0,
730+
);
731+
$blocker_counts = array(
732+
'inventory_known' => $empty_blockers,
733+
'fresh_probe' => $empty_blockers,
734+
);
735+
$blocker_counts[ $probe_source ] = array(
736+
'dirty_worktree' => (int) ( $skipped_reason['dirty_worktree'] ?? 0 ),
737+
'unpushed_commits' => (int) ( $skipped_reason['unpushed_commits'] ?? 0 ),
738+
);
705739
return array(
706740
'included' => true,
707741
'dry_run' => true,
708742
'skip_github' => true,
709743
'inventory_only' => ! empty($cleanup['inventory_only']),
710-
'summary' => $cleanup['summary'] ?? array(),
744+
'blocker_probe_source' => $probe_source,
745+
'blocker_counts' => $blocker_counts,
746+
'summary' => $summary,
711747
'biggest_candidates' => array_slice($candidates, 0, 10),
712-
'skipped_by_reason' => $cleanup['summary']['skipped_by_reason'] ?? array(),
713-
'candidates_by_signal' => $cleanup['summary']['candidates_by_signal'] ?? array(),
748+
'skipped_by_reason' => $skipped_reason,
749+
'candidates_by_signal' => $summary['candidates_by_signal'] ?? array(),
714750
);
715751
}
716752

@@ -732,6 +768,9 @@ private function build_workspace_fast_stats( array $worktrees, ?array $cleanup,
732768
'cleanup_eligible_unprobed_count' => count($cleanup_candidates),
733769
'valid_clean_count' => 0,
734770
'valid_dirty_count' => 0,
771+
'inventory_known_dirty_count' => 0,
772+
'inventory_known_blocker_count' => 0,
773+
'fresh_probed_blocker_count' => 0,
735774
'invalid_broken_orphan_count' => 0,
736775
'unmanaged_skipped_count' => 0,
737776
'dirty_probe_skipped_count' => 0,
@@ -774,12 +813,18 @@ private function build_workspace_fast_stats( array $worktrees, ?array $cleanup,
774813

775814
if ( (int) $dirty > 0 ) {
776815
++$counts['valid_dirty_count'];
816+
++$counts['inventory_known_dirty_count'];
777817
} else {
778818
++$counts['valid_clean_count'];
779819
}
780820
}
781821

782822
$blocked_dirty = (int) ( $cleanup_summary['skipped_by_reason']['dirty_worktree'] ?? 0 ) + (int) ( $cleanup_summary['skipped_by_reason']['unpushed_commits'] ?? 0 );
823+
if ( ! empty($cleanup['inventory_only']) ) {
824+
$counts['inventory_known_blocker_count'] = $blocked_dirty;
825+
} else {
826+
$counts['fresh_probed_blocker_count'] = $blocked_dirty;
827+
}
783828
if ( $blocked_dirty > $counts['valid_dirty_count'] ) {
784829
$counts['valid_dirty_count'] = $blocked_dirty;
785830
}

0 commit comments

Comments
 (0)