diff --git a/.github/workflows/vigia.yml b/.github/workflows/vigia.yml
new file mode 100644
index 0000000..518c249
--- /dev/null
+++ b/.github/workflows/vigia.yml
@@ -0,0 +1,29 @@
+# Tests del vigia de vencimientos
+# -----------------------------------------
+# A diferencia de los smoke tests de SAIJ, estos no dependen de ninguna API
+# externa: arman expedientes sinteticos en una carpeta temporal y comprueban
+# que el escaneo separe bien lo que vence, lo que nadie computo y lo que no se
+# puede computar por falta de jurisdiccion.
+#
+# Sin red y sin datos reales, asi que no hay motivo para que sean tolerantes a
+# fallos: si fallan, fallan.
+
+name: Vigia de vencimientos
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - name: Tests del vigia
+ run: node test/vigia-test.mjs
diff --git a/CLAUDE.md b/CLAUDE.md
index 60ad12a..2e38915 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,6 +41,15 @@ computar plazos o citar procedimiento. Leé la ficha de jurisdicción correspond
área en SAIJ (fuente primaria, en vivo).
4. Confirmá jurisdicción/fuero antes de cualquier cómputo de plazos.
+## Al empezar la semana
+
+Corré la skill **`argentina-vigia`**: revisa todos los expedientes de una pasada
+y separa lo que vence pronto, lo que nadie computó todavía, y lo que no se puede
+computar porque a la ficha le falta la jurisdicción.
+
+El plazo que se pierde casi nunca es el que estabas mirando. Es la fila que
+quedó en `🔲 a computar` en uno de los otros casos.
+
## Reglas de operación (no negociables)
- **Plazos por jurisdicción:** días hábiles judiciales ≠ hábiles administrativos ≠
@@ -63,7 +72,8 @@ computar plazos o citar procedimiento. Leé la ficha de jurisdicción correspond
`saij_buscar_doctrina`, `saij_documento(uuid)`. Cubre Nacional/Federal y legislación
Local de Jujuy y Salta.
- **Skills** — `abogacia-argentina` (router y ejes normativos por área),
- `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`.
+ `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`,
+ `argentina-vigia`.
## Privacidad
diff --git a/README.md b/README.md
index 5347457..0e4171d 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ y se computa el plazo aplicable.
| Capa | Qué hace |
|------|----------|
-| **Skills** | `abogacia-argentina` (router generalista por área: laboral, civil, penal, familia, consumidor, societario, administrativo, previsional, tributario, tránsito, protección de datos), `argentina-formatos` (anatomía de cada tipo de escrito: demanda ≠ descargo ≠ recurso ≠ nota), `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`. Todas propias, MIT. |
+| **Skills** | `abogacia-argentina` (router generalista por área: laboral, civil, penal, familia, consumidor, societario, administrativo, previsional, tributario, tránsito, protección de datos), `argentina-formatos` (anatomía de cada tipo de escrito: demanda ≠ descargo ≠ recurso ≠ nota), `argentina-plazos`, `argentina-diagnostico`, `argentina-bucles`, `saij-argentina`. Todas propias, MIT., `argentina-vigia` (repaso de vencimientos de todos los expedientes) |
| **MCP SAIJ** | Búsqueda en vivo de jurisprudencia, legislación y doctrina + texto completo por uuid. Código propio, cero dependencias. |
| **Multi-jurisdicción** | CABA/Nacional (CPCCN, CPPF), Jujuy y Salta — extensible con una ficha por provincia. Pregunta el fuero al abrir cada caso. |
| **Workspace de casos** | Cada caso es una subcarpeta en `casos/` con ficha, escritos, documentación, jurisprudencia y plazos. |
diff --git a/componentes/vigia/escanear.mjs b/componentes/vigia/escanear.mjs
new file mode 100755
index 0000000..4f20e48
--- /dev/null
+++ b/componentes/vigia/escanear.mjs
@@ -0,0 +1,221 @@
+#!/usr/bin/env node
+/**
+ * Escanea `casos/` y reporta el estado de los plazos de cada expediente.
+ *
+ * Este script NO computa ningún plazo, y eso es deliberado. El cómputo depende
+ * de la jurisdicción, del tipo de plazo, de la feria y de las acordadas del
+ * tribunal, y la regla del proyecto es que eso lo resuelve `argentina-plazos`
+ * después de confirmar el fuero. Un script que se pusiera a restar días
+ * hábiles estaría adivinando exactamente lo que la regla prohíbe adivinar.
+ *
+ * Lo que sí hace es lo mecánico, que es donde se pierden los plazos de verdad:
+ * recorrer veinte carpetas, leer veinte tablas, y decir cuáles tienen filas sin
+ * computar, cuáles vencen pronto y cuáles no se pueden computar todavía porque
+ * a la ficha le falta la jurisdicción.
+ *
+ * Uso:
+ * node componentes/vigia/escanear.mjs [--casos
] [--json] [--dias ]
+ *
+ * --casos dónde están los expedientes (default: ./casos)
+ * --json salida en JSON, para encadenar
+ * --dias ventana de "vence pronto" en días corridos (default: 15)
+ */
+
+import { readdir, readFile, stat } from 'node:fs/promises';
+import path from 'node:path';
+
+const args = process.argv.slice(2);
+const opt = (nombre, porDefecto) => {
+ const i = args.indexOf(`--${nombre}`);
+ return i >= 0 && args[i + 1] ? args[i + 1] : porDefecto;
+};
+const DIR_CASOS = opt('casos', 'casos');
+const VENTANA = Number(opt('dias', '15'));
+const JSON_OUT = args.includes('--json');
+
+/** Marcas del proyecto: 🔲 es pendiente, 🔴 es fatal. */
+const PENDIENTE = /🔲/u;
+const FATAL = /🔴/u;
+
+/** Una fila de la tabla de plazos, tal como está escrita. */
+function parsearFilas(md) {
+ const filas = [];
+ for (const linea of md.split('\n')) {
+ const l = linea.trim();
+ if (!l.startsWith('|') || !l.endsWith('|')) continue;
+ const celdas = l.slice(1, -1).split('|').map((c) => c.trim());
+ if (celdas.length < 5) continue;
+ // Cabecera y separador de la tabla markdown.
+ if (/^-+$/.test(celdas[0].replace(/[\s:]/g, ''))) continue;
+ if (/^acto\b/i.test(celdas[0])) continue;
+ // Fila vacía de la plantilla.
+ if (celdas.every((c) => c === '')) continue;
+ // La fila de ejemplo que trae la plantilla, entre guiones bajos.
+ if (/^_.*_$/.test(celdas[0])) continue;
+
+ filas.push({
+ acto: celdas[0],
+ tipo: celdas[1],
+ inicio: celdas[2],
+ vencimiento: celdas[3],
+ estado: celdas[4],
+ });
+ }
+ return filas;
+}
+
+/** Una fecha suelta escrita como la escribe una persona. */
+function leerFecha(texto) {
+ if (!texto) return null;
+ const t = texto.trim();
+ let m = t.match(/(\d{4})-(\d{2})-(\d{2})/);
+ if (m) return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
+ m = t.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
+ if (m) return new Date(Date.UTC(+m[3], +m[2] - 1, +m[1]));
+ return null;
+}
+
+/** La jurisdicción y el fuero declarados en la ficha, o null si faltan. */
+function leerFicha(md) {
+ const campo = (etiqueta) => {
+ const re = new RegExp(`\\*\\*${etiqueta}:?\\*\\*\\s*(.*)`, 'i');
+ const m = md.match(re);
+ if (!m) return null;
+ // Se descarta el paréntesis de ayuda que trae la plantilla sin completar.
+ const v = m[1].replace(/\(.*?\)/g, '').trim();
+ return v === '' ? null : v;
+ };
+ return {
+ caratula: campo('Carátula'),
+ expediente: campo('N° de expediente'),
+ jurisdiccion: campo('Jurisdicción'),
+ fuero: campo('Fuero'),
+ estado: campo('Estado actual'),
+ };
+}
+
+const hoy = new Date();
+hoy.setUTCHours(0, 0, 0, 0);
+const diasHasta = (f) => Math.round((f - hoy) / 86400000);
+
+async function escanear() {
+ let entradas;
+ try {
+ entradas = await readdir(DIR_CASOS, { withFileTypes: true });
+ } catch {
+ return { error: `no pude leer ${DIR_CASOS}`, casos: [] };
+ }
+
+ const casos = [];
+ for (const e of entradas) {
+ if (!e.isDirectory() || e.name.startsWith('_') || e.name.startsWith('.')) continue;
+ const base = path.join(DIR_CASOS, e.name);
+
+ let ficha = {};
+ try {
+ ficha = leerFicha(await readFile(path.join(base, 'FICHA.md'), 'utf8'));
+ } catch {
+ ficha = { falta_ficha: true };
+ }
+
+ let filas = [];
+ let sinArchivo = false;
+ try {
+ filas = parsearFilas(await readFile(path.join(base, 'plazos.md'), 'utf8'));
+ } catch {
+ sinArchivo = true;
+ }
+
+ const plazos = filas.map((f) => {
+ const venc = leerFecha(f.vencimiento);
+ return {
+ ...f,
+ fatal: FATAL.test(f.estado) || FATAL.test(f.acto),
+ sinComputar: venc === null || PENDIENTE.test(f.estado),
+ dias: venc ? diasHasta(venc) : null,
+ };
+ });
+
+ casos.push({
+ caso: e.name,
+ ...ficha,
+ sinArchivoDePlazos: sinArchivo,
+ // La regla dura del proyecto: sin jurisdicción no se computa nada.
+ puedeComputarse: Boolean(ficha.jurisdiccion && ficha.fuero),
+ plazos,
+ });
+ }
+ return { casos };
+}
+
+function informar({ casos, error }) {
+ if (error) {
+ console.error(` ${error}`);
+ process.exitCode = 2;
+ return;
+ }
+ if (casos.length === 0) {
+ console.log(' No hay expedientes en ' + DIR_CASOS);
+ return;
+ }
+
+ const bloqueados = casos.filter((c) => !c.puedeComputarse);
+ const sinComputar = casos.flatMap((c) =>
+ c.plazos.filter((p) => p.sinComputar).map((p) => ({ ...p, caso: c.caso })),
+ );
+ const proximos = casos
+ .flatMap((c) => c.plazos.filter((p) => p.dias !== null).map((p) => ({ ...p, caso: c.caso })))
+ .filter((p) => p.dias <= VENTANA)
+ .sort((a, b) => a.dias - b.dias);
+
+ console.log(`\n ${casos.length} expedientes revisados el ${hoy.toISOString().slice(0, 10)}\n`);
+
+ // Lo vencido y lo inminente primero: es lo único que puede costar un derecho hoy.
+ if (proximos.length) {
+ console.log(' ── Vencen dentro de ' + VENTANA + ' días ──');
+ for (const p of proximos) {
+ const marca = p.dias < 0 ? '⛔ VENCIDO' : p.fatal ? '🔴 fatal ' : ' ';
+ const cuando = p.dias < 0 ? `hace ${-p.dias}d` : `en ${p.dias}d`;
+ console.log(` ${marca} ${cuando.padEnd(9)} ${p.caso} · ${p.acto}`);
+ }
+ console.log('');
+ }
+
+ // El modo de falla real: nadie lo computó todavía.
+ if (sinComputar.length) {
+ console.log(' ── Sin computar ──');
+ console.log(' (una fila sin vencimiento no es un plazo holgado: es un plazo que nadie miró)');
+ for (const p of sinComputar) {
+ console.log(` 🔲 ${p.caso} · ${p.acto || '(sin acto)'} · ${p.tipo || 'tipo sin declarar'}`);
+ }
+ console.log('');
+ }
+
+ // Y lo que ni siquiera se puede computar todavía.
+ if (bloqueados.length) {
+ console.log(' ── No se pueden computar: falta jurisdicción o fuero en la FICHA ──');
+ for (const c of bloqueados) {
+ const falta = [
+ c.jurisdiccion ? null : 'jurisdicción',
+ c.fuero ? null : 'fuero',
+ ].filter(Boolean).join(' y ');
+ console.log(` ⚠️ ${c.caso} · falta ${falta || 'la ficha entera'}`);
+ }
+ console.log('');
+ }
+
+ const limpios = casos.length - new Set([
+ ...proximos.map((p) => p.caso),
+ ...sinComputar.map((p) => p.caso),
+ ...bloqueados.map((c) => c.caso),
+ ]).size;
+ console.log(` ${limpios} expediente(s) sin nada pendiente en esta ventana.`);
+
+ if (proximos.some((p) => p.dias < 0) || sinComputar.length || bloqueados.length) {
+ process.exitCode = 1;
+ }
+}
+
+const resultado = await escanear();
+if (JSON_OUT) console.log(JSON.stringify(resultado, null, 2));
+else informar(resultado);
diff --git a/skills/argentina-vigia/SKILL.md b/skills/argentina-vigia/SKILL.md
new file mode 100755
index 0000000..3a98339
--- /dev/null
+++ b/skills/argentina-vigia/SKILL.md
@@ -0,0 +1,90 @@
+---
+name: argentina-vigia
+description: "Revisa todos los expedientes abiertos de una sola pasada y reporta qué vence pronto, qué plazo nadie computó todavía, y qué caso no se puede computar porque a la ficha le falta la jurisdicción o el fuero. Usar al empezar el día o la semana, al volver de la feria, antes de irse de licencia, o cuando el usuario diga «qué tengo pendiente», «cómo vienen los plazos», «revisá los casos» o «qué se me vence»."
+license: MIT
+---
+
+# Vigía de vencimientos · Todos los expedientes de una pasada
+
+Skill original de Agente Smith (MIT).
+
+Las otras skills trabajan **un** caso. Esta mira **todos** a la vez, porque el
+plazo que se pierde en una Defensoría casi nunca es el que estabas mirando.
+
+## El modo de falla que esta skill persigue
+
+No es el plazo que computaste y se te pasó. Ese lo tenés en la cabeza.
+
+Es la fila que quedó así:
+
+```
+| contestar demanda | hábiles judiciales | | | 🔲 a computar |
+```
+
+Alguien abrió el caso, cargó el acto, y no volvió. Con tres expedientes eso se
+nota. Con veinte no se nota nunca, y desde afuera se ve idéntico a un caso al
+día: una tabla prolija, sin fechas rojas, porque **no hay fechas**.
+
+Por eso el reporte separa tres cosas distintas y no las mezcla:
+
+| | qué es | por qué duele |
+|---|---|---|
+| **vence pronto** | tiene fecha y está cerca | lo que ya sabés |
+| **sin computar** | no tiene fecha porque nadie la calculó | lo que no sabés que no sabés |
+| **bloqueado** | no se puede calcular: falta jurisdicción o fuero en la `FICHA.md` | el que parece un problema administrativo y es un plazo escondido |
+
+## Procedimiento
+
+**1. Escaneá lo mecánico.**
+
+```bash
+node componentes/vigia/escanear.mjs
+```
+
+Recorre `casos/`, lee cada `FICHA.md` y cada `plazos.md`, y devuelve el estado.
+Con `--json` sale estructurado; con `--dias N` cambiás la ventana de "vence
+pronto", que por defecto son 15 días corridos.
+
+**El script no computa ningún plazo, a propósito.** Sólo lee fechas ya escritas
+y cuenta días corridos hasta ellas para ordenar la urgencia. Restar días hábiles
+sin saber la jurisdicción, la feria y las acordadas del tribunal sería adivinar
+justo lo que la regla del proyecto prohíbe adivinar.
+
+**2. Los bloqueados van primero.**
+
+Un caso sin jurisdicción o sin fuero en la ficha **no tiene un problema de
+formulario: tiene todos sus plazos sin computar y no lo parece.** Antes de
+seguir, pedile al defensor esos datos. No los deduzcas del nombre del juzgado ni
+del tipo de carátula.
+
+**3. Recién ahí, computá lo que falta.**
+
+Para cada fila `🔲`, activá **`argentina-plazos`** con la jurisdicción y el fuero
+de la ficha. Esa skill pregunta lo que le falte; dejala preguntar. Escribí el
+resultado en la `plazos.md` del caso, con la norma fuente en la columna de tipo.
+
+**4. Marcá lo que quede sin verificar.**
+
+Si computaste un vencimiento pero no pudiste confirmar la feria o una acordada
+del tribunal en SAIJ, la fila queda `🔲` con la nota de qué falta verificar. Un
+plazo computado a medias que figura como cerrado es peor que uno sin computar,
+porque deja de aparecer en este reporte.
+
+## Cuándo correrla
+
+Al empezar la semana, al volver de la feria, antes de tomarte licencia, y cuando
+recibís un expediente de otro defensor. Los cuatro son momentos donde el estado
+real de los plazos y el que uno recuerda se separan.
+
+## Lo que esta skill no hace
+
+**No manda avisos ni recordatorios.** Reporta cuando la corrés. Un sistema que
+avisa solo hace falta y es la evolución natural, pero necesita un lugar donde
+correr programado; mientras tanto, esto se corre y se lee.
+
+**No abre el expediente electrónico.** Lee lo que está escrito en `casos/`. Si
+el juzgado notificó algo que nadie cargó, el vigía no lo sabe. Sincronizar con
+el sistema del Poder Judicial es otro trabajo.
+
+**No decide qué es urgente.** Ordena por fecha y marca lo fatal con 🔴 si vos lo
+marcaste. La prioridad entre dos plazos del mismo día la pone el defensor.
diff --git a/test/vigia-test.mjs b/test/vigia-test.mjs
new file mode 100755
index 0000000..66b4bb0
--- /dev/null
+++ b/test/vigia-test.mjs
@@ -0,0 +1,180 @@
+#!/usr/bin/env node
+// Tests del vigia de vencimientos
+// ------------------------------------------------
+// Arma expedientes sinteticos en una carpeta temporal y comprueba que el
+// escaneo separe bien los tres estados que importan: lo que vence pronto, lo
+// que nadie computo, y lo que no se puede computar porque falta la
+// jurisdiccion en la ficha.
+//
+// Los expedientes de prueba se crean y se borran en /tmp. Nunca se escribe
+// nada dentro de casos/, ni con datos inventados: la regla de la casa es que
+// ahi no entra nada que se parezca a un caso.
+//
+// Uso:
+// node test/vigia-test.mjs
+//
+// Exit codes:
+// 0 = todo ok
+// 1 = algun test fallo
+
+import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+import { fileURLToPath } from "node:url";
+
+const ejecutar = promisify(execFile);
+const AQUI = path.dirname(fileURLToPath(import.meta.url));
+const ESCANER = path.join(AQUI, "..", "componentes", "vigia", "escanear.mjs");
+
+let pasaron = 0;
+let fallaron = 0;
+
+function ok(nombre) {
+ pasaron += 1;
+ process.stderr.write(`[vigia] ok ${nombre}\n`);
+}
+function mal(nombre, detalle) {
+ fallaron += 1;
+ process.stderr.write(`[vigia] MAL ${nombre}\n ${detalle}\n`);
+}
+function comprobar(nombre, condicion, detalle) {
+ condicion ? ok(nombre) : mal(nombre, detalle);
+}
+
+/** Una fecha a N dias de hoy, en el formato que usa la plantilla. */
+function enDias(n) {
+ const d = new Date();
+ d.setUTCDate(d.getUTCDate() + n);
+ return d.toISOString().slice(0, 10);
+}
+
+async function caso(base, nombre, ficha, plazos) {
+ const dir = path.join(base, nombre);
+ await mkdir(dir, { recursive: true });
+ await writeFile(path.join(dir, "FICHA.md"), ficha, "utf8");
+ if (plazos !== null) {
+ await writeFile(path.join(dir, "plazos.md"), plazos, "utf8");
+ }
+}
+
+const CABECERA =
+ "| Acto / plazo | Tipo | Inicio | Vencimiento | Estado |\n" +
+ "|---|---|---|---|---|\n";
+
+async function main() {
+ const base = await mkdtemp(path.join(tmpdir(), "vigia-"));
+
+ // Al dia, con un vencimiento fatal cerca.
+ await caso(
+ base,
+ "0001-expediente-de-prueba",
+ "## Jurisdicción y fuero\n- **Jurisdicción:** CABA-Nacional\n- **Fuero:** previsional\n",
+ CABECERA + `| expresar agravios | hábiles judiciales | ${enDias(-9)} | ${enDias(6)} | 🔴 computado |\n`,
+ );
+
+ // Una fila que nadie computo: el modo de falla que persigue esta skill.
+ await caso(
+ base,
+ "0002-expediente-de-prueba",
+ "## Jurisdicción y fuero\n- **Jurisdicción:** Salta\n- **Fuero:** civil y comercial\n",
+ CABECERA + "| contestar demanda | hábiles judiciales | 2026-08-25 | | 🔲 a computar |\n",
+ );
+
+ // Ficha con los parentesis de la plantilla sin completar.
+ await caso(
+ base,
+ "0003-expediente-de-prueba",
+ "## Jurisdicción y fuero\n- **Jurisdicción:** (CABA-Nacional / Jujuy / Salta)\n- **Fuero:** (civil / penal)\n",
+ CABECERA + "| algo | hábiles judiciales | | | 🔲 a computar |\n",
+ );
+
+ // Ya vencido.
+ await caso(
+ base,
+ "0004-expediente-de-prueba",
+ "## Jurisdicción y fuero\n- **Jurisdicción:** Jujuy\n- **Fuero:** penal\n",
+ CABECERA + `| interponer recurso | hábiles judiciales | ${enDias(-12)} | ${enDias(-3)} | 🔴 computado |\n`,
+ );
+
+ // Sin nada pendiente: vence lejos.
+ await caso(
+ base,
+ "0005-expediente-de-prueba",
+ "## Jurisdicción y fuero\n- **Jurisdicción:** Salta\n- **Fuero:** familia\n",
+ CABECERA + `| audiencia | corridos | ${enDias(1)} | ${enDias(120)} | computado |\n`,
+ );
+
+ let salida;
+ try {
+ const r = await ejecutar("node", [ESCANER, "--casos", base, "--json"]);
+ salida = JSON.parse(r.stdout);
+ } catch (e) {
+ // exit 1 es esperado cuando hay pendientes: la salida sigue siendo valida.
+ if (e.stdout) salida = JSON.parse(e.stdout);
+ else {
+ mal("el escaner corre", String(e).slice(0, 160));
+ return;
+ }
+ }
+
+ const por = (n) => salida.casos.find((c) => c.caso.startsWith(n));
+
+ comprobar("encuentra los cinco expedientes", salida.casos.length === 5,
+ `encontro ${salida.casos.length}`);
+
+ comprobar("lee jurisdicción y fuero de la ficha",
+ por("0001").jurisdiccion === "CABA-Nacional" && por("0001").fuero === "previsional",
+ JSON.stringify(por("0001")).slice(0, 120));
+
+ comprobar("una ficha con los paréntesis sin completar no cuenta como completa",
+ por("0003").puedeComputarse === false,
+ "0003 quedo como computable y no deberia");
+
+ comprobar("una ficha completa sí habilita el cómputo",
+ por("0002").puedeComputarse === true,
+ "0002 quedo como no computable");
+
+ comprobar("marca sin computar la fila sin vencimiento",
+ por("0002").plazos.some((p) => p.sinComputar),
+ "no detecto la fila 🔲 de 0002");
+
+ comprobar("no marca sin computar una fila con fecha",
+ por("0001").plazos.every((p) => !p.sinComputar),
+ "marco como pendiente una fila ya computada");
+
+ comprobar("cuenta los días hasta el vencimiento",
+ por("0001").plazos[0].dias === 6,
+ `dias = ${por("0001").plazos[0].dias}, esperaba 6`);
+
+ comprobar("un vencimiento pasado da días negativos",
+ por("0004").plazos[0].dias === -3,
+ `dias = ${por("0004").plazos[0].dias}, esperaba -3`);
+
+ comprobar("reconoce el marcador de plazo fatal",
+ por("0004").plazos[0].fatal === true,
+ "no vio el 🔴");
+
+ comprobar("ignora la fila de ejemplo de la plantilla",
+ por("0005").plazos.length === 1,
+ `0005 tiene ${por("0005").plazos.length} filas, esperaba 1`);
+
+ // La ventana no debe arrastrar lo que vence dentro de cuatro meses.
+ const lejano = por("0005").plazos[0];
+ comprobar("un vencimiento lejano no entra en la ventana",
+ lejano.dias > 15,
+ `dias = ${lejano.dias}`);
+
+ // El escaner nunca computa: una fila sin fecha se queda sin fecha.
+ comprobar("el escáner NO inventa un vencimiento",
+ por("0002").plazos[0].dias === null,
+ "le puso fecha a un plazo que nadie computo");
+
+ await rm(base, { recursive: true, force: true });
+
+ process.stderr.write(`\n[vigia] ${pasaron} ok, ${fallaron} mal\n`);
+ process.exitCode = fallaron > 0 ? 1 : 0;
+}
+
+await main();