Accept lens-only Scattermind build orders
This commit is contained in:
@@ -464,9 +464,15 @@ function cleanDecisionContext(input = {}) {
|
||||
const featureSet = objectFrom(input.featureSet);
|
||||
const artifact = objectFrom(input.artifact || featureSet.artifact);
|
||||
const conceptMap = objectFrom(input.conceptMap || featureSet.conceptMap || artifact.conceptMap);
|
||||
const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || featureSet.lenses);
|
||||
const riskLens = objectFrom(conceptMapLenses.risk);
|
||||
const structuredContext = objectFrom(input.context);
|
||||
const sourceContext = firstObject(input.decisionContext, featureSet.decisionContext, artifact.decisionContext, conceptMap.decisionContext, structuredContext, conceptMap.context);
|
||||
const textContextGuardrails = guardrailsFromContextText(typeof input.context === 'string' ? input.context : '');
|
||||
const textContextGuardrails = guardrailsFromContextText([
|
||||
typeof input.context === 'string' ? input.context : '',
|
||||
riskLens.content || riskLens.text || '',
|
||||
conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || '',
|
||||
].filter(Boolean).join('\n'));
|
||||
return {
|
||||
targetAudience: cleanText(input.targetAudience || featureSet.targetAudience || sourceContext.targetAudience || conceptMap.targetAudience || '', 180),
|
||||
constraints: uniqueList([
|
||||
@@ -494,7 +500,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', 'value', 'layer']);
|
||||
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']);
|
||||
return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8);
|
||||
}
|
||||
|
||||
@@ -566,9 +572,54 @@ function normalizeCandidateGroup(group = []) {
|
||||
return normalizeOptionIds(options);
|
||||
}
|
||||
|
||||
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:')
|
||||
.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 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';
|
||||
return '';
|
||||
}
|
||||
|
||||
function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lenses.channel') {
|
||||
const fragments = sentenceFragments(text);
|
||||
const labelled = fragments.filter(fragment => laneFromBuildOrderLabel(fragment));
|
||||
return labelled.map((fragment, index) => {
|
||||
const lane = laneFromBuildOrderLabel(fragment);
|
||||
return {
|
||||
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.' : ''),
|
||||
suggestedLane: lane,
|
||||
rankerHints: lane === 'do-first'
|
||||
? { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 }
|
||||
: lane === 'validate-next'
|
||||
? { value: 7, effort: 3, confidence: 6, urgency: 5, risk: 3 }
|
||||
: undefined,
|
||||
sourceSection,
|
||||
};
|
||||
}).filter(item => item.action);
|
||||
}
|
||||
|
||||
function optionsFromBody(body = {}) {
|
||||
const featureSet = objectFrom(body.featureSet);
|
||||
const conceptMap = objectFrom(body.conceptMap || featureSet.conceptMap);
|
||||
const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || featureSet.lenses);
|
||||
const buildOrderLens = objectFrom(conceptMapLenses.channel || conceptMapLenses.buildOrder || conceptMap.buildOrder);
|
||||
const directCandidateGroup = compactCandidateGroup([
|
||||
{ items: body.features, sourceSection: 'features' },
|
||||
{ items: featureSet.features, sourceSection: 'feature-set.features' },
|
||||
@@ -590,6 +641,9 @@ function optionsFromBody(body = {}) {
|
||||
]);
|
||||
const groupedCandidates = [...directCandidateGroup, ...conceptMapCandidateGroup];
|
||||
if (groupedCandidates.length) return normalizeCandidateGroup(groupedCandidates);
|
||||
const buildOrderText = buildOrderLens.content || buildOrderLens.text || (typeof conceptMap.buildOrder === 'string' ? conceptMap.buildOrder : '');
|
||||
const buildOrderOptions = optionsFromBuildOrderText(buildOrderText);
|
||||
if (buildOrderOptions.length) return normalizeCandidateGroup([{ items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }]);
|
||||
if (Array.isArray(body.options)) {
|
||||
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