Skip to content
wiki.fftac.org

Spiralist Engine - Source Excerpt 08

Back to Spiralist Engine

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-engine/spiralist-engine.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/spiralist-engine/spiralist-engine.php

$errors[] = sprintf(
                'Edge "%s -> %s" has a condition that does not resolve the source output state "%s".',
                $from_node,
                $to_node,
                $source_state
            );
            continue;
        }

        $adjacency[$from_node][] = $to_node;
        $reverse_adjacency[$to_node][] = $from_node;
        $weak_adjacency[$from_node][] = $to_node;
        $weak_adjacency[$to_node][] = $from_node;
    }

    foreach ($nodes as $node_id => $node) {
        if (empty($adjacency[$node_id])) {
            $errors[] = sprintf('Node "%s" has no valid outgoing edge.', $node_id);
        }
    }

    if (!empty($nodes)) {
        $root_id = array_key_first($nodes);
        $visited = [];
        $queue = $root_id !== null ? [$root_id] : [];

        while ($queue) {
            $current = array_shift($queue);
            if ($current === null || isset($visited[$current])) {
                continue;
            }

            $visited[$current] = true;

            foreach ((array) ($weak_adjacency[$current] ?? []) as $next) {
                if (!isset($visited[$next])) {
                    $queue[] = $next;
                }
            }
        }

        foreach (array_keys($nodes) as $node_id) {
            if (!isset($visited[$node_id])) {
                $errors[] = sprintf('Node "%s" is disconnected from the published graph.', $node_id);
            }
        }
    }

    $cycle_nodes = spiralist_engine_get_cycle_nodes($adjacency);

    if (empty($cycle_nodes) && !empty($nodes)) {
        $errors[] = 'The execution graph has no cycle, so it cannot continue indefinitely.';
    }

    if (!empty($cycle_nodes)) {
        $can_reach_cycle = [];
        $queue = $cycle_nodes;

        while ($queue) {
            $current = array_shift($queue);
            if (isset($can_reach_cycle[$current])) {
                continue;
            }

            $can_reach_cycle[$current] = true;

            foreach ((array) ($reverse_adjacency[$current] ?? []) as $previous) {
                if (!isset($can_reach_cycle[$previous])) {
                    $queue[] = $previous;
                }
            }
        }

        foreach (array_keys($nodes) as $node_id) {
            if (!isset($can_reach_cycle[$node_id])) {
                $errors[] = sprintf(
                    'Node "%s" cannot reach a repeatable transformation cycle, so the system invariant fails.',
                    $node_id
                );
            }
        }
    }

    return [
        'valid' => empty($errors),
        'errors' => array_values(array_unique($errors)),
        'graph' => $graph,
        'adjacency' => $adjacency,
        'cycle_nodes' => $cycle_nodes,
        'canonical_context' => $canonical_context,
    ];
}

function spiralist_engine_revert_post_status(int $post_id, string $status): void
{
    static $reverting = false;

    if ($reverting || $post_id <= 0 || $status === '') {
        return;
    }

    $current_status = get_post_status($post_id);
    if ($current_status === $status) {
        return;
    }

    $reverting = true;
    remove_action('save_post', 'spiralist_engine_handle_post_save', 10);
    wp_update_post(
        [
            'ID' => $post_id,
            'post_status' => $status,
        ]
    );
    add_action('save_post', 'spiralist_engine_handle_post_save', 10, 3);
    $reverting = false;
}

function spiralist_engine_abort_save(int $post_id, string $message, string $fallback_status = ''): void
{
    if ($fallback_status !== '') {
        spiralist_engine_revert_post_status($post_id, $fallback_status);
    }

    wp_die(
        nl2br(esc_html($message)),
        'Spiralist Engine Validation Error',
        [
            'back_link' => true,
        ]
    );
}

function spiralist_engine_canonicalize_value($value)
{
    if (!is_array($value)) {
        return $value;
    }

    if (spiralist_engine_is_list($value)) {
        $normalized = array_map('spiralist_engine_canonicalize_value', $value);
        usort(
            $normalized,
            static function ($left, $right): int {
                return strcmp(spiralist_engine_json_encode($left), spiralist_engine_json_encode($right));
            }
        );

        return array_values($normalized);
    }

    $normalized = [];

    foreach ($value as $key => $item) {
        $normalized[(string) $key] = spiralist_engine_canonicalize_value($item);
    }

    ksort($normalized);

    return $normalized;
}

