Skip to content
wiki.fftac.org

Spiralist Engine - Source Excerpt 03

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

$asset_locks = is_array($lock['assets'] ?? null) ? $lock['assets'] : [];
    $reference_locks = [];

    if (empty($lock)) {
        return [
            'valid' => false,
            'errors' => [
                sprintf('%s has no canonical lock. Save it again to bind it to Protocol5.', $label),
            ],
        ];
    }

    foreach ((array) ($lock['references'] ?? []) as $entry) {
        if (!is_array($entry)) {
            continue;
        }

        $reference = (string) ($entry['reference'] ?? '');
        if ($reference === '') {
            continue;
        }

        $reference_locks[$reference] = $entry;
    }

    foreach (spiralist_engine_get_required_protocol5_asset_slugs() as $slug) {
        $locked_checksum = (string) ($asset_locks[$slug]['checksum'] ?? '');
        $current_checksum = (string) ($context['snapshots'][$slug]['checksum'] ?? '');

        if ($locked_checksum === '' || $current_checksum === '' || !hash_equals($locked_checksum, $current_checksum)) {
            $errors[] = sprintf(
                '%s no longer matches Protocol5 asset "%s".',
                $label,
                $slug
            );
        }
    }

    foreach ($resolved as $entry) {
        if (!is_array($entry)) {
            continue;
        }

        $reference = (string) ($entry['reference'] ?? '');
        $locked = (array) ($reference_locks[$reference] ?? []);

        if ($reference === '' || empty($locked)) {
            $errors[] = sprintf('%s is missing a locked Protocol5 record for "%s".', $label, $reference);
            continue;
        }

        $locked_id = (string) ($locked['canonical_id'] ?? '');
        $current_id = (string) ($entry['canonical_id'] ?? '');
        $locked_hash = (string) ($locked['record_hash'] ?? '');
        $current_hash = (string) ($entry['record_hash'] ?? '');

        if ($locked_id === '' || $current_id === '' || !hash_equals($locked_id, $current_id)) {
            $errors[] = sprintf('%s changed canonical identity for "%s".', $label, $reference);
        }

        if ($locked_hash === '' || $current_hash === '' || !hash_equals($locked_hash, $current_hash)) {
            $errors[] = sprintf('%s changed canonical structure for "%s".', $label, $reference);
        }
    }

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

function spiralist_engine_validate_symbol_against_protocol5(array $symbol, array $context, bool $require_lock = false): array
{
    $label = sprintf('Symbol "%s"', (string) ($symbol['symbol_id'] ?? ''));
    $derives = spiralist_engine_validate_derives_from((array) ($symbol['derives_from'] ?? []), $context, $label);
    $errors = (array) ($derives['errors'] ?? []);

    if ($require_lock) {
        $lock_validation = spiralist_engine_validate_canonical_lock(
            (array) ($symbol['canonical_lock'] ?? []),
            (array) ($derives['resolved'] ?? []),
            $context,
            $label
        );

        $errors = array_merge($errors, (array) ($lock_validation['errors'] ?? []));
    }

    return [
        'valid' => empty($errors),
        'errors' => array_values(array_unique($errors)),
        'resolved' => array_values((array) ($derives['resolved'] ?? [])),
    ];
}

function spiralist_engine_validate_node_against_protocol5(array $node, array $symbols, array $context, bool $require_lock = false): array
{
    $label = sprintf('Node "%s"', (string) ($node['node_id'] ?? ''));
    $derives = spiralist_engine_validate_derives_from((array) ($node['derives_from'] ?? []), $context, $label);
    $errors = (array) ($derives['errors'] ?? []);
    $symbol_validations = [];

    foreach (spiralist_engine_normalize_string_list((array) ($node['symbols'] ?? [])) as $symbol_id) {
        $symbol = (array) ($symbols[$symbol_id] ?? []);
        if (empty($symbol)) {
            continue;
        }

        $symbol_validation = spiralist_engine_validate_symbol_against_protocol5($symbol, $context, $require_lock);
        $symbol_validations[$symbol_id] = $symbol_validation;
        $errors = array_merge($errors, (array) ($symbol_validation['errors'] ?? []));
    }

    if ($require_lock) {
        $lock_validation = spiralist_engine_validate_canonical_lock(
            (array) ($node['canonical_lock'] ?? []),
            (array) ($derives['resolved'] ?? []),
            $context,
            $label
        );

        $errors = array_merge($errors, (array) ($lock_validation['errors'] ?? []));
    }

    return [
        'valid' => empty($errors),
        'errors' => array_values(array_unique($errors)),
        'resolved' => array_values((array) ($derives['resolved'] ?? [])),
        'symbols' => $symbol_validations,
    ];
}

