콘텐츠로 이동

코드 인텔리전스

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를 지웁니다. 질문 하나마다 전체 테스트 실행이 필요하며, 실행하기 전에 생각해 낸 질문에만 답해 줍니다.

중지된 프레임은 그 순간에 관한 모든 질문에 한 번에 답합니다. 모든 지역 변수, 모든 호출자, 그리고 프레임 자체의 스코프에서 평가된 표현식까지요. 멈춰 버린 프로세스나 열 번에 한 번만 재현되는 실패의 경우, 이것은 오후 한나절과 1분의 차이입니다.

인클로저처럼, 같은 이유로 경계가 있습니다

섹션 제목: “인클로저처럼, 같은 이유로 경계가 있습니다”

디버그 어댑터는 중지된 프로세스를 소유하며, 그 프로세스가 쥐고 있는 것 — 포트, 잠금, 데이터베이스 연결 — 을 함께 붙듭니다. 아무도 조종하지 않는 세션은 그것을 시작한 턴보다 오래 사는 누수입니다. 그래서 하드 TTL이 있고, 대화당 정확히 하나의 세션만 허용되며, 두 번째 launch는 조용히 첫 번째를 대체하는 대신 거부됩니다. 첫 번째를 끝내지 않고 두 번째를 시작하는 에이전트는 거의 확실히 첫 번째를 놓치고 있는 것이기 때문입니다.

Debug Adapter Protocol은 Language Server Protocol의 Content-Length 프레이밍만 공유하고 그 위의 것은 아무것도 공유하지 않습니다. 상관 키도 다르고, 실패 형태도 다르며, LSP에 대응물이 없는 세 번째 메시지 유형(이벤트)도 있습니다. 프레이밍은 한 곳에만 존재하고, 두 프로토콜은 그 위에 각각 올라갑니다.

프레이밍을 두 번 작성하면 “메시지가 두 청크에 걸쳐 있다”, “한 청크에 메시지 세 개가 들어 있다”, “서버가 첫 프레임 전에 stdout에 배너를 썼다”에 대한 답이 두 벌 생기고, 그중 하나만 수정될 것입니다.