fix(scripts): compute next ADR as max+1 not count+1 in scaffold (#182)

* fix(scripts): compute next ADR as max+1 not count+1 in scaffold

* docs(handoff): add handoff and ADR-079 for ADR numbering fix

* docs(handoff): set PR number

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-07-31 22:13:30 +03:00 committed by GitHub
parent e954284ec3
commit 0bf97cf1c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 56 additions and 3 deletions

View file

@ -49,9 +49,25 @@ if [ -n "$EXISTING_ADR" ]; then
ADR="$EXISTING_ADR"
echo "ADR already exists: $ADR"
else
NEXT_N="$(ls "$ADR_DIR" 2>/dev/null | grep -E '^[0-9]{3}-' | wc -l | awk '{print $1+1}')"
# Next ADR number = max(existing) + 1, NOT count + 1.
# count+1 collides when files are added out of order or in parallel
# (e.g. two PRs both computed wc -l = 72 → both wrote ADR-073, see issue #176).
# max+1 is race-resistant: each run reads the current highest number.
# Guard loop handles the residual race: if max+1 file already exists
# (created between the max-read and our write), increment to a free slot.
# 10# forces base-10 (leading zeros like 078 are invalid octal in bash).
# `|| true` neutralizes grep's exit 1 under set -euo pipefail when the
# directory is empty (no ADRs yet → max=0 → next=001).
MAX_N="$(ls "$ADR_DIR" 2>/dev/null | grep -E '^[0-9]{3}-' | cut -c1-3 | sort -n | tail -n1 || true)"
NEXT_N=$((10#${MAX_N:-0} + 1))
while :; do
NN="$(printf "%03d" "$NEXT_N")"
ADR="$ADR_DIR/${NN}-pr-${PR}-${SLUG}.md"
if ! ls "$ADR_DIR"/${NN}-pr-*.md >/dev/null 2>&1; then
break
fi
NEXT_N=$((NEXT_N + 1))
done
cat > "$ADR" <<EOF
# ADR-${NN}: <title>

View file

@ -0,0 +1,18 @@
# ADR-079: ADR numbering = max(existing)+1, not count+1
## Статус
Accepted (2026-07-31, PR#182)
## Контекст
`scaffold-handoff.sh` вычислял номер следующего ADR как `ls docs/decisions | grep -E '^[0-9]{3}-' | wc -l + 1`. Это привело к коллизии: PR#174 и PR#175 оба создали `ADR-073` (два файла `073-pr-174-*.md` и `073-pr-175-*.md`), так как оба запуска посчитали одинаковое количество файлов. Алгоритм `count+1` неустойчив к гонкам, ручному добавлению ADR и «дырам» в нумерации (удалённые файлы оставляют пропуски, счётчик отстаёт от max).
## Решение
Заменить `count+1` на `max(existing)+1`: `ls | grep -E '^[0-9]{3}-' | cut -c1-3 | sort -n | tail -n1` берёт максимальный существующий номер, +1 даёт следующий. Дополнительно:
- `10#${MAX_N:-0}` — base-10 forcing (номера вида `078` невалидны в bash octal arithmetic).
- `|| true` на grep pipeline — нейтрализует exit 1 при пустой директории (`set -euo pipefail`).
- Guard loop — если вычисленный `NN` уже занят (residual race: файл создан между max-read и write), инкремент до свободного слота.
## Альтернативы
- Atomic file lock (`flock`) — отвергнут: избыточно для git-репозитория, где commits серийные; guard loop покрывает residual window.
- Проверка только существования файла перед записью (без max+1) — отвергнута: не решает «дыры» в нумерации и не дает детерминированный следующий номер.
- Переименование существующих ADR-073 — вне scope (ломает ссылки в memory/handoff/AGENTS.md, issue #176 явно исключает).

View file

@ -0,0 +1,19 @@
---
pr: 182
title: fix(scripts): ADR numbering collision in scaffold-handoff.sh
---
## Что сделано
`scaffold-handoff.sh` вычислял next ADR как `ls | wc -l + 1`. Заменён на `max(existing) + 1` с guard loop для residual race и base-10 forcing (`10#`) для leading-zero номеров. Добавлены комментарии, объясняющие почему max+1, а не count+1.
## Почему
`count+1` не гарантирует уникальность: при параллельных pipeline или ручном добавлении ADR счётчик отстаёт → коллизия (два файла `073-pr-174-*.md` и `073-pr-175-*.md`). `max+1` race-resistant: каждый запуск читает текущий max. Guard loop покрывает residual race (файл создан между max-read и write).
## Pending
— (после merge: memory-syncer обновит ADR Index — добавит ADR-079, оставит ADR-073 collision note как есть)
## Watch out
- Существующие ADR-073 (оба) НЕ переименовывались — вне scope, ссылки в memory/handoff уже расставлены.
- `10#` обязателен: номера `078`/`079` с leading zero невалидны в bash octal arithmetic.
- `|| true` на grep обязателен: empty dir → grep exit 1 → `set -euo pipefail` убивает скрипт.
- Тестового слоя для scaffold нет — критерий «если есть test-слой» не применим.