import 'dotenv/config'; import express from 'express'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import crypto from 'node:crypto'; import { Client, TablesDB, ID, Query } from 'node-appwrite'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = Number(process.env.PORT || 3045); const endpoint = process.env.APPWRITE_ENDPOINT || process.env.APPWRITE_LOCAL_ENDPOINT || process.env.APPWRITE_SELF_HOSTED_URL; const projectId = process.env.APPWRITE_PROJECT_ID || process.env.APPWRITE_LOCAL_PROJECT_ID || process.env.APPWRITE_SELF_HOSTED_PROJECT_ID; const apiKey = process.env.APPWRITE_API_KEY || process.env.APPWRITE_LOCAL_API_KEY || process.env.APPWRITE_SELF_HOSTED_API_KEY; const databaseId = process.env.RANK_APPWRITE_DATABASE_ID || process.env.APPWRITE_DATABASE_ID || 'priority_rank'; const ideasTableId = process.env.RANK_IDEAS_TABLE_ID || 'ideas'; const milestonesTableId = process.env.RANK_MILESTONES_TABLE_ID || 'milestones'; const activityTableId = process.env.RANK_ACTIVITY_TABLE_ID || 'activity'; const agentToken = process.env.RANK_AGENT_TOKEN || process.env.PRIORITY_AGENT_TOKEN || ''; const appVersion = process.env.APP_VERSION || 'rank-local'; const appwriteTimeoutMs = Number(process.env.APPWRITE_TIMEOUT_MS || 7000); function withTimeout(promise, label = 'operation') { let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} timed out after ${appwriteTimeoutMs}ms`)), appwriteTimeoutMs); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } if (!endpoint || !projectId || !apiKey) { console.warn('[rank] Missing Appwrite configuration; /api/health will report degraded.'); } const client = new Client(); if (endpoint) client.setEndpoint(endpoint); if (projectId) client.setProject(projectId); if (apiKey) client.setKey(apiKey); const tables = new TablesDB(client); const app = express(); app.use(express.json({ limit: '256kb' })); app.use(express.static(path.join(__dirname, 'public'), { etag: true, maxAge: process.env.NODE_ENV === 'production' ? '10m' : 0, })); function clampInt(value, fallback, min = 0, max = 10) { const n = Number.parseInt(value, 10); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); } function cleanText(value, max = 1000) { return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, max); } function cleanMultiline(value, max = 6000) { return String(value ?? '').replace(/\r\n/g, '\n').trim().slice(0, max); } function scoreIdea({ impact, effort, confidence, urgency }) { const i = clampInt(impact, 5); const e = clampInt(effort, 5, 1, 10); const c = clampInt(confidence, 5); const u = clampInt(urgency, 5); return Number((((i * 2.4) + (c * 1.2) + (u * 1.4)) / Math.max(1, e)).toFixed(2)); } function ideaDataFromInput(input = {}, defaults = {}) { const title = cleanText(input.title || input.name, 180); if (!title) throw Object.assign(new Error('Every feature needs a title.'), { code: 400 }); const data = { title, description: cleanMultiline(input.description || input.brief || '', 5000), source: cleanText(input.source || defaults.source || 'import', 40), sourceName: cleanText(input.sourceName || input.owner || defaults.sourceName || 'Prioritix feature set', 80), status: cleanText(input.status || defaults.status || 'inbox', 40), milestoneId: cleanText(input.milestoneId || input.milestone || defaults.milestoneId || 'inbox', 64), impact: clampInt(input.impact, defaults.impact ?? 5), effort: clampInt(input.effort, defaults.effort ?? 5, 1, 10), confidence: clampInt(input.confidence, defaults.confidence ?? 6), urgency: clampInt(input.urgency, defaults.urgency ?? 5), rank: clampInt(input.rank, defaults.rank ?? 0, -100000, 100000), labels: encodeList(input.labels || input.category || input.categories || []), notes: cleanMultiline(input.notes || '', 4000), archived: Boolean(input.archived ?? false), }; data.score = scoreIdea(data); return data; } function publicIdea(row) { return { id: row.$id, createdAt: row.$createdAt, updatedAt: row.$updatedAt, title: row.title, description: row.description || '', source: row.source || 'human', sourceName: row.sourceName || '', status: row.status || 'inbox', milestoneId: row.milestoneId || 'inbox', impact: row.impact ?? 5, effort: row.effort ?? 5, confidence: row.confidence ?? 5, urgency: row.urgency ?? 5, score: row.score ?? 0, rank: row.rank ?? 0, labels: parseList(row.labels), notes: row.notes || '', archived: Boolean(row.archived), }; } function publicMilestone(row) { return { id: row.$id, createdAt: row.$createdAt, updatedAt: row.$updatedAt, name: row.name, description: row.description || '', horizon: row.horizon || '', color: row.color || '#8cf7ff', position: row.position ?? 0, active: row.active !== false, }; } function parseList(value) { if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.map(String).slice(0, 12) : []; } catch { return String(value).split(',').map(s => s.trim()).filter(Boolean).slice(0, 12); } } function encodeList(value) { if (Array.isArray(value)) return JSON.stringify(value.map(v => cleanText(v, 32)).filter(Boolean).slice(0, 12)); return JSON.stringify(parseList(value)); } function cleanTextList(value, maxItems = 6, maxText = 180) { const list = Array.isArray(value) ? value : parseList(value); return list.map(item => cleanText(item, maxText)).filter(Boolean).slice(0, maxItems); } function cleanFlexibleTextList(value, maxItems = 8, maxText = 180) { if (Array.isArray(value)) return value.map(item => cleanText(item, maxText)).filter(Boolean).slice(0, maxItems); const text = cleanMultiline(value || '', maxItems * maxText); if (!text) return []; return text .split(/\n|;|\|/) .map(item => item.replace(/^\s*[-*•\d.)]+\s*/, '').trim()) .filter(Boolean) .map(item => cleanText(item, maxText)) .slice(0, maxItems); } function uniqueList(items = [], maxItems = 8) { const seen = new Set(); return items.filter(item => { const cleaned = cleanText(item, 180); const key = cleaned.toLowerCase(); if (!cleaned || seen.has(key)) return false; seen.add(key); return true; }).slice(0, maxItems); } function contextSentences(value = '') { return cleanMultiline(value, 3000) .split(/\n|;|\.|\|/) .map(item => item.replace(/^\s*[-*•\d.)]+\s*/, '').trim()) .filter(Boolean); } function guardrailsFromContextText(value = '') { const nonGoals = []; const constraints = []; for (const sentence of contextSentences(value)) { const cleaned = cleanText(sentence, 180); if (!cleaned) continue; if (/^(avoid|no|do not|don't|dont|must not|never|non-goal|non goal|not yet|out of scope)\b/i.test(cleaned)) nonGoals.push(cleaned.replace(/^non[- ]goal\s*:\s*/i, '')); else if (/\b(keep|leave|hold)\b[^.!?]{0,80}\b(out|later|until proof|after proof|not yet)\b/i.test(cleaned)) nonGoals.push(cleaned); else if (/\b(not a dashboard|not another dashboard|no dashboard swamp|dashboard swamp)\b/i.test(cleaned)) nonGoals.push(cleaned); else if (/\b(avoid|no auth|no account|no billing|no workspace|without accounts|before proof|manual proof|solo builder|constraint)\b/i.test(cleaned)) constraints.push(cleaned); } return { nonGoals: uniqueList(nonGoals), constraints: uniqueList(constraints) }; } function cleanMetricHints(item = {}) { const raw = { ...(item.factors && typeof item.factors === 'object' ? item.factors : {}), ...(item.scoring && typeof item.scoring === 'object' ? item.scoring : {}), ...(item.ranker_hints && typeof item.ranker_hints === 'object' ? item.ranker_hints : {}), ...(item.metric_hints && typeof item.metric_hints === 'object' ? item.metric_hints : {}), ...(item.rankerHints && typeof item.rankerHints === 'object' ? item.rankerHints : {}), }; const aliases = { value: ['value', 'impact', 'userImpact', 'user_impact', 'userValueScore', 'user_value_score'], effort: ['effort', 'buildEffort', 'build_effort', 'complexity'], confidence: ['confidence', 'certainty'], urgency: ['urgency', 'timing'], revenue: ['revenue', 'commercial', 'buyerSignal'], novelty: ['novelty', 'differentiation', 'originality'], risk: ['risk', 'assumptionRisk', 'assumption_risk', 'scopeRisk', 'scope_risk'], }; return Object.fromEntries(Object.entries(aliases).map(([metric, keys]) => { const found = keys.map(key => raw[key] ?? item[key]).find(value => value !== undefined && value !== null && value !== ''); const parsed = Number.parseFloat(found); return [metric, Number.isFinite(parsed) ? Math.min(10, Math.max(1, parsed)) : null]; }).filter(([, value]) => value !== null)); } function blendMetric(heuristic, explicit, weight = 0.58) { if (!Number.isFinite(explicit)) return heuristic; return Math.max(1, Math.min(10, heuristic * (1 - weight) + explicit * weight)); } function rowsFrom(result) { const rows = result?.rows || result?.documents; if (!Array.isArray(rows)) throw new Error('Appwrite returned an invalid table response'); return rows; } function assertRow(row) { if (!row?.$id) throw new Error('Appwrite returned an invalid row response'); return row; } function requireAgent(req, res, next) { if (!agentToken) return next(); const header = req.get('authorization') || ''; const token = header.startsWith('Bearer ') ? header.slice(7) : req.get('x-rank-token'); const tokenBuffer = Buffer.from(token || ''); const expectedBuffer = Buffer.from(agentToken); if (tokenBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(tokenBuffer, expectedBuffer)) return next(); return res.status(401).json({ error: 'agent token required' }); } async function logActivity(type, message, ideaId = '', meta = '') { try { await tables.createRow({ databaseId, tableId: activityTableId, rowId: ID.unique(), data: { type, message: cleanText(message, 300), ideaId, meta: cleanText(meta, 800) }, }); } catch (error) { console.warn('[rank] activity log failed', error.message); } } app.get('/api/health', async (_req, res) => { const health = { ok: false, app: 'rank', version: appVersion, appwriteConfigured: Boolean(endpoint && projectId && apiKey), appwriteReachable: false, tableReachable: false, tables: { ideas: false, milestones: false, activity: false }, }; try { if (health.appwriteConfigured) { const tableProbes = [ ['ideas', ideasTableId], ['milestones', milestonesTableId], ['activity', activityTableId], ]; for (const [name, tableId] of tableProbes) { const probe = await withTimeout( tables.listRows({ databaseId, tableId, queries: [Query.limit(1)] }), `Appwrite health probe (${name})` ); rowsFrom(probe); health.tables[name] = true; } health.appwriteReachable = true; health.tableReachable = Object.values(health.tables).every(Boolean); health.ok = health.tableReachable; } } catch (error) { health.error = error.message; } res.status(health.ok ? 200 : 503).json(health); }); app.get('/api/bootstrap', async (_req, res) => { const [ideas, milestones, activity] = await withTimeout(Promise.all([ tables.listRows({ databaseId, tableId: ideasTableId, queries: [Query.equal('archived', false), Query.orderDesc('score'), Query.orderAsc('rank'), Query.limit(100)] }), tables.listRows({ databaseId, tableId: milestonesTableId, queries: [Query.equal('active', true), Query.orderAsc('position'), Query.limit(50)] }), tables.listRows({ databaseId, tableId: activityTableId, queries: [Query.orderDesc('$createdAt'), Query.limit(18)] }).catch(() => ({ rows: [], documents: [] })), ]), 'Appwrite bootstrap'); res.json({ version: appVersion, ideas: rowsFrom(ideas).map(publicIdea), milestones: rowsFrom(milestones).map(publicMilestone), activity: rowsFrom(activity).map(row => ({ id: row.$id, createdAt: row.$createdAt, type: row.type, message: row.message, ideaId: row.ideaId || '' })), scoring: '((impact×2.4)+(confidence×1.2)+(urgency×1.4))/effort', }); }); app.post('/api/ideas', requireAgent, async (req, res) => { const data = ideaDataFromInput(req.body, { source: 'human', status: 'inbox', milestoneId: 'inbox' }); const row = assertRow(await tables.createRow({ databaseId, tableId: ideasTableId, rowId: ID.unique(), data })); await logActivity('idea.created', `Captured “${data.title}”`, row.$id, data.source); res.status(201).json(publicIdea(row)); }); app.post('/api/feature-set/import', requireAgent, async (req, res) => { const features = Array.isArray(req.body.features) ? req.body.features.slice(0, 100) : []; if (!features.length) return res.status(400).json({ error: 'Feature set must include a non-empty features array.' }); const defaults = req.body.defaults && typeof req.body.defaults === 'object' ? req.body.defaults : {}; const created = []; const errors = []; for (const [index, feature] of features.entries()) { try { const data = ideaDataFromInput(feature, { source: 'import', sourceName: req.body.name || 'Prioritix feature set', ...defaults }); const row = assertRow(await tables.createRow({ databaseId, tableId: ideasTableId, rowId: ID.unique(), data })); created.push(publicIdea(row)); } catch (error) { errors.push({ index, title: cleanText(feature?.title || feature?.name || '', 180), error: error.message }); } } if (created.length) await logActivity('feature_set.imported', `Imported ${created.length} feature${created.length === 1 ? '' : 's'}`, '', req.body.name || 'feature-set-v1'); res.status(created.length ? 201 : 400).json({ ok: errors.length === 0, imported: created.length, created, errors }); }); app.patch('/api/ideas/:id', requireAgent, async (req, res) => { const allowed = ['title', 'description', 'source', 'sourceName', 'status', 'milestoneId', 'impact', 'effort', 'confidence', 'urgency', 'rank', 'labels', 'notes', 'archived']; const data = {}; for (const key of allowed) { if (!(key in req.body)) continue; if (['impact', 'effort', 'confidence', 'urgency', 'rank'].includes(key)) data[key] = clampInt(req.body[key], key === 'effort' ? 5 : 0, key === 'effort' ? 1 : -100000, key === 'rank' ? 100000 : 10); else if (key === 'description' || key === 'notes') data[key] = cleanMultiline(req.body[key], key === 'description' ? 5000 : 4000); else if (key === 'labels') data[key] = encodeList(req.body[key]); else if (key === 'archived') data[key] = Boolean(req.body[key]); else data[key] = cleanText(req.body[key], key === 'title' ? 180 : 80); } if (['impact', 'effort', 'confidence', 'urgency'].some(k => k in data)) { const current = assertRow(await tables.getRow({ databaseId, tableId: ideasTableId, rowId: req.params.id })); data.score = scoreIdea({ ...current, ...data }); } const row = assertRow(await tables.updateRow({ databaseId, tableId: ideasTableId, rowId: req.params.id, data })); await logActivity('idea.updated', `Updated “${row.title}”`, row.$id, Object.keys(data).join(',')); res.json(publicIdea(row)); }); app.post('/api/milestones', requireAgent, async (req, res) => { const name = cleanText(req.body.name, 80); if (!name) return res.status(400).json({ error: 'name is required' }); const data = { name, description: cleanMultiline(req.body.description, 1000), horizon: cleanText(req.body.horizon || '', 80), color: cleanText(req.body.color || '#8cf7ff', 24), position: clampInt(req.body.position, 0, -10000, 10000), active: req.body.active !== false, }; const row = assertRow(await tables.createRow({ databaseId, tableId: milestonesTableId, rowId: ID.unique(), data })); await logActivity('milestone.created', `Added milestone “${name}”`, '', row.$id); res.status(201).json(publicMilestone(row)); }); app.patch('/api/milestones/:id', requireAgent, async (req, res) => { const data = {}; for (const key of ['name', 'description', 'horizon', 'color', 'position', 'active']) { if (!(key in req.body)) continue; if (key === 'position') data[key] = clampInt(req.body[key], 0, -10000, 10000); else if (key === 'active') data[key] = Boolean(req.body[key]); else if (key === 'description') data[key] = cleanMultiline(req.body[key], 1000); else data[key] = cleanText(req.body[key], key === 'name' ? 80 : 120); } const row = assertRow(await tables.updateRow({ databaseId, tableId: milestonesTableId, rowId: req.params.id, data })); await logActivity('milestone.updated', `Updated milestone “${row.name}”`, '', row.$id); res.json(publicMilestone(row)); }); app.post('/api/reorder', requireAgent, async (req, res) => { const updates = Array.isArray(req.body.updates) ? req.body.updates.slice(0, 100) : []; const changed = []; for (const item of updates) { if (!item?.id) continue; const data = {}; if ('rank' in item) data.rank = clampInt(item.rank, 0, -100000, 100000); if ('milestoneId' in item) data.milestoneId = cleanText(item.milestoneId, 64); if ('status' in item) data.status = cleanText(item.status, 40); if (Object.keys(data).length) { const row = assertRow(await tables.updateRow({ databaseId, tableId: ideasTableId, rowId: item.id, data })); changed.push(publicIdea(row)); } } if (changed.length) await logActivity('ideas.reordered', `Re-ranked ${changed.length} item${changed.length === 1 ? '' : 's'}`); res.json({ changed }); }); const judgementModes = { progress: { label: 'Fastest useful progress', weights: { value: 1.2, feasibility: 1.55, confidence: 1.25, urgency: 1.15, revenue: 0.55, novelty: 0.35, risk: -1.05 }, next: 'Test the highest-ranked low-effort option manually before adding product machinery.', }, mvp: { label: 'Best MVP order', weights: { value: 1.55, feasibility: 1.25, confidence: 1.15, urgency: 0.85, revenue: 0.75, novelty: 0.45, risk: -1.2 }, next: 'Build the smallest slice that proves the core user promise, then defer everything that needs the proof first.', }, revenue: { label: 'Revenue potential', weights: { value: 1.15, feasibility: 0.75, confidence: 0.95, urgency: 1.05, revenue: 1.85, novelty: 0.35, risk: -1.0 }, next: 'Validate willingness to pay before polishing delivery. A paid manual version beats a beautiful unpaid feature.', }, risk: { label: 'Safest proof order', weights: { value: 0.95, feasibility: 1.0, confidence: 1.45, urgency: 0.8, revenue: 0.45, novelty: 0.25, risk: -1.85 }, next: 'Start with the option that reduces risk without betting the roadmap on it.', }, validation: { label: 'Fastest validation', weights: { value: 1.25, feasibility: 1.7, confidence: 1.35, urgency: 1.1, revenue: 0.35, novelty: 0.25, risk: -0.85 }, next: 'Choose the option that can produce real user evidence fastest, even if the first version is manual.', }, learning: { label: 'Biggest assumption test', weights: { value: 1.1, feasibility: 1.35, confidence: -0.35, urgency: 0.55, revenue: 0.45, novelty: 0.45, risk: 1.15 }, next: 'Pick the riskiest important assumption that can be tested cheaply. The goal is learning, not shipping surface area.', }, originality: { label: 'Most original / differentiated', weights: { value: 1.0, feasibility: 0.65, confidence: 0.75, urgency: 0.55, revenue: 0.65, novelty: 1.85, risk: -0.9 }, next: 'Keep the unusual angle, but demand one proof step so originality does not become expensive weirdness.', }, }; const wordSets = { revenue: ['pay', 'paid', 'price', 'pricing', 'stripe', 'checkout', 'invoice', 'sales', 'sell', 'buyer', 'subscription', 'revenue', 'client', 'customer', 'conversion', 'upsell'], value: ['pain', 'problem', 'save', 'faster', 'clear', 'clarity', 'decision', 'feedback', 'user', 'customer', 'relief', 'manual', 'core', 'must', 'need', 'trust'], effort: ['dashboard', 'workspace', 'workspaces', 'collaboration', 'realtime', 'integration', 'integrations', 'automation', 'accounts', 'auth', 'billing', 'subscription', 'pricing', 'checkout', 'invoices', 'ai', 'model', 'mobile', 'sync', 'admin', 'export', 'exportable', 'slack', 'notion', 'team', 'voting'], risk: ['unclear', 'maybe', 'complex', 'hard', 'risky', 'unknown', 'depends', 'enterprise', 'platform', 'everything', 'all users', 'team voting', 'marketplace', 'saved workspaces', 'team voting'], novelty: ['new', 'novel', 'different', 'unique', 'original', 'weird', 'unexpected', 'expert', 'judgement', 'reflection', 'rank', 'compare'], urgency: ['now', 'launch', 'blocker', 'first', 'mvp', 'today', 'week', 'urgent', 'stuck', 'before', 'next'], }; function hits(text, words) { const lower = ` ${String(text || '').toLowerCase()} `; return words.reduce((count, word) => count + (lower.includes(word) ? 1 : 0), 0); } function parseOptionsFromText(value, sourceSection = 'optionsText') { const text = cleanMultiline(value, 12000); const candidateText = /\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\b[:\s]/i.test(text) ? text.replace(/^[\s\S]*?\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\b[:\s]*/i, '') : text; const cleanedLines = candidateText.split('\n').map(line => line.replace(/^\s*[-*•\d.)]+\s*/, '').trim()).filter(Boolean); const sentenceList = candidateText .replace(/\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\s*:/gi, '\n') .split(/\n|;|\|/) .map(part => part.replace(/^\s*[-*•\d.)]+\s*/, '').trim()) .filter(Boolean); const commaList = sentenceList.length === 1 ? sentenceList[0].split(/,|\s+and\s+|[.!?]\s+/i).map(part => part.trim()).filter(Boolean) : []; const optionLines = cleanedLines.length >= 2 ? cleanedLines : sentenceList.length >= 2 ? sentenceList : commaList.length >= 2 ? commaList : candidateText.split(/[;|]/).map(part => part.trim()).filter(Boolean); return optionLines.slice(0, 24).map((line, index) => { const normalized = line .replace(/^\s*(maybe|possibly|also|plus|and|or|then|later|eventually|build|add|include|some kind of|kind of)\b\s*/i, '') .replace(/\?+$/, '') .trim(); const [rawTitle, ...rest] = normalized.split(/\s*[-–—:]\s+/); return { id: `option-${index + 1}`, title: cleanText(rawTitle || normalized || line, 140), description: cleanText(rest.join(' — '), 420), provenance: { sourceSection }, }; }).filter(item => item.title && !/^(i('| a)?m|i am|we are|i only|want|need)\b/i.test(item.title)); } function objectFrom(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } function bridgeEnvelopeFrom(input = {}) { const body = objectFrom(input); return objectFrom( body.rankerInput || body.ranker_input || body.rankerHandoff || body.ranker_handoff || body.rankerBridge || body.ranker_bridge || body.rankReady || body.rank_ready || body.bridge || body.bridgePayload || body.bridge_payload || body.continuation || body.continuationPlan || body.continuation_plan ); } function featureSetFrom(input = {}) { const body = objectFrom(input); const envelope = bridgeEnvelopeFrom(body); return objectFrom( body.featureSet || body.feature_set || body.candidateSet || body.candidate_set || body.candidateFeatureSet || body.candidate_feature_set || body.rankReadyFeatureSet || body.rank_ready_feature_set || body.buildOrderPreview || body.build_order_preview || envelope.featureSet || envelope.feature_set || envelope.candidateSet || envelope.candidate_set || envelope.candidateFeatureSet || envelope.candidate_feature_set || envelope.rankReadyFeatureSet || envelope.rank_ready_feature_set || envelope.buildOrderPreview || envelope.build_order_preview ); } function looksLikeRankPayload(value = {}) { return Boolean( value.schema || value.featureSet || value.feature_set || value.candidateSet || value.candidate_set || value.candidateFeatureSet || value.candidate_feature_set || value.rankerInput || value.ranker_input || value.rankerHandoff || value.ranker_handoff || value.rankerBridge || value.ranker_bridge || value.rankReady || value.rank_ready || value.bridge || value.bridgePayload || value.bridge_payload || value.continuation || value.continuationPlan || value.continuation_plan || value.snapshot || value.conceptMap || value.concept_map || value.buildOrder || value.build_order || value.lenses || value.reference_code || value.referenceCode || value.artifactId || value.sourceArtifactId || value.source_artifact_id || value.snapshotTitle || value.snapshot_title || value.originalPrompt || value.original_prompt || value.sourceSummary || value.source_summary || value.glimpseJson || value.glimpse_json || value.snapshotJson || value.snapshot_json || value.fullReadingJson || value.full_reading_json || value.fullReading || value.full_reading || value.conceptMapJson || value.concept_map_json || value.buildOrderPreview || value.build_order_preview || value.opening_reflection || value.restated_idea || value.ideaText || value.idea_text || Array.isArray(value.features) || Array.isArray(value.actions) || Array.isArray(value.recommendedActions) || Array.isArray(value.recommended_actions) || Array.isArray(value.suggestedActions) || Array.isArray(value.suggested_actions) || Array.isArray(value.nextActions) || Array.isArray(value.next_actions) || Array.isArray(value.nextMoves) || Array.isArray(value.next_moves) || Array.isArray(value.possibleNextMoves) || Array.isArray(value.possible_next_moves) || Array.isArray(value.suggestedNextMoves) || Array.isArray(value.suggested_next_moves) || Array.isArray(value.recommendations) || Array.isArray(value.opportunities) || Array.isArray(value.candidates) || Array.isArray(value.candidateActions) || Array.isArray(value.candidate_actions) || Array.isArray(value.candidateMoves) || Array.isArray(value.candidate_moves) || Array.isArray(value.rankReadyActions) || Array.isArray(value.rank_ready_actions) || Array.isArray(value.doFirst) || Array.isArray(value.do_first) || Array.isArray(value.continueFirst) || Array.isArray(value.continue_first) || Array.isArray(value.makeTangible) || Array.isArray(value.make_tangible) || Array.isArray(value.validateNext) || Array.isArray(value.validate_next) || Array.isArray(value.evidenceNext) || Array.isArray(value.evidence_next) || Array.isArray(value.tryNext) || Array.isArray(value.try_next) || Array.isArray(value.deferred) || Array.isArray(value.holdForLater) || Array.isArray(value.hold_for_later) || Array.isArray(value.parkingLot) || Array.isArray(value.parking_lot) || Array.isArray(value.setAside) || Array.isArray(value.set_aside) || Array.isArray(value.threads_to_hold) || Array.isArray(value.threadsToHold) || Array.isArray(value.actionThreads) || Array.isArray(value.action_threads) || Array.isArray(value.questions_to_sit_with) || Array.isArray(value.questionsToSitWith) || Array.isArray(value.evidenceQuestions) || Array.isArray(value.evidence_questions) || Array.isArray(value.decisionQuestions) || Array.isArray(value.decision_questions) || Array.isArray(value.questionsToAnswer) || Array.isArray(value.questions_to_answer) || Array.isArray(value.followupQuestions) || Array.isArray(value.followup_questions) || Array.isArray(value.openQuestions) || Array.isArray(value.open_questions) ); } function extractFirstJsonObject(text = '') { const start = text.indexOf('{'); if (start < 0) return ''; let depth = 0; let inString = false; let escaped = false; for (let index = start; index < text.length; index += 1) { const char = text[index]; if (escaped) { escaped = false; continue; } if (char === '\\' && inString) { escaped = true; continue; } if (char === '"') { inString = !inString; continue; } if (inString) continue; if (char === '{') depth += 1; if (char === '}') { depth -= 1; if (depth === 0) return text.slice(start, index + 1); } } return ''; } function parseEmbeddedRankPayload(value = '') { const text = cleanMultiline(value, 12000) .replace(/^```(?:json)?\s*/i, '') .replace(/```$/i, '') .trim(); const jsonText = text.startsWith('{') && text.endsWith('}') ? text : extractFirstJsonObject(text); if (!jsonText) return null; try { const parsed = JSON.parse(jsonText); return looksLikeRankPayload(parsed) ? parsed : null; } catch { return null; } } function expandEmbeddedRankPayload(body = {}) { const original = objectFrom(body); for (const key of ['payload', 'rankPayload', 'scattermindPayload', 'conceptMapJson', 'concept_map_json', 'fullReadingJson', 'full_reading_json', 'fullReading', 'full_reading', 'idea', 'ideaText', 'optionsText']) { const embedded = typeof original[key] === 'string' ? parseEmbeddedRankPayload(original[key]) : looksLikeRankPayload(original[key]) ? original[key] : null; if (!embedded) continue; const expanded = { ...original, ...embedded }; if (key === 'idea' && !embedded.idea && !embedded.ideaText) expanded.idea = ''; if (key === 'optionsText' && !embedded.optionsText) expanded.optionsText = ''; if (original.mode && !embedded.mode) expanded.mode = original.mode; if (original.context && !embedded.context) expanded.context = original.context; expanded._embeddedPayloadSource = key; return expanded; } return original; } function parseObjectJsonString(value = '') { if (typeof value !== 'string' || !value.trim()) return null; try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null; } catch { return null; } } function expandStoredScattermindReading(body = {}) { const original = objectFrom(body); const storedSnapshot = original.glimpseJson || original.glimpse_json || original.snapshotJson || original.snapshot_json || ''; const parsedSnapshot = (typeof storedSnapshot === 'string' ? parseObjectJsonString(storedSnapshot) : objectFrom(storedSnapshot)) || {}; const storedReading = original.fullReadingJson || original.full_reading_json || original.fullReading || original.full_reading || original.conceptMapJson || original.concept_map_json || ''; const parsedReading = (typeof storedReading === 'string' ? parseObjectJsonString(storedReading) : objectFrom(storedReading)) || {}; if ((!parsedReading || !Object.keys(parsedReading).length) && (!parsedSnapshot || !Object.keys(parsedSnapshot).length)) return original; const explicitSnapshot = objectFrom(original.snapshot || original.glimpse); const expanded = { ...parsedSnapshot, ...parsedReading, ...original, snapshot: Object.keys(explicitSnapshot).length ? explicitSnapshot : parsedSnapshot, lenses: original.lenses || parsedReading.lenses || parsedSnapshot.lenses, threads_to_hold: original.threads_to_hold || original.threadsToHold || parsedReading.threads_to_hold || parsedReading.threadsToHold || parsedSnapshot.threads_to_hold || parsedSnapshot.threadsToHold, questions_to_sit_with: original.questions_to_sit_with || original.questionsToSitWith || parsedReading.questions_to_sit_with || parsedReading.questionsToSitWith || parsedSnapshot.questions_to_sit_with || parsedSnapshot.questionsToSitWith, closing_note: original.closing_note || original.closingNote || parsedReading.closing_note || parsedReading.closingNote || parsedSnapshot.closing_note || parsedSnapshot.closingNote, reference_code: original.reference_code || original.referenceCode || parsedReading.reference_code || parsedReading.referenceCode || parsedSnapshot.reference_code || parsedSnapshot.referenceCode, working_name: original.working_name || original.workingName || parsedReading.working_name || parsedReading.workingName || parsedSnapshot.working_name || parsedSnapshot.workingName, opening_reflection: original.opening_reflection || original.openingReflection || parsedReading.opening_reflection || parsedReading.openingReflection || parsedSnapshot.opening_reflection || parsedSnapshot.openingReflection, restated_idea: original.restated_idea || original.restatedIdea || parsedReading.restated_idea || parsedReading.restatedIdea || parsedSnapshot.restated_idea || parsedSnapshot.restatedIdea, ideaText: original.ideaText || original.idea_text || parsedReading.ideaText || parsedReading.idea_text, context: original.context || parsedReading.context || '', }; expanded._storedScattermindReading = true; return expanded; } function cleanProvenance(input = {}) { const envelope = bridgeEnvelopeFrom(input); const featureSet = featureSetFrom(input); const artifact = objectFrom(input.artifact || envelope.artifact || featureSet.artifact); const conceptMap = objectFrom(input.conceptMap || input.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); const snapshot = objectFrom(input.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); const source = objectFrom(input.source || envelope.source || featureSet.source || artifact.source); const sourceSummary = input.sourceSummary || input.source_summary || input.opening_reflection || input.restated_idea || envelope.sourceSummary || envelope.source_summary || envelope.opening_reflection || envelope.restated_idea || artifact.sourceSummary || artifact.source_summary || artifact.opening_reflection || snapshot.sourceSummary || snapshot.source_summary || snapshot.restated_idea || conceptMap.sourceSummary || conceptMap.source_summary || conceptMap.opening_reflection || conceptMap.restated_idea || ''; return { schema: cleanText(input.schema || envelope.schema || featureSet.schema || artifact.schema || input.type || envelope.type || 'prioritix-feature-set-v1', 80), source: cleanText(input.sourceName || input.source_name || envelope.sourceName || envelope.source_name || featureSet.sourceName || featureSet.source_name || source.name || artifact.sourceName || artifact.source_name || 'Scattermind', 80), artifactId: cleanText(input.artifactId || input.artifact_id || input.sourceArtifactId || input.source_artifact_id || input.referenceCode || input.reference_code || envelope.artifactId || envelope.artifact_id || envelope.sourceArtifactId || envelope.source_artifact_id || envelope.referenceCode || envelope.reference_code || artifact.id || source.artifactId || source.artifact_id || conceptMap.artifactId || conceptMap.artifact_id || conceptMap.id || conceptMap.referenceCode || conceptMap.reference_code || snapshot.artifactId || snapshot.artifact_id || snapshot.id || '', 120), snapshotTitle: cleanText(input.snapshotTitle || input.snapshot_title || input.working_name || input.workingName || envelope.snapshotTitle || envelope.snapshot_title || envelope.working_name || envelope.workingName || artifact.snapshotTitle || artifact.snapshot_title || snapshot.title || snapshot.name || conceptMap.snapshotTitle || conceptMap.snapshot_title || conceptMap.working_name || conceptMap.workingName || input.ideaTitle || input.idea_title || envelope.ideaTitle || envelope.idea_title || '', 160), conceptMapId: cleanText(input.conceptMapId || input.concept_map_id || envelope.conceptMapId || envelope.concept_map_id || artifact.conceptMapId || artifact.concept_map_id || conceptMap.id || conceptMap.artifactId || conceptMap.artifact_id || input.referenceCode || input.reference_code || envelope.referenceCode || envelope.reference_code || conceptMap.referenceCode || conceptMap.reference_code || '', 120), originalPrompt: cleanMultiline(input.originalPrompt || input.original_prompt || input.initialPrompt || input.initial_prompt || input.ideaText || input.idea_text || input.prompt || envelope.originalPrompt || envelope.original_prompt || envelope.initialPrompt || envelope.initial_prompt || envelope.ideaText || envelope.idea_text || envelope.prompt || artifact.originalPrompt || artifact.original_prompt || source.originalPrompt || source.original_prompt || snapshot.originalPrompt || snapshot.original_prompt || snapshot.prompt || conceptMap.originalPrompt || conceptMap.original_prompt || conceptMap.ideaText || conceptMap.idea_text || input.idea || envelope.idea || '', 1200), sourceSummary: cleanMultiline(sourceSummary, 1200), }; } function lensContent(lens = {}) { if (Array.isArray(lens)) return lens; if (typeof lens === 'string' || typeof lens === 'number') return String(lens); const obj = objectFrom(lens); return obj.content || obj.text || obj.summary || obj.items || ''; } function cleanSentenceList(value = '', maxItems = 8, maxText = 180) { if (Array.isArray(value)) return cleanFlexibleTextList(value, maxItems, maxText); return contextSentences(value).map(item => cleanText(item, maxText)).filter(Boolean).slice(0, maxItems); } function collectContextList(sources = [], aliases = [], maxItems = 8) { return uniqueList(sources.flatMap(source => { const obj = objectFrom(source); return aliases.flatMap(alias => cleanFlexibleTextList(obj[alias], maxItems, 180)); }), maxItems); } function firstContextText(sources = [], aliases = []) { for (const source of sources) { const obj = objectFrom(source); for (const alias of aliases) { const cleaned = cleanText(obj[alias], 180); if (cleaned) return cleaned; } } return ''; } function contextGuardrailText(value = '') { if (!value || typeof value !== 'object' || Array.isArray(value)) return cleanMultiline(value || '', 3000); const obj = objectFrom(value); return cleanMultiline(obj.summary || obj.description || obj.notes || obj.brief || obj.text || '', 3000); } function guardrailTextItems(value = [], maxItems = 8) { return cleanFlexibleTextList(value, maxItems, 260).filter(item => /\b(avoid|no|do not|don't|dont|must not|never|non-goal|non goal|not yet|out of scope|defer|hold for later|set aside|probably noise|park|do not let this become|don't let this become|not a dashboard|dashboard swamp)\b/i.test(item)); } function inferIdeaRouteFromText(value = '') { const text = String(value || '').toLowerCase(); if (/\b(game|games|gaming|player|players|playtest|prototype|steam|itch|console|pc|deck|roguelike|roguelite|rpg|shooter|bullet\s*hell|bullet-heaven|survival|enemy|enemies|weapon|weapons|level|arena|controller|joystick|driving|car|vehicle|combat|fps|platformer|metroidvania|puzzle game)\b/.test(text)) return 'game'; if (/\b(service|agency|consulting|freelance|offer|client|local|shop|restaurant|webshop|sales|appointment)\b/.test(text)) return 'service_business'; if (/\b(book|novel|story|comic|film|music|art|creative|writing|podcast|newsletter|course|zine)\b/.test(text)) return 'creative_project'; if (/\b(community|members|audience|creator|content|social|discord|forum|club|fans|followers)\b/.test(text)) return 'content_community'; if (/\b(physical|hardware|device|toy|wearable|material|manufactur|packaging|retail|kitchen|furniture|sensor)\b/.test(text)) return 'physical_product'; if (/\b(internal|ops|admin|backoffice|team tool|employee|staff|inventory|support queue|process)\b/.test(text)) return 'internal_tool'; if (/\b(saas|app|software|dashboard|workflow|api|plugin|extension|tool|users?|accounts?|subscription|crm|automation|webapp|mobile app)\b/.test(text)) return 'saas_app'; return ''; } function routeGuardrailNonGoals(route = '') { if (route === 'game') { return [ 'Avoid accounts, dashboards, workspaces, collaboration, CRM, admin panels, onboarding, saved boards, and subscription tiers unless the source explicitly asks for business software.', ]; } return []; } function cleanDecisionContext(input = {}) { const envelope = bridgeEnvelopeFrom(input); const featureSet = featureSetFrom(input); const artifact = objectFrom(input.artifact || envelope.artifact || featureSet.artifact); const conceptMap = objectFrom(input.conceptMap || input.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); const snapshot = objectFrom(input.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || envelope.lenses || featureSet.lenses); const riskLens = conceptMapLenses.risk || conceptMapLenses.risks || conceptMapLenses.boundaries || conceptMapLenses.notYet; const audienceLens = conceptMapLenses.audience || conceptMapLenses.who || conceptMapLenses.customer || conceptMapLenses.users; const constraintsLens = conceptMapLenses.constraints || conceptMapLenses.boundaries || conceptMapLenses.scope; const assumptionsLens = conceptMapLenses.assumptions || conceptMapLenses.unknowns || conceptMapLenses.openQuestions; const structuredContext = objectFrom(input.context); const contextSources = [ input.decisionContext, envelope.decisionContext, featureSet.decisionContext, artifact.decisionContext, conceptMap.decisionContext, snapshot.decisionContext, structuredContext, envelope.context, conceptMap.context, snapshot.context, featureSet.context, artifact.context, ]; const textContextGuardrails = guardrailsFromContextText([ contextGuardrailText(input.context || ''), contextGuardrailText(envelope.context || ''), contextGuardrailText(featureSet.context || ''), contextGuardrailText(artifact.context || ''), contextGuardrailText(snapshot.context || ''), contextGuardrailText(conceptMap.context || ''), lensContent(riskLens), lensContent(constraintsLens), conceptMap.risk || conceptMap.whatNotToBuildYet || conceptMap.notYet || conceptMap.closing_note || conceptMap.closingNote || '', snapshot.risk || snapshot.whatNotToBuildYet || snapshot.notYet || snapshot.closing_note || snapshot.closingNote || '', envelope.closing_note || envelope.closingNote || featureSet.closing_note || featureSet.closingNote || input.closing_note || input.closingNote || '', ...guardrailTextItems(conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, 8), ...guardrailTextItems(snapshot.threads_to_hold || snapshot.threadsToHold || snapshot.actionThreads || snapshot.action_threads, 8), ...guardrailTextItems(envelope.threads_to_hold || envelope.threadsToHold || envelope.actionThreads || envelope.action_threads, 8), ...guardrailTextItems(featureSet.threads_to_hold || featureSet.threadsToHold || featureSet.actionThreads || featureSet.action_threads, 8), ...guardrailTextItems(input.threads_to_hold || input.threadsToHold || input.actionThreads || input.action_threads, 8), ].filter(Boolean).join('\n')); const ideaRoute = cleanText(input.ideaType || input.idea_type || input.category || input.route || envelope.ideaType || envelope.idea_type || featureSet.ideaType || featureSet.idea_type || artifact.ideaType || artifact.idea_type || conceptMap.ideaType || conceptMap.idea_type || snapshot.ideaType || snapshot.idea_type || inferIdeaRouteFromText([ input.idea, input.ideaText, input.idea_text, input.context, input.opening_reflection, input.restated_idea, envelope.idea, envelope.ideaText, envelope.context, conceptMap.opening_reflection, conceptMap.restated_idea, snapshot.restated_idea, lensContent(conceptMapLenses.shape), lensContent(conceptMapLenses.channel), ].filter(Boolean).join('\n')), 60); return { ideaRoute, targetAudience: cleanText(input.targetAudience || input.target_audience || envelope.targetAudience || envelope.target_audience || featureSet.targetAudience || featureSet.target_audience || snapshot.targetAudience || snapshot.target_audience || firstContextText(contextSources, ['targetAudience', 'target_audience', 'audience', 'who', 'whoItHelps', 'who_it_helps', 'customer', 'users']) || conceptMap.targetAudience || conceptMap.target_audience || lensContent(audienceLens), 180), constraints: uniqueList([ ...cleanFlexibleTextList(input.constraints || envelope.constraints || featureSet.constraints || snapshot.constraints || conceptMap.constraints, 8, 180), ...collectContextList(contextSources, ['constraints', 'constraint', 'boundaries', 'scope'], 8), ...cleanSentenceList(lensContent(constraintsLens), 8, 180), ...textContextGuardrails.constraints, ], 8), nonGoals: uniqueList([ ...cleanFlexibleTextList(input.nonGoals || input.non_goals || input.avoid || envelope.nonGoals || envelope.non_goals || envelope.avoid || featureSet.nonGoals || featureSet.non_goals || featureSet.avoid || snapshot.nonGoals || snapshot.non_goals || snapshot.avoid || conceptMap.nonGoals || conceptMap.non_goals || conceptMap.avoid, 8, 180), ...collectContextList(contextSources, ['nonGoals', 'non_goals', 'nonGoal', 'non_goal', 'avoid', 'notYet', 'not_yet', 'doNotBuild', 'do_not_build'], 8), ...textContextGuardrails.nonGoals, ...routeGuardrailNonGoals(ideaRoute), ], 8), assumptions: uniqueList([ ...cleanFlexibleTextList(input.assumptions || envelope.assumptions || featureSet.assumptions || snapshot.assumptions || conceptMap.assumptions, 6, 180), ...collectContextList(contextSources, ['assumptions', 'assumption', 'unknowns', 'openQuestions'], 6), ...cleanSentenceList(lensContent(assumptionsLens), 6, 180), ], 6), }; } function cleanContextText(value = '') { if (!value || typeof value !== 'object' || Array.isArray(value)) return cleanMultiline(value || '', 3000); const pieces = [ value.summary || value.description || value.notes || value.brief || '', value.targetAudience && `Target audience: ${value.targetAudience}`, ...cleanFlexibleTextList(value.constraints, 8, 180).map(item => `Constraint: ${item}`), ...cleanFlexibleTextList(value.nonGoals || value.avoid, 8, 180).map(item => `Non-goal: ${item}`), ...cleanFlexibleTextList(value.assumptions, 6, 180).map(item => `Assumption: ${item}`), ].filter(Boolean); return cleanMultiline(pieces.join('\n'), 3000); } function meaningfulTokens(text = '') { const stop = new Set(['the', 'and', 'with', 'from', 'that', 'this', 'into', 'before', 'after', 'until', 'user', 'users', 'idea', 'ideas', 'build', 'order', 'works', 'first', 'avoid', 'without', 'more', 'full', 'proof', 'manual', 'value', 'layer', 'result', 'sense', 'copyable', 'source', 'traced', 'source-traced', 'evidence', 'thread', 'threads']); return [...new Set(String(text).toLowerCase().match(/[a-z0-9][a-z0-9-]{3,}/g) || [])].filter(token => !stop.has(token)).slice(0, 8); } function isGuardrailAvoidanceMention(lowerText = '', token = '') { if (!token) return false; const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`\\b(without|avoid(?:ing)?|no|not|skip|defer|wait(?:ing)?|hold(?:ing)?|before adding|before building)\\b[^.!?]{0,60}\\b${escaped}\\b`, 'i').test(lowerText); } function nonGoalConflicts(optionText, decisionContext = {}) { const lower = String(optionText || '').toLowerCase(); return (decisionContext.nonGoals || []).filter(nonGoal => { const tokens = meaningfulTokens(nonGoal); return tokens.length > 0 && tokens.some(token => { const singular = token.endsWith('ies') ? `${token.slice(0, -3)}y` : token.replace(/(?:es|s)$/, ''); const forms = [token, singular].filter(form => form.length >= 4); return forms.some(form => lower.includes(form) && !isGuardrailAvoidanceMention(lower, form)); }); }); } function normalizeFeatureOption(item, index, fallbackId = 'feature', defaultSourceSection = '', defaultRecommendedLane = '') { const rawValue = typeof item === 'string' || typeof item === 'number' ? String(item) : ''; const raw = rawValue ? { action: rawValue } : objectFrom(item); const title = cleanText(raw.title || raw.name || raw.action || raw.move || raw.nextMove || raw.next_move || raw.nextStep || raw.next_step || raw.recommendedNextStep || raw.recommended_next_step || raw.experiment || raw.testName || raw.test_name || raw.hypothesis || raw.label || '', 140); const proofSteps = cleanTextList(raw.proofSteps || raw.proof_steps || raw.proof || raw.validationSteps || raw.validation_steps || raw.steps || raw.method, 5, 180); const dependencies = cleanTextList(raw.dependencies || raw.blockedBy, 5, 120); const evidenceNeeded = cleanText(raw.evidenceNeeded || raw.evidence_needed || raw.evidence || raw.test || raw.evidenceQuestion || raw.evidence_question || raw.questionToAnswer || raw.question_to_answer || raw.question || raw.learningGoal || raw.learning_goal || '', 260); const userValue = cleanText(raw.userValue || raw.user_value || raw.value || raw.outcome || raw.why, 260); const risk = cleanText(raw.risk || raw.assumption || raw.unknown || '', 220); const nextStep = cleanText(raw.nextStep || raw.next_step || raw.nextAction || raw.next_action || raw.firstStep || raw.first_step || raw.manualStep || raw.manual_step || raw.actionToTake || raw.action_to_take || '', 260); const successSignal = cleanText(raw.successSignal || raw.success_signal || raw.successCriteria || raw.success_criteria || raw.successMetric || raw.success_metric || raw.greenLight || raw.green_light || raw.signalToSee || raw.signal_to_see || '', 260); const killSignal = cleanText(raw.killSignal || raw.kill_signal || raw.stopSignal || raw.stop_signal || raw.redFlag || raw.red_flag || raw.failureSignal || raw.failure_signal || raw.cutIf || raw.cut_if || '', 260); const rawLane = cleanText(raw.lane || '', 40); const laneLooksLikeHint = Boolean(normalizeLaneHint(rawLane)); const sourceSection = cleanText(raw.sourceSection || raw.source_section || raw.section || raw.origin || (!laneLooksLikeHint ? rawLane : '') || defaultSourceSection, 80); const explicitSourceId = cleanText(raw.sourceId || raw.source_id || raw.sourceArtifactId || raw.source_artifact_id || raw.sourceItemId || raw.source_item_id || raw.traceId || raw.trace_id || '', 120); const sourceId = explicitSourceId || cleanText(raw.id || (sourceSection ? `${sourceSection}#${index + 1}` : ''), 120); const sourceTitle = cleanText(raw.sourceTitle || raw.source_title || raw.sourceHeading || raw.source_heading || raw.lensTitle || raw.lens_title || raw.heading || '', 140); const sourceQuote = cleanMultiline(raw.sourceQuote || raw.source_quote || raw.sourceExcerpt || raw.source_excerpt || raw.evidenceQuote || raw.evidence_quote || raw.quote || raw.originalText || raw.original_text || raw.rawText || raw.raw_text || rawValue, 420); const recommendedLane = cleanText(raw.recommendedLane || raw.recommended_lane || raw.laneHint || raw.lane_hint || raw.suggestedLane || raw.suggested_lane || (laneLooksLikeHint ? rawLane : '') || defaultRecommendedLane || '', 40).toLowerCase(); const descriptionParts = [ raw.description || raw.brief || (raw.hypothesis && raw.hypothesis !== title ? `Hypothesis: ${raw.hypothesis}` : ''), userValue && `User value: ${userValue}`, evidenceNeeded && `Evidence needed: ${evidenceNeeded}`, risk && `Risk: ${risk}`, nextStep && `Next step: ${nextStep}`, successSignal && `Success signal: ${successSignal}`, killSignal && `Kill signal: ${killSignal}`, proofSteps.length && `Proof steps: ${proofSteps.join('; ')}`, dependencies.length && `Dependencies: ${dependencies.join(', ')}`, ].filter(Boolean); return { id: cleanText(raw.id || raw.key || `${fallbackId}-${index + 1}`, 80) || `${fallbackId}-${index + 1}`, title, description: cleanText(descriptionParts.join(' '), 760), factors: { userValue, evidenceNeeded, risk, nextStep, successSignal, killSignal, proofSteps, dependencies, recommendedLane, metricHints: cleanMetricHints(raw) }, provenance: { sourceId, sourceSection, sourceTitle, sourceQuote, }, }; } function normalizeOptionIds(options = []) { const seen = new Map(); return options.map((option, index) => { const baseId = cleanText(option.id || `option-${index + 1}`, 80) || `option-${index + 1}`; const count = seen.get(baseId) || 0; seen.set(baseId, count + 1); if (count === 0) return { ...option, id: baseId }; return { ...option, id: cleanText(`${baseId}-${count + 1}`, 80), provenance: { ...(option.provenance || {}), originalId: baseId, idNormalized: true, }, }; }); } function compactCandidateGroup(group = []) { return group.filter(entry => Array.isArray(entry?.items) && entry.items.length > 0); } function buildOrderSectionGroup(buildOrder = {}, baseSection = 'buildOrder') { const source = objectFrom(buildOrder); return compactCandidateGroup([ { 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' }, ]); } function normalizeCandidateGroup(group = []) { const options = group.flatMap(entry => { const fallbackId = entry.sourceSection.toLowerCase().includes('action') ? 'action' : 'feature'; return (entry.items || []).slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, fallbackId, entry.sourceSection, entry.defaultLane)); }).filter(item => item.title).slice(0, 24); return normalizeOptionIds(options); } const buildOrderLabelSeparator = '\\s*(?::|[-–—])\\s*'; const buildOrderLabelPattern = '(build first|start here|ship first|first week|week one|first-week build order|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)'; const buildOrderLabelRegex = new RegExp(`^${buildOrderLabelPattern}${buildOrderLabelSeparator}`, 'i'); function sentenceFragments(text = '') { return cleanMultiline(text, 4000) .replace(new RegExp(`\\s+${buildOrderLabelPattern}${buildOrderLabelSeparator}`, 'gi'), '\n$1: ') .split(/\n|;|\s+[•-]\s+/) .map(part => part.trim()) .filter(Boolean); } function titleFromBuildOrderFragment(value = '') { const cleaned = cleanText(value.replace(buildOrderLabelRegex, ''), 220); const first = cleaned.split(/\s[-–—:]\s|\.\s/)[0] || cleaned; return cleanText(first.replace(/[.!?]+$/g, ''), 120); } function laneFromBuildOrderLabel(fragment = '') { if (new RegExp(`^(build first|start here|ship first|first week|week one|first-week build order|continue first|make tangible first|make tangible)${buildOrderLabelSeparator}`, 'i').test(fragment)) return 'do-first'; if (new RegExp(`^(try next|evidence next|learn next|test manually|validate next)${buildOrderLabelSeparator}`, 'i').test(fragment)) return 'validate-next'; if (new RegExp(`^(hold for later|not yet|defer|do not build yet|don't build yet)${buildOrderLabelSeparator}`, 'i').test(fragment)) return 'defer'; if (new RegExp(`^(set aside|out of scope|probably noise|park)${buildOrderLabelSeparator}`, 'i').test(fragment)) return 'park'; return ''; } function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lenses.channel', sourceTitle = 'Build Order') { 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.' : 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 } : lane === 'validate-next' ? { value: 7, effort: 3, confidence: 6, urgency: 5, risk: 3 } : undefined, sourceSection, sourceItemId: `${sourceSection}#${index + 1}`, sourceTitle, sourceExcerpt: fragment, }; }).filter(item => item.action); } function proofStepFragments(text = '') { return cleanMultiline(text, 2600) .replace(/\b(proof steps?|test manually|validate|evidence to collect|next proof)\s*:/gi, '\n') .split(/\n|;|\s+[•-]\s+|\.\s+/) .map(part => part.replace(/^\s*[-*•\d.)]+\s*/, '').trim()) .filter(Boolean) .filter(part => /\b(run|ask|show|send|test|validate|prototype|mock|observe|interview|collect|measure|prove|disprove|manual|concierge|offer|script|reply|signal)\b/i.test(part)) .filter(part => !/^\s*(avoid|no|do not|don't|defer|probably noise|park|hold for later)\b/i.test(part)) .slice(0, 3); } function proofTitleFromFragment(fragment = '') { const cleaned = cleanText(fragment .replace(/^(run|ask|show|send|test|validate|prototype|mock|observe|interview|collect|measure)\b\s*/i, match => match.trim()[0].toUpperCase() + match.trim().slice(1).toLowerCase() + ' ') .replace(/\s+before\s+.*$/i, '') .replace(/\s+and\s+record\s+.*$/i, ''), 180); return cleanText(cleaned.split(/\s[-–—:]\s/)[0] || cleaned, 120); } function optionsFromProofLensText(text = '', sourceSection = 'concept-map.lenses.question', sourceTitle = 'Proof Steps') { return proofStepFragments(text).map((fragment, index) => ({ id: `proof-step-${index + 1}`, action: proofTitleFromFragment(fragment), why: 'Scattermind named this as proof to collect before promoting more build surface.', evidence: fragment, validationSteps: [fragment], suggestedLane: 'validate-next', rankerHints: { value: 7, effort: 2, confidence: 6, urgency: 6, risk: 3 }, sourceSection, sourceItemId: `${sourceSection}#${index + 1}`, sourceTitle, sourceExcerpt: fragment, })).filter(item => item.action); } function laneFromActionThread(text = '') { if (/\b(probably noise|set aside|park|parking lot|do not build|don't build|not worth|distraction)\b/i.test(text)) return 'park'; if (/\b(defer|not yet|later|hold for later|after proof|wait until)\b/i.test(text)) return 'defer'; if (/^(manual|start|ship|build|show|turn one)\b/i.test(text)) return 'do-first'; if (/\b(test|validate|proof|ask|interview|observe|learn|evidence|signal)\b/i.test(text)) return 'validate-next'; return ''; } function optionsFromActionThreads(items = [], sourceSection = 'concept-map.threadsToHold', sourceTitle = 'Action thread') { if (!Array.isArray(items)) return []; return items.slice(0, 8).map((item, index) => { const raw = typeof item === 'string' || typeof item === 'number' ? String(item) : ''; const objectItem = objectFrom(item); const text = cleanMultiline(raw || objectItem.text || objectItem.content || objectItem.thread || objectItem.action || objectItem.title || '', 420); const lane = laneFromActionThread(text); return { id: `action-thread-${index + 1}`, action: titleFromBuildOrderFragment(text), why: text, evidence: /\b(evidence|signal|proof|test|validate|ask|observe)\b/i.test(text) ? text : 'What smallest real-world signal would prove this action deserves the active build slot?', 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, sourceItemId: `${sourceSection}#${index + 1}`, sourceTitle: cleanText(objectItem.sourceTitle || objectItem.source_title || sourceTitle, 140), sourceExcerpt: text, }; }).filter(item => item.action); } function questionActionTitle(text = '') { const cleaned = cleanText(text.replace(/\?+$/g, ''), 180); if (!cleaned) return ''; if (/^(can|could|will|would|should|does|do|is|are|what|where|when|who|how|why)\b/i.test(cleaned)) return cleanText(`Answer: ${cleaned}`, 140); return cleanText(cleaned, 140); } function optionsFromQuestionsToSitWith(items = [], sourceSection = 'concept-map.questionsToSitWith', sourceTitle = 'Question to sit with') { if (!Array.isArray(items)) return []; return items.slice(0, 8).map((item, index) => { const raw = typeof item === 'string' || typeof item === 'number' ? String(item) : ''; const objectItem = objectFrom(item); const question = cleanMultiline(raw || objectItem.question || objectItem.text || objectItem.content || objectItem.title || '', 420); return { id: `question-${index + 1}`, action: questionActionTitle(question), why: 'Scattermind left this as an open question; Ranker treats it as evidence to collect, not a build feature.', evidence: question, validationSteps: cleanTextList(objectItem.validationSteps || objectItem.validation_steps || objectItem.steps || objectItem.proofSteps || objectItem.proof_steps, 4, 180), suggestedLane: 'validate-next', rankerHints: { value: 6, effort: 2, confidence: 5, urgency: 5, risk: 3 }, sourceSection, sourceItemId: `${sourceSection}#${index + 1}`, sourceTitle: cleanText(objectItem.sourceTitle || objectItem.source_title || sourceTitle, 140), sourceExcerpt: question, }; }).filter(item => item.action && item.evidence); } function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') { const reading = objectFrom(source); const lenses = objectFrom(reading.lenses); const rawShapeLens = lenses.shape || reading.lens || reading.shape; const shapeLens = objectFrom(rawShapeLens); const shapeText = cleanMultiline(lensContent(rawShapeLens), 700); const workingName = cleanText(reading.working_name || reading.workingName || reading.snapshotTitle || reading.snapshot_title || reading.title || reading.name || '', 90); const restatedIdea = cleanText(reading.restated_idea || reading.restatedIdea || reading.ideaText || reading.idea_text || reading.idea || '', 260); const questions = cleanFlexibleTextList(reading.questions_to_sit_with || reading.questionsToSitWith || reading.evidenceQuestions || reading.evidence_questions || reading.decisionQuestions || reading.decision_questions || reading.questionsToAnswer || reading.questions_to_answer || reading.followupQuestions || reading.followup_questions || reading.openQuestions || reading.open_questions, 4, 240); const hasSnapshotShape = Boolean(workingName || restatedIdea || shapeText || questions.length); if (!hasSnapshotShape || (!shapeText && !restatedIdea && questions.length < 1)) return []; const firstQuestion = questions[0] || 'What smallest manual proof would show this is worth building first?'; const proofTitle = workingName ? `Manual proof for ${workingName}` : restatedIdea ? `Manual proof: ${restatedIdea}` : questionActionTitle(firstQuestion); const proofExcerpt = shapeText || restatedIdea || firstQuestion; const proofOption = { id: 'snapshot-manual-proof', action: proofTitle, why: restatedIdea || shapeText || 'The Snapshot clarified a possible direction, but it still needs a small real-world proof before product machinery.', evidence: firstQuestion, validationSteps: ['Run one manual version before building supporting UI', 'Ask 3 target users what they would do next'], suggestedLane: 'do-first', rankerHints: { value: 8, effort: 2, confidence: 6, urgency: 7, risk: 3 }, sourceSection: `${sourceSection}.lenses.shape`, sourceItemId: `${sourceSection}.lenses.shape#1`, sourceTitle: cleanText(shapeLens.title || 'Snapshot shape', 140), sourceExcerpt: proofExcerpt, }; const questionOptions = optionsFromQuestionsToSitWith( questions.slice(0, 3), `${sourceSection}.questionsToSitWith`, 'Snapshot decision question' ); const options = [proofOption, ...questionOptions].slice(0, 4); return options.length >= 2 ? options : []; } function firstArraySource(entries = []) { return entries.find(entry => Array.isArray(entry.items) && entry.items.length > 0) || { items: [], sourceSection: '' }; } function optionsFromBody(body = {}) { const envelope = bridgeEnvelopeFrom(body); const featureSet = featureSetFrom(body); const artifact = objectFrom(body.artifact || envelope.artifact || featureSet.artifact); const conceptMap = objectFrom(body.conceptMap || body.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); const snapshot = objectFrom(body.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || envelope.lenses || featureSet.lenses); const buildOrderLens = objectFrom(conceptMapLenses.channel || conceptMapLenses.buildOrder || conceptMap.buildOrder); const directCandidateGroup = compactCandidateGroup([ { items: body.features, sourceSection: 'features' }, { items: envelope.features, sourceSection: 'ranker-input.features' }, { items: featureSet.features, sourceSection: 'feature-set.features' }, { items: body.actions, sourceSection: 'actions' }, { items: envelope.actions, sourceSection: 'ranker-input.actions' }, { items: featureSet.actions, sourceSection: 'feature-set.actions' }, { items: body.nextActions || body.next_actions || body.nextSteps || body.next_steps || body.recommendedNextSteps || body.recommended_next_steps || body.recommendedActions || body.recommended_actions || body.suggestedActions || body.suggested_actions, sourceSection: 'nextActions' }, { items: envelope.nextActions || envelope.next_actions || envelope.nextSteps || envelope.next_steps || envelope.recommendedNextSteps || envelope.recommended_next_steps || envelope.recommendedActions || envelope.recommended_actions || envelope.suggestedActions || envelope.suggested_actions, sourceSection: 'ranker-input.nextActions' }, { items: featureSet.nextActions || featureSet.next_actions || featureSet.nextSteps || featureSet.next_steps || featureSet.recommendedNextSteps || featureSet.recommended_next_steps || featureSet.recommendedActions || featureSet.recommended_actions || featureSet.suggestedActions || featureSet.suggested_actions, sourceSection: 'feature-set.nextActions' }, { items: body.nextMoves || body.next_moves, sourceSection: 'nextMoves' }, { items: envelope.nextMoves || envelope.next_moves, sourceSection: 'ranker-input.nextMoves' }, { items: featureSet.nextMoves || featureSet.next_moves, sourceSection: 'feature-set.nextMoves' }, { items: body.possibleNextMoves || body.possible_next_moves || body.suggestedNextMoves || body.suggested_next_moves || body.recommendations || body.opportunities, sourceSection: 'possibleNextMoves' }, { items: envelope.possibleNextMoves || envelope.possible_next_moves || envelope.suggestedNextMoves || envelope.suggested_next_moves || envelope.recommendations || envelope.opportunities, sourceSection: 'ranker-input.possibleNextMoves' }, { items: featureSet.possibleNextMoves || featureSet.possible_next_moves || featureSet.suggestedNextMoves || featureSet.suggested_next_moves || featureSet.recommendations || featureSet.opportunities, sourceSection: 'feature-set.possibleNextMoves' }, { items: body.candidates, sourceSection: 'candidates' }, { items: envelope.candidates, sourceSection: 'ranker-input.candidates' }, { items: featureSet.candidates, sourceSection: 'feature-set.candidates' }, { items: body.candidateActions || body.candidate_actions || body.candidateMoves || body.candidate_moves || body.rankReadyActions || body.rank_ready_actions, sourceSection: 'candidateActions' }, { items: envelope.candidateActions || envelope.candidate_actions || envelope.candidateMoves || envelope.candidate_moves || envelope.rankReadyActions || envelope.rank_ready_actions, sourceSection: 'ranker-input.candidateActions' }, { items: featureSet.candidateActions || featureSet.candidate_actions || featureSet.candidateMoves || featureSet.candidate_moves || featureSet.rankReadyActions || featureSet.rank_ready_actions, sourceSection: 'feature-set.candidateActions' }, { items: body.doFirst || body.do_first || body.buildFirst || body.build_first || body.buildNow || body.build_now || body.continueFirst || body.continue_first || body.makeTangible || body.make_tangible || body.startHere || body.start_here, sourceSection: 'doFirst', defaultLane: 'do-first' }, { items: envelope.doFirst || envelope.do_first || envelope.buildFirst || envelope.build_first || envelope.buildNow || envelope.build_now || envelope.continueFirst || envelope.continue_first || envelope.makeTangible || envelope.make_tangible || envelope.startHere || envelope.start_here, sourceSection: 'ranker-input.doFirst', defaultLane: 'do-first' }, { items: featureSet.doFirst || featureSet.do_first || featureSet.buildFirst || featureSet.build_first || featureSet.buildNow || featureSet.build_now || featureSet.continueFirst || featureSet.continue_first || featureSet.makeTangible || featureSet.make_tangible || featureSet.startHere || featureSet.start_here, sourceSection: 'feature-set.doFirst', defaultLane: 'do-first' }, { items: body.experiments, sourceSection: 'experiments', defaultLane: 'validate-next' }, { items: body.validationTests || body.validation_tests, sourceSection: 'experiments', defaultLane: 'validate-next' }, { items: body.proofTests || body.proof_tests, sourceSection: 'experiments', defaultLane: 'validate-next' }, { items: envelope.experiments, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, { items: envelope.validationTests || envelope.validation_tests, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, { items: envelope.proofTests || envelope.proof_tests, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, { items: featureSet.experiments, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, { items: featureSet.validationTests || featureSet.validation_tests, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, { items: featureSet.proofTests || featureSet.proof_tests, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, { items: body.validateNext || body.validate_next || body.validate || body.validation || body.evidenceNext || body.evidence_next || body.tryNext || body.try_next || body.learnNext || body.learn_next || body.testManually || body.test_manually, sourceSection: 'validateNext', defaultLane: 'validate-next' }, { items: body.deferred || body.defer || body.later || body.afterProof || body.after_proof || body.holdForLater || body.hold_for_later || body.notYet || body.not_yet, sourceSection: 'deferred', defaultLane: 'defer' }, { items: body.parkingLot || body.parking_lot || body.park || body.parked || body.probablyNoise || body.probably_noise || body.setAside || body.set_aside || body.outOfScope || body.out_of_scope, sourceSection: 'parkingLot', defaultLane: 'park' }, { items: envelope.validateNext || envelope.validate_next || envelope.validate || envelope.validation || envelope.evidenceNext || envelope.evidence_next || envelope.tryNext || envelope.try_next || envelope.learnNext || envelope.learn_next || envelope.testManually || envelope.test_manually, sourceSection: 'ranker-input.validateNext', defaultLane: 'validate-next' }, { items: envelope.deferred || envelope.defer || envelope.later || envelope.afterProof || envelope.after_proof || envelope.holdForLater || envelope.hold_for_later || envelope.notYet || envelope.not_yet, sourceSection: 'ranker-input.deferred', defaultLane: 'defer' }, { items: envelope.parkingLot || envelope.parking_lot || envelope.park || envelope.parked || envelope.probablyNoise || envelope.probably_noise || envelope.setAside || envelope.set_aside || envelope.outOfScope || envelope.out_of_scope, sourceSection: 'ranker-input.parkingLot', defaultLane: 'park' }, { items: featureSet.validateNext || featureSet.validate_next || featureSet.validate || featureSet.validation || featureSet.evidenceNext || featureSet.evidence_next || featureSet.tryNext || featureSet.try_next || featureSet.learnNext || featureSet.learn_next || featureSet.testManually || featureSet.test_manually, sourceSection: 'feature-set.validateNext', defaultLane: 'validate-next' }, { items: featureSet.deferred || featureSet.defer || featureSet.later || featureSet.afterProof || featureSet.after_proof || featureSet.holdForLater || featureSet.hold_for_later || featureSet.notYet || featureSet.not_yet, sourceSection: 'feature-set.deferred', defaultLane: 'defer' }, { items: featureSet.parkingLot || featureSet.parking_lot || featureSet.park || featureSet.parked || featureSet.probablyNoise || featureSet.probably_noise || featureSet.setAside || featureSet.set_aside || featureSet.outOfScope || featureSet.out_of_scope, sourceSection: 'feature-set.parkingLot', defaultLane: 'park' }, ]); const conceptMapCandidateGroup = compactCandidateGroup([ { items: conceptMap.nextActions || conceptMap.next_actions || conceptMap.nextSteps || conceptMap.next_steps || conceptMap.recommendedNextSteps || conceptMap.recommended_next_steps || conceptMap.recommendedActions || conceptMap.recommended_actions || conceptMap.suggestedActions || conceptMap.suggested_actions, sourceSection: 'concept-map.nextActions' }, { items: conceptMap.nextMoves || conceptMap.next_moves, sourceSection: 'concept-map.nextMoves' }, { items: conceptMap.possibleNextMoves || conceptMap.possible_next_moves || conceptMap.suggestedNextMoves || conceptMap.suggested_next_moves || conceptMap.recommendations || conceptMap.opportunities, sourceSection: 'concept-map.possibleNextMoves' }, { items: conceptMap.features, sourceSection: 'concept-map.features' }, { items: conceptMap.candidates, sourceSection: 'concept-map.candidates' }, { items: conceptMap.candidateActions || conceptMap.candidate_actions || conceptMap.candidateMoves || conceptMap.candidate_moves || conceptMap.rankReadyActions || conceptMap.rank_ready_actions, sourceSection: 'concept-map.candidateActions' }, { items: conceptMap.doFirst || conceptMap.do_first || conceptMap.buildFirst || conceptMap.build_first || conceptMap.buildNow || conceptMap.build_now || conceptMap.continueFirst || conceptMap.continue_first || conceptMap.makeTangible || conceptMap.make_tangible || conceptMap.startHere || conceptMap.start_here, sourceSection: 'concept-map.doFirst', defaultLane: 'do-first' }, { items: conceptMap.validateNext || conceptMap.validate_next || conceptMap.validate || conceptMap.validation || conceptMap.evidenceNext || conceptMap.evidence_next || conceptMap.tryNext || conceptMap.try_next || conceptMap.learnNext || conceptMap.learn_next || conceptMap.testManually || conceptMap.test_manually, sourceSection: 'concept-map.validateNext', defaultLane: 'validate-next' }, { items: conceptMap.experiments, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, { items: conceptMap.validationTests || conceptMap.validation_tests, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, { items: conceptMap.proofTests || conceptMap.proof_tests, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, { items: conceptMap.deferred || conceptMap.defer || conceptMap.later || conceptMap.afterProof || conceptMap.after_proof || conceptMap.holdForLater || conceptMap.hold_for_later || conceptMap.notYet || conceptMap.not_yet, sourceSection: 'concept-map.deferred', defaultLane: 'defer' }, { items: conceptMap.parkingLot || conceptMap.parking_lot || conceptMap.park || conceptMap.parked || conceptMap.probablyNoise || conceptMap.probably_noise || conceptMap.setAside || conceptMap.set_aside || conceptMap.outOfScope || conceptMap.out_of_scope, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' }, ]); const snapshotCandidateGroup = compactCandidateGroup([ { items: snapshot.nextActions || snapshot.next_actions || snapshot.nextSteps || snapshot.next_steps || snapshot.recommendedNextSteps || snapshot.recommended_next_steps || snapshot.recommendedActions || snapshot.recommended_actions || snapshot.suggestedActions || snapshot.suggested_actions, sourceSection: 'snapshot.nextActions' }, { items: snapshot.nextMoves || snapshot.next_moves, sourceSection: 'snapshot.nextMoves' }, { items: snapshot.possibleNextMoves || snapshot.possible_next_moves || snapshot.suggestedNextMoves || snapshot.suggested_next_moves || snapshot.recommendations || snapshot.opportunities, sourceSection: 'snapshot.possibleNextMoves' }, { items: snapshot.actions, sourceSection: 'snapshot.actions' }, { items: snapshot.features, sourceSection: 'snapshot.features' }, { items: snapshot.candidates, sourceSection: 'snapshot.candidates' }, { items: snapshot.candidateActions || snapshot.candidate_actions || snapshot.candidateMoves || snapshot.candidate_moves || snapshot.rankReadyActions || snapshot.rank_ready_actions, sourceSection: 'snapshot.candidateActions' }, { items: snapshot.doFirst || snapshot.do_first || snapshot.buildFirst || snapshot.build_first || snapshot.buildNow || snapshot.build_now || snapshot.continueFirst || snapshot.continue_first || snapshot.makeTangible || snapshot.make_tangible || snapshot.startHere || snapshot.start_here, sourceSection: 'snapshot.doFirst', defaultLane: 'do-first' }, { items: snapshot.validateNext || snapshot.validate_next || snapshot.validate || snapshot.validation || snapshot.evidenceNext || snapshot.evidence_next || snapshot.tryNext || snapshot.try_next || snapshot.learnNext || snapshot.learn_next || snapshot.testManually || snapshot.test_manually, sourceSection: 'snapshot.validateNext', defaultLane: 'validate-next' }, { items: snapshot.experiments, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, { items: snapshot.validationTests || snapshot.validation_tests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, { items: snapshot.proofTests || snapshot.proof_tests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, { items: snapshot.deferred || snapshot.defer || snapshot.later || snapshot.afterProof || snapshot.after_proof || snapshot.holdForLater || snapshot.hold_for_later || snapshot.notYet || snapshot.not_yet, sourceSection: 'snapshot.deferred', defaultLane: 'defer' }, { items: snapshot.parkingLot || snapshot.parking_lot || snapshot.park || snapshot.parked || snapshot.probablyNoise || snapshot.probably_noise || snapshot.setAside || snapshot.set_aside || snapshot.outOfScope || snapshot.out_of_scope, sourceSection: 'snapshot.parkingLot', defaultLane: 'park' }, ]); const groupedCandidates = [ ...directCandidateGroup, ...snapshotCandidateGroup, ...conceptMapCandidateGroup, ...buildOrderSectionGroup(body.buildOrder || body.build_order, 'buildOrder'), ...buildOrderSectionGroup(body.buildOrderPreview || body.build_order_preview, 'buildOrderPreview'), ...buildOrderSectionGroup(envelope.buildOrder || envelope.build_order, 'ranker-input.buildOrder'), ...buildOrderSectionGroup(envelope.buildOrderPreview || envelope.build_order_preview, 'ranker-input.buildOrderPreview'), ...buildOrderSectionGroup(featureSet.buildOrder || featureSet.build_order, 'feature-set.buildOrder'), ...buildOrderSectionGroup(featureSet.buildOrderPreview || featureSet.build_order_preview, 'feature-set.buildOrderPreview'), ...buildOrderSectionGroup(snapshot.buildOrder || snapshot.build_order, 'snapshot.buildOrder'), ...buildOrderSectionGroup(snapshot.buildOrderPreview || snapshot.build_order_preview, 'snapshot.buildOrderPreview'), ...buildOrderSectionGroup(conceptMap.buildOrder || conceptMap.build_order, 'concept-map.buildOrder'), ...buildOrderSectionGroup(conceptMap.buildOrderPreview || conceptMap.build_order_preview, 'concept-map.buildOrderPreview'), ]; if (groupedCandidates.length) return normalizeCandidateGroup(groupedCandidates); const buildOrderText = lensContent(conceptMapLenses.channel) || lensContent(conceptMapLenses.buildOrder) || lensContent(conceptMap.buildOrder) || buildOrderLens.content || buildOrderLens.text || ''; 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) { const proofLens = objectFrom(conceptMapLenses.question || conceptMapLenses.proof || conceptMapLenses.validation || conceptMapLenses.evidence); const proofLensText = lensContent(conceptMapLenses.question) || lensContent(conceptMapLenses.proof) || lensContent(conceptMapLenses.validation) || lensContent(conceptMapLenses.evidence) || ''; const proofSourceTitle = cleanText(proofLens.title || 'Proof Steps', 140); const proofOptions = optionsFromProofLensText(proofLensText, 'concept-map.lenses.question', proofSourceTitle); return normalizeCandidateGroup([ { items: buildOrderOptions, sourceSection: 'concept-map.lenses.channel' }, ...(proofOptions.length ? [{ items: proofOptions, sourceSection: 'concept-map.lenses.question', defaultLane: 'validate-next' }] : []), ]); } const actionThreadSource = firstArraySource([ { items: conceptMap.threads_to_hold || conceptMap.threadsToHold || conceptMap.actionThreads || conceptMap.action_threads, sourceSection: 'concept-map.threadsToHold' }, { items: snapshot.threads_to_hold || snapshot.threadsToHold || snapshot.actionThreads || snapshot.action_threads, sourceSection: 'snapshot.threadsToHold' }, { items: envelope.threads_to_hold || envelope.threadsToHold || envelope.actionThreads || envelope.action_threads, sourceSection: 'ranker-input.threadsToHold' }, { items: featureSet.threads_to_hold || featureSet.threadsToHold || featureSet.actionThreads || featureSet.action_threads, sourceSection: 'feature-set.threadsToHold' }, { items: body.threads_to_hold || body.threadsToHold || body.actionThreads || body.action_threads, sourceSection: 'threadsToHold' }, ]); const actionThreadOptions = optionsFromActionThreads( actionThreadSource.items, actionThreadSource.sourceSection || 'threadsToHold', 'Thread to hold' ); if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); const questionSource = firstArraySource([ { items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' }, { items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' }, { items: envelope.questions_to_sit_with || envelope.questionsToSitWith || envelope.evidenceQuestions || envelope.evidence_questions || envelope.decisionQuestions || envelope.decision_questions || envelope.questionsToAnswer || envelope.questions_to_answer || envelope.followupQuestions || envelope.followup_questions || envelope.openQuestions || envelope.open_questions, sourceSection: 'ranker-input.questionsToSitWith' }, { items: featureSet.questions_to_sit_with || featureSet.questionsToSitWith || featureSet.evidenceQuestions || featureSet.evidence_questions || featureSet.decisionQuestions || featureSet.decision_questions || featureSet.questionsToAnswer || featureSet.questions_to_answer || featureSet.followupQuestions || featureSet.followup_questions || featureSet.openQuestions || featureSet.open_questions, sourceSection: 'feature-set.questionsToSitWith' }, { items: body.questions_to_sit_with || body.questionsToSitWith || body.evidenceQuestions || body.evidence_questions || body.decisionQuestions || body.decision_questions || body.questionsToAnswer || body.questions_to_answer || body.followupQuestions || body.followup_questions || body.openQuestions || body.open_questions, sourceSection: 'questionsToSitWith' }, ]); const questionOptions = optionsFromQuestionsToSitWith( questionSource.items, questionSource.sourceSection || 'questionsToSitWith', 'Question to sit with' ); if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]); const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot'); const snapshotReadingOptions = nestedSnapshotReadingOptions.length ? nestedSnapshotReadingOptions : optionsFromSnapshotReading(body, 'snapshot'); if (snapshotReadingOptions.length >= 2) return normalizeCandidateGroup([{ items: snapshotReadingOptions, sourceSection: 'snapshot', defaultLane: 'validate-next' }]); if (Array.isArray(body.options)) { return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title)); } const fallbackText = body.optionsText || envelope.optionsText || featureSet.optionsText || conceptMap.optionsText || body.idea || body.ideaText || envelope.idea || envelope.ideaText || ''; const fallbackSourceSection = body.optionsText || envelope.optionsText || featureSet.optionsText || conceptMap.optionsText ? 'optionsText' : body.idea ? 'idea' : body.ideaText ? 'ideaText' : 'optionsText'; return normalizeOptionIds(parseOptionsFromText(fallbackText, fallbackSourceSection)); } function scoreOption(option, mode, context = '', decisionContext = {}) { const factors = option.factors || {}; const text = `${option.title} ${option.description} ${factors.userValue || ''} ${factors.evidenceNeeded || ''} ${factors.risk || ''} ${(factors.proofSteps || []).join(' ')} ${context}`; const effortHits = hits(text, wordSets.effort); const riskHits = hits(text, wordSets.risk); const coreLoopHits = hits(text, ['ranked feedback map', 'feedback map', 'pasted feature', 'feature lists', 'first-pass', 'decision brief', 'expert reflections']); const bridgeHits = hits(text, ['snapshot', 'concept map', 'feature set', 'build order', 'rank-ready', 'provenance', 'next moves']); const proofHits = hits(text, ['evidence needed', 'proof steps', 'manual', 'test with', 'validate', '3 users', 'interview', 'mock', 'prototype']); const swampHits = hits(text, ['dashboard', 'workspace', 'workspaces', 'auth', 'accounts', 'billing', 'subscription', 'team voting', 'collaboration', 'admin']); const dependencyPenalty = Math.min(2.2, (factors.dependencies || []).length * 0.45); const laneHint = factors.recommendedLane || ''; const normalizedLaneHint = normalizeLaneHint(laneHint); const conflicts = nonGoalConflicts(`${option.title} ${option.description}`, decisionContext); const nonGoalPenalty = Math.min(14, conflicts.length * 7); const laneBoost = /do|first|now|build/.test(laneHint) ? 1.35 : /validate|test|proof/.test(laneHint) ? 0.35 : /defer|park|cut/.test(laneHint) ? -0.75 : 0; const lanePenalty = normalizedLaneHint === 'park' ? 18 : normalizedLaneHint === 'defer' ? 9 : 0; const heuristicMetrics = { value: Math.min(10, 4.8 + hits(text, wordSets.value) * 0.9 + coreLoopHits * 0.8 + bridgeHits * 0.75 + proofHits * 0.2 + hits(context, wordSets.value) * 0.15), feasibility: Math.max(1, Math.min(10, 8.2 - effortHits * 0.8 - riskHits * 0.35 - dependencyPenalty - conflicts.length * 1.1 + Math.min(1.1, coreLoopHits * 0.28 + bridgeHits * 0.18 + proofHits * 0.12))), confidence: Math.max(1, Math.min(10, 5.8 + coreLoopHits * 0.35 + bridgeHits * 0.28 + proofHits * 0.32 + hits(text, ['manual', 'existing', 'already', 'simple', 'clear', 'known']) * 0.7 - hits(text, ['maybe', 'unknown', 'new market', 'all users']) * 0.9)), urgency: Math.min(10, 4.9 + hits(text, wordSets.urgency) * 0.9), revenue: Math.min(10, 3.8 + hits(text, wordSets.revenue) * 1.05), novelty: Math.min(10, 4.1 + hits(text, wordSets.novelty) * 0.95), risk: Math.min(10, 2.5 + riskHits * 1.1 + Math.max(0, effortHits - 2) * 0.45 + swampHits * 0.2 + dependencyPenalty + conflicts.length * 1.25), }; const hinted = factors.metricHints || {}; const hintedFeasibility = Number.isFinite(hinted.effort) ? 11 - hinted.effort : undefined; const metrics = { value: blendMetric(heuristicMetrics.value, hinted.value), feasibility: blendMetric(heuristicMetrics.feasibility, hintedFeasibility), confidence: blendMetric(heuristicMetrics.confidence, hinted.confidence), urgency: blendMetric(heuristicMetrics.urgency, hinted.urgency), revenue: blendMetric(heuristicMetrics.revenue, hinted.revenue), novelty: blendMetric(heuristicMetrics.novelty, hinted.novelty), risk: blendMetric(heuristicMetrics.risk, hinted.risk), }; const weights = mode.weights; const weighted = Object.entries(weights).reduce((sum, [key, weight]) => sum + metrics[key] * weight, 0); const possible = Object.entries(weights).reduce((sum, [_key, weight]) => sum + Math.abs(weight) * 10, 0); const score = Math.max(0, Math.min(100, Math.round(((weighted + laneBoost) + Math.abs(Math.min(0, weights.risk || 0)) * 10) / possible * 100) - lanePenalty - nonGoalPenalty)); return { ...metrics, score, nonGoalConflicts: conflicts }; } function normalizeLaneHint(value = '') { const hint = cleanText(value, 40).toLowerCase().replace(/_/g, '-'); 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 ''; } function laneFor(option, rankIndex, total, activeRankIndex = rankIndex, activeTotal = total) { const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || ''); // Ranker should defend build order, not blindly obey Scattermind. Positive // hints can nudge scoring, but explicit negative hints and source non-goals // are safety rails: if the source already marked something as not-now, or // if the candidate conflicts with the source guardrails, never promote it // into the active proof slice just because keyword scoring liked it. The // first eligible item should still become Do first even when a hard-railed // candidate sorts above it, otherwise the handoff can end up with no defended // first move — exactly the dashboard-swamp failure the bridge exists to avoid. if (hintedLane === 'park') return { id: 'park', label: 'Park / cut', action: 'Keep out of the active plan', source: 'hint' }; if (hintedLane === 'defer') return { id: 'defer', label: 'Defer', action: 'Sequence after proof', source: 'hint' }; if (option.metrics?.nonGoalConflicts?.length) return { id: 'defer', label: 'Defer', action: 'Resolve source guardrail first', source: 'source-non-goal' }; if (activeRankIndex === 0) return { id: 'do', label: 'Do first', action: 'Build/test now' }; if (hintedLane === 'test') return { id: 'test', label: 'Validate next', action: 'Find evidence', source: 'hint' }; if (activeRankIndex < Math.max(2, Math.ceil(activeTotal * 0.32))) return { id: 'test', label: 'Validate next', action: 'Find evidence' }; if (rankIndex >= Math.max(2, Math.floor(total * 0.72))) return { id: 'park', label: 'Park / cut', action: 'Keep out of the active plan' }; return { id: 'defer', label: 'Defer', action: 'Sequence after proof' }; } function scoreDriversFor(option) { const m = option.metrics || {}; const drivers = []; if (m.nonGoalConflicts?.length) drivers.push('source guardrail conflict'); if (m.value >= 7) drivers.push('strong user value'); if (m.feasibility >= 7) drivers.push('low delivery drag'); if (m.confidence >= 7) drivers.push('clear proof path'); if (m.revenue >= 6.5) drivers.push('buyer signal'); if (m.novelty >= 6.5) drivers.push('differentiated angle'); if (option.factors?.evidenceNeeded) drivers.push('explicit evidence needed'); if ((option.factors?.dependencies || []).length === 0) drivers.push('few dependencies'); if (m.risk >= 6.5) drivers.push('high assumption risk'); return drivers.slice(0, 4); } function reasonFor(option) { const m = option.metrics; const drivers = scoreDriversFor(option).filter(driver => driver !== 'source guardrail conflict' && driver !== 'high assumption risk'); if (m.nonGoalConflicts?.length) return `it conflicts with the source non-goal “${m.nonGoalConflicts[0]}”, so it should not lead the build order`; if (option.lane?.id === 'do' && /snapshot|concept map|feature set|build order|rank/i.test(`${option.title} ${option.description}`)) return 'it strengthens the Scattermind → Ranker bridge instead of inventing a generic workspace'; if (drivers.length >= 2) return `it wins on ${drivers.join(', ')} while staying inside the current proof slice`; if (option.factors?.evidenceNeeded && m.confidence >= 6.4) return 'it names the evidence needed, so the next move can be tested instead of guessed'; if (m.revenue >= 6.4) return 'it has a clearer buyer or money signal than the rest of the list'; if (m.risk >= 6.5) return 'it is important enough to investigate, but carries assumption risk that should be tested before build'; if (m.novelty >= 6.7) return 'it is more differentiated than the safe options, but still needs proof'; return 'it has the best balanced tradeoff across value, effort, confidence, and timing'; } function concernFor(option) { const m = option.metrics; if (m.nonGoalConflicts?.length) return `Source context says not to do this yet: ${m.nonGoalConflicts.join('; ')}.`; if ((option.factors?.dependencies || []).length >= 3) return 'Too many prerequisites. Split the proof slice before treating this as build-ready.'; if (option.factors?.risk) return `The explicit risk is: ${option.factors.risk}.`; if (m.risk >= 6.5) return 'The hidden risk is pretending this is ready to build before the core assumption is proven.'; if (m.feasibility <= 4.5) return 'The likely trap is scope creep: this may need too much machinery for an MVP.'; if (m.confidence <= 4.5) return 'Evidence looks thin. Treat this as a question, not a roadmap item.'; if (m.revenue <= 4 && m.value >= 6) return 'Useful does not automatically mean sellable. Check willingness to pay or repeat use.'; return 'The main risk is sequencing: do it only if it supports the first useful proof.'; } function evidenceQuestionFor(option) { if (!option) return ''; if (option.factors?.evidenceNeeded) return option.factors.evidenceNeeded; if (option.lane?.id === 'park') return `What would make “${option.title}” worth reopening later?`; if (option.metrics?.revenue >= 6.4) return `Will a real buyer ask for or pay for “${option.title}” before it is polished?`; if (option.metrics?.risk >= 6.2) return `What is the cheapest test that could disprove “${option.title}”?`; return `Can 3–5 target users understand and act on “${option.title}” without extra explanation?`; } function nextStepFor(option) { if (!option) return ''; if (option.factors?.nextStep) return option.factors.nextStep; if (option.lane?.id === 'do') return `Run one manual proof of “${option.title}” before building supporting machinery.`; if (option.lane?.id === 'test') return `Design the smallest evidence test for “${option.title}” and collect signal from real users.`; if (option.lane?.id === 'defer') return `Keep “${option.title}” sequenced after the active proof; do not parallel-build it.`; return `Park “${option.title}” unless new evidence changes the decision.`; } function successSignalFor(option) { if (!option) return ''; if (option.factors?.successSignal) return option.factors.successSignal; if (option.metrics?.revenue >= 6.4) return 'A real prospect asks for the outcome, accepts a price, or requests the next step.'; if (option.lane?.id === 'do') return 'At least 2 of 3 real users can name why this should be first and what they would do next.'; if (option.lane?.id === 'test') return 'The test produces a clear yes/no learning, not polite interest.'; return 'New evidence makes this more urgent than the current active lane.'; } function killSignalFor(option) { if (!option) return ''; if (option.factors?.killSignal) return option.factors.killSignal; if (option.metrics?.nonGoalConflicts?.length) return 'It still conflicts with the source guardrails after review.'; if (option.metrics?.feasibility <= 4.5) return 'The proof slice needs platform work before any user signal exists.'; return 'People understand the idea but do not take, request, or value the next step.'; } function scoringNotesFor(option) { const notes = []; const m = option.metrics || {}; if (option.factors?.evidenceNeeded) notes.push('Boosted because it names evidence to collect.'); if (m.nonGoalConflicts?.length) notes.push('Penalized because it conflicts with source guardrails.'); if (m.feasibility >= 7) notes.push('Boosted for low delivery drag.'); if (m.value >= 7) notes.push('Boosted for user value.'); if (m.revenue >= 6.5) notes.push('Boosted for buyer signal.'); if (m.risk >= 6.5) notes.push('Flagged as assumption-heavy.'); if ((option.factors?.dependencies || []).length >= 2) notes.push('Penalized for dependencies.'); return notes.slice(0, 4); } function whatWouldChangeRanking(top, second, risky) { if (!top) return ['Add at least two concrete next moves with evidence needed.']; const changes = []; if (top.factors?.evidenceNeeded) changes.push(`If evidence fails for “${top.title}” (${top.factors.evidenceNeeded}), move it out of Do first.`); else changes.push(`If “${top.title}” cannot name a cheap proof step, demote it until the evidence question is clear.`); if (second) changes.push(`If “${second.title}” gets stronger proof or meaningfully lower effort, it can overtake the current first move.`); if (risky && risky.id !== top.id) changes.push(`If the riskiest assumption around “${risky.title}” is answered cheaply, re-rank before building around it.`); changes.push('If the source Concept Map adds new constraints or non-goals, re-run the order instead of editing around stale context.'); return changes.slice(0, 4); } function createDecisionBrief({ idea, context, mode, ranked, provenance, decisionContext }) { const activeRanked = ranked.filter(item => ['do', 'test'].includes(item.lane.id)); const top = ranked.find(item => item.lane.id === 'do') || activeRanked[0] || ranked[0]; const second = activeRanked.find(item => item.id !== top?.id) || ranked.find(item => item.id !== top?.id); const risky = ranked.slice().sort((a, b) => b.metrics.risk - a.metrics.risk)[0]; const deferred = ranked.filter(item => ['defer', 'park'].includes(item.lane.id)).slice(0, 3); const sourceLabel = [provenance?.snapshotTitle, provenance?.artifactId].filter(Boolean).join(' · '); const theme = top ? `The strongest signal is “${top.title}” because ${reasonFor(top)}.` : 'The list needs at least two options before ranking becomes useful.'; const quickGlance = top ? { topPick: top.title, topLane: top.lane?.label || '', whyThisWins: reasonFor(top), nextAction: nextStepFor(top), evidenceQuestion: evidenceQuestionFor(top), biggestTrap: concernFor(top), doNotBuildYet: deferred.slice(0, 2).map(item => item.title), sourceTrace: { sourceSection: top.provenance?.sourceSection || '', sourceId: top.provenance?.sourceId || '', sourceTitle: top.provenance?.sourceTitle || '', sourceQuote: top.provenance?.sourceQuote || '', }, } : null; const decisionReceipt = top ? { activeMove: top.title, activeLane: top.lane?.label || 'Do first', firstProofStep: nextStepFor(top), evidenceQuestion: evidenceQuestionFor(top), doNotStartYet: deferred.slice(0, 3).map(item => item.title), sourceAnchor: [top.provenance?.sourceSection, top.provenance?.sourceId || top.provenance?.sourceTitle].filter(Boolean).join(' · '), handoffRule: 'Only the Do first item is active. Validate one proof, then re-rank before promoting anything else.', } : null; const assumptions = [ ...(decisionContext?.assumptions || []), ...(decisionContext?.constraints || []).map(item => `Constraint: ${item}`), ...(decisionContext?.nonGoals || []).map(item => `Non-goal: ${item}`), ].slice(0, 6); return { headline: top ? `Start with ${top.title}` : 'Add options to get a ranked feedback map', summary: `${theme}${second ? ` “${second.title}” is the nearest follow-up, not a parallel first step.` : ''}${sourceLabel ? ` Source: ${sourceLabel}.` : ''}`, quickGlance, decisionReceipt, source: provenance ? { schema: provenance.schema, source: provenance.source, artifactId: provenance.artifactId, snapshotTitle: provenance.snapshotTitle, conceptMapId: provenance.conceptMapId, originalPromptExcerpt: cleanText(provenance.originalPrompt, 260), sourceSummaryExcerpt: cleanText(provenance.sourceSummary, 260), } : null, expertReflections: [ { lens: 'Product expert', text: top ? `The ranking says the first job is not to build more surface area; it is to prove the highest-signal option. ${mode.next}` : 'A product expert would ask for concrete alternatives before giving serious advice.', }, { lens: 'Scattermind simplifier', text: deferred.length ? `Park ${deferred.map(item => `“${item.title}”`).join(', ')} for now. Not bad ideas — just mental tabs you do not need open while testing the first move.` : 'The list is already narrow. Keep it that way; do not add a fake backlog around a simple decision.', }, { lens: 'Structured operator', text: risky ? `The decision quality depends on one assumption: ${risky.title}. What evidence would make this move up or down the list? Write that before committing resources.` : 'Expose the criteria, the confidence, and the thing that would change the decision. That is what makes this defensible.', }, ], next48Hours: top ? [ provenance?.artifactId ? `Open the source artifact (${provenance.artifactId}) and mark “${top.title}” as the defended first move.` : `Write a one-paragraph test for “${top.title}”.`, top.factors?.evidenceNeeded ? `Evidence to collect: ${top.factors.evidenceNeeded}.` : 'Name the evidence that would make this decision obviously right or wrong.', 'Put it in front of 3–5 real people or run it manually once.', `Do not touch ${deferred[0] ? `“${deferred[0].title}”` : 'the parked ideas'} until the first signal is real.`, ] : ['Paste 3–10 options.', 'Choose what the ranking should care about.', 'Run the first-pass judgement.'], assumptions, whatWouldChangeRanking: whatWouldChangeRanking(top, second, risky), caution: 'This is first-pass judgement, not an oracle. Change the criteria if the context changes.', }; } function rankConfidenceFor(ranked = []) { if (ranked.length < 2) return { level: 'low', reason: 'There are not enough comparable options for a confident ranking.' }; const gap = Math.abs((ranked[0].metrics?.score || 0) - (ranked[1].metrics?.score || 0)); if (gap <= 3) return { level: 'close call', reason: `The top two options are only ${gap} points apart, so treat the winner as a sequencing bet, not a law.` }; if (gap <= 8) return { level: 'medium', reason: `The top option leads by ${gap} points, but the follow-up is still worth rechecking after one proof cycle.` }; return { level: 'strong', reason: `The top option leads by ${gap} points and has the clearest first-proof profile.` }; } function closeCallsFor(ranked = []) { const calls = []; for (let index = 0; index < ranked.length - 1; index += 1) { const current = ranked[index]; const next = ranked[index + 1]; const gap = Math.abs((current.metrics?.score || 0) - (next.metrics?.score || 0)); if (gap <= 5) calls.push({ pair: [current.title, next.title], gap, note: `“${current.title}” barely beats “${next.title}”; rerank if new evidence changes effort, confidence, or buyer signal.`, }); } return calls.slice(0, 3); } function compactBuildItems(items = []) { return items.map(item => ({ id: item.id, title: item.title, reason: item.reason, nextStep: item.nextStep, evidenceQuestion: item.evidenceQuestion, successSignal: item.successSignal, killSignal: item.killSignal, concern: item.concern, sourceSection: item.provenance?.sourceSection || '', sourceId: item.provenance?.sourceId || '', sourceTitle: item.provenance?.sourceTitle || '', sourceQuote: item.provenance?.sourceQuote || '', laneSource: item.lane?.source || 'ranked', score: item.metrics?.score ?? null, confidence: item.metrics?.confidence ?? null, })); } function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expectsSourceTrace = false }) { const uniqueWarnings = [...new Set(warnings)]; const activeItems = ranked.filter(item => ['do', 'test'].includes(item.lane?.id)); const guardrailWarnings = uniqueWarnings.filter(item => /^active item .* conflicts with source non-goals/i.test(item)); const sourceWarnings = uniqueWarnings.filter(item => /^missing (source section|source context provenance)/i.test(item)); const evidenceWarnings = uniqueWarnings.filter(item => /^missing evidence needed for active item/i.test(item)); const duplicateWarnings = uniqueWarnings.filter(item => /^duplicate source id/i.test(item)); const missingArtifact = uniqueWarnings.includes('missing source artifact id'); const blockers = [ ...guardrailWarnings, ...(expectsSourceTrace ? sourceWarnings : []), ...(expectsSourceTrace ? evidenceWarnings : []), ]; const nextChecks = []; if (guardrailWarnings.length) nextChecks.push('Resolve source non-goal conflicts before treating any active item as build-ready.'); if (expectsSourceTrace && sourceWarnings.length) nextChecks.push('Attach source section / prompt or summary provenance from the Scattermind artifact.'); if (expectsSourceTrace && evidenceWarnings.length) nextChecks.push('Add evidenceNeeded to every Do first / Validate next candidate before handoff.'); if (missingArtifact) nextChecks.push(expectsSourceTrace ? 'Attach the Scattermind artifact id so the build order can be traced back.' : 'Attach a source artifact id if this came from Scattermind rather than a plain paste.'); if (duplicateWarnings.length) nextChecks.push('Review normalized duplicate IDs before downstream tools store references.'); if (!nextChecks.length && activeItems.length) nextChecks.push('Use the Do first item as the only active build slice, then rerank after evidence changes.'); const status = guardrailWarnings.length ? 'blocked' : blockers.length ? 'needs-source-context' : uniqueWarnings.length ? 'usable-with-warnings' : 'ready'; const labels = { ready: 'Ready for Ranker handoff', 'usable-with-warnings': 'Usable, but provenance is thin', 'needs-source-context': 'Needs source context before handoff', blocked: 'Blocked by source guardrails', }; const summaries = { ready: `Active order is traceable and evidence-shaped for ${activeItems.length} active item${activeItems.length === 1 ? '' : 's'}.`, 'usable-with-warnings': 'Ranking can be read now, but downstream bridge consumers should review the warnings before storing it as a defended handoff.', 'needs-source-context': 'A Scattermind-shaped artifact is present, but active items are missing trace or evidence fields needed to defend the order later.', blocked: 'An active item conflicts with source non-goals; do not continue until the guardrail is resolved or the item moves out of the active lanes.', }; return { status, label: labels[status], summary: summaries[status], blockers, nextChecks: nextChecks.slice(0, 5), activeItemCount: activeItems.length, warningCount: uniqueWarnings.length, sourceComplete: Boolean(!expectsSourceTrace || ((provenance?.originalPrompt || provenance?.sourceSummary) && activeItems.every(item => item.provenance?.sourceSection))), }; } function activeSliceFor({ ranked = [], provenance = {}, readiness = {} }) { const active = ranked.find(item => item.lane?.id === 'do') || ranked.find(item => item.lane?.id === 'test') || ranked[0] || null; const heldBack = ranked.filter(item => item.id !== active?.id && ['test', 'defer', 'park'].includes(item.lane?.id)).slice(0, 4); if (!active) return null; return { schema: 'ranker-active-slice-v1', item: { id: active.id, title: active.title, lane: active.lane?.id || 'do', laneLabel: active.lane?.label || 'Do first', reason: reasonFor(active), }, proof: { nextStep: nextStepFor(active), evidenceQuestion: evidenceQuestionFor(active), successSignal: successSignalFor(active), killSignal: killSignalFor(active), }, source: { artifactId: provenance?.artifactId || '', conceptMapId: provenance?.conceptMapId || '', snapshotTitle: provenance?.snapshotTitle || '', sourceSection: active.provenance?.sourceSection || '', sourceId: active.provenance?.sourceId || '', sourceTitle: active.provenance?.sourceTitle || '', sourceQuote: active.provenance?.sourceQuote || '', }, notNow: heldBack.map(item => ({ id: item.id, title: item.title, lane: item.lane?.id || 'defer', reason: reasonFor(item) })), readinessStatus: readiness?.status || '', rule: 'Only this active slice is build-ready. Do not promote Validate next / Defer / Park items until evidence changes and Ranker is rerun.', }; } function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {}, readiness = {}, activeSlice = null }) { const lanes = [ ['do', 'Do first'], ['test', 'Validate next'], ['defer', 'Defer'], ['park', 'Park / cut'], ]; const sourceLine = [ provenance?.source || 'Scattermind', provenance?.snapshotTitle, provenance?.artifactId, ].filter(Boolean).join(' · '); const sourceContextLine = provenance?.originalPrompt ? `Original prompt: ${cleanText(provenance.originalPrompt, 240)}` : provenance?.sourceSummary ? `Source summary: ${cleanText(provenance.sourceSummary, 240)}` : ''; const contextLines = [ decisionContext?.ideaRoute ? `Idea route: ${decisionContext.ideaRoute}` : '', decisionContext?.targetAudience ? `Audience: ${decisionContext.targetAudience}` : '', ...(decisionContext?.constraints || []).slice(0, 3).map(item => `Constraint: ${item}`), ...(decisionContext?.nonGoals || []).slice(0, 3).map(item => `Non-goal: ${item}`), ].filter(Boolean); const itemLines = lanes.flatMap(([laneId, label]) => { const laneItems = ranked.filter(item => item.lane?.id === laneId).slice(0, laneId === 'do' ? 1 : 3); return [ `## ${label}`, ...(laneItems.length ? laneItems.map(item => { const sourceTrace = [item.provenance?.sourceSection, item.provenance?.sourceId].filter(Boolean).join(' · '); return [ `- ${item.title}`, ` Why: ${reasonFor(item)}.`, ` Next: ${nextStepFor(item)}`, ` Evidence: ${evidenceQuestionFor(item)}`, sourceTrace ? ` Source: ${sourceTrace}` : '', item.provenance?.sourceQuote ? ` Quote: ${cleanText(item.provenance.sourceQuote, 180)}` : '', ].filter(Boolean).join('\n'); }) : ['- None']), '', ]; }); const activeSliceLines = activeSlice ? [ '## Active slice', `- Move: ${activeSlice.item.title}`, `- Proof: ${activeSlice.proof.nextStep}`, `- Evidence question: ${activeSlice.proof.evidenceQuestion}`, activeSlice.source.sourceSection ? `- Source: ${[activeSlice.source.sourceSection, activeSlice.source.sourceId].filter(Boolean).join(' · ')}` : '', activeSlice.notNow.length ? `- Do not start yet: ${activeSlice.notNow.map(item => item.title).join('; ')}` : '', ].filter(Boolean).join('\n') : ''; return [ '# Ranker build-order handoff', sourceLine ? `Source: ${sourceLine}` : '', sourceContextLine, readiness?.status ? `Readiness: ${readiness.status} — ${readiness.summary || ''}` : '', activeSliceLines, contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '', ...itemLines, 'Rule: only the Do first item is active. Re-rank after evidence changes; do not quietly turn this into a workspace/dashboard backlog.', ].filter(Boolean).join('\n\n'); } function createHandoffContract({ ranked, provenance, decisionContext }) { const warnings = []; const expectsSourceTrace = Boolean(provenance?.artifactId || provenance?.conceptMapId || provenance?.snapshotTitle); if (!provenance?.artifactId && provenance?.originalPrompt) warnings.push('missing source artifact id'); if (expectsSourceTrace && !provenance?.originalPrompt && !provenance?.sourceSummary) warnings.push('missing source context provenance'); const itemTrace = ranked.map(item => { 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 (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, title: item.title, lane: item.lane?.id || 'defer', sourceSection: item.provenance?.sourceSection || '', sourceId: item.provenance?.sourceId || '', sourceTitle: item.provenance?.sourceTitle || '', sourceQuote: item.provenance?.sourceQuote || '', originalId: item.provenance?.originalId || '', idNormalized: Boolean(item.provenance?.idNormalized), evidenceNeeded: item.factors?.evidenceNeeded || '', nextStep: item.nextStep || '', successSignal: item.successSignal || '', killSignal: item.killSignal || '', nonGoalConflicts: item.metrics?.nonGoalConflicts || [], }; }); const uniqueWarnings = [...new Set(warnings)]; const readiness = handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace }); const activeSlice = activeSliceFor({ ranked, provenance, readiness }); return { schema: 'rank-feedback-result-v1', source: { schema: provenance?.schema || '', source: provenance?.source || '', artifactId: provenance?.artifactId || '', snapshotTitle: provenance?.snapshotTitle || '', conceptMapId: provenance?.conceptMapId || '', originalPromptExcerpt: cleanText(provenance?.originalPrompt || '', 260), sourceSummaryExcerpt: cleanText(provenance?.sourceSummary || '', 260), hasOriginalPrompt: Boolean(provenance?.originalPrompt), hasSourceSummary: Boolean(provenance?.sourceSummary), requiresSourceTrace: expectsSourceTrace, }, decisionContext: { ideaRoute: decisionContext?.ideaRoute || '', targetAudience: decisionContext?.targetAudience || '', constraints: decisionContext?.constraints || [], nonGoals: decisionContext?.nonGoals || [], assumptions: decisionContext?.assumptions || [], }, itemTrace, warnings: uniqueWarnings, readiness, activeSlice, copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness, activeSlice }), }; } app.post('/api/rank-feedback', (req, res) => { const body = expandStoredScattermindReading(expandEmbeddedRankPayload(req.body || {})); const envelope = bridgeEnvelopeFrom(body); const idea = cleanMultiline(body?.idea || body?.ideaText || body?.idea_text || body?.opening_reflection || body?.restated_idea || envelope.idea || envelope.ideaText || envelope.idea_text || envelope.opening_reflection || envelope.restated_idea || '', 3000); const context = cleanContextText(body?.context || envelope.context || ''); const modeId = cleanText(body?.mode || envelope.mode || 'progress', 40); const mode = judgementModes[modeId] || judgementModes.progress; const provenance = cleanProvenance(body || {}); const decisionContext = cleanDecisionContext(body || {}); let options = optionsFromBody(body || {}); if (options.length < 2) return res.status(400).json({ error: 'Paste at least two options, features, ideas, or next moves to rank.' }); const bridgeContext = `${idea}\n${context}\n${provenance.snapshotTitle}\n${provenance.schema}\n${decisionContext.targetAudience}\n${decisionContext.constraints.join('\n')}\n${decisionContext.assumptions.join('\n')}`; const scoredOptions = options.map(option => ({ ...option, metrics: scoreOption(option, mode, bridgeContext, decisionContext) })) .sort((a, b) => b.metrics.score - a.metrics.score || b.metrics.value - a.metrics.value || a.metrics.risk - b.metrics.risk); const activeEligibleTotal = scoredOptions.filter(option => { const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || ''); return !['park', 'defer'].includes(hintedLane) && !option.metrics?.nonGoalConflicts?.length; }).length; let activeRankIndex = 0; options = scoredOptions.map((option, index, arr) => { const hintedLane = normalizeLaneHint(option.factors?.recommendedLane || ''); const activeEligible = !['park', 'defer'].includes(hintedLane) && !option.metrics?.nonGoalConflicts?.length; const lane = laneFor(option, index, arr.length, activeEligible ? activeRankIndex : Number.POSITIVE_INFINITY, activeEligibleTotal); if (activeEligible) activeRankIndex += 1; const rankedOption = { ...option, rank: index + 1, lane }; return { ...rankedOption, reason: reasonFor(rankedOption), concern: concernFor(rankedOption), nextStep: nextStepFor(rankedOption), evidenceQuestion: evidenceQuestionFor(rankedOption), successSignal: successSignalFor(rankedOption), killSignal: killSignalFor(rankedOption), scoreDrivers: scoreDriversFor(rankedOption), scoringNotes: scoringNotesFor(rankedOption), }; }); const brief = createDecisionBrief({ idea, context, mode, ranked: options, provenance, decisionContext }); const handoff = createHandoffContract({ ranked: options, provenance, decisionContext }); res.json({ ok: true, mode: { id: modeId in judgementModes ? modeId : 'progress', label: mode.label }, input: { idea, context, optionCount: options.length, provenance, decisionContext, embeddedPayloadSource: body._embeddedPayloadSource || '' }, ranked: options, brief, rankConfidence: rankConfidenceFor(options), closeCalls: closeCallsFor(options), handoff, availableModes: Object.entries(judgementModes).map(([id, item]) => ({ id, label: item.label })), buildOrder: { doFirst: options.filter(item => item.lane.id === 'do').map(item => item.id), validateNext: options.filter(item => item.lane.id === 'test').map(item => item.id), defer: options.filter(item => item.lane.id === 'defer').map(item => item.id), park: options.filter(item => item.lane.id === 'park').map(item => item.id), }, buildOrderDetails: { doFirst: compactBuildItems(options.filter(item => item.lane.id === 'do')), validateNext: compactBuildItems(options.filter(item => item.lane.id === 'test')), defer: compactBuildItems(options.filter(item => item.lane.id === 'defer')), park: compactBuildItems(options.filter(item => item.lane.id === 'park')), }, }); }); app.get(/.*/, (_req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); app.use((error, _req, res, _next) => { console.error('[rank]', error); res.status(error.code && error.code >= 400 && error.code < 600 ? error.code : 500).json({ error: error.message || 'Internal error' }); }); app.listen(PORT, () => console.log(`[rank] ${appVersion} listening on :${PORT}`));