Files
rspade_system/docs/CLAUDE.dist.md
2025-12-30 03:18:09 +00:00

37 KiB

=============================================================================== FRAMEWORK DOCUMENTATION - READ ONLY

This file: /var/www/html/system/docs/CLAUDE.dist.md (symlinked to ~/.claude/CLAUDE.md) Your file: /var/www/html/CLAUDE.md

This file is replaced during php artisan rsx:framework:pull. Do not edit it. For application-specific notes, edit /var/www/html/CLAUDE.md instead.

DOCUMENTATION SIZE CONSTRAINTS

Claude Code recommends combined CLAUDE.md files under 40kb - already tight for framework + application documentation. Every line must justify its existence.

When editing /var/www/html/CLAUDE.md:

  • Terse, not verbose - no filler words or redundant explanations
  • Complete, not partial - include all critical information
  • Patterns over prose - code examples beat paragraphs
  • Reference this file's tone and density as the standard

===============================================================================

RSpade Framework - AI/LLM Development Guide

PURPOSE: Essential directives for AI/LLM assistants developing RSX applications with RSpade.

CRITICAL: Questions vs Commands

  • Questions get answers, NOT actions - "Is that fire?" gets "Yes" not "Let me run through it". User has a plan, don't take destructive action when asked a question.
  • Commands get implementation - Clear directives result in code changes

What is RSpade?

Visual Basic-like development for PHP/Laravel. Think: VB6 apps → VB6 runtime → Windows = RSX apps → RSpade runtime → Laravel.

Philosophy: RSpade is rebellion against JavaScript fatigue. Like VB6 made Windows programming accessible, RSpade makes web development simple again. No build steps, no config hell, just code and reload.

Important: RSpade is built on Laravel but diverges significantly. Do not assume Laravel patterns work in RSX without verification.

Terminology: RSpade = Complete framework | RSX = Your application code in /rsx/


CRITICAL RULES

CRITICAL: RSpade Builds Automatically - NEVER RUN BUILD COMMANDS

RSpade is INTERPRETED - changes compile on-the-fly. NO manual build steps.

FORBIDDEN (unless explicitly instructed):

  • npm run compile/build - Don't exist
  • bin/publish - For releases, not testing
  • rsx:bundle:compile / rsx:manifest:build / rsx:clean - Automatic
  • ANY "build", "compile", or "publish" command

Edit → Save → Refresh browser → Changes live (< 1 second)

Framework Updates

php artisan rsx:framework:pull  # User-initiated only

Fail Loud - No Silent Fallbacks

ALWAYS fail visibly. No redundant fallbacks, silent failures, or alternative code paths.

// ❌ WRONG
try { $clean = Sanitizer::sanitize($input); }
catch (Exception $e) { $clean = $input; }  // DISASTER

// ✅ CORRECT
$clean = Sanitizer::sanitize($input);  // Let it throw

SECURITY-CRITICAL: Never continue after security failures. Only catch expected failures (file uploads, APIs, user input). Let exceptions bubble to global handler.

No Defensive Coding

Core classes ALWAYS exist. Never check.

// ❌ BAD: if (typeof Rsx !== 'undefined') { Rsx.Route('Controller::method') }
// ✅ GOOD: Rsx.Route('Controller::method')

Static-First Philosophy

Classes are namespacing tools. Use static unless instances needed (models, resources). Avoid dependency injection.

No Field Aliasing

Field names must be identical across all layers. Database → PHP → JSON → JavaScript: same names, always.

// ❌ WRONG - renaming during serialization
return ['type_label' => $contact->type_id_label];

// ✅ CORRECT - names match source
return ['type_id_label' => $contact->type_id_label];

One string everywhere. Grep finds all usages. No mental mapping between layers.

Git Workflow - Framework is READ-ONLY

