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:
@@ -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-токене с валидной сессией.
|
||||
```
|
||||
@@ -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`
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user