function spiralist_engine_symbol_definition_changed(int $post_id, array $new_record): bool
{
    $existing_post = get_post($post_id);
    if (!$existing_post instanceof WP_Post) {
        return false;
    }

    $existing = spiralist_engine_build_symbol_record($existing_post);
    $current_status = (string) $existing_post->post_status;
    $original_status = sanitize_key((string) wp_unslash($_POST['original_post_status'] ?? $current_status));

    if ($current_status !== 'publish' && $original_status !== 'publish') {
        return false;
    }

    $existing_compare = spiralist_engine_canonicalize_value(
        [
            'symbol_id' => $existing['symbol_id'],
            'symbol_type' => $existing['symbol_type'],
            'operation_type' => $existing['operation_type'],
            'inputs' => $existing['inputs'],
            'outputs' => $existing['outputs'],
            'rules' => $existing['rules'],
            'derives_from' => $existing['derives_from'],
        ]
    );

    $new_compare = spiralist_engine_canonicalize_value(
        [
            'symbol_id' => $new_record['symbol_id'] ?? '',
            'symbol_type' => $new_record['symbol_type'] ?? '',
            'operation_type' => $new_record['operation_type'] ?? '',
            'inputs' => $new_record['inputs'] ?? [],
            'outputs' => $new_record['outputs'] ?? [],
            'rules' => $new_record['rules'] ?? [],
            'derives_from' => $new_record['derives_from'] ?? [],
        ]
    );

    return spiralist_engine_json_encode($existing_compare) !== spiralist_engine_json_encode($new_compare);
}

function spiralist_engine_persist_record_meta(int $post_id, string $post_type, array $record): void
{
    if ($post_type === 'spiralist_symbol') {
        update_post_meta($post_id, 'symbol_id', (string) ($record['symbol_id'] ?? ''));
        update_post_meta($post_id, 'symbol_type', (string) ($record['symbol_type'] ?? ''));
        update_post_meta($post_id, 'operation_type', (string) ($record['operation_type'] ?? ''));
        spiralist_engine_update_json_meta($post_id, 'inputs', spiralist_engine_normalize_string_list((array) ($record['inputs'] ?? [])));
        spiralist_engine_update_json_meta($post_id, 'outputs', spiralist_engine_normalize_string_list((array) ($record['outputs'] ?? [])));
        spiralist_engine_update_json_meta($post_id, 'rules', (array) ($record['rules'] ?? []));
        spiralist_engine_update_json_meta($post_id, 'derives_from', spiralist_engine_normalize_string_list((array) ($record['derives_from'] ?? [])));
        spiralist_engine_update_json_meta($post_id, 'canonical_lock', (array) ($record['canonical_lock'] ?? []));
        return;
    }

    if ($post_type === 'spiralist_node') {
        update_post_meta($post_id, 'node_id', (string) ($record['node_id'] ?? ''));
        update_post_meta($post_id, 'input_state', (string) ($record['input_state'] ?? ''));
        update_post_meta($post_id, 'output_state', (string) ($record['output_state'] ?? ''));
        spiralist_engine_update_json_meta($post_id, 'symbols', spiralist_engine_normalize_string_list((array) ($record['symbols'] ?? [])));
        spiralist_engine_update_json_meta($post_id, 'transformation_chain', (array) ($record['transformation_chain'] ?? []));
        spiralist_engine_update_json_meta($post_id, 'derives_from', spiralist_engine_normalize_string_list((array) ($record['derives_from'] ?? [])));
        spiralist_engine_update_json_meta($post_id, 'presentation', (array) ($record['presentation'] ?? []));
        spiralist_engine_update_json_meta($post_id, 'canonical_lock', (array) ($record['canonical_lock'] ?? []));
        return;
    }

    if ($post_type === 'spiralist_edge') {
        update_post_meta($post_id, 'from_node', (string) ($record['from_node'] ?? ''));
        update_post_meta($post_id, 'to_node', (string) ($record['to_node'] ?? ''));
        spiralist_engine_update_json_meta($post_id, 'condition', (array) ($record['condition'] ?? []));
        return;
    }

    if ($post_type === 'spiralist_instance') {
        update_post_meta($post_id, 'user_id', (int) ($record['user_id'] ?? 0));
        update_post_meta($post_id, 'current_node', (string) ($record['current_node'] ?? ''));