From ca186f2a013b2686581ca4a12cc06e0a6e0e41b0 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 16:17:12 +0200 Subject: [PATCH] Accept stored Scattermind concept map rows --- README.md | 2 +- scripts/check-rank-feedback.mjs | 38 +++++++++++++++++++++++++++- server.js | 44 ++++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ef354f4..8e9c6fc 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. 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 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/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 534f70b..3cd57da 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -1254,7 +1254,43 @@ try { assert.equal(threadsFallback.handoff.readiness.status, 'ready'); assert.deepEqual(threadsFallback.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, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, threadsFallbackTop: threadsFallback.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 storedScattermindRowResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + referenceCode: 'SM-STORED-1', + ideaText: 'I paid for a Scattermind Concept Map and now want the actual stored row ranked without hand-copying lenses.', + context: 'Solo operator. Manual proof first. Avoid account dashboards and saved workspaces before one user acts.', + mode: 'mvp', + fullReadingJson: JSON.stringify({ + working_name: 'Stored Row Bridge', + opening_reflection: 'The paid Concept Map says the continuation engine should produce one active next move, not a generic dashboard.', + lenses: { + risk: { title: 'What Can Mislead You', content: 'Avoid account dashboards and saved workspaces before one user acts. Do not build billing or collaboration yet.' }, + channel: { title: 'Build Order', content: 'Build first: Stored-row build-order preview - rank the paid Concept Map directly from its saved fullReadingJson. Test manually: Copyable stored-row handoff - paste the decision brief into notes and ask one tired user what to do next. Defer: Result visual polish after the handoff proof. Probably noise: Account dashboard with saved workspaces, billing, and collaboration.' }, + }, + threads_to_hold: ['Manual proof remains the active slice.', 'Do not let this become a dashboard.'], + questions_to_sit_with: ['Can the user act on the first move without opening a workspace?'], + closing_note: 'Start with the stored-row build-order preview.', + reference_code: 'SM-STORED-1', + }), + }), + }); + assert.equal(storedScattermindRowResponse.status, 200); + const storedScattermindRow = await storedScattermindRowResponse.json(); + assert.equal(storedScattermindRow.input.provenance.artifactId, 'SM-STORED-1'); + assert.equal(storedScattermindRow.input.provenance.snapshotTitle, 'Stored Row Bridge'); + assert.match(storedScattermindRow.input.provenance.originalPrompt, /actual stored row ranked/); + assert.equal(storedScattermindRow.input.optionCount, 4); + assert.equal(storedScattermindRow.ranked[0].id, 'build-order-1'); + assert.equal(storedScattermindRow.ranked[0].provenance.sourceTitle, 'Build Order'); + assert.match(storedScattermindRow.ranked[0].provenance.sourceQuote, /Stored-row build-order preview/); + assert.ok(storedScattermindRow.input.decisionContext.nonGoals.includes('Avoid account dashboards and saved workspaces before one user acts')); + assert.equal(storedScattermindRow.ranked.find(item => item.id === 'build-order-4').lane.id, 'park'); + assert.equal(storedScattermindRow.handoff.readiness.status, 'ready'); + assert.deepEqual(storedScattermindRow.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, directEnvelopeSectionsTop: directEnvelopeSections.ranked[0].id, softDirectLaneAliasesTop: softDirectLaneAliases.ranked[0].id, threadsFallbackTop: threadsFallback.ranked[0].id, storedScattermindRowTop: storedScattermindRow.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 c94d44b..1f97579 100644 --- a/server.js +++ b/server.js @@ -656,6 +656,48 @@ function expandEmbeddedRankPayload(body = {}) { return original; } +function parseObjectJsonString(value = '') { + if (typeof value !== 'string' || !value.trim()) return null; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +} + +function expandStoredScattermindReading(body = {}) { + const original = objectFrom(body); + const storedReading = original.fullReadingJson + || original.full_reading_json + || original.fullReading + || original.full_reading + || original.conceptMapJson + || original.concept_map_json + || ''; + const parsedReading = typeof storedReading === 'string' + ? parseObjectJsonString(storedReading) + : objectFrom(storedReading); + if (!parsedReading || !Object.keys(parsedReading).length) return original; + + const expanded = { + ...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, + ideaText: original.ideaText || original.idea_text || parsedReading.ideaText || parsedReading.idea_text, + context: original.context || parsedReading.context || '', + }; + expanded._storedScattermindReading = true; + return expanded; +} + function cleanProvenance(input = {}) { const envelope = bridgeEnvelopeFrom(input); const featureSet = featureSetFrom(input); @@ -1595,7 +1637,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) { } app.post('/api/rank-feedback', (req, res) => { - const body = expandEmbeddedRankPayload(req.body || {}); + const body = expandStoredScattermindReading(expandEmbeddedRankPayload(req.body || {})); 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 || '');