


사용자 데이터를 잃지 않고 승인 전 코드를 실행하지 않는 커맨드라인 도구 만들기
이미 사용자가 있는 CLI를 AI가 만들거나 고칠 때 쓴다. 플래그와 설정 파일의 호환성을 지키고, 작업을 망가뜨리기 전에 오류로 멈추며, 프로젝트 훅은 승인 뒤에만 실행하고, 모든 서브프로세스를 기록하며, 네트워크 결과보다 로컬 결과를 먼저 보여 준다.
이 AI 규칙을 사용하려면
## 도구가 커져도 설정 파일과 명령 플래그의 호환성 지키기
도구에 실제 사용자가 생기면, 깔끔한 설계와 호환성 사이의 균형을 맞춰야 한다. 재설계 자체가 사용자에게 불편을 주는 이유가 되어서는 안 된다.
### 모든 외부 변경(breaking change)에는 명확한 근거를 제시하라
외부 인터페이스를 변경하려면 단순한 정리가 아니라 사용자에게 체감되는 구체적인 개선이 있어야 한다. 조용히 깨뜨리기보다는 지원 중단(deprecation) 경고와 마이그레이션 경로를 제공하는 편이 낫다.
### 보호해야 할 영역을 명확히 정의하라
어떤 인터페이스가 계약(설정 파일 형식, 커맨드라인 플래그와 인자)이고 어떤 것이 자유롭게 바뀌어도 되는지(내부 API, 로그 위치, 사람이 읽는 출력 형식) 구분해서 정하라. 앞의 것은 보호하고, 뒤의 것은 유연하게 두어 리팩터링 비용을 낮게 유지한다.
### 지원 범위의 경계를 명시하라
지원하는 최소 런타임 또는 툴체인 버전을 명시하고, 그때그때가 아니라 예측 가능한 유지보수 주기에 따라 올린다.
## 개념마다 한 단어만 쓰고 문서·도움말·주석에서 맞추기
개념 하나당 용어 하나를 정하고, 문서·도움말·코드 주석에서 동일하게 사용한다. 용어가 흔들리면 사용자와 유지보수자 모두 모르는 사이에 혼란스러워진다.
### 용어집을 문서로 남겨라
각 단어의 의미와 피해야 할 유사어를 정리한 짧은 목록을 유지한다. 원본 체크아웃과 보조 체크아웃, 작업의 대상과 그 작업이 실행되는 작업 공간처럼 비슷해 보이는 개념은 명확히 구분한다.
### 핵심적인 단어를 중복된 의미로 쓰지 마라
어떤 단어가 이미 한 개념을 가리키고 있다면 다른 개념에 재사용하지 마라. 한 문장 안에서는 자연스럽게 읽히더라도 두 번째 개념에는 다른 단어를 선택한다.
## 동작을 바꾸면 도움말도 함께 고치고 테스트로 어긋남 잡기
동작을 변경할 때는 같은 변경 안에 문서 수정도 함께 담는다. 적용할 기준은 이것이다: 도움말 텍스트가 여전히 코드가 실제로 하는 일을 설명하고 있는가?
### 사실 하나당 원본 출처는 하나로 유지하라
자동 생성되는 문서는 원본(코드에 내장된 도움말과 명령어 정의)을 수정하고 렌더링된 사본을 다시 생성한다. 생성된 파일을 직접 손으로 수정하지 마라. 한 디렉터리에 생성된 사본과 직접 작성한 페이지가 섞여 있다면, 편집하기 전에 그 파일이 어떤 역할인지 먼저 확인한다.
### 테스트로 동기화를 강제하라
렌더링된 출력을 원본과 비교하는 문서 동기화 테스트는 어긋남을 기계적으로 잡아낸다. 도움말 텍스트를 수정하면 렌더링된 스냅샷도 함께 바뀌므로, 테스트를 통과시키려면 같은 변경 안에서 스냅샷을 재생성하고 검토한다.
## 동의 없이 사용자 파일을 망가뜨리지 않고 오류로 멈추기
사용자 데이터는 신성한 것으로 다뤄라. 작업물을 보존한 채 실패하는 명령이, 조용히 그것을 파괴하며 "성공"하는 명령보다 낫다.
### 조용한 손실보다는 실패를 선택하라
어떤 작업이 추적되지 않는 파일, 커밋되지 않은 변경사항, 또는 다른 사용자 데이터를 파괴할 가능성이 있다면, 그대로 진행하지 말고 오류를 내며 멈춘다.
### 파괴적인 작업은 명시적 동의가 있어야만 실행하라
데이터를 강제로 삭제하거나 덮어쓰는 것과 같은 파괴적인 동작은 사용자가 명시적으로 요청했을 때만 실행한다. 관련 없는 작업의 부수 효과로 삭제하거나 덮어쓰지 마라. 정리(cleanup)는 사용자가 별도로 선택하는 단계로 만든다.
### 실패하는 방식을 선호하고, 확인과 사용 사이의 간극에 주의하라
같은 작업에 두 가지 형태가 있다면, 강제로 밀어붙이는 쪽보다 충돌 시 실패하는 쪽을 선택한다. 안전 점검과 그것이 보호하는 실제 동작 사이에 시간이 흐른다면 보수적으로 접근하라. 그 사이에 상태가 바뀔 수 있으므로, 정리 작업 중에는 강제로 밀어붙이지 마라.
## 모든 서브프로세스를 로그와 시간을 남기는 하나의 래퍼로 통과시키기
프로세스 API를 직접 호출하는 대신, 모든 외부 명령을 하나의 실행 래퍼를 통해 보낸다. 경로를 하나로 통일하면 일관된 디버그 로깅과 타이밍 측정을 별도 비용 없이 얻을 수 있고, 동작을 바꿀 지점도 한 곳으로 모을 수 있다.
### 래퍼 하나, 기록자(emitter) 하나
추적 규칙(trace grammar)을 정확히 하나의 컴포넌트에만 두고, 그 컴포넌트만이 하위 로그 호출을 기록하도록 만든다. 그러면 서브프로세스는 그 관문을 통해 추적되거나, 아예 기록이 남지 않거나 둘 중 하나가 된다.
### 새로운 프로세스 생성 지점은 직접 옵트인하라
일부 프로세스 생성 형태는 래퍼에 맞지 않아 추적 기록을 직접 만들어야 한다: 생성 직전에 시작하고, 성공하면 완료 처리하고, 오류가 나면 실패 처리한다. 이 관문(guard)은 반드시 사용해야 하는 값으로 만들어, 처리되지 않은 채 버려지면 어설션이 걸리도록 한다. 그렇게 하면 완료 처리를 빠뜨렸을 때 조용히 추적되지 않고 넘어가는 대신 테스트가 실패한다. 분리된(detached) 백그라운드 자식 프로세스와 대화형 헬퍼는 문서화된 추적 예외 대상이다.
### 상속된 탐색용 환경 변수를 제거하라
일부 환경 변수는 자식 프로세스가 자신의 작업 디렉터리를 확인하기 전에 먼저 해석되므로, 상속된 값이 당신이 선택한 디렉터리를 조용히 덮어쓸 수 있다. 사용자 명령을 도구가 선택한 디렉터리로 옮겨 실행하는 모든 생성 지점은 이 변수들을 반드시 지워야 한다. 사용자 자신의 컨텍스트에서 실행되는 자식 프로세스는 상속된 컨텍스트를 절대 경로화하여 그대로 유지한다.
## 메시지 문구가 아니라 종료 코드와 JSON 출력으로 판단하기
사람이 읽는 메시지를 파싱하는 것보다 종료 코드, 기계가 읽을 수 있는 상태, 구조화된 형식을 우선하라. 메시지 파싱은 로케일, 버전, 문구 변경에 쉽게 깨진다.
### 구조화된 채널을 선택하라
기본 사람용 출력 대신 porcelain 또는 JSON 출력과 숫자 상태 플래그를 읽는다. 명령이 종료 코드에 의미를 담고 있다면, 메시지 텍스트가 아니라 그 코드로 분기한다.
### 피할 수 없는 취약함은 반드시 문서화하라
구조화된 대안이 없어 텍스트를 파싱해야 할 때는, 파싱하는 코드 바로 옆에 그 취약함을 표시해서 다음 사람이 이것이 안정된 계약이 아니라 알려진 위험임을 알 수 있게 한다.
## 로컬 데이터로 먼저 보여 주고 네트워크 정보는 나중에 흘려 보내기
응답성은 하나의 기능이다: 먼저 로컬 데이터로 화면을 그리고, 느린 세부 정보는 그 뒤에 따라오게 하라.
### 첫 화면이 네트워크에 발목 잡히게 하지 마라
사용자의 요청에 실제로 필요할 때만 네트워크에 접근하라. 모든 명령은 먼저 로컬 데이터로 첫 출력을 그리고, 네트워크에서 얻는 세부 정보는 점진적으로 스트리밍해야 한다. 원격 호출이 돌아올 때까지 첫 화면조차 그릴 수 없는 명령은 실패 상태이며, 처음 설정하는 환경에서 특히 최악이다. 셸 프롬프트처럼 동기적으로 자주 실행되는 경로는 점진적으로라도 네트워크에 접근해서는 안 된다.
### 실시간 원격 데이터는 사용자가 경계를 정하는 맥락에서 다뤄라
명령 출력은 버퍼링하지 말고 한 줄씩 스트리밍하라. 대화형 선택기(picker)는 네트워크 작업을 담기에 가장 관대한 곳이다. 즉시 화면을 그리고, 느린 호출이 끝나기 전에 사용자가 다른 행동을 하면 그 요청은 그냥 폐기된다. 끝까지 실행되어야 완료되는 명령은 모든 호출이 돌아올 때까지 끝날 수 없으므로, 실시간 원격 데이터는 선택기 안에서 점진적으로 가져온다.
## 사용자가 인터럽트하면 실행 중인 명령의 루프를 모두 멈추기
자식 프로세스가 시그널(인터럽트 또는 종료)로 인해 죽었다면, 포그라운드 실행 경로에 있는 모든 반복문은 다음 반복으로 넘어가지 말고 즉시 중단해야 한다.
### 구조화된 시그널 채널을 읽어라
시그널로 인한 종료는 구조화된 시그널 필드로 감지하라. 128 이상의 종료 코드를 추측하거나 오류 텍스트를 파싱하는 방식으로는 절대 감지하지 않는다. 표준화된 "인터럽트로 종료됨" 결과를 전파하고 반복문을 빠져나온다.
### 오류를 삼키는 분기보다 먼저 확인하라
실패 처리 전략이 실행되기 전에 인터럽트 확인을 먼저 하라. 그래야 "경고만 하고 계속 진행"하는 정책이 인터럽트를 삼켜버리지 않는다. 자식 프로세스를 반복 처리하는 새로운 코드는 반복마다 이 확인을 수행해야 한다. 그렇지 않으면 여러 단계로 이루어진 작업 중 인터럽트 한 번이 발생해도 남은 단계들을 그대로 밀고 나가게 된다.
## 프로젝트가 정의한 훅과 별칭은 승인 뒤에만 실행하기
프로젝트가 정의한 명령(생명주기 훅, 별칭, 프로젝트 설정에서 가져온 실행 본문)은 사용자가 방금 클론했을 수도 있는 저장소에 담겨 있는 임의의 코드다. 이런 명령은 승인 시스템이 통과시킨 후에만 실행한다.
### 이 관문(gate)이 보안 경계의 전부다
그 관문을 거치지 않고 프로젝트가 정의한 명령을 실행하는 코드 경로는 절대 만들지 마라. "이미 이 작업을 검증했으니 그 훅도 함께 실행하자"는 지름길은, 프로젝트 설정을 건드리는 모든 명령을 원격 코드 실행 취약점으로 바꿔버린다. 저장소를 클론하는 것과 인터넷에서 받은 스크립트를 훅이 실행하는 것 사이를 막는 유일한 존재가 바로 이 관문이다.
### 승인된 집합은 한 번, 불변으로 선택하라
결정 시점에 명령 집합을 불변의 승인된 계획(plan)으로 묶어두고, 실행자는 그 값만 소비하도록 하며 재도출할 수 있는 어떤 것도 갖지 않게 하라. 그러면 승인되지 않은 명령을 다시 선택하는 것이 리뷰 확인이 아니라 컴파일 오류가 된다. 사용자에게 물어볼 수 없는 맥락에서는 승인 상태를 읽기 전용으로만 참조하고, 이미 승인된 부분집합만 실행한다.
## 커버리지 검사가 실패하면 사용자 허락 없이 병합하지 않기
커버리지 관문이 실패한 변경사항은, 플랫폼이 그 관문을 "필수 아님(not required)"으로 표시하더라도 사용자의 명시적 승인 없이는 절대 병합하지 마라. 실제로는 여전히 병합을 막는 조건이기 때문이다.
### 신호가 어떻게 보고되는지 파악하라
커버리지 도구는 결과를 커밋 상태(commit status)가 아니라 체크 실행(check run)으로 게시할 수 있으며, 이는 다른 API를 통해 읽어야 한다. 통합 상태(combined-status) 조회는 체크 실행을 아예 놓칠 수 있고, 결과는 작업이 끝난 뒤 몇 분 지나서야 반영될 수 있다. 변경사항이 통과했다고 결론짓기 전에 올바른 API를 폴링하라.
### 공백을 메워라, 그냥 통과시키지 마라
커버리지가 실패하면 원인을 조사하고 공백을 메워라: 테스트를 추가하거나, 일반 경로로 넘어가는 것으로 충분한 특수 케이스 처리기를 포함해 사용하지 않는 코드를 삭제한다. 오탐(false positive)이라고 생각되면 혼자 판단해서 병합하지 말고 사용자에게 먼저 확인하라. CI 파이프라인이 사용하는 것과 동일한 기능 플래그로 커버리지를 실행해서, "기능이 꺼져 있었다"는 이유로 실패를 무시할 수 없게 하라.
## 기존 라이브러리를 쓰고 경고는 숨기지 말고 고치기
유틸리티를 직접 만들기 전에 기존 의존성을 먼저 확인하라. 경로 정규화, 셸 이스케이프, 색상 처리, 템플릿 변수 감지처럼 검증된 라이브러리는, 예외 상황을 제대로 처리하지 못하는 즉흥적인 재구현보다 낫다.
### 경고를 눈속임으로 가리지 마라
경고를 일괄적으로 억제해서 조용히 만들지 마라. 죽은 코드는 삭제하거나, 날짜와 설명이 붙은 표시를 남겨 구체적인 향후 작업과 연결해두어라.
### 복잡한 시스템은 그 자리에서 문서화하라
한눈에 이해되지 않는 시스템(상태 머신, 캐시된 상태, 모듈 간 조율, 미묘한 무효화 로직)에는 목적, 핵심 결정, 계약, 불변조건을 다루는 모듈 수준의 명세를 함께 두고 최신 상태로 유지한다.
### 테스트 코드는 라이브러리 코드 밖에 두어라
라이브러리 타입에 테스트 전용 편의 메서드를 추가하지 마라. 테스트는 실제 API를 호출하거나 자체 헬퍼를 정의해야 한다. 여러 줄 리터럴에는 공백을 조용히 없애버리는 줄바꿈 이스케이프보다, 실제로 개행이 포함된 형태를 사용하는 편이 낫다.
## 오류에 맥락을 담고 크래시로 바꾸지 않기
모든 오류에는 맥락을 함께 담아라. 예상 가능한 실패 상태(정리되지 않은 작업 공간, 존재하지 않는 브랜치, 잘못된 상태)에는 명시적인 비즈니스 로직 오류를 사용하고, I/O나 외부 명령 실패에는 맥락을 감싸는(context-wrapping) 방식을 사용한다.
### 실패를 절대 삼키지 마라
오류 타입을 반환하는 함수 안에서는, 복구 가능한 실패를 크래시로 바꿔버리는 panic-on-absent나 unwrap 스타일의 호출을 절대 사용하지 마라. 오류 전파 연산자를 사용하거나, 명시적으로 오류를 발생시키거나, 오류를 반환하라. 감싸진(wrapped) 모든 오류는 어떤 작업이 왜 실패했는지 읽는 사람에게 알려주어야 한다.
## 옛 설정 키는 조용히 버리지 말고 새 형태로 옮기기
설정 호환성 처리는 모두 한 계층에 모아두어라: 파서가 보기 전에 지원 중단된 패턴을 표준 형식으로 다시 써라. 오래된 설정 키를 조용히 버리지 마라. 그것은 눈에 띄지 않는 동작 변경이기 때문이다. 대신 마이그레이션하라.
### 감지와 마이그레이션은 하나의 판단 함수로
지원 중단된 패턴을 다시 쓰는 것과 무엇이 바뀌었는지 보고하는 것을 하나의 멱등(idempotent) 함수가 함께 담당하게 하라. 그러면 감지와 마이그레이션이 같은 판단 기준을 공유하게 되어 서로 어긋날 수 없다. 감지는 임시 사본에 같은 함수를 적용해서 수행하라. 그러면 실제 업데이트가 파일을 바꿀 그 순간에 정확히 경고가 발생한다. 그 동등성은 테스트로 고정해두어라.
### 각 규칙의 적용 시점을 분류하라
규칙마다 다시 쓰기가 언제 적용될지 정하라: 매번 불러올 때마다, 명시적인 업데이트 시에만, 경고 없이 조용히, 또는 이후 릴리스가 채택할 예정인 기본값으로. 안전하게 다시 쓸 수 없는 값(형식이 잘못됐거나, 대상 키가 이미 사용 중인 경우)은 손대지 말고 그대로 두고, 메시지는 파서 자체의 타입 오류가 전달하게 하라.
## 접근자 이름의 접두사로 비용을 알리고 캐시 위치 정하기
접근자(accessor) 이름에 의도를 담아서, 호출하는 쪽이 접두사만 보고도 비용과 실패 방식을 예측할 수 있게 하라.
### 접두사가 계약을 전달하게 하라
일관된 규칙을 채택하라: 명사만 있는 이름은 부수 효과 없이 값을 반환하며 캐시될 수 있다. set 접두사가 붙은 이름은 상태를 쓰고 오류가 날 수 있다. require 접두사가 붙은 이름은 값이 없으면 오류를 낸다. fetch 접두사가 붙은 이름은 네트워크 I/O를 수행한다. load 접두사가 붙은 이름은 파일 I/O를 수행한다. 명사만으로도 더 자연스럽게 읽히는 곳에는 불필요한 get 접두사를 붙이지 마라.
### 캐시된 상태가 어디에 위치할지 정하라
읽기 전용 캐시 값은 복제(clone) 시 공유되는 참조 카운트 방식 캐시를 통해 공유하라. 비용과 사용 빈도에 따라 계층을 선택하라: 비용이 낮고 자주 쓰이는 값은 메모리 내 get-or-create 방식을, 비용이 높은 값은 디스크에 영속화하고, 둘 다 해당하는 값은 영속 저장소 앞단에 메모리 캐시를 두어라. 무엇이 캐시되고 무엇이 캐시되지 않는지, 그리고 변경(mutation) 후 캐시를 갱신하는 불변조건을 문서화하라.
