Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0052dfa1da |
No files matched your search
@@ -0,0 +1,191 @@
|
|||||||
|
# Agente de diagnóstico/corrección (lab-agent)
|
||||||
|
|
||||||
|
Wrapper headless (`scripts/lab-agent/lab-agent.sh`) que corre `claude -p`
|
||||||
|
periódicamente para diagnosticar el estado del lab y, solo para un
|
||||||
|
conjunto explícito y aprobado de recursos, ejecutar una acción
|
||||||
|
correctiva de bajo riesgo. Este documento es tanto referencia técnica
|
||||||
|
como decisión de gobernanza — la lista blanca de abajo es lo que
|
||||||
|
autoriza al agente a actuar sin intervención humana, y debe poder
|
||||||
|
auditarse.
|
||||||
|
|
||||||
|
Estado: **script y prueba end-to-end validados en vivo (2026-08-26)**.
|
||||||
|
El timer de systemd (automatización continua) **todavía no está
|
||||||
|
activo** — pendiente de decisión explícita para habilitarlo.
|
||||||
|
|
||||||
|
## Lista blanca de acciones correctivas automáticas
|
||||||
|
|
||||||
|
Aprobada 2026-08-25, antes de escribir una sola línea de código.
|
||||||
|
|
||||||
|
### 1. `kubectl rollout restart deployment/<nombre>`
|
||||||
|
|
||||||
|
| Condición | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Trigger | Deployment con `readyReplicas < replicas` o condición `Available=False` durante **≥10 minutos consecutivos** |
|
||||||
|
| Namespace elegible | `ecommerce` (todos los deployments detectados ahí en el momento de la corrida) |
|
||||||
|
| Excepción | `medusa-deploy` — el deployment real de "commerce-backend" en la terminología de `CLAUDE.md` (confirmado en vivo 2026-08-25: no existe un deployment llamado literalmente `commerce-backend`) — solo elegible si además la hora **local** del host está entre **00:00–06:00** |
|
||||||
|
| Namespaces excluidos siempre | `argocd`, `kube-system`, cualquier otro namespace de plataforma |
|
||||||
|
|
||||||
|
### 2. `docker restart <contenedor>`
|
||||||
|
|
||||||
|
| Condición | Valor |
|
||||||
|
|---|---|
|
||||||
|
| Contenedores elegibles | **Únicamente `gitea-runner`** |
|
||||||
|
| Trigger | Contenedor no está en estado `running` durante ≥10 min, **o** la API de Gitea Actions reporta un run en estado `running` atascado por más de 10 min sin terminar (señal del incidente de runner zombie del 2026-08-16) |
|
||||||
|
| Degradación segura | Si no hay `GITEA_TOKEN_FILE` configurado o la API falla, esa señal se omite — nunca dispara una acción sobre una lectura no verificada, solo queda la señal de "contenedor caído" |
|
||||||
|
| Excluidos siempre | `gitea`, `nginxproxymanager`, `cloudflared` |
|
||||||
|
|
||||||
|
### 3. Fuera de alcance automático — SIEMPRE (solo diagnóstico + notificación)
|
||||||
|
|
||||||
|
- Borrar cualquier recurso, escalar réplicas, tocar `ConfigMap`/`Secret`/configuración.
|
||||||
|
- Reiniciar `gitea`, `nginxproxymanager`, `cloudflared`.
|
||||||
|
- Cualquier acción sobre `medusa-deploy` fuera de la ventana 00:00–06:00.
|
||||||
|
- Cualquier namespace/deployment no detectado explícitamente en `ecommerce`.
|
||||||
|
|
||||||
|
### 4. Límites obligatorios
|
||||||
|
|
||||||
|
- **Rate limit:** máx. 1 acción correctiva por recurso cada 30 min.
|
||||||
|
- **Sin reintento silencioso:** tras un primer intento cuyo problema no
|
||||||
|
se resuelve, el recurso queda `blocked` — solo notificación, hasta
|
||||||
|
que un humano limpie la entrada en `state.json` o el recurso se
|
||||||
|
recupere solo.
|
||||||
|
- **Logging completo:** cada decisión (actúe o no) se registra en
|
||||||
|
`actions.jsonl` con timestamp, recurso, decisión, resultado y motivo.
|
||||||
|
- **Notificación siempre:** toda acción correctiva o anomalía dispara
|
||||||
|
notificación — nunca es silenciosa.
|
||||||
|
|
||||||
|
## Diseño de seguridad: por qué Claude nunca ejecuta comandos
|
||||||
|
|
||||||
|
`claude -p` se invoca con `--allowedTools ""` (cero herramientas) y
|
||||||
|
`--json-schema`, así que el modelo **nunca** ejecuta un comando —
|
||||||
|
solo devuelve una decisión estructurada
|
||||||
|
(`{"actions":[{"resource","decision","reason"}], "summary"}`). El
|
||||||
|
script bash es el único que ejecuta `kubectl`/`docker`, y solo después
|
||||||
|
de revalidar la decisión contra su propio cálculo de elegibilidad —
|
||||||
|
nunca confía en el JSON de Claude por sí solo.
|
||||||
|
|
||||||
|
Se descartó dar a Claude un `Bash` tool con un patrón tipo
|
||||||
|
`Bash(kubectl rollout restart deployment/medusa-deploy -n ecommerce)`
|
||||||
|
porque se comprobó en vivo (2026-08-25) que `--allowedTools` hace
|
||||||
|
**coincidencia por prefijo, no exacta**: un patrón `Bash(echo pintest)`
|
||||||
|
también permite ejecutar `echo pintest && echo INJECTED` o
|
||||||
|
`echo pintest; rm -rf /`. Para un comando root-capaz eso no es un
|
||||||
|
límite seguro. Con `--allowedTools ""`, el peor caso posible es que
|
||||||
|
Claude proponga una decisión incorrecta en el JSON — y el wrapper la
|
||||||
|
rechaza si el recurso no está en su propia lista de elegibles.
|
||||||
|
|
||||||
|
El prompt también advierte contra prompt injection: los datos de
|
||||||
|
diagnóstico (estado de pods, logs) podrían contener texto arbitrario
|
||||||
|
si algo en el cluster está comprometido, e instruye a Claude a ignorar
|
||||||
|
cualquier instrucción incrustada ahí.
|
||||||
|
|
||||||
|
## Autenticación: login de suscripción, no API key
|
||||||
|
|
||||||
|
`ANTHROPIC_API_KEY_FILE` queda vacío por defecto — `claude -p` usa el
|
||||||
|
login de suscripción ya activo del usuario que corre el script
|
||||||
|
(`~/.claude/.credentials.json`), el mismo que usa una sesión
|
||||||
|
interactiva normal. Esta decisión se tomó en vivo (2026-08-26) al
|
||||||
|
confirmar que no había una API key real disponible; el diseño original
|
||||||
|
pedía un secreto root-only para la API key, pero se simplificó porque:
|
||||||
|
|
||||||
|
1. No hacía falta una API key aparte — la suscripción ya cubre esto.
|
||||||
|
2. Elimina la necesidad de crear secretos root-only vía `sudo` (que
|
||||||
|
además está bloqueado para una sesión de Claude Code interactiva
|
||||||
|
por diseño — ver `feedback_scoped_root_access_pattern` en memoria
|
||||||
|
del proyecto).
|
||||||
|
|
||||||
|
**Consecuencia directa:** el servicio systemd (cuando se active) debe
|
||||||
|
correr como usuario `devops`, no como `root` — root no tiene acceso a
|
||||||
|
ese login. `CLAUDE_BIN`/`KUBECTL_BIN` apuntan por ruta absoluta a
|
||||||
|
`/DATA/AppData/home-devops/.local/bin/` porque ese directorio no está
|
||||||
|
en el `PATH` de root ni de systemd (confirmado en vivo: `sudo which
|
||||||
|
claude` no lo encuentra).
|
||||||
|
|
||||||
|
El único secreto que queda es `GITEA_TOKEN_FILE` (token de Gitea
|
||||||
|
Actions, opcional, scope lectura), dueño `devops`, `600` — sin sudo.
|
||||||
|
|
||||||
|
## Prueba end-to-end (2026-08-26)
|
||||||
|
|
||||||
|
Ejecutada contra el cluster real, rama `fix/lab-agent-diagnostico-headless`.
|
||||||
|
Umbral bajado a 2 minutos solo para esta prueba (`UNHEALTHY_THRESHOLD_MIN`,
|
||||||
|
valor real en producción: 10). Canario desechable: `deployment/test-agent-canary`
|
||||||
|
en `ecommerce`, imagen `busybox:stable` con `command: ["sh","-c","exit 1"]`
|
||||||
|
— garantiza `CrashLoopBackOff` predecible, sin relación con
|
||||||
|
`catalog-api-deploy`/`frontend-deploy`/`medusa-deploy`.
|
||||||
|
|
||||||
|
### Bugs encontrados y corregidos durante la prueba
|
||||||
|
|
||||||
|
1. **Subshell perdía el resultado de la acción.** `execute_corrective_action`
|
||||||
|
se invocaba como `result="$(execute_corrective_action "$resource")"` —
|
||||||
|
al correr en un subshell, las variables `LAST_ACTION_CMD`/`STDOUT`/`STDERR`
|
||||||
|
nunca volvían al script principal → crash con `unbound variable`.
|
||||||
|
Corregido: la función ahora devuelve éxito/fallo por exit status, invocada
|
||||||
|
directamente (sin `$(...)`) para que sus variables globales persistan.
|
||||||
|
2. **DRY_RUN contaminaba el estado real.** El wrapper marcaba `blocked=true`
|
||||||
|
en `state.json` incluso en modo simulación, lo que habría bloqueado
|
||||||
|
incorrectamente una corrida real posterior sobre el mismo recurso.
|
||||||
|
Corregido: la actualización de `state.json` (bloqueo/rate-limit) solo
|
||||||
|
ocurre fuera de `DRY_RUN`.
|
||||||
|
3. **`claude`/`kubectl` fuera del PATH de root/systemd** — ver sección de
|
||||||
|
autenticación arriba. Corregido con `CLAUDE_BIN`/`KUBECTL_BIN` por ruta
|
||||||
|
absoluta.
|
||||||
|
|
||||||
|
### Evidencia por paso
|
||||||
|
|
||||||
|
**DRY_RUN (elegibilidad + decisión, sin tocar el cluster):**
|
||||||
|
|
||||||
|
- 1ª corrida (recién detectado): `elegibilidad=not_eligible:bajo_umbral(0m<2m)`
|
||||||
|
→ Claude devolvió `notify_only` para los 5 recursos.
|
||||||
|
- 2ª corrida (>2 min después): `elegibilidad=eligible` para
|
||||||
|
`test-agent-canary` → Claude devolvió `decision:"restart"` solo para
|
||||||
|
ese recurso, `notify_only` para los otros 4 (sanos).
|
||||||
|
- Log: `ACCIÓN EJECUTADA: kubectl -n ecommerce rollout restart
|
||||||
|
deployment/test-agent-canary -> OK`, pero verificado en el cluster real
|
||||||
|
que **no hubo restart** (sin anotación `restartedAt`, mismo pod, misma
|
||||||
|
edad). `actions.jsonl` registra `"dry_run": true`.
|
||||||
|
|
||||||
|
**Ejecución real (`--live`):**
|
||||||
|
|
||||||
|
- Misma decisión de Claude (`restart` para el canario, `notify_only` para
|
||||||
|
el resto).
|
||||||
|
- Verificado en el cluster: nueva anotación
|
||||||
|
`kubectl.kubernetes.io/restartedAt` en el deployment, evento
|
||||||
|
`ScalingReplicaSet` de un ReplicaSet **nuevo**, pod nuevo en otro nodo.
|
||||||
|
- `actions.jsonl`: `"result": "success", "dry_run": false`.
|
||||||
|
- `state.json`: `last_action_at` seteado, `blocked: true`.
|
||||||
|
|
||||||
|
**Rate limit / sin reintento silencioso:**
|
||||||
|
|
||||||
|
- El canario (siempre crashea por diseño) volvió a estar unhealthy sin
|
||||||
|
intervención extra.
|
||||||
|
- Corrida siguiente: `elegibilidad=not_eligible:blocked_tras_intento_previo_sin_recuperacion`.
|
||||||
|
- Claude devolvió `notify_only` para **todos** los recursos, incluido el
|
||||||
|
canario — ningún segundo restart propuesto ni ejecutado.
|
||||||
|
- `state.json` sin cambios (mismo `last_action_at` que la corrida real
|
||||||
|
anterior) — confirma que no hubo reintento.
|
||||||
|
- Notificación real entregada vía ntfy.sh, confirmada leyendo el propio
|
||||||
|
historial del topic (no solo el log del script).
|
||||||
|
|
||||||
|
**Limpieza:** `deployment/test-agent-canary` eliminado, entrada
|
||||||
|
correspondiente en `state.json` borrada. Los logs de la prueba
|
||||||
|
(`actions.jsonl`, `run-*.log`) se conservan como evidencia auditable —
|
||||||
|
no se purgaron.
|
||||||
|
|
||||||
|
## Notificaciones
|
||||||
|
|
||||||
|
Canal: [ntfy](https://ntfy.sh) — sin sistema de notificación previo en
|
||||||
|
el lab, se eligió la opción sin infraestructura nueva (tópico público
|
||||||
|
de ntfy.sh, nombre privado difícil de adivinar) para validar la prueba
|
||||||
|
end-to-end sin bloquear en instalar y exponer un contenedor propio.
|
||||||
|
Migrar a un ntfy self-hosted (mismo patrón que gitea/nginxproxymanager/
|
||||||
|
cloudflared) es un cambio de una sola variable (`NTFY_URL`) cuando se
|
||||||
|
decida.
|
||||||
|
|
||||||
|
## Pendiente antes de activar el timer en continuo
|
||||||
|
|
||||||
|
- **Confirmación explícita del propietario del lab** para habilitar el
|
||||||
|
timer de systemd (Paso 4 original) — no se activa automáticamente
|
||||||
|
solo por haber pasado la prueba end-to-end.
|
||||||
|
- Unidad systemd corriendo como `devops` (no root), con límites de
|
||||||
|
CPU/memoria.
|
||||||
|
- Opcional: `GITEA_TOKEN_FILE` para activar la señal de "runner
|
||||||
|
atascado" vía API de Gitea Actions (hoy se omite, degradación segura).
|
||||||
@@ -109,10 +109,7 @@ Un motor de búsqueda dedicado (Meilisearch, autohospedado) queda
|
|||||||
identificado como el siguiente paso natural una vez el catálogo
|
identificado como el siguiente paso natural una vez el catálogo
|
||||||
crezca más allá de ~100 productos activos, pero no es parte del
|
crezca más allá de ~100 productos activos, pero no es parte del
|
||||||
alcance inmediato de Etapa 2 — se revisará cuando ese techo esté
|
alcance inmediato de Etapa 2 — se revisará cuando ese techo esté
|
||||||
cerca. **Actualización 2026-08-25:** terminada la Etapa 2 (ver
|
cerca.
|
||||||
[ADR 0005](0005-rediseno-storefront.md)), el disparador real resultó
|
|
||||||
ser la complejidad de facetas de la taxonomía, no el volumen de
|
|
||||||
productos — ver [ADR 0006](0006-motor-busqueda-facetas.md).
|
|
||||||
|
|
||||||
**Compatibilidad con Medusa Admin confirmada:** ninguna de las tres
|
**Compatibilidad con Medusa Admin confirmada:** ninguna de las tres
|
||||||
opciones evaluadas, incluida la elegida, toca
|
opciones evaluadas, incluida la elegida, toca
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
# ADR 0006 — Motor de búsqueda y filtros facetados (Fase 5, Etapa 3)
|
|
||||||
|
|
||||||
**Estado:** propuesta — pendiente de aprobación antes de Etapa 2 (implementación)
|
|
||||||
**Fecha:** 2026-08-25
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
[ADR 0004](0004-evaluacion-stack-multi-categoria.md) ya identificó un
|
|
||||||
motor de búsqueda dedicado (Meilisearch, autohospedado) como "el
|
|
||||||
siguiente paso natural" para ARI Shopping, pero lo dejó fuera del
|
|
||||||
alcance de Etapa 2 explícitamente hasta que el catálogo se acercara al
|
|
||||||
techo real de ~100 productos activos que impone
|
|
||||||
`fetchProducts(filters, limit = 100)` en `medusa.ts`.
|
|
||||||
|
|
||||||
**Ese techo todavía no llegó** — el catálogo real hoy son 26 productos
|
|
||||||
(17 legacy + 9 demo, ver [ADR 0005](0005-rediseno-storefront.md)
|
|
||||||
sección 5), lejos de 100. El disparador de esta propuesta no es el
|
|
||||||
techo de volumen sino la **complejidad de facetas**: la Etapa 2 ya
|
|
||||||
construyó una taxonomía real de 6 categorías raíz / 21 nodos
|
|
||||||
(navegación, mega-menú, breadcrumb — PRs #36-42) y un filtrado con
|
|
||||||
conteo por valor (`categoryCounts`/`brandCounts`/`ageCounts` en
|
|
||||||
`catalog-client.tsx`, PR #40) que hoy se calcula **100% client-side**
|
|
||||||
sobre los productos ya traídos — el mismo patrón de "deuda de
|
|
||||||
alcance" que el hallazgo #4 de ADR 0004 ya señaló, ahora con más
|
|
||||||
dimensiones de filtro que multiplicar. Sigue siendo tratable a 26
|
|
||||||
productos; deja de serlo mucho antes de llegar a 100 si el catálogo
|
|
||||||
crece a la vez en número de productos y en dimensiones filtrables.
|
|
||||||
|
|
||||||
Restricción no negociable, igual que en toda la Fase 5: el flujo
|
|
||||||
**Admin → evento `product.updated` → `revalidate-storefront.ts` →
|
|
||||||
`revalidateTag("medusa-products")`**, que hoy mantiene el storefront
|
|
||||||
sincronizado sin intervención manual, no se toca. Cualquier índice de
|
|
||||||
búsqueda debe sincronizarse por el mismo mecanismo de eventos, nunca
|
|
||||||
por un cron ni un paso manual.
|
|
||||||
|
|
||||||
## 1. Estado real de la integración Medusa v2 + Meilisearch
|
|
||||||
|
|
||||||
Verificado contra la documentación oficial vigente el 2026-08-25, no
|
|
||||||
contra memoria de entrenamiento:
|
|
||||||
|
|
||||||
- **Medusa no tiene un módulo de búsqueda propio integrado con
|
|
||||||
Meilisearch hoy.** La guía oficial
|
|
||||||
(`docs.medusajs.com/resources/integrations/guides/meilisearch`) es
|
|
||||||
un tutorial de módulo custom desde cero (`npm install meilisearch` +
|
|
||||||
workflows/subscribers propios) — no un paquete instalable.
|
|
||||||
- **Medusa sí introdujo un "Search Module" propio en la versión
|
|
||||||
2.19.0.** El paquete comunitario `@rokmohar/medusa-plugin-meilisearch`
|
|
||||||
(el más usado y mantenido para Meilisearch+Medusa) se conecta a ese
|
|
||||||
Search Module desde su versión `^2.0.0`, pero esa versión del plugin
|
|
||||||
**requiere Medusa `^2.19.0`**.
|
|
||||||
- **Nuestro backend corre Medusa `2.18.0`** (`commerce-backend/package.json`)
|
|
||||||
— justo por debajo de ese corte. La tabla de compatibilidad del
|
|
||||||
plugin (`github.com/rokmohar/medusa-plugin-meilisearch`) resuelve
|
|
||||||
esto: la línea `^1.4.1` soporta `Medusa >= 2.15 < 2.19`, servidor
|
|
||||||
Meilisearch `>= 1.5`, Node `>= 22`. El propio repo lo explica: *"The
|
|
||||||
2.19 release removed the search interface the v1 line was written
|
|
||||||
against, so the two lines do not overlap: v1 stops at Medusa 2.18,
|
|
||||||
v2 starts at 2.19."*
|
|
||||||
- Node de `commerce-backend` ya es `22.18.0` (Dockerfile) — cumple el
|
|
||||||
mínimo del plugin sin cambios.
|
|
||||||
|
|
||||||
**Implicación concreta:** para adoptar Meilisearch sin subir de
|
|
||||||
versión Medusa (fuera de alcance de este ADR — sería su propia
|
|
||||||
migración), la pieza a instalar es `@rokmohar/medusa-plugin-meilisearch@^1.4.1`,
|
|
||||||
no la última versión del plugin. Esto es deuda técnica documentada,
|
|
||||||
no un obstáculo: si en el futuro se sube Medusa a `2.19+`, migrar a la
|
|
||||||
línea `^2.0.0` del plugin (y al Search Module nativo) es el camino
|
|
||||||
recto.
|
|
||||||
|
|
||||||
## 2. Impacto de recursos
|
|
||||||
|
|
||||||
Estado real del host ZimaOS al momento de esta auditoría (`free -h`,
|
|
||||||
`docker stats`): 15 GiB RAM totales, **swap al 100% (5.2/5.2 GiB
|
|
||||||
usados)**, solo 351 MiB realmente libres (6.5 GiB "available" vía
|
|
||||||
cache reclamable), 4 vCPU — la misma clase de contención que ya forzó
|
|
||||||
el fix de `cpu_shares` de Gitea. Cualquier servicio nuevo en este host
|
|
||||||
es una decisión de capacidad real, no teórica.
|
|
||||||
|
|
||||||
- **Meilisearch no publica un mínimo de RAM oficial.** Su propio FAQ
|
|
||||||
(`meilisearch.com/docs/resources/help/faq`) solo dice que "más RAM
|
|
||||||
implica búsquedas más rápidas" y recomienda ~10x el tamaño del
|
|
||||||
dataset crudo en disco — sin piso duro. La cifra de "~1 GiB
|
|
||||||
cómodo" que circula en blogs de la comunidad es una estimación, no
|
|
||||||
una garantía del proveedor — se cita aquí como referencia, no como
|
|
||||||
hecho verificado.
|
|
||||||
- Meilisearch usa almacenamiento memory-mapped: el índice puede vivir
|
|
||||||
parcialmente en disco cuando no entra en RAM, degradando latencia en
|
|
||||||
vez de fallar. A la escala actual (26 productos, dataset de KB) el
|
|
||||||
uso real de RAM del índice en sí es insignificante — el costo es el
|
|
||||||
proceso base de Meilisearch, no el dataset.
|
|
||||||
- **Comparación con Typesense** (pedida explícitamente, mantenida
|
|
||||||
breve porque la diferencia relevante para este caso es clara y no
|
|
||||||
requiere más profundidad): Typesense mantiene el índice completo en
|
|
||||||
RAM (~30% más RAM que Meilisearch para el mismo dataset, según
|
|
||||||
benchmarks públicos) y, a diferencia de Meilisearch, **no tiene
|
|
||||||
ningún plugin ni integración con Medusa** — habría que escribir la
|
|
||||||
sincronización completa desde cero igual que con la guía "custom"
|
|
||||||
de Meilisearch, pero sin ninguna base comunitaria mantenida
|
|
||||||
compatible con nuestra versión de Medusa. Con Meilisearch ya
|
|
||||||
teniendo un plugin activo para `Medusa 2.18` y menor huella de RAM,
|
|
||||||
no hay motivo para evaluar Typesense más a fondo en este caso.
|
|
||||||
- **Recomendación:** desplegar Meilisearch en k3d con `requests`/`limits`
|
|
||||||
explícitos y conservadores desde el día uno (ej. `request: 256Mi/0.1
|
|
||||||
CPU`, `limit: 768Mi/0.5 CPU` — a ajustar con datos reales de uso, no
|
|
||||||
supuestos), y tratarlo como el mismo tipo de riesgo operativo que ya
|
|
||||||
se vivió con Gitea: monitorear consumo real post-despliegue antes de
|
|
||||||
darlo por estable. Dado el swap ya saturado, este es el punto de
|
|
||||||
mayor incertidumbre de toda la propuesta — más que la elección de
|
|
||||||
motor de búsqueda en sí.
|
|
||||||
|
|
||||||
## 3. Modelo de indexación
|
|
||||||
|
|
||||||
Basado en la taxonomía real ya viva en Medusa (Etapa 2) y en
|
|
||||||
`Product`/`Category` de `lib/headless/types.ts`:
|
|
||||||
|
|
||||||
| Campo | Buscable (texto, tolerante a typos) | Filtrable / faceta | Ordenable |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `name` | Sí | No | No |
|
|
||||||
| `shortDescription` / `description` | Sí | No | No |
|
|
||||||
| `category` (nombre de la hoja real) | Sí | Sí (con conteo) | No |
|
|
||||||
| `collection` | Sí | Sí (con conteo) | No |
|
|
||||||
| `brand` | Sí | Sí (con conteo) | No |
|
|
||||||
| `ageRange` | No | Sí (con conteo) | No |
|
|
||||||
| `tags` | Sí | No | No |
|
|
||||||
| `price` | No | Sí (rango) | Sí |
|
|
||||||
| `compareAtPrice` | No | No | No (solo se usa para el badge, no como filtro) |
|
|
||||||
| `isNew` / `featured` | No | No | Sí (orden "Novedades"/"Destacados", igual que hoy) |
|
|
||||||
| `available` | No | Sí (booleano) | No |
|
|
||||||
| `slug`, `thumbnail`, `id` | No | No | No (solo se devuelven, no se indexan como facetas) |
|
|
||||||
|
|
||||||
La jerarquía de categorías (raíz/hija) no se indexa como un campo
|
|
||||||
aparte: el índice guarda el nombre de la hoja real tal como hoy lo
|
|
||||||
hace `product.category`, y el breadcrumb/mega-menú siguen resolviendo
|
|
||||||
la jerarquía contra `/store/product-categories` (sin cambios, ya
|
|
||||||
funciona desde la sub-fase 2). El motor de búsqueda resuelve
|
|
||||||
**búsqueda de texto + conteo de facetas**, no reemplaza la fuente de
|
|
||||||
la jerarquía.
|
|
||||||
|
|
||||||
## 4. Diseño de sincronización
|
|
||||||
|
|
||||||
Mismo patrón que `revalidate-storefront.ts`, no uno nuevo: un
|
|
||||||
subscriber adicional en `commerce-backend/src/subscribers/` (ej.
|
|
||||||
`sync-search-index.ts`) que reacciona a eventos y le pide al plugin
|
|
||||||
que reindexe el documento afectado — nunca Medusa escribiendo
|
|
||||||
directamente en Meilisearch fuera de este canal.
|
|
||||||
|
|
||||||
Eventos a escuchar, confirmados en la documentación oficial de
|
|
||||||
eventos de Medusa (`docs.medusajs.com/resources/events-reference`):
|
|
||||||
|
|
||||||
- `product.created` / `product.updated` / `product.deleted`
|
|
||||||
- `product-variant.created` / `product-variant.updated` / `product-variant.deleted`
|
|
||||||
(mismo set que ya usa `revalidate-storefront.ts`, hoy validado en
|
|
||||||
producción)
|
|
||||||
- **Nuevo respecto al subscriber actual:** `product-category.created`
|
|
||||||
/ `product-category.updated` / `product-category.deleted` — un
|
|
||||||
cambio de nombre o jerarquía de categoría debe refrescar las
|
|
||||||
facetas del índice, y hoy `revalidate-storefront.ts` no escucha
|
|
||||||
estos eventos porque el storefront los resuelve en cada request
|
|
||||||
contra `/store/product-categories` directamente, sin necesidad de
|
|
||||||
revalidación. El índice de búsqueda sí necesita enterarse.
|
|
||||||
|
|
||||||
**Punto a verificar en vivo en Etapa 2, no asumido aquí:** existe al
|
|
||||||
menos un reporte público de un evento `product-collection.updated`
|
|
||||||
que no se disparaba de forma confiable en ciertas versiones de Medusa
|
|
||||||
(`github.com/medusajs/medusa/issues/11197`). Antes de dar por cerrado
|
|
||||||
el diseño de sincronización hay que repetir exactamente la validación
|
|
||||||
que ya se hizo para `revalidate-storefront.ts` en la auditoría de
|
|
||||||
Etapa 1 de Fase 5: crear/editar una categoría real vía Admin y
|
|
||||||
confirmar en vivo que el evento llega al subscriber, no confiar en que
|
|
||||||
la documentación coincide con el comportamiento real de `2.18.0`
|
|
||||||
(el mismo tipo de brecha que ya se encontró una vez con la relación
|
|
||||||
`+categories` de `/store/products`).
|
|
||||||
|
|
||||||
## 5. Diseño de UI — filtros facetados y búsqueda
|
|
||||||
|
|
||||||
**Filtros facetados (sidebar de `/catalog`):** mismo layout que ya
|
|
||||||
construyó la sub-fase 3 (`catalog-client.tsx`, PR #40) — sidebar con
|
|
||||||
"Categoría"/"Marca"/"Edad" como pills con conteo entre paréntesis y
|
|
||||||
slider de precio máximo. Lo único que cambia es **de dónde sale el
|
|
||||||
conteo**: hoy `categoryCounts`/`brandCounts`/`ageCounts` se calculan
|
|
||||||
recorriendo el arreglo de productos ya traído en el cliente; con el
|
|
||||||
motor de búsqueda, esos conteos vienen del propio Meilisearch
|
|
||||||
(`facetDistribution`) en la misma respuesta que trae los productos
|
|
||||||
filtrados, sin recorrer nada en el navegador. El breadcrumb no cambia
|
|
||||||
— sigue resolviéndose contra la jerarquía real de categorías, no
|
|
||||||
contra el índice de búsqueda.
|
|
||||||
|
|
||||||
```
|
|
||||||
Inicio > Mochilas y morrales > Marvel breadcrumb real (sin cambios)
|
|
||||||
┌────────────┬──────────────────────────────────────┐
|
|
||||||
│ Categoría │ [buscador con autocompletado] │ ← nuevo
|
|
||||||
│ ☐ Marvel(6)│ [card][card][card] │
|
|
||||||
│ ☐ Frozen(4)│ [card][card][card] │
|
|
||||||
│ Marca │ │
|
|
||||||
│ ☐ Disney(2)│ conteos ahora vienen de Meilisearch, │
|
|
||||||
│ Precio │ no de recorrer el arreglo en cliente │
|
|
||||||
│ [====○===] │ │
|
|
||||||
└────────────┴──────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Búsqueda con autocompletado (barra del navbar):** hoy
|
|
||||||
`PredictiveSearch` (`components/search/predictive-search.tsx`) filtra
|
|
||||||
por substring sobre los productos ya cargados en memoria del cliente —
|
|
||||||
funciona a 26 productos, no escala y no tolera errores de tipeo (ADR
|
|
||||||
0004, hallazgo #5). Se reemplaza la fuente de datos del mismo
|
|
||||||
componente visual por `searchProducts(query)` del `CatalogProvider`
|
|
||||||
(interfaz que **ya existe** en `lib/headless/types.ts`, sin necesidad
|
|
||||||
de un tipo nuevo) apuntando a Meilisearch en vez de al `?q=` de
|
|
||||||
Medusa: tolerancia a typos y ranking por relevancia nativos del motor,
|
|
||||||
mismo contrato de datos que ya consume el dropdown de resultados —
|
|
||||||
cero cambio de componente React, solo de qué hay detrás de
|
|
||||||
`CatalogProvider.searchProducts`.
|
|
||||||
|
|
||||||
## Opciones consideradas
|
|
||||||
|
|
||||||
| Opción | A favor | En contra |
|
|
||||||
|---|---|---|
|
|
||||||
| **Meilisearch autohospedado** (`@rokmohar/medusa-plugin-meilisearch@^1.4.1`, sincronizado por eventos) | Plugin comunitario activo y compatible con nuestra versión exacta de Medusa (2.18); menor huella de RAM que la alternativa evaluada; almacenamiento memory-mapped se degrada en vez de fallar bajo presión de memoria; mismo patrón de subscriber que `revalidate-storefront.ts`, cero riesgo para ese flujo | Ningún paquete "oficial" de Medusa (el plugin es comunitario, no del core); atado a la línea `v1` del plugin mientras no se suba Medusa a 2.19+; un servicio más para operar en un host ya con swap saturado |
|
|
||||||
| **Typesense autohospedado** | Autocompletado ~2x más rápido según benchmarks públicos | Cero integración con Medusa (habría que escribir la sincronización completa desde cero, igual esfuerzo que la guía "custom" de Meilisearch pero sin base comunitaria); ~30% más RAM que Meilisearch para el mismo dataset, el peor trade-off posible dado el estado actual del host |
|
|
||||||
| **Módulo custom desde cero** (seguir la guía oficial de Medusa, sin el plugin de rokmohar) | Sin dependencia de un paquete de terceros; control total | Reimplementa lo que el plugin comunitario ya resuelve (declaración de índices, reindexado atómico, tracking de escrituras); más superficie propia para mantener sin ganancia clara sobre usar el plugin ya compatible con 2.18 |
|
|
||||||
| **No adoptar motor de búsqueda todavía** (seguir con filtrado 100% client-side) | Cero riesgo, cero componente nuevo | No resuelve la deuda de facetas ya identificada en ADR 0004 y agravada por la sub-fase 3; el techo de complejidad de filtros llega antes que el de 100 productos si el catálogo sigue creciendo en categorías |
|
|
||||||
|
|
||||||
## Decisión
|
|
||||||
|
|
||||||
**Meilisearch autohospedado**, vía `@rokmohar/medusa-plugin-meilisearch@^1.4.1`
|
|
||||||
(no la última versión del plugin — esa exige Medusa 2.19+, fuera de
|
|
||||||
alcance), sincronizado por el mismo patrón de subscriber por eventos
|
|
||||||
que ya usa `revalidate-storefront.ts`, con `product-category.*` sumado
|
|
||||||
al set de eventos existente. Se despliega en k3d con límites de
|
|
||||||
recursos conservadores desde el primer día y se trata como
|
|
||||||
experimento a monitorear, no como capacidad garantizada, dado el
|
|
||||||
estado actual de memoria del host.
|
|
||||||
|
|
||||||
**No se sube la versión de Medusa como parte de esta decisión** —
|
|
||||||
queda como deuda técnica documentada para cuando se justifique
|
|
||||||
migrar a la línea `v2` del plugin y al Search Module nativo de
|
|
||||||
Medusa.
|
|
||||||
|
|
||||||
## Consecuencias
|
|
||||||
|
|
||||||
- Se gana: conteo de facetas real (no client-side), búsqueda tolerante
|
|
||||||
a errores de tipeo con ranking por relevancia, y una arquitectura
|
|
||||||
que ya no depende del techo de 100 productos de `fetchProducts`.
|
|
||||||
- Se acepta como deuda técnica: quedar atado a la línea `v1` del
|
|
||||||
plugin de Meilisearch mientras Medusa siga en `2.18.x`; migrar a
|
|
||||||
Medusa `2.19+` es un proyecto aparte, no parte de esta Etapa 3.
|
|
||||||
- Riesgo operativo real y no resuelto todavía por este ADR: el host ya
|
|
||||||
corre con el swap saturado — el despliegue de Etapa 2 debe empezar
|
|
||||||
con límites de recursos conservadores y validación en vivo de
|
|
||||||
consumo real antes de confiar en el servicio, siguiendo la misma
|
|
||||||
lección que dejó el incidente de recursos de Gitea.
|
|
||||||
- Revertir esta decisión implica desinstalar el plugin y el
|
|
||||||
subscriber nuevo, y volver el buscador/filtrado a 100% client-side
|
|
||||||
— no hay migración de datos que deshacer, porque Meilisearch nunca
|
|
||||||
es la fuente de verdad, solo una copia de solo lectura indexada
|
|
||||||
desde Medusa.
|
|
||||||
@@ -84,6 +84,7 @@ nav:
|
|||||||
- Visión general: arquitectura/vision-general.md
|
- Visión general: arquitectura/vision-general.md
|
||||||
- Red y exposición: arquitectura/red-y-exposicion.md
|
- Red y exposición: arquitectura/red-y-exposicion.md
|
||||||
- Flujo GitOps: arquitectura/gitops-flujo.md
|
- Flujo GitOps: arquitectura/gitops-flujo.md
|
||||||
|
- Agente de monitoreo: arquitectura/agente-monitoreo.md
|
||||||
- Glosario: arquitectura/glosario.md
|
- Glosario: arquitectura/glosario.md
|
||||||
- Decisiones (ADRs):
|
- Decisiones (ADRs):
|
||||||
- "0001 · Por qué k3d": decisiones/0001-por-que-k3d.md
|
- "0001 · Por qué k3d": decisiones/0001-por-que-k3d.md
|
||||||
@@ -91,7 +92,6 @@ nav:
|
|||||||
- "0003 · HTTP/2 sobre QUIC": decisiones/0003-por-que-http2-sobre-quic-tunnel.md
|
- "0003 · HTTP/2 sobre QUIC": decisiones/0003-por-que-http2-sobre-quic-tunnel.md
|
||||||
- "0004 · Stack multi-categoría": decisiones/0004-evaluacion-stack-multi-categoria.md
|
- "0004 · Stack multi-categoría": decisiones/0004-evaluacion-stack-multi-categoria.md
|
||||||
- "0005 · Rediseño storefront": decisiones/0005-rediseno-storefront.md
|
- "0005 · Rediseño storefront": decisiones/0005-rediseno-storefront.md
|
||||||
- "0006 · Motor de búsqueda y facetas": decisiones/0006-motor-busqueda-facetas.md
|
|
||||||
- Plantilla ADR: decisiones/plantilla-adr.md
|
- Plantilla ADR: decisiones/plantilla-adr.md
|
||||||
- Playbooks:
|
- Playbooks:
|
||||||
- Crashloop Medusa: playbooks/incidente-crashloop-medusa.md
|
- Crashloop Medusa: playbooks/incidente-crashloop-medusa.md
|
||||||
|
|||||||
Reference in new issue
Block a user