diff --git a/README.md b/README.md index 3fab05d..ef354f4 100644 --- a/README.md +++ b/README.md @@ -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. -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: diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 3d7130f..50f17c3 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -105,6 +105,15 @@ try { assert.ok(data.brief.whatWouldChangeRanking.some(item => /evidence fails|re-run the order/i.test(item))); assert.ok(Array.isArray(data.brief.assumptions)); 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.ok(data.handoff.readiness.nextChecks.some(item => /Do first item/i.test(item))); assert.match(data.handoff.copyableText, /# Ranker build-order handoff/); diff --git a/server.js b/server.js index 93bed4f..919cd03 100644 --- a/server.js +++ b/server.js @@ -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 = [ ['do', 'Do first'], ['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 [ '# Ranker build-order handoff', sourceLine ? `Source: ${sourceLine}` : '', sourceContextLine, readiness?.status ? `Readiness: ${readiness.status} — ${readiness.summary || ''}` : '', + activeSliceLines, contextLines.length ? ['## Carried context', ...contextLines.map(item => `- ${item}`)].join('\n') : '', ...itemLines, '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 readiness = handoffReadinessFor({ ranked, provenance, warnings: uniqueWarnings, expectsSourceTrace }); + const activeSlice = activeSliceFor({ ranked, provenance, readiness }); return { schema: 'rank-feedback-result-v1', @@ -1503,7 +1547,8 @@ function createHandoffContract({ ranked, provenance, decisionContext }) { itemTrace, warnings: uniqueWarnings, readiness, - copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness }), + activeSlice, + copyableText: copyableHandoffText({ ranked, provenance, decisionContext, readiness, activeSlice }), }; }