Support closing-note rank fallback
This commit is contained in:
@@ -51,7 +51,7 @@ Candidate items may include optional 1–10 `rankerHints` (`value`, `effort`, `c
|
|||||||
|
|
||||||
Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. Ranker also accepts the current Scattermind storage-row shape with `referenceCode`, `ideaText`, `context`, and string-valued `fullReadingJson` / `full_reading_json`; it expands the saved paid Concept Map before ranking so operators do not have to hand-copy lenses out of Appwrite rows. If the row only has free Snapshot data (`glimpseJson` / `glimpse_json` / `snapshotJson`), Ranker expands that Snapshot into a minimal continuation order: one manual proof plus the first evidence question, with the Snapshot reference code/title preserved for provenance. The public paste form mirrors this: a prose-wrapped/fenced Appwrite row paste stays intact for the API instead of unwrapping the stringified reading into nonsense fields. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` / `handoff.source.originalPromptExcerpt` or, when the original prompt is unavailable, `sourceSummaryExcerpt` carry the source context so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from.
|
Candidate trace note: candidate-level `sourceItemId` / `traceId`, `sourceTitle` / `lensTitle`, and `sourceExcerpt` / `sourceQuote` are preserved in ranked items, `buildOrderDetails`, and `handoff.itemTrace`. Lens-only Build Order text is also split into deterministic `concept-map.lenses.channel#N` source IDs with the original labelled sentence carried as `sourceQuote`, so pasted paid Concept Maps remain traceable even without explicit candidate objects. String items in laned Build Order arrays now also receive deterministic section-local source IDs such as `concept-map.buildOrder.validateNext#1` and carry the original string as `sourceQuote`, so simple Scattermind exports stay addressable downstream instead of becoming anonymous `feature-1` rows. Ranker also accepts the current Scattermind storage-row shape with `referenceCode`, `ideaText`, `context`, and string-valued `fullReadingJson` / `full_reading_json`; it expands the saved paid Concept Map before ranking so operators do not have to hand-copy lenses out of Appwrite rows. If the row only has free Snapshot data (`glimpseJson` / `glimpse_json` / `snapshotJson`), Ranker expands that Snapshot into a minimal continuation order: one manual proof plus the first evidence question, with the Snapshot reference code/title preserved for provenance. The public paste form mirrors this: a prose-wrapped/fenced Appwrite row paste stays intact for the API instead of unwrapping the stringified reading into nonsense fields. The decision `brief.quickGlance.sourceTrace` now repeats the winning item's source section/id/title/quote, and both `brief.source.originalPromptExcerpt` / `handoff.source.originalPromptExcerpt` or, when the original prompt is unavailable, `sourceSummaryExcerpt` carry the source context so a downstream Scattermind handoff can show why the build order exists without digging through `input.provenance`. Scattermind should use these when a next move came from a specific Concept Map lens sentence, so Ranker can defend not just what wins but where the judgement came from.
|
||||||
|
|
||||||
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`, with either colon or reader-friendly dash separators (`Continue first: …`, `Continue first — …`, `Evidence next - …`). 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. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, and evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`) so Scattermind can pass a paid Concept Map preview without renaming it into software-feature language.
|
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`, with either colon or reader-friendly dash separators (`Continue first: …`, `Continue first — …`, `Evidence next - …`). 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. Ranker also accepts softer continuation envelopes named `rankerBridge`, `continuation`, or `continuationPlan`, candidate arrays named `possibleNextMoves`, `suggestedNextMoves`, `recommendations`, or `opportunities`, laned `buildOrderPreview` / `build_order_preview` objects, and evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`). If a paid Concept Map has no labelled Build Order/action threads but does include `closing_note` / `closingNote` plus decision questions, Ranker treats the closing note as the active 48-hour Do first move and keeps the questions in Validate next. This lets Scattermind pass reader-friendly Concept Map copy without renaming everything into software-feature language.
|
||||||
|
|
||||||
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. Ranker now also infers a light `ideaRoute` from the Scattermind source text and carries it in `input.decisionContext` / `handoff.decisionContext`; for game concepts it automatically adds anti-SaaS non-goals so account/dashboard/workspace/subscription candidates cannot win a playable-prototype build order just because they were phrased loudly. 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.
|
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. Ranker now also infers a light `ideaRoute` from the Scattermind source text and carries it in `input.decisionContext` / `handoff.decisionContext`; for game concepts it automatically adds anti-SaaS non-goals so account/dashboard/workspace/subscription candidates cannot win a playable-prototype build order just because they were phrased loudly. 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.
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,37 @@ try {
|
|||||||
assert.equal(threadGuardrail.buildOrder.doFirst[0], 'build-order-2', 'thread-level guardrail should demote a labelled Build first dashboard and promote the first eligible manual move');
|
assert.equal(threadGuardrail.buildOrder.doFirst[0], 'build-order-2', 'thread-level guardrail should demote a labelled Build first dashboard and promote the first eligible manual move');
|
||||||
assert.match(threadGuardrail.brief.decisionReceipt.sourceAnchor, /concept-map\.lenses\.channel/);
|
assert.match(threadGuardrail.brief.decisionReceipt.sourceAnchor, /concept-map\.lenses\.channel/);
|
||||||
|
|
||||||
|
const closingNoteFallbackResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceName: 'Scattermind',
|
||||||
|
artifactId: 'concept_map_closing_note_only',
|
||||||
|
originalPrompt: 'Clarify a rough service idea where the paid output has questions but no labelled Build Order text.',
|
||||||
|
conceptMap: {
|
||||||
|
working_name: 'Service proof without labels',
|
||||||
|
opening_reflection: 'The Concept Map says to prove a manual package teardown before building a product surface.',
|
||||||
|
lenses: {
|
||||||
|
shape: { title: 'Recommended Direction', content: 'Start as a manual service proof before adding software.' },
|
||||||
|
question: { title: 'Proof Steps', content: 'Ask two freelancers to send one messy offer page and record whether the teardown helps.' },
|
||||||
|
},
|
||||||
|
questions_to_sit_with: [
|
||||||
|
'Will one freelancer act on a manual package teardown within 48 hours?',
|
||||||
|
'What would make the teardown feel worth paying for?',
|
||||||
|
],
|
||||||
|
closing_note: 'Spend the next 48 hours running one manual package teardown for a real freelancer before designing any dashboard.',
|
||||||
|
reference_code: 'SM-CLOSE',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.equal(closingNoteFallbackResponse.status, 200);
|
||||||
|
const closingNoteFallback = await closingNoteFallbackResponse.json();
|
||||||
|
assert.equal(closingNoteFallback.buildOrder.doFirst[0], 'closing-note-next-move');
|
||||||
|
assert.match(closingNoteFallback.brief.decisionReceipt.firstProofStep, /running one manual package teardown/i);
|
||||||
|
assert.match(closingNoteFallback.brief.decisionReceipt.sourceAnchor, /concept-map\.closingNote/);
|
||||||
|
assert.equal(closingNoteFallback.buildOrderDetails.validateNext[0].sourceSection, 'concept-map.questionsToSitWith');
|
||||||
|
assert.equal(closingNoteFallback.handoff.readiness.status, 'ready');
|
||||||
|
|
||||||
const softDashLabelResponse = await fetch(`${base}/api/rank-feedback`, {
|
const softDashLabelResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -1236,6 +1236,31 @@ function optionsFromQuestionsToSitWith(items = [], sourceSection = 'concept-map.
|
|||||||
}).filter(item => item.action && item.evidence);
|
}).filter(item => item.action && item.evidence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionFromClosingNote(text = '', sourceSection = 'concept-map.closingNote', sourceTitle = '48-hour next move') {
|
||||||
|
const note = cleanMultiline(text, 520);
|
||||||
|
if (!note) return null;
|
||||||
|
return {
|
||||||
|
id: 'closing-note-next-move',
|
||||||
|
action: titleFromBuildOrderFragment(note),
|
||||||
|
why: 'Scattermind named this as the single best next move in the Concept Map closing note.',
|
||||||
|
evidence: /\b(evidence|signal|proof|test|validate|ask|observe|show|playtest|prototype)\b/i.test(note)
|
||||||
|
? note
|
||||||
|
: 'What smallest real-world signal would prove this 48-hour move deserves the active build slot?',
|
||||||
|
nextStep: note,
|
||||||
|
suggestedLane: 'do-first',
|
||||||
|
rankerHints: { value: 8, effort: 2, confidence: 7, urgency: 8, risk: 2 },
|
||||||
|
sourceSection,
|
||||||
|
sourceItemId: `${sourceSection}#1`,
|
||||||
|
sourceTitle,
|
||||||
|
sourceExcerpt: note,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function closingNoteFromSource(source = {}) {
|
||||||
|
const obj = objectFrom(source);
|
||||||
|
return cleanMultiline(obj.closing_note || obj.closingNote || obj.next48Hours || obj.next_48_hours || obj.startHere || obj.start_here || '', 520);
|
||||||
|
}
|
||||||
|
|
||||||
function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') {
|
function optionsFromSnapshotReading(source = {}, sourceSection = 'snapshot') {
|
||||||
const reading = objectFrom(source);
|
const reading = objectFrom(source);
|
||||||
const lenses = objectFrom(reading.lenses);
|
const lenses = objectFrom(reading.lenses);
|
||||||
@@ -1422,6 +1447,13 @@ function optionsFromBody(body = {}) {
|
|||||||
'Thread to hold'
|
'Thread to hold'
|
||||||
);
|
);
|
||||||
if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]);
|
if (actionThreadOptions.length >= 2) return normalizeCandidateGroup([{ items: actionThreadOptions, sourceSection: actionThreadSource.sourceSection || 'threadsToHold' }]);
|
||||||
|
const closingNoteSource = [
|
||||||
|
{ text: closingNoteFromSource(conceptMap), sourceSection: 'concept-map.closingNote' },
|
||||||
|
{ text: closingNoteFromSource(snapshot), sourceSection: 'snapshot.closingNote' },
|
||||||
|
{ text: closingNoteFromSource(envelope), sourceSection: 'ranker-input.closingNote' },
|
||||||
|
{ text: closingNoteFromSource(featureSet), sourceSection: 'feature-set.closingNote' },
|
||||||
|
{ text: closingNoteFromSource(body), sourceSection: 'closingNote' },
|
||||||
|
].find(entry => entry.text) || { text: '', sourceSection: '' };
|
||||||
const questionSource = firstArraySource([
|
const questionSource = firstArraySource([
|
||||||
{ items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' },
|
{ items: conceptMap.questions_to_sit_with || conceptMap.questionsToSitWith || conceptMap.evidenceQuestions || conceptMap.evidence_questions || conceptMap.decisionQuestions || conceptMap.decision_questions || conceptMap.questionsToAnswer || conceptMap.questions_to_answer || conceptMap.followupQuestions || conceptMap.followup_questions || conceptMap.openQuestions || conceptMap.open_questions, sourceSection: 'concept-map.questionsToSitWith' },
|
||||||
{ items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' },
|
{ items: snapshot.questions_to_sit_with || snapshot.questionsToSitWith || snapshot.evidenceQuestions || snapshot.evidence_questions || snapshot.decisionQuestions || snapshot.decision_questions || snapshot.questionsToAnswer || snapshot.questions_to_answer || snapshot.followupQuestions || snapshot.followup_questions || snapshot.openQuestions || snapshot.open_questions, sourceSection: 'snapshot.questionsToSitWith' },
|
||||||
@@ -1434,6 +1466,17 @@ function optionsFromBody(body = {}) {
|
|||||||
questionSource.sourceSection || 'questionsToSitWith',
|
questionSource.sourceSection || 'questionsToSitWith',
|
||||||
'Question to sit with'
|
'Question to sit with'
|
||||||
);
|
);
|
||||||
|
const closingNoteOption = optionFromClosingNote(
|
||||||
|
closingNoteSource.text,
|
||||||
|
closingNoteSource.sourceSection || 'closingNote',
|
||||||
|
'Concept Map closing note'
|
||||||
|
);
|
||||||
|
if (closingNoteOption && questionOptions.length) {
|
||||||
|
return normalizeCandidateGroup([
|
||||||
|
{ items: [closingNoteOption], sourceSection: closingNoteSource.sourceSection || 'closingNote', defaultLane: 'do-first' },
|
||||||
|
{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]);
|
if (questionOptions.length >= 2) return normalizeCandidateGroup([{ items: questionOptions, sourceSection: questionSource.sourceSection || 'questionsToSitWith', defaultLane: 'validate-next' }]);
|
||||||
const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot');
|
const nestedSnapshotReadingOptions = optionsFromSnapshotReading(snapshot, 'snapshot');
|
||||||
const snapshotReadingOptions = nestedSnapshotReadingOptions.length
|
const snapshotReadingOptions = nestedSnapshotReadingOptions.length
|
||||||
|
|||||||
Reference in New Issue
Block a user