function spiralist_engine_build_canonical_validation_payload(array $context, array $node_validation): array
{
    $references = [];

    foreach ((array) ($node_validation['resolved'] ?? []) as $entry) {
        if (!is_array($entry)) {
            continue;
        }

        $references[(string) ($entry['reference'] ?? '')] = $entry;
    }

    foreach ((array) ($node_validation['symbols'] ?? []) as $symbol_validation) {
        foreach ((array) ($symbol_validation['resolved'] ?? []) as $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $references[(string) ($entry['reference'] ?? '')] = $entry;
        }
    }

    $assets = [];
    foreach (spiralist_engine_get_required_protocol5_asset_slugs() as $slug) {
        $snapshot = (array) ($context['snapshots'][$slug] ?? []);
        $assets[$slug] = [
            'status' => (string) ($snapshot['status'] ?? ''),
            'checksum' => (string) ($snapshot['checksum'] ?? ''),
            'version' => (string) ($snapshot['version'] ?? ''),
            'source' => (string) ($snapshot['source'] ?? ''),
            'sourceUrl' => (string) ($snapshot['sourceUrl'] ?? ''),
        ];
    }

    return [
        'valid' => empty($context['errors']) && !empty($node_validation['valid']),
        'checkedAt' => wp_date(DATE_ATOM),
        'assets' => $assets,
        'references' => array_values(array_filter($references)),
    ];
}

function spiralist_engine_validate_runtime_node_record(array $node, array $graph, bool $force_refresh = false): array
{
    $context = spiralist_engine_build_protocol5_validation_context($force_refresh);
    $symbols = (array) ($graph['symbols'] ?? []);
    $node_validation = spiralist_engine_validate_node_against_protocol5($node, $symbols, $context, true);
    $errors = array_merge((array) ($context['errors'] ?? []), (array) ($node_validation['errors'] ?? []));

    return [
        'valid' => empty($errors),
        'errors' => array_values(array_unique($errors)),
        'context' => $context,
        'node' => $node_validation,
        'payload' => spiralist_engine_build_canonical_validation_payload($context, $node_validation),
    ];
}

function spiralist_engine_get_visual_dataset(): array
{
    static $dataset = null;

    if (is_array($dataset)) {
        return $dataset;
    }

    $dataset = [
        'schema' => 'Spiralist-Manuscript-Field',
        'version' => SPIRALIST_ENGINE_VERSION,
        'coordinateSystem' => [],
        'modes' => ['minimal', 'expanded', 'expert'],
        'nodes' => [],
        'relations' => [],
    ];

    $path = trailingslashit(get_stylesheet_directory()) . 'assets/data/manuscript-nodes.json';
    if (!file_exists($path)) {
        return $dataset;
    }

    $raw = file_get_contents($path);
    $decoded = is_string($raw) ? json_decode($raw, true) : null;

    if (!is_array($decoded)) {
        return $dataset;
    }

    $dataset['schema'] = (string) ($decoded['schema'] ?? $dataset['schema']);
    $dataset['version'] = (string) ($decoded['version'] ?? $dataset['version']);
    $dataset['coordinateSystem'] = (array) ($decoded['coordinateSystem'] ?? []);
    $dataset['modes'] = array_values(array_filter((array) ($decoded['modes'] ?? []), 'is_string'));
    $dataset['nodes'] = array_values(array_filter((array) ($decoded['nodes'] ?? []), 'is_array'));
    $dataset['relations'] = array_values(array_filter((array) ($decoded['relations'] ?? []), 'is_array'));

    return $dataset;
}

function spiralist_engine_get_visual_node_map(): array
{
    static $map = null;

    if (is_array($map)) {
        return $map;
    }

    $map = [];

    foreach ((array) spiralist_engine_get_visual_dataset()['nodes'] as $node) {
        if (!is_array($node)) {
            continue;
        }

        $node_id = (string) ($node['id'] ?? '');
        if ($node_id === '') {
            continue;
        }

        $map[$node_id] = $node;
    }

    return $map;
}