From ce2e9a65b70b873d19a0875c1d41cdb0c16146df Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Tue, 26 May 2026 22:43:17 +0200 Subject: [PATCH] Accept Scattermind action-set rank feedback --- README.md | 4 ++-- scripts/check-rank-feedback.mjs | 31 ++++++++++++++++++++++++++++++- server.js | 19 ++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d5e0a10..737b48f 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ 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 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`, 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. -Feature 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. +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. Recommended payload shape: diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 58cd9a0..80e2d4e 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -110,7 +110,36 @@ try { assert.ok(hinted.ranked[0].factors.metricHints.value >= 9); assert.ok(hinted.ranked.find(item => item.id === 'ai-autopilot-roadmap').metrics.risk > hinted.ranked[0].metrics.risk); - console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); + const actionsResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema: 'prioritix-feature-set-v1', + sourceName: 'Scattermind', + artifactId: 'concept_map_actions', + snapshotTitle: 'Workshop idea continuation', + originalPrompt: 'I want to turn a workshop idea into the first useful thing to build.', + idea: 'Scattermind emitted next actions rather than feature objects; Ranker should still defend build order.', + context: 'Candidate action set from Concept Map. Keep it action-first and avoid generic workspace layers.', + mode: 'mvp', + featureSet: { + actions: [ + { id: 'manual-preview', action: 'Manual build-order preview', why: 'A user sees one defended next move before any app machinery exists.', evidence: 'Can two tired users explain what to do next?', validationSteps: ['Create one static preview from the Concept Map'], suggestedLane: 'do-first', sourceSection: 'concept-map.nextActions' }, + { id: 'saved-workspace', action: 'Saved project workspace', why: 'Keep every idea and roadmap in an account dashboard.', dependencies: ['auth', 'database permissions', 'workspace model', 'sync'], risk: 'Dashboard swamp before the continuation proof.', suggestedLane: 'park', sourceSection: 'concept-map.parkingLot' }, + { id: 'text-export', action: 'Copyable decision brief', why: 'Let the user paste the defended order into notes or a chat.', evidence: 'Does a plain text brief help them act within 48 hours?', validationSteps: ['Export the top lane and concerns as text'], suggestedLane: 'validate-next', sourceSection: 'concept-map.nextActions' }, + ], + }, + }), + }); + assert.equal(actionsResponse.status, 200); + const actions = await actionsResponse.json(); + assert.equal(actions.input.optionCount, 3); + assert.equal(actions.ranked[0].id, 'manual-preview', 'action-shaped Concept Map next moves should be rankable without a features wrapper'); + assert.equal(actions.ranked.find(item => item.id === 'manual-preview').provenance.sourceSection, 'concept-map.nextActions'); + assert.equal(actions.ranked.find(item => item.id === 'saved-workspace').lane.id, 'park'); + assert.deepEqual(actions.handoff.warnings, []); + + console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); } finally { server.kill('SIGTERM'); } diff --git a/server.js b/server.js index 41cefe0..efbba0e 100644 --- a/server.js +++ b/server.js @@ -432,11 +432,24 @@ function normalizeFeatureOption(item, index, fallbackId = 'feature') { }; } +function firstArray(...values) { + return values.find(Array.isArray) || null; +} + function optionsFromBody(body = {}) { const featureSet = body.featureSet && typeof body.featureSet === 'object' ? body.featureSet : {}; - const rawFeatures = Array.isArray(body.features) ? body.features : Array.isArray(featureSet.features) ? featureSet.features : null; - if (rawFeatures) { - return rawFeatures.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index)).filter(item => item.title); + const rawCandidates = firstArray( + body.features, + featureSet.features, + body.actions, + featureSet.actions, + body.nextMoves, + featureSet.nextMoves, + body.candidates, + featureSet.candidates + ); + if (rawCandidates) { + return rawCandidates.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index)).filter(item => item.title); } if (Array.isArray(body.options)) { return body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option')).filter(item => item.title);