# 정의부(declaration) 수집 설계 — 2026-09-21 ## 왜 필요한가 이 인덱스의 최종 용도는 **비슷한 로직을 새로 짤 때 기존 코드를 찾아 복사해 오는 것**이다. 그런데 인덱스에 담긴 조각(logic_chunk)의 코드 원문은 **로직만**이다. ABAP 은 내부테이블·스트럭처· 상수·필드심볼을 TOP 인클루드나 FORM 머리에 따로 선언하므로, 로직만 복사해 붙여넣으면 `GT_LOAD 는 선언되지 않았습니다` 류의 문법 오류가 난다. 그래서 **정의부를 인덱싱 시점에 따로 수집**한다. 조각 코드에는 계속 로직만 둔다(검색 품질을 지키기 위해서다 — 선언 수백 줄이 조각에 섞이면 조각의 뜻이 흐려진다). 수집한 정의부를 **쓸지 말지는 판단하지 않는다.** 붙여넣는 쪽의 LLM 이 정한다: 이미 같은 선언이 있으면 버리고, 이름이 겹치면 바꾸고, 타입만 빌려 쓰기도 한다. 인덱스는 "이 조각을 가져가려면 이것들이 함께 필요하다"는 **재료와 분류**만 정확히 넘긴다. ## 무엇을 수집하는가 `parser/declarations.py` 가 선언 문장을 **붙여넣을 수 있는 한 문장 단위**로 잘라 `declaration` 테이블에 넣는다 (`index/db.py`). | 항목 | 내용 | |---|---| | kind | data / types / constants / tables / field-symbol / parameter / select-option / ranges / type-pool / controls / class / macro | | scope | `global`(TOP·공용 인클루드) / `unit`(FORM·METHOD 머리의 로컬 선언) | | code | **선언 원문** — 체인 항목이면 헤드를 다시 붙이고 `,` 를 `.` 로 닫아 유효한 문장으로 만든다 | | depends | 이 선언이 참조하는 다른 이름 (`LIKE GT_DATA` → GT_DATA, `TYPE TABLE OF TY_X` → TY_X) | 세 가지가 핵심이다. 1. **`BEGIN OF … END OF` 는 한 덩어리다.** 업무 프로그램의 내부테이블은 거의 전부 이 형태다 (`DATA : BEGIN OF GT_UPLOAD OCCURS 0, … END OF GT_UPLOAD.`). `parser/dataflow.py` 의 심볼 추출은 이걸 건너뛰지만(추적에는 이름만 필요하다), 정의부에는 반드시 통째로 있어야 한다. 2. **체인 항목도 혼자 설 수 있어야 한다.** `DATA: a TYPE i,\n b TYPE i.` 의 `b` 만 잘라내면 `b TYPE i.` 라 붙여넣을 수 없다 → `DATA:\n b TYPE i.` 로 헤드를 복원한다. 이를 위해 `parser/statements.py` 가 체인 전개 시 **항목별 줄 범위**를 기록한다. 3. **로컬 클래스·매크로도 정의부다.** `CLASS lcl_x DEFINITION … ENDCLASS`, `DEFINE … END-OF-DEFINITION` 은 unit 통째를 한 건으로 담는다. ## 조각 → 정의부 조립 (`index/decls.py`) 1. 조각 코드의 식별자를 모은다 (파서 토크나이저 · 문장 단위) 2. `declaration` 에서 찾는다 — **unit 로컬 먼저, 없으면 전역** 3. 찾은 선언의 `depends` 를 **재귀로** 끌어온다 (`LT_DATA LIKE GT_DATA` → GT_DATA) 4. 의존이 먼저 오도록(post-order) 정렬해 `declaration_code` 한 덩어리로 만든다 결과는 네 갈래로 분류해 넘긴다. | 필드 | 뜻 | 붙여넣는 쪽이 할 일 | |---|---|---| | `declarations` / `declaration_code` | 함께 가야 하는 선언 원문 | 있으면 버리거나, 이름을 바꿔 쓰거나, 그대로 넣는다 | | `declaration_external_refs` | DDIC 사전 객체 (BKPF, LVC_T_FCAT …) | 가져갈 게 없다. 대상 시스템에 있는지만 본다 | | `declaration_params` | 이 조각이 쓰는 FORM 파라미터 | 호출 측에서 넘어오는 값이다 | | `declaration_unresolved` | 변수처럼 생겼는데 선언을 못 찾은 이름 | 수집 안 된 인클루드·함수 인터페이스일 수 있다 | ### 판정에서 가른 두 가지 - **`LIKE BSEG-WRBTR` 은 작업영역이 아니다.** 사전에서 타입만 빌려 쓰는 것이라 TOP 의 `TABLES: BSEG.` 를 딸려 보내면 안 된다. 반면 `bseg-wrbtr = 100.` 처럼 **작업영역을 실제로 쓰면** 그 선언이 필요하다. 그래서 참조를 컴포넌트까지 보존해 둘을 구분한다. - **호출문의 형식 파라미터는 변수가 아니다.** `CALL FUNCTION 'X' EXPORTING i_bukrs = gv_bukrs` 의 `i_bukrs` 와 `EXCEPTIONS no_rate_found` 는 호출 대상의 인터페이스다. 이름만 보면 변수와 구분되지 않아(둘 다 `I_`/`NO_`) 문장 구조로 걸러낸다. ### 저장하지 않고 읽을 때 계산한다 조각↔선언 대응은 `logic_chunk` 에 캐시하지 않는다. `declaration` 과 조각 코드만 있으면 결정론적으로 다시 나오므로, 캐시하면 조각 재추출·선언 변경과 엇갈려 따로 늙기만 한다. 조각 하나당 수십 줄을 토큰화하는 비용이라 조회 경로에서 충분히 싸다. ## 실측 (샘플 55본 / 조각 3,212건, 2026-09-21) - 선언 18,049건 수집 (data 11.7k · constants 1.7k · field-symbol 1.0k · tables 1.0k · type-pool 488 · macro 295 · class 91 …) - 조각의 **79%** 가 정의부를 갖는다 (평균 3.5건, 최대 21건). 나머지 21%는 호출·제어·메시지처럼 프로그램 선언을 쓰지 않는 조각이다. - 선언을 못 찾은 이름이 남은 조각 **3%** — DDIC 테이블(J_1BBRANCH), 함수 인터페이스 파라미터, 화면 필드가 대부분이다. ### 곁가지로 고친 파서 버그 3건 정의부를 조립하면서 "선언이 없다"고 새어 나온 이름들을 좇아가 드러난 것들이다. 1. `CLASS lcl_x DEFINITION DEFERRED.` 를 블록으로 열어 **그 뒤 인클루드 전체를 CLASS_DEF 하나로 삼켰다** (ENDCLASS 가 없다). ZCO_ALV 의 선언 수십 건이 통째로 사라져 있었다. 2. FORM 시그니처의 파라미터를 **첫 개만** 잡았다 (`USING p_date LIKE x p_days LIKE y` 에서 TYPE/LIKE 를 만나면 멈췄다). 3. `PARAMETER`(단수형)를 선언 키워드로 몰랐다 — 미인식 문장 123건 → 3건. ## API | Method | Path | 정의부 관련 | |---|---|---| | GET | `/chunks/{chunk_id}` | `declarations`, `declaration_code`, `declaration_external_refs`, `declaration_params`, `declaration_unresolved` (끄려면 `?decls=false`) | | GET | `/programs/{name}/units/{unit}/code` | 같은 필드 (unit 통째로 가져갈 때) | | GET | `/programs/{name}/declarations?scope=global\|unit` | 프로그램의 선언 카탈로그 | ## 위키에서의 표시 원문은 프로그램 문서의 `## 정의부` 섹션에 **한 번만** 싣는다 (인클루드별, 전역 선언 전체 + 조각이 실제로 쓰는 unit 로컬 선언). 조각 쪽에는 이름만 두고 그 항목으로 가는 **문서 내 링크**를 건다. ```markdown - 정의부(복사 시 함께 필요): [`GT_LOAD`](#decl-zfic0020top-6-gt_load), [`C_X`](#decl-zficom-213-c_x) … #### GT_LOAD — data · L6-L56 {#decl-zfic0020top-6-gt_load} ``` `{#id}` 는 제목에 앵커를 다는 표기이고, 뷰어(`wiki_out/viewer.html`)가 이걸 `id` 로 바꿔 눌러서 이동하게 만든다(이동 후 1.4초 강조). **뷰어 전용 기능이 아니다** — 마크다운으로 읽으면 링크 텍스트가 곧 선언 이름이라 LLM 에게는 지금까지와 똑같이 읽히고, 앵커는 무시된다. 링크와 앵커가 어긋나면 조용히 죽으므로 짝을 테스트로 묶어 뒀다 (`tests/test_wiki_out.py::test_chunk_links_to_declaration`). 실측 11,051개 링크 / 깨진 링크 0개.