Módulo 8: Project Define Mercados Strategy

El filtro estratégico aplicado

Descripción

Las cuatro capas anteriores del one-pager —visión, dónde jugar, cómo ganar, moats— son, todas, análisis: confirman qué es verdad sobre Mercado hoy. Esta capa es la primera que decide: toma el backlog completo del trimestre —las cinco apuestas que ya conoces de product-thinking-for-engineers-guide más la apuesta trampa que esta guía agregó— y le aplica strategicFilter con la estrategia que las cuatro capas anteriores ya justificaron, pieza por pieza. El resultado llena la capa strategicFilter del one-pager, y es el primer punto del documento donde "estrategia" deja de ser un sustantivo y se convierte en un criterio que acepta o rechaza apuestas concretas.

Conexión con el módulo. Esta lección reutiliza riceScore y strategicFilter verbatim del módulo 7, con la misma estrategia y el mismo backlog completo que ese módulo ya verificó. La diferencia con esta lección es de origen, no de cálculo: aquí, winOn y avoid no son un dato aislado — cada valor se traza explícitamente hasta la capa exacta de este one-pager que lo produjo.

Una analogía cotidiana: el criterio de admisión, ya escrito con los datos del solicitante

Una universidad que decide sus criterios de admisión no los inventa el día que llegan las solicitudes — los deriva de su propia identidad: qué tipo de estudiante encaja con su misión, qué habilidades espera formar, a qué tipo de futuro egresado quiere graduar. Una vez que esos criterios están escritos, aplicarlos a cada solicitud es mecánico y objetivo: se compara el expediente contra el criterio, no contra el humor del comité ese día. El trabajo difícil —definir qué tipo de estudiante encaja— ya se hizo antes; aplicar el criterio es, comparativamente, la parte fácil.

Esta capa del one-pager es exactamente ese momento de aplicación. El trabajo difícil —decidir en qué dimensiones gana Mercado (lección 4) y qué descarta explícitamente la visión (módulo 2)— ya está hecho. winOn y avoid, en esta lección, no son una declaración nueva: son la traducción directa de decisiones que las capas anteriores ya justificaron con evidencia.

Ejemplo trabajado: el backlog completo, con la estrategia trazada hasta su origen

function riceScore({ reach, impact, confidence, effort }) {
  return (reach * impact * confidence) / effort;
}

function strategicFilter(backlog, strategy) {
  return backlog.map((bet) => {
    const reinforces = bet.servesDimensions.filter((d) => strategy.winOn.includes(d));
    const conflicts = bet.servesDimensions.filter((d) => strategy.avoid.includes(d));
    return {
      feature: bet.feature,
      servesDimensions: bet.servesDimensions,
      reinforces,
      conflicts,
      inStrategy: reinforces.length > 0 && conflicts.length === 0,
      riceScore: Number(riceScore(bet.rice).toFixed(2)),
    };
  });
}

function prioritize(items) {
  return items.slice().sort((a, b) => b.riceScore - a.riceScore);
}

// La estrategia de Mercado, trazada hasta su origen en este one-pager:
// winOn viene de la diferenciación real que la lección 4 confirmó
// (curatedDiscovery, localSellerTrust -- aquí bajo el alias 'sellerTrust'
// que usa el backlog, heredado del módulo 7); avoid viene de lo que la
// visión (lección 2 / módulo 2) descarta explícitamente: lowestPriceRace.
const mercadoStrategy = { winOn: ['curatedDiscovery', 'sellerTrust'], avoid: ['price'] };

