OpenAI Codex · Code Mode

apply-patch, 모델을 위해 설계한 diff

모델은 줄 번호 없는 패치를 채우고, 사람이 보는 건 사후에 계산한 unified diff예요. 같은 수정, 두 가지 형식 — 각자 어디서 엎어지는지 보세요.

강의 목표읽고 나면 두 가지를 말할 수 있어요. 모델에게 주는 diff가 왜 @@ -l,s +l,s 네 숫자가 아니라 컨텍스트 앵커 한 줄만 쓰는지, 그리고 컨텍스트가 안 맞을 때 왜 파일 하나는 통째로 디스크에 안 쓰이며, 여러 파일에 걸쳐서는 그 보장이 왜 성립하지 않는지요.
먼저 해보기 · 같은 수정, 두 가지 쓰기
greet 안의 passreturn 123으로 바꾸기
디스크의 파일
두 줄 삽입으로 바꾸면 왼쪽 네 숫자가 어긋나요. 앵커가 사라진 쪽으로 바꾸면 오른쪽이 펜을 내려놓아요.
unified diff네 숫자맞춰야 함
시작을 기다리는 중.
apply-patch한 줄 앵커지금 검색
@@ def greet():
시작을 기다리는 중.
논리 궤적 · 애니메이션 각 단계가 소스의 어느 구간에 대응하는지
  1. 문법의 @@는 앵커만 있고 시작 줄과 폭은 없어요parser.rs L20
  2. 파일을 갱신하는 chunk는 파일에 나타나는 순서대로여야 해요parser.rs L74
  3. change_context가 있으면 line_index부터 아래로 이 줄을 찾아요file_update.rs L99
  4. 먼저 줄 전체 정확 비교, 그다음 줄 끝 공백을 지우고, 그다음 양쪽 trimseek_sequence.rs L40
  5. 앵커를 못 찾으면 바로 Failed to find context를 내고, 근처 줄로 짐작하지 않아요file_update.rs L109
  6. 한 파일의 chunk를 메모리에서 다 계산한 뒤에야 write_file을 호출해요lib.rs L695
  7. 파일 여러 개에서 실패하면 이미 커밋된 delta를 들고 돌아오고, 롤백은 없어요lib.rs L453
  8. 사람이 보는 unified diff는 사후에 TextDiff로 따로 계산해요file_update.rs L328
재생을 눌러, 같은 수정이 두 diff 쓰기에서 어떻게 위치를 잡는지 보세요.
네 숫자unified diff의 @@ 헤더는 옛 시작·옛 폭·새 시작·새 폭을 한꺼번에 맞춰야 해요. 위에 두 줄을 넣으면 네 숫자가 함께 죽어요.
한 줄 앵커apply-patch는 @@ def greet():만 쓰고 런타임에 찾아요. 두 줄을 넣어도 맞아요. 줄 번호가 이 형식에 들어오지 않았거든요.
안 맞으면 멈춤앵커 줄이 바뀌면 오른쪽은 Failed to find context를 내고 파일은 그대로예요. 왼쪽 형식은 줄 번호 근처에서 fuzz하다가 이웃 함수에 붙을 수 있어요.
수업용 스케치: 파일 덩어리와 줄 번호는 두 형식의 위치 잡기를 대조하기 위한 수업 설정입니다. 논리 궤적 오른쪽 줄 번호는 openai/codex 저장소 commit 4f39251a01에 대응합니다.
아이디어 1 · 모델은 한 형식을 채우고, 사람은 다른 형식을 봐요
어떤 문제를 푸는가

모델에게 함수 하나를 고치게 해요. greet 안의 passreturn 123으로. 표준 unified diff를 뱉고 첫 줄을 @@ -47,3 +47,3 @@로 씁니다.

방금 다른 편집이 두 줄을 넣어서 greet는 이미 49행이에요. 모델은 줄 번호가 있는 발췌에서 세었고, 패치를 쓸 때 시작 줄과 폭을 다시 더해야 해요. 네 숫자가 함께 틀리는 게 일상입니다.

patch(1)은 줄 번호로 찾고, 없으면 fuzz해요. fuzz도 실패하면 패치 전체가 죽어요. 더 골치 아픈 건 이웃 함수에 변경을 붙일 수 있다는 점이에요. 테스트는 초록인데, 틀린 함수를 고친 거죠.

아이디어는 무엇인가

Codex는 채우기와 읽기를 갈라요. 모델이 채우는 쪽에는 줄 번호가 없어요. 한 구간을 갱신할 때 첫 줄은 @@ def greet():뿐이에요. 홀로 있는 @@는 현재 위치부터 계속 찾으라는 뜻이고, 위치 잡기는 런타임의 seek_sequence에 맡깁니다.

사람이 변경을 볼 때, UI는 similar::TextDiff로 표준 unified diff를 따로 만들어요. 도구 인자에 있던 Codex 형식은 여기서 이미 쓰임을 다합니다.

