# Guía IA — Desarrollo de Módulos en Klee Framework

Este documento es la referencia técnica completa para que un asistente IA desarrolle módulos en este framework. Contiene reglas, patrones con código real y ejemplos prácticos.

---

## 1. Objetivo operativo

La IA debe:

1. Implementar cambios **mínimos y compatibles**.
2. Respetar la arquitectura **MVC legada** del proyecto.
3. Priorizar **estabilidad** sobre refactor masivo.
4. **Validar** cada cambio (lint, doctor, flujo funcional).
5. Usar el scaffolding existente (`bin/make`) cuando aplique.

---

## 2. Reglas obligatorias

| Regla | Detalle |
|---|---|
| No SQL en controladores | Todo acceso a datos va en `app/models/` |
| No SQL en vistas | La vista solo renderiza datos recibidos |
| No cambiar contratos legacy | El formato de rutas `?c=&a=` es sagrado |
| No crear rutas declarativas | El router declarativo no está activo |
| Respetar naming existente | Nombres de clases, tablas y campos según convenciones del proyecto |
| CSRF en todo formulario POST | Siempre incluir `_csrf_token` como hidden input |
| Campos POST con namespace | `NombreModelo[NombreAtributo]` — **nunca plano** |

---

## 3. Proceso obligatorio

### Paso A: Análisis

Antes de tocar código:

```
1. Identificar controlador, modelo y vistas del módulo afectado
2. Leer getOptionsAttributes() del modelo para entender la estructura
3. Leer loadAccessControl() del controlador para ver acciones existentes
4. Buscar referencias al módulo en Modules.php
5. Verificar si hay migración/seeder existente
```

### Paso B: Implementación

Orden estricto:

```
1. Migración (si hay cambio de BD)
2. Modelo (atributos, métodos de datos)
3. Controlador (acciones, loadAccessControl)
4. Vistas (formularios, listas, detalle)
5. Modules.php (registro si es módulo nuevo)
6. Seeders (permisos, datos iniciales)
```

### Paso C: Validación

```bash
# Lint de archivos modificados
php -l app/controllers/MiController.php
php -l app/models/MiModel.php

# Diagnóstico general
php bin/doctor
```

### Paso D: Cierre

- Resumir archivos creados/modificados.
- Indicar si se requiere ejecutar `php bin/migrate up` o `php bin/seed`.
- Señalar riesgos o dependencias.

---

## 4. Patrones de código — Referencia completa

### 4.1 Modelo

```php
<?php

class ProductoModel extends Model
{
    protected static $TABLE_NAME = 'productos';
    protected static $VIEW_NAME  = 'productos';
    protected static $LOG   = true;    // audit log activado
    protected static $CACHE = false;   // cache desactivado

    public static function getOptionsAttributes()
    {
        return array(
            array('Type' => 'AutoincrementId', 'Name' => 'Id'),
            array('Type' => 'text', 'Name' => 'Nombre', 'Title' => 'Nombre', 'Required' => true),
            array('Type' => 'text', 'Name' => 'Codigo', 'Title' => 'Código', 'Required' => true),
            array('Type' => 'textarea', 'Name' => 'Descripcion', 'Title' => 'Descripción'),
            array('Type' => 'decimal', 'Name' => 'Precio', 'Required' => true),
            array('Type' => 'integer', 'Name' => 'Stock', 'Required' => true),
            array('Type' => 'select', 'Name' => 'CategoriaId', 'Title' => 'Categoría', 'Tabla' => 'categorias', 'Required' => true),
            array('Type' => 'select', 'Name' => 'Estado', 'Tabla' => 'estados', 'Required' => true),
            array('Type' => 'RegistrationDate', 'Name' => 'FechaRegistro'),
            array('Type' => 'RegistrationUser', 'Name' => 'UsuarioRegistro'),
            array('Type' => 'ModificationDate', 'Name' => 'FechaModificacion'),
            array('Type' => 'ModificationUser', 'Name' => 'UsuarioModificacion'),
        );
    }

    // Métodos de negocio del modelo van aquí:
    public static function getActivos()
    {
        $model = new static();
        return $model->findAll(array(
            'WHERE' => array(array('name' => 'Estado', 'value' => 1)),
            'ORDER' => 'Nombre ASC',
        ));
    }
}
```

### 4.2 Tipos de atributo disponibles

