From 55e75bc793fe82fa5f19ca2a1f5d302d3ff40085 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Wed, 27 May 2026 15:56:19 +0200 Subject: [PATCH] Add compact decision receipt for Ranker handoff --- README.md | 2 +- public/app.js | 25 +++++++++++++++++++++++++ public/styles.css | 4 +++- scripts/check-rank-feedback.mjs | 5 +++++ server.js | 10 ++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 408605f..af72426 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 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. 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/public/app.js b/public/app.js index 9299f5f..3433107 100644 --- a/public/app.js +++ b/public/app.js @@ -186,6 +186,30 @@ function renderHandoffStatus(handoff = {}) { `; } +function renderDecisionReceipt(receipt = {}) { + if (!receipt.activeMove) return ''; + const held = receipt.doNotStartYet || []; + return ` +
+
+ Active move + ${escapeHtml(receipt.activeMove)} +
+
+ First proof step +

${escapeHtml(receipt.firstProofStep || 'Run the smallest manual proof before product machinery.')}

+
+
+ Evidence question +

${escapeHtml(receipt.evidenceQuestion || 'What would make this ranking obviously right or wrong?')}

+
+ ${held.length ? `
Do not start yet
    ${held.map((item) => `
  • ${escapeHtml(item)}
  • `).join('')}
` : ''} + ${receipt.sourceAnchor ? `Source anchor: ${escapeHtml(receipt.sourceAnchor)}` : ''} + ${escapeHtml(receipt.handoffRule || 'Only the Do first item is active.')} +
+ `; +} + function renderRankCard(item) { return `
@@ -314,6 +338,7 @@ function renderResults(data) {
Proof to collect

${escapeHtml(glance.evidenceQuestion || 'Name the evidence that would change the ranking.')}

Trap to avoid

${escapeHtml(glance.biggestTrap || brief.caution || 'Do not treat first-pass judgement as final truth.')}

+ ${renderDecisionReceipt(brief.decisionReceipt || {})} ${renderSourceTrace(glance.sourceTrace || {})}
diff --git a/public/styles.css b/public/styles.css index a252a98..eaf7f5d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -39,4 +39,6 @@ button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text- .lane-column .rank-card{box-shadow:none;margin:0;border-width:1.5px}.lane-column .rank-card h3{font-size:clamp(20px,2vw,28px)}.lane-column .metrics{grid-template-columns:1fr}.signal-pills{margin:10px 0}.signal-pills span{background:#f0e8d9;font-size:10px;padding:5px 8px}.action-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:14px 0}.action-strip>div{border:1.5px solid var(--hair);background:#fff6e6;padding:10px}.action-strip span{color:var(--blue2);font-size:10px}.action-strip p{margin:0;font-size:13px;line-height:1.35}.reflection-room{margin-top:24px}.expert-card{position:relative}.expert-card::before{content:"“";position:absolute;right:14px;top:0;font-size:72px;line-height:1;color:rgba(36,92,255,.16);font-weight:1000} @media (max-width:1100px){.lane-board{grid-template-columns:repeat(2,minmax(0,1fr))}.quick-glance{grid-template-columns:repeat(2,minmax(0,1fr))}.action-strip{grid-template-columns:1fr}} @media (max-width:700px){.lane-board,.quick-glance{grid-template-columns:1fr}.memo-head::after{position:static;display:inline-block;margin-top:14px;transform:none}.result-actions .button{width:100%;justify-content:center}.quick-glance{box-shadow:6px 6px 0 var(--ink)}.quick-glance>div{min-height:auto}} -.source-trace{margin:-4px 0 22px;padding:14px 16px;border:2px dashed var(--ink);background:#fff6e5;color:var(--ink2);box-shadow:5px 5px 0 rgba(21,19,15,.16)}.source-trace span,.handoff-card>span{display:block;margin-bottom:8px;color:var(--blue2);text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:1000}.source-trace b{display:block;margin-bottom:6px;font-size:14px}.source-trace p{margin:0;color:var(--muted);line-height:1.45}.item-source-trace{margin:12px 0;padding:9px 10px;border:1.5px dashed var(--hair);background:#fffaf1;color:var(--ink2)}.item-source-trace summary{cursor:pointer;color:var(--blue2);font-size:11px;font-weight:1000;letter-spacing:.08em;text-transform:uppercase}.item-source-trace p{margin:8px 0 6px;color:var(--muted);font-size:13px;line-height:1.4}.item-source-trace small{display:block;color:var(--muted);font-size:11px;font-weight:800}.handoff-card{border-left:8px solid var(--green)}.handoff-card.status-usable-with-warnings{border-left-color:var(--amber)}.handoff-card.status-needs-source-context,.handoff-card.status-blocked{border-left-color:var(--red)}.handoff-warnings{display:grid;gap:6px;margin-top:12px}.handoff-warnings code{display:block;white-space:normal;border:1px solid var(--hair);background:#f3eee4;padding:7px 9px;font-size:12px;color:var(--ink2)} +.decision-receipt{display:grid;grid-template-columns:1.1fr 1fr 1fr 1fr;gap:10px;margin:-6px 0 22px;padding:12px;border:2px solid var(--ink);background:#fffaf1;box-shadow:6px 6px 0 rgba(21,19,15,.18)}.decision-receipt>div{border:1.5px solid var(--hair);background:#fff6e5;padding:12px}.decision-receipt span{display:block;margin-bottom:7px;color:var(--blue2);text-transform:uppercase;letter-spacing:.12em;font-size:10px;font-weight:1000}.decision-receipt strong{display:block;font-size:clamp(20px,2.3vw,30px);line-height:1;letter-spacing:-.05em}.decision-receipt p{margin:0;color:var(--ink2);line-height:1.35}.decision-receipt ul{margin:0;padding-left:18px;color:var(--ink2)}.decision-receipt small{grid-column:1/-1;color:var(--muted);font-weight:850}.source-trace{margin:-4px 0 22px;padding:14px 16px;border:2px dashed var(--ink);background:#fff6e5;color:var(--ink2);box-shadow:5px 5px 0 rgba(21,19,15,.16)}.source-trace span,.handoff-card>span{display:block;margin-bottom:8px;color:var(--blue2);text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:1000}.source-trace b{display:block;margin-bottom:6px;font-size:14px}.source-trace p{margin:0;color:var(--muted);line-height:1.45}.item-source-trace{margin:12px 0;padding:9px 10px;border:1.5px dashed var(--hair);background:#fffaf1;color:var(--ink2)}.item-source-trace summary{cursor:pointer;color:var(--blue2);font-size:11px;font-weight:1000;letter-spacing:.08em;text-transform:uppercase}.item-source-trace p{margin:8px 0 6px;color:var(--muted);font-size:13px;line-height:1.4}.item-source-trace small{display:block;color:var(--muted);font-size:11px;font-weight:800}.handoff-card{border-left:8px solid var(--green)}.handoff-card.status-usable-with-warnings{border-left-color:var(--amber)}.handoff-card.status-needs-source-context,.handoff-card.status-blocked{border-left-color:var(--red)}.handoff-warnings{display:grid;gap:6px;margin-top:12px}.handoff-warnings code{display:block;white-space:normal;border:1px solid var(--hair);background:#f3eee4;padding:7px 9px;font-size:12px;color:var(--ink2)} +@media (max-width:1100px){.decision-receipt{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media (max-width:700px){.decision-receipt{grid-template-columns:1fr;box-shadow:5px 5px 0 rgba(21,19,15,.18)}} diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 1c106e4..de8f328 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -80,6 +80,11 @@ try { assert.match(data.ranked.find(item => item.id === 'bridge-contract').factors.evidenceNeeded, /Concept Map/); assert.ok(data.ranked.find(item => item.id === 'bridge-contract').factors.metricHints.value === undefined); assert.equal(data.brief.source.artifactId, 'snapshot_123'); + assert.equal(data.brief.decisionReceipt.activeMove, data.brief.quickGlance.topPick); + assert.match(data.brief.decisionReceipt.firstProofStep, /manual proof/i); + assert.deepEqual(data.brief.decisionReceipt.doNotStartYet.slice(0, 2), ['Subscription billing layer', 'Accounts and saved workspaces']); + assert.match(data.brief.decisionReceipt.sourceAnchor, /concept-map\.nextMoves/); + assert.match(data.brief.decisionReceipt.handoffRule, /Only the Do first item is active/); assert.match(data.brief.source.originalPromptExcerpt, /tiny shop idea/); assert.match(data.brief.summary, /Source: Tiny shop idea clarity pass · snapshot_123/); assert.equal(data.handoff.schema, 'rank-feedback-result-v1'); diff --git a/server.js b/server.js index d569ede..f7b9bce 100644 --- a/server.js +++ b/server.js @@ -1241,6 +1241,15 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision sourceQuote: top.provenance?.sourceQuote || '', }, } : null; + const decisionReceipt = top ? { + activeMove: top.title, + activeLane: top.lane?.label || 'Do first', + firstProofStep: nextStepFor(top), + evidenceQuestion: evidenceQuestionFor(top), + doNotStartYet: deferred.slice(0, 3).map(item => item.title), + sourceAnchor: [top.provenance?.sourceSection, top.provenance?.sourceId || top.provenance?.sourceTitle].filter(Boolean).join(' · '), + handoffRule: 'Only the Do first item is active. Validate one proof, then re-rank before promoting anything else.', + } : null; const assumptions = [ ...(decisionContext?.assumptions || []), ...(decisionContext?.constraints || []).map(item => `Constraint: ${item}`), @@ -1250,6 +1259,7 @@ function createDecisionBrief({ idea, context, mode, ranked, provenance, decision headline: top ? `Start with ${top.title}` : 'Add options to get a ranked feedback map', summary: `${theme}${second ? ` “${second.title}” is the nearest follow-up, not a parallel first step.` : ''}${sourceLabel ? ` Source: ${sourceLabel}.` : ''}`, quickGlance, + decisionReceipt, source: provenance ? { schema: provenance.schema, source: provenance.source,