Compare commits

...

86 Commits

Author SHA1 Message Date
OpenClaw Bot a348acf6ef Refocus Ranker as Prioritix idea-layer sorting 2026-05-31 22:51:08 +02:00
OpenClaw Bot 577fd081d6 Show proof script on active Ranker slice 2026-05-28 17:11:12 +02:00
OpenClaw Bot 76d62d2862 Warm up first-screen proof script and headline for non-AI-native users
- proofScriptFor: use plain language when no source artifact is present
  (no 'Scattermind' name-dropping for raw paste users). Source-aware
  script is preserved when artifact context exists.
- firstScreen.headline: 'One thing now: [title]' replaces 'Build only
  this first: [title]' — simpler declarative for tired users.
- proof script fallback now includes a 'what happens next' sentence
  so the user knows evidence changes the decision.
- Updated check-rank-feedback.mjs headline assertion to match.
2026-05-28 17:05:12 +02:00
OpenClaw Bot 4212b4d7c8 Carry Concept Map thread signals into Ranker 2026-05-28 00:38:31 +02:00
OpenClaw Bot 066717221c Carry Concept Map questions with build order 2026-05-28 00:33:33 +02:00
OpenClaw Bot 6a2cc34759 Accept game-specific Scattermind build fields 2026-05-28 00:26:29 +02:00
OpenClaw Bot 896198fe07 Accept nested proof gate bridge fields 2026-05-28 00:17:48 +02:00
OpenClaw Bot f424b61730 Accept Scattermind first-week build order handoffs 2026-05-28 00:12:33 +02:00
OpenClaw Bot 074892b7e3 Accept proof plan bridge aliases 2026-05-28 00:06:43 +02:00
OpenClaw Bot 0b7ca6d009 Accept rank-ready build order bridge payloads 2026-05-27 23:58:21 +02:00
OpenClaw Bot 70dfbf6817 Expose pass-fail proof gates in Ranker brief 2026-05-27 23:52:26 +02:00
OpenClaw Bot 67c42d5ab6 Accept ranked build order bridge alias 2026-05-27 23:48:44 +02:00
OpenClaw Bot 5fad144a8f Add proof script to Ranker handoff 2026-05-27 23:42:10 +02:00
OpenClaw Bot 9c0f4bebd3 Accept reader-friendly paid build order labels 2026-05-27 23:38:19 +02:00
OpenClaw Bot aae093904c Accept reader-friendly Scattermind build order aliases 2026-05-27 23:34:45 +02:00
OpenClaw Bot 4f0553c26d Preserve Scattermind build-order proof signals 2026-05-27 21:08:00 +02:00
OpenClaw Bot 18c85b21dc Accept Scattermind assumption test aliases 2026-05-27 21:03:08 +02:00
OpenClaw Bot 74b6ad2d85 Handle numbered Scattermind build order labels 2026-05-27 20:57:35 +02:00
OpenClaw Bot 2fa061120c Expose active source quote in Ranker receipt 2026-05-27 20:51:03 +02:00
OpenClaw Bot 77b5395962 Accept scalar Scattermind build order sections 2026-05-27 20:44:53 +02:00
OpenClaw Bot ce3885d406 Handle pasted Scattermind payload wrappers 2026-05-27 20:36:51 +02:00
OpenClaw Bot a66788e394 Warn on duplicate Ranker source traces 2026-05-27 20:32:50 +02:00
OpenClaw Bot fe07245710 Accept Scattermind roadmap proof labels 2026-05-27 20:27:14 +02:00
OpenClaw Bot f44d5c3cc3 Preserve Scattermind action-thread signals 2026-05-27 20:22:25 +02:00
OpenClaw Bot 0447e89ca6 Accept Scattermind first-48-hour handoffs 2026-05-27 20:13:21 +02:00
OpenClaw Bot 9f712a3b93 Handle single Scattermind action thread handoffs 2026-05-27 20:08:55 +02:00
OpenClaw Bot 46846cada7 Accept soft Scattermind action fields 2026-05-27 20:02:51 +02:00
OpenClaw Bot 9c35af237f Accept Scattermind private reading envelopes 2026-05-27 19:57:34 +02:00
OpenClaw Bot 85e2a83d3b Accept comma-separated Scattermind build labels 2026-05-27 19:50:39 +02:00
OpenClaw Bot fae434391b Accept stringified Ranker bridge envelopes 2026-05-27 19:45:52 +02:00
OpenClaw Bot 13622de5a0 Support closing-note rank fallback 2026-05-27 19:38:53 +02:00
OpenClaw Bot 90beb50459 Surface bridge handoff readiness in results 2026-05-27 19:31:32 +02:00
OpenClaw Bot c26bd4bfb0 Accept structured bridge decision context 2026-05-27 19:23:23 +02:00
OpenClaw Bot dc75206fcd Accept paid Concept Map build-order labels 2026-05-27 19:18:45 +02:00
OpenClaw Bot 4e1b36d7b4 Add active slice first-screen receipt 2026-05-27 19:14:04 +02:00
OpenClaw Bot bff9b1e7c3 Add route guardrails to Ranker handoff 2026-05-27 19:09:29 +02:00
OpenClaw Bot 39287ea2e3 Accept Scattermind evidence question aliases 2026-05-27 19:01:17 +02:00
OpenClaw Bot b2744a791b Accept dash-separated Scattermind lane labels 2026-05-27 18:56:50 +02:00
OpenClaw Bot 7ed035af82 Carry Scattermind proof lens into ranking 2026-05-27 18:50:34 +02:00
OpenClaw Bot e463f4bc2a Carry Scattermind thread guardrails into Ranker 2026-05-27 18:46:15 +02:00
OpenClaw Bot ec27d13330 Accept softer Scattermind continuation aliases 2026-05-27 18:37:07 +02:00
OpenClaw Bot c3edf9f29d Harden soft Scattermind guardrails 2026-05-27 17:08:37 +02:00
OpenClaw Bot 759c1b2fe4 Accept stored Scattermind snapshot rows 2026-05-27 17:04:52 +02:00
OpenClaw Bot 85c8067185 Accept wrapped Scattermind thread fallbacks 2026-05-27 16:58:31 +02:00
OpenClaw Bot 0271bfcbf6 Accept rank-ready candidate action aliases 2026-05-27 16:53:58 +02:00
OpenClaw Bot 8bea868fb2 Harden Scattermind stored row paste handoff 2026-05-27 16:45:56 +02:00
OpenClaw Bot 2459d253e1 Harden pasted Scattermind thread handoffs 2026-05-27 16:38:12 +02:00
OpenClaw Bot 6a34916697 Accept free Scattermind snapshot handoffs 2026-05-27 16:33:49 +02:00
OpenClaw Bot a225586296 Support Scattermind question-only rank handoffs 2026-05-27 16:27:12 +02:00
OpenClaw Bot ca186f2a01 Accept stored Scattermind concept map rows 2026-05-27 16:17:12 +02:00
OpenClaw Bot 421913dc2c Accept Scattermind action thread fallbacks 2026-05-27 16:11:38 +02:00
OpenClaw Bot 460f088a8b Add active slice handoff contract 2026-05-27 16:05:12 +02:00
OpenClaw Bot 92a086824c Accept recommended action aliases in Ranker bridge 2026-05-27 16:01:24 +02:00
OpenClaw Bot 55e75bc793 Add compact decision receipt for Ranker handoff 2026-05-27 15:56:19 +02:00
OpenClaw Bot 10084afb96 Accept soft direct Scattermind lane sections 2026-05-27 15:50:52 +02:00
OpenClaw Bot f7d459a629 Accept direct Scattermind bridge envelope sections 2026-05-27 15:45:55 +02:00
OpenClaw Bot e3cff7266c Harden Scattermind bridge envelope import 2026-05-27 15:40:55 +02:00
OpenClaw Bot 1a829e05af Harden Ranker summary guardrails 2026-05-27 15:34:47 +02:00
OpenClaw Bot 5cd5dd2fcf Expose Ranker source traces in result cards 2026-05-27 15:30:34 +02:00
OpenClaw Bot 470965b8b7 Harden Scattermind summary handoff provenance 2026-05-27 15:23:34 +02:00
OpenClaw Bot e22cb30061 Add copyable Ranker handoff text 2026-05-27 14:58:29 +02:00
OpenClaw Bot 8e6040e90c Add source citation copy for Ranker handoff 2026-05-27 14:51:20 +02:00
OpenClaw Bot 3ff7272720 Probe Ranker health tables 2026-05-27 14:47:51 +02:00
OpenClaw Bot 578f954020 test: add deterministic rank-smoke for /api/rank-feedback
Add scripts/rank-smoke.mjs to verify:
- Core response shape and handoff contract
- Deterministic scoring for same payload
- Build order lane sanity (do/validate/defer/park)
- Guardrail defense: recommendedLane=park respected, non-goal conflicts prevent do lane

