diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 3177754..dc50978 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -583,7 +583,33 @@ try { assert.ok(embeddedJson.input.decisionContext.nonGoals.includes('Avoid saved workspaces before one copyable result lands')); assert.deepEqual(embeddedJson.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, duplicateIds: duplicateIds.ranked.map(item => item.id), provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); + const fencedJsonResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + optionsText: `Here is the Scattermind export I copied:\n\n\`\`\`json\n${JSON.stringify({ + reference_code: 'SM-FENCE1', + working_name: 'Fenced pasted Concept Map', + ideaText: 'The user copied a fenced Scattermind JSON export plus surrounding prose.', + lenses: { + risk: 'Avoid saved workspaces and account dashboard before the first manual proof.', + channel: 'Build first: Fenced JSON handoff - detect the pasted Concept Map and defend the first move. Test manually: Copy the ranked brief into notes and ask one user what they would do next. Probably noise: Account dashboard with saved workspaces and collaboration.', + }, + })}\n\`\`\`\n\nPlease rank this.`, + mode: 'mvp', + }), + }); + assert.equal(fencedJsonResponse.status, 200); + const fencedJson = await fencedJsonResponse.json(); + assert.equal(fencedJson.input.embeddedPayloadSource, 'optionsText'); + assert.equal(fencedJson.input.provenance.artifactId, 'SM-FENCE1'); + assert.equal(fencedJson.input.optionCount, 3); + assert.equal(fencedJson.ranked[0].id, 'build-order-1'); + assert.equal(fencedJson.ranked.find(item => item.id === 'build-order-3').lane.id, 'park'); + assert.ok(fencedJson.input.decisionContext.nonGoals.includes('Avoid saved workspaces and account dashboard before the first manual proof')); + assert.deepEqual(fencedJson.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, duplicateIds: duplicateIds.ranked.map(item => item.id), provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2)); } finally { server.kill('SIGTERM'); } diff --git a/server.js b/server.js index 588dedd..bd6f14a 100644 --- a/server.js +++ b/server.js @@ -486,11 +486,45 @@ function looksLikeRankPayload(value = {}) { ); } +function extractFirstJsonObject(text = '') { + const start = text.indexOf('{'); + if (start < 0) return ''; + let depth = 0; + let inString = false; + let escaped = false; + for (let index = start; index < text.length; index += 1) { + const char = text[index]; + if (escaped) { + escaped = false; + continue; + } + if (char === '\\' && inString) { + escaped = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + if (inString) continue; + if (char === '{') depth += 1; + if (char === '}') { + depth -= 1; + if (depth === 0) return text.slice(start, index + 1); + } + } + return ''; +} + function parseEmbeddedRankPayload(value = '') { - const text = cleanMultiline(value, 12000); - if (!text.startsWith('{') || !text.endsWith('}')) return null; + const text = cleanMultiline(value, 12000) + .replace(/^```(?:json)?\s*/i, '') + .replace(/```$/i, '') + .trim(); + const jsonText = text.startsWith('{') && text.endsWith('}') ? text : extractFirstJsonObject(text); + if (!jsonText) return null; try { - const parsed = JSON.parse(text); + const parsed = JSON.parse(jsonText); return looksLikeRankPayload(parsed) ? parsed : null; } catch { return null;