| Tipo | Clase PHP | SQL típico | Uso |
|---|---|---|---|
| `AutoincrementId` | AutoincrementId | `INT AUTO_INCREMENT PRIMARY KEY` | PK del modelo |
| `text` | Text | `VARCHAR(100)` | Texto corto |
| `textarea` | Textarea | `TEXT` | Texto largo |
| `email` | Email | `VARCHAR(250)` | Correo con validación |
| `password` | Password | `VARCHAR(255)` | Contraseña con confirmación |
| `select` | Select | `INT` / `VARCHAR` | Dropdown (requiere `Tabla`) |
| `radio` | Radio | `INT` / `VARCHAR` | Radio buttons |
| `checkbox` | Checkbox | `TINYINT(1)` | Boolean |
| `date` | Date | `DATE` | Fecha (YYYY-MM-DD) |
| `FechaHora` | FechaHora | `DATETIME` | Fecha y hora |
| `time` | Time | `TIME` | Solo hora |
| `integer` | Integer | `INT` | Número entero |
| `decimal` | Decimal | `DECIMAL(12,2)` | Número decimal |
| `money` | Money | `DECIMAL(12,2)` | Valor monetario |
| `hidden` | Hidden | Cualquiera | Campo no visible |
| `image` | Image | `VARCHAR(255)` | Upload de imagen |
| `document` | Document | `VARCHAR(255)` | Upload de archivo |
| `htmlEditor` | HtmlEditor | `TEXT` / `LONGTEXT` | Editor rico |
| `encrypted` | Encrypted | `TEXT` | Valor cifrado |
| `slider` | Slider | `INT` / `DECIMAL` | Control deslizante |
| `consecutive` | Consecutive | `INT` | Numeración secuencial |
| `UniqueId` | UniqueId | `VARCHAR(36)` | UUID |
| `RegistrationDate` | RegistrationDate | `DATETIME` | Auto: fecha creación |
| `RegistrationUser` | RegistrationUser | `INT` | Auto: usuario creador |
| `ModificationDate` | ModificationDate | `DATETIME` | Auto: fecha modificación |
| `ModificationUser` | ModificationUser | `INT` | Auto: usuario modificador |

### 4.3 Opciones de atributo

```php
array(
    'Type'      => 'text',          // obligatorio: tipo de atributo
    'Name'      => 'Nombre',        // obligatorio: nombre de la columna
    'Title'     => 'Nombre Completo', // etiqueta en formulario
    'Required'  => true,            // validación obligatoria
    'Tabla'     => 'categorias',    // tabla origen (solo select/radio)
    'Exportar'  => true,            // incluir en exportaciones
    'textHelp'  => 'Ingrese aquí',  // placeholder/ayuda
    'MaxLength' => 200,             // longitud máxima
    'Multiple'  => true,            // multiselect (solo select)
)
```

### 4.4 Controlador CRUD completo

