Add active slice handoff contract

This commit is contained in:
OpenClaw Bot
2026-05-27 16:05:12 +02:00
parent 92a086824c
commit 460f088a8b
3 changed files with 57 additions and 3 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. 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. 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.
Recommended payload shape: Recommended payload shape:
+9
View File
@@ -105,6 +105,15 @@ try {
assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item))); assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item)));
assert.ok(Array.isArray(data.brief.assumptions)); assert.ok(Array.isArray(data.brief.assumptions));
assert.equal(data.handoff.readiness.status, 'ready'); assert.equal(data.handoff.readiness.status, 'ready');
assert.equal(data.handoff.activeSlice.schema, 'ranker-active-slice-v1');
assert.equal(data.handoff.activeSlice.item.id, data.buildOrder.doFirst[0]);
assert.match(data.handoff.activeSlice.proof.nextStep, /manual proof/i);
assert.equal(data.handoff.activeSlice.source.artifactId, 'snapshot_123');
assert.equal(data.handoff.activeSlice.source.sourceSection, 'concept-map.nextMoves');
assert.ok(data.handoff.activeSlice.notNow.some(item => item.id === 'export'));
assert.match(data.handoff.activeSlice.rule, /Only this active slice is build-ready/);
assert.match(data.handoff.copyableText, /## Active slice/);
assert.match(data.handoff.copyableText, /- Do not start yet:/);
assert.equal(data.handoff.readiness.sourceComplete, true); assert.equal(data.handoff.readiness.sourceComplete, true);
assert.ok(data.handoff.readiness.nextChecks.some(item => /Do first item/i.test(item))); assert.ok(data.handoff.readiness.nextChecks.some(item => /Do first item/i.test(item)));
assert.match(data.handoff.copyableText, /# Ranker build-order handoff/); assert.match(data.handoff.copyableText, /# Ranker build-order handoff/);
+47 -2
View File
@@ -1396,7 +1396,41 @@ function handoffReadinessFor({ ranked = [], provenance = {}, warnings = [], expe
}; };
} }
function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {}, readiness = {} }) { function activeSliceFor({ ranked = [], provenance = {}, readiness = {} }) {
const active = ranked.find(item => item.lane?.id === 'do') || ranked.find(item => item.lane?.id === 'test') || ranked[0] || null;
const heldBack = ranked.filter(item => item.id !== active?.id && ['test', 'defer', 'park'].includes(item.lane?.id)).slice(0, 4);
if (!active) return null;
return {
schema: 'ranker-active-slice-v1',
item: {
id: active.id,
title: active.title,
lane: active.lane?.id || 'do',
laneLabel: active.lane?.label || 'Do first',
reason: reasonFor(active),
},
proof: {
nextStep: nextStepFor(active),
evidenceQuestion: evidenceQuestionFor(active),
successSignal: successSignalFor(active),
killSignal: killSignalFor(active),
},
source: {
artifactId: provenance?.artifactId || '',
conceptMapId: provenance?.conceptMapId || '',
snapshotTitle: provenance?.snapshotTitle || '',
sourceSection: active.provenance?.sourceSection || '',
sourceId: active.provenance?.sourceId || '',
sourceTitle: active.provenance?.sourceTitle || '',
sourceQuote: active.provenance?.sourceQuote || '',
},
notNow: heldBack.map(item => ({ id: item.id, title: item.title, lane: item.lane?.id || 'defer', reason: reasonFor(item) })),
readinessStatus: readiness?.status || '',
rule: 'Only this active slice is build-ready. Do not promote Validate next / Defer / Park items until evidence changes and Ranker is rerun.',
};
}
function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {}, readiness = {}, activeSlice = null }) {
const lanes = [ const lanes = [
['do', 'Do first'], ['do', 'Do first'],
['test', 'Validate next'], ['test', 'Validate next'],
@@ -1438,11 +1472,20 @@ function copyableHandoffText({ ranked = [], provenance = {}, decisionContext = {
'', '',
]; ];
}); });
const activeSliceLines = activeSlice ? [
'## Active slice',
`- Move: ${activeSlice.item.title}`,
`- Proof: ${activeSlice.proof.nextStep}`,
`- Evidence question: ${activeSlice.proof.evidenceQuestion}`,
activeSlice.source.sourceSection ? `- Source: ${[activeSlice.source.sourceSection, activeSlice.source.sourceId].filter(Boolean).join(' · ')}` : '',
activeSlice.notNow.length ? `- Do not start yet: ${activeSlice.notNow.map(item => item.title).join('; ')}` : '',
].filter(Boolean).join('\n') : '';
return [ return [
'# Ranker build-order handoff', '# Ranker build-order handoff',
sourceLine ? `Source: ${sourceLine}` : '', sourceLine ? `Source: ${sourceLine}` : '',
sourceContextLine, sourceContextLine,
readiness?.status ? `Readiness: ${readiness.status}${readiness.summary || ''}` : '', readiness?.status ? `Readiness: ${readiness.status}${readiness.summary || ''}` : '',
activeSliceLines,
contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '', contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '',
...itemLines, ...itemLines,
'Rule: only the Do first item is active. Re-rank after evidence changes; do not quietly turn this into a workspace/dashboard backlog.', 'Rule: only the Do first item is active. Re-rank after evidence changes; do not quietly turn this into a workspace/dashboard backlog.',
@@ -1479,6 +1522,7 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
}); });
const uniqueWarnings = [...new Set(warnings)]; const uniqueWarnings = [...new Set(warnings)];
const readiness = handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace }); const readiness = handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace });
const activeSlice = activeSliceFor({ ranked, provenance, readiness });
return { return {
schema: 'rank-feedback-result-v1', schema: 'rank-feedback-result-v1',
@@ -1503,7 +1547,8 @@ function createHandoffContract({ ranked, provenance, decisionContext }) {
itemTrace, itemTrace,
warnings: uniqueWarnings, warnings: uniqueWarnings,
readiness, readiness,
copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness }), activeSlice,
copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness, activeSlice }),
}; };
} }