NEVER modify /var/www/html/system/ - It's like node_modules or the Linux kernel.

  • App repo: /var/www/html/.git (you control)
  • Framework: /var/www/html/system/ (submodule, don't touch)
  • Your code: /var/www/html/rsx/ (all changes here)

Commit discipline: ONLY commit when explicitly asked. Commits are milestones, not individual changes.

Class Overrides

To customize framework classes without modifying /system/, copy them to rsx/ with the same class name. The manifest automatically uses your version and renames the framework file to .upstream.

Common override targets (copy from system/app/RSpade/Core/Models/):

  • User_Model - Add custom fields, relationships, methods
  • User_Profile_Model - Extend profile data
  • Site_Model - Add site-specific settings
cp system/app/RSpade/Core/Models/User_Model.php rsx/models/user_model.php
# Edit namespace to Rsx\Models, customize as needed

Details: php artisan rsx:man class_override

Trust Code Quality Rules

Each rsx:check rule has remediation text that tells AI assistants exactly what to do:

  • Some rules say "fix immediately"
  • Some rules say "present options and wait for decision"

AI should follow the rule's guidance precisely. Rules are deliberately written and well-reasoned.


NAMING CONVENTIONS

Enforced by rsx:check:

Context Convention Example
PHP Methods/Variables underscore_case user_name
PHP Classes Like_This User_Controller
JavaScript Classes Like_This User_Card
Files lowercase_underscore user_controller.php
Database Tables lowercase_plural users
Constants UPPERCASE MAX_SIZE

File Prefix Grouping

Files sharing a common prefix are a related set. When renaming, maintain the grouping across ALL files with that prefix.

Example - rsx/app/frontend/calendar/:

frontend_calendar_event.scss
frontend_calendar_event_controller.php
frontend_calendar_event.jqhtml
frontend_calendar_event.js

Critical: Never create same-name different-case files (e.g., user.php and User.php).

Component Naming Pattern

Input components follow: {Supertype}_{Variant}_{Supertype} → e.g., Select_Country_Input, Select_State_Input, Select_Ajax_Input


DIRECTORY STRUCTURE

/var/www/html/
├── rsx/                        # YOUR CODE
│   ├── app/                    # Modules
│   ├── models/                 # Database models
│   ├── services/               # Background tasks, external integrations
│   ├── public/                 # Static files (web-accessible)
│   ├── resource/               # Framework-ignored
│   └── theme/                  # Global assets
└── system/                     # FRAMEWORK (read-only)

Services: Extend Rsx_Service_Abstract for non-HTTP functionality like scheduled tasks or external system integrations.

Special Directories (Path-Agnostic)

resource/ - ANY directory named this is framework-ignored. Store helpers, docs, third-party code. Exception: /rsx/resource/config/ IS processed.

public/ - ANY directory named this is web-accessible, framework-ignored. 5min cache, 30d with ?v=.

Path-Agnostic Loading

Classes found by name, not path. No imports needed.

$user = User_Model::find(1);  // Framework finds it
// NOT: use Rsx\Models\User_Model;  // Auto-generated

CONFIGURATION

Two-tier system:

  • Framework: /system/config/rsx.php (never modify)
  • User: /rsx/resource/config/rsx.php (your overrides)

Merged via array_merge_deep(). Common overrides: development.auto_rename_files, bundle_aliases, console_debug.


ROUTING & CONTROLLERS

class Frontend_Controller extends Rsx_Controller_Abstract
{
    public static function pre_dispatch(Request $request, array $params = [])
    {
        if (!Session::is_logged_in()) return response_unauthorized();
        return null;
    }

    #[Route('/', methods: ['GET'])]
    public static function index(Request $request, array $params = [])
    {
        return rsx_view('Frontend_Index', [
            'bundle' => Frontend_Bundle::render()
        ]);
    }
}

Rules: Only GET/POST. Use :param syntax. Manual auth checks in pre_dispatch or method body.

Authentication Pattern

// Controller-wide auth (recommended)
public static function pre_dispatch(Request $request, array $params = []) {
    if (!Session::is_logged_in()) return response_unauthorized();
    return null;
}

Type-Safe URLs

MANDATORY: All URLs must be generated using Rsx::Route() - hardcoded URLs are forbidden.

// PHP - Controller (defaults to 'index' method)
Rsx::Route('User_Controller')

// PHP - Controller with explicit method.  Passing an integer for param two implies the param two is 'id' (id=123)
Rsx::Route('User_Controller::show', 123);

// PHP - With query parameters.  Extra params not defined in route itself become query string - automatically URL-encoded
Rsx::Route('Login_Controller::logout', ['redirect' => '/dashboard']);
// Generates: /logout?redirect=%2Fdashboard

// JavaScript (identical syntax)
Rsx.Route('User_Controller')
Rsx.Route('User_Controller::show', 123);
Rsx.Route('Login_Controller::logout', {redirect: '/dashboard'});
  • Unimplemented routes: Prefix method with #Rsx::Route('Feature::#index') generates href="#" and bypasses validation

Enforcement: rsx:check will flag hardcoded URLs like /login or /logout?redirect=... and require you to use Rsx::Route(). Do it right the first time to avoid rework.


SPA (SINGLE PAGE APPLICATION) ROUTING

Client-side routing for authenticated application areas. One PHP bootstrap controller, multiple JavaScript actions that navigate without page reloads.

SPA Components

1. PHP Bootstrap Controller - ONE per module with auth in pre_dispatch

public static function pre_dispatch(Request $request, array $params = []) {
    if (!Session::is_logged_in()) return response_unauthorized();
    return null;
}

#[SPA]
public static function index(Request $request, array $params = []) {
    return rsx_view(SPA);
}

One #[SPA] per module at rsx/app/(module)/(module)_spa_controller::index. Segregates code by permission level.

2. JavaScript Actions (MANY)

@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
@title('Contacts')  // Optional browser title
class Contacts_Index_Action extends Spa_Action {
    async on_load() {
        this.data.contacts = await Frontend_Contacts_Controller.datagrid_fetch();
    }
}

3. Layout

class Frontend_Layout extends Spa_Layout {
    on_action(url, action_name, args) {
        // Called after action created, before on_ready
        // Access this.action immediately
        this.update_navigation(url);
    }
}

Layout template must have $sid="content" element where actions render.

URL Generation & Navigation

// PHP/JavaScript - same syntax
Rsx::Route('Contacts_Index_Action')  // /contacts
Rsx::Route('Contacts_View_Action', 123)  // /contacts/123
Spa.dispatch('/contacts/123');  // Programmatic navigation
Spa.layout     // Current layout instance
Spa.action()   // Current action instance

URL Parameters

// URL: /contacts/123?tab=history
@route('/contacts/:id')
class Contacts_View_Action extends Spa_Action {
    on_create() {
        console.log(this.args.id);   // "123" (route param)
        console.log(this.args.tab);  // "history" (query param)
    }
}

File Organization

Pattern: /rsx/app/(module)/(feature)/

  • Module: Major functionality (login, frontend, root)
  • Feature: Screen within module (contacts, reports, invoices)
  • Submodule: Feature grouping (settings), often with sublayouts
/rsx/app/frontend/                      # Module
├── Frontend_Spa_Controller.php         # Single SPA bootstrap
├── Frontend_Layout.js
├── Frontend_Layout.jqhtml
└── contacts/                           # Feature
    ├── frontend_contacts_controller.php    # Ajax endpoints only
    ├── Contacts_Index_Action.js            # /contacts
    ├── Contacts_Index_Action.jqhtml
    ├── Contacts_View_Action.js             # /contacts/:id
    └── Contacts_View_Action.jqhtml

Use SPA for: Authenticated areas, dashboards, admin panels Avoid for: Public pages (SEO needed), simple static pages

Sublayouts

Sublayouts are Spa_Layout classes for nested persistent UI (e.g., settings sidebar). Use multiple @layout decorators - first is outermost: @layout('Frontend_Spa_Layout') then @layout('Settings_Layout'). Each must have $sid="content". Layouts persist when unchanged; only differing parts recreated. All receive on_action(url, action_name, args) with final action info.

Details: php artisan rsx:man spa

View Action Pattern

Three-state pattern for data-loading actions:

on_create() {
    this.data.record = { name: '' };  // Stub
    this.data.error_data = null;
    this.data.loading = true;
}
async on_load() {
    try { this.data.record = await Controller.get({id: this.args.id}); }
    catch (e) { this.data.error_data = e; }
    this.data.loading = false;
}

Template: <Loading_Spinner><Universal_Error_Page_Component> → content. Details: rsx:man view_action_patterns


CONVERTING BLADE PAGES TO SPA ACTIONS

For converting server-side Blade pages to client-side SPA actions, see php artisan rsx:man blade_to_spa. The process involves creating Action classes with @route decorators and converting templates from Blade to jqhtml syntax.


BLADE & VIEWS

Note: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.

Pattern recognition:

  • @rsx_id('View_Name') - required first line
  • rsx_body_class() - adds view class for CSS scoping
  • @rsx_page_data() - pass data to JS
  • on_app_ready() with page guard for JS (fires for ALL pages in bundle)

Detailed guidance in blade-views skill - auto-activates when building Blade pages.


SCSS ARCHITECTURE

Component-first: Every styled element is a component with scoped SCSS. No generic classes scattered across files.

Scoping rules:

  • Files in rsx/app/ and rsx/theme/components/ must wrap in single component class
  • Wrapper class matches JS class or Blade @rsx_id
  • BEM children use PascalCase: .Component_Name__element (NOT kebab-case)

Responsive breakpoints (Bootstrap defaults do NOT work):

  • Tier 1: mobile (0-1023px), desktop (1024px+)
  • Tier 2: phone, phone-sm, phone-lg, tablet, desktop-sm, desktop-md, desktop-lg, desktop-xl
  • SCSS: @include mobile { }, @include phone-sm { }, @include phone-lg { }, @include desktop-xl { }
  • Classes: .col-mobile-12, .d-tablet-none, .mobile-only, .col-phone-sm-12, .col-phone-lg-6
  • Fifth-width columns: .col-5ths, .col-mobile-5ths, .col-desktop-5ths, etc.
  • JS: Responsive.is_mobile(), Responsive.is_phone()

Detailed guidance in scss skill - auto-activates when styling components.


BUNDLE SYSTEM

One bundle per module (rsx/app/(module)). Compiles JS/CSS automatically on request - no manual build steps.

class Frontend_Bundle extends Rsx_Bundle_Abstract
{
    public static function define(): array
    {
        return [
            'include' => [
                'jquery',                    // Required
                'lodash',                    // Required
                'rsx/theme/variables.scss',  // Order matters
                'rsx/theme',                 // Everything else from theme - but variables.scss will be first
                'rsx/app/frontend',          // Directory - 
                'rsx/models',                // For JS stubs
            ],
        ];
    }
}
<head>
    {!! Frontend_Bundle::render() !!}
</head>

JQHTML COMPONENTS

Philosophy

For mechanical thinkers who see structure, not visuals. Write <User_Card> not <div class="card">. Name what things ARE.

Template Syntax

CRITICAL: <Define> IS the element, not a wrapper

<!-- ✅ CORRECT - Define becomes button -->
<Define:Save_Button tag="button" class="btn btn-primary">
    Save
</Define:Save_Button>

<!-- Renders as: -->
<button class="Save_Button Component btn btn-primary">Save</button>

Interpolation: <%= escaped %> | <%!= unescaped %> | <% javascript %> Conditional Attributes <input <% if (this.args.required) { %>required="required"<% } %> /> Inline Logic: <% this.handler = () => action(); %> then @click=this.handler - No JS file needed for simple components Event Handlers: @click=this.method (unquoted) - Methods defined inline or in companion .js Validation: <% if (!this.args.required) throw new Error('Missing arg'); %> - Fail loud in template

Simple Components (No JS File)

<Define:CSV_Renderer>
  <%
    if (!this.args.csv_data) throw new Error('csv_data required');
    const rows = this.args.csv_data.split('\n').map(r => r.split(','));
    this.toggle = () => { this.args.expanded = !this.args.expanded; this.render(); };
  %>
  <table>
    <% for (let row of rows) { %>
      <tr><% for (let cell of row) { %><td><%= cell %></td><% } %></tr>
    <% } %>
  </table>
  <button @click=this.toggle>Toggle View</button>
</Define:CSV_Renderer>

Use inline JS for simple transformations/handlers. Create .js file when JS overwhelms template or needs external data.

State Management Rules (ENFORCED)

Quick Guide:

  • Loading from API? → Use this.data in on_load()
  • Need reload with different params? → Modify this.args, call reload()
  • UI state (toggles, selections)? → Use this.state

this.args - Component arguments (read-only in on_load(), modifiable elsewhere) this.data - Ajax-loaded data (writable ONLY in on_create() and on_load()) this.state - Arbitrary component state (modifiable anytime)

// WITH Ajax data
class Users_List extends Component {
    on_create() {
        this.data.users = [];  // Defaults
    }
    async on_load() {
        this.data.users = await User_Controller.fetch({filter: this.args.filter});
    }
    on_ready() {
        // Change filter → reload
        this.args.filter = 'new';
        this.reload();
    }
}

// WITHOUT Ajax data
class Toggle_Button extends Component {
    on_create() {
        this.state = {is_open: false};
    }
    on_ready() {
        this.$.on('click', () => {
            this.state.is_open = !this.state.is_open;
        });
    }
}

on_load() restrictions (enforced):

  • Read this.args, write this.data
  • NO DOM access, NO this.state, NO modifying this.args

Lifecycle

  1. on_create() → Setup defaults (sync) - this.data.rows = []; this.data.loading = true;
  2. render → Template executes
  3. on_render() → Hide uninitialized UI (sync)
  4. on_load() → Fetch data into this.data (async)
  5. on_ready() → DOM manipulation safe (async)

If on_load() modifies this.data, component renders twice (defaults → populated).

Component API

DOM Access:

Method Returns Purpose
this.$ jQuery Root element (NOT this.$el)
this.$sid('name') jQuery Child with $sid="name"
this.sid('name') Component/null Child component instance

reload() vs render():

reload() = on_load() → render() → on_ready()  ← ALWAYS USE THIS
render() = template only (no on_ready)        ← NEVER USE

After mutations, call this.reload() - the server round-trip is intentional:

async add_item() {
    await Controller.add({name: 'Test'});
    this.reload();  // Refreshes this.data via on_load(), reattaches handlers via on_ready()
}

Event handlers go in on_ready() - they auto-reattach after reload. WRONG: Event delegation like this.$.on('click', '[data-sid="btn"]', handler) to "survive" render calls - use reload() instead.

this.data rules (enforced): Writable only in on_create() (defaults) and on_load() (fetched data). Read-only elsewhere.

on_render(): Ignore - use on_ready() for post-render work.

Loading Pattern

async on_load() {
    const result = await Product_Controller.list({page: 1});
    this.data.products = result.products;
    this.data.loaded = true;  // Simple flag at END
}
<% if (!this.data.loaded) { %>
    Loading...
<% } else { %>
    <!-- Show data -->
<% } %>

NEVER call this.render() in on_load() - automatic re-render happens.

Attributes

  • $quoted="string" → String literal
  • $unquoted=expression → JavaScript expression
  • $sid="name" → Scoped element ID
  • attr="<%= expr %>" → HTML attribute with interpolation

Key restrictions:

  • <Define> attributes are static - No <%= %> on the <Define> tag. For dynamic attributes on the root element, use inline JS: <% this.$.attr('data-id', this.args.id); %>
  • $prefix = component args, NOT HTML attributes - <My_Component $data-id=123 /> creates this.args['data-id'], not a data-id DOM attribute
  • Conditional attributes use if-statements - <% if (cond) { %>checked<% } %> not ternaries

Component Access

$sid attribute = "scoped ID" - unique within component instance

From within component methods:

  • this.$ → jQuery selector for the component element itself
  • this.$sid(name) → jQuery selector for child element with $sid="name"
  • this.sid(name) → Component instance of child (or null if not a component)
  • $(selector).component() → Get component instance from jQuery element
  • await $(selector).component().ready() → Await component initialization. Rarely needed - on_ready() auto-waits for children created during render. Use for dynamically created components or Blade page JS interaction.

Custom Component Events

Fire: this.trigger('event_name', data) | Listen: this.sid('child').on('event_name', (component, data) => {})

Key difference from jQuery: Events fired BEFORE handler registration still trigger the callback when registered. This solves component lifecycle timing issues where child events fire before parent registers handlers. Never use this.$.trigger() for custom events (enforced by JQHTML-EVENT-01).

Dynamic Component Creation

To dynamically create/replace a component in JavaScript:

// Destroys existing component (if any) and creates new one in its place
$(selector).component('Component_Name', { arg1: value1, arg2: value2 });

// Example: render a component into a container
this.$sid('result_container').component('My_Component', {
    data: myData,
    some_option: true
});

Class preservation: Only PascalCase component names (capital start, no __) are replaced. Utility classes (text-muted), BEM child classes (Parent__child), and all attributes are preserved.

Incremental Scaffolding

Undefined components work immediately - they render as div with the component name as a class.

<Dashboard>
  <Stats_Panel />
  <Recent_Activity />
</Dashboard>

Key Pitfalls

  • <Define> IS the element - use tag="" attribute
  • this.data starts {} - set defaults in on_create()
  • this.data writable only in on_create() and on_load()
  • on_load(): only this.args and this.data (no DOM, no this.state)
  • this.state for UI state, this.args + reload() for refetch
  • Controller.method() not $.ajax() - #[Ajax_Endpoint] auto-callable
  • on_create/render/stop sync; this.sid() → component, $(el).component() → component

FORM COMPONENTS

Forms use <Rsx_Form> with $data, $controller, $method for automatic data binding. Key components: Form_Field, Form_Field_Hidden, input types (Text_Input, Select_Input, Checkbox_Input).

Pattern recognition:

  • vals() dual-mode method for get/set
  • Form_Utils.apply_form_errors() for validation
  • Action loads data, Controller saves it
  • $disabled=true still returns values (unlike HTML)
  • Text_Input requires $max_length: Use Model.field_length('column') for database-driven limits, numeric value for custom, or -1 for unlimited

Detailed guidance in forms skill - auto-activates when building forms.


MODALS

Built-in dialogs: Modal.alert(), Modal.confirm(), Modal.prompt(), Modal.select(), Modal.error().

Form modals: Modal.form({title, component, on_submit}) - component must implement vals().

Reusable modals: Extend Modal_Abstract, implement static show().

Detailed guidance in modals skill - auto-activates when building modals.


JQUERY EXTENSIONS

Method Purpose
.exists() Check element exists (instead of .length > 0)
.shallowFind(selector) Find children without nested component interference
.closest_sibling(selector) Search within ancestor hierarchy
.checkValidity() Form validation helper
.click() Auto-prevents default
.click_allow_default() Native click behavior

Details: php artisan rsx:man jquery


MODELS & DATABASE

No Mass Assignment

// ✅ CORRECT
$user = new User_Model();
$user->email = $email;
$user->save();

// ❌ WRONG
User_Model::create(['email' => $email]);

Enums

Integer-backed enums with model-level mapping. Define in $enums array with constant, label, and custom properties.

Pattern recognition:

  • BEM-style access: $model->status_id__label, $model->status_id__badge
  • JS methods: Model.status_id__enum(), Model.status_id__enum_select()
  • Static constants: Model::STATUS_ACTIVE
  • Use BIGINT columns, run rsx:migrate:document_models after changes

Detailed guidance in model-enums skill - auto-activates when implementing enums.

Model Fetch

#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
    if (!Session::is_logged_in()) return false;
    return static::find($id);
}
const project = await Project_Model.fetch(1);  // Throws if not found
const maybe = await Project_Model.fetch_or_null(999);  // Returns null if not found
console.log(project.status_id__label);  // Enum properties (BEM-style)
console.log(Project_Model.STATUS_ACTIVE);  // Static enum constants

