Clarify rank feedback source trace contract

This commit is contained in:
OpenClaw Bot
2026-05-27 00:15:17 +02:00
parent 602937d9b2
commit 771b5e7c02
3 changed files with 9 additions and 5 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ Ranker's continuation job is narrow:
Candidate items may include optional 110 `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.
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.
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.
Recommended payload shape:
+2
View File
@@ -84,6 +84,7 @@ try {
assert.equal(data.handoff.schema, 'rank-feedback-result-v1');
assert.equal(data.handoff.source.artifactId, 'snapshot_123');
assert.equal(data.handoff.source.hasOriginalPrompt, true);
assert.equal(data.handoff.source.requiresSourceTrace, true);
assert.equal(data.handoff.itemTrace.length, data.ranked.length);
assert.equal(data.handoff.itemTrace.find(item => item.id === 'bridge-contract').sourceSection, 'concept-map.nextMoves');
assert.ok(data.input.decisionContext.constraints.includes('Solo builder'));
@@ -113,6 +114,7 @@ try {
assert.ok(messyIdeaOnly.ranked.some(item => /Pricing calculator/i.test(item.title)));
assert.equal(messyIdeaOnly.ranked.find(item => /dashboard/i.test(item.title)).lane.source, 'source-non-goal');
assert.ok(!/dashboard/i.test(messyIdeaOnly.ranked[0].title), 'dashboard-flavored candidate must not win tired-user first pass');
assert.equal(messyIdeaOnly.handoff.source.requiresSourceTrace, false);
assert.ok(!messyIdeaOnly.handoff.warnings.some(item => /missing source section|missing original prompt/.test(item)));
assert.ok(messyIdeaOnly.handoff.warnings.includes('missing source artifact id'));
+6 -4
View File
@@ -1005,13 +1005,14 @@ function compactBuildItems(items = []) {
function createHandoffContract({ ranked, provenance, decisionContext }) {
const warnings = [];
if (!provenance?.artifactId) warnings.push('missing source artifact id');
if (!provenance?.originalPrompt) warnings.push('missing original prompt provenance');
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');
const itemTrace = ranked.map(item => {
if (!item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`);
if (expectsSourceTrace && !item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`);
if (item.provenance?.idNormalized) warnings.push(`duplicate source id ${item.provenance.originalId} normalized to ${item.id}`);
if (!item.factors?.evidenceNeeded && ['do', 'test'].includes(item.lane?.id)) warnings.push(`missing evidence needed for active item ${item.id}`);
if (expectsSourceTrace && !item.factors?.evidenceNeeded && ['do', 'test'].includes(item.lane?.id)) warnings.push(`missing evidence needed for active item ${item.id}`);
if (item.metrics?.nonGoalConflicts?.length && ['do', 'test'].includes(item.lane?.id)) warnings.push(`active item ${item.id} conflicts with source non-goals: ${item.metrics.nonGoalConflicts.join('; ')}`);
return {
id: item.id,
@@ -1035,6 +1036,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
snapshotTitle: provenance?.snapshotTitle || '',
conceptMapId: provenance?.conceptMapId || '',
hasOriginalPrompt: Boolean(provenance?.originalPrompt),
requiresSourceTrace: expectsSourceTrace,
},
decisionContext: {
targetAudience: decisionContext?.targetAudience || '',