diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 0108e76..6aba208 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -1043,7 +1043,50 @@ try { assert.equal(summaryGuardrail.ranked.find(item => item.id === 'billing-summary').lane.source, 'source-non-goal'); assert.deepEqual(summaryGuardrail.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, duplicateIds: duplicateIds.ranked.map(item => item.id), readiness: data.handoff.readiness.status, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); + const bridgeEnvelopeResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema: 'scattermind-ranker-bridge-v1', + rankerInput: { + schema: 'prioritix-feature-set-v1', + sourceName: 'Scattermind', + reference_code: 'SM-BRIDGE-ENV-1', + working_name: 'Envelope bridge export', + ideaText: 'A paid Concept Map clarified the idea and now needs a defended build order.', + context: { + targetAudience: 'Tired solo operator', + constraints: ['Manual source-traced proof before product shell'], + avoid: ['Avoid accounts, billing, and saved workspaces before bridge proof'], + }, + candidateSet: { + nextActions: [ + { id: 'envelope-manual-proof', action: 'Envelope manual build-order proof', why: 'Proves a wrapped Scattermind export can become a Rank-ready first move.', evidenceNeeded: 'Can one wrapped Concept Map produce a traceable Do first lane?', sourceSection: 'rankerInput.candidateSet.nextActions', sourceItemId: 'env-next-1', suggestedLane: 'do-first', rankerHints: { value: 9, effort: 2, confidence: 8, urgency: 8, risk: 2 } }, + { id: 'envelope-copyable-brief', action: 'Envelope copyable decision brief', why: 'Keeps provenance and next step portable.', evidenceNeeded: 'Does the copyable handoff include the wrapped source?', sourceSection: 'rankerInput.candidateSet.nextActions', sourceItemId: 'env-next-2', suggestedLane: 'validate-next' }, + ], + parkingLot: [ + { id: 'envelope-workspace', action: 'Envelope saved workspace dashboard', why: 'Accounts, billing, saved projects, and collaboration.', evidenceNeeded: 'No bridge proof yet.', suggestedLane: 'park', sourceSection: 'rankerInput.candidateSet.parkingLot', sourceItemId: 'env-park-1' }, + ], + }, + }, + }), + }); + assert.equal(bridgeEnvelopeResponse.status, 200); + const bridgeEnvelope = await bridgeEnvelopeResponse.json(); + assert.equal(bridgeEnvelope.input.provenance.artifactId, 'SM-BRIDGE-ENV-1'); + assert.equal(bridgeEnvelope.input.provenance.snapshotTitle, 'Envelope bridge export'); + assert.equal(bridgeEnvelope.input.provenance.originalPrompt, 'A paid Concept Map clarified the idea and now needs a defended build order.'); + assert.equal(bridgeEnvelope.input.decisionContext.targetAudience, 'Tired solo operator'); + assert.ok(bridgeEnvelope.input.decisionContext.nonGoals.includes('Avoid accounts, billing, and saved workspaces before bridge proof')); + assert.equal(bridgeEnvelope.input.context, 'Target audience: Tired solo operator\nConstraint: Manual source-traced proof before product shell\nNon-goal: Avoid accounts, billing, and saved workspaces before bridge proof'); + assert.equal(bridgeEnvelope.ranked[0].id, 'envelope-manual-proof'); + assert.equal(bridgeEnvelope.ranked[0].provenance.sourceSection, 'rankerInput.candidateSet.nextActions'); + assert.equal(bridgeEnvelope.ranked.find(item => item.id === 'envelope-workspace').lane.id, 'park'); + assert.equal(bridgeEnvelope.handoff.readiness.status, 'ready'); + assert.match(bridgeEnvelope.handoff.copyableText, /SM-BRIDGE-ENV-1/); + assert.deepEqual(bridgeEnvelope.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, 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 fb6a938..208bcc7 100644 --- a/server.js +++ b/server.js @@ -486,11 +486,61 @@ function objectFrom(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } +function bridgeEnvelopeFrom(input = {}) { + const body = objectFrom(input); + return objectFrom( + body.rankerInput + || body.ranker_input + || body.rankerHandoff + || body.ranker_handoff + || body.rankReady + || body.rank_ready + || body.bridge + || body.bridgePayload + || body.bridge_payload + ); +} + +function featureSetFrom(input = {}) { + const body = objectFrom(input); + const envelope = bridgeEnvelopeFrom(body); + return objectFrom( + body.featureSet + || body.feature_set + || body.candidateSet + || body.candidate_set + || body.candidateFeatureSet + || body.candidate_feature_set + || body.rankReadyFeatureSet + || body.rank_ready_feature_set + || envelope.featureSet + || envelope.feature_set + || envelope.candidateSet + || envelope.candidate_set + || envelope.candidateFeatureSet + || envelope.candidate_feature_set + || envelope.rankReadyFeatureSet + || envelope.rank_ready_feature_set + ); +} + function looksLikeRankPayload(value = {}) { return Boolean( value.schema || value.featureSet || value.feature_set + || value.candidateSet + || value.candidate_set + || value.candidateFeatureSet + || value.candidate_feature_set + || value.rankerInput + || value.ranker_input + || value.rankerHandoff + || value.ranker_handoff + || value.rankReady + || value.rank_ready + || value.bridgePayload + || value.bridge_payload || value.snapshot || value.conceptMap || value.concept_map @@ -589,19 +639,20 @@ function expandEmbeddedRankPayload(body = {}) { } function cleanProvenance(input = {}) { - const featureSet = objectFrom(input.featureSet || input.feature_set); - const artifact = objectFrom(input.artifact || featureSet.artifact); - const conceptMap = objectFrom(input.conceptMap || input.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); - const snapshot = objectFrom(input.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); - const source = objectFrom(input.source || featureSet.source || artifact.source); - const sourceSummary = input.sourceSummary || input.source_summary || input.opening_reflection || input.restated_idea || artifact.sourceSummary || artifact.source_summary || artifact.opening_reflection || snapshot.sourceSummary || snapshot.source_summary || snapshot.restated_idea || conceptMap.sourceSummary || conceptMap.source_summary || conceptMap.opening_reflection || conceptMap.restated_idea || ''; + const envelope = bridgeEnvelopeFrom(input); + const featureSet = featureSetFrom(input); + const artifact = objectFrom(input.artifact || envelope.artifact || featureSet.artifact); + const conceptMap = objectFrom(input.conceptMap || input.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); + const snapshot = objectFrom(input.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); + const source = objectFrom(input.source || envelope.source || featureSet.source || artifact.source); + const sourceSummary = input.sourceSummary || input.source_summary || input.opening_reflection || input.restated_idea || envelope.sourceSummary || envelope.source_summary || envelope.opening_reflection || envelope.restated_idea || artifact.sourceSummary || artifact.source_summary || artifact.opening_reflection || snapshot.sourceSummary || snapshot.source_summary || snapshot.restated_idea || conceptMap.sourceSummary || conceptMap.source_summary || conceptMap.opening_reflection || conceptMap.restated_idea || ''; return { - schema: cleanText(input.schema || featureSet.schema || artifact.schema || input.type || 'prioritix-feature-set-v1', 80), - source: cleanText(input.sourceName || input.source_name || featureSet.sourceName || featureSet.source_name || source.name || artifact.sourceName || artifact.source_name || 'Scattermind', 80), - artifactId: cleanText(input.artifactId || input.artifact_id || input.sourceArtifactId || input.source_artifact_id || input.referenceCode || input.reference_code || artifact.id || source.artifactId || source.artifact_id || conceptMap.artifactId || conceptMap.artifact_id || conceptMap.id || conceptMap.referenceCode || conceptMap.reference_code || snapshot.artifactId || snapshot.artifact_id || snapshot.id || '', 120), - snapshotTitle: cleanText(input.snapshotTitle || input.snapshot_title || input.working_name || input.workingName || artifact.snapshotTitle || artifact.snapshot_title || snapshot.title || snapshot.name || conceptMap.snapshotTitle || conceptMap.snapshot_title || conceptMap.working_name || conceptMap.workingName || input.ideaTitle || input.idea_title || '', 160), - conceptMapId: cleanText(input.conceptMapId || input.concept_map_id || artifact.conceptMapId || artifact.concept_map_id || conceptMap.id || conceptMap.artifactId || conceptMap.artifact_id || input.referenceCode || input.reference_code || conceptMap.referenceCode || conceptMap.reference_code || '', 120), - originalPrompt: cleanMultiline(input.originalPrompt || input.original_prompt || input.initialPrompt || input.initial_prompt || input.ideaText || input.idea_text || input.prompt || artifact.originalPrompt || artifact.original_prompt || source.originalPrompt || source.original_prompt || snapshot.originalPrompt || snapshot.original_prompt || snapshot.prompt || conceptMap.originalPrompt || conceptMap.original_prompt || conceptMap.ideaText || conceptMap.idea_text || input.idea || '', 1200), + schema: cleanText(input.schema || envelope.schema || featureSet.schema || artifact.schema || input.type || envelope.type || 'prioritix-feature-set-v1', 80), + source: cleanText(input.sourceName || input.source_name || envelope.sourceName || envelope.source_name || featureSet.sourceName || featureSet.source_name || source.name || artifact.sourceName || artifact.source_name || 'Scattermind', 80), + artifactId: cleanText(input.artifactId || input.artifact_id || input.sourceArtifactId || input.source_artifact_id || input.referenceCode || input.reference_code || envelope.artifactId || envelope.artifact_id || envelope.sourceArtifactId || envelope.source_artifact_id || envelope.referenceCode || envelope.reference_code || artifact.id || source.artifactId || source.artifact_id || conceptMap.artifactId || conceptMap.artifact_id || conceptMap.id || conceptMap.referenceCode || conceptMap.reference_code || snapshot.artifactId || snapshot.artifact_id || snapshot.id || '', 120), + snapshotTitle: cleanText(input.snapshotTitle || input.snapshot_title || input.working_name || input.workingName || envelope.snapshotTitle || envelope.snapshot_title || envelope.working_name || envelope.workingName || artifact.snapshotTitle || artifact.snapshot_title || snapshot.title || snapshot.name || conceptMap.snapshotTitle || conceptMap.snapshot_title || conceptMap.working_name || conceptMap.workingName || input.ideaTitle || input.idea_title || envelope.ideaTitle || envelope.idea_title || '', 160), + conceptMapId: cleanText(input.conceptMapId || input.concept_map_id || envelope.conceptMapId || envelope.concept_map_id || artifact.conceptMapId || artifact.concept_map_id || conceptMap.id || conceptMap.artifactId || conceptMap.artifact_id || input.referenceCode || input.reference_code || envelope.referenceCode || envelope.reference_code || conceptMap.referenceCode || conceptMap.reference_code || '', 120), + originalPrompt: cleanMultiline(input.originalPrompt || input.original_prompt || input.initialPrompt || input.initial_prompt || input.ideaText || input.idea_text || input.prompt || envelope.originalPrompt || envelope.original_prompt || envelope.initialPrompt || envelope.initial_prompt || envelope.ideaText || envelope.idea_text || envelope.prompt || artifact.originalPrompt || artifact.original_prompt || source.originalPrompt || source.original_prompt || snapshot.originalPrompt || snapshot.original_prompt || snapshot.prompt || conceptMap.originalPrompt || conceptMap.original_prompt || conceptMap.ideaText || conceptMap.idea_text || input.idea || envelope.idea || '', 1200), sourceSummary: cleanMultiline(sourceSummary, 1200), }; } @@ -643,11 +694,12 @@ function contextGuardrailText(value = '') { } function cleanDecisionContext(input = {}) { - const featureSet = objectFrom(input.featureSet || input.feature_set); - const artifact = objectFrom(input.artifact || featureSet.artifact); - const conceptMap = objectFrom(input.conceptMap || input.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); - const snapshot = objectFrom(input.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); - const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || featureSet.lenses); + const envelope = bridgeEnvelopeFrom(input); + const featureSet = featureSetFrom(input); + const artifact = objectFrom(input.artifact || envelope.artifact || featureSet.artifact); + const conceptMap = objectFrom(input.conceptMap || input.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); + const snapshot = objectFrom(input.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); + const conceptMapLenses = objectFrom(conceptMap.lenses || input.lenses || envelope.lenses || featureSet.lenses); const riskLens = conceptMapLenses.risk || conceptMapLenses.risks || conceptMapLenses.boundaries || conceptMapLenses.notYet; const audienceLens = conceptMapLenses.audience || conceptMapLenses.who || conceptMapLenses.customer || conceptMapLenses.users; const constraintsLens = conceptMapLenses.constraints || conceptMapLenses.boundaries || conceptMapLenses.scope; @@ -655,11 +707,13 @@ function cleanDecisionContext(input = {}) { const structuredContext = objectFrom(input.context); const contextSources = [ input.decisionContext, + envelope.decisionContext, featureSet.decisionContext, artifact.decisionContext, conceptMap.decisionContext, snapshot.decisionContext, structuredContext, + envelope.context, conceptMap.context, snapshot.context, featureSet.context, @@ -667,6 +721,7 @@ function cleanDecisionContext(input = {}) { ]; const textContextGuardrails = guardrailsFromContextText([ contextGuardrailText(input.context || ''), + contextGuardrailText(envelope.context || ''), contextGuardrailText(featureSet.context || ''), contextGuardrailText(artifact.context || ''), contextGuardrailText(snapshot.context || ''), @@ -677,20 +732,20 @@ function cleanDecisionContext(input = {}) { snapshot.risk || snapshot.whatNotToBuildYet || snapshot.notYet || '', ].filter(Boolean).join('\n')); return { - targetAudience: cleanText(input.targetAudience || input.target_audience || featureSet.targetAudience || featureSet.target_audience || snapshot.targetAudience || snapshot.target_audience || firstContextText(contextSources, ['targetAudience', 'target_audience', 'audience', 'who', 'whoItHelps', 'who_it_helps', 'customer', 'users']) || conceptMap.targetAudience || conceptMap.target_audience || lensContent(audienceLens), 180), + targetAudience: cleanText(input.targetAudience || input.target_audience || envelope.targetAudience || envelope.target_audience || featureSet.targetAudience || featureSet.target_audience || snapshot.targetAudience || snapshot.target_audience || firstContextText(contextSources, ['targetAudience', 'target_audience', 'audience', 'who', 'whoItHelps', 'who_it_helps', 'customer', 'users']) || conceptMap.targetAudience || conceptMap.target_audience || lensContent(audienceLens), 180), constraints: uniqueList([ - ...cleanFlexibleTextList(input.constraints || featureSet.constraints || snapshot.constraints || conceptMap.constraints, 8, 180), + ...cleanFlexibleTextList(input.constraints || envelope.constraints || featureSet.constraints || snapshot.constraints || conceptMap.constraints, 8, 180), ...collectContextList(contextSources, ['constraints', 'constraint', 'boundaries', 'scope'], 8), ...cleanSentenceList(lensContent(constraintsLens), 8, 180), ...textContextGuardrails.constraints, ], 8), nonGoals: uniqueList([ - ...cleanFlexibleTextList(input.nonGoals || input.non_goals || input.avoid || featureSet.nonGoals || featureSet.non_goals || featureSet.avoid || snapshot.nonGoals || snapshot.non_goals || snapshot.avoid || conceptMap.nonGoals || conceptMap.non_goals || conceptMap.avoid, 8, 180), + ...cleanFlexibleTextList(input.nonGoals || input.non_goals || input.avoid || envelope.nonGoals || envelope.non_goals || envelope.avoid || featureSet.nonGoals || featureSet.non_goals || featureSet.avoid || snapshot.nonGoals || snapshot.non_goals || snapshot.avoid || conceptMap.nonGoals || conceptMap.non_goals || conceptMap.avoid, 8, 180), ...collectContextList(contextSources, ['nonGoals', 'non_goals', 'nonGoal', 'non_goal', 'avoid', 'notYet', 'not_yet', 'doNotBuild', 'do_not_build'], 8), ...textContextGuardrails.nonGoals, ], 8), assumptions: uniqueList([ - ...cleanFlexibleTextList(input.assumptions || featureSet.assumptions || snapshot.assumptions || conceptMap.assumptions, 6, 180), + ...cleanFlexibleTextList(input.assumptions || envelope.assumptions || featureSet.assumptions || snapshot.assumptions || conceptMap.assumptions, 6, 180), ...collectContextList(contextSources, ['assumptions', 'assumption', 'unknowns', 'openQuestions'], 6), ...cleanSentenceList(lensContent(assumptionsLens), 6, 180), ], 6), @@ -865,16 +920,19 @@ function optionsFromBuildOrderText(text = '', sourceSection = 'concept-map.lense } function optionsFromBody(body = {}) { - const featureSet = objectFrom(body.featureSet || body.feature_set); - const artifact = objectFrom(body.artifact || featureSet.artifact); - const conceptMap = objectFrom(body.conceptMap || body.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); - const snapshot = objectFrom(body.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); - const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || featureSet.lenses); + const envelope = bridgeEnvelopeFrom(body); + const featureSet = featureSetFrom(body); + const artifact = objectFrom(body.artifact || envelope.artifact || featureSet.artifact); + const conceptMap = objectFrom(body.conceptMap || body.concept_map || envelope.conceptMap || envelope.concept_map || featureSet.conceptMap || featureSet.concept_map || artifact.conceptMap || artifact.concept_map); + const snapshot = objectFrom(body.snapshot || envelope.snapshot || featureSet.snapshot || artifact.snapshot || conceptMap.snapshot); + const conceptMapLenses = objectFrom(conceptMap.lenses || body.lenses || envelope.lenses || featureSet.lenses); const buildOrderLens = objectFrom(conceptMapLenses.channel || conceptMapLenses.buildOrder || conceptMap.buildOrder); const directCandidateGroup = compactCandidateGroup([ { items: body.features, sourceSection: 'features' }, + { items: envelope.features, sourceSection: 'ranker-input.features' }, { items: featureSet.features, sourceSection: 'feature-set.features' }, { items: body.actions, sourceSection: 'actions' }, + { items: envelope.actions, sourceSection: 'ranker-input.actions' }, { items: featureSet.actions, sourceSection: 'feature-set.actions' }, { items: body.nextActions || body.next_actions || body.nextSteps || body.next_steps || body.recommendedNextSteps || body.recommended_next_steps, sourceSection: 'nextActions' }, { items: featureSet.nextActions || featureSet.next_actions || featureSet.nextSteps || featureSet.next_steps || featureSet.recommendedNextSteps || featureSet.recommended_next_steps, sourceSection: 'feature-set.nextActions' }, @@ -888,6 +946,9 @@ 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: 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' }, ]); const conceptMapCandidateGroup = compactCandidateGroup([ { items: conceptMap.nextActions || conceptMap.next_actions || conceptMap.nextSteps || conceptMap.next_steps || conceptMap.recommendedNextSteps || conceptMap.recommended_next_steps, sourceSection: 'concept-map.nextActions' }, @@ -919,6 +980,7 @@ function optionsFromBody(body = {}) { ...snapshotCandidateGroup, ...conceptMapCandidateGroup, ...buildOrderSectionGroup(body.buildOrder || body.build_order, 'buildOrder'), + ...buildOrderSectionGroup(envelope.buildOrder || envelope.build_order, 'ranker-input.buildOrder'), ...buildOrderSectionGroup(featureSet.buildOrder || featureSet.build_order, 'feature-set.buildOrder'), ...buildOrderSectionGroup(snapshot.buildOrder || snapshot.build_order, 'snapshot.buildOrder'), ...buildOrderSectionGroup(conceptMap.buildOrder || conceptMap.build_order, 'concept-map.buildOrder'), @@ -943,8 +1005,8 @@ function optionsFromBody(body = {}) { if (Array.isArray(body.options)) { return normalizeOptionIds(body.options.slice(0, 24).map((item, index) => normalizeFeatureOption(item, index, 'option', 'options')).filter(item => item.title)); } - const fallbackText = body.optionsText || featureSet.optionsText || conceptMap.optionsText || body.idea || body.ideaText || ''; - const fallbackSourceSection = body.optionsText || featureSet.optionsText || conceptMap.optionsText + const fallbackText = body.optionsText || envelope.optionsText || featureSet.optionsText || conceptMap.optionsText || body.idea || body.ideaText || envelope.idea || envelope.ideaText || ''; + const fallbackSourceSection = body.optionsText || envelope.optionsText || featureSet.optionsText || conceptMap.optionsText ? 'optionsText' : body.idea ? 'idea' @@ -1402,9 +1464,10 @@ function createHandoffContract({ ranked, provenance, decisionContext }) { app.post('/api/rank-feedback', (req, res) => { const body = expandEmbeddedRankPayload(req.body || {}); - const idea = cleanMultiline(body?.idea || body?.opening_reflection || body?.restated_idea || '', 3000); - const context = cleanContextText(body?.context || ''); - const modeId = cleanText(body?.mode || 'progress', 40); + const envelope = bridgeEnvelopeFrom(body); + const idea = cleanMultiline(body?.idea || body?.ideaText || body?.idea_text || body?.opening_reflection || body?.restated_idea || envelope.idea || envelope.ideaText || envelope.idea_text || envelope.opening_reflection || envelope.restated_idea || '', 3000); + const context = cleanContextText(body?.context || envelope.context || ''); + const modeId = cleanText(body?.mode || envelope.mode || 'progress', 40); const mode = judgementModes[modeId] || judgementModes.progress; const provenance = cleanProvenance(body || {}); const decisionContext = cleanDecisionContext(body || {});