Web search — handoff para el agente que continúa
Lee esto antes de tocar nada. Es la mitad que el cuerpo del PR #274 no cubre: ése está escrito para un revisor humano, éste para quien sigue el trabajo. Base:staging. Rama: claude/web-search-provider-eval-ai6un9.
Diseño completo: web-search-provider-evaluation.md
Métricas y fixtures: web-search-fixtures-and-metrics.md
Tablero: proyecto Driftless Web Search Provider Evaluation (b875dad7), 17 tarjetas.
1. Antes de correr un solo test — la trampa del entorno
vitest resuelve @driftless/db, @driftless/model-gateway y @driftless/telemetry
desde dist/. En un checkout limpio esos dist/ no existen y todo el suite
de radar y chat falla con Failed to resolve entry for package — que
parece un error de código y no lo es.
apps/api):
2. Qué existe ya, y qué NO
Todo lo implementado es provider-neutral: no cambia según quién gane el benchmark. Ninguno de estos módulos está cableado enradar.module.ts — son
inertes hasta que alguien los registre.
No existe: ningún adapter, ninguna fixture, el bench runner, el cableado.
3. Lo siguiente: WS-04, el adapter del candidato principal
Es la pieza más pequeña que queda y desbloquea el resto.Lo que ya está verificado sobre la API (2026-08-01)
No hace falta que lo re-investigues; sí conviene que lo re-confirmes contra la documentación antes de escribir el mapeo, porque estos precios se mueven.
La clave ya está desplegada (
ParallelAdapter la usa para FindAll). No hay
proveedor nuevo, ni secreto nuevo, ni revisión legal nueva.
El mapeo que el port espera
mode: 'cheap' | 'balanced' | 'exhaustive'→ los tiers del vendor. El dominio nunca dice el nombre del tier.excerpts[].span→ tienes que calcularlo tú: el vendor da el texto, no el offset. Búscalo en el markdown que devuelve y emiteoffset:start-end. Si no puedes localizarlo, emitespan: null— no inventes uno.publishedAt→publish_date, ynullsi el vendor no lo da. Nunca la hora de fetch.contentHash→nullensearch()yextract()(no tuvimos los bytes), presente solo enfetch().
El candado te va a frenar, y está bien
radar-architecture.spec.ts falla el build si un token del vendor aparece fuera
de radar/adapters/. Ya están añadidos los de web search. Si tu adapter no
compila el spec, no es un bug del spec.
4. Fixtures (WS-03) — cuestan cero
Corregido respecto de lo que dije antes: el bloqueo no era dinero, era que yo no tengo llaves en mi contenedor. Las capas gratuitas cubren la captura completa:Cómo capturar
Las nueve clases obligatorias están enweb-search-fixtures-and-metrics.md §2.
Resumen de lo que importa al capturar:
_shared/es el control del experimento. El mismo HTML y el mismo PDF para todos los extractores. Si cada proveedor recibe su propia copia de la página, la comparación mide copias, no proveedores.- Clases 1–3 y 9 son capturas reales. Clases 4–8 (429, 5xx, timeout,
partial, inyección) pueden ser sintéticas y se marcan
synthetic: true. - Redacta con el módulo, no a mano:
Vive en
adapters/y no engtm/por una razón que el candado hace cumplir: redacta datos de wire de proveedor, así que nombra headers de vendor (x-api-key), y eso no puede aparecer en el dominio. Las fixtures que redacta también viven bajoadapters/.redact()preserva la longitud en bytes para que los offsets sigan apuntando al mismo texto. Sireport.lengthChangedestrue, recaptura elexpected/— nunca ajustes el span a mano. manifest.jsonconsha256ycaptured_atpor archivo, más elsha256del cuerpo original. Sin fecha no sabes de cuándo es la web que mediste; sin hash no puedes detectar que alguien editó una fixture para que un test pasara.
5. Invariantes que no se rompen
Si un cambio necesita romper alguno de estos, no es un ajuste: es una decisión de producto y necesita un humano.observed_atsale de la página.nullsi la página no dice fecha. Rellenarlo con la hora de fetch convierte un boletín de 2019 leído hoy en algo observado hoy, y después nada aguas abajo puede notarlo.- La web nunca crea un
claim_typeque un registro posee. Corrobora o fecha; no establece ni corrige. Para licencias, sanciones e insolvencia, algo equivocado no es evidencia más débil — es daño a una empresa real. live_verifiedes inalcanzable desde un índice de búsqueda. Un índice dice lo que el proveedor vio alguna vez.- La familia y el origen los decide el HOST final, no el proveedor. Dos proveedores que devuelven el mismo host son un solo origen.
- Una claim derivada de web no dispara escrituras. Es la capa que queda en pie cuando el detector de inyección falla — y falla, aproximadamente una de cada diez veces contra ataques optimizados.
- La herramienta cara nunca entra al cinturón sola. La escalada se
demuestra contra
gtm_provider_attempts, no se afirma. - Ningún pack corre sin
cost_policy.parseCostPolicy()lanza. No lo debilites con un default.
6. Orden sugerido
validate y su acceptance. Córrelos: son el
contrato, no una sugerencia.
7. Lo que está deliberadamente fuera de alcance
Anotado para que no reaparezca como “mejora” a media implementación — §0.2 del documento de diseño tiene el razonamiento completo:- Encender
tool-policy.ts(está dark a propósito; su activación es una decision card humana que toca todas las superficies) - Promover
gtm_provider_waterfalla Collection - Cascada general por capacidad (enriquecimiento antes que web)
- Monitores por segmento y
FindAll previewcomo sonda
8. Dos cosas pendientes de decisión humana
- El aviso del hook de diseño marca dos
<img>enuntrusted.spec.tscomo imágenes rotas. Son payloads de ataque dentro de un spec de seguridad — el vector bajo prueba. Es un falso positivo y no está silenciado: suprimirlo requiere confirmación del dueño. - Los precios se verificaron el 2026-08-01 contra las páginas oficiales. Re-verifícalos antes de contratar: cuatro proveedores de esta categoría cambiaron precio o cerraron en los últimos doce meses.
