Refocus Ranker as Prioritix idea-layer sorting
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
"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",
|
||||
|
||||
+15
-10
@@ -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 = {
|
||||
|
||||
+24
-21
@@ -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.3-proof-script" />
|
||||
<title>Ranker — feedback front door for messy decisions</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=2.2.3-proof-script" />
|
||||
<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 the messy backlog as-is, 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,28 +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>Paste the backlog, rough notes, feature dump, possible next moves, or a raw Scattermind Concept Map JSON export. Ranker will extract candidates when it can; use the optional candidate box only if you already have a cleaner list.</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>Paste the messy backlog / idea dump / Concept Map JSON <b>required</b></span>
|
||||
<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>Candidate moves, if you want to separate them <em>optional</em></span>
|
||||
<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">
|
||||
@@ -98,7 +101,7 @@ Or paste a Scattermind Concept Map JSON object here; Ranker will preserve source
|
||||
</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>
|
||||
@@ -121,12 +124,12 @@ Or paste a Scattermind Concept Map JSON object here; Ranker will preserve source
|
||||
|
||||
<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.3-proof-script" type="module"></script>
|
||||
<script src="/app.js?v=2.3.0-prioritix" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -159,6 +159,42 @@ try {
|
||||
assert.equal(messyIdeaOnly.handoff.readiness.status, 'usable-with-warnings');
|
||||
assert.ok(messyIdeaOnly.handoff.readiness.nextChecks.some(item => /source artifact id if this came from Scattermind/i.test(item)));
|
||||
|
||||
const prioritixLayerResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
idea: 'A simple product for freelancers to package tiny fixed-price offers.',
|
||||
optionsText: `Idea: A simple product for freelancers to package tiny fixed-price offers.
|
||||
Validation questions:
|
||||
A) Will freelancers pay for critique before software?
|
||||
B) Which offer type hurts enough to buy help?
|
||||
Feedback themes:
|
||||
- They want templates
|
||||
- They distrust generic AI copy
|
||||
Features:
|
||||
1. Offer builder with 5 questions
|
||||
2. Public mini-page for one offer
|
||||
3. Stripe payment link helper
|
||||
Constraints:
|
||||
I only have one weekend and no audience.
|
||||
Instruction:
|
||||
Sort the idea layer, not my constraints.`,
|
||||
context: 'Solo builder. Either validation questions, feedback, or features may be useful for the first Prioritix run.',
|
||||
mode: 'validation',
|
||||
}),
|
||||
});
|
||||
assert.equal(prioritixLayerResponse.status, 200);
|
||||
const prioritixLayer = await prioritixLayerResponse.json();
|
||||
const prioritixTitles = prioritixLayer.ranked.map(item => item.title);
|
||||
assert.equal(prioritixLayer.input.optionCount, 7, 'Prioritix should extract mixed idea-layer items and ignore context/instruction sections');
|
||||
assert.ok(prioritixTitles.some(title => /Will freelancers pay for critique before software/i.test(title)));
|
||||
assert.ok(prioritixTitles.some(title => /Which offer type hurts enough to buy help/i.test(title)));
|
||||
assert.ok(prioritixTitles.some(title => /They want templates/i.test(title)));
|
||||
assert.ok(prioritixTitles.some(title => /Offer builder with 5 questions/i.test(title)));
|
||||
assert.ok(!prioritixTitles.some(title => /I only have one weekend|Sort the idea layer|Instruction|Constraints/i.test(title)), 'constraints/instructions must not become ranked items');
|
||||
assert.ok(!prioritixTitles.includes('A'));
|
||||
assert.ok(!prioritixTitles.includes('B'));
|
||||
|
||||
const hardRailResponse = await fetch(`${base}/api/rank-feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -448,19 +448,70 @@ function hits(text, words) {
|
||||
return words.reduce((count, word) => count + (lower.includes(word) ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
const sortableTextHeadings = /^(features?|functionality|functions?|options?|variants?|claims?|promises?|risks?|assumptions?|experiments?|tests?|proof steps?|validation questions?|questions?|feedback themes?|feedback|audiences?|segments?|use cases?|possible next moves?|next moves?|moves?|candidates?|things to sort|sortable layer|xyz)\s*:\s*$/i;
|
||||
const contextTextHeadings = /^(idea|context|constraints?|instructions?|instruction|notes?|non[- ]?goals?|avoid|do not sort|don't sort|dont sort|background|goal|goals?)\s*:\s*$/i;
|
||||
const inlineSortablePrefix = /^(features?|functionality|functions?|options?|variants?|claims?|promises?|risks?|assumptions?|experiments?|tests?|proof steps?|validation questions?|questions?|feedback themes?|feedback|audiences?|segments?|use cases?|possible next moves?|next moves?|moves?|candidates?|things to sort|sortable layer|xyz)\s*:\s*/i;
|
||||
|
||||
function normalizeSortableTextLine(line = '') {
|
||||
return cleanText(line, 900)
|
||||
.replace(/^\s*(?:[-*•]|\d+[.)]|[a-z][.)])\s*/i, '')
|
||||
.replace(/^\s*(?:rough\s+idea|idea|option|feature|function|variant|claim|risk|experiment|test|question|feedback|audience|segment|move)\s+[a-z0-9]+\s*[:.)-]\s*/i, '')
|
||||
.replace(/^\s*(maybe|possibly|also|plus|and|or|then|later|eventually|build|add|include|some kind of|kind of)\b\s*/i, '')
|
||||
.replace(/\?+$/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function splitOptionTitleDescription(line = '') {
|
||||
const normalized = normalizeSortableTextLine(line);
|
||||
const [rawTitle, ...rest] = normalized.split(/\s[-–—]\s/);
|
||||
return {
|
||||
title: cleanText(rawTitle || normalized || line, 140),
|
||||
description: cleanText(rest.join(' — '), 420),
|
||||
};
|
||||
}
|
||||
|
||||
function parseOptionsFromText(value, sourceSection = 'optionsText') {
|
||||
const text = cleanMultiline(value, 12000);
|
||||
const candidateText = /\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\b[:\s]/i.test(text)
|
||||
? text.replace(/^[\s\S]*?\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\b[:\s]*/i, '')
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(Boolean);
|
||||
const hasExplicitSortableHeading = lines.some(line => sortableTextHeadings.test(line));
|
||||
const extracted = [];
|
||||
let activeSection = hasExplicitSortableHeading ? 'context' : 'sortable';
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (sortableTextHeadings.test(line)) {
|
||||
activeSection = 'sortable';
|
||||
continue;
|
||||
}
|
||||
if (contextTextHeadings.test(line)) {
|
||||
activeSection = 'context';
|
||||
continue;
|
||||
}
|
||||
const inlineSortable = line.match(inlineSortablePrefix);
|
||||
if (inlineSortable) {
|
||||
activeSection = 'sortable';
|
||||
const rest = line.replace(inlineSortablePrefix, '').trim();
|
||||
if (rest) extracted.push(rest);
|
||||
continue;
|
||||
}
|
||||
if (activeSection !== 'sortable') continue;
|
||||
if (/^(constraints?|instructions?|context|notes?|goal|goals?)\s*:/i.test(line)) continue;
|
||||
extracted.push(line);
|
||||
}
|
||||
|
||||
const looseText = !hasExplicitSortableHeading && /\b(maybe|possibly|options?|features?|functionality|variants?|claims?|risks?|experiments?|validation questions?|feedback themes?|audiences?|next moves?)\b[:\s]/i.test(text)
|
||||
? text.replace(/^[\s\S]*?\b(maybe|possibly|options?|features?|functionality|variants?|claims?|risks?|experiments?|validation questions?|feedback themes?|audiences?|next moves?)\b[:\s]*/i, '')
|
||||
: text;
|
||||
const cleanedLines = candidateText.split('\n').map(line => line.replace(/^\s*[-*•\d.)]+\s*/, '').trim()).filter(Boolean);
|
||||
const candidateText = (hasExplicitSortableHeading ? extracted.join('\n').trim() : '') || looseText
|
||||
.replace(/\b(maybe|possibly|options?|features?|functionality|variants?|claims?|risks?|experiments?|validation questions?|feedback themes?|audiences?|next moves?)\s*:/gi, '\n')
|
||||
.trim();
|
||||
const cleanedLines = candidateText.split('\n').map(normalizeSortableTextLine).filter(Boolean);
|
||||
const sentenceList = candidateText
|
||||
.replace(/\b(maybe|possibly|options?|features?|ideas?|next moves?|functionality|backlog)\s*:/gi, '\n')
|
||||
.split(/\n|;|\|/)
|
||||
.map(part => part.replace(/^\s*[-*•\d.)]+\s*/, '').trim())
|
||||
.map(normalizeSortableTextLine)
|
||||
.filter(Boolean);
|
||||
const commaList = sentenceList.length === 1
|
||||
? sentenceList[0].split(/,|\s+and\s+|[.!?]\s+/i).map(part => part.trim()).filter(Boolean)
|
||||
? sentenceList[0].split(/,|\s+and\s+|[.!?]\s+/i).map(normalizeSortableTextLine).filter(Boolean)
|
||||
: [];
|
||||
const optionLines = cleanedLines.length >= 2
|
||||
? cleanedLines
|
||||
@@ -468,20 +519,16 @@ function parseOptionsFromText(value, sourceSection = 'optionsText') {
|
||||
? sentenceList
|
||||
: commaList.length >= 2
|
||||
? commaList
|
||||
: candidateText.split(/[;|]/).map(part => part.trim()).filter(Boolean);
|
||||
: candidateText.split(/[;|]/).map(normalizeSortableTextLine).filter(Boolean);
|
||||
return optionLines.slice(0, 24).map((line, index) => {
|
||||
const normalized = line
|
||||
.replace(/^\s*(maybe|possibly|also|plus|and|or|then|later|eventually|build|add|include|some kind of|kind of)\b\s*/i, '')
|
||||
.replace(/\?+$/, '')
|
||||
.trim();
|
||||
const [rawTitle, ...rest] = normalized.split(/\s*[-–—:]\s+/);
|
||||
const { title, description } = splitOptionTitleDescription(line);
|
||||
return {
|
||||
id: `option-${index + 1}`,
|
||||
title: cleanText(rawTitle || normalized || line, 140),
|
||||
description: cleanText(rest.join(' — '), 420),
|
||||
title,
|
||||
description,
|
||||
provenance: { sourceSection },
|
||||
};
|
||||
}).filter(item => item.title && !/^(i('| a)?m|i am|we are|i only|want|need)\b/i.test(item.title));
|
||||
}).filter(item => item.title && !/^(i('| a)?m|i am|we are|i only|want|need|rank the|sort the|do not|don't|dont)\b/i.test(item.title));
|
||||
}
|
||||
|
||||
function objectFrom(value) {
|
||||
|
||||
Reference in New Issue
Block a user