Skip to content

release: 0.5.10 — dry-run 이 spec 번호를 태우던 것 - #12

Merged
bluecheat merged 1 commit into
mainfrom
feature/init-spec-dir-dry-run-no-reserve
Sep 9, 2026
Merged

bluecheat merged 1 commit into
mainfrom
feature/init-spec-dir-dry-run-no-reserve

Conversation

@bluecheat

@bluecheat bluecheat commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

무엇이 문제였나

init-spec-dir.sh --dry-run 이 "would create" 라고 보고해놓고 번호를 실제로 예약했어요.

$ init-spec-dir.sh --slug tmp-verify --tier standard --dry-run --json
{"status":"ok","files":["spec.md","tasks.md"]}     # 안 만든다더니…

$ ls .ax/docs/spec/ | tail -2
023-tmp-verify                                      # 빈 디렉토리가 남고
024-tmp-verify
$ ls .ax/docs/spec/.numbers/ | tail -2
023                                                 # 원장에 번호가 점유됨
024

원장 규약이 "한 번 쓰인 번호는 재사용 안 함" 이라 결번이 영구로 남아요. 실사용 리포에서 dry-run 두 번에 023·024 가 타고 다음 spec 이 025 부터 시작할 뻔했어요. 빈 디렉토리라 git 이 추적을 안 해서 git status 에는 .numbers/ 만 뜨고, 그것도 무시 대상이 아니라 커밋에 딸려갈 수 있었어요.

원인

--reserve 는 쓰기 예요 — 번호 원장 선점 + 디렉토리 생성까지 원자적으로 합니다. 그런데 호출부가 dry-run 상태에서도 --dry-run 을 안 넘겼어요.

# init-spec-dir.sh:82 (before)
RES=$(bash "$SCRIPT_DIR/next-spec-num.sh" --reserve --slug "$SLUG" --json 2>/dev/null || true)

왜 기존 검사가 못 잡았나

next-spec-num.sh 는 가드가 이미 양쪽에 있었어요 — --reserve 경로(:186)와 sync_ledger(:121). §39 도 "--dry-run 이 파일을 쓰면 그건 dry-run 이 아니에요" 를 next-spec-num·spec-review·tasks-gate 세 곳에 대해 검사하고요.

원시 함수는 지켜졌는데, 그걸 부르는 합성 지점이 목록에서 빠져 있었어요. §7 의 e2e 테스트가 init-spec-dir --dry-run 을 실제로 부르긴 하는데 JSON 봉투 모양만 보고 부작용은 안 봐요.

무엇을 바꿨나

  • dry-run 이면 next-spec-num.sh 에 --dry-run 을 넘겨요. jq 없을 때의 계산 fallback 도 같이 — --reserve 없이도 sync_ledger 가 .numbers/ 를 만들거든요.
  • RESERVED_BY_US 는 dry-run 에서 false 로 남겨요. --dry-run 만 넘기고 두면 아래 가드가 [ -e "$DEST" ] && [ "$RESERVED_BY_US" != true ] 라 통째로 꺼져서, 실재하는 spec 을 두고 만들 수 있다고 보고해요. 예약을 안 했으면 소유권도 없어요.

예약이 디렉토리를 만드는 설계 자체는 안 건드렸어요. 동시 세션 번호 경합 방어라서요 — 주석에 "실사용 리포에서 spec 2 쌍·ADR 7 쌍이 이렇게 겹쳤어요" 라고 근거가 남아 있고, RESERVED_BY_US 와 "예약된 디렉토리는 비어 있어야 정상" 가드가 그 설계를 떠받쳐요. dry-run 경로만 우회합니다.

옵션 전달은 변수 unquoted 전개 대신 if/else 로 갈랐어요. macOS bash 3.2 가 CI 대상이라 빈 배열 전개가 set -u 에서 터지고, unquoted 는 SC2086 을 부릅니다.

행동 변화 (의도된 것)

연속 dry-run 이 023·024 대신 023·023 을 줘요. 예약을 안 하니 당연한 결과이고, "dry-run 이 약속한 번호 = 실제로 받는 번호" 가 맞는 semantics 예요. 테스트로 고정했어요.

왜 릴리즈인가

