From 4212b4d7c82bfd34df74fcfa84d1f1e2cf8ef81f Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 28 May 2026 00:38:31 +0200 Subject: [PATCH] Carry Concept Map thread signals into Ranker --- scripts/check-rank-feedback.mjs | 43 +++++++++++++++++++++++- server.js | 58 +++++++++++++++++++++------------ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index f083749..44b2629 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -240,6 +240,46 @@ try { assert.equal(softGuardrail.buildOrder.doFirst[0], 'decision-strip'); assert.ok(!/dashboard/i.test(softGuardrail.brief.quickGlance.topPick)); + const paidConceptMapThreadResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + referenceCode: 'SM-THREAD', + ideaText: 'A tired freelancer wants help deciding what to sell first without building a whole software platform.', + context: 'Solo builder. Manual proof first.', + fullReadingJson: JSON.stringify({ + working_name: 'Service Offer Compass', + opening_reflection: 'Start with one manual offer teardown before adding product machinery.', + lenses: { + channel: { + title: 'Build Order', + content: 'Build first: Run one manual service-offer teardown for a freelancer. Test manually: Ask three freelancers if the teardown changes what they would sell next. Defer: Save templates until the manual proof works.', + }, + question: { + title: 'Proof Steps', + content: 'Show the manual teardown to three freelancers and ask what they would do in the next 48 hours.', + }, + }, + threads_to_hold: [ + 'Success signal: two freelancers ask for the teardown or use it to change their offer.', + 'Failure signal: people like the advice but do not change what they sell next.', + 'Do not let this become a dashboard, account workspace, or template library before proof.', + ], + questions_to_sit_with: ['Who has a messy offer decision this week?'], + closing_note: 'Run one manual teardown in the next 48 hours.', + reference_code: 'SM-THREAD', + }), + }), + }); + assert.equal(paidConceptMapThreadResponse.status, 200); + const paidConceptMapThread = await paidConceptMapThreadResponse.json(); + assert.equal(paidConceptMapThread.input.provenance.artifactId, 'SM-THREAD'); + assert.ok(paidConceptMapThread.ranked.some(item => item.provenance.sourceSection === 'concept-map.threadsToHold'), 'paid Concept Map action threads should be ranked alongside Build Order text'); + assert.ok(paidConceptMapThread.buildOrderDetails.park.some(item => /dashboard|account workspace|template library/i.test(item.title)), 'do-not-let-this-become thread should be visible in Park, not lost behind the Build Order lens'); + assert.ok(paidConceptMapThread.input.decisionContext.nonGoals.some(item => /dashboard, account workspace/i.test(item)), 'thread guardrails should still become source non-goals'); + assert.equal(paidConceptMapThread.handoff.activeSlice.proof.successSignal, 'two freelancers ask for the teardown or use it to change their offer'); + assert.equal(paidConceptMapThread.handoff.activeSlice.proof.killSignal, 'people like the advice but do not change what they sell next'); + const snakeDecisionContextResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -2103,7 +2143,8 @@ try { assert.equal(storedScattermindRow.input.provenance.artifactId, 'SM-STORED-1'); assert.equal(storedScattermindRow.input.provenance.snapshotTitle, 'Stored Row Bridge'); assert.match(storedScattermindRow.input.provenance.originalPrompt, /actual stored row ranked/); - assert.equal(storedScattermindRow.input.optionCount, 5); + assert.equal(storedScattermindRow.input.optionCount, 6); + assert.ok(storedScattermindRow.ranked.some(item => item.provenance.sourceSection === 'concept-map.threadsToHold')); assert.equal(storedScattermindRow.ranked[0].id, 'build-order-1'); assert.equal(storedScattermindRow.ranked[0].provenance.sourceTitle, 'Build Order'); assert.match(storedScattermindRow.ranked[0].provenance.sourceQuote, /Stored-row build-order preview/); diff --git a/server.js b/server.js index d6b5cc4..3e42006 100644 --- a/server.js +++ b/server.js @@ -1663,21 +1663,6 @@ function optionsFromBody(body = {}) { questionSource.sourceSection || 'questionsToSitWith', 'Question to sit with' ); - if (buildOrderOptions.length) { - const proofLens = objectFrom(conceptMapLenses.question || conceptMapLenses.proof || conceptMapLenses.validation || conceptMapLenses.evidence); - const proofLensText = lensContent(conceptMapLenses.question) - || lensContent(conceptMapLenses.proof) - || lensContent(conceptMapLenses.validation) - || lensContent(conceptMapLenses.evidence) - || ''; - const proofSourceTitle = cleanText(proofLens.title || 'Proof Steps', 140); - const proofOptions = optionsFromProofLensText(proofLensText, 'concept-map.lenses.question', proofSourceTitle); - return normalizeCandidateGroup([ - { items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }, - ...(proofOptions.length ? [{ items: proofOptions, sourceSection: 'concept-map.lenses.question', defaultLane: 'validate-next' }] : []), - ...(questionOptions.length ? [{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }] : []), - ]); - } 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' }, @@ -1690,6 +1675,23 @@ function optionsFromBody(body = {}) { actionThreadSource.sourceSection || 'threadsToHold', 'Thread to hold' ); + if (buildOrderOptions.length) { + const proofLens = objectFrom(conceptMapLenses.question || conceptMapLenses.proof || conceptMapLenses.validation || conceptMapLenses.evidence); + const proofLensText = lensContent(conceptMapLenses.question) + || lensContent(conceptMapLenses.proof) + || lensContent(conceptMapLenses.validation) + || lensContent(conceptMapLenses.evidence) + || ''; + const proofSourceTitle = cleanText(proofLens.title || 'Proof Steps', 140); + const proofOptions = optionsFromProofLensText(proofLensText, 'concept-map.lenses.question', proofSourceTitle); + const supplementalActionThreadOptions = actionThreadOptions.filter(item => item.greenFlag || item.redFlag || ['defer', 'park'].includes(item.suggestedLane)); + return normalizeCandidateGroup([ + { items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }, + ...(supplementalActionThreadOptions.length ? [{ items: supplementalActionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }] : []), + ...(proofOptions.length ? [{ items: proofOptions, sourceSection: 'concept-map.lenses.question', defaultLane: 'validate-next' }] : []), + ...(questionOptions.length ? [{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }] : []), + ]); + } if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); if (actionThreadOptions.length === 1 && questionOptions.length) { return normalizeCandidateGroup([ @@ -1894,6 +1896,18 @@ function killSignalFor(option) { return 'People understand the idea but do not take, request, or value the next step.'; } +function carriedProofSignalFor(active, ranked = [], signalKey = 'successSignal') { + if (!active) return ''; + if (active.factors?.[signalKey]) return active.factors[signalKey]; + const threadSignal = ranked.find(item => ( + item.id !== active.id + && /threads(?:ToHold|_to_hold)?/i.test(item.provenance?.sourceSection || '') + && item.factors?.[signalKey] + )); + if (threadSignal?.factors?.[signalKey]) return threadSignal.factors[signalKey]; + return signalKey === 'killSignal' ? killSignalFor(active) : successSignalFor(active); +} + function scoringNotesFor(option) { const notes = []; const m = option.metrics || {}; @@ -1945,6 +1959,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision const activeSourceQuote = cleanText(top?.provenance?.sourceQuote || '', 260); const activeSourceTitle = cleanText(top?.provenance?.sourceTitle || '', 140); const activeProofScript = top ? proofScriptFor(top, provenance) : ''; + const activeSuccessSignal = carriedProofSignalFor(top, ranked, 'successSignal'); + const activeKillSignal = carriedProofSignalFor(top, ranked, 'killSignal'); const firstScreen = top ? { headline: `Build only this first: ${top.title}`, primaryAction: nextStepFor(top), @@ -1954,8 +1970,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision sourceAnchor: activeSourceAnchor, sourceTitle: activeSourceTitle, sourceQuote: activeSourceQuote, - passSignal: successSignalFor(top), - stopSignal: killSignalFor(top), + passSignal: activeSuccessSignal, + stopSignal: activeKillSignal, proofCadence: 'Run one tiny proof cycle, then rerank before adding surface area.', holdBack: deferred.slice(0, 3).map(item => ({ title: item.title, lane: item.lane?.label || 'Not now', reason: reasonFor(item) })), guardrails: (decisionContext?.nonGoals || []).slice(0, 3), @@ -1967,8 +1983,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision firstProofStep: nextStepFor(top), evidenceQuestion: evidenceQuestionFor(top), proofScript: activeProofScript, - passSignal: successSignalFor(top), - stopSignal: killSignalFor(top), + passSignal: activeSuccessSignal, + stopSignal: activeKillSignal, proofCadence: 'Run one tiny proof cycle, then rerank before adding surface area.', doNotStartYet: deferred.slice(0, 3).map(item => item.title), sourceAnchor: activeSourceAnchor, @@ -2136,8 +2152,8 @@ function activeSliceFor({ ranked = [], provenance = {}, readiness = {} }) { nextStep: nextStepFor(active), evidenceQuestion: evidenceQuestionFor(active), proofScript: proofScriptFor(active, provenance), - successSignal: successSignalFor(active), - killSignal: killSignalFor(active), + successSignal: carriedProofSignalFor(active, ranked, 'successSignal'), + killSignal: carriedProofSignalFor(active, ranked, 'killSignal'), }, source: { artifactId: provenance?.artifactId || '',