"""式样书解析器 — 读取详细设计书 .md,提取 ProgramMeta 信息。 从外部 agent(jcl-cobol-data-create)移植,适配 V3 接口。 """ import re from dataclasses import dataclass, field from pathlib import Path from typing import Optional @dataclass class FileInfo: no: int = 0 file_db_name: str = "" identifier: str = "" dd_name: str = "" io: str = "" copy_group: str = "" record_format: str = "" record_len: int = 0 medium: str = "" remarks: str = "" @dataclass class KeyInfo: no: int = 0 file_name: str = "" sort_condition: str = "" key_condition: str = "" @dataclass class ModuleInfo: no: int = 0 function: str = "" program_id: str = "" copy_name: str = "" @dataclass class CopyField: level: int = 0 name: str = "" raw_name: str = "" pic_type: str = "" pic_bytes: int = 0 @dataclass class TableColumn: no: int = 0 name_jp: str = "" name_en: str = "" type: str = "" max_len: str = "" decimal_digits: str = "" byte_count: str = "" nullable: bool = True is_pk: bool = False @dataclass class TableInfo: table_name: str = "" db_id: str = "" copy_id: str = "" columns: list = field(default_factory=list) pk_columns: list = field(default_factory=list) @dataclass class ProgramMeta: program_id: str = "" program_name: str = "" system_name: str = "" pgm_type: str = "" pgm_pattern: str = "" summary_lines: list = field(default_factory=list) prerequisites: list = field(default_factory=list) files: list = field(default_factory=list) keys: list = field(default_factory=list) modules: list = field(default_factory=list) process_detail: str = "" output_records: str = "" input_type: str = "file" copy_fields: dict = field(default_factory=dict) db_tables: dict = field(default_factory=dict) def _extract_section(text: str, section_name: str) -> str: """Extract a section by name from markdown text (case-insensitive).""" lines = text.split("\n") result = [] in_section = False section_pattern = re.compile( rf"^#+\s*{re.escape(section_name)}\s*$", re.IGNORECASE ) next_section = re.compile(r"^#+\s", re.IGNORECASE) for line in lines: if section_pattern.match(line): in_section = True continue if in_section and next_section.match(line): break if in_section: result.append(line) return "\n".join(result).strip() def _parse_table_rows(text: str) -> list[dict]: """Parse a markdown table into list of dicts.""" lines = [l.strip() for l in text.split("\n") if l.strip()] if not lines: return [] header_line = None sep_line = None data_start = 0 for i, line in enumerate(lines): if line.startswith("|") and not header_line: header_line = line elif line.startswith("|") and header_line and not sep_line: if set(line.strip("|").replace("-", "").replace(" ", "").replace("|", "")) == set(): sep_line = line data_start = i + 1 break if not header_line: return [] headers = [h.strip() for h in header_line.strip("|").split("|")] rows = [] for line in lines[data_start:]: if not line.startswith("|"): continue cells = [c.strip() for c in line.strip("|").split("|")] row = {} for i, h in enumerate(headers): if i < len(cells): row[h] = cells[i] else: row[h] = "" rows.append(row) return rows class DesignDataInputParser: """解析式样书 .md,返回 ProgramMeta。""" def __init__(self): pass def parse(self, design_md_text: str, source_text: str) -> ProgramMeta: """解析式样书文本和源码文本,返回 ProgramMeta。""" meta = ProgramMeta() self._design_text = design_md_text self._source_text = source_text self._parse_basic_info(meta) self._parse_use_files(meta) self._parse_keys(meta) self._parse_modules(meta) self._parse_process_detail(meta) self._parse_output_records(meta) self._determine_input_type(meta) return meta def _parse_basic_info(self, meta: ProgramMeta): rows = _parse_table_rows(_extract_section(self._design_text, "基本情報")) for row in rows: item = row.get("項目", "") value = row.get("内容", "") if item == "システム名": meta.system_name = value elif item == "プログラムID": meta.program_id = value elif item == "プログラム名": meta.program_name = value elif item == "PGMタイプ": meta.pgm_type = value elif item == "PGMパターン": meta.pgm_pattern = value elif item == "機能概要": meta.summary_lines.append(value) def _parse_use_files(self, meta: ProgramMeta): rows = _parse_table_rows(_extract_section(self._design_text, "使用ファイル一覧")) for row in rows: try: no_str = row.get("NO", "0") no = int(no_str) if no_str and no_str.strip() not in ("", "—") else 0 rec_len_str = row.get("レコード長", "0") rec_len = ( int(rec_len_str) if rec_len_str and rec_len_str.strip() not in ("", "—") else 0 ) f = FileInfo( no=no, file_db_name=row.get("使用ファイル/DB名", ""), identifier=row.get("識別子", ""), dd_name=row.get("DD名", ""), io=row.get("I/O", ""), copy_group=row.get("COPY群", ""), record_format=row.get("形式", ""), record_len=rec_len, medium=row.get("媒体", ""), remarks=row.get("備考", ""), ) meta.files.append(f) except (ValueError, KeyError): continue def _parse_keys(self, meta: ProgramMeta): rows = _parse_table_rows(_extract_section(self._design_text, "キー項目一覧")) for row in rows: try: k = KeyInfo( no=int(row.get("NO", "0")), file_name=row.get("ファイル名", ""), sort_condition=row.get("ソート条件(キー項目)", ""), key_condition=row.get("キー条件(マッチング/キーブレイク)", ""), ) meta.keys.append(k) except (ValueError, KeyError): continue def _parse_modules(self, meta: ProgramMeta): rows = _parse_table_rows( _extract_section(self._design_text, "使用モジュール一覧") ) for row in rows: try: m = ModuleInfo( no=int(row.get("NO", "0")), function=row.get("機能", ""), program_id=row.get("プログラムID", ""), copy_name=row.get("使用COPY名", ""), ) meta.modules.append(m) except (ValueError, KeyError): continue def _parse_process_detail(self, meta: ProgramMeta): meta.process_detail = _extract_section(self._design_text, "処理詳細") def _parse_output_records(self, meta: ProgramMeta): meta.output_records = _extract_section(self._design_text, "出力レコード定義") def _determine_input_type(self, meta: ProgramMeta): input_mediums = set() for f in meta.files: if "I" in f.io: input_mediums.add(f.medium) if not input_mediums: meta.input_type = "file" elif input_mediums == {"PS"}: meta.input_type = "file" elif input_mediums == {"DB"}: meta.input_type = "db" else: meta.input_type = "mixed"