수정이 templates/ 안에 있어서 버전이 올라야 프로젝트에 닿아요. 캐시가 버전 문자열로 디렉토리를 나누니(cache/goax/goax/<ver>/) 스탬프가 그대로면 marketplace update + install 을 돌려도 새 디렉토리가 안 생겨요 — 0.5.8 이 그렇게 멈춰 있었죠. 그래서 fix 커밋이 아니라 릴리즈로 냅니다.

검증

smoke 3 건 추가. 스크립트만 되돌려서 정말 회귀를 잡는지 확인했어요.

검사 패치본 스크립트만 되돌림
디렉토리·원장 둘 다 안 만듦 ✓ ✗ dirs=1, ledger=yes
연속 호출이 같은 번호 ✓ ✗ 002 → 003
일반 실행은 여전히 생성 ✓ ✗ (dry-run 이 약속한 번호와 실제 생성 번호 불일치)
  • 전체 smoke 559 통과 / 0 실패 (버전 일관성 4 건 + changelog 최신 ↔ VERSION 포함)

남은 질문

.ax/docs/spec/.numbers/ 가 .gitignore 에 없어요. 로컬 상태로 볼지 팀 공유 원장으로 볼지에 따라 갈려서 이 PR 에 넣지 않았어요 — 커밋하면 파일이 수백 개 생기고 머지 충돌이 잦아지고, 무시하면 같은 머신 동시 세션에서만 경합 방어가 됩니다. 원 설계 의도에 맞춰 정하는 게 맞을 것 같아요.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HMJX7Dvtu6KFKQH5sx9W5z

--reserve 는 번호 원장 선점 + 디렉토리 생성까지 하는 쓰기다. init-spec-dir 이
dry-run 일 때도 --dry-run 을 안 넘기고 불러서, "would create" 라고 보고해놓고
번호를 실제로 태웠다. 원장 규약이 "한 번 쓰인 번호는 재사용 안 함" 이라
결번이 영구로 남는다. 빈 디렉토리라 git 이 추적을 안 해서 git status 에는
.numbers/ 만 뜨고, 그것도 무시 대상이 아니라 커밋에 딸려갈 수 있었다.

실사용 리포에서 dry-run 두 번에 023·024 가 타고 025 부터 시작할 뻔했다.

next-spec-num.sh 자체는 --reserve 경로(line 186)와 sync_ledger(line 121)
양쪽에 가드가 있다. 안 넘긴 건 이 호출부뿐이었다. §39 가 "--dry-run 이 파일을
쓰면 그건 dry-run 이 아니에요" 를 세 스크립트에 대해 이미 검사하는데,
init-spec-dir 만 그 목록에서 빠져 있었다.

예약이 디렉토리를 만드는 설계 자체는 건드리지 않는다 — 동시 세션 번호 경합
방어라서 (주석: 실사용에서 spec 2 쌍·ADR 7 쌍이 겹침). dry-run 경로만 우회한다.

RESERVED_BY_US 를 dry-run 에서 false 로 유지하는 게 같이 필요하다. --dry-run
만 넘기고 두면, 아래 "이미 존재" 가드가 [ -e "$DEST" ] && [ ... != true ] 라
통째로 꺼져서 실재하는 spec 을 두고 만들 수 있다고 보고한다. 예약을 안 했으면
소유권도 없다. jq 없을 때의 계산 fallback 도 sync_ledger 가 .numbers/ 를
만들어서 같이 넘긴다.

행동 변화: 연속 dry-run 이 023·024 대신 023·023 을 준다. 예약을 안 하니 당연한
결과이고 그게 맞는 semantics 다. 테스트로 고정했다.

옵션 전달은 변수 unquoted 전개 대신 if/else 로 갈랐다. macOS bash 3.2 가 CI
대상이라 빈 배열 전개가 set -u 에서 터지고, unquoted 는 SC2086 을 부른다.

templates/ 안의 수정이라 버전이 올라야 프로젝트에 닿는다. 캐시가 버전
문자열로 디렉토리를 나눠서(cache/goax/goax/<ver>/) 스탬프가 그대로면
marketplace update + install 을 돌려도 새 디렉토리가 안 생긴다 — 0.5.8 이
그렇게 멈춰 있었다. 그래서 릴리즈로 낸다.

