Categoría: Plumería (no contiene lógica de negocio) Estado: Propio sano (mantenido del target V2 documentado) Tamaño esperado: ~2-3k LOC
Dos transversales que envuelven todas las capas de V3 dándoles durabilidad, observabilidad, idempotencia y propagación de eventos. No deciden, no calcifican, no traducen — solo proveen el sustrato durable y el bus de eventos.
| Transversal | Función |
|---|---|
| TX.Signals | Bus inmutable tipado que acumula eventos de las capas y los proyecta a superficies (tags ERP, comments, audit log, alerts internas). |
| TX.Temporal | Durabilidad, retries por dominio de fallo, idempotencia por workflow_id, aislamiento de entornos. |
StandaloneIntent, ExecutionPlan, signals tipados) entre activities sin inspeccionarlas.Las capas emiten eventos tipados sin saber quién los consumirá. El bus dedupea, valida tipado, y proyecta a las superficies correspondientes al final del pipeline (no en runtime).
"Sistema nervioso del pipeline; las fases emiten signals como side-channel observacional y la transversal los proyecta al final una sola vez."
SIGNAL_REGISTRY central con todos los signals válidos. Cada uno tiene SignalSpec:
SignalSpec {
code: str, # "DUDA_CUENTA", "ERROR_LINEAS_VS_TOTAL", ...
severity: ERROR | WARNING | INFO,
intent: REVIEW | INFORM | BLOCK,
audience: list[INTERNAL | CLIENT_VIA_ERP_TAG | CLIENT_VIA_ERP_COMMENT | AUDIT_LOG],
template_message_es: str, # i18n string template
blocks_execution: bool,
dedupe_key_template: str?,
}
Cuando una capa emite un signal con código no registrado en SIGNAL_REGISTRY → falla. Tipado runtime.
| Categoría | Ejemplos | Audiencia típica |
|---|---|---|
| Decisión IA con duda | DUDA_CUENTA, DUDA_IMPUTACION, DUDA_DEDUCIBILIDAD |
tag ERP + comment + audit |
| Decisión IA con fallback | AVISO_CUENTA_GENERICA, AVISO_IMPUTACION_GENERICA |
tag ERP + audit |
| Error material | ERROR_LINEAS_VS_TOTAL, ERROR_CUENTA_INVALIDA, ERROR_FECHA |
tag ERP + comment + alert |
| Provider info | PROVEEDOR_NUEVO, PROVEEDOR_PRIMERA_VEZ_EN_TRIMESTRE |
tag ERP + audit |
| Calcificación | REGLA_APLICADA, REGLA_PROPUESTA_PROMOVIDA, REGLA_INVALIDADA |
audit + alert ops |
| Reverse-sync | EDITED_BY_HUMAN, CRITERION_CHANGED, APPROVED_BY_HUMAN |
audit + (a veces) alert ops |
| Validación post-write | READBACK_DRIFT, COMPENSATION_REQUIRED |
audit + alert |
Cada signal tiene dedupe_key (template parametrizado por document_id, plan_id, etc.). El bus mantiene seen_dedupe_keys por workflow execution. Si un signal con la misma key se emite dos veces → segundo emit se descarta.
Esto previene duplicación cuando una capa emite el mismo signal varias veces durante un workflow.
Al final del workflow (después del Executor, antes de cerrar el WF), un componente signal_projector consume el SignalBag y proyecta:
| Surface | Cómo |
|---|---|
| Tags ERP | holded.add_tags(document, [signal.code para signal con CLIENT_VIA_ERP_TAG audience]) |
| Comments ERP | holded.post_comment(document, render_signals_as_md([signal con CLIENT_VIA_ERP_COMMENT])) con dedupe por hash global |
| Audit log Intelia | INSERT en signal_audit_log table, todos los signals del WF |
| Alerts internas | Slack/PostHog para signals con severity=ERROR o cuando un patrón excede umbral |
La projection es idempotente (al re-ejecutarse, no duplica tags ni comments). Esto es importante porque la lógica de signal lives in different layers — solo se materializa una vez al final.
Esta sección hereda el diseño de transversal-signals.md del target V2. Cambios menores:
ValidationFinding (Fase 5) y FinalValidationFinding (Fase 9) a signals via tabla declarativa (validation_code → SignalSpec).messages_es.py, no en if/elif del código.El pipeline corre como parent workflow + child workflows durables. Cada bloque racional de side effect (LLM call, group provider idempotente, DB read/write) es un activity. Crashes a mid-pipeline reanudan desde último boundary exitoso.
"Es plumería, no lógica de negocio. La pipeline real es invocable desde tests, evals y los activities — todos por igual."
AccountingV3OrchestrationWorkflow (parent)
│
┌─────────────────┴────────────────┐
│ │
MotorWorkflow (child) IntegratorWorkflow (child)
(M1: fases 1-5) (I1: plan gen + executor + final val)
│ │
┌───────┴───────┐ ┌───────┴───────┐
│ activities │ │ activities │
│ - prepare_ │ │ - lookup_plan_│
│ facts │ │ store │
│ - decide_ │ │ - call_plan_ │
│ account │ │ generator │
│ - decide_cost_│ │ - execute_ │
│ center │ │ provider_ │
│ - decide_ │ │ group │
│ fiscal │ │ - capture_ │
│ - compose_ │ │ readback │
│ plan │ │ - validate_ │
│ - validate_ │ │ final │
│ invariants │ └───────────────┘
└───────────────┘
│
▼
┌────────────────────────────┐
│ HighConfidence? │
│ ├─ Yes → executor │
│ └─ No → dashboard wait │
└────────────────────────────┘
Para reverse-sync (T4), hay un workflow distinto que corre asíncrono:
ReverseSyncMonitorWorkflow (independiente, continuous)
│
├── poll_holded_changes_activity
├── interpret_comments_activity (LLM)
└── emit_signals_to_bus_activity
Para calcificación (T3), workflow más complejo:
CalcificationLoopWorkflow (independiente, scheduled)
│
├── collect_evidence_activity
├── propose_rule_activity (genera candidates)
├── shadow_test_activity (compara contra agente N=15 ejecuciones)
├── wait_for_intelia_ops_approval (signal-based)
└── promote_rule_activity
| Dominio | Política | Reasoning |
|---|---|---|
| LLM call | 3 reintentos, backoff exponencial 1s → 10s | LLM falla por rate limit o glitch transitorio |
| Provider API write | 5-10 reintentos con backoff | Holded a veces 5xx; recovery alto |
| DB read | 10 reintentos rápidos | Casi siempre conexión transitoria |
| DB write con constraint | 1 intento, no retry | Constraint failure = bug; no se arregla solo |
non_retryable=True para errores de input validation, schema mismatch, etc.
workflow_id determinista por document_id + version. Reuse policy REJECT_DUPLICATE en prod. Disparar el mismo documento dos veces no duplica trabajo.
Para reprocessing (e.g. re-run con nueva versión de M1): workflow_id = f"doc-{document_id}-v{m1_version}". Nuevo workflow_id permite re-run sin colisión.
Workers arrancan con dos namespaces:
intelia-prod (o intelia-stage): para workflows productivos.evals: aislado, para batch evals sin contaminar history productivo.Tests locales saltan Temporal completamente.
TX es plumería pura. NO contiene lógica fiscal, contable, ni de integración. Solo:
Si una decisión semántica entra en TX (e.g. "si el plan tiene confidence baja, skip el executor"), está en el sitio equivocado. Esa decisión vive en I1 o en S1, no en el orchestration.
| Riesgo | Mitigación |
|---|---|
| Signals se quedan in-memory y no se proyectan si el workflow crashea | Signal bag persiste en Temporal state como parte del workflow → recoverable on resume |
| Latencia adicional por mucha activity granular | Activities con failure domain compartido se agrupan; no 1 activity por call HTTP individual |
workflow_id colisión cuando reprocessing manual |
version suffix explícito en re-runs; Intelia ops controla |
| Dedupe key de comments rompe cuando cambia COMPOSER_VERSION | dedupe_key incluye composer_version para forzar repost cuando se actualiza copy |
| Activities con LLM consumen mucha cola y bloquean otros | Worker pool tuneado por tipo de activity. LLM activities tienen pool dedicado |
| Reverse-sync monitor workflow corre forever pero consume memoria/recursos | Continuous workflow con continue_as_new periódico para liberar history |
Patología 1: cada capa empieza a emitir signals indiscriminadamente. A los 12 meses hay 200+ tipos de signals, la projection es ruido, el contable ve 20 tags por documento. Mitigación: catálogo central SIGNAL_REGISTRY con review obligatorio para añadir nuevo signal. Lint que prohíbe emit_signal("custom_code", ...) con código fuera del registry.
Patología 2: dedupe de comments rota. Comments duplicados en Holded porque el dedupe_key no incluye un campo. Mitigación: hash de signal bag completo, no per-signal. Si el bag cambia → repost; si no → skip.
Patología 3: workflow_id no determinista cuando hay reprocessing concurrente. Dos jobs intentan re-procesar el mismo documento → race. Mitigación: workflow_id con version explícito + check de "ya en progress" antes de start_workflow.
Patología 4: Temporal history se infla con signals muchos. Cada signal añade evento al history. 1000 signals por workflow → history pesa MB. Mitigación: signals con audience=INTERNAL se persisten fuera del workflow (a DB Intelia). El workflow solo carga referencias.
Catálogo Signal Registry: tipos exactos. ¿Cuántos signals? Propuesta: ~40 al arranque, expandir según necesidad con review obligatorio.
Versioning del SIGNAL_REGISTRY. Cuando se renombra un signal, ¿qué pasa con audit logs históricos que tienen el código viejo? Política de aliasing.
Granularidad de activities. ¿call_plan_generator es una activity (con todas las tool calls del LLM dentro) o son varias activities (una por tool call)? Trade-off granularidad vs latencia.
continue_as_new en monitor workflows. ¿Cada cuánto? Si muy frecuente, sobre-coste; si poco, history grande. Propuesta: cada 1000 events.
¿Activities asíncronas tras workflow completion? Como projection a alert canal lento. Permite que el workflow cierre rápido y la projection sucede async.
Aislamiento de entornos para multi-tenancy. ¿Cada cliente tiene su namespace? ¿O todos en el mismo? Decisión que afecta seguridad y operacional.
| Métrica | Target |
|---|---|
tx_workflow_latency_p95 |
<60s para path feliz, <120s para con LLM Plan Generator |
tx_workflow_failure_rate |
<0.5% (excluyendo non-retryable errors expected) |
tx_signal_projection_success_rate |
>99% (signals proyectan a tags/comments/audit) |
tx_temporal_history_size_avg |
<500 events por workflow (sino, dividir en sub-workflows) |
tx_reverse_sync_workflow_lag |
<30s desde edit del contable a signal emitido |
tx_dedupe_save_rate |
Mide signals deduplicados — útil para detectar bugs de over-emission |
El target V2 ya tiene transversales Signals + Temporal bien diseñadas (transversal-signals.md, transversal-pipeline-temporal.md). V3 las conserva mayoritariamente, con cambios menores:
Adapter explícito Validation → Signals. En V3 hay menos validaciones (filtro de 3 condiciones), pero las que sobreviven generan signals vía tabla declarativa.
Workflows nuevos para T3 y T4. El target V2 no los tenía porque T3 y T4 no estaban diseñadas.
Signal Registry como contrato versionado (no solo dict). Cualquier signal nuevo pasa por PR con review.
Persistencia de signals INTERNAL fuera del workflow history para mantener Temporal history compact.
El resto del diseño (parent + 2 children, durabilidad, retries por dominio, idempotencia por workflow_id, aislamiento de entornos, pipeline invocable sin Temporal) se mantiene.
Componentes documentados:
v3-architecture.md — alto nivelm1-motor.mdi1-integrator-holded.mdt3-calcification.mdt4-reverse-sync.mds1-ux-hybrid.mdtx-signals-temporal.mdEsperando versión paralela de Codex en ../v3-conceptual-codex/ para síntesis final.