Semana 12 — Integración continua y dependencias
Objetivos
- Definir integración continua (como práctica, no como herramienta) y explicar qué hace que un build sea confiable: reproducible, rápido, con el resultado a la vista de todos y arreglado apenas se rompe.
- Construir un pipeline de GitHub Actions que revise cada pull request y que la protección de la rama
mainhaga obligatorio. - Explicar semver, los rangos de versiones, el lockfile y la diferencia entre
npm installynpm ci, y por qué una dependencia directa trae decenas de transitivas. - Evaluar una dependencia antes de agregarla y leer con criterio un reporte de
npm audit. - Reconocer los ataques a la cadena de suministro, incluidos los paquetes inventados por un modelo de lenguaje (slopsquatting), y las defensas que existen.
Sesión A — Integración continua y build
Lecturas:
- SWEG, cap. 23, Continuous Integration.
- SWEG, cap. 18, Build Systems and Build Philosophy, hasta Modern Build Systems inclusive.
Lecturas complementarias:
- Martin Fowler, Continuous Integration (2024): las prácticas, una por una.
- GitHub, documentación de GitHub Actions, en especial Workflow syntax y Secure use reference.
Temas:
- Integración continua es una práctica: cada integrante integra su trabajo a la rama principal al menos una vez al día, y cada integración se verifica con un build automático que incluye las pruebas. El servidor de CI solo la hace cumplible. Un equipo con un pipeline perfecto y ramas que viven tres semanas no hace integración continua.
- Las reglas que la hacen funcionar: un solo repositorio y una rama principal, un build que se prueba a sí mismo (self-testing build), que corre en minutos, cuyo resultado ve todo el equipo, y que cuando se rompe se arregla antes que cualquier otra cosa (o se revierte el commit). Un build que pasa “casi siempre” no da información.
- Qué es el build cuando Node ejecuta TypeScript sin compilar: no hay un paso que genere JavaScript, pero sí hay un build. Para Kallpa son
tsc(tipos),eslinty las pruebas: exactamente lo que corremake check. En la semana 13 se agrega la imagen de contenedor. La regla: el pipeline llama a lo mismo que corre una persona en su computadora, no a una copia con otros comandos. - Build reproducible (SWEG cap. 18; el cap. 23 habla de pruebas herméticas): con las mismas entradas da el mismo resultado, sin depender de lo que esté instalado en la máquina. Por eso las versiones de las herramientas se fijan. “En mi computadora pasa” casi siempre significa que algo no estaba fijado: la versión de Node, de TypeScript, de una dependencia, la zona horaria.
- En Google (SWEG cap. 23), presubmit son las pruebas que corren antes de integrar un cambio (rápidas, confiables) y post-submit las que corren después (más lentas o más grandes). En GitHub esto se traduce en un workflow que corre en
pull_requesty otro, o el mismo, enpushamain. Una prueba flaky en presubmit es peor que no tenerla: enseña al equipo a ignorar el rojo. - Conceptos de GitHub Actions: un workflow (un archivo YAML en
.github/workflows/) se dispara con eventos; tiene jobs que corren en un runner (una máquina virtual nueva cada vez) y cada job tiene steps, que son comandos (run:) o actions reutilizables (uses: actions/checkout@...). Los jobs corren en paralelo salvo que uno declareneeds:. - Seguridad del pipeline: el
GITHUB_TOKENcon los permisos mínimos (permissions: contents: read), los secrets nunca en el código ni en los logs, y las actions de terceros fijadas a una versión. Una etiqueta comov45se puede mover: en marzo de 2025 alguien reescribió las etiquetas detj-actions/changed-filespara que apuntaran a un commit que imprimía los secrets en los logs (CVE-2025-30066). Fijar al SHA del commit evita eso. - Error común: un pipeline que no impide nada. Si
mainno está protegida y no se exige que el check pase, el pipeline es decorativo: el pull request se integra en rojo “porque era urgente”. La protección de rama con required status checks es lo que convierte el pipeline en una regla. Vale también para el código que escribe un agente: el pipeline atrapa lo que rompió sin darse cuenta, y por eso a un agente no se le da permiso para saltarse los checks ni para cambiar el workflow sin revisión.
Sesión B — Dependencias y cadena de suministro
Lecturas:
- SWEG, cap. 21, Dependency Management.
- Russ Cox, Our Software Dependency Problem (2019): cómo inspeccionar una dependencia antes de aceptarla.
Lecturas complementarias:
- Versionado semántico 2.0.0 (en español).
- npm,
npm ci,package-lock.jsonynpm audit. - Spracklen y otros, We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs (USENIX Security Symposium, 2025).
- SLSA (niveles de garantía de la cadena de suministro) y OpenSSF Scorecard.
Temas:
- Una dependencia es código que no escribieron y del que igual responden: sus errores, sus vulnerabilidades y sus cambios son ahora de ustedes. Agregar una es fácil; mantenerla, años de trabajo. SWEG lo resume en que la gestión de dependencias es más difícil que el control de versiones porque el tiempo y la escala no se controlan.
- Versionado semántico (
MAJOR.MINOR.PATCH): MAJOR cambia cuando se rompe la compatibilidad, MINOR cuando se agrega algo compatible, PATCH cuando se corrige algo compatible. Es una promesa de quien publica, no una garantía: por la ley de Hyrum, un PATCH puede romper a quien dependía de un comportamiento que nadie prometió. Rangos enpackage.json:^8.72.1acepta cualquier8.x.ydesde8.72.1(en versiones0.x, el rango es más estrecho:^0.23.5se detiene antes de0.24.0);~8.72.1, solo8.72.x;8.72.1, exactamente esa versión. El lockfile (package-lock.json) registra la versión exacta y el hash (integrity) de cada paquete instalado, también los transitivos.npm installpuede actualizar el lockfile;npm ciinstala exactamente lo que dice, borranode_modulesantes y falla sipackage.jsony el lockfile no coinciden. En el pipeline siempre vanpm ci. - Dependencias transitivas: en el laboratorio, seis devDependencies directas (TypeScript, los tipos de Node, ESLint y sus reglas, typescript-eslint y fast-check) instalan 100 paquetes. El problema del diamante: dos de ellos piden versiones incompatibles de un tercero; npm suele resolverlo instalando dos copias, y el conflicto duro reaparece con las
peerDependencies. LaspeerDependenciesdeclaran con qué versión de otro paquete se espera convivir: por eso el curso sigue en TypeScript 6 aunque ya exista la 7, porque typescript-eslint todavía no la soporta, ynpm outdatedlo muestra como Latest 7.0.2 frente a Wanted 6.0.3. - Antes de agregar una dependencia (Russ Cox): ¿existe y es la que creo?, ¿quién la mantiene y cuándo fue el último release?, ¿tiene pruebas?, ¿cuántas dependencias trae?, ¿qué licencia tiene?, ¿la puedo reemplazar con unas pocas líneas propias o con la biblioteca estándar de Node? Kallpa no tiene ninguna dependencia de producción a propósito:
node:http,node:sqliteynode:testalcanzan. En el proyecto, cada dependencia nueva se justifica en un ADR. - Cadena de suministro: typosquatting (un paquete con un nombre casi igual a uno popular), dependency confusion (un paquete público con el nombre de uno interno), un mantenedor comprometido o malicioso. El caso de referencia es xz utils (CVE-2024-3094): una puerta trasera que alguien metió en 2024 después de ganarse durante dos años la confianza del único mantenedor. Defensas: lockfile con hashes,
npm audit signatures(firmas del registro y atestaciones de procedencia), SBOM (la lista de todo lo que va en el producto), SLSA, Scorecard. - Slopsquatting: un modelo de lenguaje sugiere
npm installde un paquete que no existe, alguien registra ese nombre con código malicioso y espera. El estudio de Spracklen y otros generó 576 000 muestras de código con 16 modelos: en promedio, al menos 5.2 % de los paquetes sugeridos por modelos comerciales y 21.7 % de los sugeridos por modelos abiertos no existían, con más de 205 000 nombres inventados distintos. Basta con que un nombre inventado se repita para que valga la pena registrarlo. La defensa es la misma de siempre, aplicada sin excepción: no se instala nada que nadie haya verificado. - Leer
npm auditcon criterio. En la corrida del laboratorio (1 de noviembre de 2026) reporta 11 vulnerabilidades moderadas, todas por una versión deminimatchque usa ESLint, con riesgo de denegación de servicio. Son dependencias de desarrollo: no llegan al producto (npm audit --omit=devencuentra 0) y para aprovecharla alguien tendría que controlar los patrones de archivos que recibe ESLint, que son los de su propia configuración. La decisión se escribe: se acepta el riesgo, se espera el release de ESLint que lo corrige y se vuelve a mirar. - Error común: correr
npm audit fix --forcepara que el reporte quede en cero. En este caso propone instalar ESLint 4.0.0, una versión de 2017: al correrlo, los hallazgos suben en lugar de bajar ymake checkdeja de pasar. Otro error común: ponernpm auditsin nivel en el pipeline; sale con código 1 por cualquier hallazgo y el pipeline queda en rojo desde el primer día.
Laboratorio — Un pipeline de GitHub Actions para Kallpa
Guía: labs/README.md (secciones 4 y 8). Referencias: GitHub, Workflow syntax, actions/setup-node y About protected branches; npm, npm ci.
El runner de GitHub no puede usar la imagen del curso, así que el repositorio tiene que declarar sus herramientas: es la única semana en la que se instala algo con npm. Se instalan las mismas versiones que trae la imagen (ver env/Dockerfile), así el pipeline y el contenedor revisan lo mismo. Trabajen en su repositorio de Kallpa, en una rama pipeline-ci. Los comandos de npm y make van en la shell del contenedor; git push y la configuración de GitHub, en la terminal de su computadora.
- Fijar las herramientas como devDependencies, con versión exacta, y mirar lo que llegó:
Abrannpm install --save-dev --save-exact typescript@6.0.3 @types/node@26.6.6 eslint@10.14.0 \ @eslint/js@10.0.1 typescript-eslint@8.72.1 fast-check@4.10.2 npm ls # las seis directas npm ls --all | less # todo el árbolpackage-lock.jsony busquen un paquete que no instalaron ustedes: ¿quién lo pidió (npm explain <paquete>)? Hagan commit depackage.jsonypackage-lock.json;node_modules/ya está en.gitignore. - Simular el runner.
npm ciymake checkcon unPATHque no incluya las herramientas de la imagen:
Prueben tambiénrm -rf node_modules && npm ci env PATH="$PWD/node_modules/.bin:/usr/local/bin:/usr/bin:/bin" make checkenv PATH=/usr/local/bin:/usr/bin:/bin make check: sinnode_modules/.binen elPATH,makeno encuentratsc(make: tsc: No such file or directory), que es lo que pasa en el runner. Anoten este detalle para el workflow. Ojo: dentro del contenedor losimporttodavía encuentran los paquetes de la imagen, así que un paquete que falte enpackage.jsonsolo se descubre en el pipeline. - Escribir
.github/workflows/ci.yml: que corra en cadapull_requesty en cadapushamain, conpermissions: contents: read, un job enubuntu-latestcontimeout-minutes: 10,actions/checkout,actions/setup-node(node-version: 26,cache: npm),npm ci, un step que agreguenode_modules/.binalPATH(echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH") ymake check. Fijen las actions al menos a su versión mayor y anoten en un comentario cómo se fijarían al SHA. - Desde su computadora, subir la rama y abrir el pull request. Ver el check en la pestaña Checks, abrir el log de cada step y anotar cuánto tardó el job y cuánto de ese tiempo fue
npm ci. - Romper el pipeline a propósito: en otra rama, un cambio que no pase
tsc(por ejemplo, pasar unstringdonde se espera unnumber) y otro que haga fallar una prueba. Abrir el pull request, encontrar en el log la línea exacta del error y cerrarlo sin hacer merge. - Proteger
main(en GitHub: Settings → Branches o Rules): exigir pull request con una aprobación y el status check del job de CI. Comprobar que el botón de merge queda bloqueado mientras el check está en rojo. Hacer merge del pull request del paso 4 con la aprobación de otra persona. Con una cuenta gratuita, la protección de ramas solo está disponible en repositorios públicos; en uno privado hace falta GitHub Pro (gratis con GitHub Education) o una organización con plan pagado. - Dependencias con criterio, en el contenedor: correr
npm audit,npm audit --omit=devynpm audit --audit-level=high; echo $?, ynpm outdated. Expliquen en la descripción del pull request qué reporta cada uno, por qué no se correnpm audit fix --forcey qué nivel denpm auditpondrían en el pipeline (agréguenlo como step). Corran tambiénnpm audit signaturesy anoten qué verifica. - Paquetes que no existen.
code/week-12/sugerencia.mdtrae un cambio escrito para la clase, como los que suele proponer una herramienta de IA para Kallpa, con tresnpm install. Antes de instalar nada, revisen cada nombre connpm view <paquete> name version license repository.url time.modifiedy con la página del paquete en npmjs.com: ¿existe?, ¿es el que dice ser?, ¿quién lo mantiene?, ¿hace falta? Decidan cuál aceptarían (si alguno) y escriban la decisión como lo harían en su registro de uso de IA.time.modifiedes un dato del registro, no la fecha del último release: compárenlo connpm view <paquete> time --json.
Entregas
- No hay entregas esta semana. P4 (
projects/p4-entrega-continua/README.md), publicado en la semana 11, pide el pipeline de CI/CD de su proyecto: lo que hicieron hoy sobre Kallpa es el primer paso. Entrega en semana 14.
Preguntas de repaso
- Un equipo tiene un pipeline que corre en cada push, pero cada integrante trabaja dos semanas en su rama antes de hacer merge. ¿Hace integración continua? ¿Qué cambiarían primero?
- ¿Qué diferencia hay entre
npm installynpm ci? ¿Cuál va en el pipeline y por qué? ¿Qué pasa si alguien editapackage.jsony se olvida del lockfile? package.jsondice"eslint": "^10.14.0". ¿Qué versiones acepta? ¿Y con"~10.14.0"? Den un ejemplo, con la ley de Hyrum, de cómo un release PATCH podría romper Kallpa.npm auditreporta 11 vulnerabilidades moderadas en dependencias de desarrollo y proponenpm audit fix --force. ¿Qué harían y cómo lo documentarían? ¿Qué cambiaría si la vulnerabilidad estuviera en una dependencia que llega a producción?- Una herramienta de IA les sugiere
npm installde un paquete con un nombre razonable que nadie del equipo conoce. Describan, paso a paso, cómo lo verificarían. ¿Por qué no basta con comprobar que el nombre existe en el registro?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (16)Pruebas (7)Compilación (3)
Ejemplos
Los programas que usan los apuntes y la clase.
#!/usr/bin/env bash
# Escenarios del apunte que no usan la red: cada uno trabaja en una copia de
# Kallpa dentro de build/. Los comandos que importan se imprimen con $.
set -u
unset MAKELEVEL MAKEFLAGS MFLAGS
HERE="$(cd "$(dirname "$0")" && pwd)"
KALLPA="${KALLPA:-$HERE/../../kallpa}"
GIT=(git -c user.name=Kallpa -c user.email=kallpa@example.com -c init.defaultBranch=main)
# Con una tubería, el código de salida sería el del último comando: no se muestra.
show() {
echo; echo "\$ $*"
bash -c "$*" 2>&1
local code=$?
case "$*" in *\|*) ;; *) echo "[código de salida: $code]" ;; esac
}
fresh() {
local dir="$HERE/build/$1"
rm -rf "$dir"
mkdir -p "$HERE/build"
cp -r "$KALLPA" "$dir"
rm -f "$dir"/data/*.db "$dir"/data/*.db-*
cd "$dir" || exit 2
"${GIT[@]}" init -q
"${GIT[@]}" add -A
"${GIT[@]}" commit -qm "Kallpa publicada"
}
case "${1:-}" in
tipos)
fresh tipos
# Node solo quita los tipos: a formatSoles le llega un string, y como
# cents / 100 lo convierte en número, el resultado es el mismo.
sed -i 's/formatSoles(c.priceCents)/formatSoles(String(c.priceCents))/' src/notifications/notifier.ts
show "git --no-pager diff -U0 | tail -3 | cut -c1-120"
show "make test | tail -8"
show "make typecheck"
;;
colision)
fresh colision
# Rama A: cambia el nombre de formatSoles en todas las líneas donde se usa.
"${GIT[@]}" switch -qc renombra-soles
sed -i 's/formatSoles/centsToSoles/g' src/domain/pricing.ts src/notifications/notifier.ts test/pricing.test.ts
"${GIT[@]}" commit -qam "Renombra formatSoles a centsToSoles"
echo "--- rama renombra-soles"
make check >/dev/null 2>&1; echo "make check: código de salida $?"
# Rama B, desde el mismo punto: usa la función con su nombre de siempre.
"${GIT[@]}" switch -q main
"${GIT[@]}" switch -qc linea-de-caja
cat >> src/domain/report.ts <<'TS'
// Una línea para imprimir la caja del día.
export function summaryLine(summary: DailySummary): string {
return `${summary.date}: ${summary.bookings} reservas, ${formatSoles(summary.revenueCents)}`;
}
TS
sed -i "1i import { formatSoles } from './pricing.ts';" src/domain/report.ts
"${GIT[@]}" commit -qam "Agrega la línea de caja del día"
echo "--- rama linea-de-caja"
make check >/dev/null 2>&1; echo "make check: código de salida $?"
echo "--- las dos juntas"
"${GIT[@]}" switch -q main
"${GIT[@]}" merge -q --no-edit renombra-soles
export GIT_AUTHOR_NAME=Kallpa GIT_AUTHOR_EMAIL=kallpa@example.com GIT_COMMITTER_NAME=Kallpa GIT_COMMITTER_EMAIL=kallpa@example.com
show "git merge --no-edit linea-de-caja"
show "make check"
;;
zona)
cd "$HERE" || exit 2
show "TZ=America/Lima node --test ejemplos/fecha-local.check.ts | grep -E '^(✔|✖) la fecha|^ℹ (pass|fail)'"
show "TZ=UTC node --test ejemplos/fecha-local.check.ts | grep -E '^(✔|✖) la fecha|^ℹ (pass|fail)' | head -4"
;;
*)
echo "Uso: escenarios.sh tipos|colision|zona" >&2
exit 2
;;
esac// Las mismas reglas que Kallpa: las recomendadas de JavaScript y de
// typescript-eslint, sin información de tipos.
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
export default defineConfig(
{ ignores: ['build/', 'solution/', 'esperado/'] },
js.configs.recommended,
tseslint.configs.recommended,
);// La probabilidad de que un build salga en rojo sin que nadie haya roto nada,
// si cada una de `tests` pruebas falla sola con probabilidad `p` y las fallas
// son independientes.
export function falseRedProbability(p: number, tests: number): number {
return 1 - (1 - p) ** tests;
}// Lo que se puede saber de un proyecto leyendo solo su package-lock.json.
// Uso: node lockfile-report.ts <package-lock.json>
import { readFileSync } from 'node:fs';
export type LockEntry = {
version?: string;
resolved?: string;
integrity?: string;
license?: string;
dev?: boolean;
hasInstallScript?: boolean;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
export type Lockfile = { lockfileVersion: number; packages: Record<string, LockEntry> };
const MARK = 'node_modules/';
export function nameOf(key: string): string {
return key.slice(key.lastIndexOf(MARK) + MARK.length);
}
export function installed(lock: Lockfile): [string, LockEntry][] {
return Object.entries(lock.packages).filter(([key]) => key !== '');
}
// La misma búsqueda que hace Node: primero node_modules de la propia carpeta,
// después la de cada carpeta de arriba.
export function resolveFrom(lock: Lockfile, fromKey: string, name: string): string | null {
let base = fromKey;
for (;;) {
const candidate = `${base === '' ? '' : `${base}/`}${MARK}${name}`;
if (candidate in lock.packages) return candidate;
if (base === '') return null;
const cut = base.lastIndexOf(MARK);
base = cut <= 0 ? '' : base.slice(0, cut - 1);
}
}
// Lo que un paquete instalado arrastra por sus `dependencies`, sin contarlo a
// él. No sigue peerDependencies: esas las pide, pero no las trae.
export function closure(lock: Lockfile, key: string): Set<string> {
const seen = new Set<string>();
const pending = [key];
while (pending.length > 0) {
const current = pending.pop() as string;
for (const name of Object.keys(lock.packages[current]?.dependencies ?? {})) {
const target = resolveFrom(lock, current, name);
if (target !== null && !seen.has(target) && target !== key) {
seen.add(target);
pending.push(target);
}
}
}
return seen;
}
// Quién pide cada paquete, y con qué rango.
export function requesters(lock: Lockfile, name: string): { from: string; range: string }[] {
return Object.entries(lock.packages)
.filter(([key]) => key !== '')
.flatMap(([key, entry]) => {
const range = entry.dependencies?.[name];
return range === undefined ? [] : [{ from: nameOf(key), range }];
});
}
export type Report = {
total: number;
direct: { name: string; version: string; brings: number }[];
nested: { name: string; versions: string[] }[];
licenses: { license: string; count: number }[];
installScripts: string[];
hosts: string[];
withoutIntegrity: string[];
};
export function report(lock: Lockfile): Report {
const entries = installed(lock);
const root = lock.packages[''] ?? {};
const directNames = Object.keys({ ...root.dependencies, ...root.devDependencies }).sort();
const direct = directNames.map((name) => {
const key = `${MARK}${name}`;
return { name, version: lock.packages[key]?.version ?? '?', brings: closure(lock, key).size };
});
const versions = new Map<string, Set<string>>();
for (const [key, entry] of entries) {
const name = nameOf(key);
versions.set(name, (versions.get(name) ?? new Set()).add(entry.version ?? '?'));
}
const nested = [...versions]
.filter(([, found]) => found.size > 1)
.map(([name, found]) => ({ name, versions: [...found].sort() }))
.sort((a, b) => a.name.localeCompare(b.name));
const licenseCount = new Map<string, number>();
for (const [, entry] of entries) {
const license = entry.license ?? '(sin licencia declarada)';
licenseCount.set(license, (licenseCount.get(license) ?? 0) + 1);
}
const licenses = [...licenseCount]
.map(([license, count]) => ({ license, count }))
.sort((a, b) => b.count - a.count || a.license.localeCompare(b.license));
return {
total: entries.length,
direct,
nested,
licenses,
installScripts: entries.filter(([, e]) => e.hasInstallScript === true).map(([key]) => nameOf(key)),
hosts: [...new Set(entries.map(([, e]) => new URL(e.resolved ?? 'about:blank').host))].sort(),
withoutIntegrity: entries.filter(([, e]) => e.integrity === undefined).map(([key]) => nameOf(key)),
};
}
export function loadLockfile(path: string): Lockfile {
return JSON.parse(readFileSync(path, 'utf8')) as Lockfile;
}
function print(r: Report): void {
console.log(`Paquetes instalados: ${r.total}`);
console.log('Directos y lo que traen por sus dependencies:');
for (const d of r.direct) console.log(` ${d.name}@${d.version}: ${d.brings}`);
console.log('Con más de una versión en el árbol:');
for (const n of r.nested) console.log(` ${n.name}: ${n.versions.join(', ')}`);
console.log('Licencias:');
for (const l of r.licenses) console.log(` ${String(l.count).padStart(3)} ${l.license}`);
console.log(`Con script de instalación: ${r.installScripts.length}`);
console.log(`Sin hash: ${r.withoutIntegrity.length}`);
console.log(`Descargados de: ${r.hosts.join(', ')}`);
}
if (import.meta.main) {
const path = process.argv[2];
if (path === undefined) {
console.error('Uso: node lockfile-report.ts <package-lock.json>');
process.exit(2);
}
print(report(loadLockfile(path)));
}#!/usr/bin/env bash
# Las corridas del apunte que usan el registro de npm. Cada comando se imprime
# con $, como en una terminal, y la salida es la de hoy: puede no coincidir con
# la del apunte, porque el registro cambia.
set -u
unset MAKELEVEL MAKEFLAGS MFLAGS
HERE="$(cd "$(dirname "$0")" && pwd)"
KALLPA="${KALLPA:-$HERE/../../kallpa}"
TOOLS=(typescript@6.0.3 @types/node@26.6.6 eslint@10.14.0 @eslint/js@10.0.1 typescript-eslint@8.72.1 fast-check@4.10.2)
RUNNER_PATH_BASE=/usr/local/bin:/usr/bin:/bin
# Con una tubería, el código de salida sería el del último comando: no se muestra.
show() {
echo; echo "\$ $*"
bash -c "$*" 2>&1
local code=$?
case "$*" in *\|*) ;; *) echo "[código de salida: $code]" ;; esac
}
# El error de uso de npm trae toda la ayuda de la orden: se corta antes de ella.
short() { echo; echo "\$ $*"; bash -c "$*" 2>&1 | sed '/^npm error Clean install a project/,$d'; echo "[código de salida: ${PIPESTATUS[0]}]"; }
quiet_install() {
local dir="$HERE/build/$1"
rm -rf "$dir"
mkdir -p "$HERE/build"
cp -r "$KALLPA" "$dir"
rm -f "$dir"/data/*.db "$dir"/data/*.db-*
cd "$dir" || exit 2
npm install --no-audit --no-fund --save-dev --save-exact "${TOOLS[@]}" >/dev/null 2>&1
}
case "${1:-}" in
arbol)
rm -rf "$HERE/build/arbol"; mkdir -p "$HERE/build"; cp -r "$KALLPA" "$HERE/build/arbol"
cd "$HERE/build/arbol" || exit 2
show "npm install --save-dev --save-exact ${TOOLS[*]}"
show "npm ls"
show "npm ls --all --parseable | wc -l"
show "npm ls minimatch"
show "npm explain minimatch | head -5"
show "npm explain debug | head -4"
show "wc -l package-lock.json"
show "du -sh node_modules"
;;
runner)
quiet_install runner
show "rm -rf node_modules && npm ci --no-fund"
show "env PATH=\"\$PWD/node_modules/.bin:$RUNNER_PATH_BASE\" make check | tail -9"
show "env PATH=$RUNNER_PATH_BASE make check"
;;
lockfile)
quiet_install lockfile
cp package.json /tmp/w12-package.json; cp package-lock.json /tmp/w12-lock.json
echo "--- alguien edita package.json y se olvida del lockfile"
sed -i 's/"fast-check": "4.10.2"/"fast-check": "4.10.1"/' package.json
short "npm ci"
cp /tmp/w12-package.json package.json
echo; echo "--- no hay lockfile"
rm package-lock.json
short "npm ci"
cp /tmp/w12-lock.json package-lock.json
echo; echo "--- el hash de ms en el lockfile no es el del paquete que llega"
node -e "const fs=require('fs');const l=JSON.parse(fs.readFileSync('package-lock.json'));l.packages['node_modules/ms'].integrity=l.packages['node_modules/fast-check'].integrity;fs.writeFileSync('package-lock.json',JSON.stringify(l,null,2))"
rm -rf node_modules; npm cache clean --force >/dev/null 2>&1
show "npm ci 2>&1 | grep -E 'code EINTEGRITY|integrity checksum' | cut -c1-110"
cp /tmp/w12-lock.json package-lock.json
rm -f /tmp/w12-package.json /tmp/w12-lock.json
;;
auditar)
quiet_install auditar
show "npm audit | grep -v -E 'Depends on|node_modules/'"
show "npm audit --omit=dev"
show "npm audit --audit-level=high > /dev/null"
show "npm audit --audit-level=moderate > /dev/null"
show "npm audit --json | jq -c '.metadata.vulnerabilities'"
show "npm audit --json | jq -r '.vulnerabilities | to_entries[] | select(.value.isDirect) | .key'"
show "npm outdated"
show "npm audit signatures"
;;
fuerza)
quiet_install fuerza
show "npm audit fix --force --dry-run 2>&1 | grep -E 'Updating'"
show "npm audit fix --force --no-fund > /dev/null 2>&1"
show "npm pkg get devDependencies"
show "npm audit 2>&1 | tail -7 | head -1"
show "env PATH=\"\$PWD/node_modules/.bin:$RUNNER_PATH_BASE\" make check"
;;
peer)
quiet_install peer
show "npm view typescript-eslint@8.72.1 peerDependencies"
show "npm install --no-audit --no-fund --save-dev --save-exact typescript@7.0.2 2>&1 | cut -c1-90 | grep -v 'overriding peer' | sed -n 1,12p"
show "env PATH=\"\$PWD/node_modules/.bin:$RUNNER_PATH_BASE\" make typecheck"
show "env PATH=\"\$PWD/node_modules/.bin:$RUNNER_PATH_BASE\" make lint 2>&1 | head -3 | cut -c1-90"
show "rm -rf node_modules && npm ci 2>&1 | cut -c1-90 | sed -n 1,5p"
;;
rangos)
rm -rf "$HERE/build/rangos"; mkdir -p "$HERE/build/rangos/antes" "$HERE/build/rangos/hoy"
for dir in antes hoy; do
echo '{ "name": "rangos", "private": true, "devDependencies": { "eslint": "^10.0.0" } }' > "$HERE/build/rangos/$dir/package.json"
done
cd "$HERE/build/rangos/antes" || exit 2
show "npm install --no-audit --no-fund --before=2026-03-01"
show "npm ls eslint --depth=0"
cd "$HERE/build/rangos/hoy" || exit 2
show "npm install --no-audit --no-fund"
show "npm ls eslint --depth=0"
show "npm view '@eslint/config-array@^0.23.5' version | tail -1"
;;
falta)
quiet_install falta
show "npm uninstall --no-audit --no-fund @eslint/js"
show "make check | tail -8"
;;
sbom)
quiet_install sbom
show "npm sbom --sbom-format cyclonedx | jq -r '.bomFormat, .specVersion, (.components | length)'"
show "npm sbom --sbom-format cyclonedx --omit=dev | jq '.components | length'"
show "npm sbom --sbom-format cyclonedx | jq -c '.components[] | select(.name == \"minimatch\") | {name, version, purl, licenses}'"
;;
paquetes)
show "npm view request name version license repository.url time.modified deprecated"
show "npm view fast-check name version license repository.url time.modified dependencies"
show "npm view kallpa-slots-helper name version license repository.url time.modified | cut -c1-90"
show "npm view debug@4.4.2 version 2>&1 | head -2"
;;
*)
echo "Uso: registro.sh arbol|runner|falta|lockfile|auditar|fuerza|peer|rangos|sbom|paquetes" >&2
exit 2
;;
esac// Revisa un workflow de GitHub Actions contra las reglas de la semana, sin
// correrlo. No dice si el workflow funciona: eso solo lo dice el runner.
// Uso: node revisar-workflow.ts .github/workflows/ci.yml
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
export type Finding = { level: 'error' | 'aviso'; rule: string; message: string };
type Step = {
uses?: unknown;
run?: unknown;
shell?: unknown;
with?: Record<string, unknown>;
['continue-on-error']?: unknown;
};
type Job = { steps?: Step[]; ['continue-on-error']?: unknown };
type Workflow = {
on?: unknown;
permissions?: unknown;
jobs?: Record<string, Job>;
};
const FULL_SHA = /^[0-9a-f]{40}$/;
const VERSION_TAG = /^v\d+(\.\d+){0,2}$/;
function eventsOf(on: unknown): Record<string, unknown> {
if (typeof on === 'string') return { [on]: null };
if (Array.isArray(on)) return Object.fromEntries(on.map((event) => [String(event), null]));
return (on ?? {}) as Record<string, unknown>;
}
function pushesToMain(push: unknown): boolean {
if (push === undefined) return false;
const branches = (push as { branches?: unknown } | null)?.branches;
return branches === undefined || (Array.isArray(branches) && branches.includes('main'));
}
export function checkWorkflow(workflow: Workflow): Finding[] {
const findings: Finding[] = [];
const error = (rule: string, message: string) => findings.push({ level: 'error', rule, message });
const warn = (rule: string, message: string) => findings.push({ level: 'aviso', rule, message });
const events = eventsOf(workflow.on);
if (!('pull_request' in events)) error('evento-pr', 'No corre en pull_request: no revisa nada antes del merge.');
if (!pushesToMain(events.push)) error('evento-push', 'No corre en push a main.');
if ('pull_request_target' in events) {
error('pull-request-target', 'pull_request_target corre con los permisos de la rama base; con código de un fork es peligroso.');
}
const permissions = workflow.permissions;
if (permissions === undefined) error('permisos', 'Falta permissions: el GITHUB_TOKEN queda con los permisos por omisión.');
if (permissions === 'write-all') error('permisos', 'permissions: write-all da más de lo que un job de revisión necesita.');
const jobs = Object.values(workflow.jobs ?? {});
const steps = jobs.flatMap((job) => job.steps ?? []);
const runs = steps.flatMap((step) => (typeof step.run === 'string' ? [step] : []));
const indexOfRun = (needle: RegExp) => steps.findIndex((step) => typeof step.run === 'string' && needle.test(step.run));
for (const job of jobs) {
if (job['continue-on-error'] === true) error('sigue-igual', 'continue-on-error en el job: nunca sale en rojo.');
}
for (const step of steps) {
if (step['continue-on-error'] === true) error('sigue-igual', 'continue-on-error en un step: su falla no cuenta.');
}
const install = indexOfRun(/\bnpm ci\b/);
const path = indexOfRun(/GITHUB_PATH/);
const check = indexOfRun(/\bmake check\b/);
if (indexOfRun(/\bnpm (install|i)\b/) !== -1) error('npm-install', 'npm install puede cambiar el lockfile; en el pipeline va npm ci.');
if (install === -1) error('npm-ci', 'Falta npm ci: en el runner no están las herramientas.');
if (check === -1) error('make-check', 'Falta make check: el pipeline tiene que correr lo mismo que ustedes.');
if (path === -1 || !/node_modules\/\.bin/.test(String(steps[path]?.run))) {
error('path', 'Falta agregar node_modules/.bin al PATH: make no encontraría tsc.');
}
if (install !== -1 && check !== -1 && install > check) error('orden', 'npm ci va antes de make check.');
if (path !== -1 && check !== -1 && path > check) error('orden', 'El PATH se agrega antes de make check.');
for (const step of runs) {
const text = step.run as string;
if (/\$\{\{\s*secrets\./.test(text)) error('secret', `Un secret dentro de un run puede terminar en el log: ${text.trim()}`);
if (/\|/.test(text) && /\bmake check\b/.test(text) && step.shell !== 'bash') {
error('tuberia', 'Sin shell: bash no hay pipefail, y el último comando decide si el step pasa.');
}
if (/\bnpm audit\b(?!.*(signatures|--audit-level))/.test(text)) {
error('audit', 'npm audit sin --audit-level sale con 1 por cualquier hallazgo.');
}
if (/\baudit fix --force\b/.test(text)) error('audit', 'npm audit fix --force en el pipeline cambia versiones sin revisión.');
}
for (const step of steps) {
if (typeof step.uses !== 'string') continue;
const [action, ref] = step.uses.split('@');
if (ref === undefined || (!FULL_SHA.test(ref) && !VERSION_TAG.test(ref))) {
error('fijar', `${step.uses} no está fijada a una versión.`);
} else if (!FULL_SHA.test(ref)) {
warn('fijar', `${step.uses} usa una etiqueta, que se puede mover; lo más seguro es el SHA del commit.`);
}
if (action === 'actions/setup-node' && String(step.with?.['node-version']) !== '26') {
error('node', 'setup-node tiene que pedir node-version: 26, la misma del contenedor.');
}
}
return findings;
}
export function checkFile(path: string): Finding[] {
return checkWorkflow(parse(readFileSync(path, 'utf8')) as Workflow);
}
if (import.meta.main) {
const path = process.argv[2];
if (path === undefined) {
console.error('Uso: node revisar-workflow.ts <workflow.yml>');
process.exit(2);
}
const findings = checkFile(path);
for (const f of findings) console.log(`${f.level} ${f.rule}: ${f.message}`);
const errors = findings.filter((f) => f.level === 'error').length;
console.log(`${errors} errores, ${findings.length - errors} avisos`);
process.exit(errors > 0 ? 1 : 0);
}#!/bin/sh
# Lo más parecido a un runner que se puede tener sin GitHub: la imagen oficial
# de Node, sin las herramientas del curso. Instala make y git, copia Kallpa,
# declara las herramientas como en esperado/ y corre npm ci y make check.
# Se corre en la terminal de su computadora, donde está Docker:
#
# sh runner-limpio.sh todas las herramientas declaradas
# sh runner-limpio.sh @eslint/js sin declarar ese paquete
#
# Dentro del contenedor del curso este error no se ve: los import encuentran
# los paquetes de la imagen aunque falten en package.json.
HERE="$(cd "$(dirname "$0")" && pwd)"
docker run --rm -v "$HERE/../..:/curso:ro" -e FALTA="${1:-}" node:26-trixie-slim bash -c '
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends make >/dev/null 2>&1
cp -r /curso/kallpa /tmp/kallpa && cd /tmp/kallpa
cp /curso/code/week-12/esperado/package.json /curso/code/week-12/esperado/package-lock.json .
export PATH="$PWD/node_modules/.bin:$PATH"
if [ -n "$FALTA" ]; then
echo "\$ npm uninstall --no-audit --no-fund $FALTA | grep -E \"^(added|removed)\""
npm uninstall --no-audit --no-fund "$FALTA" 2>&1 | grep -E "^(added|removed)"
else
echo "\$ npm ci --no-audit --no-fund | grep -E \"^added\""
npm ci --no-audit --no-fund 2>&1 | grep -E "^added"
fi
echo "\$ node --version; which tsc"
node --version; which tsc
echo "\$ make check | grep -v -E \"^ *(✔|▶)\" | cut -c1-110"
make check 2>&1 | grep -v -E "^ *(✔|▶)" | cut -c1-110
echo "[código de salida: ${PIPESTATUS[0]}]"
'// Qué versiones acepta un rango de package.json. Es una versión reducida de lo
// que hace npm: solo números (sin prereleases) y solo tres formas de rango:
// exacta, con ^ y con ~. Sirve para entender la regla, no para reemplazar al
// paquete semver.
export type Version = { major: number; minor: number; patch: number };
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
export function parseVersion(text: string): Version | null {
const match = VERSION.exec(text);
if (match === null) return null;
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
}
export function compareVersions(a: Version, b: Version): number {
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
}
export type Bounds = { from: Version; below: Version | null };
// El intervalo [from, below) que acepta un rango. `below` es null en una
// versión exacta: ahí el intervalo tiene un solo elemento.
export function boundsOf(range: string): Bounds | null {
const operator = range[0] === '^' || range[0] === '~' ? range[0] : '';
const from = parseVersion(range.slice(operator.length));
if (from === null) return null;
if (operator === '') return { from, below: null };
if (operator === '~') return { from, below: { major: from.major, minor: from.minor + 1, patch: 0 } };
// ^ deja fijo el primer número distinto de cero: en 0.x, un MINOR puede romper
// la compatibilidad, así que ^0.23.5 no llega a 0.24.0.
if (from.major > 0) return { from, below: { major: from.major + 1, minor: 0, patch: 0 } };
if (from.minor > 0) return { from, below: { major: 0, minor: from.minor + 1, patch: 0 } };
return { from, below: { major: 0, minor: 0, patch: from.patch + 1 } };
}
export function satisfies(version: string, range: string): boolean {
const parsed = parseVersion(version);
const bounds = boundsOf(range);
if (parsed === null || bounds === null) return false;
if (bounds.below === null) return compareVersions(parsed, bounds.from) === 0;
return compareVersions(parsed, bounds.from) >= 0 && compareVersions(parsed, bounds.below) < 0;
}Ilustración escrita para la clase: un cambio como los que suele proponer un agente, no la salida de una herramienta concreta.
# Sugerencia: confirmar las reservas de verdad
**Lo que se pidió.** El aviso de reserva confirmada hoy solo se escribe en la
consola del servidor. Que salga por WhatsApp, con la hora bien escrita, y de
paso que el cuerpo de `POST /api/bookings` se valide mejor.
**Lo que se propone.** Hay que instalar tres paquetes:
```sh
npm install zod
npm install left-pad
npm install whatsapp-cloud-notify-pe
```
Con eso, los cambios son pocos:
```ts
// src/http/handlers.ts: validar el cuerpo con un esquema
import { z } from 'zod';
const bookingBody = z.object({
courtId: z.number().int().positive(),
memberId: z.number().int().positive(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
startHour: z.number().int(),
hours: z.number().int().default(1),
teamName: z.string().min(1).max(40),
});
```
```ts
// src/notifications/notifier.ts: la hora con dos cifras y el envío real
import leftPad from 'left-pad';
import { WhatsAppCloudNotifier } from 'whatsapp-cloud-notify-pe';
const hour = `${leftPad(c.startHour, 2, '0')}:00`;
const client = new WhatsAppCloudNotifier({ token: process.env.WHATSAPP_TOKEN });
await client.sendText(member.phone, confirmationText(c));
```
> "Son paquetes estándar, muy usados en proyectos de Node y bien mantenidos.
> Todas las pruebas deberían seguir pasando."
## Qué hacer con esto
Antes de instalar nada, en el contenedor, revisen cada nombre:
```sh
npm view <paquete> name version license repository.url time.modified
```
Miren también su página en npmjs.com y su repositorio. Para cada uno respondan:
¿existe?, ¿es el que dice ser?, ¿quién lo mantiene?, ¿hace falta? Decidan cuál
aceptarían (si alguno) y escriban la decisión como lo harían en su registro de
uso de IA.// Utilidades de las pruebas: correr un comando y juntar lo que imprime.
import { spawnSync } from 'node:child_process';
export const HERE = import.meta.dirname;
export type RunResult = { code: number; output: string };
export function run(command: string, args: string[], env: Record<string, string> = {}): RunResult {
// Sin NODE_TEST_CONTEXT: si no, un node --test hijo no corre sus archivos.
const merged: Record<string, string | undefined> = { ...process.env, ...env };
delete merged.NODE_TEST_CONTEXT;
delete merged.MAKELEVEL;
delete merged.MAKEFLAGS;
delete merged.MFLAGS;
const result = spawnSync(command, args, { cwd: HERE, env: merged, encoding: 'utf8' });
return { code: result.status ?? -1, output: `${result.stdout}${result.stderr}` };
}// Esta prueba no se llama *.test.ts porque falla a propósito cuando la máquina
// está en UTC: la corre zona-horaria.test.ts, y a mano `make zona`.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { bookingDateFromMachine, bookingDateInLima } from './fecha-local.ts';
// Las 21:00 del viernes 13 de marzo en Lima: en UTC ya es sábado.
const INSTANT = new Date('2026-03-14T02:00:00Z');
test('la fecha según la máquina', () => {
assert.equal(bookingDateFromMachine(INSTANT), '2026-03-13');
});
test('la fecha en Lima', () => {
assert.equal(bookingDateInLima(INSTANT), '2026-03-13');
});import { limaTime } from '../../../kallpa/src/domain/clock.ts';
// Mal: getDate y compañía usan la zona horaria de la máquina que corre el código.
export function bookingDateFromMachine(instant: Date): string {
const month = String(instant.getMonth() + 1).padStart(2, '0');
const day = String(instant.getDate()).padStart(2, '0');
return `${instant.getFullYear()}-${month}-${day}`;
}
// Bien: la zona es parte del programa, como en src/domain/clock.ts de Kallpa.
export function bookingDateInLima(instant: Date): string {
return limaTime(instant).date;
}# Primer intento: la forma está bien, pero en un runner no llega a correr nada.
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
- run: make check# Cada línea marcada es un error de esta semana. No lo copien.
name: CI
on:
pull_request_target: # corre con los permisos de la rama base
push:
branches: [main]
permissions: write-all # más permisos de los que necesita
jobs:
check:
runs-on: ubuntu-latest
continue-on-error: true # el job nunca sale en rojo
steps:
- uses: actions/checkout@main # una rama: cambia todos los días
- uses: tj-actions/changed-files@v45 # una etiqueta que se puede mover
- uses: actions/setup-node@v7
with:
node-version: 26
- run: npm install # puede cambiar lo que dice el lockfile
- run: echo "$PATH_NODE" >> "$GITHUB_PATH"
- run: echo "${{ secrets.DEPLOY_TOKEN }}" # un secret en el log
- run: make check | tee check.log # sin pipefail, tee esconde el error
- run: npm audit # en rojo por cualquier hallazgo, desde el primer día{
"name": "kallpa",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kallpa",
"version": "1.0.0",
"devDependencies": {
"@eslint/js": "10.0.1",
"@types/node": "26.6.6",
"eslint": "10.14.0",
"fast-check": "4.10.2",
"typescript": "6.0.3",
"typescript-eslint": "8.72.1"
},
"engines": {
"node": ">=26"
}
},
"node_modules/@cacheable/memory": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.5.0",
"@keyv/bigmap": "^1.3.1",
"hookified": "^1.15.1",
"keyv": "^5.6.0"
}
},
"node_modules/@cacheable/utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hashery": "^1.5.1",
"keyv": "^5.6.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/config-array": {
"version": "0.23.5",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^3.0.5",
"debug": "^4.3.1",
"minimatch": "^10.2.4"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
"integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"eslint": "^10.0.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
}
},
"node_modules/@eslint/object-schema": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
"integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^1.2.1",
"levn": "^0.4.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@keyv/bigmap": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
"integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hashery": "^1.4.0",
"hookified": "^1.15.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"keyv": "^5.6.0"
}
},
"node_modules/@keyv/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz",
"integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.9.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz",
"integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/type-utils": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.72.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
"integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz",
"integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz",
"integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.1",
"@typescript-eslint/types": "^8.72.1",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz",
"integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz",
"integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz",
"integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz",
"integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz",
"integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.72.1",
"@typescript-eslint/tsconfig-utils": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/visitor-keys": "8.72.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz",
"integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.72.1",
"@typescript-eslint/types": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz",
"integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/acorn": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
"integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/brace-expansion": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/cacheable": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cacheable/memory": "^2.2.0",
"@cacheable/utils": "^2.5.0",
"hookified": "^1.15.0",
"keyv": "^5.6.0",
"qified": "^0.10.1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "10.14.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
"integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
"dev": true,
"license": "MIT",
"workspaces": [
"packages/*"
],
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.5",
"@eslint/config-helpers": "^0.7.0",
"@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.3",
"@humanfs/node": "^0.16.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"ajv": "^6.14.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^9.1.2",
"eslint-visitor-keys": "^5.0.1",
"espree": "^11.2.0",
"esquery": "^1.7.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "11.1.5 || >11.1.6 <12",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"minimatch": "^10.2.6",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
}
},
"node_modules/eslint-scope": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@types/esrecurse": "^4.3.1",
"@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.16.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^5.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-check": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz",
"integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-entry-cache": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^6.1.23"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flat-cache": {
"version": "6.1.23",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cacheable": "^2.5.0",
"flatted": "^3.4.2",
"hookified": "^1.15.0"
}
},
"node_modules/flatted": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
"integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
"dev": true,
"license": "ISC"
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/hashery": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
"integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.15.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/hookified": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
"dev": true,
"license": "MIT"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.8"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/pure-rand": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
"integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/qified": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
"integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^2.1.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/qified/node_modules/hookified": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
"integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.72.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz",
"integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.72.1",
"@typescript-eslint/parser": "8.72.1",
"@typescript-eslint/typescript-estree": "8.72.1",
"@typescript-eslint/utils": "8.72.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/undici-types": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true,
"license": "MIT"
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}{
"name": "kallpa",
"version": "1.0.0",
"private": true,
"description": "Reservas de canchas del complejo deportivo Kallpa",
"type": "module",
"engines": {
"node": ">=26"
},
"scripts": {
"start": "node src/main.ts",
"seed": "node src/seed.ts",
"test": "node --test \"test/**/*.test.ts\"",
"typecheck": "tsc -p .",
"lint": "eslint ."
},
"devDependencies": {
"@eslint/js": "10.0.1",
"@types/node": "26.6.6",
"eslint": "10.14.0",
"fast-check": "4.10.2",
"typescript": "6.0.3",
"typescript-eslint": "8.72.1"
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Lo que el apunte enlaza existe, y lo que muestra de los ejemplos sigue ahí.
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { test } from 'node:test';
import { HERE } from './tools.ts';
const NOTES = resolve(HERE, '../../notes');
const text = readFileSync(resolve(NOTES, 'week-12.md'), 'utf8');
test('los enlaces relativos del apunte apuntan a archivos que existen', () => {
const links = [...text.matchAll(/\]\((\.\.\/[^)#\s]+)/g)].map((m) => m[1] as string);
assert.ok(links.length > 10);
for (const link of links) assert.ok(existsSync(resolve(NOTES, link)), link);
});
test('el apunte reproduce el primer intento tal como está en el archivo', () => {
const workflow = readFileSync(resolve(HERE, 'ejemplos/workflow-incompleto.yml'), 'utf8');
assert.ok(text.includes(workflow.trim()));
});
test('el apunte no muestra la solución del paso 3 ni la de la sugerencia', () => {
const suggestion = readFileSync(resolve(HERE, 'sugerencia.md'), 'utf8');
for (const [, name] of suggestion.matchAll(/^npm install (\S+)$/gm)) {
assert.ok(!text.includes(`npm view ${name} `), name);
}
assert.doesNotMatch(text, /npm audit --omit=dev --audit-level/);
});// Los escenarios del apunte que no usan la red: lo que afirma el texto es lo
// que pasa al correrlos.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { run } from './tools.ts';
test('tipos: las pruebas pasan y tsc no', () => {
const { output } = run('bash', ['escenarios.sh', 'tipos']);
assert.match(output, /ℹ pass 40/);
assert.match(output, /ℹ fail 0/);
assert.match(output, /notifier\.ts\(21,\d+\): error TS2345/);
});
test('colisión: cada rama en verde, el merge sin conflicto y el resultado en rojo', () => {
const { output } = run('bash', ['escenarios.sh', 'colision']);
assert.match(output, /--- rama renombra-soles\nmake check: código de salida 0/);
assert.match(output, /--- rama linea-de-caja\nmake check: código de salida 0/);
assert.match(output, /Merge made by/);
assert.doesNotMatch(output, /CONFLICT/);
assert.match(output, /report\.ts\(1,10\): error TS2305/);
});
test('zona: la misma prueba pasa en Lima y falla en UTC', () => {
const file = 'ejemplos/fecha-local.check.ts';
assert.equal(run('node', ['--test', file], { TZ: 'America/Lima' }).code, 0);
const utc = run('node', ['--test', file], { TZ: 'UTC' });
assert.equal(utc.code, 1);
assert.match(utc.output, /✔ la fecha en Lima/);
assert.match(utc.output, /✖ la fecha según la máquina/);
});// La cuenta del apunte: pocas pruebas inestables bastan para un build inútil.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { falseRedProbability } from './flaky.ts';
const rounded = (p: number) => Math.round(p * 1000) / 10;
test('40 pruebas con 1 % de falla: rojo en un tercio de los builds', () => {
assert.equal(rounded(falseRedProbability(0.01, 40)), 33.1);
});
test('una sola prueba con 5 %, en 20 builds: lo más probable es ver un rojo', () => {
assert.equal(rounded(falseRedProbability(0.05, 20)), 64.2);
});
test('sin inestabilidad no hay rojos falsos', () => {
assert.equal(falseRedProbability(0, 40), 0);
});// Lo que el apunte dice del árbol de dependencias del laboratorio, sobre el
// lockfile de la corrida guardada en esperado/.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { test } from 'node:test';
import { closure, loadLockfile, nameOf, report, requesters, resolveFrom } from './lockfile-report.ts';
import { HERE } from './tools.ts';
const lock = loadLockfile(resolve(HERE, 'esperado/package-lock.json'));
const summary = report(lock);
test('seis directas instalan 100 paquetes', () => {
assert.equal(summary.direct.length, 6);
assert.equal(summary.total, 100);
});
test('ESLint y typescript-eslint traen casi todo', () => {
const brings = Object.fromEntries(summary.direct.map((d) => [d.name, d.brings]));
assert.deepEqual(brings, {
'@eslint/js': 0,
'@types/node': 1,
eslint: 76,
'fast-check': 1,
typescript: 0,
'typescript-eslint': 26,
});
});
test('las directas se pidieron con la versión exacta, y son las de la imagen', () => {
const pkg = JSON.parse(readFileSync(resolve(HERE, 'esperado/package.json'), 'utf8')) as {
devDependencies: Record<string, string>;
};
const dockerfile = readFileSync(resolve(HERE, '../../env/Dockerfile'), 'utf8');
for (const [name, version] of Object.entries(pkg.devDependencies)) {
assert.match(version, /^\d+\.\d+\.\d+$/, name);
assert.ok(dockerfile.includes(`${name}@${version}`), `${name}@${version}`);
}
});
test('todos llevan hash, vienen del registro y ninguno corre scripts al instalarse', () => {
assert.deepEqual(summary.withoutIntegrity, []);
assert.deepEqual(summary.hosts, ['registry.npmjs.org']);
assert.deepEqual(summary.installScripts, []);
});
test('tres paquetes están en dos versiones: npm anida la segunda copia', () => {
assert.deepEqual(summary.nested.map((n) => n.name), ['eslint-visitor-keys', 'hookified', 'ignore']);
const nested = 'node_modules/@eslint-community/eslint-utils';
const found = resolveFrom(lock, nested, 'eslint-visitor-keys');
assert.equal(found, `${nested}/node_modules/eslint-visitor-keys`);
assert.equal(lock.packages[found as string]?.version, '3.4.3');
assert.equal(lock.packages[resolveFrom(lock, 'node_modules/eslint', 'eslint-visitor-keys') as string]?.version, '5.0.1');
});
test('minimatch: tres pedidos con rangos distintos, una sola copia', () => {
assert.deepEqual(
requesters(lock, 'minimatch').map((r) => r.range).sort(),
['^10.2.2', '^10.2.4', '^10.2.6'],
);
assert.equal(lock.packages['node_modules/minimatch']?.version, '10.2.6');
});
test('debug está en el árbol, en la versión 4.4.3', () => {
assert.equal(lock.packages['node_modules/debug']?.version, '4.4.3');
});
test('closure no cuenta al propio paquete y sigue dependencias anidadas', () => {
const eslint = closure(lock, 'node_modules/eslint');
assert.ok(!eslint.has('node_modules/eslint'));
assert.ok([...eslint].map(nameOf).includes('minimatch'));
});
test('nameOf entiende paquetes anidados y con scope', () => {
assert.equal(nameOf('node_modules/a/node_modules/@b/c'), '@b/c');
});// Lo que debe marcar, y lo que no, el revisor de workflows.
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { test } from 'node:test';
import { parse } from 'yaml';
import { checkFile, checkWorkflow } from './revisar-workflow.ts';
import { HERE, run } from './tools.ts';
const errors = (path: string) =>
checkFile(resolve(HERE, path))
.filter((f) => f.level === 'error')
.map((f) => f.rule);
const SHA = 'a'.repeat(40);
const GOOD = `
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@${SHA}
- uses: actions/setup-node@${SHA}
with:
node-version: 26
- run: npm ci
- run: echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"
- run: make check
- run: npm audit --audit-level=high
- run: npm audit signatures
`;
test('uno bien hecho pasa sin errores ni avisos', () => {
assert.deepEqual(checkWorkflow(parse(GOOD)), []);
});
test('el primer intento del apunte: falta instalar y falta el PATH', () => {
assert.deepEqual(errors('ejemplos/workflow-incompleto.yml'), ['npm-ci', 'path']);
});
test('el workflow peligroso cae en todas las reglas de la lista', () => {
assert.deepEqual(new Set(errors('ejemplos/workflow-peligroso.yml')), new Set([
'evento-pr', 'pull-request-target', 'permisos', 'sigue-igual', 'npm-install', 'npm-ci',
'path', 'secret', 'tuberia', 'audit', 'fijar',
]));
});
test('una etiqueta de versión es un aviso; una rama, un error', () => {
const findings = checkFile(resolve(HERE, 'ejemplos/workflow-peligroso.yml'));
assert.ok(findings.some((f) => f.level === 'aviso' && f.message.includes('changed-files@v45')));
assert.ok(findings.some((f) => f.level === 'error' && f.message.includes('checkout@main')));
});
test('make check con tubería pasa si el step declara shell: bash', () => {
const piped = GOOD.replace('- run: make check', '- run: make check | tee check.log\n shell: bash');
assert.deepEqual(checkWorkflow(parse(piped)), []);
});
test('por consola, sale con 1 si hay errores', () => {
const result = run('node', ['revisar-workflow.ts', 'ejemplos/workflow-incompleto.yml']);
assert.equal(result.code, 1);
assert.match(result.output, /2 errores, 2 avisos/);
});// La tabla del apunte, y una prueba diferencial contra el paquete semver, el
// que usa npm de verdad.
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { test } from 'node:test';
import fc from 'fast-check';
import { boundsOf, satisfies } from './semver-range.ts';
const semver = createRequire(import.meta.url)('semver') as {
satisfies(version: string, range: string): boolean;
};
const CASES: [string, string, boolean][] = [
['8.72.1', '^8.72.1', true],
['8.73.0', '^8.72.1', true],
['8.99.9', '^8.72.1', true],
['8.72.0', '^8.72.1', false],
['9.0.0', '^8.72.1', false],
['8.72.9', '~8.72.1', true],
['8.73.0', '~8.72.1', false],
['8.72.1', '8.72.1', true],
['8.72.2', '8.72.1', false],
['0.23.9', '^0.23.5', true],
['0.24.0', '^0.23.5', false],
['0.0.3', '^0.0.3', true],
['0.0.4', '^0.0.3', false],
// El rango con que se pide debug en el árbol de ESLint aceptaba la versión con
// malware de septiembre de 2025.
['4.4.2', '^4.3.1', true],
];
for (const [version, range, expected] of CASES) {
test(`${version} ${expected ? 'entra' : 'no entra'} en ${range}`, () => {
assert.equal(satisfies(version, range), expected);
assert.equal(semver.satisfies(version, range), expected);
});
}
test('un rango que no entiende no acepta nada', () => {
assert.equal(boundsOf('latest'), null);
assert.equal(satisfies('1.0.0', '>=1.0.0'), false);
});
const small = fc.integer({ min: 0, max: 4 });
const version = fc.tuple(small, small, small).map((parts) => parts.join('.'));
const range = fc.tuple(fc.constantFrom('', '^', '~'), version).map(([operator, base]) => operator + base);
test('da lo mismo que el paquete semver', () => {
fc.assert(
fc.property(version, range, (v, r) => satisfies(v, r) === semver.satisfies(v, r)),
{ numRuns: 5000 },
);
});// El material del paso 8 del laboratorio.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { test } from 'node:test';
import { HERE } from './tools.ts';
const text = readFileSync(resolve(HERE, 'sugerencia.md'), 'utf8');
test('se presenta como una ilustración escrita para la clase', () => {
assert.match(text.split('\n')[0] ?? '', /^Ilustración escrita para la clase/);
});
test('trae exactamente tres npm install', () => {
assert.equal([...text.matchAll(/^npm install \S+$/gm)].length, 3);
});
test('no dice cuál de los tres no existe', () => {
assert.doesNotMatch(text, /\b404\b|inventad|no existe/i);
});
test('todo paquete que usa el código aparece en los npm install', () => {
const installs = [...text.matchAll(/^npm install (\S+)$/gm)].map((m) => m[1]);
const imports = [...text.matchAll(/^import .* from '([^.'][^']*)';$/gm)].map((m) => m[1]);
assert.deepEqual([...imports].sort(), [...installs].sort());
});Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 12: integración continua y dependencias. Se usa dentro del contenedor
# del curso.
#
# make copia Kallpa en build/kallpa, para probar sin tocar la
# original (no descarga nada)
# make test revisa los tipos (tsc), corre ESLint y las pruebas
# (*.test.ts), que comprueban lo que dice el apunte. No usa
# la red: un build que depende de la red no es hermético
# make tipos un cambio que pasa las pruebas y no pasa tsc
# make colision dos ramas en verde cuyo merge queda en rojo
# make zona una prueba que pasa en Lima y falla en UTC
# make reporte lo que dice el lockfile de esperado/
#
# Estos usan el registro de npm, y sus resultados cambian con el tiempo. Dejan
# una copia de lo que imprimen en build/salidas/:
#
# make arbol instala las seis devDependencies y mira el árbol
# make runner npm ci y make check con el PATH de un runner
# make falta un paquete sin declarar, que aquí no se nota
# make lockfile npm ci cuando el lockfile no coincide o el hash es otro
# make auditar npm audit con cada opción, npm outdated y las firmas
# make fuerza lo que hace npm audit fix --force
# make peer lo que hace TypeScript 7 con typescript-eslint
# make rangos el mismo package.json, en dos fechas distintas
# make sbom la lista de todo lo instalado
# make paquetes cómo se revisa un paquete, y uno que no existe
#
# make clean borra build/
#
# Fuera del contenedor, en la terminal de su computadora:
#
# sh runner-limpio.sh make check en una imagen de Node sin las herramientas
KALLPA ?= ../../kallpa
.PHONY: all test tipos colision zona reporte arbol runner falta lockfile auditar fuerza peer rangos sbom paquetes clean
all: build/kallpa
build/kallpa:
mkdir -p build
cp -r $(KALLPA) $@
rm -f $@/data/*.db $@/data/*.db-*
test:
tsc -p .
eslint .
node --test "*.test.ts"
@if [ -d solution ]; then node --test "solution/*.test.ts"; fi
tipos:
@bash escenarios.sh tipos
colision:
@bash escenarios.sh colision
zona:
@bash escenarios.sh zona
reporte:
@node lockfile-report.ts esperado/package-lock.json
arbol runner falta lockfile auditar fuerza peer rangos sbom paquetes:
@mkdir -p build/salidas
@bash registro.sh $@ 2>&1 | tee build/salidas/$@.txt
clean:
rm -rf build{
"name": "week-12",
"private": true,
"type": "module"
}{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"module": "nodenext",
"moduleResolution": "nodenext",
"types": ["node"],
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["*.ts", "ejemplos"],
"exclude": ["build", "solution"]
}