diff --git a/README.md b/README.md index 134439c..d5e0a10 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`. 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 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. +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. Recommended payload shape: diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index a234437..58cd9a0 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -47,10 +47,10 @@ try { featureSet: { features: [ { id: 'bridge-contract', title: 'Snapshot to Ranker feature-set contract', description: 'Convert Concept Map next moves into a rank-ready feature set with provenance.', userValue: 'Preserves the user prompt and source artifact so build order is defensible.', evidenceNeeded: 'Can one generated Concept Map create 3-7 sane next moves with source sections?', proofSteps: ['Run one fixture through /api/rank-feedback'], recommendedLane: 'do-first', sourceSection: 'concept-map.nextMoves' }, - { id: 'build-order-preview', title: 'Build order preview', description: 'Show do first, validate next, defer, and park with reasons.', userValue: 'A tired builder sees the next move without opening a dashboard.', evidenceNeeded: 'Can 3 non-AI-native users understand the first recommended action?', proofSteps: ['Show a static result screen to 3 people'], recommendedLane: 'validate-next' }, - { id: 'workspace', title: 'Accounts and saved workspaces', description: 'Full dashboard with auth, workspace collaboration, team voting, and sync.', dependencies: ['auth', 'teams', 'saved projects', 'sync'], risk: 'Turns the bridge into generic dashboard swamp before the first proof.' }, - { id: 'billing', title: 'Subscription billing layer', description: 'Pricing, checkout, invoices, account plans, and admin controls.', dependencies: ['account model', 'checkout', 'fulfillment'], risk: 'Payment machinery before the continuation value is proven.' }, - { id: 'export', title: 'Exportable decision brief', description: 'Simple brief for sharing the defended build order.', evidenceNeeded: 'Does a plain brief help a user act within 48 hours?', recommendedLane: 'validate-next' }, + { id: 'build-order-preview', title: 'Build order preview', description: 'Show do first, validate next, defer, and park with reasons.', userValue: 'A tired builder sees the next move without opening a dashboard.', evidenceNeeded: 'Can 3 non-AI-native users understand the first recommended action?', proofSteps: ['Show a static result screen to 3 people'], recommendedLane: 'validate-next', sourceSection: 'concept-map.nextMoves' }, + { id: 'workspace', title: 'Accounts and saved workspaces', description: 'Full dashboard with auth, workspace collaboration, team voting, and sync.', dependencies: ['auth', 'teams', 'saved projects', 'sync'], risk: 'Turns the bridge into generic dashboard swamp before the first proof.', sourceSection: 'concept-map.deferred' }, + { id: 'billing', title: 'Subscription billing layer', description: 'Pricing, checkout, invoices, account plans, and admin controls.', dependencies: ['account model', 'checkout', 'fulfillment'], risk: 'Payment machinery before the continuation value is proven.', sourceSection: 'concept-map.deferred' }, + { id: 'export', title: 'Exportable decision brief', description: 'Simple brief for sharing the defended build order.', evidenceNeeded: 'Does a plain brief help a user act within 48 hours?', recommendedLane: 'validate-next', sourceSection: 'concept-map.nextMoves' }, { id: 'parked-bridge-dashboard', title: 'Saved Snapshot dashboard with provenance', description: 'Looks bridge-adjacent, but Scattermind already marked it as not-now because it needs auth, saved workspaces, and collaboration first.', recommendedLane: 'park', sourceSection: 'concept-map.parkingLot' }, ], }, @@ -75,6 +75,12 @@ try { assert.ok(data.ranked.find(item => item.id === 'bridge-contract').factors.metricHints.value === undefined); assert.equal(data.brief.source.artifactId, 'snapshot_123'); assert.match(data.brief.summary, /Source: Tiny shop idea clarity pass · snapshot_123/); + assert.equal(data.handoff.schema, 'rank-feedback-result-v1'); + assert.equal(data.handoff.source.artifactId, 'snapshot_123'); + assert.equal(data.handoff.source.hasOriginalPrompt, true); + assert.equal(data.handoff.itemTrace.length, data.ranked.length); + assert.equal(data.handoff.itemTrace.find(item => item.id === 'bridge-contract').sourceSection, 'concept-map.nextMoves'); + assert.deepEqual(data.handoff.warnings, []); 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); diff --git a/server.js b/server.js index c1959bd..41cefe0 100644 --- a/server.js +++ b/server.js @@ -571,6 +571,39 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance }) { }; } +function createHandoffContract({ ranked, provenance }) { + const warnings = []; + if (!provenance?.artifactId) warnings.push('missing source artifact id'); + if (!provenance?.originalPrompt) warnings.push('missing original prompt provenance'); + + const itemTrace = ranked.map(item => { + if (!item.provenance?.sourceSection) warnings.push(`missing source section for ${item.id}`); + if (!item.factors?.evidenceNeeded && ['do', 'test'].includes(item.lane?.id)) warnings.push(`missing evidence needed for active item ${item.id}`); + return { + id: item.id, + title: item.title, + lane: item.lane?.id || 'defer', + sourceSection: item.provenance?.sourceSection || '', + sourceId: item.provenance?.sourceId || '', + evidenceNeeded: item.factors?.evidenceNeeded || '', + }; + }); + + return { + schema: 'rank-feedback-result-v1', + source: { + schema: provenance?.schema || '', + source: provenance?.source || '', + artifactId: provenance?.artifactId || '', + snapshotTitle: provenance?.snapshotTitle || '', + conceptMapId: provenance?.conceptMapId || '', + hasOriginalPrompt: Boolean(provenance?.originalPrompt), + }, + itemTrace, + warnings: [...new Set(warnings)], + }; +} + app.post('/api/rank-feedback', (req, res) => { const idea = cleanMultiline(req.body?.idea || '', 3000); const context = cleanMultiline(req.body?.context || '', 3000); @@ -591,12 +624,15 @@ app.post('/api/rank-feedback', (req, res) => { concern: concernFor(rankedOption), }; }); + const brief = createDecisionBrief({ idea, context, mode, ranked: options, provenance }); + const handoff = createHandoffContract({ ranked: options, provenance }); res.json({ ok: true, mode: { id: modeId in judgementModes ? modeId : 'progress', label: mode.label }, input: { idea, context, optionCount: options.length, provenance }, ranked: options, - brief: createDecisionBrief({ idea, context, mode, ranked: options, provenance }), + brief, + handoff, 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),