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.
|
||||
|
||||
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.
|
||||
|
||||
Recommended payload shape:
|
||||
|
||||
@@ -618,6 +618,60 @@ try {
|
||||
assert.ok(scattermindPaidShape.ranked.find(item => item.id === 'build-order-4').metrics.nonGoalConflicts.length >= 1);
|
||||
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`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -682,7 +682,7 @@ function cleanContextText(value = '') {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -769,10 +769,10 @@ function compactCandidateGroup(group = []) {
|
||||
function buildOrderSectionGroup(buildOrder = {}, baseSection = 'buildOrder') {
|
||||
const source = objectFrom(buildOrder);
|
||||
return compactCandidateGroup([
|
||||
{ items: source.doFirst || source.do_first || source.buildFirst || source.buildNow || source.now, 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.defer || source.deferred || source.later || source.afterProof, 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.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 || 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 || 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 || 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 = '') {
|
||||
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+/)
|
||||
.map(part => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
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;
|
||||
return cleanText(first, 120);
|
||||
}
|
||||
|
||||
function laneFromBuildOrderLabel(fragment = '') {
|
||||
if (/^(build first|start here|ship first)\s*:/i.test(fragment)) return 'do-first';
|
||||
if (/^(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 (/^(probably noise|park)\s*:/i.test(fragment)) return 'park';
|
||||
if (/^(build first|start here|ship first|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 (/^(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';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -815,7 +815,13 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense
|
||||
id: `build-order-${index + 1}`,
|
||||
action: titleFromBuildOrderFragment(fragment),
|
||||
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,
|
||||
rankerHints: lane === 'do-first'
|
||||
? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }
|
||||
@@ -965,10 +971,10 @@ function scoreOption(option, mode, context = '', decisionContext = {}) {
|
||||
|
||||
function normalizeLaneHint(value = '') {
|
||||
const hint = cleanText(value, 40).toLowerCase().replace(/_/g, '-');
|
||||
if (/^(do|do-first|first|build|build-now|now)$/.test(hint)) return 'do';
|
||||
if (/^(validate|validate-next|test|test-next|proof|evidence)$/.test(hint)) return 'test';
|
||||
if (/^(defer|later|sequence-later|after-proof)$/.test(hint)) return 'defer';
|
||||
if (/^(park|cut|drop|icebox|not-now)$/.test(hint)) return 'park';
|
||||
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|evidence-next|try-next|learn-next)$/.test(hint)) return 'test';
|
||||
if (/^(defer|later|sequence-later|after-proof|hold-for-later|not-yet)$/.test(hint)) return 'defer';
|
||||
if (/^(park|cut|drop|icebox|not-now|set-aside|out-of-scope)$/.test(hint)) return 'park';
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user