Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
id: GF-141
title: 훅 주석에 남은 pre-commit·bats 언급 정리
status: Done
assignee:
- '@claude'
created_date: '2026-10-03 08:11'
updated_date: '2026-10-03 12:21'
labels:
- docs
- hooks
dependencies: []
references:
- decision-16
- decision-21
- decision-23
- GF-127
- GF-128
documentation:
- backlog/docs/doc-12 - consistency.bats-바이트-동일성-검사-폐지-경위와-대체-행위-검증.md
- 'https://claude.ai/artifact/6rCLn6LHGfXNpZKSstZR4W'
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
훅 해부 아티팩트를 코드와 대조하다(2026-10-03) 훅 주석 일부가 이미 사라진 구조를 설명하고 있는 걸 발견했다. 파일 하나만 읽고 동작을 파악하는 게 이 저장소의 감사 가능성 원칙(decision-16)이라, 주석이 틀리면 그 원칙이 깨진다.

발견한 곳:
- hooks/prepare-commit-msg 첫머리: "지금 하는 일은 두 가지"라고 하지만 실제로는 검증마커 기록까지 세 가지다.
- hooks/commit-msg validate_format(): 타입 목록 일치를 tests/robustness-commit-msg.bats가 검증한다고 적었는데 bats는 decision-21로 사라졌다(지금은 tests/test_config_keys_match_hooks.py).
- hooks/commit-msg 끝의 finally 주석, hooks/post-commit detect_verify_bypass() 주석: pre-commit이 마커를 남긴다고 적었지만 pre-commit 훅은 없고 prepare-commit-msg가 쓴다.

범위: GF-127/GF-128이 commit-msg·post-commit의 로직을 prepare-commit-msg로 옮기며 두 파일을 정리할 예정이라 AC 3·4는 그때 다시 바뀔 수 있다. 승격 시점(2026-10-03)에 유저가 그 겹침을 알고도 AC 전체를 이대로 진행하기로 했다.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 prepare-commit-msg 첫머리 주석이 실제 동작(재생 면제·에디터 거부·마커 기록)과 일치한다
- [x] #2 commit-msg 주석이 가리키는 테스트 파일이 실제로 존재한다
- [x] #3 commit-msg finally 주석이 마커 작성 주체를 prepare-commit-msg로 적는다
- [x] #4 post-commit detect_verify_bypass() 주석이 마커 작성 주체를 prepare-commit-msg로 적는다
- [x] #5 동작 변경 없음 — 기존 테스트 전부 통과
- [x] #6 post-commit 첫머리 판단 근거 주석이 마커 작성 주체를 prepare-commit-msg로, 쓰는 조건을 실제대로 적는다
<!-- AC:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. 각 AC의 주석을 실제 코드 동작에 맞게 고친다(주석만, 로직 불변).
2. AC 하나(또는 같은 파일 두 줄)마다 전체 테스트와 ruff를 돌리고 커밋한다.
3. 마지막에 main과 HEAD의 훅 AST가 같은지 비교해 동작 불변을 증명한다.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
AC1~4 주석 정정 완료(da3bae4, 889a100, a89e37e). 매 커밋 전 python3 -m unittest discover -s tests 106건 OK, ruff check 통과.

AC 밖 발견: hooks/post-commit 첫머리(6행) "pre-commit은 시작 시점에 마커를 지우고, 통과하면 끝에 다시 쓴다" — pre-commit은 없고 prepare-commit-msg는 마커를 조건 없이 쓴다. 범위 확장 여부를 유저에게 확인 중.

유저 승인(2026-10-03)으로 post-commit 첫머리 주석을 AC #6으로 추가.

검증: python3 -m unittest discover -s tests 106건 OK(최종 0f40967 기준), ruff check 통과. main..HEAD 훅 3개의 ast.dump가 동일 — 바뀐 건 주석뿐이다. 주석이 가리키는 tests/test_config_keys_match_hooks.py 존재 확인, 훅에 pre-commit·.bats 언급이 남지 않음(GF-126 이력 서술 1곳은 사실이라 유지).
<!-- SECTION:NOTES:END -->

## Final Summary

