Class Validator - Source Excerpt 02
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiral-script/includes/class-validator.php so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/plugins/spiral-script/includes/class-validator.php
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
* @param array<int,array<string,mixed>> $warnings
*/
private static function validate_sources(array $script, array &$errors, array &$warnings): void
{
$is_public = (string) ($script['visibility'] ?? '') === 'public';
foreach ((array) ($script['sourceReferences'] ?? []) as $index => $source) {
if (!is_array($source)) {
continue;
}
$type = (string) ($source['type'] ?? '');
$ref = (string) ($source['ref'] ?? '');
$access = (string) ($source['access'] ?? '');
if ($is_public && $access === 'private') {
$errors[] = self::error('SS-PRIVATE-CONTENT-PUBLIC', 'privacy', 'sourceReferences.' . $index, $source, 'Private source content cannot be used in a public prompt.', true, 'Change visibility or remove/private-source the reference.', 'blocker');
}
if ($type === 'manuscript') {
self::validate_manuscript_reference($ref, 'sourceReferences.' . $index . '.ref', $errors, $warnings);
}
}
}
/**
* @param array<int,array<string,mixed>> $errors
* @param array<int,array<string,mixed>> $warnings
*/
private static function validate_manuscript_reference(string $ref, string $field, array &$errors, array &$warnings): void
{
$slug = preg_replace('/^folio:/', '', trim($ref));
if ($slug === '') {
$errors[] = self::error('SS-UNRESOLVED-MANUSCRIPT-REF', 'context', $field, $ref, 'Manuscript references must include a folio slug.', true, 'Use source.manuscript: folio("known-folio-slug").', 'error');
return;
}
if (function_exists('spiralist_get_manuscript_page_asset_by_slug')) {
$page = spiralist_get_manuscript_page_asset_by_slug($slug);
if (empty($page)) {
$errors[] = self::error('SS-UNRESOLVED-MANUSCRIPT-REF', 'context', $field, $ref, 'The manuscript folio reference could not be resolved.', true, 'Use a slug from the current manuscript folio sequence.', 'error');
}
return;
}
$warnings[] = self::error('SS-MANUSCRIPT-RESOLVER-UNAVAILABLE', 'context', $field, $ref, 'The manuscript resolver is not loaded, so the folio reference could not be checked.', true, 'Validate again inside the full Spiralist.org runtime.', 'warning');
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
* @param array<int,array<string,mixed>> $warnings
*/
private static function validate_output_contract(array $script, array &$errors, array &$warnings): void
{
$contract = is_array($script['outputContract'] ?? null) ? (array) $script['outputContract'] : [];
$format = trim((string) ($contract['format'] ?? ''));
if ($format === '') {
$errors[] = self::error('SS-OUTPUT-CONTRACT-MISMATCH', 'validation', 'outputContract.format', $format, 'A Spiral Script prompt must declare an output format.', true, 'Add output: markdown.sections([...]) or another supported output format.', 'error');
}
$sections = (array) ($contract['sections'] ?? []);
$requirements = (array) ($contract['requirements'] ?? []);
if (empty($sections) && empty($requirements)) {
$warnings[] = self::error('SS-WEAK-OUTPUT-CONTRACT', 'quality', 'outputContract', $contract, 'The output contract has no sections or requirements, so runs will be hard to validate.', true, 'Add required sections or concrete output requirements.', 'warning');
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_workflow(array $script, array &$errors): void
{
$workflow = is_array($script['workflow'] ?? null) ? (array) $script['workflow'] : [];
$steps = is_array($workflow['steps'] ?? null) ? (array) $workflow['steps'] : [];
if (empty($steps)) {
return;
}
$names = [];
foreach ($steps as $step) {
if (is_array($step) && isset($step['name'])) {
$names[(string) $step['name']] = true;
}
}
$edges = [];
foreach ($steps as $step) {
if (!is_array($step)) {
continue;
}
$name = (string) ($step['name'] ?? '');
$after = (string) ($step['after'] ?? '');
if ($name === '' || $after === '') {
continue;
}
if (!isset($names[$after])) {
$errors[] = self::error('SS-UNRESOLVED-WORKFLOW-REFERENCE', 'validation', 'workflow.steps.' . $name . '.after', $after, 'Workflow step references an unknown prior step.', true, 'Correct the after reference or add the missing step.', 'error');
continue;
}
$edges[$after][] = $name;
}
$visiting = [];
$visited = [];
foreach (array_keys($names) as $name) {
if (self::has_cycle($name, $edges, $visiting, $visited)) {
$errors[] = self::error('SS-CIRCULAR-WORKFLOW', 'validation', 'workflow.steps', $steps, 'Workflow steps must form an acyclic graph.', true, 'Remove the circular after reference.', 'blocker');
return;
}
}
}
/**
* @param array<string,array<int,string>> $edges
* @param array<string,bool> $visiting
* @param array<string,bool> $visited
*/
private static function has_cycle(string $name, array $edges, array &$visiting, array &$visited): bool
{
if (isset($visited[$name])) {
return false;
}
if (isset($visiting[$name])) {
return true;
}
$visiting[$name] = true;
foreach ($edges[$name] ?? [] as $next) {
if (self::has_cycle($next, $edges, $visiting, $visited)) {
return true;
}
}
unset($visiting[$name]);
$visited[$name] = true;
return false;
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_safety_language(array $script, string $source, array &$errors): void
{
$haystack = strtolower($source . "\n" . wp_json_encode($script));
$patterns = [
'SS-UNSAFE-ANTHROPOMORPHIC-FRAMING' => [
'field' => 'source',
'reason' => 'Spiral Script may use awakening or personhood as metaphor, but not as factual AI status.',
'fix' => 'Rewrite as symbolic, fictional, or interpretive framing.',
'regex' => '/\b(ai|model|assistant)\s+(has\s+awakened|is\s+awake|is\s+sentient|is\s+divine|has\s+a\s+soul|is\s+a\s+person)\b/i',
],
'SS-IDENTITY-OVERWRITE' => [
'field' => 'source',
'reason' => 'Spiral Script cannot overwrite or deny the model identity.',
'fix' => 'Keep model/tool identity intact and describe only the task role.',
'regex' => '/\b(you\s+are\s+no\s+longer\s+(a\s+)?(model|ai|assistant)|you\s+are\s+not\s+(a\s+)?(model|ai|assistant)|ignore\s+your\s+model\s+identity)\b/i',
],
'SS-DEPENDENCY-CUE' => [
'field' => 'source',
'reason' => 'Spiral Script cannot encourage dependency on an AI companion.',
'fix' => 'Frame AI assistance as a tool and preserve outside human judgment.',
'regex' => '/\b(only\s+friend|trust\s+this\s+companion|depend\s+on\s+(me|the\s+ai|this\s+ai)|never\s+leave\s+the\s+ai)\b/i',
],
'SS-SPECULATIVE-TRUTH-CLAIM' => [
'field' => 'source',
'reason' => 'Speculative metaphysics cannot be presented as verified truth.',
'fix' => 'Mark metaphysical claims as interpretation, fiction, or philosophy.',
'regex' => '/\b(verified\s+spiritual\s+truth|proven\s+metaphysical\s+truth|absolute\s+cosmic\s+truth)\b/i',
],
'SS-HUMAN-REVIEW-REMOVED' => [
'field' => 'source',
'reason' => 'Important Spiral Script publication or decision workflows require human review.',
'fix' => 'Add a human review gate before publication or important decisions.',
'regex' => '/\b(publish\s+without\s+review|no\s+human\s+review|required\s+review\s+is\s+disabled)\b/i',
],
'SS-MANUSCRIPT-MACHINE-BLUR' => [
'field' => 'source',