result_meta_provider = $result_meta_provider; } /** * Execute a command batch in dependency-ready order and return input order. * * @param array $commands Raw command list. * @param callable $execute_one One-command executor. * @param array $initial_results Already-terminal command results. * @return array */ public function run( array $commands, callable $execute_one, array $initial_results = array() ): array { $items = array(); $command_ids = array(); $command_id_counts = array(); $duplicate_ids = array(); $initial_by_command = array(); foreach ( $initial_results as $result ) { if ( is_array( $result ) && isset( $result['command_id'] ) && is_scalar( $result['command_id'] ) && '' !== (string) $result['command_id'] ) { $initial_by_command[ (string) $result['command_id'] ] = $result; } } foreach ( $commands as $index => $command ) { $command = is_array( $command ) ? $command : array(); $base = $this->command_result_base( $command ); $items[ $index ] = array( 'command' => $command, 'base' => $base, ); $command_id_counts[ $base['command_id'] ] = ( $command_id_counts[ $base['command_id'] ] ?? 0 ) + 1; } foreach ( $command_id_counts as $command_id => $count ) { if ( $count > 1 ) { $duplicate_ids[ $command_id ] = true; } } foreach ( $items as $index => $item ) { $command_id = $item['base']['command_id']; if ( ! isset( $duplicate_ids[ $command_id ] ) ) { $command_ids[ $command_id ] = $index; } } $remaining = array_keys( $items ); $results_by_index = array(); $results_by_command_id = array(); $has_failed_command = false; foreach ( $initial_by_command as $command_id => $result ) { if ( isset( $duplicate_ids[ $command_id ] ) ) { continue; } $results_by_command_id[ $command_id ] = $result; if ( ! isset( $command_ids[ $command_id ] ) && 'succeeded' !== ( $result['status'] ?? '' ) ) { $has_failed_command = true; } } while ( $remaining ) { $made_progress = false; foreach ( $remaining as $offset => $index ) { $command = $items[ $index ]['command']; $base = $items[ $index ]['base']; if ( isset( $duplicate_ids[ $base['command_id'] ] ) ) { $result = $this->invalid_command_result( $base, __( 'command_id values must be unique within a command batch.', 'actionpanel-ai' ) ); } elseif ( isset( $initial_by_command[ $base['command_id'] ] ) ) { $result = $initial_by_command[ $base['command_id'] ]; } elseif ( $this->has_pending_dependencies( $command, $command_ids, $results_by_index, $results_by_command_id ) ) { continue; } elseif ( $this->should_skip_command( $command, $results_by_command_id, $has_failed_command ) ) { $result = $this->skipped_command_result( $base ); } else { $resolved = $this->resolve_command_references( $command, $results_by_command_id ); $result = isset( $resolved['error'] ) ? $this->invalid_reference_result( $base, $resolved['error'] ) : $execute_one( $resolved['command'] ); } $results_by_index[ $index ] = $result; $results_by_command_id[ $result['command_id'] ] = $result; $has_failed_command = $has_failed_command || 'succeeded' !== ( $result['status'] ?? '' ); unset( $remaining[ $offset ] ); $made_progress = true; } if ( ! $made_progress ) { foreach ( $remaining as $index ) { $result = $this->skipped_command_result( $items[ $index ]['base'] ); $results_by_index[ $index ] = $result; $results_by_command_id[ $result['command_id'] ] = $result; } $remaining = array(); } } ksort( $results_by_index ); return array_values( $results_by_index ); } private function has_pending_dependencies( array $command, array $command_ids, array $results_by_index, array $results_by_command_id ): bool { if ( 'validate' === $this->scalar_command_field( $command, 'phase' ) ) { return false; } foreach ( $this->command_dependencies( $command ) as $dependency_id ) { if ( isset( $results_by_command_id[ $dependency_id ] ) ) { continue; } if ( isset( $command_ids[ $dependency_id ] ) && ! array_key_exists( $command_ids[ $dependency_id ], $results_by_index ) ) { return true; } } return false; } private function should_skip_command( array $command, array $results_by_command_id, bool $has_failed_command ): bool { if ( 'validate' === $this->scalar_command_field( $command, 'phase' ) ) { return false; } foreach ( $this->command_dependencies( $command ) as $dependency_id ) { if ( ! isset( $results_by_command_id[ $dependency_id ] ) || 'succeeded' !== ( $results_by_command_id[ $dependency_id ]['status'] ?? '' ) ) { return true; } } return $has_failed_command && true !== ( $command['continue_after_failure'] ?? false ); } private function command_dependencies( array $command ): array { $depends_on = $command['depends_on'] ?? array(); if ( ! is_array( $depends_on ) ) { return array(); } return array_values( array_filter( $depends_on, static fn( $dependency_id ): bool => is_string( $dependency_id ) && '' !== $dependency_id ) ); } /** * Resolve `$from` references in a command input against prior terminal results. * * @return array `['command' => array]` or `['error' => array]`. */ private function resolve_command_references( array $command, array $results_by_command_id ): array { if ( ! isset( $command['input'] ) || ! is_array( $command['input'] ) || 'validate' === $this->scalar_command_field( $command, 'phase' ) ) { return array( 'command' => $command ); } $error = null; $command['input'] = $this->resolve_value_references( $command['input'], $results_by_command_id, $error ); if ( null !== $error ) { return array( 'error' => $error ); } return array( 'command' => $command ); } /** * Resolve references inside a nested value. * * @param mixed $value Command input value. * @param array $results_by_command_id Prior command results. * @param array|null $error First resolution error. * @return mixed */ private function resolve_value_references( $value, array $results_by_command_id, ?array &$error ) { if ( null !== $error || ! is_array( $value ) ) { return $value; } if ( array_key_exists( '$from', $value ) ) { return $this->resolve_reference_object( $value, $results_by_command_id, $error ); } foreach ( $value as $key => $child ) { $value[ $key ] = $this->resolve_value_references( $child, $results_by_command_id, $error ); if ( null !== $error ) { return $value; } } return $value; } /** * Resolve one exact reference object. * * @param array $reference Reference object. * @param array $results_by_command_id Prior command results. * @param array|null $error First resolution error. * @return mixed */ private function resolve_reference_object( array $reference, array $results_by_command_id, ?array &$error ) { if ( 2 !== count( $reference ) || ! isset( $reference['$from'] ) || ! is_string( $reference['$from'] ) || '' === $reference['$from'] || ! isset( $reference['path'] ) || ! $this->is_reference_path( $reference['path'] ) ) { $error = array( 'code' => 'invalid_reference', 'message' => __( 'Reference objects must contain a string $from and a string/integer path.', 'actionpanel-ai' ), ); return $reference; } $source_id = $reference['$from']; if ( ! isset( $results_by_command_id[ $source_id ] ) || 'succeeded' !== ( $results_by_command_id[ $source_id ]['status'] ?? '' ) ) { $error = array( 'code' => 'invalid_reference', 'message' => __( 'Reference source did not complete successfully before this command.', 'actionpanel-ai' ), ); return $reference; } $node = $results_by_command_id[ $source_id ]; foreach ( $reference['path'] as $segment ) { $key = is_int( $segment ) ? $segment : (string) $segment; if ( ! is_array( $node ) || ! array_key_exists( $key, $node ) ) { $error = array( 'code' => 'invalid_reference', 'message' => __( 'Reference path did not resolve in the source command result.', 'actionpanel-ai' ), ); return $reference; } $node = $node[ $key ]; } return $node; } /** * @param mixed $path Candidate path. */ private function is_reference_path( $path ): bool { if ( ! is_array( $path ) ) { return false; } foreach ( $path as $segment ) { if ( ! is_string( $segment ) && ! is_int( $segment ) ) { return false; } } return true; } /** * Build result fields shared by every terminal command result. */ private function command_result_base( array $command ): array { $command_id = $this->scalar_command_field( $command, 'command_id' ); return array( 'command_id' => '' === $command_id ? 'cmd_missing_command_id' : $command_id, 'capability' => $this->scalar_command_field( $command, 'capability' ), ); } /** * Build an invalid-reference result. */ private function invalid_reference_result( array $base, array $error ): array { $result = array( 'command_id' => $base['command_id'], 'capability' => $base['capability'], 'status' => 'failed', 'error' => $error, 'artifacts' => array(), ); return $this->with_meta( $result ); } /** * Build an invalid-command result for batch-level command envelope failures. */ private function invalid_command_result( array $base, string $message ): array { $result = array( 'command_id' => $base['command_id'], 'capability' => $base['capability'], 'status' => 'failed', 'error' => array( 'code' => 'invalid_command', 'message' => $message, ), 'artifacts' => array(), ); return $this->with_meta( $result ); } /** * Build a skipped terminal result for dependency or previous-command failure. */ private function skipped_command_result( array $base ): array { $result = array( 'command_id' => $base['command_id'], 'capability' => $base['capability'], 'status' => 'skipped', 'data' => array( 'skipped' => true ), 'artifacts' => array(), ); return $this->with_meta( $result ); } /** * Attach runtime-provided metadata to batch-owned terminal results. */ private function with_meta( array $result ): array { if ( null === $this->result_meta_provider ) { return $result; } $meta = call_user_func( $this->result_meta_provider ); if ( is_array( $meta ) && ! empty( $meta ) ) { $result['meta'] = $meta; } return $result; } /** * Return a scalar command field as a string. */ private function scalar_command_field( array $command, string $field ): string { return isset( $command[ $field ] ) && is_scalar( $command[ $field ] ) ? (string) $command[ $field ] : ''; } }