From 080f35e2304bba5d6a291b90026726f0281b2931 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 00:43:28 +0200 Subject: [PATCH] Expose prompt and source trace in rank handoff --- README.md | 2 +- scripts/check-rank-feedback.mjs | 6 ++++++ server.js | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a54155d..fb2717f 100644 --- a/README.md +++ b/README.md @@ -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 `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 Concept Map 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`. 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`. 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. Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen. diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 6a8ea4a..6d762c4 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -80,9 +80,11 @@ try { assert.match(data.ranked.find(item => item.id === 'bridge-contract').factors.evidenceNeeded, /Concept Map/); assert.ok(data.ranked.find(item => item.id === 'bridge-contract').factors.metricHints.value === undefined); assert.equal(data.brief.source.artifactId, 'snapshot_123'); + assert.match(data.brief.source.originalPromptExcerpt, /tiny shop idea/); assert.match(data.brief.summary, /Source: Tiny shop idea clarity pass · snapshot_123/); assert.equal(data.handoff.schema, 'rank-feedback-result-v1'); assert.equal(data.handoff.source.artifactId, 'snapshot_123'); + assert.match(data.handoff.source.originalPromptExcerpt, /tiny shop idea/); assert.equal(data.handoff.source.hasOriginalPrompt, true); assert.equal(data.handoff.source.requiresSourceTrace, true); assert.equal(data.handoff.itemTrace.length, data.ranked.length); @@ -662,6 +664,10 @@ try { assert.equal(sourceExcerpt.ranked[0].provenance.sourceId, 'lens-channel-1'); assert.equal(sourceExcerpt.ranked[0].provenance.sourceTitle, 'Build Order'); assert.match(sourceExcerpt.ranked[0].provenance.sourceQuote, /Build first/); + assert.match(sourceExcerpt.brief.source.originalPromptExcerpt, /source excerpts/); + assert.equal(sourceExcerpt.brief.quickGlance.sourceTrace.sourceId, 'lens-channel-1'); + assert.match(sourceExcerpt.brief.quickGlance.sourceTrace.sourceQuote, /defended next move/); + assert.equal(sourceExcerpt.handoff.source.originalPromptExcerpt, 'Scattermind exported source excerpts for each recommended move.'); assert.equal(sourceExcerpt.buildOrderDetails.doFirst[0].sourceId, 'lens-channel-1'); assert.match(sourceExcerpt.buildOrderDetails.doFirst[0].sourceQuote, /defended next move/); assert.equal(sourceExcerpt.handoff.itemTrace.find(item => item.id === 'copyable-brief').sourceTitle, 'Build Order'); diff --git a/server.js b/server.js index ddc8cb1..78d2da0 100644 --- a/server.js +++ b/server.js @@ -1037,6 +1037,12 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision evidenceQuestion: evidenceQuestionFor(top), biggestTrap: concernFor(top), doNotBuildYet: deferred.slice(0, 2).map(item => item.title), + sourceTrace: { + sourceSection: top.provenance?.sourceSection || '', + sourceId: top.provenance?.sourceId || '', + sourceTitle: top.provenance?.sourceTitle || '', + sourceQuote: top.provenance?.sourceQuote || '', + }, } : null; const assumptions = [ ...(decisionContext?.assumptions || []), @@ -1053,6 +1059,7 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision artifactId: provenance.artifactId, snapshotTitle: provenance.snapshotTitle, conceptMapId: provenance.conceptMapId, + originalPromptExcerpt: cleanText(provenance.originalPrompt, 260), } : null, expertReflections: [ { @@ -1160,6 +1167,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) { artifactId: provenance?.artifactId || '', snapshotTitle: provenance?.snapshotTitle || '', conceptMapId: provenance?.conceptMapId || '', + originalPromptExcerpt: cleanText(provenance?.originalPrompt || '', 260), hasOriginalPrompt: Boolean(provenance?.originalPrompt), requiresSourceTrace: expectsSourceTrace, },