Strengthen rank feedback decision brief

This commit is contained in:
OpenClaw Bot
2026-05-26 23:57:48 +02:00
parent c13f16c0e7
commit 25c7c08543
6 changed files with 315 additions and 47 deletions
+143 -7
View File
@@ -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 35 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')),
},
});
});