const backlog = [
  { feature: 'fasterCheckout', servesDimensions: ['convenience'], rice: { reach: 8000, impact: 2, confidence: 0.8, effort: 2 } },
  { feature: 'recommendations', servesDimensions: ['curatedDiscovery'], rice: { reach: 5000, impact: 1, confidence: 0.5, effort: 3 } },
  { feature: 'sellerTools', servesDimensions: ['sellerTrust'], rice: { reach: 1200, impact: 2, confidence: 0.8, effort: 2 } },
  { feature: 'reviews', servesDimensions: ['sellerTrust'], rice: { reach: 6000, impact: 0.5, confidence: 0.8, effort: 1 } },
  { feature: 'improvedSearch', servesDimensions: ['catalogBreadth'], rice: { reach: 9000, impact: 1, confidence: 0.5, effort: 3 } },
  { feature: 'lowestPriceMatch', servesDimensions: ['price'], rice: { reach: 9500, impact: 3, confidence: 0.8, effort: 3 } },
];

console.log('=== strategicFilter: el backlog completo de Mercado ===\n');
const filtered = strategicFilter(backlog, mercadoStrategy).sort((a, b) => b.riceScore - a.riceScore);
console.table(filtered.map((b) => ({ feature: b.feature, servesDimensions: b.servesDimensions.join(', '), inStrategy: b.inStrategy, riceScore: b.riceScore })));

const eligible = filtered.filter((b) => b.inStrategy);
const rejected = filtered.filter((b) => !b.inStrategy);

console.log('\n=== El roadmap final, RICE solo entre quienes pasaron el filtro ===\n');
const roadmap = prioritize(eligible);
console.table(roadmap.map((b) => ({ feature: b.feature, riceScore: b.riceScore })));
console.log('Roadmap de Mercado: ' + roadmap.map((b) => b.feature).join(' > ') + '.');

console.log('\n=== Lo que el filtro evitó ===\n');
console.log('Apuestas descartadas por el filtro: ' + rejected.map((b) => b.feature + ' (riceScore=' + b.riceScore + ')').join(', ') + '.');

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== strategicFilter: el backlog completo de Mercado ===

┌─────────┬────────────────────┬────────────────────┬────────────┬───────────┐
│ (index) │      feature       │  servesDimensions  │ inStrategy │ riceScore │
├─────────┼────────────────────┼────────────────────┼────────────┼───────────┤
│    0    │ 'lowestPriceMatch' │      'price'       │   false    │   7600    │
│    1    │  'fasterCheckout'  │   'convenience'    │   false    │   6400    │
│    2    │     'reviews'      │   'sellerTrust'    │    true    │   2400    │
│    3    │  'improvedSearch'  │  'catalogBreadth'  │   false    │   1500    │
│    4    │   'sellerTools'    │   'sellerTrust'    │    true    │    960    │
│    5    │ 'recommendations'  │ 'curatedDiscovery' │    true    │  833.33   │
└─────────┴────────────────────┴────────────────────┴────────────┴───────────┘

=== El roadmap final, RICE solo entre quienes pasaron el filtro ===

┌─────────┬───────────────────┬───────────┐
│ (index) │      feature      │ riceScore │
├─────────┼───────────────────┼───────────┤
│    0    │     'reviews'     │   2400    │
│    1    │   'sellerTools'   │    960    │
│    2    │ 'recommendations' │  833.33   │
└─────────┴───────────────────┴───────────┘
Roadmap de Mercado: reviews > sellerTools > recommendations.

=== Lo que el filtro evitó ===

Apuestas descartadas por el filtro: lowestPriceMatch (riceScore=7600), fasterCheckout (riceScore=6400), improvedSearch (riceScore=1500).

El resultado es idéntico al que ya viste en el módulo 7, y eso es exactamente lo que debería pasar: si mercadoStrategy en verdad viene de las capas anteriores de este one-pager, tiene que reproducir la misma estrategia que el módulo 7 ya verificó de forma independiente. Lo nuevo aquí no es el número — es que ahora puedes trazar cada valor de winOn y avoid hasta una capa específica y verificada: curatedDiscovery viene de la diferenciación real confirmada en la lección 4; sellerTrust también, bajo el alias que el backlog del módulo 7 usa para el mismo pilar; price en avoid viene de lowestPriceRace, uno de los tres excludes explícitos de la visión que llenaste en la lección 2.

Profundización: una identificación que ya notaste, y que vale la pena nombrar