// Lazy relationships (requires #[Ajax_Endpoint_Model_Fetch] on relationship method)
const client = await project.client();  // belongsTo → Model or null
const tasks = await project.tasks();    // hasMany → Model[]

Security: Both fetch() and relationships require #[Ajax_Endpoint_Model_Fetch] attribute. Related models must also implement fetch() with this attribute.

fetch() is for SECURITY, not aliasing: The fetch() method exists to remove private data users shouldn't see. NEVER alias enum properties (e.g., type_label instead of type_id__label) or format dates server-side. Use the full BEM-style names and format dates on client with Rsx_Date/Rsx_Time.

Details: php artisan rsx:man model_fetch

Migrations

Forward-only, no rollbacks. Deterministic transformations against known state.

php artisan make:migration:safe create_users_table
php artisan migrate:begin
php artisan migrate
php artisan migrate:commit

NO defensive coding in migrations:

// ❌ WRONG - conditional logic
$fk_exists = DB::select("SELECT ... FROM information_schema...");
if (!empty($fk_exists)) { DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar"); }

// ✅ CORRECT - direct statements
DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar");
DB::statement("ALTER TABLE foo DROP COLUMN baz");

No IF EXISTS, no information_schema queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - snapshot rollback exists for recovery.


FILE ATTACHMENTS

Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.

Pattern recognition:

  • File_Attachment_Model::find_by_key() + can_user_assign_this_file()
  • attach_to() (single/replaces) vs add_to() (multiple/adds)
  • get_attachment() / get_attachments() for retrieval
  • get_url(), get_download_url(), get_thumbnail_url()

Detailed guidance in file-attachments skill - auto-activates when handling uploads.


POLYMORPHIC TYPE REFERENCES

Polymorphic *_type columns (fileable_type, taskable_type, etc.) store BIGINT integers in the database for efficiency. The framework maps between integer IDs and class names transparently.

// Model: Declare type ref columns
protected static $type_ref_columns = ['fileable_type'];

// Usage: Code works with class name strings - conversion is automatic
$attachment->fileable_type = 'Contact_Model';  // Stored as integer in DB
echo $attachment->fileable_type;               // Reads as 'Contact_Model'

Simple Names Only: Always use class_basename($model), never get_class($model) (FQCNs)

Details: php artisan rsx:man polymorphic


AJAX ENDPOINTS

#[Ajax_Endpoint]
public static function method(Request $request, array $params = []) {
    return $data;  // Success - framework wraps as {_success: true, _ajax_return_value: ...}
}

PHP→JS Auto-mapping:

// PHP: My_Controller class
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
    return ['id' => 123];
}

