"""엔티티 위키 페이지 — tables/ functions/ tcodes/ (계획서 v4 §5.7, 수정사항 8번). `table_ref` / `call_edge` 에서 **LLM 없이 결정론적으로** 만든다. 에이전트가 "이 테이블을 쓰는 프로그램 전부", "이 FM 을 호출하는 곳 전부" 를 한 문서에서 보게 하는 게 목적이다. 프로그램당 파일이 아니라 **엔티티당 파일**이라 1만 본으로 늘어도 파일 수는 DDIC 테이블·FM 개수로 묶인다 (구조 변경으로 없어진 units/ 의 70만 파일 문제가 재발하지 않는다). `type` 값: abap-table / abap-function / abap-tcode. """ from __future__ import annotations import re import sqlite3 from pathlib import Path from config.settings import settings from parser.refs import FUNCTION_CALL_KINDS from .merge import merge_write from .okf_writer import PIPELINE, _fm, _lst, _now, _q MAX_ROWS = 300 # 한 문서에 싣는 사용처 상한 def _safe(name: str) -> str: return re.sub(r"[^0-9A-Za-z가-힣_.\-]", "_", name or "_") def _unit_name(unit_id: str | None) -> str: if not unit_id: return "" return unit_id.split("#")[3] if unit_id.count("#") >= 3 else unit_id # ------------------------------------------------------------------ tables/ def _render_table(con: sqlite3.Connection, name: str) -> str: rows = con.execute( "SELECT tr.program, tr.mode, tr.unit_id, u.name AS unit_name, u.unit_type, " "p.title_ko, p.devclass " "FROM table_ref tr LEFT JOIN unit u ON u.unit_id=tr.unit_id " "LEFT JOIN program p ON p.name=tr.program " "WHERE tr.table_name=? ORDER BY tr.mode, tr.program, u.name", (name,), ).fetchall() readers = sorted({r["program"] for r in rows if r["mode"] == "read"}) writers = sorted({r["program"] for r in rows if r["mode"] == "write"}) ddic = con.execute("SELECT text_ko FROM ddic_table WHERE name=?", (name,)).fetchone() text_ko = (ddic["text_ko"] if ddic else "") or "" desc = f"{name} — 읽기 {len(readers)}개 / 쓰기 {len(writers)}개 프로그램" fm = [ "type: abap-table", f"title: {_q(name + (' — ' + text_ko if text_ko else ''))}", f"description: {_q(text_ko or desc)}", f"resource: {_q('abap://table/' + name)}", f"tags: {_lst([t for t in ([text_ko] if text_ko else [])][:5])}", f"generated: {{ by: {_q(PIPELINE)}, at: {_q(_now())} }}", "status: stable", f"x-read-programs: {len(readers)}", f"x-write-programs: {len(writers)}", ] body: list[str] = [] if not text_ko: # DD02T 미확보 (ASSUMPTIONS.md §5) — 데이터가 들어오면 타이틀이 채워진다 body += ["> DDIC 테이블 텍스트 미확보 — 이름·사용처만 제공한다.", ""] body += [f"## 쓰기 ({len(writers)}개 프로그램)", ""] body += ([f"- [{p}](/programs/{p}.md)" for p in writers] or ["- (없음)"]) body += ["", f"## 읽기 ({len(readers)}개 프로그램)", ""] body += ([f"- [{p}](/programs/{p}.md)" for p in readers] or ["- (없음)"]) body += ["", "## 사용 지점 (unit 단위)", "", "| 프로그램 | unit | 유형 | 모드 |", "|---|---|---|---|"] for r in rows[:MAX_ROWS]: body.append(f"| [{r['program']}](/programs/{r['program']}.md) " f"| {r['unit_name'] or _unit_name(r['unit_id'])} | {r['unit_type'] or ''} | {r['mode']} |") if len(rows) > MAX_ROWS: body.append(f"| … | {len(rows) - MAX_ROWS}건 더 | | |") body.append("") return _fm(fm) + "\n" + "\n".join(body) # --------------------------------------------------------------- functions/ def _render_function(con: sqlite3.Connection, name: str) -> str: ph = ",".join("?" * len(FUNCTION_CALL_KINDS)) rows = con.execute( f"SELECT e.program, e.from_unit, e.call_type, e.line, u.name AS unit_name, u.unit_type, p.title_ko " f"FROM call_edge e LEFT JOIN unit u ON u.unit_id=e.from_unit " f"LEFT JOIN program p ON p.name=e.program " f"WHERE e.external_name=? AND e.call_type IN ({ph}) ORDER BY e.program, e.line", (name, *FUNCTION_CALL_KINDS), ).fetchall() callers = sorted({r["program"] for r in rows}) rfc = any(r["call_type"] == "call_function_rfc" for r in rows) fm = [ "type: abap-function", f"title: {_q(name)}", f"description: {_q(f'{name} — 호출 프로그램 {len(callers)}개' + (' (RFC 호출 포함)' if rfc else ''))}", f"resource: {_q('abap://function/' + name)}", f"tags: {_lst(['RFC'] if rfc else [])}", f"generated: {{ by: {_q(PIPELINE)}, at: {_q(_now())} }}", "status: stable", f"x-caller-programs: {len(callers)}", f"x-call-sites: {len(rows)}", ] body = ["> 이 펑션모듈 자체의 소스는 수집 대상이 아니다 (ASSUMPTIONS.md §4) — 호출처만 제공한다.", "", f"## 호출 프로그램 ({len(callers)}개)", ""] body += ([f"- [{p}](/programs/{p}.md)" for p in callers] or ["- (없음)"]) body += ["", "## 호출 지점", "", "| 프로그램 | unit | 종류 | 줄 |", "|---|---|---|---|"] for r in rows[:MAX_ROWS]: body.append(f"| [{r['program']}](/programs/{r['program']}.md) " f"| {r['unit_name'] or _unit_name(r['from_unit'])} | {r['call_type']} | {r['line']} |") if len(rows) > MAX_ROWS: body.append(f"| … | {len(rows) - MAX_ROWS}건 더 | | |") body.append("") return _fm(fm) + "\n" + "\n".join(body) # ------------------------------------------------------------------ tcodes/ def _render_tcode(con: sqlite3.Connection, code: str, program: str, text_ko: str) -> str: fm = [ "type: abap-tcode", f"title: {_q(code + (' — ' + text_ko if text_ko else ''))}", f"description: {_q(text_ko or code)}", f"resource: {_q('abap://tcode/' + code)}", "tags: []", f"generated: {{ by: {_q(PIPELINE)}, at: {_q(_now())} }}", "status: stable", f"x-program: {program or ''}", ] body = ["## 프로그램", "", f"- [{program}](/programs/{program}.md)" if program else "- (미확보)", ""] return _fm(fm) + "\n" + "\n".join(body) # ------------------------------------------------------------------ 진입점 def write_entity_wiki(con: sqlite3.Connection, wiki_dir: Path | None = None, limit: int | None = None) -> dict: """tables/ functions/ tcodes/ 를 (재)생성한다. 결정론적이라 프로그램 적재 후 언제든 호출 가능.""" wiki_dir = wiki_dir or settings.wiki_dir stats = {"tables": 0, "functions": 0, "tcodes": 0} names = [r["table_name"] for r in con.execute( "SELECT DISTINCT table_name FROM table_ref WHERE table_name IS NOT NULL AND table_name!='' " "ORDER BY table_name")] for name in names[:limit] if limit else names: rel = f"tables/{_safe(name)}.md" merge_write(wiki_dir / rel, _render_table(con, name), code_hash=None, wiki_dir=wiki_dir, doc_rel=rel) stats["tables"] += 1 ph = ",".join("?" * len(FUNCTION_CALL_KINDS)) fnames = [r["external_name"] for r in con.execute( f"SELECT DISTINCT external_name FROM call_edge WHERE external_name IS NOT NULL " f"AND external_name!='' AND call_type IN ({ph}) ORDER BY external_name", FUNCTION_CALL_KINDS)] for name in fnames[:limit] if limit else fnames: rel = f"functions/{_safe(name)}.md" merge_write(wiki_dir / rel, _render_function(con, name), code_hash=None, wiki_dir=wiki_dir, doc_rel=rel) stats["functions"] += 1 # TSTC 미확보 상태에서는 0건 — 데이터가 들어오면 그대로 생성된다 (ASSUMPTIONS.md §5) for r in con.execute("SELECT tcode, program, text_ko FROM tcode ORDER BY tcode"): rel = f"tcodes/{_safe(r['tcode'])}.md" merge_write(wiki_dir / rel, _render_tcode(con, r["tcode"], r["program"] or "", r["text_ko"] or ""), code_hash=None, wiki_dir=wiki_dir, doc_rel=rel) stats["tcodes"] += 1 return stats