NujacsaintS's picture
reseed
f51c224
Raw
History Blame Contribute Delete
47.7 kB
<?php
defined( 'ABSPATH' ) || exit;
/**
* Read-only admin visibility layer.
*
* Top-level menu "SA-Orchestration" with three subpages:
* - Demand Trace (form + full trace including v0.2 closure fields)
* - Recent Arbitrations (last 20 arbitrations)
* - Acceptance Events (last 20 acceptance events)
*
* No editing. No new tables. Reuses SA_Orch_Rest::get_demand_trace() and
* SA_Orch_Acceptance helpers — no logic duplication.
*/
class SA_Orch_Admin {
const PARENT_SLUG = 'sa-orchestration';
const ARB_SLUG = 'sa-orchestration-arbitrations';
const EVENT_SLUG = 'sa-orchestration-events';
const LLM_SLUG = 'sa-orchestration-llm';
const CAP = 'manage_options';
public static function register_menu() {
add_menu_page(
'SA-Orchestration',
'SA-Orchestration',
self::CAP,
self::PARENT_SLUG,
[ __CLASS__, 'render_trace' ],
'dashicons-networking',
80
);
// First submenu — reusing the parent slug renames the auto-generated
// submenu from "SA-Orchestration" to "Demand Trace".
add_submenu_page(
self::PARENT_SLUG,
'Demand Trace',
'Demand Trace',
self::CAP,
self::PARENT_SLUG,
[ __CLASS__, 'render_trace' ]
);
add_submenu_page(
self::PARENT_SLUG,
'Recent Arbitrations',
'Recent Arbitrations',
self::CAP,
self::ARB_SLUG,
[ __CLASS__, 'render_arbitrations' ]
);
add_submenu_page(
self::PARENT_SLUG,
'Acceptance Events',
'Acceptance Events',
self::CAP,
self::EVENT_SLUG,
[ __CLASS__, 'render_events' ]
);
add_submenu_page(
self::PARENT_SLUG,
'Language Model Providers',
'Language Model Providers',
self::CAP,
self::LLM_SLUG,
[ __CLASS__, 'render_llm_settings' ]
);
}
/* ---------- Demand Trace ---------- */
public static function render_trace() {
if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' );
$surface = isset( $_GET['demand_surface'] ) ? sanitize_text_field( wp_unslash( $_GET['demand_surface'] ) ) : '';
$ref = isset( $_GET['demand_ref'] ) ? sanitize_text_field( wp_unslash( $_GET['demand_ref'] ) ) : '';
echo '<div class="wrap">';
echo '<h1>Demand Trace</h1>';
echo '<p>Walk a Demand through arbitration → plan_links → acceptance chain → closure verdicts.</p>';
// Picker form (replaces v0.6.x free-form text inputs).
self::render_picker_form( $surface, $ref );
if ( $surface !== '' && $ref !== '' ) {
// Reuse the REST handler. Bypasses URI routing (avoids URL-encoding fragility around `#`).
$req = new WP_REST_Request( 'GET' );
$req->set_url_params( [ 'surface' => $surface, 'ref' => $ref ] );
$resp = SA_Orch_Rest::get_demand_trace( $req );
$data = $resp->get_data();
self::render_trace_results( $data );
}
echo '</div>';
}
/* ---------- Demand Trace picker (v0.6.2) ---------- */
/**
* Demand picker:
* - surface dropdown (closed enumeration of 4)
* - quick-select strip (SA-ORCH-TEST fixtures pinned + last-traced)
* - searchable browser of candidates for the selected surface
* - manual ref entry as fallback (validated against known per-surface patterns)
*
* No new schema; no new endpoints. All candidate data is rendered inline
* as JSON for client-side filtering.
*/
private static function render_picker_form( $surface, $ref ) {
$candidates = [
'fluent-support' => self::candidates_fluent_support(),
'fluent-boards' => self::candidates_fluent_boards(),
'fluentcrm' => self::candidates_fluentcrm(),
'derived' => self::candidates_derived(),
];
$quick = self::quick_select_demands( 10 );
$surface_labels = [
'fluent-support' => 'fluent-support — Fluent Support tickets',
'fluent-boards' => 'fluent-boards — Fluent Boards tasks',
'fluentcrm' => 'fluentcrm — FluentCRM subscribers',
'derived' => 'derived — rowless / inferred demands',
];
// Per-surface validation regex (mirrors SA_Orch_Bridge_Fluent::validate / resolve).
$patterns = [
'fluent-support' => '^ticket#\\d+$',
'fluent-boards' => '^task#\\d+$',
'fluentcrm' => '^subscriber#\\d+$',
'derived' => '^derived#[A-Za-z0-9_\\-]+$',
];
echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" id="sa-orch-trace-form">';
echo '<input type="hidden" name="page" value="' . esc_attr( self::PARENT_SLUG ) . '" />';
// ---------- Quick-select ----------
if ( ! empty( $quick ) ) {
echo '<div class="sa-orch-quick" style="margin:1em 0; padding:10px 14px; background:#f6f7f7; border:1px solid #dcdcde; max-width:900px">';
echo '<h3 style="margin:0 0 6px; font-size:13px; text-transform:uppercase; letter-spacing:0.05em; color:#50575e">Quick-select</h3>';
echo '<p class="description" style="margin:0 0 8px">SA-ORCH-TEST fixtures pinned, then the most recently traced Demands.</p>';
echo '<ul style="margin:0; padding:0; list-style:none">';
foreach ( $quick as $d ) {
$url = admin_url( 'admin.php?page=' . self::PARENT_SLUG
. '&demand_surface=' . urlencode( $d['surface'] )
. '&demand_ref=' . urlencode( $d['ref'] ) );
$tag = $d['is_test']
? '<span style="background:#fff2c2;border:1px solid #c9a300;border-radius:3px;padding:0 6px;font-size:11px;margin-right:6px;color:#7a5b00">TEST</span> '
: '';
echo '<li style="padding:3px 0">'
. $tag
. '<a href="' . esc_url( $url ) . '">'
. '<code>' . esc_html( $d['surface'] . ' / ' . $d['ref'] ) . '</code>'
. ( $d['label'] !== '' ? ' — ' . esc_html( $d['label'] ) : '' )
. '</a></li>';
}
echo '</ul></div>';
}
// ---------- Surface + ref ----------
echo '<table class="form-table" role="presentation" style="max-width:900px"><tbody>';
echo '<tr><th scope="row"><label for="demand_surface_select">Demand surface</label></th><td>';
echo '<select id="demand_surface_select" name="demand_surface" class="regular-text">';
echo '<option value="">— pick a surface —</option>';
foreach ( $surface_labels as $sid => $slabel ) {
echo '<option value="' . esc_attr( $sid ) . '"' . selected( $surface, $sid, false ) . '>'
. esc_html( $slabel ) . '</option>';
}
echo '</select>';
echo '<p class="description">Closed vocabulary; extending requires touching <code>SA_Orch_Bridge_Fluent</code>.</p>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="demand_ref_input">Demand ref</label></th><td>';
echo '<input type="text" id="demand_ref_input" name="demand_ref" value="' . esc_attr( $ref ) . '"';
echo ' class="regular-text" placeholder="pick from the list below, or type" autocomplete="off" />';
echo '<span id="sa-orch-ref-status" style="margin-left:10px; font-size:12px"></span>';
echo '<p class="description">Format depends on surface: <code>ticket#NNN</code> · <code>task#NNN</code> · <code>subscriber#NNN</code> · <code>derived#slug</code>. Type or pick.</p>';
echo '</td></tr>';
echo '</tbody></table>';
echo '<p class="submit" style="margin:6px 0 14px"><button type="submit" class="button button-primary">Trace</button></p>';
// ---------- Browser ----------
echo '<div class="sa-orch-browse" style="margin:0 0 1.5em; padding:10px 14px; background:#fff; border:1px solid #dcdcde; max-width:900px">';
echo '<h3 style="margin:0 0 8px; font-size:13px; text-transform:uppercase; letter-spacing:0.05em; color:#50575e">Browse candidates</h3>';
echo '<input type="search" id="sa-orch-filter" placeholder="filter by ID or label…" style="width:100%; max-width:560px" autocomplete="off" />';
echo '<div id="sa-orch-candidate-help" class="description" style="margin:6px 0">Pick a surface to load candidates.</div>';
echo '<ul id="sa-orch-candidate-list" style="margin:0; padding:0; list-style:none; max-height:360px; overflow:auto; border:1px solid #f0f0f1; border-radius:3px"></ul>';
echo '<p class="description" style="margin:8px 0 0"><em>Click any row to fill the Demand ref field. Ref is also free-form for advanced use; format is validated when you type.</em></p>';
echo '</div>';
// ---------- Inline JSON + JS ----------
$payload = [
'candidates' => $candidates,
'patterns' => $patterns,
];
echo '<script>window.SA_ORCH_PICKER = ' . wp_json_encode( $payload ) . ';</script>';
echo '<script>' . self::picker_inline_js() . '</script>';
echo '</form>';
}
/**
* Vanilla-JS controller for the picker. No jQuery dependency.
* Wires: surface change → repopulate browser; filter input → live filter;
* candidate click → fill ref input; ref input → pattern hint.
*/
private static function picker_inline_js() {
return <<<'JS'
(function () {
var data = window.SA_ORCH_PICKER || { candidates: {}, patterns: {} };
var surfaceSel = document.getElementById('demand_surface_select');
var refInput = document.getElementById('demand_ref_input');
var filterInp = document.getElementById('sa-orch-filter');
var listEl = document.getElementById('sa-orch-candidate-list');
var helpEl = document.getElementById('sa-orch-candidate-help');
var statusEl = document.getElementById('sa-orch-ref-status');
if (!surfaceSel || !refInput || !listEl || !helpEl || !filterInp) return;
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function renderCandidates() {
var surface = surfaceSel.value;
var query = (filterInp.value || '').trim().toLowerCase();
listEl.innerHTML = '';
if (!surface) {
helpEl.textContent = 'Pick a surface to load candidates.';
filterInp.disabled = true;
return;
}
filterInp.disabled = false;
var pool = (data.candidates || {})[surface] || [];
if (!pool.length) {
helpEl.textContent = 'No candidates known for this surface.';
return;
}
var matches = pool;
if (query) {
matches = pool.filter(function (c) {
return c.ref.toLowerCase().indexOf(query) !== -1
|| (c.label || '').toLowerCase().indexOf(query) !== -1;
});
}
helpEl.textContent = matches.length + ' / ' + pool.length + ' candidate'
+ (pool.length === 1 ? '' : 's')
+ (query ? ' matching "' + query + '"' : '');
var frag = document.createDocumentFragment();
matches.slice(0, 200).forEach(function (c) {
var li = document.createElement('li');
li.style.padding = '5px 8px';
li.style.cursor = 'pointer';
li.style.borderBottom = '1px solid #f0f0f1';
li.style.lineHeight = '1.4';
var tag = c.is_test
? '<span style="background:#fff2c2;border:1px solid #c9a300;border-radius:3px;padding:0 5px;font-size:11px;margin-right:6px;color:#7a5b00">TEST</span>'
: '';
li.innerHTML = tag + '<code>' + escapeHtml(c.ref) + '</code>'
+ (c.label ? ' — ' + escapeHtml(c.label) : '');
li.addEventListener('mouseover', function () { li.style.background = '#f6f7f7'; });
li.addEventListener('mouseout', function () { if (!li.classList.contains('selected')) li.style.background = ''; });
li.addEventListener('click', function () {
refInput.value = c.ref;
Array.prototype.forEach.call(
listEl.querySelectorAll('li.selected'),
function (e) { e.classList.remove('selected'); e.style.background = ''; }
);
li.classList.add('selected');
li.style.background = '#fff7c2';
validateRef();
refInput.focus();
});
frag.appendChild(li);
});
listEl.appendChild(frag);
}
function validateRef() {
var surface = surfaceSel.value;
var ref = refInput.value.trim();
if (!surface || !ref) {
statusEl.textContent = '';
return;
}
var pattern = (data.patterns || {})[surface];
if (!pattern) {
statusEl.textContent = '';
return;
}
if ((new RegExp(pattern)).test(ref)) {
statusEl.textContent = '✓ valid format';
statusEl.style.color = '#0a7d24';
} else {
statusEl.textContent = '✗ does not match expected pattern';
statusEl.style.color = '#a02b1f';
}
}
surfaceSel.addEventListener('change', function () {
filterInp.value = '';
renderCandidates();
validateRef();
});
filterInp.addEventListener('input', renderCandidates);
refInput.addEventListener('input', validateRef);
// Initial state (e.g. arrived via permalink with both fields preset).
renderCandidates();
validateRef();
})();
JS;
}
/* ---------- Candidate fetchers (per surface) ---------- */
/** Fluent Support tickets, SA-ORCH-TEST pinned first. Cap 50. */
private static function candidates_fluent_support() {
global $wpdb;
$rows = $wpdb->get_results(
"SELECT id, title FROM {$wpdb->prefix}fs_tickets
ORDER BY (title LIKE '[SA-ORCH-TEST]%') DESC, id DESC
LIMIT 50"
);
$out = [];
foreach ( (array) $rows as $r ) {
$title = (string) $r->title;
$out[] = [
'ref' => 'ticket#' . (int) $r->id,
'label' => self::truncate( $title, 80 ),
'is_test' => ( strpos( $title, '[SA-ORCH-TEST]' ) === 0 ),
];
}
return $out;
}
/** Fluent Boards tasks, most-recent first. Cap 50. */
private static function candidates_fluent_boards() {
global $wpdb;
// Defensive: Fluent Boards may not be installed.
$exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fbs_tasks' ) );
if ( ! $exists ) return [];
$rows = $wpdb->get_results(
"SELECT id, title FROM {$wpdb->prefix}fbs_tasks
ORDER BY id DESC LIMIT 50"
);
$out = [];
foreach ( (array) $rows as $r ) {
$title = (string) ( $r->title ?? '' );
$out[] = [
'ref' => 'task#' . (int) $r->id,
'label' => self::truncate( $title, 80 ),
'is_test' => ( strpos( $title, '[SA-ORCH-TEST]' ) === 0 ),
];
}
return $out;
}
/** FluentCRM subscribers, most-recent first. Cap 50. Label = full_name|email. */
private static function candidates_fluentcrm() {
global $wpdb;
$exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fc_subscribers' ) );
if ( ! $exists ) return [];
$rows = $wpdb->get_results(
"SELECT id, full_name, email FROM {$wpdb->prefix}fc_subscribers
ORDER BY id DESC LIMIT 50"
);
$out = [];
foreach ( (array) $rows as $r ) {
$name = trim( (string) ( $r->full_name ?? '' ) );
$email = (string) ( $r->email ?? '' );
$label = $name !== '' ? ( $name . ( $email ? " <{$email}>" : '' ) ) : $email;
$out[] = [
'ref' => 'subscriber#' . (int) $r->id,
'label' => self::truncate( $label, 80 ),
'is_test' => false,
];
}
return $out;
}
/**
* 'derived' surface has no underlying surface table — list every distinct
* derived demand_ref ever recorded in arbitrations or acceptance events.
*/
private static function candidates_derived() {
global $wpdb;
$arb = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'arbitration';
$evt = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event';
$rows = $wpdb->get_results(
"SELECT demand_ref, MAX(last_at) AS last_at FROM (
SELECT demand_ref, MAX(created_at) AS last_at FROM {$arb} WHERE demand_surface='derived' GROUP BY demand_ref
UNION ALL
SELECT demand_ref, MAX(observed_at) AS last_at FROM {$evt} WHERE demand_surface='derived' GROUP BY demand_ref
) sub
GROUP BY demand_ref
ORDER BY last_at DESC LIMIT 50"
);
$out = [];
foreach ( (array) $rows as $r ) {
$out[] = [
'ref' => (string) $r->demand_ref,
'label' => '(derived demand, no surface row)',
'is_test' => false,
];
}
return $out;
}
/**
* Quick-select list: SA-ORCH-TEST tickets pinned, then most-recently-traced
* demands across all surfaces (by max(arbitration.created_at, event.observed_at)).
*/
private static function quick_select_demands( $limit = 10 ) {
global $wpdb;
$arb = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'arbitration';
$evt = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event';
$rows = $wpdb->get_results(
"SELECT demand_surface, demand_ref, MAX(last_at) AS last_at FROM (
SELECT demand_surface, demand_ref, MAX(created_at) AS last_at FROM {$arb} GROUP BY demand_surface, demand_ref
UNION ALL
SELECT demand_surface, demand_ref, MAX(observed_at) AS last_at FROM {$evt} GROUP BY demand_surface, demand_ref
) sub
GROUP BY demand_surface, demand_ref
ORDER BY last_at DESC LIMIT 30"
);
if ( empty( $rows ) ) return [];
// Resolve labels + is_test per row.
$out = [];
foreach ( $rows as $r ) {
$surface = (string) $r->demand_surface;
$ref = (string) $r->demand_ref;
list( $label, $is_test ) = self::resolve_label( $surface, $ref );
$out[] = [
'surface' => $surface,
'ref' => $ref,
'label' => $label,
'is_test' => $is_test,
'last_at' => (string) $r->last_at,
];
}
// Pin SA-ORCH-TEST first; preserve last_at ordering within each group.
usort( $out, function ( $a, $b ) {
if ( $a['is_test'] !== $b['is_test'] ) return $a['is_test'] ? -1 : 1;
return strcmp( $b['last_at'], $a['last_at'] );
} );
return array_slice( $out, 0, (int) $limit );
}
/** Resolve a (surface, ref) to a human label + is_test flag. Best-effort, tolerant of missing tables. */
private static function resolve_label( $surface, $ref ) {
global $wpdb;
if ( $surface === 'fluent-support' && preg_match( '/^ticket#(\d+)$/', $ref, $m ) ) {
$title = (string) $wpdb->get_var( $wpdb->prepare(
"SELECT title FROM {$wpdb->prefix}fs_tickets WHERE id=%d", (int) $m[1]
) );
return [ self::truncate( $title, 80 ), strpos( $title, '[SA-ORCH-TEST]' ) === 0 ];
}
if ( $surface === 'fluent-boards' && preg_match( '/^task#(\d+)$/', $ref, $m ) ) {
if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fbs_tasks' ) ) ) {
$title = (string) $wpdb->get_var( $wpdb->prepare(
"SELECT title FROM {$wpdb->prefix}fbs_tasks WHERE id=%d", (int) $m[1]
) );
return [ self::truncate( $title, 80 ), strpos( $title, '[SA-ORCH-TEST]' ) === 0 ];
}
}
if ( $surface === 'fluentcrm' && preg_match( '/^subscriber#(\d+)$/', $ref, $m ) ) {
if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fc_subscribers' ) ) ) {
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT full_name, email FROM {$wpdb->prefix}fc_subscribers WHERE id=%d", (int) $m[1]
) );
if ( $row ) {
$name = trim( (string) $row->full_name );
$email = (string) $row->email;
$label = $name !== '' ? ( $name . ( $email ? " <{$email}>" : '' ) ) : $email;
return [ self::truncate( $label, 80 ), false ];
}
}
}
if ( $surface === 'derived' ) {
return [ '(derived demand, no surface row)', false ];
}
return [ '', false ];
}
private static function truncate( $s, $n ) {
$s = (string) $s;
if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
return mb_strlen( $s ) > $n ? ( mb_substr( $s, 0, $n - 1 ) . '…' ) : $s;
}
return strlen( $s ) > $n ? ( substr( $s, 0, $n - 1 ) . '…' ) : $s;
}
private static function render_trace_results( array $data ) {
$arbs = $data['arbitrations'] ?? [];
$links = $data['plan_links'] ?? [];
$events = $data['acceptance'] ?? [];
echo '<h2>Closure</h2>';
echo '<table class="widefat striped" style="max-width:600px"><tbody>';
echo '<tr><th scope="row" style="width:40%">closure_state_basic</th><td><code>' . esc_html( (string) ( $data['closure_state_basic'] ?? '' ) ) . '</code></td></tr>';
echo '<tr><th scope="row">closure_state_role_aware</th><td><code>' . esc_html( (string) ( $data['closure_state_role_aware'] ?? '' ) ) . '</code></td></tr>';
$auth = $data['closure_authority_valid'] ?? null;
$auth_disp = $auth === null ? 'null' : ( $auth ? 'true' : 'false' );
echo '<tr><th scope="row">closure_authority_valid</th><td><code>' . esc_html( $auth_disp ) . '</code></td></tr>';
echo '</tbody></table>';
// === Deterministic Trace Audit (layer 2 — rules-based) ===
self::render_reasoning_review( $data );
echo '<h2>Arbitrations <span class="count">(' . count( $arbs ) . ')</span></h2>';
if ( empty( $arbs ) ) {
echo '<p><em>No arbitrations recorded for this demand.</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
echo '<th>ID</th><th>Hash</th><th>Projection Type</th><th>Truth Class</th><th>Actor</th><th>Correlation</th><th>Created (UTC)</th><th>Asterion</th>';
echo '</tr></thead><tbody>';
foreach ( $arbs as $a ) {
echo '<tr>';
echo '<td>' . (int) $a->id . '</td>';
echo '<td><code>' . esc_html( substr( (string) $a->hash, 0, 12 ) ) . '…</code></td>';
echo '<td>' . esc_html( (string) $a->projection_type ) . '</td>';
echo '<td>' . esc_html( (string) $a->truth_class ) . '</td>';
echo '<td>' . esc_html( (string) ( $a->actor_user_id ?? '—' ) ) . '</td>';
echo '<td><code>' . esc_html( substr( (string) $a->correlation_id, 0, 8 ) ) . '…</code></td>';
echo '<td>' . esc_html( (string) $a->created_at ) . '</td>';
echo '<td><code>' . esc_html( (string) $a->asterion_path ) . '</code></td>';
echo '</tr>';
echo '<tr><td></td><td colspan="7"><strong>Rationale:</strong> ' . esc_html( (string) $a->rationale ) . '</td></tr>';
}
echo '</tbody></table>';
}
echo '<h2>Plan Links <span class="count">(' . count( $links ) . ')</span></h2>';
if ( empty( $links ) ) {
echo '<p><em>No plan links — this Demand has no Plan refs (consistent with <code>scope-direct-no-plan</code> or <code>no-plan</code> arbitration).</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
echo '<th>ID</th><th>Arbitration</th><th>Plan Surface</th><th>Plan Ref</th><th>Link Kind</th><th>Created (UTC)</th>';
echo '</tr></thead><tbody>';
foreach ( $links as $l ) {
echo '<tr>';
echo '<td>' . (int) $l->id . '</td>';
echo '<td>' . (int) $l->arbitration_id . '</td>';
echo '<td>' . esc_html( (string) $l->plan_surface ) . '</td>';
echo '<td><code>' . esc_html( (string) $l->plan_ref ) . '</code></td>';
echo '<td>' . esc_html( (string) $l->link_kind ) . '</td>';
echo '<td>' . esc_html( (string) $l->created_at ) . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
}
echo '<h2>Acceptance Events <span class="count">(' . count( $events ) . ')</span></h2>';
if ( empty( $events ) ) {
echo '<p><em>No acceptance events.</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
echo '<th>ID</th><th>Event Type</th><th>Actor Role</th><th>Actor</th><th>Truth Class</th><th>Prior</th><th>Observed (UTC)</th><th>Asterion</th>';
echo '</tr></thead><tbody>';
foreach ( $events as $e ) {
echo '<tr>';
echo '<td>' . (int) $e->id . '</td>';
echo '<td>' . esc_html( (string) $e->event_type ) . '</td>';
echo '<td>' . esc_html( (string) $e->actor_role ) . '</td>';
echo '<td>' . esc_html( (string) ( $e->actor_user_id ?? '—' ) ) . '</td>';
echo '<td>' . esc_html( (string) $e->truth_class ) . '</td>';
echo '<td>' . esc_html( (string) ( $e->prior_event_id ?? '—' ) ) . '</td>';
echo '<td>' . esc_html( (string) $e->observed_at ) . '</td>';
echo '<td><code>' . esc_html( (string) $e->asterion_path ) . '</code></td>';
echo '</tr>';
if ( ! empty( $e->details ) ) {
echo '<tr><td></td><td colspan="7"><strong>Details:</strong> ' . esc_html( (string) $e->details ) . '</td></tr>';
}
}
echo '</tbody></table>';
}
}
/**
* Deterministic Trace Audit block.
*
* Local rules-based audit over the trace data. No API calls, no LLM, no
* persistence. Recomputed on every page load.
*
* This is the FLOOR of the review stack:
* 1. Trace data
* 2. Deterministic Trace Audit ← this section
* 3. Strategy LLM Review (future; will sit above this audit)
* 4. Human arbitration (final meaning; always wins)
*/
private static function render_reasoning_review( array $data ) {
$r = SA_Orch_Reasoning::evaluate( $data );
echo '<h2>Deterministic Trace Audit <span class="count">(rules-based, mode: ' . esc_html( (string) ( $r['mode'] ?? 'deterministic' ) ) . ')</span></h2>';
echo '<p><em>Local rules-based audit — repeatable, no API, no LLM. Computed on each page load. Floor of a four-layer stack: ';
echo '<strong>(1) Trace data</strong> &rarr; <strong>(2) this Deterministic Trace Audit</strong> &rarr; <strong>(3) Strategy LLM Review</strong> (future) &rarr; <strong>(4) Human arbitration</strong> (final).</em></p>';
$color_map = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f', 'n/a' => '#666' ];
$color = $color_map[ $r['closure_confidence'] ] ?? '#666';
echo '<table class="widefat striped" style="max-width:900px"><tbody>';
echo '<tr><th scope="row" style="width:25%">Summary</th><td>' . esc_html( $r['summary'] ) . '</td></tr>';
echo '<tr><th scope="row">Closure confidence</th><td>';
echo '<strong style="color:' . esc_attr( $color ) . '">' . esc_html( strtoupper( (string) $r['closure_confidence'] ) ) . '</strong>';
echo ' &mdash; ' . esc_html( $r['closure_confidence_explanation'] );
echo '</td></tr>';
// Needs Human Arbitration — prominent row
echo '<tr><th scope="row">Needs Human Arbitration</th><td>';
if ( ! empty( $r['needs_human_arbitration'] ) ) {
echo '<strong style="color:#a02b1f">&#9888; YES &mdash; ' . esc_html( (string) $r['human_arbitration_message'] ) . '</strong>';
echo '<p style="margin:6px 0 0">Triggered by:</p>';
echo '<ul style="margin:0; padding-left:1.2em">';
foreach ( (array) $r['human_arbitration_triggers'] as $t ) {
echo '<li>' . esc_html( $t ) . '</li>';
}
echo '</ul>';
} else {
echo '<em>No &mdash; none of the deterministic escalation triggers fired.</em>';
}
echo '</td></tr>';
$render_list = function ( $label, $items ) {
echo '<tr><th scope="row">' . esc_html( $label ) . '</th><td>';
if ( empty( $items ) ) {
echo '<em>(none)</em>';
} else {
echo '<ul style="margin:0; padding-left:1.2em">';
foreach ( $items as $i ) {
echo '<li>' . esc_html( $i ) . '</li>';
}
echo '</ul>';
}
echo '</td></tr>';
};
$render_list( 'Inconsistencies', $r['inconsistencies'] );
$render_list( 'Potential scope drift', $r['scope_drift'] );
$render_list( 'Notes', $r['notes'] );
echo '</tbody></table>';
echo '<p style="margin-top:8px"><em>Layer 3 — Strategy LLM Review — sits below this section. The audit remains as a sanity-check / fallback layer underneath any LLM verdict.</em></p>';
// --- Strategy Review (v0.5) ---
self::render_strategy_review( $data, $r );
}
/**
* Strategy LLM Review block (layer 3).
*
* Consumes the trace + the deterministic audit; delegates to the active
* provider via SA_Orch_Strategy::review(). Strategy Review is advisory:
* it cannot override closure_state or needs_human_arbitration.
*/
private static function render_strategy_review( array $data, array $deterministic_audit ) {
$strategy = SA_Orch_Strategy::review( $data, $deterministic_audit );
$provider_id = (string) ( $strategy['provider_id'] ?? 'simulated' );
$provider_name = (string) ( $strategy['provider_name'] ?? 'Simulated (no API call)' );
$configured_id = (string) ( $strategy['configured_provider_id'] ?? $provider_id );
$configured_name = (string) ( $strategy['configured_provider_name'] ?? $provider_name );
$fallback_used = ! empty( $strategy['fallback_used'] );
$provider_err = $strategy['provider_error'] ?? null;
$heading_suffix = $fallback_used
? '(layer 3 — used: ' . esc_html( $provider_id ) . ', configured: ' . esc_html( $configured_id ) . ' &mdash; <strong style="color:#a76b00">FALLBACK</strong>)'
: '(layer 3 — provider: ' . esc_html( $provider_id ) . ')';
echo '<h2>Strategy Review (simulated LLM) <span class="count">' . $heading_suffix . '</span></h2>';
echo '<p><em>Layer 3 of the review stack. Provider in use: <code>' . esc_html( $provider_name ) . '</code>. ';
if ( $fallback_used ) {
echo 'Configured provider (<code>' . esc_html( $configured_name ) . '</code>) failed and was replaced by the simulated fallback. ';
}
echo 'Compares its interpretation against the deterministic audit. ';
echo '<strong>Strategy Review cannot override <code>closure_state</code> or the <code>needs_human_arbitration</code> flag</strong> — those remain governed by the deterministic audit and ultimately by human arbitration.</em></p>';
if ( $provider_err ) {
echo '<div class="notice notice-warning"><p><strong>Provider error (configured: <code>' . esc_html( $configured_id ) . '</code>):</strong> ' . esc_html( $provider_err ) . ' &mdash; fell back to <code>simulated</code>.</p></div>';
}
// Color hints
$color_map_conf = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f' ];
$color_map_agree = [ 'true' => '#0a7d24', 'partial' => '#a76b00', 'false' => '#a02b1f' ];
$strat_conf = (string) ( $strategy['confidence'] ?? 'low' );
$strat_agree = (string) ( $strategy['agreement_with_audit'] ?? 'partial' );
$strat_color = $color_map_conf[ $strat_conf ] ?? '#666';
$agree_color = $color_map_agree[ $strat_agree ] ?? '#666';
echo '<table class="widefat striped" style="max-width:900px"><tbody>';
echo '<tr><th scope="row" style="width:25%">Summary</th><td>' . esc_html( (string) ( $strategy['summary'] ?? '' ) ) . '</td></tr>';
echo '<tr><th scope="row">Agreement with Deterministic Audit</th><td>';
echo '<strong style="color:' . esc_attr( $agree_color ) . '">' . esc_html( strtoupper( $strat_agree ) ) . '</strong>';
echo '</td></tr>';
echo '<tr><th scope="row">Additional risks (not caught by audit)</th><td>';
$risks = (array) ( $strategy['additional_risks'] ?? [] );
if ( empty( $risks ) ) {
echo '<em>(none)</em>';
} else {
echo '<ul style="margin:0; padding-left:1.2em">';
foreach ( $risks as $r ) {
echo '<li>' . esc_html( (string) $r ) . '</li>';
}
echo '</ul>';
}
echo '</td></tr>';
echo '<tr><th scope="row">Suggested next action</th><td>' . esc_html( (string) ( $strategy['suggested_next_action'] ?? '' ) ) . '</td></tr>';
echo '<tr><th scope="row">Confidence</th><td>';
echo '<strong style="color:' . esc_attr( $strat_color ) . '">' . esc_html( strtoupper( $strat_conf ) ) . '</strong>';
echo '</td></tr>';
echo '</tbody></table>';
// Visual side-by-side comparison
self::render_audit_strategy_comparison( $deterministic_audit, $strategy );
}
/**
* Side-by-side comparison of Deterministic Audit (layer 2) and Strategy Review (layer 3).
*/
private static function render_audit_strategy_comparison( array $audit, array $strategy ) {
$color_map_conf = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f', 'n/a' => '#666' ];
$audit_conf = (string) ( $audit['closure_confidence'] ?? 'n/a' );
$strat_conf = (string) ( $strategy['confidence'] ?? 'low' );
$audit_inc = count( (array) ( $audit['inconsistencies'] ?? [] ) );
$audit_drift = count( (array) ( $audit['scope_drift'] ?? [] ) );
$strat_extra = count( (array) ( $strategy['additional_risks'] ?? [] ) );
$needs_hum = ! empty( $audit['needs_human_arbitration'] );
echo '<h3 style="margin-top:1.2em">Deterministic Audit &harr; Strategy Review (side-by-side)</h3>';
echo '<table class="widefat striped" style="max-width:900px"><thead><tr>';
echo '<th style="width:25%">&nbsp;</th>';
echo '<th>Deterministic Audit (layer 2)</th>';
echo '<th>Strategy Review (layer 3)</th>';
echo '</tr></thead><tbody>';
$audit_conf_color = $color_map_conf[ $audit_conf ] ?? '#666';
$strat_conf_color = $color_map_conf[ $strat_conf ] ?? '#666';
echo '<tr><th scope="row">Confidence</th>';
echo '<td><strong style="color:' . esc_attr( $audit_conf_color ) . '">' . esc_html( strtoupper( $audit_conf ) ) . '</strong></td>';
echo '<td><strong style="color:' . esc_attr( $strat_conf_color ) . '">' . esc_html( strtoupper( $strat_conf ) ) . '</strong></td>';
echo '</tr>';
echo '<tr><th scope="row">Findings counted</th>';
echo '<td>' . (int) $audit_inc . ' inconsistenc' . ( $audit_inc === 1 ? 'y' : 'ies' ) . ', ' . (int) $audit_drift . ' scope-drift indicator' . ( $audit_drift === 1 ? '' : 's' ) . '</td>';
echo '<td>' . (int) $strat_extra . ' additional risk' . ( $strat_extra === 1 ? '' : 's' ) . '</td>';
echo '</tr>';
echo '<tr><th scope="row">Closure authority</th>';
echo '<td>' . ( $needs_hum ? '<strong>flagged needs_human_arbitration = YES</strong>' : 'no escalation flag' ) . '</td>';
echo '<td><em>cannot override the audit</em></td>';
echo '</tr>';
echo '<tr><th scope="row">Mode / character</th>';
echo '<td>rules-based, repeatable</td>';
echo '<td>interpretive, narrative (' . esc_html( (string) ( $strategy['provider_id'] ?? 'simulated' ) ) . ')</td>';
echo '</tr>';
echo '</tbody></table>';
echo '<p style="margin-top:8px"><em>Reminder: <code>closure_state_basic</code>, <code>closure_state_role_aware</code>, <code>closure_authority_valid</code>, and <code>needs_human_arbitration</code> are governed by the deterministic audit (layer 2) and ultimately by human arbitration (layer 4). Strategy Review is advisory.</em></p>';
}
/* ---------- Recent Arbitrations ---------- */
public static function render_arbitrations() {
if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' );
global $wpdb;
$p = $wpdb->prefix . SA_ORCH_DB_PREFIX;
$rows = $wpdb->get_results(
"SELECT id, demand_surface, demand_ref, projection_type, truth_class, actor_user_id, created_at
FROM {$p}arbitration
ORDER BY created_at DESC, id DESC LIMIT 20"
);
echo '<div class="wrap">';
echo '<h1>Recent Arbitrations</h1>';
echo '<p>Last 20 arbitration records, newest first. Click <strong>Trace</strong> to walk the full chain.</p>';
if ( empty( $rows ) ) {
echo '<p><em>No arbitrations recorded yet.</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
echo '<th>ID</th><th>Demand</th><th>Projection Type</th><th>Truth Class</th><th>Actor</th><th>Created (UTC)</th><th>Action</th>';
echo '</tr></thead><tbody>';
foreach ( $rows as $r ) {
$trace_url = admin_url( 'admin.php?page=' . self::PARENT_SLUG
. '&demand_surface=' . urlencode( $r->demand_surface )
. '&demand_ref=' . urlencode( $r->demand_ref ) );
echo '<tr>';
echo '<td>' . (int) $r->id . '</td>';
echo '<td><code>' . esc_html( $r->demand_surface . ' / ' . $r->demand_ref ) . '</code></td>';
echo '<td>' . esc_html( (string) $r->projection_type ) . '</td>';
echo '<td>' . esc_html( (string) $r->truth_class ) . '</td>';
echo '<td>' . esc_html( (string) ( $r->actor_user_id ?? '—' ) ) . '</td>';
echo '<td>' . esc_html( (string) $r->created_at ) . '</td>';
echo '<td><a href="' . esc_url( $trace_url ) . '">Trace</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
echo '</div>';
}
/* ---------- Language Model Providers (settings page) ---------- */
public static function render_llm_settings() {
if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' );
$message = '';
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
check_admin_referer( 'sa_orch_llm_settings' );
SA_Orch_LLM_Settings::save( wp_unslash( $_POST ) );
$message = 'Settings saved.';
}
$settings = SA_Orch_LLM_Settings::get();
$choices = SA_Orch_LLM_Providers::get_provider_choices();
$token_label = SA_Orch_LLM_Settings::token_status_label();
echo '<div class="wrap">';
echo '<h1>Language Model Providers</h1>';
if ( $message ) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
}
echo '<p>Configure the provider used by the Strategy Review (layer 3 of the review stack: trace data &rarr; deterministic audit &rarr; <strong>strategy review</strong> &rarr; human arbitration).</p>';
echo '<p><strong>Local-dev posture:</strong> secrets are stored in <code>wp_options</code>. Tokens are never displayed back &mdash; saving with a blank token field preserves the existing value. This storage layer is the attachment seam for a future SA-Core key vault; replacing it later will not change the consumer surface.</p>';
echo '<form method="post">';
wp_nonce_field( 'sa_orch_llm_settings' );
echo '<table class="form-table" role="presentation"><tbody>';
echo '<tr><th scope="row">Enabled</th><td>';
echo '<label><input type="checkbox" name="enabled" value="1"' . checked( ! empty( $settings['enabled'] ), true, false ) . ' /> Use the selected provider for Strategy Review.</label>';
echo '<p class="description">When unchecked, Strategy Review falls back to the simulated provider regardless of the dropdown selection.</p>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="sa_orch_provider">Provider</label></th><td>';
echo '<select id="sa_orch_provider" name="provider">';
foreach ( $choices as $id => $label ) {
echo '<option value="' . esc_attr( $id ) . '"' . selected( $settings['provider'], $id, false ) . '>' . esc_html( $label ) . '</option>';
}
echo '</select>';
echo '<p class="description">Only <code>simulated</code> is functional in v0.5. Other providers are listed as inert placeholders to surface the seam.</p>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="sa_orch_model">Model name</label></th><td>';
echo '<input type="text" id="sa_orch_model" name="model" value="' . esc_attr( $settings['model'] ) . '" class="regular-text" placeholder="e.g. gpt-4o, claude-sonnet-4-5, llama-3.1-70b" />';
echo '<p class="description">Free-form. Future providers may validate against their own catalog.</p>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="sa_orch_api_base_url">API base URL</label></th><td>';
echo '<input type="text" id="sa_orch_api_base_url" name="api_base_url" value="' . esc_attr( $settings['api_base_url'] ) . '" class="regular-text" placeholder="e.g. https://api.openai.com/v1" />';
echo '<p class="description">Used by HTTP-backed providers. Ignored by the simulated provider.</p>';
echo '</td></tr>';
echo '<tr><th scope="row"><label for="sa_orch_api_token">API token / key</label></th><td>';
echo '<input type="password" id="sa_orch_api_token" name="api_token" value="" class="regular-text" autocomplete="new-password" placeholder="enter token to set or update; leave blank to preserve" />';
echo '<p class="description">' . esc_html( $token_label ) . ' Tokens are stored in <code>wp_options</code> for local dev only and are never displayed back through the UI. Leave the field blank to keep the existing value.</p>';
echo '</td></tr>';
echo '</tbody></table>';
echo '<p class="submit"><button type="submit" class="button button-primary">Save Settings</button></p>';
echo '</form>';
echo '<hr />';
echo '<h2>Status</h2>';
echo '<table class="widefat striped" style="max-width:600px"><tbody>';
echo '<tr><th scope="row" style="width:40%">Active provider (resolved)</th><td><code>' . esc_html( SA_Orch_LLM_Providers::get_active_provider()->id() ) . '</code></td></tr>';
echo '<tr><th scope="row">Settings.enabled</th><td><code>' . ( ! empty( $settings['enabled'] ) ? 'true' : 'false' ) . '</code></td></tr>';
echo '<tr><th scope="row">Settings.provider</th><td><code>' . esc_html( (string) $settings['provider'] ) . '</code></td></tr>';
echo '<tr><th scope="row">Settings.model</th><td><code>' . esc_html( (string) $settings['model'] ?: '—' ) . '</code></td></tr>';
echo '<tr><th scope="row">Settings.api_base_url</th><td><code>' . esc_html( (string) $settings['api_base_url'] ?: '—' ) . '</code></td></tr>';
echo '<tr><th scope="row">Settings.api_token</th><td>' . ( SA_Orch_LLM_Settings::has_token() ? '<code>***** (set)</code>' : '<code>(not set)</code>' ) . '</td></tr>';
echo '</tbody></table>';
echo '<p style="margin-top:8px"><em>v0.5 ships with the <code>simulated</code> provider only — no external API call is made by this plugin under any settings configuration. Adding real providers (OpenAI, Anthropic, local model endpoint, custom HTTP) is a matter of implementing <code>SA_Orch_LLM_Provider_Interface</code> and adding a case to <code>SA_Orch_LLM_Providers::instantiate()</code>.</em></p>';
echo '</div>';
}
/* ---------- Acceptance Events ---------- */
public static function render_events() {
if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' );
global $wpdb;
$p = $wpdb->prefix . SA_ORCH_DB_PREFIX;
$rows = $wpdb->get_results(
"SELECT id, event_type, actor_role, demand_surface, demand_ref, observed_at, truth_class, actor_user_id
FROM {$p}acceptance_event
ORDER BY observed_at DESC, id DESC LIMIT 20"
);
echo '<div class="wrap">';
echo '<h1>Acceptance Events</h1>';
echo '<p>Last 20 acceptance events, newest first. Append-only — these rows are never updated or deleted.</p>';
if ( empty( $rows ) ) {
echo '<p><em>No acceptance events recorded yet.</em></p>';
} else {
echo '<table class="widefat striped"><thead><tr>';
echo '<th>ID</th><th>Event Type</th><th>Actor Role</th><th>Actor</th><th>Demand</th><th>Truth Class</th><th>Observed (UTC)</th><th>Action</th>';
echo '</tr></thead><tbody>';
foreach ( $rows as $r ) {
$trace_url = admin_url( 'admin.php?page=' . self::PARENT_SLUG
. '&demand_surface=' . urlencode( $r->demand_surface )
. '&demand_ref=' . urlencode( $r->demand_ref ) );
echo '<tr>';
echo '<td>' . (int) $r->id . '</td>';
echo '<td>' . esc_html( (string) $r->event_type ) . '</td>';
echo '<td>' . esc_html( (string) $r->actor_role ) . '</td>';
echo '<td>' . esc_html( (string) ( $r->actor_user_id ?? '—' ) ) . '</td>';
echo '<td><code>' . esc_html( $r->demand_surface . ' / ' . $r->demand_ref ) . '</code></td>';
echo '<td>' . esc_html( (string) $r->truth_class ) . '</td>';
echo '<td>' . esc_html( (string) $r->observed_at ) . '</td>';
echo '<td><a href="' . esc_url( $trace_url ) . '">Trace</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
echo '</div>';
}
}