RcmEmailAutomation / sa-orchestration /includes /class-sa-arbitration.php
NujacsaintS's picture
reseed
f51c224
Raw
History Blame Contribute Delete
7.58 kB
<?php
defined( 'ABSPATH' ) || exit;
class SA_Orch_Arbitration {
/**
* Write an arbitration record + optional plan_link rows.
*
* Required:
* - demand_surface (string) e.g. 'fluent-support'
* - demand_ref (string) e.g. 'ticket#12'
* - rationale (non-empty string)
* - projection_type (string) e.g. 'scope-direct-no-plan'
*
* Optional:
* - actor_user_id (int; default current user)
* - correlation_id (string; default UUID v4)
* - causation_id (string|null)
* - truth_class (string; default 'canonical')
* - plan_refs (array of [ surface, ref, link_kind? ])
*
* Returns array on success, WP_Error on failure.
*/
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'] ) : '';
$rationale = isset( $args['rationale'] ) ? trim( (string) $args['rationale'] ) : '';
$projection_type = isset( $args['projection_type'] ) ? trim( (string) $args['projection_type'] ) : '';
if ( $demand_surface === '' || $demand_ref === '' ) {
return new WP_Error( 'missing_demand', 'demand_surface and demand_ref are required' );
}
if ( $rationale === '' ) {
return new WP_Error( 'missing_rationale', 'rationale is required (mandatory per SARA discipline)' );
}
if ( $projection_type === '' ) {
return new WP_Error( 'missing_projection_type', 'projection_type is required' );
}
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 surface 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}" );
}
$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; }
$correlation_id = isset( $args['correlation_id'] ) && $args['correlation_id'] !== ''
? (string) $args['correlation_id']
: wp_generate_uuid4();
$causation_id = isset( $args['causation_id'] ) && $args['causation_id'] !== ''
? (string) $args['causation_id']
: null;
$truth_class = isset( $args['truth_class'] ) && $args['truth_class'] !== ''
? (string) $args['truth_class']
: 'canonical';
$plan_refs = isset( $args['plan_refs'] ) && is_array( $args['plan_refs'] ) ? $args['plan_refs'] : [];
// Validate plan_refs each
foreach ( $plan_refs as $i => $pr ) {
$ps = isset( $pr['surface'] ) ? trim( (string) $pr['surface'] ) : '';
$prr = isset( $pr['ref'] ) ? trim( (string) $pr['ref'] ) : '';
if ( $ps === '' || $prr === '' ) {
return new WP_Error( 'invalid_plan_ref', "plan_refs[$i] missing surface or ref" );
}
if ( ! in_array( $ps, SA_Orch_Bridge_Fluent::known_surfaces(), true ) ) {
return new WP_Error( 'unknown_surface', "plan_refs[$i].surface '$ps' is not known" );
}
if ( ! SA_Orch_Bridge_Fluent::validate( $ps, $prr ) ) {
return new WP_Error( 'invalid_plan_ref', "plan_refs[$i] does not resolve: $ps/$prr" );
}
}
$created_at = current_time( 'mysql', true );
$hash = self::compute_hash( $demand_surface, $demand_ref, $created_at, $actor_user_id, $correlation_id );
// Asterion-first: ledger entry written before any DB row.
$frontmatter = [
'kind' => 'arbitration',
'hash' => $hash,
'demand_surface' => $demand_surface,
'demand_ref' => $demand_ref,
'projection_type' => $projection_type,
'actor_user_id' => $actor_user_id,
'correlation_id' => $correlation_id,
'causation_id' => $causation_id,
'truth_class' => $truth_class,
'plan_refs' => array_map( function ( $pr ) {
return ( $pr['surface'] ?? '' ) . ':' . ( $pr['ref'] ?? '' );
}, $plan_refs ),
'created_at' => $created_at,
];
$body = "## Rationale\n\n" . $rationale . "\n";
$rel_path = SA_Orch_Asterion::write( 'arbitration', $hash, $frontmatter, $body );
if ( is_wp_error( $rel_path ) ) {
return $rel_path;
}
// Now insert the runtime cache row.
$table = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'arbitration';
$ok = $wpdb->insert( $table, [
'hash' => $hash,
'demand_surface' => $demand_surface,
'demand_ref' => $demand_ref,
'rationale' => $rationale,
'projection_type' => $projection_type,
'actor_user_id' => $actor_user_id,
'correlation_id' => $correlation_id,
'causation_id' => $causation_id,
'truth_class' => $truth_class,
'asterion_path' => $rel_path,
'created_at' => $created_at,
] );
if ( $ok === false ) {
return new WP_Error( 'db_insert_failed', $wpdb->last_error, [ 'asterion_path' => $rel_path ] );
}
$arbitration_id = (int) $wpdb->insert_id;
$link_ids = [];
foreach ( $plan_refs as $pr ) {
$link_id = SA_Orch_Link::write( [
'arbitration_id' => $arbitration_id,
'plan_surface' => $pr['surface'],
'plan_ref' => $pr['ref'],
'link_kind' => $pr['link_kind'] ?? 'derived',
] );
if ( is_wp_error( $link_id ) ) {
return $link_id;
}
$link_ids[] = $link_id;
}
// v0.6.0: notify projection / side-effect listeners. Decoupled — listeners
// (e.g. SA_Orch_Projection_Fluent_Support) react via WordPress action hooks.
do_action( 'sa_orch_arbitration_written', [
'id' => $arbitration_id,
'hash' => $hash,
'demand_surface' => $demand_surface,
'demand_ref' => $demand_ref,
'projection_type' => $projection_type,
'truth_class' => $truth_class,
'plan_link_ids' => $link_ids,
'correlation_id' => $correlation_id,
'asterion_path' => $rel_path,
] );
return [
'id' => $arbitration_id,
'hash' => $hash,
'asterion_path' => $rel_path,
'plan_link_ids' => $link_ids,
'correlation_id' => $correlation_id,
];
}
private static function compute_hash( $demand_surface, $demand_ref, $created_at, $actor_user_id, $correlation_id ) {
$material = implode( '|', [
$demand_surface,
$demand_ref,
$created_at,
(int) $actor_user_id,
$correlation_id,
] );
return substr( hash( 'sha256', $material ), 0, 32 );
}
}