```php
<?php

class ProductoController extends Controller
{
    public static $TITLE_NAME  = 'productos';
    public static $MODULE_NAME = 'producto';
    protected $ViewFolder = 'producto';

    protected function loadAccessControl()
    {
        $this->AccessControl = array(
            'create'       => '@',
            'edit'         => '@',
            'remove'       => '@',
            'view'         => '@',
            'list'         => '@',
            'dataListAjax' => '@',
        );
    }

    // ── CREAR ──────────────────────────────────────
    public function createAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        // Procesar POST
        if (isset($_POST[get_class($this->Model)])) {
            if ($this->Model->save()) {
                UserFlash::setFlash('Success', 'Producto creado correctamente');
                Router::redirect_to_action($this->Module, 'view',
                    array('Id' => $this->Model->Id->getValue()));
            }
            // Si save() falla, cae al render del formulario con los datos cargados
        }

        Controller::generateCsrfToken();
        $parameters = $this->loadMetadata();
        $parameters['model'] = $this->Model;
        View::render_view($this->ViewFolder . '/create', $parameters);
    }

    // ── EDITAR ─────────────────────────────────────
    public function editAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        if (isset($_POST[get_class($this->Model)])) {
            $this->Model->loadById($_POST[get_class($this->Model)]['Id']);
            if ($this->Model->save()) {
                UserFlash::setFlash('Success', 'Producto actualizado');
                Router::redirect_to_action($this->Module, 'view',
                    array('Id' => $this->Model->Id->getValue()));
            }
        }

        if (isset($_GET['Id'])) {
            Controller::generateCsrfToken();
            $this->Model->loadById($_GET['Id']);
            $parameters = $this->loadMetadata();
            $parameters['model'] = $this->Model;
            View::render_view($this->ViewFolder . '/edit', $parameters);
        } else {
            UserFlash::setFlash('Error', 'Parámetros inválidos');
            Router::redirect_to_action($this->Module, 'list');
        }
    }

    // ── VER ────────────────────────────────────────
    public function viewAction()
    {
        Menu::setActive('herramientas_producto');
        $this->Model = new ProductoModel();

        if (isset($_GET['Id'])) {
            $this->Model->loadById($_GET['Id']);
            $parameters = $this->loadMetadata();
            $parameters['model'] = $this->Model;
            View::render_view($this->ViewFolder . '/view', $parameters);
        } else {
            UserFlash::setFlash('Error', 'Parámetros inválidos');
            Router::redirect_to_action($this->Module, 'list');
        }
    }

    // ── ELIMINAR ───────────────────────────────────
    public function removeAction()
    {
        Menu::setActive('herramientas_producto');

        if (isset($_GET['Id'])) {
            $model = new ProductoModel();
            $model->loadById($_GET['Id']);
            if ($model->deleteFromParameters(array(
                'WHERE' => array(array('name' => 'Id', 'value' => $_GET['Id']))
            ))) {
                UserFlash::setFlash('Success', 'Producto eliminado');
            } else {
                UserFlash::setFlash('Error', 'No se pudo eliminar');
            }
        }

        Router::redirect_to_action($this->Module, 'list');
    }

    // ── LISTAR ─────────────────────────────────────
    public function listAction()
    {
        Menu::setActive('herramientas_producto');
        $parameters = $this->loadMetadata();
        $table = $this->getListAjaxObject();
        $parameters['listaHtml'] = $table->getHtml();
        View::render_view($this->ViewFolder . '/list', $parameters);
    }

    public function dataListAjaxAction()
    {
        $table = $this->getListAjaxObject();
        Response::json($table->generateDataListAjax());
    }

    protected function getListAjaxObject()
    {
        $table = new ListaAjax($this->Module, $this->CurrentAction);
        $model = new ProductoModel();
        $table->setModel($model);

        $fieldsShow = array('Id', 'Codigo', 'Nombre', 'Precio', 'Stock', 'Estado');
        $titles     = array('ID', 'Código', 'Nombre', 'Precio', 'Stock', 'Estado');

        $table->setData(null, null, $titles, null, $fieldsShow);
        $table->setPermission($this->Permission);
        $table->setState(true);

        return $table;
    }
}
```

### 4.5 Vista — Formulario (`_form.php`)

```php
<form method="post"
      action="<?php echo Router::create_action_url($controllerName, $currentAction); ?>"
      enctype="multipart/form-data" class="form">

    <!-- CSRF Token (OBLIGATORIO en todo formulario POST) -->
    <input type="hidden" name="_csrf_token"
           value="<?php echo Controller::generateCsrfToken(); ?>">

    <!-- ID del registro (para edición) -->
    <input type="hidden" name="ProductoModel[Id]"
           value="<?php echo $model->Id->getValue(); ?>">

    <div class="card mb-5 mb-xl-10">
        <div class="card-header">
            <h3 class="card-title">Datos del Producto</h3>
        </div>
        <div class="card-body">

            <!-- Campo de texto -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label required fw-bold fs-6">
                    <?php echo $model->Nombre->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <input type="text"
                           name="ProductoModel[Nombre]"
                           class="form-control form-control-lg form-control-solid"
                           value="<?php echo $model->Nombre->getValue(); ?>"
                           placeholder="Nombre del producto" />
                </div>
            </div>

            <!-- Campo select con Select2 -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label required fw-bold fs-6">
                    <?php echo $model->CategoriaId->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <select name="ProductoModel[CategoriaId]"
                            class="form-select form-select-solid"
                            data-control="select2"
                            data-placeholder="Seleccione...">
                        <option value="">Seleccione...</option>
                        <!-- Select carga opciones desde el atributo -->
                    </select>
                </div>
            </div>

            <!-- Campo decimal -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label required fw-bold fs-6">
                    <?php echo $model->Precio->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <input type="number" step="0.01"
                           name="ProductoModel[Precio]"
                           class="form-control form-control-lg form-control-solid"
                           value="<?php echo $model->Precio->getValue(); ?>" />
                </div>
            </div>

            <!-- Textarea -->
            <div class="row mb-6">
                <label class="col-lg-3 col-form-label fw-bold fs-6">
                    <?php echo $model->Descripcion->getTitle(); ?>
                </label>
                <div class="col-lg-9 fv-row">
                    <textarea name="ProductoModel[Descripcion]"
                              class="form-control form-control-solid"
                              rows="4"><?php echo $model->Descripcion->getValue(); ?></textarea>
                </div>
            </div>

        </div>
        <div class="card-footer d-flex justify-content-end py-6">
            <a href="<?php echo Router::create_action_url($controllerName, 'list'); ?>"
               class="btn btn-light me-3">Cancelar</a>
            <button type="submit" class="btn btn-primary">Guardar</button>
        </div>
    </div>
</form>
```

