From 25c7c08543858fb92fcac5842c5f140864ad48b4 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Tue, 26 May 2026 23:57:48 +0200 Subject: [PATCH] Strengthen rank feedback decision brief --- README.md | 2 +- public/app.js | 155 +++++++++++++++++++++++++++----- public/index.html | 34 +++---- public/styles.css | 15 +++- scripts/check-rank-feedback.mjs | 6 ++ server.js | 150 +++++++++++++++++++++++++++++-- 6 files changed, 315 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 3ae37b0..0c326d8 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 Concept Map directly, so Scattermind can hand off `conceptMap.nextActions / nextMoves` without renaming them into fake software features. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. 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. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. 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 Concept Map directly, so Scattermind can hand off `conceptMap.nextActions / nextMoves` without renaming them into fake software features. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions/constraints/non-goals, and `whatWouldChangeRanking` checks, lane-level `buildOrderDetails` with each item's reason, next step, evidence question, source section, lane source, score, and confidence, 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. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. 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`, inside `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker turns that structured context into readable scoring text instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. 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). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. diff --git a/public/app.js b/public/app.js index 4ed18dd..b647718 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ const sample = { - idea: 'Scattermind clarified a messy course idea. Now I need the first build order, not a dashboard.', + idea: 'Scattermind clarified a messy course idea. Now I need feedback on the feature/functionality 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 @@ -11,9 +11,17 @@ const sample = { mode: 'mvp', }; +const laneMeta = { + do: { title: 'Do first', note: 'The active first move. Give it oxygen before the backlog starts breeding.' }, + test: { title: 'Validate next', note: 'Strong candidates, but they need evidence before build time.' }, + defer: { title: 'Defer', note: 'Useful later. Dangerous if it steals attention now.' }, + park: { title: 'Park / cut', note: 'Not in the active decision. Reopen only if new evidence earns it.' }, +}; + const form = document.querySelector('#rankForm'); const results = document.querySelector('#results'); const toastEl = document.querySelector('#toast'); +let lastResult = null; function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); @@ -43,7 +51,7 @@ function laneClass(lane) { return `lane-${lane?.id || 'defer'}`; } -function renderMetrics(metrics) { +function renderMetrics(metrics = {}) { const items = [ ['Value', metrics.value], ['Feasible', metrics.feasibility], @@ -56,45 +64,147 @@ function renderMetrics(metrics) { `).join('')}`; } +function renderPills(items = []) { + if (!items.length) return ''; + return `
${items.map((item) => `${escapeHtml(item)}`).join('')}
`; +} + +function renderRankCard(item) { + return ` +
+
+ #${item.rank} + ${escapeHtml(item.lane?.label || 'Ranked')} + ${item.metrics.score} +
+

${escapeHtml(item.title)}

+ ${item.description && item.description !== item.title ? `

${escapeHtml(item.description)}

` : ''} + ${renderPills(item.scoreDrivers || [])} +

Why: ${escapeHtml(item.reason)}.

+

Concern: ${escapeHtml(item.concern)}

+
+
Next step

${escapeHtml(item.nextStep || 'Run the smallest proof step.')}

+
Evidence question

${escapeHtml(item.evidenceQuestion || 'What proof would change this ranking?')}

+
Success / kill signal

${escapeHtml(item.successSignal || '')} ${escapeHtml(item.killSignal ? `Kill if: ${item.killSignal}` : '')}

+
+ ${renderMetrics(item.metrics)} + ${renderPills(item.scoringNotes || [])} +
+ `; +} + +function renderLane(ranked, laneId) { + const items = ranked.filter((item) => item.lane?.id === laneId); + const meta = laneMeta[laneId]; + return ` +
+
+ ${escapeHtml(meta.title)} + ${items.length} +
+

${escapeHtml(meta.note)}

+
+ ${items.length ? items.map(renderRankCard).join('') : 'No items landed here.'} +
+
+ `; +} + +function markdownBrief(data) { + const brief = data.brief || {}; + const glance = brief.quickGlance || {}; + const ranked = data.ranked || []; + const lanes = ['do', 'test', 'defer', 'park']; + const laneTitles = { do: 'Do first', test: 'Validate next', defer: 'Defer', park: 'Park / cut' }; + return [ + `# ${brief.headline || 'Ranked feedback map'}`, + '', + brief.summary || '', + '', + '## Quick judgement', + `- Top pick: ${glance.topPick || 'n/a'}`, + `- Why it wins: ${glance.whyThisWins || 'n/a'}`, + `- Next action: ${glance.nextAction || 'n/a'}`, + `- Evidence question: ${glance.evidenceQuestion || 'n/a'}`, + `- Biggest trap: ${glance.biggestTrap || 'n/a'}`, + `- Confidence: ${data.rankConfidence?.level || 'n/a'} — ${data.rankConfidence?.reason || ''}`, + '', + ...lanes.flatMap((lane) => { + const items = ranked.filter((item) => item.lane?.id === lane); + return [`## ${laneTitles[lane]}`, ...(items.length ? items.map((item) => `- #${item.rank} ${item.title}: ${item.reason}. Next: ${item.nextStep}`) : ['- None']) , '']; + }), + '## Next 48 hours', + ...(brief.next48Hours || []).map((step) => `- ${step}`), + ].join('\n'); +} + +async function copyText(text, label) { + try { + await navigator.clipboard.writeText(text); + toast(`${label} copied.`); + } catch { + toast('Clipboard blocked. Browser being a tiny tyrant.'); + } +} + +function attachResultActions(data) { + document.querySelector('#copyBrief')?.addEventListener('click', () => copyText(markdownBrief(data), 'Decision brief')); + document.querySelector('#copyActions')?.addEventListener('click', () => copyText((data.brief?.next48Hours || []).map((step, index) => `${index + 1}. ${step}`).join('\n'), '48h actions')); + document.querySelector('#copyJson')?.addEventListener('click', () => copyText(JSON.stringify({ brief: data.brief, ranked: data.ranked, buildOrder: data.buildOrderDetails, handoff: data.handoff }, null, 2), 'JSON handoff')); +} + function renderResults(data) { + lastResult = data; const ranked = data.ranked || []; const brief = data.brief || {}; + const glance = brief.quickGlance || {}; results.hidden = false; results.innerHTML = ` -
-

${escapeHtml(data.mode?.label || 'Ranked feedback')}

+
+

${escapeHtml(data.mode?.label || 'Ranked feedback')} · first-pass judgement memo

${escapeHtml(brief.headline || 'Ranked feedback map')}

${escapeHtml(brief.summary || '')}

+
Not an oracle${escapeHtml(data.rankConfidence?.level || 'first pass')} confidence${ranked.length} items judged
-
- ${ranked.map((item) => ` -
-
- #${item.rank} - ${escapeHtml(item.lane?.label || 'Ranked')} - ${item.metrics.score} -
-

${escapeHtml(item.title)}

- ${item.description && item.description !== item.title ? `

${escapeHtml(item.description)}

` : ''} -

Why: ${escapeHtml(item.reason)}.

-

Concern: ${escapeHtml(item.concern)}

- ${renderMetrics(item.metrics)} -
- `).join('')} +
+
Do this first${escapeHtml(glance.topPick || 'Add options')}
+
Why it wins

${escapeHtml(glance.whyThisWins || 'The list needs more comparable options.')}

+
Proof to collect

${escapeHtml(glance.evidenceQuestion || 'Name the evidence that would change the ranking.')}

+
Trap to avoid

${escapeHtml(glance.biggestTrap || brief.caution || 'Do not treat first-pass judgement as final truth.')}

+
+ +
+ + +
-
+
+ ${['do', 'test', 'defer', 'park'].map((lane) => renderLane(ranked, lane)).join('')} +
+ +
Next 48 hours
    ${(brief.next48Hours || []).map((step) => `
  1. ${escapeHtml(step)}
  2. `).join('')}
+
+ Rank confidence +

${escapeHtml(data.rankConfidence?.level || 'First pass')} — ${escapeHtml(data.rankConfidence?.reason || brief.caution || '')}

+
${(brief.whatWouldChangeRanking || []).length ? `
What would change the order
    ${brief.whatWouldChangeRanking.map((change) => `
  1. ${escapeHtml(change)}
  2. `).join('')}
` : ''} + ${(data.closeCalls || []).length ? ` +
+ Close calls +
    ${data.closeCalls.map((call) => `
  1. ${escapeHtml(call.note)}
  2. `).join('')}
+
+ ` : ''} ${(brief.assumptions || []).length ? `
Context carried forward @@ -102,7 +212,7 @@ function renderResults(data) {
` : ''} ${(brief.expertReflections || []).map((reflection) => ` -
+
${escapeHtml(reflection.lens)}

${escapeHtml(reflection.text)}

@@ -110,6 +220,7 @@ function renderResults(data) {

${escapeHtml(brief.caution || 'First-pass judgement, not an oracle.')}

`; + attachResultActions(data); results.scrollIntoView({ behavior: 'smooth', block: 'start' }); } @@ -118,7 +229,7 @@ async function createFeedbackMap(event) { const submit = form.querySelector('button[type="submit"]'); const payload = Object.fromEntries(new FormData(form).entries()); submit.disabled = true; - submit.textContent = 'Ranking…'; + submit.textContent = 'Judging…'; try { const response = await fetch('/api/rank-feedback', { method: 'POST', diff --git a/public/index.html b/public/index.html index 5d33573..eb7ebc0 100644 --- a/public/index.html +++ b/public/index.html @@ -3,10 +3,10 @@ - - - Ranker — ranked feedback maps for messy decisions - + + + Ranker — feedback front door for messy decisions +
@@ -22,9 +22,9 @@
-

Decision feedback for scatterminds, builders, and structured people

-

Dump your options. See what deserves attention first.

-

Ranker turns a messy list of ideas, features, offers, roadmap items, or next moves into a ranked feedback map: what to test, build, defer, or drop — and why.

+

Feedback intake for ideas, features, offers, and next moves

+

Submit the mess. Get a defended first move.

+

Ranker is the front door to useful feedback: paste an idea plus the features or possible moves around it, choose the judgement lens, and get a decision brief with reasons, risks, expert reflections, and next steps.

Rank a messy list @@ -57,19 +57,19 @@
-

MVP · no account · no dashboard swamp

-

Rank the pile

-

Paste one idea plus the possible features, directions, offers, or next steps. Pick what the ranking should care about. Ranker gives a first-pass decision brief.

+

MVP · feedback front door · no account · no dashboard swamp

+

Send a decision brief to the room

+

Describe what you want feedback on, then list the possible features, directions, offers, or next steps. Ranker gives you the quick judgement first, then the deeper reflections.