From 5dcc668bd43c384d5e88d9f3a398306de613af45 Mon Sep 17 00:00:00 2001 From: leocagli Date: Fri, 28 Aug 2026 21:08:56 -0300 Subject: [PATCH] feat: vigia de vencimientos, que mira todos los expedientes de una pasada MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Las skills que hay trabajan un caso. Esta mira todos a la vez, porque el plazo que se pierde en una Defensoria casi nunca es el que estabas mirando. ## El modo de falla que persigue No es el plazo que computaste y se te paso: ese lo tenes en la cabeza. Es la fila que quedo asi: | contestar demanda | habiles judiciales | | | 🔲 a computar | Alguien abrio el caso, cargo el acto, y no volvio. Con tres expedientes se nota. Con veinte no se nota nunca, y desde afuera se ve identico a un caso al dia: una tabla prolija, sin fechas rojas, porque no hay fechas. Por eso el reporte separa tres cosas y no las mezcla: - vence pronto: tiene fecha y esta cerca. Lo que ya sabes. - sin computar: no tiene fecha porque nadie la calculo. Lo que no sabes que no sabes. - bloqueado: no se puede calcular porque a la FICHA le falta la jurisdiccion o el fuero. Parece un problema de formulario y es un plazo escondido. ## El script no computa ningun plazo, a proposito `componentes/vigia/escanear.mjs` lee fechas ya escritas y cuenta dias corridos hasta ellas para ordenar la urgencia. Nada mas. Restar dias habiles sin saber la jurisdiccion, la feria y las acordadas del tribunal seria adivinar justo lo que la regla de la casa prohibe adivinar. El computo sigue siendo de `argentina-plazos`, que pregunta lo que le falte. Esa division tambien es lo que hace util el reporte: el escaner puede decir con certeza "esta fila no tiene fecha" sin arriesgar una fecha equivocada, y una fecha equivocada que figura como cerrada es peor que ninguna, porque deja de aparecer en el reporte siguiente. ## Detalles que salieron de leer la plantilla Una ficha con los parentesis de ayuda sin completar cuenta como incompleta: - **Jurisdiccion:** (CABA-Nacional / Jujuy / Salta) Eso no es una jurisdiccion declarada, es la plantilla intacta, y tratarlo como dato habilitaria un computo sobre un fuero inventado. Hay un test para eso. La fila de ejemplo de la plantilla, entre guiones bajos, se ignora. Si no, todo caso recien abierto apareceria con un plazo pendiente que no existe y el reporte se volveria ruido a la semana. ## Verificacion node test/vigia-test.mjs 12 ok, 0 mal Los expedientes de prueba se arman en /tmp y se borran. Nunca se escribe nada dentro de casos/, ni siquiera inventado: la regla de la casa es que ahi no entra nada que se parezca a un caso. Se agrego el workflow que los corre. A diferencia de los smoke de SAIJ, estos no tocan ninguna API, asi que no hay motivo para que sean tolerantes a fallos. --- .github/workflows/vigia.yml | 29 +++++ CLAUDE.md | 12 +- README.md | 2 +- componentes/vigia/escanear.mjs | 221 ++++++++++++++++++++++++++++++++ skills/argentina-vigia/SKILL.md | 90 +++++++++++++ test/vigia-test.mjs | 180 ++++++++++++++++++++++++++ 6 files changed, 532 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/vigia.yml create mode 100755 componentes/vigia/escanear.mjs create mode 100755 skills/argentina-vigia/SKILL.md create mode 100755 test/vigia-test.mjs 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();