소스코드 입력을 AST 분석으로 넘기는 흐름
작성일: 2026년 6월 26일
목적
이 문서는 docs/roadmaps/2026-06-25-source-analysis-scoring-architecture.md의 앞단을 보강한다.
기준 로드맵에서는 이미 다음 흐름을 정리했다.
project source
-> AST chunks or direct chunks
-> official docs JSONL retrieval
-> on-demand LLM verification
-> score DB
-> classified filesystem여기서는 사용자가 프롬프트와 소스코드를 넣었을 때, 그 입력을 AST Parser에 어떤 형태로 넘길지 정리한다.
핵심 원칙
프롬프트와 소스코드는 같은 입력 묶음에 들어오지만 역할은 다르다.
| 입력 | 역할 |
|---|---|
| 사용자 프롬프트 | 분석 의도, 질문 유형, 기대 출력 방향 |
| 소스코드 | AST Parser가 실제로 파싱할 대상 |
| 파일 경로/프로젝트 정보 | 분석 단위 식별, 프로젝트 단위 분류를 위한 메타데이터 |
AST Parser는 소스코드를 파싱한다. 프롬프트는 AST 문법 분석 대상이 아니다.
다만 프롬프트는 이후 단계에서 어떤 조각을 우선 볼지, 어떤 Retriever를 사용할지, Qwen 3.6에 어떤 검증 요청을 만들지 결정하는 데 사용한다.
입력 패키지
사용자 입력은 먼저 하나의 분석 요청으로 묶는다.
analysis_request
prompt
source
source_kind
project_id
source_file
source_sha256
source_origin각 항목의 의미는 다음과 같다.
| 항목 | 설명 |
|---|---|
prompt |
사용자가 입력한 질문. 예: “이 코드가 뭔 뜻이야?”, “Godot 4로 변환 필요해?” |
source |
원본 소스코드 텍스트. 가능한 한 그대로 보존한다. |
source_kind |
gdscript, scene, resource, project_config, markdown, unknown 등 입력 종류 |
project_id |
GitHub repo 또는 로컬 프로젝트를 구분하기 위한 ID |
source_file |
프로젝트 안의 파일 경로. 단일 코드 붙여넣기라면 임시 경로를 둔다. |
source_sha256 |
원본 소스 텍스트의 해시. 붙여넣기와 파일 입력 모두 요청 생성 시점에 계산한다. |
source_origin |
github, local_directory, uploaded_file, pasted_snippet 등 출처 |
이 단계에서는 출력 JSONL 스키마나 score DB 컬럼을 확정하지 않는다. 목적은 AST Parser와 이후 Retriever/LLM 단계가 같은 입력을 같은 기준으로 추적할 수 있게 만드는 것이다.
단일 소스코드 붙여넣기
사용자가 웹 UI 또는 CLI에 프롬프트와 소스코드를 직접 넣는 경우다.
예:
prompt: 이거 Godot 4로 변환 필요해?
source:
extends KinematicBody2D
func _physics_process(delta):
move_and_slide()처리 흐름:
- 입력을
analysis_request로 묶는다. source_origin은pasted_snippet으로 둔다.source_file은 실제 파일 경로가 없으므로pasted://snippet-<id>.gd같은 임시 식별자를 둔다.source_sha256은 붙여넣은 원문 코드 기준으로 즉시 계산한다.source_kind는 확장자 힌트, 코드 내용, 사용자가 지정한 언어를 기준으로 추정한다.- AST Parser에는
source,source_kind,source_file,source_sha256을 넘긴다. - 프롬프트는 Parser에 섞지 않고
analysis_request.prompt로 보관한다.
프로젝트 디렉터리 입력
GitHub repo 또는 로컬 폴더를 분석하는 경우다.
처리 흐름:
- 프로젝트 루트를
project_id로 묶는다. - 파일시스템을 상대 경로 기준으로 훑는다.
- 파일마다 포함/제외 여부를 먼저 기록한다.
- 제외된 파일은 웹 UI나 로그에서 확인할 수 있게
excluded_files로 남긴다. - 포함된 파일은
# <relative/path>헤더가 붙은 평문 블록으로 펼친다. .gd파일은 AST Parser로 보내 함수/코드 조각을 원본 순서대로 만든다.- AST Parser 대상이 아닌 포함 파일은 파일 성격에 맞는 직접 조각화 규칙을 적용한다.
- 각 조각은 원본 경로, 원본 순서, 해시를 가진 채 Retriever 요청으로 넘어간다.
초기 대상 파일은 기준 로드맵과 동일하다.
| 파일 | AST/조각화 방향 |
|---|---|
.gd |
GDScript AST, 함수, 클래스, signal, 변수, API 호출 후보 |
.tscn |
scene node type, script reference, resource reference, exported property |
.tres |
resource type, script class, material/shader/resource 단서 |
project.godot |
Godot 버전 힌트, autoload, renderer, feature, input map |
| README/문서 | 기본적으로 소스코드 AST 분석에서는 제외 후보. 제외 여부와 이유를 UI/log에 남긴다. |
프로젝트 소스 평문 전개
프로젝트 단위 입력은 먼저 파일별 평문 묶음으로 펼친다. 이 평문은 LLM에 그대로 한 번에 넣기 위한 최종 프롬프트가 아니라, 파일 경계와 상대 경로를 잃지 않기 위한 중간 표현이다.
기본 형태:
# project.godot
; original project.godot content
# scripts/player.gd
extends KinematicBody2D
func _physics_process(delta):
move_and_slide(velocity)
# scenes/player.tscn
[gd_scene load_steps=2 format=3]
[node name="Player" type="CharacterBody2D"]규칙:
- 파일 헤더는
# <relative/path>형태로 둔다. - 상대 경로는 프로젝트 루트 기준이다.
- 파일 본문은 가능한 한 원문 그대로 둔다.
- 이 평문 묶음은 파일 경계 확인과 디버깅에 사용한다.
- 실제 처리에서는 이 전체 평문을 한 번에 LLM이나 AST Parser에 넘기지 않는다.
- 헤더로 나뉜 각 파일은 먼저 포함/제외 판정을 받는다.
- 제외된 파일은
excluded_files로 남긴다. - 포함된
.gd파일은analysis_request를 거쳐 AST Parser로 들어간다. - 포함된 non-
.gd텍스트/설정 파일은 직접 조각화기로 들어간다.
이렇게 하면 사용자가 제공한 “프로젝트 소스 전체”를 사람이 읽을 수 있는 형태로 남기면서도, 이후 단계에서는 제외 파일, AST 조각, direct chunk, Retriever payload를 파일/조각 단위로 추적할 수 있다.
클론 프로젝트 기준 확인 계획
AST Parser에서 Retriever로 넘기는 흐름은 추상 예시만으로는 확인하기 어렵다. 따라서 실제 Godot 프로젝트 하나를 클론하고, 그 프로젝트의 파일 트리, 평문 전개, AST chunk/direct chunk 분리, Retriever 요청 반복 흐름을 기준으로 확인한다.
기준 프로젝트는 다음처럼 둔다.
| 항목 | 값 |
|---|---|
| repository | godotengine/godot-demo-projects |
| project path | 2d/dodge_the_creeps |
| 선택 이유 | Godot 공식 데모이고, .gd, .tscn, project.godot, README, 에셋 참조가 함께 있어 프로젝트 입력 흐름을 작게 확인하기 좋다. |
| 저장 기준 | 클론한 소스는 레포에 커밋하지 않는다. 문서에는 트리와 처리 계획만 남긴다. |
클론 기준:
git clone --depth 1 --filter=blob:none --sparse https://github.com/godotengine/godot-demo-projects.git /tmp/idea_world_godot_demo_projects
cd /tmp/idea_world_godot_demo_projects
git sparse-checkout set 2d/dodge_the_creeps이 클론은 흐름 확인용이다. 레포 안에 클론한 프로젝트 소스나 에셋을 복사하지 않는다.
확인 대상은 클론한 프로젝트의 전체 파일 경로다. 먼저 전체 경로를 정렬된 순서로 펼치고, 각 파일을 LLM이 읽을 수 있는 조각으로 나눈다. 여기서 별도 중간 DB를 만들지 않는다. 핵심은 “레포 전체를 한 번에 읽히는 것”이 아니라 “경로 순서대로 조각을 만들고, 조각마다 여러 번 호출하는 것”이다.
2d/dodge_the_creeps
.gitignore
LICENSE
README.md
art/House In a Forest Loop.ogg
art/House In a Forest Loop.ogg.import
art/enemyFlyingAlt_1.png
art/enemyFlyingAlt_1.png.import
art/enemyFlyingAlt_2.png
art/enemyFlyingAlt_2.png.import
art/enemySwimming_1.png
art/enemySwimming_1.png.import
art/enemySwimming_2.png
art/enemySwimming_2.png.import
art/enemyWalking_1.png
art/enemyWalking_1.png.import
art/enemyWalking_2.png
art/enemyWalking_2.png.import
art/gameover.wav
art/gameover.wav.import
art/playerGrey_up1.png
art/playerGrey_up1.png.import
art/playerGrey_up2.png
art/playerGrey_up2.png.import
art/playerGrey_walk1.png
art/playerGrey_walk1.png.import
art/playerGrey_walk2.png
art/playerGrey_walk2.png.import
fonts/FONTLOG.txt
fonts/LICENSE.txt
fonts/Xolonium-Regular.ttf
fonts/Xolonium-Regular.ttf.import
hud.gd
hud.gd.uid
hud.tscn
icon.webp
icon.webp.import
main.gd
main.gd.uid
main.tscn
mob.gd
mob.gd.uid
mob.tscn
player.gd
player.gd.uid
player.tscn
project.godot
screenshots/.gdignore
screenshots/dodge.png파일별 조각화 기준:
| 파일 종류 | 조각화 방식 | 이후 호출 방식 |
|---|---|---|
.gd |
AST Parser가 함수, signal, 변수, class extends, API call 조각을 원본 순서대로 만든다. | 각 AST 조각마다 Retriever를 호출하고, 프롬프트 + AST 조각 + 검색 결과를 LLM에 보낸다. |
project.godot |
AST Parser에 넣지 않는다. 포함 대상이면 section/key-value, main scene, feature, input map 조각을 만든다. | 조각을 바로 Retriever로 보내거나, .gd AST 판단의 주변 컨텍스트로 붙인다. |
.tscn, .tres |
AST Parser에 넣지 않는다. 포함 대상이면 node block, ext_resource, sub_resource, connection 조각을 만든다. | 조각을 바로 Retriever로 보내거나, 연결된 .gd 파일의 AST 판단에 주변 컨텍스트로 붙인다. |
.md, .txt, LICENSE |
소스코드 AST 분석에서는 기본 제외 후보로 둔다. | 제외 여부와 이유를 UI/log에 남긴다. 별도 문서 분석 모드가 아니라면 Retriever로 보내지 않는다. |
.import, .uid, .gitignore, .gdignore |
AST Parser에 넣지 않는다. 포함 대상이면 줄 또는 key-value 조각을 만든다. | 조각을 바로 Retriever로 보내거나, 파일 경로/리소스 관계를 확인하는 주변 컨텍스트로만 사용한다. |
| 이미지, 오디오, 폰트 등 바이너리 | AST Parser에도 넣지 않고, LLM에 원본 bytes도 넣지 않는다. 경로와 제외 이유만 남긴다. | 해당 경로가 .tscn, .import 같은 텍스트 조각에 등장할 때 관계 정보로만 사용한다. |
즉 AST Parser에 들어가는 대상은 .gd 파일이다. 레포지토리 전체를 경로 순서대로 펼치되, 그 결과는 “한 번에 LLM으로 보내는 큰 본문”이 아니다. # <relative/path> 헤더는 파일이 어떤 경로에서 왔고, 이후 어떤 함수/코드/설정 조각으로 쪼개져 Retriever에 들어갔는지 추적하기 위한 표시다. .md처럼 제외하기로 한 파일은 제외 목록에 남기고, 포함된 .gd 파일은 AST Parser가 함수 또는 코드 조각 단위로 순서대로 나눈다. 이후 LLM 호출은 항상 프롬프트 + 현재 조각 + Retriever 검색 결과로 여러 번 수행한다.
for each file in repository path order:
record file path under "# <relative/path>"
if file is excluded by source-analysis policy:
save excluded_files entry with reason
continue
if file.path endswith ".gd":
ast_chunks = ast_parse(full_file_text)
for each ast_chunk in ast_chunks:
retrieved = retrieve(build_query_from_chunk(prompt, ast_chunk))
response = call_llm(prompt + ast_chunk + retrieved)
validate(response)
save_chunk_result(response)
else:
text_chunks = split_without_ast(file)
for each text_chunk in text_chunks:
retrieved = retrieve(build_query_from_chunk(prompt, text_chunk))
response = call_llm(prompt + text_chunk + retrieved)
validate(response)
save_chunk_result(response)최종 LLM 입력 단위는 항상 다음 형태다.
llm_judgment_request
user_prompt
project_id
source_file
chunk_id
chunk_order
chunk_kind
chunk_text
retrieved_evidenceRetriever 검색 입력
Retriever는 내가 제공한 코드 조각을 DB에 검색하는 역할만 한다. 파일 경로, chunk id, chunk order는 검색 조건이 아니라 추적 정보다. 그래서 구현에서는 두 객체를 분리한다.
chunk_trace
project_id
source_file
source_sha256
chunk_id
chunk_order
chunk_kindretriever_query
query_text
query_terms
raw_chunk_text
symbol_candidates
api_call_candidates
reference_candidates
prompt_termschunk_trace는 UI, 로그, 재시작, 디버깅, LLM 응답 연결에만 쓴다. Retriever 검색 함수에는 retriever_query만 넘긴다.
AST/direct chunk
-> build_retriever_query(chunk_text, extracted_candidates, user_prompt)
-> retriever.search(retriever_query)
-> orchestrator attaches chunk_trace to returned hits
-> prompt + chunk_text + retrieved_hits -> LLM judgment즉 검색은 다음 값으로 한다.
| 검색에 사용 | 검색에 사용하지 않음 |
|---|---|
chunk_text |
source_file |
symbol_candidates |
chunk_id |
api_call_candidates |
chunk_order |
reference_candidates |
source_sha256 |
prompt_terms |
project_id |
파일 경로가 필요한 이유는 검색이 아니라 추적이다. 예를 들어 player.gd:function:_process라는 trace가 있어야 웹 UI에서 “이 응답이 player.gd의 네 번째 조각에서 나왔다”를 보여줄 수 있다. 하지만 Retriever가 DB에서 근거를 찾을 때는 player.gd라는 경로가 아니라 _process 함수 안의 코드와 API 후보로 검색해야 한다.
예를 들어 player.gd의 _process 함수 조각은 먼저 추적 정보와 검색 정보를 분리한다.
chunk_trace
project_id: "github:godotengine/godot-demo-projects/2d/dodge_the_creeps"
source_file: "player.gd"
source_sha256: "87f4fcf7481dba031f74a363475cee75b81c3d42eb1347b318f6b824e37329a6"
chunk_id: "player.gd:function:_process"
chunk_order: 4
chunk_kind: "function"retriever_query
raw_chunk_text: |
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
symbol_candidates:
- "Vector2"
- "AnimatedSprite2D"
api_call_candidates:
- "Input.is_action_pressed"
- "Vector2.ZERO"
- "velocity.normalized"
- "position.clamp"
reference_candidates: []
prompt_terms:
- "Godot 3"
- "Godot 4"
- "migration"
query_terms:
- "Input.is_action_pressed"
- "Vector2.ZERO"
- "velocity.normalized"
- "position.clamp"
- "Vector2"
- "AnimatedSprite2D"
- "_process"
- "Godot 3"
- "Godot 4"
- "migration"
query_text: "Input.is_action_pressed Vector2.ZERO velocity.normalized position.clamp Vector2 AnimatedSprite2D _process Godot 3 Godot 4 migration"Retriever 검색 코드 기준
아래 코드는 구현 방향을 고정하기 위한 기준이다. Retriever에는 RetrieverQuery만 넘긴다. ChunkTrace는 Retriever 밖에서 orchestrator가 들고 있다가 검색 결과와 다시 묶는다.
from dataclasses import dataclass, field
import re
@dataclass(frozen=True)
class ChunkTrace:
project_id: str
source_file: str
source_sha256: str
chunk_id: str
chunk_order: int
chunk_kind: str
@dataclass(frozen=True)
class RetrieverQuery:
query_text: str
query_terms: list[str]
raw_chunk_text: str
symbol_candidates: list[str] = field(default_factory=list)
api_call_candidates: list[str] = field(default_factory=list)
reference_candidates: list[str] = field(default_factory=list)
prompt_terms: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class RetrievalHit:
table_name: str
record_id: str
score: float
title: str
content: str
metadata: dict
IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)?")
def unique_keep_order(values: list[str]) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for value in values:
normalized = value.strip()
if normalized and normalized not in seen:
seen.add(normalized)
result.append(normalized)
return result
def extract_identifiers(chunk_text: str) -> list[str]:
return unique_keep_order(IDENTIFIER_RE.findall(chunk_text))
def extract_prompt_terms(user_prompt: str) -> list[str]:
terms: list[str] = []
prompt = user_prompt.lower()
if "godot 3" in prompt or "godot3" in prompt:
terms.append("Godot 3")
if "godot 4" in prompt or "godot4" in prompt:
terms.append("Godot 4")
if "변환" in user_prompt or "마이그레이션" in user_prompt or "migration" in prompt:
terms.extend(["migration", "deprecated", "renamed"])
if "설명" in user_prompt or "뜻" in user_prompt or "explain" in prompt:
terms.extend(["description", "usage"])
return terms
def build_retriever_query(
*,
user_prompt: str,
chunk_text: str,
symbol_candidates: list[str],
api_call_candidates: list[str],
reference_candidates: list[str],
) -> RetrieverQuery:
prompt_terms = extract_prompt_terms(user_prompt)
query_terms = unique_keep_order(
api_call_candidates
+ symbol_candidates
+ reference_candidates
+ extract_identifiers(chunk_text)
+ prompt_terms
)
return RetrieverQuery(
query_text=" ".join(query_terms),
query_terms=query_terms,
raw_chunk_text=chunk_text,
symbol_candidates=symbol_candidates,
api_call_candidates=api_call_candidates,
reference_candidates=reference_candidates,
prompt_terms=prompt_terms,
)
def build_embedding_text(query: RetrieverQuery) -> str:
return "\n".join(
[
query.raw_chunk_text,
" ".join(query.api_call_candidates),
" ".join(query.symbol_candidates),
" ".join(query.reference_candidates),
" ".join(query.prompt_terms),
]
)
def retrieve_for_query(db, embedder, query: RetrieverQuery, limit_per_table: int = 5) -> list[RetrievalHit]:
query_embedding = embedder.embed_query(build_embedding_text(query))
tables = ["docs_chunks", "api_mapping", "label_prototypes"]
hits: list[RetrievalHit] = []
for table_name in tables:
hits.extend(search_table(db, table_name, query, query_embedding, limit_per_table))
hits.sort(key=lambda hit: hit.score, reverse=True)
return hits
def search_table(db, table_name: str, query: RetrieverQuery, query_embedding: list[float], limit: int) -> list[RetrievalHit]:
sql = """
select
%(table_name)s as table_name,
id::text as record_id,
title,
content,
metadata,
1 - (embedding <=> %(query_embedding)s::vector) as vector_score,
ts_rank(search_tsv, websearch_to_tsquery('simple', %(query_text)s)) as text_score,
similarity(search_text, %(query_text)s) as trigram_score
from godot_rag.dynamic_retrieval_view
where table_name = %(table_name)s
and (
embedding is not null
or
search_tsv @@ websearch_to_tsquery('simple', %(query_text)s)
or search_text %% %(query_text)s
)
order by
embedding <=> %(query_embedding)s::vector asc,
ts_rank(search_tsv, websearch_to_tsquery('simple', %(query_text)s)) desc,
similarity(search_text, %(query_text)s) desc
limit %(limit)s
"""
rows = db.fetch_all(
sql,
{
"table_name": table_name,
"query_text": query.query_text,
"query_embedding": query_embedding,
"limit": limit,
},
)
return [
RetrievalHit(
table_name=row["table_name"],
record_id=row["record_id"],
score=float(row["vector_score"] or 0) + float(row["text_score"] or 0) + float(row["trigram_score"] or 0),
title=row["title"],
content=row["content"],
metadata={
**row["metadata"],
"query_text": query.query_text,
"query_terms": query.query_terms,
},
)
for row in rows
]
def retrieve_with_trace(db, embedder, trace: ChunkTrace, query: RetrieverQuery) -> dict:
hits = retrieve_for_query(db, embedder, query)
return {
"trace": trace,
"retriever_query": query,
"hits": hits,
}위 코드에서 중요한 점은 다음과 같다.
- Retriever 검색 함수는
source_file,chunk_id,chunk_order를 검색 조건으로 받지 않는다. - Retriever는 코드 조각에서 만든
query_embedding으로 vector search를 하고,query.query_text는 keyword/trigram 보조 검색에 사용한다. query_embedding은raw_chunk_text, API 후보, 심볼 후보, 프롬프트 의도어를 합쳐 만든다.query.query_text는 파일 내부 코드 조각에서 뽑은 API, 심볼, 식별자, 프롬프트 의도어로 만든다.- 파일 경로와 chunk 순서는
ChunkTrace에만 남긴다. retrieve_with_trace는 검색 이후 결과와 trace를 묶는 orchestrator 함수다. 검색 자체는retrieve_for_query가 한다.docs_chunks,api_mapping,label_prototypes는 같은RetrieverQuery로 검색한다.
godot_rag.dynamic_retrieval_view는 새 판단 로직이 아니라 검색용 평탄화 view다. 세 테이블의 payload 구조가 달라도 Retriever 입장에서는 같은 컬럼을 보게 만든다.
dynamic_retrieval_view
table_name
id
title
content
embedding
search_text
search_tsv
metadata검색 대상은 다음처럼 펼친다. 여기서도 source_file, source_url 같은 출처 필드는 metadata로 보존하되, 기본 검색 문자열에는 넣지 않는다. 검색은 코드 조각의 API/심볼/패턴으로 해야 한다.
| 테이블 | title |
content |
search_text에 들어가는 값 |
|---|---|---|---|
docs_chunks |
문서 제목, section path, symbol | 문서 본문 chunk | doc_type, symbol, section_path, content, api_symbols |
api_mapping |
변경된 API 또는 이전 API 이름 | 마이그레이션 설명, 변경 이유, 예시 | old_symbol, new_symbol, change_type, description, before_code, after_code |
label_prototypes |
prototype 이름 또는 라벨 | 함수 사용 방식, 인자 구성, 호출 패턴 변경 예시 | label, input_pattern, prompt_pattern, before_code, after_code, expected_response, explanation |
예시 SQL view 형태:
create or replace view godot_rag.dynamic_retrieval_view as
select
'docs_chunks' as table_name,
id,
coalesce(payload->>'title', payload->>'symbol', source_file) as title,
coalesce(payload->>'content', '') as content,
embedding,
concat_ws(
' ',
payload->>'doc_type',
payload->>'symbol',
payload->>'section_path',
payload->>'content',
payload->>'api_symbols'
) as search_text,
search_tsv,
payload || jsonb_build_object('source_file', source_file, 'source_url', payload->>'source_url') as metadata
from godot_rag.docs_chunks
union all
select
'api_mapping' as table_name,
id,
concat_ws(' -> ', payload->>'old_symbol', payload->>'new_symbol') as title,
concat_ws(' ', payload->>'description', payload->>'rationale') as content,
embedding,
concat_ws(
' ',
payload->>'old_symbol',
payload->>'new_symbol',
payload->>'change_type',
payload->>'description',
payload->>'before_code',
payload->>'after_code'
) as search_text,
search_tsv,
payload || jsonb_build_object('source_file', source_file, 'source_url', payload->>'source_url') as metadata
from godot_rag.api_mapping
union all
select
'label_prototypes' as table_name,
id,
coalesce(payload->>'label', payload->>'name', source_file) as title,
concat_ws(' ', payload->>'expected_response', payload->>'explanation') as content,
embedding,
concat_ws(
' ',
payload->>'label',
payload->>'input_pattern',
payload->>'prompt_pattern',
payload->>'before_code',
payload->>'after_code',
payload->>'expected_response',
payload->>'explanation'
) as search_text,
search_tsv,
payload || jsonb_build_object('source_file', source_file) as metadata
from godot_rag.label_prototypes;이 view 위에서 Retriever가 실제로 검색하는 값은 query_text다. 예를 들어 _process chunk라면 다음처럼 검색한다.
query_text =
"Input.is_action_pressed Vector2.ZERO velocity.normalized position.clamp Vector2 AnimatedSprite2D _process Godot 3 Godot 4 migration"검색 결과는 LLM에 바로 믿고 넣는 정답이 아니라 후보 근거다. LLM 판단 요청에는 chunk와 검색 후보를 함께 넣고, Qwen 3.6이 “이 근거가 현재 chunk와 관련 있는지”를 다시 검증한다.
전송 디버깅 기준
AI가 .gd 파일에서 “핵심 함수만” 임의로 골라 보내거나, 반대로 파일 전체를 하나의 LLM 요청으로 보내는 일을 막기 위해, 전송 전후를 diff로 확인할 수 있어야 한다. 확인 대상은 “원문 파일 전체가 LLM으로 갔는가”가 아니라 “원문 파일에서 만들어진 함수/코드 조각이 원본 순서대로 빠짐없이 추적되고, 각 조각이 Retriever 요청으로 넘어갔는가”이다.
파일 전개 단계에서는 먼저 경로별 원문 블록을 만든다.
# player.gd
extends Area2D
signal hit
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
func _ready():
screen_size = get_viewport_rect().size
hide()
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
func start(pos):
position = pos
rotation = 0
show()
$CollisionShape2D.disabled = false
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)그 다음 AST Parser가 만든 조각 목록을 남긴다.
ast_chunk_trace
source_file: "player.gd"
source_kind: "gdscript"
source_sha256: "87f4fcf7481dba031f74a363475cee75b81c3d42eb1347b318f6b824e37329a6"
chunks:
- chunk_order: 1
chunk_id: "player.gd:class_extends:1"
node_kind: "class_extends"
code_text: "extends Area2D"
- chunk_order: 2
chunk_id: "player.gd:signal:hit"
node_kind: "signal"
code_text: "signal hit"
- chunk_order: 3
chunk_id: "player.gd:function:_ready"
node_kind: "function"
code_text: |
func _ready():
screen_size = get_viewport_rect().size
hide()
- chunk_order: 4
chunk_id: "player.gd:function:_process"
node_kind: "function"
code_text: |
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
- chunk_order: 5
chunk_id: "player.gd:function:start"
node_kind: "function"
code_text: |
func start(pos):
position = pos
rotation = 0
show()
$CollisionShape2D.disabled = false
- chunk_order: 6
chunk_id: "player.gd:function:_on_body_entered"
node_kind: "function"
code_text: |
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)Retriever 전송 직전 payload도 chunk 단위로 남긴다.
retrieval_payload
source_file: "player.gd"
chunk_order: 4
chunk_id: "player.gd:function:_process"
chunk_text: |
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0검증 규칙:
expanded_source_block["player.gd"].sha256 == ast_chunk_trace.source_sha256
ast_chunk_trace.chunks[].chunk_order is strictly increasing
retrieval_payload.chunk_id exists in ast_chunk_trace.chunks[].chunk_id
retrieval_payload.chunk_order matches ast_chunk_trace.chunks[].chunk_order이 검증이 실패하면 전송하지 않는다. 이 규칙은 .gd 파일에서 특히 중요하다. 의도는 .gd 파일 전체를 LLM에 넣는 것이 아니라, .gd에서 나온 함수/코드 조각이 원본 경로와 원본 순서를 잃지 않고 Retriever까지 넘어가는지 확인하는 것이다.
이 프로젝트를 입력으로 받으면 먼저 사람이 확인 가능한 평문 묶음으로 펼친다. 아래 블록은 바이너리 에셋을 제외한 텍스트 파일의 원문 전체를 # <relative/path> 단위로 보여준다. 여기에는 .md, LICENSE처럼 소스코드 분석에서는 제외 후보가 되는 파일도 포함되어 있다. 문서 예시에서는 상대경로 안의 내용을 일부 생략하지 않고 남겨 추적 기준을 확인한다. 다만 구현에서 제외 정책이 적용되면, 제외된 파일은 Retriever로 보내지 않고 excluded_files에 이유와 함께 표시한다.
# .gitignore
.import
logs/
# LICENSE
MIT License
Copyright (c) 2017 KidsCanCode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# README.md
# Dodge the Creeps
This is a simple game where your character must move
and avoid the enemies for as long as possible.
This is a finished version of the game featured in the
["Your first 2D game"](https://docs.godotengine.org/en/latest/getting_started/first_2d_game/index.html)
tutorial in the documentation. For more details,
consider following the tutorial in the documentation.
Language: GDScript
Renderer: Compatibility
> [!NOTE]
>
> There is a C# version available [here](https://github.com/godotengine/godot-demo-projects/tree/master/mono/dodge_the_creeps).
Check out this demo on the asset library: https://godotengine.org/asset-library/asset/2712
## Screenshots


## Copying
`art/House In a Forest Loop.ogg` Copyright © 2012 [HorrorPen](https://opengameart.org/users/horrorpen), [CC-BY 3.0: Attribution](https://creativecommons.org/licenses/by/3.0/). Source: https://opengameart.org/content/loop-house-in-a-forest
Images are from "Abstract Platformer". Created in 2016 by kenney.nl, [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). Source: https://www.kenney.nl/assets/abstract-platformer
Font is "Xolonium". Copyright © 2011-2016 Severin Meyer <sev.ch@web.de>, with Reserved Font Name Xolonium, SIL open font license version 1.1. Details are in `fonts/LICENSE.txt`.
# art/House In a Forest Loop.ogg.import
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://sgfduhhw4pno"
path="res://.godot/imported/House In a Forest Loop.ogg-1a6a72ae843ad792b7039931227e8d50.oggvorbisstr"
[deps]
source_file="res://art/House In a Forest Loop.ogg"
dest_files=["res://.godot/imported/House In a Forest Loop.ogg-1a6a72ae843ad792b7039931227e8d50.oggvorbisstr"]
[params]
loop=true
loop_offset=0.0
bpm=0.0
beat_count=0
bar_beats=4
# art/enemyFlyingAlt_1.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dun74wipekpfq"
path="res://.godot/imported/enemyFlyingAlt_1.png-559f599b16c69b112c1b53f6332e9489.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemyFlyingAlt_1.png"
dest_files=["res://.godot/imported/enemyFlyingAlt_1.png-559f599b16c69b112c1b53f6332e9489.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/enemyFlyingAlt_2.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vusf51hepduk"
path="res://.godot/imported/enemyFlyingAlt_2.png-31dc7310eda6e1b721224f3cd932c076.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemyFlyingAlt_2.png"
dest_files=["res://.godot/imported/enemyFlyingAlt_2.png-31dc7310eda6e1b721224f3cd932c076.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/enemySwimming_1.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d182mv7y80xqy"
path="res://.godot/imported/enemySwimming_1.png-dd0e11759dc3d624c8a704f6e98a3d80.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemySwimming_1.png"
dest_files=["res://.godot/imported/enemySwimming_1.png-dd0e11759dc3d624c8a704f6e98a3d80.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/enemySwimming_2.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmgglhdyowipd"
path="res://.godot/imported/enemySwimming_2.png-4c0cbc0732264c4ea3290340bd4a0a62.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemySwimming_2.png"
dest_files=["res://.godot/imported/enemySwimming_2.png-4c0cbc0732264c4ea3290340bd4a0a62.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/enemyWalking_1.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgwhuvn7qb4iy"
path="res://.godot/imported/enemyWalking_1.png-5af6eedbe61b701677d490ffdc1e6471.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemyWalking_1.png"
dest_files=["res://.godot/imported/enemyWalking_1.png-5af6eedbe61b701677d490ffdc1e6471.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/enemyWalking_2.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyw702efe6meu"
path="res://.godot/imported/enemyWalking_2.png-67c480ed60c35e95f5acb0436246b935.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/enemyWalking_2.png"
dest_files=["res://.godot/imported/enemyWalking_2.png-67c480ed60c35e95f5acb0436246b935.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/gameover.wav.import
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://td2mgko63p61"
path="res://.godot/imported/gameover.wav-98c95c744b35280048c2bd093cf8a356.sample"
[deps]
source_file="res://art/gameover.wav"
dest_files=["res://.godot/imported/gameover.wav-98c95c744b35280048c2bd093cf8a356.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=true
edit/normalize=true
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
# art/playerGrey_up1.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bcow5c46vixno"
path="res://.godot/imported/playerGrey_up1.png-6bd114d0a6beac91f48e3a7314d44564.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/playerGrey_up1.png"
dest_files=["res://.godot/imported/playerGrey_up1.png-6bd114d0a6beac91f48e3a7314d44564.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/playerGrey_up2.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dw3lwgwhpbfx8"
path="res://.godot/imported/playerGrey_up2.png-d6aba85f5f2675ebc7045efa7552ee79.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/playerGrey_up2.png"
dest_files=["res://.godot/imported/playerGrey_up2.png-d6aba85f5f2675ebc7045efa7552ee79.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/playerGrey_walk1.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2aofu01vxvea"
path="res://.godot/imported/playerGrey_walk1.png-c4773fe7a7bf85d7ab732eb4458c2742.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/playerGrey_walk1.png"
dest_files=["res://.godot/imported/playerGrey_walk1.png-c4773fe7a7bf85d7ab732eb4458c2742.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# art/playerGrey_walk2.png.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ddjou2q6gxlfr"
path="res://.godot/imported/playerGrey_walk2.png-34d2d916366100182d08037c51884043.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/playerGrey_walk2.png"
dest_files=["res://.godot/imported/playerGrey_walk2.png-34d2d916366100182d08037c51884043.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# fonts/FONTLOG.txt
Please distribute this file along with the Xolonium fonts when possible.
Source
Find the sourcefiles of Xolonium at
<gitlab.com/sev/xolonium>
Credits
Xolonium is created with FontForge <fontforge.org>,
Inkscape <inkscape.org>, Python <python.org>, and
FontTools <github.com/fonttools>.
It originated as a custom font for the open-source
game Xonotic <xonotic.org>. With many thanks to the
Xonotic community for your support.
Supported OpenType features
case Provides case sensitive placement of punctuation,
brackets, and math symbols for uppercase text.
frac Replaces number/number sequences with diagonal fractions.
Numbers that touch a slash should not exceed 10 digits.
kern Provides kerning for Latin, Greek, and Cyrillic scripts.
locl Dutch: Replaces j with a stressed version if it follows í.
Sami: Replaces n-form Eng with the preferred N-form version.
Romanian and Moldovan: Replaces ŞşŢţ with the preferred ȘșȚț.
pnum Replaces monospaced digits with proportional versions.
sinf Replaces digits with scientific inferiors below the baseline.
subs Replaces digits with subscript versions on the baseline.
sups Replaces digits with superscript versions.
zero Replaces zero with a slashed version.
Supported glyph sets
Adobe Latin 3
OpenType W1G
ISO 8859-1 Western European
ISO 8859-2 Central European
ISO 8859-3 South European
ISO 8859-4 North European
ISO 8859-5 Cyrillic
ISO 8859-7 Greek
ISO 8859-9 Turkish
ISO 8859-10 Nordic
ISO 8859-13 Baltic Rim
ISO 8859-14 Celtic
ISO 8859-15 Western European
ISO 8859-16 South-Eastern European
Available glyphs
!"#$%&'()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
`abcdefghijklmnopqrstuvwxyz{|}~
¡¢£¤¥¦§¨©ª«¬ ®¯°±²³´µ¶·¸¹º»¼½¾¿
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß
àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğ
ĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľ
ĿŀŁłŃńŅņŇňŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞş
ŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽž
ƒǺǻǼǽǾǿȘșȚțȷ
ˆˇˉ˘˙˚˛˜˝
ͺ;΄΅Ά·ΈΉΊΌΎΏΐ
ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰ
αβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ
ЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОП
РСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп
рстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџ
ѢѣѲѳѴѵҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩ
ҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽӀӁӂӇӈӋӌӏӐӑӒӓ
ӔӕӖӗӘәӜӝӞӟӠӡӢӣӤӥӦӧӨөӮӯӰӱӲӳӴӵӶӷӸӹ
Ԥԥ
ḂḃḊḋḞḟṀṁṖṗṠṡṪṫẀẁẂẃẄẅẞỲỳ
‒–—―‘’‚‛“”„‟†‡•…‰′″‹›‽‾⁄
⁰⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎
₤₦₩₫€₯₱₹₺₽₿
℅ℓ№℗™Ω℮
⅛⅜⅝⅞
←↑→↓
∂∆∏∑−∕∙√∞∟∫≈≠≤≥
⌖
■▬▮▰▲▶▼◀◆◊●◢◣◤◥
☄★☠☢☣⚙⚛⚠⚡⛔
❇❈❌❤❰❱❲❳
fffiflffiffl
🌌🌍🌎🌏👽💣🔥🔫
😁😃😄😆😇😈😉😊😎😐😒😕😘
😛😝😞😟😠😣😭😮😲😴😵
🚀
Debugging glyphs
U+EFFD Font version
U+F000 Font hinting indicator
Changelog
Xolonium 4.1 2016-11-22 Severin Meyer <sev.ch@web.de>
Reverted frac OpenType feature to a more stable implementation
Xolonium 4.0 2016-10-08 Severin Meyer <sev.ch@web.de>
Decreased width of most glyphs
Thinner vertical stems in Xolonium-Regular
Thicker horizontal stems in Xolonium-Bold
Revised diagonal stems
Lowered middle bars
Revised diacritical bars
Added glyphs:
ӏẞ₿
U+2007 U+2008 U+2009 U+200A U+202F
U+EFFD U+F000
Revised glyphs:
$&,JKQRXkwxy~¢¤ßǻ˜ζκλμξφЖУжћѴѵ∕₱₺₦₩€ℓ№≈ffffiffl
❤🌍🌎🌏😁😄😇😈😉😊😘😭😮😴🚀
Removed uncommon glyphs:
ʼnſʼҌҍҎҏҾҿӃӄӇӈӚӛӪӫӬӭ
U+0312 U+0313 U+0326
Simplified OpenType features pnum, zero, and case
Removed OpenType feature dlig
Revised vertical metrics
Merged outlines of composite glyphs in otf version
Added ttf version with custom outlines and instructions
Added woff and woff2 version
Xolonium 3.1 2015-06-10 Severin Meyer <sev.ch@web.de>
Added currency glyphs:
₦₩₫₱₹₺₽
Revised glyph:
₯
Relicensed public release under the SIL Open Font License 1.1
Xolonium 3.0 2015-05-04 Severin Meyer <sev.ch@web.de>
Decreased width of glyphs
Decreased descender height
Increased height of super/subscript glyphs
Revised width of dashes, underscore, and overscore
Sharper bends with more circular proportions
Decreased stroke thickness of mathematical glyphs
Revised diacritical marks
Revised diacritical bars
Revised Cyrillic hooks
Revised glyphs:
GQRYjmuwßŊŒſƒǻfffiffiffl
ΞΨΩδζιξπςστυφω
ЉЄДЛУЭЯбдлэяєљђєћѢѣҨҩҼҽӃӄӘә
#$&'()*,/69?@[]{}~¡£¤¥§©®¿
‹›₤€₯ℓ№℗℮←↑→↓∂∏∑∞≈▰☄❈❰❱❲❳😝
Raised vertical position of mathematical glyphs
Unified advance width of numeral and monetary glyphs
Unified advance width of mathematical glyphs
Revised bearings
Rewrote kern feature
Bolder Xolonium-Bold with improved proportions
Updated glyph names to conform to the AGLFN 1.7
Revised hints and PS Private Dictionary
Added glyphs:
ӶӷԤԥ
Added OpenType features:
case frac liga locl pnum sinf subs sups zero
Xolonium 2.4 2014-12-23 Severin Meyer <sev.ch@web.de>
Added dingbats:
⛔💣🔥
Revised size and design of emoticons
Revised dingbats:
⌖☄☠☣⚙⚛⚠⚡❇❈🌌🌍🌎🌏🔫
Removed dingbat:
💥
Xolonium 2.3 2014-08-14 Severin Meyer <sev.ch@web.de>
Bugfixed ε and έ, thanks to bowzee for the feedback
Xolonium 2.2 2014-03-01 Severin Meyer <sev.ch@web.de>
Added dingbats:
⌖◆●❌💥
Revised dingbats:
•←↑→↓◊☄★☠☣⚙⚛⚠⚡❇❈❤🌌🌍🌎🌏👽🔫🚀
Removed dingbats:
♻✪💡📡🔋🔧🔭
Xolonium 2.1 2013-10-20 Severin Meyer <sev.ch@web.de>
Added dingbats:
←↑→↓❰❱❲❳■▬▮▰▲▶▼◀◢◣◤◥
☄★☠☢☣♻⚙⚛⚠⚡✪❇❈❤
🌌🌍🌎🌏👽💡📡🔋🔧🔫🔭🚀
😁😃😄😆😇😈😉😊😎😐😒😕
😘😛😝😞😟😠😣😭😮😲😴😵
Xolonium 2.0.1 2013-07-12 Severin Meyer <sev.ch@web.de>
Reorganised and simplified files
Xolonium 2.0 2012-08-11 Severin Meyer <sev.ch@web.de>
Revised bends
Revised thickness of uppercase diagonal stems
Revised diacritical marks
Revised hints and PS Private Dictionary
Revised glyphs:
*1469@DPRly{}§©®¶ÐÞƒΘΞαεζνξνυЄЉЊ
ЏБЗЛУЧЪЫЬЭЯбзлчъыьэяєљњџ•€∂∙√∞∫≠
Completed glyph sets:
Adobe Latin 3
OpenType World Glyph Set 1 (W1G)
Ghostscript Standard (ghostscript-fonts-std-8.11)
Added OpenType kern feature
Added Xolonium-Bold
Xolonium 1.2 2011-02-12 Severin Meyer <sev.ch@web.de>
Revised glyphs:
D·Ðı
Completed glyph sets:
ISO 8859-7 (Greek)
Unicode Latin Extended-A block
Added glyphs:
†‡•…‰⁄™∂∑−√∞≠≤≥
Xolonium 1.1 2011-01-17 Severin Meyer <sev.ch@web.de>
Revised placement of cedilla and ogonek in accented glyphs
Revised glyphs:
,;DKTjkvwxy¥§Ð˛€
Completed glyph sets:
ISO 8859-2 (Central European)
ISO 8859-3 (South European, Esperanto)
ISO 8859-4 (North European)
ISO 8859-5 (Cyrillic)
ISO 8859-9 (Turkish)
ISO 8859-10 (Nordic)
ISO 8859-13 (Baltic Rim)
ISO 8859-14 (Celtic)
ISO 8859-16 (South-Eastern European)
Added glyphs:
ȷʼ̒ ЀЍѐѝ‒–—‘’‚‛“”„‟‹›
Xolonium 1.0 2011-01-04 Severin Meyer <sev.ch@web.de>
Completed glyph sets:
ISO 8859-1 (Western European)
ISO 8859-15 (Western European)
Added glyphs:
ĄĆĘŁŃŚŹŻąćęłńśźżıˆˇ˙˚˛˜
# fonts/LICENSE.txt
Copyright 2011-2016 Severin Meyer <sev.ch@web.de>,
with Reserved Font Name Xolonium.
This Font Software is licensed under the SIL Open Font License,
Version 1.1. This license is copied below, and is also available
with a FAQ at <http://scripts.sil.org/OFL>
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
# fonts/Xolonium-Regular.ttf.import
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bgv586r20ps8e"
path="res://.godot/imported/Xolonium-Regular.ttf-bc2981e3069cff4c34dd7c8e2bb73fba.fontdata"
[deps]
source_file="res://fonts/Xolonium-Regular.ttf"
dest_files=["res://.godot/imported/Xolonium-Regular.ttf-bc2981e3069cff4c34dd7c8e2bb73fba.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
# hud.gd
extends CanvasLayer
signal start_game
func show_message(text):
$MessageLabel.text = text
$MessageLabel.show()
$MessageTimer.start()
func show_game_over():
show_message("Game Over")
await $MessageTimer.timeout
$MessageLabel.text = "Dodge the\nCreeps"
$MessageLabel.show()
await get_tree().create_timer(1).timeout
$StartButton.show()
func update_score(score):
$ScoreLabel.text = str(score)
func _on_StartButton_pressed():
$StartButton.hide()
start_game.emit()
func _on_MessageTimer_timeout():
$MessageLabel.hide()
# hud.gd.uid
uid://c1g57034r2c0
# hud.tscn
[gd_scene format=3 uid="uid://b0efehuavobda"]
[ext_resource type="Script" uid="uid://c1g57034r2c0" path="res://hud.gd" id="1"]
[ext_resource type="FontFile" uid="uid://bgv586r20ps8e" path="res://fonts/Xolonium-Regular.ttf" id="2_2jm3i"]
[sub_resource type="InputEventAction" id="InputEventAction_fopy7"]
action = &"start_game"
[sub_resource type="Shortcut" id="4"]
events = [SubResource("InputEventAction_fopy7")]
[node name="HUD" type="CanvasLayer" unique_id=126421993]
script = ExtResource("1")
[node name="ScoreLabel" type="Label" parent="." unique_id=1314826100]
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 78.0
grow_horizontal = 2
theme_override_fonts/font = ExtResource("2_2jm3i")
theme_override_font_sizes/font_size = 60
text = "0"
horizontal_alignment = 1
[node name="MessageLabel" type="Label" parent="." unique_id=1611528703]
anchors_preset = 14
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_top = -79.5
offset_bottom = 79.5
grow_horizontal = 2
grow_vertical = 2
theme_override_fonts/font = ExtResource("2_2jm3i")
theme_override_font_sizes/font_size = 60
text = "Dodge the
Creeps"
horizontal_alignment = 1
[node name="StartButton" type="Button" parent="." unique_id=1561548516]
anchors_preset = 7
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -90.0
offset_top = -200.0
offset_right = 90.0
offset_bottom = -100.0
grow_horizontal = 2
grow_vertical = 0
theme_override_fonts/font = ExtResource("2_2jm3i")
theme_override_font_sizes/font_size = 60
shortcut = SubResource("4")
text = "Start"
[node name="MessageTimer" type="Timer" parent="." unique_id=1675980570]
one_shot = true
[connection signal="pressed" from="StartButton" to="." method="_on_StartButton_pressed"]
[connection signal="timeout" from="MessageTimer" to="." method="_on_MessageTimer_timeout"]
# icon.webp.import
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dfklrdtaun0xt"
path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.webp"
dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
# main.gd
extends Node
@export var mob_scene: PackedScene
var score
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
$HUD.show_game_over()
$Music.stop()
$DeathSound.play()
func new_game():
get_tree().call_group(&"mobs", &"queue_free")
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
$HUD.update_score(score)
$HUD.show_message("Get Ready")
$Music.play()
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on Path2D.
var mob_spawn_location = get_node(^"MobPath/MobSpawnLocation")
mob_spawn_location.progress_ratio = randf()
# Set the mob's position to a random location.
mob.position = mob_spawn_location.position
# Set the mob's direction perpendicular to the path direction.
var direction = mob_spawn_location.rotation + PI / 2
# Add some randomness to the direction.
direction += randf_range(-PI / 4, PI / 4)
mob.rotation = direction
# Choose the velocity for the mob.
var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
mob.linear_velocity = velocity.rotated(direction)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_ScoreTimer_timeout():
score += 1
$HUD.update_score(score)
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
# main.gd.uid
uid://c4wt6ace7hycd
# main.tscn
[gd_scene format=3 uid="uid://bggkaprn62fwm"]
[ext_resource type="Script" uid="uid://c4wt6ace7hycd" path="res://main.gd" id="1_0r6n5"]
[ext_resource type="PackedScene" uid="uid://cao351pllxqpa" path="res://mob.tscn" id="2_50pww"]
[ext_resource type="PackedScene" uid="uid://bwhlkliwp13p4" path="res://player.tscn" id="3_veqnc"]
[ext_resource type="PackedScene" uid="uid://b0efehuavobda" path="res://hud.tscn" id="4_0qnje"]
[ext_resource type="AudioStream" uid="uid://sgfduhhw4pno" path="res://art/House In a Forest Loop.ogg" id="5_55d8h"]
[ext_resource type="AudioStream" uid="uid://td2mgko63p61" path="res://art/gameover.wav" id="6_hp1r0"]
[sub_resource type="Curve2D" id="1"]
_data = {
"points": PackedVector2Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 480, 0, 0, 0, 0, 0, 480, 720, 0, 0, 0, 0, 0, 720, 0, 0, 0, 0, 0, 0)
}
point_count = 5
[node name="Main" type="Node" unique_id=1975992027]
script = ExtResource("1_0r6n5")
mob_scene = ExtResource("2_50pww")
[node name="ColorRect" type="ColorRect" parent="." unique_id=569320965]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.219608, 0.372549, 0.380392, 1)
[node name="Player" parent="." unique_id=927660131 instance=ExtResource("3_veqnc")]
[node name="MobTimer" type="Timer" parent="." unique_id=228987391]
wait_time = 0.5
[node name="ScoreTimer" type="Timer" parent="." unique_id=451982858]
[node name="StartTimer" type="Timer" parent="." unique_id=238316384]
wait_time = 2.0
one_shot = true
[node name="StartPosition" type="Marker2D" parent="." unique_id=568887530]
position = Vector2(240, 450)
[node name="MobPath" type="Path2D" parent="." unique_id=694323229]
curve = SubResource("1")
[node name="MobSpawnLocation" type="PathFollow2D" parent="MobPath" unique_id=1080274542]
[node name="HUD" parent="." unique_id=1879130737 instance=ExtResource("4_0qnje")]
[node name="Music" type="AudioStreamPlayer" parent="." unique_id=267371681]
stream = ExtResource("5_55d8h")
[node name="DeathSound" type="AudioStreamPlayer" parent="." unique_id=1715684712]
stream = ExtResource("6_hp1r0")
[connection signal="hit" from="Player" to="." method="game_over"]
[connection signal="timeout" from="MobTimer" to="." method="_on_MobTimer_timeout"]
[connection signal="timeout" from="ScoreTimer" to="." method="_on_ScoreTimer_timeout"]
[connection signal="timeout" from="StartTimer" to="." method="_on_StartTimer_timeout"]
[connection signal="start_game" from="HUD" to="." method="new_game"]
# mob.gd
extends RigidBody2D
func _ready():
var mob_types = Array($AnimatedSprite2D.sprite_frames.get_animation_names())
$AnimatedSprite2D.animation = mob_types.pick_random()
$AnimatedSprite2D.play()
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
# mob.gd.uid
uid://cypxpb8arjrqt
# mob.tscn
[gd_scene format=3 uid="uid://cao351pllxqpa"]
[ext_resource type="Script" uid="uid://cypxpb8arjrqt" path="res://mob.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://dun74wipekpfq" path="res://art/enemyFlyingAlt_1.png" id="2"]
[ext_resource type="Texture2D" uid="uid://vusf51hepduk" path="res://art/enemyFlyingAlt_2.png" id="3"]
[ext_resource type="Texture2D" uid="uid://dgwhuvn7qb4iy" path="res://art/enemyWalking_1.png" id="4"]
[ext_resource type="Texture2D" uid="uid://dyw702efe6meu" path="res://art/enemyWalking_2.png" id="5"]
[ext_resource type="Texture2D" uid="uid://d182mv7y80xqy" path="res://art/enemySwimming_1.png" id="6"]
[ext_resource type="Texture2D" uid="uid://dmgglhdyowipd" path="res://art/enemySwimming_2.png" id="7"]
[sub_resource type="SpriteFrames" id="1"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("2")
}, {
"duration": 1.0,
"texture": ExtResource("3")
}],
"loop": true,
"name": &"fly",
"speed": 3.0
}, {
"frames": [{
"duration": 1.0,
"texture": ExtResource("6")
}, {
"duration": 1.0,
"texture": ExtResource("7")
}],
"loop": true,
"name": &"swim",
"speed": 4.0
}, {
"frames": [{
"duration": 1.0,
"texture": ExtResource("4")
}, {
"duration": 1.0,
"texture": ExtResource("5")
}],
"loop": true,
"name": &"walk",
"speed": 4.0
}]
[sub_resource type="CapsuleShape2D" id="2"]
radius = 37.0
height = 100.0
[node name="Mob" type="RigidBody2D" unique_id=371809901 groups=["mobs"]]
collision_mask = 0
gravity_scale = 0.0
script = ExtResource("1")
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=1998522389]
scale = Vector2(0.75, 0.75)
sprite_frames = SubResource("1")
animation = &"walk"
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1880423722]
rotation = 1.5708
shape = SubResource("2")
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="." unique_id=959995349]
[connection signal="screen_exited" from="VisibleOnScreenNotifier2D" to="." method="_on_VisibilityNotifier2D_screen_exited"]
# player.gd
extends Area2D
signal hit
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
func _ready():
screen_size = get_viewport_rect().size
hide()
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
func start(pos):
position = pos
rotation = 0
show()
$CollisionShape2D.disabled = false
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)
# player.gd.uid
uid://6s0lxctks3qn
# player.tscn
[gd_scene format=3 uid="uid://bwhlkliwp13p4"]
[ext_resource type="Script" uid="uid://6s0lxctks3qn" path="res://player.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://b2aofu01vxvea" path="res://art/playerGrey_walk1.png" id="2"]
[ext_resource type="Texture2D" uid="uid://ddjou2q6gxlfr" path="res://art/playerGrey_walk2.png" id="3"]
[ext_resource type="Texture2D" uid="uid://bcow5c46vixno" path="res://art/playerGrey_up1.png" id="4"]
[ext_resource type="Texture2D" uid="uid://dw3lwgwhpbfx8" path="res://art/playerGrey_up2.png" id="5"]
[sub_resource type="SpriteFrames" id="1"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("2")
}, {
"duration": 1.0,
"texture": ExtResource("3")
}],
"loop": true,
"name": &"right",
"speed": 5.0
}, {
"frames": [{
"duration": 1.0,
"texture": ExtResource("4")
}, {
"duration": 1.0,
"texture": ExtResource("5")
}],
"loop": true,
"name": &"up",
"speed": 5.0
}]
[sub_resource type="CapsuleShape2D" id="2"]
radius = 27.0
height = 68.0
[sub_resource type="Gradient" id="3"]
colors = PackedColorArray(1, 1, 1, 0.501961, 1, 1, 1, 0)
[sub_resource type="GradientTexture1D" id="4"]
gradient = SubResource("3")
[sub_resource type="Curve" id="5"]
_data = [Vector2(0.00501098, 0.5), 0.0, 0.0, 0, 0, Vector2(0.994989, 0.324), 0.0, 0.0, 0, 0]
point_count = 2
[sub_resource type="CurveTexture" id="6"]
curve = SubResource("5")
[sub_resource type="ParticleProcessMaterial" id="7"]
gravity = Vector3(0, 0, 0)
scale_curve = SubResource("6")
color_ramp = SubResource("4")
[node name="Player" type="Area2D" unique_id=2141725708]
z_index = 10
script = ExtResource("1")
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="." unique_id=1437394421]
scale = Vector2(0.5, 0.5)
sprite_frames = SubResource("1")
animation = &"right"
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1954506745]
shape = SubResource("2")
[node name="Trail" type="GPUParticles2D" parent="." unique_id=1747300857]
z_index = -1
amount = 10
texture = ExtResource("2")
speed_scale = 2.0
process_material = SubResource("7")
[connection signal="body_entered" from="." to="." method="_on_body_entered"]
# project.godot
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Dodge the Creeps"
config/description="This is a simple game where your character must move
and avoid the enemies for as long as possible.
This is a finished version of the game featured in the 'Your first 2D game'
tutorial in the documentation. For more details, consider
following the tutorial in the documentation."
config/tags=PackedStringArray("2d", "demo", "official")
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.6")
config/icon="res://icon.webp"
[display]
window/size/viewport_width=480
window/size/viewport_height=720
window/size/window_width_override=480
window/size/window_height_override=720
window/stretch/mode="canvas_items"
[input]
move_left={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
]
}
move_right={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":15,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
]
}
move_up={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
]
}
move_down={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
]
}
start_game={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
# screenshots/.gdignore
바이너리 에셋은 원본 bytes를 위 평문 본문에는 넣지 않지만, repository path order와 제외/참조 목록에는 반드시 남긴다. 예를 들어 main.tscn이 res://art/gameover.wav를 가리키면, LLM 판단 입력에는 main.tscn에서 나온 chunk, art/gameover.wav가 참조된 경로 메타데이터, Retriever 검색 결과, 사용자 프롬프트가 같은 요청 객체 안에 들어간다. 바이너리 원본 bytes 자체는 Retriever나 LLM으로 보내지 않는다.
파일 단위 전달 예시
평문 묶음에서 # player.gd 블록을 떼어내면 다음처럼 하나의 analysis_request가 된다.
analysis_request
prompt: "이 프로젝트가 Godot 3인지 Godot 4인지 확인하고, 필요한 경우 변환 근거를 찾아줘."
project_id: "github:godotengine/godot-demo-projects/2d/dodge_the_creeps"
source_origin: "github"
source_file: "player.gd"
source_kind: "gdscript"
source_sha256: "87f4fcf7481dba031f74a363475cee75b81c3d42eb1347b318f6b824e37329a6"
source: |
extends Area2D
signal hit
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
func _ready():
screen_size = get_viewport_rect().size
hide()
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
func start(pos):
position = pos
rotation = 0
show()
$CollisionShape2D.disabled = false
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)AST Parser에는 위 요청 전체가 아니라 소스 분석에 필요한 필드만 넘긴다.
ast_parse_input
project_id: "github:godotengine/godot-demo-projects/2d/dodge_the_creeps"
source_file: "player.gd"
source_kind: "gdscript"
source_text: |
extends Area2D
signal hit
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
func _ready():
screen_size = get_viewport_rect().size
hide()
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
func start(pos):
position = pos
rotation = 0
show()
$CollisionShape2D.disabled = false
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)
source_sha256: "87f4fcf7481dba031f74a363475cee75b81c3d42eb1347b318f6b824e37329a6"Parser는 파일을 조각으로 나눈다. 실제 구현체가 확정되기 전까지는 다음 수준의 조각을 기대한다.
ast_parse_output
source_file: "player.gd"
chunks:
- node_kind: "class_extends"
code_text: "extends Area2D"
symbol_candidates: ["Area2D"]
- node_kind: "signal"
code_text: "signal hit"
symbol_candidates: ["hit"]
- node_kind: "export_variable"
code_text: "@export var speed = 400"
symbol_candidates: ["@export", "speed"]
- node_kind: "function"
code_text: |
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed(&"move_right"):
velocity.x += 1
if Input.is_action_pressed(&"move_left"):
velocity.x -= 1
if Input.is_action_pressed(&"move_down"):
velocity.y += 1
if Input.is_action_pressed(&"move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = &"right"
$AnimatedSprite2D.flip_v = false
$Trail.rotation = 0
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = &"up"
rotation = PI if velocity.y > 0 else 0
api_call_candidates: ["Input.is_action_pressed", "Vector2.ZERO", "position.clamp"]
- node_kind: "function"
code_text: |
func _on_body_entered(_body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred(&"disabled", true)
api_call_candidates: ["hit.emit", "set_deferred"]각 chunk는 다시 Retriever 요청으로 넘어간다.
retrieval_request
project_id: "github:godotengine/godot-demo-projects/2d/dodge_the_creeps"
source_file: "player.gd"
chunk_id: "player.gd:function:_process"
prompt_intent: "version_check_and_explain"
query_terms:
- "Input.is_action_pressed"
- "Vector2.ZERO"
- "position.clamp"
- "@export"
candidate_tables:
- "docs_chunks"
- "api_mapping"
- "label_prototypes"이 흐름은 player.gd만의 특별한 처리가 아니다. 다만 파일 종류에 따라 조각 생성 단계가 다르다. main.gd, mob.gd, hud.gd는 analysis_request -> ast_parse_input -> ast_parse_output.chunks[] -> retrieval_request를 반복한다. project.godot, main.tscn처럼 AST Parser 대상이 아닌 파일은 analysis_request -> direct_chunk_output.chunks[] -> retrieval_request를 반복한다.
이 예시 프로젝트에서 확인할 반복 전송 흐름은 다음과 같다.
- 프로젝트 루트를 하나의
project_id로 잡는다. - Godot 관련 텍스트 파일을 상대 경로 기준으로 수집한다.
- 각 파일을
# <relative/path>헤더가 붙은 평문 블록으로 펼친다. - 각 평문 블록을 다시 파일 단위
analysis_request로 만든다. - 각
analysis_request에서source_sha256을 계산한다. .gd파일은 AST Parser가ast_parse_output.chunks[]를 만든다.- AST Parser 대상이 아닌 포함 파일은 직접 조각화기가
direct_chunk_output.chunks[]를 만든다. - 각 chunk를 기준으로 Retriever 요청을 만든다.
- Retriever 결과와 chunk를 묶어 LLM 판단 요청을 반복한다.
- 모든 필요한 파일/chunk 요청이 정상 응답해야 프로젝트 단위 판단 후보로 올린다.
즉 프로젝트 하나는 다음처럼 여러 요청으로 쪼개진다.
dodge_the_creeps/project.godot
-> analysis_request
-> direct_chunk_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/main.gd
-> analysis_request
-> ast_parse_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/player.gd
-> analysis_request
-> ast_parse_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/player.tscn
-> analysis_request
-> direct_chunk_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/mob.gd
-> analysis_request
-> ast_parse_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/mob.tscn
-> analysis_request
-> direct_chunk_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/hud.gd
-> analysis_request
-> ast_parse_output.chunks[]
-> retrieval_request per chunk
dodge_the_creeps/hud.tscn
-> analysis_request
-> direct_chunk_output.chunks[]
-> retrieval_request per chunk이때 한 파일이나 한 chunk가 실패했다고 프로젝트 전체를 바로 확정하지 않는다. 실패한 요청은 pending 또는 retry 대상으로 남기고, 정상 응답한 chunk만 누적한다. 프로젝트 단위 판단은 필요한 반복 요청들이 충분히 응답한 뒤에만 수행한다.
반복 요청 흐름
프로젝트 하나는 LLM 요청 한 번으로 판정하지 않는다.
프로젝트 입력은 다음처럼 여러 요청으로 나뉜다.
project
-> file requests
-> AST chunks or direct chunks
-> retrieval requests
-> LLM judgment requests
-> project-level aggregation각 단계의 반복 단위는 다음과 같다.
| 단계 | 반복 단위 |
|---|---|
| 파일 스캔 | 프로젝트 안의 파일 |
| AST Parser | 포함된 .gd 파일 |
| 직접 조각화 | AST Parser 대상이 아닌 포함 파일 |
| 제외 기록 | .md, 바이너리 등 제외 대상 파일 |
| Retriever | AST chunk 또는 direct chunk |
| Qwen 3.6 판단 | AST/direct chunk + 검색 근거 |
| 프로젝트 분류 | chunk 판단 결과 전체 |
따라서 프로젝트 하나가 참/거짓으로 끝나는 것이 아니라, 프로젝트 안의 여러 파일과 여러 조각이 각각 정상적으로 처리되어야 한다. 최종 프로젝트 판단은 모든 필요한 조각의 Retriever/LLM/Validator 결과가 모인 뒤에만 할 수 있다.
Qwen 3.6은 온디맨드로 구동하는 전제이므로, 한 번의 큰 요청으로 끝내기보다 AST chunk 또는 direct chunk 단위로 여러 번 호출하는 방향이 맞다. 이때 이미 완료된 chunk는 재사용하고, 실패하거나 중단된 chunk만 다시 요청한다.
AST Parser에 넘기는 데이터
AST Parser에는 프롬프트를 직접 넣지 않는다.
Parser 입력은 다음처럼 최소화한다.
ast_parse_input
project_id
source_file
source_kind
source_text
source_sha256이렇게 분리하는 이유는 다음과 같다.
- 같은 소스코드를 여러 프롬프트로 다시 분석할 수 있다.
- 프롬프트가 바뀌어도 AST 파싱 결과는 재사용할 수 있다.
- Parser가 “질문 의도” 때문에 소스 구조를 다르게 해석하지 않게 한다.
- 이후 Retriever/LLM 단계에서만 프롬프트를 사용해 판단 방향을 바꿀 수 있다.
AST Parser 출력
Parser는 소스 전체를 바로 LLM에 넘기기보다, 추적 가능한 조각 목록을 만든다.
ast_parse_output
project_id
source_file
source_sha256
parse_status
chunks[]각 chunk는 다음 정보를 가진다.
chunk
chunk_id
source_file
span
node_kind
symbol_candidates
api_call_candidates
code_text
surrounding_context| 항목 | 설명 |
|---|---|
chunk_id |
프로젝트/파일/범위 기반의 결정적 ID |
span |
line/column 또는 byte offset 범위 |
node_kind |
function, class, call, signal, property, scene_node 등 |
symbol_candidates |
클래스명, 타입명, 메서드명 후보 |
api_call_candidates |
Godot API 호출 후보 |
code_text |
해당 조각의 원문 코드 |
surrounding_context |
판단에 필요한 주변 코드. 전체 파일이 아니라 필요한 범위만 둔다. |
직접 조각화 출력
AST Parser 대상이 아닌 포함 파일은 direct_chunk_output을 만든다. 이 출력은 AST가 아니라 텍스트/설정/scene 구조를 원본 순서대로 나눈 결과다.
direct_chunk_output
project_id
source_file
source_sha256
chunk_status
chunks[]각 chunk는 다음 정보를 가진다.
direct_chunk
chunk_id
source_file
chunk_order
span
chunk_kind
chunk_text
reference_candidates예:
direct_chunk
source_file: "main.tscn"
chunk_order: 3
chunk_kind: "ext_resource"
chunk_text: "[ext_resource type=\"Script\" uid=\"uid://c4wt6ace7hycd\" path=\"res://main.gd\" id=\"1_0r6n5\"]"
reference_candidates:
- "res://main.gd".md처럼 제외하기로 한 파일은 direct_chunk_output을 만들지 않고 excluded_files에 남긴다.
프롬프트와 조각 결합
프롬프트는 Parser 뒤에서 다시 결합한다.
analysis_request.prompt
+ ast chunk 또는 direct chunk
+ project/file metadata
-> retrieval_request예:
| 프롬프트 의도 | 우선 Retriever |
|---|---|
| “이 코드가 뭔 뜻이야?” | docs_chunks |
| “Godot 4로 변환 필요해?” | api_mapping, label_prototypes |
| “이 코드가 Godot 3이야 4야?” | docs_chunks, api_mapping, label_prototypes 모두 후보 |
이 단계에서도 특정 테이블만 특별 취급하지 않는다. 프롬프트와 AST 조각의 성격에 따라 필요한 공식문서 JSONL 근거를 찾는다.
LLM에 넘기는 단위
Qwen 3.6에는 원본 프로젝트 전체를 한 번에 넘기지 않는다.
LLM 호출 단위는 다음 묶음이다.
llm_judgment_request
prompt
project_id
source_file
chunk_id
chunk_order
chunk_kind
chunk_text
surrounding_context
retrieved_evidence
judgment_contract여기서 judgment_contract는 아직 최종 JSONL 스키마가 아니다. 현재 단계에서는 LLM이 무엇을 판단해야 하는지 알려주는 요청 계약이다.
retrieved_evidence는 아직 최종 저장 스키마가 아니라 검색 결과 묶음을 가리키는 추상 필드다. 이후 저장 단계에서는 근거 종류에 따라 docs_chunks 근거 ID, api_mapping 근거 ID, label_prototypes 근거 ID처럼 분리될 수 있다. 이 문서에서는 “검색된 공식문서 JSONL 근거 묶음”이라는 의미로만 사용한다.
예:
판단할 것:
- 이 코드 조각이 Godot 3 API를 쓰는지
- Godot 4 기준으로 유효한지
- 마이그레이션 근거가 있는지
- 검색된 공식문서 근거가 실제 코드와 관련 있는지재사용 경계
같은 소스코드라도 프롬프트가 다르면 Retriever/LLM 판단은 달라질 수 있다.
반대로 같은 소스코드라면 AST Parser 결과는 재사용할 수 있다.
| 단계 | 재사용 기준 |
|---|---|
| AST parse | source_sha256, source_kind, Parser version |
| chunk 생성 | source_sha256, chunking version |
| Retriever 검색 | prompt intent, chunk symbols, retrieval version |
| LLM 판단 | prompt, chunk, evidence, model, prompt version |
이 경계를 지키면 “소스 구조 분석”과 “질문 의도에 따른 판단”이 섞이지 않는다.
중단/재개 관점
분석 요청이 길어질 수 있으므로 각 단계는 재개 가능해야 한다.
최소 저장 단위:
- 파일 스캔 완료 여부
- 파일별 AST parse 상태
- chunk 생성 상태
- chunk별 Retriever 검색 상태
- chunk별 LLM 판단 상태
- Validator 통과 여부
RunPod 또는 로컬 앱이 중간에 멈추면 처리 중이던 chunk만 다시 pending으로 되돌리고, 완료된 파일과 chunk는 재사용한다.
아직 확정하지 않는 것
이 문서에서는 다음을 확정하지 않는다.
다만 의존 순서는 기록한다.
| 순서 | 항목 | 이유 | 현재 추적 위치 |
|---|---|---|---|
| 1 | AST Parser 구현체 | 어떤 parser를 쓰는지에 따라 chunk 구조와 실패 복구 방식이 달라진다. | 이 문서의 AST Parser에 넘기는 데이터, AST Parser 출력 |
| 2 | 최종 JSONL 응답 스키마 | LLM 판단 결과를 어떤 JSONL로 검증할지 정해야 Validator를 붙일 수 있다. | docs/roadmaps/2026-06-25-source-analysis-scoring-architecture.md |
| 3 | score DB 컬럼 | JSONL 응답과 Retriever 근거 ID가 정리된 뒤 저장 컬럼을 정할 수 있다. | docs/retrospectives/2026-06-25-source-analysis-scoring.md |
| 4 | 파일시스템 분류 라벨 | score DB에 어떤 판단 결과가 쌓이는지 본 뒤 프로젝트 라벨을 정한다. | docs/roadmaps/2026-06-25-source-analysis-scoring-architecture.md |
| 5 | SFT/DPO 생성 규칙 | 분류된 파일시스템이 쌓인 뒤 별도 설계한다. | docs/roadmaps/2026-06-25-source-analysis-scoring-architecture.md |
현재 확정하는 것은 하나다.
프롬프트와 소스코드는 하나의 분석 요청으로 묶되,
AST Parser에는 소스코드와 파일 메타데이터만 넘기고,
프롬프트는 Parser 이후 Retriever/LLM 판단 단계에서 사용한다.