@@ -266,20 +266,29 @@ public function worktree_active_no_signal_merged_apply( array $opts = array() ):
266266 return array ( 'report_action_counts ' => $ report ['summary ' ]['by_suggested_action ' ] ?? array () );
267267 },
268268 'prepare_row ' => function ( array $ row ): array |\WP_Error {
269- $ is_merged_to_default = 'merged_to_default ' === (string ) ( $ row ['suggested_action ' ] ?? '' )
270- || (
271- 0 === (int ) ( $ row ['dirty ' ] ?? -1 )
272- && 0 === (int ) ( $ row ['unpushed ' ] ?? -1 )
273- && 0 === (int ) ( $ row ['commits_outside_default ' ] ?? -1 )
274- );
275-
276- if ( ! $ is_merged_to_default ) {
277- return new \WP_Error ('not_merged_to_default ' , 'row is not a clean merged-to-default candidate ' );
269+ $ action = (string ) ( $ row ['suggested_action ' ] ?? '' );
270+ if ( in_array ($ action , array ( 'merged_to_default ' , 'patch_equivalent_default ' ), true ) ) {
271+ return $ row ;
278272 }
279273
280- return $ row ;
274+ if (
275+ 0 === (int ) ( $ row ['dirty ' ] ?? -1 )
276+ && 0 === (int ) ( $ row ['unpushed ' ] ?? -1 )
277+ && 0 === (int ) ( $ row ['commits_outside_default ' ] ?? -1 )
278+ ) {
279+ return $ row ;
280+ }
281+
282+ return new \WP_Error ('not_merged_to_default ' , 'row is not a clean merged-to-default candidate ' );
283+ },
284+ 'build_metadata ' => function ( array $ row ): array |\WP_Error {
285+ $ action = (string ) ( $ row ['suggested_action ' ] ?? '' );
286+ if ( 'patch_equivalent_default ' === $ action ) {
287+ return $ this ->build_active_no_signal_patch_equivalent_to_default_metadata ($ row );
288+ }
289+
290+ return $ this ->build_active_no_signal_merged_to_default_metadata ($ row );
281291 },
282- 'build_metadata ' => fn ( array $ row ): array |\WP_Error => $ this ->build_active_no_signal_merged_to_default_metadata ($ row ),
283292 'build_planned ' => static function ( array $ row , array $ metadata ): array {
284293 return array (
285294 'handle ' => (string ) ( $ row ['handle ' ] ?? '' ),
@@ -710,6 +719,39 @@ private function build_active_no_signal_merged_to_default_metadata( array $row )
710719 return $ metadata ;
711720 }
712721
722+ /**
723+ * Build cleanup metadata from one clean patch-equivalent-to-default evidence row.
724+ *
725+ * @param array<string,mixed> $row Evidence row.
726+ * @return array<string,mixed>|\WP_Error
727+ */
728+ private function build_active_no_signal_patch_equivalent_to_default_metadata ( array $ row ): array |\WP_Error {
729+ $ handle = (string ) ( $ row ['handle ' ] ?? '' );
730+ $ repo = (string ) ( $ row ['repo ' ] ?? '' );
731+ $ branch = (string ) ( $ row ['branch ' ] ?? '' );
732+ $ path = (string ) ( $ row ['path ' ] ?? '' );
733+
734+ $ evidence = $ this ->build_current_patch_equivalent_to_default_cleanup_evidence ($ handle , $ repo , $ branch , $ path );
735+ if ( is_wp_error ($ evidence ) ) {
736+ return $ evidence ;
737+ }
738+
739+ $ metadata = $ this ->build_active_no_signal_cleanup_metadata (
740+ $ row ,
741+ $ handle ,
742+ $ repo ,
743+ $ branch ,
744+ (string ) ( $ evidence ['path ' ] ?? $ path ),
745+ WorktreeContextInjector::STATE_MERGED
746+ );
747+ $ metadata ['auto_finalized_by ' ] = 'active_no_signal_merged_apply ' ;
748+ $ metadata ['auto_finalized_signal ' ] = 'patch-equivalent-to-default ' ;
749+ $ metadata ['auto_finalized_reason ' ] = sprintf ('active/no-signal report found branch patch-equivalent to %s ' , (string ) ( $ evidence ['default_ref ' ] ?? 'remote default ' ));
750+ $ metadata ['cleanup_eligibility_evidence ' ] = $ evidence ;
751+
752+ return $ metadata ;
753+ }
754+
713755 /**
714756 * Build cleanup metadata from one clean remote-tracking evidence row.
715757 *
@@ -971,6 +1013,87 @@ private function build_current_merged_to_default_cleanup_evidence( string $handl
9711013 );
9721014 }
9731015
1016+ /**
1017+ * Recompute patch-equivalent-to-default evidence for the current worktree state.
1018+ *
1019+ * @param string $handle Workspace handle.
1020+ * @param string $repo Repository name.
1021+ * @param string $branch Branch name.
1022+ * @param string $path Worktree path.
1023+ * @return array<string,mixed>|\WP_Error
1024+ */
1025+ private function build_current_patch_equivalent_to_default_cleanup_evidence ( string $ handle , string $ repo , string $ branch , string $ path ): array |\WP_Error {
1026+ foreach (
1027+ array (
1028+ 'handle ' => $ handle ,
1029+ 'repo ' => $ repo ,
1030+ 'branch ' => $ branch ,
1031+ 'path ' => $ path ,
1032+ ) as $ field => $ value
1033+ ) {
1034+ if ( '' === $ value ) {
1035+ return new \WP_Error ('missing_identity ' , 'missing required identity field: ' . $ field );
1036+ }
1037+ }
1038+
1039+ $ facts = $ this ->validate_current_cleanup_worktree (
1040+ $ repo ,
1041+ $ path ,
1042+ $ branch ,
1043+ array (
1044+ 'require_clean ' => true ,
1045+ 'dirty_error_message ' => 'refusing to mark dirty worktree cleanup_eligible from patch-equivalent-to-default evidence ' ,
1046+ 'unpushed_error_message ' => 'refusing to mark worktree with unpushed commits cleanup_eligible from patch-equivalent-to-default evidence ' ,
1047+ )
1048+ );
1049+ if ( is_wp_error ($ facts ) ) {
1050+ return $ facts ;
1051+ }
1052+
1053+ $ real_path = (string ) $ facts ['real_path ' ];
1054+ $ primary_path = (string ) $ facts ['primary_path ' ];
1055+ $ dirty = (int ) $ facts ['dirty ' ];
1056+ $ unpushed = (int ) $ facts ['unpushed ' ];
1057+
1058+ $ default_ref = $ this ->resolve_remote_default_ref ($ primary_path , self ::CLEANUP_GIT_PROBE_TIMEOUT );
1059+ if ( ! is_string ($ default_ref ) || '' === $ default_ref ) {
1060+ return new \WP_Error ('missing_default_ref ' , 'primary checkout default ref could not be resolved ' );
1061+ }
1062+
1063+ $ upstream_equivalence = $ this ->build_clean_upstream_equivalence_evidence ($ primary_path , $ real_path , $ default_ref , $ branch );
1064+ if ( 'equivalent_clean ' !== (string ) ( $ upstream_equivalence ['effective_status ' ] ?? '' ) ) {
1065+ return new \WP_Error ('not_patch_equivalent_to_default ' , 'current branch is not patch-equivalent to remote default ' );
1066+ }
1067+
1068+ $ branch_ref = 'refs/heads/ ' . $ branch ;
1069+ $ branch_head = $ this ->run_git ($ primary_path , sprintf ('rev-parse --verify %s ' , escapeshellarg ($ branch_ref )), self ::CLEANUP_GIT_PROBE_TIMEOUT );
1070+ if ( is_wp_error ($ branch_head ) ) {
1071+ return $ branch_head ;
1072+ }
1073+ $ default_head = $ this ->run_git ($ primary_path , sprintf ('rev-parse --verify %s ' , escapeshellarg ($ default_ref . '^{commit} ' )), self ::CLEANUP_GIT_PROBE_TIMEOUT );
1074+ if ( is_wp_error ($ default_head ) ) {
1075+ return $ default_head ;
1076+ }
1077+
1078+ return array (
1079+ 'signal ' => 'patch-equivalent-to-default ' ,
1080+ 'finalized_state ' => WorktreeContextInjector::STATE_MERGED ,
1081+ 'reason ' => 'branch commits are patch-equivalent to the remote default ref ' ,
1082+ 'detected_at ' => gmdate ('c ' ),
1083+ 'handle ' => $ handle ,
1084+ 'repo ' => $ repo ,
1085+ 'branch ' => $ branch ,
1086+ 'path ' => $ real_path ,
1087+ 'default_ref ' => $ default_ref ,
1088+ 'branch_ref ' => $ branch_ref ,
1089+ 'branch_head ' => trim ( (string ) ( $ branch_head ['output ' ] ?? '' )),
1090+ 'default_head ' => trim ( (string ) ( $ default_head ['output ' ] ?? '' )),
1091+ 'upstream_equivalence ' => $ upstream_equivalence ,
1092+ 'dirty ' => (int ) $ dirty ,
1093+ 'unpushed ' => (int ) $ unpushed ,
1094+ );
1095+ }
1096+
9741097 /**
9751098 * Revalidate the current worktree state before writing cleanup metadata.
9761099 *
@@ -1710,14 +1833,6 @@ private function suggest_active_no_signal_action( array $row ): string {
17101833 return 'merged_to_default ' ;
17111834 }
17121835
1713- $ effective_status = (string ) ( $ row ['upstream_equivalence ' ]['effective_status ' ] ?? '' );
1714- if ( 'equivalent_clean ' === $ effective_status ) {
1715- return 'patch_equivalent_default ' ;
1716- }
1717- if ( 'contained_non_default_remote ' === $ effective_status ) {
1718- return 'contained_non_default_remote ' ;
1719- }
1720-
17211836 if ( (int ) ( $ row ['dirty ' ] ?? 0 ) > 0 || (int ) ( $ row ['unpushed ' ] ?? 0 ) > 0 ) {
17221837 return 'unsafe_dirty_or_unpushed ' ;
17231838 }
@@ -1730,6 +1845,14 @@ private function suggest_active_no_signal_action( array $row ): string {
17301845 return 'active_open_pr ' ;
17311846 }
17321847
1848+ $ effective_status = (string ) ( $ row ['upstream_equivalence ' ]['effective_status ' ] ?? '' );
1849+ if ( 'equivalent_clean ' === $ effective_status ) {
1850+ return 'patch_equivalent_default ' ;
1851+ }
1852+ if ( 'contained_non_default_remote ' === $ effective_status ) {
1853+ return 'contained_non_default_remote ' ;
1854+ }
1855+
17331856 if ( true === ( $ row ['remote_tracking ' ] ?? null ) ) {
17341857 return 'remote_tracking_clean ' ;
17351858 }
0 commit comments