Keep hard-railed bridge items out of do-first
This commit is contained in:
@@ -928,19 +928,22 @@ function normalizeLaneHint(value = '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
function laneFor(option, rankIndex, total) {
|
||||
function laneFor(option, rankIndex, total, activeRankIndex = rankIndex, activeTotal = 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 and source non-goals
|
||||
// are safety rails: if the source already marked something as not-now, or
|
||||
// if the candidate conflicts with the source guardrails, never promote it
|
||||
// into the active proof slice just because keyword scoring liked it.
|
||||
// into the active proof slice just because keyword scoring liked it. The
|
||||
// first eligible item should still become Do first even when a hard-railed
|
||||
// candidate sorts above it, otherwise the handoff can end up with no defended
|
||||
// first move — exactly the dashboard-swamp failure the bridge exists to avoid.
|
||||
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 (option.metrics?.nonGoalConflicts?.length) return { id: 'defer', label: 'Defer', action: 'Resolve source guardrail first', source: 'source-non-goal' };
|
||||
if (rankIndex === 0) return { id: 'do', label: 'Do first', action: 'Build/test now' };
|
||||
if (activeRankIndex === 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 (activeRankIndex < Math.max(2, Math.ceil(activeTotal * 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' };
|
||||
}
|
||||
@@ -1045,8 +1048,9 @@ function whatWouldChangeRanking(top, second, risky) {
|
||||
}
|
||||
|
||||
function createDecisionBrief({ idea, context, mode, ranked, provenance, decisionContext }) {
|
||||
const top = ranked[0];
|
||||
const second = ranked[1];
|
||||
const activeRanked = ranked.filter(item => ['do', 'test'].includes(item.lane.id));
|
||||
const top = ranked.find(item => item.lane.id === 'do') || activeRanked[0] || ranked[0];
|
||||
const second = activeRanked.find(item => item.id !== top?.id) || ranked.find(item => item.id !== top?.id);
|
||||
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(' · ');
|
||||
@@ -1271,23 +1275,31 @@ app.post('/api/rank-feedback', (req, res) => {
|
||||
let options = optionsFromBody(body || {});
|
||||
if (options.length < 2) return res.status(400).json({ error: 'Paste at least two options, features, ideas, or next moves to rank.' });
|
||||
const bridgeContext = `${idea}\n${context}\n${provenance.snapshotTitle}\n${provenance.schema}\n${decisionContext.targetAudience}\n${decisionContext.constraints.join('\n')}\n${decisionContext.assumptions.join('\n')}`;
|
||||
options = options.map(option => ({ ...option, metrics: scoreOption(option, mode, bridgeContext, decisionContext) }))
|
||||
.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) => {
|
||||
const lane = laneFor(option, index, arr.length);
|
||||
const rankedOption = { ...option, rank: index + 1, lane };
|
||||
return {
|
||||
...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 scoredOptions = options.map(option => ({ ...option, metrics: scoreOption(option, mode, bridgeContext, decisionContext) }))
|
||||
.sort((a, b) => b.metrics.score - a.metrics.score || b.metrics.value - a.metrics.value || a.metrics.risk - b.metrics.risk);
|
||||
const activeEligibleTotal = scoredOptions.filter(option => {
|
||||
const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || '');
|
||||
return !['park', 'defer'].includes(hintedLane) && !option.metrics?.nonGoalConflicts?.length;
|
||||
}).length;
|
||||
let activeRankIndex = 0;
|
||||
options = scoredOptions.map((option, index, arr) => {
|
||||
const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || '');
|
||||
const activeEligible = !['park', 'defer'].includes(hintedLane) && !option.metrics?.nonGoalConflicts?.length;
|
||||
const lane = laneFor(option, index, arr.length, activeEligible ? activeRankIndex : Number.POSITIVE_INFINITY, activeEligibleTotal);
|
||||
if (activeEligible) activeRankIndex += 1;
|
||||
const rankedOption = { ...option, rank: index + 1, lane };
|
||||
return {
|
||||
...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 });
|
||||
const handoff = createHandoffContract({ ranked: options, provenance, decisionContext });
|
||||
res.json({
|
||||
|
||||
Reference in New Issue
Block a user