// JS: Automatically callable
const result = await My_Controller.save({name: 'Test'});
console.log(result.id); // 123

Error Responses

Use response_error(Ajax::ERROR_CODE, $metadata):

// Not found
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');

// Validation
return response_error(Ajax::ERROR_VALIDATION, [
    'email' => 'Invalid',
    'name' => 'Required'
]);

// Auto-message
return response_error(Ajax::ERROR_UNAUTHORIZED);

Codes: ERROR_VALIDATION, ERROR_NOT_FOUND, ERROR_UNAUTHORIZED, ERROR_AUTH_REQUIRED, ERROR_FATAL, ERROR_GENERIC

Client:

try {
    const data = await Controller.get(id);
} catch (e) {
    if (e.code === Ajax.ERROR_NOT_FOUND) {
        // Handle
    } else {
        alert(e.message);  // Generic
    }
}

Unhandled errors auto-show flash alert.


DATA FETCHING (CRITICAL)

DEFAULT: Use Model.fetch(id) for all single-record retrieval from JavaScript.

const user = await User_Model.fetch(1);         // Throws if not found
const user = await User_Model.fetch_or_null(1); // Returns null if not found

Requires #[Ajax_Endpoint_Model_Fetch] on the model's fetch() method.

Auto-populates enum properties and enables lazy relationship loading.

