diff --git a/.github/workflows/expedientes.yml b/.github/workflows/expedientes.yml new file mode 100644 index 0000000..5ad1cb6 --- /dev/null +++ b/.github/workflows/expedientes.yml @@ -0,0 +1,28 @@ +# Tests del manejo de expedientes +# ----------------------------------------- +# Comprueban sobre todo lo que el script tiene que IMPEDIR: abrir un caso sin +# jurisdiccion, usar una jurisdiccion que no tiene ficha, y archivar un caso +# con plazos todavia pendientes. +# +# No tocan ninguna API y arman todo en /tmp, asi que no hay motivo para que +# sean tolerantes a fallos. + +name: Expedientes + +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 de expedientes + run: node test/casos-test.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 60ad12a..58f694d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,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-expedientes`. ## Privacidad diff --git a/README.md b/README.md index 5347457..4f7529d 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-expedientes` (abrir, validar y archivar casos) | | **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/casos/casos.mjs b/componentes/casos/casos.mjs new file mode 100755 index 0000000..0933f0e --- /dev/null +++ b/componentes/casos/casos.mjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +/** + * Abre, lista, valida y cierra expedientes en `casos/`. + * + * Existe por una razón concreta: el vigía de vencimientos reporta como + * "bloqueado" todo caso al que le falta la jurisdicción o el fuero en la ficha, + * y esos casos no nacen bloqueados por descuido. Nacen así porque abrir un caso + * hoy es copiar una carpeta a mano y completar un formulario después, y el + * "después" a veces no llega. + * + * Este script no reemplaza ese formulario: lo pide antes de dejar el caso + * abierto, que es distinto. + * + * Uso: + * node componentes/casos/casos.mjs nuevo --jurisdiccion --fuero [--caratula "..."] + * node componentes/casos/casos.mjs listar [--json] + * node componentes/casos/casos.mjs validar [] + * node componentes/casos/casos.mjs cerrar + * + * --casos dónde viven los expedientes (default: ./casos) + */ + +import { readdir, readFile, writeFile, mkdir, cp, access } from 'node:fs/promises'; +import path from 'node:path'; + +const args = process.argv.slice(2); +const comando = args[0]; +const opt = (n, d) => { + const i = args.indexOf(`--${n}`); + return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : d; +}; +const DIR = opt('casos', 'casos'); +const PLANTILLA = opt('plantilla', '_PLANTILLA-CASO'); +const JSON_OUT = args.includes('--json'); + +/** Las tres jurisdicciones que el proyecto tiene fichadas. */ +const JURISDICCIONES = ['CABA-Nacional', 'Jujuy', 'Salta']; + +/** Los fueros que la ficha nombra. Se acepta "otro" a texto libre. */ +const FUEROS = [ + 'civil y comercial', 'penal', 'familia', 'laboral', + 'contencioso administrativo', 'previsional', +]; + +const existe = async (p) => { try { await access(p); return true; } catch { return false; } }; + +/** Lee un campo `- **Etiqueta:** valor` de una ficha, ignorando el paréntesis de ayuda. */ +function campo(md, etiqueta) { + const m = md.match(new RegExp(`\\*\\*${etiqueta}:?\\*\\*\\s*(.*)`, 'i')); + if (!m) return null; + const v = m[1].replace(/\(.*?\)/g, '').trim(); + return v === '' ? null : v; +} + +/** Reemplaza un campo de la ficha dejando el resto intacto. */ +function completar(md, etiqueta, valor) { + // Acotado a una linea a proposito: con `\s*.*` el espacio en blanco incluye + // el salto de linea, asi que completar un campo se comia el siguiente. + // Completar "Fecha de inicio" borraba "Estado actual". + const re = new RegExp(`(\\*\\*${etiqueta}:?\\*\\*)[^\\n]*`, 'i'); + return re.test(md) ? md.replace(re, `$1 ${valor}`) : md; +} + +// ---------------------------------------------------------------- nuevo + +async function nuevo() { + const nombre = args[1]; + if (!nombre || nombre.startsWith('--')) { + console.error(' falta el nombre del caso.'); + console.error(' sugerencia: AAAA-NNN-caratula-corta, por ejemplo 2026-014-perez-c-anses'); + process.exitCode = 64; + return; + } + + const jurisdiccion = opt('jurisdiccion'); + const fuero = opt('fuero'); + + // La regla dura del proyecto, aplicada al momento de crear en vez de al de + // computar: un caso sin jurisdiccion es un caso con todos sus plazos ciegos. + const faltan = []; + if (!jurisdiccion) faltan.push('--jurisdiccion'); + if (!fuero) faltan.push('--fuero'); + if (faltan.length) { + console.error(` falta ${faltan.join(' y ')}.`); + console.error(''); + console.error(' No es un dato administrativo: sin jurisdicción y fuero no se puede'); + console.error(' computar ningún plazo, y el caso queda abierto con los vencimientos'); + console.error(' invisibles. Si todavía no los sabés, no abras el caso: averiguálos.'); + console.error(''); + console.error(` jurisdicciones fichadas: ${JURISDICCIONES.join(' · ')}`); + console.error(` fueros: ${FUEROS.join(' · ')} · otro`); + process.exitCode = 64; + return; + } + + if (!JURISDICCIONES.some((j) => j.toLowerCase() === jurisdiccion.toLowerCase())) { + console.error(` "${jurisdiccion}" no es una jurisdicción fichada.`); + console.error(` Hay ficha para: ${JURISDICCIONES.join(', ')}.`); + console.error(' Si trabajás en otra, agregá su ficha en jurisdicciones/ primero:'); + console.error(' sin ella el cómputo de plazos no tiene contra qué verificarse.'); + process.exitCode = 64; + return; + } + + const destino = path.join(DIR, nombre); + if (await existe(destino)) { + console.error(` ya existe ${destino}`); + process.exitCode = 1; + return; + } + if (!(await existe(PLANTILLA))) { + console.error(` no encuentro la plantilla en ${PLANTILLA}`); + process.exitCode = 2; + return; + } + + await mkdir(DIR, { recursive: true }); + await cp(PLANTILLA, destino, { recursive: true }); + + const fichaPath = path.join(destino, 'FICHA.md'); + let ficha = await readFile(fichaPath, 'utf8'); + ficha = completar(ficha, 'Jurisdicción', jurisdiccion); + ficha = completar(ficha, 'Fuero', fuero); + const caratula = opt('caratula'); + if (caratula) ficha = completar(ficha, 'Carátula', caratula); + ficha = completar(ficha, 'Fecha de inicio', new Date().toISOString().slice(0, 10)); + await writeFile(fichaPath, ficha, 'utf8'); + + // La ficha de la jurisdicción es lo primero que hay que leer para este caso. + const notas = path.join(destino, 'notas.md'); + if (await existe(notas)) { + const j = jurisdiccion.toLowerCase().replace(/[^a-z-]/g, ''); + await writeFile(notas, + (await readFile(notas, 'utf8')) + + `\n\n## Referencia de jurisdicción\n\n` + + `Este caso tramita en **${jurisdiccion}**, fuero **${fuero}**.\n` + + `Antes de computar cualquier plazo, leé \`jurisdicciones/${j}.md\`.\n`, + 'utf8'); + } + + console.log(` Caso abierto en ${destino}`); + console.log(` jurisdicción: ${jurisdiccion} fuero: ${fuero}`); + console.log(''); + console.log(' Falta completar en FICHA.md: partes, objeto y estrategia.'); + console.log(' Los plazos van en plazos.md, computados con `argentina-plazos`.'); +} + +// --------------------------------------------------------------- listar + +async function leerCasos() { + let entradas = []; + try { + entradas = await readdir(DIR, { withFileTypes: true }); + } catch { return []; } + + const out = []; + for (const e of entradas) { + if (!e.isDirectory() || e.name.startsWith('_') || e.name.startsWith('.')) continue; + const f = path.join(DIR, e.name, 'FICHA.md'); + let md = ''; + try { md = await readFile(f, 'utf8'); } catch { /* sin ficha */ } + out.push({ + caso: e.name, + caratula: campo(md, 'Carátula'), + expediente: campo(md, 'N° de expediente'), + jurisdiccion: campo(md, 'Jurisdicción'), + fuero: campo(md, 'Fuero'), + estado: campo(md, 'Estado actual'), + completo: Boolean(campo(md, 'Jurisdicción') && campo(md, 'Fuero')), + }); + } + return out; +} + +async function listar() { + const casos = await leerCasos(); + if (JSON_OUT) { console.log(JSON.stringify(casos, null, 2)); return; } + if (!casos.length) { console.log(` No hay expedientes en ${DIR}`); return; } + + console.log(`\n ${casos.length} expediente(s)\n`); + for (const c of casos) { + const marca = c.completo ? ' ' : '⚠️'; + console.log(` ${marca} ${c.caso}`); + console.log(` ${c.caratula || '(sin carátula)'}`); + console.log(` ${c.jurisdiccion || 'SIN JURISDICCIÓN'} · ${c.fuero || 'SIN FUERO'} · ${c.estado || 'sin estado'}`); + } + const incompletos = casos.filter((c) => !c.completo).length; + if (incompletos) { + console.log(`\n ${incompletos} con la ficha incompleta: sus plazos no se pueden computar.`); + process.exitCode = 1; + } +} + +// --------------------------------------------------------------- validar + +async function validar() { + const soloUno = args[1] && !args[1].startsWith('--') ? args[1] : null; + const casos = (await leerCasos()).filter((c) => !soloUno || c.caso === soloUno); + if (!casos.length) { + console.error(soloUno ? ` no encuentro el caso ${soloUno}` : ` no hay expedientes en ${DIR}`); + process.exitCode = 1; + return; + } + + let problemas = 0; + for (const c of casos) { + const faltan = []; + if (!c.jurisdiccion) faltan.push('jurisdicción'); + if (!c.fuero) faltan.push('fuero'); + if (!c.caratula) faltan.push('carátula'); + if (!(await existe(path.join(DIR, c.caso, 'plazos.md')))) faltan.push('plazos.md'); + + if (faltan.length) { + problemas += 1; + console.log(` ⚠️ ${c.caso}: falta ${faltan.join(', ')}`); + } else { + console.log(` ok ${c.caso}`); + } + } + if (problemas) { + console.log(`\n ${problemas} caso(s) con la ficha incompleta.`); + console.log(' Los que no declaran jurisdicción y fuero tienen todos sus plazos ciegos.'); + process.exitCode = 1; + } +} + +// ---------------------------------------------------------------- cerrar + +async function cerrar() { + const nombre = args[1]; + if (!nombre || nombre.startsWith('--')) { + console.error(' falta el nombre del caso.'); + process.exitCode = 64; + return; + } + const fichaPath = path.join(DIR, nombre, 'FICHA.md'); + if (!(await existe(fichaPath))) { + console.error(` no encuentro ${fichaPath}`); + process.exitCode = 1; + return; + } + + // Antes de archivar, avisar si queda algun plazo sin resolver: un caso + // cerrado deja de mirarse, y un plazo pendiente adentro deja de existir. + const plazosPath = path.join(DIR, nombre, 'plazos.md'); + if (await existe(plazosPath)) { + const pendientes = (await readFile(plazosPath, 'utf8')) + .split('\n') + .filter((l) => l.trim().startsWith('|') && /🔲/u.test(l) && !/^_.*_$/.test(l.split('|')[1]?.trim() ?? '')); + if (pendientes.length) { + console.error(` ${nombre} tiene ${pendientes.length} plazo(s) todavía marcados 🔲.`); + console.error(''); + console.error(' Un caso archivado deja de aparecer en el vigía. Si esos plazos'); + console.error(' siguen corriendo, cerrarlo ahora los vuelve invisibles.'); + console.error(' Resolvelos o quitá la marca antes de cerrar.'); + process.exitCode = 1; + return; + } + } + + const ficha = completar(await readFile(fichaPath, 'utf8'), 'Estado actual', 'archivado'); + await writeFile(fichaPath, ficha, 'utf8'); + console.log(` ${nombre} marcado como archivado.`); + console.log(' La carpeta queda donde está: no se borra nada.'); +} + +// ---------------------------------------------------------------- despacho + +const comandos = { nuevo, listar, validar, cerrar }; +if (!comando || !comandos[comando]) { + console.error(' uso: casos.mjs [...]'); + console.error(''); + console.error(' nuevo --jurisdiccion --fuero [--caratula "..."]'); + console.error(' listar [--json]'); + console.error(' validar []'); + console.error(' cerrar '); + process.exitCode = 64; +} else { + await comandos[comando](); +} diff --git a/skills/argentina-expedientes/SKILL.md b/skills/argentina-expedientes/SKILL.md new file mode 100755 index 0000000..afc4469 --- /dev/null +++ b/skills/argentina-expedientes/SKILL.md @@ -0,0 +1,87 @@ +--- +name: argentina-expedientes +description: "Abre, lista, valida y archiva expedientes en casos/. Exige jurisdicción y fuero al crear el caso, no después, porque un expediente sin esos datos tiene todos sus plazos ciegos. Usar cuando el usuario diga «abrí un caso», «nuevo expediente», «qué casos tengo», «revisá las fichas» o «archivá el caso»." +license: MIT +--- + +# Expedientes · Abrir, listar, validar y archivar + +Skill original de Agente Smith (MIT). + +Hasta ahora abrir un caso era copiar `_PLANTILLA-CASO/` a mano y completar la +ficha después. Esta skill hace lo mismo pero **pide la jurisdicción y el fuero +antes de dejar el caso abierto**, que es la única diferencia que importa. + +## Por qué el orden cambia todo + +`argentina-vigia` reporta como **bloqueado** todo expediente al que le falta la +jurisdicción, porque sin ella no se puede computar ni un plazo. Esos casos no +nacen bloqueados por descuido: nacen así porque el formulario se completaba +después, y el después a veces no llega. + +Un caso con la ficha a medias no se ve como un problema. Se ve como un caso +nuevo. Y mientras tanto sus plazos corren sin que nadie los pueda calcular. + +Por eso acá la ficha no es un trámite posterior: es la condición para abrir. + +## Comandos + +```bash +node componentes/casos/casos.mjs nuevo --jurisdiccion --fuero [--caratula "..."] +node componentes/casos/casos.mjs listar [--json] +node componentes/casos/casos.mjs validar [] +node componentes/casos/casos.mjs cerrar +``` + +Nombre sugerido: `AAAA-NNN-caratula-corta`, por ejemplo `2026-014-perez-c-anses`. + +### nuevo + +Copia la plantilla, escribe jurisdicción, fuero, carátula y fecha de inicio en +la `FICHA.md`, y deja en `notas.md` el recordatorio de qué ficha de jurisdicción +leer para ese caso. + +**Se niega a abrir el caso si falta alguno de los dos datos.** No es rigidez: es +que un caso abierto con la ficha vacía es peor que ninguno, porque ocupa lugar +en la lista y parece atendido. + +**También rechaza una jurisdicción sin ficha propia.** Si el caso tramita en una +provincia que todavía no está en `jurisdicciones/`, hay que escribir esa ficha +primero: sin ella el cómputo de plazos no tiene contra qué verificarse, y un +plazo computado sin fuente es exactamente lo que la regla de la casa prohíbe. + +### listar y validar + +`listar` muestra todos con su jurisdicción y estado, marcando con ⚠️ los de +ficha incompleta. `validar` es lo mismo pero enfocado en qué falta. + +Los dos devuelven código distinto de cero si hay alguno incompleto, así que +sirven para encadenar o para un chequeo antes de empezar el día. + +Sirven sobre todo para los casos que ya existían antes de esta skill, o los que +alguien copió a mano. + +### cerrar + +Marca el estado como archivado. **No borra nada.** + +Se niega si quedan plazos marcados `🔲`. Un caso archivado deja de aparecer en el +vigía, así que cerrarlo con un plazo sin resolver no lo cierra: lo vuelve +invisible. Si el plazo ya no corre, quitale la marca y explicá por qué en +`notas.md`. + +## Después de abrir un caso + +1. Completá en la `FICHA.md` las partes, el objeto y la estrategia. +2. Leé la ficha de jurisdicción que quedó anotada en `notas.md`. +3. Cargá los plazos en `plazos.md` computándolos con **`argentina-plazos`**. +4. Verificá en SAIJ toda norma o jurisprudencia antes de citarla. + +## Lo que esta skill no hace + +**No inventa la carátula ni el número de expediente.** Si no se los das, quedan +vacíos y `validar` los marca. Deducirlos del nombre de la carpeta sería fabricar +un dato de un caso real. + +**No decide la jurisdicción.** Ni por el nombre del juzgado, ni por el tipo de +carátula, ni por lo que parece más probable. Se pregunta o no se abre. diff --git a/test/casos-test.mjs b/test/casos-test.mjs new file mode 100755 index 0000000..1c167f6 --- /dev/null +++ b/test/casos-test.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +// Tests del manejo de expedientes +// ------------------------------------------------ +// Comprueba lo que el script tiene que impedir, que es mas importante que lo +// que tiene que permitir: abrir un caso sin jurisdiccion, usar una +// jurisdiccion que no tiene ficha, y archivar un caso con plazos pendientes. +// +// Todo ocurre en /tmp. Nunca se escribe dentro de casos/. +// +// Uso: +// node test/casos-test.mjs + +import { mkdtemp, mkdir, writeFile, readFile, rm, cp } 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 RAIZ = path.join(AQUI, ".."); +const SCRIPT = path.join(RAIZ, "componentes", "casos", "casos.mjs"); +const PLANTILLA = path.join(RAIZ, "_PLANTILLA-CASO"); + +let pasaron = 0; +let fallaron = 0; +const ok = (n) => { pasaron += 1; process.stderr.write(`[casos] ok ${n}\n`); }; +const mal = (n, d) => { fallaron += 1; process.stderr.write(`[casos] MAL ${n}\n ${d}\n`); }; +const comprobar = (n, c, d) => (c ? ok(n) : mal(n, d)); + +/** Corre el script y devuelve {code, out, err} sin tirar. */ +async function correr(...argv) { + try { + const r = await ejecutar("node", [SCRIPT, ...argv]); + return { code: 0, out: r.stdout, err: r.stderr }; + } catch (e) { + return { code: e.code ?? 1, out: e.stdout ?? "", err: e.stderr ?? String(e) }; + } +} + +async function main() { + const base = await mkdtemp(path.join(tmpdir(), "casos-")); + const dirCasos = path.join(base, "casos"); + await mkdir(dirCasos, { recursive: true }); + const comun = ["--casos", dirCasos, "--plantilla", PLANTILLA]; + + // --- lo que tiene que impedir --- + + let r = await correr("nuevo", "0001-prueba", ...comun); + comprobar("no abre un caso sin jurisdicción ni fuero", + r.code === 64 && /jurisdicci/i.test(r.err), + `code=${r.code} err=${r.err.slice(0, 90)}`); + + r = await correr("nuevo", "0001-prueba", "--jurisdiccion", "Salta", ...comun); + comprobar("tampoco con jurisdicción pero sin fuero", + r.code === 64 && /fuero/i.test(r.err), + `code=${r.code}`); + + r = await correr("nuevo", "0001-prueba", "--jurisdiccion", "Córdoba", + "--fuero", "penal", ...comun); + comprobar("rechaza una jurisdicción que no tiene ficha", + r.code === 64 && /ficha/i.test(r.err), + `code=${r.code} err=${r.err.slice(0, 90)}`); + + // --- lo que tiene que hacer --- + + r = await correr("nuevo", "0001-prueba", "--jurisdiccion", "Salta", + "--fuero", "familia", "--caratula", "Caso sintético de prueba", ...comun); + comprobar("abre el caso cuando están los dos datos", r.code === 0, + `code=${r.code} err=${r.err.slice(0, 120)}`); + + const ficha = await readFile(path.join(dirCasos, "0001-prueba", "FICHA.md"), "utf8"); + comprobar("escribe la jurisdicción en la ficha", /\*\*Jurisdicción:\*\*\s*Salta/.test(ficha), + ficha.split("\n").find((l) => /Jurisdicci/.test(l)) ?? "no encontre la linea"); + comprobar("escribe el fuero", /\*\*Fuero:\*\*\s*familia/.test(ficha), ""); + comprobar("escribe la carátula", /Caso sintético de prueba/.test(ficha), ""); + comprobar("pone la fecha de inicio", /\*\*Fecha de inicio:\*\*\s*\d{4}-\d{2}-\d{2}/.test(ficha), ""); + + const notas = await readFile(path.join(dirCasos, "0001-prueba", "notas.md"), "utf8"); + comprobar("apunta a la ficha de jurisdicción en las notas", + /jurisdicciones\/salta\.md/.test(notas), notas.slice(-120)); + + r = await correr("nuevo", "0001-prueba", "--jurisdiccion", "Salta", "--fuero", "familia", ...comun); + comprobar("no pisa un caso que ya existe", r.code === 1, `code=${r.code}`); + + // --- validar --- + + r = await correr("validar", ...comun); + comprobar("valida en verde un caso completo", r.code === 0 && /ok\s+0001/.test(r.out), + `code=${r.code} out=${r.out.slice(0, 90)}`); + + // Un caso creado a mano, como se hacía antes: sin jurisdicción. + await cp(PLANTILLA, path.join(dirCasos, "0002-a-mano"), { recursive: true }); + r = await correr("validar", ...comun); + comprobar("detecta el caso creado a mano sin jurisdicción", + r.code === 1 && /0002/.test(r.out), `code=${r.code} out=${r.out.slice(0, 120)}`); + + r = await correr("listar", ...comun); + comprobar("listar marca los incompletos y sale distinto de cero", + r.code === 1 && /SIN JURISDICCIÓN/.test(r.out), r.out.slice(0, 120)); + + // --- cerrar --- + + const plazos = path.join(dirCasos, "0001-prueba", "plazos.md"); + await writeFile(plazos, + "| Acto | Tipo | Inicio | Vencimiento | Estado |\n|---|---|---|---|---|\n" + + "| contestar demanda | hábiles judiciales | 2026-08-01 | | 🔲 a computar |\n", "utf8"); + r = await correr("cerrar", "0001-prueba", ...comun); + comprobar("no archiva un caso con plazos sin resolver", + r.code === 1 && /🔲|invisibles/u.test(r.err), `code=${r.code} err=${r.err.slice(0, 120)}`); + + await writeFile(plazos, + "| Acto | Tipo | Inicio | Vencimiento | Estado |\n|---|---|---|---|---|\n" + + "| contestar demanda | hábiles judiciales | 2026-08-01 | 2026-08-20 | computado |\n", "utf8"); + r = await correr("cerrar", "0001-prueba", ...comun); + comprobar("archiva cuando no queda nada pendiente", r.code === 0, + `code=${r.code} err=${r.err.slice(0, 120)}`); + + const cerrada = await readFile(path.join(dirCasos, "0001-prueba", "FICHA.md"), "utf8"); + comprobar("marca el estado como archivado", /\*\*Estado actual:\*\*\s*archivado/.test(cerrada), ""); + + await rm(base, { recursive: true, force: true }); + process.stderr.write(`\n[casos] ${pasaron} ok, ${fallaron} mal\n`); + process.exitCode = fallaron > 0 ? 1 : 0; +} + +await main();