diff --git a/.opencode/scripts/scaffold-handoff.sh b/.opencode/scripts/scaffold-handoff.sh index f7b4c99..e2d6fe9 100755 --- a/.opencode/scripts/scaffold-handoff.sh +++ b/.opencode/scripts/scaffold-handoff.sh @@ -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}')" - NN="$(printf "%03d" "$NEXT_N")" - ADR="$ADR_DIR/${NN}-pr-${PR}-${SLUG}.md" + # 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" < diff --git a/docs/decisions/079-pr-182-adr-numbering-collision.md b/docs/decisions/079-pr-182-adr-numbering-collision.md new file mode 100644 index 0000000..1294f60 --- /dev/null +++ b/docs/decisions/079-pr-182-adr-numbering-collision.md @@ -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 явно исключает). \ No newline at end of file diff --git a/docs/handoff/pr-182-adr-numbering-collision.md b/docs/handoff/pr-182-adr-numbering-collision.md new file mode 100644 index 0000000..ba8d5bb --- /dev/null +++ b/docs/handoff/pr-182-adr-numbering-collision.md @@ -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-слой» не применим. \ No newline at end of file