Skip to content

Commit 05c3022

Browse files
Explain remaining metadata reconcile blockers (#855)
* Explain remaining metadata reconcile blockers * Fix reconcile metadata lint alignment --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent c551383 commit 05c3022

4 files changed

Lines changed: 187 additions & 18 deletions

inc/Workspace/WorkspaceMetadataReconciliation.php

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ public function worktree_reconcile_metadata( array $opts = array() ): array|\WP_
146146
$plan['summary']['prefiltered'] = $prefilter['summary'];
147147
if ( null !== $pagination ) {
148148
$plan['pagination'] = $pagination;
149-
$plan['evidence'] = array(
149+
if ( ! empty($pagination['complete']) ) {
150+
$plan['remaining_blockers'] = $this->build_worktree_metadata_reconciliation_remaining_blockers($proposals, array(), $skipped, $dry_run, $limit, null !== $budget_context ? (string) $budget_context['label'] : '');
151+
}
152+
$plan['evidence'] = array(
150153
'scope' => 'paginated metadata reconciliation dry-run',
151154
'note' => 'Only candidate rows with missing, incomplete, invalid, or finalizable metadata ran per-worktree dirty, unpushed, merge-signal, and GitHub probes. Run the next_offset page until complete for full inventory review.',
152155
'fields_skipped_by_listing' => (array) ( $listing['fields_skipped'] ?? array() ),
@@ -252,7 +255,7 @@ private function drain_worktree_metadata_reconciliation_budget( int $limit, int
252255
$pagination['next_command'] = $next_command;
253256
}
254257

255-
return array(
258+
$result = array(
256259
'success' => true,
257260
'dry_run' => false,
258261
'applied' => true,
@@ -280,6 +283,11 @@ private function drain_worktree_metadata_reconciliation_budget( int $limit, int
280283
)
281284
),
282285
);
286+
if ( $complete ) {
287+
$result['remaining_blockers'] = $this->build_worktree_metadata_reconciliation_remaining_blockers($proposals, $written, $skipped, false, $limit, $budget_label);
288+
}
289+
290+
return $result;
283291
}
284292

285293
/**
@@ -1498,6 +1506,9 @@ private function apply_worktree_metadata_reconciliation_plan( array $plan ): arr
14981506
if ( ! empty($plan['direct_apply']) && count($written) > 0 ) {
14991507
$result['pagination'] = $this->restart_worktree_metadata_reconciliation_pagination( (array) $result['pagination'] );
15001508
}
1509+
if ( ! empty($result['pagination']['complete']) ) {
1510+
$result['remaining_blockers'] = $this->build_worktree_metadata_reconciliation_remaining_blockers($planned, $written, $skipped, false, (int) ( $result['pagination']['limit'] ?? self::METADATA_RECONCILE_DEFAULT_LIMIT ), '');
1511+
}
15011512
}
15021513
if ( isset($plan['evidence']) && is_array($plan['evidence']) ) {
15031514
$result['evidence'] = array_merge(
@@ -1691,6 +1702,79 @@ private function build_worktree_metadata_reconciliation_summary( int $inspected,
16911702
);
16921703
}
16931704

1705+
/**
1706+
* Explain why metadata reconciliation blockers remain after a complete bounded pass.
1707+
*
1708+
* @param array<int,array<string,mixed>> $proposals Proposal rows.
1709+
* @param array<int,array<string,mixed>> $written Written rows.
1710+
* @param array<int,array<string,mixed>> $skipped Skipped rows.
1711+
* @param bool $dry_run Whether this was a dry-run page.
1712+
* @param int $limit Page size for generated commands.
1713+
* @param string $budget Optional budget label.
1714+
* @return array<string,mixed>
1715+
*/
1716+
private function build_worktree_metadata_reconciliation_remaining_blockers( array $proposals, array $written, array $skipped, bool $dry_run, int $limit, string $budget ): array {
1717+
$written_handles = array_flip(array_map(fn( $row ) => (string) ( $row['handle'] ?? '' ), $written));
1718+
$pending_proposals = array_values(array_filter($proposals, fn( $row ) => '' === (string) ( $row['handle'] ?? '' ) || ! isset($written_handles[ (string) $row['handle'] ] )));
1719+
1720+
$proposed_by_reason = array();
1721+
$proposed_by_state = array();
1722+
foreach ( $pending_proposals as $row ) {
1723+
$reason = (string) ( $row['reason_code'] ?? 'unknown' );
1724+
$state = (string) ( $row['proposed_metadata']['lifecycle_state'] ?? 'unknown' );
1725+
$proposed_by_reason[ $reason ] = (int) ( $proposed_by_reason[ $reason ] ?? 0 ) + 1;
1726+
$proposed_by_state[ $state ] = (int) ( $proposed_by_state[ $state ] ?? 0 ) + 1;
1727+
}
1728+
ksort($proposed_by_reason);
1729+
ksort($proposed_by_state);
1730+
1731+
$skipped_by_reason = array();
1732+
$manual_repair = 0;
1733+
$retry_may_help = 0;
1734+
foreach ( $skipped as $row ) {
1735+
$reason = (string) ( $row['reason_code'] ?? 'unknown' );
1736+
$skipped_by_reason[ $reason ] = (int) ( $skipped_by_reason[ $reason ] ?? 0 ) + 1;
1737+
if ( in_array($reason, array( 'probe_timeout', 'git_probe_failed', 'github_unknown' ), true) ) {
1738+
++$retry_may_help;
1739+
} else {
1740+
++$manual_repair;
1741+
}
1742+
}
1743+
ksort($skipped_by_reason);
1744+
1745+
$budget_arg = '' !== $budget ? ' --until-budget=' . $budget : '';
1746+
if ( $dry_run && array() !== $pending_proposals ) {
1747+
$next_action = 'apply_reviewed_plan';
1748+
$next_command = sprintf('studio wp datamachine-code workspace worktree reconcile-metadata --apply --limit=%d --offset=0%s --format=json', $limit, $budget_arg);
1749+
$why = 'The complete dry-run found repairable rows; applying the reviewed plan can reduce these blockers.';
1750+
} elseif ( $retry_may_help > 0 ) {
1751+
$next_action = 'retry_after_transient_probe_failure';
1752+
$next_command = sprintf('studio wp datamachine-code workspace worktree reconcile-metadata --apply --limit=%d --offset=0%s --format=json', $limit, $budget_arg);
1753+
$why = 'Some rows were skipped because probes were unavailable or timed out; another pass may help after the underlying probe issue clears.';
1754+
} elseif ( $manual_repair > 0 ) {
1755+
$next_action = 'manual_repair_required';
1756+
$next_command = null;
1757+
$why = 'The bounded pass is complete and remaining skipped rows need operator repair before reconciliation can classify them.';
1758+
} else {
1759+
$next_action = 'none';
1760+
$next_command = null;
1761+
$why = 'The bounded pass is complete and no metadata reconciliation blockers remain in the scanned candidate set.';
1762+
}
1763+
1764+
return array(
1765+
'total' => count($pending_proposals) + count($skipped),
1766+
'needs_apply' => count($pending_proposals),
1767+
'manual_repair' => $manual_repair,
1768+
'retry_may_help' => $retry_may_help,
1769+
'proposed_by_reason' => $proposed_by_reason,
1770+
'proposed_by_state' => $proposed_by_state,
1771+
'skipped_by_reason' => $skipped_by_reason,
1772+
'next_action' => $next_action,
1773+
'next_command' => $next_command,
1774+
'why' => $why,
1775+
);
1776+
}
1777+
16941778
/**
16951779
* Summarize the slowest worktree rows from an expensive report page.
16961780
*

inc/Workspace/WorkspaceWorktreeCleanupEngine.php

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -274,19 +274,22 @@ public function worktree_cleanup_merged( array $opts = array() ): array|\WP_Erro
274274
}
275275
$skipped[] = array_merge(
276276
array(
277-
'handle' => $handle,
278-
'repo' => $repo,
279-
'branch' => $branch,
280-
'path' => $wt_path,
281-
'reason_code' => 'missing_metadata',
282-
'reason' => 'missing repo/branch/path',
283-
'missing_fields' => $missing_fields,
284-
'hydrated_fields' => $identity['hydrated_fields'],
285-
'identity_conflicts' => $identity['conflicts'],
286-
'stored_identity' => $identity['stored_identity'],
287-
'hint' => 'Run workspace worktree prune if this is a stale registry entry; inspect manually if the path still exists.',
288-
'created_at' => $created_at,
289-
'metadata' => $metadata,
277+
'handle' => $handle,
278+
'repo' => $repo,
279+
'branch' => $branch,
280+
'path' => $wt_path,
281+
'reason_code' => 'missing_metadata',
282+
'reason' => 'missing repo/branch/path',
283+
'missing_fields' => $missing_fields,
284+
'hydrated_fields' => $identity['hydrated_fields'],
285+
'identity_conflicts' => $identity['conflicts'],
286+
'stored_identity' => $identity['stored_identity'],
287+
'reconcile_reason_code' => array() === $identity['conflicts'] ? 'missing_identity' : 'manual_review_identity_metadata',
288+
'reconcile_skipped_state' => 'manual_repair_required',
289+
'reconcile_next_action' => 'repair the missing identity fields or stale registry row before another reconcile pass can classify cleanup safety',
290+
'hint' => 'Run workspace worktree prune if this is a stale registry entry; inspect manually if the path still exists.',
291+
'created_at' => $created_at,
292+
'metadata' => $metadata,
290293
), $disk_fields
291294
);
292295
continue;

inc/Workspace/WorkspaceWorktreeInventoryCleanup.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,12 @@ private function worktree_cleanup_inventory_only( string $older_than, string $so
9595
if ( ! is_array($metadata) || array() === $metadata ) {
9696
$skipped[] = array_merge(
9797
$base_row, array(
98-
'reason_code' => 'needs_metadata_reconcile',
99-
'reason' => 'inventory row has no lifecycle metadata; metadata reconciliation is required before cleanup planning can classify it',
100-
'hint' => 'Run workspace worktree reconcile-metadata --dry-run --limit=25 --offset=0 --until-budget=30s --format=json to generate reviewed metadata reconciliation rows.',
98+
'reason_code' => 'needs_metadata_reconcile',
99+
'reason' => 'inventory row has no lifecycle metadata; metadata reconciliation is required before cleanup planning can classify it',
100+
'reconcile_reason_code' => 'missing_metadata',
101+
'reconcile_skipped_state' => 'not_yet_reconciled',
102+
'reconcile_next_action' => 'run complete reconcile dry-run, then apply reviewed repairable rows; if complete output lists this row under skipped_by_reason, follow that reason before retrying cleanup',
103+
'hint' => 'Run workspace worktree reconcile-metadata --dry-run --limit=25 --offset=0 --until-budget=30s --format=json to generate reviewed metadata reconciliation rows.',
101104
)
102105
);
103106
continue;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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/WorkspaceMetadataReconciliation.php';
10+
11+
function worktree_metadata_reconciliation_remaining_blockers_assert_same( mixed $expected, mixed $actual, string $message ): void {
12+
if ( $expected !== $actual ) {
13+
throw new RuntimeException(sprintf('%s Expected %s, got %s.', $message, var_export($expected, true), var_export($actual, true)));
14+
}
15+
}
16+
17+
$reconciler = new class {
18+
use DataMachineCode\Workspace\WorkspaceMetadataReconciliation;
19+
};
20+
21+
$method = new ReflectionMethod($reconciler, 'build_worktree_metadata_reconciliation_remaining_blockers');
22+
23+
$dry_run = $method->invoke(
24+
$reconciler,
25+
array(
26+
array(
27+
'handle' => 'repo@one',
28+
'reason_code' => 'metadata_backfill',
29+
'proposed_metadata' => array( 'lifecycle_state' => 'active' ),
30+
),
31+
),
32+
array(),
33+
array(
34+
array(
35+
'handle' => 'repo@manual',
36+
'reason_code' => 'missing_identity',
37+
),
38+
),
39+
true,
40+
25,
41+
'30s'
42+
);
43+
44+
worktree_metadata_reconciliation_remaining_blockers_assert_same(2, $dry_run['total'], 'dry-run complete pass counts proposals and skips as remaining blockers');
45+
worktree_metadata_reconciliation_remaining_blockers_assert_same(1, $dry_run['needs_apply'], 'dry-run complete pass reports repairable rows needing apply');
46+
worktree_metadata_reconciliation_remaining_blockers_assert_same(array( 'metadata_backfill' => 1 ), $dry_run['proposed_by_reason'], 'dry-run groups repairable blockers by reconcile reason');
47+
worktree_metadata_reconciliation_remaining_blockers_assert_same(array( 'active' => 1 ), $dry_run['proposed_by_state'], 'dry-run groups repairable blockers by proposed state');
48+
worktree_metadata_reconciliation_remaining_blockers_assert_same(array( 'missing_identity' => 1 ), $dry_run['skipped_by_reason'], 'dry-run groups skipped blockers by reconcile reason');
49+
worktree_metadata_reconciliation_remaining_blockers_assert_same('apply_reviewed_plan', $dry_run['next_action'], 'dry-run next action points at apply when it can help');
50+
worktree_metadata_reconciliation_remaining_blockers_assert_same('studio wp datamachine-code workspace worktree reconcile-metadata --apply --limit=25 --offset=0 --until-budget=30s --format=json', $dry_run['next_command'], 'dry-run next command is actionable');
51+
52+
$manual = $method->invoke(
53+
$reconciler,
54+
array(
55+
array(
56+
'handle' => 'repo@written',
57+
'reason_code' => 'metadata_backfill',
58+
'proposed_metadata' => array( 'lifecycle_state' => 'active' ),
59+
),
60+
),
61+
array( array( 'handle' => 'repo@written' ) ),
62+
array( array( 'handle' => 'repo@manual', 'reason_code' => 'manual_review_identity_metadata' ) ),
63+
false,
64+
25,
65+
''
66+
);
67+
68+
worktree_metadata_reconciliation_remaining_blockers_assert_same(1, $manual['total'], 'apply complete pass excludes written proposals from remaining blockers');
69+
worktree_metadata_reconciliation_remaining_blockers_assert_same(0, $manual['needs_apply'], 'apply complete pass has no pending proposals after write');
70+
worktree_metadata_reconciliation_remaining_blockers_assert_same(1, $manual['manual_repair'], 'apply complete pass identifies manual repair blockers');
71+
worktree_metadata_reconciliation_remaining_blockers_assert_same('manual_repair_required', $manual['next_action'], 'apply complete pass does not suggest another pass for manual blockers');
72+
worktree_metadata_reconciliation_remaining_blockers_assert_same(null, $manual['next_command'], 'manual repair blockers do not get a misleading next command');
73+
74+
$source = file_get_contents(dirname(__DIR__) . '/inc/Workspace/WorkspaceWorktreeInventoryCleanup.php');
75+
if ( false === $source || ! str_contains($source, "'reconcile_reason_code' => 'missing_metadata'") || ! str_contains($source, "'reconcile_skipped_state' => 'not_yet_reconciled'") ) {
76+
throw new RuntimeException('Inventory needs_metadata_reconcile rows must expose reconcile reason and skipped state.');
77+
}
78+
79+
echo "worktree-metadata-reconciliation-remaining-blockers: ok\n";

0 commit comments

Comments
 (0)