Accept soft Scattermind action fields

This commit is contained in:
OpenClaw Bot
2026-05-27 20:02:51 +02:00
parent 9c35af237f
commit 46846cada7
3 changed files with 64 additions and 12 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. It also accepts private reading/API envelopes with object-valued `reading` / `fullReading` and `glimpse`, preserving `referenceCode` and `initialPrompt` while treating `reading` as the paid Concept Map. Stringified bridge envelopes in fields such as `rankerInput`, `rankerBridge`, `rankReady`, `bridgePayload`, or `continuationPlan` are expanded the same way, so Appwrite/string-copy handoffs do not have to be manually unwrapped before ranking. 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 into the bridge handoff. 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. It also accepts private reading/API envelopes with object-valued `reading` / `fullReading` and `glimpse`, preserving `referenceCode` and `initialPrompt` while treating `reading` as the paid Concept Map. Stringified bridge envelopes in fields such as `rankerInput`, `rankerBridge`, `rankReady`, `bridgePayload`, or `continuationPlan` are expanded the same way, so Appwrite/string-copy handoffs do not have to be manually unwrapped before ranking. 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 into the bridge handoff.
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. 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`). Direct candidate objects may use reader-friendly prose keys like `text`, `content`, `summary`, `why_it_matters`, `evidence_to_collect`, `first_proof_step`, `green_flag`, and `red_flag`; Ranker normalizes those into title, value, evidence, next-step, success, and kill-signal fields so Scattermind does not have to rename paid Concept Map language into software-feature jargon. 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.
+42
View File
@@ -238,6 +238,48 @@ try {
assert.equal(snakeDecisionContext.buildOrder.doFirst[0], 'copyable-slice'); assert.equal(snakeDecisionContext.buildOrder.doFirst[0], 'copyable-slice');
assert.equal(snakeDecisionContext.handoff.readiness.status, 'ready'); assert.equal(snakeDecisionContext.handoff.readiness.status, 'ready');
const softActionObjectResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rankerBridge: {
sourceName: 'Scattermind',
artifactId: 'concept_map_soft_action_objects',
snapshotTitle: 'Soft action object handoff',
originalPrompt: 'Clarify a practical continuation without forcing software-feature field names.',
context: 'Solo builder. Avoid account dashboards before manual proof.',
nextActions: [
{
id: 'manual-proof-text-item',
text: 'Run one manual paid-style teardown from the Concept Map',
why_it_matters: 'This proves the continuation value before any app surface.',
evidence_to_collect: 'Will one tired builder act from the teardown without another explanation?',
first_proof_step: 'Send the teardown to one real builder and ask what they would do first.',
green_flag: 'They can name the next move and why it wins.',
red_flag: 'They ask for a dashboard instead of acting on the decision.',
},
{
id: 'saved-account-dashboard',
content: 'Saved account dashboard for every Concept Map',
evidence_to_collect: 'Would users want this later?',
ranker_hints: { value: 10, effort: 1, confidence: 9, urgency: 9, risk: 1 },
},
],
},
}),
});
assert.equal(softActionObjectResponse.status, 200);
const softActionObject = await softActionObjectResponse.json();
assert.equal(softActionObject.input.provenance.artifactId, 'concept_map_soft_action_objects');
assert.equal(softActionObject.buildOrder.doFirst[0], 'manual-proof-text-item');
assert.equal(softActionObject.ranked.find(item => item.id === 'manual-proof-text-item').title, 'Run one manual paid-style teardown from the Concept Map');
assert.match(softActionObject.ranked.find(item => item.id === 'manual-proof-text-item').factors.evidenceNeeded, /tired builder/);
assert.match(softActionObject.handoff.activeSlice.proof.nextStep, /Send the teardown/);
assert.match(softActionObject.handoff.activeSlice.proof.successSignal, /next move/);
assert.match(softActionObject.handoff.activeSlice.proof.killSignal, /dashboard/);
assert.equal(softActionObject.ranked.find(item => item.id === 'saved-account-dashboard').lane.source, 'source-non-goal');
assert.equal(softActionObject.handoff.readiness.status, 'ready');
const threadGuardrailResponse = await fetch(`${base}/api/rank-feedback`, { const threadGuardrailResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
+21 -11
View File
@@ -1046,15 +1046,15 @@ function nonGoalConflicts(optionText, decisionContext = {}) {
function normalizeFeatureOption(item, index, fallbackId = 'feature', defaultSourceSection = '', defaultRecommendedLane = '') { function normalizeFeatureOption(item, index, fallbackId = 'feature', defaultSourceSection = '', defaultRecommendedLane = '') {
const rawValue = typeof item === 'string' || typeof item === 'number' ? String(item) : ''; const rawValue = typeof item === 'string' || typeof item === 'number' ? String(item) : '';
const raw = rawValue ? { action: rawValue } : objectFrom(item); const raw = rawValue ? { action: rawValue } : objectFrom(item);
const title = cleanText(raw.title || raw.name || raw.action || raw.move || raw.nextMove || raw.next_move || raw.nextStep || raw.next_step || raw.recommendedNextStep || raw.recommended_next_step || raw.experiment || raw.testName || raw.test_name || raw.hypothesis || raw.label || '', 140); const title = cleanText(raw.title || raw.name || raw.action || raw.move || raw.nextMove || raw.next_move || raw.nextStep || raw.next_step || raw.recommendedNextStep || raw.recommended_next_step || raw.experiment || raw.testName || raw.test_name || raw.hypothesis || raw.label || raw.text || raw.content || raw.summary || '', 140);
const proofSteps = cleanTextList(raw.proofSteps || raw.proof_steps || raw.proof || raw.validationSteps || raw.validation_steps || raw.steps || raw.method, 5, 180); const proofSteps = cleanTextList(raw.proofSteps || raw.proof_steps || raw.proof || raw.firstProof || raw.first_proof || raw.validationSteps || raw.validation_steps || raw.steps || raw.method, 5, 180);
const dependencies = cleanTextList(raw.dependencies || raw.blockedBy, 5, 120); const dependencies = cleanTextList(raw.dependencies || raw.blockedBy || raw.blocked_by, 5, 120);
const evidenceNeeded = cleanText(raw.evidenceNeeded || raw.evidence_needed || raw.evidence || raw.test || raw.evidenceQuestion || raw.evidence_question || raw.questionToAnswer || raw.question_to_answer || raw.question || raw.learningGoal || raw.learning_goal || '', 260); const evidenceNeeded = cleanText(raw.evidenceNeeded || raw.evidence_needed || raw.evidenceToCollect || raw.evidence_to_collect || raw.evidence || raw.test || raw.proofQuestion || raw.proof_question || raw.evidenceQuestion || raw.evidence_question || raw.questionToAnswer || raw.question_to_answer || raw.question || raw.learningGoal || raw.learning_goal || '', 260);
const userValue = cleanText(raw.userValue || raw.user_value || raw.value || raw.outcome || raw.why, 260); const userValue = cleanText(raw.userValue || raw.user_value || raw.value || raw.outcome || raw.why || raw.whyItMatters || raw.why_it_matters || raw.whyNow || raw.why_now, 260);
const risk = cleanText(raw.risk || raw.assumption || raw.unknown || '', 220); const risk = cleanText(raw.risk || raw.assumption || raw.unknown || raw.watchOut || raw.watch_out || '', 220);
const nextStep = cleanText(raw.nextStep || raw.next_step || raw.nextAction || raw.next_action || raw.firstStep || raw.first_step || raw.manualStep || raw.manual_step || raw.actionToTake || raw.action_to_take || '', 260); const nextStep = cleanText(raw.nextStep || raw.next_step || raw.nextAction || raw.next_action || raw.firstStep || raw.first_step || raw.manualStep || raw.manual_step || raw.actionToTake || raw.action_to_take || raw.firstProofStep || raw.first_proof_step || '', 260);
const successSignal = cleanText(raw.successSignal || raw.success_signal || raw.successCriteria || raw.success_criteria || raw.successMetric || raw.success_metric || raw.greenLight || raw.green_light || raw.signalToSee || raw.signal_to_see || '', 260); const successSignal = cleanText(raw.successSignal || raw.success_signal || raw.successCriteria || raw.success_criteria || raw.successMetric || raw.success_metric || raw.greenLight || raw.green_light || raw.greenFlag || raw.green_flag || raw.signalToSee || raw.signal_to_see || '', 260);
const killSignal = cleanText(raw.killSignal || raw.kill_signal || raw.stopSignal || raw.stop_signal || raw.redFlag || raw.red_flag || raw.failureSignal || raw.failure_signal || raw.cutIf || raw.cut_if || '', 260); const killSignal = cleanText(raw.killSignal || raw.kill_signal || raw.stopSignal || raw.stop_signal || raw.redFlag || raw.red_flag || raw.failureSignal || raw.failure_signal || raw.cutIf || raw.cut_if || raw.stopIf || raw.stop_if || '', 260);
const rawLane = cleanText(raw.lane || '', 40); const rawLane = cleanText(raw.lane || '', 40);
const laneLooksLikeHint = Boolean(normalizeLaneHint(rawLane)); const laneLooksLikeHint = Boolean(normalizeLaneHint(rawLane));
const sourceSection = cleanText(raw.sourceSection || raw.source_section || raw.section || raw.origin || (!laneLooksLikeHint ? rawLane : '') || defaultSourceSection, 80); const sourceSection = cleanText(raw.sourceSection || raw.source_section || raw.section || raw.origin || (!laneLooksLikeHint ? rawLane : '') || defaultSourceSection, 80);
@@ -1074,11 +1074,21 @@ function normalizeFeatureOption(item, index, fallbackId = 'feature', defaultSour
proofSteps.length && `Proof steps: ${proofSteps.join('; ')}`, proofSteps.length && `Proof steps: ${proofSteps.join('; ')}`,
dependencies.length && `Dependencies: ${dependencies.join(', ')}`, dependencies.length && `Dependencies: ${dependencies.join(', ')}`,
].filter(Boolean); ].filter(Boolean);
const conflictText = cleanText([
title,
raw.description || raw.brief || (raw.hypothesis && raw.hypothesis !== title ? raw.hypothesis : ''),
userValue,
evidenceNeeded,
risk,
nextStep,
proofSteps.join(' '),
dependencies.join(' '),
].filter(Boolean).join(' '), 1200);
return { return {
id: cleanText(raw.id || raw.key || `${fallbackId}-${index + 1}`, 80) || `${fallbackId}-${index + 1}`, id: cleanText(raw.id || raw.key || `${fallbackId}-${index + 1}`, 80) || `${fallbackId}-${index + 1}`,
title, title,
description: cleanText(descriptionParts.join(' '), 760), description: cleanText(descriptionParts.join(' '), 760),
factors: { userValue, evidenceNeeded, risk, nextStep, successSignal, killSignal, proofSteps, dependencies, recommendedLane, metricHints: cleanMetricHints(raw) }, factors: { userValue, evidenceNeeded, risk, nextStep, successSignal, killSignal, proofSteps, dependencies, recommendedLane, metricHints: cleanMetricHints(raw), conflictText },
provenance: { provenance: {
sourceId, sourceId,
sourceSection, sourceSection,
@@ -1558,7 +1568,7 @@ function scoreOption(option, mode, context = '', decisionContext = {}) {
const dependencyPenalty = Math.min(2.2, (factors.dependencies || []).length * 0.45); const dependencyPenalty = Math.min(2.2, (factors.dependencies || []).length * 0.45);
const laneHint = factors.recommendedLane || ''; const laneHint = factors.recommendedLane || '';
const normalizedLaneHint = normalizeLaneHint(laneHint); const normalizedLaneHint = normalizeLaneHint(laneHint);
const conflicts = nonGoalConflicts(`${option.title} ${option.description}`, decisionContext); const conflicts = nonGoalConflicts(factors.conflictText || `${option.title} ${option.description}`, decisionContext);
const nonGoalPenalty = Math.min(14, conflicts.length * 7); const nonGoalPenalty = Math.min(14, conflicts.length * 7);
const laneBoost = /do|first|now|build/.test(laneHint) ? 3.1 : /validate|test|proof/.test(laneHint) ? 0.25 : /defer|park|cut/.test(laneHint) ? -0.75 : 0; const laneBoost = /do|first|now|build/.test(laneHint) ? 3.1 : /validate|test|proof/.test(laneHint) ? 0.25 : /defer|park|cut/.test(laneHint) ? -0.75 : 0;
const lanePenalty = normalizedLaneHint === 'park' ? 18 : normalizedLaneHint === 'defer' ? 9 : 0; const lanePenalty = normalizedLaneHint === 'park' ? 18 : normalizedLaneHint === 'defer' ? 9 : 0;