Add self-installing pre-commit gate: frontend commits demand green check, zone-mixing and never-commit files blocked, plus exact version pins and engine-strict npmrc

This commit is contained in:
Claude (backend session) 2026-08-02 19:39:29 +03:00
parent 553f1a33cc
commit 7832be76f4
9 changed files with 169 additions and 8 deletions

6
frontend/.npmrc Normal file
View file

@ -0,0 +1,6 @@
# engines из package.json — ошибка установки, а не молчаливое предупреждение:
# на старом Node каскад падает дальше по цепочке непонятнее.
engine-strict=true
# Пины точные; ^ при доустановке пакета появляться не должен (политика STACK_DECISIONS §1).
save-exact=true

View file

@ -16,6 +16,10 @@ npm run shot # снимок витрины в .shots/ — открыть
Для скриншот-цикла нужен Chromium Playwright и локальные библиотеки в `.tooling/`
(ставятся без sudo, процедура — `docs/FRONTEND_PLAN.md` §4).
`npm install` заодно ставит pre-commit хук (`scripts/githooks/`): коммит с frontend-путями
не проходит без зелёного `npm run check`; смесь frontend/ с чужой зоной и файлы
«никогда не коммитить» блокируются для всех (D39.88). Обход — `git commit --no-verify`.
## Что здесь будет
Веб-приложение поверх `../platform/`. MVP — не IDE, а **дашборд + читалка**: библиотека книг,

View file

@ -433,7 +433,13 @@ JSX-спред `<div {...props} />` со `style` внутри объекта ·
обе половины гейта цвета жили в одной команде, а CSS-половину гоняет именно stylelint.
`npm run check:full` = `check``vite build``shot`. Отдельного e2e-набора в S1 нет:
единственная браузерная проверка — скриншот-цикл, он и стоит в `check:full`.
Git-хуков нет.
Git-хук один — pre-commit (запрос владельца 02.08, вторая фронт-сессия; отменяет прежнее
«хуков нет»: CI в репозитории отсутствует, до его появления хук — единственный машинный рубеж).
Зонный фрагмент `scripts/githooks/pre-commit` зовёт тот же `npm run check` (один список
инструментов, не дубль) только когда в коммите есть frontend-пути; плюс блок файлов
«никогда не коммитить» и блок смеси frontend/ с чужой зоной (картина инцидентов D39.88).
Ставится сам: `npm install` через prepare кладёт диспетчер в `.git/hooks/pre-commit`
(идемпотентно, чужой хук не перетирает). Осознанный обход — `git commit --no-verify`.
**Не входит в S1** (и не должно появиться раньше срока): экраны, оболочка трёх панелей,
слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда

View file

