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
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env node
// #634: таблица «Snapshot» в docs/STATUS.md — генерируется, а не пишется руками.
//
// Раньше строка версии и счётчики в STATUS.md правились вручную и отставали от
// дерева за дни (review R5-2 уже убрал оттуда числа тестов по той же причине).
// Теперь всё, что вычислимо из дерева и git, считает этот скрипт из тех же
// источников, что и гейты: версии — `parseVersionSources` релизного контракта
// (`scripts/release-contract.mjs`), счётчики — `testInventory`
// (`scripts/inventory.mjs`), теги — `git tag`. Метки issue сюда не входят:
// api.github.com из песочницы агента недостижим, а статус живёт в метках.
//
// node scripts/status-snapshot.mjs напечатать блок
// node scripts/status-snapshot.mjs --write заменить блок в docs/STATUS.md
// node scripts/status-snapshot.mjs --check exit 1, если блок в файле устарел
// [--date=YYYY-MM-DD] [--root=<клон>]
//
// `--check` не стоит в CI сознательно: на shallow-checkout тегов нет, а
// счётчик тестов меняется с каждым новым тестом — гейт краснел бы на каждой
// задаче. Блок — снимок на дату в первой строке; свежий — одна команда.
import { readFileSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseVersionSources } from './release-contract.mjs';
import { testInventory } from './inventory.mjs';
import { isMainModule } from './spawn-portable.mjs';
export const BEGIN = '<!-- status-snapshot:begin — generated by `node scripts/status-snapshot.mjs --write`; do not edit by hand -->';
export const END = '<!-- status-snapshot:end -->';
export const STATUS_FILE = 'docs/STATUS.md';
const STABLE_TAG = /^v\d+\.\d+\.\d+$/;
const PRERELEASE_TAG = /^v\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/;
const git = (root, args) => {
const run = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
return run.status === 0 ? run.stdout.trim() : null;
};
/** Сводка версий: одно значение во всех источниках либо список расхождений. */
export function summarizeVersions(sources) {
const entries = Object.entries(sources);
const values = [...new Set(entries.map(([, value]) => value))];
if (values.length === 1 && values[0]) return { version: values[0], sources: entries.length, mismatches: [] };
const counts = new Map();
for (const [, value] of entries) counts.set(value, (counts.get(value) || 0) + 1);
const majority = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
return {
version: majority,
sources: entries.length,
mismatches: entries.filter(([, value]) => value !== majority).map(([name, value]) => `${name}=${JSON.stringify(value)}`),
};
}
/** Последние по дате создания стабильный и пре-релизный теги (порядок `git tag --sort=-creatordate`). */
export function latestTags(tagsNewestFirst) {
return {
stable: tagsNewestFirst.find((tag) => STABLE_TAG.test(tag)) || null,
prerelease: tagsNewestFirst.find((tag) => PRERELEASE_TAG.test(tag)) || null,
};
}
/** Файлы, из которых `parseVersionSources` читает версию, — те же, что у релизного контракта. */
export const VERSION_FILES = Object.freeze({
packageJson: 'package.json',
packageLock: 'package-lock.json',
manifest: 'custom_components/houseplan/manifest.json',
constSource: 'custom_components/houseplan/const.py',
cardSource: 'src/houseplan-card.ts',
editorRuntimeSource: 'src/houseplan-editor-runtime.ts',
});
/** Собрать входы из дерева и git. Всё, что недоступно, — null, а не догадка. */
export function collectSnapshot(root, { date = new Date().toISOString().slice(0, 10) } = {}) {
const read = (rel) => readFileSync(join(root, rel), 'utf8');
const versions = summarizeVersions(parseVersionSources(Object.fromEntries(
Object.entries(VERSION_FILES).map(([key, rel]) => [key, read(rel)]))));
const tagList = git(root, ['tag', '--sort=-creatordate']);
const tags = latestTags(tagList ? tagList.split('\n').filter(Boolean) : []);
// Расстояние «N коммитов после тега» сюда не входит: закоммиченный блок
// устаревал бы на собственном коммите, и `--check` не имел бы смысла.
return { date, versions, tags, tests: testInventory(root) };
}
const code = (value) => (value ? `\`${value}\`` : 'n/a — tags are not available in this checkout');
/** Отрисовать блок. Чистая функция: вход — результат collectSnapshot. */
export function renderSnapshot({ date, versions, tags, tests }) {
const version = versions.mismatches.length
? `**${versions.version}** — NOT synchronized: ${versions.mismatches.join(', ')}`
: `**${versions.version}** in all ${versions.sources} version sources (\`scripts/release-contract.mjs\`)`;
const counts = tests.map((row) => `${row.label.replace(/\s*\(.*\)$/, '')} ${row.count}`).join(' · ');
return [
BEGIN,
'| Item | State |',
'|---|---|',
`| Generated | ${date} — rerun \`node scripts/status-snapshot.mjs\` for the current tree |`,
`| Version | ${version} |`,
`| Latest stable tag | ${code(tags.stable)} |`,
`| Latest prerelease tag | ${code(tags.prerelease)} |`,
`| Tests | ${counts} (\`npm run inventory\`) |`,
END,
].join('\n');
}
/** Заменить блок между маркерами; маркеров нет или их больше одной пары — ошибка. */
export function replaceBlock(text, block) {
const start = text.indexOf(BEGIN);
const end = text.indexOf(END);
if (start < 0 || end < start || text.indexOf(BEGIN, start + 1) >= 0 || text.indexOf(END, end + 1) >= 0)
throw new Error(`${STATUS_FILE}: expected exactly one ${BEGIN} … ${END} block`);
return text.slice(0, start) + block + text.slice(end + END.length);
}
/** Дата из существующего блока — чтобы `--check` сравнивал содержание, а не день запуска. */
export function blockDate(text) {
return /\| Generated \| (\d{4}-\d{2}-\d{2}) /.exec(text)?.[1] || null;
}
if (isMainModule(import.meta.url)) {
const arg = (name) => process.argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
const root = resolve(arg('root') || fileURLToPath(new URL('..', import.meta.url)));
const file = join(root, STATUS_FILE);
const current = process.argv.includes('--write') || process.argv.includes('--check') ? readFileSync(file, 'utf8') : '';
const date = arg('date') || (process.argv.includes('--check') ? blockDate(current) : undefined);
const block = renderSnapshot(collectSnapshot(root, date ? { date } : {}));
if (process.argv.includes('--write')) {
writeFileSync(file, replaceBlock(current, block));
console.log(`${STATUS_FILE}: snapshot block updated`);
} else if (process.argv.includes('--check')) {
if (replaceBlock(current, block) !== current) {
console.error(`${STATUS_FILE}: snapshot block is stale — run node scripts/status-snapshot.mjs --write`);
process.exit(1);
}
console.log(`${STATUS_FILE}: snapshot block is current`);
} else {
console.log(block);
}
}