# 수정사항 10건 검토·적용 기록 (2026-09-16) 원본: `수정사항.txt`. 이 파일은 **2026-09-16 구조 변경(로직 조각 도입) 이전** 기준으로 작성돼 일부 항목은 이미 해소된 상태였다. 항목별 판정과 적용 내용을 남긴다. | # | 항목 | 판정 | 적용 | |---|---|---|---| | 1 | LLM 요약이 검색 색인에 미반영 | 부분 유효 | ✅ 적용 | | 2 | 프로그램 단위 요약 미생성 | **이미 완료** | 코드 변경 없음 | | 3 | 텍스트 심볼 치환 | 프롬프트 완료 / FTS 미반영 | ✅ FTS 반영 | | 4 | 용어 사전이 질의 확장에 미사용 | 유효 | ✅ 적용 | | 5 | keywords_en · sap_objects 필드 없음 | **이미 완료** | 코드 변경 없음 | | 6 | 중복 프로그램 처리 없음 | 유효 | ✅ 적용 | | 7 | 정답셋 4문항 | 유효 | ⏸ **범위 제외** (사용자 지시) | | 8 | 엔티티·프로세스 위키 페이지 없음 | 유효 | ✅ 엔티티 적용 / 프로세스 보류 | | 9 | unit 문서 70만 파일 문제 | **구조 변경으로 해소** | 무효 | | 10 | 요약 병렬 실행 없음 | 유효 | ✅ 적용 | ## 이미 완료였던 항목 (2·5·9) - **2번** — `summarize/runner.py:ensure_program_summary()` 가 `ProgramSummary` 를 만들어 `program.summary_json` 에 저장하고, `UNIT_PROMPT` 에 `program_purpose` · `main_flow` · `key_internal_tables` 를 문맥으로 주입한다. DB 에 요약이 0건이었던 것은 미구현이 아니라 OpenRouter 무료 티어의 **429 Too Many Requests** 로 실행이 실패해서였다 (`unit.summary_error` 6건 전부 429). - **5번** — `schemas.LogicChunk` 에 `purpose_en` · `keywords_ko` · `keywords_en` · `sap_objects` 가 모두 있고 프롬프트가 명시적으로 요구한다. - **9번** — `units/` 문서 생성은 중단됐다. 로직 조각은 프로그램 문서 안의 H3/H4 섹션이다. 원본 파일의 "units/ 를 git 제외로 둔 것은 좋은 완화책" 서술은 현재 상태와 맞지 않는다 (`.gitignore` 에 `units` 항목이 없고 디렉토리도 없다). ## 적용 내용 ### 1번 — 요약을 검색 색인에 반영 구조 변경으로 `chunk_fts` 가 신설돼 조각의 `purpose`/`keywords`/`objects` 는 이미 색인되고 있었고, `extract_unit` 도 요약 후 `unit_fts` 를 갱신하고 있었다. 남아 있던 두 구멍만 막았다. - **적재 시점 `unit_fts.purpose` 의 장식 문자** — `index/loader.py` 가 `header_comment` 를 날것으로 넣어 1,177행 중 **867행이 `'----------------'`** 이었다. `clean_comment()` 를 적용해 0행으로. - `clean_comment` 를 `query/tools.py` → `index/db.py` 로 이동 (loader 가 써야 하는데 `index → query` 는 의존 역방향). `query.tools.clean_comment` 이름은 하위호환으로 유지. - **`program_fts` 가 타이틀·패키지명뿐** — `business_purpose_ko` / `main_flow` / `keywords_ko` / `keywords_en` / `business_tags` / `sap_module` / `related_tcodes` / 조각 키워드 / 조각 SAP 객체 / 테이블명 / 텍스트 심볼을 색인에 넣었다. - **색인을 두 테이블로 나눴다**: `program_fts`(이름·타이틀·패키지) + `program_desc_fts`(서술). 한 행에 몰아넣었더니 회귀가 났다 — bm25 는 **행 전체 길이로 정규화**하므로 요약이 붙어 길어진 행이 타이틀만 있는 짧은 행보다 낮게 나온다. 실측으로 타이틀이 정확히 `"총계정원장 조회"`인 ZFIR10070 이 `"총계정원장 조회하는 프로그램"` 질의에서 **1위 → 33위**로 밀렸다 (행 길이 1,872자 vs 40자). 컬럼 가중치로는 해결되지 않는다(정규화는 행 단위). 나누면 각 테이블 안 행 길이가 비슷해지고, 서술 히트는 `_DESC_WEIGHT=0.6` 으로 **더해질 뿐** 이름·타이틀 일치를 밀어내지 않는다. 회귀 테스트: `tests/test_dedupe_parallel.py::test_long_summary_does_not_outrank_title_match` - 세 FTS 모두 `rank`(모든 컬럼 가중치 1) 대신 **가중 bm25**(`index/db.py:bm25_rank`)를 쓴다. - FTS5 는 컬럼 추가가 불가하므로 `index/db.py:_migrate_fts()` 가 컬럼 구성 변화를 감지해 DROP→재생성한다 (전부 파생 데이터라 안전). - `refresh_program_fts(con, program)` 단건 경로를 추가했다. 전량 재구축을 `/ingest` 마다 돌리면 1만 본에서 O(N²) 가 된다. - 검색 결과의 `reason` 이 근거를 구분한다: `이름/타이틀 일치` > `키워드 일치` > `요약 일치` > `동의어 확장`. ### 3번 — 텍스트 심볼을 FTS 에 반영 - `index/db.py:text_symbol_phrases()` — 코드 범위에 등장하는 `TEXT-nnn` 을 한국어 원문으로 치환. - 조각 저장 시 `chunk_fts.keywords` / `bigrams` 에, 프로그램 색인에는 `program_fts.purpose` 에 넣는다. - 결과: `"계정코드를 입력하세요"`, `"회계단위 간편선택"` 같은 **화면 문구로도** 검색된다 (LLM 무관). ### 4번 — 용어 사전 질의 확장 - `config/glossary.py` — 사전 로딩을 한 곳으로 모았다. 기존에는 `wiki_out/run.py` 안에 인라인 정규식으로만 있어 검색 쪽에서 쓸 수 없었다. **양방향 동의어 맵**을 만든다 (`입고`→`GR/MSEG/101` 뿐 아니라 `GR`→`입고/MSEG` 도 성립). - `query/expand.py` — **2단 검색**. 원질의로 먼저 찾고, 결과가 `top_k` 미만일 때만 동의어 식으로 보충하며 점수에 `EXPANDED_WEIGHT=0.35` 감쇠를 걸고 `matched_by='동의어 확장'` 으로 표시한다. - 동의어를 원질의와 같은 OR 버킷에 섞지 않은 이유: `fts_or` 는 토큰과 한글 2-gram 을 전부 OR 로 이어 이미 재현율 편향이다. 여기에 동의어까지 같은 가중치로 넣으면 `"총계정원장"` 질의가 `"원장"` 2-gram 하나로 걸린 문서와 동일 점수가 된다. - `config/domain_glossary.yaml` 에 MM 섹션(입고·출고·자재·구매오더·이동유형 …)을 시드했다. 기존 사전이 FI 전용이라 원본 파일이 예로 든 "입고 → GR/101" 이 실제로는 동작하지 않았다. ### 6번 — 중복 unit 조각 복제 - `summarize/runner.py:_dedupe_plan()` + `clone_unit_chunks()`. - `code_hash` 가 같은 unit 은 **대표 1건만 LLM 에 보내고** 나머지는 조각을 복제한다. 이번 배치 밖에서 이미 추출된 동일 코드 unit 이 있으면 **LLM 호출 0회**로 복제한다. - **줄 번호 평행이동이 핵심이다** — 코드는 같아도 include 안 위치가 달라 `target.line_start - rep.line_start` 만큼 밀어야 한다. 테스트가 복제된 줄 범위의 코드 원문이 대표와 동일한지 확인한다. - 실측(샘플 14본): 중복 그룹 108개 / unit 287개 → **LLM 호출 179회(62%) 절감 가능**. - `--no-dedupe` 로 끌 수 있다. ### 8번 — 엔티티 위키 페이지 - `wiki_out/entities.py` — `tables/` `functions/` `tcodes/` 를 `table_ref` · `call_edge` 에서 **LLM 없이 결정론적으로** 생성. 실측 산출: 테이블 146건, 펑션 94건. - `tables/.md` — 읽는 프로그램 / 쓰는 프로그램 / unit 단위 사용 지점 - `functions/.md` — 호출 프로그램 / 호출 지점 / RFC 여부 - `tcodes/` — `tcode` 테이블 미확보로 현재 0건 (데이터가 들어오면 그대로 생성됨) - 프로그램당 파일이 아니라 **엔티티당 파일**이라 9번의 파일 수 폭발이 재발하지 않는다. - `index.md` 를 계층 진입점으로 재작성 — **SAP 모듈 → 패키지 → 프로그램**. 모듈은 요약의 `sap_module`, 요약 전에는 `Z<모듈><번호>` 관행에서 추정한다. - **프로세스 페이지는 보류.** 프로그램을 가로지르는 reduce 단계가 필요하고, 조각 데이터가 실제로 쌓인 뒤에 판단하는 게 맞다 (원본 파일도 "별도 작업"으로 인정). ### 10번 — 병렬 실행 - `llm_concurrency` 가 **선언만 있고 읽는 코드가 없는 죽은 설정**이었다. 실제로 연결했다. - sqlite 커넥션은 스레드 간 공유가 안 되므로 3단으로 갈랐다: `build_unit_prompts()` (DB 읽기, 메인) → `_run_calls_parallel()` (LLM 호출, 워커) → `finish_unit()` (검증·저장, 메인). - **동시성만 올리면 429 가 늘어난다.** `OpenAICompatClient.complete_json` 에 429/5xx 지수 백오프 재시도(`Retry-After` 존중, jitter 포함)를 같이 넣었다. 기존에는 재시도가 없어 429 한 번에 unit 이 `failed` 로 굳었다. ## 함께 고친 것 (직전 분석에서 보고한 버그) ### 파서 토큰화 — 필드심볼·테이블본문 대입 `-fieldname = 'X'` 가 `['', '-', 'fieldname', '=', ...]` 로 쪼개져 일반 대입 판정(`up[1] == "="`)이 깨졌다. 결과로 **쓰기 지점이 누락**되고 미인식 문장으로 잡혔다. - `parser/tokenizer.py` — 필드심볼이 `-컴포넌트` / `->메서드` 까지 한 토큰. `&1` 매크로 파라미터도 토큰화. - `parser/statements.py:assignment_eq_index()` — 대입 판정을 한 곳으로. `X = `, `X[] = `, `X[ key = v ] = ` 형태를 모두 인식한다. `dataflow` 와 `run`(미인식 리포트)이 같은 판정을 쓴다. - 효과 (샘플 14본 / 문장 24,578건. 베이스라인 커밋 `35a15aa` 를 worktree 로 꺼내 직접 측정): | 지표 | 이전 | 이후 | 차이 | |---|---|---|---| | 미인식 문장 | 1,055건 (4.29%) | **30건 (0.12%)** | −1,025 | | 최악 프로그램 | ZMMR71110_API **12.03%** | ZBACKUP_FI **0.46%** | | | 쓰기 지점 총계 | 7,327 | **8,251** | **+924** | | └ 필드심볼 대상 | 207 | **1,067** | **+860** | | └ `itab[]` 형태 | 230 | **307** | **+77** | `itab[] = ...` 와 `-comp = ...` 는 내부테이블을 채우는 흔한 관용구라 `trace_variable`("gt_head 가 어디서 채워지나")의 정확도에 직접 영향이 있었다. 이전에도 필드심볼 쓰기가 207건 잡혔던 것은 `READ TABLE ... ASSIGNING ` / `LOOP AT ... ASSIGNING ` 경로는 별도 분기라 영향을 받지 않았기 때문이다. ### 외부 호출 목록 오염 `CL_GUI_ALV_GRID=>MC_FC_AUF` 같은 **정적 상수 읽기**가 `method_ref` 로 기록돼 호출로 취급됐다. `query/tools.py:_structure_summary` 는 걸러냈지만 `wiki_out/okf_writer.py` 는 걸르지 않아 `x-calls` 40건이 거의 전부 ALV 상수와 `CALL SCREEN` 의 화면번호(`"100"`)였다. - `parser/refs.py:EXTERNAL_CALL_KINDS` / `FUNCTION_CALL_KINDS` 로 기준을 한 곳에 두고 요약·위키·엔티티 페이지가 공유한다. - ZFIR10070 의 `x-calls`: ALV 상수 40건 → **실제 FM·외부 PERFORM·SUBMIT 14건**. ### 설치 명령이 동작하지 않았음 README 가 안내하는 `pip install -e ".[dev]"` 가 실패했다 — flat-layout 자동 탐색이 `data/` `wiki/` 까지 패키지로 오인한다. `pyproject.toml` 에 `[tool.setuptools] packages` 를 명시했다. ### 로더 강제 재적재 파서·색인 로직이 바뀌면 소스가 그대로라도 산출물이 달라지는데, `source_hash` 가 같으면 스킵돼 반영할 방법이 없었다. `python -m index.loader --force` 를 추가했다. ## 7번 (정답셋)을 제외한 결과 1·4번의 효과를 숫자로 확인할 수단이 없다. 확인한 것은 **기능이 동작한다는 사실**뿐이다: - `ACDOCT` / `FAGLL03` 로 질의 → 총계정원장 프로그램이 `동의어 확장` 으로 나온다 - `"회계 담당자가 월 단위로 확인"`(요약 본문) → 프로그램이 나온다 - `"계정코드를 입력하세요"`(텍스트 심볼) → 프로그램이 나온다 정밀도가 좋아졌는지, 아래 계수들이 적절한지, 2단 검색이 베이스라인보다 나은지는 **측정하지 않았다**. 현 정답셋 5문항으로는 개선도 회귀도 보이지 않는다. 정답셋 50~100문항이 생긴 뒤에 튜닝할 값: | 값 | 위치 | 현재 | 뜻 | |---|---|---|---| | `EXPANDED_WEIGHT` | `query/expand.py` | 0.35 | 동의어 히트 점수 감쇠 | | `MAX_SYNONYMS` | `query/expand.py` | 24 | 질의당 동의어 상한 | | `_DESC_WEIGHT` | `query/tools.py` | 0.6 | 요약·키워드 색인 히트 가중 | | `_BM25_WEIGHTS` | `index/db.py` | 표 참조 | FTS 컬럼별 bm25 가중치 | | 2단 검색 보충 조건 | `query/tools.py:_fts_rows` | `len(결과) < limit` | 동의어 단계 발동 시점 | **이번 회귀가 이 과제의 필요성을 그대로 보여준다.** `find` recall@5 가 1.0 → 0.833 으로 떨어진 것을 5문항 정답셋이 잡아냈지만, 원인 파악은 수동 디버깅으로 했다. 문항이 50개면 어떤 유형의 질의가 깨졌는지가 바로 드러난다. 위 5개 값을 감으로 정한 상태가 현재의 가장 큰 미확정 요소다.