@ -18,6 +18,9 @@
после того, как две ошибки S1 пришли из непрочитанного канона; читать до кода, не после.
- `npm run check` зелёный (5 шагов, 31 тест, тип-осведомлённый линт), `npm run check:full`
зелёный, гейт доступности в скриншот-цикле зелёный.
- **Гейты стоят и на коммите:** pre-commit хук (02.08, вторая сессия) гоняет `npm run check`
для коммитов с frontend-путями и блокирует смесь зон и файлы «никогда не коммитить»;
ставится сам при `npm install`. Обход — только осознанный `git commit --no-verify`.
## Решения владельца по продукту
@ -171,7 +174,40 @@ NDJSON — это шов ДВИЖОК↔ПЛАТФОРМА (D39.85), а фрон
## Хроника
### 02.08 — сессия S0+S1 (первая фронт-сессия)
### 02.08 — вторая фронт-сессия: ревью скелета свежим взглядом + защита коммитов
**Ревью S0/S1 подтверждает состояние:** `check` (5 гейтов, 31 тест), `build` (377 мс, Rolldown)
и `npm audit` (0 уязвимостей) зелёные; конфиги перечитаны построчно — расхождений с доками
не найдено; известные хвосты гейтов уже честно лежат в Ф-9/Ф-10, не дублировал.
**Главная дыра скелета была не в коде, а вокруг него:** CI нет (проверено: ни `.github/`,
ни других CI-конфигов), git-хуков нет — то есть вся построенная S1 система гейтов работала,
только если сессия сама вспомнит про `npm run check`. При этом `START_PROMT.MD` трекается
и почти всегда модифицирован — голый `git commit -a` унёс бы его молча.
**Закрыто pre-commit хуком** (запрос владельца; отменяет «Git-хуков в MVP нет» из
`STACK_DECISIONS.md` §3 — та строка писалась в паре с CI, которого нет):
- зонный фрагмент `scripts/githooks/pre-commit` (трекается): для коммитов с frontend-путями —
тот же `npm run check` (~10 сек), не дубль списка инструментов; для ЛЮБОГО коммита — блок
файлов «никогда не коммитить» (`START_PROMT.MD`, `.claude/settings.local.json`) и блок
смеси frontend/ с чужой зоной — машинное принуждение D39.88 (легитимной смеси не существует:
фронт коммитит только свою зону, чужие зоны frontend/ не коммитят);
- локальный диспетчер `.git/hooks/pre-commit` (не в git) зонно-нейтрален: подхватывает
`<зона>/scripts/githooks/pre-commit` любой зоны без правки себя; ставится инсталлером
`install.mjs` из npm `prepare` — каждый `npm install` сам обновляет защиту;
- проверено девятью сценариями в изолированном клоне: чужая зона проходит мгновенно ·
запрещённый файл блок · смесь зон блок · литеральный цвет в TSX валит check и блок ·
чистый коммит проходит · pathspec-коммит при чужом застейдженном файле не уносит чужое
(временный индекс git виден хуку корректно) · повторная установка идемпотентна ·
чужой pre-commit не перетирается · вне git-репозитория тихий пропуск;
- догфудинг по мандату самопроверки: хук поймал ошибку в собственном инсталлере
(TS7006 в `install.mjs` — strict-тайпчек `checkJs` дотягивается и до `scripts/`).
**Мелочи той же сессии:** два каретных пина (`^4.12.1` axe, `^4.7.2` eslint-comments)
приведены к точным — единственное расхождение с политикой пинов §1; заведён `.npmrc`
(`engine-strict` — несовпадение Node падает на установке, а не непонятно дальше;
`save-exact` — карет не появится при доустановке).
**Заленжено:** `e9a6bb2` план S0 · `b98afb5` весь код S1 · `d8437d6` закрытие дыр после ревью.

View file

@ -107,7 +107,10 @@ npm run check:full # + vite build + e2e
CI вызывает **именно их**, а не дублирует список инструментов. Path-фильтры на уровне job'ов
(правка CSS не должна гонять тесты Go) плюс агрегирующий job с явной проверкой
`contains(needs.*.result,'failure')||contains(needs.*.result,'cancelled')`. Git-хуков в MVP нет.
`contains(needs.*.result,'failure')||contains(needs.*.result,'cancelled')`. ~~Git-хуков в MVP нет~~
пересмотрено 02.08 запросом владельца: CI ещё не поднят, и до него pre-commit — единственный
машинный рубеж. Хук зовёт те же `npm run check`-команды, не дубль списка (`scripts/githooks/`,
детали — `FRONTEND_PLAN.md` §7).
**Визуальный гейт с эталонными скриншотами — ОТЛОЖЕН.** Он флейкует между платформами, а наш
референс снят на macOS при 2x, целевая платформа — Windows. Вместо него **контракт-тест токенов**:

View file

@ -16,8 +16,8 @@
"react-router": "8.3.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.12.1",
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
"@axe-core/playwright": "4.12.1",
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
"@eslint/js": "10.0.1",
"@testing-library/react": "16.3.2",
"@types/node": "22.20.1",

View file

@ -12,7 +12,8 @@
"preview": "vite preview",
"check": "prettier --check . && eslint . --max-warnings 0 && stylelint \"src/**/*.css\" && tsc --noEmit && vitest run",
"check:full": "npm run check && npm run build && npm run shot",
"shot": "node scripts/shot.mjs"
"shot": "node scripts/shot.mjs",
"prepare": "node scripts/githooks/install.mjs"
},
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
@ -23,8 +24,8 @@
"react-router": "8.3.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.12.1",
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
"@axe-core/playwright": "4.12.1",
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
"@eslint/js": "10.0.1",
"@testing-library/react": "16.3.2",
"@types/node": "22.20.1",

View file

