Accept stored Scattermind concept map rows

This commit is contained in:
OpenClaw Bot
2026-05-27 16:17:12 +02:00
parent 421913dc2c
commit ca186f2a01
3 changed files with 81 additions and 3 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ Ranker's continuation job is narrow:
Candidate items may include optional 110 `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 items may include optional 110 `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. 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.
+37 -1
View File
@@ -1254,7 +1254,43 @@ try {
assert.equal(threadsFallback.handoff.readiness.status, 'ready'); assert.equal(threadsFallback.handoff.readiness.status, 'ready');
assert.deepEqual(threadsFallback.handoff.warnings, []); 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 { } finally {
server.kill('SIGTERM'); server.kill('SIGTERM');
} }
+43 -1
View File
@@ -656,6 +656,48 @@ function expandEmbeddedRankPayload(body = {}) {
return original; 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 = {}) { function cleanProvenance(input = {}) {
const envelope = bridgeEnvelopeFrom(input); const envelope = bridgeEnvelopeFrom(input);
const featureSet = featureSetFrom(input); const featureSet = featureSetFrom(input);
@@ -1595,7 +1637,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
} }
app.post('/api/rank-feedback', (req, res) => { app.post('/api/rank-feedback', (req, res) => {
const body = expandEmbeddedRankPayload(req.body || {}); const body = expandStoredScattermindReading(expandEmbeddedRankPayload(req.body || {}));
const envelope = bridgeEnvelopeFrom(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 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 || ''); const context = cleanContextText(body?.context || envelope.context || '');