검증: 패치본 smoke 559 통과 0 실패 (버전 일관성 4건 + changelog 최신 ↔ VERSION 포함). 스크립트만 되돌리면 새 검사 3건이
전부 실패한다 — dirs=1·ledger=yes, 번호 002→003 증가, dry-run 이 약속한
번호와 실제 생성 번호 불일치.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMJX7Dvtu6KFKQH5sx9W5z
@bluecheat
bluecheat force-pushed the feature/init-spec-dir-dry-run-no-reserve branch from 033fef4 to 86451f2 Compare September 9, 2026 03:38
@bluecheat bluecheat changed the title fix(spec): init-spec-dir --dry-run 이 번호를 예약하던 것 release: 0.5.10 — dry-run 이 spec 번호를 태우던 것 Sep 9, 2026
@bluecheat
bluecheat merged commit fb3a07e into main Sep 9, 2026
2 checks passed
bluecheat pushed a commit that referenced this pull request Sep 9, 2026
`.ax/*.suggested` 는 한 단계만 잡는다. provision 은 세 곳 더 만든다 —
.ax/spirit/{values,tone,README}.md.suggested (§6.10),
.ax/mistakes/README.md.suggested (§6.5),
.ax/_templates/spec/<f>.suggested (§4-b).
전부 무시되지 않아 git status 에 ?? 로 뜨고, 실수로 커밋될 수 있었다.
실사용 리포에서 업데이트 한 번에 두 건이 실제로 샜다.

.ax/**/*.suggested 를 더한다.

smoke 의 기존 검사가 이걸 못 본 건 리터럴 grep 이라서다 — 다섯 엔트리가
파일에 다 있어도 중첩은 안 잡히는데, 있는지만 봤다. 그래서 임시 repo 에
템플릿을 깔고 git check-ignore 로 실제 판정을 보는 검사를 더한다.

버전은 올리지 않는다. 0.5.10 (#12) 위에 리베이스해서 그 릴리즈에 같이 실린다 —
templates/ 안의 수정이라 버전이 안 오르면 프로젝트에 안 닿는데, .gitignore 한 줄
때문에 릴리즈를 하나 더 파는 건 과하다. changelog/0.5.10.md 에 항목을 더했다.

검증: 패치 되돌리면 중첩 3건이 실패하고 기존 엔트리 검사는 그대로 통과한다
(눈이 먼 자리가 그대로 드러난다). 전체 smoke 557 통과 0 실패.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMJX7Dvtu6KFKQH5sx9W5z
bluecheat added a commit that referenced this pull request Sep 9, 2026
`.ax/*.suggested` 는 한 단계만 잡는다. provision 은 세 곳 더 만든다 —
.ax/spirit/{values,tone,README}.md.suggested (§6.10),
.ax/mistakes/README.md.suggested (§6.5),
.ax/_templates/spec/<f>.suggested (§4-b).
전부 무시되지 않아 git status 에 ?? 로 뜨고, 실수로 커밋될 수 있었다.
실사용 리포에서 업데이트 한 번에 두 건이 실제로 샜다.

.ax/**/*.suggested 를 더한다.

smoke 의 기존 검사가 이걸 못 본 건 리터럴 grep 이라서다 — 다섯 엔트리가
파일에 다 있어도 중첩은 안 잡히는데, 있는지만 봤다. 그래서 임시 repo 에
템플릿을 깔고 git check-ignore 로 실제 판정을 보는 검사를 더한다.

버전은 올리지 않는다. 0.5.10 (#12) 위에 리베이스해서 그 릴리즈에 같이 실린다 —
templates/ 안의 수정이라 버전이 안 오르면 프로젝트에 안 닿는데, .gitignore 한 줄
때문에 릴리즈를 하나 더 파는 건 과하다. changelog/0.5.10.md 에 항목을 더했다.

검증: 패치 되돌리면 중첩 3건이 실패하고 기존 엔트리 검사는 그대로 통과한다
(눈이 먼 자리가 그대로 드러난다). 전체 smoke 557 통과 0 실패.


Claude-Session: https://claude.ai/code/session_01HMJX7Dvtu6KFKQH5sx9W5z

Co-authored-by: stone <stone@daangn.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bluecheat
bluecheat deleted the feature/init-spec-dir-dry-run-no-reserve branch September 9, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants