Harden soft Scattermind guardrails

This commit is contained in:
OpenClaw Bot
2026-05-27 17:08:37 +02:00
parent 759c1b2fe4
commit c3edf9f29d
3 changed files with 31 additions and 2 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle`
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.
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; 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 source-traced manual proof plus evidence-question candidates instead of rejecting the Snapshot for having fewer than two explicit next moves. 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. 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.
Recommended payload shape: Recommended payload shape:
+27
View File
@@ -171,6 +171,33 @@ try {
assert.equal(hardRail.brief.quickGlance.topPick, 'Manual source-traced build order preview'); assert.equal(hardRail.brief.quickGlance.topPick, 'Manual source-traced build order preview');
assert.equal(hardRail.handoff.readiness.status, 'ready'); assert.equal(hardRail.handoff.readiness.status, 'ready');
const softGuardrailResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schema: 'prioritix-feature-set-v1',
sourceName: 'Scattermind',
artifactId: 'concept_map_soft_guardrails',
idea: 'A Concept Map clarified that the continuation should stay lightweight and defend one next move.',
context: 'Solo builder. This is not a dashboard. Keep auth, billing, saved workspaces, and collaboration out until proof.',
mode: 'mvp',
featureSet: {
features: [
{ id: 'decision-strip', title: 'One-screen decision strip', description: 'Show the active build slice with source trace and the first proof step.', evidenceNeeded: 'Can a tired user explain the first move?', rankerHints: { value: 8, effort: 2, confidence: 7, urgency: 7, risk: 2 } },
{ id: 'saved-workspace', title: 'Saved workspace dashboard', description: 'Accounts, auth, billing, team collaboration, and saved project dashboards.', rankerHints: { value: 10, effort: 1, confidence: 9, urgency: 9, risk: 1 } },
{ id: 'copy-brief', title: 'Copy decision brief', description: 'Copy the defended order and source anchor into notes.', recommendedLane: 'validate-next' },
],
},
}),
});
assert.equal(softGuardrailResponse.status, 200);
const softGuardrail = await softGuardrailResponse.json();
assert.ok(softGuardrail.input.decisionContext.nonGoals.some(item => /not a dashboard/i.test(item)), 'soft dashboard language should become a non-goal');
assert.ok(softGuardrail.input.decisionContext.nonGoals.some(item => /Keep auth, billing, saved workspaces/i.test(item)), 'keep-out-until-proof language should become a non-goal');
assert.equal(softGuardrail.ranked.find(item => item.id === 'saved-workspace').lane.source, 'source-non-goal');
assert.equal(softGuardrail.buildOrder.doFirst[0], 'decision-strip');
assert.ok(!/dashboard/i.test(softGuardrail.brief.quickGlance.topPick));
const hintedResponse = await fetch(`${base}/api/rank-feedback`, { const hintedResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
+3 -1
View File
@@ -182,7 +182,9 @@ function guardrailsFromContextText(value = '') {
const cleaned = cleanText(sentence, 180); const cleaned = cleanText(sentence, 180);
if (!cleaned) continue; if (!cleaned) continue;
if (/^(avoid|no|do not|don't|dont|must not|never|non-goal|non goal|not yet|out of scope)\b/i.test(cleaned)) nonGoals.push(cleaned.replace(/^non[- ]goal\s*:\s*/i, '')); if (/^(avoid|no|do not|don't|dont|must not|never|non-goal|non goal|not yet|out of scope)\b/i.test(cleaned)) nonGoals.push(cleaned.replace(/^non[- ]goal\s*:\s*/i, ''));
else if (/\b(avoid|no auth|no account|no billing|no workspace|not a dashboard|without accounts|before proof|manual proof|solo builder|constraint)\b/i.test(cleaned)) constraints.push(cleaned); else if (/\b(keep|leave|hold)\b[^.!?]{0,80}\b(out|later|until proof|after proof|not yet)\b/i.test(cleaned)) nonGoals.push(cleaned);
else if (/\b(not a dashboard|not another dashboard|no dashboard swamp|dashboard swamp)\b/i.test(cleaned)) nonGoals.push(cleaned);
else if (/\b(avoid|no auth|no account|no billing|no workspace|without accounts|before proof|manual proof|solo builder|constraint)\b/i.test(cleaned)) constraints.push(cleaned);
} }
return { nonGoals: uniqueList(nonGoals), constraints: uniqueList(constraints) }; return { nonGoals: uniqueList(nonGoals), constraints: uniqueList(constraints) };
} }