Files
ABAP-Indexing/wiki_out/entities.py
T

174 lines
8.0 KiB
Python

"""엔티티 위키 페이지 — 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