Strengthen rank feedback decision brief
This commit is contained in:
@@ -393,9 +393,19 @@ const judgementModes = {
|
||||
next: 'Validate willingness to pay before polishing delivery. A paid manual version beats a beautiful unpaid feature.',
|
||||
},
|
||||
risk: {
|
||||
label: 'Risk reduction',
|
||||
label: 'Safest proof order',
|
||||
weights: { value: 0.95, feasibility: 1.0, confidence: 1.45, urgency: 0.8, revenue: 0.45, novelty: 0.25, risk: -1.85 },
|
||||
next: 'Start where uncertainty is highest and evidence is cheapest. Do not build around an untested assumption.',
|
||||
next: 'Start with the option that reduces risk without betting the roadmap on it.',
|
||||
},
|
||||
validation: {
|
||||
label: 'Fastest validation',
|
||||
weights: { value: 1.25, feasibility: 1.7, confidence: 1.35, urgency: 1.1, revenue: 0.35, novelty: 0.25, risk: -0.85 },
|
||||
next: 'Choose the option that can produce real user evidence fastest, even if the first version is manual.',
|
||||
},
|
||||
learning: {
|
||||
label: 'Biggest assumption test',
|
||||
weights: { value: 1.1, feasibility: 1.35, confidence: -0.35, urgency: 0.55, revenue: 0.45, novelty: 0.45, risk: 1.15 },
|
||||
next: 'Pick the riskiest important assumption that can be tested cheaply. The goal is learning, not shipping surface area.',
|
||||
},
|
||||
originality: {
|
||||
label: 'Most original / differentiated',
|
||||
@@ -423,11 +433,11 @@ function parseOptionsFromText(value) {
|
||||
const lines = text.split('\n').map(line => line.replace(/^\s*[-*•\d.)]+\s*/, '').trim()).filter(Boolean);
|
||||
const optionLines = lines.length >= 2 ? lines : text.split(/[;|]/).map(part => part.trim()).filter(Boolean);
|
||||
return optionLines.slice(0, 24).map((line, index) => {
|
||||
const [rawTitle, ...rest] = line.split(/\s[-–—:]\s/);
|
||||
const [rawTitle, ...rest] = line.split(/\s*[-–—:]\s+/);
|
||||
return {
|
||||
id: `option-${index + 1}`,
|
||||
title: cleanText(rawTitle || line, 140),
|
||||
description: cleanText(rest.join(' — ') || line, 420),
|
||||
description: cleanText(rest.join(' — '), 420),
|
||||
};
|
||||
}).filter(item => item.title);
|
||||
}
|
||||
@@ -664,7 +674,7 @@ function scoreOption(option, mode, context = '', decisionContext = {}) {
|
||||
const normalizedLaneHint = normalizeLaneHint(laneHint);
|
||||
const conflicts = nonGoalConflicts(`${option.title} ${option.description}`, decisionContext);
|
||||
const nonGoalPenalty = Math.min(14, conflicts.length * 7);
|
||||
const laneBoost = /do|first|now|build/.test(laneHint) ? 0.55 : /validate|test|proof/.test(laneHint) ? 0.25 : /defer|park|cut/.test(laneHint) ? -0.75 : 0;
|
||||
const laneBoost = /do|first|now|build/.test(laneHint) ? 1.35 : /validate|test|proof/.test(laneHint) ? 0.35 : /defer|park|cut/.test(laneHint) ? -0.75 : 0;
|
||||
const lanePenalty = normalizedLaneHint === 'park' ? 18 : normalizedLaneHint === 'defer' ? 9 : 0;
|
||||
const heuristicMetrics = {
|
||||
value: Math.min(10, 4.8 + hits(text, wordSets.value) * 0.9 + coreLoopHits * 0.8 + bridgeHits * 0.75 + proofHits * 0.2 + hits(context, wordSets.value) * 0.15),
|
||||
@@ -719,14 +729,30 @@ function laneFor(option, rankIndex, total) {
|
||||
return { id: 'defer', label: 'Defer', action: 'Sequence after proof' };
|
||||
}
|
||||
|
||||
function scoreDriversFor(option) {
|
||||
const m = option.metrics || {};
|
||||
const drivers = [];
|
||||
if (m.nonGoalConflicts?.length) drivers.push('source guardrail conflict');
|
||||
if (m.value >= 7) drivers.push('strong user value');
|
||||
if (m.feasibility >= 7) drivers.push('low delivery drag');
|
||||
if (m.confidence >= 7) drivers.push('clear proof path');
|
||||
if (m.revenue >= 6.5) drivers.push('buyer signal');
|
||||
if (m.novelty >= 6.5) drivers.push('differentiated angle');
|
||||
if (option.factors?.evidenceNeeded) drivers.push('explicit evidence needed');
|
||||
if ((option.factors?.dependencies || []).length === 0) drivers.push('few dependencies');
|
||||
if (m.risk >= 6.5) drivers.push('high assumption risk');
|
||||
return drivers.slice(0, 4);
|
||||
}
|
||||
|
||||
function reasonFor(option) {
|
||||
const m = option.metrics;
|
||||
const drivers = scoreDriversFor(option).filter(driver => driver !== 'source guardrail conflict' && driver !== 'high assumption risk');
|
||||
if (m.nonGoalConflicts?.length) return `it conflicts with the source non-goal “${m.nonGoalConflicts[0]}”, so it should not lead the build order`;
|
||||
if (option.lane?.id === 'do' && /snapshot|concept map|feature set|build order|rank/i.test(`${option.title} ${option.description}`)) return 'it strengthens the Scattermind → Ranker bridge instead of inventing a generic workspace';
|
||||
if (drivers.length >= 2) return `it wins on ${drivers.join(', ')} while staying inside the current proof slice`;
|
||||
if (option.factors?.evidenceNeeded && m.confidence >= 6.4) return 'it names the evidence needed, so the next move can be tested instead of guessed';
|
||||
if (m.feasibility >= 7.2 && m.value >= 6.2) return 'it has high enough value with low enough delivery drag to create fast signal';
|
||||
if (m.revenue >= 6.4) return 'it has a clearer buyer or money signal than the rest of the list';
|
||||
if (m.risk >= 6.5) return 'it is interesting, but carries assumption risk that should be tested before build';
|
||||
if (m.risk >= 6.5) return 'it is important enough to investigate, but carries assumption risk that should be tested before build';
|
||||
if (m.novelty >= 6.7) return 'it is more differentiated than the safe options, but still needs proof';
|
||||
return 'it has the best balanced tradeoff across value, effort, confidence, and timing';
|
||||
}
|
||||
@@ -735,6 +761,7 @@ function concernFor(option) {
|
||||
const m = option.metrics;
|
||||
if (m.nonGoalConflicts?.length) return `Source context says not to do this yet: ${m.nonGoalConflicts.join('; ')}.`;
|
||||
if ((option.factors?.dependencies || []).length >= 3) return 'Too many prerequisites. Split the proof slice before treating this as build-ready.';
|
||||
if (option.factors?.risk) return `The explicit risk is: ${option.factors.risk}.`;
|
||||
if (m.risk >= 6.5) return 'The hidden risk is pretending this is ready to build before the core assumption is proven.';
|
||||
if (m.feasibility <= 4.5) return 'The likely trap is scope creep: this may need too much machinery for an MVP.';
|
||||
if (m.confidence <= 4.5) return 'Evidence looks thin. Treat this as a question, not a roadmap item.';
|
||||
@@ -742,6 +769,51 @@ function concernFor(option) {
|
||||
return 'The main risk is sequencing: do it only if it supports the first useful proof.';
|
||||
}
|
||||
|
||||
function evidenceQuestionFor(option) {
|
||||
if (!option) return '';
|
||||
if (option.factors?.evidenceNeeded) return option.factors.evidenceNeeded;
|
||||
if (option.lane?.id === 'park') return `What would make “${option.title}” worth reopening later?`;
|
||||
if (option.metrics?.revenue >= 6.4) return `Will a real buyer ask for or pay for “${option.title}” before it is polished?`;
|
||||
if (option.metrics?.risk >= 6.2) return `What is the cheapest test that could disprove “${option.title}”?`;
|
||||
return `Can 3–5 target users understand and act on “${option.title}” without extra explanation?`;
|
||||
}
|
||||
|
||||
function nextStepFor(option) {
|
||||
if (!option) return '';
|
||||
if (option.lane?.id === 'do') return `Run one manual proof of “${option.title}” before building supporting machinery.`;
|
||||
if (option.lane?.id === 'test') return `Design the smallest evidence test for “${option.title}” and collect signal from real users.`;
|
||||
if (option.lane?.id === 'defer') return `Keep “${option.title}” sequenced after the active proof; do not parallel-build it.`;
|
||||
return `Park “${option.title}” unless new evidence changes the decision.`;
|
||||
}
|
||||
|
||||
function successSignalFor(option) {
|
||||
if (!option) return '';
|
||||
if (option.metrics?.revenue >= 6.4) return 'A real prospect asks for the outcome, accepts a price, or requests the next step.';
|
||||
if (option.lane?.id === 'do') return 'At least 2 of 3 real users can name why this should be first and what they would do next.';
|
||||
if (option.lane?.id === 'test') return 'The test produces a clear yes/no learning, not polite interest.';
|
||||
return 'New evidence makes this more urgent than the current active lane.';
|
||||
}
|
||||
|
||||
function killSignalFor(option) {
|
||||
if (!option) return '';
|
||||
if (option.metrics?.nonGoalConflicts?.length) return 'It still conflicts with the source guardrails after review.';
|
||||
if (option.metrics?.feasibility <= 4.5) return 'The proof slice needs platform work before any user signal exists.';
|
||||
return 'People understand the idea but do not take, request, or value the next step.';
|
||||
}
|
||||
|
||||
function scoringNotesFor(option) {
|
||||
const notes = [];
|
||||
const m = option.metrics || {};
|
||||
if (option.factors?.evidenceNeeded) notes.push('Boosted because it names evidence to collect.');
|
||||
if (m.nonGoalConflicts?.length) notes.push('Penalized because it conflicts with source guardrails.');
|
||||
if (m.feasibility >= 7) notes.push('Boosted for low delivery drag.');
|
||||
if (m.value >= 7) notes.push('Boosted for user value.');
|
||||
if (m.revenue >= 6.5) notes.push('Boosted for buyer signal.');
|
||||
if (m.risk >= 6.5) notes.push('Flagged as assumption-heavy.');
|
||||
if ((option.factors?.dependencies || []).length >= 2) notes.push('Penalized for dependencies.');
|
||||
return notes.slice(0, 4);
|
||||
}
|
||||
|
||||
function whatWouldChangeRanking(top, second, risky) {
|
||||
if (!top) return ['Add at least two concrete next moves with evidence needed.'];
|
||||
const changes = [];
|
||||
@@ -760,6 +832,15 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
|
||||
const deferred = ranked.filter(item => ['defer', 'park'].includes(item.lane.id)).slice(0, 3);
|
||||
const sourceLabel = [provenance?.snapshotTitle, provenance?.artifactId].filter(Boolean).join(' · ');
|
||||
const theme = top ? `The strongest signal is “${top.title}” because ${reasonFor(top)}.` : 'The list needs at least two options before ranking becomes useful.';
|
||||
const quickGlance = top ? {
|
||||
topPick: top.title,
|
||||
topLane: top.lane?.label || '',
|
||||
whyThisWins: reasonFor(top),
|
||||
nextAction: nextStepFor(top),
|
||||
evidenceQuestion: evidenceQuestionFor(top),
|
||||
biggestTrap: concernFor(top),
|
||||
doNotBuildYet: deferred.slice(0, 2).map(item => item.title),
|
||||
} : null;
|
||||
const assumptions = [
|
||||
...(decisionContext?.assumptions || []),
|
||||
...(decisionContext?.constraints || []).map(item => `Constraint: ${item}`),
|
||||
@@ -768,6 +849,7 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
|
||||
return {
|
||||
headline: top ? `Start with ${top.title}` : 'Add options to get a ranked feedback map',
|
||||
summary: `${theme}${second ? ` “${second.title}” is the nearest follow-up, not a parallel first step.` : ''}${sourceLabel ? ` Source: ${sourceLabel}.` : ''}`,
|
||||
quickGlance,
|
||||
source: provenance ? {
|
||||
schema: provenance.schema,
|
||||
source: provenance.source,
|
||||
@@ -801,6 +883,45 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
|
||||
};
|
||||
}
|
||||
|
||||
function rankConfidenceFor(ranked = []) {
|
||||
if (ranked.length < 2) return { level: 'low', reason: 'There are not enough comparable options for a confident ranking.' };
|
||||
const gap = Math.abs((ranked[0].metrics?.score || 0) - (ranked[1].metrics?.score || 0));
|
||||
if (gap <= 3) return { level: 'close call', reason: `The top two options are only ${gap} points apart, so treat the winner as a sequencing bet, not a law.` };
|
||||
if (gap <= 8) return { level: 'medium', reason: `The top option leads by ${gap} points, but the follow-up is still worth rechecking after one proof cycle.` };
|
||||
return { level: 'strong', reason: `The top option leads by ${gap} points and has the clearest first-proof profile.` };
|
||||
}
|
||||
|
||||
function closeCallsFor(ranked = []) {
|
||||
const calls = [];
|
||||
for (let index = 0; index < ranked.length - 1; index += 1) {
|
||||
const current = ranked[index];
|
||||
const next = ranked[index + 1];
|
||||
const gap = Math.abs((current.metrics?.score || 0) - (next.metrics?.score || 0));
|
||||
if (gap <= 5) calls.push({
|
||||
pair: [current.title, next.title],
|
||||
gap,
|
||||
note: `“${current.title}” barely beats “${next.title}”; rerank if new evidence changes effort, confidence, or buyer signal.`,
|
||||
});
|
||||
}
|
||||
return calls.slice(0, 3);
|
||||
}
|
||||
|
||||
function compactBuildItems(items = []) {
|
||||
return items.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
reason: item.reason,
|
||||
nextStep: item.nextStep,
|
||||
evidenceQuestion: item.evidenceQuestion,
|
||||
concern: item.concern,
|
||||
sourceSection: item.provenance?.sourceSection || '',
|
||||
sourceId: item.provenance?.sourceId || '',
|
||||
laneSource: item.lane?.source || 'ranked',
|
||||
score: item.metrics?.score ?? null,
|
||||
confidence: item.metrics?.confidence ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function createHandoffContract({ ranked, provenance, decisionContext }) {
|
||||
const warnings = [];
|
||||
if (!provenance?.artifactId) warnings.push('missing source artifact id');
|
||||
@@ -864,6 +985,12 @@ app.post('/api/rank-feedback', (req, res) => {
|
||||
...rankedOption,
|
||||
reason: reasonFor(rankedOption),
|
||||
concern: concernFor(rankedOption),
|
||||
nextStep: nextStepFor(rankedOption),
|
||||
evidenceQuestion: evidenceQuestionFor(rankedOption),
|
||||
successSignal: successSignalFor(rankedOption),
|
||||
killSignal: killSignalFor(rankedOption),
|
||||
scoreDrivers: scoreDriversFor(rankedOption),
|
||||
scoringNotes: scoringNotesFor(rankedOption),
|
||||
};
|
||||
});
|
||||
const brief = createDecisionBrief({ idea, context, mode, ranked: options, provenance, decisionContext });
|
||||
@@ -874,13 +1001,22 @@ app.post('/api/rank-feedback', (req, res) => {
|
||||
input: { idea, context, optionCount: options.length, provenance, decisionContext },
|
||||
ranked: options,
|
||||
brief,
|
||||
rankConfidence: rankConfidenceFor(options),
|
||||
closeCalls: closeCallsFor(options),
|
||||
handoff,
|
||||
availableModes: Object.entries(judgementModes).map(([id, item]) => ({ id, label: item.label })),
|
||||
buildOrder: {
|
||||
doFirst: options.filter(item => item.lane.id === 'do').map(item => item.id),
|
||||
validateNext: options.filter(item => item.lane.id === 'test').map(item => item.id),
|
||||
defer: options.filter(item => item.lane.id === 'defer').map(item => item.id),
|
||||
park: options.filter(item => item.lane.id === 'park').map(item => item.id),
|
||||
},
|
||||
buildOrderDetails: {
|
||||
doFirst: compactBuildItems(options.filter(item => item.lane.id === 'do')),
|
||||
validateNext: compactBuildItems(options.filter(item => item.lane.id === 'test')),
|
||||
defer: compactBuildItems(options.filter(item => item.lane.id === 'defer')),
|
||||
park: compactBuildItems(options.filter(item => item.lane.id === 'park')),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user