docs(process): ролевые конспекты, замер входа, Snapshot генерируется, TESTING.md разделён

Вход агента до первого файла кода стоил ≈ 26 700 слов (аудит 22.09).

- docs/process/AUTHOR.md и REVIEWER.md — выжимки PROCESS.md: каждый пункт
  ссылается на раздел канона, ключевые формулировки дословные;
  test/process-digests.test.mjs сверяет якоря, ссылки и правила.
- scripts/entry-cost.mjs — маршрут чтения по роли и бюджет (автор ≤ 12 000
  слов, AC1); AGENTS.md «Read this first» называет те же маршруты.
- docs/STATUS.md: блок Snapshot генерирует scripts/status-snapshot.mjs
  (версии — release-contract, счётчики — inventory, теги — git); feature
  surface и ранние milestones перенесены дословно в docs/STATUS-FEATURES.md.
- docs/TESTING.md — действующая инструкция (684 строки, AC3); ручные
  чек-листы и приложения по issue перенесены дословно в docs/testing-notes/
  с индексом и тестом на полноту.
- Промпт ревьюера в process.yml читает конспект вместо пересказа правил;
  машинные требования (строка вердикта, REVIEW_DOC, запрет fetch, таблица
  «чем краснеет», разделы повторного раунда) сохранены и закреплены тестом.
- PROCESS.md: правила не менялись; добавлены ссылка на конспекты в шапке и
  уточнение в §10.4, что ревьюер конвейера читает конспект.
- 7 мутантов в реестре.

Issue: #634
User-Visible: no
This commit is contained in:
Claude
2026-09-24 02:33:08 +00:00
committed by claude[bot]
parent fe92ce067a
commit 7dc7597260
30 changed files with 5590 additions and 4352 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// #634: стоимость входа агента — сколько слов он читает до первого файла кода.
//
// Аудит 22.09 намерил ≈ 26 700 слов обязательного входа (SCOPE, AGENTS, полный
// PROCESS, STATUS) — 43–77 k токенов до первой строки кода. Ролевые конспекты
// `docs/process/AUTHOR.md` и `REVIEWER.md` заменяют на входе полный канон, а
// этот скрипт держит цену входа измеримой: маршрут по роли — один список здесь,
// `AGENTS.md` («Read this first») называет те же файлы в том же порядке, и тест
// `test/entry-cost.test.mjs` сверяет оба и краснеет на превышении бюджета.
//
// node scripts/entry-cost.mjs таблица по всем маршрутам
// node scripts/entry-cost.mjs --check exit 1, если маршрут с бюджетом его превысил
// node scripts/entry-cost.mjs --json
//
// Слово — как у `wc -w`: непустой отрезок между пробельными символами. Токены
// не считаются: их число зависит от токенизатора, а слова — нет.
// Вне замера сознательно: `CODEX-RUNBOOK.md` и `CLAUDE.md` папки владельца —
// они не в репозитории, и CI их не видит; тело issue — своё у каждой задачи.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isMainModule } from './spawn-portable.mjs';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
/** Маршруты входа по роли. `budget: null` — только замер, без порога. */
export const ROUTES = Object.freeze({
author: {
budget: 12000, // AC1 #634
files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/AUTHOR.md', 'docs/STATUS.md'],
},
reviewer: {
budget: 9000,
files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/REVIEWER.md'],
},
// Тот, кто правит конвейер, гейты или сам процесс, читает канон целиком.
canon: {
budget: null,
files: ['docs/SCOPE.md', 'AGENTS.md', 'PROCESS.md', 'docs/STATUS.md'],
},
});
export const countWords = (text) => text.split(/\s+/).filter(Boolean).length;
/** Замер одного маршрута: слова по файлам, сумма и вердикт бюджета. */
export function measureRoute(route, read = (rel) => readFileSync(join(ROOT, rel), 'utf8')) {
const files = route.files.map((file) => ({ file, words: countWords(read(file)) }));
const total = files.reduce((sum, row) => sum + row.words, 0);
const over = route.budget != null && total > route.budget;
return { files, total, budget: route.budget, over };
}
export function measureAll(routes = ROUTES, read) {
return Object.fromEntries(Object.entries(routes).map(([name, route]) => [name, measureRoute(route, read)]));
}
export function renderTable(results) {
const lines = [];
for (const [name, result] of Object.entries(results)) {
const limit = result.budget == null ? 'без бюджета' : `бюджет ${result.budget}`;
const mark = result.over ? ' ПРЕВЫШЕН' : '';
lines.push(`${name}: ${result.total} слов (${limit})${mark}`);
for (const row of result.files) lines.push(` ${String(row.words).padStart(6)} ${row.file}`);
}
return lines.join('\n');
}
if (isMainModule(import.meta.url)) {
const results = measureAll();
if (process.argv.includes('--json')) console.log(JSON.stringify(results, null, 2));
else console.log(renderTable(results));
if (process.argv.includes('--check')) {
const failed = Object.entries(results).filter(([, result]) => result.over).map(([name]) => name);
if (failed.length) {
console.error(`entry-cost: превышен бюджет входа: ${failed.join(', ')}`);
process.exit(1);
}
}
}