Fase 1b: cableado del matcher (ingesta polling, runner shadow/full, comparador)

- config: bloque matcher (MATCHER_MODE off/shadow/full, IRON_PASSWORD, audiencia,
  poll, batch) + validacion
- db: colecciones subscriptions/activefires/notifications_shadow/matcher_state +
  ensureMatcherIndexes (2dsphere en notifications_shadow para el dedupe 500m)
- matcher/context: MatchContext sobre Mongo real (candidatas geo $near 1000km +
  audiencia; dedupe geo $near 500m; lang del owner)
- matcher/ingest: polling de activefires por createdAt>checkpoint (Mongo 3.2 sin
  change streams), checkpoint persistido en matcher_state
- matcher/runner: loop shadow(->notifications_shadow)/full(->notifications),
  idempotente por dedupe+checkpoint
- matcher/compare: comparador de shadow (shadowOnly=spam, realOnly=faltantes,
  contentMismatches) — el gate antes del cutover
- index: arranca el matcher; fake-repo ampliado (upsert, sort, $gt/$lt)
- docs/env/README actualizados

73 tests en verde, typecheck y build OK.
This commit is contained in:
vjrj 2026-07-13 18:07:06 +02:00
parent b3aeb6108f
commit a1ed206985
13 changed files with 531 additions and 19 deletions

View file

@ -108,6 +108,29 @@ MODE=full OWNED_CHANNELS=fcm
3. Nada se pierde: los docs pendientes siguen en `notifications` con
`notified`/`emailNotified` sin marcar.
## Fase 1b — matcher geoespacial (rollout)
El matcher **genera** los docs `notifications` (fuego → subs 2dsphere) que hoy
crea node-red. Config: `MATCHER_MODE` (`off`/`shadow`/`full`), `IRON_PASSWORD`
(sella `sealed`; = `settings.private.ironPassword`), `MATCHER_AUDIENCE`
(`web,mobile`). Ingesta por **polling** de `activefires` (Mongo 3.2 no tiene
change streams). Ver `docs/legacy-matching.md`.
Secuencia (empezar solo con la fase 1a estable):
1. **`MATCHER_MODE=shadow`** ≥3 días / varios ciclos NASA reales. Escribe en
`notifications_shadow` (no toca `notifications`, node-red sigue generando).
2. **Comparar** con `src/matcher/compare.ts` (`compareShadow`): correr el
comparador sobre la ventana y exigir **`shadowOnly` vacío** (0 notifs de más =
0 spam) — cada `realOnly` (faltante) se analiza uno a uno; `contentMismatches`
revisados.
3. **Cutover** (con confirmación explícita): desactivar el subflow de matching en
node-red (exportar el flow antes como respaldo; documentar qué nodos) y poner
`MATCHER_MODE=full`. node-red deja de insertar en `notifications`; el matcher
pasa a ser el único origen. Los workers de 1a consumen igual.
4. **Rollback**: reimportar el flow de node-red y `MATCHER_MODE=shadow`/`off`.
Los workers de 1a siguen consumiendo `notifications` venga de donde venga.
## Checklist de verificación (por paso)
- [ ] Tests en verde (`npm test`).