idea_world_labDEV JOURNAL
2026년 6월 27일 토요일

소스 입력에서 AST와 Retriever까지 이어지는 라인 추적 흐름

작성일: 2026년 6월 27일

목적

이 문서는 docs/roadmaps/2026-06-26-source-to-ast-input-flow.md에서 일관성이 흐려졌던 부분을 27일 기준으로 다시 정리한 문서다.

핵심은 새 설계 단위를 다시 정의하는 것이 아니다. 이미 전체 아키텍처와 DB 설계 방향은 따로 정리되어 있다. 여기서는 실제 입력이 들어왔을 때 어느 파일의 어느 줄이 AST Parser로 들어가고, 어느 조각이 Retriever 검색에 쓰이고, 어떤 근거 후보가 돌아와서 LLM 판단으로 이어지는지를 끝까지 추적한다.

고정할 원칙은 단순하다.

파일 경로는 위치를 추적하기 위해 붙인다.
Retriever에는 파일 경로가 아니라 코드/본문 조각이 들어간다.
.gd 파일은 AST Parser가 함수와 선언 단위로 자른다.
그 외 필요한 텍스트 파일은 줄 범위 단위로 직접 자른다.
각 조각은 순서대로 Retriever와 LLM 판단을 거친다.

입력 형태

프로젝트를 클론하거나 업로드하면 먼저 파일을 상대경로와 함께 펼친다.

형태는 다음처럼 보인다.

# <상대경로>
<파일 본문>

# <상대경로>
<파일 본문>

이 표기는 사람이 디버깅하기 위한 것이다. # player.gd 같은 헤더는 “이 아래 본문이 어느 파일에서 왔는지”를 보여주는 표시일 뿐이다.

Retriever 검색에는 # player.gd가 들어가지 않는다. AST Parser에도 # player.gd가 들어가지 않는다. 들어가는 것은 헤더 아래의 실제 파일 본문이다.

예시 입력

아래는 작은 Godot 프로젝트가 펼쳐진 모습이다. E001, E002 같은 번호는 설명용 라인 번호이며 실제 입력 문자에는 포함되지 않는다.

E001 # project.godot
E002 config_version=5
E003
E004 [application]
E005 config/name="Mini Dodge"
E006 run/main_scene="res://player.tscn"
E007
E008 # player.gd
E009 extends Area2D
E010
E011 signal hit
E012
E013 @export var speed = 400
E014 var screen_size
E015
E016 func _ready():
E017 	screen_size = get_viewport_rect().size
E018 	hide()
E019
E020 func _process(delta):
E021 	var velocity = Vector2.ZERO
E022 	if Input.is_action_pressed(&"move_right"):
E023 		velocity.x += 1
E024 	if Input.is_action_pressed(&"move_left"):
E025 		velocity.x -= 1
E026
E027 	if velocity.length() > 0:
E028 		velocity = velocity.normalized() * speed
E029 		$AnimatedSprite2D.play()
E030 	else:
E031 		$AnimatedSprite2D.stop()
E032
E033 	position += velocity * delta
E034 	position = position.clamp(Vector2.ZERO, screen_size)
E035
E036 func start(pos):
E037 	position = pos
E038 	show()
E039 	$CollisionShape2D.disabled = false
E040
E041 # player.tscn
E042 [gd_scene load_steps=3 format=3 uid="uid://mini"]
E043
E044 [ext_resource type="Script" path="res://player.gd" id="1"]
E045
E046 [node name="Player" type="Area2D"]
E047 script = ExtResource("1")

1단계: 파일 경계 나누기

먼저 # <상대경로> 헤더를 기준으로 파일을 나눈다.

project.godot은 E001에서 시작한다. 실제 본문은 E002부터 E006까지다. E001은 파일 경계 표시이므로 분석 본문이 아니다.

player.gd는 E008에서 시작한다. 실제 본문은 E009부터 E039까지다. E008은 파일 경계 표시이므로 AST Parser에 들어가지 않는다.

player.tscn은 E041에서 시작한다. 실제 본문은 E042부터 E047까지다. E041은 파일 경계 표시이므로 Retriever 검색 본문이 아니다.

이 단계에서 UI는 “현재 player.gd의 E009-E039를 처리한다”처럼 보여줄 수 있다. 하지만 검색어는 아직 만들지 않는다.

2단계: 파일 종류별 처리 방향 정하기

player.gd는 GDScript 파일이다. 그래서 E009-E039 전체가 AST Parser로 들어간다.

project.godot은 설정 파일이다. AST Parser로 보내지 않고, 필요한 섹션 단위로 직접 조각낸다.

