From 85c80671854cf58bb78154a8ccc65de163bd84ff Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 16:58:31 +0200 Subject: [PATCH] Accept wrapped Scattermind thread fallbacks --- scripts/check-rank-feedback.mjs | 29 +++++++++++++++++++++++++++++ server.js | 30 ++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 808193e..973188b 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -290,6 +290,35 @@ try { assert.match(storedRowPaste.handoff.copyableText, /CM-STORED-77/); assert.equal(storedRowPaste.ranked.find(item => /Account dashboard/i.test(item.title)).lane.id, 'park'); + const wrappedThreadFallbackResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + mode: 'mvp', + rankerInput: { + sourceName: 'Scattermind', + artifactId: 'CM-WRAPPED-THREADS', + snapshotTitle: 'Wrapped thread fallback Concept Map', + originalPrompt: 'I clarified an idea, but the bridge envelope only has action threads so far.', + context: 'Solo builder. Manual proof first. Avoid saved workspaces and account dashboards before evidence.', + threadsToHold: [ + 'Start by manually turning one Concept Map into a source-traced build order preview', + 'Validate with three tired non-AI-native users before adding product UI', + 'Hold for later: polished saved workspace after proof', + ], + }, + }), + }); + assert.equal(wrappedThreadFallbackResponse.status, 200); + const wrappedThreadFallback = await wrappedThreadFallbackResponse.json(); + assert.equal(wrappedThreadFallback.input.provenance.artifactId, 'CM-WRAPPED-THREADS'); + assert.equal(wrappedThreadFallback.input.optionCount, 3); + assert.equal(wrappedThreadFallback.ranked[0].provenance.sourceSection, 'ranker-input.threadsToHold'); + assert.match(wrappedThreadFallback.brief.quickGlance.topPick, /source-traced build order preview/i); + assert.equal(wrappedThreadFallback.ranked.find(item => /saved workspace/i.test(item.title)).lane.id, 'defer'); + assert.equal(wrappedThreadFallback.handoff.readiness.status, 'ready'); + assert.deepEqual(wrappedThreadFallback.handoff.warnings, []); + const snapshotOnlyResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 90093e4..44bd94b 100644 --- a/server.js +++ b/server.js @@ -1116,6 +1116,10 @@ function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') { return options.length >= 2 ? options : []; } +function firstArraySource(entries = []) { + return entries.find(entry => Array.isArray(entry.items) && entry.items.length > 0) || { items: [], sourceSection: '' }; +} + function optionsFromBody(body = {}) { const envelope = bridgeEnvelopeFrom(body); const featureSet = featureSetFrom(body); @@ -1221,18 +1225,32 @@ function optionsFromBody(body = {}) { ); const buildOrderOptions = optionsFromBuildOrderText(buildOrderText, 'concept-map.lenses.channel', buildOrderSourceTitle); if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]); + const actionThreadSource = firstArraySource([ + { items: conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, sourceSection: 'concept-map.threadsToHold' }, + { items: snapshot.threads_to_hold || snapshot.threadsToHold || snapshot.actionThreads || snapshot.action_threads, sourceSection: 'snapshot.threadsToHold' }, + { items: envelope.threads_to_hold || envelope.threadsToHold || envelope.actionThreads || envelope.action_threads, sourceSection: 'ranker-input.threadsToHold' }, + { items: featureSet.threads_to_hold || featureSet.threadsToHold || featureSet.actionThreads || featureSet.action_threads, sourceSection: 'feature-set.threadsToHold' }, + { items: body.threads_to_hold || body.threadsToHold || body.actionThreads || body.action_threads, sourceSection: 'threadsToHold' }, + ]); const actionThreadOptions = optionsFromActionThreads( - body.threads_to_hold || body.threadsToHold || body.actionThreads || body.action_threads || conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, - conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads ? 'concept-map.threadsToHold' : 'threadsToHold', + actionThreadSource.items, + actionThreadSource.sourceSection || 'threadsToHold', 'Thread to hold' ); - if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: 'concept-map.threadsToHold' }]); + if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); + const questionSource = firstArraySource([ + { items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' }, + { items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' }, + { items: envelope.questions_to_sit_with || envelope.questionsToSitWith || envelope.openQuestions || envelope.open_questions, sourceSection: 'ranker-input.questionsToSitWith' }, + { items: featureSet.questions_to_sit_with || featureSet.questionsToSitWith || featureSet.openQuestions || featureSet.open_questions, sourceSection: 'feature-set.questionsToSitWith' }, + { items: body.questions_to_sit_with || body.questionsToSitWith || body.openQuestions || body.open_questions, sourceSection: 'questionsToSitWith' }, + ]); const questionOptions = optionsFromQuestionsToSitWith( - body.questions_to_sit_with || body.questionsToSitWith || body.openQuestions || body.open_questions || conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.openQuestions || conceptMap.open_questions, - conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.openQuestions || conceptMap.open_questions ? 'concept-map.questionsToSitWith' : 'questionsToSitWith', + questionSource.items, + questionSource.sourceSection || 'questionsToSitWith', 'Question to sit with' ); - if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: 'concept-map.questionsToSitWith', defaultLane: 'validate-next' }]); + if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]); const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot'); const snapshotReadingOptions = nestedSnapshotReadingOptions.length ? nestedSnapshotReadingOptions