From 7832be76f40067d53ce0e62ccf662a25ebf8d80d Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sun, 2 Aug 2026 19:39:29 +0300 Subject: [PATCH] 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 --- frontend/.npmrc | 6 +++ frontend/README.md | 4 ++ frontend/docs/FRONTEND_PLAN.md | 8 +++- frontend/docs/PROGRESS.md | 38 +++++++++++++++++- frontend/docs/STACK_DECISIONS.md | 5 ++- frontend/package-lock.json | 4 +- frontend/package.json | 7 ++-- frontend/scripts/githooks/install.mjs | 57 +++++++++++++++++++++++++++ frontend/scripts/githooks/pre-commit | 48 ++++++++++++++++++++++ 9 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 frontend/.npmrc create mode 100644 frontend/scripts/githooks/install.mjs create mode 100755 frontend/scripts/githooks/pre-commit diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 00000000..ba2681da --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,6 @@ +# engines из package.json — ошибка установки, а не молчаливое предупреждение: +# на старом Node каскад падает дальше по цепочке непонятнее. +engine-strict=true + +# Пины точные; ^ при доустановке пакета появляться не должен (политика STACK_DECISIONS §1). +save-exact=true diff --git a/frontend/README.md b/frontend/README.md index 2425101f..679247df 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -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, а **дашборд + читалка**: библиотека книг, diff --git a/frontend/docs/FRONTEND_PLAN.md b/frontend/docs/FRONTEND_PLAN.md index d93e1db9..f3aa362f 100644 --- a/frontend/docs/FRONTEND_PLAN.md +++ b/frontend/docs/FRONTEND_PLAN.md @@ -433,7 +433,13 @@ JSX-спред `
` со `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 — включается, когда diff --git a/frontend/docs/PROGRESS.md b/frontend/docs/PROGRESS.md index 036f8946..153f4ff6 100644 --- a/frontend/docs/PROGRESS.md +++ b/frontend/docs/PROGRESS.md @@ -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` закрытие дыр после ревью. diff --git a/frontend/docs/STACK_DECISIONS.md b/frontend/docs/STACK_DECISIONS.md index 962915ba..f1f25ee0 100644 --- a/frontend/docs/STACK_DECISIONS.md +++ b/frontend/docs/STACK_DECISIONS.md @@ -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. Вместо него **контракт-тест токенов**: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4fe62175..689317a7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index ed3d0a28..354fe60a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/scripts/githooks/install.mjs b/frontend/scripts/githooks/install.mjs new file mode 100644 index 00000000..09b2d5d2 --- /dev/null +++ b/frontend/scripts/githooks/install.mjs @@ -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}`); diff --git a/frontend/scripts/githooks/pre-commit b/frontend/scripts/githooks/pre-commit new file mode 100755 index 00000000..83c21f16 --- /dev/null +++ b/frontend/scripts/githooks/pre-commit @@ -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