Harden Scattermind stored row paste handoff
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 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. 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.
|
||||
|
||||
|
||||
+3
-1
@@ -64,6 +64,7 @@ function parsePastedJsonPayload(value) {
|
||||
parsed.schema || parsed.featureSet || parsed.feature_set || parsed.candidateSet || parsed.candidate_set || parsed.candidateFeatureSet || parsed.candidate_feature_set || parsed.rankReadyFeatureSet || parsed.rank_ready_feature_set
|
||||
|| 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.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)
|
||||
@@ -117,7 +118,8 @@ function payloadFromForm(formPayload) {
|
||||
const optionsJson = parsePastedJsonPayload(formPayload.optionsText);
|
||||
const embedded = ideaJson || optionsJson;
|
||||
if (!embedded) return formPayload;
|
||||
const unwrapped = embedded.payload || embedded.rankPayload || embedded.scattermindPayload || embedded.conceptMapJson || embedded;
|
||||
const unwrapCandidate = embedded.payload || embedded.rankPayload || embedded.scattermindPayload || embedded;
|
||||
const unwrapped = unwrapCandidate && typeof unwrapCandidate === 'object' && !Array.isArray(unwrapCandidate) ? unwrapCandidate : embedded;
|
||||
const merged = { ...unwrapped };
|
||||
if (!merged.mode && formPayload.mode) merged.mode = formPayload.mode;
|
||||
if (String(formPayload.context || '').trim() && !merged.context) merged.context = formPayload.context;
|
||||
|
||||
@@ -255,6 +255,41 @@ try {
|
||||
assert.equal(nestedConcept.ranked.find(item => item.id === 'lesson-library').lane.id, 'park');
|
||||
assert.deepEqual(nestedConcept.handoff.warnings, []);
|
||||
|
||||
const storedRowPayload = {
|
||||
referenceCode: 'CM-STORED-77',
|
||||
working_name: 'Saved paid Concept Map row',
|
||||
ideaText: 'I sell practical maker workshops and need the first continuation move.',
|
||||
context: 'Solo operator. Manual proof first. Avoid account dashboards before buyer evidence.',
|
||||
fullReadingJson: JSON.stringify({
|
||||
opening_reflection: 'A maker wants the next build step, not another planning surface.',
|
||||
lenses: {
|
||||
channel: 'Continue first: Concierge build-order preview from one workshop idea. Evidence next: Ask 3 makers whether the preview changes what they would build this week. Hold for later: Polished saved workspace after proof. Set aside: Account dashboard and team voting before evidence.',
|
||||
},
|
||||
threads_to_hold: ['Start by manually turning one workshop concept into a build order preview', 'Validate with three makers before adding persistence'],
|
||||
}),
|
||||
};
|
||||
const storedRowPasteResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
idea: `Saved Scattermind row pasted from Appwrite:\n\n\`\`\`json\n${JSON.stringify(storedRowPayload)}\n\`\`\``,
|
||||
mode: 'mvp',
|
||||
}),
|
||||
});
|
||||
assert.equal(storedRowPasteResponse.status, 200);
|
||||
const storedRowPaste = await storedRowPasteResponse.json();
|
||||
assert.equal(storedRowPaste.input.embeddedPayloadSource, 'idea');
|
||||
assert.equal(storedRowPaste.input.provenance.artifactId, 'CM-STORED-77');
|
||||
assert.equal(storedRowPaste.input.provenance.snapshotTitle, 'Saved paid Concept Map row');
|
||||
assert.match(storedRowPaste.input.provenance.originalPrompt, /maker workshops/);
|
||||
assert.equal(storedRowPaste.ranked[0].id, storedRowPaste.buildOrder.doFirst[0]);
|
||||
assert.match(storedRowPaste.brief.quickGlance.topPick, /Concierge build-order preview/i);
|
||||
assert.match(storedRowPaste.brief.quickGlance.sourceTrace.sourceSection, /concept-map\.lenses\.channel/);
|
||||
assert.equal(storedRowPaste.handoff.source.hasOriginalPrompt, true);
|
||||
assert.equal(storedRowPaste.handoff.readiness.status, 'ready');
|
||||
assert.match(storedRowPaste.handoff.copyableText, /CM-STORED-77/);
|
||||
assert.equal(storedRowPaste.ranked.find(item => /Account dashboard/i.test(item.title)).lane.id, 'park');
|
||||
|
||||
const snapshotOnlyResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -559,6 +559,12 @@ function looksLikeRankPayload(value = {}) {
|
||||
|| value.original_prompt
|
||||
|| value.sourceSummary
|
||||
|| value.source_summary
|
||||
|| value.fullReadingJson
|
||||
|| value.full_reading_json
|
||||
|| value.fullReading
|
||||
|| value.full_reading
|
||||
|| value.conceptMapJson
|
||||
|| value.concept_map_json
|
||||
|| value.opening_reflection
|
||||
|| value.restated_idea
|
||||
|| value.ideaText
|
||||
@@ -651,7 +657,7 @@ function parseEmbeddedRankPayload(value = '') {
|
||||
|
||||
function expandEmbeddedRankPayload(body = {}) {
|
||||
const original = objectFrom(body);
|
||||
for (const key of ['payload', 'rankPayload', 'scattermindPayload', 'conceptMapJson', 'idea', 'ideaText', 'optionsText']) {
|
||||
for (const key of ['payload', 'rankPayload', 'scattermindPayload', 'conceptMapJson', 'concept_map_json', 'fullReadingJson', 'full_reading_json', 'fullReading', 'full_reading', 'idea', 'ideaText', 'optionsText']) {
|
||||
const embedded = typeof original[key] === 'string'
|
||||
? parseEmbeddedRankPayload(original[key])
|
||||
: looksLikeRankPayload(original[key])
|
||||
|
||||
Reference in New Issue
Block a user