@ -0,0 +1,57 @@
// Ставит в .git/hooks/pre-commit диспетчер зонных хуков. Идемпотентен: свой (по маркеру)
// перезаписывает свежей версией, чужой не трогает. Зовётся из npm prepare — то есть
// каждый npm install/ci в frontend/ ставит защиту сам, отдельного шага у сессии нет.
import { execSync } from 'node:child_process';
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
const MARKER = 'textmachine zone-hook dispatcher';
// Диспетчер зонно-нейтрален: каждая зона может положить свой фрагмент в
// <зона>/scripts/githooks/pre-commit, и он подхватится без правки этого файла.
const dispatcher = `#!/bin/sh
# ${MARKER} v1 сгенерирован frontend/scripts/githooks/install.mjs; правки перетираются.
status=0
for hook in */scripts/githooks/pre-commit; do
[ -x "$hook" ] || continue
"$hook" || status=1
done
exit $status
`;
/** @param {string} args */
function git(args) {
return execSync(`git ${args}`, { encoding: 'utf8' }).trim();
}
let gitDir;
try {
gitDir = git('rev-parse --git-common-dir');
} catch {
console.log('githooks: не git-репозиторий — установка хука пропущена');
process.exit(0);
}
let hooksPath = '';
try {
hooksPath = git('config core.hooksPath');
} catch {
// не задан — стандартное расположение, путь ниже
}
if (hooksPath !== '') {
console.log(`githooks: задан core.hooksPath=${hooksPath} — ставить туда не берусь.`);
console.log('Подключите frontend/scripts/githooks/pre-commit из своего pre-commit вручную.');
process.exit(0);
}
const hooksDir = resolve(process.cwd(), gitDir, 'hooks');
const target = resolve(hooksDir, 'pre-commit');
if (existsSync(target) && !readFileSync(target, 'utf8').includes(MARKER)) {
console.log(`githooks: ${target} уже существует и написан не нами — не перетираю.`);
console.log('Подключите frontend/scripts/githooks/pre-commit из своего pre-commit вручную.');
process.exit(0);
}
mkdirSync(hooksDir, { recursive: true });
writeFileSync(target, dispatcher);
chmodSync(target, 0o755);
console.log(`githooks: pre-commit диспетчер установлен → ${target}`);

View file

@ -0,0 +1,48 @@
#!/bin/sh
# Зонный pre-commit фрагмент frontend/. Запускается диспетчером .git/hooks/pre-commit,
# которого ставит scripts/githooks/install.mjs (автоматически при npm install).
# Для коммитов без frontend-путей отрабатывает за миллисекунды и молчит.
# Крайний обход (осознанный, в лог не попадает): git commit --no-verify.
staged=$(git diff --cached --name-only)
[ -z "$staged" ] && exit 0
# 1) Файлы из списка «никогда не коммитить» (CLAUDE.md, гардрейлы): живой бриф владельца
# и личные настройки прав. START_PROMT.MD трекается и часто модифицирован — голый
# `git commit -a` унёс бы его молча.
forbidden=$(printf '%s\n' "$staged" | grep -E '^START_PROMT\.MD$|(^|/)\.claude/settings\.local\.json$')
if [ -n "$forbidden" ]; then
echo 'pre-commit: в коммите файл из списка «никогда не коммитить» (CLAUDE.md, гардрейлы):' >&2
printf ' %s\n' "$forbidden" >&2
echo 'Коммитьте pathspec-формой без него: git commit -m "..." -- <свои пути>' >&2
exit 1
fi
# 2) Смесь frontend/ и чужой зоны в одном коммите — картина обоих инцидентов 02.08
# (голый `git commit` унёс чужой индекс, D39.88). Фронт коммитит ТОЛЬКО frontend/,
# остальные зоны frontend/ не коммитят — легитимной смеси не существует.
front=$(printf '%s\n' "$staged" | grep -c '^frontend/')
other=$(printf '%s\n' "$staged" | grep -cv '^frontend/')
if [ "$front" -gt 0 ] && [ "$other" -gt 0 ]; then
echo 'pre-commit: коммит смешивает frontend/ с другой зоной — так уезжает чужой индекс (D39.88).' >&2
echo 'Разделите: git commit -m "..." -- <пути одной зоны>' >&2
printf '%s\n' "$staged" | sed 's/^/ /' >&2
exit 1
fi
[ "$front" -eq 0 ] && exit 0
# 3) Гейт качества зоны: тот же npm run check, что гоняют сессии руками, — один список
# инструментов, не дубль (STACK_DECISIONS §3 «одна команда проверки»).
if [ ! -d frontend/node_modules ]; then
echo 'pre-commit: нет frontend/node_modules — выполните npm ci в frontend/ и повторите.' >&2
exit 1
fi
if ! git diff --quiet -- frontend/; then
echo 'pre-commit: в frontend/ есть незакоммиченные правки вне индекса; check идёт по рабочему дереву.' >&2
fi
echo 'pre-commit: npm run check (frontend), ~10 сек...' >&2
if ! npm --prefix frontend run check; then
echo 'pre-commit: check красный — коммит остановлен. Чинить, не обходить.' >&2
exit 1
fi