코드 인텔리전스
Control Center는 에이전트에게 코드를 읽는 것 이상으로 코드를 이해하는 네 가지 방법을 제공합니다. 이들은 정말로 서로 다른 질문에 답하며, 하나가 아니라 넷인 이유는 어떤 단일 메커니즘도 이 질문 모두에 답하지 못하기 때문입니다.
| 메커니즘 | 답하는 질문 | 비용 |
|---|---|---|
| 코드 그래프 | 여기에 무엇이 있고 무엇이 이에 의존하는가 | 인덱스 패스 한 번, 조회는 저렴함 |
| 언어 서버 | 실제로 컴파일되는지, 이 타입은 무엇인지 | 프로젝트당 서버 프로세스 하나 |
| 구조적 검색 | 이 형태는 어디에 나타나는가 | 파일당 파싱 한 번 |
| 디버거 | 이 순간, 이 프레임에서 무엇이 참이었는가 | 중지된 프로세스 하나 |
언어 서버가 메우는 간극
섹션 제목: “언어 서버가 메우는 간극”코드 그래프는 tree-sitter로 만들어집니다. 심볼, 호출 엣지, 영향 반경을 담고 있습니다. 빠르고 저렴하지만 타입에 대해서는 아무것도 모릅니다. 방금 작성한 호출이 컴파일되는지, 그 필드가 객체에 존재하는지, 이름을 바꿀 때 재내보내기를 놓쳤는지는 알려주지 못합니다.
언어 서버가 없으면 에이전트는 테스트할 때에야 — 아니면 영영 — 알게 됩니다. 그럴듯한 코드를 작성하고 넘어가면, 실패는 세 턴 뒤에 다른 변경 사항에 얹혀 모습을 드러냅니다.
따라서 진짜 이득은 lsp 도구 자체가 아닙니다. write, edit, apply_patch가 감싸진다는 점입니다. 모든 쓰기 후에 파일이 언어 서버를 거치고, 새로 도입된 진단이 도구 결과에 접혀 들어갑니다. 에이전트는 물어볼 생각을 했든 안 했든 상관없이, 빌드가 깨진 바로 그 순간에 그것을 알게 됩니다.
새로운 진단만
섹션 제목: “새로운 진단만”이미 에이전트에게 알려 준 내용을 반복하는 것은 아무것도 말하지 않는 것보다 못합니다. 결과가 에이전트가 대충 훑고 넘어가게 되는 잡음으로 가득 차고, 정작 새로운 오류 하나도 그 잡음에 묻혀 버립니다. 실행별 원장이 이미 보고된 항목을 추적하는데, 기준은 심각도, 코드, 메시지이며 위치는 아닙니다. 따라서 네 줄 아래로 내려갔을 뿐인 진단은 새로 보고되지 않습니다.
필요할 때만 시작하고, 없으면 솔직하게
섹션 제목: “필요할 때만 시작하고, 없으면 솔직하게”부팅 시에는 어떤 서버도 시작되지 않습니다. 서버는 이를 필요로 하는 첫 요청에 시작되며, 그 요청은 거의 항상 해당 서버가 담당하는 파일에 대한 첫 번째 편집입니다. 유휴 상태가 된 서버는 정리됩니다. Dart를 전혀 건드리지 않는 호스트는 분석기를 시작하지 않습니다.
탐지는 교집합입니다. 체크아웃에 루트 마커가 있어야 하고, 바이너리가 해석되어야 합니다. 마커만 있으면 “이 프로젝트는 Python이다”라는 사실만 알 뿐 서버가 설치되었는지는 알 수 없습니다. 해석 가능한 바이너리만 있으면 호스트에 툴체인이 있다는 것만 알 뿐 이 체크아웃에 대해서는 알 수 없습니다. 둘 다 있을 때에만 쿼리가 성공할 수 있으며, 제공되지만 항상 실패하는 도구는 아예 없는 도구보다 못합니다.
grep이 있는데 구조적 검색이 필요한 이유
섹션 제목: “grep이 있는데 구조적 검색이 필요한 이유”grep은 문자를 매칭합니다. 이름을 찾을 때는 맞는 도구지만, 형태를 찾을 때는 틀린 도구입니다.
구조적 패턴은 파일과 동일한 문법으로 파싱되므로, dispose($X)는 공백이나 줄바꿈, 감싸는 방식과 상관없이 해당 호출을 찾아내고, 문자열 리터럴이나 주석 안에서 같은 문자를 매칭하는 일은 없습니다.
이것을 단순히 단계만 몇 개 더 많은 정규식 이상으로 만들어 주는 특성은, 하나의 패턴에서 반복된 메타변수가 동일한 텍스트를 캡처해야 한다는 점입니다.
if ($X != null) $X.dispose()이 패턴은 자신이 검사한 대상을 dispose하는 가드는 매칭하고, 다른 것을 dispose하는 가드는 매칭하지 않습니다. 이것은 텍스트만으로는 전혀 표현할 수 없는 부류의 버그입니다.
이미 연결된 tree-sitter에서 실행됩니다
섹션 제목: “이미 연결된 tree-sitter에서 실행됩니다”두 번째 엔진은 없습니다. cc_natives는 이미 코드 그래프를 위해 tree-sitter와 그 문법을 로드하며, 컴파일된 쿼리 캐시와 언어 테이블도 갖추고 있습니다. 매처는 어차피 파싱되고 있던 트리 위에서 동작하는 순수 Dart입니다. 두 번째 구조적 엔진을 만든다는 것은 두 번째 파서, 모든 플랫폼에서 빌드하고 스테이징해야 하는 두 번째 문법 세트, 그리고 “호출 표현식이란 무엇인가”에 대한 두 개의 답을 의미합니다.
조각에는 있을 자리가 필요합니다
섹션 제목: “조각에는 있을 자리가 필요합니다”문법은 온전한 파일이 무엇인지만 정의합니다. Dart의 최상위 수준에서 그대로 파싱하면, dispose(x)는 완벽하게 유효한 함수 시그니처 — x 매개변수를 받는 dispose라는 선언 — 이며, 그것이 잘못되었음을 알려 줄 오류는 어디에도 없습니다. 그 파싱 결과 위에 세운 매처는 선언을 찾고, 호출은 찾지 못한 채, 겉모습대로 읽힌 적이 없는 패턴에 대해 “일치 항목 없음”이라고 보고합니다.
그래서 패턴은 스캐폴드 사다리 — 문장, 표현식, 클래스 멤버, 최상위 — 를 차례로 시도되고, 처음으로 받아들이는 스캐폴드가 선택됩니다. 어떤 컨텍스트도 받아들이지 않는 조각은 검색해 놓고 조용히 찾지 못한 것으로 처리되는 대신, 파싱할 수 없음으로 보고됩니다.
재작성이 쓰지 않고 스테이징하는 이유
섹션 제목: “재작성이 쓰지 않고 스테이징하는 이유”구조적 재작성은 에이전트가 할 수 있는 가장 광범위한 편집입니다. 패턴 하나, 파일 마흔 개이며, 결과를 한 줄씩 읽는 사람은 아무도 없습니다. 모델도, 그것을 요청한 사람도 마찬가지입니다. 그냥 쓰고 “완료”라고 보고하면, 그 변경을 제대로 확인하는 첫 순간은 나중에 git diff를 뜰 때가 됩니다.
그래서 ast_edit는 자기가 하게 될 일 — 개수, 파일, 매칭된 지점 — 을 보고하고, 변경은 resolve가 커밋할 때에만 반영됩니다. 또한 이 덕분에 무엇이든 디스크에 닿기 전에 diff와 수락/버리기 선택이 사람 앞에 놓일 수 있습니다.
커밋은 전부 아니면 전무이며, 모든 파일이 스테이징 시점에 캡처한 내용과 대조됩니다. 스테이징과 커밋 사이에 에이전트가 그 파일 중 하나를 손으로 편집했을 수도 있고, 진단 패스가 그것을 다시 작성했을 수도 있으며, 포매터가 건드렸을 수도 있습니다. 그런데도 커밋하면 그 작업이 버려지고 diff는 의도된 것처럼 보이게 됩니다. 조금이라도 어긋나면 변경 전체가 거부됩니다. 일부만 적용된 구조적 재작성은 어느 형태로도 컴파일되지 않는 트리를 남기기 때문입니다.
print 문이 답하지 못하는 것을 디버거가 답하는 방법
섹션 제목: “print 문이 답하지 못하는 것을 디버거가 답하는 방법”print 문 루프는 이렇습니다. print를 추가하고, 전체를 다시 실행하고, 출력을 읽고, print를 지웁니다. 질문 하나마다 전체 테스트 실행이 필요하며, 실행하기 전에 생각해 낸 질문에만 답해 줍니다.
중지된 프레임은 그 순간에 관한 모든 질문에 한 번에 답합니다. 모든 지역 변수, 모든 호출자, 그리고 프레임 자체의 스코프에서 평가된 표현식까지요. 멈춰 버린 프로세스나 열 번에 한 번만 재현되는 실패의 경우, 이것은 오후 한나절과 1분의 차이입니다.
인클로저처럼, 같은 이유로 경계가 있습니다
섹션 제목: “인클로저처럼, 같은 이유로 경계가 있습니다”디버그 어댑터는 중지된 프로세스를 소유하며, 그 프로세스가 쥐고 있는 것 — 포트, 잠금, 데이터베이스 연결 — 을 함께 붙듭니다. 아무도 조종하지 않는 세션은 그것을 시작한 턴보다 오래 사는 누수입니다. 그래서 하드 TTL이 있고, 대화당 정확히 하나의 세션만 허용되며, 두 번째 launch는 조용히 첫 번째를 대체하는 대신 거부됩니다. 첫 번째를 끝내지 않고 두 번째를 시작하는 에이전트는 거의 확실히 첫 번째를 놓치고 있는 것이기 때문입니다.
하나의 전송, 두 개의 프로토콜
섹션 제목: “하나의 전송, 두 개의 프로토콜”Debug Adapter Protocol은 Language Server Protocol의 Content-Length 프레이밍만 공유하고 그 위의 것은 아무것도 공유하지 않습니다. 상관 키도 다르고, 실패 형태도 다르며, LSP에 대응물이 없는 세 번째 메시지 유형(이벤트)도 있습니다. 프레이밍은 한 곳에만 존재하고, 두 프로토콜은 그 위에 각각 올라갑니다.
프레이밍을 두 번 작성하면 “메시지가 두 청크에 걸쳐 있다”, “한 청크에 메시지 세 개가 들어 있다”, “서버가 첫 프레임 전에 stdout에 배너를 썼다”에 대한 답이 두 벌 생기고, 그중 하나만 수정될 것입니다.
관련 항목
섹션 제목: “관련 항목”- 코드 그래프로 코드 검색 — 심볼과 영향 측면
- 에이전트로 실패 테스트 디버그 — 디버거를 처음부터 끝까지
- 구조적으로 코드 리팩터 —
ast_grep과ast_edit - 내장 에이전트 도구 — 모든 연산과 인수