Preserve source trace for lens build orders
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 `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 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`. 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. 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -461,6 +461,10 @@ try {
|
|||||||
assert.equal(lensOnly.input.optionCount, 4);
|
assert.equal(lensOnly.input.optionCount, 4);
|
||||||
assert.equal(lensOnly.ranked[0].id, 'build-order-1');
|
assert.equal(lensOnly.ranked[0].id, 'build-order-1');
|
||||||
assert.equal(lensOnly.ranked[0].provenance.sourceSection, 'concept-map.lenses.channel');
|
assert.equal(lensOnly.ranked[0].provenance.sourceSection, 'concept-map.lenses.channel');
|
||||||
|
assert.equal(lensOnly.ranked[0].provenance.sourceId, 'concept-map.lenses.channel#1');
|
||||||
|
assert.equal(lensOnly.ranked[0].provenance.sourceTitle, 'Build Order');
|
||||||
|
assert.match(lensOnly.ranked[0].provenance.sourceQuote, /Build first: Manual build-order preview/);
|
||||||
|
assert.equal(lensOnly.brief.quickGlance.sourceTrace.sourceId, 'concept-map.lenses.channel#1');
|
||||||
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-2').lane.id, 'test');
|
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-2').lane.id, 'test');
|
||||||
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-3').lane.id, 'defer');
|
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-3').lane.id, 'defer');
|
||||||
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-4').lane.id, 'park');
|
assert.equal(lensOnly.ranked.find(item => item.id === 'build-order-4').lane.id, 'park');
|
||||||
@@ -492,6 +496,9 @@ try {
|
|||||||
assert.match(scattermindPaidShape.input.provenance.originalPrompt, /supper club/);
|
assert.match(scattermindPaidShape.input.provenance.originalPrompt, /supper club/);
|
||||||
assert.equal(scattermindPaidShape.input.optionCount, 4);
|
assert.equal(scattermindPaidShape.input.optionCount, 4);
|
||||||
assert.equal(scattermindPaidShape.ranked[0].id, 'build-order-1');
|
assert.equal(scattermindPaidShape.ranked[0].id, 'build-order-1');
|
||||||
|
assert.equal(scattermindPaidShape.ranked[0].provenance.sourceId, 'concept-map.lenses.channel#1');
|
||||||
|
assert.equal(scattermindPaidShape.ranked[0].provenance.sourceTitle, 'Build Order');
|
||||||
|
assert.match(scattermindPaidShape.handoff.itemTrace.find(item => item.id === 'build-order-1').sourceQuote, /One manual supper-club offer page/);
|
||||||
assert.equal(scattermindPaidShape.ranked.find(item => item.id === 'build-order-4').lane.id, 'park');
|
assert.equal(scattermindPaidShape.ranked.find(item => item.id === 'build-order-4').lane.id, 'park');
|
||||||
assert.ok(scattermindPaidShape.input.decisionContext.nonGoals.includes('Avoid accounts and saved workspaces before anyone pays'));
|
assert.ok(scattermindPaidShape.input.decisionContext.nonGoals.includes('Avoid accounts and saved workspaces before anyone pays'));
|
||||||
assert.ok(scattermindPaidShape.ranked.find(item => item.id === 'build-order-4').metrics.nonGoalConflicts.length >= 1);
|
assert.ok(scattermindPaidShape.ranked.find(item => item.id === 'build-order-4').metrics.nonGoalConflicts.length >= 1);
|
||||||
|
|||||||
@@ -779,7 +779,7 @@ function laneFromBuildOrderLabel(fragment = '') {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lenses.channel') {
|
function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lenses.channel', sourceTitle = 'Build Order') {
|
||||||
const fragments = sentenceFragments(text);
|
const fragments = sentenceFragments(text);
|
||||||
const labelled = fragments.filter(fragment => laneFromBuildOrderLabel(fragment));
|
const labelled = fragments.filter(fragment => laneFromBuildOrderLabel(fragment));
|
||||||
return labelled.map((fragment, index) => {
|
return labelled.map((fragment, index) => {
|
||||||
@@ -796,6 +796,9 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense
|
|||||||
? { value: 7, effort: 3, confidence: 6, urgency: 5, risk: 3 }
|
? { value: 7, effort: 3, confidence: 6, urgency: 5, risk: 3 }
|
||||||
: undefined,
|
: undefined,
|
||||||
sourceSection,
|
sourceSection,
|
||||||
|
sourceItemId: `${sourceSection}#${index + 1}`,
|
||||||
|
sourceTitle,
|
||||||
|
sourceExcerpt: fragment,
|
||||||
};
|
};
|
||||||
}).filter(item => item.action);
|
}).filter(item => item.action);
|
||||||
}
|
}
|
||||||
@@ -838,7 +841,15 @@ function optionsFromBody(body = {}) {
|
|||||||
|| buildOrderLens.content
|
|| buildOrderLens.content
|
||||||
|| buildOrderLens.text
|
|| buildOrderLens.text
|
||||||
|| '';
|
|| '';
|
||||||
const buildOrderOptions = optionsFromBuildOrderText(buildOrderText);
|
const buildOrderSourceTitle = cleanText(
|
||||||
|
objectFrom(conceptMapLenses.channel).title
|
||||||
|
|| objectFrom(conceptMapLenses.buildOrder).title
|
||||||
|
|| objectFrom(conceptMap.buildOrder).title
|
||||||
|
|| buildOrderLens.title
|
||||||
|
|| 'Build Order',
|
||||||
|
140
|
||||||
|
);
|
||||||
|
const buildOrderOptions = optionsFromBuildOrderText(buildOrderText, 'concept-map.lenses.channel', buildOrderSourceTitle);
|
||||||
if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]);
|
if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]);
|
||||||
if (Array.isArray(body.options)) {
|
if (Array.isArray(body.options)) {
|
||||||
return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title));
|
return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title));
|
||||||
|
|||||||
Reference in New Issue
Block a user