Accept soft Scattermind bridge lane labels
This commit is contained in:
@@ -51,6 +51,8 @@ Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `c
|
|||||||
|
|
||||||
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` 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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
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. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. 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. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. 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.
|
||||||
|
|
||||||
Recommended payload shape:
|
Recommended payload shape:
|
||||||
|
|||||||
@@ -618,6 +618,60 @@ try {
|
|||||||
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);
|
||||||
assert.deepEqual(scattermindPaidShape.handoff.warnings, []);
|
assert.deepEqual(scattermindPaidShape.handoff.warnings, []);
|
||||||
|
|
||||||
|
const softLabelLensResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
reference_code: 'SM-SOFT-LABELS',
|
||||||
|
working_name: 'Gentle Scattermind continuation labels',
|
||||||
|
ideaText: 'Scattermind avoids verdict language but still needs to hand Ranker a continuation order.',
|
||||||
|
context: 'Solo builder. Manual proof before machinery. Avoid accounts and saved workspaces.',
|
||||||
|
mode: 'mvp',
|
||||||
|
lenses: {
|
||||||
|
risk: 'Avoid accounts and saved workspaces until one copyable result makes sense.',
|
||||||
|
channel: 'Continue first: One source-traced build order preview - make the next move tangible without a dashboard. Try next: Copyable decision brief - ask three tired users what they would do next. Hold for later: Visual polish after the rough brief works. Set aside: Saved workspace with accounts, auth, billing, and collaboration.',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(softLabelLensResponse.status, 200);
|
||||||
|
const softLabelLens = await softLabelLensResponse.json();
|
||||||
|
assert.equal(softLabelLens.input.optionCount, 4);
|
||||||
|
assert.equal(softLabelLens.ranked[0].id, 'build-order-1');
|
||||||
|
assert.equal(softLabelLens.ranked[0].lane.id, 'do');
|
||||||
|
assert.match(softLabelLens.ranked[0].provenance.sourceQuote, /Continue first: One source-traced build order preview/);
|
||||||
|
assert.equal(softLabelLens.ranked.find(item => item.id === 'build-order-2').lane.id, 'test');
|
||||||
|
assert.equal(softLabelLens.ranked.find(item => item.id === 'build-order-3').lane.id, 'defer');
|
||||||
|
assert.equal(softLabelLens.ranked.find(item => item.id === 'build-order-4').lane.id, 'park');
|
||||||
|
assert.equal(softLabelLens.handoff.readiness.status, 'ready');
|
||||||
|
assert.deepEqual(softLabelLens.handoff.warnings, []);
|
||||||
|
|
||||||
|
const softLabelObjectResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceName: 'Scattermind',
|
||||||
|
artifactId: 'concept_map_soft_object_labels',
|
||||||
|
originalPrompt: 'Scattermind exported soft continuation sections instead of verdict-named lanes.',
|
||||||
|
idea: 'Ranker should accept softer object keys without forcing Scattermind to say build/kill.',
|
||||||
|
mode: 'mvp',
|
||||||
|
conceptMap: {
|
||||||
|
buildOrder: {
|
||||||
|
continueFirst: [{ id: 'manual-preview', move: 'Manual continuation preview', evidenceQuestion: 'Can one user explain the next move?' }],
|
||||||
|
evidenceNext: ['Copyable brief comprehension check'],
|
||||||
|
holdForLater: ['Visual system polish'],
|
||||||
|
setAside: ['Account workspace dashboard'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(softLabelObjectResponse.status, 200);
|
||||||
|
const softLabelObject = await softLabelObjectResponse.json();
|
||||||
|
assert.equal(softLabelObject.input.optionCount, 4);
|
||||||
|
assert.equal(softLabelObject.buildOrder.doFirst[0], 'manual-preview');
|
||||||
|
assert.equal(softLabelObject.ranked.find(item => item.id === 'feature-1').lane.id, 'test');
|
||||||
|
assert.equal(softLabelObject.ranked.find(item => item.title === 'Visual system polish').lane.id, 'defer');
|
||||||
|
assert.equal(softLabelObject.ranked.find(item => item.title === 'Account workspace dashboard').lane.id, 'park');
|
||||||
|
|
||||||
const mergedContextResponse = await fetch(`${base}/api/rank-feedback`, {
|
const mergedContextResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -682,7 +682,7 @@ function cleanContextText(value = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function meaningfulTokens(text = '') {
|
function meaningfulTokens(text = '') {
|
||||||
const stop = new Set(['the', 'and', 'with', 'from', 'that', 'this', 'into', 'before', 'after', 'user', 'users', 'idea', 'ideas', 'build', 'first', 'avoid', 'without', 'more', 'full', 'proof', 'manual', 'value', 'layer']);
|
const stop = new Set(['the', 'and', 'with', 'from', 'that', 'this', 'into', 'before', 'after', 'until', 'user', 'users', 'idea', 'ideas', 'build', 'first', 'avoid', 'without', 'more', 'full', 'proof', 'manual', 'value', 'layer', 'result', 'sense', 'copyable']);
|
||||||
return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8);
|
return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,10 +769,10 @@ function compactCandidateGroup(group = []) {
|
|||||||
function buildOrderSectionGroup(buildOrder = {}, baseSection = 'buildOrder') {
|
function buildOrderSectionGroup(buildOrder = {}, baseSection = 'buildOrder') {
|
||||||
const source = objectFrom(buildOrder);
|
const source = objectFrom(buildOrder);
|
||||||
return compactCandidateGroup([
|
return compactCandidateGroup([
|
||||||
{ items: source.doFirst || source.do_first || source.buildFirst || source.buildNow || source.now, sourceSection: `${baseSection}.doFirst`, defaultLane: 'do-first' },
|
{ items: source.doFirst || source.do_first || source.buildFirst || source.buildNow || source.now || source.continueFirst || source.continue_first || source.makeTangible || source.make_tangible || source.startHere || source.start_here, sourceSection: `${baseSection}.doFirst`, defaultLane: 'do-first' },
|
||||||
{ items: source.validateNext || source.validate_next || source.testNext || source.testManually || source.validation, sourceSection: `${baseSection}.validateNext`, defaultLane: 'validate-next' },
|
{ items: source.validateNext || source.validate_next || source.testNext || source.testManually || source.validation || source.evidenceNext || source.evidence_next || source.tryNext || source.try_next || source.learnNext || source.learn_next, sourceSection: `${baseSection}.validateNext`, defaultLane: 'validate-next' },
|
||||||
{ items: source.defer || source.deferred || source.later || source.afterProof, sourceSection: `${baseSection}.defer`, defaultLane: 'defer' },
|
{ items: source.defer || source.deferred || source.later || source.afterProof || source.holdForLater || source.hold_for_later || source.notYet || source.not_yet, sourceSection: `${baseSection}.defer`, defaultLane: 'defer' },
|
||||||
{ items: source.park || source.parkingLot || source.parking_lot || source.parked || source.probablyNoise || source.probably_noise || source.noise, sourceSection: `${baseSection}.park`, defaultLane: 'park' },
|
{ items: source.park || source.parkingLot || source.parking_lot || source.parked || source.probablyNoise || source.probably_noise || source.noise || source.setAside || source.set_aside || source.outOfScope || source.out_of_scope, sourceSection: `${baseSection}.park`, defaultLane: 'park' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,23 +786,23 @@ 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|test manually|validate next|defer|probably noise|park|do not build yet|don't build yet)\s*:/gi, '\n$1:')
|
.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:')
|
||||||
.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|test manually|validate next|defer|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|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)\s*:/i.test(fragment)) return 'do-first';
|
if (/^(build first|start here|ship first|continue first|make tangible first|make tangible)\s*:/i.test(fragment)) return 'do-first';
|
||||||
if (/^(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 (/^(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 (/^(probably noise|park)\s*:/i.test(fragment)) return 'park';
|
if (/^(set aside|out of scope|probably noise|park)\s*:/i.test(fragment)) return 'park';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,7 +815,13 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense
|
|||||||
id: `build-order-${index + 1}`,
|
id: `build-order-${index + 1}`,
|
||||||
action: titleFromBuildOrderFragment(fragment),
|
action: titleFromBuildOrderFragment(fragment),
|
||||||
why: fragment.replace(/^\s*[^:]{1,40}:\s*/, '').trim(),
|
why: fragment.replace(/^\s*[^:]{1,40}:\s*/, '').trim(),
|
||||||
evidence: /test|validate|proof|prove|signal|ask|show/i.test(fragment) ? fragment : (lane === 'do-first' ? 'Prove this first move manually before adding product machinery.' : ''),
|
evidence: /test|validate|proof|prove|signal|ask|show/i.test(fragment)
|
||||||
|
? fragment
|
||||||
|
: lane === 'do-first'
|
||||||
|
? 'Prove this first move manually before adding product machinery.'
|
||||||
|
: lane === 'validate-next'
|
||||||
|
? 'Collect the smallest real signal before promoting this into the build lane.'
|
||||||
|
: '',
|
||||||
suggestedLane: lane,
|
suggestedLane: lane,
|
||||||
rankerHints: lane === 'do-first'
|
rankerHints: lane === 'do-first'
|
||||||
? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }
|
? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }
|
||||||
@@ -965,10 +971,10 @@ function scoreOption(option, mode, context = '', decisionContext = {}) {
|
|||||||
|
|
||||||
function normalizeLaneHint(value = '') {
|
function normalizeLaneHint(value = '') {
|
||||||
const hint = cleanText(value, 40).toLowerCase().replace(/_/g, '-');
|
const hint = cleanText(value, 40).toLowerCase().replace(/_/g, '-');
|
||||||
if (/^(do|do-first|first|build|build-now|now)$/.test(hint)) return 'do';
|
if (/^(do|do-first|first|build|build-now|now|continue-first|make-tangible|start-here)$/.test(hint)) return 'do';
|
||||||
if (/^(validate|validate-next|test|test-next|proof|evidence)$/.test(hint)) return 'test';
|
if (/^(validate|validate-next|test|test-next|proof|evidence|evidence-next|try-next|learn-next)$/.test(hint)) return 'test';
|
||||||
if (/^(defer|later|sequence-later|after-proof)$/.test(hint)) return 'defer';
|
if (/^(defer|later|sequence-later|after-proof|hold-for-later|not-yet)$/.test(hint)) return 'defer';
|
||||||
if (/^(park|cut|drop|icebox|not-now)$/.test(hint)) return 'park';
|
if (/^(park|cut|drop|icebox|not-now|set-aside|out-of-scope)$/.test(hint)) return 'park';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user