Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""엔티티 위키 페이지 — 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
|
||||
Reference in New Issue
Block a user