fix(process): free the reviewer prompt budget, rules stay in REVIEWER.md (#750)

The code-review prompt sat at exactly 1 400 of its 1 400 words (#634), so
any new line turned the budget test red, and #707/#726 already had to route
their notes through job outputs. Part of the text was dead or a retelling
of the reviewer digest, which #634 says the prompt must not repeat:

- the mutants fragment of the track line: since #709 `mutants` is always
  false, so «прогнаны Validate» was unreachable;
- «Отсутствие мутантов по диффу — не находка» in the show line: a rule of
  every track, already in REVIEWER.md «Трек show» and §10.4;
- three retellings — the repeated round, the gate scope and the severity
  paragraph — now one-line references to the REVIEWER.md sections. The ban
  on a separate issue for an in-scope Medium stays in the prompt: the model
  files issues itself, and that mistake is expensive.

What only the prompt said moves into REVIEWER.md with links to the canon:
the spec delta is the diff of the issue body, a doubt about locality means
a full review with a stated reason, the three smoke-select answers
(docs/TESTING.md), and geometry without invariants in the report is an
unrun gate.

The prompt is now 1 130 words; the 1 400 threshold stays, the difference
is headroom. A new test ties every «docs/process/REVIEWER.md, «X»»
reference in the prompt to an existing `## X` section.

Issue: #750
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 14:27:08 +00:00
committed by claude[bot]
parent 653d94ef09
commit de3e1f00e1
3 changed files with 59 additions and 40 deletions
+9 -36
View File
@@ -1170,9 +1170,9 @@ jobs:
ничего не вернул на правки и цикла не образует (#227).
Номер захода нужен для имени документа — два документа с
одинаковым номером затёрли бы друг друга.
Трек: ${{ needs.prepare.outputs.track || 'ask' }} (PROCESS.md §5) · мутанты по диффу на материале: ${{ needs.prepare.outputs.mutants == 'false' && 'не запрашивались' || 'прогнаны Validate' }}
Трек: ${{ needs.prepare.outputs.track || 'ask' }} (PROCESS.md §5)
${{ needs.prepare.outputs.track == 'show' && needs.guard.outputs.stage == 'code' && 'Трек show: объём и серьёзность находок — docs/process/REVIEWER.md, «Трек show» (#696). Отсутствие мутантов по диффу — не находка.' || '' }}
${{ needs.prepare.outputs.track == 'show' && needs.guard.outputs.stage == 'code' && 'Трек show: объём и серьёзность находок — docs/process/REVIEWER.md, «Трек show» (#696).' || '' }}
${{ needs.prepare.outputs.rebase_note }}
@@ -1212,17 +1212,8 @@ jobs:
FILTERING.md, STAIRS.md, RADAR.md, PDF-EXPORT.md,
STYLING-HOOKS.md.
**Если цикл не первый — объём разбора по дельте, а не заново**
(PROCESS.md §2.10, issue #214): найди вердикт и материал
предыдущего раунда (блок «Материал раунда» его документа), объяви
дельту `git diff <тот SHA>..HEAD` (для spec — дифф тела issue или
файла ТЗ), по каждой находке покажи, чем именно она закрыта —
строка кода или текста, а не заявление автора, — и заново проверяй
только AC, чьё доказательство дельта задевает. Разбор остаётся
ПОЛНЫМ, если дельта не локальна: ребейз на ушедший вперёд dev,
смена контракта, новая подсистема, объём сопоставим с задачей.
Сомневаешься — разбирай полностью и скажи почему. Сокращается
объём РАЗБОРА, а не строгость.
Цикл не первый — разбор по дельте (PROCESS.md §2.10):
docs/process/REVIEWER.md, «Повторный раунд».
Для этапа spec: ТЗ живёт в теле issue (#517) — читай его, а не файл.
Файлы docs/specs/<NN>-*.md — архив ТЗ до 2026-09-10: если такой файл
@@ -1265,22 +1256,8 @@ jobs:
`npm run build` со сверкой трёх копий бандла. Свежесть скриншотов
документации — не гейт задачи (#697): её обновляет бот на dev.
${{ needs.guard.outputs.stage == 'spec' && 'Зависимости и Chromium на этапе spec не ставились (#696).' || (needs.prepare.outputs.track == 'show' && 'Зависимости установлены (`npm ci` не нужен); Chromium — только если тело issue называет смоук (#696).' || 'Зависимости уже установлены workflow, Chromium тоже — `npm ci` выполнять не нужно.') }}
По диффу и AC: браузерные смоки — названные в AC
плюс вывод `node scripts/smoke-select.mjs --base <base> --head <head>`,
приложенный к комментарию с решением по каждой строке: прогнал либо
не прогнал и почему. Три вида ответа инструмента разные: «прямое
совпадение», «зарегистрированная связь», «НЕОПРЕДЕЛЁННОСТЬ» — связь
не доказана, и это не разрешение ничего не прогонять; слабые связи —
повод посмотреть, а не обязанность прогонять;
`npm run golden:verify` при метке `ci:golden`;
`python -m pytest tests_backend -q` при правке
`custom_components/**/*.py`; инварианты модели
`npm run invariants -- --config <экспорт>` при правке геометрии или
ссылок на неё (#254) — задача меняет геометрию, а инварианты в
отчёте не названы, это непрогнанный гейт, а не мелочь;
performance-профили, если названы в AC. Дисциплина «тест должен
уметь падать» не отменяется, но применяется к тем тестам, которые
ты прогонял.
По диффу и AC — смоки, golden, pytest, инварианты, performance:
docs/process/REVIEWER.md, «Объём гейтов».
**В комментарии обязателен перечень: какие гейты прогнал, какие нет и
почему.** Раздел «чего не проверял» в документе ревью — не
@@ -1288,13 +1265,9 @@ jobs:
Ты НЕ правишь ни ТЗ, ни продуктовый код. Только оцениваешь.
Серьёзность: High блокирует; Medium В СКОУПЕ задачи чинится в ней
же — без High это жёлтый вердикт и возврат автору, отдельный issue
НЕ заводится (#202); Low либо правится, либо снимается с записью.
Жёлтый вердикт допустим и при полностью выполненных AC, если
изменение не решает заявленный сценарий или ухудшает смежный.
Продуктовое рассуждение расширяет вопросы, но не отменяет AC и не
даёт права менять скоуп.
Серьёзность и вердикт — docs/process/REVIEWER.md, «Находки и вердикт»:
Medium В СКОУПЕ — жёлтый вердикт и возврат автору, отдельный issue
НЕ заводится (#202).
Только Medium-находку ВНЕ скоупа задачи (попутный дефект соседнего
поведения, который в этой ветке чинить нельзя) заведи отдельным
+14 -3
View File
@@ -84,6 +84,15 @@
при правке Python; инварианты модели при правке геометрии; performance —
если назван в AC. Полные наборы — предрелизный гейт, а не гейт ревью
([§8](../../PROCESS.md#8-гейты)).
- Три ответа `smoke-select` разные: прямое совпадение и зарегистрированная
связь — прогон; «НЕОПРЕДЕЛЁННОСТЬ» — связь не доказана, и это не разрешение
ничего не прогонять; слабая связь — повод посмотреть, а не обязанность
прогонять ([`docs/TESTING.md`](../TESTING.md#локальный-набор-перед-пушем-343),
[§8](../../PROCESS.md#8-гейты)).
- Инварианты модели — `npm run invariants -- --config <экспорт>` при правке
геометрии или ссылок на неё (#254): задача меняет геометрию, а инварианты в
отчёте не названы — это непрогнанный гейт, а не мелочь
([§8](../../PROCESS.md#8-гейты)).
- Условие честности сужения: ревьюер обязан перечислить, какие гейты прогнал,
какие нет и почему ([§8](../../PROCESS.md#8-гейты)).
- Зелёный `pytest tests_backend` без Home Assistant `test_ha_*.py` не
@@ -95,8 +104,9 @@
- Предмет повторного раунда — дельта, а не задача целиком: найти вердикт и
материал предыдущего раунда (блок «Материал раунда»), объявить
`git diff <тот SHA>..HEAD`, по каждой находке показать, чем она закрыта,
заново проверить только AC, которые дельта задевает
`git diff <тот SHA>..HEAD` (для ТЗ — дифф тела issue или архивного файла ТЗ),
по каждой находке показать, чем она закрыта — строкой кода или текста, а не
заявлением автора, — заново проверить только AC, которые дельта задевает
([§2.10](../../PROCESS.md#210-повторный-раунд-ревью--объём-по-дельте)).
- Если SHA не резолвится — это не находка, а обычное дело: материал ищется по
дереву и блобу. Находка — SHA, мёртвый уже в момент публикации отчёта
@@ -106,7 +116,8 @@
([§2.10](../../PROCESS.md#210-повторный-раунд-ревью--объём-по-дельте)).
- Разбор остаётся полным, если дельта не локальна: ребейз на ушедший вперёд
`dev`, смена контракта, новая подсистема, объём сопоставим с задачей.
Сокращается объём разбора, а не строгость
Сомнение в локальности — полный разбор с названной причиной. Сокращается
объём разбора, а не строгость
([§2.10](../../PROCESS.md#210-повторный-раунд-ревью--объём-по-дельте)).
- Перед разбором подсистемы — её строки в `docs/reviews/INDEX.md`
([§2.10](../../PROCESS.md#210-повторный-раунд-ревью--объём-по-дельте)).
+36 -1
View File
@@ -194,7 +194,42 @@ test('#634 промпт ревьюера: конспект вместо пере
'Затем верни JSON по схеме',
]) assert.ok(flat.includes(required), `промпт потерял: ${required}`);
// Правила живут в каноне; промпт, снова набравший пересказ, — возврат к 60–90 k
// контекста до первого git diff (аудит 22.09). До #634 — 1 642 слова.
// контекста до первого git diff (аудит 22.09). До #634 — 1 642 слова, к #750 —
// ровно 1 400, после #750 — 1 130: порог прежний, разница — запас.
const words = prompt.split(/\s+/).filter(Boolean).length;
assert.ok(words <= 1400, `промпт ревьюера ${words} слов > 1400`);
// #750 AC1: мёртвая строка мутантов (с #709 `mutants` всегда false) и правило
// всех треков, которое REVIEWER.md уже несёт, в промпт не возвращаются.
assert.doesNotMatch(prompt, /needs\.prepare\.outputs\.mutants/, 'промпт читает мёртвый выход mutants');
assert.doesNotMatch(flat, /Отсутствие мутантов/, 'правило «отсутствие мутантов — не находка» — в REVIEWER.md, не в промпте');
});
// #750 AC2: промпт вместо пересказа ссылается на разделы конспекта — ссылка
// без раздела оставила бы ревьюера без правила, которое промпт больше не несёт.
test('#750 AC2: ссылки промпта на REVIEWER.md ведут в существующие разделы; перенесённое на месте', () => {
const workflow = read('.github/workflows/_process.yml');
const prompt = workflow.slice(workflow.indexOf(' prompt: |\n'), workflow.indexOf(' claude_args: |'));
const flat = prompt.replace(/\s+/g, ' ');
const reviewer = read('docs/process/REVIEWER.md');
const sections = new Set(headings(reviewer).filter((h) => h.level === 2).map((h) => h.text));
const cited = [...flat.matchAll(/docs\/process\/REVIEWER\.md, «([^»]+)»/g)].map((m) => m[1]);
for (const name of ['Трек show', 'Повторный раунд', 'Объём гейтов', 'Находки и вердикт']) {
assert.ok(cited.includes(name), `промпт не ссылается на «${name}»`);
}
for (const name of cited) assert.ok(sections.has(name), `REVIEWER.md: нет раздела «## ${name}», на который ссылается промпт`);
// Запрет на отдельный issue для Medium в скоупе остаётся в промпте: модель
// заводит issue сама, и ошибка дорогая.
assert.ok(flat.includes('отдельный issue НЕ заводится (#202)'), 'промпт потерял запрет на issue для Medium в скоупе');
const bullets = (anchor) => topLevelBullets(sectionText(reviewer, anchor) ?? '');
const gates = bullets('объём-гейтов');
const uncertain = gates.find((bullet) => bullet.includes('НЕОПРЕДЕЛЁННОСТЬ'));
assert.ok(uncertain, '«Объём гейтов»: нет трёх ответов smoke-select с «НЕОПРЕДЕЛЁННОСТЬ»');
assert.match(uncertain, /связь не доказана/);
assert.ok(markdownLinks(uncertain).some((link) => link.file.endsWith('TESTING.md')), 'ответы smoke-select ссылаются на docs/TESTING.md');
assert.ok(gates.some((bullet) => /инвариант/i.test(bullet) && bullet.includes('непрогнанный гейт')),
'«Объём гейтов»: нет строки «геометрия без инвариантов в отчёте — непрогнанный гейт»');
const delta = norm(bullets('повторный-раунд').join('\n'));
assert.ok(delta.includes(norm('дифф тела issue')), '«Повторный раунд»: дельта ТЗ — дифф тела issue');
assert.ok(delta.includes(norm('Сомнение в локальности — полный разбор')), '«Повторный раунд»: сомнение — полный разбор');
});