Module 8: Project Define Mercados Strategy
The strategic filter applied
Description
The one-pager's four previous layers —vision, where to play, how to win, moats— are, all of them, analysis: they confirm what's true about Mercado today. This layer is the first that decides: it takes the quarter's complete backlog —the five bets you already know from product-thinking-for-engineers-guide plus the trap bet this guide added— and applies strategicFilter with the strategy the four previous layers already justified, piece by piece. The result fills the one-pager's strategicFilter layer, and it's the document's first point where "strategy" stops being a noun and becomes a criterion that accepts or rejects concrete bets.
Connection to the module. This lesson reuses riceScore and strategicFilter verbatim from module 7, with the same strategy and the same complete backlog that module already verified. The difference with this lesson is one of origin, not calculation: here, winOn and avoid aren't an isolated data point — every value traces explicitly back to the exact layer of this one-pager that produced it.
An everyday analogy: the admission criteria, already written with the applicant's data
A university deciding its admission criteria doesn't invent them the day applications arrive — it derives them from its own identity: what kind of student fits its mission, what skills it hopes to train, what kind of future graduate it wants to produce. Once those criteria are written, applying them to each application is mechanical and objective: the record gets compared against the criterion, not against the committee's mood that day. The hard work —deciding what kind of student fits— already happened before; applying the criterion is, comparatively, the easy part.
This one-pager layer is exactly that moment of application. The hard work —deciding which dimensions Mercado wins on (lesson 4) and what the vision explicitly rules out (module 2)— is already done. winOn and avoid, in this lesson, aren't a new declaration: they're the direct translation of decisions the earlier layers already justified with evidence.
Worked example: the complete backlog, with the strategy traced back to its origin
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);
}
// Mercado's strategy, traced back to its origin in this one-pager:
// winOn comes from the real differentiation lesson 4 confirmed
// (curatedDiscovery, localSellerTrust -- here under the alias
// 'sellerTrust' that the backlog uses, inherited from module 7); avoid
// comes from what the vision (lesson 2 / module 2) explicitly rules out:
// 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: Mercado\'s complete backlog ===\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=== The final roadmap, RICE only among those who passed the filter ===\n');
const roadmap = prioritize(eligible);
console.table(roadmap.map((b) => ({ feature: b.feature, riceScore: b.riceScore })));
console.log('Mercado\'s roadmap: ' + roadmap.map((b) => b.feature).join(' > ') + '.');
console.log('\n=== What the filter avoided ===\n');
console.log('Bets discarded by the filter: ' + rejected.map((b) => b.feature + ' (riceScore=' + b.riceScore + ')').join(', ') + '.');
What to expect. Running the file with Node produces exactly this output:
=== strategicFilter: Mercado's complete backlog ===
┌─────────┬────────────────────┬────────────────────┬────────────┬───────────┐
│ (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 │
└─────────┴────────────────────┴────────────────────┴────────────┴───────────┘
=== The final roadmap, RICE only among those who passed the filter ===
┌─────────┬───────────────────┬───────────┐
│ (index) │ feature │ riceScore │
├─────────┼───────────────────┼───────────┤
│ 0 │ 'reviews' │ 2400 │
│ 1 │ 'sellerTools' │ 960 │
│ 2 │ 'recommendations' │ 833.33 │
└─────────┴───────────────────┴───────────┘
Mercado's roadmap: reviews > sellerTools > recommendations.
=== What the filter avoided ===
Bets discarded by the filter: lowestPriceMatch (riceScore=7600), fasterCheckout (riceScore=6400), improvedSearch (riceScore=1500).
The result is identical to what you already saw in module 7, and that's exactly what should happen: if mercadoStrategy really comes from this one-pager's earlier layers, it has to reproduce the same strategy module 7 already verified independently. What's new here isn't the number — it's that now you can trace every value of winOn and avoid back to a specific, verified layer: curatedDiscovery comes from the real differentiation confirmed in lesson 4; sellerTrust too, under the alias the module 7 backlog uses for the same pillar; price in avoid comes from lowestPriceRace, one of the vision's three explicit excludes that you filled in lesson 2.
Deep dive: an identification you may have already noticed, worth naming
If you carefully compared lesson 4 and this lesson, you may have noticed something: lesson 4 (differentiationMap, inherited from module 4) calls one of its pillars localSellerTrust; this lesson (strategicFilter, inherited from module 7) calls the same concept sellerTrust. It's not an error in this guide — it's a real consequence of modules 3, 4, 6, and 7 being written at different times, each with its own vocabulary, never reconciled with each other because, until this module, they never had to coexist in the same document.
This is, in fact, a completely normal situation in real product strategy: different teams, at different times, end up naming the same concept slightly differently in separate documents. A real one-pager's job isn't to pretend the discrepancy doesn't exist, nor to pick one name and hope nobody notices the other — it's to explicitly reconcile it, at the place where the two documents meet for the first time. That's why this lesson's code comment says it plainly: 'sellerTrust', the name the backlog uses, inherited from module 7. Lesson 8, which chains the four layers into a single pipeline, is going to formalize this reconciliation with a small alias dictionary — the kind of bridge any engineer who integrates systems written by different teams recognizes immediately.
Common mistakes
Declaring winOn and avoid without being able to trace each value back to an earlier layer. What happens: someone copies this lesson's mercadoStrategy without being able to explain, if asked, why winOn has those two exact values and not others. Why it happens: once the strategy object "looks right" and produces the expected result, verifying its origin feels like an unnecessary extra step. How to spot it: ask "where does each winOn value come from?" — if the answer is "that's how it was in module 7" instead of "from the real differentiation confirmed in lesson 4," traceability got lost. How to fix it: always require the complete chain, as in this lesson's code comment — every strategy value must point to a previously verified layer, never "because it worked before."
Ignoring the sellerTrust / localSellerTrust discrepancy instead of naming it. What happens: while assembling the complete one-pager, someone notices the two names don't match and, instead of explaining it, silently changes one of the two source documents so "everything matches" — altering data other lessons of the guide already verified. Why it happens: a naming discrepancy feels like an error that needs to be "fixed" silently, not like legitimate information worth keeping and explaining. How to spot it: if your final one-pager has no note explaining why two layers name the same concept differently, someone probably altered source data to hide the discrepancy. How to fix it: never change a previous module's verbatim data so things "match" — reconcile the difference with an explicit note, as this lesson does, and leave the acknowledgment visible in the final document.
Treating this layer's result as if it already included the moat reading. What happens: the team sees reviews > sellerTools > recommendations and treats it as the final, complete roadmap, without yet connecting that result to the previous lesson's moat audit — which of these three bets, besides belonging to the strategy, is building something defensible? Why it happens: strategicFilter produces a result that feels complete on its own — there's an order, there are numbers, there's a roadmap — and the question only lesson 8 is going to answer, by crossing this layer against the previous one, is missing. How to spot it: if your final roadmap mentions nothing about which moat each bet deepens, you're missing the whole one-pager's most important layer. How to fix it: proceed to lesson 8, which crosses this exact result against lesson 5's moat audit — the step that reveals recommendations belongs to the strategy but doesn't yet build anything defensible.
Exercises
Exercise 1 — Verify the derived winOn by hand. Using lesson 4's result (realDiffs = curatedDiscovery, localSellerTrust) and the alias { localSellerTrust: 'sellerTrust' }, write out the winOn array that would result from applying that alias to each element of realDiffs, in the same order.
See solution
['curatedDiscovery', 'sellerTrust'] — curatedDiscovery has no alias, so it passes unchanged; localSellerTrust does have an alias, so it translates to sellerTrust. The order is preserved because realDiffs keeps the order dimensions appear in differentiationMap's original array (curatedDiscovery before localSellerTrust). The result matches exactly this lesson's mercadoStrategy.winOn — proof that the value "hardcoded" here is, in fact, derivable.
Exercise 2 — Predict the effect of a badly derived winOn. If someone, by mistake, left winOn as ['curatedDiscovery', 'localSellerTrust'] (without applying the alias), what would happen to the result for sellerTools and reviews, which serve 'sellerTrust' in the backlog?
See solution
Both bets would fall to inStrategy: false. reinforces is calculated as bet.servesDimensions.filter((d) => strategy.winOn.includes(d)) — with servesDimensions: ['sellerTrust'] and winOn containing 'localSellerTrust' instead of 'sellerTrust', no element matches, so reinforces would be [], and inStrategy would be false for both. The final roadmap would shrink to a single bet (recommendations), losing two bets that do belong to the strategy — a silent naming error, with no syntax error at all, that would completely change the filter's result. It's exactly the risk the "Deep dive" section's explicit reconciliation exists to prevent.
Exercise 3 — Explain the name reconciliation to a new colleague. An engineer joining the team, reviewing this lesson's code, asks why differentiationMap uses localSellerTrust but strategicFilter uses sellerTrust for what looks like the same concept. Write, in 2-3 sentences, your explanation.
See solution
A sample answer: "They're the same concept —the trust a buyer has in Mercado's local sellers— named differently in two modules of the guide written at different times, with nobody reconciling them until we got to this one-pager, where both have to coexist. Instead of pretending they're different things or silently changing one of the original datasets, we use a small alias dictionary to translate between the two, and document it explicitly in the code — it's exactly the kind of friction that shows up when you integrate systems or documents from different teams, and the right way to resolve it is to name it, not hide it."
Summary and next step
In this lesson you filled the one-pager's fifth layer: strategicFilter, applied to Mercado's complete backlog with a strategy whose origin you can trace, value by value, back to this same document's earlier layers. The resulting roadmap —reviews > sellerTools > recommendations— matches exactly module 7's, and that match is proof that the strategy declared here is consistent with everything the guide already verified.
Before moving on you should be able to: explain where every value of winOn and avoid comes from, and explain the sellerTrust / localSellerTrust discrepancy without needing to consult this lesson again.
Lesson 7 assembles the five layers into the final presentation document — the complete one-pager, formatted for a stakeholder who never saw any of the seven previous modules. Lesson 8, the final project, goes one step further: it chains the four executable models into a single pipeline, derives winOn and avoid in code (not by hand, as in this lesson), and crosses the resulting roadmap against the moat audit — the whole guide's final finding.
Resources
- Roger Martin, "Decoding the Strategy Choice Cascade" — rogermartin.medium.com/decoding-the-strategy-choice-cascade-475d40555eb1. Every choice in the cascade depends on the one before it — tracing
winOnandavoidback to this one-pager's earlier layers is that same cascade, made explicit in code. In English. - Melissa Perri, Escaping the Build Trap — oreilly.com/library/view/escaping-the-build/9781491973767. The build trap, avoided here with a filter a team can apply without reinventing its strategy every quarter. In English.
- Marty Cagan (SVPG), "Product Strategy" — svpg.com/product-strategy-overview. Product strategy as the criterion that makes it possible to say no to objectively good ideas — this layer's exact result. In English.