Carry Concept Map thread signals into Ranker

This commit is contained in:
OpenClaw Bot
2026-05-28 00:38:31 +02:00
parent 066717221c
commit 4212b4d7c8
2 changed files with 79 additions and 22 deletions
+42 -1
View File
@@ -240,6 +240,46 @@ try {
assert.equal(softGuardrail.buildOrder.doFirst[0], 'decision-strip'); assert.equal(softGuardrail.buildOrder.doFirst[0], 'decision-strip');
assert.ok(!/dashboard/i.test(softGuardrail.brief.quickGlance.topPick)); 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`, { const snakeDecisionContextResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -2103,7 +2143,8 @@ try {
assert.equal(storedScattermindRow.input.provenance.artifactId, 'SM-STORED-1'); assert.equal(storedScattermindRow.input.provenance.artifactId, 'SM-STORED-1');
assert.equal(storedScattermindRow.input.provenance.snapshotTitle, 'Stored Row Bridge'); assert.equal(storedScattermindRow.input.provenance.snapshotTitle, 'Stored Row Bridge');
assert.match(storedScattermindRow.input.provenance.originalPrompt, /actual stored row ranked/); 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].id, 'build-order-1');
assert.equal(storedScattermindRow.ranked[0].provenance.sourceTitle, 'Build Order'); assert.equal(storedScattermindRow.ranked[0].provenance.sourceTitle, 'Build Order');
assert.match(storedScattermindRow.ranked[0].provenance.sourceQuote, /Stored-row build-order preview/); assert.match(storedScattermindRow.ranked[0].provenance.sourceQuote, /Stored-row build-order preview/);
+37 -21
View File
@@ -1663,21 +1663,6 @@ function optionsFromBody(body = {}) {
questionSource.sourceSection || 'questionsToSitWith', questionSource.sourceSection || 'questionsToSitWith',
'Question to sit with' '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([ const actionThreadSource = firstArraySource([
{ items: conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, sourceSection: 'concept-map.threadsToHold' }, { 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: snapshot.threads_to_hold || snapshot.threadsToHold || snapshot.actionThreads || snapshot.action_threads, sourceSection: 'snapshot.threadsToHold' },
@@ -1690,6 +1675,23 @@ function optionsFromBody(body = {}) {
actionThreadSource.sourceSection || 'threadsToHold', actionThreadSource.sourceSection || 'threadsToHold',
'Thread to hold' '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 >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]);
if (actionThreadOptions.length === 1 && questionOptions.length) { if (actionThreadOptions.length === 1 && questionOptions.length) {
return normalizeCandidateGroup([ return normalizeCandidateGroup([
@@ -1894,6 +1896,18 @@ function killSignalFor(option) {
return 'People understand the idea but do not take, request, or value the next step.'; 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) { function scoringNotesFor(option) {
const notes = []; const notes = [];
const m = option.metrics || {}; const m = option.metrics || {};
@@ -1945,6 +1959,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
const activeSourceQuote = cleanText(top?.provenance?.sourceQuote || '', 260); const activeSourceQuote = cleanText(top?.provenance?.sourceQuote || '', 260);
const activeSourceTitle = cleanText(top?.provenance?.sourceTitle || '', 140); const activeSourceTitle = cleanText(top?.provenance?.sourceTitle || '', 140);
const activeProofScript = top ? proofScriptFor(top, provenance) : ''; const activeProofScript = top ? proofScriptFor(top, provenance) : '';
const activeSuccessSignal = carriedProofSignalFor(top, ranked, 'successSignal');
const activeKillSignal = carriedProofSignalFor(top, ranked, 'killSignal');
const firstScreen = top ? { const firstScreen = top ? {
headline: `Build only this first: ${top.title}`, headline: `Build only this first: ${top.title}`,
primaryAction: nextStepFor(top), primaryAction: nextStepFor(top),
@@ -1954,8 +1970,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
sourceAnchor: activeSourceAnchor, sourceAnchor: activeSourceAnchor,
sourceTitle: activeSourceTitle, sourceTitle: activeSourceTitle,
sourceQuote: activeSourceQuote, sourceQuote: activeSourceQuote,
passSignal: successSignalFor(top), passSignal: activeSuccessSignal,
stopSignal: killSignalFor(top), stopSignal: activeKillSignal,
proofCadence: 'Run one tiny proof cycle, then rerank before adding surface area.', 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) })), 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), guardrails: (decisionContext?.nonGoals || []).slice(0, 3),
@@ -1967,8 +1983,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
firstProofStep: nextStepFor(top), firstProofStep: nextStepFor(top),
evidenceQuestion: evidenceQuestionFor(top), evidenceQuestion: evidenceQuestionFor(top),
proofScript: activeProofScript, proofScript: activeProofScript,
passSignal: successSignalFor(top), passSignal: activeSuccessSignal,
stopSignal: killSignalFor(top), stopSignal: activeKillSignal,
proofCadence: 'Run one tiny proof cycle, then rerank before adding surface area.', proofCadence: 'Run one tiny proof cycle, then rerank before adding surface area.',
doNotStartYet: deferred.slice(0, 3).map(item => item.title), doNotStartYet: deferred.slice(0, 3).map(item => item.title),
sourceAnchor: activeSourceAnchor, sourceAnchor: activeSourceAnchor,
@@ -2136,8 +2152,8 @@ function activeSliceFor({ ranked = [], provenance = {}, readiness = {} }) {
nextStep: nextStepFor(active), nextStep: nextStepFor(active),
evidenceQuestion: evidenceQuestionFor(active), evidenceQuestion: evidenceQuestionFor(active),
proofScript: proofScriptFor(active, provenance), proofScript: proofScriptFor(active, provenance),
successSignal: successSignalFor(active), successSignal: carriedProofSignalFor(active, ranked, 'successSignal'),
killSignal: killSignalFor(active), killSignal: carriedProofSignalFor(active, ranked, 'killSignal'),
}, },
source: { source: {
artifactId: provenance?.artifactId || '', artifactId: provenance?.artifactId || '',