{"id":"cs-1782352330369-d2m920a2","status":"done","createdAt":1782352330369,"finishedAt":1782352378206,"meta":{"projectName":"ALimpiezas","projectPath":"C:\\xampp\\htdocs\\ALimpiezas","messagePreview":"Documenta las CONVENCIONES del proyecto.\n\nEntrega en Markdown:\n1. **Naming** — archivos, clases, funciones\n2. **Patrones","phaseId":"conventions"},"events":[{"event":"status","data":{"text":"Modo completo — hasta 80 herramientas, usa solo las necesarias"},"at":1782352330372},{"event":"status","data":{"text":"Agente"},"at":1782352330374},{"event":"agent","data":{"agentId":"agent-0dd6ad87-ad7f-4149-92f8-c1368e81c7b8","projectPath":"C:\\xampp\\htdocs\\ALimpiezas"},"at":1782352330374},{"event":"changes","data":{"sessionId":"cs-1782352330372-rd7dff","pending":0},"at":1782352330374},{"event":"status","data":{"text":"Procesando…"},"at":1782352330427},{"event":"tool_activity","data":{"activity":{"id":"tool_1f4a2505-bf83-4a77-8d9c-08b06d3cb22","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:13.696Z"}},"at":1782352333696},{"event":"tool_activity","data":{"activity":{"id":"tool_1a02e8df-d44e-4a36-9877-d2ce17e3d3f","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:13.817Z"}},"at":1782352333817},{"event":"tool_activity","data":{"activity":{"id":"tool_08a74f65-278d-4d57-8db7-bcb53dd984a","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:13.855Z"}},"at":1782352333856},{"event":"text","data":{"chunk":"Voy a revisar la raíz del proyecto y archivos clave de configuración para extraer convenciones reales del código.\n"},"at":1782352378206},{"event":"tool_activity","data":{"activity":{"id":"tool_9969656f-ae5d-4cf1-83e9-80ae26ae64e","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:13.976Z"}},"at":1782352333976},{"event":"tool_activity","data":{"activity":{"id":"tool_08a74f65-278d-4d57-8db7-bcb53dd984a","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:14.223Z"}},"at":1782352334223},{"event":"tool_activity","data":{"activity":{"id":"tool_9969656f-ae5d-4cf1-83e9-80ae26ae64e","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:14.227Z"}},"at":1782352334227},{"event":"tool_activity","data":{"activity":{"id":"tool_1a02e8df-d44e-4a36-9877-d2ce17e3d3f","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:14.228Z"}},"at":1782352334228},{"event":"tool_activity","data":{"activity":{"id":"tool_1f4a2505-bf83-4a77-8d9c-08b06d3cb22","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:15.688Z"}},"at":1782352335688},{"event":"tool_activity","data":{"activity":{"id":"tool_98302bcf-0115-4eaa-8fef-c35e0720337","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\composer.json","detail":"","at":"2026-06-25T01:52:16.910Z"}},"at":1782352336910},{"event":"tool_activity","data":{"activity":{"id":"tool_0f708b31-1d3e-4a2a-bd50-afdaeaaf396","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\phpunit.xml","detail":"","at":"2026-06-25T01:52:16.911Z"}},"at":1782352336911},{"event":"tool_activity","data":{"activity":{"id":"tool_98302bcf-0115-4eaa-8fef-c35e0720337","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\composer.json","detail":"","at":"2026-06-25T01:52:17.012Z"}},"at":1782352337012},{"event":"tool_activity","data":{"activity":{"id":"tool_0f708b31-1d3e-4a2a-bd50-afdaeaaf396","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\phpunit.xml","detail":"","at":"2026-06-25T01:52:17.014Z"}},"at":1782352337014},{"event":"tool_activity","data":{"activity":{"id":"tool_cdbb9d73-79e7-4948-a8ff-1e603b0f8ee","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\phpunit-plugins.xml","detail":"","at":"2026-06-25T01:52:19.185Z"}},"at":1782352339185},{"event":"tool_activity","data":{"activity":{"id":"tool_3d2c5435-eb2a-40e1-a423-9d8c0e2c3d5","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MANUAL-AGENTE-IA.md","detail":"","at":"2026-06-25T01:52:21.370Z"}},"at":1782352341370},{"event":"tool_activity","data":{"activity":{"id":"tool_89aa97d5-a77d-4924-b69f-bb595f2a792","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:21.377Z"}},"at":1782352341377},{"event":"tool_activity","data":{"activity":{"id":"tool_59b302fa-42a3-4f79-ad8a-ee650d79475","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:21.484Z"}},"at":1782352341484},{"event":"tool_activity","data":{"activity":{"id":"tool_89aa97d5-a77d-4924-b69f-bb595f2a792","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:21.539Z"}},"at":1782352341539},{"event":"tool_activity","data":{"activity":{"id":"tool_59b302fa-42a3-4f79-ad8a-ee650d79475","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:21.673Z"}},"at":1782352341673},{"event":"tool_activity","data":{"activity":{"id":"tool_f021c3eb-a052-4810-860f-8dbb8fc38e3","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Init.php","detail":"","at":"2026-06-25T01:52:22.815Z"}},"at":1782352342815},{"event":"tool_activity","data":{"activity":{"id":"tool_bff18639-c891-4543-8947-8174cb5db9b","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:52:22.948Z"}},"at":1782352342948},{"event":"tool_activity","data":{"activity":{"id":"tool_f021c3eb-a052-4810-860f-8dbb8fc38e3","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Init.php","detail":"","at":"2026-06-25T01:52:22.962Z"}},"at":1782352342962},{"event":"tool_activity","data":{"activity":{"id":"tool_f2294d87-3648-4989-bbd2-5812b61480f","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Model\\AiExpenseQueue.php","detail":"","at":"2026-06-25T01:52:23.007Z"}},"at":1782352343007},{"event":"tool_activity","data":{"activity":{"id":"tool_bff18639-c891-4543-8947-8174cb5db9b","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Controller\\MobileExpenses.php","detail":"","at":"2026-06-25T01:52:23.046Z"}},"at":1782352343046},{"event":"tool_activity","data":{"activity":{"id":"tool_29e32a68-2210-4f82-bed4-12c3c853a6c","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","detail":"","at":"2026-06-25T01:52:23.124Z"}},"at":1782352343124},{"event":"tool_activity","data":{"activity":{"id":"tool_f2294d87-3648-4989-bbd2-5812b61480f","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Model\\AiExpenseQueue.php","detail":"","at":"2026-06-25T01:52:23.137Z"}},"at":1782352343137},{"event":"tool_activity","data":{"activity":{"id":"tool_12bd66b9-ec66-4701-982b-f9975109368","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\config.php","detail":"","at":"2026-06-25T01:52:23.190Z"}},"at":1782352343190},{"event":"tool_activity","data":{"activity":{"id":"tool_f26e7610-1e9f-4808-816f-fc0a1dcfaeb","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:23.222Z"}},"at":1782352343222},{"event":"tool_activity","data":{"activity":{"id":"tool_29e32a68-2210-4f82-bed4-12c3c853a6c","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\MobileConfig.php","detail":"","at":"2026-06-25T01:52:23.230Z"}},"at":1782352343230},{"event":"tool_activity","data":{"activity":{"id":"tool_12bd66b9-ec66-4701-982b-f9975109368","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\config.php","detail":"","at":"2026-06-25T01:52:23.283Z"}},"at":1782352343283},{"event":"tool_activity","data":{"activity":{"id":"tool_f26e7610-1e9f-4808-816f-fc0a1dcfaeb","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:23.468Z"}},"at":1782352343468},{"event":"tool_activity","data":{"activity":{"id":"tool_f13230ea-a172-4140-8853-ce220a02062","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\README.md","detail":"","at":"2026-06-25T01:52:25.532Z"}},"at":1782352345532},{"event":"tool_activity","data":{"activity":{"id":"tool_1ca5e946-0ea6-495e-ac21-f07cf9f74ac","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\bootstrap.php","detail":"","at":"2026-06-25T01:52:25.533Z"}},"at":1782352345533},{"event":"tool_activity","data":{"activity":{"id":"tool_ed91a912-b3bb-4b98-b527-ffef2e45a9d","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\facturascripts.ini","detail":"","at":"2026-06-25T01:52:25.536Z"}},"at":1782352345536},{"event":"tool_activity","data":{"activity":{"id":"tool_6645b936-9a62-4efb-875c-04c4c812fe4","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Table\\ai_expense_queue.xml","detail":"","at":"2026-06-25T01:52:25.536Z"}},"at":1782352345536},{"event":"tool_activity","data":{"activity":{"id":"tool_7334b896-3396-45b7-80d8-23e858235e4","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\.gitignore","detail":"","at":"2026-06-25T01:52:25.593Z"}},"at":1782352345593},{"event":"tool_activity","data":{"activity":{"id":"tool_f13230ea-a172-4140-8853-ce220a02062","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\README.md","detail":"","at":"2026-06-25T01:52:25.630Z"}},"at":1782352345630},{"event":"tool_activity","data":{"activity":{"id":"tool_ed91a912-b3bb-4b98-b527-ffef2e45a9d","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\facturascripts.ini","detail":"","at":"2026-06-25T01:52:25.634Z"}},"at":1782352345634},{"event":"tool_activity","data":{"activity":{"id":"tool_6645b936-9a62-4efb-875c-04c4c812fe4","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Table\\ai_expense_queue.xml","detail":"","at":"2026-06-25T01:52:25.637Z"}},"at":1782352345637},{"event":"tool_activity","data":{"activity":{"id":"tool_1ca5e946-0ea6-495e-ac21-f07cf9f74ac","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\bootstrap.php","detail":"","at":"2026-06-25T01:52:25.643Z"}},"at":1782352345643},{"event":"tool_activity","data":{"activity":{"id":"tool_7334b896-3396-45b7-80d8-23e858235e4","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\.gitignore","detail":"","at":"2026-06-25T01:52:25.685Z"}},"at":1782352345685},{"event":"tool_activity","data":{"activity":{"id":"tool_f6dec824-28d8-4f41-b507-28a6836b8f5","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:25.737Z"}},"at":1782352345737},{"event":"tool_activity","data":{"activity":{"id":"tool_10b921d4-f54e-4cad-a45a-45248fad637","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\Core\\ToolsTest.php","detail":"","at":"2026-06-25T01:52:27.542Z"}},"at":1782352347542},{"event":"tool_activity","data":{"activity":{"id":"tool_106d37e8-b1cb-47df-a54e-b45903fb449","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\tools\\bootstrap-plugin.php","detail":"","at":"2026-06-25T01:52:27.543Z"}},"at":1782352347543},{"event":"tool_activity","data":{"activity":{"id":"tool_bc6e04a8-c5b2-483b-b520-0a250e5d9a6","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:52:27.651Z"}},"at":1782352347651},{"event":"tool_activity","data":{"activity":{"id":"tool_10b921d4-f54e-4cad-a45a-45248fad637","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\Core\\ToolsTest.php","detail":"","at":"2026-06-25T01:52:27.742Z"}},"at":1782352347742},{"event":"tool_activity","data":{"activity":{"id":"tool_106d37e8-b1cb-47df-a54e-b45903fb449","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\tools\\bootstrap-plugin.php","detail":"","at":"2026-06-25T01:52:27.757Z"}},"at":1782352347757},{"event":"tool_activity","data":{"activity":{"id":"tool_e43180dd-cf3f-4b0f-85e4-43950b49147","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\index.php","detail":"","at":"2026-06-25T01:52:27.758Z"}},"at":1782352347758},{"event":"tool_activity","data":{"activity":{"id":"tool_1c72aa7b-a59b-4ad1-8847-a055f504471","name":"glob","status":"running","summary":"glob: ","detail":"","at":"2026-06-25T01:52:27.759Z"}},"at":1782352347759},{"event":"tool_activity","data":{"activity":{"id":"tool_bc6e04a8-c5b2-483b-b520-0a250e5d9a6","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\AiExpenseProcessor.php","detail":"","at":"2026-06-25T01:52:27.762Z"}},"at":1782352347762},{"event":"tool_activity","data":{"activity":{"id":"tool_355e5911-f073-4f37-b34e-6ffe49a7211","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\ai-service\\main.py","detail":"","at":"2026-06-25T01:52:27.854Z"}},"at":1782352347854},{"event":"tool_activity","data":{"activity":{"id":"tool_e43180dd-cf3f-4b0f-85e4-43950b49147","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\index.php","detail":"","at":"2026-06-25T01:52:27.883Z"}},"at":1782352347883},{"event":"tool_activity","data":{"activity":{"id":"tool_1c72aa7b-a59b-4ad1-8847-a055f504471","name":"glob","status":"completed","summary":"glob: ","detail":"","at":"2026-06-25T01:52:27.914Z"}},"at":1782352347914},{"event":"tool_activity","data":{"activity":{"id":"tool_355e5911-f073-4f37-b34e-6ffe49a7211","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\ai-service\\main.py","detail":"","at":"2026-06-25T01:52:27.998Z"}},"at":1782352347998},{"event":"tool_activity","data":{"activity":{"id":"tool_e5cd9245-151f-4267-984c-3f197f3ef0b","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\phpcs.xml","detail":"","at":"2026-06-25T01:52:29.137Z"}},"at":1782352349137},{"event":"tool_activity","data":{"activity":{"id":"tool_2783add7-ffa8-482c-ba85-093691e50c6","name":"read","status":"running","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\ExpenseInvoiceCreator.php","detail":"","at":"2026-06-25T01:52:29.137Z"}},"at":1782352349137},{"event":"tool_activity","data":{"activity":{"id":"tool_1cf47410-52c1-49c3-8677-4b4448abf1f","name":"read","status":"running","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:52:29.140Z"}},"at":1782352349140},{"event":"tool_activity","data":{"activity":{"id":"tool_070e61e8-ce0d-4174-96d6-3770a3dd9bf","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Translation\\es_ES.json","detail":"","at":"2026-06-25T01:52:29.148Z"}},"at":1782352349148},{"event":"tool_activity","data":{"activity":{"id":"tool_975adbf5-7c37-45ab-b7c8-eb61a57d8c5","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MOBILE-BRIDGE-TEST.md","detail":"","at":"2026-06-25T01:52:29.210Z"}},"at":1782352349210},{"event":"tool_activity","data":{"activity":{"id":"tool_e5cd9245-151f-4267-984c-3f197f3ef0b","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\phpcs.xml","detail":"","at":"2026-06-25T01:52:29.235Z"}},"at":1782352349235},{"event":"tool_activity","data":{"activity":{"id":"tool_2783add7-ffa8-482c-ba85-093691e50c6","name":"read","status":"completed","summary":"read: …ampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Lib\\ExpenseInvoiceCreator.php","detail":"","at":"2026-06-25T01:52:29.240Z"}},"at":1782352349240},{"event":"tool_activity","data":{"activity":{"id":"tool_1cf47410-52c1-49c3-8677-4b4448abf1f","name":"read","status":"completed","summary":"read: …htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\View\\Master\\MobileLayout.html.twig","detail":"","at":"2026-06-25T01:52:29.246Z"}},"at":1782352349246},{"event":"tool_activity","data":{"activity":{"id":"tool_070e61e8-ce0d-4174-96d6-3770a3dd9bf","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Plugins\\ALimpiezasMobile\\Translation\\es_ES.json","detail":"","at":"2026-06-25T01:52:29.246Z"}},"at":1782352349246},{"event":"tool_activity","data":{"activity":{"id":"tool_975adbf5-7c37-45ab-b7c8-eb61a57d8c5","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\docs\\MOBILE-BRIDGE-TEST.md","detail":"","at":"2026-06-25T01:52:29.349Z"}},"at":1782352349349},{"event":"tool_activity","data":{"activity":{"id":"tool_e3ed939e-b454-4e3f-b741-c755f53e162","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\Traits\\RandomDataTrait.php","detail":"","at":"2026-06-25T01:52:30.684Z"}},"at":1782352350684},{"event":"tool_activity","data":{"activity":{"id":"tool_ccb94385-0b98-48fe-b792-04e73b066a8","name":"read","status":"running","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\test-plugins.php","detail":"","at":"2026-06-25T01:52:30.770Z"}},"at":1782352350770},{"event":"tool_activity","data":{"activity":{"id":"tool_e3ed939e-b454-4e3f-b741-c755f53e162","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\Traits\\RandomDataTrait.php","detail":"","at":"2026-06-25T01:52:30.779Z"}},"at":1782352350779},{"event":"tool_activity","data":{"activity":{"id":"tool_ccb94385-0b98-48fe-b792-04e73b066a8","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\Test\\test-plugins.php","detail":"","at":"2026-06-25T01:52:30.878Z"}},"at":1782352350878},{"event":"tool_activity","data":{"activity":{"id":"tool_28db45cb-a40c-47da-b6a9-e95aa681caa","name":"read","status":"completed","summary":"read: C:\\xampp\\htdocs\\ALimpiezas\\.htaccess","detail":"","at":"2026-06-25T01:52:31.050Z"}},"at":1782352351050},{"event":"text","data":{"chunk":"# Convenciones del proyecto ALimpiezas\n\nERP basado en **FacturaScripts 2026.4** con plugin propio `ALimpiezasMobile` y microservicio Python `ai-service/`. Las convenciones siguen el framework upstream; el código custom vive casi siempre en `Plugins/`.\n\n---\n\n## 1. Naming\n\n### Namespaces (PSR-4)\n\nDefinidos en `composer.json`:\n\n| Prefijo | Carpeta | Uso |\n|---------|---------|-----|\n| `FacturaScripts\\Core\\` | `Core/` | Framework base (upstream) |\n| `FacturaScripts\\Dinamic\\` | `Dinamic/` | Capa desplegada (symlinks/copias tras `Plugins::deploy`) |\n| `FacturaScripts\\Plugins\\{NombrePlugin}\\` | `Plugins/{NombrePlugin}/` | **Código custom** |\n| `FacturaScripts\\Test\\` | `Test/` | Tests PHPUnit |\n\nEjemplo real del plugin:\n\n```3:9:Plugins/ALimpiezasMobile/Init.php\nnamespace FacturaScripts\\Plugins\\ALimpiezasMobile;\n\nuse FacturaScripts\\Core\\DbUpdater;\nuse FacturaScripts\\Core\\Template\\InitClass;\nuse FacturaScripts\\Core\\Tools;\n\nclass Init extends InitClass\n```\n\n### Archivos y carpetas del plugin\n\nEstructura estándar FacturaScripts + ALimpiezas:\n\n```\nPlugins/ALimpiezasMobile/\n├── facturascripts.ini      # metadatos del plugin\n├── Init.php                # hooks init/update/uninstall\n├── Controller/             # MobileExpenses.php, MobileHome.php…\n├── Lib/                    # lógica de negocio (MobileConfig, AiExpenseProcessor…)\n├── Model/                  # AiExpenseQueue.php\n├── Table/                  # ai_expense_queue.xml\n├── View/                   # MobileExpenses.html.twig, Master/MobileLayout.html.twig\n├── Assets/                 # CSS/JS → se copian a Dinamic/Assets/\n└── Translation/            # es_ES.json\n```\n\n| Tipo | Convención | Ejemplo |\n|------|------------|---------|\n| Plugin | PascalCase, sin espacios | `ALimpiezasMobile` |\n| Controlador | PascalCase = nombre de ruta | `MobileExpenses` → `/ALimpiezas/MobileExpenses` |\n| Clase Lib | PascalCase descriptivo | `ExpenseInvoiceCreator`, `VoiceCommandBridge` |\n| Modelo | PascalCase; tablas `snake_case` | `AiExpenseQueue` → `ai_expense_queue` |\n| Vista Twig | Mismo nombre que controlador | `MobileExpenses.html.twig` |\n| Layout | `View/Master/` | `MobileLayout.html.twig` |\n| Tabla XML | `Table/{nombre_tabla}.xml` | `ai_expense_queue.xml` |\n| Assets | `Assets/CSS/`, `Assets/JS/` | `mobile.css`, `mobile.js` |\n| Constantes de modo | `MODE_*` en clases Lib | `MobileConfig::MODE_OCR` |\n\n### Modelos del core (español de negocio)\n\nLos modelos ERP usan nombres en español alineados con tablas BD:\n\n- `FacturaProveedor`, `FacturaCliente`, `Proveedor`, `Cliente`\n- Líneas: `LineaFacturaProveedor`, `LineaAlbaranCliente`\n- Métodos estáticos obligatorios: `primaryColumn()`, `tableName()`\n\n```38:46:Plugins/ALimpiezasMobile/Model/AiExpenseQueue.php\n    public static function primaryColumn(): string\n    {\n        return 'id';\n    }\n\n    public static function tableName(): string\n    {\n        return 'ai_expense_queue';\n    }\n```\n\n### Funciones y métodos\n\n| Patrón | Convención |\n|--------|------------|\n| Hooks de controlador | `getPageData()`, `privateCore()` |\n| Hooks de plugin | `init()`, `update()`, `uninstall()` en `Init` |\n| Helpers estáticos en `Lib/` | `camelCase`: `processUpload()`, `createFromQueue()` |\n| Respuestas JSON/API | array con claves `ok`, `message` (y opcionalmente `reasons`, `hint`) |\n| Acciones POST | parámetro `action` con valores en minúsculas: `upload`, `confirm`, `reject` |\n| Settings | grupo en minúsculas: `'alimpiezas'` |\n| Claves i18n | kebab-case con prefijo plugin | `\"alimpiezas-mobile-expenses\"` |\n\n### Configuración global (`config.php`)\n\nConstantes con prefijo `FS_`:\n\n```3:19:config.php\ndefine('FS_ROUTE', '/ALimpiezas');\ndefine('FS_DB_TYPE', 'mysql');\ndefine('FS_DB_HOST', 'localhost');\ndefine('FS_DB_NAME', 'alimpiezas_fs');\ndefine('FS_LANG', 'es_ES');\ndefine('FS_TIMEZONE', 'Europe/Madrid');\ndefine('FS_DEBUG', false);\ndefine('FS_INITIAL_USER', 'admin');\n```\n\n### Estilo PHP\n\n- **PSR-12** (con excepciones documentadas en `phpcs.xml`)\n- `declare(strict_types=1)` no es obligatorio en todo el repo\n- PHPDoc con copyright FacturaScripts en archivos del core/tests\n- PHP ≥ 8.1\n\n---\n\n## 2. Patrones repetidos\n\n### Capas Core → Dinamic → Plugin\n\n```\nPlugins/ALimpiezasMobile/     ← editar aquí\n        ↓ Plugins::deploy()\nDinamic/                      ← generado, no versionar (.gitignore)\n        ↓ usa modelos/lib del core mergeados\nCore/                         ← upstream FacturaScripts\n```\n\nEn plugins, importar modelos del ERP vía **`FacturaScripts\\Dinamic\\Model\\*`**, no `Core\\Model`:\n\n```9:13:Plugins/ALimpiezasMobile/Lib/ExpenseInvoiceCreator.php\nuse FacturaScripts\\Dinamic\\Model\\AttachedFile;\nuse FacturaScripts\\Dinamic\\Model\\AttachedFileRelation;\nuse FacturaScripts\\Dinamic\\Model\\FacturaProveedor;\nuse FacturaScripts\\Dinamic\\Model\\Proveedor;\nuse FacturaScripts\\Plugins\\ALimpiezasMobile\\Model\\AiExpenseQueue;\n```\n\n### Hook `Init` del plugin\n\nSemilla de settings + migraciones de tabla en `update()`:\n\n```15:40:Plugins/ALimpiezasMobile/Init.php\n    public function update(): void\n    {\n        if (!Tools::settings('alimpiezas', 'ai_service_url')) {\n            Tools::settingsSet('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090');\n        }\n        // ...\n        DbUpdater::createTable('ai_expense_queue');\n        DbUpdater::updateTable('ai_expense_queue');\n    }\n```\n\n### Controlador: GET vista / POST JSON por `action`\n\n```31:75:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function privateCore(&$response, $user, $permissions)\n    {\n        parent::privateCore($response, $user, $permissions);\n\n        if ($this->request->method() !== 'POST') {\n            $this->setTemplate('MobileExpenses');\n            $this->title = 'Gastos e IA';\n            $this->loadQueue($user->nick);\n            return;\n        }\n\n        $action = $this->request->request->get('action', '');\n\n        switch ($action) {\n            case 'upload':\n                $this->jsonResponse($response, $this->handleUpload($user));\n                return;\n            // ...\n        }\n\n        $this->jsonResponse($response, ['ok' => false, 'message' => 'Acción no válida']);\n    }\n```\n\n`getPageData()` registra entrada de menú:\n\n```20:28:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function getPageData(): array\n    {\n        $data = parent::getPageData();\n        $data['menu'] = 'ALimpiezasMobile';\n        $data['title'] = 'alimpiezas-mobile-expenses';\n        $data['icon'] = 'fa-solid fa-camera';\n        $data['showonmenu'] = true;\n        $data['ordernum'] = 2;\n        return $data;\n    }\n```\n\n### Settings en BD (`Tools::settings`)\n\nPatrón central para configuración runtime (no `config.php`):\n\n```13:34:Plugins/ALimpiezasMobile/Lib/MobileConfig.php\n    public static function expenseMode(): string\n    {\n        $mode = Tools::settings('alimpiezas', 'expense_mode', self::MODE_OCR);\n        return in_array($mode, [self::MODE_MANUAL, self::MODE_OCR, self::MODE_AI], true)\n            ? $mode\n            : self::MODE_OCR;\n    }\n\n    public static function aiServiceUrl(): string\n    {\n        return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');\n    }\n```\n\n| API | Uso |\n|-----|-----|\n| `Tools::settings('grupo', 'clave', $default)` | Lectura |\n| `Tools::settingsSet('grupo', 'clave', $valor)` | Escritura |\n| `Tools::config('route')` | Constantes de `config.php` |\n\n### Lib como servicios estáticos\n\nLa lógica pesada va en `Lib/`, no en controladores:\n\n- `AiExpenseProcessor` — cola, upload, confirmación\n- `ExpenseInvoiceCreator` — crea `FacturaProveedor` + `Calculator::calculate`\n- `MobileConfig` — lectura de settings\n- `VoiceCommandBridge` — parseo de intenciones de voz\n\nRespuesta estándar:\n\n```php\n['ok' => false, 'message' => '...', 'reasons' => [...], 'hint' => '...']\n```\n\n### Definición de tablas vía XML\n\n`Table/ai_expense_queue.xml` + `DbUpdater::createTable()` en `Init::update()`.\n\n### Despliegue de plugin (obligatorio tras cambios)\n\n```powershell\nphp tools/bootstrap-plugin.php\n```\n\nEquivale a `Plugins::enable('ALimpiezasMobile')` + `Plugins::deploy(true, true)`.\n\n### Assets y Twig\n\nSiempre referenciar assets **desplegados** en `Dinamic/Assets/`:\n\n```15:17:Plugins/ALimpiezasMobile/View/Master/MobileLayout.html.twig\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625e\">\n```\n\nHelpers Twig habituales: `asset()`, `formToken()`, variable `fsc`.\n\n### Microservicio IA (Python/FastAPI)\n\n- Variables de entorno: `TESSERACT_CMD`, `OPENAI_API_KEY`\n- Endpoints: `/health`, `/api/v1/extract-invoice`, `/api/v1/transcribe`\n- Gestión: `tools/ai-service-manager.ps1`\n\n### Apache rewrite\n\nSolo `Dinamic/Assets/` y `node_modules/` quedan fuera del front controller:\n\n```15:20:.htaccess\n<IfModule mod_rewrite.c>\n   RewriteEngine On\n   RewriteBase /ALimpiezas\n   RewriteCond %{REQUEST_URI} !Dinamic/Assets/ [NC]\n   RewriteCond %{REQUEST_URI} !node_modules/ [NC]\n   RewriteRule . index.php [L]\n</IfModule>\n```\n\n---\n\n## 3. Tests\n\n### Ubicación\n\n| Suite | Carpeta | Config |\n|-------|---------|--------|\n| Core | `Test/Core/` (Model, Lib, Controller…) | `phpunit.xml` |\n| Plugins | `Test/Plugins/` (definido, **aún vacío**) | `phpunit-plugins.xml` |\n| Fixtures | `Test/__files/` | — |\n| Traits reutilizables | `Test/Traits/` | `RandomDataTrait`, `DefaultSettingsTrait` |\n\n### Convención de nombres\n\n- Archivos: `*Test.php` (ej. `ToolsTest.php`, `FacturaProveedorTest.php`)\n- Clase: `{Nombre}Test extends TestCase`\n- Métodos: `test{Cosa}(): void`\n\n```27:32:Test/Core/ToolsTest.php\nfinal class ToolsTest extends TestCase\n{\n    public function testAscii(): void\n    {\n        $this->assertEquals(' aeiou aeiou aeiou aeiou ao cn ', Tools::ascii(' aeiou áéíóú àèìòù âêîôû ãõ çñ '));\n    }\n```\n\n### Cómo ejecutarlos\n\nRequiere `config.php` válido y BD accesible (PHPUnit usa la BD real).\n\n```powershell\ncd C:\\xampp\\htdocs\\ALimpiezas\ncomposer install --ignore-platform-req=ext-pgsql\n\n# Suite core (por defecto)\ncomposer test\n# o\nvendor/bin/phpunit\n\n# Suite plugins (cuando existan tests en Test/Plugins/)\nvendor/bin/phpunit -c phpunit-plugins.xml\n\n# Cobertura\ncomposer test:coverage\n\n# Análisis estático (solo Core/)\ncomposer phpstan\n\n# Estilo (Core/ + Test/)\ncomposer cs-check\ncomposer cs-fix\n```\n\nBootstrap: `Test/bootstrap.php` carga `config.php` del proyecto y limpia caché.\n\n**Nota ALimpiezas:** no hay tests propios del plugin en `Test/Plugins/` todavía; QA manual documentada en `docs/MANUAL-AGENTE-IA.md` (curl health, assets, bootstrap).\n\n---\n\n## 4. Config\n\n### Jerarquía de configuración\n\n| Fuente | Qué guarda | Versionar |\n|--------|------------|-----------|\n| `config.php` | BD, ruta (`FS_ROUTE`), idioma, timezone, debug, credenciales iniciales | **No** (`.gitignore`) |\n| Tabla `settings` vía `Tools::settings` | Config runtime del plugin (`alimpiezas.*`) | Sí (datos en BD) |\n| Variables de entorno (`ai-service`) | `TESSERACT_CMD`, `OPENAI_API_KEY` | **No** |\n| `.env` | Ignorado por git; no es el mecanismo principal de PHP | **No** |\n\n### `config.php` — constantes clave\n\n| Constante | Propósito |\n|-----------|-----------|\n| `FS_ROUTE` | Prefijo URL (`/ALimpiezas`) |\n| `FS_DB_*` | Conexión MariaDB |\n| `FS_LANG` / `FS_TIMEZONE` | Locale |\n| `FS_DEBUG` | Modo depuración |\n| `FS_INITIAL_USER` / `FS_INITIAL_PASS` | Usuario tras instalación |\n| `FS_HIDDEN_PLUGINS` | Plugins ocultos |\n\nAcceso en código: `Tools::config('route')`, `Tools::config('db_name')`, etc.\n\n### Settings del plugin (`grupo: alimpiezas`)\n\n| Clave | Default | Descripción |\n|-------|---------|-------------|\n| `expense_mode` | `ocr` | `manual` \\| `ocr` \\| `ai` |\n| `ai_service_url` | `http://127.0.0.1:8090` | URL microservicio |\n| `fiscal_regime` | `autonomo` | Régimen fiscal |\n| `voice_use_service` | `0` | Transcripción remota on/off |\n| `ollama_url` / `ollama_model` | (opcional) | LLM local |\n\n### Metadatos del plugin\n\n```1:5:Plugins/ALimpiezasMobile/facturascripts.ini\nname = 'ALimpiezasMobile'\ndescription = 'App móvil PWA e integración IA para autónomos (ALimpiezas)'\nversion = 1\nmin_version = 2026.001\nmin_php = 8.1\n```\n\n### Archivos generados (no editar)\n\n- `MyFiles/` — caché Twig, rutas, changelog BD\n- `Dinamic/` — despliegue dinámico del plugin\n\n---\n\n## 5. Anti-patrones a evitar\n\n| Anti-patrón | Por qué | Qué hacer |\n|-------------|---------|-----------|\n| Editar `Dinamic/` directamente | Se sobrescribe en cada `Plugins::deploy()` | Cambiar en `Plugins/ALimpiezasMobile/` + `php tools/bootstrap-plugin.php` |\n| Servir assets desde `Plugins/.../Assets/` | Apache no los expone; 403 en producción | Usar `asset('Dinamic/Assets/...')` tras deploy |\n| Commitear `config.php` con credenciales | Está en `.gitignore` por diseño | Mantener local; documentar plantilla sin secretos |\n| Usar `FacturaScripts\\Core\\Model\\*` en plugins | Pierde extensiones de otros plugins | Usar `FacturaScripts\\Dinamic\\Model\\*` |\n| Lógica de negocio en controladores | Dificulta tests y reutilización | Mover a `Lib/` (como `AiExpenseProcessor`) |\n| Olvidar `DbUpdater` en `Init::update()` | Tablas/columnas no migran | Definir `Table/*.xml` + llamar `createTable`/`updateTable` |\n| Modificar `Core/` para features custom | Conflictos al actualizar FacturaScripts | Extender vía plugin en `Plugins/` |\n| Asumir `.env` para PHP | El ERP usa `config.php` + `Tools::settings` | Reservar `.env` al microservicio Python |\n| Tests del plugin inexistentes | Regresiones silenciosas en cola IA/facturas | Añadir `Test/Plugins/ALimpiezasMobile/` cuando sea posible |\n| Hardcodear `/ALimpiezas` sin `FS_ROUTE` | Rompe en otros entornos | `Tools::config('route')` o `define('FS_ROUTE', ...)` |\n| POST sin validar `action` | Respuestas inconsistentes | `switch ($action)` con default `['ok' => false, ...]` |\n| Crear facturas sin `Calculator::calculate` | Totales/IVA incorrectos | Seguir patrón de `ExpenseInvoiceCreator` |\n\n---\n\n## Resumen rápido para agentes\n\n1. **Código nuevo** → `Plugins/ALimpiezasMobile/`\n2. **Tras cambios** → `php tools/bootstrap-plugin.php`\n3. **Modelos ERP** → `Dinamic\\Model\\...`\n4. **Config app** → `Tools::settings('alimpiezas', ...)`\n5. **Infra local** → `config.php` + MariaDB `alimpiezas_fs`\n6. **Tests** → `composer test` (core); plugin sin suite propia aún\n7. **Referencia técnica** → `docs/MANUAL-AGENTE-IA.md`"},"at":1782352378206},{"event":"status","data":{"text":"Procesando…"},"at":1782352378195},{"event":"phase_saved","data":{"phaseId":"conventions","progress":{"status":"done","completedAt":"2026-06-25T01:52:58.201Z","name":"Convenciones","summary":"Voy a revisar la raíz del proyecto y archivos clave de configuración para extraer convenciones reales del código.\n# Convenciones del proyecto ALimpiezas\n\nERP basado en **FacturaScripts 2026.4** con plugin propio `ALimpiezasMobile` y microservicio Python `ai-service/`. Las convenciones siguen el framework upstream; el código custom vive casi siempre en `Plugins/`.\n\n---\n\n## 1. Naming\n\n### Namespaces (PSR-4)\n\nDefinidos en `composer.json`:\n\n| Prefijo | Carpeta | Uso |\n|---------|---------|-----|\n| `FacturaScripts\\Core\\` | `Core/` | Framework base (upstream) |\n| `FacturaScripts\\Dinamic\\` | `Dinamic","fullText":"Voy a revisar la raíz del proyecto y archivos clave de configuración para extraer convenciones reales del código.\n# Convenciones del proyecto ALimpiezas\n\nERP basado en **FacturaScripts 2026.4** con plugin propio `ALimpiezasMobile` y microservicio Python `ai-service/`. Las convenciones siguen el framework upstream; el código custom vive casi siempre en `Plugins/`.\n\n---\n\n## 1. Naming\n\n### Namespaces (PSR-4)\n\nDefinidos en `composer.json`:\n\n| Prefijo | Carpeta | Uso |\n|---------|---------|-----|\n| `FacturaScripts\\Core\\` | `Core/` | Framework base (upstream) |\n| `FacturaScripts\\Dinamic\\` | `Dinamic/` | Capa desplegada (symlinks/copias tras `Plugins::deploy`) |\n| `FacturaScripts\\Plugins\\{NombrePlugin}\\` | `Plugins/{NombrePlugin}/` | **Código custom** |\n| `FacturaScripts\\Test\\` | `Test/` | Tests PHPUnit |\n\nEjemplo real del plugin:\n\n```3:9:Plugins/ALimpiezasMobile/Init.php\nnamespace FacturaScripts\\Plugins\\ALimpiezasMobile;\n\nuse FacturaScripts\\Core\\DbUpdater;\nuse FacturaScripts\\Core\\Template\\InitClass;\nuse FacturaScripts\\Core\\Tools;\n\nclass Init extends InitClass\n```\n\n### Archivos y carpetas del plugin\n\nEstructura estándar FacturaScripts + ALimpiezas:\n\n```\nPlugins/ALimpiezasMobile/\n├── facturascripts.ini      # metadatos del plugin\n├── Init.php                # hooks init/update/uninstall\n├── Controller/             # MobileExpenses.php, MobileHome.php…\n├── Lib/                    # lógica de negocio (MobileConfig, AiExpenseProcessor…)\n├── Model/                  # AiExpenseQueue.php\n├── Table/                  # ai_expense_queue.xml\n├── View/                   # MobileExpenses.html.twig, Master/MobileLayout.html.twig\n├── Assets/                 # CSS/JS → se copian a Dinamic/Assets/\n└── Translation/            # es_ES.json\n```\n\n| Tipo | Convención | Ejemplo |\n|------|------------|---------|\n| Plugin | PascalCase, sin espacios | `ALimpiezasMobile` |\n| Controlador | PascalCase = nombre de ruta | `MobileExpenses` → `/ALimpiezas/MobileExpenses` |\n| Clase Lib | PascalCase descriptivo | `ExpenseInvoiceCreator`, `VoiceCommandBridge` |\n| Modelo | PascalCase; tablas `snake_case` | `AiExpenseQueue` → `ai_expense_queue` |\n| Vista Twig | Mismo nombre que controlador | `MobileExpenses.html.twig` |\n| Layout | `View/Master/` | `MobileLayout.html.twig` |\n| Tabla XML | `Table/{nombre_tabla}.xml` | `ai_expense_queue.xml` |\n| Assets | `Assets/CSS/`, `Assets/JS/` | `mobile.css`, `mobile.js` |\n| Constantes de modo | `MODE_*` en clases Lib | `MobileConfig::MODE_OCR` |\n\n### Modelos del core (español de negocio)\n\nLos modelos ERP usan nombres en español alineados con tablas BD:\n\n- `FacturaProveedor`, `FacturaCliente`, `Proveedor`, `Cliente`\n- Líneas: `LineaFacturaProveedor`, `LineaAlbaranCliente`\n- Métodos estáticos obligatorios: `primaryColumn()`, `tableName()`\n\n```38:46:Plugins/ALimpiezasMobile/Model/AiExpenseQueue.php\n    public static function primaryColumn(): string\n    {\n        return 'id';\n    }\n\n    public static function tableName(): string\n    {\n        return 'ai_expense_queue';\n    }\n```\n\n### Funciones y métodos\n\n| Patrón | Convención |\n|--------|------------|\n| Hooks de controlador | `getPageData()`, `privateCore()` |\n| Hooks de plugin | `init()`, `update()`, `uninstall()` en `Init` |\n| Helpers estáticos en `Lib/` | `camelCase`: `processUpload()`, `createFromQueue()` |\n| Respuestas JSON/API | array con claves `ok`, `message` (y opcionalmente `reasons`, `hint`) |\n| Acciones POST | parámetro `action` con valores en minúsculas: `upload`, `confirm`, `reject` |\n| Settings | grupo en minúsculas: `'alimpiezas'` |\n| Claves i18n | kebab-case con prefijo plugin | `\"alimpiezas-mobile-expenses\"` |\n\n### Configuración global (`config.php`)\n\nConstantes con prefijo `FS_`:\n\n```3:19:config.php\ndefine('FS_ROUTE', '/ALimpiezas');\ndefine('FS_DB_TYPE', 'mysql');\ndefine('FS_DB_HOST', 'localhost');\ndefine('FS_DB_NAME', 'alimpiezas_fs');\ndefine('FS_LANG', 'es_ES');\ndefine('FS_TIMEZONE', 'Europe/Madrid');\ndefine('FS_DEBUG', false);\ndefine('FS_INITIAL_USER', 'admin');\n```\n\n### Estilo PHP\n\n- **PSR-12** (con excepciones documentadas en `phpcs.xml`)\n- `declare(strict_types=1)` no es obligatorio en todo el repo\n- PHPDoc con copyright FacturaScripts en archivos del core/tests\n- PHP ≥ 8.1\n\n---\n\n## 2. Patrones repetidos\n\n### Capas Core → Dinamic → Plugin\n\n```\nPlugins/ALimpiezasMobile/     ← editar aquí\n        ↓ Plugins::deploy()\nDinamic/                      ← generado, no versionar (.gitignore)\n        ↓ usa modelos/lib del core mergeados\nCore/                         ← upstream FacturaScripts\n```\n\nEn plugins, importar modelos del ERP vía **`FacturaScripts\\Dinamic\\Model\\*`**, no `Core\\Model`:\n\n```9:13:Plugins/ALimpiezasMobile/Lib/ExpenseInvoiceCreator.php\nuse FacturaScripts\\Dinamic\\Model\\AttachedFile;\nuse FacturaScripts\\Dinamic\\Model\\AttachedFileRelation;\nuse FacturaScripts\\Dinamic\\Model\\FacturaProveedor;\nuse FacturaScripts\\Dinamic\\Model\\Proveedor;\nuse FacturaScripts\\Plugins\\ALimpiezasMobile\\Model\\AiExpenseQueue;\n```\n\n### Hook `Init` del plugin\n\nSemilla de settings + migraciones de tabla en `update()`:\n\n```15:40:Plugins/ALimpiezasMobile/Init.php\n    public function update(): void\n    {\n        if (!Tools::settings('alimpiezas', 'ai_service_url')) {\n            Tools::settingsSet('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090');\n        }\n        // ...\n        DbUpdater::createTable('ai_expense_queue');\n        DbUpdater::updateTable('ai_expense_queue');\n    }\n```\n\n### Controlador: GET vista / POST JSON por `action`\n\n```31:75:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function privateCore(&$response, $user, $permissions)\n    {\n        parent::privateCore($response, $user, $permissions);\n\n        if ($this->request->method() !== 'POST') {\n            $this->setTemplate('MobileExpenses');\n            $this->title = 'Gastos e IA';\n            $this->loadQueue($user->nick);\n            return;\n        }\n\n        $action = $this->request->request->get('action', '');\n\n        switch ($action) {\n            case 'upload':\n                $this->jsonResponse($response, $this->handleUpload($user));\n                return;\n            // ...\n        }\n\n        $this->jsonResponse($response, ['ok' => false, 'message' => 'Acción no válida']);\n    }\n```\n\n`getPageData()` registra entrada de menú:\n\n```20:28:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function getPageData(): array\n    {\n        $data = parent::getPageData();\n        $data['menu'] = 'ALimpiezasMobile';\n        $data['title'] = 'alimpiezas-mobile-expenses';\n        $data['icon'] = 'fa-solid fa-camera';\n        $data['showonmenu'] = true;\n        $data['ordernum'] = 2;\n        return $data;\n    }\n```\n\n### Settings en BD (`Tools::settings`)\n\nPatrón central para configuración runtime (no `config.php`):\n\n```13:34:Plugins/ALimpiezasMobile/Lib/MobileConfig.php\n    public static function expenseMode(): string\n    {\n        $mode = Tools::settings('alimpiezas', 'expense_mode', self::MODE_OCR);\n        return in_array($mode, [self::MODE_MANUAL, self::MODE_OCR, self::MODE_AI], true)\n            ? $mode\n            : self::MODE_OCR;\n    }\n\n    public static function aiServiceUrl(): string\n    {\n        return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');\n    }\n```\n\n| API | Uso |\n|-----|-----|\n| `Tools::settings('grupo', 'clave', $default)` | Lectura |\n| `Tools::settingsSet('grupo', 'clave', $valor)` | Escritura |\n| `Tools::config('route')` | Constantes de `config.php` |\n\n### Lib como servicios estáticos\n\nLa lógica pesada va en `Lib/`, no en controladores:\n\n- `AiExpenseProcessor` — cola, upload, confirmación\n- `ExpenseInvoiceCreator` — crea `FacturaProveedor` + `Calculator::calculate`\n- `MobileConfig` — lectura de settings\n- `VoiceCommandBridge` — parseo de intenciones de voz\n\nRespuesta estándar:\n\n```php\n['ok' => false, 'message' => '...', 'reasons' => [...], 'hint' => '...']\n```\n\n### Definición de tablas vía XML\n\n`Table/ai_expense_queue.xml` + `DbUpdater::createTable()` en `Init::update()`.\n\n### Despliegue de plugin (obligatorio tras cambios)\n\n```powershell\nphp tools/bootstrap-plugin.php\n```\n\nEquivale a `Plugins::enable('ALimpiezasMobile')` + `Plugins::deploy(true, true)`.\n\n### Assets y Twig\n\nSiempre referenciar assets **desplegados** en `Dinamic/Assets/`:\n\n```15:17:Plugins/ALimpiezasMobile/View/Master/MobileLayout.html.twig\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625e\">\n```\n\nHelpers Twig habituales: `asset()`, `formToken()`, variable `fsc`.\n\n### Microservicio IA (Python/FastAPI)\n\n- Variables de entorno: `TESSERACT_CMD`, `OPENAI_API_KEY`\n- Endpoints: `/health`, `/api/v1/extract-invoice`, `/api/v1/transcribe`\n- Gestión: `tools/ai-service-manager.ps1`\n\n### Apache rewrite\n\nSolo `Dinamic/Assets/` y `node_modules/` quedan fuera del front controller:\n\n```15:20:.htaccess\n<IfModule mod_rewrite.c>\n   RewriteEngine On\n   RewriteBase /ALimpiezas\n   RewriteCond %{REQUEST_URI} !Dinamic/Assets/ [NC]\n   RewriteCond %{REQUEST_URI} !node_modules/ [NC]\n   RewriteRule . index.php [L]\n</IfModule>\n```\n\n---\n\n## 3. Tests\n\n### Ubicación\n\n| Suite | Carpeta | Config |\n|-------|---------|--------|\n| Core | `Test/Core/` (Model, Lib, Controller…) | `phpunit.xml` |\n| Plugins | `Test/Plugins/` (definido, **aún vacío**) | `phpunit-plugins.xml` |\n| Fixtures | `Test/__files/` | — |\n| Traits reutilizables | `Test/Traits/` | `RandomDataTrait`, `DefaultSettingsTrait` |\n\n### Convención de nombres\n\n- Archivos: `*Test.php` (ej. `ToolsTest.php`, `FacturaProveedorTest.php`)\n- Clase: `{Nombre}Test extends TestCase`\n- Métodos: `test{Cosa}(): void`\n\n```27:32:Test/Core/ToolsTest.php\nfinal class ToolsTest extends TestCase\n{\n    public function testAscii(): void\n    {\n        $this->assertEquals(' aeiou aeiou aeiou aeiou ao cn ', Tools::ascii(' aeiou áéíóú àèìòù âêîôû ãõ çñ '));\n    }\n```\n\n### Cómo ejecutarlos\n\nRequiere `config.php` válido y BD accesible (PHPUnit usa la BD real).\n\n```powershell\ncd C:\\xampp\\htdocs\\ALimpiezas\ncomposer install --ignore-platform-req=ext-pgsql\n\n# Suite core (por defecto)\ncomposer test\n# o\nvendor/bin/phpunit\n\n# Suite plugins (cuando existan tests en Test/Plugins/)\nvendor/bin/phpunit -c phpunit-plugins.xml\n\n# Cobertura\ncomposer test:coverage\n\n# Análisis estático (solo Core/)\ncomposer phpstan\n\n# Estilo (Core/ + Test/)\ncomposer cs-check\ncomposer cs-fix\n```\n\nBootstrap: `Test/bootstrap.php` carga `config.php` del proyecto y limpia caché.\n\n**Nota ALimpiezas:** no hay tests propios del plugin en `Test/Plugins/` todavía; QA manual documentada en `docs/MANUAL-AGENTE-IA.md` (curl health, assets, bootstrap).\n\n---\n\n## 4. Config\n\n### Jerarquía de configuración\n\n| Fuente | Qué guarda | Versionar |\n|--------|------------|-----------|\n| `config.php` | BD, ruta (`FS_ROUTE`), idioma, timezone, debug, credenciales iniciales | **No** (`.gitignore`) |\n| Tabla `settings` vía `Tools::settings` | Config runtime del plugin (`alimpiezas.*`) | Sí (datos en BD) |\n| Variables de entorno (`ai-service`) | `TESSERACT_CMD`, `OPENAI_API_KEY` | **No** |\n| `.env` | Ignorado por git; no es el mecanismo principal de PHP | **No** |\n\n### `config.php` — constantes clave\n\n| Constante | Propósito |\n|-----------|-----------|\n| `FS_ROUTE` | Prefijo URL (`/ALimpiezas`) |\n| `FS_DB_*` | Conexión MariaDB |\n| `FS_LANG` / `FS_TIMEZONE` | Locale |\n| `FS_DEBUG` | Modo depuración |\n| `FS_INITIAL_USER` / `FS_INITIAL_PASS` | Usuario tras instalación |\n| `FS_HIDDEN_PLUGINS` | Plugins ocultos |\n\nAcceso en código: `Tools::config('route')`, `Tools::config('db_name')`, etc.\n\n### Settings del plugin (`grupo: alimpiezas`)\n\n| Clave | Default | Descripción |\n|-------|---------|-------------|\n| `expense_mode` | `ocr` | `manual` \\| `ocr` \\| `ai` |\n| `ai_service_url` | `http://127.0.0.1:8090` | URL microservicio |\n| `fiscal_regime` | `autonomo` | Régimen fiscal |\n| `voice_use_service` | `0` | Transcripción remota on/off |\n| `ollama_url` / `ollama_model` | (opcional) | LLM local |\n\n### Metadatos del plugin\n\n```1:5:Plugins/ALimpiezasMobile/facturascripts.ini\nname = 'ALimpiezasMobile'\ndescription = 'App móvil PWA e integración IA para autónomos (ALimpiezas)'\nversion = 1\nmin_version = 2026.001\nmin_php = 8.1\n```\n\n### Archivos generados (no editar)\n\n- `MyFiles/` — caché Twig, rutas, changelog BD\n- `Dinamic/` — despliegue dinámico del plugin\n\n---\n\n## 5. Anti-patrones a evitar\n\n| Anti-patrón | Por qué | Qué hacer |\n|-------------|---------|-----------|\n| Editar `Dinamic/` directamente | Se sobrescribe en cada `Plugins::deploy()` | Cambiar en `Plugins/ALimpiezasMobile/` + `php tools/bootstrap-plugin.php` |\n| Servir assets desde `Plugins/.../Assets/` | Apache no los expone; 403 en producción | Usar `asset('Dinamic/Assets/...')` tras deploy |\n| Commitear `config.php` con credenciales | Está en `.gitignore` por diseño | Mantener local; documentar plantilla sin secretos |\n| Usar `FacturaScripts\\Core\\Model\\*` en plugins | Pierde extensiones de otros plugins | Usar `FacturaScripts\\Dinamic\\Model\\*` |\n| Lógica de negocio en controladores | Dificulta tests y reutilización | Mover a `Lib/` (como `AiExpenseProcessor`) |\n| Olvidar `DbUpdater` en `Init::update()` | Tablas/columnas no migran | Definir `Table/*.xml` + llamar `createTable`/`updateTable` |\n| Modificar `Core/` para features custom | Conflictos al actualizar FacturaScripts | Extender vía plugin en `Plugins/` |\n| Asumir `.env` para PHP | El ERP usa `config.php` + `Tools::settings` | Reservar `.env` al microservicio Python |\n| Tests del plugin inexistentes | Regresiones silenciosas en cola IA/facturas | Añadir `Test/Plugins/ALimpiezasMobile/` cuando sea posible |\n| Hardcodear `/ALimpiezas` sin `FS_ROUTE` | Rompe en otros entornos | `Tools::config('route')` o `define('FS_ROUTE', ...)` |\n| POST sin validar `action` | Respuestas inconsistentes | `switch ($action)` con default `['ok' => false, ...]` |\n| Crear facturas sin `Calculator::calculate` | Totales/IVA incorrectos | Seguir patrón de `ExpenseInvoiceCreator` |\n\n---\n\n## Resumen rápido para agentes\n\n1. **Código nuevo** → `Plugins/ALimpiezasMobile/`\n2. **Tras cambios** → `php tools/bootstrap-plugin.php`\n3. **Modelos ERP** → `Dinamic\\Model\\...`\n4. **Config app** → `Tools::settings('alimpiezas', ...)`\n5. **Infra local** → `config.php` + MariaDB `alimpiezas_fs`\n6. **Tests** → `composer test` (core); plugin sin suite propia aún\n7. **Referencia técnica** → `docs/MANUAL-AGENTE-IA.md`"}},"at":1782352378203},{"event":"done","data":{"ok":true,"bridgeSessionId":"bs-1782352378199-6adj1f","agentId":"agent-0dd6ad87-ad7f-4149-92f8-c1368e81c7b8","runId":"run-05509707-a769-43fd-a1a2-39e1fa486bd5","status":"finished","result":"# Convenciones del proyecto ALimpiezas\n\nERP basado en **FacturaScripts 2026.4** con plugin propio `ALimpiezasMobile` y microservicio Python `ai-service/`. Las convenciones siguen el framework upstream; el código custom vive casi siempre en `Plugins/`.\n\n---\n\n## 1. Naming\n\n### Namespaces (PSR-4)\n\nDefinidos en `composer.json`:\n\n| Prefijo | Carpeta | Uso |\n|---------|---------|-----|\n| `FacturaScripts\\Core\\` | `Core/` | Framework base (upstream) |\n| `FacturaScripts\\Dinamic\\` | `Dinamic/` | Capa desplegada (symlinks/copias tras `Plugins::deploy`) |\n| `FacturaScripts\\Plugins\\{NombrePlugin}\\` | `Plugins/{NombrePlugin}/` | **Código custom** |\n| `FacturaScripts\\Test\\` | `Test/` | Tests PHPUnit |\n\nEjemplo real del plugin:\n\n```3:9:Plugins/ALimpiezasMobile/Init.php\nnamespace FacturaScripts\\Plugins\\ALimpiezasMobile;\n\nuse FacturaScripts\\Core\\DbUpdater;\nuse FacturaScripts\\Core\\Template\\InitClass;\nuse FacturaScripts\\Core\\Tools;\n\nclass Init extends InitClass\n```\n\n### Archivos y carpetas del plugin\n\nEstructura estándar FacturaScripts + ALimpiezas:\n\n```\nPlugins/ALimpiezasMobile/\n├── facturascripts.ini      # metadatos del plugin\n├── Init.php                # hooks init/update/uninstall\n├── Controller/             # MobileExpenses.php, MobileHome.php…\n├── Lib/                    # lógica de negocio (MobileConfig, AiExpenseProcessor…)\n├── Model/                  # AiExpenseQueue.php\n├── Table/                  # ai_expense_queue.xml\n├── View/                   # MobileExpenses.html.twig, Master/MobileLayout.html.twig\n├── Assets/                 # CSS/JS → se copian a Dinamic/Assets/\n└── Translation/            # es_ES.json\n```\n\n| Tipo | Convención | Ejemplo |\n|------|------------|---------|\n| Plugin | PascalCase, sin espacios | `ALimpiezasMobile` |\n| Controlador | PascalCase = nombre de ruta | `MobileExpenses` → `/ALimpiezas/MobileExpenses` |\n| Clase Lib | PascalCase descriptivo | `ExpenseInvoiceCreator`, `VoiceCommandBridge` |\n| Modelo | PascalCase; tablas `snake_case` | `AiExpenseQueue` → `ai_expense_queue` |\n| Vista Twig | Mismo nombre que controlador | `MobileExpenses.html.twig` |\n| Layout | `View/Master/` | `MobileLayout.html.twig` |\n| Tabla XML | `Table/{nombre_tabla}.xml` | `ai_expense_queue.xml` |\n| Assets | `Assets/CSS/`, `Assets/JS/` | `mobile.css`, `mobile.js` |\n| Constantes de modo | `MODE_*` en clases Lib | `MobileConfig::MODE_OCR` |\n\n### Modelos del core (español de negocio)\n\nLos modelos ERP usan nombres en español alineados con tablas BD:\n\n- `FacturaProveedor`, `FacturaCliente`, `Proveedor`, `Cliente`\n- Líneas: `LineaFacturaProveedor`, `LineaAlbaranCliente`\n- Métodos estáticos obligatorios: `primaryColumn()`, `tableName()`\n\n```38:46:Plugins/ALimpiezasMobile/Model/AiExpenseQueue.php\n    public static function primaryColumn(): string\n    {\n        return 'id';\n    }\n\n    public static function tableName(): string\n    {\n        return 'ai_expense_queue';\n    }\n```\n\n### Funciones y métodos\n\n| Patrón | Convención |\n|--------|------------|\n| Hooks de controlador | `getPageData()`, `privateCore()` |\n| Hooks de plugin | `init()`, `update()`, `uninstall()` en `Init` |\n| Helpers estáticos en `Lib/` | `camelCase`: `processUpload()`, `createFromQueue()` |\n| Respuestas JSON/API | array con claves `ok`, `message` (y opcionalmente `reasons`, `hint`) |\n| Acciones POST | parámetro `action` con valores en minúsculas: `upload`, `confirm`, `reject` |\n| Settings | grupo en minúsculas: `'alimpiezas'` |\n| Claves i18n | kebab-case con prefijo plugin | `\"alimpiezas-mobile-expenses\"` |\n\n### Configuración global (`config.php`)\n\nConstantes con prefijo `FS_`:\n\n```3:19:config.php\ndefine('FS_ROUTE', '/ALimpiezas');\ndefine('FS_DB_TYPE', 'mysql');\ndefine('FS_DB_HOST', 'localhost');\ndefine('FS_DB_NAME', 'alimpiezas_fs');\ndefine('FS_LANG', 'es_ES');\ndefine('FS_TIMEZONE', 'Europe/Madrid');\ndefine('FS_DEBUG', false);\ndefine('FS_INITIAL_USER', 'admin');\n```\n\n### Estilo PHP\n\n- **PSR-12** (con excepciones documentadas en `phpcs.xml`)\n- `declare(strict_types=1)` no es obligatorio en todo el repo\n- PHPDoc con copyright FacturaScripts en archivos del core/tests\n- PHP ≥ 8.1\n\n---\n\n## 2. Patrones repetidos\n\n### Capas Core → Dinamic → Plugin\n\n```\nPlugins/ALimpiezasMobile/     ← editar aquí\n        ↓ Plugins::deploy()\nDinamic/                      ← generado, no versionar (.gitignore)\n        ↓ usa modelos/lib del core mergeados\nCore/                         ← upstream FacturaScripts\n```\n\nEn plugins, importar modelos del ERP vía **`FacturaScripts\\Dinamic\\Model\\*`**, no `Core\\Model`:\n\n```9:13:Plugins/ALimpiezasMobile/Lib/ExpenseInvoiceCreator.php\nuse FacturaScripts\\Dinamic\\Model\\AttachedFile;\nuse FacturaScripts\\Dinamic\\Model\\AttachedFileRelation;\nuse FacturaScripts\\Dinamic\\Model\\FacturaProveedor;\nuse FacturaScripts\\Dinamic\\Model\\Proveedor;\nuse FacturaScripts\\Plugins\\ALimpiezasMobile\\Model\\AiExpenseQueue;\n```\n\n### Hook `Init` del plugin\n\nSemilla de settings + migraciones de tabla en `update()`:\n\n```15:40:Plugins/ALimpiezasMobile/Init.php\n    public function update(): void\n    {\n        if (!Tools::settings('alimpiezas', 'ai_service_url')) {\n            Tools::settingsSet('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090');\n        }\n        // ...\n        DbUpdater::createTable('ai_expense_queue');\n        DbUpdater::updateTable('ai_expense_queue');\n    }\n```\n\n### Controlador: GET vista / POST JSON por `action`\n\n```31:75:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function privateCore(&$response, $user, $permissions)\n    {\n        parent::privateCore($response, $user, $permissions);\n\n        if ($this->request->method() !== 'POST') {\n            $this->setTemplate('MobileExpenses');\n            $this->title = 'Gastos e IA';\n            $this->loadQueue($user->nick);\n            return;\n        }\n\n        $action = $this->request->request->get('action', '');\n\n        switch ($action) {\n            case 'upload':\n                $this->jsonResponse($response, $this->handleUpload($user));\n                return;\n            // ...\n        }\n\n        $this->jsonResponse($response, ['ok' => false, 'message' => 'Acción no válida']);\n    }\n```\n\n`getPageData()` registra entrada de menú:\n\n```20:28:Plugins/ALimpiezasMobile/Controller/MobileExpenses.php\n    public function getPageData(): array\n    {\n        $data = parent::getPageData();\n        $data['menu'] = 'ALimpiezasMobile';\n        $data['title'] = 'alimpiezas-mobile-expenses';\n        $data['icon'] = 'fa-solid fa-camera';\n        $data['showonmenu'] = true;\n        $data['ordernum'] = 2;\n        return $data;\n    }\n```\n\n### Settings en BD (`Tools::settings`)\n\nPatrón central para configuración runtime (no `config.php`):\n\n```13:34:Plugins/ALimpiezasMobile/Lib/MobileConfig.php\n    public static function expenseMode(): string\n    {\n        $mode = Tools::settings('alimpiezas', 'expense_mode', self::MODE_OCR);\n        return in_array($mode, [self::MODE_MANUAL, self::MODE_OCR, self::MODE_AI], true)\n            ? $mode\n            : self::MODE_OCR;\n    }\n\n    public static function aiServiceUrl(): string\n    {\n        return rtrim(Tools::settings('alimpiezas', 'ai_service_url', 'http://127.0.0.1:8090'), '/');\n    }\n```\n\n| API | Uso |\n|-----|-----|\n| `Tools::settings('grupo', 'clave', $default)` | Lectura |\n| `Tools::settingsSet('grupo', 'clave', $valor)` | Escritura |\n| `Tools::config('route')` | Constantes de `config.php` |\n\n### Lib como servicios estáticos\n\nLa lógica pesada va en `Lib/`, no en controladores:\n\n- `AiExpenseProcessor` — cola, upload, confirmación\n- `ExpenseInvoiceCreator` — crea `FacturaProveedor` + `Calculator::calculate`\n- `MobileConfig` — lectura de settings\n- `VoiceCommandBridge` — parseo de intenciones de voz\n\nRespuesta estándar:\n\n```php\n['ok' => false, 'message' => '...', 'reasons' => [...], 'hint' => '...']\n```\n\n### Definición de tablas vía XML\n\n`Table/ai_expense_queue.xml` + `DbUpdater::createTable()` en `Init::update()`.\n\n### Despliegue de plugin (obligatorio tras cambios)\n\n```powershell\nphp tools/bootstrap-plugin.php\n```\n\nEquivale a `Plugins::enable('ALimpiezasMobile')` + `Plugins::deploy(true, true)`.\n\n### Assets y Twig\n\nSiempre referenciar assets **desplegados** en `Dinamic/Assets/`:\n\n```15:17:Plugins/ALimpiezasMobile/View/Master/MobileLayout.html.twig\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/bootstrap/dist/css/bootstrap.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('node_modules/@fortawesome/fontawesome-free/css/all.min.css') }}\">\n    <link rel=\"stylesheet\" href=\"{{ asset('Dinamic/Assets/CSS/mobile.css') }}?v=20260625e\">\n```\n\nHelpers Twig habituales: `asset()`, `formToken()`, variable `fsc`.\n\n### Microservicio IA (Python/FastAPI)\n\n- Variables de entorno: `TESSERACT_CMD`, `OPENAI_API_KEY`\n- Endpoints: `/health`, `/api/v1/extract-invoice`, `/api/v1/transcribe`\n- Gestión: `tools/ai-service-manager.ps1`\n\n### Apache rewrite\n\nSolo `Dinamic/Assets/` y `node_modules/` quedan fuera del front controller:\n\n```15:20:.htaccess\n<IfModule mod_rewrite.c>\n   RewriteEngine On\n   RewriteBase /ALimpiezas\n   RewriteCond %{REQUEST_URI} !Dinamic/Assets/ [NC]\n   RewriteCond %{REQUEST_URI} !node_modules/ [NC]\n   RewriteRule . index.php [L]\n</IfModule>\n```\n\n---\n\n## 3. Tests\n\n### Ubicación\n\n| Suite | Carpeta | Config |\n|-------|---------|--------|\n| Core | `Test/Core/` (Model, Lib, Controller…) | `phpunit.xml` |\n| Plugins | `Test/Plugins/` (definido, **aún vacío**) | `phpunit-plugins.xml` |\n| Fixtures | `Test/__files/` | — |\n| Traits reutilizables | `Test/Traits/` | `RandomDataTrait`, `DefaultSettingsTrait` |\n\n### Convención de nombres\n\n- Archivos: `*Test.php` (ej. `ToolsTest.php`, `FacturaProveedorTest.php`)\n- Clase: `{Nombre}Test extends TestCase`\n- Métodos: `test{Cosa}(): void`\n\n```27:32:Test/Core/ToolsTest.php\nfinal class ToolsTest extends TestCase\n{\n    public function testAscii(): void\n    {\n        $this->assertEquals(' aeiou aeiou aeiou aeiou ao cn ', Tools::ascii(' aeiou áéíóú àèìòù âêîôû ãõ çñ '));\n    }\n```\n\n### Cómo ejecutarlos\n\nRequiere `config.php` válido y BD accesible (PHPUnit usa la BD real).\n\n```powershell\ncd C:\\xampp\\htdocs\\ALimpiezas\ncomposer install --ignore-platform-req=ext-pgsql\n\n# Suite core (por defecto)\ncomposer test\n# o\nvendor/bin/phpunit\n\n# Suite plugins (cuando existan tests en Test/Plugins/)\nvendor/bin/phpunit -c phpunit-plugins.xml\n\n# Cobertura\ncomposer test:coverage\n\n# Análisis estático (solo Core/)\ncomposer phpstan\n\n# Estilo (Core/ + Test/)\ncomposer cs-check\ncomposer cs-fix\n```\n\nBootstrap: `Test/bootstrap.php` carga `config.php` del proyecto y limpia caché.\n\n**Nota ALimpiezas:** no hay tests propios del plugin en `Test/Plugins/` todavía; QA manual documentada en `docs/MANUAL-AGENTE-IA.md` (curl health, assets, bootstrap).\n\n---\n\n## 4. Config\n\n### Jerarquía de configuración\n\n| Fuente | Qué guarda | Versionar |\n|--------|------------|-----------|\n| `config.php` | BD, ruta (`FS_ROUTE`), idioma, timezone, debug, credenciales iniciales | **No** (`.gitignore`) |\n| Tabla `settings` vía `Tools::settings` | Config runtime del plugin (`alimpiezas.*`) | Sí (datos en BD) |\n| Variables de entorno (`ai-service`) | `TESSERACT_CMD`, `OPENAI_API_KEY` | **No** |\n| `.env` | Ignorado por git; no es el mecanismo principal de PHP | **No** |\n\n### `config.php` — constantes clave\n\n| Constante | Propósito |\n|-----------|-----------|\n| `FS_ROUTE` | Prefijo URL (`/ALimpiezas`) |\n| `FS_DB_*` | Conexión MariaDB |\n| `FS_LANG` / `FS_TIMEZONE` | Locale |\n| `FS_DEBUG` | Modo depuración |\n| `FS_INITIAL_USER` / `FS_INITIAL_PASS` | Usuario tras instalación |\n| `FS_HIDDEN_PLUGINS` | Plugins ocultos |\n\nAcceso en código: `Tools::config('route')`, `Tools::config('db_name')`, etc.\n\n### Settings del plugin (`grupo: alimpiezas`)\n\n| Clave | Default | Descripción |\n|-------|---------|-------------|\n| `expense_mode` | `ocr` | `manual` \\| `ocr` \\| `ai` |\n| `ai_service_url` | `http://127.0.0.1:8090` | URL microservicio |\n| `fiscal_regime` | `autonomo` | Régimen fiscal |\n| `voice_use_service` | `0` | Transcripción remota on/off |\n| `ollama_url` / `ollama_model` | (opcional) | LLM local |\n\n### Metadatos del plugin\n\n```1:5:Plugins/ALimpiezasMobile/facturascripts.ini\nname = 'ALimpiezasMobile'\ndescription = 'App móvil PWA e integración IA para autónomos (ALimpiezas)'\nversion = 1\nmin_version = 2026.001\nmin_php = 8.1\n```\n\n### Archivos generados (no editar)\n\n- `MyFiles/` — caché Twig, rutas, changelog BD\n- `Dinamic/` — despliegue dinámico del plugin\n\n---\n\n## 5. Anti-patrones a evitar\n\n| Anti-patrón | Por qué | Qué hacer |\n|-------------|---------|-----------|\n| Editar `Dinamic/` directamente | Se sobrescribe en cada `Plugins::deploy()` | Cambiar en `Plugins/ALimpiezasMobile/` + `php tools/bootstrap-plugin.php` |\n| Servir assets desde `Plugins/.../Assets/` | Apache no los expone; 403 en producción | Usar `asset('Dinamic/Assets/...')` tras deploy |\n| Commitear `config.php` con credenciales | Está en `.gitignore` por diseño | Mantener local; documentar plantilla sin secretos |\n| Usar `FacturaScripts\\Core\\Model\\*` en plugins | Pierde extensiones de otros plugins | Usar `FacturaScripts\\Dinamic\\Model\\*` |\n| Lógica de negocio en controladores | Dificulta tests y reutilización | Mover a `Lib/` (como `AiExpenseProcessor`) |\n| Olvidar `DbUpdater` en `Init::update()` | Tablas/columnas no migran | Definir `Table/*.xml` + llamar `createTable`/`updateTable` |\n| Modificar `Core/` para features custom | Conflictos al actualizar FacturaScripts | Extender vía plugin en `Plugins/` |\n| Asumir `.env` para PHP | El ERP usa `config.php` + `Tools::settings` | Reservar `.env` al microservicio Python |\n| Tests del plugin inexistentes | Regresiones silenciosas en cola IA/facturas | Añadir `Test/Plugins/ALimpiezasMobile/` cuando sea posible |\n| Hardcodear `/ALimpiezas` sin `FS_ROUTE` | Rompe en otros entornos | `Tools::config('route')` o `define('FS_ROUTE', ...)` |\n| POST sin validar `action` | Respuestas inconsistentes | `switch ($action)` con default `['ok' => false, ...]` |\n| Crear facturas sin `Calculator::calculate` | Totales/IVA incorrectos | Seguir patrón de `ExpenseInvoiceCreator` |\n\n---\n\n## Resumen rápido para agentes\n\n1. **Código nuevo** → `Plugins/ALimpiezasMobile/`\n2. **Tras cambios** → `php tools/bootstrap-plugin.php`\n3. **Modelos ERP** → `Dinamic\\Model\\...`\n4. **Config app** → `Tools::settings('alimpiezas', ...)`\n5. **Infra local** → `config.php` + MariaDB `alimpiezas_fs`\n6. **Tests** → `composer test` (core); plugin sin suite propia aún\n7. **Referencia técnica** → `docs/MANUAL-AGENTE-IA.md`","changeSessionId":"cs-1782352330372-rd7dff","pendingChanges":0,"changes":[],"transcriptPath":null,"notifyTitle":"Agente terminado","notifyBody":"Voy a revisar la raíz del proyecto y archivos clave de configuración para extraer convenciones reales del código.\n# Conv"},"at":1782352378203}]}
