diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 52c6a09..0207677 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -343,6 +343,37 @@ try { assert.equal(closingNoteFallback.buildOrderDetails.validateNext[0].sourceSection, 'concept-map.questionsToSitWith'); assert.equal(closingNoteFallback.handoff.readiness.status, 'ready'); + const singleThreadQuestionResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceName: 'Scattermind', + artifactId: 'concept_map_single_thread_questions', + originalPrompt: 'Clarify a local class idea and carry one action thread plus proof questions into Ranker.', + conceptMap: { + working_name: 'Local class first proof', + opening_reflection: 'The idea should become one manual proof before any scheduling platform.', + action_threads: [ + 'Start by running one manual class invite test with five local parents and record who replies.', + ], + questions_to_sit_with: [ + 'Will five local parents reply to a plain invite without a polished booking page?', + 'What objection appears before price, timing, or trust?', + ], + reference_code: 'SM-ONE-THREAD', + }, + }), + }); + assert.equal(singleThreadQuestionResponse.status, 200); + const singleThreadQuestion = await singleThreadQuestionResponse.json(); + assert.equal(singleThreadQuestion.input.optionCount, 3, 'one action thread plus questions should still become a rankable continuation set'); + assert.equal(singleThreadQuestion.buildOrder.doFirst[0], 'action-thread-1'); + assert.equal(singleThreadQuestion.buildOrderDetails.doFirst[0].sourceSection, 'concept-map.threadsToHold'); + assert.equal(singleThreadQuestion.buildOrderDetails.validateNext.length, 2); + assert.equal(singleThreadQuestion.buildOrderDetails.validateNext[0].sourceSection, 'concept-map.questionsToSitWith'); + assert.match(singleThreadQuestion.brief.decisionReceipt.firstProofStep, /manual proof/i); + assert.equal(singleThreadQuestion.handoff.readiness.status, 'ready'); + const softDashLabelResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 830d9d6..411c18e 100644 --- a/server.js +++ b/server.js @@ -1505,14 +1505,6 @@ function optionsFromBody(body = {}) { actionThreadSource.sourceSection || 'threadsToHold', 'Thread to hold' ); - if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); - const closingNoteSource = [ - { text: closingNoteFromSource(conceptMap), sourceSection: 'concept-map.closingNote' }, - { text: closingNoteFromSource(snapshot), sourceSection: 'snapshot.closingNote' }, - { text: closingNoteFromSource(envelope), sourceSection: 'ranker-input.closingNote' }, - { text: closingNoteFromSource(featureSet), sourceSection: 'feature-set.closingNote' }, - { text: closingNoteFromSource(body), sourceSection: 'closingNote' }, - ].find(entry => entry.text) || { text: '', sourceSection: '' }; const questionSource = firstArraySource([ { items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' }, { items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' }, @@ -1525,6 +1517,20 @@ function optionsFromBody(body = {}) { questionSource.sourceSection || 'questionsToSitWith', 'Question to sit with' ); + if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); + if (actionThreadOptions.length === 1 && questionOptions.length) { + return normalizeCandidateGroup([ + { items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }, + { items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }, + ]); + } + const closingNoteSource = [ + { text: closingNoteFromSource(conceptMap), sourceSection: 'concept-map.closingNote' }, + { text: closingNoteFromSource(snapshot), sourceSection: 'snapshot.closingNote' }, + { text: closingNoteFromSource(envelope), sourceSection: 'ranker-input.closingNote' }, + { text: closingNoteFromSource(featureSet), sourceSection: 'feature-set.closingNote' }, + { text: closingNoteFromSource(body), sourceSection: 'closingNote' }, + ].find(entry => entry.text) || { text: '', sourceSection: '' }; const closingNoteOption = optionFromClosingNote( closingNoteSource.text, closingNoteSource.sourceSection || 'closingNote',