Carry Concept Map thread signals into Ranker
This commit is contained in:
@@ -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/);
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user