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:' | '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:' | 'role-mismatch:' | '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:'. */ 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 ); } }