지식 그래프로 통째로 그려, 자연어로 캐묻고 고치는 RAGcgr)는 여러 언어가 섞인 큰 저장소를 Tree-sitter로 파싱해, "어떤 함수가 어떤 함수를 부르고, 어떤 클래스가 무엇을 상속하는가"라는 구조를 Memgraph 그래프 DB에 통째로 저장한다. 그 다음 사용자는 자연어로 "User 클래스를 상속하는 클래스 전부 찾아줘" 같은 질문을 던지고, AI 에이전트가 그걸 그래프 질의어(Cypher)로 번역해 정확히 답한다. 일반 RAG가 임베딩 유사도로 "비슷한 코드 조각"을 찾는다면, 이건 CALLS·INHERITS 같은 명시적 구조 엣지를 가진 영속 그래프라 "이걸 호출하는 곳 전부", "상속 사슬", "데드코드" 같은 다중 홉 질문을 정확히 답한다. 조회에 그치지 않고 AST 기반 외과적 편집(변경 전 diff 미리보기)까지 한다.
code-graph-rag는 두 개의 큰 부품으로 이뤄진다. 하나는 다국어 파서 — Tree-sitter로 소스코드를 훑어 함수·클래스·메서드·모듈과 그 사이의 관계를 뽑아 Memgraph 그래프에 적재한다. 다른 하나는 RAG 시스템 — 사용자의 자연어 질문을 받아 AI가 Cypher(그래프 질의어)로 번역하고, 그래프에서 정확한 답을 길어 올려 설명하거나, 코드를 직접 수정한다.
핵심은 "검색을 유사도가 아니라 구조로 한다"는 것이다. 여러 언어가 섞인 모노레포도 하나의 통합 스키마로 다루기 때문에, Python 서비스가 부르는 Go 핸들러, TypeScript 프런트가 호출하는 Rust 함수를 언어 경계를 넘어 이어서 질의할 수 있다.
보통의 코드 검색 AI는 질문과 의미가 비슷한 코드 조각을 임베딩으로 골라 온다. "로그인 관련 코드"를 물으면 로그인처럼 보이는 조각들을 준다. 하지만 "이 함수를 실제로 호출하는 곳이 몇 군데인가", "이 클래스의 상속 사슬 끝은 어디인가"는 유사도로는 못 푼다.
code-graph-rag는 코드를 노드(함수·클래스)와 화살표(호출·상속·정의)로 된 지도로 미리 그려 둔다. 그래서 "A를 호출하는 모든 것", "아무도 안 부르는 죽은 함수"처럼 관계를 여러 번 타고 가는 질문에 정확한 답을 준다. 유사도 검색(시맨틱)은 "의도"를 물을 때 보조로 함께 쓴다.
실제 한 번의 질의는 이렇게 흐른다. 사용자는 Cypher를 몰라도 되고, 오케스트레이터 에이전트가 자연어를 그대로 Cypher 전용 에이전트에 넘긴다.
질문 : "User 클래스를 상속하는 클래스를 전부 보여줘"
│ (ORCHESTRATOR 에이전트가 자연어 그대로 CYPHER 에이전트에 위임)
▼
Cypher: MATCH (c:Class)-[:INHERITS]->(p:Class)
WHERE p.name ENDS WITH '.User'
RETURN c.qualified_name, c.path
│ (읽기전용 검증 통과 → Memgraph 실행)
▼
답변 : AdminUser (auth/models.py:14)
GuestUser (auth/models.py:41)
ServiceUser (svc/accounts.py:88)
설계 목표는 "코드베이스를 환각 없이(grounded), 구조에 근거해 다루기"다. LLM이 코드 내용을 지어내지 않고, 실제 그래프에 있는 노드·엣지만 근거로 삼기 때문에 큰 저장소에서도 답이 사실에 붙어 있다.
비슷한 "코드 챗봇"은 많지만, code-graph-rag는 네 가지가 겹쳐서 트렌딩에 올랐다. 정확도를 구조로 끌어올렸고, API 키 없이 완전 오프라인으로 돌릴 수 있으며, 편집까지 하고, MCP 서버로 다른 AI 도구에 그대로 꽂힌다.
순수 임베딩 RAG는 "비슷한 조각"만 찾아 큰 코드베이스에서 자주 헛다리를 짚는다. code-graph-rag는 CALLS·INHERITS·DEFINES 같은 명시적 엣지를 가진 영속 그래프라, "몇 개인가", "무엇이 이것에 의존하나", "여기서 도달 가능한가" 같은 다중 홉·집계 질문에 정확하다. 게다가 그래프 검색과 UniXcoder 시맨틱 검색을 함께 쓰는 하이브리드라 구조와 의도를 모두 잡는다.
아무 설정도 하지 않으면 LLM은 로컬 Ollama(llama3.2), 임베딩은 로컬 UniXcoder, 그래프 DB는 로컬 Memgraph를 쓴다. 즉 코드를 외부로 한 바이트도 보내지 않고 통째로 오프라인에서 색인·질의할 수 있어, 사내 코드·규제 산업에서 그대로 쓸 수 있다(에어갭·온프레미스 상용 옵션도 존재). 원하면 OpenAI·Anthropic·Google·Azure로 스위치만 바꾸면 된다.
단순 질의를 넘어 AST 기반 외과적 편집(변경 전 diff 미리보기), ast-grep 패턴 기반 구조적 검색·치환, 언어별 최적화(승인 워크플로), 진입점에서 도달 불가능한 데드코드 탐지까지 한다. "찾아줘"에서 "바꿔줘"로 넘어간 게 차별점이다.
cgr mcp-server 한 줄이면 Model Context Protocol 서버가 떠서, Claude Code·Cursor·Gemini CLI 같은 클라이언트가 이 지식 그래프를 도구로 직접 질의·편집한다. 다른 코딩 에이전트의 "장기 기억"이자 "코드 지도" 역할을 하도록 설계됐다.
| 도구 | 검색 방식 | code-graph-rag와의 차이 |
|---|---|---|
| 순수 벡터 RAG | 임베딩 유사도(비슷한 조각) | "몇 개·누가 호출·상속 사슬" 같은 구조 질문에 약함 |
| Cursor / Copilot | 컨텍스트 윈도우 + 임베딩 | 실시간 보조 중심. 모노레포 전체를 영속 그래프로 색인하지 않음 |
| grep / IDE 검색 | 텍스트·심볼 매칭 | 언어 경계·의미(자연어 의도)를 넘지 못함 |
| code-graph-rag | 지식 그래프(구조) + 시맨틱(의도) | 다중 홉 정확도 + 언어 통합 + AST 편집 + 로컬 구동 |
Python 100%(requires-python >= 3.12) 프로젝트다. 스택은 "코드를 언어 무관하게 뜯는 파서 층", "구조를 담는 그래프/벡터 저장소 층", "질의·편집을 지휘하는 에이전트 층" 세 겹으로 나뉜다. 주목할 점은 기본 구성이 클라우드가 아니라 전부 로컬이라는 것 — Ollama·UniXcoder·Memgraph·Qdrant가 모두 사용자 머신에서 돈다.
| 패키지 | 역할 |
|---|---|
| pydantic-ai ≥2.0 | LLM 에이전트 프레임워크. ORCHESTRATOR·CYPHER 두 에이전트와 도구(tool) 정의의 뼈대. |
| pymgclient ≥1.5.1 | Memgraph 접속 드라이버(Bolt 프로토콜). 노드·엣지 적재와 Cypher 실행. |
| tree-sitter 0.25.2 | 언어 무관 파서 런타임. 소스를 AST(구문 트리)로 만든다. 기본은 Python 문법만 포함. |
| tiktoken ≥0.12 | 토큰 카운트. 질의 결과를 토큰 상한(16000)으로 잘라 컨텍스트 폭주를 막는다. |
| diff-match-patch | 코드 편집 시 diff 생성·패치 적용. 외과적 수정의 미리보기 근거. |
| watchdog ≥6.0 | 파일 변경 실시간 감시. 저장하면 그래프를 증분 갱신(realtime_updater.py). |
| typer · rich · prompt-toolkit | CLI 프레임워크 + 컬러 출력 + 대화형 입력. cgr 명령의 표면. |
| mcp ≥1.28 | Model Context Protocol 서버. Claude Code 등 외부 에이전트가 그래프를 도구로 쓰게 노출. |
| defusedxml | XML 폭탄 방어. 신뢰 못 할 입력을 안전하게 파싱. |
이 프로젝트는 LLM을 역할별로 둘로 나눈다. 도구를 쥐고 대화를 지휘하는 ORCHESTRATOR와, 자연어를 Cypher로 번역만 하는 전용 CYPHER다. 번역은 값싼 빠른 모델에, 지휘는 강한 모델에 배정해 비용을 분리한다.
| 항목 | 내용 |
|---|---|
| 지원 provider | ollama(기본) · anthropic · openai · google(AI Studio/Vertex) · azure · litellm_proxy · minimax |
| 기본값(무설정) | ORCHESTRATOR·CYPHER 모두 로컬 Ollama llama3.2 (API 키 불필요) |
| 지정 방법 | 환경변수 ORCHESTRATOR_PROVIDER/MODEL/API_KEY, CYPHER_PROVIDER/MODEL/API_KEY 또는 CLI 플래그 |
| 혼합 예시 | --orchestrator google:gemini-flash --cypher google:gemini-flash-lite |
| 구성요소 | 기술 |
|---|---|
| 파서 | Tree-sitter(13개 문법, extras treesitter-full). C++는 선택적 libclang, C#는 선택적 Roslyn 하이브리드로 정밀화. Ruby는 tree-sitter 대신 ast-grep 티어. |
| 그래프 DB | Memgraph(MAGE 그래프 알고리즘 포함 이미지). Bolt 7687, Lab UI 3000. 노드는 라벨별 배치 UNWIND + MERGE로 적재. |
| 임베딩 | 기본 UniXcoder(microsoft/unixcoder-base, 768차원, torch/transformers 로컬). 대안으로 OpenAI 호환 엔드포인트(그러면 torch 불필요). |
| 벡터 스토어 | Qdrant(기본, 로컬 파일 모드 가능) 또는 Milvus Lite(CGR_VECTOR_STORE_BACKEND로 전환). |
| 구조 검색·발견 | ast-grep로 AST 패턴 검색·치환 + Pattern/CodeSmell/SecurityIssue 발견 노드 생성. |
기본 설치는 Python 파싱만 켜져 있어 가볍다. 필요에 따라 조립하는 구조다 — treesitter-full(13개 언어 문법), semantic(Qdrant+torch+transformers 시맨틱 검색), milvus(대안 벡터DB), ast-grep(구조 검색+Ruby 지원). 즉 "Python만 쓰는 소형"부터 "다국어 + 시맨틱 + 편집"까지 사용자가 무게를 고른다.
동작은 크게 두 시기로 나뉜다. 색인 시기(--update-graph)엔 소스를 파싱해 그래프를 짓고, 질의 시기(대화형)엔 자연어를 Cypher로 번역해 답한다. 아래가 전체 데이터 흐름이다.
graph_updater.py)그래프는 한 번에 짓지 않고 세 번 훑어 짓는다. 왜 나눌까? 파일 간 호출을 풀려면 먼저 "어떤 함수가 어디 정의됐는지"가 다 있어야 하기 때문이다. 로그 마커도 PASS_1_STRUCTURE / PASS_2_FILES / PASS_3_CALLS로 남는다.
| 패스 | 하는 일 | 만드는 것 |
|---|---|---|
| Pass 1 · 구조 | 폴더·파일·모듈·패키지 뼈대 스캔 | Project/Folder/File/Module 노드 + CONTAINS_* 엣지 |
| Pass 2 · 파일 | 파일별 Tree-sitter 파싱, 정의·import 추출 | Function/Class/Method/Interface/Enum 노드 + DEFINES/IMPORTS |
| Pass 3 · 호출 | 파일 경계를 넘는 호출·참조 해소 | CALLS/REFERENCES/INSTANTIATES 엣지 |
이후 오버라이드 처리 → 엔드포인트 방출(라우터 프리픽스 해소) → ast-grep 기반 패턴/스멜/보안 발견 → 고아 노드 프루닝 → 시맨틱 임베딩 생성이 이어진다. 두 번째 실행부터는 파일 해시 캐시로 변경분만 재파싱하고, 안 바뀐 파일의 정의는 그래프에서 되살린다(증분 갱신).
cypher_queries.py)수만 개 노드를 안전하게 넣기 위해, 문자열을 이어 붙이지 않고 파라미터 바인딩 + MERGE(있으면 갱신, 없으면 생성)로 적재한다. 라벨별 유니크 제약으로 중복을 막는다.
# cypher_queries.py — 라벨별 배치 노드 적재 쿼리 생성
def build_merge_node_query(label: str, id_key: str) -> str:
return f"MERGE (n:{label} {{{id_key}: row.id}})\nSET n += row.props"
# 실제로는 UNWIND $rows AS row 로 감싸 수천 건을 한 번에 MERGE (배치 1000)
services/, prompts.py)사용자와 대화하는 오케스트레이터는 절대 Cypher를 직접 쓰지 않는다. 자연어 질문을 CypherGenerator(전용 에이전트)에 넘겨 번역만 시키고, 돌아온 쿼리는 실행 전에 세 겹으로 검사한다. 이 검증이 "AI가 실수로 DB를 지우거나 무한 경로로 메모리를 터뜨리는" 사고를 막는다.
async def generate(self, natural_language_query: str) -> str:
result = await self.agent.run(natural_language_query)
query = _clean_cypher_response(result.output)
_validate_cypher_read_only(query) # DELETE/SET/CREATE 등 쓰기 키워드 차단
_validate_no_unbounded_paths(query) # [:CALLS*] 같은 무한 가변경로 → OOM 방지
_validate_call_procedures(query) # 허용된 MAGE 프로시저만 CALL 허용
return query
프롬프트(prompts.py)도 정교하다. Ollama 같은 로컬 모델에는 더 엄격한 규칙(UNION 금지 등)을 주고, 가변 길이 경로엔 반드시 상한을 붙이게([:CALLS*1..6]) 지시하며, 순환 그래프 분석엔 Memgraph의 MAGE 프로시저(pagerank.get, path.expand 등)를 쓰게 한다.
모든 언어를 하나의 스키마로 담는 게 이 프로젝트의 정수다. 코드 노드는 qualified_name(예 Project.folder.ClassName)·path·start_line/end_line 등을 갖는다.
| 분류 | 대표 요소 |
|---|---|
| 구조 노드 | Project, Package, Folder, File, Module, Class, Function, Method, Interface, Enum, Type |
| 외부·발견 노드 | ExternalPackage, ExternalModule, Resource(파일/DB/네트워크 I/O), Pattern, CodeSmell, SecurityIssue |
| 구조 관계 | CONTAINS_*, DEFINES, IMPORTS, EXPORTS, INHERITS, IMPLEMENTS, OVERRIDES |
| 행위 관계 | CALLS, REFERENCES, INSTANTIATES, READS_FROM, WRITES_TO, FLOWS_TO(데이터흐름 taint), EXPOSES |
Cypher에서 -[:CALLS*]->는 "호출을 몇 번이든 타고 가라"는 뜻이라, 순환이 있는 코드 그래프에선 경로가 폭증한다. 그래서 code-graph-rag는 LLM이 만든 쿼리에 상한이 없으면 실행 전에 거부하고, 프롬프트에서도 항상 *1..6처럼 깊이를 못 박게 한다. 데드코드 탐지처럼 진짜 전방위 순회가 필요할 땐 Cypher 대신 클라이언트 쪽 BFS로 돌려 DB 타임아웃을 피한다.
구조가 말해 주는 설계 의도는 분명하다. "그래프를 짓는 쪽"(graph_updater·parsers·services)과 "그래프에 묻는 쪽"(tools·prompts·mcp)이 깔끔히 갈려 있다. 새 언어를 추가할 땐 parsers/에 Tree-sitter 문법을 얹거나(cgr language add-grammar) ast-grep YAML 패턴 하나만 추가하면 되고, 새 도구를 붙일 땐 tools/에 함수를 하나 등록하면 오케스트레이터가 그걸 쓸 수 있게 된다. 확장점이 폴더 경계에 그대로 드러난다.
code-graph-rag는 "요즘 AI 코딩 도구가 안에서 뭘 하는가"를 통째로 보여주는 교재다. 파싱부터 그래프, LLM 에이전트, 안전 검증까지 한 저장소에 다 들어 있다. 계층별로 배울 것을 정리하면 이렇다.
13개 언어를 각각 다른 파서로 다루지 않고, Tree-sitter라는 공통 런타임 위에서 언어별 문법만 갈아 끼운다. "소스 텍스트 → 구문 트리(AST) → 노드 추출"이라는 컴파일러 프런트엔드의 첫 단계를 실물로 배운다. C++의 libclang, C#의 Roslyn 하이브리드를 보면 왜 순수 Tree-sitter만으론 부족한지(매크로·오버로드·partial class)도 알게 된다.
"무엇을 노드로, 무엇을 엣지로 둘 것인가"라는 스키마 설계가 이 프로젝트의 지적 핵심이다. 함수·클래스는 노드, 호출·상속·정의는 엣지 — 이 20종 노드/24종 관계 스키마를 뜯어 보면, 도메인을 그래프로 옮기는 감각을 익힌다. 이는 코드뿐 아니라 어떤 복잡한 도메인에도 쓰이는 기술이다.
SQL이 표(table)의 언어라면 Cypher는 그래프의 언어다. MATCH (a)-[:CALLS]->(b)처럼 "패턴을 그림으로 그려" 질의한다. 가변 길이 경로(*1..6), 집계, MAGE 그래프 알고리즘까지 — 이 저장소의 프롬프트에 담긴 Cypher 규칙만 읽어도 실전 패턴이 손에 잡힌다.
"도구를 든 에이전트"가 어떻게 구성되는지 실전 규모로 볼 수 있다. 도구 정의, 재시도 정책, 출력 타입 제약, 그리고 역할을 나눈 2-에이전트(지휘용 강한 모델 + 번역용 값싼 모델) 설계는 비용·정확도를 함께 잡는 실용 패턴이다.
구조 질문은 그래프로, 의도 질문("인증 관련 코드")은 벡터 시맨틱으로 — 두 검색을 언제 어떻게 섞는지 오케스트레이터 프롬프트에 그대로 하드코딩돼 있다. "RAG = 벡터 검색"이라는 통념을 넘어서는 좋은 반례다.
LLM이 만든 Cypher를 실행 전에 검증하고, shell 도구는 allowlist(ls,rg,cat,git,pytest…)로 제한하며, 위험 명령엔 확인을 받는다. "AI가 만든 걸 그대로 실행하지 않는다"는 감각 — 에이전트를 프로덕션에 쓸 때 반드시 필요한 방어를 정확히 짚는다.
거창한 하드웨어는 필요 없다. 진짜 전제조건은 Docker(그래프·벡터 DB를 컨테이너로 띄우기)와 Python 3.12+다. GPU는 로컬 임베딩을 빠르게 하려면 좋지만 없어도 CPU로 돌고, 임베딩을 외부 API로 넘기면 아예 필요 없다.
| 항목 | 요구 / 참고 |
|---|---|
| Python | 3.12 이상 (3.12/3.13/3.14 분류자) |
| Docker & Compose | 필수. cgr daemon up이 Memgraph + Qdrant 스택을 띄운다(compose 파일 패키지 내장) |
| 빌드 도구 | cmake(pymgclient 빌드), ripgrep(rg, shell 검색 도구). macOS brew install cmake ripgrep |
| 패키지 매니저 | uv 권장 (또는 pipx/pip). uv tool install "code-graph-rag[treesitter-full,semantic]" |
| GPU | 선택. UniXcoder 임베딩을 CGR_EMBEDDING_DEVICE=cuda/mps/cpu로. 외부 임베딩 쓰면 torch 불필요 |
| LLM | 완전 로컬이면 Ollama 설치(llama3.2). 클라우드면 해당 provider API 키 |
CGR_STACK_BIND_HOST=0.0.0.0을 명시해야 하고, MCP를 비-loopback으로 열면 MCP_HTTP_AUTH_TOKEN이 강제된다. "왜 다른 기기에서 접속이 안 되지?"의 답은 대개 이 기본값이다.uv tool install "code-graph-rag[treesitter-full,semantic]" # 설치
cp .env.example .env # 모델·DB 설정 (무설정이면 Ollama 로컬)
cgr daemon up # Memgraph + Qdrant 스택 기동 (Docker)
cgr doctor # 의존성·서비스 점검
cgr start --repo-path /my/repo --update-graph # 그래프에 적재
cgr start --repo-path /my/repo # 대화형 질의 시작
여러 저장소를 같은 그래프에 적재하면(레포마다 --update-graph 반복) 그래프를 공유해 크로스 레포 질의가 된다. 프로젝트별 지시문은 .cgr.md, 무시 패턴은 .cgrignore(+ .gitignore 병합)로 준다.
가진 파이썬 프로젝트 하나를 cgr start --repo-path . --update-graph로 적재한 뒤, "가장 많이 호출되는 함수", "이 클래스의 메서드 목록", "아무도 안 부르는 함수"를 자연어로 물어본다. Memgraph Lab(localhost:3000)에서 그래프를 눈으로도 본다. 목표: 구조 질의가 유사도 검색과 어떻게 다른지 체감.
AI를 거치지 않고 Lab 콘솔에서 직접 MATCH (c:Class)-[:INHERITS]->(p) RETURN c,p, MATCH (f:Function)<-[:CALLS]-(x) RETURN f.name, count(x) ORDER BY count(x) DESC를 실행해 본다. 배우는 것: 그래프 패턴 매칭·집계의 문법, LLM이 무엇으로 번역하는지의 실체.
tree-sitter-python으로 파일 하나를 파싱해 함수·클래스 정의를 찾아 출력하는 40줄 스크립트를 짠다. 여유가 되면 그 결과를 (:Function {name}) 노드로 Memgraph에 MERGE해 본다. 배우는 것: AST 순회 + "코드→노드" 변환의 최소 형태(= graph_updater의 축소판).
pydantic-ai(또는 임의 LLM SDK)로 "자연어를 받아 Cypher만 반환하는" 에이전트를 만들고, 반환된 쿼리에 DELETE/SET/CREATE가 있으면 거부하는 검증 함수를 붙인다. 무한 경로(* 상한 없음)도 막아 본다. 배우는 것: 역할 분리 에이전트 + AI 출력 가드레일.
claude mcp add --transport stdio code-graph-rag -- code-graph-rag mcp-server로 등록해, Claude Code가 내 코드 그래프를 도구로 질의·편집하게 만든다. 그 다음 index_repository·query_code_graph 같은 MCP 도구가 실제로 어떻게 호출되는지 로그로 관찰한다. 배우는 것: MCP로 도구를 외부 에이전트에 노출하는 실전 통합.
| 주차 | 주제 | 무엇을 · 왜 |
|---|---|---|
| 1주 | AST와 Tree-sitter | 구문 트리의 개념, tree-sitter-python으로 함수·클래스 추출. 파싱이 곧 그래프의 원료임을 이해. |
| 2주 | 그래프 데이터 모델링 | 노드/엣지 스키마 설계, 속성 그래프 개념. "코드를 그래프로 옮기면 무엇이 쉬워지나"를 손으로. |
| 3주 | Cypher · Memgraph | 패턴 매칭·집계·가변 경로·MAGE 알고리즘(pagerank/path.expand). Lab으로 시각화. |
| 4주 | 임베딩 · 벡터 검색 | UniXcoder 같은 코드 임베딩, Qdrant/Milvus 유사도 검색. 그래프와 벡터를 언제 섞나. |
| 5주 | LLM 에이전트 (pydantic-ai) | 도구 정의·재시도·출력 타입, 역할 분리 다중 에이전트, 비용/정확도 트레이드오프. |
| 6주 | MCP · AI 안전 설계 | MCP로 도구 노출, 생성 쿼리 검증·allowlist·확인 프롬프트. 에이전트를 안전하게 배포하는 법. |
처음부터 LLM 에이전트·프롬프트를 파려 하면 정작 이 프로젝트의 토대(파싱→그래프)를 건너뛰게 된다. 1~3주에 "코드가 어떻게 노드·엣지가 되는가"를 손으로 재현하면(여기서 code-graph-rag의 절반이 이해된다), 4주에 시맨틱을 얹고, 5~6주에 에이전트·MCP·안전 검증으로 넘어가는 순서가 가장 단단하다. 그래프 없이는 RAG도 없다.
MATCH (a)-[:CALLS]->(b)처럼 찾고 싶은 패턴을 그림으로 그려 질의한다. 이 프로젝트에선 사용자가 아니라 CYPHER 에이전트가 자연어를 번역해 만든다.microsoft/unixcoder-base, 768차원)는 코드 특화 임베딩 모델로, 기본은 로컬에서 실행된다.cgr mcp-server로 이 지식 그래프를 다른 에이전트의 도구로 노출한다.