Harden Scattermind summary handoff provenance
This commit is contained in:
@@ -49,7 +49,7 @@ Ranker's continuation job is narrow:
|
|||||||
|
|
||||||
Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `snapshot.context` or `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper artifact guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first.
|
Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `snapshot.context` or `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper artifact guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first.
|
||||||
|
|
||||||
Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` and `handoff.source.originalPromptExcerpt` carry a short prompt excerpt so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from.
|
Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` / `handoff.source.originalPromptExcerpt` or, when the original prompt is unavailable, `sourceSummaryExcerpt` carry the source context so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from.
|
||||||
|
|
||||||
Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Hold for later`, or `Set aside`; Build Order objects can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `holdForLater`, and `setAside`. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote`.
|
Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Hold for later`, or `Set aside`; Build Order objects can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `holdForLater`, and `setAside`. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote`.
|
||||||
|
|
||||||
|
|||||||
@@ -649,6 +649,39 @@ try {
|
|||||||
assert.equal(softLabelLens.handoff.readiness.status, 'ready');
|
assert.equal(softLabelLens.handoff.readiness.status, 'ready');
|
||||||
assert.deepEqual(softLabelLens.handoff.warnings, []);
|
assert.deepEqual(softLabelLens.handoff.warnings, []);
|
||||||
|
|
||||||
|
const summaryOnlyConceptMapResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
reference_code: 'SM-SUMMARY-ONLY',
|
||||||
|
working_name: 'Tired maker continuation pass',
|
||||||
|
opening_reflection: 'A tired maker needs one defended first-week build order after Scattermind clarified the idea.',
|
||||||
|
context: 'Solo builder. Manual proof first. Avoid accounts and saved workspaces before the build order lands.',
|
||||||
|
mode: 'mvp',
|
||||||
|
lenses: {
|
||||||
|
risk: 'Avoid accounts and saved workspaces before the first copyable result works.',
|
||||||
|
channel: 'First week: Manual source-traced preview - turn the Concept Map into one defended next move. Evidence next: Copyable handoff comprehension check - ask three tired users what they would do next. Hold for later: Visual polish after comprehension proof. Set aside: Saved account workspace with dashboard and billing.',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(summaryOnlyConceptMapResponse.status, 200);
|
||||||
|
const summaryOnlyConceptMap = await summaryOnlyConceptMapResponse.json();
|
||||||
|
assert.equal(summaryOnlyConceptMap.input.idea, 'A tired maker needs one defended first-week build order after Scattermind clarified the idea.');
|
||||||
|
assert.equal(summaryOnlyConceptMap.input.provenance.artifactId, 'SM-SUMMARY-ONLY');
|
||||||
|
assert.equal(summaryOnlyConceptMap.input.provenance.snapshotTitle, 'Tired maker continuation pass');
|
||||||
|
assert.match(summaryOnlyConceptMap.input.provenance.sourceSummary, /first-week build order/);
|
||||||
|
assert.equal(summaryOnlyConceptMap.input.provenance.originalPrompt, '');
|
||||||
|
assert.equal(summaryOnlyConceptMap.ranked[0].lane.id, 'do');
|
||||||
|
assert.match(summaryOnlyConceptMap.ranked[0].provenance.sourceQuote, /First week: Manual source-traced preview/);
|
||||||
|
assert.equal(summaryOnlyConceptMap.ranked.find(item => item.id === 'build-order-2').lane.id, 'test');
|
||||||
|
assert.equal(summaryOnlyConceptMap.handoff.source.hasOriginalPrompt, false);
|
||||||
|
assert.equal(summaryOnlyConceptMap.handoff.source.hasSourceSummary, true);
|
||||||
|
assert.equal(summaryOnlyConceptMap.handoff.readiness.status, 'ready');
|
||||||
|
assert.equal(summaryOnlyConceptMap.handoff.readiness.sourceComplete, true);
|
||||||
|
assert.deepEqual(summaryOnlyConceptMap.handoff.warnings, []);
|
||||||
|
assert.match(summaryOnlyConceptMap.handoff.copyableText, /Source summary: A tired maker needs one defended first-week build order/);
|
||||||
|
assert.match(summaryOnlyConceptMap.brief.source.sourceSummaryExcerpt, /first-week build order/);
|
||||||
|
|
||||||
const softLabelObjectResponse = await fetch(`${base}/api/rank-feedback`, {
|
const softLabelObjectResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -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 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 snapshot = objectFrom(input.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot);
|
||||||
const source = objectFrom(input.source || featureSet.source || artifact.source);
|
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 {
|
return {
|
||||||
schema: cleanText(input.schema || featureSet.schema || artifact.schema || input.type || 'prioritix-feature-set-v1', 80),
|
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),
|
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),
|
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),
|
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),
|
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 = '') {
|
function sentenceFragments(text = '') {
|
||||||
return cleanMultiline(text, 4000)
|
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+/)
|
.split(/\n|;|\s+[•-]\s+/)
|
||||||
.map(part => part.trim())
|
.map(part => part.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function titleFromBuildOrderFragment(value = '') {
|
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;
|
const first = cleaned.split(/\s[-–—:]\s|\.\s/)[0] || cleaned;
|
||||||
return cleanText(first, 120);
|
return cleanText(first, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
function laneFromBuildOrderLabel(fragment = '') {
|
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 (/^(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 (/^(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';
|
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,
|
snapshotTitle: provenance.snapshotTitle,
|
||||||
conceptMapId: provenance.conceptMapId,
|
conceptMapId: provenance.conceptMapId,
|
||||||
originalPromptExcerpt: cleanText(provenance.originalPrompt, 260),
|
originalPromptExcerpt: cleanText(provenance.originalPrompt, 260),
|
||||||
|
sourceSummaryExcerpt: cleanText(provenance.sourceSummary, 260),
|
||||||
} : null,
|
} : null,
|
||||||
expertReflections: [
|
expertReflections: [
|
||||||
{
|
{
|
||||||
@@ -1226,7 +1229,7 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
|
|||||||
const uniqueWarnings = [...new Set(warnings)];
|
const uniqueWarnings = [...new Set(warnings)];
|
||||||
const activeItems = ranked.filter(item => ['do', 'test'].includes(item.lane?.id));
|
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 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 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 duplicateWarnings = uniqueWarnings.filter(item => /^duplicate source id/i.test(item));
|
||||||
const missingArtifact = uniqueWarnings.includes('missing source artifact id');
|
const missingArtifact = uniqueWarnings.includes('missing source artifact id');
|
||||||
@@ -1238,7 +1241,7 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
|
|||||||
const nextChecks = [];
|
const nextChecks = [];
|
||||||
|
|
||||||
if (guardrailWarnings.length) nextChecks.push('Resolve source non-goal conflicts before treating any active item as build-ready.');
|
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 (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 (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.');
|
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),
|
nextChecks: nextChecks.slice(0, 5),
|
||||||
activeItemCount: activeItems.length,
|
activeItemCount: activeItems.length,
|
||||||
warningCount: uniqueWarnings.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?.snapshotTitle,
|
||||||
provenance?.artifactId,
|
provenance?.artifactId,
|
||||||
].filter(Boolean).join(' · ');
|
].filter(Boolean).join(' · ');
|
||||||
|
const sourceContextLine = provenance?.originalPrompt
|
||||||
|
? `Original prompt: ${cleanText(provenance.originalPrompt, 240)}`
|
||||||
|
: provenance?.sourceSummary
|
||||||
|
? `Source summary: ${cleanText(provenance.sourceSummary, 240)}`
|
||||||
|
: '';
|
||||||
const contextLines = [
|
const contextLines = [
|
||||||
decisionContext?.targetAudience ? `Audience: ${decisionContext.targetAudience}` : '',
|
decisionContext?.targetAudience ? `Audience: ${decisionContext.targetAudience}` : '',
|
||||||
...(decisionContext?.constraints || []).slice(0, 3).map(item => `Constraint: ${item}`),
|
...(decisionContext?.constraints || []).slice(0, 3).map(item => `Constraint: ${item}`),
|
||||||
@@ -1316,7 +1324,7 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
|
|||||||
return [
|
return [
|
||||||
'# Ranker build-order handoff',
|
'# Ranker build-order handoff',
|
||||||
sourceLine ? `Source: ${sourceLine}` : '',
|
sourceLine ? `Source: ${sourceLine}` : '',
|
||||||
provenance?.originalPrompt ? `Original prompt: ${cleanText(provenance.originalPrompt, 240)}` : '',
|
sourceContextLine,
|
||||||
readiness?.status ? `Readiness: ${readiness.status} — ${readiness.summary || ''}` : '',
|
readiness?.status ? `Readiness: ${readiness.status} — ${readiness.summary || ''}` : '',
|
||||||
contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '',
|
contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '',
|
||||||
...itemLines,
|
...itemLines,
|
||||||
@@ -1328,7 +1336,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
|
|||||||
const warnings = [];
|
const warnings = [];
|
||||||
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
|
const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle);
|
||||||
if (!provenance?.artifactId && provenance?.originalPrompt) warnings.push('missing source artifact id');
|
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 => {
|
const itemTrace = ranked.map(item => {
|
||||||
if (expectsSourceTrace && !item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`);
|
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 || '',
|
snapshotTitle: provenance?.snapshotTitle || '',
|
||||||
conceptMapId: provenance?.conceptMapId || '',
|
conceptMapId: provenance?.conceptMapId || '',
|
||||||
originalPromptExcerpt: cleanText(provenance?.originalPrompt || '', 260),
|
originalPromptExcerpt: cleanText(provenance?.originalPrompt || '', 260),
|
||||||
|
sourceSummaryExcerpt: cleanText(provenance?.sourceSummary || '', 260),
|
||||||
hasOriginalPrompt: Boolean(provenance?.originalPrompt),
|
hasOriginalPrompt: Boolean(provenance?.originalPrompt),
|
||||||
|
hasSourceSummary: Boolean(provenance?.sourceSummary),
|
||||||
requiresSourceTrace: expectsSourceTrace,
|
requiresSourceTrace: expectsSourceTrace,
|
||||||
},
|
},
|
||||||
decisionContext: {
|
decisionContext: {
|
||||||
@@ -1382,7 +1392,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
|
|||||||
|
|
||||||
app.post('/api/rank-feedback', (req, res) => {
|
app.post('/api/rank-feedback', (req, res) => {
|
||||||
const body = expandEmbeddedRankPayload(req.body || {});
|
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 context = cleanContextText(body?.context || '');
|
||||||
const modeId = cleanText(body?.mode || 'progress', 40);
|
const modeId = cleanText(body?.mode || 'progress', 40);
|
||||||
const mode = judgementModes[modeId] || judgementModes.progress;
|
const mode = judgementModes[modeId] || judgementModes.progress;
|
||||||
|
|||||||
Reference in New Issue
Block a user