Accept stored Scattermind snapshot rows
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 || '',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user