Files
cobol-java-v3/docs/superpowers/specs/2026-07-12-design-data-generator.md
T

11 KiB
Raw Blame History

DesignDataGenerator — 式样书驱动测试数据生成

1. 概要

目的

V3 现有两条数据生成路径:

  • 白盒数据cobol_testgen.generate_data()):基于 MC/DC 路径枚举,覆盖决策点
  • LLM 测试数据Agent2Data.design()):基于 FieldTree + DeepSeek API

缺少一条能读取日文详细设计书(.md)并生成机能测试数据的路径。

DesignDataGenerator 填补这个空缺。它解析式样书中的业务规则、输入输出定义、DB 结构,通过 LLM 生成有业务意义的测试数据。与白盒数据合并后,覆盖可以同时命中决策点分支和业务场景路径。

与其他 Agent 的关系

Agent 输入 输出 位置
Agent1Parser COPYBOOK FieldTree agents/agent1_parser.py
DesignDataGenerator 式样书 .md + 源码 + COPYBOOK list[dict](机能数据) agents/design_data.py ← 新增
Agent2Data FieldTree TestSuiteLLM 数据) agents/agent2_data.py
Agent3Diagnostic FieldResult 诊断文本 agents/agent3_diagnostic.py

2. 接口定义

DesignDataGenerator

class DesignDataGenerator:
    def __init__(
        self,
        llm_client: LLMClient,
        cpy_dirs: list[str | Path],
        rules_dir: str | Path = "rules",
    ):
        ...

    def generate(
        self,
        design_md_text: str,                    # 式样书 .md 全文
        source_text: str,                        # COBOL 源码全文
        file_db_md_text: str | None = None,      # COPY句定义书 .md(可选)
        db_md_text: str | None = None,            # DB 定义书 .md(可选)
        replacing_rules: dict[str, str] | None = None,   # REPLACING 展开规则
        v3_field_names: list[str] | None = None,          # V3 字段名参考列表
    ) -> list[dict]:
        """生成机能测试数据。

        Returns:
            list[dict]: 每条记录为 {field_name: value} 格式。
                        字段名已被 REPLACING 展开,与 generate_data() 兼容。
        """
        ...

调用侧入口

def generate_all_data(
    program_id: str,
    src_text: str,
    copybook_dirs: list[str | Path],
    st: dict | None = None,           # extract_structure 结果(传入可跳过重复解析)
    config: Config | None = None,     # 用于读取 LLM / 设计书路径配置
    design_doc_dir: str | Path | None = None,  # 式样书目录
    file_db_md_path: str | Path | None = None, # COPY句定义书路径
    db_md_path: str | Path | None = None,      # DB定义书路径
    merge_strategy: str = "merge_to_normal",   # merge_to_normal / as_separate_scenes / auto
) -> list[dict]:
    """白盒数据 + 机能数据 + 策略补充 全量生成与合并。

    合并策略:
      - merge_to_normal: 全部合到一条记录集中
      - as_separate_scenes: 返回 (main_records, func_records),由调用方决定如何分场景
      - auto: DEPRECATED
    """
    ...

3. 架构与数据流

[式样书 .md] ──┐
[源码 .cbl] ───┤
[COPYBOOK] ────┤
[DB定义书 .md] ┘
        │
        ▼
  DesignDataGenerator.generate()
        │
        ├── InputParser    解析式样书 → ProgramMeta
        ├── RuleLoader     模式匹配 → 规则文本
        ├── PromptBuilder  LLM 提示词
        ├── LLMClient      DeepSeek API
        ├── 字段名映射     REPLACING 展开 + V3 字段名参考
        └── 返回 list[dict]
        │
        ▼
  generate_all_data()
    ├── ① generate_data()      → 白盒数据
    ├── ② DesignDataGenerator  → 机能数据(式样书存在时)
    ├── ③ strategy_supplement() → 策略补充
    ├── ④ 去重(基于 hash)
    └── ⑤ 返回 merged_records

4. 字段名兼容性处理

问题

外部 Agent 从 COPYBOOK 解析字段名,含有未展开的 (A) 占位符:

(A)EMP-ID → V3 是 R01EMP-IDREPLACING ==(A)== BY ==R01== 后)
(A)DATE   → V3 是 R01DATE

映射算法

def _resolve_field_names(
    records: list[dict],
    replacing_rules: dict[str, str] | None,
    v3_field_names: set[str] | None,
) -> list[dict]:
    """将外部 Agent 输出的字段名映射为 V3 兼容名称。"""
    if not (replacing_rules or v3_field_names):
        return records  # 无需映射

    result = []
    for rec in records:
        mapped = {}
        for key, val in rec.items():
            new_key = key

            # 第一步:REPLACING 展开
            if replacing_rules:
                for old, new in replacing_rules.items():
                    if old in new_key:
                        new_key = new_key.replace(old, new)

            # 第二步:如果 V3 字段名参考列表存在,精确匹配
            if v3_field_names and new_key not in v3_field_names:
                # 尝试去掉连字符
                no_hyphen = new_key.replace("-", "")
                if no_hyphen in v3_field_names:
                    new_key = no_hyphen
                # 尝试下划线转空
                no_underscore = new_key.replace("_", "")
                if no_underscore in v3_field_names:
                    new_key = no_underscore
                else:
                    continue  # 无法映射 → 丢弃

            mapped[new_key] = val
        result.append(mapped)
    return result

