183 lines
7.0 KiB
JavaScript
183 lines
7.0 KiB
JavaScript
// Checkpoint between gate and deep-dive. Asks only when budget was default AND >=1 candidate got skipped — every question is a tax on the user.
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import { formatCandidateLine } from './display-labels.mjs';
|
|
|
|
const TOP_INVESTIGATING_PREVIEW = 5;
|
|
const MAX_FULL_INVESTIGATING_PREVIEW = 10;
|
|
export function buildBudgetSummary(gate) {
|
|
const toLaunch = Array.isArray(gate?.toLaunch) ? gate.toLaunch : [];
|
|
const gated = Array.isArray(gate?.gated) ? gate.gated : [];
|
|
const budgetSource = gate?.budget?.source ?? 'default';
|
|
const currentBudget =
|
|
typeof gate?.budget?.maxCandidates === 'number'
|
|
? gate.budget.maxCandidates
|
|
: (gate?.budget?.maxCandidates === 'all' ? Infinity : 6);
|
|
|
|
// Only budget skips can be reached by raising the budget; disqualified/coveredBy can't.
|
|
const skippedByBudget = gated.filter((g) =>
|
|
typeof g.gatedReason === 'string' && g.gatedReason.startsWith('skippedByBudget')
|
|
);
|
|
const skipped = skippedByBudget.length;
|
|
const totalPassed = toLaunch.length + skipped;
|
|
|
|
const reasonParts = [];
|
|
if (budgetSource !== 'default') reasonParts.push(`user pre-set budget via ${budgetSource}`);
|
|
if (skipped === 0) reasonParts.push('no candidates skipped by budget');
|
|
const shouldAsk = budgetSource === 'default' && skipped > 0;
|
|
const reason = shouldAsk
|
|
? `default budget skipped ${skipped} candidate(s); ask user whether to expand`
|
|
: reasonParts.join('; ') || 'no expansion possible';
|
|
|
|
const summarize = (c) => ({
|
|
kind: c.kind,
|
|
route: c.route ?? c.hostname ?? null,
|
|
displayRoute: c.displayRoute ?? null,
|
|
o11ySignal: c.o11ySignal ?? null,
|
|
priority: c.priority ?? null,
|
|
});
|
|
|
|
const investigatingPreviewCount = typeof currentBudget === 'number' && currentBudget <= MAX_FULL_INVESTIGATING_PREVIEW
|
|
? currentBudget
|
|
: TOP_INVESTIGATING_PREVIEW;
|
|
const topInvestigating = toLaunch.slice(0, investigatingPreviewCount).map(summarize);
|
|
const topSkipped = skippedByBudget.map(summarize);
|
|
const options = buildOptions(toLaunch.length, skipped);
|
|
const questionText = buildQuestionText({ shouldAsk, totalPassed, currentBudget });
|
|
const printContract = shouldAsk
|
|
? 'Print chatPreview verbatim by copying exactChatMessage.body as a chat message before asking questionText. Do not summarize, truncate, reorder, shorten, or rewrite options.'
|
|
: null;
|
|
const questionPayload = shouldAsk ? buildQuestionPayload(questionText, options) : null;
|
|
const chatPreview = buildChatPreview({ shouldAsk, totalPassed, currentBudget, skipped, topInvestigating, topSkipped, reason });
|
|
const exactChatMessage = buildExactChatMessage(chatPreview);
|
|
return {
|
|
shouldAsk,
|
|
reason,
|
|
totalPassed,
|
|
currentBudget: currentBudget === Infinity ? 'all' : currentBudget,
|
|
budgetSource,
|
|
skipped,
|
|
topInvestigating,
|
|
topSkipped,
|
|
options,
|
|
printContract,
|
|
chatPreview,
|
|
exactChatMessage,
|
|
printCheck: shouldAsk ? buildPrintCheck({ exactChatMessage, skipped }) : null,
|
|
questionText,
|
|
questionPayload,
|
|
};
|
|
}
|
|
|
|
function buildChatPreview({ shouldAsk, totalPassed, currentBudget, skipped, topInvestigating, topSkipped, reason }) {
|
|
if (!shouldAsk) return `Audit scope: no question needed — ${reason}.`;
|
|
const lines = [];
|
|
lines.push(`Found ${totalPassed} potential issue${totalPassed === 1 ? '' : 's'} worth checking. By default I'll inspect the ${currentBudget} strongest now; ${skipped} will stay in the report for a larger run.`);
|
|
lines.push(`Choose a larger scope if you want broader coverage. More checks take longer.`);
|
|
if (topInvestigating.length > 0) {
|
|
lines.push('');
|
|
lines.push(`Checking now${topInvestigating.length < currentBudget ? ` (${topInvestigating.length} shown)` : ''}:`);
|
|
topInvestigating.forEach((c, i) => lines.push(` ${i + 1}. ${formatCandidateLine(c)}`));
|
|
}
|
|
if (topSkipped.length > 0) {
|
|
lines.push('');
|
|
lines.push(`Only checked if you expand this run (${topSkipped.length}):`);
|
|
topSkipped.forEach((c, i) => lines.push(` ${i + 1}. ${formatCandidateLine(c)}`));
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function buildExactChatMessage(body) {
|
|
return {
|
|
body,
|
|
lineCount: body.split('\n').length,
|
|
sha256: createHash('sha256').update(body).digest('hex'),
|
|
};
|
|
}
|
|
|
|
function buildPrintCheck({ exactChatMessage, skipped }) {
|
|
return {
|
|
bodyField: 'exactChatMessage.body',
|
|
sameAs: 'chatPreview',
|
|
requiredLineCount: exactChatMessage.lineCount,
|
|
requiredSha256: exactChatMessage.sha256,
|
|
requiredSkippedRows: skipped,
|
|
requiredSkippedHeading: `Only checked if you expand this run (${skipped}):`,
|
|
forbiddenSummaryPatterns: [
|
|
'\\btop skipped\\b',
|
|
'\\bmore (?:candidate|candidates|routes|entries|items|in gated list)\\b',
|
|
'\\b\\d+\\s*[-–—]\\s*\\d+\\.\\s+\\d+\\s+more\\b',
|
|
'\\betc\\.\\b',
|
|
],
|
|
instruction: 'The budget message is valid only when every line from exactChatMessage.body is preserved exactly. If you cannot verify that, print exactChatMessage.body again before asking the question.',
|
|
};
|
|
}
|
|
|
|
function buildQuestionText({ shouldAsk, totalPassed, currentBudget }) {
|
|
if (!shouldAsk) return '';
|
|
return `How many potential issues should I check in this run?`;
|
|
}
|
|
|
|
function buildOptions(currentCount, skippedCount) {
|
|
if (skippedCount === 0) return [];
|
|
const total = currentCount + skippedCount;
|
|
return [
|
|
{
|
|
label: `Check ${currentCount} (default)`,
|
|
value: currentCount,
|
|
recommended: true,
|
|
description: 'Fastest first pass; checks the strongest cost and performance signals.',
|
|
rationale: 'fastest first pass; checks the strongest cost and performance signals',
|
|
},
|
|
{
|
|
label: `Check all ${total}`,
|
|
value: 'all',
|
|
recommended: false,
|
|
description: 'Most complete; takes longer because every flagged route is investigated.',
|
|
rationale: 'most complete; takes longer because every flagged route is investigated',
|
|
},
|
|
{
|
|
label: 'Pick a number',
|
|
value: 'custom',
|
|
recommended: false,
|
|
description: `Check more than ${currentCount} without running the full ${total}.`,
|
|
rationale: `checks more than ${currentCount} without running the full ${total}`,
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildQuestionPayload(questionText, options) {
|
|
return {
|
|
questions: [{
|
|
question: questionText,
|
|
header: 'Audit scope',
|
|
multiSelect: false,
|
|
options: options.map((o) => ({
|
|
label: o.label,
|
|
description: o.description ?? o.rationale,
|
|
})),
|
|
}],
|
|
};
|
|
}
|
|
|
|
export function renderBudgetSummaryMarkdown(s) {
|
|
const lines = [];
|
|
lines.push(`## Audit scope`);
|
|
lines.push('');
|
|
if (!s.shouldAsk) {
|
|
lines.push(`_No question needed — ${s.reason}._`);
|
|
return lines.join('\n');
|
|
}
|
|
for (const ln of s.chatPreview.split('\n')) lines.push(ln);
|
|
lines.push('');
|
|
lines.push('### Options');
|
|
lines.push('');
|
|
for (const o of s.options) {
|
|
const tag = o.recommended ? ' (recommended)' : '';
|
|
lines.push(`- **${o.label}${tag}** — ${o.rationale}`);
|
|
}
|
|
lines.push('');
|
|
lines.push(`**Question:** ${s.questionText}`);
|
|
return lines.join('\n');
|
|
}
|