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:
parent
553f1a33cc
commit
7832be76f4
9 changed files with 169 additions and 8 deletions
6
frontend/.npmrc
Normal file
6
frontend/.npmrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# engines из package.json — ошибка установки, а не молчаливое предупреждение:
|
||||||
|
# на старом Node каскад падает дальше по цепочке непонятнее.
|
||||||
|
engine-strict=true
|
||||||
|
|
||||||
|
# Пины точные; ^ при доустановке пакета появляться не должен (политика STACK_DECISIONS §1).
|
||||||
|
save-exact=true
|
||||||
|
|
@ -16,6 +16,10 @@ npm run shot # снимок витрины в .shots/ — открыть
|
||||||
Для скриншот-цикла нужен Chromium Playwright и локальные библиотеки в `.tooling/`
|
Для скриншот-цикла нужен Chromium Playwright и локальные библиотеки в `.tooling/`
|
||||||
(ставятся без sudo, процедура — `docs/FRONTEND_PLAN.md` §4).
|
(ставятся без 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, а **дашборд + читалка**: библиотека книг,
|
Веб-приложение поверх `../platform/`. MVP — не IDE, а **дашборд + читалка**: библиотека книг,
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,13 @@ JSX-спред `<div {...props} />` со `style` внутри объекта ·
|
||||||
обе половины гейта цвета жили в одной команде, а CSS-половину гоняет именно stylelint.
|
обе половины гейта цвета жили в одной команде, а CSS-половину гоняет именно stylelint.
|
||||||
`npm run check:full` = `check` → `vite build` → `shot`. Отдельного e2e-набора в S1 нет:
|
`npm run check:full` = `check` → `vite build` → `shot`. Отдельного e2e-набора в S1 нет:
|
||||||
единственная браузерная проверка — скриншот-цикл, он и стоит в `check:full`.
|
единственная браузерная проверка — скриншот-цикл, он и стоит в `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** (и не должно появиться раньше срока): экраны, оболочка трёх панелей,
|
**Не входит в S1** (и не должно появиться раньше срока): экраны, оболочка трёх панелей,
|
||||||
слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда
|
слой данных и MSW, React Compiler (Ф-2), токен-гейт на отступы (Ф-4 — включается, когда
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@
|
||||||
после того, как две ошибки S1 пришли из непрочитанного канона; читать до кода, не после.
|
после того, как две ошибки S1 пришли из непрочитанного канона; читать до кода, не после.
|
||||||
- `npm run check` зелёный (5 шагов, 31 тест, тип-осведомлённый линт), `npm run check:full`
|
- `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` закрытие дыр после ревью.
|
**Заленжено:** `e9a6bb2` план S0 · `b98afb5` весь код S1 · `d8437d6` закрытие дыр после ревью.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,10 @@ npm run check:full # + vite build + e2e
|
||||||
|
|
||||||
CI вызывает **именно их**, а не дублирует список инструментов. Path-фильтры на уровне job'ов
|
CI вызывает **именно их**, а не дублирует список инструментов. Path-фильтры на уровне job'ов
|
||||||
(правка CSS не должна гонять тесты Go) плюс агрегирующий 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. Вместо него **контракт-тест токенов**:
|
референс снят на macOS при 2x, целевая платформа — Windows. Вместо него **контракт-тест токенов**:
|
||||||
|
|
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -16,8 +16,8 @@
|
||||||
"react-router": "8.3.0"
|
"react-router": "8.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.12.1",
|
"@axe-core/playwright": "4.12.1",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@types/node": "22.20.1",
|
"@types/node": "22.20.1",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "prettier --check . && eslint . --max-warnings 0 && stylelint \"src/**/*.css\" && tsc --noEmit && vitest run",
|
"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",
|
"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": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|
@ -23,8 +24,8 @@
|
||||||
"react-router": "8.3.0"
|
"react-router": "8.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.12.1",
|
"@axe-core/playwright": "4.12.1",
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@testing-library/react": "16.3.2",
|
"@testing-library/react": "16.3.2",
|
||||||
"@types/node": "22.20.1",
|
"@types/node": "22.20.1",
|
||||||
|
|
|
||||||
57
frontend/scripts/githooks/install.mjs
Normal file
57
frontend/scripts/githooks/install.mjs
Normal 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}`);
|
||||||
48
frontend/scripts/githooks/pre-commit
Executable file
48
frontend/scripts/githooks/pre-commit
Executable 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
|
||||||
Loading…
Add table
Reference in a new issue