Si comparaste con atención la lección 4 y esta lección, quizás notaste algo: la lección 4 (differentiationMap, heredada del módulo 4) llama a uno de sus pilares localSellerTrust; esta lección (strategicFilter, heredada del módulo 7) llama al mismo concepto sellerTrust. No es un error de esta guía — es una consecuencia real de que los módulos 3, 4, 6 y 7 se escribieron en momentos distintos, cada uno con su propio vocabulario, y nunca se reconciliaron entre sí porque, hasta este módulo, nunca tuvieron que convivir en el mismo documento.

Esta es, de hecho, una situación completamente normal en estrategia de producto real: distintos equipos, en distintos momentos, terminan nombrando el mismo concepto de formas ligeramente distintas en documentos separados. El trabajo de un one-pager de verdad no es fingir que la discrepancia no existe, ni elegir un nombre y esperar que nadie note el otro — es reconciliarla explícitamente, en el lugar donde los dos documentos se encuentran por primera vez. Por eso el comentario del código de esta lección lo dice sin rodeos: 'sellerTrust' que usa el backlog, heredado del módulo 7. La lección 8, que encadena las cuatro capas en un solo pipeline, va a formalizar esta reconciliación con un pequeño diccionario de alias — la clase de puente que cualquier ingeniero que integra sistemas escritos por equipos distintos reconoce de inmediato.

Errores comunes

Declarar winOn y avoid sin poder trazar cada valor hasta una capa anterior. Qué pasa: alguien copia mercadoStrategy de esta lección sin poder explicar, si se lo preguntan, por qué winOn tiene esos dos valores exactos y no otros. Por qué pasa: una vez que el objeto de estrategia "se ve bien" y produce el resultado esperado, verificar su origen se siente como un paso extra innecesario. Cómo detectarlo: pregunta "¿de dónde viene cada valor de winOn?" — si la respuesta es "así estaba en el módulo 7" en vez de "de la diferenciación real confirmada en la lección 4", la trazabilidad se perdió. Cómo corregirlo: exige siempre la cadena completa, como en el comentario del código de esta lección — cada valor de la estrategia debe apuntar a una capa anterior verificada, nunca a "porque sí funcionó antes".

Ignorar la discrepancia sellerTrust / localSellerTrust en vez de nombrarla. Qué pasa: al construir el one-pager completo, alguien nota que los dos nombres no coinciden y, en vez de explicarlo, silenciosamente cambia uno de los dos documentos fuente para que "todo combine" — alterando datos que otras lecciones de la guía ya verificaron. Por qué pasa: una discrepancia de nombres se siente como un error que hay que "arreglar" en silencio, no como información legítima que vale la pena conservar y explicar. Cómo detectarlo: si tu one-pager final no tiene ninguna nota explicando por qué dos capas nombran el mismo concepto de forma distinta, alguien probablemente alteró datos fuente para ocultar la discrepancia. Cómo corregirlo: nunca cambies los datos verbatim de un módulo anterior para que "combinen" — reconcilia la diferencia con una nota explícita, como esta lección, y deja el reconocimiento visible en el documento final.

Tratar el resultado de esta capa como si ya incluyera la lectura de moats. Qué pasa: el equipo ve reviews > sellerTools > recommendations y lo trata como el roadmap final y completo, sin conectar todavía ese resultado con la auditoría de moats de la lección anterior — ¿cuál de estas tres apuestas, además de pertenecer a la estrategia, está construyendo algo defendible? Por qué pasa: strategicFilter produce un resultado que se siente completo por sí mismo — hay un orden, hay números, hay un roadmap — y falta la pregunta que solo la lección 8 va a responder cruzando esta capa contra la anterior. Cómo detectarlo: si tu roadmap final no menciona nada sobre qué moat profundiza cada apuesta, te falta la capa más importante de todo el one-pager. Cómo corregirlo: sigue a la lección 8, que cruza este resultado exacto contra la auditoría de moats de la lección 5 — el paso que revela que recommendations pertenece a la estrategia pero todavía no construye nada defendible.

Ejercicios