출처:codex-rs/apply-patch/src/file_update.rs 328–329행

이 문법은 ‘줄 번호 없음’을 생성 규칙에 넣었어요. @@ 두 가지 쓰기 모두 텍스트 앵커만 달고 있어요.

codex-rs/apply-patch/src/parser.rs20–22행
//! change_context: ("@@" | "@@ " /(.+)/) LF
//! change_line: ("+" | "-" | " ") /(.+)/ LF
//! eof_line: "*** End of File" LF
소스 스냅샷 안내: 로컬 저장소 openai/codex 기준, 확인 파일 codex-rs/apply-patch/src/parser.rs, commit 4f39251a01, 확인일 2026-08-22. 코드 블록은 소스 원문을 유지합니다. 이 세 줄이 모델에게 주는 diff 헤더예요. 앵커는 있고 줄 번호는 없습니다.

모델에게 보내는 설명서는 이 언어를 “stripped-down, file-oriented diff format designed to be easy to parse and safe to apply”라고 적어요. Add, Delete, Move는 문법의 세 표시이고, 파서가 표시대로 나눕니다. 모델은 ---, +++, /dev/null, rename 헤더를 어떻게 붙이는지 외울 필요가 없어요.

모델이 채움 모델 패치를 씀 apply-patch @@ 앵커, 줄 번호 없음 seek_sequence 디스크에서 지금 찾아 새 내용을 계산 사람이 읽음 이미 계산된 옛·새 텍스트 도구 인자 형식은 여기서 끝 TextDiff 사후에 unified diff 생성 UI 사람이 보는 그 한 부
수업용 구조도: 같은 수정, 모델은 앵커를 채우고 사람은 줄 번호를 봐요.
왜 오래가는가

좌표는 덧셈이고, 내용은 인식이에요. 모델이 줄 번호를 세는 일은 모델을 바꾸든 언어를 바꾸든 나아지지 않아요. ‘어느 구간인가’를 채울 때의 산술에서 적용할 때의 문자열 검색으로 옮기는 이 분업은 Rust에도, unified diff라는 구체 형식에도 기대지 않습니다.

아이디어 2 · 공백은 느슨해져도 되고, 위치는 짐작하지 않아요
어떤 문제를 푸는가

모델이 패치를 쓸 때 줄 끝에 공백 하나, 또는 파일은 en-dash인데 빼기로 쓰는 일은 흔한 사고예요. 매번 통째로 실패하면 모델은 다시 쓸 수밖에 없어요. 줄 번호 근처를 몇 줄 더 시험하면, fuzz가 틀린 함수에 붙는 옛길로 돌아갑니다.

아이디어는 무엇인가

seek_sequence는 네 단계, 촘촘한 쪽에서 느슨한 쪽으로 찾아요. 1단계 줄 전체 정확 일치. 2단계 줄 끝 공백을 지우고 비교. 3단계 양쪽 trim(). 4단계 흔한 Unicode 짧은 가로와 굽은 따옴표를 ASCII로. 네 단계가 모두 실패하면 빈 값을 돌려 “Failed to find context” 또는 “Failed to find expected lines”를 냅니다. 근처 몇 줄을 다시 시험하는 루프는 없어요.

출처:codex-rs/apply-patch/src/seek_sequence.rs 40–114행

초기에 사고 하나가 있어서, 이상한 Unicode 문자 때문에 4단계를 넣었어요. 공백과 문장부호만 느슨하게 하고 위치는 느슨하게 하지 않아요. 전각 중문 따옴표는 정규화 표에 없어서, 모델이 전각 왼쪽 따옴표를 쓰고 파일은 반각이면 네 단계가 모두 실패합니다.

정확 일치 trim_end 양쪽 trim normalise 네 단계 모두 실패, 빈 값 줄 번호로 위아래 몇 줄 옮겨 재시도 없음 바로 오류
수업용 흐름도: 공백과 짧은 가로는 통과할 수 있고, 줄 번호 어긋남은 이 네 단계 안에 없어요.
왜 오래가는가

관용은 차이 두 종류를 갈라야 해요. 줄 끝 공백과 굽은 따옴표는 의미 없는 바이트 차이라 정규화해도 돼요. 줄 번호가 두 줄 어긋난 것은 틀린 자리에 붙는 일이니, 오류를 내고 모델이 다시 쓰게 해야 해요. 이 경계는 다른 언어로 다시 써도 성립합니다.

아이디어 3 · 한 파일은 계산이 끝난 뒤 쓰고, 여러 파일에는 트랜잭션이 없어요
어떤 문제를 푸는가

패치 하나에 chunk가 둘이에요. 두 번째가 안 맞는데 첫 번째는 이미 들어갔으면 파일이 반제품이 돼요. 디버그하는 사람은 절반만 적용된 파일을 보게 되고, 통째 실패보다 고치기 더 어려워요.

아이디어는 무엇인가

