Carry Scattermind thread guardrails into Ranker

This commit is contained in:
OpenClaw Bot
2026-05-27 18:46:15 +02:00
parent ec27d13330
commit e463f4bc2a
2 changed files with 45 additions and 3 deletions
+32
View File
@@ -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' },
+13 -3
View File
@@ -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);
} }