Skip to content

Commit a785759

Browse files
Refactor cleanup evidence behind DMC boundary (#778)
* refactor: route cleanup evidence through DMC boundary * fix: satisfy cleanup evidence lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 8f910e2 commit a785759

5 files changed

Lines changed: 174 additions & 38 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
namespace DataMachineCode\Abilities;
1616

1717
use DataMachineCode\Support\PermissionHelper;
18+
use DataMachineCode\Cleanup\WorkspaceCleanupRunEvidenceStore;
1819
use DataMachineCode\Workspace\CleanupRunService;
1920
use DataMachineCode\Workspace\RemoteWorkspaceBackend;
2021
use DataMachineCode\Workspace\RunnerWorkspacePublisher;
@@ -564,15 +565,15 @@ private function registerAbilities(): void {
564565
'input_schema' => array(
565566
'type' => 'object',
566567
'properties' => array(
567-
'repo' => array(
568+
'repo' => array(
568569
'type' => 'string',
569570
'description' => 'Workspace handle: `<repo>` (primary) or `<repo>@<branch-slug>` (worktree).',
570571
),
571-
'path' => array(
572+
'path' => array(
572573
'type' => 'string',
573574
'description' => 'Relative file path within the repo.',
574575
),
575-
'content' => array(
576+
'content' => array(
576577
'type' => 'string',
577578
'description' => 'File content to write.',
578579
),
@@ -607,39 +608,39 @@ private function registerAbilities(): void {
607608
'input_schema' => array(
608609
'type' => 'object',
609610
'properties' => array(
610-
'repo' => array(
611+
'repo' => array(
611612
'type' => 'string',
612613
'description' => 'Workspace handle: `<repo>` (primary) or `<repo>@<branch-slug>` (worktree).',
613614
),
614-
'path' => array(
615+
'path' => array(
615616
'type' => 'string',
616617
'description' => 'Relative file path within the repo.',
617618
),
618-
'old_string' => array(
619+
'old_string' => array(
619620
'type' => 'string',
620621
'description' => 'Text to find.',
621622
),
622-
'new_string' => array(
623+
'new_string' => array(
623624
'type' => 'string',
624625
'description' => 'Replacement text.',
625626
),
626-
'search' => array(
627+
'search' => array(
627628
'type' => 'string',
628629
'description' => 'Alias for old_string.',
629630
),
630-
'replace' => array(
631+
'replace' => array(
631632
'type' => 'string',
632633
'description' => 'Alias for new_string.',
633634
),
634-
'old' => array(
635+
'old' => array(
635636
'type' => 'string',
636637
'description' => 'Alias for old_string.',
637638
),
638-
'new' => array(
639+
'new' => array(
639640
'type' => 'string',
640641
'description' => 'Alias for new_string.',
641642
),
642-
'replace_all' => array(
643+
'replace_all' => array(
643644
'type' => 'boolean',
644645
'description' => 'Replace all occurrences (default false).',
645646
),
@@ -1297,47 +1298,47 @@ private function registerAbilities(): void {
12971298
'input_schema' => array(
12981299
'type' => 'object',
12991300
'properties' => array(
1300-
'repo' => array(
1301+
'repo' => array(
13011302
'type' => 'string',
13021303
'description' => 'Primary repo name (no @-suffix).',
13031304
),
1304-
'branch' => array(
1305+
'branch' => array(
13051306
'type' => 'string',
13061307
'description' => 'Branch to check out in the worktree (e.g. fix/foo-bar). Slashes become dashes in the on-disk slug.',
13071308
),
1308-
'from' => array(
1309+
'from' => array(
13091310
'type' => 'string',
13101311
'description' => 'Base ref when creating the branch (default origin/HEAD).',
13111312
),
1312-
'inject_context' => array(
1313+
'inject_context' => array(
13131314
'type' => 'boolean',
13141315
'description' => 'Inject the originating site\'s agent context (MEMORY.md, USER.md, RULES.md) into the new worktree. Default true. Set false to create a bare worktree.',
13151316
),
1316-
'bootstrap' => array(
1317+
'bootstrap' => array(
13171318
'type' => 'boolean',
13181319
'description' => 'Run detected bootstrap steps (submodule init plus root or one-level nested package-manager/composer installs) after creating the worktree. Default true. Steps are skipped gracefully when their trigger file or tool is missing. Set false for a bare checkout (e.g. when only reading code).',
13191320
),
1320-
'allow_stale' => array(
1321+
'allow_stale' => array(
13211322
'type' => 'boolean',
13221323
'description' => 'Bypass the staleness gate. When false (default), any branch/base behind the remote default branch is refused, and a new worktree more than `datamachine_worktree_stale_threshold` commits behind upstream is rolled back with a staleness error. Set true to opt in to a known-stale checkout.',
13231324
),
13241325
'allow_unverified_freshness' => array(
13251326
'type' => 'boolean',
13261327
'description' => 'Bypass the fetch-failure freshness gate. When false (default), worktree creation is refused if remote freshness cannot be verified. Set true only for intentional offline work with local refs.',
13271328
),
1328-
'rebase_base' => array(
1329+
'rebase_base' => array(
13291330
'type' => 'boolean',
13301331
'description' => 'After creating the worktree, rebase onto the upstream tip (the branch\'s @{upstream} for existing branches, origin/<base> for new branches off a local base). Default false. On rebase conflicts the rebase is aborted; the worktree stays at its pre-rebase state and `rebase_succeeded: false` is surfaced.',
13311332
),
1332-
'force' => array(
1333+
'force' => array(
13331334
'type' => 'boolean',
13341335
'description' => 'Explicitly bypass the disk-budget refusal threshold. The disk-budget report still appears in output so the override is visible.',
13351336
),
1336-
'task_url' => array(
1337+
'task_url' => array(
13371338
'type' => 'string',
13381339
'description' => 'Optional task/issue URL (e.g. GitHub issue link) to record on the worktree for ownership/duplicate detection. Falls back to DATAMACHINE_TASK_URL env when omitted.',
13391340
),
1340-
'task_ref' => array(
1341+
'task_ref' => array(
13411342
'type' => 'string',
13421343
'description' => 'Optional short task/issue reference (e.g. `org/repo#123`) recorded alongside task_url. Falls back to DATAMACHINE_TASK_REF env when omitted.',
13431344
),
@@ -2665,7 +2666,7 @@ public static function readFile( array $input ): array|\WP_Error {
26652666
if ( is_wp_error($handle_check) ) {
26662667
return $handle_check;
26672668
}
2668-
$reader = new WorkspaceReader($workspace);
2669+
$reader = new WorkspaceReader($workspace);
26692670
if ( RemoteWorkspaceBackend::should_handle() && null !== self::showLocalWorkspaceHandleIfPresent($workspace, (string) ( $input['repo'] ?? '' )) ) {
26702671
return $reader->read_file(
26712672
$input['repo'] ?? '',
@@ -2713,7 +2714,7 @@ public static function listDirectory( array $input ): array|\WP_Error {
27132714
if ( is_wp_error($handle_check) ) {
27142715
return $handle_check;
27152716
}
2716-
$reader = new WorkspaceReader($workspace);
2717+
$reader = new WorkspaceReader($workspace);
27172718
if ( RemoteWorkspaceBackend::should_handle() && null !== self::showLocalWorkspaceHandleIfPresent($workspace, (string) ( $input['repo'] ?? '' )) ) {
27182719
return $reader->list_directory(
27192720
$input['repo'] ?? '',
@@ -2752,7 +2753,7 @@ public static function grepFiles( array $input ): array|\WP_Error {
27522753
if ( is_wp_error($handle_check) ) {
27532754
return $handle_check;
27542755
}
2755-
$reader = new WorkspaceReader($workspace);
2756+
$reader = new WorkspaceReader($workspace);
27562757
if ( RemoteWorkspaceBackend::should_handle() && null !== self::showLocalWorkspaceHandleIfPresent($workspace, (string) ( $input['repo'] ?? '' )) ) {
27572758
return $reader->grep(
27582759
$input['repo'] ?? '',
@@ -4389,7 +4390,7 @@ public static function workspaceCleanupUntilEmpty( array $input ): array|\WP_Err
43894390
* @return array<string,mixed>|\WP_Error
43904391
*/
43914392
public static function workspaceCleanupStatus( array $input ): array|\WP_Error {
4392-
return ( new CleanupRunService() )->status( (string) ( $input['run_id'] ?? '' ));
4393+
return ( new WorkspaceCleanupRunEvidenceStore() )->read( (string) ( $input['run_id'] ?? '' ));
43934394
}
43944395

43954396
/**
@@ -4399,7 +4400,7 @@ public static function workspaceCleanupStatus( array $input ): array|\WP_Error {
43994400
* @return array<string,mixed>|\WP_Error
44004401
*/
44014402
public static function workspaceCleanupEvidence( array $input ): array|\WP_Error {
4402-
return ( new CleanupRunService() )->evidence( (string) ( $input['run_id'] ?? '' ));
4403+
return ( new WorkspaceCleanupRunEvidenceStore() )->read( (string) ( $input['run_id'] ?? '' ), true );
44034404
}
44044405

44054406
/**
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
/**
3+
* Cleanup run evidence store that routes across DMC-owned storage adapters.
4+
*
5+
* @package DataMachineCode\Cleanup
6+
*/
7+
8+
namespace DataMachineCode\Cleanup;
9+
10+
defined('ABSPATH') || exit;
11+
12+
class CompositeCleanupRunEvidenceStore implements CleanupRunEvidenceStoreInterface {
13+
14+
15+
16+
public function __construct(
17+
private ?CleanupRunEvidenceStoreInterface $workspace_store = null,
18+
private ?CleanupRunEvidenceStoreInterface $job_store = null
19+
) {
20+
$this->workspace_store ??= new WorkspaceCleanupRunEvidenceStore();
21+
$this->job_store ??= new DataMachineJobCleanupRunEvidenceStore();
22+
}
23+
24+
/**
25+
* Read one cleanup run from the store implied by its identifier.
26+
*
27+
* @param string $run_id Stable cleanup run identifier.
28+
* @param bool $include_evidence Whether to include raw evidence records.
29+
* @param bool $include_details Whether to include verbose diagnostic details.
30+
* @return array<string,mixed>|\WP_Error
31+
*/
32+
public function read( string $run_id, bool $include_evidence = false, bool $include_details = false ): array|\WP_Error {
33+
if ( $this->is_job_cleanup_run_id($run_id) ) {
34+
return $this->job_store->read($run_id, $include_evidence, $include_details);
35+
}
36+
37+
return $this->workspace_store->read($run_id, $include_evidence, $include_details);
38+
}
39+
40+
private function is_job_cleanup_run_id( string $run_id ): bool {
41+
$run_id = trim($run_id);
42+
return is_numeric($run_id) || 1 === preg_match('/^cleanup-run-\d+$/', $run_id);
43+
}
44+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Cleanup run evidence store backed by DMC workspace cleanup rows.
4+
*
5+
* @package DataMachineCode\Cleanup
6+
*/
7+
8+
namespace DataMachineCode\Cleanup;
9+
10+
use DataMachineCode\Workspace\CleanupRunService;
11+
12+
defined('ABSPATH') || exit;
13+
14+
class WorkspaceCleanupRunEvidenceStore implements CleanupRunEvidenceStoreInterface {
15+
16+
17+
18+
public function __construct(
19+
private ?CleanupRunService $cleanup_run_service = null
20+
) {
21+
$this->cleanup_run_service ??= new CleanupRunService();
22+
}
23+
24+
/**
25+
* Read one DMC workspace cleanup run.
26+
*
27+
* @param string $run_id Stable cleanup run identifier.
28+
* @param bool $include_evidence Whether to include raw evidence records.
29+
* @param bool $include_details Whether to include verbose diagnostic details.
30+
* @return array<string,mixed>|\WP_Error
31+
*/
32+
public function read( string $run_id, bool $include_evidence = false, bool $include_details = false ): array|\WP_Error {
33+
unset($include_details);
34+
35+
return $include_evidence ? $this->cleanup_run_service->evidence($run_id) : $this->cleanup_run_service->status($run_id);
36+
}
37+
}

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
use DataMachine\Cli\BaseCommand;
2020
use DataMachineCode\Cli\CliResponseRenderer;
2121
use DataMachineCode\Cli\CliRepeatableOptionParser;
22+
use DataMachineCode\Cleanup\CompositeCleanupRunEvidenceStore;
2223
use DataMachineCode\Cleanup\CleanupRunEvidenceStoreInterface;
23-
use DataMachineCode\Cleanup\DataMachineJobCleanupRunEvidenceStore;
2424
use DataMachineCode\Workspace\Workspace;
2525
use DataMachineCode\Workspace\WorktreeContextInjector;
2626
use DataMachineCode\Workspace\WorkspaceMutationLock;
@@ -766,16 +766,12 @@ public function cleanup( array $args, array $assoc_args ): void {
766766

767767
case 'status':
768768
case 'evidence':
769-
if ( ! $this->is_job_cleanup_run_id( (string) ( $args[1] ?? '' )) ) {
770-
$this->run_cleanup_control_ability($operation, (string) ( $args[1] ?? '' ), $assoc_args);
771-
return;
772-
}
773-
$job_id = $this->cleanup_run_job_id( (string) ( $args[1] ?? '' ));
774-
if ( $job_id <= 0 ) {
769+
$run_id = (string) ( $args[1] ?? '' );
770+
if ( '' === trim($run_id) ) {
775771
WP_CLI::error('Usage: wp datamachine-code workspace cleanup ' . $operation . ' <run-id>');
776772
return;
777773
}
778-
$this->render_cleanup_run_status($job_id, $assoc_args, 'evidence' === $operation);
774+
$this->render_cleanup_run_status($run_id, $assoc_args, 'evidence' === $operation);
779775
return;
780776

781777
case 'resume':
@@ -1244,8 +1240,8 @@ private function render_worktree_emergency_cleanup_result_from_ability( array|\W
12441240
$this->render_worktree_emergency_cleanup_result($result, $assoc_args);
12451241
}
12461242

1247-
private function render_cleanup_run_status( int $job_id, array $assoc_args, bool $evidence ): void {
1248-
$output = $this->cleanup_run_evidence_store()->read($this->cleanup_run_id($job_id), $evidence, ! empty($assoc_args['verbose']));
1243+
private function render_cleanup_run_status( string $run_id, array $assoc_args, bool $evidence ): void {
1244+
$output = $this->cleanup_run_evidence_store()->read($run_id, $evidence, ! empty($assoc_args['verbose']));
12491245
if ( $output instanceof \WP_Error ) {
12501246
WP_CLI::error($output->get_error_message());
12511247
return;
@@ -1256,7 +1252,7 @@ private function render_cleanup_run_status( int $job_id, array $assoc_args, bool
12561252

12571253
private function cleanup_run_evidence_store(): CleanupRunEvidenceStoreInterface {
12581254
if ( null === $this->cleanup_run_evidence_store ) {
1259-
$this->cleanup_run_evidence_store = new DataMachineJobCleanupRunEvidenceStore();
1255+
$this->cleanup_run_evidence_store = new CompositeCleanupRunEvidenceStore();
12601256
}
12611257

12621258
return $this->cleanup_run_evidence_store;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace {
6+
if ( ! defined('ABSPATH') ) {
7+
define('ABSPATH', __DIR__ . '/fixtures/');
8+
}
9+
10+
require_once dirname(__DIR__) . '/inc/Cleanup/CleanupRunEvidenceStoreInterface.php';
11+
require_once dirname(__DIR__) . '/inc/Cleanup/CompositeCleanupRunEvidenceStore.php';
12+
13+
use DataMachineCode\Cleanup\CleanupRunEvidenceStoreInterface;
14+
use DataMachineCode\Cleanup\CompositeCleanupRunEvidenceStore;
15+
16+
final class FakeCleanupRunEvidenceStore implements CleanupRunEvidenceStoreInterface {
17+
/** @var array<int,array<string,mixed>> */
18+
public array $calls = array();
19+
20+
public function __construct(
21+
private string $source
22+
) {}
23+
24+
public function read( string $run_id, bool $include_evidence = false, bool $include_details = false ): array|\WP_Error {
25+
$this->calls[] = compact('run_id', 'include_evidence', 'include_details');
26+
27+
return array(
28+
'source' => $this->source,
29+
'run_id' => $run_id,
30+
'include_evidence' => $include_evidence,
31+
'include_details' => $include_details,
32+
);
33+
}
34+
}
35+
36+
function cleanup_boundary_assert_same( mixed $expected, mixed $actual, string $message ): void {
37+
if ( $expected !== $actual ) {
38+
throw new RuntimeException(sprintf("%s\nExpected: %s\nActual: %s", $message, var_export($expected, true), var_export($actual, true)));
39+
}
40+
}
41+
42+
$workspace = new FakeCleanupRunEvidenceStore('workspace');
43+
$job = new FakeCleanupRunEvidenceStore('job');
44+
$store = new CompositeCleanupRunEvidenceStore($workspace, $job);
45+
46+
$result = $store->read('cleanup-run-20260504120000-abc123', true, true);
47+
cleanup_boundary_assert_same('workspace', $result['source'] ?? null, 'Timestamp cleanup run IDs should route to the DMC workspace evidence store.');
48+
cleanup_boundary_assert_same(true, $workspace->calls[0]['include_evidence'] ?? null, 'Workspace evidence flag should pass through.');
49+
50+
$result = $store->read('cleanup-run-123', false, true);
51+
cleanup_boundary_assert_same('job', $result['source'] ?? null, 'Job cleanup run IDs should route to the Data Machine job adapter.');
52+
cleanup_boundary_assert_same(true, $job->calls[0]['include_details'] ?? null, 'Job details flag should pass through.');
53+
54+
$result = $store->read('123');
55+
cleanup_boundary_assert_same('job', $result['source'] ?? null, 'Numeric cleanup run IDs should route to the Data Machine job adapter.');
56+
57+
echo "cleanup run evidence store boundary test passed.\n";
58+
}

0 commit comments

Comments
 (0)