m1-motor

M1 — Motor de decisión IA

Categoría: SoA portable Estado: Propio sano Tamaño esperado: ~6-8k LOC (núcleo IA + reglas calcificadas semánticas)


1. Responsabilidad

Producir, para cada documento entrante, una decisión contable completa y ERP-agnostic que cualquier integrador (Holded, Sage, Odoo) pueda traducir a operaciones nativas. La decisión es el StandaloneIntent + InternalAccountingPlan + ContextualDecision + ValidationResult.

Es el activo de largo plazo del producto. Sobrevive a cualquier cambio de ERP de abajo.

2. NO es su responsabilidad

  • No habla con Holded (ni con ningún ERP concreto). Cero menciones a tax_key, subdomain, payloads.
  • No decide cómo escribir. Eso lo hace I1 Plan Generator a partir del intent.
  • No persiste verdad contable canónica. El libro diario vive en el ERP, no aquí.
  • No invoca el ejecutor. Produce decisión y la entrega al integrador.
  • No mantiene account_lines paralelo al ledger. Si V3 conserva alguna proyección, vive en I1 como sombra del SoR ajeno, no en M1.

3. Sub-fases internas

El motor sigue las cinco fases del target V2 documentado (que están bien diseñadas):

Sub-fase Tipo Output
Facts Lectura DB + LLM compiler AccountingInputPackage (frozen)
Decisions (3 LLMs) LLM con guards + repair StandaloneIntent
Internal Plan Determinista (función pura) InternalAccountingPlan
Contextual (kernel + planner + delta_builder) Determinista ContextualDecision
Validation Determinista, filtrada a ~5-7 invariantes ValidationResult

Cambios respecto al target V2 documentado:

  • Facts dividido por dueño internamente: facts/extracted/, facts/memory/, facts/erp_snapshot/. La sub-fase reúne las tres, pero los modelos son distintos y se versionan por separado.
  • Decisions integra calcificación semántica nativa: antes de llamar a un LLM, lookup en M1.RuleStore (calcificación nivel motor). Si hay regla → usar regla. Si no → llamar LLM.
  • Internal Plan mantiene función pura. Los defaults de DeterministicAccountingConfig ahora son audit por cliente (T3 detecta gaps).
  • Contextual mantiene la separación kernel (decisión) + delta_builder (mecánica) del target.
  • Validation lista cerrada de invariantes (~5-7) tras el filtro de 3 condiciones.

4. Frontera de salida

El Motor entrega al integrador una tupla inmutable:

{
    standalone_intent: StandaloneIntent,
    internal_plan: InternalAccountingPlan,
    contextual_decision: ContextualDecision,
    validation_result: ValidationResult (status=PASSED),
    motor_metadata: {
        rule_versions: dict[str, str],
        llm_versions: dict[str, str],
        input_hash: str,
        decision_audit_log_id: UUID,
    }
}

El motor_metadata es el contrato de trazabilidad con T3 y T4. Sin él, la calcificación no puede saber qué versión de qué regla produjo qué decisión.

5. Patrones de calcificación en M1

T3 puede observar y proponer reglas semánticas al Motor. Tipos:

Tipo Descripción Ubicación
account_rule "Para este shape de documento + proveedor → cuenta X" sustituye llamada a LLM Account en Fase 2
cost_center_rule "Para este shape de línea + cliente → distribución Y" sustituye llamada a LLM CostCenter
fiscal_rule "Para este tipo + receptor → deducibilidad Z + retención W" sustituye llamada a LLM Fiscal
contextual_rule "Para este patrón de relación documento-previo → naturaleza N" sustituye decisión del kernel contextual

Las reglas semánticas viven en M1.RuleStore. No mezclan con reglas de I1 (que son traducción a Holded). Una regla semántica dice "para este input fiscal → este intent"; una regla de I1 dice "para este intent + estado Holded → esta operación API".

6. Frontera con I1

El Motor nunca importa nada de holded_integration/. Ni a nivel de tipo, ni a nivel de constante. Esta regla se verifica con un lint custom: cualquier import de holded_* desde motor/ falla CI.

Si el Motor necesita facts del ERP (e.g. period locks de Holded para decidir si redatar), los recibe via facts/erp_snapshot/, que es input del Motor, no dependencia. El erp_snapshot lo produce I1 antes del Motor.

7. Riesgos arquitectónicos y mitigaciones

