Add active slice handoff contract
This commit is contained in:
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
|||||||
@@ -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 }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user