Support closing-note rank fallback

This commit is contained in:
OpenClaw Bot
2026-05-27 19:38:53 +02:00
parent 90beb50459
commit 13622de5a0
3 changed files with 75 additions and 1 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ Candidate items may include optional 110 `rankerHints` (`value`, `effort`, `c
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. 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`, with either colon or reader-friendly dash separators (`Continue first: …`, `Continue first — …`, `Evidence next - …`). 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. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, and evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`) so Scattermind can pass a paid Concept Map preview without renaming it into software-feature language. 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`, with either colon or reader-friendly dash separators (`Continue first: …`, `Continue first — …`, `Evidence next - …`). 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. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, and evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`). If a paid Concept Map has no labelled Build Order/action threads but does include `closing_note` / `closingNote` plus decision questions, Ranker treats the closing note as the active 48-hour Do first move and keeps the questions in Validate next. This lets Scattermind pass reader-friendly Concept Map copy without renaming everything into software-feature language.
Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; soft guardrail language such as “this is not a dashboard” or “keep auth/billing/workspaces out until proof” is promoted into non-goals, not merely background context. Ranker now also infers a light `ideaRoute` from the Scattermind source text and carries it in `input.decisionContext` / `handoff.decisionContext`; for game concepts it automatically adds anti-SaaS non-goals so account/dashboard/workspace/subscription candidates cannot win a playable-prototype build order just because they were phrased loudly. The result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. Handoff `activeSlice` (`ranker-active-slice-v1`) is the compact machine-readable continuation unit: one active item, its proof/evidence/success/kill signals, source anchor, held-back items, readiness status, and the rule that only this slice is build-ready. For tired first-screen users, `brief.decisionReceipt` repeats the one active move, first proof step, evidence question, held-back items, source anchor, and the handoff rule that only Do first is active; use it as the compact result strip before showing the full lane board. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen. Exact free Snapshot JSON (`working_name`, `restated_idea`, `lenses.shape`, `questions_to_sit_with`, `reference_code`) is rankable too: Ranker derives a manual proof active slice plus evidence questions, carrying the Snapshot reference code/title into provenance so a Snapshot-only handoff does not need a paid Concept Map before it can produce a useful build order. If a Concept Map only carries `questions_to_sit_with` / `questionsToSitWith` / `openQuestions` and no explicit build-order lanes or action threads, Ranker converts those questions into Validate-next evidence actions with source trace instead of pretending they are software features. Lane safety note: explicit Scattermind `defer` / `park` hints are hard rails, not mild suggestions. Source `nonGoals` / `avoid` guardrails are also hard enough to keep conflicting candidates out of Do first / Validate next even when their local scoring hints look attractive; soft guardrail language such as “this is not a dashboard” or “keep auth/billing/workspaces out until proof” is promoted into non-goals, not merely background context. Ranker now also infers a light `ideaRoute` from the Scattermind source text and carries it in `input.decisionContext` / `handoff.decisionContext`; for game concepts it automatically adds anti-SaaS non-goals so account/dashboard/workspace/subscription candidates cannot win a playable-prototype build order just because they were phrased loudly. The result will mark the lane source as `source-non-goal` so the handoff can explain that the candidate needs guardrail resolution before active work. Handoff `source.requiresSourceTrace` is true only when a real source artifact/title is present; plain idea-only ranking still warns about a missing artifact ID when it carries prompt provenance, but it does not spam source-section/evidence warnings meant for Scattermind artifacts. Handoff `readiness` now gives downstream bridge consumers a deterministic gate: `ready`, `usable-with-warnings`, `needs-source-context`, or `blocked`, with blockers and next checks for missing evidence, source trace, duplicate IDs, or active source-non-goal conflicts. Handoff `activeSlice` (`ranker-active-slice-v1`) is the compact machine-readable continuation unit: one active item, its proof/evidence/success/kill signals, source anchor, held-back items, readiness status, and the rule that only this slice is build-ready. For tired first-screen users, `brief.decisionReceipt` repeats the one active move, first proof step, evidence question, held-back items, source anchor, and the handoff rule that only Do first is active; use it as the compact result strip before showing the full lane board. For low-friction handoff, `/api/rank-feedback` also detects a raw Scattermind/Concept Map JSON object pasted into `idea`, `ideaText`, `optionsText`, or wrapper keys such as `payload`; it expands that object before ranking and reports `input.embeddedPayloadSource` so the public form can accept copy/paste exports without a custom import screen. Exact free Snapshot JSON (`working_name`, `restated_idea`, `lenses.shape`, `questions_to_sit_with`, `reference_code`) is rankable too: Ranker derives a manual proof active slice plus evidence questions, carrying the Snapshot reference code/title into provenance so a Snapshot-only handoff does not need a paid Concept Map before it can produce a useful build order. If a Concept Map only carries `questions_to_sit_with` / `questionsToSitWith` / `openQuestions` and no explicit build-order lanes or action threads, Ranker converts those questions into Validate-next evidence actions with source trace instead of pretending they are software features.
+31
View File
@@ -270,6 +270,37 @@ try {
assert.equal(threadGuardrail.buildOrder.doFirst[0], 'build-order-2', 'thread-level guardrail should demote a labelled Build first dashboard and promote the first eligible manual move'); assert.equal(threadGuardrail.buildOrder.doFirst[0], 'build-order-2', 'thread-level guardrail should demote a labelled Build first dashboard and promote the first eligible manual move');
assert.match(threadGuardrail.brief.decisionReceipt.sourceAnchor, /concept-map\.lenses\.channel/); assert.match(threadGuardrail.brief.decisionReceipt.sourceAnchor, /concept-map\.lenses\.channel/);
const closingNoteFallbackResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceName: 'Scattermind',
artifactId: 'concept_map_closing_note_only',
originalPrompt: 'Clarify a rough service idea where the paid output has questions but no labelled Build Order text.',
conceptMap: {
working_name: 'Service proof without labels',
opening_reflection: 'The Concept Map says to prove a manual package teardown before building a product surface.',
lenses: {
shape: { title: 'Recommended Direction', content: 'Start as a manual service proof before adding software.' },
question: { title: 'Proof Steps', content: 'Ask two freelancers to send one messy offer page and record whether the teardown helps.' },
},
questions_to_sit_with: [
'Will one freelancer act on a manual package teardown within 48 hours?',
'What would make the teardown feel worth paying for?',
],
closing_note: 'Spend the next 48 hours running one manual package teardown for a real freelancer before designing any dashboard.',
reference_code: 'SM-CLOSE',
},
}),
});
assert.equal(closingNoteFallbackResponse.status, 200);
const closingNoteFallback = await closingNoteFallbackResponse.json();
assert.equal(closingNoteFallback.buildOrder.doFirst[0], 'closing-note-next-move');
assert.match(closingNoteFallback.brief.decisionReceipt.firstProofStep, /running one manual package teardown/i);
assert.match(closingNoteFallback.brief.decisionReceipt.sourceAnchor, /concept-map\.closingNote/);
assert.equal(closingNoteFallback.buildOrderDetails.validateNext[0].sourceSection, 'concept-map.questionsToSitWith');
assert.equal(closingNoteFallback.handoff.readiness.status, 'ready');
const softDashLabelResponse = await fetch(`${base}/api/rank-feedback`, { const softDashLabelResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
+43
View File
@@ -1236,6 +1236,31 @@ function optionsFromQuestionsToSitWith(items = [], sourceSection = 'concept-map.
}).filter(item => item.action && item.evidence); }).filter(item => item.action && item.evidence);
} }
function optionFromClosingNote(text = '', sourceSection = 'concept-map.closingNote', sourceTitle = '48-hour next move') {
const note = cleanMultiline(text, 520);
if (!note) return null;
return {
id: 'closing-note-next-move',
action: titleFromBuildOrderFragment(note),
why: 'Scattermind named this as the single best next move in the Concept Map closing note.',
evidence: /\b(evidence|signal|proof|test|validate|ask|observe|show|playtest|prototype)\b/i.test(note)
? note
: 'What smallest real-world signal would prove this 48-hour move deserves the active build slot?',
nextStep: note,
suggestedLane: 'do-first',
rankerHints: { value: 8, effort: 2, confidence: 7, urgency: 8, risk: 2 },
sourceSection,
sourceItemId: `${sourceSection}#1`,
sourceTitle,
sourceExcerpt: note,
};
}
function closingNoteFromSource(source = {}) {
const obj = objectFrom(source);
return cleanMultiline(obj.closing_note || obj.closingNote || obj.next48Hours || obj.next_48_hours || obj.startHere || obj.start_here || '', 520);
}
function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') { function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') {
const reading = objectFrom(source); const reading = objectFrom(source);
const lenses = objectFrom(reading.lenses); const lenses = objectFrom(reading.lenses);
@@ -1422,6 +1447,13 @@ function optionsFromBody(body = {}) {
'Thread to hold' 'Thread to hold'
); );
if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]); if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]);
const closingNoteSource = [
{ text: closingNoteFromSource(conceptMap), sourceSection: 'concept-map.closingNote' },
{ text: closingNoteFromSource(snapshot), sourceSection: 'snapshot.closingNote' },
{ text: closingNoteFromSource(envelope), sourceSection: 'ranker-input.closingNote' },
{ text: closingNoteFromSource(featureSet), sourceSection: 'feature-set.closingNote' },
{ text: closingNoteFromSource(body), sourceSection: 'closingNote' },
].find(entry => entry.text) || { text: '', sourceSection: '' };
const questionSource = firstArraySource([ const questionSource = firstArraySource([
{ items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' }, { items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' },
{ items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' }, { items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' },
@@ -1434,6 +1466,17 @@ function optionsFromBody(body = {}) {
questionSource.sourceSection || 'questionsToSitWith', questionSource.sourceSection || 'questionsToSitWith',
'Question to sit with' 'Question to sit with'
); );
const closingNoteOption = optionFromClosingNote(
closingNoteSource.text,
closingNoteSource.sourceSection || 'closingNote',
'Concept Map closing note'
);
if (closingNoteOption && questionOptions.length) {
return normalizeCandidateGroup([
{ items: [closingNoteOption], sourceSection: closingNoteSource.sourceSection || 'closingNote', defaultLane: 'do-first' },
{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' },
]);
}
if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]); if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]);
const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot'); const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot');
const snapshotReadingOptions = nestedSnapshotReadingOptions.length const snapshotReadingOptions = nestedSnapshotReadingOptions.length