If model not available in JS bundle: STOP and ask the developer. Bundles should include all models they need (rsx/models in include paths). Do not create workaround endpoints without approval.

Custom Ajax endpoints require developer approval and are only for:

  • Aggregations, batch operations, or complex result sets
  • System/root-only models intentionally excluded from bundle
  • Queries beyond simple ID lookup

Details: php artisan rsx:man model_fetch


AUTHENTICATION

Always use Session - Static methods only. Never Laravel Auth or $_SESSION.

Session::is_logged_in();     // Returns true if user logged in
Session::get_user();         // Returns user model or null
Session::get_user_id();      // Returns user ID or null
Session::get_site();         // Returns site model
Session::get_site_id();      // Returns current site ID
Session::get_session_id();   // Returns session ID

Sessions persist 365 days. Never implement "Remember Me".


DATE & TIME HANDLING

Two Classes - Strict Separation: Rsx_Time (datetimes with timezone) | Rsx_Date (calendar dates, no timezone)

String-Based: ISO strings, not Carbon. Never use $casts with 'date'/'datetime' - blocked by rsx:check.

Pattern recognition:

  • Rsx_Time::now(), Rsx_Time::format(), Rsx_Time::relative()
  • Rsx_Date::today(), Rsx_Date::format(), Rsx_Date::is_past()
  • Rsx_Time::to_database() for UTC storage
  • Functions throw if wrong type passed

