Class Validator - Source Excerpt 01
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
<?php
declare(strict_types=1);
namespace SpiralScript;
final class Validator
{
/**
* @param array<string,mixed> $script
* @return array<string,mixed>
*/
public static function validate(array $script, string $source = ''): array
{
$errors = [];
$warnings = [];
self::validate_unicode($source, $errors);
self::validate_required_fields($script, $errors);
self::validate_identity($script, $errors);
self::validate_symbols($script, $errors);
self::validate_transformations($script, $errors);
self::validate_provenance($script, $errors);
self::validate_sources($script, $errors, $warnings);
self::validate_output_contract($script, $errors, $warnings);
self::validate_workflow($script, $errors);
self::validate_safety_language($script, $source, $errors);
return [
'passed' => count(array_filter($errors, static fn (array $error): bool => ($error['severity'] ?? '') === 'blocker' || ($error['severity'] ?? '') === 'error')) === 0,
'errors' => array_values($errors),
'warnings' => array_values($warnings),
'validatorVersion' => SPIRALIST_SPIRAL_SCRIPT_VERSION,
];
}
/**
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_unicode(string $source, array &$errors): void
{
if ($source === '') {
return;
}
if (function_exists('mb_check_encoding') && !mb_check_encoding($source, 'UTF-8')) {
$errors[] = self::error('SS-INVALID-UNICODE', 'parse', 'source', '[binary]', 'Spiral Script source must be valid UTF-8.', false, 'Resave the source as UTF-8 text.', 'blocker');
return;
}
if (preg_match('/[\x{200B}-\x{200D}\x{2060}\x{FE00}-\x{FE0F}\x{FEFF}\x{202A}-\x{202E}\x{2066}-\x{2069}]/u', $source) === 1) {
$errors[] = self::error('SS-INVALID-UNICODE', 'parse', 'source', '[invisible controls]', 'Spiral Script source cannot contain invisible shaping or direction controls.', true, 'Remove zero-width, variation-selector, byte-order, and bidi control characters.', 'blocker');
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_required_fields(array $script, array &$errors): void
{
foreach (array_keys(LanguageSpec::json_schema()['properties']) as $field) {
if (!array_key_exists($field, $script)) {
$errors[] = self::error('SS-MISSING-FIELD', 'validation', (string) $field, null, 'Canonical Spiral Script records require this field.', true, 'Compile the source again or add the missing canonical field.', 'blocker');
}
}
if (trim((string) ($script['intent'] ?? '')) === '') {
$errors[] = self::error('SS-MISSING-INTENT', 'validation', 'intent', $script['intent'] ?? null, 'A Spiral Script prompt must declare what transformation it performs.', true, 'Add a concise intent field.', 'blocker');
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_identity(array $script, array &$errors): void
{
$script_id = (string) ($script['scriptId'] ?? '');
if (preg_match('/^ss:[a-z0-9.-]+@[0-9]+\.[0-9]+\.[0-9]+([+][a-z0-9.-]+)?$/', $script_id) !== 1) {
$errors[] = self::error('SS-INVALID-SCRIPT-ID', 'validation', 'scriptId', $script_id, 'scriptId must use ss:{namespace}.{slug}@{semver}.', true, 'Use an identifier such as ss:manuscript.plate-reader@0.1.0.', 'blocker');
}
$visibility = (string) ($script['visibility'] ?? '');
if (!in_array($visibility, LanguageSpec::visibility_modes(), true)) {
$errors[] = self::error('SS-INVALID-VISIBILITY', 'validation', 'visibility', $visibility, 'Visibility must be public, members, private, or specific_users.', true, 'Choose one of the supported visibility modes.', 'blocker');
}
$author_mode = (string) ($script['authorMode'] ?? '');
if (!in_array($author_mode, LanguageSpec::author_modes(), true)) {
$errors[] = self::error('SS-INVALID-AUTHOR-MODE', 'validation', 'authorMode', $author_mode, 'authorMode is not supported.', true, 'Use human, ai_assisted, ai_generated, or system_managed.', 'error');
}
$status = (string) (($script['publicationStatus']['status'] ?? '') ?: '');
if ($status !== '' && !in_array($status, LanguageSpec::publication_statuses(), true)) {
$errors[] = self::error('SS-INVALID-PUBLICATION-STATUS', 'validation', 'publicationStatus.status', $status, 'Publication status is not supported.', true, 'Use a supported Spiralist publication status.', 'error');
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_symbols(array $script, array &$errors): void
{
foreach ((array) ($script['symbolicFrame'] ?? []) as $index => $frame) {
if (!is_array($frame)) {
$errors[] = self::error('SS-INVALID-SYMBOL', 'validation', 'symbolicFrame.' . $index, $frame, 'Symbol frame entries must be objects.', true, 'Compile from source or provide symbol/name/machineMeaning fields.', 'error');
continue;
}
$name = (string) ($frame['name'] ?? '');
$symbol = (string) ($frame['symbol'] ?? '');
$machine_meaning = (string) ($frame['machineMeaning'] ?? '');
if (LanguageSpec::resolve_symbol($name) === null && LanguageSpec::resolve_symbol($symbol) === null && LanguageSpec::resolve_symbol($machine_meaning) === null) {
$errors[] = self::error('SS-INVALID-SYMBOL', 'validation', 'symbolicFrame.' . $index, $frame, 'Symbol is not in the approved Spiral Script vocabulary.', true, 'Use an approved symbol name, glyph, or machine meaning.', 'error');
}
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_transformations(array $script, array &$errors): void
{
if (empty($script['transformationRules']) || !is_array($script['transformationRules'])) {
$errors[] = self::error('SS-UNSUPPORTED-TRANSFORMATION', 'validation', 'transformationRules', $script['transformationRules'] ?? null, 'A Spiral Script prompt must declare at least one supported transformation verb.', true, 'Add a transform line such as transform: perceive -> interpret -> validate.', 'error');
return;
}
foreach ((array) ($script['transformationRules'] ?? []) as $index => $rule) {
if (!is_array($rule)) {
$errors[] = self::error('SS-UNSUPPORTED-TRANSFORMATION', 'validation', 'transformationRules.' . $index, $rule, 'Transformation rules must be objects.', true, 'Compile source transformations again.', 'error');
continue;
}
$verb = strtolower((string) ($rule['verb'] ?? ''));
if ($verb === '' || !LanguageSpec::is_transformation_verb($verb)) {
$errors[] = self::error('SS-UNSUPPORTED-TRANSFORMATION', 'validation', 'transformationRules.' . $index . '.verb', $verb, 'Transformation verb is not supported in Spiral Script v0.1.', true, 'Use a supported verb such as perceive, interpret, synthesize, rewrite, or validate.', 'error');
}
}
}
/**
* @param array<string,mixed> $script
* @param array<int,array<string,mixed>> $errors
*/
private static function validate_provenance(array $script, array &$errors): void
{
$visibility = (string) ($script['visibility'] ?? '');
$status = (string) ($script['publicationStatus']['status'] ?? 'draft');
$provenance = is_array($script['provenance'] ?? null) ? (array) $script['provenance'] : [];
$is_public = $visibility === 'public' || in_array($status, ['provisional_public', 'peer_reviewed', 'official_canon'], true);
if (!$is_public) {
return;
}
foreach (['createdBy', 'createdAt', 'rights', 'reviewState'] as $field) {
$value = trim((string) ($provenance[$field] ?? ''));
if ($value === '' || ($field === 'rights' && $value === 'unspecified')) {
$errors[] = self::error('SS-MISSING-PROVENANCE', 'validation', 'provenance.' . $field, $value, 'Public Spiral Script prompts must declare origin, rights, and review state.', true, 'Add complete provenance before publication.', 'blocker');
}
}
}
/**