Riesgo Mitigación
Las reglas calcificadas semánticas crecen sin disciplina y el Motor se vuelve un rule engine opaco Métricas obligatorias: % docs servidos por regla vs LLM, alarma si una regla cubre >40% de docs (señal de regla demasiado amplia)
DeterministicAccountingConfig tiene tantos defaults que un cliente nuevo arranca con criterios silenciosamente equivocados T3 monitoriza signals aviso-cuenta-generica por cliente. Si supera umbral, alerta a Intelia ops para auditar config
LLM Compiler de guía de cliente produce CompiledGuide inconsistente con el chart of accounts Validator post-compile que cruza códigos del CompiledGuide contra el chart actual. Inconsistencia → bloquea promote, señaliza humano
decision_guards.py regresa al patrón actual (1142 LOC mezclando decisión + lectura SoR) Disciplina de código: guards consumen facts normalizados. Lecturas SoR viven en facts/erp_snapshot/, no en guards. Lint custom
El Motor no se entera de que el contable cambia el criterio (criterio drift) T4 propaga signal CRITERION_CHANGED al Motor. Intelia ops decide si invalidar reglas calcificadas afectadas

8. Lo que puede salir mal

Patología 1: el Motor se vuelve un proxy del ERP. Si por presión de implementación se mete "si Holded está locked, redatar" dentro del Motor en lugar de en facts/erp_snapshot/, el Motor empieza a saber de Holded. En 6 meses tendremos facts.holded_period_lock_status referenciado en el kernel contextual y el motor ya no es portable. Vigilancia: lint + code review.

Patología 2: regla calcificada con razonamiento desplazado. Si una regla calcificada account_rule es del tipo "proveedor Glovo → cuenta 62980000", está bien. Si es "proveedor Glovo → cuenta 62980000, EXCEPTO si Holded ya tiene mappings para él", está mal: la regla está consultando estado del SoR. La regla debería ser puramente función de los facts, no del estado del ERP. Vigilancia: schema estricto de reglas calcificadas.

Patología 3: bootstrap cruzado introduce sesgo masivo. Cliente nuevo en sector hostelería arranca con reglas heredadas de "cliente mediano hostelería". Si "cliente mediano hostelería" es una empresa muy específica con criterios poco transferibles, el cliente nuevo va a recibir muchas correcciones del contable en primeras semanas. Vigilancia: T3 marca reglas bootstrapped como bootstrap_origin=X; si correction_rate>30% en primeros 30 docs, invalida en bloque y vuelve a 100% agente LLM hasta calcificar de novo.

9. Open questions

  1. ¿Multi-tax-entity per team afecta al Motor? Probablemente el Motor opera por tax_entity_id aislado. Pero el bootstrap cruzado puede ser a nivel team. Decidir si la granularidad de calcificación es tax_entity o team.

  2. Shape del input_hash para lookup en M1.RuleStore. ¿Qué subset del AccountingInputPackage lo determina? Si incluye provider_history completo, casi nunca hay hit. Si solo provider + document_type + amount range, las reglas calcifican sobre patrones demasiado amplios y dan intents incorrectos.

  3. Versioning de guides cliente. Cuando el cliente edita su guía contable, ¿las reglas calcificadas se invalidan automáticamente? ¿O quedan vivas hasta que el LLM las contradiga?

  4. ¿LLM Cost Center merece existir si el cliente tiene reglas declarativas exhaustivas? Si todas las líneas matchean reglas del CompiledGuide con confidence=1, ¿skipear el LLM? Decisión de design para Fase 2.

  5. ¿Cómo se versiona previous_accounting_state que consume el kernel contextual? Si V2 emitió 3 versiones de un documento (post-edición, post-bug-fix), ¿el kernel compara contra última o contra "estable"?

10. Métricas operacionales

Métrica Target
%_docs_served_by_motor_rule_vs_llm Crece con el tiempo. >50% a los 6 meses por team
motor_latency_p95 <2s (sub-fases deterministas) + <8s (Fase 2 con LLMs paralelos)
decision_audit_completeness 100%. Cada decisión tiene rule_versions + input_hash en log
validation_failed_rate <1%. Si sube, hay bug en algún sub-componente del motor
criterion_drift_alerts_per_month_per_client <2. Si sube, las reglas calcificadas no representan el criterio del cliente

Próximos docs