-
Notifications
You must be signed in to change notification settings - Fork 7
feat: vigia de vencimientos, que mira todos los expedientes de una pasada #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <dir>] [--json] [--dias <n>] | ||
| * | ||
| * --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; | ||
| } | ||
|
Comment on lines
+214
to
+216
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| const resultado = await escanear(); | ||
| if (JSON_OUT) console.log(JSON.stringify(resultado, null, 2)); | ||
| else informar(resultado); | ||
|
Comment on lines
+214
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Edge Case: UTC-based 'hoy' can shift day count for evening runs
hoyis derived fromnew Date()then floored withsetUTCHours(0,0,0,0), and deadlines are parsed to UTC midnight. For a user in Argentina (UTC-3) running the scanner in the evening, the UTC date is already the next calendar day, so a deadline due 'tomorrow' local time is reported asen 0d(and an overdue-by-today boundary can flip). Since the tool's audience is Argentine courts, anchor 'today' to the local (or an explicit America/Argentina) calendar date rather than UTC to avoid off-by-one urgency counts.Was this helpful? React with 👍 / 👎