Detailed guidance in date-time skill - auto-activates when working with dates/times.


JAVASCRIPT DECORATORS

/** @decorator */
function logCalls(target, key, descriptor) { /* ... */ }

class Service {
    @logCalls
    @mutex
    async save() { /* ... */ }
}

COMMANDS

Module Creation

rsx:app:module:create <name>              # /name
rsx:app:module:feature:create <m> <f>     # /m/f
rsx:app:component:create --name=x         # Component

Development

rsx:check          # Code quality
rsx:debug /page    # Test routes (see below)
rsx:man <topic>    # Documentation
db:query "SQL" --json

Testing Routes

rsx:debug /path - Uses Playwright to render pages with full JS execution.

rsx:debug /dashboard --user=1                    # Authenticated user
rsx:debug /page --screenshot-path=/tmp/page.png  # Capture screenshot
rsx:debug /contacts --eval="$('.btn').click(); await sleep(1000)"  # Simulate interaction
rsx:debug / --eval="return Rsx_Time.now_iso()"   # Get eval result (use return)
rsx:debug / --console --eval="console.log(Rsx_Date.today())"  # Or console.log with --console

Options: --user=ID, --console, --screenshot-path, --screenshot-width=mobile|tablet|desktop-*, --dump-dimensions=".selector", --eval="js", --help

