Files
rank/scripts/check-rank-feedback.mjs
T
2026-05-26 22:43:17 +02:00

146 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
const port = 43045 + Math.floor(Math.random() * 1000);
const base = `http://127.0.0.1:${port}`;
const server = spawn(process.execPath, ['server.js'], {
cwd: new URL('..', import.meta.url),
env: { ...process.env, PORT: String(port), APPWRITE_ENDPOINT: '', APPWRITE_PROJECT_ID: '', APPWRITE_API_KEY: '' },
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
server.stdout.on('data', chunk => { output += chunk; });
server.stderr.on('data', chunk => { output += chunk; });
async function waitForServer() {
const deadline = Date.now() + 6000;
while (Date.now() < deadline) {
try {
const response = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ optionsText: '- one\n- two' }),
});
if (response.status < 500) return;
} catch {
await new Promise(resolve => setTimeout(resolve, 120));
}
}
throw new Error(`server did not become ready:\n${output}`);
}
try {
await waitForServer();
const response = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schema: 'prioritix-feature-set-v1',
sourceName: 'Scattermind',
artifactId: 'snapshot_123',
snapshotTitle: 'Tiny shop idea clarity pass',
originalPrompt: 'I have a tiny shop idea and do not know what to build first.',
idea: 'Scattermind clarified a small product idea. Ranker must defend the next build order, not create a dashboard.',
context: 'Solo builder. Need a rank-ready build order after Snapshot / Concept Map. Avoid accounts, workspaces, and team voting.',
mode: 'mvp',
featureSet: {
features: [
{ id: 'bridge-contract', title: 'Snapshot to Ranker feature-set contract', description: 'Convert Concept Map next moves into a rank-ready feature set with provenance.', userValue: 'Preserves the user prompt and source artifact so build order is defensible.', evidenceNeeded: 'Can one generated Concept Map create 3-7 sane next moves with source sections?', proofSteps: ['Run one fixture through /api/rank-feedback'], recommendedLane: 'do-first', sourceSection: 'concept-map.nextMoves' },
{ id: 'build-order-preview', title: 'Build order preview', description: 'Show do first, validate next, defer, and park with reasons.', userValue: 'A tired builder sees the next move without opening a dashboard.', evidenceNeeded: 'Can 3 non-AI-native users understand the first recommended action?', proofSteps: ['Show a static result screen to 3 people'], recommendedLane: 'validate-next', sourceSection: 'concept-map.nextMoves' },
{ id: 'workspace', title: 'Accounts and saved workspaces', description: 'Full dashboard with auth, workspace collaboration, team voting, and sync.', dependencies: ['auth', 'teams', 'saved projects', 'sync'], risk: 'Turns the bridge into generic dashboard swamp before the first proof.', sourceSection: 'concept-map.deferred' },
{ id: 'billing', title: 'Subscription billing layer', description: 'Pricing, checkout, invoices, account plans, and admin controls.', dependencies: ['account model', 'checkout', 'fulfillment'], risk: 'Payment machinery before the continuation value is proven.', sourceSection: 'concept-map.deferred' },
{ id: 'export', title: 'Exportable decision brief', description: 'Simple brief for sharing the defended build order.', evidenceNeeded: 'Does a plain brief help a user act within 48 hours?', recommendedLane: 'validate-next', sourceSection: 'concept-map.nextMoves' },
{ id: 'parked-bridge-dashboard', title: 'Saved Snapshot dashboard with provenance', description: 'Looks bridge-adjacent, but Scattermind already marked it as not-now because it needs auth, saved workspaces, and collaboration first.', recommendedLane: 'park', sourceSection: 'concept-map.parkingLot' },
],
},
}),
});
assert.equal(response.status, 200);
const data = await response.json();
assert.equal(data.ok, true);
assert.equal(data.input.provenance.artifactId, 'snapshot_123');
assert.equal(data.input.provenance.source, 'Scattermind');
assert.match(data.input.provenance.originalPrompt, /tiny shop idea/);
assert.equal(data.ranked.length, 6);
assert.deepEqual(Object.keys(data.buildOrder), ['doFirst', 'validateNext', 'defer', 'park']);
assert.equal(data.ranked[0].id, data.buildOrder.doFirst[0]);
assert.notEqual(data.ranked[0].id, 'workspace', 'dashboard swamp must not win the bridge fixture');
assert.ok(['defer', 'park'].includes(data.ranked.find(item => item.id === 'workspace').lane.id));
assert.ok(['defer', 'park'].includes(data.ranked.find(item => item.id === 'billing').lane.id));
assert.equal(data.ranked.find(item => item.id === 'parked-bridge-dashboard').lane.id, 'park', 'explicit Scattermind park hints must stay out of the active proof slice');
assert.equal(data.ranked.find(item => item.id === 'parked-bridge-dashboard').lane.source, 'hint');
assert.equal(data.ranked.find(item => item.id === 'bridge-contract').provenance.sourceSection, 'concept-map.nextMoves');
assert.match(data.ranked.find(item => item.id === 'bridge-contract').factors.evidenceNeeded, /Concept Map/);
assert.ok(data.ranked.find(item => item.id === 'bridge-contract').factors.metricHints.value === undefined);
assert.equal(data.brief.source.artifactId, 'snapshot_123');
assert.match(data.brief.summary, /Source: Tiny shop idea clarity pass · snapshot_123/);
assert.equal(data.handoff.schema, 'rank-feedback-result-v1');
assert.equal(data.handoff.source.artifactId, 'snapshot_123');
assert.equal(data.handoff.source.hasOriginalPrompt, true);
assert.equal(data.handoff.itemTrace.length, data.ranked.length);
assert.equal(data.handoff.itemTrace.find(item => item.id === 'bridge-contract').sourceSection, 'concept-map.nextMoves');
assert.deepEqual(data.handoff.warnings, []);
assert.ok(data.brief.next48Hours.some(item => /Open the source artifact \(snapshot_123\)/i.test(item)));
assert.ok(data.brief.next48Hours.some(item => /Evidence to collect/i.test(item)));
assert.match(data.brief.summary, /nearest follow-up|strongest signal/i);
const hintedResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schema: 'prioritix-feature-set-v1',
sourceName: 'Scattermind',
artifactId: 'concept_map_metric_hints',
idea: 'A concept map produced possible next moves for an overwhelmed solo builder.',
context: 'Defend build order from explicit Scattermind scoring hints plus text; do not let flashy platform language win.',
mode: 'mvp',
featureSet: {
features: [
{ id: 'manual-concierge-proof', title: 'Manual concierge proof', description: 'Personally rank three real idea snapshots and turn each into a small build order preview.', rankerHints: { value: 9, effort: 2, confidence: 8, urgency: 8, risk: 2 }, evidenceNeeded: 'Will a tired user act on the first recommended move?', proofSteps: ['Run 3 manual previews'] },
{ id: 'ai-autopilot-roadmap', title: 'AI autopilot roadmap platform', description: 'Generate a full automated roadmap with dashboard, workspace, team voting, sync, and integrations.', rankerHints: { value: 5, effort: 9, confidence: 3, urgency: 3, risk: 9 } },
{ id: 'nice-export', title: 'Clean export of the build order', description: 'Turn the defended order into a shareable text brief.', scoring: { impact: 7, complexity: 3, certainty: 7, timing: 5, assumptionRisk: 3 }, recommendedLane: 'validate-next' },
],
},
}),
});
assert.equal(hintedResponse.status, 200);
const hinted = await hintedResponse.json();
assert.equal(hinted.ranked[0].id, 'manual-concierge-proof', 'explicit low-effort/high-confidence Scattermind hints should defend the manual proof slice');
assert.ok(hinted.ranked[0].factors.metricHints.value >= 9);
assert.ok(hinted.ranked.find(item => item.id === 'ai-autopilot-roadmap').metrics.risk > hinted.ranked[0].metrics.risk);
const actionsResponse = await fetch(`${base}/api/rank-feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schema: 'prioritix-feature-set-v1',
sourceName: 'Scattermind',
artifactId: 'concept_map_actions',
snapshotTitle: 'Workshop idea continuation',
originalPrompt: 'I want to turn a workshop idea into the first useful thing to build.',
idea: 'Scattermind emitted next actions rather than feature objects; Ranker should still defend build order.',
context: 'Candidate action set from Concept Map. Keep it action-first and avoid generic workspace layers.',
mode: 'mvp',
featureSet: {
actions: [
{ id: 'manual-preview', action: 'Manual build-order preview', why: 'A user sees one defended next move before any app machinery exists.', evidence: 'Can two tired users explain what to do next?', validationSteps: ['Create one static preview from the Concept Map'], suggestedLane: 'do-first', sourceSection: 'concept-map.nextActions' },
{ id: 'saved-workspace', action: 'Saved project workspace', why: 'Keep every idea and roadmap in an account dashboard.', dependencies: ['auth', 'database permissions', 'workspace model', 'sync'], risk: 'Dashboard swamp before the continuation proof.', suggestedLane: 'park', sourceSection: 'concept-map.parkingLot' },
{ id: 'text-export', action: 'Copyable decision brief', why: 'Let the user paste the defended order into notes or a chat.', evidence: 'Does a plain text brief help them act within 48 hours?', validationSteps: ['Export the top lane and concerns as text'], suggestedLane: 'validate-next', sourceSection: 'concept-map.nextActions' },
],
},
}),
});
assert.equal(actionsResponse.status, 200);
const actions = await actionsResponse.json();
assert.equal(actions.input.optionCount, 3);
assert.equal(actions.ranked[0].id, 'manual-preview', 'action-shaped Concept Map next moves should be rankable without a features wrapper');
assert.equal(actions.ranked.find(item => item.id === 'manual-preview').provenance.sourceSection, 'concept-map.nextActions');
assert.equal(actions.ranked.find(item => item.id === 'saved-workspace').lane.id, 'park');
assert.deepEqual(actions.handoff.warnings, []);
console.log(JSON.stringify({ ok: true, top: data.ranked[0].id, hintedTop: hinted.ranked[0].id, actionTop: actions.ranked[0].id, provenance: data.input.provenance, buildOrder: data.buildOrder }, null, 2));
} finally {
server.kill('SIGTERM');
}