Tightens Scattermind→Ranker bridge contract without deployment changes.
2026-05-27 09:36:35 +02:00
OpenClaw Bot b5e791ae33 Accept soft Scattermind bridge lane labels 2026-05-27 01:32:17 +02:00
OpenClaw Bot bf29b7ab95 Accept next steps bridge aliases 2026-05-27 01:26:37 +02:00
OpenClaw Bot 1c4897694c Accept snake-case Scattermind bridge exports 2026-05-27 01:23:02 +02:00
OpenClaw Bot 242fe235a5 Accept schema-light Scattermind snapshot pastes 2026-05-27 01:17:06 +02:00
OpenClaw Bot fcc81c254f Support Snapshot artifacts in Ranker bridge 2026-05-27 01:13:29 +02:00
OpenClaw Bot b69d7e5386 Expose bridge handoff status in Ranker results 2026-05-27 01:08:27 +02:00
OpenClaw Bot bf451fad3e Keep hard-railed bridge items out of do-first 2026-05-27 01:01:49 +02:00
OpenClaw Bot b8c518f7cb Preserve source trace for laned build-order strings 2026-05-27 00:58:30 +02:00
OpenClaw Bot 1f8739444c Accept Scattermind experiment sections 2026-05-27 00:55:19 +02:00
OpenClaw Bot 9dbcd7770b Add rank handoff readiness gate 2026-05-27 00:51:03 +02:00
OpenClaw Bot 6cd5c52683 Preserve source trace for lens build orders 2026-05-27 00:46:40 +02:00
OpenClaw Bot 080f35e230 Expose prompt and source trace in rank handoff 2026-05-27 00:43:28 +02:00
OpenClaw Bot 5d85e2028c Preserve Scattermind source excerpts in rank handoff 2026-05-27 00:40:12 +02:00
OpenClaw Bot 29c0b1c244 Treat candidate lane as rank hint 2026-05-27 00:35:36 +02:00
OpenClaw Bot fd0bb3857e Harden pasted Scattermind JSON handoff 2026-05-27 00:31:53 +02:00
OpenClaw Bot c2f61bc959 Preserve Scattermind action signals in Ranker handoff 2026-05-27 00:28:37 +02:00
OpenClaw Bot 428e9c337f Accept laned Scattermind build order objects 2026-05-27 00:24:02 +02:00
OpenClaw Bot 4b3fb9e7d9 Accept pasted Scattermind concept maps 2026-05-27 00:19:33 +02:00
OpenClaw Bot 771b5e7c02 Clarify rank feedback source trace contract 2026-05-27 00:15:17 +02:00
OpenClaw Bot 602937d9b2 Accept messy idea dumps for feedback ranking 2026-05-27 00:12:09 +02:00
OpenClaw Bot 802657b638 Accept paid Scattermind concept map shape 2026-05-27 00:07:54 +02:00
OpenClaw Bot b556c67a4c Merge Scattermind decision context sources 2026-05-27 00:03:49 +02:00
8 changed files with 4227 additions and 174 deletions
+7 -3
View File
@@ -45,11 +45,15 @@ Ranker's continuation job is narrow:
`Snapshot / Concept Map → candidate feature/action set → Rank-ready build order`
`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, or `candidates` either at the top level or under `featureSet`, and it can consume a nested Concept Map directly, so Scattermind can hand off `conceptMap.nextActions / nextMoves` without renaming them into fake software features. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions/constraints/non-goals, and `whatWouldChangeRanking` checks, lane-level `buildOrderDetails` with each item's reason, next step, evidence question, source section, lane source, score, and confidence, plus a `handoff` object (`rank-feedback-result-v1`) with source provenance, item trace rows, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof.
`POST /api/rank-feedback` accepts a `prioritix-feature-set-v1`-style payload from Scattermind and returns ranked items plus `buildOrder.doFirst / validateNext / defer / park`. It accepts candidate arrays as `features`, `actions`, `nextMoves`, `candidates`, `experiments`, `validationTests`, or `proofTests` either at the top level or under `featureSet`, and it can consume nested Snapshot or Concept Map artifacts directly, so Scattermind can hand off `snapshot.nextActions / nextMoves / recommendedActions / suggestedActions` or `conceptMap.nextActions / nextMoves / recommendedActions / suggestedActions` without renaming them into fake software features. Experiment/proof-test arrays default into Validate next candidates, preserving source provenance while keeping them out of the active build-first lane. Sectioned Concept Maps may also include `validateNext`, `deferred`, and `parkingLot`; Ranker combines those sections into one build-order pass while preserving `sourceSection` and treating deferred/parked sections as lane hints. Ranker also accepts an already-laned Build Order object at top level, under `featureSet`, or as `conceptMap.buildOrder` / `conceptMap.buildOrderPreview` / `conceptMap.rankedBuildOrder` (`doFirst` / `validateNext` / `deferred` / `parkingLot`, with aliases like `buildNow`, `testManually`, and `probablyNoise`). String items in those lanes are normalized into candidate actions, while object items can use `move`, `questionToAnswer`, or `evidenceQuestion` aliases. Normalized candidate objects may also use `lane` as a lane hint (`do-first`, `validate-next`, `defer`, `park`) without overwriting `sourceSection`, so Scattermind exports that separate recommendation from provenance remain traceable. Empty wrapper arrays are ignored rather than allowed to shadow a real nested Concept Map action set, and non-empty normalized wrappers are merged with Concept Map validation/deferred/parking sections rather than dropping that context. That keeps partially-normalized Scattermind exports rankable without losing the source lane contract. It also returns a `brief` with source, next-48-hour actions, carried-forward assumptions, source trace, and what would change the ranking, plus `handoff.copyableText`: a plain-text Build Order handoff with Do first / Validate next / Defer / Park, readiness, carried context, source trace, and the explicit rule that only the Do first item is active. This gives Scattermind (or a tired copy/paste user) a stable artifact to save without reconstructing the decision from JSON fields. Lane-level `buildOrderDetails` still carry each item's reason, next step, evidence question, success/kill signals, source section, lane source, score, and confidence, while the `handoff` object (`rank-feedback-result-v1`) carries source provenance, item trace rows, explicit next-step/success/kill signals when Scattermind provides them, copyable text, and contract warnings for missing artifact IDs, source sections, original prompt provenance, or evidence on active items. If Scattermind sends the current paid Concept Map shape as lenses rather than arrays, Ranker can parse `conceptMap.lenses.channel.content` / `buildOrder` labels (`Build first`, `Test manually`, `Defer`, `Probably noise`) into rank-ready candidates and can read guardrails from the risk lens. Keep this contract action-first; do not use it as a reason to add generic dashboard, auth, billing, or workspace layers before the bridge has proof.
Candidate items may include optional 110 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`). Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker turns that structured context into readable scoring text instead of leaking `[object Object]`. If Scattermind only has a flat context string, Ranker now extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable.
Candidate items may include optional 110 `rankerHints` (`value`, `effort`, `confidence`, `urgency`, `revenue`, `novelty`, `risk`) and nested `proofGate` / `proof_gate` / `validationGate` objects with `nextStep`, `evidenceQuestion`, `successCriteria` / `passSignal`, `stopSignal` / `killSignal`, and proof `steps`; Ranker flattens that proof gate into the active slice, first-screen receipt, and handoff contract so Scattermind does not have to duplicate every proof field at candidate top level. Ranker blends those explicit Scattermind hints with text heuristics; `effort` is inverted into feasibility. Hints improve the defended order, but `recommendedLane: "defer"` or `"park"` remains a safety rail so dashboard-swamp items do not get promoted by flashy wording. For clean bridge handoff, Scattermind should send `sourceSection` and `evidenceNeeded` on each active next move. Scattermind can also send `targetAudience`, `constraints`, `assumptions`, and `nonGoals` / `avoid` at the top level, in `featureSet`, inside `snapshot.context` or `conceptMap.context`, or as a structured top-level `context` object with `summary`, `targetAudience`, `constraints`, `nonGoals` / `avoid`, and `assumptions`; Ranker merges these sources rather than letting a shallow wrapper context shadow deeper artifact guardrails. Ranker also accepts structured `decisionContext` / `decision_context`, `rankerContext` / `ranker_context`, `handoffContext` / `handoff_context`, and `bridgeContext` / `bridge_context` objects at the top level, inside bridge envelopes, feature sets, artifacts, Snapshots, or Concept Maps, so Scattermind can keep context in a machine-named contract field without losing guardrails. Lens-only Concept Maps may additionally send audience / constraints / assumptions / risk lens content, and Ranker will split sentence-style lens notes into readable decision context instead of leaking `[object Object]`. If Scattermind only has a flat context string or schema-light structured context summaries (`context.summary`, `snapshot.context.summary`, `conceptMap.context.summary`, etc.), Ranker extracts simple guardrails such as `Solo builder`, `Manual proof`, `Avoid ...`, `No ...`, `Non-goal: ...`, `Not yet ...`, and `Do not ...` into `input.decisionContext` / `handoff.decisionContext` so early bridge exports still protect against dashboard/auth/billing drift. Ranker returns that decision context in `input.decisionContext` and `handoff.decisionContext`, and penalizes candidates that conflict with source non-goals (for example saved workspaces/auth/billing before the continuation proof). If Scattermind sends duplicate candidate IDs, Ranker keeps the first ID, suffixes later duplicates (`preview-2`), and reports the normalization in `handoff.warnings` / `handoff.itemTrace` so downstream build-order references remain addressable. Ranker also accepts Scattermind's paid Concept Map object directly when it arrives with top-level `reference_code`, `working_name`, `ideaText`, and string-valued `lenses.channel` / `lenses.risk` fields; the reference code becomes source provenance, the working name becomes the source title, and labelled Build Order text is turned into rank-ready candidates without requiring Scattermind to wrap it first.
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.
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`; whole-payload wrappers may use `rankPayload` / `rank_payload` or `scattermindPayload` / `scattermind_payload`, and already-laned objects may be named `rankReadyBuildOrder` / `rank_ready_build_order` as a more explicit Scattermind handoff alias. 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. Route-specific paid Scattermind exports can send game/prototype fields directly: `buildDecisions` / `build_decisions` become Do first candidates, `playtestQuestions` / `playtest_questions` become Validate next candidates, `explicitDeferrals` / `explicit_deferrals` become Defer candidates, and `doNotLetThisBecome` / `do_not_let_this_become` becomes Park candidates plus non-goal guardrails. 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, `firstWeekBuildOrder` / `first_week_build_order` objects from Scattermind's paid first-week Build Order language, 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`, `proofPlan` / `proof_plan`, `validationQuestions` / `validation_questions`, `validationPlan` / `validation_plan`, `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.
Recommended payload shape:
+3 -2
View File
@@ -4,11 +4,12 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "npm run check",
"start": "node server.js",
"check": "node --check server.js && node --check scripts/setup-appwrite.mjs && node --check scripts/check-rank-feedback.mjs && node --check public/app.js && node scripts/check-rank-feedback.mjs",
"setup:appwrite": "node scripts/setup-appwrite.mjs",
"smoke": "node scripts/smoke.mjs"
"smoke": "node scripts/smoke.mjs",
"rank-smoke": "node scripts/rank-smoke.mjs"
},
"keywords": [],
"author": "",
+281 -12
View File
@@ -1,14 +1,19 @@
const sample = {
idea: 'Scattermind clarified a messy course idea. Now I need feedback on the feature/functionality order, not a dashboard.',
optionsText: `- Manual build-order preview from one Concept Map
- Copyable decision brief with Do first / Validate next / Defer / Park
- Evidence questions beside each next move
- Accounts and saved workspaces
- Team voting on roadmap priority
- Subscription billing layer
- Polished export for sharing the defended order`,
context: 'Snapshot / Concept Map handoff, solo builder, tired non-AI-native user, avoid auth/workspaces/billing before proof.',
mode: 'mvp',
idea: 'Scattermind clarified a messy freelancer-offer idea. Now I need Prioritix to sort the decision layer: some items are features, some are validation questions, and some are feedback themes.',
optionsText: `Validation questions:
- Will freelancers pay for critique before software?
- Which offer type hurts enough to buy help?
Feedback themes:
- They want templates
- They distrust generic AI copy
Features:
- Manual offer critique
- Pricing calculator
- Proposal generator
- Public mini-page for one offer
- Client dashboard`,
context: 'Solo builder, one weekend, no auth/workspaces/billing before proof. Either validation or features can be useful for the first run.',
mode: 'validation',
};
const laneMeta = {
@@ -51,6 +56,89 @@ function laneClass(lane) {
return `lane-${lane?.id || 'defer'}`;
}
function parsePastedJsonPayload(value) {
const text = String(value || '').trim()
.replace(/^```(?:json)?\s*/i, '')
.replace(/```$/i, '')
.trim();
const jsonText = text.startsWith('{') && text.endsWith('}') ? text : extractFirstJsonObject(text);
if (!jsonText) return null;
try {
const parsed = JSON.parse(jsonText);
const looksLikeBridgePayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed) && (
parsed.schema || parsed.featureSet || parsed.feature_set || parsed.candidateSet || parsed.candidate_set || parsed.candidateFeatureSet || parsed.candidate_feature_set || parsed.rankReadyFeatureSet || parsed.rank_ready_feature_set
|| parsed.snapshot || parsed.conceptMap || parsed.concept_map || parsed.buildOrder || parsed.build_order || parsed.firstWeekBuildOrder || parsed.first_week_build_order || parsed.rankedBuildOrder || parsed.ranked_build_order || parsed.rankReadyBuildOrder || parsed.rank_ready_build_order || parsed.lenses
|| parsed.payload || parsed.rankPayload || parsed.rank_payload || parsed.scattermindPayload || parsed.scattermind_payload || parsed.conceptMapJson || parsed.rankerInput || parsed.ranker_input || parsed.rankerHandoff || parsed.ranker_handoff || parsed.rankerBridge || parsed.ranker_bridge || parsed.rankReady || parsed.rank_ready || parsed.bridge || parsed.bridgePayload || parsed.bridge_payload || parsed.continuation || parsed.continuationPlan || parsed.continuation_plan
|| parsed.concept_map_json || parsed.fullReadingJson || parsed.full_reading_json || parsed.fullReading || parsed.full_reading
|| parsed.glimpseJson || parsed.glimpse_json || parsed.snapshotJson || parsed.snapshot_json || parsed.buildOrderPreview || parsed.build_order_preview || parsed.firstWeekBuildOrder || parsed.first_week_build_order || parsed.rankedBuildOrder || parsed.ranked_build_order || parsed.rankReadyBuildOrder || parsed.rank_ready_build_order
|| parsed.reference_code || parsed.referenceCode || parsed.artifactId || parsed.sourceArtifactId || parsed.source_artifact_id
|| parsed.ideaText || parsed.idea_text || parsed.originalPrompt || parsed.original_prompt || parsed.sourceSummary || parsed.source_summary || parsed.opening_reflection || parsed.restated_idea
|| Array.isArray(parsed.features) || Array.isArray(parsed.actions) || Array.isArray(parsed.candidates)
|| Array.isArray(parsed.candidateActions) || Array.isArray(parsed.candidate_actions) || Array.isArray(parsed.candidateMoves) || Array.isArray(parsed.candidate_moves)
|| Array.isArray(parsed.rankReadyActions) || Array.isArray(parsed.rank_ready_actions)
|| Array.isArray(parsed.recommendedActions) || Array.isArray(parsed.recommended_actions) || Array.isArray(parsed.suggestedActions) || Array.isArray(parsed.suggested_actions)
|| Array.isArray(parsed.nextActions) || Array.isArray(parsed.next_actions) || Array.isArray(parsed.nextMoves) || Array.isArray(parsed.next_moves)
|| Array.isArray(parsed.possibleNextMoves) || Array.isArray(parsed.possible_next_moves) || Array.isArray(parsed.suggestedNextMoves) || Array.isArray(parsed.suggested_next_moves)
|| 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.evidenceQuestions) || Array.isArray(parsed.evidence_questions) || Array.isArray(parsed.proofQuestions) || Array.isArray(parsed.proof_questions) || Array.isArray(parsed.proofPlan) || Array.isArray(parsed.proof_plan) || Array.isArray(parsed.validationQuestions) || Array.isArray(parsed.validation_questions) || Array.isArray(parsed.validationPlan) || Array.isArray(parsed.validation_plan) || 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)
|| Array.isArray(parsed.buildDecisions) || Array.isArray(parsed.build_decisions) || Array.isArray(parsed.explicitDeferrals) || Array.isArray(parsed.explicit_deferrals) || Array.isArray(parsed.playtestQuestions) || Array.isArray(parsed.playtest_questions) || Array.isArray(parsed.doNotLetThisBecome) || Array.isArray(parsed.do_not_let_this_become) || typeof parsed.doNotLetThisBecome === 'string' || typeof parsed.do_not_let_this_become === 'string'
);
return looksLikeBridgePayload ? parsed : null;
} catch {
return null;
}
}
function extractFirstJsonObject(text = '') {
const start = text.indexOf('{');
if (start < 0) return '';
let depth = 0;
let inString = false;
let escaped = false;
for (let index = start; index < text.length; index += 1) {
const char = text[index];
if (escaped) {
escaped = false;
continue;
}
if (char === '\\' && inString) {
escaped = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (inString) continue;
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) return text.slice(start, index + 1);
}
}
return '';
}
function payloadFromForm(formPayload) {
const ideaJson = parsePastedJsonPayload(formPayload.idea);
const optionsJson = parsePastedJsonPayload(formPayload.optionsText);
const embedded = ideaJson || optionsJson;
if (!embedded) return formPayload;
const unwrapCandidate = embedded.payload || embedded.rankPayload || embedded.rank_payload || embedded.scattermindPayload || embedded.scattermind_payload || embedded.rankerBridge || embedded.ranker_bridge || embedded.continuation || embedded.continuationPlan || embedded.continuation_plan || embedded;
const unwrapped = unwrapCandidate && typeof unwrapCandidate === 'object' && !Array.isArray(unwrapCandidate) ? unwrapCandidate : embedded;
const merged = { ...unwrapped };
if (!merged.mode && formPayload.mode) merged.mode = formPayload.mode;
if (String(formPayload.context || '').trim() && !merged.context) merged.context = formPayload.context;
return merged;
}
function renderMetrics(metrics = {}) {
const items = [
['Value', metrics.value],
@@ -69,6 +157,154 @@ function renderPills(items = []) {
return `<div class="signal-pills">${items.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div>`;
}
function renderSourceTrace(sourceTrace = {}) {
const hasTrace = sourceTrace.sourceSection || sourceTrace.sourceId || sourceTrace.sourceQuote;
if (!hasTrace) return '';
const label = [sourceTrace.sourceTitle, sourceTrace.sourceId || sourceTrace.sourceSection].filter(Boolean).join(' · ');
return `
<div class="source-trace">
<span>Source trace</span>
${label ? `<b>${escapeHtml(label)}</b>` : ''}
${sourceTrace.sourceQuote ? `<p>${escapeHtml(sourceTrace.sourceQuote)}</p>` : ''}
</div>
`;
}
function renderItemSourceTrace(item = {}) {
const provenance = item.provenance || {};
const trace = {
sourceSection: provenance.sourceSection || '',
sourceId: provenance.sourceId || '',
sourceTitle: provenance.sourceTitle || '',
sourceQuote: provenance.sourceQuote || '',
};
const hasTrace = trace.sourceSection || trace.sourceId || trace.sourceQuote;
if (!hasTrace) return '';
const label = [trace.sourceTitle, trace.sourceId || trace.sourceSection].filter(Boolean).join(' · ');
return `
<details class="item-source-trace">
<summary>Source trace${label ? ` · ${escapeHtml(label)}` : ''}</summary>
${trace.sourceQuote ? `<p>${escapeHtml(trace.sourceQuote)}</p>` : ''}
${trace.sourceSection ? `<small>Section: ${escapeHtml(trace.sourceSection)}</small>` : ''}
</details>
`;
}
function readinessTone(status = '') {
if (status === 'ready') return 'ready';
if (status === 'usable-with-warnings') return 'warn';
return status ? 'blocked' : '';
}
function renderHandoffStatus(handoff = {}) {
const readiness = handoff.readiness || {};
if (!readiness.status) return '';
const warnings = handoff.warnings || [];
return `
<article class="brief-card handoff-card status-${escapeHtml(readiness.status)}">
<span>Bridge handoff readiness</span>
<p><b>${escapeHtml(readiness.label || readiness.status)}</b> — ${escapeHtml(readiness.summary || '')}</p>
${(readiness.nextChecks || []).length ? `<ol>${readiness.nextChecks.map((step) => `<li>${escapeHtml(step)}</li>`).join('')}</ol>` : ''}
${warnings.length ? `<div class="handoff-warnings">${warnings.map((warning) => `<code>${escapeHtml(warning)}</code>`).join('')}</div>` : ''}
</article>
`;
}
function renderBridgeHandoffStrip(data = {}) {
const handoff = data.handoff || {};
const readiness = handoff.readiness || {};
if (!readiness.status) return '';
const source = handoff.source || data.brief?.source || data.input?.provenance || {};
const activeSlice = handoff.activeSlice || {};
const activeItem = activeSlice.item || {};
const sourceBits = [source.snapshotTitle, source.artifactId].filter(Boolean).join(' · ');
const tone = readinessTone(readiness.status);
return `
<section class="handoff-strip status-${escapeHtml(tone)}" aria-label="Bridge handoff readiness">
<div>
<span>Bridge readiness</span>
<strong>${escapeHtml(readiness.label || readiness.status)}</strong>
<p>${escapeHtml(readiness.summary || 'Read the warnings before handing this back to Scattermind.')}</p>
</div>
<div>
<span>Active slice</span>
<strong>${escapeHtml(activeItem.title || data.brief?.decisionReceipt?.activeMove || 'No active move')}</strong>
<p>${escapeHtml(activeSlice.rule || data.brief?.decisionReceipt?.handoffRule || 'Only the Do first item is active.')}</p>
</div>
<div>
<span>Source</span>
<strong>${escapeHtml(sourceBits || 'No artifact attached')}</strong>
<p>${escapeHtml(source.originalPromptExcerpt || source.sourceSummaryExcerpt || 'Add source context before treating this as a durable bridge handoff.')}</p>
</div>
${(readiness.nextChecks || []).length ? `<small>Next check: ${escapeHtml(readiness.nextChecks[0])}</small>` : ''}
</section>
`;
}
function renderFirstScreen(firstScreen = {}) {
if (!firstScreen.headline) return '';
const held = firstScreen.holdBack || [];
const guardrails = firstScreen.guardrails || [];
return `
<section class="active-slice-strip" aria-label="Active build slice">
<div class="active-slice-main">
<span>One thing to do now</span>
<h3>${escapeHtml(firstScreen.headline)}</h3>
<p>${escapeHtml(firstScreen.primaryAction || 'Run the smallest manual proof before product machinery.')}</p>
</div>
<div>
<span>Proof question</span>
<p>${escapeHtml(firstScreen.proofQuestion || 'What evidence would change this order?')}</p>
</div>
<div>
<span>Why this wins</span>
<p>${escapeHtml(firstScreen.why || 'It is the best first proof slice.')}</p>
</div>
<div class="proof-gate">
<span>Pass / stop signals</span>
<p><b>Pass:</b> ${escapeHtml(firstScreen.passSignal || 'A real user can name why this should be first.')}</p>
<p><b>Stop:</b> ${escapeHtml(firstScreen.stopSignal || 'The proof creates no clear action, request, or value signal.')}</p>
<small>${escapeHtml(firstScreen.proofCadence || 'Run one tiny proof cycle, then rerank.')}</small>
</div>
${held.length ? `<div><span>Hold back</span><ul>${held.map((item) => `<li><b>${escapeHtml(item.title)}</b>${item.lane ? `${escapeHtml(item.lane)}` : ''}</li>`).join('')}</ul></div>` : ''}
${firstScreen.proofScript ? `<blockquote class="active-proof-script"><span>Say this to test it</span>${escapeHtml(firstScreen.proofScript)}</blockquote>` : ''}
${firstScreen.sourceQuote ? `<blockquote class="active-source-quote"><span>${escapeHtml(firstScreen.sourceTitle || 'Source quote')}</span>${escapeHtml(firstScreen.sourceQuote)}</blockquote>` : ''}
${guardrails.length ? `<small>Guardrails: ${guardrails.map(escapeHtml).join(' · ')}</small>` : ''}
${firstScreen.sourceAnchor ? `<small>Source anchor: ${escapeHtml(firstScreen.sourceAnchor)}</small>` : ''}
<small>${escapeHtml(firstScreen.rule || 'One active move. Everything else waits.')}</small>
</section>
`;
}
function renderDecisionReceipt(receipt = {}) {
if (!receipt.activeMove) return '';
const held = receipt.doNotStartYet || [];
return `
<section class="decision-receipt" aria-label="Decision receipt">
<div>
<span>Active move</span>
<strong>${escapeHtml(receipt.activeMove)}</strong>
</div>
<div>
<span>First proof step</span>
<p>${escapeHtml(receipt.firstProofStep || 'Run the smallest manual proof before product machinery.')}</p>
</div>
<div>
<span>Evidence question</span>
<p>${escapeHtml(receipt.evidenceQuestion || 'What would make this ranking obviously right or wrong?')}</p>
</div>
<div>
<span>Proof gate</span>
<p><b>Pass:</b> ${escapeHtml(receipt.passSignal || 'Evidence makes the active move obviously worth keeping first.')}</p>
<p><b>Stop:</b> ${escapeHtml(receipt.stopSignal || 'Evidence does not create a clear next action.')}</p>
</div>
${held.length ? `<div><span>Do not start yet</span><ul>${held.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>` : ''}
${receipt.sourceAnchor ? `<small>Source anchor: ${escapeHtml(receipt.sourceAnchor)}</small>` : ''}
<small>${escapeHtml(receipt.handoffRule || 'Only the Do first item is active.')}</small>
</section>
`;
}
function renderRankCard(item) {
return `
<article class="rank-card ${laneClass(item.lane)}">
@@ -87,6 +323,7 @@ function renderRankCard(item) {
<div><span>Evidence question</span><p>${escapeHtml(item.evidenceQuestion || 'What proof would change this ranking?')}</p></div>
<div><span>Success / kill signal</span><p>${escapeHtml(item.successSignal || '')} ${escapeHtml(item.killSignal ? `Kill if: ${item.killSignal}` : '')}</p></div>
</div>
${renderItemSourceTrace(item)}
${renderMetrics(item.metrics)}
${renderPills(item.scoringNotes || [])}
</article>
@@ -110,10 +347,31 @@ function renderLane(ranked, laneId) {
`;
}
function sourceCitation(data) {
const brief = data.brief || {};
const source = brief.source || data.handoff?.source || data.input?.provenance || {};
const trace = brief.quickGlance?.sourceTrace || {};
const parts = [
source.artifactId ? `Artifact: ${source.artifactId}` : '',
source.conceptMapId ? `Concept Map: ${source.conceptMapId}` : '',
source.snapshotTitle ? `Title: ${source.snapshotTitle}` : '',
trace.sourceSection ? `Source section: ${trace.sourceSection}` : '',
trace.sourceId ? `Source item: ${trace.sourceId}` : '',
trace.sourceTitle ? `Source title: ${trace.sourceTitle}` : '',
trace.sourceQuote ? `Source quote: ${trace.sourceQuote}` : '',
source.originalPromptExcerpt ? `Original prompt: ${source.originalPromptExcerpt}` : '',
source.sourceSummaryExcerpt ? `Source summary: ${source.sourceSummaryExcerpt}` : '',
].filter(Boolean);
return parts.length ? parts.join('\n') : 'No source citation was carried into this result.';
}
function markdownBrief(data) {
if (data.handoff?.copyableText) return data.handoff.copyableText;
const brief = data.brief || {};
const glance = brief.quickGlance || {};
const ranked = data.ranked || [];
const citation = sourceCitation(data);
const hasCitation = !citation.startsWith('No source citation');
const lanes = ['do', 'test', 'defer', 'park'];
const laneTitles = { do: 'Do first', test: 'Validate next', defer: 'Defer', park: 'Park / cut' };
return [
@@ -128,6 +386,7 @@ function markdownBrief(data) {
`- Evidence question: ${glance.evidenceQuestion || 'n/a'}`,
`- Biggest trap: ${glance.biggestTrap || 'n/a'}`,
`- Confidence: ${data.rankConfidence?.level || 'n/a'}${data.rankConfidence?.reason || ''}`,
...(hasCitation ? ['', '## Source citation', citation] : []),
'',
...lanes.flatMap((lane) => {
const items = ranked.filter((item) => item.lane?.id === lane);
@@ -150,7 +409,9 @@ async function copyText(text, label) {
function attachResultActions(data) {
document.querySelector('#copyBrief')?.addEventListener('click', () => copyText(markdownBrief(data), 'Decision brief'));
document.querySelector('#copyActions')?.addEventListener('click', () => copyText((data.brief?.next48Hours || []).map((step, index) => `${index + 1}. ${step}`).join('\n'), '48h actions'));
document.querySelector('#copyProofScript')?.addEventListener('click', () => copyText(data.brief?.firstScreen?.proofScript || data.handoff?.activeSlice?.proof?.proofScript || '', 'Proof script'));
document.querySelector('#copyJson')?.addEventListener('click', () => copyText(JSON.stringify({ brief: data.brief, ranked: data.ranked, buildOrder: data.buildOrderDetails, handoff: data.handoff }, null, 2), 'JSON handoff'));
document.querySelector('#copySource')?.addEventListener('click', () => copyText(sourceCitation(data), 'Source citation'));
}
function renderResults(data) {
@@ -164,20 +425,26 @@ function renderResults(data) {
<p class="eyebrow">${escapeHtml(data.mode?.label || 'Ranked feedback')} · first-pass judgement memo</p>
<h2>${escapeHtml(brief.headline || 'Ranked feedback map')}</h2>
<p>${escapeHtml(brief.summary || '')}</p>
<div class="memo-stamps"><span>Not an oracle</span><span>${escapeHtml(data.rankConfidence?.level || 'first pass')} confidence</span><span>${ranked.length} items judged</span></div>
<div class="memo-stamps"><span>Not an oracle</span><span>${escapeHtml(data.rankConfidence?.level || 'first pass')} confidence</span><span>${ranked.length} items judged</span><span>${escapeHtml(data.handoff?.readiness?.label || data.handoff?.readiness?.status || 'handoff unchecked')}</span></div>
</div>
${renderBridgeHandoffStrip(data)}
<section class="quick-glance" aria-label="Quick judgement">
<div><span>Do this first</span><strong>${escapeHtml(glance.topPick || 'Add options')}</strong></div>
<div><span>Why it wins</span><p>${escapeHtml(glance.whyThisWins || 'The list needs more comparable options.')}</p></div>
<div><span>Proof to collect</span><p>${escapeHtml(glance.evidenceQuestion || 'Name the evidence that would change the ranking.')}</p></div>
<div><span>Trap to avoid</span><p>${escapeHtml(glance.biggestTrap || brief.caution || 'Do not treat first-pass judgement as final truth.')}</p></div>
</section>
${renderFirstScreen(brief.firstScreen || {})}
${renderDecisionReceipt(brief.decisionReceipt || {})}
${renderSourceTrace(glance.sourceTrace || {})}
<div class="result-actions" aria-label="Copy result actions">
<button class="button ghost" type="button" id="copyBrief">Copy decision brief</button>
<button class="button ghost" type="button" id="copyActions">Copy 48h actions</button>
<button class="button ghost" type="button" id="copyProofScript">Copy proof script</button>
<button class="button ghost" type="button" id="copyJson">Copy JSON handoff</button>
<button class="button ghost" type="button" id="copySource">Copy source citation</button>
</div>
<div class="lane-board">
@@ -193,6 +460,7 @@ function renderResults(data) {
<span>Rank confidence</span>
<p><b>${escapeHtml(data.rankConfidence?.level || 'First pass')}</b> — ${escapeHtml(data.rankConfidence?.reason || brief.caution || '')}</p>
</article>
${renderHandoffStatus(data.handoff || {})}
${(brief.whatWouldChangeRanking || []).length ? `
<article class="brief-card next-card">
<span>What would change the order</span>
@@ -227,7 +495,8 @@ function renderResults(data) {
async function createFeedbackMap(event) {
event.preventDefault();
const submit = form.querySelector('button[type="submit"]');
const payload = Object.fromEntries(new FormData(form).entries());
const payload = payloadFromForm(Object.fromEntries(new FormData(form).entries()));
if (!String(payload.optionsText || '').trim() && !payload.conceptMap && !payload.featureSet && !payload.lenses) payload.optionsText = payload.idea;
submit.disabled = true;
submit.textContent = 'Judging…';
try {
+28 -23
View File
@@ -4,9 +4,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f3eee4" />
<meta name="rank-version" content="2.2.0-feedback-front-door" />
<title>Ranker — feedback front door for messy decisions</title>
<link rel="stylesheet" href="/styles.css?v=2.2.0-feedback-front-door" />
<meta name="rank-version" content="2.3.0-prioritix" />
<title>Ranker / Prioritix — sort the messy layer inside an idea</title>
<link rel="stylesheet" href="/styles.css?v=2.3.0-prioritix" />
</head>
<body>
<main class="page-shell">
@@ -22,11 +22,11 @@
<div class="hero-grid" id="top">
<div class="hero-copy">
<p class="eyebrow">Feedback intake for ideas, features, offers, and next moves</p>
<h1 id="hero-title">Submit the mess. Get a defended first move.</h1>
<p class="lede">Ranker is the front door to useful feedback: paste an idea plus the features or possible moves around it, choose the judgement lens, and get a decision brief with reasons, risks, expert reflections, and next steps.</p>
<p class="eyebrow">Prioritix for ideas: features, validation, feedback, risks, audiences, and next moves</p>
<h1 id="hero-title">Sort the messy layer inside an idea.</h1>
<p class="lede">Ranker / Prioritix takes the piece of an idea that needs ordering — features, functionality, validation questions, feedback themes, risks, experiments, or audiences — and returns a defended next-step map.</p>
<div class="hero-actions">
<a class="button primary" href="#try">Rank a messy list</a>
<a class="button primary" href="#try">Sort an idea layer</a>
<button class="button ghost" type="button" id="loadSampleTop">Load sample</button>
</div>
<div class="promise-row" aria-label="What Ranker returns">
@@ -57,26 +57,31 @@
<section class="decision-tool" id="try" aria-labelledby="try-title">
<div class="tool-intro">
<p class="eyebrow">MVP · feedback front door · no account · no dashboard swamp</p>
<h2 id="try-title">Send a decision brief to the room</h2>
<p>Describe what you want feedback on, then list the possible features, directions, offers, or next steps. Ranker gives you the quick judgement first, then the deeper reflections.</p>
<p class="eyebrow">MVP · idea-layer sorting · no account · no dashboard swamp</p>
<h2 id="try-title">Send the messy idea layer to Prioritix</h2>
<p>Paste the rough idea plus the layer you need sorted: features, functions, validation questions, feedback themes, risks, audiences, experiments, possible next moves, or a raw Scattermind Concept Map JSON export. Ranker will extract sortable items when it can; use the optional box only if you already have a cleaner list.</p>
</div>
<form class="rank-form" id="rankForm">
<label>
<span>What do you want feedback on?</span>
<textarea name="idea" rows="4" placeholder="Example: Im building a tool that helps freelancers package their services and decide what to sell first."></textarea>
<span>Paste the idea + messy layer / Concept Map JSON <b>required</b></span>
<textarea name="idea" rows="8" required placeholder="Example: Im building a tool that helps freelancers package their services and decide what to sell first. Maybe offer critique, pricing calculator, proposal generator, landing page copywriter, client persona mapper, and some kind of dashboard later? I only have a week and want the fastest useful proof.
Or paste a Scattermind Concept Map JSON object here; Ranker will preserve source provenance and extract the build order."></textarea>
</label>
<label>
<span>Possible moves / features / functionality <b>required</b></span>
<textarea name="optionsText" rows="9" required placeholder="One per line. Bullets, rambling, half-thoughts are fine.
<span>Sortable items, if you want to separate them <em>optional</em></span>
<textarea name="optionsText" rows="7" placeholder="Optional. One per line is easiest, but bullets and half-thoughts are fine.
Validation questions:
- Will freelancers pay for critique before software?
- Which offer type hurts enough to buy help?
Feedback themes:
- They want templates
- They distrust generic AI copy
Features:
- Offer critique
- Pricing calculator
- Proposal generator
- Client persona mapper
- Landing page copywriter
- Client dashboard"></textarea>
- Pricing calculator"></textarea>
</label>
<fieldset class="mode-picker">
@@ -96,7 +101,7 @@
</label>
<div class="form-actions">
<button class="button primary" type="submit">Create ranked feedback map</button>
<button class="button primary" type="submit">Create Prioritix map</button>
<button class="button ghost" type="button" id="loadSample">Use sample</button>
</div>
</form>
@@ -119,12 +124,12 @@
<section class="why-section" id="why" aria-labelledby="why-title">
<p class="eyebrow">Positioning</p>
<h2 id="why-title">Scattermind clarifies one idea. Ranker judges the possible moves.</h2>
<p>That makes Ranker broader: scattered people get relief, structured people get a second opinion, and builders get a defensible build order before they waste time on the wrong piece.</p>
<h2 id="why-title">Scattermind evaluates one idea. Ranker / Prioritix sorts the layer that needs order.</h2>
<p>Sometimes that layer is features. Sometimes it is validation questions, feedback, risks, audiences, claims, experiments, or next moves. The point is not “features first”; the point is a defensible order before more development.</p>
</section>
</main>
<div class="toast" id="toast" hidden></div>
<script src="/app.js?v=2.2.0-feedback-front-door" type="module"></script>
<script src="/app.js?v=2.3.0-prioritix" type="module"></script>
</body>
</html>
+9
View File
@@ -39,3 +39,12 @@ 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}}
.decision-receipt{display:grid;grid-template-columns:1.1fr 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)}}
.active-slice-strip{display:grid;grid-template-columns:1.25fr 1fr 1fr 1fr 1fr;gap:10px;margin:-6px 0 22px;padding:12px;border:3px solid var(--ink);background:#15130f;color:#fff;box-shadow:8px 8px 0 var(--blue)}
.handoff-strip{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin:22px 0 -6px;padding:12px;border:3px solid var(--ink);background:#fffaf1;box-shadow:7px 7px 0 rgba(21,19,15,.20)}.handoff-strip.status-ready{box-shadow:7px 7px 0 var(--green)}.handoff-strip.status-warn{box-shadow:7px 7px 0 var(--amber)}.handoff-strip.status-blocked{box-shadow:7px 7px 0 var(--red)}.handoff-strip>div{border:1.5px solid var(--hair);background:#fff6e5;padding:13px}.handoff-strip span{display:block;margin-bottom:7px;color:var(--blue2);text-transform:uppercase;letter-spacing:.12em;font-size:10px;font-weight:1000}.handoff-strip strong{display:block;margin-bottom:7px;font-size:clamp(18px,2vw,26px);line-height:1;letter-spacing:-.04em}.handoff-strip p{margin:0;color:var(--ink2);line-height:1.35}.handoff-strip small{grid-column:1/-1;color:var(--muted);font-weight:850}
.active-slice-strip>div{border:1.5px solid rgba(255,255,255,.32);background:linear-gradient(145deg,rgba(255,255,255,.10),rgba(255,255,255,.03));padding:13px}
.active-slice-strip span{display:block;margin-bottom:7px;color:#bfcaff;text-transform:uppercase;letter-spacing:.12em;font-size:10px;font-weight:1000}.active-slice-strip h3{margin:0 0 8px;font-size:clamp(24px,3vw,42px);line-height:.9;letter-spacing:-.06em}.active-slice-strip p{margin:0;color:#f7efe1;line-height:1.38}.active-slice-strip ul{margin:0;padding-left:18px;color:#f7efe1}.active-slice-strip small{grid-column:1/-1;color:#d9ddff;font-weight:850}.active-proof-script,.active-source-quote{grid-column:1/-1;margin:0;padding:12px 14px;border:1.5px dashed rgba(255,255,255,.42);background:rgba(255,255,255,.06);color:#f7efe1;line-height:1.42}.active-proof-script{border-style:solid;background:rgba(36,92,255,.18);font-size:clamp(15px,1.45vw,18px)}.active-proof-script span,.active-source-quote span{color:#d9ddff}.active-slice-main{background:linear-gradient(145deg,rgba(36,92,255,.28),rgba(255,255,255,.04))!important}
@media (max-width:1100px){.active-slice-strip,.handoff-strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:700px){.active-slice-strip,.handoff-strip{grid-template-columns:1fr;box-shadow:5px 5px 0 var(--blue)}}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
["import { strict as assert } from 'node:assert'; const BASE_URL = process.env.RANK_BASE_URL || `http://127.0.0.1:${process.env.PORT || 3045}`; const headers = { 'Content-Type': 'application/json' }; if (process.env.RANK_AGENT_TOKEN) headers.Authorization = `Bearer ${process.env.RANK_AGENT_TOKEN}`; async function req(path, options = {}) { const res = await fetch(BASE_URL + path, { headers: { ...headers, ...(options.headers || {}) }, ...options, body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body, }); const text = await res.text(); const data = text ? JSON.parse(text) : null; if (!res.ok) throw new Error(res.status + ' ' + path + ': ' + (data?.error || text)); return data; } (async () => { const health = await req('/api/health'); assert.equal(health.ok, true, 'health ok'); const payload = { idea: 'Build a simple feedback map for early idea triage.', context: 'Target audience: indie builders. Constraints: no auth, no workspace, no billing.', features: [ { title: 'Add rank-ready build order export', description: 'Export the ranked list as JSON.' }, { title: 'Add lane boost controls', description: 'Adjust lane boost weights per session.' }, { title: 'Add decision brief vignettes', description: 'Show a quick glance card for tired non-AI-native users.' }, { title: 'Add provenance trace', description: 'Capture source section and original prompt.' }, { title: 'Add non-goal guardrails', description: 'Penalize options conflicting with source non-goals.' }, ], mode: 'progress', }; const rank = await req('/api/rank-feedback', { method: 'POST', body: payload }); assert.ok(rank.ok, 'rank.ok true'); assert.ok(Array.isArray(rank.ranked), 'ranked array present'); assert.ok(rank.brief, 'brief present'); assert.ok(rank.brief.quickGlance, 'brief.quickGlance present'); assert.ok(rank.handoff, 'handoff contract present'); assert.equal(rank.handoff.schema, 'rank-feedback-result-v1', 'handoff schema'); assert.ok(['ready','usable-with-warnings','needs-source-context','blocked'].includes(rank.handoff.readiness.status), 'readiness.status valid'); assert.ok(Array.isArray(rank.buildOrder.doFirst), 'buildOrder.doFirst array'); assert.ok(Array.isArray(rank.buildOrder.validateNext), 'buildOrder.validateNext array'); assert.ok(Array.isArray(rank.buildOrder.defer), 'buildOrder.defer array'); assert.ok(Array.isArray(rank.buildOrder.park), 'buildOrder.park array'); assert.ok(rank.buildOrder.doFirst.length >= 1, 'at least one Do first'); assert.ok(rank.buildOrder.validateNext.length >= 1, 'at least one Validate next'); const rank2 = await req('/api/rank-feedback', { method: 'POST', body: payload }); assert.equal(rank.ranked[0].score, rank2.ranked[0].score, 'first score deterministic'); assert.deepEqual(rank.ranked.map(r => r.id), rank2.ranked.map(r => r.id), 'order deterministic'); const parkPayload = { ...payload, features: [ { title: 'Park this idea explicitly', description: 'This should remain in park even if scoring likes it.', recommendedLane: 'park' }, { title: 'Normal feature that should be do first', description: 'Low effort, high value, clear evidence needed.' }, ], }; const parkRank = await req('/api/rank-feedback', { method: 'POST', body: parkPayload }); const parkItem = parkRank.ranked.find(r => r.title === 'Park this idea explicitly'); assert.ok(parkItem, 'park item exists'); assert.equal(parkItem.lane.id, 'park', 'explicit park hint respected: lane=park'); const nonGoalPayload = { ...payload, context: payload.context + '\nConstraints: no dashboards, no auth, no workspace.', features: [ { title: 'Build a dashboard', description: 'Full workspace with auth and billing.' }, { title: 'Add simple CLI', description: 'Export decisions to a file.' }, ], }; const nonGoalRank = await req('/api/rank-feedback', { method: 'POST', body: nonGoalPayload }); const dashboardItem = nonGoalRank.ranked.find(r => r.title === 'Build a dashboard'); assert.ok(dashboardItem, 'dashboard item exists'); assert.notEqual(dashboardItem.lane.id, 'do', 'non-goal conflict prevents do lane'); console.log(JSON.stringify({ ok: true, health: { ok: health.ok, version: health.version }, rank: { firstScore: rank.ranked[0].score, doFirstCount: rank.buildOrder.doFirst.length, validateCount: rank.buildOrder.validateNext.length, readiness: rank.handoff.readiness.status }, guardrails: { parkHint: parkItem.lane.id, nonGoal: dashboardItem.lane.id } }, null, 2)); })();"]
+1530 -133
View File
File diff suppressed because it is too large Load Diff