From 0df844f77f53f42e91e623c7c6bfd8d766903f65 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 9 Dec 2025 05:32:28 +0000 Subject: [PATCH] Add SPA+Route code quality rule, async eval support for rsx:debug Tighten CLAUDE.md from 44KB to 35KB without information loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/RSpade/CodeQuality/CLAUDE.md | 17 ++ .../SpaAttributeMisuse_CodeQualityRule.php | 159 ++++++++++++++++++ .../Commands/Rsx/Route_Debug_Command.php | 7 + bin/route-debug.js | 63 +++---- 4 files changed, 205 insertions(+), 41 deletions(-) create mode 100755 app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php diff --git a/app/RSpade/CodeQuality/CLAUDE.md b/app/RSpade/CodeQuality/CLAUDE.md index 0897a58b2..65c206090 100755 --- a/app/RSpade/CodeQuality/CLAUDE.md +++ b/app/RSpade/CodeQuality/CLAUDE.md @@ -131,6 +131,21 @@ The Code Quality system is a modular, extensible framework for enforcing coding - Suggests placeholder URLs for unimplemented routes - Severity: High +### Manifest Rules (`Rules/Manifest/`) + +1. **SpaAttributeMisuseRule** (PHP-SPA-01) + - Detects #[SPA] combined with #[Route] on same method + - #[SPA] is for bootstrap entry points only, not route definitions + - Routes in SPA modules are defined in JavaScript actions with @route() + - Runs at manifest-time for immediate feedback + - Severity: Critical + +2. **InstanceMethodsRule** (MANIFEST-INST-01) + - Enforces static-only classes unless marked Instantiatable + - Checks both PHP (#[Instantiatable]) and JS (@Instantiatable) + - Walks inheritance chain to check ancestors + - Severity: Medium + ### Sanity Check Rules (`Rules/SanityChecks/`) 1. **PhpSanityCheckRule** (PHP-SC-001) @@ -470,6 +485,8 @@ By default, code quality rules run only when `php artisan rsx:check` is executed Only the following rules are approved for manifest-time execution: - **BLADE-SCRIPT-01** (InlineScriptRule): Prevents inline JavaScript in Blade files (critical architecture violation) - **JQHTML-INLINE-01** (JqhtmlInlineScriptRule): Prevents inline scripts/styles in Jqhtml template files (critical architecture violation) +- **PHP-SPA-01** (SpaAttributeMisuseRule): Prevents combining #[SPA] with #[Route] attributes (critical architecture misunderstanding) +- **MANIFEST-INST-01** (InstanceMethodsRule): Enforces static-only classes unless Instantiatable (framework convention) All other rules should return `false` from `is_called_during_manifest_scan()`. diff --git a/app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php b/app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php new file mode 100755 index 000000000..7efda2198 --- /dev/null +++ b/app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php @@ -0,0 +1,159 @@ + $file_metadata) { + // Skip non-PHP files + if (($file_metadata['extension'] ?? '') !== 'php') { + continue; + } + + // Skip if no public static methods + if (empty($file_metadata['public_static_methods'])) { + continue; + } + + // Check each method + foreach ($file_metadata['public_static_methods'] as $method_name => $method_info) { + $attributes = $method_info['attributes'] ?? []; + + // Check if method has both #[SPA] and #[Route] + $has_spa = isset($attributes['SPA']); + $has_route = isset($attributes['Route']); + + if ($has_spa && $has_route) { + $line = $method_info['line'] ?? 1; + $class_name = $file_metadata['class'] ?? 'Unknown'; + + $this->add_violation( + $file, + $line, + "Method '{$method_name}' has both #[SPA] and #[Route] attributes. These should not be combined.", + "#[SPA]\n#[Route('...')]\npublic static function {$method_name}(...)", + $this->build_suggestion($class_name, $method_name), + 'critical' + ); + } + } + } + } + + /** + * Build detailed suggestion explaining SPA architecture + */ + private function build_suggestion(string $class_name, string $method_name): string + { + $lines = []; + $lines[] = "The #[SPA] and #[Route] attributes serve different purposes and should not be combined."; + $lines[] = ""; + $lines[] = "WHAT #[SPA] DOES:"; + $lines[] = " - Marks a method as an SPA bootstrap entry point"; + $lines[] = " - Returns rsx_view(SPA) which loads the JavaScript SPA shell"; + $lines[] = " - Acts as a catch-all for client-side routing"; + $lines[] = " - Typically ONE per feature/bundle (e.g., Frontend_Spa_Controller::index)"; + $lines[] = ""; + $lines[] = "HOW SPA ROUTING WORKS:"; + $lines[] = " - Routes are defined in JavaScript action classes with @route() decorator"; + $lines[] = " - Example: @route('/contacts') on Contacts_Index_Action.js"; + $lines[] = " - The SPA router matches URLs to actions CLIENT-SIDE"; + $lines[] = " - Server only provides the bootstrap shell, not individual pages"; + $lines[] = ""; + $lines[] = "TO FIX:"; + $lines[] = " 1. Remove the #[Route] attribute from the #[SPA] method"; + $lines[] = " 2. Create JavaScript action classes for each route you need:"; + $lines[] = ""; + $lines[] = " // Example: rsx/app/frontend/contacts/Contacts_Index_Action.js"; + $lines[] = " @route('/contacts')"; + $lines[] = " @layout('Frontend_Layout')"; + $lines[] = " @spa('{$class_name}::{$method_name}')"; + $lines[] = " class Contacts_Index_Action extends Spa_Action { }"; + $lines[] = ""; + $lines[] = "See: php artisan rsx:man spa"; + + return implode("\n", $lines); + } +} diff --git a/app/RSpade/Commands/Rsx/Route_Debug_Command.php b/app/RSpade/Commands/Rsx/Route_Debug_Command.php index daf3d9b0e..2257e5fc6 100755 --- a/app/RSpade/Commands/Rsx/Route_Debug_Command.php +++ b/app/RSpade/Commands/Rsx/Route_Debug_Command.php @@ -548,6 +548,13 @@ class Route_Debug_Command extends Command $this->line(' php artisan rsx:debug /demo --eval="Rsx.is_dev()" --no-body'); $this->line(''); + $this->comment('POST-LOAD INTERACTIONS (click buttons, test modals, etc):'); + $this->line(' php artisan rsx:debug /page --user=1 --eval="$(\'[data-sid=btn_edit]\').click(); await new Promise(r => setTimeout(r, 2000));"'); + $this->line(' # Click button, wait 2s for modal'); + $this->line(' php artisan rsx:debug /form --eval="$(\'#submit\').click(); await new Promise(r => setTimeout(r, 1000));"'); + $this->line(' # Submit form and capture result'); + $this->line(''); + $this->comment('DEBUGGING OUTPUT:'); $this->line(' php artisan rsx:debug / --console # All console output'); $this->line(' php artisan rsx:debug / --console-log # Alias for --console'); diff --git a/bin/route-debug.js b/bin/route-debug.js index 748f4b3f0..87f3efcf4 100755 --- a/bin/route-debug.js +++ b/bin/route-debug.js @@ -50,7 +50,8 @@ function parse_args() { console.log(' --dump-element= Extract and display HTML of specific element'); console.log(' --storage Dump localStorage and sessionStorage contents'); console.log(' --full Enable all display options for maximum info'); - console.log(' --eval= Execute JavaScript code in the page context'); + console.log(' --eval= Execute async JavaScript after page loads (supports await)'); + console.log(' Example: --eval="$(\'[data-sid=btn_edit]\').click(); await new Promise(r => setTimeout(r, 2000));"'); console.log(' --timeout= Navigation timeout in milliseconds (minimum 30000ms, default 30000ms)'); console.log(' --console-debug-filter= Filter console_debug to specific channel'); console.log(' --console-debug-benchmark Include benchmark timing in console_debug'); @@ -638,52 +639,32 @@ function parse_args() { } // Execute eval code if --eval option is passed + // This runs AFTER page is fully loaded and ready, supports async/await if (options.eval_code) { try { - const evalResult = await page.evaluate((code) => { - return new Promise((resolve) => { - // Wrap the eval code in a function - const __eval_func = function() { + const evalResult = await page.evaluate(async (code) => { + try { + // Wrap code in async function to support await + const asyncFunc = new Function('return (async () => { ' + code + ' })()'); + const result = await asyncFunc(); + + // Convert result to string representation + if (result === undefined) { + return 'undefined'; + } else if (result === null) { + return 'null'; + } else if (typeof result === 'object') { try { - // Use eval directly for more complex expressions - const result = eval(code); - - // Convert result to string representation - if (result === undefined) { - return 'undefined'; - } else if (result === null) { - return 'null'; - } else if (typeof result === 'object') { - try { - return JSON.stringify(result, null, 2); - } catch (e) { - return String(result); - } - } else { - return String(result); - } - } catch (error) { - return `Error: ${error.message}`; + return JSON.stringify(result, null, 2); + } catch (e) { + return String(result); } - }; - - // Wait for RSX framework to be fully initialized - if (window.Rsx && window.Rsx.on) { - // Use Rsx._debug_ready event which fires after all initialization - window.Rsx.on('_debug_ready', function() { - resolve(__eval_func()); - }); } else { - // Fallback for non-RSX pages - if (document.readyState === 'complete' || document.readyState === 'interactive') { - setTimeout(function() { resolve(__eval_func()); }, 500); - } else { - document.addEventListener('DOMContentLoaded', function() { - setTimeout(function() { resolve(__eval_func()); }, 500); - }); - } + return String(result); } - }); + } catch (error) { + return `Error: ${error.message}`; + } }, options.eval_code); console.log('\nJavaScript Eval Result:');