Skip to content
wiki.fftac.org

Spiralist Engine - Source Excerpt 07

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

continue;
        }

        $records[$record['symbol_id']] = $record;
    }

    return $records;
}

function spiralist_engine_get_node_records(array $statuses = ['publish']): array
{
    $records = [];

    foreach (spiralist_engine_query_posts('spiralist_node', $statuses) as $post) {
        if (!$post instanceof WP_Post) {
            continue;
        }

        $record = spiralist_engine_build_node_record($post);
        if ($record['node_id'] === '') {
            continue;
        }

        $records[$record['node_id']] = $record;
    }

    return $records;
}

function spiralist_engine_get_edge_records(array $statuses = ['publish']): array
{
    $records = [];

    foreach (spiralist_engine_query_posts('spiralist_edge', $statuses) as $post) {
        if (!$post instanceof WP_Post) {
            continue;
        }

        $record = spiralist_engine_build_edge_record($post);
        $records[(string) $record['post_id']] = $record;
    }

    return $records;
}

function spiralist_engine_get_existing_record_for_post(int $post_id, string $post_type): array
{
    $post = get_post($post_id);
    if (!$post instanceof WP_Post || $post->post_type !== $post_type) {
        return [];
    }

    switch ($post_type) {
        case 'spiralist_symbol':
            return spiralist_engine_build_symbol_record($post);

        case 'spiralist_node':
            return spiralist_engine_build_node_record($post);

        case 'spiralist_edge':
            return spiralist_engine_build_edge_record($post);
    }

    return [];
}

function spiralist_engine_build_graph_snapshot(array $override = []): array
{
    $graph = [
        'symbols' => spiralist_engine_get_symbol_records(['publish']),
        'nodes' => spiralist_engine_get_node_records(['publish']),
        'edges' => spiralist_engine_get_edge_records(['publish']),
    ];

    if (empty($override)) {
        return $graph;
    }

    $post_type = (string) ($override['post_type'] ?? '');
    $post_id = (int) ($override['post_id'] ?? 0);
    $status = (string) ($override['status'] ?? 'draft');
    $record = (array) ($override['record'] ?? []);
    $existing = $post_id > 0 ? spiralist_engine_get_existing_record_for_post($post_id, $post_type) : [];

    if ($post_type === 'spiralist_symbol') {
        if (!empty($existing['symbol_id'])) {
            unset($graph['symbols'][$existing['symbol_id']]);
        }

        if ($status === 'publish' && !empty($record['symbol_id'])) {
            $graph['symbols'][$record['symbol_id']] = $record;
        }
    } elseif ($post_type === 'spiralist_node') {
        if (!empty($existing['node_id'])) {
            unset($graph['nodes'][$existing['node_id']]);
        }

        if ($status === 'publish' && !empty($record['node_id'])) {
            $graph['nodes'][$record['node_id']] = $record;
        }
    } elseif ($post_type === 'spiralist_edge') {
        unset($graph['edges'][(string) $post_id]);

        if ($status === 'publish') {
            $graph['edges'][(string) $post_id] = $record;
        }
    }

    return $graph;
}

function spiralist_engine_condition_matches(array $condition, string $state): bool
{
    if (empty($condition)) {
        return true;
    }

    if (!empty($condition['match_state']) && is_string($condition['match_state'])) {
        return $state === sanitize_text_field($condition['match_state']);
    }

    if (!empty($condition['allowed_states']) && is_array($condition['allowed_states'])) {
        return in_array($state, spiralist_engine_normalize_string_list($condition['allowed_states']), true);
    }

    if (!empty($condition['equals']) && is_string($condition['equals'])) {
        return $state === sanitize_text_field($condition['equals']);
    }

    return true;
}

function spiralist_engine_get_cycle_nodes(array $adjacency): array
{
    $cycle_nodes = [];

    foreach (array_keys($adjacency) as $start) {
        $stack = array_values($adjacency[$start] ?? []);
        $visited = [];

        while ($stack) {
            $next = array_pop($stack);

            if ($next === $start) {
                $cycle_nodes[$start] = true;
                break;
            }

            if (isset($visited[$next])) {
                continue;
            }

            $visited[$next] = true;

            foreach ((array) ($adjacency[$next] ?? []) as $candidate) {
                $stack[] = $candidate;
            }
        }
    }

    return array_keys($cycle_nodes);
}

