Files
houseplan-card/scripts/status-snapshot.mjs
T
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

139 lines
7.8 KiB
JavaScript
Raw 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: таблица «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);
}
}