Compare commits
86 Commits
25c7c08543
...
a348acf6ef
| Author | SHA1 | Date | |
|---|---|---|---|
| a348acf6ef | |||
| 577fd081d6 | |||
| 76d62d2862 | |||
| 4212b4d7c8 | |||
| 066717221c | |||
| 6a2cc34759 | |||
| 896198fe07 | |||
| f424b61730 | |||
| 074892b7e3 | |||
| 0b7ca6d009 | |||
| 70dfbf6817 | |||
| 67c42d5ab6 | |||
| 5fad144a8f | |||
| 9c0f4bebd3 | |||
| aae093904c | |||
| 4f0553c26d | |||
| 18c85b21dc | |||
| 74b6ad2d85 | |||
| 2fa061120c | |||
| 77b5395962 | |||
| ce3885d406 | |||
| a66788e394 | |||
| fe07245710 | |||
| f44d5c3cc3 | |||
| 0447e89ca6 | |||
| 9f712a3b93 | |||
| 46846cada7 | |||
| 9c35af237f | |||
| 85e2a83d3b | |||
| fae434391b | |||
| 13622de5a0 | |||
| 90beb50459 | |||
| c26bd4bfb0 | |||
| dc75206fcd | |||
| 4e1b36d7b4 | |||
| bff9b1e7c3 | |||
| 39287ea2e3 | |||
| b2744a791b | |||
| 7ed035af82 | |||
| e463f4bc2a | |||
| ec27d13330 | |||
| c3edf9f29d | |||
| 759c1b2fe4 | |||
| 85c8067185 | |||
| 0271bfcbf6 | |||
| 8bea868fb2 | |||
| 2459d253e1 | |||
| 6a34916697 | |||
| a225586296 | |||
| ca186f2a01 | |||
| 421913dc2c | |||
| 460f088a8b | |||
| 92a086824c | |||
| 55e75bc793 | |||
| 10084afb96 | |||
| f7d459a629 | |||
| e3cff7266c | |||
| 1a829e05af | |||
| 5cd5dd2fcf | |||
| 470965b8b7 | |||
| e22cb30061 | |||
| 8e6040e90c | |||
| 3ff7272720 | |||
| 578f954020 | |||
| b5e791ae33 | |||
| bf29b7ab95 | |||
| 1c4897694c | |||
| 242fe235a5 | |||
| fcc81c254f | |||
| b69d7e5386 | |||
| bf451fad3e | |||
| b8c518f7cb | |||
| 1f8739444c | |||
| 9dbcd7770b | |||
| 6cd5c52683 | |||
| 080f35e230 | |||
| 5d85e2028c | |||
| 29c0b1c244 | |||
| fd0bb3857e | |||
| c2f61bc959 | |||
| 428e9c337f | |||
| 4b3fb9e7d9 | |||
| 771b5e7c02 | |||
| 602937d9b2 | |||
| 802657b638 | |||
| b556c67a4c |
@@ -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 1–10 `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 1–10 `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
@@ -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
@@ -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
@@ -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: I’m 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: I’m 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>
|
||||
|
||||
@@ -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)}}
|
||||
|
||||
+2368
-1
File diff suppressed because it is too large
Load Diff
@@ -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)); })();"]
|
||||
Reference in New Issue
Block a user