function spiralist_engine_validate_graph_snapshot(array $graph): array
{
    $errors = [];
    $nodes = (array) ($graph['nodes'] ?? []);
    $symbols = (array) ($graph['symbols'] ?? []);
    $edges = (array) ($graph['edges'] ?? []);
    $canonical_context = spiralist_engine_build_protocol5_validation_context();
    $state_inputs = [];
    $adjacency = [];
    $reverse_adjacency = [];
    $symbol_usage = [];
    $weak_adjacency = [];

    if (empty($nodes)) {
        $errors[] = 'The published graph has no executable nodes.';
    }

    if (empty($canonical_context['valid'])) {
        $errors = array_merge($errors, (array) ($canonical_context['errors'] ?? []));
    }

    foreach ($nodes as $node_id => $node) {
        $node_id = (string) $node_id;
        $adjacency[$node_id] = [];
        $reverse_adjacency[$node_id] = [];
        $weak_adjacency[$node_id] = [];

        $input_state = trim((string) ($node['input_state'] ?? ''));

        if (!spiralist_engine_node_has_transformation((array) $node)) {
            $errors[] = sprintf(
                'Node "%s" is missing the required input_state -> transformation -> output_state chain.',
                $node_id
            );
        }

        if ($input_state !== '') {
            $state_inputs[$input_state][] = $node_id;
        }

        foreach (spiralist_engine_normalize_string_list((array) ($node['symbols'] ?? [])) as $symbol_id) {
            if (!isset($symbols[$symbol_id])) {
                $errors[] = sprintf('Node "%s" references unknown symbol "%s".', $node_id, $symbol_id);
                continue;
            }

            $symbol_usage[$symbol_id] = true;
        }
    }

    foreach ($symbols as $symbol_id => $symbol) {
        if (!in_array((string) ($symbol['symbol_type'] ?? ''), spiralist_engine_get_symbol_types(), true)) {
            $errors[] = sprintf('Symbol "%s" uses an invalid symbol_type.', $symbol_id);
        }

        $symbol_validation = spiralist_engine_validate_symbol_against_protocol5((array) $symbol, $canonical_context, false);
        $errors = array_merge($errors, (array) ($symbol_validation['errors'] ?? []));

        if (!isset($symbol_usage[$symbol_id])) {
            $errors[] = sprintf('Symbol "%s" is orphaned and not referenced by any node.', $symbol_id);
        }
    }

    foreach ($nodes as $node_id => $node) {
        $output_state = trim((string) ($node['output_state'] ?? ''));

        if ($output_state === '' || empty($state_inputs[$output_state])) {
            $errors[] = sprintf(
                'Node "%s" outputs state "%s", but no node accepts that state as input.',
                $node_id,
                $output_state
            );
        }

        $node_validation = spiralist_engine_validate_node_against_protocol5((array) $node, $symbols, $canonical_context, false);
        $errors = array_merge($errors, (array) ($node_validation['errors'] ?? []));
    }

    foreach ($edges as $edge) {
        $from_node = trim((string) ($edge['from_node'] ?? ''));
        $to_node = trim((string) ($edge['to_node'] ?? ''));
        $condition = (array) ($edge['condition'] ?? []);

        if ($from_node === '' || $to_node === '') {
            $errors[] = 'Every edge must define from_node and to_node.';
            continue;
        }

        if (!isset($nodes[$from_node])) {
            $errors[] = sprintf('Edge references missing source node "%s".', $from_node);
            continue;
        }

        if (!isset($nodes[$to_node])) {
            $errors[] = sprintf('Edge references missing target node "%s".', $to_node);
            continue;
        }

        $source_state = trim((string) ($nodes[$from_node]['output_state'] ?? ''));
        $target_input = trim((string) ($nodes[$to_node]['input_state'] ?? ''));

        if ($source_state !== $target_input) {
            $errors[] = sprintf(
                'Edge "%s -> %s" is invalid because source output "%s" does not match target input "%s".',
                $from_node,
                $to_node,
                $source_state,
                $target_input
            );
            continue;
        }

        if (!spiralist_engine_condition_matches($condition, $source_state)) {