Harden Scattermind rank feedback bridge

This commit is contained in:
OpenClaw Bot
2026-05-26 22:12:27 +02:00
parent f75dbb1a10
commit e532c6d910
3 changed files with 135 additions and 17 deletions
+62 -16
View File
@@ -333,7 +333,7 @@ const judgementModes = {
const wordSets = {
revenue: ['pay', 'paid', 'price', 'pricing', 'stripe', 'checkout', 'invoice', 'sales', 'sell', 'buyer', 'subscription', 'revenue', 'client', 'customer', 'conversion', 'upsell'],
value: ['pain', 'problem', 'save', 'faster', 'clear', 'clarity', 'decision', 'feedback', 'user', 'customer', 'relief', 'manual', 'core', 'must', 'need', 'trust'],
effort: ['dashboard', 'workspace', 'workspaces', 'collaboration', 'realtime', 'integration', 'integrations', 'automation', 'accounts', 'auth', 'billing', 'ai', 'model', 'mobile', 'sync', 'admin', 'export', 'exportable', 'slack', 'notion', 'team', 'voting'],
effort: ['dashboard', 'workspace', 'workspaces', 'collaboration', 'realtime', 'integration', 'integrations', 'automation', 'accounts', 'auth', 'billing', 'subscription', 'pricing', 'checkout', 'invoices', 'ai', 'model', 'mobile', 'sync', 'admin', 'export', 'exportable', 'slack', 'notion', 'team', 'voting'],
risk: ['unclear', 'maybe', 'complex', 'hard', 'risky', 'unknown', 'depends', 'enterprise', 'platform', 'everything', 'all users', 'team voting', 'marketplace', 'saved workspaces', 'team voting'],
novelty: ['new', 'novel', 'different', 'unique', 'original', 'weird', 'unexpected', 'expert', 'judgement', 'reflection', 'rank', 'compare'],
urgency: ['now', 'launch', 'blocker', 'first', 'mvp', 'today', 'week', 'urgent', 'stuck', 'before', 'next'],
@@ -358,15 +358,52 @@ function parseOptionsFromText(value) {
}).filter(item => item.title);
}
function cleanProvenance(input = {}) {
const artifact = input.artifact && typeof input.artifact === 'object' ? input.artifact : {};
const source = input.source && typeof input.source === 'object' ? input.source : {};
return {
schema: cleanText(input.schema || artifact.schema || input.type || 'prioritix-feature-set-v1', 80),
source: cleanText(input.sourceName || source.name || artifact.sourceName || 'Scattermind', 80),
artifactId: cleanText(input.artifactId || input.sourceArtifactId || artifact.id || source.artifactId || '', 120),
snapshotTitle: cleanText(input.snapshotTitle || artifact.snapshotTitle || input.ideaTitle || '', 160),
conceptMapId: cleanText(input.conceptMapId || artifact.conceptMapId || '', 120),
};
}
function optionsFromBody(body = {}) {
const featureSet = body.featureSet && typeof body.featureSet === 'object' ? body.featureSet : {};
const rawFeatures = Array.isArray(body.features) ? body.features : Array.isArray(featureSet.features) ? featureSet.features : null;
if (rawFeatures) {
return rawFeatures.slice(0, 24).map((item, index) => ({
id: cleanText(item?.id || item?.key || `feature-${index + 1}`, 80) || `feature-${index + 1}`,
title: cleanText(item?.title || item?.name || item?.action || '', 140),
description: cleanText(item?.description || item?.brief || item?.why || item?.evidenceNeeded || '', 520),
provenance: {
sourceId: cleanText(item?.sourceId || item?.sourceArtifactId || item?.id || '', 120),
sourceSection: cleanText(item?.sourceSection || item?.section || item?.lane || '', 80),
},
})).filter(item => item.title);
}
if (Array.isArray(body.options)) {
return body.options.slice(0, 24).map((item, index) => ({
id: cleanText(item?.id || `option-${index + 1}`, 80) || `option-${index + 1}`,
title: cleanText(item?.title || item?.name || '', 140),
description: cleanText(item?.description || item?.brief || '', 420),
})).filter(item => item.title);
}
return parseOptionsFromText(body.optionsText || featureSet.optionsText || '');
}
function scoreOption(option, mode, context = '') {
const text = `${option.title} ${option.description} ${context}`;
const effortHits = hits(text, wordSets.effort);
const riskHits = hits(text, wordSets.risk);
const coreLoopHits = hits(text, ['ranked feedback map', 'feedback map', 'pasted feature', 'feature lists', 'first-pass', 'decision brief', 'expert reflections']);
const bridgeHits = hits(text, ['snapshot', 'concept map', 'feature set', 'build order', 'rank-ready', 'provenance', 'next moves']);
const metrics = {
value: Math.min(10, 4.8 + hits(text, wordSets.value) * 0.9 + coreLoopHits * 0.8 + hits(context, wordSets.value) * 0.15),
feasibility: Math.max(1, Math.min(10, 8.2 - effortHits * 0.8 - riskHits * 0.35 + Math.min(1.1, coreLoopHits * 0.28))),
confidence: Math.max(1, Math.min(10, 5.8 + coreLoopHits * 0.35 + hits(text, ['manual', 'existing', 'already', 'simple', 'clear', 'known']) * 0.7 - hits(text, ['maybe', 'unknown', 'new market', 'all users']) * 0.9)),
value: Math.min(10, 4.8 + hits(text, wordSets.value) * 0.9 + coreLoopHits * 0.8 + bridgeHits * 0.75 + hits(context, wordSets.value) * 0.15),
feasibility: Math.max(1, Math.min(10, 8.2 - effortHits * 0.8 - riskHits * 0.35 + Math.min(1.1, coreLoopHits * 0.28 + bridgeHits * 0.18))),
confidence: Math.max(1, Math.min(10, 5.8 + coreLoopHits * 0.35 + bridgeHits * 0.28 + hits(text, ['manual', 'existing', 'already', 'simple', 'clear', 'known']) * 0.7 - hits(text, ['maybe', 'unknown', 'new market', 'all users']) * 0.9)),
urgency: Math.min(10, 4.9 + hits(text, wordSets.urgency) * 0.9),
revenue: Math.min(10, 3.8 + hits(text, wordSets.revenue) * 1.05),
novelty: Math.min(10, 4.1 + hits(text, wordSets.novelty) * 0.95),
@@ -388,6 +425,7 @@ function laneFor(option, rankIndex, total) {
function reasonFor(option) {
const m = option.metrics;
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 (m.feasibility >= 7.2 && m.value >= 6.2) return 'high enough value with low enough delivery drag to create fast signal';
if (m.revenue >= 6.4) return 'clearer buyer or money signal than the rest of the list';
if (m.risk >= 6.5) return 'interesting, but it carries assumption risk that should be tested before build';
@@ -441,25 +479,33 @@ app.post('/api/rank-feedback', (req, res) => {
const context = cleanMultiline(req.body?.context || '', 3000);
const modeId = cleanText(req.body?.mode || 'progress', 40);
const mode = judgementModes[modeId] || judgementModes.progress;
let options = Array.isArray(req.body?.options)
? req.body.options.slice(0, 24).map((item, index) => ({ id: `option-${index + 1}`, title: cleanText(item?.title || item?.name || '', 140), description: cleanText(item?.description || item?.brief || '', 420) })).filter(item => item.title)
: parseOptionsFromText(req.body?.optionsText || '');
const provenance = cleanProvenance(req.body || {});
let options = optionsFromBody(req.body || {});
if (options.length < 2) return res.status(400).json({ error: 'Paste at least two options, features, ideas, or next moves to rank.' });
options = options.map(option => ({ ...option, metrics: scoreOption(option, mode, `${idea}\n${context}`) }))
const bridgeContext = `${idea}\n${context}\n${provenance.snapshotTitle}\n${provenance.schema}`;
options = options.map(option => ({ ...option, metrics: scoreOption(option, mode, bridgeContext) }))
.sort((a, b) => b.metrics.score - a.metrics.score || b.metrics.value - a.metrics.value || a.metrics.risk - b.metrics.risk)
.map((option, index, arr) => ({
...option,
rank: index + 1,
lane: laneFor(option, index, arr.length),
reason: reasonFor(option),
concern: concernFor(option),
}));
.map((option, index, arr) => {
const lane = laneFor(option, index, arr.length);
const rankedOption = { ...option, rank: index + 1, lane };
return {
...rankedOption,
reason: reasonFor(rankedOption),
concern: concernFor(rankedOption),
};
});
res.json({
ok: true,
mode: { id: modeId in judgementModes ? modeId : 'progress', label: mode.label },
input: { idea, context, optionCount: options.length },
input: { idea, context, optionCount: options.length, provenance },
ranked: options,
brief: createDecisionBrief({ idea, context, mode, ranked: options }),
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),
},
});
});