Accept stored Scattermind concept map 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. 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.
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
Reference in New Issue
Block a user