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/`
|
||||
(ставятся без 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, а **дашборд + читалка**: библиотека книг,
|
||||
|
|
|
|||
|
|
@ -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 — включается, когда
|
||||
|
|
|
|||
|
|
@ -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` закрытие дыр после ревью.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. Вместо него **контракт-тест токенов**:
|
||||
|
|
|
|||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
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