player.tscn은 scene 파일이다. AST Parser로 보내지 않고, node/resource 블록 단위로 직접 조각낸다.

README나 Markdown 설명 문서는 소스코드 분석 모드에서는 기본적으로 제외한다. 제외된 파일도 UI에는 보여줘야 한다. 이유는 “무엇이 빠졌는지”를 사람이 확인해야 하기 때문이다.

예를 들어 README.md가 있다면 분석에서 제외되었고, 제외 이유가 “소스코드 분석 모드에서는 문서 파일을 기본 제외”라고 보여야 한다. 단, 이 제외 결정은 분석 모드의 기본값일 뿐이고, 나중에 문서까지 읽히는 모드가 필요하면 별도로 다룬다.

3단계: .gd 파일을 AST Parser로 넘기기

player.gd에서 AST Parser로 들어가는 입력은 E009-E039다.

실제로 AST Parser가 받는 본문은 다음이다.

extends Area2D

signal hit

@export var speed = 400
var screen_size

func _ready():
	screen_size = get_viewport_rect().size
	hide()

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false

여기에는 # player.gd가 없다.

AST Parser는 이 본문을 위에서 아래로 읽고, 선언과 함수 단위로 자른다.

첫 번째 조각은 E009 한 줄이다.

extends Area2D

두 번째 조각은 E011 한 줄이다.

signal hit

세 번째 조각은 E013-E014다.

@export var speed = 400
var screen_size

네 번째 조각은 E016-E018이다.

func _ready():
	screen_size = get_viewport_rect().size
	hide()

다섯 번째 조각은 E020-E034이다.

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

여섯 번째 조각은 E036-E039이다.

func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false

중요한 점은 조각이 만들어진 뒤에도 원래 줄 범위를 잃지 않는 것이다. 예를 들어 _process 조각은 계속 “player.gd의 E020-E034에서 나온 조각”으로 추적된다.

4단계: 직접 조각 처리

AST Parser 대상이 아닌 텍스트 파일도 필요한 경우 조각으로 나눈다.

project.godot에서는 E004-E006이 application 설정 조각이 된다.

[application]
config/name="Mini Dodge"
run/main_scene="res://player.tscn"

이 조각은 run/main_scene="res://player.tscn" 때문에 프로젝트의 시작 scene을 찾는 데 쓸 수 있다. Retriever 검색을 한다면 본문 안의 run/main_scene, res://player.tscn, application 같은 내용이 검색 재료가 된다. project.godot이라는 파일 경로 자체가 검색어가 되는 것은 아니다.

player.tscn에서는 E044 한 줄이 script 참조 조각이 된다.

[ext_resource type="Script" path="res://player.gd" id="1"]

E046-E047은 node 조각이 된다.

[node name="Player" type="Area2D"]
script = ExtResource("1")

이런 직접 조각은 AST chunk는 아니지만 이후 흐름은 동일하다. 본문 일부를 가지고 Retriever에 검색하고, 검색된 후보를 LLM이 다시 검증한다.

5단계: Retriever에 실제로 들어가는 내용

Retriever에는 “파일 경로”가 아니라 “현재 조각의 본문과 본문에서 뽑힌 검색 단서”가 들어간다.

예를 들어 _process 조각을 처리할 때 검색 기준이 되는 본문은 E020-E034다.

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

이 본문에서 검색에 쓸 수 있는 단서는 다음처럼 나온다.

Input.is_action_pressed
Vector2.ZERO
velocity.normalized
position.clamp
AnimatedSprite2D
_process
Godot 3
Godot 4
migration

위 단서 중 Godot 3, Godot 4, migration은 사용자 프롬프트가 “이 프로젝트가 Godot 3인지 Godot 4인지 확인하고, 필요한 경우 변환 근거를 찾아줘” 같은 요청일 때 붙는다.

Retriever 검색은 이 조각 본문과 단서를 이용한다.

벡터 검색에는 _process 함수 본문 전체와 주요 심볼이 들어간다.

키워드 검색에는 Input.is_action_pressed, Vector2.ZERO, position.clamp 같은 심볼명이 들어간다.

파일 경로 player.gd는 검색어로 쓰지 않는다. 다만 나중에 “이 검색이 player.gd의 E020-E034에서 발생했다”라고 로그와 UI에 붙이는 데 사용한다.

6단계: Retriever가 무엇을 찾는지

Retriever는 같은 조각으로 docs_chunks, api_mapping, label_prototypes를 같은 방식으로 검색한다. 세 테이블 중 하나를 특별 취급하지 않는다.

