docs: update AGENTS and README to include guidelines for Conventional Commits

- Added a section on Conventional Commits in AGENTS.md, detailing the process for generating commit messages.
- Enhanced README.md with references to the Conventional Commits rules and the necessary scripts for generating commit messages.
- Clarified the format for commit messages, specifying the language requirements for headers and bodies.
This commit is contained in:
Denozordec
2026-05-20 00:48:21 +07:00
parent c263fd5c7e
commit 4c23232c4e
5 changed files with 430 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
---
description: >-
Conventional Commits EvoBGP. Триггеры commit/коммит/закоммить/staged/commit message —
ОБЯЗАТЕЛЬНО сначала Shell scripts/commit/staged-context.ps1, скилл commit-message.
alwaysApply: false
---
# Conventional Commits (EvoBGP)
## Триггеры (применить правило + скилл)
Любой запрос на коммит или сообщение коммита: `commit`, `коммит`, `закоммить`, `git commit`, `commit message`, `conventional commit`, `staged`, «сгенерируй коммит» — в т.ч. если это указано в плане или [AGENTS.md](../../AGENTS.md).
## Обязательный запуск скрипта (MUST)
1. Прочитать скилл [`.cursor/skills/commit-message/SKILL.md`](../skills/commit-message/SKILL.md).
2. **Первым действием** выполнить Shell (из корня репо):
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
3. Сообщение коммита строить **только** по JSON из stdout скрипта (`groups`, `diff_excerpt`, `stat`).
4. **Запрещено** генерировать commit message без успешного (exit 0) запуска скрипта; не заменять скрипт одним `git diff --cached`.
## Формат (строго)
```
<type>(<scope>): <short summary in English>
<тело на русском: что изменено и зачем>
```
- **Заголовок** — только английский; императив, без точки в конце; ≤72 символов.
- **Тело** — только русский; полные предложения; пустая строка после заголовка.
- Запрещены vague-сообщения: `fix bug`, `update code`, `wip`, `misc`.
## Типы (semantic-release)
| type | Когда | Версия |
|------|--------|--------|
| `feat` | новая функциональность | minor |
| `fix` | исправление бага | patch |
| `perf` | ускорение без смены API | patch |
| `refactor` | реструктуризация без смены поведения | — |
| `docs` | только документация | — |
| `test` | тесты | — |
| `ci` | CI/CD (`.gitea/`, workflows) | — |
| `chore` | обслуживание, deps, `.cursor/` | — |
## Breaking changes
- Заголовок: `feat!` / `fix!` **или** в теле строка `BREAKING CHANGE:` (на английском ключевое слово) + описание impact **на русском**.
## Scope (EvoBGP)
Выбирать по доминирующему пути из staged diff:
| Префикс | scope |
|---------|-------|
| `internal/httpapi/` | `httpapi` |
| `internal/store/`, `internal/repository/`, `internal/db/` | `store` |
| `internal/jobs/` | `jobs` |
| `internal/pipeline/` | `pipeline` |
| `internal/birdfmt/` | `birdfmt` |
| `internal/birddeploy/` | `birddeploy` |
| `internal/bundle/`, `internal/signing/` | `bundle` |
| `cmd/` | `cmd` |
| `web/` | `web` |
| `docs/openapi.yaml`, `redocly.yaml` | `openapi` |
| `docs/` (остальное) | `docs` |
| `migrations/` | `db` |
| `.gitea/` | `ci` |
| `deploy/` | `deploy` |
| `.cursor/` | `chore` |
| прочее в корне | `chore` |
`type` определять по **содержимому diff**, не только по пути.
## Multi-change
- Несвязанные области → **отдельные коммиты** (auto-split по скиллу).
- Одна логическая фича через несколько scope (OpenAPI + httpapi + web) → **один** коммит, scope по главной области.
## Обязательный вывод агенту
Для каждого коммита:
**1. Готовое сообщение** (копировать в `git commit -m` / HEREDOC):
```
<type>(<scope>): <summary>
<тело RU>
```
**2. Пояснение (RU):** почему выбран type; риск/impact; был ли split.
## Примеры
```
feat(httpapi): add version endpoint and wire web footer
Добавлен GET /v1/version. В Web UI версия в footer берётся из API вместо захардкоженного значения.
```
```
fix(auth): correct token validation edge case
Исправлена ложная 401 при истёкшем refresh-токене с валидной сессией.
```
+124
View File
@@ -0,0 +1,124 @@
---
name: commit-message
description: >-
ОБЯЗАТЕЛЬНО при commit, коммит, закоммить, commit message, conventional commit,
staged, semantic-release, «сгенерируй коммит», git commit: ПЕРВЫМ делом Shell —
scripts/commit/staged-context.ps1; затем Conventional Commit (заголовок EN, тело RU).
---
# Commit message (EvoBGP)
## Когда применять (сразу читать этот скилл)
Триггеры в промпте пользователя или в плане/AGENTS.md:
- `commit`, `коммит`, `закоммить`, `commit message`, `conventional commit`
- `git commit`, `staged`, `сообщение коммита`, `сгенерируй коммит`
- агент собирается выполнить `git commit` или предложить текст коммита
## Шаг 0 — ОБЯЗАТЕЛЬНО (до любого текста коммита)
**Первый вызов инструментов** в этой задаче — Shell из корня репозитория:
```powershell
powershell -NoProfile -File scripts/commit/staged-context.ps1
```
| Правило | Деталь |
|---------|--------|
| **MUST** | Запустить скрипт до генерации заголовка/тела коммита |
| **MUST NOT** | Строить сообщение только по `git diff --cached` / `git status` без скрипта |
| **MUST NOT** | Пропускать скрипт, даже если diff «и так понятен» |
| Exit `1` | Index пуст — сообщить пользователю, **не коммитить** |
| Exit `0` | Разобрать JSON stdout: `staged_count`, `groups[]` (`scope`, `files`, `stat`, `diff_excerpt`) |
Дополнительно: [.cursor/rules/conventional-commits.mdc](../../rules/conventional-commits.mdc).
## Слияние групп (одна фича)
Скрипт группирует **только по путям**. Перед split проверьте логическую связность:
**Объединить в один коммит**, если это одна задача:
- `docs/openapi.yaml` + `internal/httpapi/` (+ опционально `web/`) — один endpoint/контракт;
- `migrations/` + `internal/store/` / `repository/` — одна схема;
- правки теста рядом с кодом той же фичи.
При объединении: один scope (доминирующий пакет, часто `httpapi` или `openapi`), один type, одно тело RU.
**Auto-split** — если группы **не связаны** (например `web/` + `internal/birdfmt/` без общего смысла): **отдельный коммит на группу**, без вопроса пользователю.
## Порядок коммитов при split
1. `openapi`
2. `httpapi`, `store`, `jobs`
3. `pipeline`, `birdfmt`, `birddeploy`, `bundle`
4. `web`
5. `ci`, `deploy`, `db`
6. `docs`
7. `chore`, `cmd`
## Алгоритм auto-split (уровень файлов)
1. `git status` и `git diff --cached --stat` — зафиксировать полный список staged-файлов.
2. `git reset HEAD` — снять всё из index (working tree не трогать).
3. Для каждой (объединённой) группы по порядку выше:
- `git add -- <paths…>`
- Сгенерировать сообщение по правилу conventional-commits.
- Закоммитить (см. ниже).
4. После всех коммитов — `git status` для проверки.
**Ограничение:** частичный stage одного файла с разной семантикой — предупредить; split по hunk'ам не делать; предложить разнести правки по файлам.
## Создание коммита
Только если пользователь **явно** просил закоммитить. Иначе — только вывести готовые сообщения.
```powershell
git commit -m "$( @'
<type>(<scope>): <summary in English>
<Тело на русском.>
'@ )"
```
В PowerShell для многострочного тела используйте here-string как выше или `-m` для заголовка и `-m` для тела.
Перед коммитом: `git status`, `git diff --cached` для группы — убедиться, что stage соответствует сообщению.
## Генерация текста
По `groups[].diff_excerpt`, `stat`, `files`:
- **type** — по смыслу diff (`feat` / `fix` / …), не по умолчанию `chore`.
- **scope** — из JSON группы или доминирующий при merge.
- **summary** — конкретный, английский, императив.
- **body** — русский: что, зачем, edge cases, breaking impact.
## Вывод пользователю
Для **каждого** коммита:
### 1. Готовое сообщение
```
<type>(<scope>): <summary>
<тело>
```
### 2. Пояснение (RU)
- Почему выбран type/scope.
- Риски и impact.
- Split: сколько коммитов и почему.
## Не смешивать
Один commit message — одна primary intent. Не объединять несвязанный `fix` и `feat` в один заголовок.
## Ссылки
- Правило: `.cursor/rules/conventional-commits.mdc`
- Скрипт: `scripts/commit/staged-context.ps1`
- Просмотр групп: `powershell -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`
+9
View File
@@ -36,6 +36,15 @@
- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи.
- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины.
## Коммиты (Conventional Commits)
Если пользователь просит **коммит**, **commit message**, **закоммить**, **git commit** или это следует из плана — **сразу**:
1. Shell: `powershell -NoProfile -File scripts/commit/staged-context.ps1` (первый вызов, до текста коммита).
2. Скилл [.cursor/skills/commit-message/SKILL.md](.cursor/skills/commit-message/SKILL.md) и правило [.cursor/rules/conventional-commits.mdc](.cursor/rules/conventional-commits.mdc).
Без вывода скрипта (exit 0) **не** придумывать сообщение коммита. Заголовок — EN, тело — RU; несвязанные области — auto-split (скилл).
## Команды и среда
- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`.
+5
View File
@@ -31,6 +31,11 @@
| [../.cursor/rules/engineering.mdc](../.cursor/rules/engineering.mdc) | Go, API, migrations, security, enforcement |
| [../.cursor/rules/web-shadcn.mdc](../.cursor/rules/web-shadcn.mdc) | SvelteKit, shadcn-svelte |
| [../.cursor/rules/networking-bird.mdc](../.cursor/rules/networking-bird.mdc) | BIRD2, BGP policy, IP/CIDR |
| [../.cursor/rules/conventional-commits.mdc](../.cursor/rules/conventional-commits.mdc) | Conventional Commits (заголовок EN, тело RU) |
### Сообщения коммитов (Cursor)
После `git add` попросите агента: **«сгенерируй коммит по staged»**, **«закоммить»**, **«commit message»** — агент **обязан первым делом** запустить `scripts/commit/staged-context.ps1`, затем скилл [commit-message](../.cursor/skills/commit-message/SKILL.md) (заголовок EN, тело RU, auto-split). Просмотр групп вручную: `powershell -NoProfile -File scripts/commit/staged-context.ps1 | ConvertFrom-Json`.
## Репозиторий и CI
+181
View File
@@ -0,0 +1,181 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Staged git changes grouped by EvoBGP scope for Conventional Commit generation.
.DESCRIPTION
Outputs JSON to stdout: file groups, --stat, and truncated patch per group.
Exit 1 if nothing is staged. Does not write commit messages.
.EXAMPLE
powershell -NoProfile -File scripts/commit/staged-context.ps1
powershell -NoProfile -File scripts/commit/staged-context.ps1 | ConvertFrom-Json
#>
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$MaxLinesPerGroup = 120
$MaxLinesTotal = 500
function Test-GitRepository {
$null = git rev-parse --git-dir 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Error 'staged-context: not a git repository'
exit 2
}
}
function Get-ScopeForPath {
param([string]$Path)
$p = $Path -replace '\\', '/'
# Longest / most specific prefixes first
$rules = @(
@{ Prefix = 'docs/openapi.yaml'; Scope = 'openapi' }
@{ Prefix = 'redocly.yaml'; Scope = 'openapi' }
@{ Prefix = 'internal/httpapi/'; Scope = 'httpapi' }
@{ Prefix = 'internal/store/'; Scope = 'store' }
@{ Prefix = 'internal/repository/'; Scope = 'store' }
@{ Prefix = 'internal/db/'; Scope = 'store' }
@{ Prefix = 'internal/jobs/'; Scope = 'jobs' }
@{ Prefix = 'internal/pipeline/'; Scope = 'pipeline' }
@{ Prefix = 'internal/birdfmt/'; Scope = 'birdfmt' }
@{ Prefix = 'internal/birddeploy/'; Scope = 'birddeploy' }
@{ Prefix = 'internal/bundle/'; Scope = 'bundle' }
@{ Prefix = 'internal/signing/'; Scope = 'bundle' }
@{ Prefix = 'cmd/'; Scope = 'cmd' }
@{ Prefix = 'web/'; Scope = 'web' }
@{ Prefix = 'migrations/'; Scope = 'db' }
@{ Prefix = 'docs/'; Scope = 'docs' }
@{ Prefix = '.gitea/'; Scope = 'ci' }
@{ Prefix = 'deploy/'; Scope = 'deploy' }
@{ Prefix = '.cursor/'; Scope = 'chore' }
)
foreach ($rule in $rules) {
if ($p -eq $rule.Prefix.TrimEnd('/') -or $p.StartsWith($rule.Prefix)) {
return $rule.Scope
}
}
return 'chore'
}
function Get-ScopeSortOrder {
param([string]$Scope)
$order = @{
openapi = 10
httpapi = 20
store = 21
jobs = 22
pipeline = 30
birdfmt = 31
birddeploy = 32
bundle = 33
web = 40
ci = 50
deploy = 51
db = 52
docs = 60
cmd = 70
chore = 80
}
if ($order.ContainsKey($Scope)) { return $order[$Scope] }
return 99
}
function Invoke-Git {
param([string[]]$GitArgs)
$out = & git @GitArgs 2>&1
if ($LASTEXITCODE -ne 0) {
$msg = ($out | Out-String).Trim()
throw "git $($GitArgs -join ' ') failed: $msg"
}
return ($out | Out-String).TrimEnd()
}
function New-GitArgs {
param([string[]]$Base, [string[]]$Paths)
if ($Paths.Count -eq 0) { return $Base }
return $Base + '--' + $Paths
}
function Get-TruncatedDiff {
param(
[string[]]$Files,
[int]$MaxLines,
[ref]$TotalLinesUsed
)
if ($Files.Count -eq 0) { return '' }
$remaining = $MaxLinesTotal - $TotalLinesUsed.Value
if ($remaining -le 0) {
return '[diff truncated: global line budget exceeded]'
}
$cap = [Math]::Min($MaxLines, $remaining)
$diff = Invoke-Git -GitArgs (New-GitArgs -Base @(
'diff', '--cached', '--no-color', '--unified=3'
) -Paths $Files)
if ([string]::IsNullOrWhiteSpace($diff)) { return '' }
$lines = $diff -split "`n", -1
if ($lines.Count -le $cap) {
$TotalLinesUsed.Value += $lines.Count
return $diff
}
$truncated = ($lines[0..($cap - 1)] -join "`n") + "`n... [truncated: $($lines.Count - $cap) more lines]"
$TotalLinesUsed.Value += $cap
return $truncated
}
Test-GitRepository
$stagedRaw = Invoke-Git -GitArgs @('diff', '--cached', '--name-only')
if ([string]::IsNullOrWhiteSpace($stagedRaw)) {
[Console]::Error.WriteLine('staged-context: no staged changes (git index is empty)')
exit 1
}
$stagedFiles = @(
$stagedRaw -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
)
$stagedFiles = $stagedFiles | Sort-Object -Unique
$scopeBuckets = @{}
foreach ($f in $stagedFiles) {
$scope = Get-ScopeForPath -Path $f
if (-not $scopeBuckets.ContainsKey($scope)) {
$scopeBuckets[$scope] = [System.Collections.Generic.List[string]]::new()
}
$scopeBuckets[$scope].Add($f) | Out-Null
}
$totalLinesUsed = 0
$groups = New-Object System.Collections.Generic.List[object]
$scopeKeys = @($scopeBuckets.Keys | Sort-Object { Get-ScopeSortOrder $_ })
foreach ($scope in $scopeKeys) {
$files = @($scopeBuckets[$scope] | Sort-Object)
$stat = Invoke-Git -GitArgs (New-GitArgs -Base @('diff', '--cached', '--stat') -Paths $files)
$diffExcerpt = Get-TruncatedDiff -Files $files -MaxLines $MaxLinesPerGroup -TotalLinesUsed ([ref]$totalLinesUsed)
$groups.Add([ordered]@{
scope = $scope
files = $files
stat = $stat
diff_excerpt = $diffExcerpt
}) | Out-Null
}
$result = [ordered]@{
staged_count = $stagedFiles.Count
groups = $groups.ToArray()
}
$json = $result | ConvertTo-Json -Depth 6 -Compress:$false
# UTF-8 stdout for agents / ConvertFrom-Json
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
Write-Output $json
exit 0