⌘J
En esta página 6

Engineering / Motor del Código de Trabajo

Motor del Código de Trabajo

El corazón determinista de Axel, un motor puro sin DB que calcula finiquitos y valida contratos con citas legales, y que nunca toca un LLM.

Diseño aprobado, aún no implementado (sub-proyecto #2, spec preliminar). El motor existe hoy como funciones puras en el PoC (JavaScript, con un motor simulado); lo de abajo es cómo está diseñado reconstruirlo en PHP determinista, usando el PoC como spec de UX y dominio, no como base a copiar. El repo tiene 0 PRs mergeados.

El motor del Código de Trabajo es la feature diferenciadora de Axel: la “verdad” jurídica sobre la que el LLM (la “intención”) se apoya. Entra un DTO de contrato más un corpus de reglas, sale un resultado con montos, citas legales y trazabilidad.

La verdad legal es determinista, nunca la genera un LLM. El documento del contrato lo produce el motor determinista; el LLM solo interpreta la intención del usuario antes de llegar aquí. Para un documento con valor probatorio, un modelo probabilístico no es aceptable como autor del texto legal.

Motor puro, sin DB

El motor vive en app/Domain/Labor/: PHP puro, cero dependencias de Laravel, Eloquent o HTTP. Recibe arrays / DTOs y devuelve arrays / DTOs, como el PoC. Los modelos Eloquent son la capa de persistencia que hidrata y persiste, pero la lógica vive en Domain.

app/Domain/Labor/
  ContractKinds.php      — KINDS, kindOf(), isLaboralHiring()
  ValidationRules.php    — DEFAULT_RULES, rulesFor($type, $overrides)
  Citations.php          — CT_ARTICULOS (data) + corpus curado de sentencias CSJ
  AnalyzeContract.php    — el motor: findings, verdict, citas
  SlaEngine.php          — slaInfo(), pendingSlaEvent()
  TerminationCalc.php    — finiquito (Arts. 58/59/177/187/196-202)
  Employees.php          — deriva empleados desde contratos (sin store aparte)
  Mtps.php               — ventana de 8 días, feriados SV, computus
  Geography.php          — departamentos / municipios

La frontera concreta: entra un ContractData (spatie/laravel-data) plano más el corpus de reglas, nunca un modelo Eloquent. Devuelve un SettlementResult (montos + citas legales + trazabilidad de qué regla aplicó). Las Actions hidratan el DTO desde Eloquent, invocan el motor, y persisten el resultado. El motor no toca la DB: la DB solo persiste resultados, nunca calcula.

Qué calcula

Finiquito y liquidación

TerminationCalc implementa la matemática de los Arts. 58/59/177/187/196-202 del Código de Trabajo: indemnización, tope de 4× salario mínimo, vacaciones, aguinaldo proporcional.

Validación con citas

AnalyzeContract produce findings deterministas (“salario por debajo del Art. 144”, “prueba excede Art. 28”) con su verdict y las citas legales que lo fundamentan.

Parametrizado por jurisdicción

El MVP carga solo el contenido legal de El Salvador, pero el motor nace parametrizado por jurisdicción desde el día 1: las reglas laborales son datos, no constantes fijas en el código.

// ✗ NUNCA — la jurisdicción hardcodeada en el método
$topeIndemnizacion = 4 * $salarioMinimo;

// ✓ SIEMPRE — la regla es dato del corpus, indexado por jurisdicción
$corpus = LaborCorpus::for('sv');
$resultado = TerminationCalc::compute($contractData, $corpus);

CT_ARTICULOS, las ValidationRules, los parámetros de TerminationCalc, los feriados de Mtps y la Geography se cargan indexados por jurisdicción (sv hoy). Agregar Guatemala mañana es cargar un corpus nuevo (gt), no reprogramar el motor. El disparador de graduar app/Domain/Labor/ a un paquete Composer: cuando entre la 2ª jurisdicción y se quiera versionar el motor aparte del deploy.

Contract como aggregate root

Contract es un aggregate root: las mutaciones pasan por métodos que disparan eventos, nunca por un update crudo de status.

// ✗ NUNCA — muta status a mano, sin dejar rastro
$contract->update(['status' => 'firmado']);

// ✓ SIEMPRE — el método de transición persiste status + event
$contract->sendToReview($actor);   // → contract_events (append-only)
contract_events append-only

Cada transición escribe una fila en contract_events (tipo, actor, meta, ts). No hay accessor de escritura para status: solo métodos de transición. La auditoría es completa por construcción.

Sin store de empleados

Los empleados se derivan de los contratos firmados (Employees), no viven en un segundo store. La tenura y el pasivo laboral se computan desde el contrato, no se duplican.

El dinamismo vive en la selección de bloques pre-validados, no en la generación de texto libre. El generador combina una plantilla + cláusulas atómicas + datos variables, como un mail-merge legal. Nunca prosa inventada.

El modelo de datos son dos capas con dueños distintos:

CapaTablaDueño
Global (sin tenant_id)clauses (code, legal_text, legal_citation, requires_fields, version)El curador legal, vía backoffice
Por tenant (con tenant_id)positions (los cargos de cada empresa)Cada empresa piloto
Por tenant (con tenant_id)position_clause (qué cláusulas globales aplican a cada cargo)Cada empresa piloto

Las cláusulas legales son transversales (derecho salvadoreño, igual para todos), así que viven en la tabla global sin tenant_id. Las posiciones/cargos son propias de cada tenant. El pivote position_clause vive en el tenant pero apunta a cláusulas del central: cada empresa combina su cargo con las cláusulas legales que le aplican.

Escala a 100+ contratos sin volverse inmanejable porque no hay una fila por “tipo de contrato”: hay ~30-50 cláusulas atómicas reutilizables, combinadas de formas distintas. Un contrato nuevo típicamente reutiliza el 80% de cláusulas existentes y aporta 1-2 nuevas. El catálogo escala con cláusulas (decenas), no con contratos (cientos).

DocumentGenerator toma la plantilla base + las cláusulas del cargo (con sus condiciones evaluadas) + los datos del contrato, e interpola y concatena, sin generación libre de texto.

Tests golden

Fixtures calculados a mano

Los tests del motor usan finiquitos calculados a mano por abogados como fixtures. Motor puro = tests corren en milisegundos sin DB, en cada commit. Para la pieza donde un error es daño legal, no es negociable.

Equivalencia de contenido legal

El documento generado se compara por equivalencia de contenido legal (mismos datos, cálculos y texto de cláusula), no byte a byte: el formato/maquetación puede diferir del PoC.

El corpus curado es el moat, no el motor

Una vez especificado, el motor determinista se replica en meses. El moat real es el corpus jurídico-laboral salvadoreño curado (incluye el corpus curado de jurisprudencia clave de la CSJ vinculado a cada cálculo), el producto integrado y la distribución. Ver Capa LLM para cómo el copilot cita ese corpus sin inventar.