Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<?php
namespace App\RSpade\CodeQuality\Support;
#[Instantiatable]
class CacheManager
{
private string $cache_dir;
private array $memory_cache = [];
public function __construct(string $cache_dir = null)
{
if ($cache_dir === null) {
// Try to use Laravel's storage_path if available
if (function_exists('storage_path')) {
$this->cache_dir = storage_path('rsx-tmp/cache/code-quality');
} else {
// Fall back to a default location
$this->cache_dir = '/var/www/html/storage/rsx-tmp/cache/code-quality';
}
} else {
$this->cache_dir = $cache_dir;
}
if (!is_dir($this->cache_dir)) {
mkdir($this->cache_dir, 0755, true);
}
}
public function get_sanitized_file(string $file_path): ?array
{
// Check memory cache first
if (isset($this->memory_cache[$file_path])) {
return $this->memory_cache[$file_path];
}
$cache_key = $this->get_cache_key($file_path);
$cache_file = $this->cache_dir . '/' . $cache_key . '.json';
if (!file_exists($cache_file)) {
return null;
}
$file_mtime = filemtime($file_path);
$cache_mtime = filemtime($cache_file);
// Cache is stale
if ($file_mtime > $cache_mtime) {
unlink($cache_file);
return null;
}
$data = json_decode(file_get_contents($cache_file), true);
$this->memory_cache[$file_path] = $data;
return $data;
}
public function set_sanitized_file(string $file_path, array $data): void
{
$this->memory_cache[$file_path] = $data;
$cache_key = $this->get_cache_key($file_path);
$cache_file = $this->cache_dir . '/' . $cache_key . '.json';
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
}
public function clear(): void
{
$this->memory_cache = [];
$files = glob($this->cache_dir . '/*.json');
foreach ($files as $file) {
unlink($file);
}
}
public function clear_file(string $file_path): void
{
unset($this->memory_cache[$file_path]);
$cache_key = $this->get_cache_key($file_path);
$cache_file = $this->cache_dir . '/' . $cache_key . '.json';
if (file_exists($cache_file)) {
unlink($cache_file);
}
}
private function get_cache_key(string $file_path): string
{
// Create a unique cache key based on file path
// Use hash to avoid filesystem issues with long paths
return md5($file_path) . '_' . basename($file_path, '.php');
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\RSpade\CodeQuality\Support;
class FileSanitizer
{
/**
* Get PHP content with comments removed
* This ensures we don't match patterns inside comments
* Uses PHP tokenizer to properly strip comments (from line 711 of monolith)
*/
public static function sanitize_php(string $content): array
{
// Use PHP tokenizer to properly strip comments
$tokens = token_get_all($content);
$lines = [];
$current_line = '';
foreach ($tokens as $token) {
if (is_array($token)) {
$token_type = $token[0];
$token_content = $token[1];
// Skip comment tokens
if ($token_type === T_COMMENT || $token_type === T_DOC_COMMENT) {
// Add empty lines to preserve line numbers
$comment_lines = explode("\n", $token_content);
foreach ($comment_lines as $idx => $comment_line) {
if ($idx === 0 && $current_line !== '') {
// First line of comment - complete current line
$lines[] = $current_line;
$current_line = '';
} elseif ($idx > 0) {
// Additional comment lines
$lines[] = '';
}
}
} else {
// Add non-comment content
$content_parts = explode("\n", $token_content);
foreach ($content_parts as $idx => $part) {
if ($idx > 0) {
$lines[] = $current_line;
$current_line = $part;
} else {
$current_line .= $part;
}
}
}
} else {
// Single character tokens
$current_line .= $token;
}
}
// Add the last line if any
if ($current_line !== '' || count($lines) === 0) {
$lines[] = $current_line;
}
$sanitized_content = implode("\n", $lines);
return [
'content' => $sanitized_content,
'lines' => $lines,
'original_lines' => explode("\n", $content),
];
}
/**
* Get sanitized JavaScript content for checking
* Removes comments and string contents to avoid false positives
* Uses external Node.js script (from line 769 of monolith)
*/
public static function sanitize_javascript(string $file_path): array
{
// Create cache directory if it doesn't exist
if (function_exists('storage_path')) {
$cache_dir = storage_path('rsx-tmp/cache/js-sanitized');
} else {
$cache_dir = '/var/www/html/storage/rsx-tmp/cache/js-sanitized';
}
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Generate cache path based on relative file path
$base_path = function_exists('base_path') ? base_path() : '/var/www/html';
$relative_path = str_replace($base_path . '/', '', $file_path);
$cache_path = $cache_dir . '/' . str_replace('/', '_', $relative_path) . '.sanitized';
// Check if cache is valid
if (file_exists($cache_path)) {
$source_mtime = filemtime($file_path);
$cache_mtime = filemtime($cache_path);
if ($cache_mtime >= $source_mtime) {
// Cache is valid, return cached content
$sanitized_content = file_get_contents($cache_path);
return [
'content' => $sanitized_content,
'lines' => explode("\n", $sanitized_content),
'original_lines' => explode("\n", file_get_contents($file_path)),
];
}
}
// Run sanitizer to generate fresh cache
$sanitizer_path = $base_path . '/bin/js-sanitizer.js';
$command = sprintf('node %s %s 2>/dev/null',
escapeshellarg($sanitizer_path),
escapeshellarg($file_path)
);
$sanitized = shell_exec($command);
// If sanitization failed, fall back to original content
if ($sanitized === null || $sanitized === '') {
$sanitized = file_get_contents($file_path);
}
// Save to cache
file_put_contents($cache_path, $sanitized);
return [
'content' => $sanitized,
'lines' => explode("\n", $sanitized),
'original_lines' => explode("\n", file_get_contents($file_path)),
];
}
/**
* Sanitize file based on extension
* Note: PHP takes content, JavaScript takes file_path (matching monolith behavior)
*/
public static function sanitize(string $file_path, ?string $content = null): array
{
$extension = pathinfo($file_path, PATHINFO_EXTENSION);
if ($extension === 'php') {
if ($content === null) {
$content = file_get_contents($file_path);
}
return self::sanitize_php($content);
} elseif (in_array($extension, ['js', 'jsx', 'ts', 'tsx'])) {
// JavaScript sanitization needs file path, not content
return self::sanitize_javascript($file_path);
} else {
// For other files, return as-is
if ($content === null) {
$content = file_get_contents($file_path);
}
return [
'content' => $content,
'lines' => explode("\n", $content),
'original_lines' => explode("\n", $content),
];
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\RSpade\CodeQuality\Support;
/**
* Provides shared initialization method suggestions based on code location
*/
class InitializationSuggestions
{
/**
* Get appropriate lifecycle method suggestions based on file path
*
* @param string $file_path The path to the JavaScript file
* @return string Formatted suggestion text
*/
public static function get_suggestion(string $file_path): string
{
$is_framework_code = str_contains($file_path, '/app/RSpade/');
$is_user_code = str_contains($file_path, '/rsx/');
if ($is_framework_code) {
return "Use framework lifecycle methods in ES6 classes:\n" .
" - _on_framework_core_define() - For core framework metadata\n" .
" - _on_framework_core_init() - For core framework initialization\n" .
" - _on_framework_module_define() - For framework module metadata\n" .
" - _on_framework_module_init() - For framework module initialization\n" .
"These methods are automatically called by the RSpade JS runtime.";
} elseif ($is_user_code) {
return "Use ES6 class lifecycle methods:\n" .
" - on_app_ready() - For final initialization (most common)\n" .
" - on_jqhtml_ready() - After all JQHTML components loaded\n" .
" - on_app_init() - For app-level setup\n" .
" - on_modules_init() - For module initialization\n" .
" - on_modules_define() - For module metadata registration\n" .
"These methods are automatically called by the RSpade JS runtime.";
} else {
return "Create an ES6 class with static lifecycle methods. These will be called automatically by the RSpade JS runtime.";
}
}
/**
* Get framework-only method suggestions
*
* @return string Formatted suggestion text for framework methods
*/
public static function get_framework_suggestion(): string
{
return "Use framework lifecycle methods instead:\n" .
" - _on_framework_core_define() - For core framework metadata\n" .
" - _on_framework_core_init() - For core framework initialization\n" .
" - _on_framework_module_define() - For framework module metadata\n" .
" - _on_framework_module_init() - For framework module initialization";
}
/**
* Get user-only method suggestions
*
* @return string Formatted suggestion text for user methods
*/
public static function get_user_suggestion(): string
{
return "Use user code lifecycle methods instead:\n" .
" - on_app_ready() - For final initialization (most common)\n" .
" - on_jqhtml_ready() - After all JQHTML components loaded\n" .
" - on_app_init() - For app-level setup\n" .
" - on_modules_init() - For module initialization\n" .
" - on_modules_define() - For module metadata registration";
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\RSpade\CodeQuality\Support;
use App\RSpade\CodeQuality\Support\ViolationCollector;
/**
* Shared rule discovery logic for CodeQualityChecker and Manifest
*
* This allows both systems to discover and instantiate rules without
* requiring the CodeQuality directory to be part of the manifest scan.
*/
class RuleDiscovery
{
/**
* Discover and load all code quality rules
*
* @param ViolationCollector $collector The violation collector to pass to rules
* @param array $config Configuration to pass to rules
* @param bool $only_manifest_scan If true, only return rules with is_called_during_manifest_scan() = true
* @param bool $exclude_manifest_scan If true, exclude rules with is_called_during_manifest_scan() = true
* @return array Array of instantiated rule objects
*/
public static function discover_rules(ViolationCollector $collector, array $config = [], bool $only_manifest_scan = false, bool $exclude_manifest_scan = false): array
{
$rules = [];
$rules_dir = base_path('app/RSpade/CodeQuality/Rules');
// Scan Rules directory for rule classes
$rule_files = glob($rules_dir . '/**/*.php', GLOB_BRACE);
foreach ($rule_files as $file_path) {
// Skip abstract rule base class itself
if (str_ends_with($file_path, 'CodeQualityRule_Abstract.php')) {
continue;
}
// Extract class metadata without loading the file
$metadata = static::extract_class_metadata($file_path);
if (!isset($metadata['class'])) {
continue;
}
// Build FQCN
$fqcn = $metadata['fqcn'] ?? null;
if (!$fqcn) {
continue;
}
// Check if it extends CodeQualityRule_Abstract
if (!isset($metadata['extends']) || $metadata['extends'] !== 'CodeQualityRule_Abstract') {
continue;
}
// Load the file
require_once $file_path;
// Check if class exists (sanity check)
if (!class_exists($fqcn)) {
// Try with full namespace if it's not found
// This handles cases where the class is in a subdirectory
$relative_path = str_replace(base_path() . '/', '', $file_path);
$relative_path = str_replace('.php', '', $relative_path);
$relative_path = str_replace('/', '\\', $relative_path);
$fqcn = '\\' . ucfirst($relative_path);
if (!class_exists($fqcn)) {
shouldnt_happen(
"CodeQuality rule class '{$fqcn}' found in file '{$file_path}' but class_exists() failed after require_once"
);
}
}
// Instantiate the rule
$rule = new $fqcn($collector, $config);
// Check if rule is enabled
if (!$rule->is_enabled()) {
continue;
}
// Filter based on manifest scan requirements
$is_manifest_scan_rule = $rule->is_called_during_manifest_scan();
if ($only_manifest_scan && !$is_manifest_scan_rule) {
continue; // Skip non-manifest-scan rules when only wanting manifest-scan rules
}
if ($exclude_manifest_scan && $is_manifest_scan_rule) {
continue; // Skip manifest-scan rules when excluding them (e.g., during rsx:check)
}
$rules[] = $rule;
}
return $rules;
}
/**
* Extract basic class metadata from a PHP file using token parsing
* This is a simplified version that doesn't require Manifest
*
* @param string $file_path Path to the PHP file
* @return array Metadata array with namespace, class, fqcn, extends
*/
protected static function extract_class_metadata(string $file_path): array
{
$content = file_get_contents($file_path);
$tokens = token_get_all($content);
$metadata = [];
$namespace = '';
$class = '';
$extends = '';
$i = 0;
$count = count($tokens);
while ($i < $count) {
// Look for namespace
if ($tokens[$i][0] === T_NAMESPACE) {
$i++;
while ($i < $count && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// PHP 8.4+ uses T_NAME_QUALIFIED for fully qualified names
if ($i < $count && (defined('T_NAME_QUALIFIED') && $tokens[$i][0] === T_NAME_QUALIFIED)) {
$namespace = $tokens[$i][1];
$i++;
} else {
// Fallback for older PHP versions
while ($i < $count && ($tokens[$i][0] === T_STRING || $tokens[$i][0] === T_NS_SEPARATOR)) {
$namespace .= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
$i++;
}
}
}
// Look for class
if ($tokens[$i][0] === T_CLASS) {
$i++;
while ($i < $count && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < $count && $tokens[$i][0] === T_STRING) {
$class = $tokens[$i][1];
$i++;
// Look for extends
while ($i < $count && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < $count && $tokens[$i][0] === T_EXTENDS) {
$i++;
while ($i < $count && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
if ($i < $count && $tokens[$i][0] === T_STRING) {
$extends = $tokens[$i][1];
}
}
break; // Found the class, stop parsing
}
}
$i++;
}
if ($class) {
$metadata['class'] = $class;
$metadata['namespace'] = $namespace;
$metadata['fqcn'] = $namespace ? $namespace . '\\' . $class : $class;
$metadata['extends'] = $extends;
}
return $metadata;
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\RSpade\CodeQuality\Support;
use App\RSpade\CodeQuality\CodeQuality_Violation;
#[Instantiatable]
class ViolationCollector
{
private array $violations = [];
private array $convention_violations = [];
private array $stats = [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0,
'convention' => 0,
];
public function add(CodeQuality_Violation $violation): void
{
if ($violation->severity === 'convention') {
$this->convention_violations[] = $violation;
$this->stats['convention']++;
} else {
$this->violations[] = $violation;
$this->stats[$violation->severity]++;
}
}
public function clear(): void
{
$this->violations = [];
$this->convention_violations = [];
$this->stats = [
'critical' => 0,
'high' => 0,
'medium' => 0,
'low' => 0,
'convention' => 0,
];
}
public function get_all(): array
{
return $this->violations;
}
public function get_by_file(string $file_path): array
{
return array_filter($this->violations, fn ($v) => $v->file_path === $file_path);
}
public function get_by_rule(string $rule_id): array
{
return array_filter($this->violations, fn ($v) => $v->rule_id === $rule_id);
}
public function get_by_severity(string $severity): array
{
return array_filter($this->violations, fn ($v) => $v->severity === $severity);
}
/**
* Get convention violation count
*/
public function get_convention_count(): int
{
return count($this->convention_violations);
}
/**
* Get convention violations only
*/
public function get_convention_violations(): array
{
return $this->convention_violations;
}
/**
* Get convention violations as arrays
*/
public function get_convention_violations_as_arrays(): array
{
$arrays = [];
foreach ($this->convention_violations as $violation) {
$arrays[] = $violation->to_array();
}
return $arrays;
}
public function get_stats(): array
{
return $this->stats;
}
public function get_total_count(): int
{
return count($this->violations);
}
/**
* Get all violations as arrays (for backward compatibility)
*/
public function get_violations_as_arrays(): array
{
$arrays = [];
foreach ($this->violations as $violation) {
$arrays[] = $violation->to_array();
}
return $arrays;
}
/**
* Check if there are convention violations
*/
public function has_convention_violations(): bool
{
return !empty($this->convention_violations);
}
public function has_critical_violations(): bool
{
return $this->stats['critical'] > 0;
}
public function has_violations(): bool
{
return !empty($this->violations);
}
public function merge(ViolationCollector $other): void
{
foreach ($other->get_all() as $violation) {
$this->add($violation);
}
foreach ($other->get_convention_violations() as $violation) {
$this->add($violation);
}
}
public function sort_by_severity(): void
{
usort($this->violations, function ($a, $b) {
$weight_diff = $b->get_severity_weight() - $a->get_severity_weight();
if ($weight_diff !== 0) {
return $weight_diff;
}
// Secondary sort by file and line
$file_cmp = strcmp($a->file_path, $b->file_path);
if ($file_cmp !== 0) {
return $file_cmp;
}
return $a->line_number - $b->line_number;
});
}
}