CreaRack-SL

Tool Dependencias — arquitectura, operación y troubleshooting

Para qué sirve esta página

Documenta cómo está construida y cómo se opera la tool Dependencias (/tools/deps): el chequeo de frescura de dependencias de CreaRack-Pro y CreaRackSL-workspace. Versión para usuario (qué hace, cómo leerla): [[feature—workspace—tool-dependencias]].

La tool no sustituye a Dependabot ni a pip-audit. Es un mirador unificado de frescura + seguridad (¿hay versión nueva, de qué tamaño es el salto, y tiene la versión en uso alguna vulnerabilidad conocida?) de los dos repos a la vez, en el sitio donde el equipo ya mira. Desde el 11-07-2026 el eje de CVEs lo cubre la propia tool vía OSV.dev (antes solo pip-audit sobre el lockfile del Local Agent).

Arquitectura (3 piezas)

GitHub Actions (CreaRack-Pro)            Cloudflare (Workspace)
┌─────────────────────────────┐         ┌──────────────────────────────┐
│ deps-freshness.yml (cron L)  │  POST   │ /api/tools/deps  (Pages Fn)   │
│  └─ scripts/deps_freshness.py│ ──────► │  └─ D1: deps_freshness        │
│     + deps_specs.py          │ Bearer  │     (DELETE + INSERT batch)   │
│     + deps_sources.py        │         └──────────────┬───────────────┘
│     + deps_holds.py          │                        │ GET
│     • parsea manifiestos     │         ┌──────────────▼───────────────┐
│     • resuelve horquillas    │         │ /tools/deps (Astro + React)   │
│     • aplica retenciones     │         │  DepsPanel.tsx → tabla        │
│     • "instalada": PROD →    │         └───────────────────────────────┘
│        runner → lockfile     │
│     • consulta PyPI / npm    │
│     • calcula desfase semver │
│     • cruza con OSV.dev (CVE)│
└─────────────────────────────┘
        │ GET + Bearer (INTERNAL_TOOLS_TOKEN)
        ▼
 https://crearack.com/api/installed-packages   (Django · PROD)

Es el mismo patrón que ai-eval / dashboard: colector en CI → endpoint Pages Function → D1 → React island en Astro.

Nota (28-08-2026): los campos hold_* (ver «Retenciones deliberadas» abajo) ya viajan en el POST /api/tools/deps, tienen columna D1 dedicada (migración 0053) y se pintan en la UI — badge ámbar, tarjeta y pestaña propias. El desplegable «¿Cómo leo esta tabla?» vive ahora en DepsExplainer.tsx, separado de DepsPanel.tsx (Regla 5: el panel queda en 464 líneas).

Archivos