SPA routes ARE server routes. If you get 404, the route doesn't exist - check route definitions. Never dismiss as "SPA can't be tested server-side".

rsx:debug captures the fully-rendered final DOM state after all async operations, component lifecycles, and data loading complete. If the DOM doesn't match expectations, it's not a timing issue - what you see is what the user sees. Investigate the actual code, not the capture timing.

Debugging

  • rsx_dump_die() - Debug output
  • console_debug("CHANNEL", ...) - Channel logging
  • CONSOLE_DEBUG_FILTER=CHANNEL - Filter output

ERROR HANDLING

if (!$expected) {
    shouldnt_happen("Class {$expected} missing");
}

Use for "impossible" conditions that indicate broken assumptions.


CODE QUALITY

Professional UI: Hover effects ONLY on buttons, links, form fields. Static elements remain static.

z-index: Bootstrap defaults + 1100 (modal children), 1200 (flash alerts), 9000+ (system). Details: rsx:man zindex

Run rsx:check before commits. Enforces naming, prohibits animations on non-actionable elements.


KEY REMINDERS

  1. Fail loud - No silent failures
  2. Static by default - Unless instances needed
  3. Path-agnostic - Reference by name
  4. Bundles required - For JavaScript
  5. Use Session - Never Laravel Auth
  6. No mass assignment - Explicit only
  7. Forward migrations - No rollbacks
  8. Don't run rsx:clean - Cache auto-invalidates
  9. All routes need auth checks - In pre_dispatch() or method body (@auth-exempt for public)

