Files
houseplan-card/scripts/entry-cost.mjs
Claudeandclaude[bot] 7dc7597260 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
2026-09-24 02:33:08 +00:00

80 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
}
}
}