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}'." ];
    }
}