mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 19:28:46 +00:00
Вход агента до первого файла кода стоил ≈ 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
139 lines
7.8 KiB
JavaScript
139 lines
7.8 KiB
JavaScript
#!/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);
|
||
}
|
||
}
|