Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Deterministic Trace Audit. | |
| * | |
| * Local rules-based checks over a Demand trace. Repeatable, side-effect-free, | |
| * no external API, no LLM, no state stored. Pure function over the trace array | |
| * as returned by SA_Orch_Rest::get_demand_trace(). | |
| * | |
| * This is the FLOOR of a four-layer review stack: | |
| * | |
| * (1) Trace data — what is in the database / Asterion ledger. | |
| * (2) Deterministic Trace Audit — this layer. Rules-only, repeatable, fast. | |
| * (3) Strategy LLM Review — FUTURE. Sits above this layer and compares | |
| * its reasoning against the deterministic audit. | |
| * The audit becomes a sanity-check / fallback | |
| * when the LLM layer is wired up. | |
| * (4) Human arbitration — final meaning. Always wins. | |
| * | |
| * The class name `SA_Orch_Reasoning` is preserved as the long-term contract; | |
| * v0.4 implements only the deterministic layer. When LLM review is added, it | |
| * will live alongside this audit (the audit will not be replaced). | |
| * | |
| * Rules implemented in v0.4 (minimal deterministic set): | |
| * - Closure authority invalid (acceptance_affirmed by non-stakeholder) | |
| * - projection_type ↔ plan_link count mismatch | |
| * - Affirmation without prior result_claimed | |
| * - prior_event_id chain integrity | |
| * - Multiple arbitrations on same Demand → scope drift | |
| * - projection_type other than scope-direct(-no-plan) → scope drift | |
| * - truth_class=derived on arbitration → scope drift | |
| * - dispute → affirm pattern → informational note | |
| * - acceptance_revoked present → informational note | |
| * - Needs Human Arbitration triggers (v0.4.1): | |
| * * closure_authority_valid = false | |
| * * truth_class=derived AND no acceptance events | |
| * * multiple arbitrations on same Demand | |
| * * acceptance_disputed is the latest event | |
| */ | |
| class SA_Orch_Reasoning { | |
| /** | |
| * Mode constant. v0.4 is deterministic-only; the LLM seam is future work. | |
| */ | |
| const MODE = 'deterministic'; | |
| /** | |
| * Evaluate a trace and return the deterministic audit report. | |
| * | |
| * Input shape: trace array as returned by SA_Orch_Rest::get_demand_trace(). | |
| * | |
| * Output: | |
| * [ | |
| * 'mode' => 'deterministic', | |
| * 'summary' => string, | |
| * 'inconsistencies' => string[], | |
| * 'scope_drift' => string[], | |
| * 'notes' => string[], | |
| * 'closure_confidence' => 'high' | 'medium' | 'low' | 'n/a', | |
| * 'closure_confidence_explanation' => string, | |
| * 'needs_human_arbitration' => bool, | |
| * 'human_arbitration_message' => string, // verbatim user-facing line | |
| * 'human_arbitration_triggers' => string[], // reasons firing the flag | |
| * ] | |
| */ | |
| public static function evaluate( array $trace ) { | |
| $arbs = isset( $trace['arbitrations'] ) ? (array) $trace['arbitrations'] : []; | |
| $links = isset( $trace['plan_links'] ) ? (array) $trace['plan_links'] : []; | |
| $events = isset( $trace['acceptance'] ) ? (array) $trace['acceptance'] : []; | |
| $basic = $trace['closure_state_basic'] ?? null; | |
| $authority = $trace['closure_authority_valid'] ?? null; | |
| // Event-type flags | |
| $has_claim = false; | |
| $has_dispute = false; | |
| $has_affirm = false; | |
| $has_revoke = false; | |
| foreach ( $events as $e ) { | |
| if ( $e->event_type === 'result_claimed' ) $has_claim = true; | |
| if ( $e->event_type === 'acceptance_disputed' ) $has_dispute = true; | |
| if ( $e->event_type === 'acceptance_affirmed' ) $has_affirm = true; | |
| if ( $e->event_type === 'acceptance_revoked' ) $has_revoke = true; | |
| } | |
| $inconsistencies = []; | |
| $drift = []; | |
| $notes = []; | |
| // --- Inconsistencies --- | |
| // Closure authority invalid: latest acceptance_affirmed by non-stakeholder | |
| if ( $authority === false ) { | |
| $latest_affirm = null; | |
| foreach ( array_reverse( $events ) as $e ) { | |
| if ( $e->event_type === 'acceptance_affirmed' ) { $latest_affirm = $e; break; } | |
| } | |
| if ( $latest_affirm ) { | |
| $inconsistencies[] = "Closure authority invalid: acceptance_affirmed event #{$latest_affirm->id} authored by actor_role='{$latest_affirm->actor_role}' (must be 'stakeholder' for authoritative closure)."; | |
| } | |
| } | |
| // projection_type vs plan_link count mismatch | |
| foreach ( $arbs as $arb ) { | |
| $arb_id = (int) $arb->id; | |
| $arb_links = count( array_filter( $links, function ( $l ) use ( $arb_id ) { | |
| return (int) $l->arbitration_id === $arb_id; | |
| } ) ); | |
| if ( $arb->projection_type === 'scope-direct-no-plan' && $arb_links > 0 ) { | |
| $inconsistencies[] = "Arbitration #{$arb->id} declares projection_type='scope-direct-no-plan' but has {$arb_links} plan link(s) — inconsistent."; | |
| } | |
| if ( in_array( $arb->projection_type, [ 'scope-direct', 'scope-decomposed', 'scope-merged' ], true ) && $arb_links === 0 ) { | |
| $inconsistencies[] = "Arbitration #{$arb->id} declares projection_type='{$arb->projection_type}' but has no plan links — projection promised a Plan but none recorded."; | |
| } | |
| } | |
| // Affirmation without claim (closure path is incomplete) | |
| if ( $has_affirm && ! $has_claim ) { | |
| $inconsistencies[] = "Acceptance was affirmed without any prior result_claimed event — closure path is incomplete."; | |
| } | |
| // prior_event_id chain integrity | |
| $event_ids = array_map( function ( $e ) { return (int) $e->id; }, $events ); | |
| foreach ( $events as $e ) { | |
| if ( $e->prior_event_id !== null && $e->prior_event_id !== '' ) { | |
| $prior = (int) $e->prior_event_id; | |
| if ( $prior > 0 && ! in_array( $prior, $event_ids, true ) ) { | |
| $inconsistencies[] = "Event #{$e->id} references prior_event_id={$prior}, which is not in this Demand's event chain."; | |
| } | |
| } | |
| } | |
| // --- Scope drift --- | |
| if ( count( $arbs ) > 1 ) { | |
| $drift[] = "Demand has " . count( $arbs ) . " arbitrations recorded — review for re-interpretation across arbitration moments."; | |
| } | |
| foreach ( $arbs as $arb ) { | |
| if ( ! in_array( $arb->projection_type, [ 'scope-direct', 'scope-direct-no-plan' ], true ) ) { | |
| $drift[] = "Arbitration #{$arb->id} uses projection_type='{$arb->projection_type}' — explicit scope transformation. Verify the Plan honors the original Demand intent."; | |
| } | |
| if ( $arb->truth_class === 'derived' ) { | |
| $drift[] = "Arbitration #{$arb->id} has truth_class='derived' — Demand was reconstructed from Plan-side evidence. Original scope is not directly verifiable."; | |
| } | |
| } | |
| // --- Notes (informational) --- | |
| if ( $has_dispute && $has_affirm ) { | |
| $notes[] = "This Demand experienced a dispute before its current affirmation. Review the dispute → affirmation transition for completeness of resolution."; | |
| } | |
| if ( $has_revoke ) { | |
| $notes[] = "An acceptance_revoked event is present — a previously-affirmed acceptance was withdrawn."; | |
| } | |
| if ( ! empty( $events ) && ! $has_claim && ! $has_dispute && ! $has_affirm && ! $has_revoke ) { | |
| $notes[] = "Acceptance events present but none of the standard types (claim/dispute/affirm/revoke) — review event_type values."; | |
| } | |
| // --- Summary + confidence --- | |
| $summary = self::build_summary( $arbs, $links, $events, $basic, $authority ); | |
| $confidence = self::compute_confidence( | |
| $basic, | |
| $authority, | |
| $has_dispute, | |
| $has_revoke, | |
| count( $inconsistencies ), | |
| count( $drift ), | |
| count( $events ) | |
| ); | |
| // --- Needs Human Arbitration (v0.4.1) --- | |
| $human = self::compute_human_arbitration( $arbs, $events, $authority ); | |
| return [ | |
| 'mode' => self::MODE, | |
| 'summary' => $summary, | |
| 'inconsistencies' => $inconsistencies, | |
| 'scope_drift' => $drift, | |
| 'notes' => $notes, | |
| 'closure_confidence' => $confidence['level'], | |
| 'closure_confidence_explanation' => $confidence['explanation'], | |
| 'needs_human_arbitration' => $human['required'], | |
| 'human_arbitration_message' => $human['message'], | |
| 'human_arbitration_triggers' => $human['triggers'], | |
| ]; | |
| } | |
| /** | |
| * Determine whether this trace requires human arbitration. | |
| * | |
| * Triggers (any one is sufficient): | |
| * 1. closure_authority_valid = false (latest acceptance_affirmed by non-stakeholder). | |
| * 2. arbitration carries truth_class='derived' AND no acceptance events exist | |
| * (Demand was reconstructed from Plan-side evidence and never confirmed). | |
| * 3. multiple arbitrations on the same Demand (re-interpretation requires | |
| * human reconciliation). | |
| * 4. latest event is acceptance_disputed (dispute is unresolved). | |
| * | |
| * The human arbitration verdict is the deterministic layer's escalation | |
| * signal. It is independent of closure_confidence — a Demand can be | |
| * "open" and still require human arbitration if the conditions warrant. | |
| */ | |
| private static function compute_human_arbitration( $arbs, $events, $authority ) { | |
| $triggers = []; | |
| // 1. closure_authority_valid = false | |
| if ( $authority === false ) { | |
| $triggers[] = "closure_authority_valid is false (acceptance_affirmed event was authored by a non-stakeholder actor)."; | |
| } | |
| // 2. derived arbitration without acceptance events | |
| $has_derived_arb = false; | |
| foreach ( $arbs as $a ) { | |
| if ( ( $a->truth_class ?? null ) === 'derived' ) { $has_derived_arb = true; break; } | |
| } | |
| if ( $has_derived_arb && count( $events ) === 0 ) { | |
| $triggers[] = "Demand is derived (truth_class='derived' on arbitration) and has no acceptance events — original stakeholder intent is unverified."; | |
| } | |
| // 3. multiple arbitrations on the same Demand | |
| if ( count( $arbs ) > 1 ) { | |
| $triggers[] = count( $arbs ) . " arbitrations exist on the same Demand — re-interpretation requires human reconciliation."; | |
| } | |
| // 4. latest event is acceptance_disputed | |
| if ( ! empty( $events ) ) { | |
| $latest = end( $events ); | |
| if ( ( $latest->event_type ?? null ) === 'acceptance_disputed' ) { | |
| $triggers[] = "Latest event is acceptance_disputed — dispute is unresolved and requires human follow-up before closure can be trusted."; | |
| } | |
| } | |
| return [ | |
| 'required' => ! empty( $triggers ), | |
| 'message' => 'This trace requires human arbitration before closure can be trusted.', | |
| 'triggers' => $triggers, | |
| ]; | |
| } | |
| private static function build_summary( $arbs, $links, $events, $basic, $authority ) { | |
| $arb_n = count( $arbs ); | |
| $link_n = count( $links ); | |
| $event_n = count( $events ); | |
| if ( $arb_n === 0 && $event_n === 0 ) { | |
| return "No arbitration and no acceptance events for this Demand. The Demand may be unrecognized by SA-Orchestration, or the surface_ref may be wrong."; | |
| } | |
| $bits = []; | |
| $bits[] = "{$arb_n} arbitration" . ( $arb_n === 1 ? '' : 's' ); | |
| $bits[] = "{$link_n} plan link" . ( $link_n === 1 ? '' : 's' ); | |
| $bits[] = "{$event_n} acceptance event" . ( $event_n === 1 ? '' : 's' ); | |
| $event_seq = ''; | |
| if ( $event_n > 0 ) { | |
| $sequence = []; | |
| foreach ( $events as $e ) $sequence[] = $e->event_type; | |
| $event_seq = ' Sequence: ' . implode( ' → ', $sequence ) . '.'; | |
| } | |
| $closure_phrase = ''; | |
| if ( $basic === 'closed' ) { | |
| if ( $authority === true ) { | |
| $closure_phrase = ' Currently closed (stakeholder-affirmed).'; | |
| } elseif ( $authority === false ) { | |
| $closure_phrase = ' Marked closed but closure authority is invalid (non-stakeholder affirmed).'; | |
| } else { | |
| $closure_phrase = ' Currently closed.'; | |
| } | |
| } elseif ( is_string( $basic ) && strpos( $basic, 'open:' ) === 0 ) { | |
| $closure_phrase = " Currently open ({$basic})."; | |
| } elseif ( $basic === 'no-events' ) { | |
| $closure_phrase = ' No acceptance events.'; | |
| } | |
| return implode( ', ', $bits ) . '.' . $event_seq . $closure_phrase; | |
| } | |
| private static function compute_confidence( $basic, $authority, $has_dispute, $has_revoke, $inc_count, $drift_count, $event_count ) { | |
| // No events — confidence not meaningful | |
| if ( $event_count === 0 || $basic === 'no-events' ) { | |
| return [ | |
| 'level' => 'n/a', | |
| 'explanation' => 'No acceptance events to evaluate; closure confidence is not yet meaningful.', | |
| ]; | |
| } | |
| // Open states | |
| if ( is_string( $basic ) && strpos( $basic, 'open:' ) === 0 ) { | |
| return [ | |
| 'level' => 'low', | |
| 'explanation' => "Demand is currently {$basic}; closure has not been claimed and confirmed.", | |
| ]; | |
| } | |
| // Closed but authority invalid | |
| if ( $basic === 'closed' && $authority === false ) { | |
| return [ | |
| 'level' => 'low', | |
| 'explanation' => 'Closed by non-stakeholder actor; closure_authority_valid=false. Treat as not authoritatively closed.', | |
| ]; | |
| } | |
| // Closed with stakeholder authority | |
| if ( $basic === 'closed' && $authority === true ) { | |
| if ( $inc_count > 0 ) { | |
| return [ | |
| 'level' => 'low', | |
| 'explanation' => "Closed by stakeholder, but {$inc_count} inconsistenc" . ( $inc_count === 1 ? 'y' : 'ies' ) . ' detected. Review before treating as final.', | |
| ]; | |
| } | |
| $reasons = []; | |
| if ( $has_dispute ) $reasons[] = 'a prior dispute was recorded'; | |
| if ( $has_revoke ) $reasons[] = 'an acceptance_revoked event exists'; | |
| if ( $drift_count ) $reasons[] = "{$drift_count} potential scope-drift indicator" . ( $drift_count === 1 ? '' : 's' ); | |
| if ( count( $reasons ) === 0 ) { | |
| return [ | |
| 'level' => 'high', | |
| 'explanation' => 'Closed by stakeholder with a clean event chain and no scope drift detected.', | |
| ]; | |
| } | |
| return [ | |
| 'level' => 'medium', | |
| 'explanation' => 'Closed by stakeholder, but ' . implode( '; ', $reasons ) . '. Review history before treating as routine.', | |
| ]; | |
| } | |
| // Fall-through (unrecognized basic state) | |
| return [ 'level' => 'low', 'explanation' => "Unrecognized closure_state_basic value: '{$basic}'." ]; | |
| } | |
| } | |