RepoArchivoRol
CreaRack-Proscripts/deps_freshness.pyColector · CLI (cross-platform, stdlib pura)
CreaRack-Proscripts/deps_specs.pyResolución de horquillas: PEP 440 (PyPI) y caret, tilde, rangos y alternativas (npm) → mayor versión publicada que el spec admite; además has_upper_bound y major_key
CreaRack-Proscripts/deps_sources.pyDe dónde salen los datos: dataclass Dep, parsers de requirements.txt / package.json y lectores de “instalada” (pip list, package-lock.json, pnpm list)
CreaRack-Proscripts/deps_holds.pyRetenciones deliberadas: carga y valida deps_holds.json, marca las filas retenidas a propósito
CreaRack-Prodeps_holds.jsonCatálogo de retenciones vigentes (raíz del repo) — no genera build, se lee en runtime del colector
CreaRack-Protests/scripts/test_deps_freshness.pyCasos sin red: resolución de horquillas, sin-techo, fix_within_spec, dedup, sonda a PROD, retenciones
CreaRack-Proconfig/urls.py → GET /api/installed-packagesEndpoint interno (Bearer INTERNAL_TOOLS_TOKEN) que devuelve las versiones Python REALES del contenedor de PROD
CreaRack-Pro.github/workflows/deps-freshness.ymlCron semanal + dispatch + push a manifiestos, a deps_holds.json o a alguno de los 4 scripts/deps_*.py
Workspacemigrations/0042_create_deps_freshness.sqlTabla D1 deps_freshness
Workspacemigrations/0048_deps_freshness_vulns.sqlColumnas del eje de seguridad (vuln_count, vuln_ids, vuln_severity)
Workspacemigrations/0052_deps_freshness_motor_fiable.sqlSeis columnas del motor fiable (manifests, resolved, installed_source, unbounded, unbounded_major, fix_within_spec)
Workspacemigrations/0053_deps_freshness_holds.sqlCuatro columnas de retenciones (hold_reason, hold_since, hold_ref, hold_until)
Workspacefunctions/api/tools/deps/index.tsAPI: GET (lee) / POST (reemplaza snapshot)
Workspacesrc/pages/tools/deps/index.astroPágina
Workspacesrc/components/tools/DepsPanel.tsxTabla + filtros + tarjetas de resumen
Workspacesrc/components/tools/DepsBadges.tsxPiezas de fila: LagCell (atraso o «Retenida a propósito»), InstalledCell (versión en uso + de dónde sale), UnboundedBadge (sin techo), FixScopeBadge (basta redeploy / cambiar pin), ManifestsNote (en N manifiestos)
Workspacesrc/components/tools/DepsExplainer.tsxDesplegable «¿Cómo leo esta tabla?», extraído de DepsPanel.tsx (Regla 5)
Workspacesrc/components/tools/DepInfoModal.tsxFicha de paquete (modal): registro en vivo + catálogo + bloque «Por qué no se actualiza» si está retenida
Workspacesrc/components/tools/depsShared.tsTipos y etiquetas compartidos (panel ↔ modal), incluidas isHeld/holdTitle
Workspacesrc/components/tools/depsCatalog.tsCatálogo curado: descripción en español de cada paquete
Workspacesrc/components/tools/ToolsHub.tsxCard en el hub /tools
Workspacesrc/styles/tools.cssEstilos .deps-*

Retenciones deliberadas (deps_holds.json, desde v1.86.10)

Una dependencia parada A PROPÓSITO (redis en 7.4.1 porque la serie 8.x corta el channel layer en vivo, Django <6.1 porque django-prometheus lo exige, ruff/mypy con pin exacto hasta auditar sus reglas nuevas) no es lo mismo que una dependencia olvidada — pero hasta este cambio salía en rojo igual que cualquier atrasada.

deps_holds.json (raíz de CreaRack-Pro) declara el catálogo. Cada entrada: package, ecosystem (pypi|npm), repo (crearack-pro|workspace), reason (llano), since (YYYY-MM-DD), ref (task/issue/PR) y opcionalmente until (condición o fecha para revisarla). scripts/deps_holds.py la carga (load), la valida (campos obligatorios, ecosystem/repo en catálogo, since con formato de fecha, sin duplicados) y la cruza contra las filas del colector (apply), rellenando hold_reason/hold_since/hold_ref/hold_until en el Dep que casa por (repo, ecosystem, paquete normalizado).

  • Fichero ausente → 0 retenciones, sin ruido (falla suave).
  • Fichero mal escrito → HoldsError, que el colector convierte en ::error:: y exit 2 — a propósito duro: un hold que no se aplicase pasaría desapercibido.
  • Hold sin dependencia que case (paquete borrado o mal escrito en deps_holds.json) → ::warning:: en el log, no rompe el run.
  • En el resumen: una fila retenida con desfase major/minor no suma en esos contadores del resumen (lag_counts); se cuenta aparte como «retenidas a propósito».

Retenciones vigentes (28-08-2026), todas task #275: redis 7.4.1 (task #204 · redis-py #2807 abierta), Django <6.1 (django-prometheus 2.5.0), ruff y mypy (pin exacto hasta septiembre 2026, con Dani y Txell).