### 4.6 Vista — Listado (`list.php`)

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <div class="card">
            <div class="card-header border-0 pt-6">
                <div class="card-title">
                    <h2>Productos</h2>
                </div>
                <div class="card-toolbar">
                    <a href="<?php echo Router::create_action_url($controllerName, 'create'); ?>"
                       class="btn btn-primary">
                        <i class="bi bi-plus-lg"></i> Agregar
                    </a>
                </div>
            </div>
            <div class="card-body pt-0">
                <?php echo $listaHtml; ?>
            </div>
        </div>
    </div>
</div>
```

### 4.7 Vista — Detalle (`view.php`)

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <div class="card mb-5 mb-xl-10">
            <div class="card-header">
                <h3 class="card-title">Detalle del Producto</h3>
                <div class="card-toolbar">
                    <a href="<?php echo Router::create_action_url($controllerName, 'edit',
                        array('Id' => $model->Id->getValue())); ?>"
                       class="btn btn-sm btn-warning me-2">Editar</a>
                    <a href="<?php echo Router::create_action_url($controllerName, 'list'); ?>"
                       class="btn btn-sm btn-light">Volver</a>
                </div>
            </div>
            <div class="card-body">
                <div class="row mb-4">
                    <label class="col-lg-3 fw-bold text-muted">
                        <?php echo $model->Nombre->getTitle(); ?>
                    </label>
                    <div class="col-lg-9">
                        <span class="fw-bolder fs-6">
                            <?php echo $model->Nombre->getValue(); ?>
                        </span>
                    </div>
                </div>
                <div class="row mb-4">
                    <label class="col-lg-3 fw-bold text-muted">
                        <?php echo $model->Precio->getTitle(); ?>
                    </label>
                    <div class="col-lg-9">
                        <span class="fw-bolder fs-6">
                            $<?php echo number_format($model->Precio->getValue(), 2); ?>
                        </span>
                    </div>
                </div>
                <!-- Repetir para cada campo relevante -->
            </div>
        </div>
    </div>
</div>
```

### 4.8 Vista — Create/Edit wrapper

Ambos archivos (`create.php` y `edit.php`) son wrappers que cargan el partial:

```php
<div class="post d-flex flex-column-fluid" id="kt_post">
    <div id="kt_content_container" class="container-xxl">
        <?php View::load_view('producto/_form', array(
            'model'          => $model,
            'controllerName' => $controllerName,
            'currentAction'  => $currentAction,
        )); ?>
    </div>
</div>
```

### 4.9 Migración

```php
<?php

class CreateProductosTable
{
    public function up(PDO $pdo)
    {
        $pdo->exec("
            CREATE TABLE IF NOT EXISTS productos (
                Id INT AUTO_INCREMENT PRIMARY KEY,
                Nombre VARCHAR(100) NOT NULL,
                Codigo VARCHAR(100) NOT NULL,
                Descripcion TEXT NULL,
                Precio DECIMAL(12,2) NOT NULL DEFAULT 0,
                Stock INT NOT NULL DEFAULT 0,
                CategoriaId INT NULL,
                Estado INT NOT NULL DEFAULT 1,
                FechaRegistro DATETIME NULL,
                UsuarioRegistro INT NULL,
                FechaModificacion DATETIME NULL,
                UsuarioModificacion INT NULL,
                TenantId INT NOT NULL DEFAULT 1
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        ");
    }

    public function down(PDO $pdo)
    {
        $pdo->exec("DROP TABLE IF EXISTS productos;");
    }
}
```

