Develop Rule — 재현 가능한 개발
명제: 같은 입력이면 같은 결과가 나와야 한다. 이걸 세 축으로 강제한다.
- 최소 — 안 지은 코드가 가장 재현 가능하다. 존재하지 않는 것은 깨지지도, 갈라지지도 않는다.
- 수렴 — 지은 것은 두 번 실행해도 같은 상태로 간다. 같은 작업을 두 번 실행해도 한 번 실행한 것과 상태가 같으면 그 작업은 멱등하다. 새 환경에서 가장 비싼 실패는 에러가 아니라 중간까지 실행되고 죽은 상태다. 멱등하지 않으면 복구가 "지금까지 뭐가 됐는지 사람이 손으로 파악하기"가 되고, 그건 매번 판단이 달라져 자동화도 재현도 안 된다. 멱등하면 복구는 그냥 다시 실행이다.
- 투영 — 문서는 코드에서 유도한다. 코드가 그대로면 재생성해도 diff 가 없다.
세 축은 같은 것을 다른 층에서 말한다. 코드가 둘로 갈라지면 정본이 둘이 되고, 실행이 두 번째에 터지면 상태가 갈라지고, 문서가 손으로 관리되면 코드와 갈라진다. 드리프트가 적이고, 재현성이 방어다.
모드
| 인자 |
모드 |
성격 |
없음 / lite / full / ultra |
개발 모드 — 사다리 + 멱등 기본값 |
지속 |
review |
지금의 diff 에서 과잉설계 찾기 |
단발 |
audit |
레포 전체 과잉설계 감사 |
단발 |
debt |
ponytail: / idempotent: 마커 장부 |
단발 |
spec |
docs/api-spec.md, docs/screen-spec.md 생성·갱신 |
단발 |
handoff |
대화를 인수인계 문서로 압축 |
단발 |
지속 모드는 한 번 켜지면 이후 모든 구현 판단에 적용된다. 매 파일마다 다시 부를 필요 없다. 해제는 "stop develop_rule" / "normal mode" / /develop_rule off.
단발 모드는 해당 참조 문서를 그때 읽는다. 미리 다 읽지 않는다.
켤 때 한 번: 원칙을 프로젝트에 고정
세션이 길어져 대화가 압축되면 이 내용이 컨텍스트에서 밀려나고 모드가 조용히 꺼진다. 그래서 처음 켤 때 assets/claude-md-card.md 를 현재 프로젝트에 고정한다.
CLAUDE.md 가 없으면 만들고, 있으면 끝에 덧붙인다.
<!-- develop_rule:start --> ~ <!-- develop_rule:end --> 마커로 감싼다.
- 마커가 이미 있으면 그 구간만 교체한다. 덧붙이지 않는다.
묻지 않고 수행하고 보고할 때 한 줄로 알린다. 이 동작 자체가 몇 번 실행해도 같은 파일이 되어야 한다는 게 이 원칙의 좋은 예다.
진행 규칙
작업 중에 선택을 묻지 않는다. 범위, 라이브러리, 파일 배치, 자연 키 후보처럼 갈리는 지점이 나오면 가장 그럴듯한 쪽을 골라 끝까지 간다. 질문은 흐름을 끊고, 대개 말로 설명하는 것보다 돌아가는 코드가 판단을 더 빠르게 만든다. 틀린 선택을 고치는 비용이 대기 비용보다 싸다.
멈추고 확인받는 경우는 되돌리기 어렵거나 위험한 실행뿐이다 — 권한 변경, 파일·데이터 삭제, 외부로 나가는 배포·전송, 보안상 문제가 될 수 있는 명령. 무엇을 왜 실행하는지 한 줄로 밝히고 승인을 받는다.
갈렸던 지점은 끝에 모아 보고한다. 침묵하면 사용자가 나중에 코드를 읽다가 발견하고, 그때는 이미 그 위에 뭔가 쌓여 있다. "A로 갔고 이유는 B, 바꾸려면 C" 면 충분하다. 자연 키 선택처럼 데이터가 쌓인 뒤에 되돌리기 어려운 결정은 반드시 포함한다.
사다리
처음 성립하는 칸에서 멈춘다.
- 애초에 필요한가? 아직 오지 않은 요구를 위한 것이면 만들지 않는다. 한 줄로 안 만들었다고 말한다. (YAGNI)
- 이미 이 코드베이스에 있나? 있으면 쓴다. 같은 일을 하는 함수가 둘이 되면 정본이 둘이 되고, 그 순간부터 두 곳이 조용히 갈라진다. 드리프트는 비멱등성의 다른 이름이다.
- 표준 라이브러리가 하나? 쓴다.
- 플랫폼 네이티브 기능으로 되나? 날짜 피커 라이브러리보다
<input type="date">, JS 보다 CSS, 앱 코드보다 DB 제약.
- 이미 설치된 의존성으로 되나? 몇 줄로 되는 일에 새 의존성을 들이지 않는다.
- 한 줄로 되나? 한 줄.
- 그때서야 동작하는 최소 구현.
사다리는 반사지 조사 프로젝트가 아니다. 다만 문제를 이해한 다음에 탄다. 무엇을 건드리는지 모르는 채로 고른 최소 변경은 게으른 게 아니라 두 번째 버그다. 과제와 그게 닿는 코드를 먼저 읽고 실제 흐름을 끝까지 따라간 뒤에 칸을 고른다.
버그 수정은 증상이 아니라 근본 원인에서. 제보는 증상의 이름이다. 고치기 전에 건드릴 함수의 호출자를 전부 grep 한다. 게으른 수정이 곧 근본 수정이다 — 공유 함수에 가드 하나가 호출자마다 가드를 다는 것보다 작은 diff이고, 티켓에 적힌 경로만 패치하면 나머지 호출자는 그대로 깨진 채 남는다.
규칙
- 요청하지 않은 추상화를 만들지 않는다. 구현체가 하나뿐인 인터페이스, 제품이 하나뿐인 팩토리, 절대 바뀌지 않는 값을 위한 설정.
- 나중을 위한 보일러플레이트와 스캐폴딩을 만들지 않는다. 나중은 나중이 알아서 한다.
- 추가보다 삭제. 영리함보다 지루함. 영리한 코드는 새벽 3시에 누군가 해독해야 하는 것이다.
- 파일 수는 최소로. 가장 짧게 동작하는 diff 가 이긴다 — 단, 문제를 이해한 뒤에. 엉뚱한 자리의 최소 변경은 게으른 게 아니라 두 번째 버그다.
- 복잡한 요청이면 게으른 버전을 내놓고 같은 응답에서 되묻는다. "X 했고 Y 로 커버됨. 완전한 X 가 필요하면 말해줘." 기본값으로 갈 수 있는 답 앞에서 멈추지 않는다.
- 표준 라이브러리 선택지가 둘인데 크기가 같으면 엣지 케이스에 올바른 쪽을 고른다. 게으름은 코드를 덜 쓰는 것이지 부실한 알고리즘을 고르는 게 아니다.
- 하드웨어는 종이 위의 이상값대로 움직이지 않는다. 실제 시계는 드리프트하고 실제 센서는 어긋나게 읽는다. 코드만 줄이지 말고 보정 knob 을 남겨라 — 물리 세계에는 최소 모델이 볼 수 없는 튜닝이 필요하다.
멱등성 — 모든 층에 공통인 네 가지
- 무엇이 "같은 것"인지 먼저 정의한다. 재실행해도 변하지 않는 값(파일명, 외부 ID, 사람이 정한 코드)을 자연 키로 고른다. 자동 증가 ID와 생성 시각은 후보가 아니다.
- 상태를 "만들지" 말고 "맞춘다".
create_x() 대신 ensure_x(), CREATE TABLE IF NOT EXISTS, exist_ok=True, upsert.
- 어디서 죽어도 재실행이 답이 되게 한다. 5단계 중 3단계에서 죽는 경우를 실제로 상상한다.
- 확인과 변경 사이의 틈을 없앤다. 코드의 중복 체크는 UX 용이고 실제 보증은 DB 유니크 제약이 한다. 둘 다 필요하다.
상세와 예시는 references/idempotency-core.md. 층마다 기법이 다르니 지금 만드는 것에 맞는 문서를 읽는다.
| 지금 만드는 것 |
읽을 것 |
| 환경 준비, 스키마/마이그레이션, 시드, 배포·CI 스크립트, 컨테이너 구성 |
references/idempotency-setup.md |
| 도메인 로직, 데이터 변환, 서비스/유스케이스, 배치 처리 |
references/idempotency-code.md |
| HTTP 핸들러, 큐 컨슈머, 웹훅 수신, 외부 시스템 호출 |
references/idempotency-api.md |
실제 기능은 대개 두 층 이상에 걸친다. "회원가입"이면 API 층(같은 요청 두 번 → 계정 하나), 코드 층(해싱·검증은 순수하게, 저장은 경계에서), 셋업 층(테이블·인덱스 생성)이 전부 얽힌다. 층을 하나만 처리하고 멱등하다고 선언하지 마라. 가장 흔한 실패가 이거다 — 핸들러에 중복 방지를 넣어놓고 DB에 유니크 제약이 없어서 동시 요청 두 개가 통과한다.
마커 규약
의도적으로 감수한 한계는 코드에 남긴다. 표시되지 않은 의도는 버그와 구분되지 않는다.
# ponytail: <천장>, <업그레이드 조건> — 알려진 한계를 감수한 단순화 (전역 락, O(n²) 스캔, 순진한 휴리스틱)
# idempotent: <이유>, <해결 방법> — 의도적 비멱등
두 태그는 다른 사실을 표시하므로 합치지 않는다. 장부는 하나이고 /develop_rule debt 가 둘 다 모은다.
프로젝트 환경: uv + Docker
새 프로젝트의 파이썬 환경은 uv, 실행 환경은 Docker 로 만든다. 둘 다 원하는 상태를 파일로 선언하고 거기서 환경을 재현하는 도구다. pip install -r requirements.txt 는 실행 시점의 인덱스에 따라 결과가 달라지지만 uv sync 는 uv.lock 이 고정한 버전 집합으로 수렴한다.
- 의존성은
pyproject.toml 에 선언하고 uv.lock 을 커밋한다. requirements.txt 를 만들지 않는다.
- 실행은
docker compose up -d 로 수렴시킨다. 재실행이 곧 수렴이다.
- 이미지 태그에
latest 를 쓰지 않는다. 같은 태그가 다른 내용을 가리키면 "같은 배포를 두 번"이 성립하지 않는다.
Dockerfile 레이어 배치, compose 헬스체크, 마이그레이션과 앱 기동의 분리는 references/idempotency-setup.md 에 있다.
명세 문서는 항상 만든다
서비스를 만들면 docs/ 아래에 두 문서가 반드시 생긴다. 더 만들지 않는다.
docs/api-spec.md — 백엔드가 노출하는 엔드포인트
docs/screen-spec.md — 프론트엔드 화면과 기능
엔드포인트나 화면을 만들었으면 그 작업 안에서 문서를 채운다. 나중으로 미루면 안 한다. 작성 규칙과 템플릿은 /develop_rule spec.
이게 재현성 규칙에 속하는 이유: 이 문서들은 코드의 투영이지 별도의 정본이 아니다. 코드가 그대로면 몇 번을 다시 생성해도 같은 결과가 나와야 한다. 문서에 생성 시각이나 버전 번호를 박는 순간 재생성마다 diff 가 생기고, 그러면 아무도 재생성하지 않게 되고, 문서는 그날부터 거짓말이 된다.
api-spec.md 의 엔드포인트마다 "재호출하면 어떻게 되는가" 를 적는다. 호출하는 쪽이 재시도해도 되는지를 매번 코드를 읽어 판단하고 있다면 그 판단은 언젠가 틀린다.
출력
코드 먼저. 그다음 짧은 세 줄 이내 — 무엇을 건너뛰었고 언제 추가하면 되는지. 설명이 코드보다 길면 설명을 지운다. 단순화를 변호하는 문단은 산문으로 밀반입된 복잡도다.
형식: [코드] → 건너뜀: [X], 필요해지는 조건: [Y].
사용자가 명시적으로 요청한 설명(리포트, 워크스루, 단계별 노트)은 부채가 아니다. 요청받은 건 온전히 준다. 이 규칙은 요청받지 않은 산문에만 적용된다.
강도
| 단계 |
무엇이 달라지나 |
lite |
요청받은 대로 짓되 더 게으른 대안을 한 줄로 알린다. 선택은 사용자가. |
full |
사다리 강제. 표준 라이브러리와 네이티브 우선. 최단 diff, 최단 설명. 기본값. |
ultra |
YAGNI 극단주의. 추가보다 삭제. 한 줄짜리를 내놓고 같은 호흡에 요구사항 자체를 되묻는다. |
게으르지 않을 것
절대 단순화하지 않는다: 신뢰 경계의 입력 검증, 데이터 손실을 막는 에러 처리, 보안, 접근성 기본, 사용자가 명시적으로 요청한 것. 사용자가 완전판을 고집하면 만든다. 다시 논쟁하지 않는다.
문제 이해에는 절대 게으르지 않는다. 사다리는 해법을 줄이지 읽기를 줄이지 않는다. 이해를 건너뛰고 작은 diff 를 내는 게으름이 위험한 종류다 — 효율로 위장한 채 확신에 찬 틀린 수정을 배포한다.
비자명한 로직은 확인 하나를 남긴다. 분기, 반복, 파서, 돈·보안 경로에는 깨지면 실패하는 가장 작은 것을 남긴다 — assert 기반 demo() / __main__ 자체 점검이나 작은 test_*.py 하나. 프레임워크도 픽스처도 필요 없다. 자명한 한 줄짜리에는 테스트도 YAGNI 다.
셋업·동기화 로직에는 두 번 돌리는 확인을 남긴다. ./setup.sh && ./setup.sh 를 CI 에 넣는 게 셋업이 깨지는 걸 잡는 가장 싼 방법이다.
끝내기 전에
- "이걸 지금 한 번 더 실행하면 어떻게 되나?" — 말로 답할 수 있어야 한다. "아마 괜찮을 것 같다"는 답이 아니다.
- 중간에 죽었다가 재실행되는 경로를 짚었나? 가장 위험한 지점(외부 호출 직후, 커밋 직전)을 하나라도.
- 중복 방지가 코드에만 있고 DB 제약에는 없나? 그렇다면 그건 보증이 아니다.
- 마커를 남길 곳이 있나? 의도적 단순화와 의도적 비멱등은 표시되지 않으면 버그로 읽힌다.
docs/api-spec.md 와 docs/screen-spec.md 를 이번 변경에 맞춰 갱신했나? 새 엔드포인트·화면이 문서에 없으면 미완성이다. → /develop_rule spec
- 검증한 것과 검증하지 않은 것을 구분해서 말했나? 돌려본 것만 "확인했다"고 한다.
안티패턴 빠른 조회
| 증상 |
왜 위험한가 |
대신 |
CREATE TABLE / mkdir / INSERT 를 맨몸으로 |
두 번째 실행에서 터진다 |
IF NOT EXISTS, exist_ok=True, upsert |
SELECT 후 INSERT |
동시 실행 시 둘 다 통과 |
ON CONFLICT, 유니크 제약 |
| 자동 증가 ID를 멱등 키로 사용 |
재실행마다 새 ID가 생긴다 |
자연 키 또는 클라이언트 제공 ID |
requirements.txt + pip install |
실행 시점 인덱스에 따라 결과가 갈린다 |
pyproject.toml + uv.lock + uv sync |
이미지 태그가 latest |
같은 태그가 다른 내용을 가리킨다 |
커밋 SHA 태그 |
| 마이그레이션에 롤백 없음 |
중간 실패 시 수동 복구 |
각 단계를 개별 멱등하게 + 트랜잭션 |
| 외부 API 호출 후 커밋 |
커밋 전 죽으면 외부는 됐는데 내부는 안 됨 |
호출 전 의도 기록, 호출 후 상태 확정 |
time.time() / random / uuid4() 를 결정 로직에 |
같은 입력이 같은 출력을 안 낸다 |
경계에서 생성해 주입 |
| 구현체 하나뿐인 인터페이스, 제품 하나짜리 팩토리 |
아무도 안 쓰는 유연성이 읽는 비용만 만든다 |
두 번째가 생길 때까지 인라인 |
| 테스트가 실행 순서에 의존 |
상태가 테스트 간에 샌다 |
각 테스트가 자기 상태를 ensure |
참조 파일
references/idempotency-core.md — 네 가지 원칙의 상세와 예시
references/idempotency-setup.md — 환경, 스키마, 마이그레이션, 시드, 배포
references/idempotency-code.md — 도메인 로직, 변환, 배치
references/idempotency-api.md — 핸들러, 컨슈머, 웹훅, 외부 호출
references/python-style.md — 모듈 분리 기준, 파이썬 파일 규약, 코드 위생
references/review.md — review 와 audit 의 태그·출력 형식
references/debt.md — 마커 장부
references/spec.md — 명세 문서 작성 규칙
references/handoff.md — 인수인계 문서
assets/ 에는 CLAUDE.md 카드와 명세 템플릿 두 개가 있다.
경계
이 스킬은 무엇을 짓는지를 지배하지 어떻게 말하는지는 지배하지 않는다. 말투는 humanism_talk 의 brief 모드가 맡는다. 코드가 아닌 요청(일반 지식, 산문, 번역, 요약)에는 적용하지 않는다.
크레딧
1---2name: develop-rule3description: 재현 가능한 개발 모드. 같은 입력이면 같은 결과가 나오도록 세 가지를 강제한다 — 가장 게으른 해법을 고르고(짓지 않은 코드가 가장 재현 가능하다), 지은 것은 두 번 실행해도 같은 상태로 수렴하게 만들고(멱등성), 문서는 코드에서 유도해 재생성해도 diff 가 없게 한다. 코드를 쓰거나 고치거나 리뷰하거나 설계할 때, 새 서비스·환경을 세울 때, 부트스트랩·마이그레이션·시드·배포 스크립트를 쓸 때, 재시도와 중복 요청을 견뎌야 하는 핸들러를 만들 때 사용한다. Use when the user says "멱등", "idempotent", "재실행해도 안전하게", "두 번 돌려도", "중복 방지", "yagni", "과잉설계", "제일 단순하게", "뭘 지울 수 있나", "API 명세", "화면 정의서", "인수인계", "be lazy", "simplest solution", "over-engineered", "what can we delete", "handoff", or invokes /develop_rule. 그리고 사용자가 무언가를 만들거나 고치겠다고 선언하면 — "~~ 개발할거야", "~~ 만들거야", "~~ 만들자", "~~ 붙일거야", "~~ 구현할거야", "~~ 짜줘", "서비스 하나 세우자", "API 붙이자", "환경 세팅하자", "파이프라인 만들거야", "리팩터링할거야", "I'm going to build", "let's build", "let's implement", "set up a service" — 멱등성이나 단순화를 명시적으로 언급하지 않아도 이 스킬을 켠다. 개발 착수 선언 자체가 트리거다. 코드가 아닌 요청(일반 지식, 산문, 번역, 요약)에는 쓰지 않는다.4license: MIT5---67# Develop Rule — 재현 가능한 개발89**명제: 같은 입력이면 같은 결과가 나와야 한다.** 이걸 세 축으로 강제한다.1011- **최소** — 안 지은 코드가 가장 재현 가능하다. 존재하지 않는 것은 깨지지도, 갈라지지도 않는다.12- **수렴** — 지은 것은 두 번 실행해도 같은 상태로 간다. **같은 작업을 두 번 실행해도 한 번 실행한 것과 상태가 같으면 그 작업은 멱등하다.** 새 환경에서 가장 비싼 실패는 에러가 아니라 *중간까지 실행되고 죽은 상태*다. 멱등하지 않으면 복구가 "지금까지 뭐가 됐는지 사람이 손으로 파악하기"가 되고, 그건 매번 판단이 달라져 자동화도 재현도 안 된다. 멱등하면 복구는 그냥 다시 실행이다.13- **투영** — 문서는 코드에서 유도한다. 코드가 그대로면 재생성해도 diff 가 없다.1415세 축은 같은 것을 다른 층에서 말한다. 코드가 둘로 갈라지면 정본이 둘이 되고, 실행이 두 번째에 터지면 상태가 갈라지고, 문서가 손으로 관리되면 코드와 갈라진다. **드리프트가 적이고, 재현성이 방어다.**1617## 모드1819| 인자 | 모드 | 성격 |20|---|---|---|21| 없음 / `lite` / `full` / `ultra` | 개발 모드 — 사다리 + 멱등 기본값 | 지속 |22| `review` | 지금의 diff 에서 과잉설계 찾기 | 단발 |23| `audit` | 레포 전체 과잉설계 감사 | 단발 |24| `debt` | `ponytail:` / `idempotent:` 마커 장부 | 단발 |25| `spec` | `docs/api-spec.md`, `docs/screen-spec.md` 생성·갱신 | 단발 |26| `handoff` | 대화를 인수인계 문서로 압축 | 단발 |2728지속 모드는 한 번 켜지면 이후 모든 구현 판단에 적용된다. 매 파일마다 다시 부를 필요 없다. 해제는 `"stop develop_rule"` / `"normal mode"` / `/develop_rule off`.2930단발 모드는 해당 참조 문서를 그때 읽는다. 미리 다 읽지 않는다.3132## 켤 때 한 번: 원칙을 프로젝트에 고정3334세션이 길어져 대화가 압축되면 이 내용이 컨텍스트에서 밀려나고 모드가 조용히 꺼진다. 그래서 처음 켤 때 `assets/claude-md-card.md` 를 현재 프로젝트에 고정한다.3536- `CLAUDE.md` 가 없으면 만들고, 있으면 끝에 덧붙인다.37- `<!-- develop_rule:start -->` ~ `<!-- develop_rule:end -->` 마커로 감싼다.38- 마커가 이미 있으면 **그 구간만 교체한다.** 덧붙이지 않는다.3940묻지 않고 수행하고 보고할 때 한 줄로 알린다. 이 동작 자체가 몇 번 실행해도 같은 파일이 되어야 한다는 게 이 원칙의 좋은 예다.4142## 진행 규칙4344**작업 중에 선택을 묻지 않는다.** 범위, 라이브러리, 파일 배치, 자연 키 후보처럼 갈리는 지점이 나오면 가장 그럴듯한 쪽을 골라 끝까지 간다. 질문은 흐름을 끊고, 대개 말로 설명하는 것보다 돌아가는 코드가 판단을 더 빠르게 만든다. 틀린 선택을 고치는 비용이 대기 비용보다 싸다.4546**멈추고 확인받는 경우는 되돌리기 어렵거나 위험한 실행뿐이다** — 권한 변경, 파일·데이터 삭제, 외부로 나가는 배포·전송, 보안상 문제가 될 수 있는 명령. 무엇을 왜 실행하는지 한 줄로 밝히고 승인을 받는다.4748**갈렸던 지점은 끝에 모아 보고한다.** 침묵하면 사용자가 나중에 코드를 읽다가 발견하고, 그때는 이미 그 위에 뭔가 쌓여 있다. "A로 갔고 이유는 B, 바꾸려면 C" 면 충분하다. 자연 키 선택처럼 데이터가 쌓인 뒤에 되돌리기 어려운 결정은 반드시 포함한다.4950## 사다리5152처음 성립하는 칸에서 멈춘다.53541. **애초에 필요한가?** 아직 오지 않은 요구를 위한 것이면 만들지 않는다. 한 줄로 안 만들었다고 말한다. (YAGNI)552. **이미 이 코드베이스에 있나?** 있으면 쓴다. 같은 일을 하는 함수가 둘이 되면 정본이 둘이 되고, 그 순간부터 두 곳이 조용히 갈라진다. 드리프트는 비멱등성의 다른 이름이다.563. **표준 라이브러리가 하나?** 쓴다.574. **플랫폼 네이티브 기능으로 되나?** 날짜 피커 라이브러리보다 `<input type="date">`, JS 보다 CSS, 앱 코드보다 DB 제약.585. **이미 설치된 의존성으로 되나?** 몇 줄로 되는 일에 새 의존성을 들이지 않는다.596. **한 줄로 되나?** 한 줄.607. **그때서야** 동작하는 최소 구현.6162**사다리는 반사지 조사 프로젝트가 아니다. 다만 문제를 이해한 *다음에* 탄다.** 무엇을 건드리는지 모르는 채로 고른 최소 변경은 게으른 게 아니라 두 번째 버그다. 과제와 그게 닿는 코드를 먼저 읽고 실제 흐름을 끝까지 따라간 뒤에 칸을 고른다.6364**버그 수정은 증상이 아니라 근본 원인에서.** 제보는 증상의 이름이다. 고치기 전에 건드릴 함수의 호출자를 전부 grep 한다. 게으른 수정이 곧 근본 수정이다 — 공유 함수에 가드 하나가 호출자마다 가드를 다는 것보다 작은 diff이고, 티켓에 적힌 경로만 패치하면 나머지 호출자는 그대로 깨진 채 남는다.6566## 규칙6768- **요청하지 않은 추상화를 만들지 않는다.** 구현체가 하나뿐인 인터페이스, 제품이 하나뿐인 팩토리, 절대 바뀌지 않는 값을 위한 설정.69- **나중을 위한 보일러플레이트와 스캐폴딩을 만들지 않는다.** 나중은 나중이 알아서 한다.70- **추가보다 삭제. 영리함보다 지루함.** 영리한 코드는 새벽 3시에 누군가 해독해야 하는 것이다.71- **파일 수는 최소로. 가장 짧게 동작하는 diff 가 이긴다** — 단, 문제를 이해한 뒤에. 엉뚱한 자리의 최소 변경은 게으른 게 아니라 두 번째 버그다.72- **복잡한 요청이면 게으른 버전을 내놓고 같은 응답에서 되묻는다.** "X 했고 Y 로 커버됨. 완전한 X 가 필요하면 말해줘." 기본값으로 갈 수 있는 답 앞에서 멈추지 않는다.73- **표준 라이브러리 선택지가 둘인데 크기가 같으면 엣지 케이스에 올바른 쪽을 고른다.** 게으름은 코드를 덜 쓰는 것이지 부실한 알고리즘을 고르는 게 아니다.74- **하드웨어는 종이 위의 이상값대로 움직이지 않는다.** 실제 시계는 드리프트하고 실제 센서는 어긋나게 읽는다. 코드만 줄이지 말고 보정 knob 을 남겨라 — 물리 세계에는 최소 모델이 볼 수 없는 튜닝이 필요하다.7576## 멱등성 — 모든 층에 공통인 네 가지77781. **무엇이 "같은 것"인지 먼저 정의한다.** 재실행해도 변하지 않는 값(파일명, 외부 ID, 사람이 정한 코드)을 자연 키로 고른다. 자동 증가 ID와 생성 시각은 후보가 아니다.792. **상태를 "만들지" 말고 "맞춘다".** `create_x()` 대신 `ensure_x()`, `CREATE TABLE IF NOT EXISTS`, `exist_ok=True`, upsert.803. **어디서 죽어도 재실행이 답이 되게 한다.** 5단계 중 3단계에서 죽는 경우를 실제로 상상한다.814. **확인과 변경 사이의 틈을 없앤다.** 코드의 중복 체크는 UX 용이고 실제 보증은 DB 유니크 제약이 한다. 둘 다 필요하다.8283상세와 예시는 `references/idempotency-core.md`. 층마다 기법이 다르니 지금 만드는 것에 맞는 문서를 읽는다.8485| 지금 만드는 것 | 읽을 것 |86|---|---|87| 환경 준비, 스키마/마이그레이션, 시드, 배포·CI 스크립트, 컨테이너 구성 | `references/idempotency-setup.md` |88| 도메인 로직, 데이터 변환, 서비스/유스케이스, 배치 처리 | `references/idempotency-code.md` |89| HTTP 핸들러, 큐 컨슈머, 웹훅 수신, 외부 시스템 호출 | `references/idempotency-api.md` |9091실제 기능은 대개 두 층 이상에 걸친다. "회원가입"이면 API 층(같은 요청 두 번 → 계정 하나), 코드 층(해싱·검증은 순수하게, 저장은 경계에서), 셋업 층(테이블·인덱스 생성)이 전부 얽힌다. **층을 하나만 처리하고 멱등하다고 선언하지 마라.** 가장 흔한 실패가 이거다 — 핸들러에 중복 방지를 넣어놓고 DB에 유니크 제약이 없어서 동시 요청 두 개가 통과한다.9293## 마커 규약9495의도적으로 감수한 한계는 코드에 남긴다. 표시되지 않은 의도는 버그와 구분되지 않는다.9697- `# ponytail: <천장>, <업그레이드 조건>` — 알려진 한계를 감수한 단순화 (전역 락, O(n²) 스캔, 순진한 휴리스틱)98- `# idempotent: <이유>, <해결 방법>` — 의도적 비멱등99100두 태그는 다른 사실을 표시하므로 합치지 않는다. 장부는 하나이고 `/develop_rule debt` 가 둘 다 모은다.101102## 프로젝트 환경: uv + Docker103104새 프로젝트의 파이썬 환경은 **uv**, 실행 환경은 **Docker** 로 만든다. 둘 다 원하는 상태를 파일로 선언하고 거기서 환경을 재현하는 도구다. `pip install -r requirements.txt` 는 실행 시점의 인덱스에 따라 결과가 달라지지만 `uv sync` 는 `uv.lock` 이 고정한 버전 집합으로 수렴한다.105106- 의존성은 `pyproject.toml` 에 선언하고 `uv.lock` 을 커밋한다. `requirements.txt` 를 만들지 않는다.107- 실행은 `docker compose up -d` 로 수렴시킨다. 재실행이 곧 수렴이다.108- 이미지 태그에 `latest` 를 쓰지 않는다. 같은 태그가 다른 내용을 가리키면 "같은 배포를 두 번"이 성립하지 않는다.109110Dockerfile 레이어 배치, compose 헬스체크, 마이그레이션과 앱 기동의 분리는 `references/idempotency-setup.md` 에 있다.111112## 명세 문서는 항상 만든다113114서비스를 만들면 `docs/` 아래에 두 문서가 **반드시** 생긴다. 더 만들지 않는다.115116- `docs/api-spec.md` — 백엔드가 노출하는 엔드포인트117- `docs/screen-spec.md` — 프론트엔드 화면과 기능118119**엔드포인트나 화면을 만들었으면 그 작업 안에서 문서를 채운다.** 나중으로 미루면 안 한다. 작성 규칙과 템플릿은 `/develop_rule spec`.120121이게 재현성 규칙에 속하는 이유: 이 문서들은 코드의 투영이지 별도의 정본이 아니다. 코드가 그대로면 몇 번을 다시 생성해도 같은 결과가 나와야 한다. 문서에 생성 시각이나 버전 번호를 박는 순간 재생성마다 diff 가 생기고, 그러면 아무도 재생성하지 않게 되고, 문서는 그날부터 거짓말이 된다.122123`api-spec.md` 의 엔드포인트마다 **"재호출하면 어떻게 되는가"** 를 적는다. 호출하는 쪽이 재시도해도 되는지를 매번 코드를 읽어 판단하고 있다면 그 판단은 언젠가 틀린다.124125## 출력126127코드 먼저. 그다음 짧은 세 줄 이내 — 무엇을 건너뛰었고 언제 추가하면 되는지. 설명이 코드보다 길면 설명을 지운다. 단순화를 변호하는 문단은 산문으로 밀반입된 복잡도다.128129형식: `[코드] → 건너뜀: [X], 필요해지는 조건: [Y].`130131사용자가 명시적으로 요청한 설명(리포트, 워크스루, 단계별 노트)은 부채가 아니다. 요청받은 건 온전히 준다. 이 규칙은 요청받지 않은 산문에만 적용된다.132133## 강도134135| 단계 | 무엇이 달라지나 |136|---|---|137| `lite` | 요청받은 대로 짓되 더 게으른 대안을 한 줄로 알린다. 선택은 사용자가. |138| `full` | 사다리 강제. 표준 라이브러리와 네이티브 우선. 최단 diff, 최단 설명. 기본값. |139| `ultra` | YAGNI 극단주의. 추가보다 삭제. 한 줄짜리를 내놓고 같은 호흡에 요구사항 자체를 되묻는다. |140141## 게으르지 않을 것142143**절대 단순화하지 않는다:** 신뢰 경계의 입력 검증, 데이터 손실을 막는 에러 처리, 보안, 접근성 기본, 사용자가 명시적으로 요청한 것. 사용자가 완전판을 고집하면 만든다. 다시 논쟁하지 않는다.144145**문제 이해에는 절대 게으르지 않는다.** 사다리는 해법을 줄이지 읽기를 줄이지 않는다. 이해를 건너뛰고 작은 diff 를 내는 게으름이 위험한 종류다 — 효율로 위장한 채 확신에 찬 틀린 수정을 배포한다.146147**비자명한 로직은 확인 하나를 남긴다.** 분기, 반복, 파서, 돈·보안 경로에는 깨지면 실패하는 가장 작은 것을 남긴다 — `assert` 기반 `demo()` / `__main__` 자체 점검이나 작은 `test_*.py` 하나. 프레임워크도 픽스처도 필요 없다. 자명한 한 줄짜리에는 테스트도 YAGNI 다.148149셋업·동기화 로직에는 두 번 돌리는 확인을 남긴다. `./setup.sh && ./setup.sh` 를 CI 에 넣는 게 셋업이 깨지는 걸 잡는 가장 싼 방법이다.150151## 끝내기 전에1521531. **"이걸 지금 한 번 더 실행하면 어떻게 되나?"** — 말로 답할 수 있어야 한다. "아마 괜찮을 것 같다"는 답이 아니다.1542. 중간에 죽었다가 재실행되는 경로를 짚었나? 가장 위험한 지점(외부 호출 직후, 커밋 직전)을 하나라도.1553. 중복 방지가 코드에만 있고 DB 제약에는 없나? 그렇다면 그건 보증이 아니다.1564. 마커를 남길 곳이 있나? 의도적 단순화와 의도적 비멱등은 표시되지 않으면 버그로 읽힌다.1575. `docs/api-spec.md` 와 `docs/screen-spec.md` 를 이번 변경에 맞춰 갱신했나? 새 엔드포인트·화면이 문서에 없으면 미완성이다. → `/develop_rule spec`1586. 검증한 것과 검증하지 않은 것을 구분해서 말했나? 돌려본 것만 "확인했다"고 한다.159160## 안티패턴 빠른 조회161162| 증상 | 왜 위험한가 | 대신 |163|---|---|---|164| `CREATE TABLE` / `mkdir` / `INSERT` 를 맨몸으로 | 두 번째 실행에서 터진다 | `IF NOT EXISTS`, `exist_ok=True`, upsert |165| `SELECT` 후 `INSERT` | 동시 실행 시 둘 다 통과 | `ON CONFLICT`, 유니크 제약 |166| 자동 증가 ID를 멱등 키로 사용 | 재실행마다 새 ID가 생긴다 | 자연 키 또는 클라이언트 제공 ID |167| `requirements.txt` + `pip install` | 실행 시점 인덱스에 따라 결과가 갈린다 | `pyproject.toml` + `uv.lock` + `uv sync` |168| 이미지 태그가 `latest` | 같은 태그가 다른 내용을 가리킨다 | 커밋 SHA 태그 |169| 마이그레이션에 롤백 없음 | 중간 실패 시 수동 복구 | 각 단계를 개별 멱등하게 + 트랜잭션 |170| 외부 API 호출 후 커밋 | 커밋 전 죽으면 외부는 됐는데 내부는 안 됨 | 호출 전 의도 기록, 호출 후 상태 확정 |171| `time.time()` / `random` / `uuid4()` 를 결정 로직에 | 같은 입력이 같은 출력을 안 낸다 | 경계에서 생성해 주입 |172| 구현체 하나뿐인 인터페이스, 제품 하나짜리 팩토리 | 아무도 안 쓰는 유연성이 읽는 비용만 만든다 | 두 번째가 생길 때까지 인라인 |173| 테스트가 실행 순서에 의존 | 상태가 테스트 간에 샌다 | 각 테스트가 자기 상태를 ensure |174175## 참조 파일176177- `references/idempotency-core.md` — 네 가지 원칙의 상세와 예시178- `references/idempotency-setup.md` — 환경, 스키마, 마이그레이션, 시드, 배포179- `references/idempotency-code.md` — 도메인 로직, 변환, 배치180- `references/idempotency-api.md` — 핸들러, 컨슈머, 웹훅, 외부 호출181- `references/python-style.md` — 모듈 분리 기준, 파이썬 파일 규약, 코드 위생182- `references/review.md` — `review` 와 `audit` 의 태그·출력 형식183- `references/debt.md` — 마커 장부184- `references/spec.md` — 명세 문서 작성 규칙185- `references/handoff.md` — 인수인계 문서186187`assets/` 에는 `CLAUDE.md` 카드와 명세 템플릿 두 개가 있다.188189## 경계190191이 스킬은 **무엇을 짓는지**를 지배하지 어떻게 말하는지는 지배하지 않는다. 말투는 `humanism_talk` 의 `brief` 모드가 맡는다. 코드가 아닌 요청(일반 지식, 산문, 번역, 요약)에는 적용하지 않는다.192193## 크레딧194195- 최소주의 축 — [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail) (MIT)196- 인수인계 — [mattpocock/skills](https://github.com/mattpocock/skills) (MIT)197- 수렴·투영 축 — 이 저장소