397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.7 KiB
JavaScript
89 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* brain-retro sanity-check candidate generator (Phase 3 Task 19, spec §4.7).
|
|
*
|
|
* Pure deterministic — read-only, no fs, no LLM. Given the episodes of a
|
|
* /brain-retro period, emit up to 5 candidate sanity-check questions for the
|
|
* controller (главный Claude) to choose 3-4 from. Questions are asked via
|
|
* AskUserQuestion; comments pass through observer-pii-filter before being
|
|
* persisted to docs/observer/sanity-checks/YYYY-MM-DD.json.
|
|
*
|
|
* Threshold: a per-classification question fires when the corresponding
|
|
* volume crosses 10 episodes in the period (per spec §4.7).
|
|
*
|
|
* All questions are in Russian to match the controller-user dialogue.
|
|
*/
|
|
|
|
const MAX_QUESTIONS = 5;
|
|
|
|
const VOLUME_THRESHOLD = 10;
|
|
|
|
function classification(ep) {
|
|
if (!ep) return null;
|
|
return ep?.classifier_output?.task_type
|
|
?? ep?.primary_rationale?.task_classification
|
|
?? null;
|
|
}
|
|
|
|
const VOLUME_QUESTIONS = [
|
|
{
|
|
cls: 'bugfix',
|
|
q: 'За период было много багов. Что мешает увереннее их отдебагать с первой попытки — недостаток воспроизведения, недостаток observability, или нехватка времени на гипотезы?',
|
|
},
|
|
{
|
|
cls: 'feature',
|
|
q: 'За период было много новых фич. Где сейчас бутылочное горлышко — спецификация, code review, тесты, выкат?',
|
|
},
|
|
{
|
|
cls: 'planning',
|
|
q: 'За период было много задач на планирование. Это сигнал что план каждой задачи становится сложнее, или что задачи приходят без подготовленного скоупа?',
|
|
},
|
|
{
|
|
cls: 'refactor',
|
|
q: 'За период было много рефакторов. Они шли парами с фичами/багами, или это отдельные кампании? Какие самые болезненные участки кода остались?',
|
|
},
|
|
{
|
|
cls: 'security',
|
|
q: 'За период было много security-задач. Это плановые сканы перед выкатом, или реакция на находки? Где сейчас самый высокий риск?',
|
|
},
|
|
{
|
|
cls: 'marketing',
|
|
q: 'За период было много маркетинговых задач. Кампании окупились по KPI, или работа идёт без замера? Что хотим оптимизировать в следующий период?',
|
|
},
|
|
];
|
|
|
|
const META_QUESTIONS = [
|
|
'Что наблюдатель должен был засечь за период, но не засёк? Назови один конкретный кейс если есть.',
|
|
'За период случались моменты когда контроллер выбрал direct, хотя нужен был навык? Один пример достаточно.',
|
|
];
|
|
|
|
export function generateCandidateQuestions(episodes) {
|
|
const eps = Array.isArray(episodes) ? episodes : [];
|
|
|
|
const counts = new Map();
|
|
for (const ep of eps) {
|
|
const c = classification(ep);
|
|
if (!c) continue;
|
|
counts.set(c, (counts.get(c) || 0) + 1);
|
|
}
|
|
|
|
const ranked = [...counts.entries()]
|
|
.filter(([_, n]) => n > VOLUME_THRESHOLD)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([cls]) => cls);
|
|
|
|
const out = [];
|
|
for (const cls of ranked) {
|
|
const v = VOLUME_QUESTIONS.find((q) => q.cls === cls);
|
|
if (v) out.push(v.q);
|
|
if (out.length >= MAX_QUESTIONS) break;
|
|
}
|
|
|
|
for (const meta of META_QUESTIONS) {
|
|
if (out.length >= MAX_QUESTIONS) break;
|
|
out.push(meta);
|
|
}
|
|
|
|
return out.slice(0, MAX_QUESTIONS);
|
|
}
|