docs_chunks에서는 Input.is_action_pressed, Vector2.ZERO, position, Area2D 같은 공식문서 설명 후보가 나올 수 있다.

api_mapping에서는 Godot 3에서 Godot 4로 바뀐 함수명, 클래스명, 속성명 후보가 나올 수 있다. 예를 들어 코드 조각에 KinematicBody2D, yield, Color8 같은 단서가 있으면 관련 migration 후보가 나올 수 있다.

label_prototypes에서는 “함수 하나의 사용 방식이 통째로 바뀌는 패턴”이나 “인자 구성과 호출 형태가 바뀌는 예시” 후보가 나올 수 있다. 단순히 이름이 바뀐 경우가 아니라 사용 방식 자체를 설명해야 하는 경우에 의미가 있다.

예를 들어 _process 조각에서는 다음 후보들이 돌아올 수 있다.

docs_chunks 후보:
  Input.is_action_pressed 공식문서 설명
  Vector2.ZERO 공식문서 설명
  Node2D.position 공식문서 설명

api_mapping 후보:
  이 조각에서는 특별한 Godot 3 -> 4 이름 변경 후보가 없을 수 있음

label_prototypes 후보:
  Input.is_action_pressed와 Vector2.ZERO를 이용한 Godot 4 이동 코드 예시

이 후보들은 정답이 아니다. “이 코드 조각과 관련 있을 가능성이 있는 근거”일 뿐이다.

7단계: Retriever 결과를 LLM에 넘기기

LLM에는 사용자 프롬프트, 현재 코드 조각, Retriever 후보가 같이 들어간다.

예를 들어 _process 조각에서는 다음처럼 들어간다.

사용자 요청:
이 프로젝트가 Godot 3인지 Godot 4인지 확인하고, 필요한 경우 변환 근거를 찾아줘.

현재 코드 조각:
player.gd의 E020-E034

func _process(delta):
	var velocity = Vector2.ZERO
	if Input.is_action_pressed(&"move_right"):
		velocity.x += 1
	if Input.is_action_pressed(&"move_left"):
		velocity.x -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

	position += velocity * delta
	position = position.clamp(Vector2.ZERO, screen_size)

Retriever 후보:
docs_chunks에서 Input.is_action_pressed 공식문서 후보
docs_chunks에서 Vector2.ZERO 공식문서 후보
docs_chunks에서 Node2D.position 공식문서 후보
label_prototypes에서 Godot 4 입력 처리 예시 후보

LLM은 여기서 두 가지를 판단한다.

첫째, 검색된 후보가 실제로 현재 코드 조각과 관련 있는지 확인한다.

둘째, 현재 코드 조각이 Godot 3 쪽 신호인지, Godot 4 쪽 신호인지, migration이 필요한지 판단한다.

이 예시에서는 Input.is_action_pressed, Vector2.ZERO, position.clamp가 Godot 4에서도 사용할 수 있는 코드 근거로 해석될 수 있다. 따라서 이 조각은 “Godot 4 코드로 볼 수 있음, migration 필요 없음” 쪽으로 누적될 가능성이 높다.

8단계: Godot 3 예시 조각의 흐름

다른 파일에 다음 조각이 있다고 가정한다.

G101 # enemy.gd
G102 extends KinematicBody2D
G103
G104 var velocity = Vector2.ZERO
G105
G106 func _physics_process(delta):
G107 	velocity = move_and_slide(velocity)

enemy.gd.gd 파일이므로 AST Parser에 G102-G107 본문이 들어간다.

첫 번째 조각은 G102다.

extends KinematicBody2D

두 번째 조각은 G106-G107이다.

func _physics_process(delta):
	velocity = move_and_slide(velocity)

extends KinematicBody2D 조각을 Retriever에 넘기면 검색 단서는 다음처럼 잡힌다.

KinematicBody2D
Godot 3
Godot 4
migration

이때 api_mapping에서는 KinematicBody2D가 Godot 4에서 CharacterBody2D 계열로 migration되는 후보가 나올 수 있다.

docs_chunks에서는 CharacterBody2D나 physics body 관련 공식문서 후보가 나올 수 있다.

label_prototypes에서는 KinematicBody2D 기반 이동 코드가 CharacterBody2Dvelocity, move_and_slide() 방식으로 바뀌는 예시 후보가 나올 수 있다.

LLM에는 다음처럼 들어간다.

사용자 요청:
이 프로젝트가 Godot 3인지 Godot 4인지 확인하고, 필요한 경우 변환 근거를 찾아줘.

현재 코드 조각:
enemy.gd의 G102

extends KinematicBody2D

