diff --git a/README.md b/README.md index 2e32ca1..fd26c07 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. 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: diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index abee496..d5f5dc2 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -171,6 +171,33 @@ try { assert.equal(hardRail.brief.quickGlance.topPick, 'Manual source-traced build order preview'); 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`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 320e315..319cec4 100644 --- a/server.js +++ b/server.js @@ -182,7 +182,9 @@ function guardrailsFromContextText(value = '') { const cleaned = cleanText(sentence, 180); 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, '')); - 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) }; }