REPLACING 规则提取

从 COBOL 源码中提取 REPLACING 规则:

def _extract_replacing_rules(source_text: str) -> dict[str, str]:
    """从 COPY ... REPLACING ... 语句中提取替换规则。"""
    rules = {}
    for m in re.finditer(
        r'COPY\s+(\w+)\s+REPLACING\s+(?:==(\w+)==\s+BY\s+==(\w+)==\s*)*\.',
        source_text, re.IGNORECASE
    ):
        for i in range(0, len(m.groups()) - 1, 2):
            old = m.group(i + 2)
            new = m.group(i + 3)
            if old and new:
                rules[old] = new
    return rules

5. 去重策略

问题

白盒数据和机能数据可能包含重复记录(完全相同或高度相似)。重复会导致:

  • COBOL 程序处理重复
  • INSERT PK 冲突
  • gcov 行计数失真

解决方案

def _dedup(
    main_records: list[dict],
    additional_records: list[dict],
    key_fields: list[str] | None = None,
) -> list[dict]:
    """合并+去重,additional 优先保留。"""
    seen = set()
    result = []

    # 先处理 additional(机能数据优先)
    for rec in additional_records:
        h = _hash(rec, key_fields)
        if h not in seen:
            seen.add(h)
            result.append(rec)

    # 后处理 main
    for rec in main_records:
        h = _hash(rec, key_fields)
        if h not in seen:
            seen.add(h)
            result.append(rec)

    return result


def _hash(rec: dict, key_fields: list[str] | None) -> tuple:
    """生成记录的特征哈希。"""
    if key_fields:
        return tuple(rec.get(k, '') for k in key_fields)
    return tuple(sorted(rec.items()))

6. 多轮融合策略

策略选项

# 在 config/programs/{pid}.yaml 或 CLI 参数中控制
merge_strategy: merge_to_normal  # 默认值
策略 行为 适用场景
merge_to_normal 机能数据合并到 normal 场景,与白盒数据一起执行 简单程序,一轮覆盖所有
as_separate_scenes 机能数据作为独立场景 run_func/ 执行 复杂程序,业务场景与白盒路径不兼容
auto 有设计书时用 as_separate_scenes,否则 merge_to_normal 通用场景

as_separate_scenes 时的目录

runtime/{pid}/
  run_normal/                   ← 白盒数据(MC/DC 路径)
    input/ json/ output/
  run_func/                     ← 机能数据(式样书驱动)
    input/ json/ output/
  gcov/run_normal/              ← 各场景独立 .gcda
  gcov/run_func/

7. 调用链路

DB 管道(orchestrator_db.py:step2_generate_inputs()

# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)

# 改为
recs = generate_all_data(
    program_id=self.program_id,
    src_text=src_text,
    copybook_dirs=cbd,
    st=st,
    config=self.config,
    design_doc_dir=str(v3_root / "详细设计书"),
    file_db_md_path=str(v3_root / "详细设计书" / "COPY句定义书.md"),
)

原生管道(cobol_testgen/__init__.py:main()

# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)

# 改为
recs = generate_all_data(
    program_id=filepath.stem,
    src_text=source,
    copybook_dirs=[str(filepath.parent / '..' / 'cpy')],
    st=st,
    design_doc_dir=str(design_doc_base),
)

8. 外部 Agent 代码迁移清单

jcl-cobol-data-create/ 迁移

源文件 目标 变更
input_parser.py agents/design_data_input_parser.py 接收文本而非路径
rule_loader.py 内置到 design_data.py 方法 简化接口
prompt_builder.py 内置到 design_data.py 使用 LLMClient
api_client.py 不需要 V3 已有 agents/llm.py
output_writer.py 不需要 不写文件
models.py 部分并入 design_data.py 按需移植数据类
rules/pgm_pattern/*.md rules/pgm_pattern/ 直接复制
rules/special_feature/*.md rules/special_feature/ 直接复制
layout/*.md layout/ 参考用

新建文件清单

文件 说明
agents/design_data.py DesignDataGenerator 主类
agents/design_data_input_parser.py 式样书解析器(从外部 Agent 移植)
cobol_testgen/data_merger.py generate_all_data() + 去重 + 字段名映射

9. 错误处理

场景

场景 行为
式样书 .md 不存在 跳过机能数据,仅返回白盒数据
LLM 调用超时 捕获异常,跳过机能数据,日志告警
LLM 返回非法 JSON try/except,返回空列表
字段名映射失败(无法匹配 V3 字段名) 丢弃该字段,日志记录
式样书存在但无法解析(格式不标准) try/except,跳过机能数据

10. 测试计划

测试 方法
式样书不存在时回退到仅白盒 mock os.path.exists → False
LLM 超时/出错时回退 mock LLMClient.call → raise TimeoutError
字段名 REPLACING 映射 Fixture 含 (A)EMP-ID → 期待 R01EMP-ID
去重逻辑 白盒 10 条 + 机能 5 条(含 2 条重复)→ 期待 13 条
多轮融合策略 as_separate_scenes 验证 run_func/ 目录被创建
实际 COBOL 执行 + gcov KIN08DBU 白盒+机能合并后覆盖率是否 >= 原 52/60