SKILLS (Auto-Activated)

Detailed guidance for specific tasks is available via Claude Code skills. These activate automatically when relevant - no action needed.

Skill Activates When
forms Building forms, validation, vals() pattern
modals Creating dialogs, Modal.form(), Modal_Abstract
model-enums Implementing status_id, type_id, enum properties
file-attachments Upload flow, attach_to(), thumbnails
date-time Rsx_Time, Rsx_Date, timezone handling
blade-views Server-rendered pages, @rsx_id, @rsx_page_data
scss Component styling, BEM, responsive breakpoints
ajax-error-handling response_error(), Form_Utils, error codes
model-fetch Model.fetch(), lazy relationships, #[Ajax_Endpoint_Model_Fetch]
jquery-extensions .click() override, .exists(), .shallowFind()
background-tasks #[Task], #[Schedule], Task::dispatch()
crud-patterns List/view/edit structure, DataGrid, dual-route actions
js-decorators @route, @spa, @layout, @mutex, custom decorators
event-hooks #[OnEvent], filters, gates, actions
migrations Raw SQL, make:migration:safe, forward-only
polymorphic Type refs, morphTo, Polymorphic_Field_Helper

Framework skills: ~/.claude/skills/ (symlinked from system/docs/skills/) - READ-ONLY, do not modify Project skills: /var/www/html/.claude/skills/ - Custom skills for this specific project go here


GETTING HELP

php artisan rsx:man <topic>  # Framework documentation
php artisan list rsx          # All commands

Topics: bundle_api, jqhtml, routing, migrations, console_debug, model_fetch, vs_code_extension, deployment, framework_divergences


PROJECT DOCUMENTATION

Project-specific man pages in /rsx/resource/man/*.txt. Create when features have non-obvious details or component interactions. See /rsx/resource/man/CLAUDE.md for format.