Retriever 후보:
api_mapping에서 KinematicBody2D -> CharacterBody2D migration 후보
docs_chunks에서 CharacterBody2D 공식문서 후보
label_prototypes에서 KinematicBody2D 이동 코드 변환 예시 후보

LLM은 이 조각을 “Godot 3 신호가 강하고 migration 필요”로 판단할 수 있다.

이후 G106-G107 조각도 별도로 처리한다.

func _physics_process(delta):
	velocity = move_and_slide(velocity)

이 조각에서는 move_and_slide(velocity) 호출 방식이 검색 단서가 된다. 함수명만 보는 것이 아니라 인자가 들어가는 호출 형태까지 본다.

이때 단순 함수명 변경이면 api_mapping 후보가 중요하고, 호출 방식 전체가 바뀌는 경우에는 label_prototypes 후보도 같이 중요해진다.

9단계: 조각 결과 누적

각 조각은 하나씩 판단된다.

player.gd의 E020-E034는 Godot 4 쪽 신호로 누적될 수 있다.

enemy.gd의 G102는 Godot 3 쪽 신호로 누적될 수 있다.

enemy.gd의 G106-G107도 Godot 3 migration 신호로 누적될 수 있다.

프로젝트 전체 판단은 파일 이름으로 하지 않는다. 파일 경로는 추적용이다. 프로젝트 전체 판단은 각 코드 조각의 판단이 누적된 결과로 한다.

예를 들어 한 프로젝트 안에서 Godot 4 신호가 대부분인데 일부 Godot 3 migration 신호가 나오면 mixed에 가까운 판단이 될 수 있다. 반대로 Godot 3 migration 신호가 대부분이면 Godot 3 프로젝트로 분류될 수 있다.

중단과 재개

RunPod나 로컬 웹 서버가 중간에 멈출 수 있다. 그래서 처리는 파일 단위가 아니라 조각 단위로 이어갈 수 있어야 한다.

예를 들어 player.gd의 E009, E011, E013-E014까지 처리했고 E016-E018에서 멈췄다면, 재시작 후에는 E016-E018부터 다시 시작해야 한다.

이미 완료된 E009, E011, E013-E014를 다시 처리하면 안 된다.

웹 UI에서는 다음이 보여야 한다.

player.gd E009 처리 완료
player.gd E011 처리 완료
player.gd E013-E014 처리 완료
player.gd E016-E018 처리 중 중단
player.gd E020-E034 대기
player.gd E036-E039 대기

이렇게 보여야 “기존에 있던 처리가 이어지는지, 처음부터 다시 시작하는지”를 확인할 수 있다.

디버깅 화면에서 보여야 하는 것

디버깅 화면은 추적을 위해 필요하다.

최소한 다음이 보여야 한다.

현재 파일: player.gd
현재 줄 범위: E020-E034
현재 조각 본문:
func _process(delta):
	...

Retriever에 들어간 검색 단서:
Input.is_action_pressed
Vector2.ZERO
position.clamp

검색된 후보:
docs_chunks: Input.is_action_pressed
docs_chunks: Vector2.ZERO
label_prototypes: Godot 4 input polling example

LLM 판단:
Godot 4 신호
migration 필요 없음

여기서 중요한 것은 “무엇이 검색에 들어갔는지”와 “무엇이 단지 추적용으로 붙었는지”가 분리되어 보이는 것이다.

player.gd라는 경로는 현재 파일 표시에는 보여도 된다. 하지만 Retriever 검색 단서 목록에는 들어가면 안 된다.

정리

27일 기준 문서에서 고정하는 흐름은 다음이다.

# <상대경로>
<본문>

-> 헤더로 파일 경계만 나눈다.
-> .gd 파일의 본문은 AST Parser로 들어간다.
-> AST Parser가 함수와 선언을 순서대로 자른다.
-> 각 조각의 실제 본문과 심볼 단서가 Retriever 검색에 들어간다.
-> docs_chunks, api_mapping, label_prototypes를 같은 방식으로 검색한다.
-> 검색 후보와 현재 조각을 LLM에 같이 넘긴다.
-> LLM 판단을 조각 단위로 저장하고 프로젝트 단위로 누적한다.

이 문서의 목적은 새 구조를 정하는 것이 아니라, 실제 입력이 어느 줄에서 잘리고 어떤 본문이 다음 단계로 넘어가는지를 확인할 수 있게 만드는 것이다. 구현할 때도 이 기준대로 웹 UI에서 현재 파일, 현재 줄 범위, 실제 전달 본문, Retriever 검색 단서, 검색 후보, LLM 판단을 함께 보여줘야 한다.