### 4.10 Seeder de permisos

```php
<?php

class ProductoPermissionsSeeder
{
    protected $order = 60;

    public function run(PDO $pdo)
    {
        $moduleName = 'Producto';
        $actions = array('list', 'view', 'create', 'edit', 'remove', 'dataListAjax');

        $stmt = $pdo->query("SELECT Id FROM roles WHERE Nombre = 'Administrador' LIMIT 1");
        $roleId = $stmt->fetchColumn();
        if (!$roleId) {
            echo "  [skip] No se encontró rol Administrador.\n";
            return;
        }

        $insert = $pdo->prepare("
            INSERT IGNORE INTO permisos (RolId, Modulo, Accion, Permiso, TenantId)
            VALUES (:RolId, :Modulo, :Accion, 1, 1)
        ");

        foreach ($actions as $action) {
            $insert->execute(array(
                'RolId'  => $roleId,
                'Modulo' => $moduleName,
                'Accion' => $action,
            ));
        }

        echo "  [ok] {$moduleName}PermissionsSeeder: " . count($actions) . " permisos.\n";
    }
}
```

### 4.11 Registro en Modules.php

```php
'producto' => array(
    'enabled'    => true,
    'controller' => 'producto',
    'metadata'   => array(
        'icon'     => '<i class="bi bi-box fs-2"></i>',
        'category' => 'Herramientas',
        'order'    => 50,
    ),
    'menu' => array(
        'name'   => 'herramientas_producto',
        'title'  => 'Productos',
        'action' => 'list',
        'group'  => 'herramientas',
    ),
    'quick_actions' => array(
        array(
            'id'          => 'qa-producto-nuevo',
            'label'       => 'Nuevo Producto',
            'keywords'    => array('crear', 'nuevo', 'producto'),
            'path'        => array('controller' => 'producto', 'action' => 'create'),
            'module'      => 'Herramientas',
            'permissions' => array(array('controller' => 'Producto', 'action' => 'create')),
        ),
    ),
),
```

---

## 5. APIs y servicios disponibles

### Request

```php
Request::get('id');            // parámetro GET
Request::post('nombre');       // parámetro POST
Request::all();                // merge GET + POST
Request::method();             // 'GET', 'POST'
Request::isAjax();             // true si es AJAX
Request::files('foto');        // $_FILES
Request::session('user');      // $_SESSION
Request::server('HTTP_HOST');  // $_SERVER
```

### Response

```php
Response::json($data);                       // JSON 200
Response::json($data, 201);                  // JSON con status
Response::error('Mensaje', 404);             // JSON error
Response::error('Fallo', 500, ['detail' => $msg]);
```

### UserFlash

```php
UserFlash::setFlash('Success', 'Operación exitosa');
UserFlash::setFlash('Error', 'No se pudo guardar');
UserFlash::setFlash('Warning', 'Revise los datos');
```

### Router

```php
// Generar URL (retorna '#' si sin permiso)
Router::create_action_url('producto', 'view', array('Id' => 5));

// Redireccionar
Router::redirect_to_action('producto', 'list');
Router::redirect_to_action('producto', 'view', array('Id' => 5));
```

### EventDispatcher

```php
EventDispatcher::listen('producto.creado', function ($payload) {
    // lógica post-creación
});
EventDispatcher::dispatch('producto.creado', ['id' => $id]);
```

### MailService

```php
$result = MailService::send(
    'destino@email.com',
    'Asunto',
    '<p>Contenido HTML</p>',
    Controller::getEmailSendConfig()
);

// O encolar:
MailService::queue('destino@email.com', 'Asunto', '<p>HTML</p>',
    Controller::getEmailSendConfig());
```

### ListaAjax (en controlador)

```php
protected function getListAjaxObject()
{
    $table = new ListaAjax($this->Module, $this->CurrentAction);
    $model = new ProductoModel();
    $table->setModel($model);

    $fieldsShow = array('Id', 'Nombre', 'Estado');
    $titles     = array('ID', 'Nombre', 'Estado');

    $table->setData(null, null, $titles, null, $fieldsShow);
    $table->setPermission($this->Permission);
    $table->setState(true);
    $table->setActions(array(
        array('name' => 'Ver', 'controller' => $this->Module, 'action' => 'view',
              'icon' => 'eye', 'color' => 'primary'),
        array('name' => 'Editar', 'controller' => $this->Module, 'action' => 'edit',
              'icon' => 'pencil', 'color' => 'warning'),
        array('name' => 'Eliminar', 'controller' => $this->Module, 'action' => 'remove',
              'icon' => 'trash', 'color' => 'danger', 'confirm' => '¿Eliminar?'),
    ));

    return $table;
}
```

