COMO FUNCIONA
El loop de Aurora
Un módulo nace de un manifiesto, depende de contratos —no de implementaciones—, se registra solo en runtime y queda verificado en cada build. El mismo ciclo para el núcleo financiero, Ventas, Inventario o cualquier software empresarial que se construya sobre la plataforma. Aquí está en código real, sin diagramas de relleno.
Aurora es una plataforma de desarrollo vertical cuyo kernel es el libro mayor (GL). Cada módulo orbita el GL y nunca escribe en journal_lines de forma directa: esa es la regla de gravedad. Lo que sigue es el recorrido de un módulo desde su declaración hasta su ejecución verificada, en cuatro tramos. El manifiesto declara qué es el módulo. Aurora Make emite el esqueleto ya cableado como paquete Aurora. El módulo depende de contratos del Platform SDK, no de las clases del kernel. El Runtime lo descubre y lo registra sin que nadie lo conecte a mano. Y en cada build, un conjunto de fitness functions verifica que el resultado respeta las invariantes de la arquitectura. El bucle no es una metáfora: es el proceso que sigue cada módulo que hoy corre en producción.
El manifiesto declara el módulo
Un módulo no se escribe a mano: se declara. El module.yaml fija la identidad y el target, los modelos con campos tipados (money se vuelve numeric(15,2) con cast a string, nunca float), enums, permisos por rol, el posting con su source y sus determinaciones, índices y checks, las filas que se siembran por empresa y la superficie Filament. Es scaffold y contrato a la vez: write-once. El generador determinístico aurora:make-module lo procesa y emite un paquete Aurora en packages/aurora/<kebab> con namespace Aurora\<Module>, ya cableado —provider, migraciones, permisos, costuras de posteo—. La lógica de dominio se escribe a mano sobre el esqueleto verde.
module:
name: Inventory
namespace: Aurora\Inventory
target: package # emite paquete Aurora; in-app si va pegado al kernel
navigation_group: Inventario
models:
StockMovement:
fields:
quantity: { type: decimal }
unit_cost: { type: money } # -> numeric(15,2), cast 'string', BCMath
total_cost: { type: money }
indexes:
- { columns: [company_id, product_id, occurred_at] }
posting:
sources: # varios cases de JournalEntrySource, UNA migración del CHECK
auto_inventory_receipt: "Recepción de inventario"
auto_inventory_issue: "Salida de inventario"
auto_inventory_adjustment: "Ajuste de inventario"
determinations:
- inventory.asset
- inventory.cogs
provisions: # se siembran por empresa al crearla (evento SDK CompanyCreated)
Warehouse:
- { name: "Bodega central", is_default: true } El módulo depende de un contrato, no del kernel
El paquete generado nunca importa App\Modules: su frontera es limpia. Para postear al GL no conoce al módulo del libro mayor; depende de un contrato runtime estable del Aurora Platform SDK. Hay dos formas válidas: el módulo emite \Events y un módulo-kernel postea (como Academia), o postea por las costuras del SDK CreatesJournalEntries / PostsJournalEntries (como Diferidos). En ambas el kernel ejecuta el asiento y el satélite jamás importa el libro mayor. Los contratos viven en packages/aurora/platform-contracts y son la única superficie que el módulo ve del kernel. El módulo evoluciona contra firmas estables, no contra clases que pueden cambiar.
<?php
declare(strict_types=1);
namespace Aurora\Platform\Contracts\Posting;
/**
* El kernel implementa este contrato; el satélite depende SOLO de la interface.
* El módulo nunca importa App\Modules\Accounting: postea por esta costura
* y el adaptador del host hace el forceFill canónico + post.
*/
interface PostsJournalEntries
{
/**
* Crea y postea un asiento canónico a partir de los datos del satélite.
* Las cuentas se resuelven por determinación (nombre canónico), nunca por código.
*/
public function post(JournalEntryData $entry): JournalEntryResult;
} El runtime lo descubre y lo registra solo
Nadie conecta el módulo a mano. Cada paquete trae su ServiceProvider, y composer lo descubre por auto-discovery. En el boot() del provider, el módulo se auto-registra en el ModuleRegistry y aporta sus checks de readiness al OnboardingRegistry. Esa es la infraestructura compartida del Aurora Runtime: descubrimiento y registro, multiempresa con aislamiento schema-per-tenant (stancl/tenancy), CompanyContext como fuente de verdad fuera de HTTP, y las filas declaradas en provisions sembradas por empresa cuando se crea —vía el evento SDK CompanyCreated—. El módulo entra al sistema sin tocar el kernel ni los demás módulos.
<?php
declare(strict_types=1);
namespace Aurora\Inventory;
use Aurora\Platform\Support\ModuleRegistry;
use Aurora\Platform\Support\Onboarding\OnboardingRegistry;
use Illuminate\Support\ServiceProvider;
class InventoryServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Auto-discovery de composer carga este provider; el módulo se registra solo.
$this->app->make(ModuleRegistry::class)
->register(new InventoryModuleManifest());
// Aporta sus checks de readiness por empresa; Core no conoce al satélite.
$this->app->make(OnboardingRegistry::class)
->register(new InventoryOnboardingProvider());
// Migraciones tenant del paquete (schema-per-tenant).
$this->loadMigrationsFrom(__DIR__.'/../database/migrations/tenant');
}
} CI verifica que respeta la arquitectura
El loop se cierra en cada build. La evolución se gobierna con ADRs e invariantes transversales (ADR 0017), y las invariantes que se pueden verificar son fitness functions de Pest Arch que corren en cada build. PackageBoundaryTest comprueba que ningún paquete importe App\Modules. ModuleManifestComplianceTest verifica que el wiring real coincida con lo declarado en el manifiesto. PlatformContractsFreezeTest congela la superficie del SDK: un cambio de firma falla ruidoso en CI en vez de romper N módulos en silencio. Y una regla durable mantiene al libro mayor sin depender de ningún módulo. Lo que el manifiesto declaró queda demostrado, no asumido.
<?php
// Un paquete Aurora NUNCA importa App\Modules: la frontera se verifica, no se confía.
arch('los paquetes aurora no importan el host')
->expect('Aurora')
->not->toUse('App\\Modules');
// El kernel financiero no depende de ningún otro módulo (regla de gravedad).
arch('la contabilidad no depende de ningún módulo')
->expect('App\\Modules\\Accounting')
->not->toUse([
'App\\Modules\\Receivables',
'App\\Modules\\Payables',
'Aurora\\Inventory',
'Aurora\\Sales',
]); Verificación
Lo que CI verifica en cada build
Las invariantes de la arquitectura no son convenciones que se recuerdan: son pruebas que corren. Si un módulo rompe una, el build falla antes del merge.
PackageBoundaryTest
Ningún paquete en packages/aurora/* importa App\Modules. La frontera limpia entre satélite y host se verifica de forma dinámica sobre cada paquete del repo.
ModuleManifestComplianceTest
Por cada module.yaml, el wiring real (provider, migraciones, permisos, costuras de posteo) coincide con lo declarado. Lo generado y lo declarado no pueden divergir en silencio.
PlatformContractsFreezeTest
Congela la superficie del Aurora Platform SDK. Un cambio de firma de un contrato falla ruidoso en CI en vez de romper N módulos dependientes sin aviso.
Accounting depends on no module
La regla de gravedad como prueba: el kernel financiero no importa ningún otro módulo. Todo orbita el GL; el GL no orbita a nadie.
Invariantes de dinero
El dinero es BCMath, nunca float: columnas numeric(15,2) con cast a string. Las cuentas se resuelven por determinación (nombre canónico), nunca por código que diverge entre plantillas.
Núcleos puros endurecidos
Calculadores y resolutores puros se prueban con property-based testing (eris) y mutation testing, para que las leyes que cumplen (conservación BCMath, idempotencia, round-trip void∘post) se sostengan ante cualquier entrada.
Mira el loop sobre módulos reales
El núcleo financiero Quorum, Ventas, Inventario, CRM, Academia y Diferidos corren hoy sobre Aurora con este mismo ciclo. Revisa los casos de uso o habla con el equipo sobre el software empresarial que necesitas construir.