<!-- SECTION:FINAL_SUMMARY:BEGIN -->
훅 주석 6곳이 이미 사라진 구조(pre-commit 훅, bats 테스트)를 설명하고 있어 실제 동작대로 고쳤다: prepare-commit-msg 역할(3가지), commit-msg의 테스트 참조와 finally 마커 주체, post-commit 첫머리와 detect_verify_bypass()의 마커 주체·조건. 로직 변경 없음 — main 대비 세 훅의 AST가 동일하고, 전체 테스트 106건과 ruff가 통과했다. AC #6은 작업 중 발견해 유저 승인으로 추가했다.
<!-- SECTION:FINAL_SUMMARY:END -->
4 changes: 2 additions & 2 deletions hooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def message_lines():
def validate_format(content):
# 커밋 타입 목록은 gitformat.conf(다중값 gitformat.type)에서 읽는다 -
# .gitmessage에 적힌 사람이 읽는 목록과 일치하는지는
# tests/robustness-commit-msg.bats가 검증한다(런타임 결합 없이 테스트로만 보장).
# tests/test_config_keys_match_hooks.py가 검증한다(런타임 결합 없이 테스트로만 보장).
types = conf_get_all("gitformat.type")
# 목록이 비면 정규식이 ^()...가 돼 모든 커밋이 알 수 없는 이유로 거부된다
# (GF-76). 조용히 진행하지 않고 원인을 밝히며 멈춘다.
Expand Down Expand Up @@ -317,7 +317,7 @@ STATUS = 1
try:
STATUS = run()
finally:
# pre-commit이 통과해 검증마커를 남긴 뒤 commit-msg가 거부하면 커밋 자체가 안
# prepare-commit-msg가 검증마커를 남긴 뒤 commit-msg가 거부하면 커밋 자체가 안
# 생성돼 post-commit이 안 돌고 마커가 안 지워진다. 이후 무관한 --no-verify
# 커밋이 그 스테일 마커를 "검증됨"으로 잘못 소비할 수 있다(GF-31). 0이 아닌
# 상태로 끝나면(거부든 예기치 못한 예외든) 항상 마커를 지운다 — 정상 통과
Expand Down
12 changes: 7 additions & 5 deletions hooks/post-commit
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# 삽입한다(decision-3). post-commit은 --no-verify를 쓰든 --amend를 하든 git이 항상
# 실행을 보장하는 훅이라는 점을 이용한다.
#
# 판단 근거: pre-commit은 실행될 때 시작 시점에 마커를 지우고, 통과하면 끝에 다시 쓴다.
# 이 훅은 그 마커의 존재 여부만 보고, 확인 후에는 항상(존재하든 안 하든) 지운다 —
# 그래야 다음 커밋으로 스테일 마커가 새지 않는다.
# 판단 근거: prepare-commit-msg는 재생 커밋 면제나 에디터 경로 거부로 빠져나가지
# 않으면 직전 마커를 지우고 조건 없이 새로 쓴다. 이 훅은 그 마커의 존재 여부만 보고,
# 확인 후에는 항상(존재하든 안 하든) 지운다 — 그래야 다음 커밋으로 스테일 마커가
# 새지 않는다.
#
# 각 트레일러 판단 단계는 이름 있는 함수로 분해돼 있고(GF-87), 실행 순서는 파일 맨
# 아래에 나열돼 있다. 다른 훅과 겹치는 블록(자기 위치 해석, conf 읽기 가드,
Expand Down Expand Up @@ -198,8 +199,9 @@ def claude_transcript_path():
return os.path.join(home, ".claude", "projects", slug, f"{session_id}.jsonl")


# --no-verify 우회 탐지(decision-3): pre-commit이 마커를 남기지 못했다는 건
# commit-msg/pre-commit이 스킵됐다는 뜻이다.
# --no-verify 우회 탐지(decision-3): 마커가 없다는 건 prepare-commit-msg가 마커를
# 남기지 못했다는 뜻이다. prepare-commit-msg는 --no-verify로 건너뛸 수 없으므로
# 지금은 재생 커밋(그 훅이 면제로 먼저 빠져나간 경우)에서만 마커가 없다.
def detect_verify_bypass():
if not VERIFIED and not trailer_exists(TRAILER_VERIFY_BYPASSED, "true"):
queue_trailer(TRAILER_VERIFY_BYPASSED, "true")
Expand Down
3 changes: 2 additions & 1 deletion hooks/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# prepare-commit-msg 훅: 커밋 규칙 강제를 훅 하나로 모으는 재설계의 본체다
# (decision-18). 지금 하는 일은 두 가지다 — 재생·병합 커밋 면제, 에디터 경로 거부.
# (decision-18). 지금 하는 일은 세 가지다 — 재생·병합 커밋 면제, 에디터 경로 거부,
# 검증마커 기록.
# 메시지 검증(GF-127)과 트레일러 삽입(GF-128)은 후속 태스크에서 구 훅에서 이 파일로
# 옮겨온다. 그때까지 commit-msg/post-commit이 그대로 자기 일을 계속한다.
#
Expand Down
Loading