Files
portal/tools/brain-retro-sanity-generator.mjs
T
Дмитрий 12f88f32c1 feat(brain): sanity-generator + brain-retro v2 + self-retrospect stub (phase 3 task 19)
Phase 3 Task 19 partial — coverage announcement §4.9 deferred to a
separate commit (touches Pravila §17, requires §15.2 pre-flight sync).

- tools/brain-retro-sanity-generator.mjs (NEW, pure):
  generateCandidateQuestions(episodes) returns ≤5 sanity questions
  derived from per-classification volume (>10 episodes per task type
  triggers a themed question: bugfix/feature/planning/refactor/security/
  marketing) plus 2 meta questions about missed activations / direct
  bypass. Reads task_type from classifier_output (v4) with fallback
  to primary_rationale.task_classification (v2/v3). Spec §4.7.
- tools/brain-retro-sanity-generator.test.mjs (NEW): 6 tests
  (bugfix >10 / feature >10 / max 5 / empty / legacy v2/v3 / strings).
- .claude/skills/brain-retro/SKILL.md:
  + description rewritten — "раз в 1-2 недели OR sanity-check threshold"
    (cadence change per spec §4.7).
  + procedure +steps 5a (sanity questions via AskUserQuestion +
    PII filter + sanity-checks/YYYY-MM-DD.json), 5b (reviewer-agent
    Task() spawn + fallback to brain-retro-opus-reviewer.mjs), 9
    (self-retrospect threshold check), 10 (cost report from
    ~/.claude/runtime/cost-daily.json), 11 (richer summary).
- .claude/skills/self-retrospect/SKILL.md (NEW) — stub skill;
  full procedure wired in Task 20 (analyzer + STATUS.md surface the
  threshold).
- docs/observer/.self-retrospect-counter.json (NEW): initial state
  {last_run_at: null, episodes_since_last: 0}.
- docs/observer/sanity-checks/.gitkeep (NEW): directory placeholder
  for sanity-answers JSON files.

Tests: 608 passed / 0 failed (+15 from Task 19 + prior). 4 pre-existing
file fails unchanged. Coverage announcement §4.9 (economy-mode.py +
Pravila §17 subsection + feedback memory + coverage-annotation-mode
flag) — deferred: touches Pravila which is in the §15.2 8-file SoT
list and needs pre-flight `git fetch origin && git log HEAD..origin/main`
before edit; flagging as Phase 3 follow-up commit.
2026-05-25 14:28:26 +03:00

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);
}