Carry Scattermind thread guardrails into Ranker
This commit is contained in:
@@ -198,6 +198,38 @@ 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 threadGuardrailResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceName: 'Scattermind',
|
||||||
|
artifactId: 'concept_map_thread_guardrails',
|
||||||
|
originalPrompt: 'Clarify a service-package idea and decide what should happen next.',
|
||||||
|
conceptMap: {
|
||||||
|
working_name: 'Service package first proof',
|
||||||
|
opening_reflection: 'The Concept Map says the next move is a small proof, not account machinery.',
|
||||||
|
lenses: {
|
||||||
|
channel: {
|
||||||
|
title: 'Build Order',
|
||||||
|
content: 'Build first: Saved workspace dashboard for every package and client. Build first: Manual package teardown preview. Test manually: Copyable decision brief. Defer: Client portal later.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
threads_to_hold: [
|
||||||
|
'Do not let this become accounts, saved dashboards, client portals, or a workspace system before the manual proof works.',
|
||||||
|
],
|
||||||
|
questions_to_sit_with: ['Will one freelancer act on the manual package teardown?'],
|
||||||
|
closing_note: 'Keep the first 48 hours manual and evidence-led.',
|
||||||
|
reference_code: 'SM-THREAD',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(threadGuardrailResponse.status, 200);
|
||||||
|
const threadGuardrail = await threadGuardrailResponse.json();
|
||||||
|
assert.ok(threadGuardrail.input.decisionContext.nonGoals.some(item => /Do not let this become accounts/i.test(item)), 'Concept Map threads_to_hold guardrails should be carried into decision context');
|
||||||
|
assert.equal(threadGuardrail.ranked.find(item => /Saved workspace dashboard/i.test(item.title)).lane.source, 'source-non-goal');
|
||||||
|
assert.equal(threadGuardrail.buildOrder.doFirst[0], 'build-order-2', 'thread-level guardrail should demote a labelled Build first dashboard and promote the first eligible manual move');
|
||||||
|
assert.match(threadGuardrail.brief.decisionReceipt.sourceAnchor, /concept-map\.lenses\.channel/);
|
||||||
|
|
||||||
const hintedResponse = await fetch(`${base}/api/rank-feedback`, {
|
const hintedResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -817,6 +817,10 @@ function contextGuardrailText(value = '') {
|
|||||||
return cleanMultiline(obj.summary || obj.description || obj.notes || obj.brief || obj.text || '', 3000);
|
return cleanMultiline(obj.summary || obj.description || obj.notes || obj.brief || obj.text || '', 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function guardrailTextItems(value = [], maxItems = 8) {
|
||||||
|
return cleanFlexibleTextList(value, maxItems, 260).filter(item => /\b(avoid|no|do not|don't|dont|must not|never|non-goal|non goal|not yet|out of scope|defer|hold for later|set aside|probably noise|park|do not let this become|don't let this become|not a dashboard|dashboard swamp)\b/i.test(item));
|
||||||
|
}
|
||||||
|
|
||||||
function cleanDecisionContext(input = {}) {
|
function cleanDecisionContext(input = {}) {
|
||||||
const envelope = bridgeEnvelopeFrom(input);
|
const envelope = bridgeEnvelopeFrom(input);
|
||||||
const featureSet = featureSetFrom(input);
|
const featureSet = featureSetFrom(input);
|
||||||
@@ -852,8 +856,14 @@ function cleanDecisionContext(input = {}) {
|
|||||||
contextGuardrailText(conceptMap.context || ''),
|
contextGuardrailText(conceptMap.context || ''),
|
||||||
lensContent(riskLens),
|
lensContent(riskLens),
|
||||||
lensContent(constraintsLens),
|
lensContent(constraintsLens),
|
||||||
conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || '',
|
conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || conceptMap.closing_note || conceptMap.closingNote || '',
|
||||||
snapshot.risk || snapshot.whatNotToBuildYet || snapshot.notYet || '',
|
snapshot.risk || snapshot.whatNotToBuildYet || snapshot.notYet || snapshot.closing_note || snapshot.closingNote || '',
|
||||||
|
envelope.closing_note || envelope.closingNote || featureSet.closing_note || featureSet.closingNote || input.closing_note || input.closingNote || '',
|
||||||
|
...guardrailTextItems(conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, 8),
|
||||||
|
...guardrailTextItems(snapshot.threads_to_hold || snapshot.threadsToHold || snapshot.actionThreads || snapshot.action_threads, 8),
|
||||||
|
...guardrailTextItems(envelope.threads_to_hold || envelope.threadsToHold || envelope.actionThreads || envelope.action_threads, 8),
|
||||||
|
...guardrailTextItems(featureSet.threads_to_hold || featureSet.threadsToHold || featureSet.actionThreads || featureSet.action_threads, 8),
|
||||||
|
...guardrailTextItems(input.threads_to_hold || input.threadsToHold || input.actionThreads || input.action_threads, 8),
|
||||||
].filter(Boolean).join('\n'));
|
].filter(Boolean).join('\n'));
|
||||||
return {
|
return {
|
||||||
targetAudience: cleanText(input.targetAudience || input.target_audience || envelope.targetAudience || envelope.target_audience || featureSet.targetAudience || featureSet.target_audience || snapshot.targetAudience || snapshot.target_audience || firstContextText(contextSources, ['targetAudience', 'target_audience', 'audience', 'who', 'whoItHelps', 'who_it_helps', 'customer', 'users']) || conceptMap.targetAudience || conceptMap.target_audience || lensContent(audienceLens), 180),
|
targetAudience: cleanText(input.targetAudience || input.target_audience || envelope.targetAudience || envelope.target_audience || featureSet.targetAudience || featureSet.target_audience || snapshot.targetAudience || snapshot.target_audience || firstContextText(contextSources, ['targetAudience', 'target_audience', 'audience', 'who', 'whoItHelps', 'who_it_helps', 'customer', 'users']) || conceptMap.targetAudience || conceptMap.target_audience || lensContent(audienceLens), 180),
|
||||||
@@ -889,7 +899,7 @@ function cleanContextText(value = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function meaningfulTokens(text = '') {
|
function meaningfulTokens(text = '') {
|
||||||
const stop = new Set(['the', 'and', 'with', 'from', 'that', 'this', 'into', 'before', 'after', 'until', 'user', 'users', 'idea', 'ideas', 'build', 'order', 'works', 'first', 'avoid', 'without', 'more', 'full', 'proof', 'manual', 'value', 'layer', 'result', 'sense', 'copyable', 'source', 'traced', 'source-traced', 'evidence']);
|
const stop = new Set(['the', 'and', 'with', 'from', 'that', 'this', 'into', 'before', 'after', 'until', 'user', 'users', 'idea', 'ideas', 'build', 'order', 'works', 'first', 'avoid', 'without', 'more', 'full', 'proof', 'manual', 'value', 'layer', 'result', 'sense', 'copyable', 'source', 'traced', 'source-traced', 'evidence', 'thread', 'threads']);
|
||||||
return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8);
|
return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user