Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
174 lines
8.0 KiB
Python
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
|