Spaces:
Sleeping
Sleeping
File size: 9,443 Bytes
f51c224 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | <?php
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 );
}
}
|