Spaces:
Sleeping
Sleeping
File size: 15,724 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 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | <?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}'." ];
}
}
|