---

## 6. CSRF — Reglas para la IA

1. **Generar token** en cada acción que renderiza un formulario:
   ```php
   Controller::generateCsrfToken();
   ```

2. **Incluir en la vista** como hidden input:
   ```html
   <input type="hidden" name="_csrf_token" value="<?php echo Controller::generateCsrfToken(); ?>">
   ```

3. **No validar manualmente** — el middleware `CsrfMiddleware` valida automáticamente en POST.

4. **Model::save() está protegido** — tiene verificación interna de CSRF que respeta el flag del middleware.

5. Si `CSRF_STRICT=false` (default), formularios legacy sin token siguen funcionando.

---

## 7. Multi-tenancy — Consideraciones

- Si la tabla tiene columna `TenantId`, el trait `HasTenant` filtra automáticamente.
- En migraciones, incluir: `TenantId INT NOT NULL DEFAULT 1`.
- No manipular `TenantId` manualmente en controladores.

---

## 8. Plantilla de decisión

Antes de cada cambio, evaluar:

| Pregunta | Si "Sí" |
|---|---|
| ¿Es local al módulo? | Proceder normalmente |
| ¿Afecta seguridad (CSRF/auth/permisos)? | Validar no-regresión, ser conservador |
| ¿Afecta rutas legacy o contratos públicos? | **No hacer** sin aprobación explícita |
| ¿Requiere migración o seeder? | Crear archivos, documentar comando |
| ¿Afecta a otros módulos? | Buscar dependencias antes de editar |

---

## 9. Checklist de entrega

Cada cambio completado por la IA debe cumplir:

- [ ] Requisito funcional cumplido.
- [ ] Sin errores de sintaxis (`php -l` en cada archivo).
- [ ] Sin ruptura de compatibilidad legacy.
- [ ] CSRF en todo formulario POST.
- [ ] `loadAccessControl()` actualizado si hay acciones nuevas.
- [ ] Permisos seedeados para acciones nuevas.
- [ ] `Menu::setActive()` en cada acción del controlador.
- [ ] Campos POST con namespace (`NombreModelo[Atributo]`).

---

## 10. Prompt recomendado para pedir trabajo a la IA

```
Módulo: [Nombre del módulo]
Objetivo: [Qué debe hacer — crear CRUD, agregar campo, nuevo reporte, etc.]
Archivos permitidos: [Rutas específicas o "todos los del módulo"]
No tocar: [Módulos o archivos que no se deben modificar]
Cambios de BD: [Sí/No — si sí, detallar columnas/tablas]
Criterio de aceptación: [Cómo verificar que funciona]
Restricciones: [Legacy/API/UI que debe mantenerse]
```

**Ejemplo:**

```
Módulo: Inventario
Objetivo: Crear CRUD completo con listado, creación, edición, detalle y eliminación
Archivos permitidos: app/controllers/InventarioController.php, app/models/InventarioModel.php, app/views/inventario/*, database/migrations/*, database/seeders/*
No tocar: Módulo de usuarios, login
Cambios de BD: Sí — crear tabla inventario con campos: Nombre, Codigo, Cantidad, Ubicacion, Estado
Criterio de aceptación: Listar, crear, editar y eliminar registros sin errores. Permisos funcionando.
Restricciones: Mantener estilo visual Metronic existente
```

---

## 11. Errores frecuentes a evitar

| Error | Corrección |
|---|---|
| Poner SQL en el controlador | Mover a método del modelo |
| Olvidar `_csrf_token` en form | Siempre incluir hidden input |
| Campos POST planos (`name="Nombre"`) | Usar `NombreModelo[Nombre]` |
| No registrar acción en `loadAccessControl` | Agregar entrada `'nuevaAccion' => '@'` |
| No llamar `Menu::setActive()` | Menú lateral no resalta la sección correcta |
| Migración sin `down()` | Siempre implementar reversibilidad |
| Editar migración ya aplicada | Crear nueva migración incremental |
| No ejecutar lint | Siempre `php -l` en archivos modificados |
