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
8 changes: 4 additions & 4 deletions inc/Abilities/WorkspaceAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ private function registerAbilities(): void {
'summary' => array( 'type' => 'object' ),
'workspace_capacity' => array(
'type' => 'object',
'description' => 'One command-level, lossless workspace capacity envelope with a stable diagnostic ID, advisory fingerprint, evidence reference, and recovery actions.',
'description' => 'Compact command-level capacity status with typed triggers and an evidence reference. Full capacity evidence remains on workspace hygiene.',
),
'repos' => array(
'type' => 'array',
Expand Down Expand Up @@ -272,7 +272,7 @@ private function registerAbilities(): void {
'dirty' => array( 'type' => 'integer' ),
'workspace_capacity' => array(
'type' => 'object',
'description' => 'Complete workspace capacity envelope including byte and inode total/used/free values and percentages, probe, status, warnings, reasons, thresholds, and remediation commands.',
'description' => 'Compact workspace capacity status and evidence reference for routine reads. Blocking states retain complete evidence; hygiene owns the full dossier.',
),
'primary_freshness' => self::primaryFreshnessSchema(),
),
Expand Down Expand Up @@ -3511,7 +3511,7 @@ public static function listRepos( array $input ): array|\WP_Error {
if ( is_wp_error( $result ) ) {
return $result;
}
$result['workspace_capacity'] = WorktreeDiskBudget::inspect($workspace->get_path());
$result['workspace_capacity'] = WorktreeDiskBudget::for_routine_read( WorktreeDiskBudget::inspect( $workspace->get_path() ) );

return $result;
}
Expand Down Expand Up @@ -3593,7 +3593,7 @@ public static function showRepo( array $input ): array|\WP_Error {
$result = ( new RemoteWorkspaceBackend() )->show( $handle );
if ( ! self::shouldFallbackToLocalWorkspace( $result ) ) {
if ( is_array( $result ) && is_dir( $workspace->get_path() ) ) {
$result['workspace_capacity'] = WorktreeDiskBudget::inspect( $workspace->get_path() );
$result['workspace_capacity'] = WorktreeDiskBudget::for_routine_read( WorktreeDiskBudget::inspect( $workspace->get_path() ) );
}
return $result;
}
Expand Down
19 changes: 12 additions & 7 deletions inc/Cli/Commands/WorkspaceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ public function path( array $args, array $assoc_args ): void {
* : Include per-row Git remote, branch, and primary freshness probes.
*
* [--full]
* : Render full disk/inode capacity evidence and recovery details instead of one compact advisory.
* : Expand complete capacity evidence when the command already produced it. Routine list output stays compact; use workspace hygiene for the full dossier.
*
* [--format=<format>]
* : Output format.
Expand Down Expand Up @@ -840,7 +840,11 @@ public function list_repos( array $args, array $assoc_args ): void {
}

if ( 'json' === ( $assoc_args['format'] ?? 'table' ) ) {
$this->renderer()->json( ! empty( $assoc_args['envelope'] ) ? $result : (array) ( $result['repos'] ?? array() ) );
$payload = ! empty( $assoc_args['envelope'] ) ? $result : (array) ( $result['repos'] ?? array() );
if ( ! empty( $assoc_args['envelope'] ) && empty( $assoc_args['full'] ) ) {
$payload = WorkspaceCompactOutput::workspace_read_result($payload);
}
$this->renderer()->json($payload);
return;
}

Expand Down Expand Up @@ -1073,7 +1077,7 @@ private function render_workspace_list_summary( array $result, array $assoc_args

$format = (string) ( $assoc_args['format'] ?? 'table' );
if ( 'json' === $format ) {
$this->renderer()->json( $summary );
$this->renderer()->json(empty($assoc_args['full']) ? WorkspaceCompactOutput::workspace_read_result($summary) : $summary);
return;
}
if ( 'csv' === $format || 'yaml' === $format ) {
Expand Down Expand Up @@ -3243,7 +3247,7 @@ private function inventory_prune_missing( array $assoc_args ): void {
* : Repository directory name.
*
* [--full]
* : Render full disk/inode capacity evidence and recovery details instead of one compact advisory.
* : Expand complete capacity evidence when the command already produced it. Routine show output stays compact; use workspace hygiene for the full dossier.
*
* [--refresh]
* : Fetch the tracked remote under a bounded timeout before classifying primary freshness.
Expand Down Expand Up @@ -3296,7 +3300,7 @@ public function show( array $args, array $assoc_args ): void {
}

if ( 'json' === $format ) {
$this->renderer()->json( $result );
$this->renderer()->json(empty($assoc_args['full']) ? WorkspaceCompactOutput::workspace_read_result($result) : $result);
return;
}

Expand Down Expand Up @@ -3341,8 +3345,9 @@ private function render_workspace_capacity_advisory( array $capacity, bool $full
if ( array() === $capacity || array() === (array) ( $capacity['trigger_reasons'] ?? array() ) ) {
return;
}
$blocking = empty( $capacity['creation_allowed'] ) || ! empty( $capacity['force_override_required'] );
if ( ! $full && ! $blocking ) {
$blocking = empty( $capacity['creation_allowed'] ) || ! empty( $capacity['force_override_required'] );
$has_full_evidence = array_key_exists( 'filesystem_free_bytes', $capacity ) && array_key_exists( 'warn_free_bytes', $capacity );
if ( ! $blocking && ( ! $full || ! $has_full_evidence ) ) {
$advisory = \DataMachineCode\Workspace\WorktreeDiskBudget::format_advisory( $capacity );
if ( '' !== $advisory ) {
WP_CLI::warning( $advisory );
Expand Down
12 changes: 12 additions & 0 deletions inc/Cli/WorkspaceCompactOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public static function worktree_add_result( array $result ): array {
);
}

/** Project routine workspace reads without embedding the full capacity dossier. */
public static function workspace_read_result( array $result ): array {
if ( isset($result['workspace_capacity']) && is_array($result['workspace_capacity']) ) {
$result['workspace_capacity'] = self::worktree_capacity_summary($result['workspace_capacity']);
}

return $result;
}

private static function worktree_capacity_summary( array $capacity ): array {
if ( array() === $capacity ) {
return array();
Expand All @@ -74,7 +83,10 @@ static function ( string $code ): array {
'diagnostic_id' => $capacity['diagnostic_id'] ?? null,
'advisory_fingerprint' => $capacity['advisory_fingerprint'] ?? null,
'evidence_reference' => $capacity['evidence_reference'] ?? null,
'evidence_command' => $capacity['evidence_command'] ?? null,
'status' => $capacity['status'] ?? null,
'worktree_count' => $capacity['worktree_count'] ?? null,
'emergency_triggered' => isset($capacity['emergency_triggered']) ? (bool) $capacity['emergency_triggered'] : null,
'force_override' => isset( $capacity['force_override'] ) ? (bool) $capacity['force_override'] : null,
'creation_allowed' => array_key_exists('creation_allowed', $capacity) ? (bool) $capacity['creation_allowed'] : ( 'refused' !== ( $capacity['status'] ?? '' ) ),
'force_override_required' => array_key_exists('force_override_required', $capacity) ? (bool) $capacity['force_override_required'] : $has_blocking_trigger,
Expand Down
4 changes: 2 additions & 2 deletions inc/Workspace/WorkspaceRepositoryLifecycle.php
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ public function show_repo( string $handle, bool $refresh = false ): array|\WP_Er
'remote' => '' !== (string) ( $context_policy['repo'] ?? '' ) ? GitHubRemote::cloneUrl( (string) $context_policy['repo'] ) : null,
'commit' => null,
'dirty' => 0,
'workspace_capacity' => WorktreeDiskBudget::inspect($this->workspace_path),
'workspace_capacity' => WorktreeDiskBudget::for_routine_read(WorktreeDiskBudget::inspect($this->workspace_path)),
'workspace_policy' => WorkspaceAliasResolver::policy_attestation($handle),
);
}
Expand Down Expand Up @@ -1076,7 +1076,7 @@ public function show_repo( string $handle, bool $refresh = false ): array|\WP_Er
$remote_freshness_ms = (int) round(( microtime(true) - $remote_freshness_started ) * 1000);

$capacity_started = microtime(true);
$capacity = WorktreeDiskBudget::inspect($this->workspace_path);
$capacity = WorktreeDiskBudget::for_routine_read(WorktreeDiskBudget::inspect($this->workspace_path));
$capacity_ms = (int) round(( microtime(true) - $capacity_started ) * 1000);
$result = array(
'success' => true,
Expand Down
42 changes: 41 additions & 1 deletion inc/Workspace/WorktreeDiskBudget.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ public static function evaluate( array $metrics, array $thresholds = array(), bo
'creation_allowed' => ! $refused,
'admission_exception' => $admission_exception,
'warnings' => $warnings,
'emergency_triggered' => array() !== $trigger_reasons,
'emergency_triggered' => array() !== $has_blocking_trigger,
'trigger_reasons' => $trigger_reasons,
'typed_trigger_reasons' => $typed_trigger_reasons,
'cleanup_dry_run_command' => 'studio wp datamachine-code workspace worktree cleanup --dry-run',
Expand Down Expand Up @@ -400,6 +400,46 @@ public static function evaluate( array $metrics, array $thresholds = array(), bo
return $budget;
}

/**
* Project a compact status for routine list/show reads.
*
* Blocking and emergency payloads stay complete so immediate remediation remains available.
*
* @param array<string,mixed> $budget Full capacity evidence.
* @return array<string,mixed>
*/
public static function for_routine_read( array $budget ): array {
if ( ! empty( $budget['emergency_triggered'] ) || empty( $budget['creation_allowed'] ) || ! empty( $budget['force_override_required'] ) ) {
return $budget;
}

return self::compact_status( $budget );
}

/**
* Compact status and evidence reference for routine workspace reads.
*
* @param array<string,mixed> $budget Full or compact capacity evidence.
* @return array<string,mixed>
*/
public static function compact_status( array $budget ): array {
return array(
'workspace_path' => (string) ( $budget['workspace_path'] ?? '' ),
'worktree_count' => isset( $budget['worktree_count'] ) && is_numeric( $budget['worktree_count'] ) ? (int) $budget['worktree_count'] : 0,
'status' => (string) ( $budget['status'] ?? 'unknown' ),
'creation_allowed' => ! empty( $budget['creation_allowed'] ),
'force_override_required' => ! empty( $budget['force_override_required'] ),
'force_override_applied' => ! empty( $budget['force_override_applied'] ),
'emergency_triggered' => ! empty( $budget['emergency_triggered'] ),
'trigger_reasons' => array_values( array_map( 'strval', (array) ( $budget['trigger_reasons'] ?? array() ) ) ),
'typed_trigger_reasons' => array_values( (array) ( $budget['typed_trigger_reasons'] ?? array() ) ),
'diagnostic_id' => (string) ( $budget['diagnostic_id'] ?? self::DIAGNOSTIC_ID ),
'advisory_fingerprint' => isset( $budget['advisory_fingerprint'] ) ? (string) $budget['advisory_fingerprint'] : null,
'evidence_reference' => isset( $budget['evidence_reference'] ) ? (string) $budget['evidence_reference'] : null,
'evidence_command' => (string) ( $budget['evidence_command'] ?? 'studio wp datamachine-code workspace hygiene --format=json' ),
);
}

/** Build a state-level fingerprint so unchanged advisories can be suppressed safely. */
private static function advisory_fingerprint( array $budget ): string {
$thresholds = array();
Expand Down
16 changes: 16 additions & 0 deletions tests/workspace-capacity-advisory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,26 @@ function capacity_advisory_assert( bool $condition, string $message ): void {
$measurement_warning = WorktreeDiskBudget::evaluate(array_merge($metrics, array( 'free_bytes' => null )));

capacity_advisory_assert('workspace_capacity' === ($warning['diagnostic_id'] ?? null), 'Capacity evidence must expose a stable diagnostic ID.');
capacity_advisory_assert(false === ($warning['emergency_triggered'] ?? true), 'Healthy disk with advisory worktree count must not set emergency_triggered.');
capacity_advisory_assert('advisory' === ($warning['typed_trigger_reasons'][0]['severity'] ?? null), 'Worktree-count pressure must stay typed advisory.');
capacity_advisory_assert(true === ($blocked['emergency_triggered'] ?? false), 'Refusal floors must reserve emergency_triggered for blocking thresholds.');
capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) === ($same_warning['advisory_fingerprint'] ?? null), 'Unchanged warning state must retain a suppressible fingerprint.');
capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) !== ($new_threshold['advisory_fingerprint'] ?? null), 'A changed active threshold must produce a new fingerprint.');
capacity_advisory_assert(($warning['advisory_fingerprint'] ?? null) !== ($blocked['advisory_fingerprint'] ?? null), 'A blocking state change must produce a new fingerprint.');
capacity_advisory_assert(str_starts_with((string) ($warning['evidence_reference'] ?? ''), 'workspace_capacity@'), 'Capacity evidence must expose a compact reference.');

$healthy = WorktreeDiskBudget::evaluate(array_merge($metrics, array( 'worktree_count' => 12 )));
$advisory_read = WorktreeDiskBudget::for_routine_read($warning);
$healthy_read = WorktreeDiskBudget::for_routine_read($healthy);
$blocked_read = WorktreeDiskBudget::for_routine_read($blocked);
capacity_advisory_assert(false === ($healthy['emergency_triggered'] ?? true) && 'ok' === ($healthy['status'] ?? null), 'Healthy capacity must remain non-emergency.');
capacity_advisory_assert($advisory_read === WorktreeDiskBudget::compact_status($warning), 'Routine list/show must compact healthy-disk advisory count status.');
capacity_advisory_assert(! isset($advisory_read['filesystem_free_bytes']) && ! isset($advisory_read['cleanup_recommendations']) && ! isset($advisory_read['emergency_cleanup_command']) && ! isset($advisory_read['recovery_actions']), 'Routine advisory reads must omit the capacity dossier and cleanup plans.');
capacity_advisory_assert('studio wp datamachine-code workspace hygiene --format=json' === ($advisory_read['evidence_command'] ?? null), 'Routine reads must retain the hygiene evidence command.');
capacity_advisory_assert(false === ($advisory_read['emergency_triggered'] ?? true) && 'advisory' === ($advisory_read['typed_trigger_reasons'][0]['severity'] ?? null), 'Compact advisory count status must keep typed advisory severity.');
capacity_advisory_assert(! isset($healthy_read['filesystem_free_bytes']) && 'ok' === ($healthy_read['status'] ?? null), 'Healthy routine reads must stay compact.');
capacity_advisory_assert(isset($blocked_read['filesystem_free_bytes']) && isset($blocked_read['cleanup_recommendations']) && true === ($blocked_read['emergency_triggered'] ?? false), 'Blocking routine reads must retain complete emergency evidence.');
capacity_advisory_assert(! str_contains(WorktreeDiskBudget::format_advisory($advisory_read), 'workspace worktree prune'), 'Compact routine advisory must not embed cleanup plans.');
capacity_advisory_assert(4 === count((array) ($warning['recovery_actions'] ?? array())), 'Worktree-count warnings must add bounded cleanup and registration previews.');
capacity_advisory_assert('studio wp datamachine-code workspace worktree cleanup-eligible-drain --limit=25 --format=json' === ($warning['recovery_actions'][2]['command'] ?? null), 'Worktree-count warnings must point to bounded cleanup preview.');
capacity_advisory_assert('studio wp datamachine-code workspace worktree prune --dry-run --format=json' === ($warning['recovery_actions'][3]['command'] ?? null), 'Worktree-count warnings must point to the non-mutating prune preview.');
Expand Down
Loading
Loading