Harden Scattermind summary handoff provenance

This commit is contained in:
OpenClaw Bot
2026-05-27 15:23:34 +02:00
parent e22cb30061
commit 470965b8b7
3 changed files with 53 additions and 10 deletions
+19 -9
View File
@@ -594,6 +594,7 @@ function cleanProvenance(input = {}) {
const conceptMap = objectFrom(input.conceptMap || input.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map);
const snapshot = objectFrom(input.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot);
const source = objectFrom(input.source || featureSet.source || artifact.source);
const sourceSummary = input.sourceSummary || input.source_summary || input.opening_reflection || input.restated_idea || artifact.sourceSummary || artifact.source_summary || artifact.opening_reflection || snapshot.sourceSummary || snapshot.source_summary || snapshot.restated_idea || conceptMap.sourceSummary || conceptMap.source_summary || conceptMap.opening_reflection || conceptMap.restated_idea || '';
return {
schema: cleanText(input.schema || featureSet.schema || artifact.schema || input.type || 'prioritix-feature-set-v1', 80),
source: cleanText(input.sourceName || input.source_name || featureSet.sourceName || featureSet.source_name || source.name || artifact.sourceName || artifact.source_name || 'Scattermind', 80),
@@ -601,6 +602,7 @@ function cleanProvenance(input = {}) {
snapshotTitle: cleanText(input.snapshotTitle || input.snapshot_title || input.working_name || input.workingName || artifact.snapshotTitle || artifact.snapshot_title || snapshot.title || snapshot.name || conceptMap.snapshotTitle || conceptMap.snapshot_title || conceptMap.working_name || conceptMap.workingName || input.ideaTitle || input.idea_title || '', 160),
conceptMapId: cleanText(input.conceptMapId || input.concept_map_id || artifact.conceptMapId || artifact.concept_map_id || conceptMap.id || conceptMap.artifactId || conceptMap.artifact_id || input.referenceCode || input.reference_code || conceptMap.referenceCode || conceptMap.reference_code || '', 120),
originalPrompt: cleanMultiline(input.originalPrompt || input.original_prompt || input.initialPrompt || input.initial_prompt || input.ideaText || input.idea_text || input.prompt || artifact.originalPrompt || artifact.original_prompt || source.originalPrompt || source.original_prompt || snapshot.originalPrompt || snapshot.original_prompt || snapshot.prompt || conceptMap.originalPrompt || conceptMap.original_prompt || conceptMap.ideaText || conceptMap.idea_text || input.idea || '', 1200),
sourceSummary: cleanMultiline(sourceSummary, 1200),
};
}
@@ -802,20 +804,20 @@ function normalizeCandidateGroup(group = []) {
function sentenceFragments(text = '') {
return cleanMultiline(text, 4000)
.replace(/\s+(build first|start here|ship first|continue first|make tangible first|make tangible|try next|evidence next|learn next|test manually|validate next|hold for later|not yet|defer|set aside|out of scope|probably noise|park|do not build yet|don't build yet)\s*:/gi, '\n$1:')
.replace(/\s+(build first|start here|ship first|first week|week one|first-week build order|continue first|make tangible first|make tangible|try next|evidence next|learn next|test manually|validate next|hold for later|not yet|defer|set aside|out of scope|probably noise|park|do not build yet|don't build yet)\s*:/gi, '\n$1:')
.split(/\n|;|\s+[•-]\s+/)
.map(part => part.trim())
.filter(Boolean);
}
function titleFromBuildOrderFragment(value = '') {
const cleaned = cleanText(value.replace(/^(build first|start here|ship first|continue first|make tangible first|make tangible|try next|evidence next|learn next|test manually|validate next|hold for later|not yet|defer|set aside|out of scope|probably noise|park|do not build yet|don't build yet)\s*:\s*/i, ''), 220);
const cleaned = cleanText(value.replace(/^(build first|start here|ship first|first week|week one|first-week build order|continue first|make tangible first|make tangible|try next|evidence next|learn next|test manually|validate next|hold for later|not yet|defer|set aside|out of scope|probably noise|park|do not build yet|don't build yet)\s*:\s*/i, ''), 220);
const first = cleaned.split(/\s[-–—:]\s|\.\s/)[0] || cleaned;
return cleanText(first, 120);
}
function laneFromBuildOrderLabel(fragment = '') {
if (/^(build first|start here|ship first|continue first|make tangible first|make tangible)\s*:/i.test(fragment)) return 'do-first';
if (/^(build first|start here|ship first|first week|week one|first-week build order|continue first|make tangible first|make tangible)\s*:/i.test(fragment)) return 'do-first';
if (/^(try next|evidence next|learn next|test manually|validate next)\s*:/i.test(fragment)) return 'validate-next';
if (/^(hold for later|not yet|defer|do not build yet|don't build yet)\s*:/i.test(fragment)) return 'defer';
if (/^(set aside|out of scope|probably noise|park)\s*:/i.test(fragment)) return 'park';
@@ -1152,6 +1154,7 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision
snapshotTitle: provenance.snapshotTitle,
conceptMapId: provenance.conceptMapId,
originalPromptExcerpt: cleanText(provenance.originalPrompt, 260),
sourceSummaryExcerpt: cleanText(provenance.sourceSummary, 260),
} : null,
expertReflections: [
{
@@ -1226,7 +1229,7 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
const uniqueWarnings = [...new Set(warnings)];
const activeItems = ranked.filter(item => ['do', 'test'].includes(item.lane?.id));
const guardrailWarnings = uniqueWarnings.filter(item => /^active item .* conflicts with source non-goals/i.test(item));
const sourceWarnings = uniqueWarnings.filter(item => /^missing (source section|original prompt provenance)/i.test(item));
const sourceWarnings = uniqueWarnings.filter(item => /^missing (source section|source context provenance)/i.test(item));
const evidenceWarnings = uniqueWarnings.filter(item => /^missing evidence needed for active item/i.test(item));
const duplicateWarnings = uniqueWarnings.filter(item => /^duplicate source id/i.test(item));
const missingArtifact = uniqueWarnings.includes('missing source artifact id');
@@ -1238,7 +1241,7 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
const nextChecks = [];
if (guardrailWarnings.length) nextChecks.push('Resolve source non-goal conflicts before treating any active item as build-ready.');
if (expectsSourceTrace && sourceWarnings.length) nextChecks.push('Attach source section / prompt provenance from the Scattermind artifact.');
if (expectsSourceTrace && sourceWarnings.length) nextChecks.push('Attach source section / prompt or summary provenance from the Scattermind artifact.');
if (expectsSourceTrace && evidenceWarnings.length) nextChecks.push('Add evidenceNeeded to every Do first / Validate next candidate before handoff.');
if (missingArtifact) nextChecks.push(expectsSourceTrace ? 'Attach the Scattermind artifact id so the build order can be traced back.' : 'Attach a source artifact id if this came from Scattermind rather than a plain paste.');
if (duplicateWarnings.length) nextChecks.push('Review normalized duplicate IDs before downstream tools store references.');
@@ -1272,7 +1275,7 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
nextChecks: nextChecks.slice(0, 5),
activeItemCount: activeItems.length,
warningCount: uniqueWarnings.length,
sourceComplete: Boolean(!expectsSourceTrace || (provenance?.originalPrompt && activeItems.every(item => item.provenance?.sourceSection))),
sourceComplete: Boolean(!expectsSourceTrace || ((provenance?.originalPrompt || provenance?.sourceSummary) && activeItems.every(item => item.provenance?.sourceSection))),
};
}
@@ -1288,6 +1291,11 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
provenance?.snapshotTitle,
provenance?.artifactId,
].filter(Boolean).join(' · ');
const sourceContextLine = provenance?.originalPrompt
? `Original prompt: ${cleanText(provenance.originalPrompt, 240)}`
: provenance?.sourceSummary
? `Source summary: ${cleanText(provenance.sourceSummary, 240)}`
: '';
const contextLines = [
decisionContext?.targetAudience ? `Audience: ${decisionContext.targetAudience}` : '',
...(decisionContext?.constraints || []).slice(0, 3).map(item => `Constraint: ${item}`),
@@ -1316,7 +1324,7 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
return [
'# Ranker build-order handoff',
sourceLine ? `Source: ${sourceLine}` : '',
provenance?.originalPrompt ? `Original prompt: ${cleanText(provenance.originalPrompt, 240)}` : '',
sourceContextLine,
readiness?.status ? `Readiness: ${readiness.status}${readiness.summary || ''}` : '',
contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '',
...itemLines,
@@ -1328,7 +1336,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
const warnings = [];
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
if (!provenance?.artifactId && provenance?.originalPrompt) warnings.push('missing source artifact id');
if (expectsSourceTrace && !provenance?.originalPrompt) warnings.push('missing original prompt provenance');
if (expectsSourceTrace && !provenance?.originalPrompt && !provenance?.sourceSummary) warnings.push('missing source context provenance');
const itemTrace = ranked.map(item => {
if (expectsSourceTrace && !item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`);
@@ -1364,7 +1372,9 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
snapshotTitle: provenance?.snapshotTitle || '',
conceptMapId: provenance?.conceptMapId || '',
originalPromptExcerpt: cleanText(provenance?.originalPrompt || '', 260),
sourceSummaryExcerpt: cleanText(provenance?.sourceSummary || '', 260),
hasOriginalPrompt: Boolean(provenance?.originalPrompt),
hasSourceSummary: Boolean(provenance?.sourceSummary),
requiresSourceTrace: expectsSourceTrace,
},
decisionContext: {
@@ -1382,7 +1392,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
app.post('/api/rank-feedback', (req, res) => {
const body = expandEmbeddedRankPayload(req.body || {});
const idea = cleanMultiline(body?.idea || '', 3000);
const idea = cleanMultiline(body?.idea || body?.opening_reflection || body?.restated_idea || '', 3000);
const context = cleanContextText(body?.context || '');
const modeId = cleanText(body?.mode || 'progress', 40);
const mode = judgementModes[modeId] || judgementModes.progress;