Fecha: 2026-07-02 · Estado: ✅ EJECUTADO (ADR-087). Commits
2edafca(backend) +6b8a934(frontend) +7360649(fix mappings), deploy verificado. round_of_16 liberado progresivamente el mismo día (5/8 cruces resueltos, 465/465 pools verificadas, mappings+sync creados). Ver ADR-087 en DECISION_LOG.md para el registro canónico. Regla: cero suposiciones. Cada afirmación de §1 está verificada contra la DB de producción,archivo:líneadel repo, o ≥2 fuentes externas. Nada se implementa sin aprobación; nada se marca hecho sin commit + test + deploy verificado (lección ADR-086).
Verificado contra Yahoo Sports, CBS Sports, NBC Sports, SI, worldcupwiki y FIFA.com (2026-07-02):
| Partido | dataJson | Fuentes externas | ✓ |
|---|---|---|---|
| m_R16_1 Paraguay–Francia | 04-jul 21:00Z | Filadelfia 5pm ET | ✓ |
| m_R16_2 Canadá–Marruecos | 04-jul 17:00Z | Houston 1pm ET | ✓ |
| m_R16_3 Brasil–Noruega | 05-jul 20:00Z | E. Rutherford 4pm ET | ✓ |
| m_R16_4 México–Inglaterra | 06-jul 00:00Z | CDMX 8pm ET | ✓ |
| m_R16_5 (POR/CRO–ESP/AUT) | 06-jul 19:00Z | Arlington 3pm ET | ✓ |
| m_R16_6 USA–Bélgica | 06-jul 21:00Z | Seattle 5pm ET | ✓ |
| m_R16_7 (ARG/CPV–AUS/EGY) | 07-jul 16:00Z | Atlanta 12pm ET (match 95) | ✓ |
| m_R16_8 (SUI/ALG–COL/GHA) | 07-jul 20:00Z | Vancouver 4pm ET (match 96) | ✓ |
| m_QF_1..4 | 09-jul 20:00Z · 10-jul 19:00Z · 11-jul 21:00Z · 12-jul 01:00Z | Boston 4pm · LA 3pm · Miami 5pm · KC 9pm ET | ✓ |
| m_SF_1/2 | 14/15-jul 19:00Z | Dallas/Atlanta 3pm ET | ✓ |
| m_3RD | 18-jul 21:00Z | Miami 5pm ET (match 103) | ✓ |
| m_FINAL | 19-jul 19:00Z | MetLife 3pm ET | ✓ |
m_3RD "Tercer Lugar" (L_SF_1 vs L_SF_2)vive DENTRO de la fasefinals(2 partidos: bronce + final).- 465/465 pools tienen config de
finalsque puntúa ambos partidos (verificado en DB): score pools gradúan marcador de m_3RD y m_FINAL; estratega dapointsPerCorrectAdvancepor acertar el ganador de CADA uno. resolveKnockoutPlaceholderssoportaL_(tournamentAdvancement.ts:454-457,465-468); la constante canónicaPLACEHOLDER_TEAM_PREFIXESincluyeL_(constants.ts:250-256, con test).- Regla FIFA 2026 verificada (Al Jazeera/SI/FOX): el bronce SÍ tiene tiempo extra + penales, igual que todo knockout → la config compartida de extra-time de
finalses semánticamente correcta para ambos.
Decisión requerida (recomendación: NO separar en fase propia). Separarlo implicaría: mutar pickTypesConfig de 465 pools ACTIVAS (choca con el invariante 3 de inmutabilidad de reglas), re-escribir 465 fixtureSnapshot, y tocar el inventario completo de listas hardcoded (§1.5). Riesgo alto a mitad de torneo, valor de jugador nulo: los jugadores YA van a poder predecir el 3er puesto. Lo que sí haremos: presentación digna (placeholder "Perdedor de Semifinal 1/2", ver §2.B).
resolveKnockoutPlaceholdersresuelve por-partido y tolera parciales ✓.- Barrier 1:
checkAndTriggerAdvancement(advancementTrigger.ts:73-98) exige fase COMPLETA para agendar. - Barrier 2:
advanceKnockoutPhaselanza error si falta un resultado (instanceAdvancement.ts:650-654 estructural, :684-688 score). - Barrier 3 (reconciliación): el guard ADR-084 (
isKnockoutPhaseReleased) bloquea el path per-pool en fases liberadas — diseñado para proteger el bracket revisado; en modo progresivo se vuelve el mecanismo que apaga el path legacy automáticamente (§2.A.5). - Estado actual: 10/16 R32 finalizados → m_R16_1/2/3/4/6 ya son 100% decidibles.
- Los resultados existen en TODAS las pools (incl. estratega) vía
publishScraperResult→ un resolver a nivel instancia puede derivar winner/loser de una pool de referencia con fuente FINAL.
- Score picks:
pickService.ts:218-223bloquea placeholder (MATCH_PENDING409) ANTES e independiente del gate ADR-084 → per-match gating YA existe ✓. - Estratega:
structuralPicks.tsNO valida placeholders (el merge :204-227 guarda lo que llegue) y el UI (KnockoutMatchCardvíaStructuralPicksManager.tsx:513-519) muestra fallback "TBD" hardcoded y permite pickear a TBD (TeamPickButton solo se deshabilita por lock/deadline). Hoy lo tapa elPHASE_NOT_RELEASED; al abrir progresivamente quedaría expuesto → hay que cerrarlo. - Front:
derivePhaseState(poolHelpers.ts:191-213) marca la fase PENDING si CUALQUIER partido tiene placeholder → con apertura progresiva debe pasar a OPEN cuando la fase esté released, gobernando cada partido por su propio estado. - El front no maneja los errores
PHASE_NOT_RELEASED/MATCH_PENDING(0 ocurrencias) — confía en el estado derivado.
Listas/switches hardcoded que se romperían con un phaseId nuevo: poolOverviewService.ts:301 (lista knockout completa), resultService.ts:423,444 (patrones que disparan advance), poolAdminService.ts:116-126,155-160, pickPresets.ts:7-26 + presets, PHASE_DISPLAY_NAMES (constants.ts:236-247), front ScoringEditor.tsx:103-106,1495-1509, exportLeaderboard.ts:39-57, i18n phases.*/phasesLong.* es/en/pt. — Con la recomendación §1.2 este inventario NO se toca.
fixtureTrackingJob.ts:34-36: reimplementa el check SINL_nit_TBD→ unm_3RDno resuelto que entre en la ventana de tracking se enviaría al scraper con "L_SF_1" como nombre de equipo.- Otras reimplementaciones locales con divergencias menores:
advancementTrigger.ts:390-393(3rd_POOL_estrecho),deadlineReminderService.ts:144-152,poolAdminService.ts:1739-41,schemas/templateData.ts:86-90. → unificar aisPlaceholderTeamId/PLACEHOLDER_TEAM_PREFIXEScanónicos.
progressiveKnockoutResolver.ts(nuevo, instance-level): al finalizar un partido knockout (hook junto acheckAndTriggerAdvancementen liveScoresJob:729, y también invocable tras HOST_OVERRIDE masivo), con dedupe idempotente: a. Deriva winner/loser del partido recién finalizado (pool de referencia, fuente ∈ FINAL_RESULT_SOURCES; empate→penales; empate en penales → NO resuelve, alerta existente). b. SustituyeW_<matchId>/L_<matchId>eninstance.dataJson(puede alimentar 2 slots: p.ej. m_SF_x alimenta m_FINAL y m_3RD). c. Propaga a losfixtureSnapshotde todas las pools (reutiliza el patrónpropagateBracketToPools, respetaknockoutBracketOverridesdel admin). d.ensureKnockoutSyncPlumbingpara el/los partidos que quedaron completos (mapping + MatchSyncState) — sync con scores garantizado por partido (ADR-086). e. Auto-release: si es el PRIMER partido resuelto de su fase → agrega el phaseId areleasedKnockoutPhases+ disparasendPhaseSummaryBroadcast(revisar copy para estado parcial: "los cruces se irán habilitando a medida que se definan").- El gate ADR-084 se conserva como mecanismo (sigue gobernando
PHASE_NOT_RELEASED); solo cambia QUIÉN lo setea para R16+ (el resolver, no el admin). El panel admin de fases + overrides quedan como válvula manual de emergencia. - Path legacy per-pool queda auto-apagado para la WC: al liberar la fase en el primer partido, el guard existente (
isKnockoutPhaseReleased) bloqueaadvanceKnockoutPhase/trigger viejo. Instancias sin gate (UCL) siguen con el flujo actual intacto. - Cerrar hueco estratega: validación
MATCH_PENDINGenstructuralPicks.tspara picks sobre partidos con placeholder (espejo de pickService:218-223). - Unificar checks de placeholder divergentes (§1.6) a la constante canónica — fija de paso el bug latente del 3er puesto en fixtureTracking.
- Tests vitest: resolver (parciales, W_/L_, doble slot de semis, idempotencia, empate-en-penales), validación estructural, unificación.
- "Ganador del partido X vs Y": enriquecer
getPlaceholderName(poolHelpers.ts:215-236):W_R32_11→ busca m_R32_11 en el snapshot → "Ganador de España vs Austria" (equipos ya resueltos); si el feeder aún tiene placeholders → fallback al label del partido ("Ganador de Octavos · Partido 5").L_SF_1→ "Perdedor de Semifinal 1". Keys nuevas es/en/pt. - Estratega:
StructuralPicksManager/KnockoutMatchCard: eliminar "TBD" hardcoded → usar el mismo placeholder enriquecido; deshabilitarTeamPickButtoncuando el equipo sea placeholder (con hint "se habilita cuando se defina el cruce"). derivePhaseState: fase released → OPEN aunque haya placeholders (cada MatchCard ya gobierna su propio estado); banner nuevo "Los cruces se van habilitando a medida que se definen" para fases parcialmente resueltas.- Score pools ya están cubiertos (MatchCard placeholder-aware, banner "Partido Pendiente") — solo se benefician del nombre enriquecido.
- Ejecutar el resolver para los R32 ya finalizados → resuelve m_R16_1/2/3/4/6 (+los que caigan hoy/mañana), crea mappings/sync rows, libera
round_of_16. - Probar primero con juan.k (email de fase + UI con cruces parciales) → luego autorizar el broadcast.
- Checklist prod: instance.dataJson + 465 snapshots con octavos resueltos; mappings/sync rows de los 5+; picks score/estratega habilitados SOLO en resueltos; placeholder legible en pendientes;
MATCH_PENDINGen estructural. - Docs: ADR-087 + TOURNAMENT_SYSTEM.md + CHANGELOG.
Separar 3er/4to como fase propia. El bronce ya fluye por finals con todo el pipeline (predicciones, scoring ambas modalidades, ET correcto, sync, emails).
- Resolver escribe mal un cruce → deriva SOLO de fuentes FINAL + respeta overrides admin + válvula manual del panel de fases + los 13 tests del gate del scraper aguas arriba.
- Estampida de emails (fase liberada con 1 solo cruce) → un solo broadcast por fase (idempotente
phaseSummaryEmailedPhasesya existente), copy adaptado a estado parcial. - Doble escritura legacy/progresivo → imposible por el guard ADR-084 (release al primer partido apaga el path viejo).
- Deadline "born-closed" → no aplica: los cruces se resuelven días antes del kickoff; ADR-085 permite al host ajustar si acaso.