ccg는 소스 주석에서 DoodlinDoc 태그(@intent, @domainRule 등)를 뽑아 심볼 노드에 바인딩함.
심볼 위에 데코레이터·어노테이션·속성·매크로가 끼어있을 때 주석→심볼 바인딩이 깨지는가가 핵심 관심사.
Red 테스트로 4개 언어 각각 fixture 파싱한 결과 (internal/parse/treesitter/binding_gap_integration_test.go):
| 언어 | 심볼 StartLine 위치 | 바인딩 결과 | 원인 |
|---|---|---|---|
Python # 주석 + @app.route·@login_required + def get_user |
def 줄 (5) |
❌ 실패 | gap=3 > maxGap(2). function_definition StartLine이 데코레이터 제외 |
Python """docstring""" + 데코레이터 + def |
def 줄 |
❌ 실패 | tree-sitter가 """..."""를 comment 노드로 인식하지 않음 — 아예 주석 수집에서 누락 |
Java /** @intent */ + @Service @Transactional @RequiredArgsConstructor + class |
첫 어노테이션 줄 (5) | ✅ 성공 | class_declaration이 어노테이션을 자식으로 포함해 StartLine이 어노테이션 줄로 잡힘 (gap=1) |
Rust /// @intent + #[tokio::main] + #[allow(...)] + fn main |
fn 줄 (5) |
⚠ 부분 실패 | gap=2로 maxGap 이내, 바인딩은 됨. 그러나 normalizer.go에 rust 케이스 누락 → /// 접두사 미제거 → @intent 태그 파싱 실패 |
C /** @intent */ + __attribute__((...)) + static inline int add |
__attribute__ 줄 (4) |
✅ 성공 | function_definition이 __attribute__를 포함 (gap=1) |
결론: 원래 진단한 "maxGap 때문에 데코레이터 건너뜀"은 Python에만 해당. 언어마다 tree-sitter 노드가 메타 표식을 포함하는지가 달라서 단일 처방 불가. 언어별 실측 기반 개별 조치 필요.
또한 Python에서 2번째 더 큰 문제 발견: """docstring"""은 comment 노드가 아니라 string 노드 → ccg 주석 수집기가 아예 못 잡음.
-
Red 테스트 작성 (Python/Java/Rust/C)
- 파일:
internal/parse/binder_test.go,internal/parse/treesitter/binding_gap_integration_test.go - Fixture:
testdata/binding_gap/{python,java,rust,c}/ - 현재 모두 실패 상태 기록 완료
- 파일:
-
P0-1. Python
function_definitionStartLine — 완료- 기대:
def줄이 아닌 첫 데코레이터 줄을 심볼 StartLine으로 (Java/C와 동일 동작) - Green 후보 1:
tags.scm(Python)에서decorated_definition을 함수 매칭으로 교체 - Green 후보 2: walker에서
function_definition의 부모가decorated_definition이면 부모 StartLine 사용 - Refactor:
class_definition도 동일 처리 (@dataclass등) - 영향 범위: Python golden.json 재생성 필요
- 기대:
-
Python docstring 2차 실측 (6 fixture +
python_docstring_variants_test.go)'''·"""·한 줄·r/f/bprefix 모두 동일한string노드 → prefix 분기 불필요- gap이 구조적으로 음수 — EndLine 가짜 설정 우회 불가
- walker + binder 둘 다 손대야 함
-
P0-2. Python 함수/클래스 docstring 수집 (구조 + 행위, 2 커밋) — 완료
- 단계 1 구조:
CommentBlock에IsDocstring bool·OwnerStartLine int추가, walker에collectDocstrings(조건:expression_statement.namedChild유일string+ 부모가block이면OwnerStartLine = 부모의 부모.StartLine) - 단계 2 행위: walker가
mergeCommentBlocks로 병합해 반환, binder에if cb.IsDocstring { if OwnerStartLine == node.StartLine → 바인딩 }경로 추가 - Red 테스트:
TestPythonDocstring_FuncDouble,FuncSingle,OneLine,Class4개 모두 Green
- 단계 1 구조:
-
P0-4. Python 모듈 docstring → File 노드 바인딩 (결정 D) — 완료
collectDocstrings에서 부모가module이면OwnerStartLine=0- binder의
NodeKindFile경로: 모듈 docstring(IsDocstring=true && OwnerStartLine==0) 또는 첫 일반 comment 분기 (첫 매치 후 break) - Red 테스트:
TestPythonDocstring_ModuleGreen
-
P0-3. Rust normalizer
///접두사 제거 — 완료internal/domain/annotation/normalizer.go:23-108의stripLinePrefix언어별 분기에rust케이스 누락- Green:
rust케이스 추가 —///(doc) 와//(일반) 둘 다 제거 - 추가 고려:
//!(inner doc),/** ... */,/*! ... */처리 - Red 테스트: rust fixture에서
ann.Tags에@intent가 들어있어야 통과
Red fixture + 통합 테스트 작성: testdata/binding_gap/{typescript,kotlin,php,cpp,go}/,
internal/parse/treesitter/binding_gap_p1_test.go + p1_ast_probe_test.go.
| 언어 | 심볼 StartLine | @intent 바인딩 | 원인 |
|---|---|---|---|
TypeScript @Injectable() + @Component(...) + export class |
class 줄 (4) |
❌ 실패 | export_statement 밑에 decorator + class_declaration이 형제. class_declaration.StartLine이 class 키워드 줄. Python decorated_definition 패턴과 유사 |
Kotlin @Composable @JvmStatic + fun |
@Composable 줄 (2) |
❌ 실패 | comment 노드 타입이 multiline_comment — walker collectComments가 comment/line_comment/block_comment만 인식. 주석 수집 자체가 누락됨 |
PHP #[Route(...)] + #[IsGranted(...)] + function |
#[Route] 줄 (3) |
❌ 실패 | StartLine 정상, gap=1 OK. 그러나 normalizer에 php 케이스 부재 → /** 블록 delimiter 미제거 → @intent 태그 파싱 실패 |
C++ [[nodiscard]] [[deprecated]] + inline int divide |
[[nodiscard]] 줄 (2) |
✅ 성공 | C와 동일 — function_definition이 [[...]] 속성 포함. gap=1 |
Go // @intent + //go:generate + type Pill int |
type Pill 줄 (5) |
⚠ 부분 성공 | //@intent와 //go:generate가 동일 CommentBlock으로 병합. gap=1 OK, @intent 바인딩 성공. 단 태그 Value에 go:generate stringer -type=Pill 섞여 들어감 — 부가 오염 |
-
P1-3. PHP normalizer 케이스 추가 — 완료
stripBlockDelimitersC-family case에"php"추가,/**,/*처리stripLinePrefix에 전용case "php"추가 —//,#,*모두 처리 (PHP는 C-style과 shell-style 주석 모두 지원)- Red 테스트:
TestWalkerBinder_PHP_Attributes_P1Measurement+TestNormalize_PhpDocComment5 케이스 Green
-
P1-2. Kotlin
multiline_comment노드 수집 — 완료walker.go:695의collectComments에multiline_comment조건 추가 (한 줄)- AST probe 결과: Kotlin
/** ... */는multiline_comment노드 타입으로 단일 노드 - Red 테스트:
TestWalkerBinder_Kotlin_Annotations_P1MeasurementGreen
-
P1-1. TypeScript
export_statement + decorator + class_declarationStartLine 보정 — 완료queries/typescript/tags.scm에(export_statement (class_declaration ...)) @definition.classwrapper 쿼리 추가- Python
decorated_definition처리 때 추가한nameIndexdedup(walker.go:343)이 그대로 동작해 더 작은 StartLine(첫 데코레이터 줄) 보존 - Red 테스트:
TestWalkerBinder_TypeScript_Decorators_P1MeasurementGreen - ⚠ 남은 범위: non-export
@Component class Foo케이스(데코레이터가 class_declaration과 sibling, export_statement 밖). 현재 fixture 없음 — 필요 시 P2에서 walker 부모 탐색으로 처리
-
P2-1. Go
//go:generate같은 디렉티브가@intent주석에 섞이는 문제 — 완료- 선택: 옵션 2 — normalizer Go 분기에서
//go:<word>시작 줄 제거 - 구현:
internal/domain/annotation/normalizer.go에isGoDirective(line)헬퍼 추가.Normalize()라인 루프에서language == "go" && isGoDirective(line)이면continue로 건너뜀 - 디렉티브 판별:
//go:뒤에 알파벳 또는_1자 이상.// go:(공백 포함)는 일반 주석으로 보존 - Red 테스트:
TestNormalize_GoDirectiveSkip4 케이스 (unit) —go:generate,go:noinline, 중간 낀 디렉티브, 공백 있는// go:보존TestWalkerBinder_Go_DirectivePollution2 케이스 (통합) —go:generate/type,go:noinline/func
- Green: 전 회귀 통과
- 선택: 옵션 2 — normalizer Go 분기에서
이 작업은 "바인딩 견고성"과 독립. 파서 레벨에서 JSDoc/YARD 호환성 확대.
-
P2-a. JSDoc
@returns→@return별칭 — 완료knownTags["returns"] = TagReturn매핑. Ordinal은 @return과 공유 카운터 사용- 테스트:
TestParse_ReturnsAlias,TestParse_ReturnAndReturnsAlias_SharedOrdinal - 커밋:
eade3f0
-
P2-b. JSDoc
@typedef,@throws처리 정책 결정 — 완료- 정책: 새 TagKind 2개 추가 —
TagThrows,TagTypedef@throws ExceptionType description→ Kind=TagThrows, Name=ExceptionType, Value=description (@param과 동일 규칙)@exception=@throwsJavadoc 공식 alias@typedef {Type} Name desc→ Kind=TagTypedef, Value=원문 보존 (구조 복잡, 검색/표시용)
- 변경:
internal/domain/graph/annotation.go상수 추가 + parser knownTags + parseTagLine param-like 분기 확장 - 테스트:
TestParse_Throws_WithType,TestParse_Throws_TypeOnly,TestParse_Exception_AliasOfThrows,TestParse_Typedef - 커밋:
0591560
- 정책: 새 TagKind 2개 추가 —
-
P2-c. YARD
@param [Type] name타입 부분 파싱 — 완료- 구조 변경:
graph.DocTag에Type string필드 추가 (별도 커밋0ff2511) - 행위 변경:
extractTypePrefix()헬퍼 —[...]/{...}balance 기반 분리, 중첩 지원parseTagLine에서 param/return/throws는 value 앞의 type prefix를 먼저 추출 →DocTag.Type- 기존
@param name desc(type 없음) 하위호환 유지
- 지원 문법: YARD
[String],[Array<String>]; JSDoc{string},{string|number} - 테스트: 7개 (param 5 + return 2)
- 커밋:
622eef5
- 구조 변경:
-
P3-1. Kotlin
testdata/eval/kotlin/Sample.kt의 golden.json 배포 — 완료- 실측:
Sample.kt.golden.json이미 존재 (커밋ccc95f8부터) ccg eval --suite parser실행 결과 kotlin 100%/100%/1.0 (Node P/R/F1, Edge P/R/F1) Green- 조치: task.md 항목을 사실에 맞게 정리
- 실측:
-
P3-2. Cross-language 통합 테스트 — 완료
- 파일:
internal/parse/treesitter/binding_gap_cross_language_test.go - 테스트:
TestCrossLanguage_IntentBinding_MinimalContract— 12개 지원 언어 table-driven - 각 언어 최소 케이스: 심볼 위 한 줄
@intent주석 + 대상 심볼 + binder 결과에@intent태그 존재 검증 - 리뷰 후속 정비(커밋
5af7446):- 초기 1 SKIP(lua)를
expectBound=falseRed 계약으로 승격 → 회귀 탐지력 확보 - Rust 빈 줄 회피 Green 케이스 유지하면서 "빈 줄 없는 Rust" Red 서브케이스 추가 → walker trailing-newline quirk 명시 고정
expectedKind graph.NodeKind필드로 Node.Kind 동시 검증, 파싱 실패 vs 바인딩 실패 진단 분리- PHP
<?php필요성,tc.spec.Name단일 원천화 등 minor 정리
- 초기 1 SKIP(lua)를
- 최종 결과: 13 PASS (11 Green + 2 Red 계약)
- 후속 과제: Rust
line_comment/ Luacomment·function_statementrange 보정 — walker 레벨 수정 필요하나 기존 gap=2 계약 회귀를 피하려면 단일 주석에 한정된 스코프로 해야 함
- 파일:
-
P4-1. indexer.go
IsDocstring/OwnerStartLine필드 전파 누락 — 완료- 리뷰에서 HIGH로 지적: P0-2 docstring 바인딩이 프로덕션 경로(
internal/service/indexer.go)에서 미동작 - Fix:
internal/service/indexer.go:127-131의 인라인 변환 루프에 두 필드 추가 → 이후 구조적으로toBinderComments(...)헬퍼로 추출 - 재발 방지:
internal/service/indexer_test.go4 케이스 (basic, docstring, non-docstring, empty) - 커밋:
97dfb3b(fix) /efda056(refactor) /72bac2c(test)
- 리뷰에서 HIGH로 지적: P0-2 docstring 바인딩이 프로덕션 경로(
-
P4-2.
nameIndexdedup이 동명 메서드를 오병합 — 완료- 리뷰에서 HIGH로 지적:
walker.go:343의 nameIndex가name단독 키라서 같은 이름의 다른 클래스 메서드가 서로 소실 - 실측: Python
Alpha.save/Beta.save와 TSAlpha.render/Beta.render모두 하나만 수집됨 (Red 확인) - Fix:
rangesOverlap()헬퍼 추가 후 nameIndex dedup 분기에 overlap 가드 — 범위가 겹치지 않으면 별개 심볼로 새 노드 추가 - Red→Green:
TestWalker_NameIndexDedup_{Python,TypeScript}_DupMethods - 커밋:
7b0a7b8(test) /9e21c05(fix)
- 리뷰에서 HIGH로 지적:
- 옵션 1:
tags.scm에서decorated_definition매칭 (tree-sitter 쿼리 변경, 가장 깨끗) - 옵션 2: walker에서 부모 노드 탐색 후 StartLine 조정 (쿼리 변경 없음, 로직 추가)
- 추천: 옵션 1 — golden.json 재생성 부담은 있으나 Java/C 동작과 일관됨
- 옵션 1: walker 주석 수집기에 Python 전용 분기 추가 (
function_definition첫 자식이 docstring인 경우) - 옵션 2: binder에 "심볼 아래 방향" 바인딩 규칙 추가 (Python 한정)
- 추천: 옵션 1 — 주석 수집 단계에서 docstring을
CommentBlock으로 승격시키면 binder는 기존 로직 그대로 (gap<1 조건 때문에 아래 방향 바인딩은 안 됨 → binder 규칙도 살짝 수정 필요할 수 있음) - ⚠ binder
gap < 1조건은 Python 전용 규칙 추가 시 재검토 필요
///,//!,/** */,/*! */전부 지원할지,///·//만 우선 지원할지- 추천: 이번 P0에서는
///·//만 처리, inner doc(//!)은 P1
- TDD: 각 항목 Red → Green → Refactor 엄수 (Red는 이미 작성 완료)
- Tidy First: 구조적 변경(쿼리 분리, normalizer 리팩터)과 행위 변경은 별도 커밋
- 테스트:
CGO_ENABLED=1 go test -tags "fts5" ./... -count=1 - 한 번에 한 항목만 In Progress
- Python docstring 이슈(P0-2)는 설계 결정 B 합의 후 착수
- Red: prefix docstring(
r/f/b/rb/fr/u) fixture 확장 및 바인딩/normalizer 테스트 추가 - Green: Python normalizer가 string prefix를 제거한 뒤 triple-quote delimiter를 제거하도록 수정
- Verify:
CGO_ENABLED=1 go test -tags "fts5" ./... -count=1전체 통과 - Verify:
lsp_diagnosticsclean on changed files
-
internal/service/indexer_test.go에 Red 테스트 추가TestBuild_IncrementalRebuild_RemovesStaleNodesBeforeUpsert- 첫 빌드에서
Keep,Remove생성 후 같은 파일을 축소 재빌드했을 때Remove가 남는 실패 재현
-
internal/service/indexer.goincremental rebuild 경로 수정- 파일 처리 트랜잭션 시작 직후
DeleteNodesByFile(ctx, relPath)호출 - 그 다음
UpsertNodes수행하여 stale node/annotation/edge 흔적 제거 후 최신 노드 저장
- 파일 처리 트랜잭션 시작 직후
-
gormstore구현 확인internal/store/gormstore/gormstore.go에DeleteNodesByFile구현이 이미 존재함을 확인- 노드 + 연결 엣지 + annotation/doc_tags cascade 삭제까지 수행 중
-
최종 검증
lsp_diagnosticscleanCGO_ENABLED=1 go test -tags "fts5" ./... -count=1전체 통과
-
Red:
Build()의 include_paths 부분 재빌드가 이전 그래프 상태를 replace하지 못하는 회귀 테스트 추가internal/service/indexer_test.gointernal/cli/build_test.gointernal/mcp/e2e_test.go
-
Green:
GraphService.Build()와 MCPwalkAndParse()를 namespace 단위 replace semantics로 수정- 빌드 시작 시
DeleteGraph(ctx)로 현재 namespace 그래프 상태 초기화 - include_paths/full rebuild 모두 "선택된 입력만 최종 상태에 남는다" 계약으로 통일
- 빌드 시작 시
-
Red: unreadable/read 실패 파일이 이전 stale state를 남기는 회귀 테스트 추가
internal/service/indexer_test.gobroken symlink 시나리오
-
Green: read/parse 실패 포함 빌드 전체를 replace semantics로 처리
- 빌드 시작 시 reset하므로 unreadable/parse failure 파일의 이전 상태가 남지 않음
-
Red: Python docstring prefix 범위를 실제 문서화 의미에 맞게 제한하는 positive/negative 테스트 추가
- 허용: plain,
r,u - 비허용:
f,b,rb,fr
- 허용: plain,
-
Green: Python docstring 수집/정규화에서 허용 prefix만 docstring으로 처리
internal/parse/treesitter/walker.gointernal/domain/annotation/normalizer.go- fixture/테스트 기대값 조정
-
Verify
lsp_diagnosticsclean on changed filesCGO_ENABLED=1 go test -tags "fts5" ./... -count=1- CLI/MCP e2e 관련 패키지 테스트 포함 전체 green
-
Red:
DeleteGraph()가 unresolved/file-owned edge를 남기는 회귀 테스트 추가internal/store/gormstore/gormstore_test.go
-
Green:
DeleteGraph()가 node 삭제 전에 namespace file_path 목록을 확보하고 file-owned edge를 먼저 삭제하도록 수정- unresolved edge / zero-node-id edge까지 정리
-
Red:
Build()의 missing root / walk 실패가 기존 graph를 지우는 회귀 테스트 추가internal/service/indexer_test.go
-
Green:
Build()는 preflight walk 성공 이후에만 graph reset 수행- invalid root는 reset 전에 즉시 실패
- per-file read/parse 실패는 reset 이후 skip 유지 (기존 정책 유지)
-
Red:
parse_projectmissing root가 graph를 지우거나 성공처럼 보이는 회귀 테스트 추가internal/mcp/handlers_test.go
-
Green:
walkAndParse()도 root stat + preflight walk 성공 이후에만 reset 수행- missing root는 error 반환, 기존 graph 보존
-
Red: MCP incremental +
include_paths가 replace semantics를 만족하지 않는 통합 테스트 추가internal/mcp/handlers_test.go
-
Green: MCP incremental 경로를
SyncWithExisting()기반으로 전환include_paths가 있으면 기존 file set과 비교해 excluded path도 삭제되도록 수정- MCP incremental 인터페이스 확장 (
SyncWithExisting)
-
Verify
lsp_diagnosticscleanCGO_ENABLED=1 go test -tags "fts5" ./... -count=1CGO_ENABLED=1 go test -tags "fts5" ./internal/cli ./internal/mcp -count=1
-
Verify:
CGO_ENABLED=1 go test -tags "fts5" ./... -count=1전체 통과- 모든 패키지 green (45개 패키지, 0 실패)
- 경고: lua parser null character (tree-sitter 라이브러리 수준, 무시 가능)
-
Verify:
lsp_diagnosticsclean on changed files- internal/ 디렉토리 50개 파일 스캔 → 0 에러
모든 재코드리뷰 후속 수정 완료 및 검증 통과
-
P0 (주석-심볼 바인딩 견고성): 완료
- Python decorated_definition + docstring 수집/바인딩
- Rust normalizer
///처리 - Kotlin multiline_comment 수집
- TypeScript export_statement wrapper
- PHP normalizer 추가
- Go directive 오염 제거
-
P1 (언어별 실측): 완료
- 12개 언어 cross-language 계약 테스트 (11 PASS + 1 Red 계약)
-
P2 (JSDoc/YARD 호환): 완료
- @returns alias
- @throws / @typedef 지원
- YARD/JSDoc 타입 prefix 파싱
-
P3 (테스트 정비): 완료
- Kotlin golden.json 배포 확인
- Cross-language @intent 바인딩 계약 테스트
-
P4 (코드리뷰 후속): 완료
- indexer 필드 전파 누락 수정
- nameIndex dedup 동명 메서드 오병합 수정
-
코드리뷰 후속 3건 (2026-04-21): 완료
- DeleteGraph() unresolved edge 정리
- Build() preflight 실패 경계 재정의
- MCP incremental + include_paths replace semantics 통일
- 기존 계획 문서 정리 (task.md / implementation.md / workthrough.md)
- 새로운 기능 개발 또는 사용자 요청 대기