diff --git a/README.md b/README.md index b1d1955..02b1d83 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Ranker's continuation job is narrow: `Snapshot / Concept Map → candidate feature/action set → Rank-ready build order` -`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, or `candidates` either at the top level or under `featureSet`, and it can consume a nested `conceptMap.nextActions / nextMoves` artifact directly, so Scattermind can hand off Concept Map next actions without renaming them into fake software features. It also returns a `handoff` object (`rank-feedback-result-v1`) with source provenance, item trace rows, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof. +`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, or `candidates` either at the top level or under `featureSet`, and it can consume a nested `conceptMap.nextActions / nextMoves` artifact directly, so Scattermind can hand off Concept Map next actions without renaming them into fake software features. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions/constraints/non-goals, and `whatWouldChangeRanking` checks, plus a `handoff` object (`rank-feedback-result-v1`) with source provenance, item trace rows, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof. Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, or inside `conceptMap.context`; Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). diff --git a/public/app.js b/public/app.js index 6d5cfe7..4ed18dd 100644 --- a/public/app.js +++ b/public/app.js @@ -1,14 +1,14 @@ const sample = { - idea: 'I’m building a lightweight product feedback tool for people with too many ideas and too many possible features. It should feel useful before becoming a workspace.', - optionsText: `- Ranked feedback map for pasted feature lists -- Expert reflections on the top options + idea: 'Scattermind clarified a messy course idea. Now I need the first build order, not a dashboard.', + optionsText: `- Manual build-order preview from one Concept Map +- Copyable decision brief with Do first / Validate next / Defer / Park +- Evidence questions beside each next move - Accounts and saved workspaces -- Team voting on feature priority -- Exportable decision brief for Slack or Notion -- Custom criteria builder -- Paid deeper product strategy pass`, - context: 'MVP, solo builder, needs to feel valuable in under two minutes, avoid dashboard swamp.', - mode: 'progress', +- Team voting on roadmap priority +- Subscription billing layer +- Polished export for sharing the defended order`, + context: 'Snapshot / Concept Map handoff, solo builder, tired non-AI-native user, avoid auth/workspaces/billing before proof.', + mode: 'mvp', }; const form = document.querySelector('#rankForm'); @@ -89,6 +89,18 @@ function renderResults(data) { Next 48 hours
    ${(brief.next48Hours || []).map((step) => `
  1. ${escapeHtml(step)}
  2. `).join('')}
+ ${(brief.whatWouldChangeRanking || []).length ? ` +
+ What would change the order +
    ${brief.whatWouldChangeRanking.map((change) => `
  1. ${escapeHtml(change)}
  2. `).join('')}
+
+ ` : ''} + ${(brief.assumptions || []).length ? ` +
+ Context carried forward + +
+ ` : ''} ${(brief.expertReflections || []).map((reflection) => `
${escapeHtml(reflection.lens)} diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 7943c78..2dc5dda 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -84,6 +84,8 @@ try { assert.ok(data.brief.next48Hours.some(item => /Open the source artifact \(snapshot_123\)/i.test(item))); assert.ok(data.brief.next48Hours.some(item => /Evidence to collect/i.test(item))); assert.match(data.brief.summary, /nearest follow-up|strongest signal/i); + assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item))); + assert.ok(Array.isArray(data.brief.assumptions)); const hintedResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', @@ -197,6 +199,8 @@ try { const nonGoal = await nonGoalResponse.json(); assert.equal(nonGoal.input.decisionContext.targetAudience, 'Tired non-AI-native solo operator'); assert.deepEqual(nonGoal.input.decisionContext.nonGoals, ['Avoid saved workspaces', 'No auth dashboard', 'No billing layer before proof']); + assert.ok(nonGoal.brief.assumptions.includes('Constraint: No account before first value')); + assert.ok(nonGoal.brief.assumptions.includes('Non-goal: Avoid saved workspaces')); assert.equal(nonGoal.ranked[0].id, 'manual-next-move', 'non-goal conflicts should beat flashy positive hints'); const workspace = nonGoal.ranked.find(item => item.id === 'workspace-autopilot'); assert.ok(workspace.metrics.nonGoalConflicts.length >= 2); diff --git a/server.js b/server.js index f61b870..541453f 100644 --- a/server.js +++ b/server.js @@ -598,13 +598,29 @@ function concernFor(option) { return 'The main risk is sequencing: do it only if it supports the first useful proof.'; } -function createDecisionBrief({ idea, context, mode, ranked, provenance }) { +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 top = ranked[0]; const second = ranked[1]; 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 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}.` : ''}`, @@ -635,6 +651,8 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance }) { '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.', }; } @@ -701,7 +719,7 @@ app.post('/api/rank-feedback', (req, res) => { concern: concernFor(rankedOption), }; }); - const brief = createDecisionBrief({ idea, context, mode, ranked: options, provenance }); + const brief = createDecisionBrief({ idea, context, mode, ranked: options, provenance, decisionContext }); const handoff = createHandoffContract({ ranked: options, provenance, decisionContext }); res.json({ ok: true,