Honor Scattermind lane hints in rank feedback
This commit is contained in:
@@ -428,7 +428,9 @@ function scoreOption(option, mode, context = '') {
|
||||
const swampHits = hits(text, ['dashboard', 'workspace', 'workspaces', 'auth', 'accounts', 'billing', 'subscription', 'team voting', 'collaboration', 'admin']);
|
||||
const dependencyPenalty = Math.min(2.2, (factors.dependencies || []).length * 0.45);
|
||||
const laneHint = factors.recommendedLane || '';
|
||||
const normalizedLaneHint = normalizeLaneHint(laneHint);
|
||||
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 lanePenalty = normalizedLaneHint === 'park' ? 18 : normalizedLaneHint === 'defer' ? 9 : 0;
|
||||
const metrics = {
|
||||
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),
|
||||
feasibility: Math.max(1, Math.min(10, 8.2 - effortHits * 0.8 - riskHits * 0.35 - dependencyPenalty + Math.min(1.1, coreLoopHits * 0.28 + bridgeHits * 0.18 + proofHits * 0.12))),
|
||||
@@ -441,12 +443,29 @@ function scoreOption(option, mode, context = '') {
|
||||
const weights = mode.weights;
|
||||
const weighted = Object.entries(weights).reduce((sum, [key, weight]) => sum + metrics[key] * weight, 0);
|
||||
const possible = Object.entries(weights).reduce((sum, [_key, weight]) => sum + Math.abs(weight) * 10, 0);
|
||||
const score = Math.max(0, Math.min(100, Math.round(((weighted + laneBoost) + Math.abs(Math.min(0, weights.risk || 0)) * 10) / possible * 100)));
|
||||
const score = Math.max(0, Math.min(100, Math.round(((weighted + laneBoost) + Math.abs(Math.min(0, weights.risk || 0)) * 10) / possible * 100) - lanePenalty));
|
||||
return { ...metrics, score };
|
||||
}
|
||||
|
||||
function normalizeLaneHint(value = '') {
|
||||
const hint = cleanText(value, 40).toLowerCase().replace(/_/g, '-');
|
||||
if (/^(do|do-first|first|build|build-now|now)$/.test(hint)) return 'do';
|
||||
if (/^(validate|validate-next|test|test-next|proof|evidence)$/.test(hint)) return 'test';
|
||||
if (/^(defer|later|sequence-later|after-proof)$/.test(hint)) return 'defer';
|
||||
if (/^(park|cut|drop|icebox|not-now)$/.test(hint)) return 'park';
|
||||
return '';
|
||||
}
|
||||
|
||||
function laneFor(option, rankIndex, total) {
|
||||
const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || '');
|
||||
// Ranker should defend build order, not blindly obey Scattermind. Positive
|
||||
// hints can nudge scoring, but explicit negative hints are safety rails:
|
||||
// if the source already marked something as defer/park, never promote it
|
||||
// into the active proof slice just because keyword scoring liked it.
|
||||
if (hintedLane === 'park') return { id: 'park', label: 'Park / cut', action: 'Keep out of the active plan', source: 'hint' };
|
||||
if (hintedLane === 'defer') return { id: 'defer', label: 'Defer', action: 'Sequence after proof', source: 'hint' };
|
||||
if (rankIndex === 0) return { id: 'do', label: 'Do first', action: 'Build/test now' };
|
||||
if (hintedLane === 'test') return { id: 'test', label: 'Validate next', action: 'Find evidence', source: 'hint' };
|
||||
if (rankIndex < Math.max(2, Math.ceil(total * 0.32))) return { id: 'test', label: 'Validate next', action: 'Find evidence' };
|
||||
if (rankIndex >= Math.max(2, Math.floor(total * 0.72))) return { id: 'park', label: 'Park / cut', action: 'Keep out of the active plan' };
|
||||
return { id: 'defer', label: 'Defer', action: 'Sequence after proof' };
|
||||
@@ -473,15 +492,23 @@ function concernFor(option) {
|
||||
return 'The main risk is sequencing: do it only if it supports the first useful proof.';
|
||||
}
|
||||
|
||||
function createDecisionBrief({ idea, context, mode, ranked }) {
|
||||
function createDecisionBrief({ idea, context, mode, ranked, provenance }) {
|
||||
const top = ranked[0];
|
||||
const second = ranked[1];
|
||||
const risky = ranked.slice().sort((a, b) => b.metrics.risk - a.metrics.risk)[0];
|
||||
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 it has ${reasonFor(top)}.` : 'The list needs at least two options before ranking becomes useful.';
|
||||
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.` : ''}`,
|
||||
summary: `${theme}${second ? ` “${second.title}” is the nearest follow-up, not a parallel first step.` : ''}${sourceLabel ? ` Source: ${sourceLabel}.` : ''}`,
|
||||
source: provenance ? {
|
||||
schema: provenance.schema,
|
||||
source: provenance.source,
|
||||
artifactId: provenance.artifactId,
|
||||
snapshotTitle: provenance.snapshotTitle,
|
||||
conceptMapId: provenance.conceptMapId,
|
||||
} : null,
|
||||
expertReflections: [
|
||||
{
|
||||
lens: 'Product expert',
|
||||
@@ -497,7 +524,7 @@ function createDecisionBrief({ idea, context, mode, ranked }) {
|
||||
},
|
||||
],
|
||||
next48Hours: top ? [
|
||||
`Write a one-paragraph test for “${top.title}”.`,
|
||||
provenance?.artifactId ? `Open the source artifact (${provenance.artifactId}) and mark “${top.title}” as the defended first move.` : `Write a one-paragraph test for “${top.title}”.`,
|
||||
top.factors?.evidenceNeeded ? `Evidence to collect: ${top.factors.evidenceNeeded}.` : 'Name the evidence that would make this decision obviously right or wrong.',
|
||||
'Put it in front of 3–5 real people or run it manually once.',
|
||||
`Do not touch ${deferred[0] ? `“${deferred[0].title}”` : 'the parked ideas'} until the first signal is real.`,
|
||||
@@ -531,7 +558,7 @@ app.post('/api/rank-feedback', (req, res) => {
|
||||
mode: { id: modeId in judgementModes ? modeId : 'progress', label: mode.label },
|
||||
input: { idea, context, optionCount: options.length, provenance },
|
||||
ranked: options,
|
||||
brief: createDecisionBrief({ idea, context, mode, ranked: options }),
|
||||
brief: createDecisionBrief({ idea, context, mode, ranked: options, provenance }),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user