Ejercicio 1 — Verifica el winOn derivado, a mano. Usando el resultado de la lección 4 (realDiffs = curatedDiscovery, localSellerTrust) y el alias { localSellerTrust: 'sellerTrust' }, escribe el arreglo winOn que resultaría de aplicar ese alias a cada elemento de realDiffs, en el mismo orden.

Ver solución

['curatedDiscovery', 'sellerTrust']curatedDiscovery no tiene alias, así que pasa sin cambios; localSellerTrust sí tiene alias, así que se traduce a sellerTrust. El orden se preserva porque realDiffs mantiene el orden en que aparecen las dimensiones en el arreglo original de differentiationMap (curatedDiscovery antes que localSellerTrust). El resultado coincide exactamente con el mercadoStrategy.winOn de esta lección — la prueba de que el valor "hardcodeado" aquí es, en realidad, derivable.

Ejercicio 2 — Predice el efecto de un winOn mal derivado. Si alguien, por error, dejara winOn como ['curatedDiscovery', 'localSellerTrust'] (sin aplicar el alias), ¿qué le pasaría al resultado de sellerTools y reviews, que sirven 'sellerTrust' en el backlog?

Ver solución

Ambas apuestas caerían a inStrategy: false. reinforces se calcula como bet.servesDimensions.filter((d) => strategy.winOn.includes(d)) — con servesDimensions: ['sellerTrust'] y winOn conteniendo 'localSellerTrust' en vez de 'sellerTrust', ningún elemento coincide, así que reinforces sería [], y inStrategy sería false para ambas. El roadmap final se reduciría a una sola apuesta (recommendations), perdiendo dos apuestas que sí pertenecen a la estrategia — un error silencioso de nomenclatura, sin ningún error de sintaxis, que cambiaría por completo el resultado del filtro. Es exactamente el riesgo que la reconciliación explícita de la sección "Profundización" existe para evitar.

Ejercicio 3 — Explica la reconciliación de nombres a un compañero nuevo. Un ingeniero que se une al equipo, revisando el código de esta lección, pregunta por qué differentiationMap usa localSellerTrust pero strategicFilter usa sellerTrust para lo que parece ser el mismo concepto. Escribe, en 2-3 frases, tu explicación.

Ver solución

Un ejemplo de respuesta: "Son el mismo concepto —la confianza que un comprador tiene en los vendedores locales de Mercado—, nombrado de forma distinta en dos módulos de la guía escritos en momentos diferentes, sin que nadie los reconciliara hasta que llegamos a este one-pager, donde los dos tienen que convivir. En vez de fingir que son cosas distintas o cambiar silenciosamente uno de los datasets originales, usamos un pequeño diccionario de alias para traducir entre ambos, y lo dejamos documentado explícitamente en el código — es exactamente el tipo de fricción que aparece cuando integras sistemas o documentos de equipos distintos, y la forma correcta de resolverla es nombrarla, no esconderla."

Resumen y siguiente paso

En esta lección llenaste la quinta capa del one-pager: strategicFilter, aplicado al backlog completo de Mercado con una estrategia cuyo origen puedes trazar, valor por valor, hasta las capas anteriores de este mismo documento. El roadmap resultante —reviews > sellerTools > recommendations— coincide exactamente con el del módulo 7, y esa coincidencia es la prueba de que la estrategia declarada aquí es consistente con todo lo que la guía ya verificó.

Antes de avanzar deberías poder: explicar de dónde viene cada valor de winOn y avoid, y explicar la discrepancia sellerTrust / localSellerTrust sin necesitar consultar esta lección de nuevo.

La lección 7 ensambla las cinco capas en el documento final de presentación — el one-pager completo, formateado para un stakeholder que nunca vio ninguno de los siete módulos anteriores. La lección 8, el proyecto final, va un paso más allá: encadena los cuatro modelos ejecutables en un solo pipeline, deriva winOn y avoid en código (no a mano, como en esta lección) y cruza el roadmap resultante contra la auditoría de moats — el hallazgo final de toda la guía.

Recursos