En la UI (desde 28-08-2026): una fila retenida ya no se pinta en rojo/naranja — el distintivo de atraso pasa a ámbar apagado con el texto «Retenida a propósito» (DepsBadges.tsx → LagCell), y el tooltip trae el motivo más el «desde», la referencia y el «revisar» (holdTitle() en depsShared.ts). Los contadores «Muy atrasadas»/«Atrasadas», la frase-resumen de arriba y la cifra de la tarjeta de Dependencias en /tools excluyen las retenidas: tienen su propia tarjeta «Retenidas a propósito» (toggle-filtro) y su propia pestaña Retenidas junto a «Pendientes»/«Todas». La ficha del paquete (DepInfoModal.tsx) repite el mismo detalle, más ordenado, en el bloque «Por qué no se actualiza».

De dónde sale cada dato

  • Pineada (columna «Pedida» en la UI): el spec crudo del manifiesto (==6.0.4, ^9.3.0, >=2.10.0,<3…). La UI muestra la versión limpia y deja el spec en el tooltip.

  • Última: API JSON oficial — PyPI https://pypi.org/pypi/<pkg>/json (info.version) y npm https://registry.npmjs.org/<pkg> (dist-tags.latest). Fuente real, nunca memoria del modelo.

  • Resuelta (campo resolved, desde 28-08-2026): la mayor versión publicada que la horquilla admite — la que de verdad se instalaría hoy en un despliegue limpio. La calcula deps_specs.resolve() contra la lista completa de versiones del registro, con PEP 440 (packaging si está disponible, respaldo propio si no) para PyPI y con caret/tilde/rangos/|| para npm. Descarta prereleases salvo que no quede ninguna final que encaje. Specs opacos (workspace:, file:, git+…) no se resuelven.

    Por qué existe: antes se evaluaba la horquilla por su mínimo (requests>=2.31.0 → «2.31.0»). Eso inventaba avisos de seguridad que no existían en ninguna instalación real — 30 falsos positivos el 28-08-2026 (22 de aiohttp, 6 de requests, 2 de pytest).

  • Instalada (columna «En uso»), en orden de preferencia y con la fuente guardada en installed_source:

    ÁmbitoManifiestoFuente de “instalada”installed_source
    Backend Pythonrequirements.txtPROD real: GET https://crearack.com/api/installed-packages (Bearer INTERNAL_TOOLS_TOKEN) → importlib.metadata.distributions() del contenedorprod
    Backend Python (respaldo)requirements.txtpip install -r + pip list --format=json del runner de CIrunner
    Local Agentterminal/agent/requirements-agent.lock= pineada (lockfile 100% ==)lockfile
    Teststests/requirements.txt · tests/parity_validation/requirements.txtsin instalada (no se instalan en este job; is_dev=1) → se usa la resuelta—
    Frontendfrontend/package.jsonfrontend/package-lock.json (leído del repo)lockfile
    Workspacepackage.jsonpnpm install + pnpm list --depth 0 --json (en CI)runner

    El colector no ejecuta pip/pnpm: el workflow genera los JSON y se los pasa por flag (--backend-installed, --workspace-installed), y la sonda a PROD va por --prod-versions-url + --prod-token. Falla suave: si PROD no contesta o devuelve vacío, se imprime un ::warning:: y la columna cae al runner; el run no se rompe.

  • Desfase (columna «Atraso»): comparación semver de (instalada || resuelta) vs última → major / minor / patch / uptodate / unknown. Tolera calver (2026.2.20) y sufijos (-preview, rc). Desde v1.86.10, una fila retenida a propósito (ver arriba) sigue calculando su desfase real, pero no suma en los contadores major/minor del resumen.

  • Sin techo (unbounded / unbounded_major, desde 28-08-2026): unbounded = la horquilla no pone límite superior (>=X en PyPI, >=X/* en npm; <, <=, ==, ~=, ^ sí lo ponen). unbounded_major = además la versión que corre ya está en un major distinto al declarado en el manifiesto — bajo 0.x el minor cuenta como major (los SDK de IA rompen igual de un 0.52 a un 0.122). No es una alarma de seguridad: es aviso de descontrol, porque cada despliegue puede traerse un salto grande que nadie decidió.

  • En N manifiestos (manifests): tras deduplicar por (repo, ecosistema, paquete), la fila conserva todos los manifiestos donde aparece el paquete, separados por coma. Gana la horquilla más restrictiva (la de menor versión resuelta; a igualdad, la que pone techo) y esa queda en manifest; la “instalada” se hereda de cualquiera de las filas del grupo que la tenga. is_dev solo se mantiene si todas lo eran.

  • Seguridad (desde 11-07-2026): POST https://api.osv.dev/v1/querybatch (lotes de 100, ecosistemas PyPI/npm) con la versión (instalada || resuelta) — nunca el mínimo del spec; sin versión resoluble no se consulta (OSV devolvería avisos de cualquier versión). El detalle de cada aviso sale de GET /v1/vulns/{id}: database_specific.severity da la severidad (GHSA: CRITICAL/HIGH/MODERATE/LOW; los PYSEC suelen venir sin nota → unknown) y los eventos fixed de affected[].ranges dan fix_within_spec. Falla suave: si OSV no responde, vuln_count=0. Campos: vuln_count, vuln_ids (coma-separados), vuln_severity (la peor). Sin API key.

  • fix_within_spec (desde 28-08-2026), solo en filas con aviso: true = alguna versión con fix cabe dentro de la horquilla ya declarada → basta un redeploy (la UI dice «basta redeploy»). false = el fix queda fuera → hay que cambiar el pin en el manifiesto. null = OSV no publica versión con fix para ese aviso. Se exige que todos los avisos de la fila tengan fix dentro del spec para dar true.

  • hold_reason / hold_since / hold_ref / hold_until: no nulo = fila retenida a propósito. Columnas D1 dedicadas desde la migración 0053 (28-08-2026); antes viajaban en el JSON del POST sin columna propia. Ver «Retenciones deliberadas» arriba.

La ficha de paquete (modal, desde 11-07-2026)

Al pinchar el nombre de un paquete, DepInfoModal.tsx abre una ficha con dos fuentes:

  • Catálogo curado (depsCatalog.ts): descripción en español, en llano, de cada paquete de los manifiestos vigilados (~100 entradas + fallbacks por prefijo para familias @codemirror/*, @fontsource/*, @radix-ui/*, @types/*). Mantenimiento: al añadir una dependencia nueva a un manifiesto, añadir su línea aquí; si falta, el modal cae a la descripción oficial del registro (inglés) — no rompe nada.
  • Registro oficial en vivo (fetch desde el navegador del staff, con CORS abierto en ambos): PyPI https://pypi.org/pypi/<pkg>/<version>/json (con la versión latest del snapshot el JSON es mucho más ligero; da license_expression/classifiers, project_urls, fecha de publicación) y npm https://registry.npmjs.org/<pkg-urlencoded>/latest (description, license, homepage, repository). Decisión de diseño: no se persiste nada en D1 ni pasa por el colector — datos siempre frescos, cero migraciones; si el registro no responde, el modal lo dice y muestra solo los datos locales (falla suave). Cache en memoria por sesión de navegador.

Si el paquete está retenido, la ficha añade el bloque «Por qué no se actualiza» con hold_reason/hold_since/hold_ref/hold_until.

Las tarjetas del resumen son además filtros toggle de la lista (estado cardFilter, que tiene prioridad sobre el seg «Pendientes/Todas/Retenidas»); la tarjeta «Total vigiladas» limpia filtros.

Operación

Lanzar a mano (forzar comprobación)

gh workflow run deps-freshness.yml -R CreaRackSL/CreaRack-Pro

Añadir o quitar una retención

Editar deps_holds.json (raíz de CreaRack-Pro) y añadir/borrar la entrada correspondiente — no requiere tocar código ni migraciones. Borrar la entrada en cuanto se suba la dependencia (el propio fichero lo recuerda en su campo _doc).

Probar en local (sin publicar)

python scripts/deps_freshness.py --dry-run

Imprime la tabla por consola (con las columnas RESUELTA, la fuente de la instalada y las notas «sin techo» / «en N manifiestos» / «retenida»). Frontend y Local Agent traen “instalada”; backend y workspace saldrán ”—” salvo que pases --backend-installed pip-list.json / --workspace-installed pnpm-list.json. Para ver la instalada REAL de producción:

python scripts/deps_freshness.py --dry-run `
  --prod-versions-url https://crearack.com/api/installed-packages `
  --prod-token $env:INTERNAL_TOOLS_TOKEN

Secrets (repo CreaRack-Pro → Settings → Secrets → Actions)

SecretUso¿Obligatorio?
MCP_TOKENBearer del Workspace (mismo que push-drift.yml)Sí
CF_ACCESS_CLIENT_ID / ..._SECRETService token CF AccessOpcional
WORKSPACE_REPO_TOKENPAT read-only a CreaRackSL-workspaceOpcional*
INTERNAL_TOOLS_TOKENBearer de GET https://crearack.com/api/installed-packages — mismo valor que el env var de Dokploy en PRODOpcional**

*Sin WORKSPACE_REPO_TOKEN el run cubre solo CreaRack-Pro (el checkout del Workspace se salta sin romper el job). Para cubrir también el Workspace hay que añadir el PAT. Ver [[runbook—platform-credentials-map]].

**Sin INTERNAL_TOOLS_TOKEN la columna «En uso» del backend sale de lo que resuelve el runner del CI (installed_source = runner), no de lo que corre en producción. El workflow tiene un paso previo Probe PROD installed packages (continue-on-error) que hace el GET y deja el código HTTP en el log: 200 = la instalada vendrá de PROD. El endpoint de Django exige un token de ≥32 caracteres; por debajo de eso deniega todo, aunque el valor coincida.

Estado 28-08-2026: el secret está pendiente de crear en el repo CreaRack-Pro. Hasta entonces las pasadas usan el respaldo del runner. Dueño: Edu.

Migración D1

wrangler d1 execute crearacksl-workspace-db --remote --file=./migrations/0042_create_deps_freshness.sql
wrangler d1 execute crearacksl-workspace-db --remote --file=./migrations/0048_deps_freshness_vulns.sql
wrangler d1 execute crearacksl-workspace-db --remote --file=./migrations/0052_deps_freshness_motor_fiable.sql
wrangler d1 execute crearacksl-workspace-db --remote --file=./migrations/0053_deps_freshness_holds.sql

(El POST /api/tools/deps falla hasta que la tabla existe; el GET devolverá vacío y la tool mostrará “Sin datos todavía”.)

La 0052 (aplicada el 28-08-2026) añade las seis columnas del motor fiable. Es un ALTER TABLE ADD COLUMN por campo, y va antes de que corra el colector nuevo: sin ellas el INSERT del POST revienta. installed_source no lleva CHECK a propósito — D1 no lo valida de forma fiable en un ADD COLUMN y el enum ya se filtra en el handler.

La 0053 (aplicada el 28-08-2026) añade las cuatro columnas de retenciones — hold_reason, hold_since, hold_ref, hold_until, todas TEXT nulables. Mismo patrón: ALTER TABLE ADD COLUMN no idempotente, una sola pasada, antes de que el colector nuevo publique. Con esto, los campos hold_* dejan de depender solo del JSON del POST y tienen columna propia en deps_freshness.

Contrato del endpoint

  • GET /api/tools/deps → { checked_at, count, items: [...] }, ordenado por: con vulnerabilidad primero (peor severidad arriba), luego desfase (major→uptodate). Auth: CF Access (navegador) vía _middleware.ts.
  • POST /api/tools/deps → body { items: [...] }. Hace DELETE + INSERT batch (snapshot completo, no histórico). Auth: Bearer (MCP_TOKENS) vía _middleware.ts. Devuelve { inserted, checked_at }. Tope de 5000 filas por snapshot (el real ronda unos cientos). Cada fila lleva, además de los campos originales, los seis del motor fiable: manifests, resolved, installed_source (prod / runner / lockfile; cualquier otro valor → NULL), unbounded, unbounded_major y fix_within_spec. Este último es tri-estado: solo un booleano explícito se guarda como 0/1; ausente o basura → NULL («OSV no publica fix»), que es distinto de 0 («cambiar pin»). Desde la migración 0053 (28-08-2026) cada fila lleva también hold_reason/hold_since/hold_ref/hold_until (no nulo = retenida; hold_since se descarta si no es YYYY-MM-DD) en columnas D1 propias — un payload viejo sin ellos entra con los cuatro a NULL.

Troubleshooting

  • La tabla sale vacía / “Sin datos”: ¿se aplicaron las migraciones 0042, 0048, 0052 y 0053? ¿ha corrido el workflow al menos una vez? Mira el último run en Actions.
  • Ninguna fila dice PROD en «En uso»: falta el secret INTERNAL_TOOLS_TOKEN en CreaRack-Pro, o el valor no coincide con el env var de Dokploy en PROD, o tiene menos de 32 caracteres (el endpoint deniega por debajo de ese largo). Diagnóstico rápido: el paso «Probe PROD installed packages» del run imprime el código HTTP; 401 = token malo, 200 = bien. En la salida del colector, la línea de resumen dice 'instalada' desde PROD=N.
  • Un paquete sale «sin techo» y no debería: el spec del manifiesto no pone límite superior. Se arregla en el manifiesto (añadir <X), no en la tool. Los ^/~ de npm y los ~=/==/< de PyPI sí cuentan como techo.
  • Un paquete aparece una sola vez estando en varios manifiestos: es el dedup por (repo, ecosistema, paquete). La fila muestra la horquilla más restrictiva y lista el resto en «en N manifiestos» (tooltip).
  • Falta el Workspace en la tabla: falta WORKSPACE_REPO_TOKEN, o el pnpm install falló (paso continue-on-error, revisa el log).
  • Un paquete sale “unknown”: el registro no devolvió versión (404, nombre raro, o red caída en ese momento). Reintenta en el siguiente run.
  • major falso en calver: las versiones tipo 2026.x se comparan numéricamente; un salto de año cuenta como major. Es esperado.
  • HTTP 200 pero no publica: el colector valida el body (inserted) y devuelve exit ≠ 0 si la respuesta no cuadra (Regla 15) → el job sale en rojo.
  • El modal no muestra la descripción curada: verificar que el nombre exacto (lowercase) está en DESCRIPTIONS de depsCatalog.ts, o que la familia tiene fallback en PREFIX_DESCRIPTIONS (prefijo con la barra: @radix-ui/). Si falta, el modal cae a la descripción oficial del registro.
  • El job sale en rojo con ::error:: sobre deps_holds.json (desde v1.86.10): el fichero está mal formado — falta un campo obligatorio, ecosystem/repo fuera de catálogo, since no es YYYY-MM-DD, o una retención duplicada para el mismo (repo, ecosystem, package). El mensaje de HoldsError dice exactamente cuál.
  • ::warning::retención sin dependencia en el log: el hold de deps_holds.json no casa con ninguna fila del colector — el paquete se borró del manifiesto o el nombre está mal escrito en el fichero.
  • El POST empieza a fallar con error de columna (desde la migración 0053): revisar que hold_reason/hold_since/hold_ref/hold_until existen en deps_freshness — la migración va antes del primer POST que los incluya.

Extensiones previstas

  • Segundo eje CVE: cruzar con OSV para marcar lo que además tiene vulnerabilidad conocida. HECHO 11-07-2026 (columna Seguridad, OSV.dev).
  • Ficha por paquete (qué es, licencia, repo). HECHO 11-07-2026 (modal + catálogo curado).
  • Frontend de retenciones: pintar hold_reason/hold_ref en DepsPanel.tsx/DepsBadges.tsx como badge «Retenida a propósito» en vez de rojo/naranja. HECHO 28-08-2026 (badge ámbar vía LagCell, tarjeta y pestaña «Retenidas», bloque «Por qué no se actualiza» en la ficha del paquete).
  • Botón “ver cambios” por fila → Context7 para la guía de migración de la versión nueva (aquí Context7 sí aplica: trae docs, no números de versión).
  • Señal a Pulse/alertas si algo está N majors atrás o con CVE.

Véase también

  • [[feature—workspace—tool-dependencias]]
  • [[entity—functions—endpoint—tools-deps]]
  • [[entity—migrations—table—deps-freshness]]
  • [[runbook—platform-credentials-map]]
  • [[entity—ci—service—deps-freshness-collector]]