docs(hygiene): сократить вход агента, у правила — один дом (#680)

Волна 3 эпика #674. AGENTS.md 650 → 187 строк: карта пакета, маршрут чтения,
правило №1, классы и треки одной строкой со ссылками, трейлеры, рабочие
деревья, хендофф и ожидание вердикта; пересказы PROCESS.md — ссылками на
разделы. Неверный список «Gate jobs» снят (списки jobs не копируются в прозу,
шапка PROCESS.md). Правила, жившие только в AGENTS, получили дом: жёлтый
вердикт при выполненных AC — PROCESS §2.7; свежесть бандла, съёмка только в
Linux (#455, HP_ALLOW_FOREIGN_CAPTURE) и смоки из AC до S7 (#151) —
TESTING.md; причуда демо-стенда и среда-зависимый smoke_opening_measure —
DEVELOPMENT › Smoke tests; отказ публикации без `Release:` и при несвежем
отпечатке бандла, отмена Validate новым пушем, кандидат беты не
promotion-only, fail-closed реестра Labs — DEVELOPMENT; предупреждение и
ошибка свежести скриншотов — CONTRIBUTING.

PROCESS.md: §13 (внедрение с открытым ⏳), §14 (блок со ссылкой на
несуществующий docs/PROCESS.md) и §7.3 (история) удалены. Ссылки «§7.2» на
правило полного разбора после ребейза ведут в §2.10, на сверку SHA перед
выводом — в §2.7; то же в сообщениях scripts/branch-state.mjs,
merge-candidate.mjs, review-doc-guard.mjs, pre-push-gate.mjs, в промпте
_process.yml и TESTING.md. Число `any` в прозе → `node scripts/no-new-any.mjs
--total` (новый режим, юнит-тест; было «1034 в 49 файлах», сейчас 862 в 52),
дата-число замороженного списка якорей монолита снято. Устаревшая команда
пересъёмки скриншотов в §8 заменена ссылкой на действующий путь.

STATUS.md 113 → 61 строка: сгенерированный снимок, текущий цикл и девять
строк решений; Workflow, CI, Toolchain, Tests, Scope, open items и политика
документации — ссылками (PROCESS §2.6, DEVELOPMENT › Release, TESTING);
локали en/ru/de/fr; закрытые «coverage, mypy strict» сняты.

DEVELOPMENT.md: file-sync и «Reproducible scripts» (прототип) удалены;
раздел Release — единственный дом релизной механики: введение, правила
тела стабильного релиза (#328, release:notes), шаг continuity:screencast,
источники версии по release-contract. CONTRIBUTING: ссылка на Release вместо
пересказа, замеры клона без чисел. TESTING: any-гейт — ссылкой на PROCESS §8.

entry-cost: автор 11 125 → 5 407 слов, ревьюер 8 464 → 4 285.

Issue: #680
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-09-27 22:33:03 +03:00
parent 972ff701d1
commit 696f5a789f
17 changed files with 378 additions and 858 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ export function rebaseAdvice({ behind, base = 'origin/dev' }) {
const commits = count === 1 ? 'коммит' : count < 5 ? 'коммита' : 'коммитов';
return `ветка отстала от ${base} на ${count} ${commits}.`
+ ` Конвейер приведёт её сам перед ревью, но после ребейза разбор станет`
+ ` полным, а не по дельте (§7.2). Дешевле сделать это здесь:`
+ ` полным, а не по дельте (§2.10). Дешевле сделать это здесь:`
+ ` git fetch origin && git rebase ${base}`;
}
@@ -46,5 +46,5 @@ export function devMovedNote({ moved, sha }) {
const at = sha ? ` Материал ревью — \`${sha}\`.` : '';
return `Пока шло ревью, \`dev\` продвинулся на ${count} ${commits}.${at}`
+ ' Вердикт вынесен по дереву, которое уже не совпадает с вершиной линии:'
+ ' слияние приведёт ветку к dev, и это другой код (§7.2).';
+ ' слияние приведёт ветку к dev, и это другой код (§2.10).';
}
+1 -1
View File
@@ -82,7 +82,7 @@ export function commentFor(action, ctx) {
+ `1. \`git fetch origin\`, затем \`git rebase origin/dev\` в ветке задачи, разрешить конфликт;\n2. запушить ветку;\n3. вернуть метку \`S7-code-review\`.\n\n`
+ `Повторный прогон ревью — не формальность: после ребейза на новый \`dev\` это другой код, и принимать его без проверки нельзя. Цикл считается по этапу, лимит на код-ревью тратится отдельно от ревью ТЗ.`;
case 'rereview':
return `**Дифф изменился при ребейзе на \`dev@${short(ctx.devNow)}\` — вердикт к нему не применим (§7.2, #492).**\n\n`
return `**Дифф изменился при ребейзе на \`dev@${short(ctx.devNow)}\` — вердикт к нему не применим (§2.10, #492).**\n\n`
+ `Материал ревью \`${short(ctx.material)}\` и кандидат \`${short(ctx.candidate)}\` дают разные patch-id: соседние правки в \`dev\` изменили содержимое патча. Кандидат опубликован в ветку; задача возвращена в \`S7-code-review\` — новый заход ревью читает актуальный код.`;
case 'validation-red':
return `**Кандидат после ребейза на \`dev@${short(ctx.devNow)}\` красный (#492).**\n\n`
+42 -7
View File
@@ -5,11 +5,13 @@
* node scripts/no-new-any.mjs # origin/dev...HEAD
* node scripts/no-new-any.mjs --base origin/dev --head HEAD
* node scripts/no-new-any.mjs --diff patch.diff # или `-` для stdin
* node scripts/no-new-any.mjs --total # весь долг src/** (#680)
*
* Зачем гейт, а не разовая типизация. В `src/**` сейчас 1034 вхождения явного
* `any` в 49 файлах — перетипизировать это одним заходом значит месяц риска ради
* нуля пользовательской ценности. Долг снимается при плановом извлечении
* подсистем (#425, прежний #34). Задача гейта одна: не давать долгу расти.
* Зачем гейт, а не разовая типизация. Явного `any` в `src/**` — сотни
* вхождений (точное число печатает `--total`); перетипизировать это одним
* заходом значит месяц риска ради нуля пользовательской ценности. Долг
* снимается при плановом извлечении подсистем (#425, прежний #34). Задача
* гейта одна: не давать долгу расти.
*
* Практический вред уже случался: несоответствие форм (`d.source.kind` против
* строкового `source`) компилятор не поймал, потому что путь был через `any`, и
@@ -51,8 +53,8 @@
* добавление: повторная вставка того же блока остаётся новым кодом.
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
@@ -103,6 +105,34 @@ export function anyKeywordLines(path, text) {
return lines;
}
/**
* Весь существующий долг (#680): число узлов `AnyKeyword` и файлов с ними по
* продуктовому TypeScript. Документы называют эту команду вместо числа — число в
* прозе отставало от дерева с первой недели.
*/
export function totalAny(files) {
let occurrences = 0;
let withAny = 0;
for (const file of files) {
let count = 0;
for (const perLine of anyKeywordLines(file.path, file.text).values()) count += perLine;
occurrences += count;
if (count) withAny += 1;
}
return { occurrences, files: withAny };
}
function productTypeScriptFiles(dir = join(ROOT, 'src')) {
const out = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) { out.push(...productTypeScriptFiles(full)); continue; }
const path = relative(ROOT, full).split('\\').join('/');
if (isProductTypeScript(path)) out.push({ path, text: readFileSync(full, 'utf8') });
}
return out;
}
/**
* Нарушения: `any` на строке, которую диапазон объявил добавленной.
*
@@ -303,6 +333,11 @@ function main(argv) {
return index >= 0 && argv[index + 1] && !argv[index + 1].startsWith('--')
? argv[index + 1] : fallback;
};
if (argv.includes('--total')) {
const total = totalAny(productTypeScriptFiles());
console.log(`Явный any в src/**/*.ts: ${total.occurrences} вхождений в ${total.files} файл(ах).`);
return 0;
}
const diffArg = value('diff');
let diff;
if (diffArg) {
@@ -352,7 +387,7 @@ function main(argv) {
console.error('\nЛибо типизируйте, либо обоснуйте на той же строке:');
console.error(' // any-ok: <конкретная причина, почему тип недоступен>');
console.error('Существующий долг снимается при извлечении подсистем (#34, #342),');
console.error('а не разовой заменой: в src/** его 1034 вхождения в 49 файлах.');
console.error('а не разовой заменой; сколько его — node scripts/no-new-any.mjs --total.');
return 1;
}
+1 -1
View File
@@ -223,7 +223,7 @@ function manualGate(argv) {
// ---- приведена ли ветка к dev ---------------------------------------------
// Конвейер ребейзит сам (#257), но после ребейза разбор становится полным, а не
// по дельте (§7.2), и конфликт всплывает в комментарии через сорок минут вместо
// по дельте (§2.10), и конфликт всплывает в комментарии через сорок минут вместо
// машины автора. Поэтому предупреждение, а не гейт: гейтом остаётся конвейер.
if (!flag('no-rebase-check')) {
const fetched = capture('git', ['fetch', '-q', 'origin', base.replace(/^origin\//, '')]);
+2 -2
View File
@@ -17,8 +17,8 @@
// Решения, принятые при реализации. Продуктового поведения не касаются, поэтому
// приняты здесь, а не у владельца; ревьюер вправе оспорить любое.
//
// 1. «Release vX.Y.Z-beta.N candidate» — НЕ релизный коммит. Promotion-only по
// AGENTS.md — это стабильный релиз; кандидат беты несёт саму работу и живёт
// 1. «Release vX.Y.Z-beta.N candidate» — НЕ релизный коммит. Promotion-only
// (`PROCESS.md` §3 п.16, `docs/DEVELOPMENT.md` › Release) — это стабильный релиз; кандидат беты несёт саму работу и живёт
// по общим правилам, включая трейлер Issue.
// 2. Классы дополнены: package.json, package-lock.json, pytest.ini, .gitignore,
// .gitattributes, .githooks/** — класс B (конфигурация сборки и гейтов).
+3 -3
View File
@@ -129,7 +129,7 @@ export function materialAnchorsFrom(text) {
* issue. Коммита с таким именем в репозитории нет и не было: клон не мелкий,
* `git rev-list --all` его не знает. Скорее всего значение снято до `amend`
* или `rebase` при публикации — то есть проверка `git rev-parse HEAD` перед
* выводом отчёта, которую требует §7.2, не выполнялась ни у автора, ни у
* выводом отчёта, которую требует §2.7, не выполнялась ни у автора, ни у
* ревьюера.
*
* Цена уже заплачена на следующем раунде: пункт «найти SHA, на котором получен
@@ -182,7 +182,7 @@ export function danglingMaterialRefusal(
+ 'Команда `git diff <sha>..HEAD` из PROCESS.md §2.10 на таком отчёте не'
+ ' работает, а следующий раунд восстанавливает коммит по содержимому'
+ ' диффа руками (#413). Сверьте SHA командой `git rev-parse HEAD`'
+ ' непосредственно перед выводом отчёта — §7.2 требует именно этого,'
+ ' непосредственно перед выводом отчёта — §2.7 требует именно этого,'
+ ' а не значения, записанного до amend или rebase.';
}
@@ -531,7 +531,7 @@ export function anchorVerdictFrom(text) {
* - текущее дерево отличается от якоря НИЧЕМ, кроме docs/reviews/** —
* сравнение делает git по содержимому, так что ребейз на ушедший dev,
* правка теста, фикстуры, скрипта или ТЗ в docs/specs дают отличие и
* полный разбор (§7.2). Смена базы без изменения дерева невозможна:
* полный разбор (§2.10). Смена базы без изменения дерева невозможна:
* дерево ветки после ребейза включает содержимое dev.
* Любое сомнение — `null`, и ревью идёт как обычно.
*