Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Append-only by design. Class exposes only write() and read methods. | |
| * No update(), no delete(). | |
| */ | |
| class SA_Orch_Acceptance { | |
| const ALLOWED_EVENT_TYPES = [ | |
| 'result_claimed', | |
| 'acceptance_disputed', | |
| 'acceptance_affirmed', | |
| 'acceptance_revoked', | |
| ]; | |
| const ALLOWED_ACTOR_ROLES = [ | |
| 'agent', | |
| 'stakeholder', | |
| 'system', | |
| 'inferred', | |
| ]; | |
| public static function write( array $args ) { | |
| global $wpdb; | |
| $demand_surface = isset( $args['demand_surface'] ) ? trim( (string) $args['demand_surface'] ) : ''; | |
| $demand_ref = isset( $args['demand_ref'] ) ? trim( (string) $args['demand_ref'] ) : ''; | |
| $event_type = isset( $args['event_type'] ) ? trim( (string) $args['event_type'] ) : ''; | |
| $actor_role = isset( $args['actor_role'] ) ? trim( (string) $args['actor_role'] ) : ''; | |
| if ( $demand_surface === '' || $demand_ref === '' ) { | |
| return new WP_Error( 'missing_demand', 'demand_surface and demand_ref are required' ); | |
| } | |
| if ( ! in_array( $event_type, self::ALLOWED_EVENT_TYPES, true ) ) { | |
| return new WP_Error( 'invalid_event_type', 'event_type must be one of: ' . implode( ', ', self::ALLOWED_EVENT_TYPES ) ); | |
| } | |
| if ( ! in_array( $actor_role, self::ALLOWED_ACTOR_ROLES, true ) ) { | |
| return new WP_Error( 'invalid_actor_role', 'actor_role must be one of: ' . implode( ', ', self::ALLOWED_ACTOR_ROLES ) ); | |
| } | |
| if ( ! in_array( $demand_surface, SA_Orch_Bridge_Fluent::known_surfaces(), true ) ) { | |
| return new WP_Error( 'unknown_surface', "demand_surface '$demand_surface' is not in known vocabulary" ); | |
| } | |
| if ( ! SA_Orch_Bridge_Fluent::validate( $demand_surface, $demand_ref ) ) { | |
| return new WP_Error( 'invalid_demand_ref', "demand_ref does not resolve: {$demand_surface}/{$demand_ref}" ); | |
| } | |
| $prior_event_id = isset( $args['prior_event_id'] ) ? (int) $args['prior_event_id'] : null; | |
| if ( $prior_event_id ) { | |
| $exists = $wpdb->get_var( $wpdb->prepare( | |
| "SELECT id FROM {$wpdb->prefix}" . SA_ORCH_DB_PREFIX . "acceptance_event WHERE id = %d", | |
| $prior_event_id | |
| ) ); | |
| if ( ! $exists ) { | |
| return new WP_Error( 'invalid_prior_event', "prior_event_id $prior_event_id not found" ); | |
| } | |
| } | |
| $actor_user_id = isset( $args['actor_user_id'] ) ? (int) $args['actor_user_id'] : get_current_user_id(); | |
| if ( $actor_user_id === 0 ) { $actor_user_id = null; } | |
| $details = isset( $args['details'] ) ? (string) $args['details'] : null; | |
| $truth_class = isset( $args['truth_class'] ) && $args['truth_class'] !== '' | |
| ? (string) $args['truth_class'] | |
| : 'canonical'; | |
| $observed_at = isset( $args['observed_at'] ) && $args['observed_at'] !== '' | |
| ? (string) $args['observed_at'] | |
| : current_time( 'mysql', true ); | |
| $recorded_at = current_time( 'mysql', true ); | |
| $hash = self::compute_hash( $demand_surface, $demand_ref, $event_type, $observed_at, $prior_event_id ); | |
| $frontmatter = [ | |
| 'kind' => 'acceptance_event', | |
| 'hash' => $hash, | |
| 'demand_surface' => $demand_surface, | |
| 'demand_ref' => $demand_ref, | |
| 'event_type' => $event_type, | |
| 'prior_event_id' => $prior_event_id, | |
| 'actor_user_id' => $actor_user_id, | |
| 'actor_role' => $actor_role, | |
| 'truth_class' => $truth_class, | |
| 'observed_at' => $observed_at, | |
| 'recorded_at' => $recorded_at, | |
| ]; | |
| $body = "## Details\n\n" . ( $details !== null && $details !== '' ? $details : '(none)' ) . "\n"; | |
| $rel_path = SA_Orch_Asterion::write( 'acceptance_event', $hash, $frontmatter, $body ); | |
| if ( is_wp_error( $rel_path ) ) { | |
| return $rel_path; | |
| } | |
| $table = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event'; | |
| $ok = $wpdb->insert( $table, [ | |
| 'hash' => $hash, | |
| 'demand_surface' => $demand_surface, | |
| 'demand_ref' => $demand_ref, | |
| 'event_type' => $event_type, | |
| 'prior_event_id' => $prior_event_id, | |
| 'actor_user_id' => $actor_user_id, | |
| 'actor_role' => $actor_role, | |
| 'details' => $details, | |
| 'truth_class' => $truth_class, | |
| 'asterion_path' => $rel_path, | |
| 'observed_at' => $observed_at, | |
| 'recorded_at' => $recorded_at, | |
| ] ); | |
| if ( $ok === false ) { | |
| return new WP_Error( 'db_insert_failed', $wpdb->last_error, [ 'asterion_path' => $rel_path ] ); | |
| } | |
| $event_id = (int) $wpdb->insert_id; | |
| // v0.6.0: notify projection / side-effect listeners. | |
| do_action( 'sa_orch_acceptance_event_written', [ | |
| 'id' => $event_id, | |
| 'hash' => $hash, | |
| 'demand_surface' => $demand_surface, | |
| 'demand_ref' => $demand_ref, | |
| 'event_type' => $event_type, | |
| 'actor_role' => $actor_role, | |
| 'actor_user_id' => $actor_user_id, | |
| 'truth_class' => $truth_class, | |
| 'asterion_path' => $rel_path, | |
| ] ); | |
| return [ | |
| 'id' => $event_id, | |
| 'hash' => $hash, | |
| 'asterion_path' => $rel_path, | |
| ]; | |
| } | |
| /** | |
| * Latest event per Demand. Useful for closure-rule queries. | |
| * Returns object|null. | |
| */ | |
| public static function latest_for_demand( $demand_surface, $demand_ref ) { | |
| global $wpdb; | |
| $table = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event'; | |
| return $wpdb->get_row( $wpdb->prepare( | |
| "SELECT * FROM {$table} | |
| WHERE demand_surface = %s AND demand_ref = %s | |
| ORDER BY observed_at DESC, id DESC LIMIT 1", | |
| $demand_surface, | |
| $demand_ref | |
| ) ); | |
| } | |
| /** | |
| * Basic closure state. | |
| * Returns: 'no-events' | 'open:<event_type>' | 'closed' | |
| * | |
| * 'closed' iff the latest event is acceptance_affirmed (regardless of who affirmed it). | |
| * Existing behavior since v0.1; preserved. | |
| */ | |
| public static function basic_closure_state( $demand_surface, $demand_ref ) { | |
| $latest = self::latest_for_demand( $demand_surface, $demand_ref ); | |
| if ( ! $latest ) return 'no-events'; | |
| if ( $latest->event_type !== 'acceptance_affirmed' ) return 'open:' . $latest->event_type; | |
| return 'closed'; | |
| } | |
| /** | |
| * Role-aware closure state. | |
| * Returns: 'no-events' | 'open:<event_type>' | 'role-mismatch:<actor_role>' | 'closed' | |
| * | |
| * 'closed' iff the latest event is acceptance_affirmed AND its actor_role is 'stakeholder'. | |
| * Otherwise reports the specific reason closure is not authoritative. | |
| * | |
| * Per SIGNAL_FLOW.md closure rule: only stakeholder affirmation closes a Demand. | |
| * Agent-self-affirmation, system-affirmation, etc. write successfully (append-only is preserved) | |
| * but do not constitute closure under this stricter rule. | |
| */ | |
| public static function role_aware_closure_state( $demand_surface, $demand_ref ) { | |
| $latest = self::latest_for_demand( $demand_surface, $demand_ref ); | |
| if ( ! $latest ) return 'no-events'; | |
| if ( $latest->event_type !== 'acceptance_affirmed' ) return 'open:' . $latest->event_type; | |
| if ( $latest->actor_role !== 'stakeholder' ) return 'role-mismatch:' . $latest->actor_role; | |
| return 'closed'; | |
| } | |
| /** | |
| * Closure authority validity. | |
| * | |
| * Returns: | |
| * null — no acceptance_affirmed has been recorded yet (latest is not affirmed, or no events at all). | |
| * Authority is not yet relevant; closure is not being claimed. | |
| * true — latest acceptance_affirmed event has actor_role = 'stakeholder'. Closure is authoritative. | |
| * false — latest acceptance_affirmed event has actor_role != 'stakeholder'. Closure is claimed | |
| * but the actor lacked closure authority. Recording is preserved (append-only); the | |
| * closure_state_role_aware field will read 'role-mismatch:<role>'. | |
| */ | |
| public static function closure_authority_valid( $demand_surface, $demand_ref ) { | |
| $latest = self::latest_for_demand( $demand_surface, $demand_ref ); | |
| if ( ! $latest ) return null; | |
| if ( $latest->event_type !== 'acceptance_affirmed' ) return null; | |
| return $latest->actor_role === 'stakeholder'; | |
| } | |
| private static function compute_hash( $demand_surface, $demand_ref, $event_type, $observed_at, $prior_event_id ) { | |
| // wp_rand suffix prevents collision when same demand+event_type+observed_at recorded twice | |
| $material = implode( '|', [ | |
| $demand_surface, | |
| $demand_ref, | |
| $event_type, | |
| $observed_at, | |
| (int) $prior_event_id, | |
| wp_rand(), | |
| ] ); | |
| return substr( hash( 'sha256', $material ), 0, 32 ); | |
| } | |
| } | |