diff --git a/README.md b/README.md index c301f75..2e32ca1 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Ranker's continuation job is narrow: 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. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `snapshot.context` or `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper artifact guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string or schema-light structured context summaries (`context.summary`, `snapshot.context.summary`, `conceptMap.context.summary`, etc.), Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, `Non-goal: ...`, `Not yet ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first. -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. Ranker also accepts the current Scattermind storage-row shape with `referenceCode`, `ideaText`, `context`, and string-valued `fullReadingJson` / `full_reading_json`; it expands the saved paid Concept Map before ranking so operators do not have to hand-copy lenses out of Appwrite rows. The public paste form mirrors this: a prose-wrapped/fenced Appwrite row paste stays intact for the API instead of unwrapping the stringified reading into nonsense fields. 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. +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. Ranker also accepts the current Scattermind storage-row shape with `referenceCode`, `ideaText`, `context`, and string-valued `fullReadingJson` / `full_reading_json`; it expands the saved paid Concept Map before ranking so operators do not have to hand-copy lenses out of Appwrite rows. If the row only has free Snapshot data (`glimpseJson` / `glimpse_json` / `snapshotJson`), Ranker expands that Snapshot into a minimal continuation order: one manual proof plus the first evidence question, with the Snapshot reference code/title preserved for provenance. The public paste form mirrors this: a prose-wrapped/fenced Appwrite row paste stays intact for the API instead of unwrapping the stringified reading into nonsense fields. 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 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. diff --git a/public/app.js b/public/app.js index 961ee31..ee41a60 100644 --- a/public/app.js +++ b/public/app.js @@ -65,6 +65,7 @@ function parsePastedJsonPayload(value) { || parsed.snapshot || parsed.conceptMap || parsed.concept_map || parsed.buildOrder || parsed.build_order || parsed.lenses || parsed.payload || parsed.rankPayload || parsed.scattermindPayload || parsed.conceptMapJson || parsed.rankerInput || parsed.ranker_input || parsed.rankerHandoff || parsed.ranker_handoff || parsed.rankReady || parsed.rank_ready || parsed.bridge || parsed.bridgePayload || parsed.bridge_payload || parsed.concept_map_json || parsed.fullReadingJson || parsed.full_reading_json || parsed.fullReading || parsed.full_reading + || parsed.glimpseJson || parsed.glimpse_json || parsed.snapshotJson || parsed.snapshot_json || parsed.reference_code || parsed.referenceCode || parsed.artifactId || parsed.sourceArtifactId || parsed.source_artifact_id || 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) diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 973188b..abee496 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -360,6 +360,41 @@ try { assert.equal(snapshotOnly.handoff.source.requiresSourceTrace, true); assert.deepEqual(snapshotOnly.handoff.warnings, []); + const storedSnapshotRowResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + referenceCode: 'SM-SNAPSHOT-ROW', + ideaText: 'I have a tiny paid workshop idea and only the free Snapshot exists so far.', + context: 'Solo operator. Manual proof first. Avoid saved workspaces before demand is visible.', + status: 'glimpse_ready', + glimpseJson: JSON.stringify({ + working_name: 'Workshop Seed', + restated_idea: 'A tiny workshop offer needs one concrete promise before becoming a platform.', + lenses: { + shape: { + title: 'Product Shape', + content: 'Start as a manually sold workshop promise, not a course dashboard. The useful first shape is a short offer and one live test. Avoid saved workspaces until people ask for repeats.', + }, + }, + questions_to_sit_with: ['Will three real prospects understand the workshop promise without extra explanation?'], + reference_code: 'SM-SNAPSHOT-ROW', + }), + mode: 'mvp', + }), + }); + assert.equal(storedSnapshotRowResponse.status, 200); + const storedSnapshotRow = await storedSnapshotRowResponse.json(); + assert.equal(storedSnapshotRow.input.provenance.artifactId, 'SM-SNAPSHOT-ROW'); + assert.equal(storedSnapshotRow.input.provenance.snapshotTitle, 'Workshop Seed'); + assert.equal(storedSnapshotRow.input.optionCount, 2, 'stored Snapshot rows should become a manual proof plus evidence question'); + assert.equal(storedSnapshotRow.ranked[0].id, 'snapshot-manual-proof'); + assert.equal(storedSnapshotRow.ranked[0].provenance.sourceSection, 'snapshot.lenses.shape'); + assert.equal(storedSnapshotRow.ranked.find(item => item.id === 'question-1').lane.id, 'test'); + assert.match(storedSnapshotRow.handoff.copyableText, /Workshop Seed/); + assert.equal(storedSnapshotRow.handoff.readiness.status, 'ready'); + assert.deepEqual(storedSnapshotRow.handoff.warnings, []); + const sectionedConceptResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 44bd94b..320e315 100644 --- a/server.js +++ b/server.js @@ -559,6 +559,10 @@ function looksLikeRankPayload(value = {}) { || value.original_prompt || value.sourceSummary || value.source_summary + || value.glimpseJson + || value.glimpse_json + || value.snapshotJson + || value.snapshot_json || value.fullReadingJson || value.full_reading_json || value.fullReading @@ -693,6 +697,14 @@ function parseObjectJsonString(value = '') { function expandStoredScattermindReading(body = {}) { const original = objectFrom(body); + const storedSnapshot = original.glimpseJson + || original.glimpse_json + || original.snapshotJson + || original.snapshot_json + || ''; + const parsedSnapshot = (typeof storedSnapshot === 'string' + ? parseObjectJsonString(storedSnapshot) + : objectFrom(storedSnapshot)) || {}; const storedReading = original.fullReadingJson || original.full_reading_json || original.fullReading @@ -700,22 +712,25 @@ function expandStoredScattermindReading(body = {}) { || original.conceptMapJson || original.concept_map_json || ''; - const parsedReading = typeof storedReading === 'string' + const parsedReading = (typeof storedReading === 'string' ? parseObjectJsonString(storedReading) - : objectFrom(storedReading); - if (!parsedReading || !Object.keys(parsedReading).length) return original; + : objectFrom(storedReading)) || {}; + if ((!parsedReading || !Object.keys(parsedReading).length) && (!parsedSnapshot || !Object.keys(parsedSnapshot).length)) return original; + const explicitSnapshot = objectFrom(original.snapshot || original.glimpse); const expanded = { + ...parsedSnapshot, ...parsedReading, ...original, - lenses: original.lenses || parsedReading.lenses, - threads_to_hold: original.threads_to_hold || original.threadsToHold || parsedReading.threads_to_hold || parsedReading.threadsToHold, - questions_to_sit_with: original.questions_to_sit_with || original.questionsToSitWith || parsedReading.questions_to_sit_with || parsedReading.questionsToSitWith, - closing_note: original.closing_note || original.closingNote || parsedReading.closing_note || parsedReading.closingNote, - reference_code: original.reference_code || original.referenceCode || parsedReading.reference_code || parsedReading.referenceCode, - working_name: original.working_name || original.workingName || parsedReading.working_name || parsedReading.workingName, - opening_reflection: original.opening_reflection || original.openingReflection || parsedReading.opening_reflection || parsedReading.openingReflection, - restated_idea: original.restated_idea || original.restatedIdea || parsedReading.restated_idea || parsedReading.restatedIdea, + snapshot: Object.keys(explicitSnapshot).length ? explicitSnapshot : parsedSnapshot, + lenses: original.lenses || parsedReading.lenses || parsedSnapshot.lenses, + threads_to_hold: original.threads_to_hold || original.threadsToHold || parsedReading.threads_to_hold || parsedReading.threadsToHold || parsedSnapshot.threads_to_hold || parsedSnapshot.threadsToHold, + questions_to_sit_with: original.questions_to_sit_with || original.questionsToSitWith || parsedReading.questions_to_sit_with || parsedReading.questionsToSitWith || parsedSnapshot.questions_to_sit_with || parsedSnapshot.questionsToSitWith, + closing_note: original.closing_note || original.closingNote || parsedReading.closing_note || parsedReading.closingNote || parsedSnapshot.closing_note || parsedSnapshot.closingNote, + reference_code: original.reference_code || original.referenceCode || parsedReading.reference_code || parsedReading.referenceCode || parsedSnapshot.reference_code || parsedSnapshot.referenceCode, + working_name: original.working_name || original.workingName || parsedReading.working_name || parsedReading.workingName || parsedSnapshot.working_name || parsedSnapshot.workingName, + opening_reflection: original.opening_reflection || original.openingReflection || parsedReading.opening_reflection || parsedReading.openingReflection || parsedSnapshot.opening_reflection || parsedSnapshot.openingReflection, + restated_idea: original.restated_idea || original.restatedIdea || parsedReading.restated_idea || parsedReading.restatedIdea || parsedSnapshot.restated_idea || parsedSnapshot.restatedIdea, ideaText: original.ideaText || original.idea_text || parsedReading.ideaText || parsedReading.idea_text, context: original.context || parsedReading.context || '', };