diff --git a/README.md b/README.md index 95cb76b..408605f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `c Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` / `handoff.source.originalPromptExcerpt` or, when the original prompt is unavailable, `sourceSummaryExcerpt` carry the source context so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from. -Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Hold for later`, or `Set aside`; Build Order objects can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `holdForLater`, and `setAside`. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote`. +Soft Scattermind labels are accepted at the bridge boundary so Scattermind does not need to use harsh verdict copy in its own product surface. Lens text can say `Continue first`, `Make tangible`, `Try next`, `Evidence next`, `Hold for later`, or `Set aside`; Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `holdForLater`, and `setAside`. Ranker maps those to `doFirst / validateNext / defer / park` while preserving the softer original label in `sourceQuote` or candidate source trace. Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; the result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen. diff --git a/public/app.js b/public/app.js index 9d89c3c..9299f5f 100644 --- a/public/app.js +++ b/public/app.js @@ -68,7 +68,10 @@ function parsePastedJsonPayload(value) { || parsed.ideaText || parsed.idea_text || parsed.originalPrompt || parsed.original_prompt || parsed.sourceSummary || parsed.source_summary || parsed.opening_reflection || parsed.restated_idea || Array.isArray(parsed.features) || Array.isArray(parsed.actions) || Array.isArray(parsed.candidates) || Array.isArray(parsed.nextActions) || Array.isArray(parsed.next_actions) || Array.isArray(parsed.nextMoves) || Array.isArray(parsed.next_moves) - || Array.isArray(parsed.validateNext) || Array.isArray(parsed.validate_next) || Array.isArray(parsed.deferred) || Array.isArray(parsed.parkingLot) || Array.isArray(parsed.parking_lot) + || Array.isArray(parsed.doFirst) || Array.isArray(parsed.do_first) || Array.isArray(parsed.continueFirst) || Array.isArray(parsed.continue_first) || Array.isArray(parsed.makeTangible) || Array.isArray(parsed.make_tangible) + || Array.isArray(parsed.validateNext) || Array.isArray(parsed.validate_next) || Array.isArray(parsed.evidenceNext) || Array.isArray(parsed.evidence_next) || Array.isArray(parsed.tryNext) || Array.isArray(parsed.try_next) + || Array.isArray(parsed.deferred) || Array.isArray(parsed.holdForLater) || Array.isArray(parsed.hold_for_later) + || Array.isArray(parsed.parkingLot) || Array.isArray(parsed.parking_lot) || Array.isArray(parsed.setAside) || Array.isArray(parsed.set_aside) ); return looksLikeBridgePayload ? parsed : null; } catch { diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 8a20fb3..1c106e4 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -1127,7 +1127,52 @@ try { assert.equal(directEnvelopeSections.handoff.readiness.status, 'ready'); assert.deepEqual(directEnvelopeSections.handoff.warnings, []); - console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, embeddedSnapshotTop: embeddedSnapshot.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, snakeCaseBridgeTop: snakeCaseBridge.ranked[0].id, nextStepsAliasTop: nextStepsAlias.ranked[0].id, summaryGuardrailTop: summaryGuardrail.ranked[0].id, bridgeEnvelopeTop: bridgeEnvelope.ranked[0].id, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); + const softDirectLaneAliasesResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema: 'scattermind-ranker-bridge-v1', + rankReady: { + schema: 'prioritix-feature-set-v1', + sourceName: 'Scattermind', + reference_code: 'SM-SOFT-LANES-1', + working_name: 'Soft direct lane labels', + ideaText: 'Scattermind used human-friendly direct lane labels rather than a buildOrder wrapper.', + context: { + targetAudience: 'Tired non-AI-native maker', + constraints: ['Keep the bridge action-first'], + avoid: ['Avoid saved workspaces before manual proof'], + }, + continueFirst: [ + { id: 'soft-continue-proof', move: 'Soft-labelled continuation proof', why: 'Accept Continue first as the active build-order lane without asking Scattermind for harsher copy.', evidenceNeeded: 'Can one soft-labelled export produce a Do first lane?', sourceItemId: 'soft-do-1', sourceSection: 'rankReady.continueFirst', rankerHints: { value: 9, effort: 2, confidence: 8, urgency: 8, risk: 2 } }, + ], + evidenceNext: [ + { id: 'soft-evidence-copy', move: 'Soft-labelled evidence brief', why: 'Preserve Evidence next as Validate next.', evidenceNeeded: 'Does the handoff keep the softer source label traceable?', sourceItemId: 'soft-test-1', sourceSection: 'rankReady.evidenceNext' }, + ], + holdForLater: [ + { id: 'soft-later-export', move: 'Soft-labelled later export polish', why: 'Nice after proof, but not first.', evidenceNeeded: 'Does polish matter after the first proof?', sourceItemId: 'soft-defer-1', sourceSection: 'rankReady.holdForLater' }, + ], + setAside: [ + { id: 'soft-saved-workspace', move: 'Soft-labelled saved workspace dashboard', why: 'Accounts and saved projects before proof.', evidenceNeeded: 'No proof yet.', sourceItemId: 'soft-park-1', sourceSection: 'rankReady.setAside' }, + ], + }, + }), + }); + assert.equal(softDirectLaneAliasesResponse.status, 200); + const softDirectLaneAliases = await softDirectLaneAliasesResponse.json(); + assert.equal(softDirectLaneAliases.input.provenance.artifactId, 'SM-SOFT-LANES-1'); + assert.equal(softDirectLaneAliases.input.optionCount, 4); + assert.equal(softDirectLaneAliases.ranked[0].id, 'soft-continue-proof'); + assert.equal(softDirectLaneAliases.ranked[0].lane.id, 'do'); + assert.equal(softDirectLaneAliases.ranked.find(item => item.id === 'soft-evidence-copy').lane.id, 'test'); + assert.equal(softDirectLaneAliases.ranked.find(item => item.id === 'soft-evidence-copy').lane.source, 'hint'); + assert.equal(softDirectLaneAliases.ranked.find(item => item.id === 'soft-later-export').lane.id, 'defer'); + assert.equal(softDirectLaneAliases.ranked.find(item => item.id === 'soft-saved-workspace').lane.id, 'park'); + assert.equal(softDirectLaneAliases.handoff.itemTrace.find(item => item.id === 'soft-saved-workspace').sourceSection, 'rankReady.setAside'); + assert.equal(softDirectLaneAliases.handoff.readiness.status, 'ready'); + assert.deepEqual(softDirectLaneAliases.handoff.warnings, []); + + console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, nestedConceptTop: nestedConcept.ranked[0].id, nonGoalTop: nonGoal.ranked[0].id, structuredContextTop: structuredContext.ranked[0].id, lensOnlyTop: lensOnly.ranked[0].id, scattermindPaidShapeTop: scattermindPaidShape.ranked[0].id, mergedContextTop: mergedContext.ranked[0].id, embeddedJsonTop: embeddedJson.ranked[0].id, fencedJsonTop: fencedJson.ranked[0].id, embeddedSnapshotTop: embeddedSnapshot.ranked[0].id, sourceExcerptTop: sourceExcerpt.ranked[0].id, snakeCaseBridgeTop: snakeCaseBridge.ranked[0].id, nextStepsAliasTop: nextStepsAlias.ranked[0].id, summaryGuardrailTop: summaryGuardrail.ranked[0].id, bridgeEnvelopeTop: bridgeEnvelope.ranked[0].id, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); } finally { server.kill('SIGTERM'); } diff --git a/server.js b/server.js index 9f17387..d569ede 100644 --- a/server.js +++ b/server.js @@ -565,11 +565,25 @@ function looksLikeRankPayload(value = {}) { || Array.isArray(value.nextMoves) || Array.isArray(value.next_moves) || Array.isArray(value.candidates) + || 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) ); } @@ -943,6 +957,9 @@ function optionsFromBody(body = {}) { { items: body.candidates, sourceSection: 'candidates' }, { items: envelope.candidates, sourceSection: 'ranker-input.candidates' }, { items: featureSet.candidates, sourceSection: 'feature-set.candidates' }, + { 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' }, @@ -952,24 +969,28 @@ function optionsFromBody(body = {}) { { 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: envelope.validateNext || envelope.validate_next || envelope.validate || envelope.validation, sourceSection: 'ranker-input.validateNext', defaultLane: 'validate-next' }, - { items: envelope.deferred || envelope.defer || envelope.later, sourceSection: 'ranker-input.deferred', defaultLane: 'defer' }, - { items: envelope.parkingLot || envelope.parking_lot || envelope.park || envelope.parked, sourceSection: 'ranker-input.parkingLot', defaultLane: 'park' }, - { items: featureSet.validateNext || featureSet.validate_next || featureSet.validate || featureSet.validation, sourceSection: 'feature-set.validateNext', defaultLane: 'validate-next' }, - { items: featureSet.deferred || featureSet.defer || featureSet.later, sourceSection: 'feature-set.deferred', defaultLane: 'defer' }, - { items: featureSet.parkingLot || featureSet.parking_lot || featureSet.park || featureSet.parked, sourceSection: 'feature-set.parkingLot', defaultLane: 'park' }, + { 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, sourceSection: 'concept-map.nextActions' }, { items: conceptMap.nextMoves || conceptMap.next_moves, sourceSection: 'concept-map.nextMoves' }, { items: conceptMap.features, sourceSection: 'concept-map.features' }, { items: conceptMap.candidates, sourceSection: 'concept-map.candidates' }, - { items: conceptMap.validateNext || conceptMap.validate_next || conceptMap.validate || conceptMap.validation, sourceSection: 'concept-map.validateNext', defaultLane: 'validate-next' }, + { 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, sourceSection: 'concept-map.deferred', defaultLane: 'defer' }, - { items: conceptMap.parkingLot || conceptMap.parking_lot || conceptMap.park || conceptMap.parked, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' }, + { 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, sourceSection: 'snapshot.nextActions' }, @@ -977,12 +998,13 @@ function optionsFromBody(body = {}) { { items: snapshot.actions, sourceSection: 'snapshot.actions' }, { items: snapshot.features, sourceSection: 'snapshot.features' }, { items: snapshot.candidates, sourceSection: 'snapshot.candidates' }, - { items: snapshot.validateNext || snapshot.validate_next || snapshot.validate || snapshot.validation, sourceSection: 'snapshot.validateNext', defaultLane: 'validate-next' }, + { 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, sourceSection: 'snapshot.deferred', defaultLane: 'defer' }, - { items: snapshot.parkingLot || snapshot.parking_lot || snapshot.park || snapshot.parked, sourceSection: 'snapshot.parkingLot', defaultLane: 'park' }, + { 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,