mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
docs(process): канон без противоречий, вход автора короче (#701)
Сверка 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
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Классы изменений, PROCESS.md §1 — одна таблица на все гейты (#701).
|
||||
//
|
||||
// Вынесена из process-gate.mjs: её читает и `validate-commit-provenance.mjs`
|
||||
// (хук commit-msg), которому process-gate сам импортирует — общий модуль
|
||||
// снимает круговую зависимость.
|
||||
// Порядок важен: D проверяется первым, иначе собранный бандл попадёт в A,
|
||||
// а demo/golden/baselines — в B.
|
||||
const CLASS_D = [
|
||||
/^dist\//,
|
||||
/^custom_components\/houseplan\/frontend\//,
|
||||
/^demo\/srv\/assets\/houseplan-card\.js$/,
|
||||
/^demo\/golden\/baselines\//,
|
||||
];
|
||||
const CLASS_A = [
|
||||
/^src\//,
|
||||
/^custom_components\/houseplan\/.*\.py$/,
|
||||
/^hacs\.json$/,
|
||||
/^custom_components\/.*\/manifest\.json$/,
|
||||
/^custom_components\/.*\/translations\//,
|
||||
];
|
||||
const CLASS_B = [
|
||||
/^test\//, /^tests_backend\//, /^demo\//, /^scripts\//,
|
||||
/^\.github\//, /^\.githooks\//, /^rollup\.config\.mjs$/, /^tsconfig.*\.json$/,
|
||||
/^package(-lock)?\.json$/, /^pytest\.ini$/, /^\.gitignore$/, /^\.gitattributes$/,
|
||||
// Пины toolchain — производные от validate.yml (#496), конфиг сборки.
|
||||
/^\.nvmrc$/, /^\.python-version$/,
|
||||
];
|
||||
const CLASS_C = [
|
||||
/^docs\//, /^README/, /^CHANGELOG/, /^AGENTS\.md$/, /^LICENSE$/,
|
||||
/^CONTRIBUTING\.md$/, /^PROCESS.*\.md$/, /^(CODE|SPEC)-REVIEW-.*\.md$/,
|
||||
// #682: архив выпущенного — документы ревью и ТЗ прошлых линий. Только
|
||||
// Markdown; исполняемого там нет (#678 вынес всё прочее из дерева).
|
||||
/^legacy\//,
|
||||
];
|
||||
|
||||
|
||||
export function classify(path) {
|
||||
if (CLASS_D.some((r) => r.test(path))) return 'D';
|
||||
if (CLASS_A.some((r) => r.test(path))) return 'A';
|
||||
if (CLASS_B.some((r) => r.test(path))) return 'B';
|
||||
if (CLASS_C.some((r) => r.test(path))) return 'C';
|
||||
return '?';
|
||||
}
|
||||
@@ -25,9 +25,12 @@ 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', 'docs/STATUS.md'],
|
||||
files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/AUTHOR.md'],
|
||||
},
|
||||
reviewer: {
|
||||
budget: 9000,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// Локальный гейт лёгкого трека одной командой (#479): `npm run gate:small`.
|
||||
//
|
||||
// PROCESS §8 перечисляет автору шесть команд, и в #476 они гонялись
|
||||
// Этот файл — единственный источник состава обязательной части §8 (#701):
|
||||
// канон и конспекты его не переписывают, а называют. В #476 команды гонялись
|
||||
// последовательно, вперемешку с гейтами, к задаче не относящимися. Здесь
|
||||
// обязательная часть §8 начинается параллельно — сборка с typecheck, «новый
|
||||
// код не добавляет any», выбор смоков по диффу. Юниты читают свежий `dist`,
|
||||
|
||||
@@ -8158,6 +8158,29 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' .hdr > .head { flex-wrap: wrap; padding: 5px 8px; gap: 6px; }',
|
||||
}],
|
||||
},
|
||||
// #701: документационный коммит трейлеров не требует — и только он.
|
||||
{
|
||||
id: 'docs-only-commit-needs-trailers-again',
|
||||
guard: 'node --test --test-name-pattern="#701" test/commit-provenance.test.mjs',
|
||||
because: '#701 (PROCESS §3 п.10): rule #1 guards product code, not a typo in a guide; a docs-only '
|
||||
+ 'commit carries no Issue/User-Visible trailers',
|
||||
patches: [{
|
||||
file: 'scripts/validate-commit-provenance.mjs',
|
||||
find: ' const exempt = isDocsOnlyCommit(changedFiles) && !issues.length && !visible.length;',
|
||||
replace: ' const exempt = false; // mutant: every commit needs trailers',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'docs-only-exemption-leaks-to-code',
|
||||
guard: 'node --test --test-name-pattern="#701" test/commit-provenance.test.mjs',
|
||||
because: '#701: one file outside class C makes the commit subject to rule #1 again; the exemption '
|
||||
+ 'must not cover a commit that also touches src/**',
|
||||
patches: [{
|
||||
file: 'scripts/validate-commit-provenance.mjs',
|
||||
find: " return changedFiles.length > 0 && changedFiles.every((file) => classify(file.replaceAll('\\\\', '/')) === 'C');",
|
||||
replace: " return changedFiles.length > 0 && changedFiles.some((file) => classify(file.replaceAll('\\\\', '/')) === 'C'); // mutant",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'header-menu-drops-pdf',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -39,36 +39,11 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { resolveValidationRange } from './validate-commit-provenance.mjs';
|
||||
|
||||
// --- классы изменений, PROCESS.md §1 ---
|
||||
// Порядок важен: D проверяется первым, иначе собранный бандл попадёт в A,
|
||||
// а demo/golden/baselines — в B.
|
||||
const CLASS_D = [
|
||||
/^dist\//,
|
||||
/^custom_components\/houseplan\/frontend\//,
|
||||
/^demo\/srv\/assets\/houseplan-card\.js$/,
|
||||
/^demo\/golden\/baselines\//,
|
||||
];
|
||||
const CLASS_A = [
|
||||
/^src\//,
|
||||
/^custom_components\/houseplan\/.*\.py$/,
|
||||
/^hacs\.json$/,
|
||||
/^custom_components\/.*\/manifest\.json$/,
|
||||
/^custom_components\/.*\/translations\//,
|
||||
];
|
||||
const CLASS_B = [
|
||||
/^test\//, /^tests_backend\//, /^demo\//, /^scripts\//,
|
||||
/^\.github\//, /^\.githooks\//, /^rollup\.config\.mjs$/, /^tsconfig.*\.json$/,
|
||||
/^package(-lock)?\.json$/, /^pytest\.ini$/, /^\.gitignore$/, /^\.gitattributes$/,
|
||||
// Пины toolchain — производные от validate.yml (#496), конфиг сборки.
|
||||
/^\.nvmrc$/, /^\.python-version$/,
|
||||
];
|
||||
const CLASS_C = [
|
||||
/^docs\//, /^README/, /^CHANGELOG/, /^AGENTS\.md$/, /^LICENSE$/,
|
||||
/^CONTRIBUTING\.md$/, /^PROCESS.*\.md$/, /^(CODE|SPEC)-REVIEW-.*\.md$/,
|
||||
// #682: архив выпущенного — документы ревью и ТЗ прошлых линий. Только
|
||||
// Markdown; исполняемого там нет (#678 вынес всё прочее из дерева).
|
||||
/^legacy\//,
|
||||
];
|
||||
// Классы изменений (PROCESS.md §1) живут в change-classes.mjs (#701): их
|
||||
// читает и хук commit-msg, который судит, нужен ли коммиту трейлер.
|
||||
import { classify } from './change-classes.mjs';
|
||||
|
||||
export { classify };
|
||||
|
||||
const CHANGELOGS = ['docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md'];
|
||||
|
||||
@@ -90,13 +65,6 @@ export const RULES = {
|
||||
10: 'DoR по моменту коммита',
|
||||
};
|
||||
|
||||
export function classify(path) {
|
||||
if (CLASS_D.some((r) => r.test(path))) return 'D';
|
||||
if (CLASS_A.some((r) => r.test(path))) return 'A';
|
||||
if (CLASS_B.some((r) => r.test(path))) return 'B';
|
||||
if (CLASS_C.some((r) => r.test(path))) return 'C';
|
||||
return '?';
|
||||
}
|
||||
|
||||
// --- разбор коммитов ---
|
||||
// Тело коммита многострочное, поэтому поля режутся не по переводам строк:
|
||||
|
||||
@@ -4,6 +4,7 @@ import { basename } from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { bundleCommitErrors } from './bundle-policy.mjs';
|
||||
import { classify } from './change-classes.mjs';
|
||||
|
||||
const TRAILER = /^([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\s*$/;
|
||||
export const ENFORCEMENT_BOUNDARY = '8e2973fa7a7cb1a80204ff95ecf3f2d7c36ed2ce';
|
||||
@@ -52,15 +53,27 @@ export function terminalTrailers(message) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* #701 (PROCESS.md §3 п.10): документационный коммит — только файлы класса C —
|
||||
* трейлеров не требует, как `skip issue` у CPython. Правило №1 охраняет
|
||||
* продуктовый код, а не опечатку в гайде. Пустой список файлов (сообщение без
|
||||
* `--staged`) — не документационный коммит: судить нечем, правило прежнее.
|
||||
* Трейлеры, если они есть, судятся всегда.
|
||||
*/
|
||||
export function isDocsOnlyCommit(changedFiles = []) {
|
||||
return changedFiles.length > 0 && changedFiles.every((file) => classify(file.replaceAll('\\', '/')) === 'C');
|
||||
}
|
||||
|
||||
export function validateCommitMessage(message, changedFiles = [], { baselineIndex = undefined, authorDate = null } = {}) {
|
||||
const trailers = terminalTrailers(message);
|
||||
const errors = [];
|
||||
const issues = trailers.get('Issue') || [];
|
||||
if (!issues.length || issues.some((value) => !/^#[1-9][0-9]*$/.test(value))) {
|
||||
const visible = trailers.get('User-Visible') || [];
|
||||
const exempt = isDocsOnlyCommit(changedFiles) && !issues.length && !visible.length;
|
||||
if (!exempt && (!issues.length || issues.some((value) => !/^#[1-9][0-9]*$/.test(value)))) {
|
||||
errors.push("missing or invalid terminal 'Issue: #<positive number>' trailer");
|
||||
}
|
||||
const visible = trailers.get('User-Visible') || [];
|
||||
if (visible.length !== 1 || !/^(yes|no)$/.test(visible[0])) {
|
||||
if (!exempt && (visible.length !== 1 || !/^(yes|no)$/.test(visible[0]))) {
|
||||
errors.push("expected exactly one terminal 'User-Visible: yes|no' trailer");
|
||||
}
|
||||
const normalizedFiles = changedFiles.map((file) => file.replaceAll('\\', '/'));
|
||||
|
||||
Reference in New Issue
Block a user