diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index b50f618..bd0e084 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -1087,6 +1087,33 @@ try { assert.ok(lensOnly.ranked.find(item => item.id === 'build-order-4').metrics.nonGoalConflicts.length >= 1); assert.deepEqual(lensOnly.handoff.warnings, []); + const signalLensResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceName: 'Scattermind', + reference_code: 'SM-SIGNAL-LENS', + working_name: 'Signal-rich Build Order', + ideaText: 'A paid Concept Map names the success and failure signal directly in its Build Order lens.', + context: 'Solo builder. Manual proof first. Avoid accounts and dashboards until the signal is real.', + mode: 'mvp', + lenses: { + risk: 'Avoid accounts and dashboards until the signal is real.', + channel: 'Build first: Manual invite proof - ask five named buyers before building signup UI. Success signal: two people ask for a date. Failure signal: nobody replies with a concrete yes. Test manually: One-page promise - show the offer and collect yes/no replies. Green flag: people ask how to book. Red flag: people only say interesting.', + }, + }), + }); + assert.equal(signalLensResponse.status, 200); + const signalLens = await signalLensResponse.json(); + assert.equal(signalLens.ranked[0].id, 'build-order-1'); + assert.equal(signalLens.ranked[0].factors.successSignal, 'two people ask for a date'); + assert.equal(signalLens.ranked[0].factors.killSignal, 'nobody replies with a concrete yes'); + assert.equal(signalLens.handoff.activeSlice.proof.successSignal, 'two people ask for a date'); + assert.equal(signalLens.handoff.activeSlice.proof.killSignal, 'nobody replies with a concrete yes'); + assert.equal(signalLens.ranked.find(item => item.id === 'build-order-2').factors.successSignal, 'people ask how to book'); + assert.equal(signalLens.ranked.find(item => item.id === 'build-order-2').factors.killSignal, 'people only say interesting'); + assert.deepEqual(signalLens.handoff.warnings, []); + const scattermindPaidShapeResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 68c0de0..ac87935 100644 --- a/server.js +++ b/server.js @@ -1216,7 +1216,7 @@ function normalizeBuildOrderFragment(fragment = '') { function sentenceFragments(text = '') { return cleanMultiline(text, 4000) .replace(new RegExp(`\\s+${buildOrderLabelPattern}${buildOrderLabelSeparator}`, 'gi'), '\n$1: ') - .split(/\n|;|\s+[•-]\s+/) + .split(/\n|;|\s+•\s+/) .map(part => normalizeBuildOrderFragment(part)) .filter(Boolean); } @@ -1240,6 +1240,8 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense const labelled = fragments.filter(fragment => laneFromBuildOrderLabel(fragment)); return labelled.map((fragment, index) => { const lane = laneFromBuildOrderLabel(fragment); + const successSignal = signalFromThreadText(fragment, ['success signal', 'green flag', 'working if', 'working when']); + const failureSignal = signalFromThreadText(fragment, ['failure signal', 'red flag', 'failing if', 'failing when', 'stop if', 'kill signal']); return { id: `build-order-${index + 1}`, action: titleFromBuildOrderFragment(fragment), @@ -1251,6 +1253,8 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense : lane === 'validate-next' ? 'Collect the smallest real signal before promoting this into the build lane.' : '', + successSignal, + killSignal: failureSignal, suggestedLane: lane, rankerHints: lane === 'do-first' ? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }