---
name: proceso-ddia-streaming
description: >
  Patrones de Stream Processing del libro DDIA (Cap 11, Martin Kleppmann)
  aplicados a archivos JSONL append-only de SWL: event sourcing, idempotencia,
  exactly-once vs at-least-once, replay desde offset. Cargar al diseñar hooks
  que persistan eventos, modificar evolucion/*.jsonl, audit-trail.jsonl o
  nudges.jsonl, o cuando un consumidor de JSONL deba ser idempotente.
when_to_use: >
  Usar cuando el usuario menciona JSONL append-only, event sourcing, audit
  trail, idempotencia, exactly-once, replay, offset, consumidor de stream,
  evolucion/, nudges, telemetria, hooks que escriben eventos.
---

# DDIA Streaming — event sourcing aplicado a JSONL de SWL

Adaptado de Martin Kleppmann, *"Designing Data-Intensive Applications"*
Capítulo 11 — Stream Processing. SWL no tiene Kafka ni stream processor
real, pero opera con archivos JSONL append-only que se comportan como
streams locales. Los patrones del Cap 11 aplican directamente.

## Cuándo cargar

- Crear un hook que persiste eventos en JSONL (telemetría, audit, evolución).
- Diseñar un consumidor de `.planning/evolucion/*.jsonl`,
  `.planning/audit.jsonl`, `.planning/comms/nudges.jsonl`.
- Diagnosticar duplicación de eventos en JSONL.
- Decidir si un hook debe ser idempotente o si basta con append.
- Implementar replay de eventos desde un offset.

## Cuándo NO cargar

- Trabajo síncrono request/response sin event log → no aplica.
- Sistema sin JSONL (sin streams locales) → cargar `proceso-ddia-fundamentos`.
- Discusiones sobre Kafka/Flink/Spark reales → SWL no opera con esos.

## Cita textual base (DDIA Cap 11, p.439-479)

### Event Sourcing (p.457, línea 21753-21798)

> "Event sourcing is a powerful technique for data modeling: from an
> application point of view [...]"

El libro distingue event sourcing de change data capture (CDC):
- **CDC**: captura cambios de una base de datos relacional como stream.
- **Event Sourcing**: la aplicación misma emite eventos como log primario;
  el estado se deriva replicando los eventos.

**SWL aplica event sourcing** (no CDC): no hay BD relacional intermedia.
Los archivos JSONL son el log primario; cualquier proyección (instintos,
métricas, dashboard) se deriva replicándolos.

### Exactly-Once vs Effectively-Once (p.476, línea 22755)

> "This principle is known as exactly-once semantics, although
> effectively-once would be a more descriptive term."

Distinción clave del libro:
- **Exactly-once teórico**: el sistema garantiza que cada evento se
  procesa una y solo una vez.
- **Effectively-once práctico**: el evento puede llegar N veces, pero
  el efecto visible es el de procesarlo 1 vez. Se logra con idempotencia.

### Idempotencia (p.478, línea 22828-22845)

> "An idempotent operation is one that you can perform multiple times,
> and it has the same effect as if you performed it only once. For
> example, setting a key in a key-value store to some fixed value is
> idempotent (writing the value again simply overwrites the value with
> an identical value), whereas incrementing a counter is not idempotent
> (performing the increment again means the value is incremented twice)."

Operación clave del libro: incluso operaciones no-idempotentes pueden
hacerse idempotentes con metadata adicional (el offset del mensaje
fuente).

---

## Aplicación a SWL — archivos JSONL del sistema

| Archivo | Productor | Consumidores | ¿Es event source? |
|---|---|---|---|
| `.planning/evolucion/evoluciones.jsonl` | `/swl:evolucionar`, hook `evolucion-detector` | `/swl:evolucion-estado`, dashboard | Sí |
| `.planning/evolucion/nudges.jsonl` | hooks varios | `/swl:salud`, `red-team-swl` | Sí |
| `.planning/evolucion/agentes.jsonl` | hook `telemetria-agentes` | `/swl:metricas` | Sí |
| `.planning/audit.jsonl` | hook `audit-trail` | auditorías, post-mortems | Sí (con Merkle) |
| `.planning/comms/*.jsonl` | `notificador-swl` | inbox, gateway | Sí |

Todos son **append-only logs** — escritura por `fs.appendFileSync` (regla
CLAUDE.md). Nadie modifica ni borra entradas existentes.

## Patrón 1 — Idempotencia con event ID

**Problema**: un hook se ejecuta dos veces (re-trigger, retry) y emite el
mismo evento. El consumidor lo procesa 2 veces, produciendo doble efecto.

**Solución aplicada en SWL**: cada evento JSONL incluye un campo `id` único
(UUID v4 o `timestamp + hash de payload`). El consumidor mantiene un
set de IDs procesados (en `.planning/.processed-ids/`) y descarta
duplicados.

Ejemplo en hooks/lib (idiomático SWL):

```js
// Productor (hook)
const crypto = require('crypto');
const evento = {
  id: crypto.randomUUID(),
  ts: new Date().toISOString(),
  type: 'agent.completed',
  payload: { ... }
};
fs.appendFileSync(ruta, JSON.stringify(evento) + '\n');

// Consumidor
const procesados = cargarSetIds(); // del filesystem
for (const linea of leerJSONL(ruta)) {
  const e = JSON.parse(linea);
  if (procesados.has(e.id)) continue; // idempotencia
  aplicar(e);
  procesados.add(e.id);
  guardarSetIds(procesados);
}
```

**Cuándo aplicar**: consumidores que ejecutan side-effects no-idempotentes
(notificaciones, mutaciones de estado, incrementos de contador).

**Cuándo NO aplicar**: consumidores que solo proyectan estado (instintos,
métricas) — la idempotencia natural (re-procesar produce mismo estado)
es suficiente.

## Patrón 2 — Replay desde offset

**Problema**: un consumidor crashea a mitad del JSONL. Al reiniciar,
¿desde dónde continúa?

**Solución aplicada en SWL**: persistir el byte-offset del último evento
procesado en archivo separado (`.planning/.offsets/consumidor-X.offset`).
Al iniciar, leer desde ese offset.

```js
const offsetFile = `.planning/.offsets/${consumidor}.offset`;
const offset = fs.existsSync(offsetFile)
  ? parseInt(fs.readFileSync(offsetFile, 'utf-8'), 10)
  : 0;

const fd = fs.openSync(ruta, 'r');
// leer desde offset, procesar evento por evento
// tras cada evento procesado: fs.writeFileSync(offsetFile, nuevoOffset)
```

**Garantía**: at-least-once. Si crashea entre procesar y guardar offset,
el evento se reprocesa. Combinado con idempotencia (Patrón 1), se logra
**effectively-once** (DDIA p.476).

## Patrón 3 — Append-only sin borrado

**Regla operacional SWL**: los archivos JSONL NUNCA se reescriben enteros
ni se editan entradas pasadas. Compactación (eliminar duplicados, agrupar)
se hace creando un archivo `.jsonl.compacted` separado, dejando el
original como audit trail.

**Por qué importa**: si un hook reescribiera `audit.jsonl` con un nuevo
contenido (incluso con razón válida), perderíamos la garantía de
inmutabilidad que `audit-trail.js` con Merkle hash depende. El audit
trail roto deja de ser auditable.

## Patrón 4 — Schema evolution en eventos JSONL

Los eventos JSONL evolucionan: campos nuevos, payloads modificados. Los
consumidores deben tolerar eventos viejos sin romperse.

Aplicar las reglas de `Skill("proceso-ddia-fundamentos")` § Schema
Evolution:
- Campos nuevos siempre opcionales.
- `schemaVersion: "1.0.0"` por evento opcional pero recomendado.
- Consumidores ignoran campos desconocidos (forward-compat).

## Patrón 5 — Backpressure (cuándo NO aplica a SWL)

DDIA trata extensamente backpressure: cuando el productor escribe más
rápido que el consumidor. **SWL NO sufre este problema** porque:
- Volumen de eventos por sesión: ~10²-10³ líneas, no millones.
- Consumidores son sincrónicos bajo demanda (`/swl:salud`, dashboard).
- No hay productor continuo de alta frecuencia.

NO implementar backpressure / rate-limiting en JSONL writers. Es
sobre-ingeniería.

## Anti-patrones específicos a evitar

- **Compactar JSONL en mismo archivo**: rompe inmutabilidad. Crear
  `.compacted` separado.
- **Confiar en orden de líneas para totalidad**: dos hooks concurrentes
  pueden escribir entrelazado. Usar IDs únicos + timestamps, NO orden.
- **Indexar JSONL en disco como BD**: el archivo es secuencial por
  diseño. Si necesitas índice, proyecta a otra estructura (instintos,
  métricas agregadas).
- **Bloquear el productor durante write**: `fs.appendFileSync` es atómico
  para writes ≤4096 bytes en sistemas POSIX (no aplica Windows, donde
  hay carrera de race). Para Windows usar `atomicAppend` de
  `hooks/lib/atomic-write.js` cuando el evento supera 4KB.

## Gotchas no obvios

- **`fs.appendFileSync` no es atómico en Windows con escrituras grandes**:
  Node usa `WriteFile` que no garantiza atomicity para >4KB. Si el
  evento JSON serializado supera 4KB, usar buffer + `atomicAppend`.
- **Lectura de JSONL con `fs.readFileSync` carga todo a memoria**: si
  un archivo crece a 100MB+, usar `readline` con stream. Pero para
  swl-ses (volumen bajo), `readFileSync` es aceptable.
- **`JSON.parse` en línea malformada lanza**: envolver en try/catch +
  log de línea problemática. NUNCA dejar que un evento corrupto rompa
  todo el replay.

## Resumen — del libro a SWL

| Concepto DDIA Cap 11 | Aplicación SWL |
|---|---|
| Event sourcing | Todos los `.planning/*.jsonl` |
| Exactly-once → effectively-once | Idempotencia + at-least-once delivery |
| Idempotencia con offset | Patrón 1 de este skill |
| Replay desde offset | Patrón 2 de este skill |
| Append-only inmutable | Regla operacional SWL |
| Backpressure | NO aplica (volumen bajo) |

## Origen

Skill creado el 2026-05-18 como parte de Opción B (ver ADR-0025). Skill
hermano: `proceso-ddia-fundamentos` (Cap 1, 4, 12 del mismo libro).
