Files
Jonathan Miller 4cc3f5bee4
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
style: fix all PHPCS PSR-12 violations across 74 files (7539 → 0 errors)
- Convert tabs to spaces (3,413 violations)
- Fix line endings, trailing whitespace, brace placement
- Break lines exceeding 150-char absolute limit
- Replace heredoc tab closers with spaces
- Fix empty elseif, forbidden function calls
- Update phpcs.xml: exclude rules inappropriate for CLI scripts
  (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder,
  empty catch blocks)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-24 17:07:51 -05:00

548 lines
15 KiB
PHP

<?php
declare(strict_types=1);
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.Validation
* INGROUP: MokoStandards.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/UnifiedValidation.php
* BRIEF: Unified validation framework
*/
/**
* Unified Validation Framework for MokoStandards
*
* Consolidates all validation logic into a single framework with plugins.
* Replaces 12+ individual validator scripts with a unified approach.
*
* Features:
* - Plugin-based architecture for extensibility
* - Path validation (files and directories)
* - Markdown validation
* - License header validation
* - Workflow validation
* - Security validation integration
* - Custom validation rules
* - Error aggregation and reporting
*
* Example usage:
* ```php
* $validator = new UnifiedValidator();
* $validator->addPlugin(new PathValidatorPlugin());
* $validator->addPlugin(new MarkdownValidatorPlugin());
*
* $context = [
* 'paths' => ['/tmp', '/usr'],
* 'markdown_files' => ['README.md']
* ];
*
* $results = $validator->validateAll($context);
* $validator->printSummary();
* ```
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @package MokoStandards\Enterprise
* @version 04.00.04
* @author MokoStandards Team
* @license GPL-3.0-or-later
*/
namespace MokoEnterprise;
use Exception;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* Result of a validation check
*/
class ValidationResult
{
public string $pluginName;
public bool $passed;
public string $message;
public array $details;
public function __construct(string $pluginName, bool $passed, string $message = '', array $details = [])
{
$this->pluginName = $pluginName;
$this->passed = $passed;
$this->message = $message;
$this->details = $details;
}
public function __toString(): string
{
$status = $this->passed ? '✓ PASS' : '✗ FAIL';
return "{$status} [{$this->pluginName}] {$this->message}";
}
}
/**
* Abstract base class for validation plugins
*/
abstract class ValidationPlugin
{
protected string $name;
protected bool $enabled = true;
public function __construct(string $name)
{
$this->name = $name;
}
/**
* Perform validation
*
* @param array<string, mixed> $context Validation context with data to validate
* @return ValidationResult Result indicating pass/fail with details
*/
abstract public function validate(array $context): ValidationResult;
public function enable(): void
{
$this->enabled = true;
}
public function disable(): void
{
$this->enabled = false;
}
public function isEnabled(): bool
{
return $this->enabled;
}
public function getName(): string
{
return $this->name;
}
}
/**
* Validates file and directory paths
*/
class PathValidatorPlugin extends ValidationPlugin
{
public function __construct()
{
parent::__construct('path_validator');
}
public function validate(array $context): ValidationResult
{
$paths = $context['paths'] ?? [];
if (empty($paths)) {
return new ValidationResult($this->name, true, 'No paths to validate');
}
$invalidPaths = [];
foreach ($paths as $path) {
if (!file_exists($path)) {
$invalidPaths[] = $path;
}
}
if (!empty($invalidPaths)) {
return new ValidationResult(
$this->name,
false,
sprintf('Found %d invalid paths', count($invalidPaths)),
['invalid_paths' => $invalidPaths]
);
}
return new ValidationResult($this->name, true, sprintf('All %d paths valid', count($paths)));
}
}
/**
* Validates Markdown files
*/
class MarkdownValidatorPlugin extends ValidationPlugin
{
public function __construct()
{
parent::__construct('markdown_validator');
}
public function validate(array $context): ValidationResult
{
$files = $context['markdown_files'] ?? [];
if (empty($files)) {
return new ValidationResult($this->name, true, 'No Markdown files to validate');
}
$issues = [];
foreach ($files as $filePath) {
if (!file_exists($filePath)) {
continue;
}
$content = file_get_contents($filePath);
// Check for broken links
if (strpos($content, '](404') !== false || strpos($content, '](broken') !== false) {
$issues[] = "{$filePath}: Potential broken links";
}
}
if (!empty($issues)) {
return new ValidationResult(
$this->name,
false,
sprintf('Found %d issues', count($issues)),
['issues' => $issues]
);
}
return new ValidationResult($this->name, true, sprintf('Validated %d Markdown files', count($files)));
}
}
/**
* Validates license headers
*/
class LicenseValidatorPlugin extends ValidationPlugin
{
public function __construct()
{
parent::__construct('license_validator');
}
public function validate(array $context): ValidationResult
{
$files = $context['source_files'] ?? [];
if (empty($files)) {
return new ValidationResult($this->name, true, 'No source files to validate');
}
$missingLicense = [];
$expectedCopyright = $context['copyright_year'] ?? '2026';
foreach ($files as $filePath) {
if (!file_exists($filePath)) {
continue;
}
try {
$content = file_get_contents($filePath);
if (strpos($content, 'Copyright') === false || strpos($content, $expectedCopyright) === false) {
$missingLicense[] = $filePath;
}
} catch (Exception $e) {
// Skip files that can't be read
}
}
if (!empty($missingLicense)) {
return new ValidationResult(
$this->name,
false,
sprintf('%d files missing proper license headers', count($missingLicense)),
['files' => $missingLicense]
);
}
return new ValidationResult($this->name, true, sprintf('All %d files have license headers', count($files)));
}
}
/**
* Validates GitHub Actions workflows
*/
class WorkflowValidatorPlugin extends ValidationPlugin
{
public function __construct()
{
parent::__construct('workflow_validator');
}
public function validate(array $context): ValidationResult
{
$workflowDir = $context['workflow_dir']
?? (is_dir('.mokogitea/workflows') ? '.mokogitea/workflows' : '.github/workflows');
if (!is_dir($workflowDir)) {
return new ValidationResult($this->name, true, 'No workflows directory');
}
// Collect workflows from primary dir; also check the other platform dir
$workflows = array_merge(
glob($workflowDir . '/*.yml') ?: [],
glob($workflowDir . '/*.yaml') ?: []
);
$altDir = ($workflowDir === '.mokogitea/workflows') ? '.github/workflows' : '.mokogitea/workflows';
if (is_dir($altDir)) {
$workflows = array_merge(
$workflows,
glob($altDir . '/*.yml') ?: [],
glob($altDir . '/*.yaml') ?: []
);
}
if (empty($workflows)) {
return new ValidationResult($this->name, true, 'No workflow files found');
}
$issues = [];
foreach ($workflows as $workflow) {
$content = file_get_contents($workflow);
// Basic checks
if (strpos($content, 'on:') === false && strpos($content, 'on :') === false) {
$issues[] = basename($workflow) . ": Missing 'on:' trigger";
}
}
if (!empty($issues)) {
return new ValidationResult(
$this->name,
false,
sprintf('Found %d workflow issues', count($issues)),
['issues' => $issues]
);
}
return new ValidationResult($this->name, true, sprintf('Validated %d workflows', count($workflows)));
}
}
/**
* Validates security concerns
*/
class SecurityValidatorPlugin extends ValidationPlugin
{
public function __construct()
{
parent::__construct('security_validator');
}
public function validate(array $context): ValidationResult
{
$scanDir = $context['scan_dir'] ?? 'scripts';
if (!is_dir($scanDir)) {
return new ValidationResult($this->name, true, 'No directory to scan');
}
try {
$validator = new SecurityValidator();
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($scanDir)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$validator->scanFile($file->getPathname());
}
}
$findings = $validator->getFindings();
$critical = array_filter($findings, function ($f) {
return in_array($f['severity'] ?? '', ['critical', 'high'], true);
});
if (!empty($critical)) {
return new ValidationResult(
$this->name,
false,
sprintf('Found %d critical security issues', count($critical)),
['critical_count' => count($critical), 'total_count' => count($findings)]
);
}
return new ValidationResult(
$this->name,
true,
sprintf('Security scan complete: %d total findings, 0 critical', count($findings))
);
} catch (Exception $e) {
return new ValidationResult($this->name, true, 'Security validator error (skipped): ' . $e->getMessage());
}
}
}
/**
* Unified validation framework
*/
class UnifiedValidator
{
private const VERSION = '04.06.00';
/** @var array<string, ValidationPlugin> */
private array $plugins = [];
/** @var array<int, ValidationResult> */
private array $results = [];
/**
* Add a validation plugin
*
* @param ValidationPlugin $plugin Plugin instance
*/
public function addPlugin(ValidationPlugin $plugin): void
{
$this->plugins[$plugin->getName()] = $plugin;
error_log("Added plugin: {$plugin->getName()}");
}
/**
* Remove a validation plugin
*
* @param string $pluginName Name of plugin to remove
*/
public function removePlugin(string $pluginName): void
{
if (isset($this->plugins[$pluginName])) {
unset($this->plugins[$pluginName]);
error_log("Removed plugin: {$pluginName}");
}
}
/**
* Get a plugin by name
*
* @param string $pluginName Name of plugin
* @return ValidationPlugin|null Plugin instance or null
*/
public function getPlugin(string $pluginName): ?ValidationPlugin
{
return $this->plugins[$pluginName] ?? null;
}
/**
* Run all enabled validation plugins
*
* @param array<string, mixed> $context Validation context data
* @return array<int, ValidationResult> List of validation results
*/
public function validateAll(array $context = []): array
{
$this->results = [];
error_log("Running " . count($this->plugins) . " validation plugins...");
foreach ($this->plugins as $pluginName => $plugin) {
if (!$plugin->isEnabled()) {
error_log("Skipping disabled plugin: {$pluginName}");
continue;
}
try {
error_log("Running plugin: {$pluginName}");
$result = $plugin->validate($context);
$this->results[] = $result;
} catch (Exception $e) {
error_log("Plugin {$pluginName} failed: {$e->getMessage()}");
$this->results[] = new ValidationResult(
$pluginName,
false,
"Plugin error: {$e->getMessage()}"
);
}
}
return $this->results;
}
/**
* Get validation results
*
* @param bool $passedOnly Return only passed results
* @param bool $failedOnly Return only failed results
* @return array<int, ValidationResult> List of validation results
*/
public function getResults(bool $passedOnly = false, bool $failedOnly = false): array
{
if ($passedOnly) {
return array_filter($this->results, fn($r) => $r->passed);
}
if ($failedOnly) {
return array_filter($this->results, fn($r) => !$r->passed);
}
return $this->results;
}
/**
* Check if all validations passed
*
* @return bool True if all validations passed
*/
public function allPassed(): bool
{
foreach ($this->results as $result) {
if (!$result->passed) {
return false;
}
}
return true;
}
/**
* Print validation summary
*/
public function printSummary(): void
{
echo "\n" . str_repeat('=', 60) . "\n";
echo "Unified Validation Summary\n";
echo str_repeat('=', 60) . "\n";
$passed = array_filter($this->results, fn($r) => $r->passed);
$failed = array_filter($this->results, fn($r) => !$r->passed);
echo "\nTotal: " . count($this->results) . " validations\n";
echo "Passed: " . count($passed) . "\n";
echo "Failed: " . count($failed) . "\n";
if (!empty($passed)) {
echo "\n✓ Passed (" . count($passed) . "):\n";
foreach ($passed as $result) {
echo " {$result}\n";
}
}
if (!empty($failed)) {
echo "\n✗ Failed (" . count($failed) . "):\n";
foreach ($failed as $result) {
echo " {$result}\n";
if (!empty($result->details)) {
foreach ($result->details as $key => $value) {
if (is_array($value) && count($value) <= 3) {
echo " {$key}: " . implode(', ', $value) . "\n";
} elseif (is_array($value)) {
echo " {$key}: " . count($value) . " items\n";
} else {
echo " {$key}: {$value}\n";
}
}
}
}
}
$status = $this->allPassed() ? '✓ ALL VALIDATIONS PASSED' : '✗ SOME VALIDATIONS FAILED';
echo "\n{$status}\n";
echo str_repeat('=', 60) . "\n\n";
}
public function getVersion(): string
{
return self::VERSION;
}
}