diff --git a/README.md b/README.md index 288f2f1..0d21369 100644 --- a/README.md +++ b/README.md @@ -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. It also accepts private reading/API envelopes with object-valued `reading` / `fullReading` and `glimpse`, preserving `referenceCode` and `initialPrompt` while treating `reading` as the paid Concept Map. Stringified bridge envelopes in fields such as `rankerInput`, `rankerBridge`, `rankReady`, `bridgePayload`, or `continuationPlan` are expanded the same way, so Appwrite/string-copy handoffs do not have to be manually unwrapped before ranking. 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 into the bridge handoff. -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`, `Validate manually`, `Manual proof`, `Hold for later`, or `Set aside`, with either colon or reader-friendly dash separators (`Continue first: …`, `Validate manually — …`, `Evidence next - …`). Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `validateManually`, `manualProof`, `holdForLater`, and `setAside`. Those section values may be arrays, a single candidate object, or a reader-friendly string/newline list; Ranker splits scalar sections into candidate actions so Scattermind does not have to wrap every lane in arrays. 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, first-48-hour action arrays (`next48Hours`, `next_48_hours`, `first48Hours`, `first_48_hours`, `nextTwoDays`, `next_two_days`), and evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `proofQuestions` / `proof_questions`, `validationQuestions` / `validation_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`). Direct candidate objects may use reader-friendly prose keys like `text`, `content`, `summary`, `step`, `task`, `instruction`, `why_it_matters`, `evidence_to_collect`, `first_proof_step`, `green_flag`, and `red_flag`; Ranker normalizes those into title, value, evidence, next-step, success, and kill-signal fields so Scattermind does not have to rename paid Concept Map language into software-feature jargon. 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. +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`, `Validate manually`, `Manual proof`, `Hold for later`, or `Set aside`, with either colon or reader-friendly dash separators (`Continue first: …`, `Validate manually — …`, `Evidence next - …`). Build Order objects and direct bridge/envelope sections can use matching camel/snake-case keys such as `continueFirst`, `evidenceNext`, `validateManually`, `manualProof`, `holdForLater`, and `setAside`. Those section values may be arrays, a single candidate object, or a reader-friendly string/newline list; Ranker splits scalar sections into candidate actions so Scattermind does not have to wrap every lane in arrays. 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, first-48-hour action arrays (`next48Hours`, `next_48_hours`, `first48Hours`, `first_48_hours`, `nextTwoDays`, `next_two_days`), evidence-question fallback arrays (`evidenceQuestions` / `evidence_questions`, `proofQuestions` / `proof_questions`, `validationQuestions` / `validation_questions`, `decisionQuestions`, `questionsToAnswer`, `followupQuestions`), and assumption-test arrays (`assumptionTests` / `assumption_tests`, `riskiestAssumptions` / `riskiest_assumptions`, `risksToTest` / `risks_to_test`) that default into Validate next. Direct candidate objects may use reader-friendly prose keys like `text`, `content`, `summary`, `step`, `task`, `instruction`, `why_it_matters`, `evidence_to_collect`, `first_proof_step`, `green_flag`, and `red_flag`; Ranker normalizes those into title, value, evidence, next-step, success, and kill-signal fields so Scattermind does not have to rename paid Concept Map language into software-feature jargon. 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. diff --git a/public/app.js b/public/app.js index 4228804..e8aa9e3 100644 --- a/public/app.js +++ b/public/app.js @@ -77,10 +77,12 @@ function parsePastedJsonPayload(value) { || Array.isArray(parsed.recommendations) || Array.isArray(parsed.opportunities) || Array.isArray(parsed.doFirst) || Array.isArray(parsed.do_first) || Array.isArray(parsed.continueFirst) || Array.isArray(parsed.continue_first) || Array.isArray(parsed.makeTangible) || Array.isArray(parsed.make_tangible) || Array.isArray(parsed.validateNext) || Array.isArray(parsed.validate_next) || Array.isArray(parsed.evidenceNext) || Array.isArray(parsed.evidence_next) || Array.isArray(parsed.tryNext) || Array.isArray(parsed.try_next) + || Array.isArray(parsed.next48Hours) || Array.isArray(parsed.next_48_hours) || Array.isArray(parsed.first48Hours) || Array.isArray(parsed.first_48_hours) || Array.isArray(parsed.nextTwoDays) || Array.isArray(parsed.next_two_days) || Array.isArray(parsed.deferred) || Array.isArray(parsed.holdForLater) || Array.isArray(parsed.hold_for_later) || Array.isArray(parsed.parkingLot) || Array.isArray(parsed.parking_lot) || Array.isArray(parsed.setAside) || Array.isArray(parsed.set_aside) || Array.isArray(parsed.threads_to_hold) || Array.isArray(parsed.threadsToHold) || Array.isArray(parsed.actionThreads) || Array.isArray(parsed.action_threads) - || Array.isArray(parsed.questions_to_sit_with) || Array.isArray(parsed.questionsToSitWith) || Array.isArray(parsed.openQuestions) || Array.isArray(parsed.open_questions) + || Array.isArray(parsed.questions_to_sit_with) || Array.isArray(parsed.questionsToSitWith) || Array.isArray(parsed.evidenceQuestions) || Array.isArray(parsed.evidence_questions) || Array.isArray(parsed.proofQuestions) || Array.isArray(parsed.proof_questions) || Array.isArray(parsed.validationQuestions) || Array.isArray(parsed.validation_questions) || Array.isArray(parsed.decisionQuestions) || Array.isArray(parsed.decision_questions) || Array.isArray(parsed.questionsToAnswer) || Array.isArray(parsed.questions_to_answer) || Array.isArray(parsed.followupQuestions) || Array.isArray(parsed.followup_questions) || Array.isArray(parsed.openQuestions) || Array.isArray(parsed.open_questions) + || Array.isArray(parsed.assumptionTests) || Array.isArray(parsed.assumption_tests) || Array.isArray(parsed.riskiestAssumptions) || Array.isArray(parsed.riskiest_assumptions) || Array.isArray(parsed.risksToTest) || Array.isArray(parsed.risks_to_test) ); return looksLikeBridgePayload ? parsed : null; } catch { diff --git a/scripts/check-rank-feedback.mjs b/scripts/check-rank-feedback.mjs index 9c41c59..b50f618 100644 --- a/scripts/check-rank-feedback.mjs +++ b/scripts/check-rank-feedback.mjs @@ -306,6 +306,35 @@ try { assert.equal(softActionObject.ranked.find(item => item.id === 'saved-account-dashboard').lane.source, 'source-non-goal'); assert.equal(softActionObject.handoff.readiness.status, 'ready'); + const assumptionTestsResponse = await fetch(`${base}/api/rank-feedback`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceName: 'Scattermind', + artifactId: 'concept_map_assumption_tests', + originalPrompt: 'Clarify a paid workshop idea and decide what must be proven before building more surface.', + context: 'Solo builder. Avoid saved workspaces and billing until manual proof works.', + conceptMap: { + working_name: 'Workshop proof map', + next_48_hours: [ + { id: 'manual-workshop-invite', task: 'Invite three real operators to a manual workshop proof', proof_question: 'Will one person commit time from the invite alone?', source_item_id: 'first-48#1' }, + ], + riskiest_assumptions: [ + { id: 'operator-wants-workshop', hypothesis: 'Operators want a workshop rather than another dashboard', evidence_to_collect: 'At least one operator asks for the manual session after seeing the promise.', source_item_id: 'assumption#1' }, + { id: 'result-usable-without-account', assumption: 'The result is useful without accounts or saved workspaces', question: 'Can they act from the copied handoff alone?', source_item_id: 'assumption#2' }, + ], + }, + }), + }); + assert.equal(assumptionTestsResponse.status, 200); + const assumptionTests = await assumptionTestsResponse.json(); + assert.equal(assumptionTests.buildOrder.doFirst[0], 'manual-workshop-invite'); + assert.equal(assumptionTests.ranked.find(item => item.id === 'operator-wants-workshop').lane.id, 'test'); + assert.equal(assumptionTests.ranked.find(item => item.id === 'operator-wants-workshop').provenance.sourceSection, 'concept-map.assumptionTests'); + assert.match(assumptionTests.ranked.find(item => item.id === 'operator-wants-workshop').factors.evidenceNeeded, /manual session/); + assert.ok(assumptionTests.handoff.activeSlice.notNow.some(item => item.id === 'operator-wants-workshop')); + assert.equal(assumptionTests.handoff.readiness.status, 'ready'); + const threadGuardrailResponse = await fetch(`${base}/api/rank-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/server.js b/server.js index 2799b76..68c0de0 100644 --- a/server.js +++ b/server.js @@ -672,6 +672,12 @@ function looksLikeRankPayload(value = {}) { || Array.isArray(value.followup_questions) || Array.isArray(value.openQuestions) || Array.isArray(value.open_questions) + || Array.isArray(value.assumptionTests) + || Array.isArray(value.assumption_tests) + || Array.isArray(value.riskiestAssumptions) + || Array.isArray(value.riskiest_assumptions) + || Array.isArray(value.risksToTest) + || Array.isArray(value.risks_to_test) ); } @@ -1479,12 +1485,15 @@ function optionsFromBody(body = {}) { { items: body.experiments, sourceSection: 'experiments', defaultLane: 'validate-next' }, { items: body.validationTests || body.validation_tests, sourceSection: 'experiments', defaultLane: 'validate-next' }, { items: body.proofTests || body.proof_tests, sourceSection: 'experiments', defaultLane: 'validate-next' }, + { items: body.assumptionTests || body.assumption_tests || body.riskiestAssumptions || body.riskiest_assumptions || body.risksToTest || body.risks_to_test, sourceSection: 'assumptionTests', defaultLane: 'validate-next' }, { items: envelope.experiments, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, { items: envelope.validationTests || envelope.validation_tests, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, { items: envelope.proofTests || envelope.proof_tests, sourceSection: 'ranker-input.experiments', defaultLane: 'validate-next' }, + { items: envelope.assumptionTests || envelope.assumption_tests || envelope.riskiestAssumptions || envelope.riskiest_assumptions || envelope.risksToTest || envelope.risks_to_test, sourceSection: 'ranker-input.assumptionTests', defaultLane: 'validate-next' }, { items: featureSet.experiments, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, { items: featureSet.validationTests || featureSet.validation_tests, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, { items: featureSet.proofTests || featureSet.proof_tests, sourceSection: 'feature-set.experiments', defaultLane: 'validate-next' }, + { items: featureSet.assumptionTests || featureSet.assumption_tests || featureSet.riskiestAssumptions || featureSet.riskiest_assumptions || featureSet.risksToTest || featureSet.risks_to_test, sourceSection: 'feature-set.assumptionTests', defaultLane: 'validate-next' }, { items: body.validateNext || body.validate_next || body.validateManually || body.validate_manually || body.validate || body.validation || body.evidenceNext || body.evidence_next || body.tryNext || body.try_next || body.learnNext || body.learn_next || body.testManually || body.test_manually || body.manualProof || body.manual_proof, sourceSection: 'validateNext', defaultLane: 'validate-next' }, { items: body.deferred || body.defer || body.later || body.afterProof || body.after_proof || body.holdForLater || body.hold_for_later || body.notYet || body.not_yet, sourceSection: 'deferred', defaultLane: 'defer' }, { items: body.parkingLot || body.parking_lot || body.park || body.parked || body.probablyNoise || body.probably_noise || body.setAside || body.set_aside || body.outOfScope || body.out_of_scope, sourceSection: 'parkingLot', defaultLane: 'park' }, @@ -1508,6 +1517,7 @@ function optionsFromBody(body = {}) { { items: conceptMap.experiments, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, { items: conceptMap.validationTests || conceptMap.validation_tests, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, { items: conceptMap.proofTests || conceptMap.proof_tests, sourceSection: 'concept-map.experiments', defaultLane: 'validate-next' }, + { items: conceptMap.assumptionTests || conceptMap.assumption_tests || conceptMap.riskiestAssumptions || conceptMap.riskiest_assumptions || conceptMap.risksToTest || conceptMap.risks_to_test, sourceSection: 'concept-map.assumptionTests', defaultLane: 'validate-next' }, { items: conceptMap.deferred || conceptMap.defer || conceptMap.later || conceptMap.afterProof || conceptMap.after_proof || conceptMap.holdForLater || conceptMap.hold_for_later || conceptMap.notYet || conceptMap.not_yet, sourceSection: 'concept-map.deferred', defaultLane: 'defer' }, { items: conceptMap.parkingLot || conceptMap.parking_lot || conceptMap.park || conceptMap.parked || conceptMap.probablyNoise || conceptMap.probably_noise || conceptMap.setAside || conceptMap.set_aside || conceptMap.outOfScope || conceptMap.out_of_scope, sourceSection: 'concept-map.parkingLot', defaultLane: 'park' }, ]); @@ -1525,6 +1535,7 @@ function optionsFromBody(body = {}) { { items: snapshot.experiments, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, { items: snapshot.validationTests || snapshot.validation_tests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, { items: snapshot.proofTests || snapshot.proof_tests, sourceSection: 'snapshot.experiments', defaultLane: 'validate-next' }, + { items: snapshot.assumptionTests || snapshot.assumption_tests || snapshot.riskiestAssumptions || snapshot.riskiest_assumptions || snapshot.risksToTest || snapshot.risks_to_test, sourceSection: 'snapshot.assumptionTests', defaultLane: 'validate-next' }, { items: snapshot.deferred || snapshot.defer || snapshot.later || snapshot.afterProof || snapshot.after_proof || snapshot.holdForLater || snapshot.hold_for_later || snapshot.notYet || snapshot.not_yet, sourceSection: 'snapshot.deferred', defaultLane: 'defer' }, { items: snapshot.parkingLot || snapshot.parking_lot || snapshot.park || snapshot.parked || snapshot.probablyNoise || snapshot.probably_noise || snapshot.setAside || snapshot.set_aside || snapshot.outOfScope || snapshot.out_of_scope, sourceSection: 'snapshot.parkingLot', defaultLane: 'park' }, ]);