RcmEmailAutomation / sa-orchestration /includes /class-sa-reasoning.php
NujacsaintS's picture
reseed
f51c224
Raw
History Blame Contribute Delete
15.7 kB
<?php
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}'." ];
}
}