한 파일 안에서 compute_replacements는 각 chunk를 먼저 치환 목록으로 모아요. 하나라도 안 맞으면 바로 오류를 돌려 write_file까지 가지 않아요. 그 파일은 그대로입니다.

출처:codex-rs/apply-patch/src/file_update.rs 109–113행

여러 파일은 다른 이야기예요. apply_hunks_to_files는 hunk 순서로 디스크에 쓰고, 실패하면 이미 커밋된 AppliedPatchDelta를 들고 돌아와요. 루프에 롤백은 없습니다. 테스트 015가 이걸 못 박았어요. 먼저 created.txt 추가에 성공한 뒤 없는 경로를 갱신하면, 디스크의 created.txt는 그대로 남아요.

출처: codex-rs/apply-patch/src/lib.rs 453행, 그리고 504행부터의 hunk 루프
같은 파일의 chunk 둘 chunk 1을 메모리에서 계산 완료 chunk 2가 안 맞아 반환 write_file에 못 가, 파일 그대로 파일 단위 hunk 둘 Add File이 이미 디스크에 없는 경로를 Update delta는 남고 created.txt도 남음
수업용 대조: ‘부분 성공 없음’ 보장은 파일 하나에만 성립해요.
모델은 앵커를 채워요. 사람은 줄 번호를 봐요. 파일 하나가 안 맞으면 쓰지 않아요.
왜 오래가는가

계산한 뒤 커밋하는 범위는, 원자적으로 다룰 수 있는 단위와 맞춰야 해요. 파일 하나는 메모리에서 치환을 다 계산한 다음 한 번 쓰면 돼요. 여러 파일은 이미 디스크에 떨어져 있어서, 롤백은 다시 써야 하고 Move처럼 출발과 도착이 모두 움직인 반성공도 처리해야 해요. 파일 사이 트랜잭션을 둘지는 제품 선택이지, 형식 자체의 약속이 아닙니다. 모델에게 주는 설명에서 한 파일의 보장을 전역 보장처럼 말하지 마세요.

가로 비교 · 같은 문제에 대한 다른 답

DeepSeek Harness: 먼저 읽어야 고칠 수 있어요

DSH의 editIntent는 이 세션이 그 파일을 관측했는지 봐요. 관측이 없으면 FS_NOT_OBSERVED를 던져요. 기록하는 것은 dev:ino:size:mtimeNs:ctimeNs를 이은 버전이지, 내용 해시가 아닙니다.

그 문을 지나도 applyLiteralEdit는 기본적으로 old_string이 한 번만 나오길 바라고, 여러 곳에 맞으면 FS_AMBIGUOUS_EDIT를 던져요. 실수 방지는 이벤트 문과 리터럴 유일성에 걸려 있어요. Codex에는 먼저 읽기 제약이 없고, 위치 정보는 패치에 쓰여 런타임에 찾으며, old_lines가 두 번 나오면 첫 곳을 쓰고 모호함 오류는 없습니다.

양쪽 모두 소스 대조 완료 · 2026-08-22 · DSH · 파일 편집의 공학

Claude Code: 안 읽었으면 거절, 여러 곳에 맞아도 거절

FileEditTool은 두 가지를 동시에 요구해요. 파일은 먼저 읽어야 하고, 안 읽었으면 errorCode 6, 원문은 File has not been read yet. old_string이 파일에 두 곳 이상이고 replace_all이 거짓이면 errorCode 9를 내며, 이 한 곳을 따로 표시하도록 컨텍스트를 더 달라고 해요.

퍼지는 따옴표만 덮어요. findActualString은 먼저 정확히 찾고, 그다음 굽은 따옴표를 곧은 따옴표로 접어 찾아요. Codex처럼 줄 끝 공백 세 단계도, 짧은 가로 정규화도 없어요. 도구 왕복을 한 번 줄이려면 Codex 형식을 베끼고, 모델에게 오류를 더 구체적으로 주려면 이 errorCode 거절 문구를 베끼세요.

양쪽 모두 소스 대조 완료 · 2026-08-22
수업 실습
01

같은 old_lines가 두 단락일 때, 어느 쪽을 고치나

파일에 완전히 같은 old_lines가 두 단락 있고, 모델은 둘째만 고치고 싶은데 @@ 앵커가 부족해요. seek_sequence는 어느 단락을 고치고, 왜일까요?

심화 한 질문: 둘째 단락만 맞추려면 앵커를 어느 줄 앞에 써야 할까요? 같은 패치에서 파일 하나 추가에 성공한 뒤 없는 경로를 갱신하면, 디스크에는 무엇이 남나요?

Takeaway:모델에게 주는 diff에는 줄 번호를 넣지 말고, 위치는 런타임이 컨텍스트를 찾게 하세요. 공백과 문장부호는 단계적으로 느슨해져도 되고, 위치는 짐작하지 않아요. 파일 하나가 안 맞으면 디스크에 쓰지 않아요. 여러 파일에 걸쳐서는 이미 쓴 파일이 디스크에 남습니다.