mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 11:18:48 +00:00
Сверка PROCESS.md, ролевых выжимок, AGENTS.md, TESTING.md, CONTRIBUTING.md и скриптов по 26 найденным расхождениям (D1–D26): трейлеры по классам изменений, gate:small как единственный источник состава, пороги ревью, путь реестра мутантов, golden по ci:golden, порядок чтения промпта ревью. - scripts/change-classes.mjs: классы A/B/C/D — один модуль для process-gate и проверки трейлеров. - commit-msg: коммит только с файлами класса C (документация) трейлеров не требует; указанные трейлеры по-прежнему проверяются. - Маршрут автора без docs/STATUS.md: 5345 → 4703 слова. - Промпт ревью читает SCOPE → AGENTS → REVIEWER, как ROUTES.reviewer. Issue: #701 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
83 lines
4.5 KiB
JavaScript
83 lines
4.5 KiB
JavaScript
#!/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({
|
||
// #701: `docs/STATUS.md` ушёл из входа автора — снимок версий и цикла нужен,
|
||
// когда сессия возобновляет работу или готовит релиз, а задачу ведёт её пакет
|
||
// (`task-packet.mjs`). Минус 691 слово на каждом входе.
|
||
author: {
|
||
budget: 12000, // AC1 #634
|
||
files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/AUTHOR.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);
|
||
}
|
||
}
|
||
}
|