From 4b8d214260c1a4e86c84c2966bd6870d960e82a3 Mon Sep 17 00:00:00 2001 From: zhang_taoming <854317252@qq.com> Date: Sun, 23 Aug 2026 10:46:56 +0800 Subject: [PATCH] =?UTF-8?q?=E9=BB=91=E7=9B=92=E6=95=B0=E6=8D=AE=E5=81=9A?= =?UTF-8?q?=E6=88=90=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 + black-box-data-create/.gitignore | 3 + black-box-data-create/AGENTS.md | 59 + black-box-data-create/README.md | 261 ++ black-box-data-create/agent/__init__.py | 57 + black-box-data-create/agent/api_client.py | 132 + black-box-data-create/agent/input_parser.py | 330 +++ black-box-data-create/agent/markdown_utils.py | 63 + black-box-data-create/agent/models.py | 89 + black-box-data-create/agent/output_writer.py | 59 + black-box-data-create/agent/prompt_builder.py | 146 ++ black-box-data-create/agent/rule_loader.py | 226 ++ .../plans/2026-07-12-testdata-agent-plan.md | 2310 +++++++++++++++++ .../specs/2026-07-12-testdata-agent-design.md | 319 +++ .../docs/未実装PGMパターン洗い出し_第一版.md | 184 ++ black-box-data-create/layout/DB定義書.md | 347 +++ .../layout/JSON格式说明v2.0.md | 435 ++++ .../layout/sample_ソース_SAN01MAT.cbl | 432 +++ .../layout/sample_詳細設計書_SAN01MAT.md | 141 + .../layout/ファイル定義書_xxxxxxx.md | 142 + .../layout/詳細設計書_XXXXXXXX.md | 145 ++ black-box-data-create/main.py | 67 + .../pgm_pattern/1-N+キーブレイク(異キー).md | 57 + .../rules/pgm_pattern/100分割.md | 34 + .../rules/pgm_pattern/25分割.md | 34 + .../rules/pgm_pattern/50分割.md | 34 + .../rules/pgm_pattern/ASCII→EBCDIC変換.md | 58 + .../rules/pgm_pattern/CSV→FB変換.md | 58 + .../rules/pgm_pattern/DB更新.md | 88 + .../rules/pgm_pattern/GETPUT(編集出力).md | 28 + .../rules/pgm_pattern/MERGE.md | 77 + .../rules/pgm_pattern/SELECT処理.md | 50 + .../rules/pgm_pattern/SORT.md | 64 + .../rules/pgm_pattern/SYSIN読込.md | 57 + .../rules/pgm_pattern/オンラインPGM.md | 53 + .../rules/pgm_pattern/キーブレイク(集約).md | 102 + .../rules/pgm_pattern/キーブレイク(集計).md | 102 + .../rules/pgm_pattern/キーブレイク(非集計).md | 73 + .../rules/pgm_pattern/サブプログラム.md | 60 + .../rules/pgm_pattern/マッチング(1-1).md | 172 ++ .../rules/pgm_pattern/マッチング(1-N).md | 195 ++ .../rules/pgm_pattern/マッチング(M-N).md | 102 + .../rules/pgm_pattern/マッチング(N-1).md | 188 ++ .../rules/pgm_pattern/ランキング生成.md | 57 + .../pgm_pattern/レイアウト編集のみ(GETPUT).md | 32 + .../rules/pgm_pattern/内部テーブル検索.md | 64 + .../rules/pgm_pattern/振り分け(EVALUATE).md | 68 + .../rules/pgm_pattern/振り分け(IF).md | 73 + .../pgm_pattern/項目チェック(半角20桁-4桁).md | 55 + .../pgm_pattern/項目チェック(重複含まず).md | 82 + .../2段階マッチング(1:1⇒1:1).md | 62 + .../2段階マッチング(M:N⇒M:N).md | 62 + .../2段階マッチング(N:1⇒N:1).md | 63 + .../rules/special_feature/条件分支.md | 27 + black-box-data-create/tests/__init__.py | 0 black-box-data-create/tests/conftest.py | 10 + .../tests/test_api_client.py | 68 + .../tests/test_copy_parser.py | 87 + .../tests/test_data/DB定義書.md | 347 +++ .../tests/test_data/ZAN01REC.cpy | 11 + .../tests/test_data/ZAN02REC.cpy | 12 + .../tests/test_data/ZAN04REC.cpy | 5 + .../tests/test_data/cpy/ZAN01REC.cpy | 11 + .../tests/test_data/cpy/ZAN04REC.cpy | 11 + .../tests/test_data/詳細設計書_ZAN04MAT.md | 144 + black-box-data-create/tests/test_db_parser.py | 53 + .../tests/test_input_parser.py | 149 ++ .../tests/test_integration.py | 77 + .../tests/test_markdown_utils.py | 65 + black-box-data-create/tests/test_models.py | 59 + .../tests/test_output_writer.py | 83 + .../tests/test_prompt_builder.py | 96 + .../tests/test_rule_loader.py | 278 ++ requirements.txt | 1 + run.py | 51 + 75 files changed, 9900 insertions(+) create mode 100644 black-box-data-create/.gitignore create mode 100644 black-box-data-create/AGENTS.md create mode 100644 black-box-data-create/README.md create mode 100644 black-box-data-create/agent/__init__.py create mode 100644 black-box-data-create/agent/api_client.py create mode 100644 black-box-data-create/agent/input_parser.py create mode 100644 black-box-data-create/agent/markdown_utils.py create mode 100644 black-box-data-create/agent/models.py create mode 100644 black-box-data-create/agent/output_writer.py create mode 100644 black-box-data-create/agent/prompt_builder.py create mode 100644 black-box-data-create/agent/rule_loader.py create mode 100644 black-box-data-create/docs/superpowers/plans/2026-07-12-testdata-agent-plan.md create mode 100644 black-box-data-create/docs/superpowers/specs/2026-07-12-testdata-agent-design.md create mode 100644 black-box-data-create/docs/未実装PGMパターン洗い出し_第一版.md create mode 100644 black-box-data-create/layout/DB定義書.md create mode 100644 black-box-data-create/layout/JSON格式说明v2.0.md create mode 100644 black-box-data-create/layout/sample_ソース_SAN01MAT.cbl create mode 100644 black-box-data-create/layout/sample_詳細設計書_SAN01MAT.md create mode 100644 black-box-data-create/layout/ファイル定義書_xxxxxxx.md create mode 100644 black-box-data-create/layout/詳細設計書_XXXXXXXX.md create mode 100644 black-box-data-create/main.py create mode 100644 black-box-data-create/rules/pgm_pattern/1-N+キーブレイク(異キー).md create mode 100644 black-box-data-create/rules/pgm_pattern/100分割.md create mode 100644 black-box-data-create/rules/pgm_pattern/25分割.md create mode 100644 black-box-data-create/rules/pgm_pattern/50分割.md create mode 100644 black-box-data-create/rules/pgm_pattern/ASCII→EBCDIC変換.md create mode 100644 black-box-data-create/rules/pgm_pattern/CSV→FB変換.md create mode 100644 black-box-data-create/rules/pgm_pattern/DB更新.md create mode 100644 black-box-data-create/rules/pgm_pattern/GETPUT(編集出力).md create mode 100644 black-box-data-create/rules/pgm_pattern/MERGE.md create mode 100644 black-box-data-create/rules/pgm_pattern/SELECT処理.md create mode 100644 black-box-data-create/rules/pgm_pattern/SORT.md create mode 100644 black-box-data-create/rules/pgm_pattern/SYSIN読込.md create mode 100644 black-box-data-create/rules/pgm_pattern/オンラインPGM.md create mode 100644 black-box-data-create/rules/pgm_pattern/キーブレイク(集約).md create mode 100644 black-box-data-create/rules/pgm_pattern/キーブレイク(集計).md create mode 100644 black-box-data-create/rules/pgm_pattern/キーブレイク(非集計).md create mode 100644 black-box-data-create/rules/pgm_pattern/サブプログラム.md create mode 100644 black-box-data-create/rules/pgm_pattern/マッチング(1-1).md create mode 100644 black-box-data-create/rules/pgm_pattern/マッチング(1-N).md create mode 100644 black-box-data-create/rules/pgm_pattern/マッチング(M-N).md create mode 100644 black-box-data-create/rules/pgm_pattern/マッチング(N-1).md create mode 100644 black-box-data-create/rules/pgm_pattern/ランキング生成.md create mode 100644 black-box-data-create/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md create mode 100644 black-box-data-create/rules/pgm_pattern/内部テーブル検索.md create mode 100644 black-box-data-create/rules/pgm_pattern/振り分け(EVALUATE).md create mode 100644 black-box-data-create/rules/pgm_pattern/振り分け(IF).md create mode 100644 black-box-data-create/rules/pgm_pattern/項目チェック(半角20桁-4桁).md create mode 100644 black-box-data-create/rules/pgm_pattern/項目チェック(重複含まず).md create mode 100644 black-box-data-create/rules/pgm_pattern/2段階マッチング(1:1⇒1:1).md create mode 100644 black-box-data-create/rules/pgm_pattern/2段階マッチング(M:N⇒M:N).md create mode 100644 black-box-data-create/rules/pgm_pattern/2段階マッチング(N:1⇒N:1).md create mode 100644 black-box-data-create/rules/special_feature/条件分支.md create mode 100644 black-box-data-create/tests/__init__.py create mode 100644 black-box-data-create/tests/conftest.py create mode 100644 black-box-data-create/tests/test_api_client.py create mode 100644 black-box-data-create/tests/test_copy_parser.py create mode 100644 black-box-data-create/tests/test_data/DB定義書.md create mode 100644 black-box-data-create/tests/test_data/ZAN01REC.cpy create mode 100644 black-box-data-create/tests/test_data/ZAN02REC.cpy create mode 100644 black-box-data-create/tests/test_data/ZAN04REC.cpy create mode 100644 black-box-data-create/tests/test_data/cpy/ZAN01REC.cpy create mode 100644 black-box-data-create/tests/test_data/cpy/ZAN04REC.cpy create mode 100644 black-box-data-create/tests/test_data/詳細設計書_ZAN04MAT.md create mode 100644 black-box-data-create/tests/test_db_parser.py create mode 100644 black-box-data-create/tests/test_input_parser.py create mode 100644 black-box-data-create/tests/test_integration.py create mode 100644 black-box-data-create/tests/test_markdown_utils.py create mode 100644 black-box-data-create/tests/test_models.py create mode 100644 black-box-data-create/tests/test_output_writer.py create mode 100644 black-box-data-create/tests/test_prompt_builder.py create mode 100644 black-box-data-create/tests/test_rule_loader.py create mode 100644 run.py diff --git a/README.md b/README.md index f607f65..9d54f42 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ python -m cobol_testgen --gcov runtime/ # 诊断脚本 python diagnose_db2.py # DB 全流程 python diagnose_kind8dbrun.py # DB 编译运行 + +# 黑盒数据生成 +python black-box-data-create/main.py --design "D:\xxxx\詳細設計書_xxxx.md" --source "D:\xxxx\xxxx.cbl" --file-db-md "D:\xxxx\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\xxxx\DB定義書.md" --output "D:\xxxx\output" + ``` ## 依赖 diff --git a/black-box-data-create/.gitignore b/black-box-data-create/.gitignore new file mode 100644 index 0000000..f849cca --- /dev/null +++ b/black-box-data-create/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +output/ diff --git a/black-box-data-create/AGENTS.md b/black-box-data-create/AGENTS.md new file mode 100644 index 0000000..e0b2463 --- /dev/null +++ b/black-box-data-create/AGENTS.md @@ -0,0 +1,59 @@ +# AGENTS.md + +## 项目 + +COBOL テストデータ生成 Agent。詳細設計書・COPYBOOK・ソースコードを解析し、DeepSeek API でテストデータ JSON/SQL を生成する。 + +- 言語: Python 3.9+ +- 依存: `requests>=2.28.0` +- テスト: `pytest` +- 入力元: `D:\cobol-tna-system` (COBOL プロジェクト) + +## コマンド + +```bash +# テスト実行 +python -m pytest tests/ -v + +# 単一プログラム実行 +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_XXX.md" \ + --source "D:\cobol-tna-system\src\XXX.cbl" \ + --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" \ + --cpy "D:\cobol-tna-system\cpy" \ + --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" \ + --output "output" +``` + +## アーキテクチャ + +パイプライン: `InputParser → RuleLoader → PromptBuilder → APIClient → OutputWriter` + +| モジュール | 役割 | +|-----------|------| +| `agent/models.py` | 全データクラス定義 | +| `agent/markdown_utils.py` | Markdown テーブル解析 | +| `agent/input_parser.py` | 設計書 + COPYBOOK + DB定義 解析 | +| `agent/rule_loader.py` | PGMパターン→ルール.md マッチング | +| `agent/prompt_builder.py` | API プロンプト組立 | +| `agent/api_client.py` | DeepSeek API + 3回リトライ | +| `agent/output_writer.py` | JSON/SQL ファイル出力 | +| `main.py` | CLI エントリポイント | + +## ルール追加方法 + +1. `rules/pgm_pattern/` に `.md` ファイルを追加 +2. `agent/rule_loader.py` の `PGM_PATTERN_MAP` にエントリ追加 + +ルールマッチング: まず `PGM_PATTERN_MAP` の完全一致 → 見つからなければルールファイル名(.md除く)が PGMパターン文字列に含まれるかチェック。半角/全角括弧違いに注意(`()` vs `()`)。 + +## よくある落とし穴 + +- **`—` (全角ダッシュ) のパース**: 設計書テーブルの `レコード長` 列などで `—` が使われると `int()` が失敗する。`input_parser.py` では `try/except` で 0 にフォールバック。 +- **同一COPYBOOKの複数REPLACING**: `ZAN01REC` が `R01`/`R02` 両方で使われる場合、`_extract_copy_replacing()` はリスト `[(copy_name, prefix), ...]` を返し、`_parse_copybooks()` が識別子でマッチする。 +- **PGMタイプ = サブ はスキップ**: `generate()` 関数で `meta.pgm_type == 'サブ'` の場合 `ValueError`。 +- **入力タイプ判定**: `使用ファイル一覧` の `I/O` 列に `'I'` を含む行の `媒体` 列を見る。`PS`=ファイル、`DB`=DB、両方=混合。 +- **APIキー**: コード内にハードコード (`sk-6156cccdc9c14d949cf5bfc5afc67a03`)。モデルは `deepseek-v4-flash`。 + +## 多keyテスト + +キーが複数項目のプログラムでは、ルールに「多keyテスト」が含まれる。基準データの各キー項目を1つだけ変えたN件の追加レコードを生成し、キー比較が正しいことを検証する。DB更新・マッチング・キーブレイク等で必須。 diff --git a/black-box-data-create/README.md b/black-box-data-create/README.md new file mode 100644 index 0000000..73cf1d9 --- /dev/null +++ b/black-box-data-create/README.md @@ -0,0 +1,261 @@ +# COBOL テストデータ生成 Agent + +COBOL プログラムの詳細設計書・COPYBOOK・ソースコードを解析し、DeepSeek v4 Flash API を使ってテストデータ(JSON/SQL)を自動生成する Python ツール。 + +## クイックスタート + +```bash +# インストール +pip install -r requirements.txt + +# テスト実行 +python -m pytest tests/ -v + +# 単一プログラムのテストデータ生成 +python main.py \ + --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN04MAT.md" \ + --source "D:\cobol-tna-system\src\ZAN04MAT.cbl" \ + --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" \ + --cpy "D:\cobol-tna-system\cpy" \ + --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" \ + --output "output" +``` + +| 引数 | 必須 | 説明 | +|------|------|------| +| `--design` | ✓ | 詳細設計書 .md のパス | +| `--source` | ✓ | COBOL ソース .cbl のパス | +| `--file-db-md` | ✓ | ファイル/DB 構造定義 .md のパス | +| `--cpy` | ✓ | COPYBOOK 格納ディレクトリ | +| `--db-md` | ✓ | DB 定義書 .md のパス | +| `--output` | | 出力ディレクトリ(デフォルト: `output`) | +| `--api-key` | | DeepSeek API Key | +| `--model` | | モデル名(デフォルト: `deepseek-v4-flash`) | +| `--rules` | | ルール格納ディレクトリ(デフォルト: `rules`) | + +### ライブラリとしての利用 + +```python +from agent import generate + +result = generate( + design_md="詳細設計書_ZAN04MAT.md", + source_cbl="src/ZAN04MAT.cbl", + file_db_md="COPY句定義書.md", + cpy_dir="cpy/", + db_md="DB定義書.md", + output_dir="output/" +) + +# result = {"output_files": {...}, "program_id": "ZAN04MAT", "groups": 5, "input_type": "file"} +``` + +## プロジェクト構造 + +``` +├── agent/ # コアモジュール +│ ├── __init__.py # generate() エントリポイント + 全エクスポート +│ ├── models.py # データクラス定義(ProgramMeta, FileInfo, CopyField 等) +│ ├── markdown_utils.py # Markdown テーブル解析ユーティリティ +│ ├── input_parser.py # 設計書・COPYBOOK・DB定義の解析 +│ ├── rule_loader.py # PGMパターン→ルールファイルのマッチング +│ ├── prompt_builder.py # DeepSeek API プロンプト組立 +│ ├── api_client.py # API 呼出 + 3回リトライ + JSON パース +│ └── output_writer.py # JSON / SQL ファイル出力 +├── rules/ # データ生成ルール(.md ファイル) +│ ├── pgm_pattern/ # PGMパターン別ルール +│ │ ├── マッチング(1-1).md +│ │ ├── マッチング(1-N).md +│ │ ├── マッチング(N-1).md +│ │ ├── DB更新.md +│ │ ├── 振り分け(IF).md +│ │ ├── 振り分け(EVALUATE).md +│ │ ├── 項目チェック(重複含まず).md +│ │ ├── 項目チェック(半角20桁-4桁).md +│ │ ├── レイアウト編集のみ(GETPUT).md +│ │ ├── キーブレイク(集計).md +│ │ ├── キーブレイク(集約).md +│ │ ├── SELECT処理.md +│ │ ├── MERGE.md +│ │ ├── 50分割.md +│ │ ├── CSV→FB変換.md +│ │ └── GETPUT(編集出力).md +│ └── special_feature/ # 特殊機能ルール +│ └── 条件分支.md +├── layout/ # ドキュメントテンプレート(参考) +├── tests/ # テストコード (48 tests) +│ └── test_data/ # テストフィクスチャ +├── main.py # CLI エントリポイント +├── requirements.txt # 依存関係 +├── AGENTS.md # AI エージェント向け指示書 +└── README.md +``` + +## 処理フロー + +``` +[詳細設計書.md] ──┐ +[ソース.cbl] ────┤ +[COPYBOOK .cpy] ─┤ +[DB定義書.md] ───┘ + │ + ▼ + InputParser ──→ ProgramMeta(全メタデータ) + │ + ▼ + RuleLoader ───→ ルール.md + 条件分支.md(該当時) + │ + ▼ + PromptBuilder ─→ API プロンプト + │ + ▼ + APIClient ────→ DeepSeek API(最大3回リトライ) + │ + ▼ + OutputWriter ──→ output/{プログラムID}/g{N}/{プログラムID}_g{N}.json + output/{プログラムID}/g{N}/{プログラムID}_g{N}.sql +``` + +## 出力形式 + +### 入力元がファイルの場合(JSON) + +```json +{ + "program": "ZAN04MAT", + "records": [ + { + "input": { + "ZAN04R01": { + "R01-APPL-ID": "A0000001", + "R01-EMP-ID": "00000001", + "R01-APPL-DATE": "20250101", + "R01-START-TIME": "0800", + "R01-END-TIME": "1700", + "R01-STATUS": "1", + "R01-OVT-TYPE": "O", + "R01-FILLER": " " + }, + "ZAN04R02": { + "R02-APPL-ID": "A0000002", + ... + } + } + } + ] +} +``` + +### 入力元が DB の場合(SQL) + +```sql +-- Group: 1 +-- 用途: 正常INSERT + 多keyテスト +INSERT INTO LEAVE_RECORDS (EMP_ID, LEAVE_TYPE, START_DATE, ...) VALUES +('00000001', '01', '20260101', ...), +('00000002', '02', '20260102', ...); +``` + +### 出力ディレクトリ構成 + +``` +output/ +└── ZAN04MAT/ + ├── g1/ + │ └── ZAN04MAT_g1.json + ├── g2/ + │ └── ZAN04MAT_g2.json + └── g3/ + └── ZAN04MAT_g3.json +``` + +## 対応 PGM パターン + +| パターン | ルールファイル | カバー数 | 多keyテスト | +|----------|---------------|----------|-------------| +| マッチング(1:1) | マッチング(1-1).md | 8 | ✓ | +| マッチング(1:N) | マッチング(1-N).md | 9 | ✓ | +| マッチング(N:1) | マッチング(N-1).md | 9 | ✓ | +| マッチング(M:N) | マッチング(M-N).md | 4 | ✓ | +| DB更新 | DB更新.md | 4 | ✓ | +| 振り分け(IF) | 振り分け(IF).md | 5 | — | +| 振り分け(EVALUATE) | 振り分け(EVALUATE).md | 5 | — | +| 項目チェック(重複含まず) | 項目チェック(重複含まず).md | 5 | ✓ | +| 項目チェック(半角20桁/4桁) | 項目チェック(半角20桁-4桁).md | 4 | ✓ | +| GETPUT | レイアウト編集のみ(GETPUT).md | 1 | — | +| キーブレイク(集計) | キーブレイク(集計).md | 7 | ✓ | +| キーブレイク(集約) | キーブレイク(集約).md | 7 | ✓ | +| SELECT処理 | SELECT処理.md | 3 | ✓ | +| MERGE | MERGE.md | 4 | ✓ | +| 50分割 | 50分割.md | 5 | — | +| CSV→FB変換 | CSV→FB変換.md | 4 | — | + +**合計: 24 プログラム対応** + +### 多keyテストとは + +キーが複数項目(例: A001, A002, A003)で構成される場合、基準データの各キー項目を 1 つだけ変えた N 件の追加データを各グループ末尾に生成する。これにより COBOL プログラムが正しい全キー項目で比較していることを検証する。 + +## ルール追加方法 + +1. `rules/pgm_pattern/` に新しい `.md` ファイルを作成 +2. `agent/rule_loader.py` の `PGM_PATTERN_MAP` にエントリを追加 +3. コード変更不要、ルール .md ファイル追加のみで拡張可能 + +ルールマッチング: まず `PGM_PATTERN_MAP` 完全一致 → なければルールファイル名(.md 除く)が PGM パターン文字列内に含まれるかチェック。 + +## 全プログラム実行コマンド + +```bash +# マッチング(1:1) +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN04MAT.md" --source "D:\cobol-tna-system\src\ZAN04MAT.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# マッチング(1:N) +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN05MAT.md" --source "D:\cobol-tna-system\src\KIN05MAT.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN07DAI.md" --source "D:\cobol-tna-system\src\KIN07DAI.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# マッチング(N:1) +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN03CHK.md" --source "D:\cobol-tna-system\src\ZAN03CHK.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# DB更新 +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN02UPD.md" --source "D:\cobol-tna-system\src\KIN02UPD.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN08DBU.md" --source "D:\cobol-tna-system\src\KIN08DBU.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU02REG.md" --source "D:\cobol-tna-system\src\KYU02REG.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU06UPD.md" --source "D:\cobol-tna-system\src\KYU06UPD.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN06UPD.md" --source "D:\cobol-tna-system\src\ZAN06UPD.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# 振り分け +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN01INP.md" --source "D:\cobol-tna-system\src\KIN01INP.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN01CHK.md" --source "D:\cobol-tna-system\src\ZAN01CHK.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# 項目チェック +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN02CHK.md" --source "D:\cobol-tna-system\src\ZAN02CHK.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN04CHK.md" --source "D:\cobol-tna-system\src\KIN04CHK.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# GETPUT +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN06CLD.md" --source "D:\cobol-tna-system\src\KIN06CLD.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU08PRI.md" --source "D:\cobol-tna-system\src\KYU08PRI.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN09CSV.md" --source "D:\cobol-tna-system\src\KIN09CSV.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# キーブレイク(集計) +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KIN03EXP.md" --source "D:\cobol-tna-system\src\KIN03EXP.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN05CAL.md" --source "D:\cobol-tna-system\src\ZAN05CAL.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# キーブレイク(集約) +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU03AGG.md" --source "D:\cobol-tna-system\src\KYU03AGG.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU04CAL.md" --source "D:\cobol-tna-system\src\KYU04CAL.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" + +# その他 +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU05DED.md" --source "D:\cobol-tna-system\src\KYU05DED.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU07DIV.md" --source "D:\cobol-tna-system\src\KYU07DIV.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU09MRG.md" --source "D:\cobol-tna-system\src\KYU09MRG.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +python main.py --design "D:\cobol-tna-system\詳細設計書\詳細設計書_KYU01CVT.md" --source "D:\cobol-tna-system\src\KYU01CVT.cbl" --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" --cpy "D:\cobol-tna-system\cpy" --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" --output "output" +``` + +## 注意事項 + +- **API Key**: DeepSeek API キーは `agent/__init__.py` にハードコードされています。本番利用時は環境変数などに変更してください。 +- **サブプログラム**: `PGMタイプ = サブ` のプログラムは処理対象外です。 +- **全角/半角**: 詳細設計書内の PGMパターン表記には全角括弧 `()` と半角括弧 `()` のバリエーションがあります。`rule_loader.py` で両方対応済みです。 +- **テスト**: 全 48 テストが `tests/test_data/` のフィクスチャを使って API を mock して動作します。 +- **所要時間**: 24 プログラム全実行で約 26 分(DeepSeek API 応答速度に依存)。 diff --git a/black-box-data-create/agent/__init__.py b/black-box-data-create/agent/__init__.py new file mode 100644 index 0000000..25c2104 --- /dev/null +++ b/black-box-data-create/agent/__init__.py @@ -0,0 +1,57 @@ +__version__ = "0.1.0" + +from agent.input_parser import InputParser +from agent.rule_loader import RuleLoader +from agent.prompt_builder import PromptBuilder +from agent.api_client import APIClient +from agent.output_writer import OutputWriter +from agent.models import ProgramMeta, FileInfo, CopyField, KeyInfo, TableColumn, TableInfo + + +def generate(design_md: str, source_cbl: str, file_db_md: str, + cpy_dir: str, db_md: str, output_dir: str = "output", + api_key: str = "sk-6156cccdc9c14d949cf5bfc5afc67a03", + api_model: str = "deepseek-v4-flash", + rules_dir: str = "rules", + max_tokens: int = 32768) -> dict: + """生成测试数据的主入口函数。""" + import os + + print(f"== 解析入力: {design_md}") + + parser = InputParser(design_md, source_cbl, file_db_md, cpy_dir, db_md) + meta = parser.run() + + if meta.pgm_type == 'サブ': + raise ValueError(f"程序 {meta.program_id} はサブプログラムです。主プログラムのみ処理対象です。") + + print(f" プログラムID: {meta.program_id}, パターン: {meta.pgm_pattern}, 入力タイプ: {meta.input_type}") + + # If rules_dir is relative, resolve from this file's location or cwd + if not os.path.isabs(rules_dir): + rules_dir = os.path.join(os.path.dirname(__file__), '..', rules_dir) + + loader = RuleLoader(rules_dir) + rules_text, group_descriptions, group_count = loader.load(meta) + + print(f" ルール読み込み完了, グループ数: {group_count}") + + builder = PromptBuilder() + prompt = builder.build(meta, rules_text, group_descriptions, group_count) + + client = APIClient(api_key=api_key, model=api_model, max_tokens=max_tokens) + print(f" API呼び出し中...") + result = client.generate(prompt) + print(f" API応答受信") + + writer = OutputWriter(output_dir) + output_files = writer.write(meta.program_id, result, meta.input_type) + + print(f" 出力完了: {len(output_files)} ファイル") + + return { + "output_files": output_files, + "program_id": meta.program_id, + "groups": group_count, + "input_type": meta.input_type, + } diff --git a/black-box-data-create/agent/api_client.py b/black-box-data-create/agent/api_client.py new file mode 100644 index 0000000..7b9697b --- /dev/null +++ b/black-box-data-create/agent/api_client.py @@ -0,0 +1,132 @@ +import json +import time +from typing import Dict, Any, Optional + +import requests + + +class APIClient: + """DeepSeek API 客户端,含重试逻辑。""" + + def __init__(self, api_key: str, model: str = 'deepseek-v4-flash', + base_url: str = 'https://api.deepseek.com/chat/completions', + max_retries: int = 3, timeout: int = 120, + max_tokens: int = 32768): + self.api_key = api_key + self.model = model + self.base_url = base_url + self.max_retries = max_retries + self.timeout = timeout + self.max_tokens = max_tokens + + def generate(self, prompt: str) -> Dict[str, Any]: + """发送 prompt 并返回 AI 生成的结果。""" + system_prompt = ( + "你是COBOL程序的测试数据生成专家。" + "请严格按照提供的规则,生成符合格式要求的测试数据。" + "输出必须是可被json.loads()直接解析的JSON,不要包裹在```json```代码块中。" + "不要在JSON前后添加任何说明文字。" + ) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ] + + last_error = None + + for attempt in range(1, self.max_retries + 1): + try: + response = self._call_api(messages) + choice = response['choices'][0] + content = choice['message']['content'] + + if choice.get('finish_reason') == 'length': + # 输出被截断:追加"压缩输出"指示后重试,避免再次浪费全部 max_tokens + last_error = RuntimeError( + "API输出被截断(finish_reason=length),已追加压缩指示重试。" + ) + error_msg = ( + "前回の出力は長すぎて途中で切れました(finish_reason=length)。" + "レコード数・フィールド数を最小限にし、FILLERなどの冗長な項目は省略して、" + "必ず完結したJSONのみを出力してください。コードブロック(```)で囲まないでください。" + ) + messages.append({"role": "assistant", "content": content}) + messages.append({"role": "user", "content": error_msg}) + continue + + data = self._parse_json(content) + if data is not None: + return data + + last_error = RuntimeError("API返回内容无法解析为JSON") + error_msg = ( + f"前回の出力は有効なJSONではありませんでした。" + f"必ず有効なJSONのみを出力してください。" + f"コードブロック(```)で囲まないでください。" + ) + messages.append({"role": "assistant", "content": content}) + messages.append({"role": "user", "content": error_msg}) + + except requests.exceptions.RequestException as e: + last_error = e + if attempt < self.max_retries: + time.sleep(2 ** attempt) + continue + + raise RuntimeError( + f"API调用失败,已重试{self.max_retries}次。" + f"最后错误: {last_error}" + ) + + def _call_api(self, messages: list) -> dict: + """单次 API 调用。""" + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', + } + payload = { + 'model': self.model, + # deepseek-v4 默认开启思考模式,会把 max_tokens 几乎全部消耗在 + # reasoning_content 上,导致 content 为空/截断。测试数据生成是 + # 结构化 JSON 输出,关闭思考模式后全部 token 用于正文。 + 'thinking': {'type': 'disabled'}, + 'messages': messages, + 'temperature': 0.3, + 'max_tokens': self.max_tokens, + } + + resp = requests.post( + self.base_url, + headers=headers, + json=payload, + timeout=self.timeout, + proxies={"http": None, "https": None}, + ) + resp.raise_for_status() + return resp.json() + + @staticmethod + def _parse_json(text: str) -> Optional[dict]: + """尝试从文本中提取 JSON。""" + text = text.strip() + + if text.startswith('```json'): + text = text[7:] + if text.startswith('```'): + text = text[3:] + if text.endswith('```'): + text = text[:-3] + text = text.strip() + + try: + return json.loads(text) + except json.JSONDecodeError: + start = text.find('{') + end = text.rfind('}') + if start >= 0 and end > start: + try: + return json.loads(text[start:end + 1]) + except json.JSONDecodeError: + pass + return None diff --git a/black-box-data-create/agent/input_parser.py b/black-box-data-create/agent/input_parser.py new file mode 100644 index 0000000..0a8401a --- /dev/null +++ b/black-box-data-create/agent/input_parser.py @@ -0,0 +1,330 @@ +# agent/input_parser.py +import re +import os +from typing import List, Dict, Optional, Tuple + +from agent.models import ( + FileInfo, KeyInfo, ModuleInfo, ProgramMeta, CopyField, TableColumn, TableInfo +) +from agent.markdown_utils import ( + extract_section, parse_table_rows, parse_table_from_section, find_row_by_key +) + + +class InputParser: + """解析 COBOL 程序的详细设计书和相关文件。""" + + def __init__(self, design_md_path: str, source_cbl_path: str, + file_db_md_path: str, cpy_dir: str, db_md_path: str): + self.design_md_path = design_md_path + self.source_cbl_path = source_cbl_path + self.file_db_md_path = file_db_md_path + self.cpy_dir = cpy_dir + self.db_md_path = db_md_path + self._design_text = '' + self._source_text = '' + + def run(self) -> ProgramMeta: + """执行完整解析,返回 ProgramMeta。""" + self._design_text = self._read_file(self.design_md_path) + self._source_text = self._read_file(self.source_cbl_path) + + meta = ProgramMeta( + program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', + copy_fields={}, db_tables={} + ) + + 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) + self._parse_copybooks(meta) + self._parse_db_definition(meta) + + return meta + + def _parse_copybooks(self, meta: ProgramMeta): + replacing_list = self._extract_copy_replacing() + + for f in meta.files: + if not f.copy_group: + continue + + copy_file = os.path.join(self.cpy_dir, f.copy_group + '.cpy') + if not os.path.exists(copy_file): + continue + + prefix = f.identifier + for i, (cpy_name, rep_val) in enumerate(replacing_list): + if cpy_name == f.copy_group and rep_val == f.identifier: + prefix = rep_val + replacing_list.pop(i) + break + else: + for i, (cpy_name, rep_val) in enumerate(replacing_list): + if cpy_name == f.copy_group: + prefix = rep_val + replacing_list.pop(i) + break + + fields = self._parse_single_copybook(copy_file, prefix) + if f.identifier not in meta.copy_fields: + meta.copy_fields[f.identifier] = fields + + def _parse_db_definition(self, meta: ProgramMeta): + """解析 DB 定义书 .md,提取表结构信息。 + 只在输入类型涉及 DB 时调用。 + """ + if meta.input_type not in ('db', 'mixed'): + return + + db_text = self._read_file(self.db_md_path) + + sections = re.split(r'\n# ', db_text) + + for section in sections: + table_match = re.match(r'^([\w\-]+)', section) + if not table_match: + continue + table_name = table_match.group(1) + + section_with_header = '## ' + section + + db_info_text = extract_section(section_with_header, 'DB基本情報') + db_info_rows = parse_table_rows(db_info_text) + + db_id = '' + copy_id = '' + for row in db_info_rows: + db_id = row.get('DB ID', '') or db_id + copy_id = row.get('COPY ID', '') or copy_id + + column_text = extract_section(section_with_header, 'カラム定義') + column_rows = parse_table_rows(column_text) + + if not column_rows: + continue + + columns = [] + pk_columns = [] + for row in column_rows: + try: + no = int(row.get('No', '0')) + except ValueError: + continue + + is_pk = row.get('PK', '') == '✓' + nullable = row.get('NULL', 'NULL許可') == 'NULL許可' + + col = TableColumn( + no=no, + name_jp=row.get('項目名', ''), + name_en=row.get('項目名(英字名)', ''), + type=row.get('TYPE', ''), + max_len=row.get('最大長', ''), + decimal_digits=row.get('小数桁', ''), + byte_count=row.get('バイト数', ''), + nullable=nullable, + is_pk=is_pk + ) + columns.append(col) + if is_pk: + pk_columns.append(col.name_en) + + table = TableInfo( + table_name=table_name, + db_id=db_id, + copy_id=copy_id, + columns=columns, + pk_columns=pk_columns + ) + meta.db_tables[table_name] = table + + def _extract_copy_replacing(self) -> List[Tuple[str, str]]: + """从 COBOL 源码提取 COPY ... REPLACING ... 映射。 + Returns: [(copy_name, replacing_value), ...] 按出现顺序保存。 + """ + result = [] + pattern = r'COPY\s+(\S+)\s+REPLACING\s+==\(A\)==\s+BY\s+==(\S+)==' + for match in re.finditer(pattern, self._source_text, re.IGNORECASE): + result.append((match.group(1), match.group(2))) + return result + + def _parse_single_copybook(self, copy_path: str, prefix: str) -> List[CopyField]: + copy_text = self._read_file(copy_path) + fields = [] + + for line in copy_text.split('\n'): + line = line.strip() + if not line or line.startswith('*'): + continue + + match = re.match( + r'(\d{2})\s+\(A\)(\S+)\s+PIC\s+(.+?)\.', + line, re.IGNORECASE + ) + if not match: + continue + + level = int(match.group(1)) + raw_name = '(A)' + match.group(2) + pic_full = match.group(3).strip() + actual_name = prefix + '-' + match.group(2) + pic_bytes = self._calculate_pic_bytes(pic_full) + + fields.append(CopyField( + level=level, + name=actual_name, + raw_name=raw_name, + pic_type=pic_full, + pic_bytes=pic_bytes + )) + + return fields + + @staticmethod + def _calculate_pic_bytes(pic: str) -> int: + pic_upper = pic.upper().strip() + + if 'COMP-3' in pic_upper: + base = pic_upper.replace('COMP-3', '').strip() + total_digits = sum(int(num) for num in re.findall(r'9\((\d+)\)', base)) + if base.startswith('S'): + return (total_digits + 2) // 2 + else: + return (total_digits + 1) // 2 + + if 'COMP' in pic_upper or 'BINARY' in pic_upper: + base = re.sub(r'\s*(COMP|BINARY)\s*', '', pic_upper).strip() + total_digits = sum(int(num) for num in re.findall(r'9\((\d+)\)', base)) + if total_digits <= 4: + return 2 + elif total_digits <= 9: + return 4 + else: + return 8 + + total = 0 + for num in re.findall(r'X\((\d+)\)', pic_upper): + total += int(num) + for num in re.findall(r'G\((\d+)\)', pic_upper): + total += int(num) * 2 + for num in re.findall(r'9\((\d+)\)', pic_upper): + total += int(num) + if total > 0: + return total + + return 0 + + @staticmethod + def _read_file(path: str) -> str: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + + def _parse_basic_info(self, meta: ProgramMeta): + rows = parse_table_from_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) + elif item == '': + if value: + meta.summary_lines.append(value) + + def _parse_use_files(self, meta: ProgramMeta): + rows = parse_table_from_section(self._design_text, '使用ファイル一覧') + for row in rows: + try: + no_str = row.get('NO', '0') + try: + no = int(no_str) if no_str and no_str.strip() not in ('', '—') else 0 + except ValueError: + no = 0 + rec_len_str = row.get('レコード長', '0') + try: + rec_len = int(rec_len_str) if rec_len_str and rec_len_str.strip() not in ('', '—') else 0 + except ValueError: + rec_len = 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群', ''), + 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_from_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_from_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' diff --git a/black-box-data-create/agent/markdown_utils.py b/black-box-data-create/agent/markdown_utils.py new file mode 100644 index 0000000..f2812ba --- /dev/null +++ b/black-box-data-create/agent/markdown_utils.py @@ -0,0 +1,63 @@ +# agent/markdown_utils.py +import re +from typing import List, Dict, Optional + + +def extract_section(md_text: str, section_title: str) -> str: + """Extract content of a ## or ### section from markdown text. + Stops at the next heading of equal or higher level (fewer or equal #'s). + For ## sections, includes ### sub-sections. + """ + heading_re = rf'^(#{{2,3}})\s+{re.escape(section_title)}[^\S\n]*$' + h_match = re.search(heading_re, md_text, re.MULTILINE) + if not h_match: + return '' + + level = len(h_match.group(1)) + start = h_match.end() + + next_re = rf'^#{{1,{level}}}\s' + n_match = re.search(next_re, md_text[start:], re.MULTILINE) + end = start + n_match.start() if n_match else len(md_text) + + return md_text[start:end] + + +def parse_table_rows(text: str) -> List[Dict[str, str]]: + """Parse a markdown table from text, return list of row dicts. + Handles tables with exactly one header row and one separator row. + """ + lines = [] + for line in text.split('\n'): + stripped = line.strip() + if stripped.startswith('|') and stripped.endswith('|'): + if re.match(r'^\|[\s\-:]+\|', stripped): + continue + lines.append(stripped) + + if len(lines) < 1: + return [] + + headers = [cell.strip() for cell in lines[0].split('|')[1:-1]] + rows = [] + for line in lines[1:]: + cells = [cell.strip() for cell in line.split('|')[1:-1]] + if len(cells) == len(headers): + rows.append(dict(zip(headers, cells))) + return rows + + +def parse_table_from_section(md_text: str, section_title: str) -> List[Dict[str, str]]: + """Find a ### section and parse its first table.""" + section_text = extract_section(md_text, section_title) + if not section_text: + return [] + return parse_table_rows(section_text) + + +def find_row_by_key(rows: List[Dict[str, str]], key_col: str, key_value: str) -> Optional[Dict[str, str]]: + """Find a table row where a specific column matches the key value.""" + for row in rows: + if row.get(key_col, '').strip() == key_value: + return row + return None diff --git a/black-box-data-create/agent/models.py b/black-box-data-create/agent/models.py new file mode 100644 index 0000000..0d7caaa --- /dev/null +++ b/black-box-data-create/agent/models.py @@ -0,0 +1,89 @@ +from dataclasses import dataclass, field +from typing import List, Dict, Optional + + +@dataclass +class FileInfo: + """使用ファイル一覧 中的一行""" + no: int + file_db_name: str + identifier: str + dd_name: str + io: str + copy_group: str + format: str + record_len: int + medium: str + remarks: str + + +@dataclass +class CopyField: + """COPYBOOK 中的单个字段""" + level: int + name: str + raw_name: str + pic_type: str + pic_bytes: int + + +@dataclass +class KeyInfo: + """キー項目一覧 中的一行""" + no: int + file_name: str + sort_condition: str + key_condition: str + + +@dataclass +class ModuleInfo: + """使用モジュール一覧 中的一行""" + no: int + function: str + program_id: str + copy_name: str + + +@dataclass +class TableColumn: + """DB 表中的单个字段""" + no: int + name_jp: str + name_en: str + type: str + max_len: str + decimal_digits: str + byte_count: str + nullable: bool + is_pk: bool + + +@dataclass +class TableInfo: + """DB 表定义""" + table_name: str + db_id: str + copy_id: str + columns: List[TableColumn] + pk_columns: List[str] + + +@dataclass +class ProgramMeta: + """程序完整元数据""" + program_id: str + program_name: str + system_name: str + pgm_type: str + pgm_pattern: str + summary_lines: List[str] + prerequisites: List[Dict[str, str]] + files: List[FileInfo] + keys: List[KeyInfo] + modules: List[ModuleInfo] + process_detail: str + output_records: str + input_type: str + copy_fields: Dict[str, List[CopyField]] + db_tables: Dict[str, TableInfo] diff --git a/black-box-data-create/agent/output_writer.py b/black-box-data-create/agent/output_writer.py new file mode 100644 index 0000000..d631182 --- /dev/null +++ b/black-box-data-create/agent/output_writer.py @@ -0,0 +1,59 @@ +import json +import os +from typing import Dict, Any + + +class OutputWriter: + """将 AI 生成的数据写入文件系统。""" + + def __init__(self, output_dir: str): + self.output_dir = output_dir + + def write(self, program_id: str, ai_result: Dict[str, Any], + input_type: str) -> Dict[str, str]: + """写入所有组的输出文件。 + + Returns: + {group_folder: written_file_path} 映射 + """ + written = {} + groups = ai_result.get('groups', ai_result) + + for group_key in sorted(groups.keys()): + group_data = groups[group_key] + group_dir = os.path.join(self.output_dir, program_id, group_key) + os.makedirs(group_dir, exist_ok=True) + + if input_type in ('file', 'mixed'): + json_path = self._write_json(group_dir, program_id, group_key, group_data) + written[f"{group_key}/json"] = json_path + + if input_type in ('db', 'mixed'): + sql_path = self._write_sql(group_dir, program_id, group_key, group_data) + written[f"{group_key}/sql"] = sql_path + + return written + + def _write_json(self, group_dir: str, program_id: str, + group_key: str, data: Any) -> str: + """写入 JSON 文件。""" + filename = f"{program_id}_{group_key}.json" + filepath = os.path.join(group_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return filepath + + def _write_sql(self, group_dir: str, program_id: str, + group_key: str, data: Any) -> str: + """写入 SQL 文件。""" + filename = f"{program_id}_{group_key}.sql" + filepath = os.path.join(group_dir, filename) + + sql_content = data if isinstance(data, str) else data.get('sql', json.dumps(data, ensure_ascii=False)) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(sql_content) + + return filepath diff --git a/black-box-data-create/agent/prompt_builder.py b/black-box-data-create/agent/prompt_builder.py new file mode 100644 index 0000000..298c143 --- /dev/null +++ b/black-box-data-create/agent/prompt_builder.py @@ -0,0 +1,146 @@ +from typing import List + +from agent.models import ProgramMeta + + +class PromptBuilder: + """将解析后的程序元数据和规则组装成 API prompt。""" + + def build(self, meta: ProgramMeta, rules_text: str, + group_descriptions: List[str], group_count: int) -> str: + """构建完整的 API prompt。""" + parts = [] + + parts.append(self._build_basic_info(meta)) + parts.append(self._build_process_detail(meta)) + parts.append(self._build_input_structures(meta)) + parts.append(self._build_output_records(meta)) + parts.append(rules_text) + parts.append(self._build_output_format(meta)) + parts.append(self._build_generation_instruction( + group_descriptions, group_count, meta.input_type + )) + + return '\n\n'.join(parts) + + def _build_basic_info(self, meta: ProgramMeta) -> str: + lines = [ + f"## プログラム基本情報", + f"- システム名: {meta.system_name}", + f"- プログラムID: {meta.program_id}", + f"- プログラム名: {meta.program_name}", + f"- PGMパターン: {meta.pgm_pattern}", + f"- 入力タイプ: {self._input_type_label(meta.input_type)}", + ] + if meta.summary_lines: + lines.append(f"- 機能概要: {' '.join(meta.summary_lines)}") + return '\n'.join(lines) + + def _build_process_detail(self, meta: ProgramMeta) -> str: + return f"## 処理詳細\n\n```\n{meta.process_detail}\n```" + + def _build_input_structures(self, meta: ProgramMeta) -> str: + parts = ["## 入力構造"] + + input_files = [f for f in meta.files if 'I' in f.io] + for f in input_files: + parts.append(f"### ファイル {f.identifier} (DD名: {f.dd_name}, COPY: {f.copy_group}, 媒体: {f.medium})") + fields = meta.copy_fields.get(f.identifier, []) + if fields: + parts.append("| 項目名 | PIC | バイト数 |") + parts.append("|--------|-----|----------|") + for cf in fields: + parts.append(f"| {cf.name} | {cf.pic_type} | {cf.pic_bytes} |") + else: + parts.append("(構造情報なし)") + + db_inputs = [f for f in meta.files if 'I' in f.io and f.medium == 'DB'] + if db_inputs or meta.db_tables: + parts.append("### DBテーブル構造") + for table_name, table in meta.db_tables.items(): + parts.append(f"**表名: {table_name}**") + pk_str = ', '.join(table.pk_columns) + parts.append(f"主キー: {pk_str}") + parts.append("| 項目名 | 英字名 | タイプ | 最大長 | KEY |") + parts.append("|--------|--------|--------|--------|-----|") + for col in table.columns: + key_mark = '✓' if col.is_pk else '' + parts.append(f"| {col.name_jp} | {col.name_en} | {col.type} | {col.max_len} | {key_mark} |") + + return '\n\n'.join(parts) + + def _build_output_records(self, meta: ProgramMeta) -> str: + return f"## 出力レコード定義\n\n```\n{meta.output_records}\n```" + + def _build_output_format(self, meta: ProgramMeta) -> str: + lines = [ + "## 出力形式", + "", + "### JSON形式(ファイル入力の場合)", + "", + "JSON構造:", + "```json", + "{", + ' "program": "{プログラムID}",', + ' "records": [', + ' { "input": { "FD名": { "項目名": "値", ... } } },', + ' ...', + ' ]', + "}", + "```", + "", + "### 項目値のルール(PIC → JSON値)", + "| PIC | JSON内表示 | 例 |", + "|-----|-----------|-----|", + "| PIC X(n) | 左詰め + スペース埋め | `\"A0000001\"` |", + "| PIC 9(n) | 右詰め + 先行ゼロ | `\"00000101\"` |", + "| PIC S9(n) | 符号 + 右詰め + 先行ゼロ | `\"+0000101\"` |", + "| PIC S9(n)V9(m) | 符号 + 右詰め + 小数点含む | `\"+001234567\"` |", + "| PIC S9(n) COMP | 通常の10進数文字列 | `\"300\"` |", + "| PIC S9(n) COMP-3 | 通常の10進数文字列 | `\"1234\"` |", + "| PIC 9(n) COMP-3 | 通常の10進数文字列 | `\"1234\"` |", + "| FILLER(純粋予約) | グループIDとレコード番号を含むパターン | `\"D000000...001\"` |", + "| FILLER(業務予約) | 全スペース(PIC X) または 全ゼロ(PIC 9) | |", + "", + "### SQL形式(DB入力の場合)", + "各グループ1つのSQLファイル: `{program}_g{groupId}.sql`", + "", + "SQL例:", + "```sql", + "-- Group: 1", + "INSERT INTO TABLE_NAME (COL1, COL2) VALUES", + "('val1', 'val2');", + "```", + "", + "### データ生成の注意", + "- 項目名に意味がある場合(DATE→日付、NAME→氏名)、実際の形式に合った値を生成すること", + "- 隣接するレコード間で、同じ項目に異なる値を設定すること", + ] + return '\n'.join(lines) + + def _build_generation_instruction(self, descriptions: List[str], + count: int, input_type: str) -> str: + lines = [ + "## 生成指示", + f"生成するグループ数: {count}", + "各グループの内容:", + ] + for i, desc in enumerate(descriptions, 1): + lines.append(f" - g{i}: {desc}") + + if input_type == 'file': + lines.append("出力: JSONファイル(1グループ=1JSONファイル)") + elif input_type == 'db': + lines.append("出力: SQL INSERTファイル(1グループ=1SQLファイル)") + else: + lines.append("出力: JSONファイル + SQL INSERTファイル") + + lines.append("") + lines.append("応答形式: JSONで返してください。最上位キー \"groups\" を使い、各グループを \"g1\", \"g2\" ... とします。") + lines.append("例: {\"groups\": {\"g1\": {\"program\": \"XXX\", \"records\": [...]}, \"g2\": {...}}}") + return '\n'.join(lines) + + @staticmethod + def _input_type_label(t: str) -> str: + labels = {'file': 'ファイル', 'db': 'DB', 'mixed': '混合(ファイル+DB)'} + return labels.get(t, t) diff --git a/black-box-data-create/agent/rule_loader.py b/black-box-data-create/agent/rule_loader.py new file mode 100644 index 0000000..6bfc4ba --- /dev/null +++ b/black-box-data-create/agent/rule_loader.py @@ -0,0 +1,226 @@ +import os +import re +from typing import List, Tuple, Optional + +from agent.models import ProgramMeta + + +PGM_PATTERN_MAP = { + 'マッチング(1:1)': 'マッチング(1-1).md', + 'マッチング(1:1)': 'マッチング(1-1).md', + 'マッチング(1:N)': 'マッチング(1-N).md', + 'マッチング(1:N)': 'マッチング(1-N).md', + 'マッチング(1:N) + EVALUATE 4パターン分岐': 'マッチング(1-N).md', + 'マッチング(N:1)': 'マッチング(N-1).md', + 'マッチング(N:1)': 'マッチング(N-1).md', + 'マッチング(M:N)': 'マッチング(M-N).md', + 'マッチングM:N⇒出力M件': 'マッチング(M-N).md', + 'マッチングM:N⇒出力N件': 'マッチング(M-N).md', + 'マッチングM:N⇒出力M×N件': 'マッチング(M-N).md', + '18(M:N→M件マッチング)': 'マッチング(M-N).md', + '19(M:N→N件マッチング+集計)': 'マッチング(M-N).md', + '20(M:N→M×N件直積出力)': 'マッチング(M-N).md', + 'レイアウト編集のみ(GETPUT)': 'レイアウト編集のみ(GETPUT).md', + 'レイアウト編集のみ(GETPUT)': 'レイアウト編集のみ(GETPUT).md', + 'レイアウト編集のみ': 'レイアウト編集のみ(GETPUT).md', + 'GETPUT(編集出力)': 'GETPUT(編集出力).md', + '項目チェック': '項目チェック(重複含まず).md', + '項目チェック(重複なし)': '項目チェック(重複含まず).md', + '項目チェック(重複なし)': '項目チェック(重複含まず).md', + '項目チェック(重複含まず)': '項目チェック(重複含まず).md', + '項目チェック(重複含む)': '項目チェック(重複含まず).md', + '項目チェック(半角20桁/4桁)': '項目チェック(半角20桁-4桁).md', + '27(半角20桁/4桁チェック)': '項目チェック(半角20桁-4桁).md', + '半角20桁/4桁': '項目チェック(半角20桁-4桁).md', + '振り分け': '振り分け(IF).md', + '振り分け(IF文)': '振り分け(IF).md', + '振り分け(IF文、EVALUATE文)': '振り分け(IF).md', + '振り分け(EVALUATE文)': '振り分け(EVALUATE).md', + '振り分け(EVALUATE文)': '振り分け(EVALUATE).md', + 'キーブレイク': 'キーブレイク(集計).md', + 'キーブレイク(集計)': 'キーブレイク(集計).md', + 'キーブレイク(集計)': 'キーブレイク(集計).md', + 'キーブレイク(集約)': 'キーブレイク(集約).md', + 'キーブレイク(集約)': 'キーブレイク(集約).md', + 'キーブレイク(集計、集約)': 'キーブレイク(集計).md', + 'キーブレイク(集計、集約の以外)': 'キーブレイク(非集計).md', + 'キーブレイク(集計集約以外)': 'キーブレイク(非集計).md', + 'キーブレイク(集計集約以外)': 'キーブレイク(非集計).md', + 'キーブレイク(非集計)': 'キーブレイク(非集計).md', + '1:Nキーブレイク(同キー集約)': 'キーブレイク(集計).md', + '1:N+キーブレイク(同キー)': 'キーブレイク(集計).md', + '1:N+同キーキーブレイク': 'キーブレイク(集計).md', + '32(1:N+同キーキーブレイク)': 'キーブレイク(集計).md', + '1:N+同キー': 'キーブレイク(集計).md', + '1:N+キーブレイク(異キー)': '1-N+キーブレイク(異キー).md', + '1:N+キーブレイク(異キー)': '1-N+キーブレイク(異キー).md', + '1:N+異キーキーブレイク': '1-N+キーブレイク(異キー).md', + '33(1:N+異キーキーブレイク)': '1-N+キーブレイク(異キー).md', + '1:N+異キー': '1-N+キーブレイク(異キー).md', + 'DB更新': 'DB更新.md', + 'DB更新 + SYSIN読込(P28)': 'DB更新.md', + 'SELECT処理': 'SELECT処理.md', + 'SELECT条件': 'SELECT処理.md', + 'SELECT条件': 'SELECT処理.md', + 'DB検索': 'SELECT処理.md', + 'DB検索': 'SELECT処理.md', + '50分割': '50分割.md', + '50分割': '50分割.md', + '25分割': '25分割.md', + '25分割': '25分割.md', + '11(25分割)': '25分割.md', + '100分割': '100分割.md', + '100分割': '100分割.md', + '12(100分割)': '100分割.md', + 'MERGE(複数ファイル結合)': 'MERGE.md', + 'MERGE(複数ファイル結合)': 'MERGE.md', + 'CSV→FB変換(改行あり)': 'CSV→FB変換.md', + 'CSV→FB変換(改行あり)': 'CSV→FB変換.md', + 'CSV→FB変換(改行なし)': 'CSV→FB変換.md', + 'CSV→FB変換(改行なし)': 'CSV→FB変換.md', + '2段階マッチング(1:1⇒1:1)': '2段階マッチング(1:1⇒1:1).md', + '2段階マッチング(1:1⇒1:1)': '2段階マッチング(1:1⇒1:1).md', + '2段階マッチング(N:1⇒N:1)': '2段階マッチング(N:1⇒N:1).md', + '2段階マッチング(N:1⇒N:1)': '2段階マッチング(N:1⇒N:1).md', + '2段階マッチング(M:N⇒M:N)': '2段階マッチング(M:N⇒M:N).md', + '2段階マッチング(M:N⇒M:N)': '2段階マッチング(M:N⇒M:N).md', + 'マッチング(1:1→1:1 2段階)': '2段階マッチング(1:1⇒1:1).md', + 'マッチング(N:1→N:1 2段階)': '2段階マッチング(N:1⇒N:1).md', + 'マッチング(M:N→M:N 2段階)': '2段階マッチング(M:N⇒M:N).md', + '内部テーブル検索': '内部テーブル検索.md', + '24(内部テーブル検索)': '内部テーブル検索.md', + 'サブプログラム使用': 'サブプログラム.md', + 'サブプログラム': 'サブプログラム.md', + 'サブPGM': 'サブプログラム.md', + '25(サブPGM)': 'サブプログラム.md', + 'SYSIN読込': 'SYSIN読込.md', + 'SYSIN読込': 'SYSIN読込.md', + '28(SYSIN読込)': 'SYSIN読込.md', + 'ASCII→EBCDIC変換': 'ASCII→EBCDIC変換.md', + 'ASCII→EBCDIC変換': 'ASCII→EBCDIC変換.md', + '29(ASCII→EBCDIC変換)': 'ASCII→EBCDIC変換.md', + 'SORT(INPUT/OUTPUT PROCEDURE)': 'SORT.md', + '34(SORT INPUT/OUTPUT PROCEDURE)': 'SORT.md', + 'SORT': 'SORT.md', + 'オンラインPGM': 'オンラインPGM.md', + 'オンラインPGM': 'オンラインPGM.md', + 'オンライン': 'オンラインPGM.md', + '編集出力(ランキング生成)': 'ランキング生成.md', + '36(ランキング生成)': 'ランキング生成.md', + 'ランキング生成': 'ランキング生成.md', + '編集出力(印刷制御)': 'GETPUT(編集出力).md', + '編集出力': 'GETPUT(編集出力).md', +} + +SPECIAL_FEATURE_CHECKS = [ + (['場合', 'EVALUATE', 'IF'], '条件分支.md'), +] + + +class RuleLoader: + """根据程序特征加载对应的数据生成规则。""" + + def __init__(self, rules_dir: str): + self.pgm_pattern_dir = os.path.join(rules_dir, 'pgm_pattern') + self.special_feature_dir = os.path.join(rules_dir, 'special_feature') + + def load(self, meta: ProgramMeta) -> Tuple[str, List[str], int]: + """加载所有相关规则,返回 (合并后的规则文本, 组描述列表, 组数)。""" + pgm_rule = self._load_pgm_pattern_rule(meta.pgm_pattern) + if pgm_rule is None: + raise FileNotFoundError( + f"PGM模式 '{meta.pgm_pattern}' の規則ファイルが見つかりません。" + f"{self.pgm_pattern_dir} に .md ファイルを追加してください。" + ) + + group_descriptions, group_count = self._parse_group_info(pgm_rule) + + parts = [pgm_rule] + for keywords, rule_file in SPECIAL_FEATURE_CHECKS: + if self._detect_feature(meta.process_detail, keywords): + feature_rule = self._read_rule_file( + os.path.join(self.special_feature_dir, rule_file) + ) + if feature_rule: + parts.append(feature_rule) + + combined = '\n\n---\n\n'.join(parts) + return combined, group_descriptions, group_count + + def _load_pgm_pattern_rule(self, pgm_pattern: str) -> Optional[str]: + """根据 PGMパターン 加载对应的规则文件。""" + filename = PGM_PATTERN_MAP.get(pgm_pattern) + if filename: + path = os.path.join(self.pgm_pattern_dir, filename) + if os.path.exists(path): + return self._read_rule_file(path) + + if os.path.isdir(self.pgm_pattern_dir): + available = sorted(os.listdir(self.pgm_pattern_dir)) + # 对每个规则文件名(去掉.md),检查它是否出现在 PGMパターン 中 + # 半角/全角括弧に加え、'1:N' vs '1-N' のコロン/ハイフン差異も吸収する + pgm_lower = pgm_pattern.lower().replace(':', '-') + for fname in available: + if fname.endswith('.md'): + rule_name = fname[:-3].lower() + if rule_name in pgm_lower: + return self._read_rule_file(os.path.join(self.pgm_pattern_dir, fname)) + + return None + + @staticmethod + def _read_rule_file(path: str) -> str: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + + @staticmethod + def _parse_group_info(rule_text: str) -> Tuple[List[str], int]: + """从规则文本中解析组信息。 + + 只统计表头首列为"组"的表格(生成的数据组), + 避免把记录明细表中的数字(如 record序号 / 第1条)误算成组号。 + """ + descriptions = [] + group_count = 0 + in_group_table = False + + for line in rule_text.split('\n'): + stripped = line.strip() + if not stripped.startswith('|'): + in_group_table = False + continue + + # 分隔行(|---|)跳过 + if re.match(r'^\|[\s\-:]+\|', stripped): + continue + + cells = [c.strip() for c in stripped.split('|')[1:-1]] + if not cells: + continue + + # 表头行:首列为"组"时进入组表格 + if cells[0] == '组': + in_group_table = True + continue + + if not in_group_table: + continue + + if len(cells) >= 2: + try: + group_num = int(cells[0]) + if group_num > group_count: + group_count = group_num + descriptions.append(cells[1]) + except ValueError: + pass + + return descriptions, group_count + + @staticmethod + def _detect_feature(process_detail: str, keywords: List[str]) -> bool: + """检测処理詳細中是否包含特定关键词。""" + for kw in keywords: + if kw in process_detail: + return True + return False diff --git a/black-box-data-create/docs/superpowers/plans/2026-07-12-testdata-agent-plan.md b/black-box-data-create/docs/superpowers/plans/2026-07-12-testdata-agent-plan.md new file mode 100644 index 0000000..cad7e2a --- /dev/null +++ b/black-box-data-create/docs/superpowers/plans/2026-07-12-testdata-agent-plan.md @@ -0,0 +1,2310 @@ +# 测试数据生成 Agent 实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 基于 Python 构建一个测试数据生成 Agent,解析 COBOL 详细设计书和 COPYBOOK,通过 DeepSeek API 生成符合规范的测试数据 JSON/SQL 文件。 + +**Architecture:** 线性管道架构。InputParser 解析输入 → RuleLoader 匹配规则 → PromptBuilder 组装 prompt → APIClient 调用 AI → OutputWriter 保存结果。各模块通过 dataclass 传递结构化数据。 + +**Tech Stack:** Python 3.9+, requests, pytest + +--- + +### Task 1: 项目初始化 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\__init__.py` +- Create: `D:\jcl-cobol-data-create\requirements.txt` +- Create: `D:\jcl-cobol-data-create\tests\__init__.py` +- Create: `D:\jcl-cobol-data-create\tests\conftest.py` + +- [ ] **Step 1: 创建 agent 包和 __init__.py** + +```python +# agent/__init__.py +__version__ = "0.1.0" +``` + +- [ ] **Step 2: 创建 requirements.txt** + +``` +requests>=2.28.0 +``` + +- [ ] **Step 3: 创建 tests 目录和 conftest.py** + +```python +# tests/__init__.py +# (空文件) +``` + +```python +# tests/conftest.py +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +@pytest.fixture +def data_dir(): + return os.path.join(os.path.dirname(__file__), 'test_data') +``` + +- [ ] **Step 4: 创建 tests/test_data 目录存放测试夹具** + +Run: `New-Item -ItemType Directory -Path "D:\jcl-cobol-data-create\tests\test_data" -Force` + +- [ ] **Step 5: 运行测试验证环境** + +Run: `python -m pytest tests/ -v` +Expected: 0 tests collected, no errors. + +--- + +### Task 2: 数据模型定义 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\models.py` +- Create: `D:\jcl-cobol-data-create\tests\test_models.py` + +- [ ] **Step 1: 定义数据模型** + +```python +# agent/models.py +from dataclasses import dataclass, field +from typing import List, Dict, Optional + + +@dataclass +class FileInfo: + """使用ファイル一覧 中的一行""" + no: int + file_db_name: str # 使用ファイル/DB名 + identifier: str # 識別子 (R01, W01, etc.) + dd_name: str # DD名 + io: str # I/O (I, O, I/U/D) + copy_group: str # COPY群 + format: str # 形式 (FB, VB) + record_len: int # レコード長 + medium: str # 媒体 (PS, DB) + remarks: str # 備考 + + +@dataclass +class CopyField: + """COPYBOOK 中的单个字段""" + level: int # 级别 (03, 05, ...) + name: str # 替换后的实际字段名 (如 R01-APPL-ID) + raw_name: str # 原始占位符名 (如 (A)APPL-ID) + pic_type: str # PIC 定义 (如 "X(008)", "9(008)", "S9(009) COMP-3") + pic_bytes: int # 字节数 + + +@dataclass +class KeyInfo: + """キー項目一覧 中的一行""" + no: int + file_name: str # ファイル名 + sort_condition: str # ソート条件(キー項目) + key_condition: str # キー条件(マッチング/キーブレイク) + + +@dataclass +class ModuleInfo: + """使用モジュール一覧 中的一行""" + no: int + function: str # 機能 + program_id: str # プログラムID + copy_name: str # 使用COPY名 + + +@dataclass +class TableColumn: + """DB 表中的单个字段""" + no: int + name_jp: str # 项目名(日文) + name_en: str # 项目名(英文) + type: str # 类型 (CHAR, DECIMAL, etc.) + max_len: str # 最大长 + decimal_digits: str # 小数桁 + byte_count: str # バイト数 + nullable: bool # NULL 许可 + is_pk: bool # 是否主键 + + +@dataclass +class TableInfo: + """DB 表定义""" + table_name: str # DB名 (如 LEAVE_RECORDS) + db_id: str # DB ID (如 EMP_MASTER) + copy_id: str # COPY ID + columns: List[TableColumn] + pk_columns: List[str] # 主键字段名列表 + + +@dataclass +class ProgramMeta: + """程序完整元数据""" + program_id: str + program_name: str + system_name: str + pgm_type: str # PGMタイプ (メイン/サブ) + pgm_pattern: str # PGMパターン + summary_lines: List[str] # 機能概要 (多行) + prerequisites: List[Dict[str, str]] # 前提条件 + files: List[FileInfo] + keys: List[KeyInfo] + modules: List[ModuleInfo] + process_detail: str # 処理詳細 全文 (从详细设计书获取) + output_records: str # 出力レコード定義 全文 + input_type: str # "file", "db", "mixed" + # 由 COPYBOOK 解析填充 + copy_fields: Dict[str, List[CopyField]] # 識別子 → 字段列表 + # DB 相关 + db_tables: Dict[str, TableInfo] # DB表名 → 表定义 +``` + +- [ ] **Step 2: 写测试验证数据模型可实例化** + +```python +# tests/test_models.py +from agent.models import FileInfo, CopyField, ProgramMeta, KeyInfo, TableColumn, TableInfo + + +def test_file_info(): + f = FileInfo( + no=1, file_db_name="OVT-SORTED", identifier="R01", + dd_name="ZAN04R01", io="I", copy_group="ZAN01REC", + format="FB", record_len=80, medium="PS", remarks="有効申請" + ) + assert f.identifier == "R01" + assert f.medium == "PS" + + +def test_copy_field(): + cf = CopyField(level=3, name="R01-APPL-ID", raw_name="(A)APPL-ID", + pic_type="X(008)", pic_bytes=8) + assert cf.name == "R01-APPL-ID" + assert cf.pic_bytes == 8 + + +def test_program_meta_defaults(): + meta = ProgramMeta( + program_id="TEST", program_name="", system_name="", + pgm_type="", pgm_pattern="", + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail="", output_records="", + input_type="file", + copy_fields={}, db_tables={} + ) + assert meta.program_id == "TEST" + assert meta.input_type == "file" + + +def test_table_info(): + col = TableColumn(no=1, name_jp="社員番号", name_en="EMP_ID", + type="CHAR", max_len="8", decimal_digits="", + byte_count="8", nullable=False, is_pk=True) + table = TableInfo(table_name="EMP_MASTER", db_id="EMP_MASTER", + copy_id="", columns=[col], pk_columns=["EMP_ID"]) + assert table.pk_columns == ["EMP_ID"] + assert len(table.columns) == 1 +``` + +- [ ] **Step 3: 运行测试** + +Run: `python -m pytest tests/test_models.py -v` +Expected: 4 tests PASS + +--- + +### Task 3: InputParser — 详细设计书解析 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\markdown_utils.py` +- Create: `D:\jcl-cobol-data-create\agent\input_parser.py` +- Create: `D:\jcl-cobol-data-create\tests\test_data\sample_design_ZAN04MAT.md` +- Create: `D:\jcl-cobol-data-create\tests\test_markdown_utils.py` +- Create: `D:\jcl-cobol-data-create\tests\test_input_parser.py` + +- [ ] **Step 1: 创建 markdown 工具函数** + +```python +# agent/markdown_utils.py +import re +from typing import List, Dict, Optional + + +def extract_section(md_text: str, section_title: str) -> str: + """Extract content of a ### section from markdown text. + Returns content between this header and the next ## or ### header, or end of file. + """ + pattern = rf'###\s+{re.escape(section_title)}\s*\n(.*?)(?=\n##|\n###|\Z)' + match = re.search(pattern, md_text, re.DOTALL) + return match.group(1) if match else '' + + +def parse_table_rows(text: str) -> List[Dict[str, str]]: + """Parse a markdown table from text, return list of row dicts. + Handles tables with exactly one header row and one separator row. + """ + lines = [] + for line in text.split('\n'): + stripped = line.strip() + if stripped.startswith('|') and stripped.endswith('|'): + # Skip separator rows like |---|------|---| + if re.match(r'^\|[\s\-:]+\|', stripped): + continue + lines.append(stripped) + + if len(lines) < 1: + return [] + + headers = [cell.strip() for cell in lines[0].split('|')[1:-1]] + rows = [] + for line in lines[1:]: + cells = [cell.strip() for cell in line.split('|')[1:-1]] + if len(cells) == len(headers): + rows.append(dict(zip(headers, cells))) + return rows + + +def parse_table_from_section(md_text: str, section_title: str) -> List[Dict[str, str]]: + """Find a ### section and parse its first table.""" + section_text = extract_section(md_text, section_title) + if not section_text: + return [] + return parse_table_rows(section_text) + + +def find_row_by_key(rows: List[Dict[str, str]], key_col: str, key_value: str) -> Optional[Dict[str, str]]: + """Find a table row where a specific column matches the key value.""" + for row in rows: + if row.get(key_col, '').strip() == key_value: + return row + return None +``` + +- [ ] **Step 2: 写 markdown_utils 的测试** + +```python +# tests/test_markdown_utils.py +from agent.markdown_utils import extract_section, parse_table_rows, parse_table_from_section, find_row_by_key + +SAMPLE_MD = """ +### 基本情報 + +| # | 項目 | 内容 | +|---|------|------| +| 1 | システム名 | 残業統計管理システム | +| 4 | PGMパターン | マッチング(1:1) | +| 5 | 機能概要 | 取消マッチング処理 | + +### 使用ファイル一覧 + +| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 媒体 | 備考 | +|----|------------------|--------|------|-----|--------|------|------| +| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | PS | | +| 2 | ERROR-LOG | W01 | ZAN04W01 | O | ZAN05REC | PS | | + +### 出力レコード定義 + +### 出力ファイル1(W01/OVT-MATCHED) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | APPL-ID | R01.APPL-ID | | +| 2 | EMP-ID | R01.EMP-ID | | +""" + + +def test_extract_section(): + result = extract_section(SAMPLE_MD, "基本情報") + assert "残業統計管理システム" in result + assert "使用ファイル一覧" not in result + + +def test_extract_section_not_found(): + result = extract_section(SAMPLE_MD, "存在しないセクション") + assert result == '' + + +def test_parse_table_rows(): + rows = parse_table_from_section(SAMPLE_MD, "基本情報") + assert len(rows) >= 3 + assert rows[0]['項目'] == 'システム名' + + +def test_find_row_by_key(): + rows = parse_table_from_section(SAMPLE_MD, "基本情報") + row = find_row_by_key(rows, '項目', 'PGMパターン') + assert row is not None + assert row['内容'] == 'マッチング(1:1)' + + +def test_parse_use_file_table(): + rows = parse_table_from_section(SAMPLE_MD, "使用ファイル一覧") + assert len(rows) == 2 + assert rows[0]['識別子'] == 'R01' + assert rows[0]['DD名'] == 'ZAN04R01' + + +def test_extract_output_records_section(): + result = extract_section(SAMPLE_MD, "出力レコード定義") + assert "出力ファイル1" in result + assert "W01/OVT-MATCHED" in result +``` + +- [ ] **Step 3: 运行测试确认 markdown_utils 通过** + +Run: `python -m pytest tests/test_markdown_utils.py -v` +Expected: all PASS + +- [ ] **Step 4: 创建测试用详细设计书 fixture** + +使用实际项目中 `詳細設計書_ZAN04MAT.md` 的副本作为测试数据。 + +Run: `Copy-Item "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN04MAT.md" "D:\jcl-cobol-data-create\tests\test_data\詳細設計書_ZAN04MAT.md"` + +- [ ] **Step 5: 实现 InputParser 设计书部分** + +```python +# agent/input_parser.py +import re +import os +from typing import List, Dict, Optional, Tuple + +from agent.models import ( + FileInfo, KeyInfo, ModuleInfo, ProgramMeta, CopyField, TableColumn, TableInfo +) +from agent.markdown_utils import ( + extract_section, parse_table_rows, parse_table_from_section, find_row_by_key +) + + +class InputParser: + """解析 COBOL 程序的详细设计书和相关文件。""" + + def __init__(self, design_md_path: str, source_cbl_path: str, + file_db_md_path: str, cpy_dir: str, db_md_path: str): + self.design_md_path = design_md_path + self.source_cbl_path = source_cbl_path + self.file_db_md_path = file_db_md_path + self.cpy_dir = cpy_dir + self.db_md_path = db_md_path + self._design_text = '' + self._source_text = '' + + def run(self) -> ProgramMeta: + """执行完整解析,返回 ProgramMeta。""" + self._design_text = self._read_file(self.design_md_path) + self._source_text = self._read_file(self.source_cbl_path) + + meta = ProgramMeta( + program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', + copy_fields={}, db_tables={} + ) + + 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) + self._parse_copybooks(meta) + self._parse_db_definition(meta) + + return meta + + @staticmethod + def _read_file(path: str) -> str: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + + def _parse_basic_info(self, meta: ProgramMeta): + rows = parse_table_from_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) + elif item == '': + # 多行功能概要用空 # 列标识续行 + if value: + meta.summary_lines.append(value) + + def _parse_use_files(self, meta: ProgramMeta): + rows = parse_table_from_section(self._design_text, '使用ファイル一覧') + for row in rows: + try: + no = int(row.get('NO', '0')) + rec_len_str = row.get('レコード長', '0') + rec_len = int(rec_len_str) if rec_len_str and rec_len_str != '' 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群', ''), + 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_from_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_from_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): + """Determine input type based on input files' medium. + Only looks at files with I/O containing 'I' (input). + """ + 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' + + def _parse_copybooks(self, meta: ProgramMeta): + """TODO: 在 Task 4 中实现""" + pass + + def _parse_db_definition(self, meta: ProgramMeta): + """TODO: 在 Task 5 中实现""" + pass +``` + +- [ ] **Step 6: 写 InputParser 设计书解析部分的测试** + +```python +# tests/test_input_parser.py +import os +from agent.input_parser import InputParser + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_design_basic_info(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', + db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_basic_info(meta) + assert meta.program_id == 'ZAN04MAT' + assert meta.pgm_pattern == 'マッチング(1:1)' + assert meta.pgm_type == 'メイン' + assert '残業統計管理システム' in meta.system_name + + +def test_parse_use_files(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_use_files(meta) + assert len(meta.files) == 5 + r01 = [f for f in meta.files if f.identifier == 'R01'][0] + assert r01.file_db_name == 'OVT-SORTED' + assert r01.copy_group == 'ZAN01REC' + assert r01.medium == 'PS' + assert r01.io == 'I' + + +def test_determine_input_type_file(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + + from agent.models import ProgramMeta, FileInfo + meta = ProgramMeta(program_id='ZAN04MAT', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + meta.files = [ + FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1', + io='I', copy_group='C1', format='FB', record_len=80, + medium='PS', remarks=''), + FileInfo(no=2, file_db_name='F2', identifier='W01', dd_name='DD2', + io='O', copy_group='C2', format='FB', record_len=80, + medium='PS', remarks=''), + ] + parser._determine_input_type(meta) + assert meta.input_type == 'file' + + +def test_determine_input_type_mixed(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + + from agent.models import ProgramMeta, FileInfo + meta = ProgramMeta(program_id='TEST', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + meta.files = [ + FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1', + io='I', copy_group='C1', format='FB', record_len=80, + medium='PS', remarks=''), + FileInfo(no=2, file_db_name='DB1', identifier='DB', dd_name='', + io='I', copy_group='', format='', record_len=0, + medium='DB', remarks=''), + ] + parser._determine_input_type(meta) + assert meta.input_type == 'mixed' + + +def test_parse_process_detail(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_process_detail(meta) + assert '1000ITTSOR' in meta.process_detail + assert 'マッチの場合' in meta.process_detail + + +def test_parse_output_records(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_output_records(meta) + assert 'OVT-MATCHED' in meta.output_records + assert 'OVT-DBCLEAN' in meta.output_records +``` + +- [ ] **Step 7: 运行测试** + +Run: `python -m pytest tests/test_input_parser.py -v` +Expected: all PASS + +--- + +### Task 4: InputParser — COPYBOOK 解析 + +**Files:** +- Modify: `D:\jcl-cobol-data-create\agent\input_parser.py` (添加 `_parse_copybooks`) +- Create: `D:\jcl-cobol-data-create\tests\test_copy_parser.py` + +- [ ] **Step 1: 添加 COPYBOOK 解析逻辑到 InputParser** + +在 `agent/input_parser.py` 的 `_parse_copybooks` 方法中追加实现,替换之前的 `pass`: + +```python +def _parse_copybooks(self, meta: ProgramMeta): + """解析 COPYBOOK 文件,应用 REPLACING 替换。""" + # Step 1: 从源码提取 COPY REPLACING 映射 + replacing_map = self._extract_copy_replacing() + + # Step 2: 遍历使用ファイル一覧 中的文件,找到对应的 COPYBOOK + for f in meta.files: + if not f.copy_group: + continue + + copy_file = os.path.join(self.cpy_dir, f.copy_group + '.cpy') + if not os.path.exists(copy_file): + continue + + # 从 REPLACING 映射获取替换前缀 + prefix = replacing_map.get(f.copy_group, f.identifier) + + fields = self._parse_single_copybook(copy_file, prefix) + if f.identifier not in meta.copy_fields: + meta.copy_fields[f.identifier] = fields + + +def _extract_copy_replacing(self) -> Dict[str, str]: + """从 COBOL 源码提取 COPY ... REPLACING ... 映射。 + Returns: {copy_name: replacing_value} e.g. {'ZAN01REC': 'R01'} + """ + result = {} + # 匹配模式: COPY ZAN01REC REPLACING ==(A)== BY ==R01==. + pattern = r'COPY\s+(\S+)\s+REPLACING\s+==\(A\)==\s+BY\s+==(\S+)==' + for match in re.finditer(pattern, self._source_text, re.IGNORECASE): + copy_name = match.group(1) + replacing_value = match.group(2) + result[copy_name] = replacing_value + return result + + +def _parse_single_copybook(self, copy_path: str, prefix: str) -> List[CopyField]: + """解析单个 COPYBOOK 文件,将 (A) 占位符替换为 'prefix-' 前缀。 + e.g. (A)APPL-ID + prefix='R01' → R01-APPL-ID + """ + copy_text = self._read_file(copy_path) + fields = [] + + for line in copy_text.split('\n'): + line = line.strip() + if not line or line.startswith('*'): + continue + + # 匹配: 03 (A)FIELD-NAME PIC X(008). + # 或: 03 (A)FIELD-NAME PIC S9(009) COMP-3. + match = re.match( + r'(\d{2})\s+\(A\)(\S+)\s+PIC\s+(.+?)\.', + line, re.IGNORECASE + ) + if not match: + continue + + level = int(match.group(1)) + raw_name = '(A)' + match.group(2) + pic_full = match.group(3).strip() + + # 替换占位符 + actual_name = prefix + '-' + match.group(2) + + pic_bytes = self._calculate_pic_bytes(pic_full) + + fields.append(CopyField( + level=level, + name=actual_name, + raw_name=raw_name, + pic_type=pic_full, + pic_bytes=pic_bytes + )) + + return fields + + +@staticmethod +def _calculate_pic_bytes(pic: str) -> int: + """Calculate byte size from PIC definition. + Handles: X(n), 9(n), S9(n)V9(m), COMP-3 variants, COMP variants. + """ + pic_upper = pic.upper().strip() + + # COMP-3 packed decimal: S9(n)V9(m) COMP-3 or S9(n) COMP-3 or 9(n) COMP-3 + if 'COMP-3' in pic_upper: + # Remove COMP-3 suffix + base = pic_upper.replace('COMP-3', '').strip() + # Extract digits: count all 9s + total_digits = sum(int(num) for num in re.findall(r'9\((\d+)\)', base)) + if base.startswith('S'): + return (total_digits + 2) // 2 + else: + return (total_digits + 1) // 2 + + # COMP / BINARY: S9(n) COMP, S9(n)V9(m) COMP + if 'COMP' in pic_upper or 'BINARY' in pic_upper: + base = re.sub(r'\s*(COMP|BINARY)\s*', '', pic_upper).strip() + total_digits = sum(int(num) for num in re.findall(r'9\((\d+)\)', base)) + if total_digits <= 4: + return 2 + elif total_digits <= 9: + return 4 + else: + return 8 + + # Simple PIC: X(n), 9(n), S9(n), S9(n)V9(m) + total = 0 + # Match X(n) + for num in re.findall(r'X\((\d+)\)', pic_upper): + total += int(num) + # Match G(n) (double-byte) + for num in re.findall(r'G\((\d+)\)', pic_upper): + total += int(num) * 2 + # Match 9(n) + for num in re.findall(r'9\((\d+)\)', pic_upper): + total += int(num) + if total > 0: + return total + + return 0 +``` + +- [ ] **Step 2: 创建 COPYBOOK 解析测试的 fixture** + +将实际 COPYBOOK 文件复制到测试目录: + +```powershell +Copy-Item "D:\cobol-tna-system\cpy\ZAN01REC.cpy" "D:\jcl-cobol-data-create\tests\test_data\ZAN01REC.cpy" +Copy-Item "D:\cobol-tna-system\cpy\ZAN02REC.cpy" "D:\jcl-cobol-data-create\tests\test_data\ZAN02REC.cpy" +Copy-Item "D:\cobol-tna-system\cpy\ZAN04REC.cpy" "D:\jcl-cobol-data-create\tests\test_data\ZAN04REC.cpy" +``` + +- [ ] **Step 3: 写 COPYBOOK 解析测试** + +```python +# tests/test_copy_parser.py +import os +import tempfile +from agent.input_parser import InputParser + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_single_copybook(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR, + db_md_path='dummy_db.md' + ) + fields = parser._parse_single_copybook( + os.path.join(FIXTURE_DIR, 'ZAN01REC.cpy'), + prefix='R01' + ) + + assert len(fields) == 8 + + # 第一个字段: (A)APPL-ID → R01-APPL-ID, PIC X(008) + f0 = fields[0] + assert f0.name == 'R01-APPL-ID' + assert f0.raw_name == '(A)APPL-ID' + assert 'X' in f0.pic_type + assert f0.pic_bytes == 8 + + # EMP-ID: PIC 9(008) + f1 = fields[1] + assert f1.name == 'R01-EMP-ID' + assert '9' in f1.pic_type + assert f1.pic_bytes == 8 + + # START-TIME: PIC 9(004) + f3 = fields[3] + assert f3.name == 'R01-START-TIME' + assert f3.pic_bytes == 4 + + # FILLER + f7 = fields[7] + assert 'FILLER' in f7.name + assert f7.pic_bytes == 46 + + +def test_extract_copy_replacing(): + source_text = """ + FD R01INNFIL. + 01 R01INNREC. + COPY ZAN01REC REPLACING ==(A)== BY ==R01==. + FD R02INNFIL. + 01 R02INNREC. + COPY ZAN04REC REPLACING ==(A)== BY ==R02==. + """ + + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR, + db_md_path='dummy_db.md' + ) + parser._source_text = source_text + + result = parser._extract_copy_replacing() + assert result['ZAN01REC'] == 'R01' + assert result['ZAN04REC'] == 'R02' + + +def test_calculate_pic_bytes_x(): + assert InputParser._calculate_pic_bytes('X(008)') == 8 + assert InputParser._calculate_pic_bytes('X(10)') == 10 + + +def test_calculate_pic_bytes_9(): + assert InputParser._calculate_pic_bytes('9(008)') == 8 + assert InputParser._calculate_pic_bytes('9(004)') == 4 + + +def test_calculate_pic_bytes_comp3(): + # S9(009) COMP-3: 9 digits → (9+2)//2 = 5 + assert InputParser._calculate_pic_bytes('S9(009) COMP-3') == 5 + # S9(7) COMP-3: 7 digits → (7+2)//2 = 4 + assert InputParser._calculate_pic_bytes('S9(7) COMP-3') == 4 + # 9(7) COMP-3: 7 digits unsigned → (7+1)//2 = 4 + assert InputParser._calculate_pic_bytes('9(7) COMP-3') == 4 + + +def test_calculate_pic_bytes_comp(): + # S9(4) COMP: <=4 → 2 bytes + assert InputParser._calculate_pic_bytes('S9(4) COMP') == 2 + # S9(9) COMP: <=9 → 4 bytes + assert InputParser._calculate_pic_bytes('S9(9) COMP') == 4 + + +def test_calculate_pic_bytes_decimal(): + # S9(7)V9(2): 7+2=9 bytes + assert InputParser._calculate_pic_bytes('S9(7)V9(2)') == 9 +``` + +- [ ] **Step 4: 运行测试** + +Run: `python -m pytest tests/test_copy_parser.py -v` +Expected: all PASS + +--- + +### Task 5: InputParser — DB 定义解析 + +**Files:** +- Modify: `D:\jcl-cobol-data-create\agent\input_parser.py` (添加 `_parse_db_definition`) +- Create: `D:\jcl-cobol-data-create\tests\test_db_parser.py` + +- [ ] **Step 1: 添加 DB 定义解析逻辑** + +在 `agent/input_parser.py` 的 `_parse_db_definition` 方法中追加实现: + +```python +def _parse_db_definition(self, meta: ProgramMeta): + """解析 DB 定义书 .md,提取表结构信息。 + 只在输入类型涉及 DB 时调用。 + """ + if meta.input_type not in ('db', 'mixed'): + return + + db_text = self._read_file(self.db_md_path) + + # 按 ## 切分每个表定义 + sections = re.split(r'\n## ', db_text) + + for section in sections: + # 提取表名(## 后的第一部分) + table_match = re.match(r'^([\w\-]+)', section) + if not table_match: + continue + table_name = table_match.group(1) + + # 查找 "DB基本情報" 子节 + db_info_text = extract_section('## ' + section, 'DB基本情報') + db_info_rows = parse_table_rows(db_info_text) + + db_id = '' + copy_id = '' + for row in db_info_rows: + db_id = row.get('DB ID', '') or db_id + copy_id = row.get('COPY ID', '') or copy_id + + # 查找 "カラム定義" 子节 + column_text = extract_section('## ' + section, 'カラム定義') + column_rows = parse_table_rows(column_text) + + columns = [] + pk_columns = [] + for row in column_rows: + try: + no = int(row.get('No', '0')) + except ValueError: + continue + + is_pk = row.get('PK', '') == '✓' + nullable = row.get('NULL', 'NULL許可') == 'NULL許可' + + col = TableColumn( + no=no, + name_jp=row.get('項目名', ''), + name_en=row.get('項目名(英字名)', ''), + type=row.get('TYPE', ''), + max_len=row.get('最大長', ''), + decimal_digits=row.get('小数桁', ''), + byte_count=row.get('バイト数', ''), + nullable=nullable, + is_pk=is_pk + ) + columns.append(col) + if is_pk: + pk_columns.append(col.name_en) + + table = TableInfo( + table_name=table_name, + db_id=db_id, + copy_id=copy_id, + columns=columns, + pk_columns=pk_columns + ) + meta.db_tables[table_name] = table +``` + +- [ ] **Step 2: 创建 DB 测试 fixture** + +```powershell +Copy-Item "D:\cobol-tna-system\詳細設計書\DB定義書.md" "D:\jcl-cobol-data-create\tests\test_data\DB定義書.md" +``` + +- [ ] **Step 3: 写 DB 解析测试** + +```python +# tests/test_db_parser.py +import os +from agent.input_parser import InputParser +from agent.models import ProgramMeta, FileInfo + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_db_definition(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir='dummy_cpy', + db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md') + ) + + meta = ProgramMeta( + program_id='TEST', program_name='テスト', system_name='', + pgm_type='メイン', pgm_pattern='DB更新', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='mixed', # DB 类型才会触发解析 + copy_fields={}, db_tables={} + ) + + parser._parse_db_definition(meta) + + assert len(meta.db_tables) > 0 + assert 'EMP_MASTER' in meta.db_tables + + emp = meta.db_tables['EMP_MASTER'] + assert emp.db_id == 'EMP_MASTER' + assert len(emp.columns) >= 3 + assert emp.pk_columns == ['EMP_ID'] + + +def test_parse_db_skipped_for_file_type(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir='dummy_cpy', + db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md') + ) + + meta = ProgramMeta( + program_id='TEST', program_name='テスト', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', # 文件类型,跳过 DB 解析 + copy_fields={}, db_tables={} + ) + + parser._parse_db_definition(meta) + assert len(meta.db_tables) == 0 +``` + +- [ ] **Step 4: 运行测试** + +Run: `python -m pytest tests/test_db_parser.py -v` +Expected: all PASS + +--- + +### Task 6: RuleLoader — 规则匹配与特殊功能检测 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\rule_loader.py` +- Create: `D:\jcl-cobol-data-create\tests\test_rule_loader.py` + +- [ ] **Step 1: 实现 RuleLoader** + +```python +# agent/rule_loader.py +import os +import re +from typing import List, Tuple, Optional + +from agent.models import ProgramMeta + + +# PGMパターン → 规则文件名 映射表 +PGM_PATTERN_MAP = { + 'マッチング(1:1)': 'マッチング(1-1).md', + 'マッチング(1:N)': 'マッチング(1-N).md', + 'マッチング(N:1)': 'マッチング(N-1).md', + 'マッチング(M:N)': 'マッチング(M-N).md', + 'レイアウト編集のみ(GETPUT)': 'レイアウト編集のみ(GETPUT).md', + 'レイアウト編集のみ': 'レイアウト編集のみ(GETPUT).md', + '項目チェック': '項目チェック.md', + '振り分け': '振り分け.md', + '振り分け(IF文、EVALUATE文)': '振り分け.md', + 'キーブレイク': 'キーブレイク.md', + 'キーブレイク(集計、集約)': 'キーブレイク(集計、集約).md', + 'キーブレイク(集計、集約の以外)': 'キーブレイク.md', + 'DB更新': 'DB更新.md', +} + +# 特殊功能检测:关键词集合 → 规则文件名 +SPECIAL_FEATURE_CHECKS = [ + (['場合', 'EVALUATE', 'IF'], '条件分支.md'), +] + + +class RuleLoader: + """根据程序特征加载对应的数据生成规则。""" + + def __init__(self, rules_dir: str): + """ + Args: + rules_dir: 规则根目录,包含 pgm_pattern/ 和 special_feature/ 子目录 + """ + self.pgm_pattern_dir = os.path.join(rules_dir, 'pgm_pattern') + self.special_feature_dir = os.path.join(rules_dir, 'special_feature') + + def load(self, meta: ProgramMeta) -> Tuple[str, List[str], int]: + """加载所有相关规则,返回 (合并后的规则文本, 组描述列表, 组数)。 + + Returns: + combined_rules: 合并后的规则文本 + group_descriptions: 各组的用途描述列表 + group_count: 组数 + """ + # 1. 加载 PGM 模式规则 + pgm_rule = self._load_pgm_pattern_rule(meta.pgm_pattern) + if pgm_rule is None: + raise FileNotFoundError( + f"PGM模式 '{meta.pgm_pattern}' 的规则文件未找到。" + f"请在 {self.pgm_pattern_dir} 中添加对应的 .md 文件。" + ) + + # 2. 解析组信息 + group_descriptions, group_count = self._parse_group_info(pgm_rule) + + # 3. 检测特殊功能 + parts = [pgm_rule] + for keywords, rule_file in SPECIAL_FEATURE_CHECKS: + if self._detect_feature(meta.process_detail, keywords): + feature_rule = self._read_rule_file( + os.path.join(self.special_feature_dir, rule_file) + ) + if feature_rule: + parts.append(feature_rule) + + combined = '\n\n---\n\n'.join(parts) + return combined, group_descriptions, group_count + + def _load_pgm_pattern_rule(self, pgm_pattern: str) -> Optional[str]: + """根据 PGMパターン 加载对应的规则文件。""" + # 先精确匹配 + filename = PGM_PATTERN_MAP.get(pgm_pattern) + if filename: + path = os.path.join(self.pgm_pattern_dir, filename) + if os.path.exists(path): + return self._read_rule_file(path) + + # 尝试模糊匹配:检查文件名是否包含关键字 + for fname in os.listdir(self.pgm_pattern_dir): + if fname.endswith('.md'): + # 尝试模糊匹配 + keyword = pgm_pattern.split('(')[0].strip() + if keyword in fname: + return self._read_rule_file(os.path.join(self.pgm_pattern_dir, fname)) + + return None + + @staticmethod + def _read_rule_file(path: str) -> str: + with open(path, 'r', encoding='utf-8') as f: + return f.read() + + @staticmethod + def _parse_group_info(rule_text: str) -> Tuple[List[str], int]: + """从规则文本中解析组信息。 + 查找表格中 '组' 列对应的行,提取组号和用途。 + """ + descriptions = [] + group_count = 0 + + # 匹配规则中的组描述表格 + # 格式: | 组 | 用途 | ... | + for line in rule_text.split('\n'): + line = line.strip() + if line.startswith('|') and not re.match(r'^\|[\s\-:]+\|', line): + cells = [c.strip() for c in line.split('|')[1:-1]] + if len(cells) >= 2: + try: + group_num = int(cells[0]) + if group_num > group_count: + group_count = group_num + descriptions.append(cells[1]) + except ValueError: + pass + + return descriptions, group_count + + @staticmethod + def _detect_feature(process_detail: str, keywords: List[str]) -> bool: + """检测処理詳細中是否包含特定关键词。""" + for kw in keywords: + if kw in process_detail: + return True + return False +``` + +- [ ] **Step 2: 写 RuleLoader 测试** + +```python +# tests/test_rule_loader.py +import os +from agent.rule_loader import RuleLoader, PGM_PATTERN_MAP +from agent.models import ProgramMeta + +RULES_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules') + + +def test_pgm_pattern_map_has_known_patterns(): + assert 'マッチング(1:1)' in PGM_PATTERN_MAP + assert 'マッチング(1:N)' in PGM_PATTERN_MAP + assert 'DB更新' in PGM_PATTERN_MAP + + +def test_load_matching_1_1_rule(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + assert len(combined) > 0 + assert 'マッチング(1:1)' in combined + assert '数据生成步骤' in combined + + +def test_load_matching_1_n_rule(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:N)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + assert len(combined) > 0 + assert 'マッチング(1:N)' in combined + + +def test_load_nonexistent_pattern_raises(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='不存在的パターン', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + try: + loader.load(meta) + assert False, 'Should have raised FileNotFoundError' + except FileNotFoundError: + pass + + +def test_detect_conditional_branch(): + process = """ + 2-1-2.ロジック分岐判定(EVALUATE) + 2-1-2-1.STATUS='1'の場合 + INSERT処理 + """ + assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True + + +def test_no_conditional_branch(): + process = """ + 2-1.マッチの場合 + R01をW01に出力 + """ + # '場合' 也匹配了! 日文中 'の場合' 表示 "在...情况下",视为条件分支 + assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True + + +def test_rule_with_conditional_feature(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='2-1-2.EVALUATEで分岐する。STATUS=1の場合INSERTする。', + output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + # 条件分支规则应该被包含 + assert '分支' in combined or '条件分支' in combined or '条件判断' in combined +``` + +- [ ] **Step 3: 运行测试** + +Run: `python -m pytest tests/test_rule_loader.py -v` +Expected: all PASS + +--- + +### Task 7: PromptBuilder — Prompt 组装 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\prompt_builder.py` +- Create: `D:\jcl-cobol-data-create\tests\test_prompt_builder.py` + +- [ ] **Step 1: 实现 PromptBuilder** + +```python +# agent/prompt_builder.py +from typing import List + +from agent.models import ProgramMeta, CopyField, TableInfo + + +class PromptBuilder: + """将解析后的程序元数据和规则组装成 API prompt。""" + + def build(self, meta: ProgramMeta, rules_text: str, + group_descriptions: List[str], group_count: int) -> str: + """构建完整的 API prompt。""" + parts = [] + + # 1. 程序基本情報 + parts.append(self._build_basic_info(meta)) + + # 2. 処理詳細 + parts.append(self._build_process_detail(meta)) + + # 3. 入力構造 (文件 + DB) + parts.append(self._build_input_structures(meta)) + + # 4. 出力レコード定義 + parts.append(self._build_output_records(meta)) + + # 5. データ生成ルール + parts.append(rules_text) + + # 6. 出力格式指示 + parts.append(self._build_output_format(meta)) + + # 7. 生成指示 + parts.append(self._build_generation_instruction( + group_descriptions, group_count, meta.input_type + )) + + return '\n\n'.join(parts) + + def _build_basic_info(self, meta: ProgramMeta) -> str: + lines = [ + f"## 程序基本情報", + f"- システム名: {meta.system_name}", + f"- プログラムID: {meta.program_id}", + f"- プログラム名: {meta.program_name}", + f"- PGMパターン: {meta.pgm_pattern}", + f"- 入力タイプ: {self._input_type_label(meta.input_type)}", + ] + if meta.summary_lines: + lines.append(f"- 機能概要: {' '.join(meta.summary_lines)}") + return '\n'.join(lines) + + def _build_process_detail(self, meta: ProgramMeta) -> str: + return f"## 処理詳細\n\n```\n{meta.process_detail}\n```" + + def _build_input_structures(self, meta: ProgramMeta) -> str: + parts = ["## 入力構造"] + + # 文件输入结构 + input_files = [f for f in meta.files if 'I' in f.io] + for f in input_files: + parts.append(f"### ファイル {f.identifier} (DD名: {f.dd_name}, COPY: {f.copy_group}, 媒体: {f.medium})") + fields = meta.copy_fields.get(f.identifier, []) + if fields: + parts.append("| 項目名 | PIC | バイト数 |") + parts.append("|--------|-----|----------|") + for cf in fields: + parts.append(f"| {cf.name} | {cf.pic_type} | {cf.pic_bytes} |") + else: + parts.append("(構造情報なし)") + + # DB 表结构 + db_inputs = [f for f in meta.files if 'I' in f.io and f.medium == 'DB'] + if db_inputs or meta.db_tables: + parts.append("### DBテーブル構造") + for table_name, table in meta.db_tables.items(): + parts.append(f"**表名: {table_name}**") + parts.append(f"主キー: {', '.join(table.pk_columns)}") + parts.append("| 項目名 | 英字名 | タイプ | 最大長 | KEY |") + parts.append("|--------|--------|--------|--------|-----|") + for col in table.columns: + key_mark = '✓' if col.is_pk else '' + parts.append(f"| {col.name_jp} | {col.name_en} | {col.type} | {col.max_len} | {key_mark} |") + + return '\n\n'.join(parts) + + def _build_output_records(self, meta: ProgramMeta) -> str: + return f"## 出力レコード定義\n\n```\n{meta.output_records}\n```" + + def _build_output_format(self, meta: ProgramMeta) -> str: + lines = [ + "## 出力形式", + "", + "### JSON形式(ファイル入力の場合)", + f"各グループ1つのJSONファイル: `{{program}}_{{group}}.json`", + "", + "JSON構造:", + "```json", + "{", + ' "program": "{プログラムID}",', + ' "records": [', + ' { "input": { "FD名": { "項目名": "値", ... } } },', + ' ...', + ' ]', + "}", + "```", + "", + "### 項目値のルール(PIC → JSON値)", + "| PIC | JSON内表示 | 例 |", + "|-----|-----------|-----|", + "| PIC X(n) | 左詰め + スペース埋め | `\"A0000001\"` |", + "| PIC 9(n) | 右詰め + 先行ゼロ | `\"00000101\"` |", + "| PIC S9(n) | 符号 + 右詰め + 先行ゼロ | `\"+0000101\"` |", + "| PIC S9(n)V9(m) | 符号 + 右詰め + 小数点含む | `\"+001234567\"` |", + "| PIC S9(n) COMP | 通常の10進数文字列 | `\"300\"` |", + "| PIC S9(n) COMP-3 | 通常の10進数文字列 | `\"1234\"` |", + "| PIC 9(n) COMP-3 | 通常の10進数文字列 | `\"1234\"` |", + "| FILLER(純粋予約) | グループIDとレコード番号を含む識別可能なパターン | `\"D000000...001\"` |", + "| FILLER(業務予約) | 全スペース(PIC X) または 全ゼロ(PIC 9) | |", + "", + "### SQL形式(DB入力の場合)", + "各グループ1つのSQLファイル: `{program}_g{groupId}.sql`", + "", + "SQL例:", + "```sql", + "-- Group: 1", + "INSERT INTO TABLE_NAME (COL1, COL2) VALUES", + "('val1', 'val2');", + "```", + "", + "### データ生成の注意", + "- 項目名に意味がある場合(DATE→日付、NAME→氏名、ADDRESS→住所等)、実際の形式に合った値を生成すること", + "- 隣接するレコード間で、できるだけ同じ項目に異なる値を設定すること", + ] + return '\n'.join(lines) + + def _build_generation_instruction(self, descriptions: List[str], + count: int, input_type: str) -> str: + lines = [ + "## 生成指示", + f"生成するグループ数: {count}", + "各グループの内容:", + ] + for i, desc in enumerate(descriptions, 1): + lines.append(f" - g{i}: {desc}") + + if input_type == 'file': + lines.append("出力: JSONファイル") + elif input_type == 'db': + lines.append("出力: SQL INSERTファイル") + else: + lines.append("出力: JSONファイル + SQL INSERTファイル(混合)") + + lines.append("") + lines.append("すべてのグループのデータを一度にJSONで返してください。") + lines.append("出力形式: { \"groups\": { \"g1\": { \"type\": \"json\"|... }, ... } }") + return '\n'.join(lines) + + @staticmethod + def _input_type_label(t: str) -> str: + labels = {'file': 'ファイル', 'db': 'DB', 'mixed': '混合(ファイル+DB)'} + return labels.get(t, t) +``` + +- [ ] **Step 2: 写 PromptBuilder 测试** + +```python +# tests/test_prompt_builder.py +from agent.prompt_builder import PromptBuilder +from agent.models import ProgramMeta, FileInfo, CopyField + + +def test_build_basic_prompt(): + meta = ProgramMeta( + program_id='ZAN04MAT', program_name='取消マッチング処理', + system_name='残業統計管理システム', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=['取消申請のマッチング'], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='1.初期処理...\n2.主処理...', + output_records='### 出力ファイル1...', + input_type='file', copy_fields={}, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# マッチング(1:1) データ生成規則\n...', + group_descriptions=['両端不一致', '逆方向両端不一致', '中間不一致'], + group_count=3 + ) + + assert 'ZAN04MAT' in prompt + assert 'マッチング(1:1)' in prompt + assert '1.初期処理' in prompt + assert '生成するグループ数: 3' in prompt + assert 'g1: 両端不一致' in prompt + assert 'g2: 逆方向両端不一致' in prompt + assert 'g3: 中間不一致' in prompt + assert 'JSON形式' in prompt + + +def test_build_with_copy_fields(): + fields = { + 'R01': [ + CopyField(level=3, name='R01-APPL-ID', raw_name='(A)APPL-ID', + pic_type='X(008)', pic_bytes=8), + CopyField(level=3, name='R01-EMP-ID', raw_name='(A)EMP-ID', + pic_type='9(008)', pic_bytes=8), + ] + } + + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], + files=[ + FileInfo(no=1, file_db_name='INPUT-FILE', identifier='R01', + dd_name='TESTR01', io='I', copy_group='TESTREC', + format='FB', record_len=80, medium='PS', remarks='') + ], + keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields=fields, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# 規則', + group_descriptions=['テスト'], + group_count=1 + ) + + assert 'R01-APPL-ID' in prompt + assert 'X(008)' in prompt + assert '8' in prompt + assert 'R01-EMP-ID' in prompt + + +def test_build_db_prompt(): + meta = ProgramMeta( + program_id='TESTDB', program_name='DB更新', system_name='', + pgm_type='メイン', pgm_pattern='DB更新', + summary_lines=[], prerequisites=[], + files=[ + FileInfo(no=1, file_db_name='INPUT-FILE', identifier='R01', + dd_name='TESTR01', io='I', copy_group='TESTREC', + format='FB', record_len=80, medium='DB', remarks='') + ], + keys=[], modules=[], + process_detail='', output_records='', + input_type='db', copy_fields={}, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# DB更新規則', + group_descriptions=['テスト'], + group_count=1 + ) + + assert 'DB' in prompt or 'SQL' in prompt + assert 'INSERT' in prompt +``` + +- [ ] **Step 3: 运行测试** + +Run: `python -m pytest tests/test_prompt_builder.py -v` +Expected: all PASS + +--- + +### Task 8: APIClient — API 调用与重试 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\api_client.py` +- Create: `D:\jcl-cobol-data-create\tests\test_api_client.py` + +- [ ] **Step 1: 实现 APIClient** + +```python +# agent/api_client.py +import json +import time +from typing import Dict, Any, Optional + +import requests + + +class APIClient: + """DeepSeek API 客户端,含重试逻辑。""" + + def __init__(self, api_key: str, model: str = 'deepseek-v4-flash', + base_url: str = 'https://api.deepseek.com/chat/completions', + max_retries: int = 3, timeout: int = 120): + self.api_key = api_key + self.model = model + self.base_url = base_url + self.max_retries = max_retries + self.timeout = timeout + + def generate(self, prompt: str) -> Dict[str, Any]: + """发送 prompt 并返回 AI 生成的结果。 + + Returns: + AI 返回的 JSON 字典 + Raises: + RuntimeError: 超过最大重试次数后仍然失败 + """ + system_prompt = ( + "你是COBOL程序的测试数据生成专家。" + "请严格按照提供的规则,生成符合格式要求的测试数据。" + "输出必须是可被json.loads()直接解析的JSON,不要包裹在```json```代码块中。" + "不要在JSON前后添加任何说明文字。" + ) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt}, + ] + + last_error = None + + for attempt in range(1, self.max_retries + 1): + try: + response = self._call_api(messages) + + # 提取 AI 返回的文本 + content = response['choices'][0]['message']['content'] + + # 尝试解析 JSON + data = self._parse_json(content) + if data is not None: + return data + + # JSON 解析失败,反馈给 AI 重试 + error_msg = ( + f"前回の出力は有効なJSONではありませんでした。" + f"必ず有効なJSONのみを出力してください。" + f"コードブロック(```)で囲まないでください。" + ) + messages.append({"role": "assistant", "content": content}) + messages.append({"role": "user", "content": error_msg}) + + except requests.exceptions.RequestException as e: + last_error = e + if attempt < self.max_retries: + time.sleep(2 ** attempt) # 指数退避 + continue + + raise RuntimeError( + f"API调用失败,已重试{self.max_retries}次。" + f"最后错误: {last_error}" + ) + + def _call_api(self, messages: list) -> dict: + """单次 API 调用。""" + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', + } + payload = { + 'model': self.model, + 'messages': messages, + 'temperature': 0.3, + 'max_tokens': 8192, + } + + resp = requests.post( + self.base_url, + headers=headers, + json=payload, + timeout=self.timeout + ) + resp.raise_for_status() + return resp.json() + + @staticmethod + def _parse_json(text: str) -> Optional[dict]: + """尝试从文本中提取 JSON。""" + text = text.strip() + + # 去除可能的 markdown 代码块包裹 + if text.startswith('```json'): + text = text[7:] + if text.startswith('```'): + text = text[3:] + if text.endswith('```'): + text = text[:-3] + text = text.strip() + + try: + return json.loads(text) + except json.JSONDecodeError: + # 尝试找到第一个 { 和最后一个 } + start = text.find('{') + end = text.rfind('}') + if start >= 0 and end > start: + try: + return json.loads(text[start:end + 1]) + except json.JSONDecodeError: + pass + return None +``` + +- [ ] **Step 2: 写 APIClient 测试(mock API)** + +```python +# tests/test_api_client.py +import json +from unittest.mock import patch, MagicMock +from agent.api_client import APIClient + + +def test_parse_json_valid(): + text = '{"groups": {"g1": {"records": []}}}' + result = APIClient._parse_json(text) + assert result is not None + assert 'groups' in result + + +def test_parse_json_with_markdown_wrapper(): + text = '```json\n{"key": "value"}\n```' + result = APIClient._parse_json(text) + assert result is not None + assert result['key'] == 'value' + + +def test_parse_json_with_text_before(): + text = '少し説明があります。\n{"key": "value"}\n以上です。' + result = APIClient._parse_json(text) + assert result is not None + assert result['key'] == 'value' + + +def test_parse_json_invalid(): + text = 'これは有効なJSONではありません。' + result = APIClient._parse_json(text) + assert result is None + + +@patch('agent.api_client.requests.post') +def test_generate_success(mock_post): + mock_response = MagicMock() + mock_response.json.return_value = { + 'choices': [{'message': {'content': '{"groups": {"g1": {"type": "json"}}}'}}] + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + client = APIClient(api_key='test-key') + result = client.generate("テストプロンプト") + + assert result['groups']['g1']['type'] == 'json' + + +@patch('agent.api_client.requests.post') +def test_generate_retry_on_json_error(mock_post): + # 第一次返回无效 JSON,第二次返回有效 JSON + bad_response = MagicMock() + bad_response.json.return_value = { + 'choices': [{'message': {'content': '無効な応答'}}] + } + bad_response.raise_for_status = MagicMock() + + good_response = MagicMock() + good_response.json.return_value = { + 'choices': [{'message': {'content': '{"result": "ok"}'}}] + } + good_response.raise_for_status = MagicMock() + + mock_post.side_effect = [bad_response, good_response] + + client = APIClient(api_key='test-key', max_retries=3) + result = client.generate("テスト") + + assert result['result'] == 'ok' + assert mock_post.call_count == 2 # 调用了2次 +``` + +- [ ] **Step 3: 运行测试** + +Run: `python -m pytest tests/test_api_client.py -v` +Expected: all PASS + +--- + +### Task 9: OutputWriter — 输出写入 + +**Files:** +- Create: `D:\jcl-cobol-data-create\agent\output_writer.py` +- Create: `D:\jcl-cobol-data-create\tests\test_output_writer.py` + +- [ ] **Step 1: 实现 OutputWriter** + +```python +# agent/output_writer.py +import json +import os +from typing import Dict, Any + + +class OutputWriter: + """将 AI 生成的数据写入文件系统。""" + + def __init__(self, output_dir: str): + self.output_dir = output_dir + + def write(self, program_id: str, ai_result: Dict[str, Any], + input_type: str) -> Dict[str, str]: + """写入所有组的输出文件。 + + Args: + program_id: 程序ID + ai_result: AI 返回的 JSON 数据,格式: {"groups": {"g1": {...}, ...}} + input_type: "file", "db", "mixed" + + Returns: + {group_folder: written_file_path} 映射 + """ + written = {} + groups = ai_result.get('groups', ai_result) # 兼容 {"records": [...]} 直接格式 + + for group_key in sorted(groups.keys()): + group_data = groups[group_key] + group_dir = os.path.join(self.output_dir, program_id, group_key) + os.makedirs(group_dir, exist_ok=True) + + if input_type in ('file', 'mixed'): + json_path = self._write_json(group_dir, program_id, group_key, group_data) + written[f"{group_key}/json"] = json_path + + if input_type in ('db', 'mixed'): + sql_path = self._write_sql(group_dir, program_id, group_key, group_data) + written[f"{group_key}/sql"] = sql_path + + return written + + def _write_json(self, group_dir: str, program_id: str, + group_key: str, data: Any) -> str: + """写入 JSON 文件。""" + filename = f"{program_id}_{group_key}.json" + filepath = os.path.join(group_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return filepath + + def _write_sql(self, group_dir: str, program_id: str, + group_key: str, data: Any) -> str: + """写入 SQL 文件。""" + filename = f"{program_id}_{group_key}.sql" + filepath = os.path.join(group_dir, filename) + + # 如果 data 中有 sql 字段,直接使用;否则将整个 data 写入 + sql_content = data if isinstance(data, str) else data.get('sql', json.dumps(data, ensure_ascii=False)) + + with open(filepath, 'w', encoding='utf-8') as f: + f.write(sql_content) + + return filepath +``` + +- [ ] **Step 2: 写 OutputWriter 测试** + +```python +# tests/test_output_writer.py +import json +import os +import tempfile +from agent.output_writer import OutputWriter + + +def test_write_json_files(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": { + "program": "TEST", + "records": [ + {"input": {"R01": {"R01-FIELD": "A001"}}} + ] + } + } + } + + written = writer.write("TESTPGM", ai_result, input_type="file") + + assert len(written) == 1 + json_path = written["g1/json"] + assert os.path.exists(json_path) + + with open(json_path, 'r', encoding='utf-8') as f: + content = json.load(f) + assert content['program'] == 'TEST' + assert len(content['records']) == 1 + + +def test_write_mixed_type(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": { + "program": "MIXED", + "records": [{"input": {"R01": {"F1": "X"}}}], + "sql": "INSERT INTO T VALUES ('x');" + } + } + } + + written = writer.write("MIXEDPGM", ai_result, input_type="mixed") + + assert len(written) == 2 + assert os.path.exists(written["g1/json"]) + assert os.path.exists(written["g1/sql"]) + + +def test_write_multiple_groups(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": {"records": [{}]}, + "g2": {"records": [{}]}, + "g3": {"records": [{}]}, + } + } + + written = writer.write("MULTI", ai_result, input_type="file") + + assert len(written) == 3 + for g in ['g1', 'g2', 'g3']: + assert f'{g}/json' in written + assert os.path.exists(written[f'{g}/json']) + + +def test_directory_structure(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + ai_result = {"groups": {"g1": {"records": [{}]}}} + writer.write("MYPROG", ai_result, input_type="file") + + expected_dir = os.path.join(tmpdir, "MYPROG", "g1") + assert os.path.isdir(expected_dir) + assert os.path.isfile(os.path.join(expected_dir, "MYPROG_g1.json")) +``` + +- [ ] **Step 3: 运行测试** + +Run: `python -m pytest tests/test_output_writer.py -v` +Expected: all PASS + +--- + +### Task 10: main.py — CLI 入口与集成测试 + +**Files:** +- Create: `D:\jcl-cobol-data-create\main.py` +- Create: `D:\jcl-cobol-data-create\tests\test_integration.py` + +- [ ] **Step 1: 确认 agent/__init__.py 导出 generate 函数** + +更新 `D:\jcl-cobol-data-create\agent\__init__.py`: + +```python +# agent/__init__.py +__version__ = "0.1.0" + +from agent.input_parser import InputParser +from agent.rule_loader import RuleLoader +from agent.prompt_builder import PromptBuilder +from agent.api_client import APIClient +from agent.output_writer import OutputWriter +from agent.models import ProgramMeta, FileInfo, CopyField, KeyInfo, TableColumn, TableInfo + + +def generate(design_md: str, source_cbl: str, file_db_md: str, + cpy_dir: str, db_md: str, output_dir: str = "output", + api_key: str = "sk-6156cccdc9c14d949cf5bfc5afc67a03", + api_model: str = "deepseek-v4-flash", + rules_dir: str = "rules") -> dict: + """生成测试数据的主入口函数。 + + Args: + design_md: 詳細設計書 .md 路径 + source_cbl: COBOL 源码 .cbl 路径 + file_db_md: 文件/DB 构造 .md 路径 + cpy_dir: COPYBOOK 存放目录路径 + db_md: DB 定义书 .md 路径 + output_dir: 输出目录 + api_key: DeepSeek API key + api_model: 模型名称 + rules_dir: 规则目录路径 + + Returns: + {"output_files": {"g1/json": "path", ...}, "program_id": "...", "groups": N} + """ + print(f"== 解析入力: {design_md}") + + # 1. 解析输入 + parser = InputParser(design_md, source_cbl, file_db_md, cpy_dir, db_md) + meta = parser.run() + + if meta.pgm_type == 'サブ': + raise ValueError(f"程序 {meta.program_id} はサブプログラムです。主プログラムのみ処理対象です。") + + print(f" プログラムID: {meta.program_id}, パターン: {meta.pgm_pattern}, 入力タイプ: {meta.input_type}") + + # 2. 加载规则 + loader = RuleLoader(rules_dir) + rules_text, group_descriptions, group_count = loader.load(meta) + + print(f" ルール読み込み完了, グループ数: {group_count}") + + # 3. 构建 prompt + builder = PromptBuilder() + prompt = builder.build(meta, rules_text, group_descriptions, group_count) + + # 4. 调用 API + client = APIClient(api_key=api_key, model=api_model) + print(f" API呼び出し中...") + result = client.generate(prompt) + print(f" API応答受信") + + # 5. 写入输出 + writer = OutputWriter(output_dir) + output_files = writer.write(meta.program_id, result, meta.input_type) + + print(f" 出力完了: {len(output_files)} ファイル") + + return { + "output_files": output_files, + "program_id": meta.program_id, + "groups": group_count, + "input_type": meta.input_type, + } +``` + +- [ ] **Step 2: 创建 main.py CLI 入口** + +```python +# main.py +#!/usr/bin/env python +""" +COBOL テストデータ生成 Agent + +使用方法: + python main.py \\ + --design "D:\\cobol-tna-system\\詳細設計書\\詳細設計書_ZAN04MAT.md" \\ + --source "D:\\cobol-tna-system\\src\\ZAN04MAT.cbl" \\ + --file-db-md "D:\\cobol-tna-system\\詳細設計書\\COPY句定義書.md" \\ + --cpy "D:\\cobol-tna-system\\cpy" \\ + --db-md "D:\\cobol-tna-system\\詳細設計書\\DB定義書.md" \\ + --output "output" +""" +import argparse +import os +import sys + +from agent import generate + +DEFAULT_RULES_DIR = os.path.join(os.path.dirname(__file__), 'rules') + + +def main(): + parser = argparse.ArgumentParser( + description='COBOLテストデータ生成Agent', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +例: + python main.py --design 詳細設計書_ZAN04MAT.md --source ZAN04MAT.cbl \\ + --cpy cpy/ --db-md DB定義書.md --output output/ + """ + ) + + parser.add_argument('--design', required=True, + help='詳細設計書 .md のパス') + parser.add_argument('--source', required=True, + help='COBOL ソース .cbl のパス') + parser.add_argument('--file-db-md', required=True, + help='ファイル/DB 構造 .md のパス') + parser.add_argument('--cpy', required=True, + help='COPYBOOK 格納ディレクトリ') + parser.add_argument('--db-md', required=True, + help='DB 定義書 .md のパス') + parser.add_argument('--output', default='output', + help='出力ディレクトリ (デフォルト: output)') + parser.add_argument('--api-key', default='sk-6156cccdc9c14d949cf5bfc5afc67a03', + help='DeepSeek API Key') + parser.add_argument('--model', default='deepseek-v4-flash', + help='API モデル名') + parser.add_argument('--rules', default=DEFAULT_RULES_DIR, + help='ルール格納ディレクトリ (デフォルト: rules/)') + + args = parser.parse_args() + + # 验证输入文件存在 + for name, path in [('--design', args.design), ('--source', args.source)]: + if not os.path.exists(path): + print(f"エラー: {name} のファイルが見つかりません: {path}", file=sys.stderr) + sys.exit(1) + + result = generate( + design_md=args.design, + source_cbl=args.source, + file_db_md=args.file_db_md, + cpy_dir=args.cpy, + db_md=args.db_md, + output_dir=args.output, + api_key=args.api_key, + api_model=args.model, + rules_dir=args.rules, + ) + + print(f"\n== 完了 ==") + print(f"プログラムID: {result['program_id']}") + print(f"グループ数: {result['groups']}") + print(f"入力タイプ: {result['input_type']}") + print(f"出力ファイル:") + for key, path in sorted(result['output_files'].items()): + print(f" {key}: {path}") + + +if __name__ == '__main__': + main() +``` + +- [ ] **Step 3: 写集成测试(跳过实际 API 调用)** + +```python +# tests/test_integration.py +import os +import tempfile +from unittest.mock import patch, MagicMock +from agent import generate + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +@patch('agent.api_client.requests.post') +def test_full_pipeline_mock_api(mock_post): + """完整流水线测试,API 调用使用 mock。""" + # Mock API response + mock_response = MagicMock() + mock_response.json.return_value = { + 'choices': [{'message': {'content': '''{ + "groups": { + "g1": { + "program": "ZAN04MAT", + "records": [ + { + "input": { + "R01INNFIL": { + "R01-APPL-ID": "A0000001", + "R01-EMP-ID": "00000101", + "R01-APPL-DATE": "20260101", + "R01-START-TIME": "0900", + "R01-END-TIME": "1800", + "R01-STATUS": "0", + "R01-OVT-TYPE": "W", + "R01-FILLER": "D000000000000000000000000000000000000000000001" + } + } + } + ] + } + } + }'''}}] + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + cpy_dir = FIXTURE_DIR + output_dir = tempfile.mkdtemp() + rules_dir = os.path.join(os.path.dirname(__file__), '..', 'rules') + + design_md = os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md') + source_cbl = os.path.join(FIXTURE_DIR, '..', '..', '..', + 'cobol-tna-system', 'src', 'ZAN04MAT.cbl') + + # 如果 source_cbl 不存在,创建一个最小副本 + if not os.path.exists(source_cbl): + source_cbl = 'dummy.cbl' + with open(source_cbl, 'w', encoding='utf-8') as f: + f.write("COPY ZAN01REC REPLACING ==(A)== BY ==R01==.\n") + + try: + result = generate( + design_md=design_md, + source_cbl=source_cbl, + file_db_md='dummy.md', + cpy_dir=cpy_dir, + db_md='dummy_db.md', + output_dir=output_dir, + api_key='test-key', + rules_dir=rules_dir, + ) + + assert result['program_id'] == 'ZAN04MAT' + assert result['groups'] > 0 + assert len(result['output_files']) > 0 + + # 验证输出文件存在 + for path in result['output_files'].values(): + assert os.path.exists(path), f"输出文件不存在: {path}" + + finally: + if source_cbl == 'dummy.cbl': + os.remove(source_cbl) +``` + +- [ ] **Step 4: 运行集成测试** + +Run: `python -m pytest tests/test_integration.py -v` +Expected: PASS + +- [ ] **Step 5: 运行全部测试** + +Run: `python -m pytest tests/ -v` +Expected: all tests PASS + +--- + +### Task 11: 首次实际运行验证 + +- [ ] **Step 1: 用 ZAN04MAT 实际运行** + +```powershell +python main.py ` + --design "D:\cobol-tna-system\詳細設計書\詳細設計書_ZAN04MAT.md" ` + --source "D:\cobol-tna-system\src\ZAN04MAT.cbl" ` + --file-db-md "D:\cobol-tna-system\詳細設計書\COPY句定義書.md" ` + --cpy "D:\cobol-tna-system\cpy" ` + --db-md "D:\cobol-tna-system\詳細設計書\DB定義書.md" ` + --output "D:\jcl-cobol-data-create\output" +``` + +- [ ] **Step 2: 验证输出** + +确认 `output/ZAN04MAT/` 下生成了 g1、g2、g3 三个文件夹,每个文件夹中有对应的 JSON 文件。 + +- [ ] **Step 3: 验证 JSON 格式** + +检查 JSON 文件是否符合 `JSON格式说明v2.0.md` 规范。 diff --git a/black-box-data-create/docs/superpowers/specs/2026-07-12-testdata-agent-design.md b/black-box-data-create/docs/superpowers/specs/2026-07-12-testdata-agent-design.md new file mode 100644 index 0000000..7870694 --- /dev/null +++ b/black-box-data-create/docs/superpowers/specs/2026-07-12-testdata-agent-design.md @@ -0,0 +1,319 @@ +# COBOL 测试数据生成 Agent 设计文档 + +## 背景 + +旧 COBOL 项目通过 Java 重写。需要基于 Python 新建一个测试数据生成 Agent,根据详细设计书自动生成 COBOL 程序的测试数据(JSON/SQL),通过 DeepSeek v4 Flash API 由 AI 生成具体数据内容。 + +--- + +## 架构概览 + +采用**线性管道架构**,模块化设计,便于后续追加规则和维护: + +``` +InputParser → RuleLoader → PromptBuilder → APIClient → OutputWriter +``` + +- **Python 侧**:负责解析结构化信息、建立映射关系、组装 prompt +- **AI 侧**:接收 prompt,根据规则生成具体的测试数据 JSON/SQL + +--- + +## 目录结构 + +``` +D:\jcl-cobol-data-create/ +├── agent/ +│ ├── __init__.py +│ ├── input_parser.py # 输入解析器 +│ ├── rule_loader.py # 规则加载器 +│ ├── prompt_builder.py # Prompt 构建器 +│ ├── api_client.py # API 客户端(DeepSeek) +│ └── output_writer.py # 输出写入器 +├── rules/ +│ ├── pgm_pattern/ # PGM模式规则 .md +│ │ ├── マッチング(1-1).md +│ │ ├── マッチング(1-N).md +│ │ └── ...(后续追加,无需改代码) +│ └── special_feature/ # 特殊功能规则 .md +│ └── 条件分支.md +├── layout/ # 文档模板(参考用) +│ └── JSON格式说明v2.0.md +├── output/ # 生成输出(运行时自动创建) +│ └── {プログラムID}/ +│ ├── g1/ +│ └── g2/ +├── main.py # CLI 入口 + generate() 库函数 +├── requirements.txt +└── README.md +``` + +--- + +## 模块详细设计 + +### 1. InputParser(输入解析器) + +**职责**:解析所有输入文件,提取结构化数据。 + +**输入**: +- `a` 詳細設計書 .md +- `b` ソース .cbl 文本文件 +- `c` 文件/DB 构造 .md(文件定義書 + COPY句定義書) +- `d` COPY 存放路径 +- `e` DB 结构 .md + +**输出**:结构化的程序元数据对象,包含: + +#### 1.1 基本情報 +- プログラムID, プログラム名, PGMタイプ(メイン/サブ), PGMパターン +- 解析方法:定位 `# 行` 列 = `PGMパターン` 的行,取 `内容` 列的值 + +#### 1.2 使用ファイル一覧 +- 每个文件的:識別子, DD名, I/O, COPY群, 媒体(PS=文件, DB=DB) +- 判定输入类型:遍历 I/O="I" 的行,根据 `媒体` 列判定 + - 全是 PS → 文件输入 + - 包含 DB → DB 输入 + - 都有 → 混合输入 + +#### 1.3 COPYBOOK 解析 + REPLACING 映射 +- 从 .cbl 源码的 FD 块中提取 `COPY xxx REPLACING ==(A)== BY ==識別子==` +- 建立映射:`識別子(R01) → COPY名(ZAN01REC) → 替换前缀(R01-)` +- 读取 COPYBOOK 文件,应用 `(A)` → 前缀替换,得到实际字段名和 PIC 定义 +- 输出示例:`R01-APPL-ID | X(8) | 8` + +#### 1.4 キー項目一覧 +- 排序条件、匹配键信息 + +#### 1.5 処理詳細 +- 完整原文(不做解析,保留原样) + +#### 1.6 出力レコード定義 +- 输出文件的项目定义和设定元信息 + +#### 1.7 DB 定义(如涉及) +- 从 DB 定义书 .md 中提取表的字段定义、类型、PK 信息 + +--- + +### 2. RuleLoader(规则加载器) + +**职责**:根据程序特征匹配对应的数据生成规则。 + +#### 2.1 PGM 模式规则匹配 +PGMパターン 值到规则文件名的映射: + +| PGMパターン | 规则文件 | +|------------|---------| +| マッチング(1:1) | マッチング(1-1).md | +| マッチング(1:N) | マッチング(1-N).md | +| マッチング(M:N) | (待添加) | +| レイアウト編集のみ(GETPUT) | (待添加) | +| 項目チェック | (待添加) | +| 振り分け | (待添加) | +| キーブレイク | (待添加) | +| キーブレイク(集計、集約) | (待添加) | +| DB更新 | (待添加) | + +- 未找到对应规则 → 报错并列出缺失的模式名 +- 读取匹配到的 .md 全文 +- 从规则中解析:组数、每组用途、每组的数据生成方法 + +#### 2.2 特殊功能检测 +扫描 `処理詳細` 文本,根据预设关键词自动检测: + +| 特殊功能 | 检测关键词 | 规则文件 | +|---------|-----------|---------| +| 条件分支 | `場合`, `EVALUATE`, `IF` | 条件分支.md | + +- 扩展方式:在检测表中追加行,同时在 `rules/special_feature/` 下添加对应 .md + +--- + +### 3. PromptBuilder(Prompt 构建器) + +**职责**:将前两个模块的所有信息组装成结构化 API prompt。 + +**Prompt 结构**: + +``` +## 程序基本情報 +- 程序ID: ZAN04MAT +- 程序名: 取消マッチング処理 +- PGMパターン: マッチング(1:1) +- 输入类型: 文件 + +## 処理詳細 +(詳細設計書の処理詳細全文) + +## 入力ファイル/DB 構造 +### 文件R01 (DD名: ZAN04R01, COPY: ZAN01REC) +| 字段名 | PIC | 字节数 | +| R01-APPL-ID | X(8) | 8 | +| R01-EMP-ID | X(8) | 8 | +... + +## DBテーブル構造(如有) +### 表名: LEAVE_RECORDS +| 字段名 | 类型 | 最大长 | KEY | +... +- 主键: APPLICATION_ID + +## 出力レコード定義 +(詳細設計書の出力レコード定義全文) + +## データ生成ルール +(匹配到的PGM模式规则 .md 全文) + +## 特殊機能ルール(如有) +(条件分支.md 全文) + +## 出力形式 +- 文件输入类型 → JSON格式(遵循以下规则) +- DB输入类型 → SQL INSERT语句 + +### 字段值规则(PIC → JSON表示) +| PIC | JSON表示 | 例 | +|-----|---------|-----| +| PIC X(n) | 左对齐 + 空格填充 | "A0000001" | +| PIC 9(n) | 右对齐 + 前补零 | "00000101" | +| PIC S9(n) COMP-3 | 十进制数字 | "1234" | +| PIC S9(n) COMP | 十进制数字 | "300" | +| FILLER(纯保留) | 有辨识性模式 | "D000...001" | +| FILLER(业务保留) | 全空格或全零 | | + +- COMP/COMP-3 类型字段输出**普通十进制数字符串**,不做特殊转换 +- 字段名含语义关键词时(如 DATE="日期"、NAME="姓名")生成符合实际含义的值 + +## 生成指示 +- 需要生成的グループ数: 3 +- 各グループの内容: + - g1: 两端不匹配 + - g2: 反向两端不匹配 + - g3: 中间不匹配 +- 出力: JSON(文件输入) +``` + +--- + +### 4. APIClient(API 客户端) + +**职责**:调用 DeepSeek API,含重试逻辑。 + +**API 配置**: +- 地址:https://api.deepseek.com/chat/completions +- 模型:`deepseek-v4-flash` +- API Key:`sk-6156cccdc9c14d949cf5bfc5afc67a03` +- timeout:120 秒 + +**请求结构**: +```json +{ + "model": "deepseek-v4-flash", + "messages": [ + { + "role": "system", + "content": "你是COBOL程序测试数据生成专家。请严格按照规则生成测试数据。输出必须是可直接解析的JSON,不要包在markdown代码块中。" + }, + { + "role": "user", + "content": "" + } + ], + "temperature": 0.3, + "max_tokens": 8192 +} +``` + +**重试逻辑**(最多 3 次): +``` +for i in 1..3: + 发送请求 + if 网络错误 || API返回错误: + continue # 重试 + if JSON解析失败: + 将错误信息追加到prompt中,重试 + if 成功: + break +``` +- 3 次全部失败 → 报错退出 + +--- + +### 5. OutputWriter(输出写入器) + +**职责**:将 AI 返回的 JSON/SQL 保存为文件。 + +**输出规则**: +- 文件输入 → `output/{プログラムID}/g{组号}/{プログラムID}_g{组号}.json` +- DB 输入 → `output/{プログラムID}/g{组号}/{プログラムID}_g{组号}.sql` +- 混合输入 → 同文件夹下同时输出 `.json` 和 `.sql` + +**输出格式**: +- JSON:遵循 `JSON格式说明v2.0.md` 规范,每个文件内含 `records` 数组 +- SQL:标准 INSERT 语句 + +``` +output/ZAN04MAT/ +├── g1/ +│ └── ZAN04MAT_g1.json +├── g2/ +│ └── ZAN04MAT_g2.json +└── g3/ + └── ZAN04MAT_g3.json +``` + +**验证**:写入前校验 JSON 合法性,不合法时返回错误给 APIClient 触发重试。 + +--- + +### 6. main.py(入口) + +**CLI 调用**: +```bash +python main.py \ + --design "詳細設計書_ZAN04MAT.md" \ + --source "src/ZAN04MAT.cbl" \ + --cpy "cpy/" \ + --db-def "DB定義書.md" \ + --output "output/" +``` + +**库调用**: +```python +from agent import generate + +result = generate( + design_md="詳細設計書_ZAN04MAT.md", + source_cbl="src/ZAN04MAT.cbl", + cpy_dir="cpy/", + db_def_md="DB定義書.md", + output_dir="output/" +) +``` + +--- + +## 依赖 + +- Python 3.9+ +- `requests` — HTTP 客户端 +- `openai` 或直接用 `requests` 调用 DeepSeek 兼容 API + +``` +requests>=2.28.0 +``` + +--- + +## 扩展指南 + +### 追加 PGM 模式规则 +1. 在 `rules/pgm_pattern/` 下创建新的 .md 文件 +2. 文件名与 PGMパターン 值按约定匹配 +3. 无需修改 Python 代码 + +### 追加特殊功能检测 +1. 在 RuleLoader 的检测映射表中追加一行 `(关键词列表, 对应规则文件)` +2. 在 `rules/special_feature/` 下创建对应 .md +3. 需要改一行 Python 代码(添加映射条目) diff --git a/black-box-data-create/docs/未実装PGMパターン洗い出し_第一版.md b/black-box-data-create/docs/未実装PGMパターン洗い出し_第一版.md new file mode 100644 index 0000000..d320b97 --- /dev/null +++ b/black-box-data-create/docs/未実装PGMパターン洗い出し_第一版.md @@ -0,0 +1,184 @@ +# 未実装PGMパターン洗い出し(第一版) + +> **実装状況(2026-08-04 更新)**:本一覧の ❌/⚠️ 項目はすべて実装済み。 +> 新規規則ファイル:`25分割.md`・`100分割.md`・`2段階マッチング.md`・`内部テーブル検索.md`・`サブプログラム.md`・`ASCII→EBCDIC変換.md`・`キーブレイク(非集計).md`・`1-N+キーブレイク(異キー).md`・`SORT.md`・`オンラインPGM.md`・`SYSIN読込.md`・`ランキング生成.md`(分類表外のNo.36) +> `agent/rule_loader.py` の PGM_PATTERN_MAP に全タイプの正式名称・詳細設計書表記のエイリアスを追加。`CSV→FB変換.md`(改行なし)を内容補強、`項目チェック` は 重複含まず/半角20桁-4桁 に分割。 +> テスト:`tests/test_rule_loader.py` に35タイプ全量+詳細設計書表記の解決テストを追加し、全101件パス。 + +> 目的:`rules/pgm_pattern` に未実装のプログラムタイプを洗い出す。 +> 出典:`D:\cobol-tna-system\品質管理\程序分类说明.md`(HINA基准33类型+追加2类型=計35类型) +> テスト基準:`D:\cobol-tna-system\品質管理\测试基准说明.md` §2.2 类型别测试基准 +> 現状確認:`D:\jcl-cobol-data-create\rules\pgm_pattern`(16ファイル)+ `agent/rule_loader.py` の PGM_PATTERN_MAP + +## 凡例 + +| 記号 | 意味 | +|------|------| +| ✅ | 実装済み(規則ファイル+マッピングあり) | +| ⚠️ | 部分実装/内容は既存規則でカバーできるがマッピング・内容が不足 | +| ❌ | 未実装(規則ファイルなし) | + +## 一、35类型总体状态 + +| No. | 类型(日) | 类型(中) | 分類 | 状態 | 現状規則 | +|-----|-----------|-----------|------|:---:|---------| +| 01 | マッチング(1:1) | 1:1匹配 | 匹配 | ✅ | マッチング(1-1).md | +| 02 | マッチング(1:N) | 1:N匹配 | 匹配 | ✅ | マッチング(1-N).md | +| 03 | マッチング(N:1) | N:1匹配 | 匹配 | ✅ | マッチング(N-1).md | +| 04 | レイアウト編集のみ(GETPUT) | 编辑输出 | 编辑 | ✅ | レイアウト編集のみ(GETPUT).md/GETPUT(編集出力).md | +| 05 | 振り分け(IF文) | IF分支 | 分支 | ✅ | 振り分け.md | +| 06 | 振り分け(EVALUATE文) | EVALUATE分支 | 分支 | ✅ | 振り分け.md(+special_feature/条件分支.md) | +| 07 | キーブレイク(集計) | key切汇总 | key切 | ✅ | キーブレイク(集計).md | +| 08 | キーブレイク(集約) | key切聚合 | key切 | ✅ | キーブレイク(集約).md | +| 09 | DB更新 | 数据库更新 | 数据库 | ✅ | DB更新.md | +| 10 | 50分割 | 50分割 | 分割 | ✅ | 50分割.md | +| 11 | 25分割 | 25分割 | 分割 | ❌ | なし(50分割.mdは注記のみ) | +| 12 | 100分割 | 100分割 | 分割 | ❌ | なし | +| 13 | 項目チェック(重複含まず) | 字段校验(不含重复) | 校验 | ✅ | 項目チェック(重複含まず).md | +| 14 | オンラインPGM | online程序 | online | ❌ | なし | +| 15 | CSV→FB変換(改行なし) | CSV→FB(无换行) | 文件转换 | ⚠️ | CSV→FB変換.md(无换行STRING合并の明示なし) | +| 16 | 2段階マッチング(1:1⇒1:1) | 二级1:1→1:1 | 匹配 | ❌ | なし | +| 17 | 2段階マッチング(N:1⇒N:1) | 二级N:1→N:1 | 匹配 | ❌ | なし | +| 18 | マッチングM:N⇒出力M件 | 组合M:N→M条 | 匹配 | ⚠️ | 内容はマッチング(M-N).mdに有/名称マッピング不足 | +| 19 | マッチングM:N⇒出力N件 | 组合M:N→N条 | 匹配 | ⚠️ | 内容はマッチング(M-N).mdに有/名称マッピング不足 | +| 20 | マッチングM:N⇒出力M×N件 | 组合M:N→M×N条 | 匹配 | ⚠️ | 内容はマッチング(M-N).mdに有/名称マッピング不足 | +| 21 | CSV→FB変換(改行あり) | CSV→FB(有换行) | 文件转换 | ✅ | CSV→FB変換.md | +| 22 | 2段階マッチング(M:N⇒M:N) | 二级M:N→M:N | 匹配 | ❌ | なし | +| 23 | SELECT条件 | SELECT条件 | 数据库 | ✅ | SELECT処理.md | +| 24 | 内部テーブル検索 | 内部表检索 | 内部处理 | ❌ | なし | +| 25 | サブプログラム使用 | 子程序调用 | 内部处理 | ❌ | なし(PGMタイプ=サブは generate() で ValueError) | +| 26 | DB検索 | 数据库检索 | 数据库 | ⚠️ | SELECT処理.md で内容はほぼカバー/名称マッピングなし | +| 27 | 項目チェック(半角20桁/4桁) | 半角校验 | 校验 | ✅ | 項目チェック(半角20桁-4桁).md | +| 28 | SYSIN読込 | SYSIN读取 | 内部处理 | ⚠️ | 独立規則なし(DB更新.md に紐づくが内容はSYSIN未対応) | +| 29 | ASCII→EBCDIC変換 | ASCII→EBCDIC转换 | 文件转换 | ❌ | なし | +| 30 | キーブレイク(集計集約以外) | key切非汇总 | key切 | ❌ | なし(MAPは集計.mdへの仮対応のみ) | +| 31 | 項目チェック(重複含む) | 字段校验(含重复) | 校验 | ✅ | 項目チェック(重複含まず).md(重複チェック手順あり) | +| 32 | 1:N+キーブレイク(同キー) | 1:N+同key切 | 混合 | ✅ | キーブレイク(集計).md | +| 33 | 1:N+キーブレイク(異キー) | 1:N+异key切 | 混合 | ❌ | なし | +| 34 | SORT(INPUT/OUTPUT PROCEDURE) | 排序处理 | 排序 | ❌ | なし | +| 35 | MERGE(複数ファイル結合) | 合并处理 | 排序 | ✅ | MERGE.md | + +**集計:✅ 16種 / ⚠️ 7種 / ❌ 12種(計35種)** + +--- + +## 二、未実装类型(12種)明细 + +### 2.1 分割系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 11 | 25分割 | S-N004(整数倍)、S-A001/A002、S-R001/R002 | 規則ファイルなし。50分割.md の注記「25/100も同じ考え方」のみで、分割数がハードコードされている。 | +| 12 | 100分割 | S-N005(整数倍)、S-A001/A002、S-R001/R002 | 同上。 | + +**建议**:50分割.md を「分割数パラメータ化」した共通規則(`分割処理.md`)に改修するか、`25分割.md`/`100分割.md` を新規作成し PGM_PATTERN_MAP に追加。 + +### 2.2 匹配系(2段階) + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 16 | 2段階マッチング(1:1⇒1:1) | AM-N001、AM-A001/A002、AM-R001 | 中間ファイルを挟む2段階マッチングの規則なし。 | +| 17 | 2段階マッチング(N:1⇒N:1) | AM-N002、AM-A001/A002、AM-R001 | 同上。 | +| 22 | 2段階マッチング(M:N⇒M:N) | AM-N008、AM-A001/A002、AM-R001 | 同上。 | + +**建议**:`2段階マッチング.md` を新規作成(または16/17/22を1規則でパターン別グループ化)。中間ファイルOPEN失敗・段間キー不一致・各段の出力件数確認を含める。 + +### 2.3 内部处理系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 24 | 内部テーブル検索 | T-N001〜T-N007、T-A001〜T-A003、T-R001 | SEARCH/SEARCH ALL/OCCURS DEPENDING ON/INDEX越界の規則なし。KIN01INP では振り分け内部の一部として使われたのみ。 | +| 25 | サブプログラム使用 | C-N001〜C-N009、C-A001〜C-A004、C-R001/R002 | 規則なし。さらに `agent/models.py`/`main.py` の generate() で PGMタイプ=サブ は ValueError でスキップされるため、先にこの制約を外す必要あり。 | + +**建议**:`内部テーブル検索.md`・`サブプログラム.md` を新規作成。サブプログラムはCALLER側(CALL)とCALLEE側(LINKAGE/EXIT PROGRAM)の両ロールを定義。 + +### 2.4 文件转换系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 29 | ASCII→EBCDIC変換 | AE-N001〜AE-N003、AE-A001〜AE-A003、AE-R001 | 変換規則なし。SHA01CVT(詳細設計書あり)が該当。 | + +**建议**:`ASCII-EBCDIC変換.md` を新規作成。変換表にない文字・往復一致性・レコード長一致を含める。 + +### 2.5 key切系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 30 | キーブレイク(集計集約以外) | KB-N003(鍵変化でマークのみ)、KB-N004/N005/N006、KB-R001 | 非集計型(鍵切替時の変化検知)の規則なし。PGM_PATTERN_MAP の `'キーブレイク(集計、集約の以外)' → キーブレイク(集計).md` は内容が対応していない仮マッピング。 | + +**建议**:`キーブレイク(非集計).md` を新規作成し、仮マッピングを置き換える。 + +### 2.6 混合系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 33 | 1:N+キーブレイク(異キー) | AM-N007 | 同キー(No.32)は実装済みだが、異キー版の規則なし。SHA07KBR(詳細設計書あり)が該当。 | + +**建议**:`1-N+キーブレイク(異キー).md` を新規作成済み(No.33)。No.32(同キー)は キーブレイク(集計).md にマッピング。 + +### 2.7 排序系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 34 | SORT(INPUT/OUTPUT PROCEDURE) | SR-N001〜SR-N010、SR-A001〜SR-A003、SR-R001〜SR-R003 | SORT文の規則なし。SHA08SRT・JIN06SRT(詳細設計書あり)が該当。 | + +**建议**:`SORT.md` を新規作成。SORT ON KEY/INPUT PROCEDURE(RELEASE)/OUTPUT PROCEDURE(RETURN)/RC確認を含める。 + +### 2.8 online系 + +| No. | 类型 | 对应测试基准(§2.2) | 现状与课题 | +|-----|------|----------------------|-----------| +| 14 | オンラインPGM | OL-N001〜OL-N004、OL-A001〜OL-A003、OL-R001 | CICS系(DFHCOMMAREA/MAP)の規則なし。現システムに該当プログラムなし(将来拡張用)。 | + +**建议**:`オンラインPGM.md` を新規作成(または保留)。DFHCOMMAREA長不一致・ROLLBACK・応答時間を含める。 + +--- + +## 三、部分実装/マッピング不足(7種) + +| No. | 类型 | 不足内容 | 対応方針 | +|-----|------|---------|---------| +| 15 | CSV→FB変換(改行なし) | 規則はfallbackでマッチするが、无换行(STRING合并、CF-N001)が明示されていない | CSV→FB変換.md に「改行なし(STRING合并)」グループを明示追加 | +| 18 | マッチングM:N⇒出力M件 | 内容はマッチング(M-N).md 组3にあるが、設計書のパターン名(例:`18(M:N→M件マッチング)`)がマッチしない | PGM_PATTERN_MAP にエイリアス追加 | +| 19 | マッチングM:N⇒出力N件 | 内容は组4にあるが、名称マッチしない | 同上 | +| 20 | マッチングM:N⇒出力M×N件 | 内容は组2(直積)にあるが、名称マッチしない | 同上 | +| 26 | DB検索 | SELECT処理.md で DB-N001/N002/N006 はほぼカバー | PGM_PATTERN_MAP に `DB検索` → SELECT処理.md のエイリアス追加 | +| 27 | 項目チェック(半角20桁/4桁) | 半角20桁/4桁境界(VF-N005/N006/A001/A002)の専用規則なし | 項目チェック(半角20桁-4桁).md を新規作成済み | +| 28 | SYSIN読込 | KIN08DBU は DB更新.md に紐づくが、SYSIN読込自体(SY-N001〜SY-R001)の規則がない | `SYSIN読込.md` を新規作成、または DB更新.md にSYSINグループを追加 | + +--- + +## 四、実装済み类型(16種)一覧 + +| No. | 类型 | 規則ファイル | +|-----|------|-------------| +| 01 | マッチング(1:1) | マッチング(1-1).md | +| 02 | マッチング(1:N) | マッチング(1-N).md | +| 03 | マッチング(N:1) | マッチング(N-1).md | +| 04 | レイアウト編集のみ(GETPUT) | レイアウト編集のみ(GETPUT).md/GETPUT(編集出力).md | +| 05 | 振り分け(IF文) | 振り分け.md | +| 06 | 振り分け(EVALUATE文) | 振り分け.md(+special_feature/条件分支.md) | +| 07 | キーブレイク(集計) | キーブレイク(集計).md | +| 08 | キーブレイク(集約) | キーブレイク(集約).md | +| 09 | DB更新 | DB更新.md | +| 10 | 50分割 | 50分割.md | +| 13 | 項目チェック(重複含まず) | 項目チェック(重複含まず).md | +| 21 | CSV→FB変換(改行あり) | CSV→FB変換.md | +| 23 | SELECT条件 | SELECT処理.md | +| 31 | 項目チェック(重複含む) | 項目チェック(重複含まず).md | +| 32 | 1:N+キーブレイク(同キー) | キーブレイク(集計).md | +| 35 | MERGE(複数ファイル結合) | MERGE.md | + +--- + +## 五、実装優先度(案) + +| 優先度 | 类型 | 理由 | +|:---:|------|------| +| P1 | 11・12(25/100分割)、34(SORT) | 詳細設計書あり(SHA09S25/SHA10S10/SHA08SRT/JIN06SRT)、且つ25/100は50分割の応用でコスト低 | +| P1 | 16・17・22(2段階マッチング)、33(1:N+異キー) | 詳細設計書あり(SHA04TWO/SHA05TWN/SHA06TWM/SHA07KBR)、マッチング系の拡張 | +| P2 | 24(内部テーブル検索)、25(サブプログラム) | 内部処理系。25は generate() の制約解除が必要 | +| P2 | 29(ASCII→EBCDIC) | 詳細設計書あり(SHA01CVT)、専用変換ロジック | +| P2 | 30(キーブレイク非集計) | 仮マッピングの是正を兼ねる | +| P3 | 15・18・19・20・26・27・28 | 内容は既存規則でほぼカバー、エイリアス/グループ追加で対応可能 | +| P3 | 14(オンライン) | 現システムに該当プログラムなし、保留可 | diff --git a/black-box-data-create/layout/DB定義書.md b/black-box-data-create/layout/DB定義書.md new file mode 100644 index 0000000..34a4ecd --- /dev/null +++ b/black-box-data-create/layout/DB定義書.md @@ -0,0 +1,347 @@ +# DB定義書 + +## 変更履歴 + +| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 | +|----|---------|--------|--------|--------|------| +| 1 | 新規作成(全8テーブル) | AI | 2026/06/23 | | サブシステムA(6) + B(2) | +| 2 | SALARYDB 4テーブル追加 | AI | 2026/07/08 | | サブシステムC(給与計算) | + +--- + +# EMP_MASTER — 社員マスタ + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | EMP_MASTER | — | PK: EMP_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 部署ID | DEPT_ID | CHAR | 4 | | 4 | NOT NULL | | | | | | | +| 3 | 氏名 | EMP_NAME | VARCHAR | 50 | | 50 | NOT NULL | | | | | | | +| 4 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=在籍, '9'=退職 | + +### 備考 + +- サブシステムA: 参照のみ(SELECT) +- サブシステムB: 使用しない + +--- + +# LEAVE_RECORDS — 休暇申請記録 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | LEAVE_RECORDS | — | PK: APPLICATION_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 申請番号 | APPLICATION_ID | INTEGER | 10 | | 4 | NOT NULL | | ✓ | | | | 自動採番 | +| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | | +| 3 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | | | | | 01/02/03/04 | +| 4 | 開始日 | START_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 6 | 終了日 | END_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 7 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=有効, '9'=取消 | + +### 備考 + +- KIN02UPDがINSERT/DELETEを実行 +- KIN03EXPがSELECT(日付展開用) +- 取消はDELETE FROM で物理削除 + +--- + +# HOLIDAY_CALENDAR — 休日カレンダー + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | HOLIDAY_CALENDAR | — | PK: HOLIDAY_DATE | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 休日日付 | HOLIDAY_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD | +| 2 | 説明 | DESCRIPTION | VARCHAR | 50 | | 50 | NULL許可 | NULL | | | | | 例:「建国記念の日」 | + +### 備考 + +- 土日は曜日判定で処理するため、このテーブルには祝日のみ格納 +- KIN03EXP, KIN06CLDがSELECTで参照 + +--- + +# SICK_LEAVE_RATE — 病欠控除率 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | SICK_LEAVE_RATE | — | PK: LEAVE_TYPE | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | ✓ | | | | '04'固定 | +| 2 | 控除率 | DEDUCTION_RATE | DECIMAL | 3 | 2 | 3 | NOT NULL | | | | | | 例:0.50(50%控除) | + +### 備考 + +- 全社員共通の設定値 +- 現時点では参照プログラム未実装(設計上定義) + +--- + +# DAILY_RECORDS — 日別勤怠記録 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | DAILY_RECORDS | — | PK: (EMP_ID, TARGET_DATE) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象日 | TARGET_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD | +| 3 | 出勤時刻 | TIME_IN | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし | +| 4 | 退勤時刻 | TIME_OUT | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし | +| 5 | 年休時間 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 6 | 事假時間 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 7 | 因公特批假時間 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 8 | 病欠時間 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 9 | 未申請欠勤時間 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 10 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP | + +### 備考 + +- KIN08DBUがKIN07DAIの出力ファイルからINSERT +- 1社員1日=1レコード。出勤日のみ存在。 + +--- + +# MONTHLY_ABSENCE — 月次統計 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | MONTHLY_ABSENCE | — | PK: (EMP_ID, YEAR_MONTH) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 年休合計 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 4 | 事假合計 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 5 | 因公特批假合計 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 6 | 病欠合計 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 7 | 未申請欠勤合計 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 8 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP | + +### 備考 + +- KIN08DBUが月次集計後にUPSERT(MERGE) +- サブシステムC(給与計算)への連携元 + +--- + +# OVT_APPLICATIONS — 個別加班申請テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 残業統計管理DB | OVT_APPLICATIONS | — | PK: APPL_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 申請番号 | APPL_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD+SEQ | +| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | | +| 3 | 申請日 | APPL_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 4 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | | | | | W=平日, H=休日 | +| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 6 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 7 | 加班時間 | OVT_HOURS | DECIMAL | 4 | 1 | 4 | NOT NULL | | | | | | 0.1h単位 | +| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | 0=有効, 9=取消 | +| 9 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | | + +### 備考 + +- ZAN06UPDがINSERT/UPDATEを実行 +- STATUS=9で取消(物理削除はしない) + +--- + +# OVT_MONTHLY — 月次集計テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 残業統計管理DB | OVT_MONTHLY | — | PK: (EMP_ID, YEAR_MONTH, OVT_TYPE) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | ✓ | | | | W=平日, H=休日 | +| 4 | 加班時間合計 | OVT_HOURS | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 5 | 加班回数 | OVT_COUNT | INTEGER | 10 | | 4 | NOT NULL | | | | | | | +| 6 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | | + +### 備考 + +- ZAN06UPDが月次集計結果をUPSERT + +--- + +# EMP-MASTER(SALARYDB) — 社員マスタ(給与計算用) + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | EMP-MASTER | KYU01REC | PK: EMP-ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 氏名 | EMP-NAME | VARCHAR | 40 | | 40 | NOT NULL | | | | | | | +| 3 | 部署コード | DEPT-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | | +| 4 | 地域コード | REGION-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | | +| 5 | 職種コード | CATEGORY-CODE | CHAR | 3 | | 3 | NOT NULL | | | | | | | +| 6 | 基本給 | BASE-SALARY | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 7 | 時給 | HOURLY-RATE | DECIMAL | 7 | 0 | 4 | NOT NULL | | | | | | | +| 8 | 扶養人数 | DEPENDENT-COUNT | SMALLINT | — | | 2 | NOT NULL | | | | | | | +| 9 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '0'=在籍, '9'=退職 | +| 10 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU02REGがINSERT/UPSERTを実行 +- KYU04CALがSELECTで参照 + +--- + +# TAX-TABLE(SALARYDB) — 所得税率テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | TAX-TABLE | — | PK: (TAX-FROM, TAX-TO) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 課税下限 | TAX-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 2 | 課税上限 | TAX-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 3 | 税率 | TAX-RATE | DECIMAL | 5 | 4 | 3 | NOT NULL | | | | | | | +| 4 | 控除額 | DEDUCTION | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | + +### 備考 + +- KYU05DEDがSELECT ... BETWEENで参照 +- 課税所得に対する超過累進税率 + +--- + +# INSURANCE-TABLE(SALARYDB) — 社会保険料率テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | INSURANCE-TABLE | — | PK: (INS-INCOME-FROM, INS-INCOME-TO) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 所得下限 | INS-INCOME-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 2 | 所得上限 | INS-INCOME-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 3 | 健康保険料率 | INS-HEALTH-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | | +| 4 | 厚生年金保険料率 | INS-PENSION-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | | +| 5 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU05DEDが内部テーブル(SEARCH ALL)に取込んで参照 +- 被保険者負担分のみ + +--- + +# SALARY-RESULTS(SALARYDB) — 給与計算結果テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | SALARY-RESULTS | — | PK: (EMP-ID, YEAR-MONTH) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR-MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 総支給額 | GROSS-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 4 | 源泉所得税 | INCOME-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 5 | 社会保険料 | INSURANCE | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 6 | 住民税 | RESIDENT-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 7 | 差引支給額 | NET-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 8 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU06UPDがINSERT/UPSERTを実行 diff --git a/black-box-data-create/layout/JSON格式说明v2.0.md b/black-box-data-create/layout/JSON格式说明v2.0.md new file mode 100644 index 0000000..9063a7d --- /dev/null +++ b/black-box-data-create/layout/JSON格式说明v2.0.md @@ -0,0 +1,435 @@ +# cobol_testgen v4 生成 JSON — 格式说明 v2.0 + +## 修订履历 + +| 版本 | 日期 | 变更内容 | +|------|------|---------| +| v2.0 | | 明确1个json文件=1组数据(group)的命名规则,格式本身与v1相同 | + +## 说明 + +v2.0 **格式本身与v1完全一致**,仅补充: +1. 一组数据单独放一个json文件 +2. 一个文件内的`records`数组包含该组的全部处理单元 +3. 命名规则追加 `_g{groupId}` 标识组号 + +--- + +## 完整规格 JSON(`output/json/{プログラム名}_g{groupId}.json`) + +### 最外层结构 + +```json +{ + "program": "SAN01MAT", + "records": [ + { + "input": { ... } + } + ] +} +``` + +**1条record = 1次处理单元**。例如匹配(1:1)时: +- 匹配成功:record内同时包含两个输入FD +- R01-only:record内只含R01的FD +- R02-only:record内只含R02的FD + +多个record按处理顺序排列,组成一组完整的测试数据。 + +--- + +## 例1:单文件CSV输入(1组1条记录) + +文件 `ZAN01CHK_g1.json`: + +```json +{ + "program": "ZAN01CHK", + "records": [ + { + "input": { + "R01INNFIL": { + "R01LINE": "A0000000, , , , ,0, , " + } + } + } + ] +} +``` + +## 例2:单文件二进制输入(1组多条记录) + +文件 `ZAN02CHK_g1.json`: + +```json +{ + "program": "ZAN02CHK", + "records": [ + { + "input": { + "R01INNFIL": { + "R01APPL-ID": "A0000001", + "R01EMP-ID": "00000101", + "R01APPL-DATE": "20000101", + "R01START-TIME":"0201", + "R01END-TIME": "0301", + "R01STATUS": "B", + "R01OVT-TYPE": "C", + "R01FILLER": "D000000000000000000000000000000000000000000001" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01APPL-ID": "A0000002", + "R01EMP-ID": "00000102", + "R01APPL-DATE": "20000102", + "R01START-TIME":"0202", + "R01END-TIME": "0302", + "R01STATUS": "C", + "R01OVT-TYPE": "D", + "R01FILLER": "D000000000000000000000000000000000000000000002" + } + } + } + ] +} +``` + +## 例3:多文件输入(与v1相同) + +文件 `ZAN03CHK_g1.json`: + +```json +{ + "program": "ZAN03CHK", + "records": [ + { + "input": { + "R01INNFIL": { + "R01APPL-ID": "A0000001", + "R01EMP-ID": "00000101", + "R01APPL-DATE": "20000101", + "R01START-TIME":"0201", + "R01END-TIME": "0301", + "R01STATUS": "B", + "R01OVT-TYPE": "C", + "R01FILLER": "D000000000000000000000000000000000000000000001" + }, + "R02INNFIL": { + "R02EMP-ID": "00000401", + "R02DATE": "20000101", + "R02TIME-IN": "0501", + "R02TIME-OUT": "0601", + "R02FILLER": "E0000000000000000000000000000000000000000000000000000001" + }, + "R03INNFIL": { + "R03HOLIDAY-DATE": "20000101", + "R03HOLIDAY-FLG": "F", + "R03FILLER": "G0000000000000000000000000000000000000000000000000000000000000000000001" + } + } + } + ] +} +``` + +## 例4:二文件匹配(マッチング1:1) + +v1中ZAN04MAT示例是1条record = 1次匹配处理。 + +对于匹配(1:1)测试,**1组数据 = 1个json文件**。1组内包含多条record(R01-only、R02-only、匹配),按处理顺序排列。 + +### 文件1:`SAN01MAT_g1.json` + +两端不匹配:先处理R01-only(a001) → 3次匹配(a002/b002, a003/b003, a004/b004) → 最后R02-only(a005)。 + +```json +{ + "program": "SAN01MAT", + "records": [ + { + "input": { + "R01INNFIL": { + "R01A001": "a001", + "R01A002": "b001", + "R01A003": "c001", + "R01DATA": "c00000000011" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a002", + "R01A002": "b002", + "R01A003": "c002", + "R01DATA": "c00000000012" + }, + "R02INNFIL": { + "R02A001": "a002", + "R02A002": "b002", + "R02A003": "c002", + "R02DATA": "c00000000012" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a003", + "R01A002": "b003", + "R01A003": "c003", + "R01DATA": "c00000000013" + }, + "R02INNFIL": { + "R02A001": "a003", + "R02A002": "b003", + "R02A003": "c003", + "R02DATA": "c00000000013" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a004", + "R01A002": "b004", + "R01A003": "c004", + "R01DATA": "c00000000014" + }, + "R02INNFIL": { + "R02A001": "a004", + "R02A002": "b004", + "R02A003": "c004", + "R02DATA": "c00000000014" + } + } + }, + { + "input": { + "R02INNFIL": { + "R02A001": "a005", + "R02A002": "b005", + "R02A003": "c005", + "R02DATA": "c00000000015" + } + } + } + ] +} +``` + +### 文件2:`SAN01MAT_g2.json` + +反向两端不匹配:先处理R02-only(a001) → 3次匹配 → 最后R01-only(a005)。 + +```json +{ + "program": "SAN01MAT", + "records": [ + { + "input": { + "R02INNFIL": { + "R02A001": "a001", + "R02A002": "b001", + "R02A003": "c001", + "R02DATA": "c00000000011" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a002", + "R01A002": "b002", + "R01A003": "c002", + "R01DATA": "c00000000012" + }, + "R02INNFIL": { + "R02A001": "a002", + "R02A002": "b002", + "R02A003": "c002", + "R02DATA": "c00000000012" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a003", + "R01A002": "b003", + "R01A003": "c003", + "R01DATA": "c00000000013" + }, + "R02INNFIL": { + "R02A001": "a003", + "R02A002": "b003", + "R02A003": "c003", + "R02DATA": "c00000000013" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a004", + "R01A002": "b004", + "R01A003": "c004", + "R01DATA": "c00000000014" + }, + "R02INNFIL": { + "R02A001": "a004", + "R02A002": "b004", + "R02A003": "c004", + "R02DATA": "c00000000014" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a005", + "R01A002": "b005", + "R01A003": "c005", + "R01DATA": "c00000000015" + } + } + } + ] +} +``` + +### 文件3:`SAN01MAT_g3.json` + +中间不匹配:先匹配(a001) → R01-only(a003) → 匹配(a004) → R02-only(a002) → 匹配(a005)。 + +```json +{ + "program": "SAN01MAT", + "records": [ + { + "input": { + "R01INNFIL": { + "R01A001": "a001", + "R01A002": "b001", + "R01A003": "c001", + "R01DATA": "c00000000011" + }, + "R02INNFIL": { + "R02A001": "a001", + "R02A002": "b001", + "R02A003": "c001", + "R02DATA": "c00000000011" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a003", + "R01A002": "b003", + "R01A003": "c003", + "R01DATA": "c00000000013" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a004", + "R01A002": "b004", + "R01A003": "c004", + "R01DATA": "c00000000014" + }, + "R02INNFIL": { + "R02A001": "a004", + "R02A002": "b004", + "R02A003": "c004", + "R02DATA": "c00000000014" + } + } + }, + { + "input": { + "R02INNFIL": { + "R02A001": "a002", + "R02A002": "b002", + "R02A003": "c002", + "R02DATA": "c00000000012" + } + } + }, + { + "input": { + "R01INNFIL": { + "R01A001": "a005", + "R01A002": "b005", + "R01A003": "c005", + "R01DATA": "c00000000015" + }, + "R02INNFIL": { + "R02A001": "a005", + "R02A002": "b005", + "R02A003": "c005", + "R02DATA": "c00000000015" + } + } + } + ] +} +``` + +--- + +## DB类型时的输出 + +输入类型为DB时,每组生成一个独立的SQL insert文件。 + +| 文件种类 | 命名规则 | 例 | +|---------|--------|-----| +| Insert SQL(一组) | `{プログラムID}_g{groupId}.sql` | `SAN01MAT_g1.sql` | + +例(组1): + +```sql +-- Group: 1 +-- 用途: 员工主表基础数据 +INSERT INTO EMPLOYEE (EMP_ID, EMP_NAME, BIRTH_DATE, SALARY) VALUES +('0000000001', '测试太郎', '1990-04-01', 350000.00), +('0000000002', '测试花子', '1995-07-15', 420000.00); +``` + +--- + +## 字段值规则 + +| PIC | JSON内表示 | 例 | +|-----|-----------|-----| +| `PIC X(n)` | 左对齐 + 空格填充 | `"A0000001"` (8桁) | +| `PIC 9(n)` | 右对齐 + 前补零 | `"00000101"` (8桁) | +| `PIC S9(n)` | 符号 + 右对齐 + 前补零 | `"+0000101"` | +| `PIC S9(n)V9(m)` | 符号 + 右对齐 + 含小数点 | `"+001234567"` (V9(2)时实际为 +0012345.67) | +| `PIC S9(n) COMP` | 十进制数字字符串(含符号) | `"300"`、`"-100000"` | +| `PIC S9(n) COMP-3` | 十进制数字字符串(含符号) | `"1234"`、`"-5678"` | +| `PIC 9(n) COMP-3` | 十进制数字字符串(无符号) | `"1234"` | +| FILLER(纯保留) | 字段名为`FILLER`的未使用区域 | 含有组ID和记录编号的可辨识模式 `"D000000...001"` | +| FILLER(业务保留) | 字段名有具体名称但实际未使用的保留区 | PIC X(n)填全空格、PIC 9(n)填全零 | + +--- + +## 输出目录结构 + +``` +output/ +├── json/ # 完整JSON(每组一个文件,输入类型=文件时输出) +│ ├── SAN01MAT_g1.json +│ ├── SAN01MAT_g2.json +│ └── SAN01MAT_g3.json +└── db/ # DB insert SQL(每组一个文件,输入类型=DB时输出) + ├── SAN01MAT_g1.sql + ├── SAN01MAT_g2.sql + └── SAN01MAT_g3.sql +``` diff --git a/black-box-data-create/layout/sample_ソース_SAN01MAT.cbl b/black-box-data-create/layout/sample_ソース_SAN01MAT.cbl new file mode 100644 index 0000000..5bc28a5 --- /dev/null +++ b/black-box-data-create/layout/sample_ソース_SAN01MAT.cbl @@ -0,0 +1,432 @@ + IDENTIFICATION DIVISION. + PROGRAM-ID. SAN01MAT. + ***************************************************************** + * システム名 : サンプルシステム * + * プログラムID : SAN01MAT * + * プログラム名 : XXXXXX処理 * + * 作成日 : YYYY-MM-DD * + * 処理概要 : マッチング(1:1)を行う。 * + * マッチの場合、ファイルW01を出力する。 * + * ファイルR01のみの場合ファイルW02を出力する。* + * ファイルR02のみの場合ファイルW03を出力する。* + * * + ***************************************************************** + * 更新履歴 * + *---------------------------------------------------------------* + * 更新日付 担当者 更新内容 * + *---------------------------------------------------------------* + * YY-MM-DD @@@ 新規作成 * + * * + ***************************************************************** + ENVIRONMENT DIVISION. + CONFIGURATION SECTION. + SOURCE-COMPUTER. IBM-ZSERIES. + OBJECT-COMPUTER. IBM-ZSERIES. + * + INPUT-OUTPUT SECTION. + FILE-CONTROL. + SELECT R01INNFIL ASSIGN TO SAN01R01. + SELECT R02INNFIL ASSIGN TO SAN01R02. + SELECT W010UTFIL ASSIGN TO SAN01W01. + SELECT W020UTFIL ASSIGN TO SAN01W02. + SELECT W030UTFIL ASSIGN TO SAN01W03. + * + DATA DIVISION. + FILE SECTION. + * + ***************************************************************** + * ##RO1## * + ***************************************************************** + FD R01INNFIL + LABEL RECORD IS STANDARD + BLOCK CONTAINS 0 + RECORDING MODE IS F. + 01 RO1INNREC. + COPY BBBBBFC REPLACING ==(A)== BY ==R01==. + * + ***************************************************************** + * ##RO2## * + ***************************************************************** + FD R02INNFIL + LABEL RECORD IS STANDARD + BLOCK CONTAINS 0 + RECORDING MODE IS F. + 01 RO2INNREC. + COPY BBBBBFC REPLACING ==(A)== BY ==R02==. + * + ***************************************************************** + * ##WO1## * + ***************************************************************** + FD W010UTFIL + LABEL RECORD IS STANDARD + BLOCK CONTAINS 0 + RECORDING MODE IS F. + 01 WO10UTREC. + COPY BBBBBFC REPLACING ==(A)== BY ==WO1==. + * + ***************************************************************** + * ##WO2## * + ***************************************************************** + FD W020UTFIL + LABEL RECORD IS STANDARD + BLOCK CONTAINS 0 + RECORDING MODE IS F. + 01 WO20UTREC. + COPY BBBBBFC REPLACING ==(A)== BY ==WO2==. + * + ***************************************************************** + * ##W03## * + ***************************************************************** + FD W030UTFIL + LABEL RECORD IS STANDARD + BLOCK CONTAINS 0 + RECORDING MODE IS F. + 01 WO30UTREC. + COPY BBBBBFC REPLACING ==(A)== BY ==WO3==. + * + WORKING-STORAGE SECTION. + * + ***************************************************************** + * SYSIN領域 * + ***************************************************************** + *01 SYSARA. + * + ***************************************************************** + * コンスタント領域 * + ***************************************************************** + 01 CNSARA. + 03 CNS-PRGIDX PIC X(008) VALUE 'SAN01MAT'. + 03 CNS-UNYSUTKES-1 PIC X(001) VALUE '1'. + 03 CNS-CFKFCA PIC X(001) VALUE 'N'. + 03 CNS-MSGSTR PIC 9(003) VALUE 001. + 03 CNS-MSGFIN PIC 9(003) VALUE 002. + 03 CNS-MSGSUBEEK PIC 9(003) VALUE 005. + 03 CNS-MSGIINKES PIC 9(003) VALUE 006. + 03 CNS-MSGOUTKES PIC 9(003) VALUE 007. + 03 CNS-MSGKEYINF PIC 9(003) VALUE 033. + 03 CNS-KN0002 PIC 9(001) VALUE 2. + 03 CNS-ABD999 PIC 9(003) VALUE 999. + * + ***************************************************************** + * フラグ領域 * + ***************************************************************** + *01 FLGARA. + * + ***************************************************************** + * カウンタ領域 * + ***************************************************************** + 01 CUNARA. + 03 CUN-RO1INN PIC S9(009) COMP-3 VALUE ZERO. + 03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO. + 03 CUN-WO1OUT PIC S9(009) COMP-3 VALUE ZERO. + 03 CUN-WO2OUT PIC S9(009) COMP-3 VALUE ZERO. + 03 CUN-WO3OUT PIC S9(009) COMP-3 VALUE ZERO. + * + ***************************************************************** + * 作業領域 * + ***************************************************************** + 01 WRKARA. + *** 運用日付 + 03 WRK-U06 PIC 9(008). + 03 WRK-U06ARA REDEFINES WRK-U06. + 05 WRK-UYOYMX PIC 9(006). + 05 WRK-UYOHII PIC 9(002). + *** マッチングキー + 03 WRK-R01KEY. + 05 WRK-R01KEY001 PIC X(010). + 05 WRK-R01KEY002 PIC X(010). + 05 WRK-R01KEY003 PIC X(010). + 03 WRK-R02KEY. + 05 WRK-R02KEY001 PIC X(010). + 05 WRK-R02KEY002 PIC X(010). + 05 WRK-R02KEY003 PIC X(010). + * + ***************************************************************** + * サブプログラム連絡領域 * + ***************************************************************** + *** 運用日付取得 + COPY D01010AC. + *** 入力件数表示/入力レコードシーケンスチェックSR用 + COPY S01MSGAC. + *** メッセージ編集出力SR用 + COPY MSG000AC. + * + PROCEDURE DIVISION. + ***************************************************************** + * サブモジュールNO: (0.0) * + * サブモジュール名: 制御処理 * + * 処理概要 : メインコントロール処理 * + ***************************************************************** + 0000MAJCOLSOR SECTION. + * + *** 初期処理 + PERFORM 1000ITTSOR. + * + *** メイン処理 + PERFORM 2000MAJSOR + UNTIL WRK-R01KEY = HIGH-VALUE + AND WRK-R02KEY = HIGH-VALUE. + * + *** 終了処理 + PERFORM 3000STPSOR. + * + 0000MAJCOLSOREXT. + GOBACK. + ***************************************************************** + * サブモジュールNO: (1.0) * + * サブモジュール名: 初期処理 * + * 処理概要 : 開始メッセージ出力・各種初期化処理 * + ***************************************************************** + 1000ITTSOR SECTION. + * + *** 開始メッセージ出力 + INITIALIZE M00MHOPAR. + MOVE CNS-MSGSTR TO M00MSGCOD. + PERFORM 4000MSGOUTSOR. + * + *** コンバイル日時出力 + INITIALIZE M00MHOPAR. + MOVE CNS-MSGKEYINF TO M00MSGCOD. + MOVE FUNCTION WHEN-COMPILED TO M00UMKDATS22(1). + MOVE 'COMPILED' TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + *** ワークエリア初期化 + INITIALIZE WRKARA. + * + *** 運用日付取得 + INITIALIZE D01UBSPAR. + CALL 'DAT01S0' USING D01UBSPAR. + IF D01FKICOD = ZERO + MOVE D01U06 TO WRK-U06 + ELSE + INITIALIZE M00MHOPAR + MOVE CNS-MSGSUBEEK TO M00MSGCOD + MOVE 'DAT01SO' TO M00UMKDATS22(1) + MOVE D01FKICOD TO M00UMKDATS22(2) + PERFORM 4000MSGOUTSOR + PERFORM 9999ABDSOR + END-IF. + * + *** 入出力ファイルOPEN + OPEN INPUT R01INNFIL + R02INNFIL + OUTPUT W010UTFIL + W020UTFIL + W030UTFIL. + * + *** #R01#を読み込み + PERFORM 1100R01INNSOR. + *** #R02#を読み込み + PERFORM 1200R02INNSOR. + * + 1000ITTSOREXT. + EXIT. + ***************************************************************** + * サブモジュールNO:(1.1) * + * サブモジュール名:##RO1##読込処理 * + * 処理概要 :レコード読込・キー設定・SEQチェック処理 * + ***************************************************************** + 1100R01INNSOR SECTION. + * + READ R01INNFIL + AT END + MOVE HIGH-VALUE TO WRK-R01KEY + NOT AT END + ADD 1 TO CUN-RO1INN + MOVE R01A001 TO WRK-R01KEY001 + MOVE R01A002 TO WRK-R01KEY002 + MOVE R01A003 TO WRK-R01KEY003 + *** 入力件数表示/シーケンスチェック処理 + MOVE 'SAN01R01' TO S01DDM. + MOVE CNS-UNYSUTKES-1 TO SO1HYOKESKJCKUB + MOVE CNS-CFKFCA TO S01JKDKIYFLG + MOVE WRK-R01KEY TO SO1SEQCECKOM + COPY CHK01SRP REPLACING ==(A)==BY ==S01==. + END-READ. + * + 1100R01INNSOREXT. + EXIT. + ***************************************************************** + * サブモジュールNO:(1.2) * + * サブモジュール名:##RO2##読込処理 * + * 処理概要 :レコード読込・キー設定・SEQチェック処理 * + ***************************************************************** + 1200R02INNSOR SECTION. + * + READ R02INNFIL + AT END + MOVE HIGH-VALUE TO WRK-R02KEY + NOT AT END + ADD 1 TO CUN-R02INN + MOVE R02A001 TO WRK-R02KEY001 + MOVE R02A002 TO WRK-R02KEY002 + MOVE R02A003 TO WRK-R02KEY003 + *** 入力件数表示/シーケンスチェック処理 + MOVE 'SAN01R02' TO S01DDM. + MOVE CNS-UNYSUTKES-1 TO SO1HYOKESKJCKUB + MOVE CNS-CFKFCA TO S01JKDKIYFLG + MOVE WRK-R02KEY TO SO1SEQCECKOM + COPY CHK01SRP REPLACING ==(A)== BY ==S01==. + END-READ. + * + 1200R02INNSOREXT. + EXIT. + ***************************************************************** + * サブプログラムNO:(2.0) * + * サブプログラム名:主処理 * + * 処理概要 :マッチング(1:1)を行う * + ***************************************************************** + 2000MAJSOR SECTION. + * + EVALUATE TRUE + *** マッチ + WHEN WRK-R01KEY = WRK-R02KEY + *** #WO1#を編集出力処理 + PERFORM 2100W010UTSOR + * + *** #RO1#を読み込み + PERFORM 1100R01INNSOR + *** #RO2#を読み込み + PERFORM 1200R02INNSOR + * + *** アンマッチ(##R01##のみ) + WHEN WRK-R01KEY < WRK-R02KEY + *** #WO2#を編集出力処理 + PERFORM 2200W020UTSOR + * + *** #RO1#を読み込み + PERFORM 1100R01INNSOR + * + *** アンマッチ(##R02##のみ) + WHEN WRK-R01KEY > WRK-R02KEY + *** #WO3#を編集出力処理 + PERFORM 2300W030UTSOR + * + *** #R02#を読み込み + PERFORM 1200R02INNSOR + END-EVALUATE. + * + 2000MAJSOREXT. + EXIT. + ***************************************************************** + * サブモジュールNO:(2.1) * + * サブモジュール名:##WO1##編集出力処理 * + * 処理概要 :レコード編集・出力処理 * + ***************************************************************** + 2100W010UTSOR SECTION. + * + MOVE RO1INNREC TO W010UTREC. + MOVE R02INNREC TO W010UTREC. + * + WRITE WO1OUTREC. + ADD 1 TO CUN-WO1OUT. + + 2100W010UTSOREXT. + EXIT. + ***************************************************************** + * サブプログラムNO:(2.2) * + * サブプログラム名:##WO2#を編集出力処理 * + * 処理概要 :レコード編集・出力処理 * + ***************************************************************** + 2200W020UTSOR SECTION. + * + MOVE RO1INNREC TO W020UTREC. + * + WRITE WO2OUTREC. + ADD 1 TO CUN-WO2OUT. + * + 2200W020UTSOREXT. + EXIT. + ***************************************************************** + * サブプログラムNO:(2.3) * + * サブプログラム名:##WO3#を編集出力処理 * + * 処理概要 :レコード編集・出力処理 * + ***************************************************************** + 2300W030UTSOR SECTION. + * + MOVE R02INNREC TO W03OUTREC. + * + WRITE WO3OUTREC. + ADD 1 TO CUN-WO3OUT. + * + 2300W030UTSOREXT. + EXIT. + ***************************************************************** + * サブモジュールNO:(3.0) * + * サブモジュール名:終了処理 * + * 処理概要 :ファイルクローズ・件数と終了メッセージ出力 * + ***************************************************************** + 3000STPSOR SECTION. + * + *** 入出力ファイルCLOSE + CLOSE R01INNFIL + R02INNFIL + W010UTFIL + W020UTFIL + W030UTFIL. + * + *** 入出力ファイル件数出力 + INITIALIZE M00MHOPAR. + MOVE CNS-MSGIINKES TO M00MSGCOD. + MOVE 'SAN01R01' TO M00UMKDATS22(1). + MOVE CUN-RO1INN TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + INITIALIZE M00MHOPAR. + MOVE CNS-MSGIINKES TO M00MSGCOD. + MOVE 'SAN01R02' TO M00UMKDATS22(1). + MOVE CUN-R02INN TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + INITIALIZE M00MHOPAR. + MOVE CNS-MSGIINKES TO M00MSGCOD. + MOVE 'SAN01W01' TO M00UMKDATS22(1). + MOVE CUN-WO1OUT TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + INITIALIZE M00MHOPAR. + MOVE CNS-MSGIINKES TO M00MSGCOD. + MOVE 'SAN01W02' TO M00UMKDATS22(1). + MOVE CUN-WO2OUT TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + INITIALIZE M00MHOPAR. + MOVE CNS-MSGIINKES TO M00MSGCOD. + MOVE 'SAN01W03' TO M00UMKDATS22(1). + MOVE CUN-WO3OUT TO M00UMKDATS22(2). + PERFORM 4000MSGOUTSOR. + * + *** 終了メッセージ出力 + INITIALIZE M00MHOPAR. + MOVE CNS-MSGFIN TO M00MSGCOD. + PERFORM 4000MSGOUTSOR. + * + 3000STPSOREXT. + EXIT. + ***************************************************************** + * サブプログラムNO:(4.0) * + * サブプログラム名:メッセージ編集出力処理 * + * 処理概要 :メッセージ編集出力サブPGM呼出 * + ***************************************************************** + 4000MSGOUTSOR SECTION. + * + MOVE CNS-KN0002 TO M00KNOCOD + MOVE CNS-KN0002 TO M00SYUCOD. + MOVE CNS-PRGIDX TO M00PRGIDX. + CALL 'MSG0SRP' USING M00MSGPAR. + * + 4000MSGOUTSOREXT. + EXIT. + ***************************************************************** + * サブプログラムNO:(9.9) * + * サブプログラム名:ABEND処理 * + * 処理概要 :ABENDサブPGM呼出 * + ***************************************************************** + 9999ABDSOR SECTION. + * + MOVE CNS-ABD999 TO M00ABDCOD. + CALL 'END01TO' USING M00MSGPAR. + * + 9999ABDSOREXT. + EXIT. diff --git a/black-box-data-create/layout/sample_詳細設計書_SAN01MAT.md b/black-box-data-create/layout/sample_詳細設計書_SAN01MAT.md new file mode 100644 index 0000000..5549762 --- /dev/null +++ b/black-box-data-create/layout/sample_詳細設計書_SAN01MAT.md @@ -0,0 +1,141 @@ +# 詳細設計書 + +## 基本情報 + +| # | 項目 | 内容 | +|---|------|------| +| 1 | システム名 | サンプルシステム | +| 2 | プログラムID | SAN01MAT | +| 3 | プログラム名 | XXXXXX処理 | +| 4 | PGMパターン | マッチング(1:1) | +| 5 | 機能概要 | マッチング(1:1)を行う。 | +| 6 | | マッチの場合、ファイルW01を出力する。 | +| 7 | | ファイルR01のみの場合ファイルW02を出力する。 | +| 8 | | ファイルR02のみの場合ファイルW03を出力する。 | + +※PGMパターン:マッチング(1:1、1:N、M:N)、レイアウト編集のみ(GETPUT)、項目チェック、振り分け、キーブレイク、キーブレイク(集計、集約)、DB更新 + +### 前提条件 + +| NO | 対象ファイル | 条件 | +|----|-------------|------| +| 1 | ファイルR01 | キー項目A001>A002>A003で昇順でソート | +| 2 | ファイルR02 | キー項目A001>A002>A003で昇順でソート | + +### 使用ファイル一覧 + +| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 | +|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------| +| 1 | ファイルR01 | R01 | SAN01R01 | I | BBBBBFC | FB | | 120 | PS | | +| 2 | ファイルR02 | R02 | SAN01R02 | I | BBBBBFC | FB | | 120 | PS | | +| 3 | ファイルW01 | W01 | SAN01W01 | O | BBBBBFC | FB | | 120 | PS | | +| 4 | ファイルW02 | W02 | SAN01W02 | O | BBBBBFC | FB | | 120 | PS | | +| 5 | ファイルW03 | W03 | SAN01W03 | O | BBBBBFC | FB | | 120 | PS | | + +### キー項目一覧 + +| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) | +|----|-----------|---------------------|-------------------------------------------| +| 1 | ファイルR01 | A001>A002>A003(重複NG) | A001>A002>A003 | +| 2 | ファイルR02 | A001>A002>A003(重複NG) | A001>A002>A003 | + +### 使用モジュール一覧 + +| NO | 機能 | プログラムID | 使用COPY名 | +|----|------|-------------|-----------| +| 1 | 入力件数表示/シーケンスチェック処理SUB | CHK01SRP | S01MSGAC | +| 2 | 運用日取得SUB | DAT01S0 | D01010AC | +| 3 | メッセージ編集出力処理SUB | MSG0SRP | MSG000AC | +| 4 | ABEND処理SUB | END01TO | MSG000AC | + +--- + +## 処理詳細 + +``` +1.初期処理(1000ITTSOR) + 1-1.開始メッセージ出力 + 【メッセージ編集】 + メッセージ番号:1(開始メッセージ) + 1-2.コンパイル日時出力 + 【メッセージ編集】 + メッセージ番号:33(コンパイル日時) + PARM1:コンパイル日時 + PARM2:'COMPILED' + 1-3.ワークエリアの初期化 + 1-4.運用日取得SUB(DAT01S0)により運用日を取得する。 + 復帰コード≠ZEROの場合、メッセージを出力し、ABEND処理SUBを呼び出し異常終了する。 + 【メッセージ編集】 + メッセージ番号:5(サブエラー) + PARM1:'DAT01SO' + PARM2:復帰コード + 【ABEND処理SUB】 + ABENDコード:999 + 1-5.使用ファイルのオーブン + 1-6.R01を読み込む。(1100R01INNSOR)(1回目) + 1-7.R02を読み込む。(1200R02INNSOR)(1回目) + +2.主処理(2000MAJSOR)(R01、R02を全て読み終えるまで下記を繰り返す) + 2-1.マッチの場合 + 2-1-1.R01をW01にそのまま出力する。(2100W010UTSOR) + *出力編集(W01)を参照 + 2-1-2.R01を読み込む。(1100R01INNSOR)(2件目以降) + 2-1-3.R02を読み込む。(1200R02INNSOR)(2件目以降) + 2-2.R01のみの場合 + 2-2-1.R01をW02にそのまま出力する。(2200W020UTSOR) + *出力編集(W02)を参照 + 2-2-2.R01を読み込む。(1100R01INNSOR)(2件目以降) + 2-3.R02のみの場合 + 2-3-1.R02をW03にそのまま出力する。(2300W030UTSOR) + *出力編集(W03)を参照 + 2-3-2.R02を読み込む。(1200R02INNSOR)(2件目以降) + +3.終了処理(3000STPSOR) + 3-1.入出力ファイルのクローズ + 3-2.入出力件数出力メッセージ出力 + 【入力メッセージ編集】 + メッセージ番号:6(入力件数メッセージ) + PARM1:当該入力ファイルのDD名 + PARM2:当該入力ファイルの件数 + 【出力メッセージ編集】 + メッセージ番号:7(出力件数メッセージ) + PARM1:当該出力ファイルのDD名 + PARM2:当該出力ファイルの件数 + 3-3.終了メッセージ出力 + 【メッセージ編集】 + メッセージ番号:2(終了メッセージ) +``` + +--- + +## 出力レコード定義 + +### 出力ファイル1(W01) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R01.同項目を設定 | | +| 2 | A002 | R01.同項目を設定 | | +| 3 | A003 | R01.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R01.同項目を設定 | | + +### 出力ファイル2(W02) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R01.同項目を設定 | | +| 2 | A002 | R01.同項目を設定 | | +| 3 | A003 | R01.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R01.同項目を設定 | | + +### 出力ファイル3(W03) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R02.同項目を設定 | | +| 2 | A002 | R02.同項目を設定 | | +| 3 | A003 | R02.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R02.同項目を設定 | | diff --git a/black-box-data-create/layout/ファイル定義書_xxxxxxx.md b/black-box-data-create/layout/ファイル定義書_xxxxxxx.md new file mode 100644 index 0000000..25de2e3 --- /dev/null +++ b/black-box-data-create/layout/ファイル定義書_xxxxxxx.md @@ -0,0 +1,142 @@ +# ファイル定義書 + +## 変更履歴 + +| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 | +|----|---------|--------|--------|--------|------| +| 1 | 新規作成 | AI | YYYY/MM/DD | | | + +## Layout(レコードレイアウト) + +### ファイル基本情報 + +| ファイル名 | COPY ID | 媒体 | レコード形式 | レコード長 | +|-----------|---------|------|-------------|-----------| +| | | PS | FB | | + +### レコード定義 + +| No | レベル | 項目名 | 項目名(英字名) | 属性 | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 | +|----|--------|--------|---------------|------|---------|--------|---------|-----------|-------------|------| +| 1 | | | | | | | | | | | +| 2 | | | | | | | | | | | +| 3 | | | | | | | | | | | +| 4 | | | | | | | | | | | + +--- + +## 記載説明 + +| 項目 | 説明 | +|------|------| +| 媒体 | ファイル(PS)の場合は"PS"と記録 | +| レコード形式 | 固定長(FB)または可変長(VB)と記録 | +| レコード長 | レコード長は全項目のバイト数を合計して記録。FDのRECORD SIZEと一致させること。REDEFINES項目は合計に含まない。 | +| レベル | レベル番号の定義:01, 03, 05, 07, 09... と飛び飛びのレベル番号とする。同じレベル番号は兄弟関係となる。 | +| 属性(PIC TYPE) | 属性(TYPE)とバイト数を記入するものとする。原則PIC記法で記録。COBOL記述に沿ったシートの記載内容に準拠。 | + +## COBOLデータ型一覧 + +| No | COBOL記述 | 略記法 | バイト数 | 説明 | 日本語説明 | 例 | バイト数(例) | +|----|-----------|--------|---------|------|-----------|-----|-------------| +| 1 | PIC X(n) | X(n) | n | 英数字 | 固定長英数字 | X(10) | 10 | +| 2 | PIC G(n) | G(n) | n×2 | DBCS文字 | ダブルバイト文字(日本語) | G(20) | 40 | +| 3 | PIC 9(n) | 9(n) | n | 数字・10進(符号無) | 符号無し整数 | 9(7) | 7 | +| 4 | PIC S9(n) | S9(n) | n | 数字・10進(符号有) | 符号付き整数 | S9(7) | 7 | +| 5 | PIC S9(n)V9(m) | S9(n)V9(m) | n+m | 数字・10進(符号有・小数) | 暗黙小数点付き(DISPLAY) | S9(7)V9(2) | 9 | +| 6 | PIC S9(n) COMP-3 | S9(n) COMP-3 | INT((n+2)/2) | パック10進(符号有) | 内部10進数 | S9(7) COMP-3 | 4 | +| 7 | PIC 9(n) COMP-3 | 9(n) COMP-3 | INT((n+1)/2) | パック10進(符号無) | 内部10進数(符号無) | 9(7) COMP-3 | 4 | +| 8 | PIC S9(n)V9(m) COMP-3 | S9(n)V9(m) COMP-3 | INT((n+m+2)/2) | パック10進(符号有・小数) | 暗黙小数点付きCOMP-3 | S9(7)V9(2) COMP-3 | 5 | +| 9 | PIC S9(4) COMP | S9(4) COMP | 2 | 2進(2バイト) | 半語長バイナリ | S9(4) COMP | 2 | +| 10 | PIC S9(9) COMP | S9(9) COMP | 4 | 2進(4バイト) | 全語長バイナリ | S9(9) COMP | 4 | +| 11 | PIC S9(18) COMP | S9(18) COMP | 8 | 2進(8バイト) | 2語長バイナリ | S9(18) COMP | 8 | +| 12 | PIC S9(n)V9(m) COMP | S9(n)V9(m) COMP | n+m≦4→2, ≦9→4, ≦18→8 | 2進(符号有・小数) | 暗黙小数点付きCOMP | S9(9)V9(2) COMP | 8 | +| 13 | PIC S9(n) BINARY | S9(n) BINARY | n≦4→2, n≦9→4, n≦18→8 | 2進(バイナリ) | COMPと同一 | S9(15) BINARY | 8 | +| 14 | ZZZ9 | ZZZ9 | Zの数+9の数 | 数字編集パターン | ゼロ抑制編集 | ZZZZ9 | 5 | + +## COBOL記述例 + +### 例1: 基本レイアウト + +COBOL定義: +``` +01 TEST-RECORD. + 03 FIELD-1 PIC X(10). + 03 FIELD-2 PIC S9(7) COMP-3. + 03 FIELD-3 PIC 9(4). + 03 FIELD-4 PIC S9(9) COMP. + 03 FILLER PIC X(5). +``` + +FD記述: + +| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 | +|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------| +| 1 | 01 | テストレコード | TEST-RECORD | | | | 1 | | | | +| 2 | 03 | フィールド1 | FIELD-1 | X | 10 | | 1 | | TEST-RECORD | | +| 3 | 03 | フィールド2 | FIELD-2 | S9(7) COMP-3 | 5 | | 11 | | TEST-RECORD | | +| 4 | 03 | フィールド3 | FIELD-3 | 9(4) | 4 | | 16 | | TEST-RECORD | | +| 5 | 03 | 予約 | FILLER | X(5) | 5 | | 20 | | TEST-RECORD | | + +### 例2: REDEFINESを使用する場合 + +COBOL定義: +``` +01 REDEF-REC. + 03 BASE-FIELD PIC X(10). + 03 REDEF-FIELD REDEFINES BASE-FIELD. + 05 INFO-1 PIC X(5). + 05 INFO-2 PIC X(5). +``` + +FD記述: + +| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 | +|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------| +| 1 | 01 | REDEFINESレコード | REDEF-REC | | | | 1 | | | | +| 2 | 03 | ベースフィールド | BASE-FIELD | X(10) | 10 | | 1 | | REDEF-REC | | +| 3 | 03 | 再定義フィールド | REDEF-FIELD | | | | 11 | BASE-FIELD | REDEF-REC | | +| 4 | 05 | 情報1 | INFO-1 | 9(5) | 5 | | 11 | | REDEF-REC | | +| 5 | 05 | 情報2 | INFO-2 | 9(5) | 5 | | 16 | | REDEF-REC | | + +### 例3: OCCURSを使用する場合 + +COBOL定義: +``` +01 OCCURS-REC. + 03 OCCURS-ITEM OCCURS 3. + 05 SUB-1 PIC X(5). + 03 AFTER-ITEM PIC 9(3). +``` + +FD記述: + +| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 | +|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------| +| 1 | 01 | OCCURSレコード | OCCURS-REC | | | | 1 | | | | +| 2 | 03 | OCCURS項目 | OCCURS-ITEM | | | 3 | 1 | | OCCURS-REC | | +| 3 | 05 | サブ項目1 | SUB-1 | X(5) | 5 | | 1 | | OCCURS-REC | | +| 4 | 03 | OCCURS後 | AFTER-ITEM | 9(3) | 3 | | 16 | | OCCURS-REC | | + +### 例4: OCCURSの多重入れ子 + +COBOL定義: +``` +01 OCCURS-MULTI-REC. + 03 GRP-ITEM OCCURS 3. + 05 SUB1 PIC X(5). + 05 SUB2 OCCURS 4. + 07 SUB2-DATA PIC 9(3). + 03 AFTER-ITEM PIC 9(3). +``` + +FD記述: + +| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 | +|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------| +| 1 | 01 | OCCURS多重入れ子レコード | OCCURS-MULTI-REC | | | | 1 | | | | +| 2 | 03 | グループ項目 | GRP-ITEM | | | 3 | 1 | | OCCURS-MULTI-REC | | +| 3 | 05 | サブ項目1 | SUB-1 | X(5) | 5 | | 1 | | OCCURS-MULTI-REC | | +| 4 | 05 | サブ項目2 | SUB-2 | | | 4 | 6 | | OCCURS-MULTI-REC | | +| 5 | 07 | サブ項目2データ | SUB2-DATA | 9(3) | 3 | | 6 | | OCCURS-MULTI-REC | | +| 6 | 03 | OCCURS後 | AFTER-ITEM | 9(3) | 3 | | 52 | | OCCURS-MULTI-REC | | diff --git a/black-box-data-create/layout/詳細設計書_XXXXXXXX.md b/black-box-data-create/layout/詳細設計書_XXXXXXXX.md new file mode 100644 index 0000000..a36a817 --- /dev/null +++ b/black-box-data-create/layout/詳細設計書_XXXXXXXX.md @@ -0,0 +1,145 @@ +# 詳細設計書 + +## 基本情報 + +| # | 項目 | 内容 | +|---|------|------| +| 1 | システム名 | SSSSシステム | +| 2 | プログラムID | SSSnnFFF | +| 3 | プログラム名 | XXXXXX処理 | +| 4 | PGMパターン | | +| 5 | 機能概要 | | +| 6 | | | +| 7 | | | +| 8 | | | + +※PGMパターン:マッチング(1:1、1:N、M:N)、レイアウト編集のみ(GETPUT)、項目チェック、振り分け、キーブレイク、キーブレイク(集計、集約)、DB更新 + +### 前提条件 + +| NO | 対象ファイル | 条件 | +|----|-------------|------| +| 1 | | | +| 2 | | | + +### 使用ファイル一覧 + +| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 | +|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------| +| 1 | | | | | | | | | | | +| 2 | | | | | | | | | | | +| 3 | | | | | | | | | | | +| 4 | | | | | | | | | | | +| 5 | | | | | | | | | | | + +### キー項目一覧 + +| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) | +|----|-----------|---------------------|-------------------------------------------| +| 1 | | | | +| 2 | | | | + +### 使用モジュール一覧 + +| NO | 機能 | プログラムID | 使用COPY名 | +|----|------|-------------|-----------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | + +--- + +## 処理詳細 + +``` +大項目(1桁):「1.」「2.」「3.」のいずれか + 中項目(2桁):対応するパラグラフ名を括弧書きで併記 + 小項目(3桁):個別処理の説明 + 詳細:メッセージ番号、PARM、条件分岐、ABENDコード等 +``` + +### 処理詳細フォーマット(記入例) + +``` +1.初期処理 (1000ITTSOR) + 1-1.開始メッセージ出力 + メッセージ番号: 1(開始メッセージ) + 1-2.コンパイル日時出力 + メッセージ番号: 33(コンパイル日時) + PARM1: コンパイル日時 + PARM2: 'COMPILED' + 1-3.ワークエリア初期化 + 1-4.サブプログラム呼出(各種初期設定) + 戻りコードがZEROの場合、正常継続。 + それ以外の場合、メッセージ出力後、ABEND処理へ遷移。 + メッセージ番号: 5(サブエラー) + PARM1: 呼出プログラム名 + PARM2: 戻りコード + ABENDコード: 999 + 1-5.入出力ファイルOPEN + 1-6.入力ファイル1を読み込み。 + 1-7.入力ファイル2を読み込み。 + +2.主処理 (2000MAJSOR)(全入力ファイルを読み切り終了するまで繰り返し) + 2-1.条件Aの場合 + 2-1-1.出力ファイル1に出力する。 + 出力後、出力ファイル1のカウンタを加算 + 2-1-2.入力ファイル1を読み込み。 + 2-2.条件Bの場合 + 2-2-1.出力ファイル2に出力する。 + 出力後、出力ファイル2のカウンタを加算 + 2-2-2.入力ファイル2を読み込み。 + 2-3.条件Cの場合 + 2-3-1.出力ファイル3に出力する。 + 出力後、出力ファイル3のカウンタを加算 + 2-3-2.入力ファイル2を読み込み。 + +3.終了処理 (3000STPSOR) + 3-1.入出力ファイルCLOSE + 3-2.入出力ファイル件数出力 + メッセージ番号: 6(入力件数) + PARM1: 該当ファイルDD名 + PARM2: 該当ファイル件数 + メッセージ番号: 7(出力件数) + PARM1: 該当ファイルDD名 + PARM2: 該当ファイル件数 + 3-3.終了メッセージ出力 + メッセージ番号: 2(終了メッセージ) +``` + +--- + +## 出力レコード定義 + +出力ファイルごとに以下の形式で記述する。 + +### 出力ファイル1(例: W01) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | +| 5 | | | | + +### 出力ファイル2(例: W02) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | +| 5 | | | | + +### 出力ファイル3(例: W03) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | +| 5 | | | | diff --git a/black-box-data-create/main.py b/black-box-data-create/main.py new file mode 100644 index 0000000..17bacab --- /dev/null +++ b/black-box-data-create/main.py @@ -0,0 +1,67 @@ +# main.py +#!/usr/bin/env python +""" +COBOL テストデータ生成 Agent + +使用方法: + python main.py --design 詳細設計書_ZAN04MAT.md --source ZAN04MAT.cbl \ + --file-db-md COPY句定義書.md --cpy cpy/ --db-md DB定義書.md --output output/ +""" +import argparse +import os +import sys + +from agent import generate + +DEFAULT_RULES_DIR = os.path.join(os.path.dirname(__file__), 'rules') + + +def main(): + parser = argparse.ArgumentParser( + description='COBOLテストデータ生成Agent' + ) + + parser.add_argument('--design', required=True, help='詳細設計書 .md のパス') + parser.add_argument('--source', required=True, help='COBOL ソース .cbl のパス') + parser.add_argument('--file-db-md', required=True, help='ファイル/DB 構造 .md のパス') + parser.add_argument('--cpy', required=True, help='COPYBOOK 格納ディレクトリ') + parser.add_argument('--db-md', required=True, help='DB 定義書 .md のパス') + parser.add_argument('--output', default='output', help='出力ディレクトリ') + parser.add_argument('--api-key', default='sk-6156cccdc9c14d949cf5bfc5afc67a03', + help='DeepSeek API Key') + parser.add_argument('--model', default='deepseek-v4-flash', help='API モデル名') + parser.add_argument('--rules', default=DEFAULT_RULES_DIR, help='ルール格納ディレクトリ') + parser.add_argument('--max-tokens', type=int, default=32768, + help='API 生成トークン上限') + + args = parser.parse_args() + + for name, path in [('--design', args.design), ('--source', args.source)]: + if not os.path.exists(path): + print(f"エラー: {name} のファイルが見つかりません: {path}", file=sys.stderr) + sys.exit(1) + + result = generate( + design_md=args.design, + source_cbl=args.source, + file_db_md=args.file_db_md, + cpy_dir=args.cpy, + db_md=args.db_md, + output_dir=args.output, + api_key=args.api_key, + api_model=args.model, + rules_dir=args.rules, + max_tokens=args.max_tokens, + ) + + print(f"\n== 完了 ==") + print(f"プログラムID: {result['program_id']}") + print(f"グループ数: {result['groups']}") + print(f"入力タイプ: {result['input_type']}") + print(f"出力ファイル:") + for key, path in sorted(result['output_files'].items()): + print(f" {key}: {path}") + + +if __name__ == '__main__': + main() diff --git a/black-box-data-create/rules/pgm_pattern/1-N+キーブレイク(異キー).md b/black-box-data-create/rules/pgm_pattern/1-N+キーブレイク(異キー).md new file mode 100644 index 0000000..836a653 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/1-N+キーブレイク(異キー).md @@ -0,0 +1,57 @@ +# 1:N+キーブレイク(異キー) データ生成規則 + +## 概要 + +1:Nマッチングの処理中に、マッチしたグループ内でキーが異なるタイミング(異キーブレイク)を検知して処理を分岐する混合型プログラムのテストデータ。 + +本规则定义4组数据,覆盖基本异key切、多组/单件组、異常系、件数確認。 + +## 输入 + +- 詳細設計書のキー項目一覧(マッチキー、ブレイクキー) +- 各入力ファイルのCOPYBOOKフィールド定義 +- キー条件:マッチキー・ブレイクキーとも昇順ソート済み + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本异key切(AM-N007) | 1:Nマッチ内でブレイクキーが変わるグループを複数含むデータ | +| 2 | 多组/单件组 | グループ数が複数、1件のみのグループを含むデータ | +| 3 | 異常系(KB-A001/KB-A002) | 前ブレイクキー未初期化、累加器/カウンタ溢出 | +| 4 | 件数確認(AM-R001/KB-R001) | マッチ件数とグループ数の整合確認データ | + +### 组1:基本异key切(AM-N007) + +マッチキー a001 に対し R02 が3件(ブレイクキー b001,b002,b003)。1:Nマッチしながらブレイクキーが変わるたびに処理が分岐することを確認。 + +### 组2:多组/单件组 + +マッチキー a001(3件)・a002(1件)・a003(2件)。複数グループと単件グループの両方を含むデータ。 + +### 组3:異常系(KB-A001/KB-A002) + +- 先頭レコードのブレイクキーが通常値(WS-PREV-KEY未初期化時の誤中断確認) +- 集約対象(件数・合計)がPIC定義を超えるデータ(SIZE ERROR確認) + +### 组4:件数確認(AM-R001/KB-R001) + +マッチ件数(正常+剩余)の合計が入力件数と一致し、グループ数=キー変化回数+1 となるデータ。 + +## 多keyテスト + +マッチキーまたはブレイクキーが複数項目の場合、各組末尾に多keyテストを追加(既存マッチング規則と同じ手順)。 + +## データ生成手順 + +1. マッチキー・ブレイクキーの特定 +2. 组1/组2: 正常系データ +3. 组3: 異常系データ +4. 各組末尾に多keyテスト +5. 生成後自检:マッチ件数合計とグループ数の整合確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/100分割.md b/black-box-data-create/rules/pgm_pattern/100分割.md new file mode 100644 index 0000000..b6438f2 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/100分割.md @@ -0,0 +1,34 @@ +# 100分割 データ生成規則 + +## 概要 + +入力ファイルのレコードを100件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。 + +本规则定义5组数据,覆盖分割数的边界场景(整数倍、余数、不足、最小、分割数>记录数)。 + +## 输入 + +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 记录数 | 说明 | +|----|------|--------|------| +| 1 | 正好100件 | 100 | 1ファイルに100件| +| 2 | 超过100件(余1件) | 101 | 100件ファイル+余1件ファイル| +| 3 | 不足100件 | 99 | 最終ファイルが99件| +| 4 | 分割数=1 | 1 | 1件入力→1ファイルのみ出力 | +| 5 | 分割数>记录数 | 3 | 出力ファイルは少数 | + +## データ生成手順 + +1. 入力ファイルのフィールド定義を取得 +2. 各組の记录数に従いレコードを生成 +3. 各フィールドはPIC定義に従い実在しそうな値を生成し、隣接レコード間で同じ項目に異なる値を設定 +4. 生成後自检(S-R001/S-R002):出力ファイル命名规则通りであること、各ファイル件数の合計=入力件数であることを確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/25分割.md b/black-box-data-create/rules/pgm_pattern/25分割.md new file mode 100644 index 0000000..995894c --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/25分割.md @@ -0,0 +1,34 @@ +# 25分割 データ生成規則 + +## 概要 + +入力ファイルのレコードを25件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。 + +本规则定义5组数据,覆盖分割数的边界场景(整数倍、余数、不足、最小、分割数>记录数)。 + +## 输入 + +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 记录数 | 说明 | +|----|------|--------|------| +| 1 | 正好25件 | 25 | 1ファイルに25件 | +| 2 | 超过25件(余1件) | 26 | 25件ファイル+余1件ファイル | +| 3 | 不足25件 | 24 | 最終ファイルが24件 | +| 4 | 分割数=1 | 1 | 1件入力→1ファイルのみ出力 | +| 5 | 分割数>记录数 | 3 | 出力ファイルは少数 | + +## データ生成手順 + +1. 入力ファイルのフィールド定義を取得 +2. 各組の记录数に従いレコードを生成 +3. 各フィールドはPIC定義に従い実在しそうな値を生成し、隣接レコード間で同じ項目に異なる値を設定 +4. 生成後自检(S-R001/S-R002):出力ファイル命名规则通りであること、各ファイル件数の合計=入力件数であることを確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/50分割.md b/black-box-data-create/rules/pgm_pattern/50分割.md new file mode 100644 index 0000000..98250ae --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/50分割.md @@ -0,0 +1,34 @@ +# 50分割 データ生成規則 + +## 概要 + +入力ファイルのレコードを50件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。 + +本规则定义5组数据,覆盖分割数的边界场景(正好、余数、不足、最小、分割数>记录数)。 + +## 输入 + +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 记录数 | 说明 | +|----|------|--------|------| +| 1 | 正好50件 | 50 | 1ファイルに50件 | +| 2 | 超过50件(余1件) | 51 | 50件ファイル+余1件ファイル | +| 3 | 不足50件 | 49 | 最終ファイルが49件 | +| 4 | 分割数=1 | 1 | 1件入力→1ファイルのみ出力 | +| 5 | 分割数>记录数 | 3 | 出力ファイルは少数 | + +## データ生成手順 + +1. 入力ファイルのフィールド定義を取得 +2. 各組の记录数に従いレコードを生成 +3. 各フィールドはPIC定義に従い実在しそうな値を生成し、隣接レコード間で同じ項目に異なる値を設定 +4. 生成後自检(S-R001/S-R002):出力ファイル命名规则通りであること、各ファイル件数の合計=入力件数であることを確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/ASCII→EBCDIC変換.md b/black-box-data-create/rules/pgm_pattern/ASCII→EBCDIC変換.md new file mode 100644 index 0000000..421a3f4 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/ASCII→EBCDIC変換.md @@ -0,0 +1,58 @@ +# ASCII→EBCDIC変換 データ生成規則 + +## 概要 + +ASCIIコードの入力データを変換表によりEBCDICコードに変換して出力するプログラムのテストデータ。 + +本规则定义5组数据,覆盖全文字変換、逆方向、制御文字、異常系、レコード長確認。 + +## 输入 + +- 詳細設計書の変換表定義(コード対応表) +- 入力・出力レコード定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | ASCII→EBCDIC 全文字(AE-N001) | 印字可能文字全種を含むデータ | +| 2 | EBCDIC→ASCII 逆方向(AE-N002) | 逆変換の一致確認データ | +| 3 | 制御文字保持(AE-N003) | 改行・タブ等の制御文字 | +| 4 | 異常系(AE-A001〜AE-A003) | 変換表にない文字、往復不一致、全角/半角混在 | +| 5 | レコード長確認(AE-R001) | 変換前後でレコード長が一致するデータ | + +### 组1:ASCII→EBCDIC 全文字(AE-N001) + +英大文字・英小文字・数字・記号など、印字可能文字全種を含むデータ。変換表の全エントリが正しく変換されることを確認。 + +### 组2:EBCDIC→ASCII 逆方向(AE-N002) + +逆方向の変換データ。組1の変換結果を逆変換したときに元の値と一致することを確認。 + +### 组3:制御文字保持(AE-N003) + +改行・タブなどの制御文字を含むデータ。変換後も制御文字が保持されることを確認。 + +### 组4:異常系(AE-A001〜AE-A003) + +- 変換表にない文字(未定義コード)→ 代替文字またはエラー処理 +- 往復一致しない文字(ASCII→EBCDIC→ASCII で値が変わる) +- 全角文字がPIC X領域に入った場合の文字化け + +### 组5:レコード長確認(AE-R001) + +変換前後のレコード長が一致することを確認するデータ(固定長前提)。 + +## データ生成手順 + +1. 変換表とレコード定義の特定 +2. 组1〜组3: 正常系データ +3. 组4: 異常系データ +4. 组5: レコード長確認データ +5. 生成後自检(AE-R001):変換後レコード長=変換前レコード長 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/CSV→FB変換.md b/black-box-data-create/rules/pgm_pattern/CSV→FB変換.md new file mode 100644 index 0000000..4b64504 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/CSV→FB変換.md @@ -0,0 +1,58 @@ +# CSV→FB変換 データ生成規則 + +## 概要 + +CSV形式の入力ファイルを読み取り、固定長(FB)レコードに変換して出力するプログラムのテストデータ。 +CSVのカンマ区切り分解、引用符内改行処理、項目チェックを含む。改行なし(STRING合并、CF-N001)と改行あり(改行展開、CF-N002)の両方の変換方式に対応する。 + +本规则定义4组数据,覆盖正常、异常、引用符・改行、空項目・最大長。 + +## 输入 + +- 入力CSVの項目定義(処理詳細から特定) +- 出力COPYBOOKのフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 正常データ | 全項目が正しいCSVレコード 3〜5件。異なる値のバリエーションを確保。改行なし変換(STRING合并、CF-N001)のケースを含む | +| 2 | 異常データ | 項目数不足、項目数過剰、不正な値、改行位置がRECORD長超過(CF-A001)、引用符未閉じ(CF-A002)のレコード | +| 3 | 引用符・改行(CF-N002〜N004) | 改行展開(1行入力→複数行出力)、項目内カンマ引用符付き、引用符内改行保持 | +| 4 | 空項目・最大長(CF-N005/N006) | 連続カンマ→空文字列処理、各項目がPIC定義ギリギリの最大長 | + +### 组1:正常データ + +全項目が正しいCSVレコード。値をPIC定義に従い生成し、隣接レコード間で異なる値のバリエーションを確保。 + +### 组2:異常データ + +- 項目数不足・項目数過剰 +- 不正な値(フォーマット違反) +- 改行位置がRECORD長超過(CF-A001: 改行展開後1行が定義長を超える) +- 引用符未閉じ(CF-A002: 閉じ"がない→残り全部1項目) + +### 组3:引用符・改行(CF-N002〜N004) + +- 有换行(改行展开): 1行入力→複数行出力(改行位置) +- 項目内カンマ引用符付き(CF-N003) +- 引用符付き文字列(CF-N004): "..."内のカンマ/改行を保持 + +### 组4:空項目・最大長(CF-N005/N006) + +- 空項目連続カンマ→空文字列処理(CF-N005) +- 全項目最大長(CF-N006): 各項目がPIC定義ギリギリ + +## データ生成手順 + +1. CSVの項目数と項目名を処理詳細から特定 +2. 正常データ: カンマ区切りCSV行文字列を生成。値はPIC定義に従う +3. 異常データ: 項目数不一致、空値、不正フォーマット、改行位置超過、引用符未閉じを含む +4. 引用符・改行データ: 引用符内カンマ・改行、改行展開を含む +5. 空項目・最大長データ: 連続カンマと各項目最大長を含む + +## 出力 + +入力はCSV文字列(カンマ区切り)として1フィールドに格納。每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/DB更新.md b/black-box-data-create/rules/pgm_pattern/DB更新.md new file mode 100644 index 0000000..dda2fe3 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/DB更新.md @@ -0,0 +1,88 @@ +# DB更新 データ生成規則 + +## 概要 + +入力ファイルのレコードをDBテーブルにINSERT/UPDATE/DELETEするプログラムのテストデータを生成する。 +出力はSQL INSERTファイル。 + +本规则定义4组数据:组1正常INSERT、组2 INSERT+UPDATE、组3 DELETE対象(无DELETE则以UPDATE/INSERT代替)、组4重复INSERT异常(DB-A001)。 + +## 输入 + +- 詳細設計書の処理詳細(SQL操作と分岐条件) +- 入力ファイルのCOPYBOOKフィールド定義 +- DBテーブル定義と主キー +- キー項目一覧 + +## 生成データ組 + +| 组 | 用途 | 内容 | 条件 | +|----|------|------|------| +| 1 | 正常INSERT | 5件以上の新規INSERT + 末尾に多keyテスト | 必須 | +| 2 | INSERT+UPDATE | 3件以上の重複主キーレコード(UPDATEトリガー用)+ 末尾に多keyテスト | 必須 | +| 3 | DELETE対象 | 2件以上のDELETE対象レコード + 末尾に多keyテスト | 処理詳細にDELETE操作がある場合はDELETE対象、ない場合はUPDATE/INSERT対象で代替 | +| 4 | 重复INSERT异常(DB-A001) | 既存主キーを再度INSERTするレコード(一意キー違反→SQLCODEエラー確認)+ 末尾に多keyテスト | 必須 | + +## 多keyテスト(各組の末尾に追加) + +キーが複数項目(例: A001, A002, A003)で構成される場合、以下のテストデータを追加する(キー項目が1つの場合は追加しない)。 + +**手順:** +1. 該当グループ内から1件のレコードを選び、基準データとする +2. キー項目がN個ある場合、N件の追加レコードを生成する +3. 各追加レコードは「1つのキー項目のみ基準データと異なり、残りのキー項目は基準データと完全一致」とする +4. 非キー項目(DATA部分)は基準データと同じ値でよい + +**例: キーが (A001, A002, A003) の場合、基準データ (a001, b001, c001) に対し:** +- 追加1: (a002, b001, c001) → A001のみ異なる → INSERTされるべき(別レコード) +- 追加2: (a001, b002, c001) → A002のみ異なる → INSERTされるべき(別レコード) +- 追加3: (a001, b001, c002) → A003のみ異なる → INSERTされるべき(別レコード) + +これにより、プログラムが正しいキー項目で比較していることを検証する。 + +## データ生成手順 + +1. **SQL操作の特定**: 処理詳細からINSERT/UPDATE/DELETE/UPSERTの有無を確認する +2. **キー項目の特定**: キー項目一覧からDBテーブルの主キーを特定する +3. **フィールド値生成**: 入力COPYBOOKの各フィールドに対しPIC定義に従い値を生成: + - PIC X(n): 左詰め・スペース埋め + - PIC 9(n): 右詰め・先行ゼロ + - PIC S9(n) COMP / COMP-3: 通常の10進数文字列(符号付き・なし) + - DATE / 日付系フィールド: 実在しそうな日付(YYYYMMDD形式) + - TIME / 時刻系フィールド: 実在しそうな時刻(HHMM形式) + - ID / 番号系フィールド: 連番 + - NAME系フィールド: 実在しそうな氏名 + - 金額 / SALARY系: 現実的な金額 +4. **組1 (正常INSERT)**: + - 全レコードの主キーを重複なしで生成 + - 各レコードの非キーフィールドは異なる値を設定 + - 末尾に多keyテストデータを追加 +5. **組2 (INSERT+UPDATE)**: + - 一部のレコードの主キーを組1と重複させる + - 重複レコードの非キーフィールド値は組1と異なる値にする + - 末尾に多keyテストデータを追加 +6. **組3 (DELETE対象)**: + - STATUS='9'などの削除フラグ付きデータ(処理詳細にDELETE操作がない場合はUPDATE/INSERT対象データで代替) + - 末尾に多keyテストデータを追加 +7. **組4 (重复INSERT异常)**: + - 組1で既に存在する主キーを再度INSERTするレコードを生成 + - 期待: 一意キー違反→SQLCODEエラー/設計書どおりのエラー処理 + - 末尾に多keyテストデータを追加 + +## 出力形式 + +各組1つのSQLファイル: `{プログラムID}_g{groupId}.sql` + +```sql +-- Group: 1 +-- 用途: 正常INSERT + 多keyテスト +INSERT INTO TABLE_NAME (COL1, COL2, ...) VALUES +('val1', 'val2', ...), +('val3', 'val4', ...); +``` + +**注意:** +- 出力は純粋なSQL INSERT文のみ(コードブロックで囲まない) +- 複数行を1つのINSERT文にまとめる(VALUESの後にカンマ区切りで複数行) + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/GETPUT(編集出力).md b/black-box-data-create/rules/pgm_pattern/GETPUT(編集出力).md new file mode 100644 index 0000000..8ab739e --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/GETPUT(編集出力).md @@ -0,0 +1,28 @@ +# GETPUT(編集出力)データ生成規則 + +## 概要 + +DBまたはファイルからデータを読み取り、編集・整形して出力するプログラムのテストデータ。 +レイアウト編集のみ(GETPUT)と同様のシンプルなREAD→編集→WRITEパターン。 + +## 入力 + +- 入力ソースのフィールド定義(COPYBOOKまたはDBテーブル定義) +- 出力レコード定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本データ | 通常値2〜5件。各フィールドに有効な値を設定。異なる値のバリエーションを確保 | + +## データ生成手順 + +1. 入力フィールドを特定 +2. 各フィールドのPIC定義に従い値を生成 +3. 実在しそうな値(日付・時刻・金額・氏名等)を生成 +4. レコード間で異なる値を設定 + +## 出力 + +各組1 JSONファイル。DBからの入力の場合はSQLファイルも出力。 diff --git a/black-box-data-create/rules/pgm_pattern/MERGE.md b/black-box-data-create/rules/pgm_pattern/MERGE.md new file mode 100644 index 0000000..74eafd9 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/MERGE.md @@ -0,0 +1,77 @@ +# MERGE(複数ファイル結合) データ生成規則 + +## 概要 + +複数の入力ファイルをキーでソート・マージし、統合された1つの出力ファイルを生成するプログラムのテストデータ。 + +本规则定义4组数据,覆盖全文件合并、键边界、重复键、未排序输入。 + +## 输入 + +- 詳細設計書のキー項目一覧(マージキー) +- 各入力ファイルのCOPYBOOKフィールド定義 +- キー条件:正常系は昇順ソート済み入力、異常系(组4)は故意に乱序 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 全ファイルカバー | 全入力ファイルのデータが出力に反映されるデータ + 末尾多keyテスト | +| 2 | キー境界 | 同一キー値が複数ファイルに分散しているケース + 末尾多keyテスト | +| 3 | 重复键(MR-N003) | 同一キーが複数ファイルに同値で出現するケース(同キー順序保持確認)+ 末尾多keyテスト | +| 4 | 未排序输入(MR-A001) | 入力ファイルが昇順でないデータ(誤マージ確認) | + +### 组1:全ファイルカバー(MR-N001) + +R01: a001→a003→a004 +R02: a001→a002→a003 + +マージ後キー順序(期待): a001→a002→a003→a004 + +### 组2:キー境界(MR-N002) + +R01: a001→a002→a003 +R02: a002 + +同一キーa002が両ファイルに存在(境界でのマージ順序確認)。 + +### 组3:重复键(MR-N003) + +R01: a001→a002→a002→a003 +R02: a002→a002→a004 + +同一キーa002が両ファイルに計4件(同キーの順序保持を確認)。 + +### 组4:未排序输入(MR-A001) + +R01: a001→a003→a002→a004(a003先于a002,违反升序) +R02: a001→a002→a003→a004(正常升序) + +说明:昇順前提が破られた入力でプログラムの誤マージ/未期待動作を確認する(不要求"正确合并")。 + +## 多keyテスト + +キーが複数項目の場合、各組末尾に追加。各キー項目のみ異なるN件を生成し、正しいキーでマージされることを検証。 + +需要注意当key为n个项目组合而成时,需要在各組末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## データ生成手順 + +1. マージキー項目を特定(キー項目一覧から) +2. 各組のキー構成に従いレコードを生成(键值升序,组4除外) +3. 非キーフィールドはPIC定義に従い実在しそうな値を生成し、隣接レコード間で異なる値を設定 +4. 組1〜組3末尾に多keyテストデータを追加 +5. 生成後自检(MR-R001相当):マージ後件数 = 全入力ファイル件数の合計 を確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/SELECT処理.md b/black-box-data-create/rules/pgm_pattern/SELECT処理.md new file mode 100644 index 0000000..8eab76b --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/SELECT処理.md @@ -0,0 +1,50 @@ +# SELECT処理 データ生成規則 + +## 概要 + +DBテーブルから条件付きSELECTでデータを取得し、取得結果に基づいて処理を行うプログラムのテストデータ。入力はDB(SELECT結果)であり、出力はSQL INSERTファイル。 + +本规则定义3组数据,覆盖基本匹配/非匹配、0件、NULL处理。 + +## 输入 + +- 詳細設計書の処理詳細(SELECT文のWHERE条件) +- DBテーブル定義(テーブル名、カラム、主キー) + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本データ | SELECT条件にマッチするデータ + マッチしないデータ + 末尾多keyテスト | +| 2 | SELECT 0件(DB-N006) | 条件に合うレコードが0件になるデータ + 末尾多keyテスト | +| 3 | NULL処理(DB-A003) | NULL項目を含むデータ(MOVE/FETCH時のNULL扱い確認)+ 末尾多keyテスト | + +## 多keyテスト + +WHERE条件に複数キーが関わる場合、基準データに対し各キー項目のみ異なるN件を生成。マッチするパターンとマッチしないパターンの両方をカバー。 + +需要注意当key为n个项目组合而成时,需要追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## データ生成手順 + +1. SELECT文のWHERE条件を抽出 +2. 条件にマッチするレコードを生成(组1) +3. 条件にマッチしないレコード(境界値)を生成(组1) +4. 条件に合うレコードが存在しないデータを生成(组2) +5. NULL項目を含むデータを生成(组3) +6. 各組末尾に多keyテストデータを追加 + +注:存在しないテーブルへのSELECT(DB-A002)はSQL実行環境で確認する。 + +## 出力 + +每组1个SQL INSERT文件:`{プログラムID}_g{groupId}.sql` + +组数约束:输出组数必须与本规则定义的3组一致(g1〜g3),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/SORT.md b/black-box-data-create/rules/pgm_pattern/SORT.md new file mode 100644 index 0000000..bc23139 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/SORT.md @@ -0,0 +1,64 @@ +# SORT データ生成規則 + +## 概要 + +SORT文(USING/GIVING、INPUT PROCEDURE、OUTPUT PROCEDURE)によりデータを並べ替えるプログラムのテストデータ。 + +本规则定义6组数据,覆盖升序、降序/多键、重复键/0条/1条、INPUT PROCEDURE、OUTPUT PROCEDURE、異常系。 + +## 输入 + +- 詳細設計書の処理詳細(SORTキー、昇順/降順、PROCEDUREの有無) +- 入出力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 简单升序(SR-N001) | SORT USING/GIVING 昇順、複数レコード | +| 2 | 降序+多键(SR-N002/SR-N003) | DESCENDING指定、第1キー+第2キーの複合キー | +| 3 | 重复键/边界(SR-N004〜SR-N006) | 同キー順序保持、0件、1件 | +| 4 | INPUT PROCEDURE(SR-N007/SR-N009) | ソート前の選別・編集(RELEASE送出) | +| 5 | OUTPUT PROCEDURE(SR-N008/SR-N010) | ソート後の集計・レポート(RETURN受取) | +| 6 | 異常系(SR-A002/SR-A003) | INPUT内でRELEASE漏れ(データ消失)、OUTPUT内でRETURN漏れ(無限ループ) | + +### 组1:简单升序(SR-N001) + +キー: c003→a001→b002(任意順の入力)→ 昇順 a001→b002→c003 に並ぶデータ。 + +### 组2:降序+多键(SR-N002/SR-N003) + +- 降順(DESCENDING)データ +- 第1キー+第2キー(例: 部署コード昇順+社員番号昇順)の複合キーデータ + +### 组3:重复键/边界(SR-N004〜SR-N006) + +- 同一キーが複数件あるデータ(安定ソートで入力順が維持されること) +- 0件のデータ(空ファイル) +- 1件のデータ + +### 组4:INPUT PROCEDURE(SR-N007/SR-N009) + +ソート前に条件で選別するデータ(例: ステータス='1'のみRELEASE)。選別漏れがないことを確認。 + +### 组5:OUTPUT PROCEDURE(SR-N008/SR-N010) + +ソート後にRETURNで受け取り集計・レポート出力するデータ。グループ集計が正しいことを確認。 + +### 组6:異常系(SR-A002/SR-A003) + +- INPUT PROCEDURE内でRELEASE漏れが起きるケース(データ消失の検証) +- OUTPUT PROCEDURE内でRETURN漏れが起きるケース(無限ループの検証) + +## データ生成手順 + +1. SORTキー・昇降順・PROCEDURE有無の特定 +2. 组1〜组5: 正常系データ +3. 组6: 異常系データ +4. 生成後自检(SR-R001):RC=0で正常終了。RC=16/20は環境依存(SR-R002/R003) + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的6组一致(g1〜g6),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/SYSIN読込.md b/black-box-data-create/rules/pgm_pattern/SYSIN読込.md new file mode 100644 index 0000000..c176e17 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/SYSIN読込.md @@ -0,0 +1,57 @@ +# SYSIN読込 データ生成規則 + +## 概要 + +JCLのSYSIN(制御カード)を読み込み、パラメータを解析して処理を分岐するプログラムのテストデータ。 + +本规则定义5组数据,覆盖正常参数、多卡连续、终了处理、異常系、解析結果確認。 + +## 输入 + +- 詳細設計書の処理詳細(SYSINカードのレイアウト、カード種別) +- パラメータ項目定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 正常参数读取(SY-N001) | 標準パラメータカード→正しく解析 | +| 2 | 多卡连续(SY-N002) | 複数カードを順次読み取り | +| 3 | 终了处理(SY-N003) | SYSINを読み終えた後の正常継続 | +| 4 | 異常系(SY-A001〜SY-A003) | フォーマット不正、0件(デフォルト処理)、OPEN失敗(環境テスト) | +| 5 | 解析結果確認(SY-R001) | 解析後のパラメータ値が期待どおり | + +### 组1:正常参数读取(SY-N001) + +標準的なパラメータカード1枚のデータ(例: 'T' カード)。カード種別の識別とパラメータ解析が正しいことを確認。 + +### 组2:多卡连续(SY-N002) + +複数カード(例: 'T' カード+'P' カード+'M' カード)を順次読み取るデータ。全カードが正しく処理されることを確認。 + +### 组3:终了处理(SY-N003) + +SYSINを読み終えた(EOF)後に正常に後続処理へ進むデータ。ファイル終了時の処理漏れがないことを確認。 + +### 组4:異常系(SY-A001〜SY-A003) + +- カードのフォーマット不正(項目数不足・不正値)→ エラー処理/ABEND +- SYSIN 0件(カードなし)→ デフォルト値で処理 +- SYSINファイル不存在(OPEN STATUS 35)→ 環境テスト項目 + +### 组5:解析結果確認(SY-R001) + +解析後のパラメータ値が期待どおりであることを確認するデータ(カード種別ごとの分岐結果を含む)。 + +## データ生成手順 + +1. カードレイアウト・カード種別の特定 +2. 组1〜组3: 正常系データ +3. 组4: 異常系データ +4. 组5: 解析結果の自检 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/オンラインPGM.md b/black-box-data-create/rules/pgm_pattern/オンラインPGM.md new file mode 100644 index 0000000..35f39b6 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/オンラインPGM.md @@ -0,0 +1,53 @@ +# オンラインPGM データ生成規則 + +## 概要 + +CICSオンライン(DFHCOMMAREA・MAP送受信)で画面入出力とDBアクセスを行うプログラムのテストデータ。 + +※ 現システム(バッチ中心)には該当プログラムがないため、将来のオンライン追加用に用意する。 + +本规则定义4组数据,覆盖COMMAREA受信、MAP入出力/画面遷移、異常系、応答確認。 + +## 输入 + +- 詳細設計書の処理詳細(DFHCOMMAREA項目、MAP名、画面遷移) +- 連絡領域(COMMAREA)のレイアウト定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | COMMAREA受信+MAP入出力(OL-N001/N002) | 渡されたCOMMAREAを正しく受信、SEND/RECEIVE MAP | +| 2 | 画面入力→DB検索→遷移(OL-N003/N004) | 画面入力値をキーにDB検索→結果表示、画面A→B→Cのデータ引継ぎ | +| 3 | 異常系(OL-A001/OL-A002) | COMMAREA長不一致、トランザクションABEND→ROLLBACK | +| 4 | 応答確認(OL-R001) | 応答時間測定用の件数バリエーション | + +### 组1:COMMAREA受信+MAP入出力(OL-N001/N002) + +CALL側から渡されたDFHCOMMAREAの内容を正しく受信するデータ。SEND MAP / RECEIVE MAP の入出力パターンをカバー。 + +### 组2:画面入力→DB検索→遷移(OL-N003/OL-N004) + +画面入力値をキーにDB検索し結果を表示するデータ。複数画面(A→B→C)間でデータが引き継がれるケースを含む。 + +### 组3:異常系(OL-A001/OL-A002) + +- DFHCOMMAREA長がCALL側と受信側で異なるデータ +- トランザクション内で異常が発生しROLLBACKされるケース + +### 组4:応答確認(OL-R001) + +応答時間測定用に件数を変えたデータ(少量・大量のバリエーション)。 + +## データ生成手順 + +1. COMMAREA・MAP定義の特定 +2. 组1/组2: 正常系データ +3. 组3: 異常系データ +4. 组4: 応答時間確認データ + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/キーブレイク(集約).md b/black-box-data-create/rules/pgm_pattern/キーブレイク(集約).md new file mode 100644 index 0000000..eadcfcd --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/キーブレイク(集約).md @@ -0,0 +1,102 @@ +# キーブレイク(集約) データ生成規則 + +## 概要 + +キー項目でソート済みの入力ファイルを読み取り、キーブレイク時に集約レコード(件数・最大・最小など)を出力するプログラムのテストデータを生成する。 + +本规则定义7组数据,覆盖测试基准说明 §2.2「key切测试(汇总 / 聚合 / 非汇总)」中聚合型相关的 KB-N002、KB-N004、KB-N005、KB-N006、KB-A001、KB-A002、KB-R001。汇总型(ADD累加)は「キーブレイク(集計).md」、非汇总型(键变化做标记)は「キーブレイク(非集計).md」を参照。 + +## 输入 + +- 詳細設計書のキー項目一覧(ソートキー) +- 入力ファイルのCOPYBOOKフィールド定義 +- 集約対象フィールド(処理詳細から件数・最大・最小の算出対象を特定) +- キー条件:键值升序,键项目为复合键时按多keyテスト追加 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本聚合(KB-N002) | 同键多件→件数/最大/最小が正しいデータ(複数グループ、各グループ複数レコード)+ 末尾に多keyテスト | +| 2 | 单键组(KB-N004) | 全件同一キー→キーブレイク1回のデータ + 末尾に多keyテスト | +| 3 | 多键组(KB-N005) | N個の異なるキー→Nグループ出力のデータ + 末尾に多keyテスト | +| 4 | 单条记录(KB-N006) | 1件1グループ(自身が集約値)のデータ + 末尾に多keyテスト | +| 5 | 前键值未初始化(KB-A001) | 先頭レコードのキーが通常値(非スペース)のデータ。WS-PREV-KEYが初期化されていない場合に先頭で誤中断しないか確認 | +| 6 | 累加器溢出(KB-A002) | 集約対象フィールドの合計がPIC定義を超えるデータ(SIZE ERROR/エラー処理確認) | +| 7 | 各组小计=合计(KB-R001) | 各グループの集約値の合計と全体合計が一致することを確認するデータ | + +### 组1:基本聚合(KB-N002) + +キー: a001(3件)→a002(2件)→a003(1件)(3グループ以上、各グループ内複数レコード、キー昇順) + +同键内の件数・最大・最小などが設計書どおり算出されることを確認する。 + +### 组2:单键组(KB-N004) + +キー: a001(5件)(全件同一キー) + +全体が1つのキーグループ→キーブレイク1回のみ。集約値(件数・最大・最小)が全件から算出されることを確認する。 + +### 组3:多键组(KB-N005) + +キー: a001(2件)→a002(1件)→a003(3件)(N個の異なるキー) + +キー変化N-1回→Nグループ出力されることを確認する。 + +### 组4:单条记录(KB-N006) + +キー: a001(1件)→a002(3件)→a003(1件)(単一レコードグループを含む) + +1件1グループの場合、そのレコード自身が集約値(件数1・値そのもの)になることを確認する。 + +### 组5:前键值未初始化(KB-A001) + +キー: a001(2件)→a002(2件)(先頭レコードのキーが通常値) + +期待:先頭で誤中断せず、1グループ目は1回の中断のみ(前鍵値フィールドが正しく初期化されていること)。 + +### 组6:累加器溢出(KB-A002) + +集約対象フィールドの合計がPIC定義を超えるデータ(例: PIC 9(3)金額 999 × 2件 → 合計1,998でオーバーフロー)。 + +期待:SIZE ERROR/設計書どおりのエラー処理が発生することを確認。 + +### 组7:各组小计=合计(KB-R001) + +各グループの集約値の合計と全体合計が一致することを確認するデータ(组1〜组4相当のグループ構成)。 + +## 多keyテスト(各組の末尾に追加) + +キーが複数項目で構成される場合: +1. 該当グループ内から1件のレコードを選び基準データとする +2. N個のキー項目に対し、それぞれ1項目のみ異なるN件の追加レコードを生成 +3. これらのレコードは別グループとして扱われるべき(集約値が別になる) + +需要注意当key为n个项目组合而成时,需要在各組末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## データ生成手順 + +1. キー項目を特定(キー項目一覧のソート条件から) +2. 集約フィールドを特定(処理詳細から件数・最大・最小の算出対象を抽出) +3. 组1: 3グループ以上、各グループ内複数レコード +4. 组2: 全件同一キー +5. 组3: N個の異なるキー +6. 组4: 単一レコードグループを含む +7. 组5: 先頭キーが通常値のデータ +8. 组6: 集約合計がPIC定義を超えるデータ +9. 组7: グループ小計と全体合計の整合確認データ +10. 各組末尾に多keyテストデータを追加 +11. 生成後自检(KB-R001):各グループの集約値の合計 = 全体合計 を確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的7组一致(g1〜g7),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/キーブレイク(集計).md b/black-box-data-create/rules/pgm_pattern/キーブレイク(集計).md new file mode 100644 index 0000000..d7842cb --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/キーブレイク(集計).md @@ -0,0 +1,102 @@ +# キーブレイク(集計) データ生成規則 + +## 概要 + +キー項目でソート済みの入力ファイルを読み取り、キーが変わるタイミング(キーブレイク)で集計値(ADD累加による金額合計等)を計算し、1グループにつき1件の集計レコードを出力するプログラムのテストデータを生成する。 + +本规则定义7组数据,覆盖测试基准说明 §2.2「key切测试(汇总 / 聚合 / 非汇总)」中汇总型相关的 KB-N001、KB-N004、KB-N005、KB-N006、KB-A001、KB-A002、KB-R001。集約型(件数・最大・最小)は「キーブレイク(集約).md」、非汇总型(键变化做标记)は「キーブレイク(非集計).md」を参照。 + +## 输入 + +- 詳細設計書のキー項目一覧(ソートキー) +- 入力ファイルのCOPYBOOKフィールド定義 +- 集計対象フィールド(処理詳細からADD/COMPUTE対象を特定) +- キー条件:键值升序,键项目为复合键时按多keyテスト追加 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本汇总(KB-N001) | 同键多件→金額合計が正しいデータ(複数グループ、各グループ複数レコード)+ 末尾に多keyテスト | +| 2 | 单键组(KB-N004) | 全件同一キー→キーブレイク1回のデータ + 末尾に多keyテスト | +| 3 | 多键组(KB-N005) | N個の異なるキー→Nグループ出力のデータ + 末尾に多keyテスト | +| 4 | 单条记录(KB-N006) | 1件1グループ(自身が集計値)のデータ + 末尾に多keyテスト | +| 5 | 前键值未初始化(KB-A001) | 先頭レコードのキーが通常値(非スペース)のデータ。WS-PREV-KEYが初期化されていない場合に先頭で誤中断しないか確認 | +| 6 | 累加器溢出(KB-A002) | 集計値(金額合計)がPIC定義を超えるデータ(SIZE ERROR/エラー処理確認) | +| 7 | 各组小计=合计(KB-R001) | 各グループの集計値の合計と全体合計が一致することを確認するデータ | + +### 组1:基本汇总(KB-N001) + +キー: a001(3件)→a002(2件)→a003(1件)(3グループ以上、各グループ内複数レコード、キー昇順) + +同键内の集計対象フィールド(例: 金額)をADD累加し、グループごとの合計が正しいことを確認する。 + +### 组2:单键组(KB-N004) + +キー: a001(5件)(全件同一キー) + +全体が1つのキーグループ→キーブレイク1回のみ。合計が全件の累加値と一致することを確認する。 + +### 组3:多键组(KB-N005) + +キー: a001(2件)→a002(1件)→a003(3件)(N個の異なるキー) + +キー変化N-1回→Nグループ出力されることを確認する。 + +### 组4:单条记录(KB-N006) + +キー: a001(1件)→a002(3件)→a003(1件)(単一レコードグループを含む) + +1件1グループの場合、そのレコード自身が集計値になることを確認する。 + +### 组5:前键值未初始化(KB-A001) + +キー: a001(2件)→a002(2件)(先頭レコードのキーが通常値) + +期待:先頭で誤中断せず、1グループ目は1回の中断のみ(前鍵値フィールドが正しく初期化されていること)。 + +### 组6:累加器溢出(KB-A002) + +集計対象フィールドの合計がPIC定義を超えるデータ(例: PIC 9(3)金額 999 × 2件 → 合計1,998でオーバーフロー)。 + +期待:SIZE ERROR/設計書どおりのエラー処理が発生することを確認。 + +### 组7:各组小计=合计(KB-R001) + +各グループの集計値の合計と全体合計が一致することを確認するデータ(组1〜组4相当のグループ構成)。 + +## 多keyテスト(各組の末尾に追加) + +キーが複数項目で構成される場合: +1. 該当グループ内から1件のレコードを選び基準データとする +2. N個のキー項目に対し、それぞれ1項目のみ異なるN件の追加レコードを生成 +3. これらのレコードは別グループとして扱われるべき(集計値が別になる) + +需要注意当key为n个项目组合而成时,需要在各組末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## データ生成手順 + +1. キー項目を特定(キー項目一覧のソート条件から) +2. 集計フィールドを特定(処理詳細からADD/COMPUTE対象を抽出) +3. 组1: 3グループ以上、各グループ2〜5レコード、キー昇順 +4. 组2: 全件同一キー +5. 组3: N個の異なるキー +6. 组4: 単一レコードグループを含む +7. 组5: 先頭キーが通常値のデータ +8. 组6: 集計値がPIC定義を超えるデータ +9. 组7: グループ小計と全体合計の整合確認データ +10. 各組末尾に多keyテストデータを追加 +11. 生成後自检(KB-R001):各グループの集計値の合計 = 全体合計 を確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的7组一致(g1〜g7),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/キーブレイク(非集計).md b/black-box-data-create/rules/pgm_pattern/キーブレイク(非集計).md new file mode 100644 index 0000000..dca60d4 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/キーブレイク(非集計).md @@ -0,0 +1,73 @@ +# キーブレイク(非集計) データ生成規則 + +## 概要 + +キーが変わるタイミングで集計値を持たずにキー変化の検知・フラグ設定・レコード出力のみを行うプログラム(集計・集約以外のキーブレイク)のテストデータ。 + +本规则定义5组数据,覆盖键变化检测、单键组、多键组、单条记录、前键值未初始化。 + +## 输入 + +- 詳細設計書のキー項目一覧(ソートキー) +- 入力ファイルのCOPYBOOKフィールド定義 +- キー条件:键值升序,键项目为复合键时按多keyテスト追加 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 键变化检测(KB-N003) | 複数グループでキー切替時のみ変化検知・マーク出力 | +| 2 | 单键组(KB-N004) | 全件同一キー→キーブレイク1回 | +| 3 | 多键组(KB-N005) | N個の異なるキー→Nグループ | +| 4 | 单条记录(KB-N006) | 1件1グループ(自身がグループ) | +| 5 | 前键值未初始化(KB-A001) | 先頭キーが通常値(非スペース)のデータ | + +### 组1:键变化检测(KB-N003) + +キー: a001(3件)→a002(2件)→a003(1件)。キー切替のタイミングで変化検知・マークが正しく出力されることを確認。 + +### 组2:单键组(KB-N004) + +キー: a001(5件)(全件同一キー)。キーブレイクが1回だけ発生することを確認。 + +### 组3:多键组(KB-N005) + +キー: a001→a002→a003→a004→a005(5件すべて異なるキー)。キー変化4回→5グループの出力を確認。 + +### 组4:单条记录(KB-N006) + +キー: a001(1件)(1件1グループ)。単独レコードが正しく1グループとして処理されることを確認。 + +### 组5:前键值未初始化(KB-A001) + +キー: a001(2件)→a002(2件)(先頭キーが通常値)。WS-PREV-KEYが初期化されていない場合に先頭で誤中断しないことを確認。 + +## 多keyテスト + +キーが複数項目の場合、各組末尾に追加。1レコードを基準に、各キー項目のみ異なるN件を生成。 + +需要注意当key为n个项目组合而成时,需要在各組末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。 + +## データ生成手順 + +1. キー項目の特定 +2. 组1: 複数グループの変化検知データ +3. 组2: 単一キーグループデータ +4. 组3: 全キー異なるデータ +5. 组4: 単件データ +6. 组5: 先頭キー通常値データ +7. 各組末尾に多keyテスト +8. 生成後自检:グループ数=キー変化回数+1 を確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/サブプログラム.md b/black-box-data-create/rules/pgm_pattern/サブプログラム.md new file mode 100644 index 0000000..556933b --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/サブプログラム.md @@ -0,0 +1,60 @@ +# サブプログラム データ生成規則 + +## 概要 + +サブプログラムをCALLで呼び出すメインプログラムのテストデータ。CALL文のバリエーション、引数渡し、RETURN-CODE、CALL先の状態保持を検証する。 + +※ 本規則はCALLER側(PGMタイプ=メイン)を対象とする。Callee単体(PGMタイプ=サブ)は generate() の対象外(AGENTS.md参照)。 + +本规则定义5组数据,覆盖基本CALL、动态CALL、返回处理、状态保持、异常系。 + +## 输入 + +- 詳細設計書の処理詳細(CALL文、引数、RETURN-CODE判定) +- CALL先サブプログラムのLINKAGE定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | CALL 字面量+参数传递(C-N001/N002) | CALL 'SUBPGM' USING ...、CALL側とLINKAGE側のパラメータ対応 | +| 2 | 动态CALL+RETURN-CODE(C-N003/N004) | CALL WS-PGM-NAME USING ...、RETURN-CODEの設定・検査 | +| 3 | 返回处理(C-N005/N006) | EXIT PROGRAMによる正常復帰、GOBACK(RETURN-CODE付き) | +| 4 | 状态保持(C-N007/C-N008) | IS INITIAL指定プログラム(毎CALL初期化)/非指定(値保持) | +| 5 | 异常系(C-A001〜C-A003) | CALL先不存在(ABEND)、パラメータ長不一致、CANCEL後再CALL | + +### 组1:CALL 字面量+参数传递(C-N001/N002) + +CALL 'SUB01DAT' USING ... のように字面量でCALLするデータ。CALL側のUSING引数とLINKAGE側のレイアウトが対応すること。 + +### 组2:动态CALL+RETURN-CODE(C-N003/N004) + +CALL WS-PGM-NAME USING ... のように変数名でCALLするデータ。サブプログラム側でRETURN-CODEを設定し、CALL側で検査するケースを含む。 + +### 组3:返回处理(C-N005/C-N006) + +EXIT PROGRAMで正常復帰するケースと、GOBACK(RETURN-CODE付き)で復帰するケースのデータ。 + +### 组4:状态保持(C-N007/C-N008) + +- IS INITIAL指定のサブプログラム: CALL毎にWSが初期化されることを確認するデータ +- 非IS INITIALのサブプログラム: CALL跨ぎでWS値が保持されることを確認するデータ + +### 组5:异常系(C-A001〜C-A003) + +- CALL先プログラムが存在しない(未ロード→ABEND) +- CALL USINGのパラメータ長がLINKAGE側と不一致 +- CANCEL後に再CALL(INITIAL状態に戻ること) + +## データ生成手順 + +1. CALL文・引数・RETURN-CODE判定の特定 +2. 组1〜组4: 正常系データ +3. 组5: 異常系データ +4. 生成後自检(C-R001/C-R002):CALL前の正常終了、CALL後のRETURN-CODE確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/マッチング(1-1).md b/black-box-data-create/rules/pgm_pattern/マッチング(1-1).md new file mode 100644 index 0000000..d8d059d --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/マッチング(1-1).md @@ -0,0 +1,172 @@ +# マッチング(1:1) 数据生成规则 + +## 概要 + +2个输入文件按指定键进行1:1匹配,输出匹配成功/仅R01/仅R02三种结果。 + +本规则定义8组数据:组1〜组4为正常系(覆盖MT-N001完全匹配、MT-N004主件剩余、MT-N005从件剩余),组5〜组8为异常系(覆盖MT-N006主键重复、MT-N007键值乱序,R01/R02两侧均测)。 + +## 输入 + +- 前提条件:从详细设计书解析匹配键(A001>A002>A003...) +- 输入文件:2个(R01、R02) +- 键条件:组1〜组4(正常系)键值唯一(重複NG)且升序排序 +- 组5(异常系):R01内故意生成重复键值 +- 组6(异常系):R01故意违反升序前提 +- 组7(异常系):R02内故意生成重复键值 +- 组8(异常系):R02故意违反升序前提 + +## 生成的数据组 + +| 组 | 用途 | R01记录数 | R02记录数 | 说明 | +|----|------|-----------|-----------|------| +| 1 | 两端不匹配 | 4 | 4 | 第1条R01-only、最后1条R02-only、中间3条以上匹配 | +| 2 | 反向两端不匹配 | 4 | 4 | 第1条R02-only、最后1条R01-only、中间3条以上匹配 | +| 3 | 中间不匹配 | 4 | 4 | 首尾匹配、中间有R01-only和R02-only各1条 | +| 4 | 完全匹配 | 4 | 4 | 双方各N件全部匹配成功,无剩余(MT-N001) | +| 5 | 主键重复-R01(异常) | 4 | 3 | R01内同一键出现2次(MT-N006) | +| 6 | 键值乱序-R01(异常) | 4 | 4 | R01违反升序前提(MT-N007) | +| 7 | 主键重复-R02(异常) | 3 | 4 | R02内同一键出现2次(MT-N006) | +| 8 | 键值乱序-R02(异常) | 4 | 4 | R02违反升序前提(MT-N007) | + +### 组1:两端不匹配 + +R01键:a001(R01-only) → a002 → a003 → a004 +R02键:a002 → a003 → a004 → a005(R02-only) + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) | R01-only | +| 第2条 | R01(a002) + R02(a002) | 匹配 | +| 第3条 | R01(a003) + R02(a003) | 匹配 | +| 第4条 | R01(a004) + R02(a004) | 匹配 | +| 第5条 | R02(a005) | R02-only | + +组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。和R02(a005)的所有key相比,只有当前第n个key与R02(a005)的key不一致。保证key的比较没有问题。 + +例(n=3): +| R01 key1 | R01 key2 | R01 key3 | R02 key1 | R02 key2 | R02 key3 | 说明 | +|----------|----------|----------|----------|----------|----------|------| +| a004 | b005 | c005 | a005 | b005 | c005 | 仅第1个key不一致 | +| a005 | b004 | c005 | a005 | b005 | c005 | 仅第2个key不一致 | +| a005 | b005 | c00c | a005 | b005 | c005 | 仅第3个key不一致 | + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数,严禁按示例机械生成3条)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与R01既有记录完全重复。 + +### 组2:反向两端不匹配 + +R01键:a002 → a003 → a004 → a005(R01-only) +R02键:a001(R02-only) → a002 → a003 → a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R02(a001) | R02-only | +| 第2条 | R01(a002) + R02(a002) | 匹配 | +| 第3条 | R01(a003) + R02(a003) | 匹配 | +| 第4条 | R01(a004) + R02(a004) | 匹配 | +| 第5条 | R01(a005) | R01-only | + +### 组3:中间不匹配 + +R01键:a001 → a003(R01-only) → a004 → a005 +R02键:a001 → a002(R02-only) → a004 → a005 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R02(a002) | R02-only | +| 第3条 | R01(a003) | R01-only | +| 第4条 | R01(a004) + R02(a004) | 匹配 | +| 第5条 | R01(a005) + R02(a005) | 匹配 | + +### 组4:完全匹配(MT-N001) + +R01键:a001 → a002 → a003 → a004 +R02键:a001 → a002 → a003 → a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R01(a002) + R02(a002) | 匹配 | +| 第3条 | R01(a003) + R02(a003) | 匹配 | +| 第4条 | R01(a004) + R02(a004) | 匹配 | + +### 组5:主键重复-R01(异常系,MT-N006) + +R01键:a001 → a002 → a002(重复) → a003 +R02键:a001 → a002 → a003 + +说明:R01中键a002出现2次,违反"键值唯一"前提。期望程序检出重复键并执行设计书规定的错误处理(错误输出/异常结束),不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R01(a002) + R02(a002) | 匹配 | +| 第3条 | R01(a002) | 重复键(第2次出现) | +| 第4条 | R01(a003) + R02(a003) | 匹配 | + +### 组6:键值乱序-R01(异常系,MT-N007) + +R01键:a001 → a003 → a002 → a004(a003先于a002,违反升序) +R02键:a001 → a002 → a003 → a004(正常升序) + +说明:R01违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R01(a003) + R02(a002) | 乱序错位(键不一致) | +| 第3条 | R01(a002) + R02(a003) | 乱序错位(键不一致) | +| 第4条 | R01(a004) + R02(a004) | 匹配 | + +### 组7:主键重复-R02(异常系,MT-N006) + +R01键:a001 → a002 → a003 +R02键:a001 → a002 → a002(重复) → a003 + +说明:R02中键a002出现2次,违反"键值唯一"前提。期望程序检出重复键并执行设计书规定的错误处理(错误输出/异常结束),不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R01(a002) + R02(a002) | 匹配 | +| 第3条 | R02(a002) | 重复键(第2次出现) | +| 第4条 | R01(a003) + R02(a003) | 匹配 | + +### 组8:键值乱序-R02(异常系,MT-N007) + +R01键:a001 → a002 → a003 → a004(正常升序) +R02键:a001 → a003 → a002 → a004(a003先于a002,违反升序) + +说明:R02违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001) + R02(a001) | 匹配 | +| 第2条 | R01(a002) + R02(a003) | 乱序错位(键不一致) | +| 第3条 | R01(a003) + R02(a002) | 乱序错位(键不一致) | +| 第4条 | R01(a004) + R02(a004) | 匹配 | + +## 数据生成步骤 + +1. 从前提条件解析匹配键字段(如 A001>A002>A003) +2. 从COPYBOOK获取各字段的PIC定义 +3. 生成键值:键值以 a001/b001/c001 格式递增。组1〜组4保证键值唯一且升序;组5/组7生成重复键;组6/组8故意乱序 +4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。 +5. 按各组规则组装records数组 +6. 组号从1开始连续编号 +7. 生成后自检(MT-R001):统计每组匹配件数M、R01-only件数r1、R02-only件数r2,校验 M + r1 = R01输入件数 且 M + r2 = R02输入件数(组6/组8乱序组因匹配结果不确定,仅确认records数组包含R01与R02的全部输入记录) + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的8组一致(g1〜g8),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。每组生成的records必须与对应组的定义完全一致。 diff --git a/black-box-data-create/rules/pgm_pattern/マッチング(1-N).md b/black-box-data-create/rules/pgm_pattern/マッチング(1-N).md new file mode 100644 index 0000000..9c9de64 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/マッチング(1-N).md @@ -0,0 +1,195 @@ +# マッチング(1:N) 数据生成规则 + +## 概要 + +2个输入文件,R01为主件(1侧),R02为从件(N侧)。1件R01可对应多件R02(1:N)。 + +本规则定义9组数据:组1〜组5为正常系(覆盖MT-N001完全匹配、MT-N002的1:N匹配、MT-N004主件剩余、MT-N005从件剩余、MT-N009极端不平衡),组6〜组9为异常系(覆盖MT-N006主键重复R01/R02、MT-N007键值乱序R01/R02)。 + +## 输入 + +- 前提条件:从详细设计书解析匹配键(A001>A002>A003...) +- 输入文件:2个(R01主件、R02从件) +- 键条件:组1〜组5(正常系)键值唯一(主件重複NG)且升序排序 +- 组6(异常系):R01内故意生成重复键值 +- 组7/组8(异常系):R01/R02故意违反升序前提 +- 组9(异常系):R02内故意生成重复键值 + +## 生成的数据组 + +| 组 | 用途 | R01记录数 | R02记录数 | 说明 | +|----|------|-----------|-----------|------| +| 1 | 1:1匹配 | 4 | 4 | 双方各N件全部1:1匹配(MT-N001),R01末尾追加n条多key变体(仅一个key不同→R01-only,验证键比较) | +| 2 | 1:N匹配 | 3 | 5 | R01一件对应R02多件,同key多条(MT-N002) | +| 3 | 主件剩余-R01 | 4 | 2 | 从件读完主件剩余→W03(MT-N004) | +| 4 | 从件剩余-R02 | 2 | 4 | 主件读完从件剩余→W02(MT-N005) | +| 5 | 极端不平衡1:N | 1 | 5 | R01=1条、R02=N条同key(MT-N009) | +| 6 | 主键重复-R01(异常) | 4 | 3 | R01内同一键出现2次(MT-N006) | +| 7 | 键值乱序-R01(异常) | 4 | 4 | R01违反升序前提(MT-N007) | +| 8 | 键值乱序-R02(异常) | 4 | 4 | R02违反升序前提(MT-N007) | +| 9 | 主键重复-R02(异常) | 3 | 4 | R02内同一键出现2次(MT-N006) | + +### 组1:1:1匹配(MT-N001) + +R01: a001→a002→a003→a004 +R02: a001→a002→a003→a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a003)+R02(a003) | 匹配 | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。和R02(a004)的所有key相比,只有当前第n个key与R02(a004)的key不一致。保证key的比较没有问题。 + +例(n=3): +| R01 key1 | R01 key2 | R01 key3 | R02 key1 | R02 key2 | R02 key3 | 说明 | +|----------|----------|----------|----------|----------|----------|------| +| a003 | b004 | c004 | a004 | b004 | c004 | 仅第1个key不一致 | +| a004 | b003 | c004 | a004 | b004 | c004 | 仅第2个key不一致 | +| a004 | b004 | c003 | a004 | b004 | c004 | 仅第3个key不一致 | + + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数,严禁按示例机械生成3条)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与R01既有记录完全重复。这n条追加记录与R02的任何key都不完全一致,作为R01-only追加在R01末尾(不影响前面全部匹配)。 + +### 组2:1:N匹配(MT-N002) + +R01: a001→a002→a003 +R02: a001→a002→a002→a002→a003(a002同key 3条) + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配(第1件) | +| 第3条 | R01(a002)+R02(a002) | 匹配(第2件) | +| 第4条 | R01(a002)+R02(a002) | 匹配(第3件) | +| 第5条 | R01(a003)+R02(a003) | 匹配 | + +### 组3:主件剩余-R01(MT-N004) + +R01: a001→a002→a003→a004 +R02: a001→a002 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a003) | R01-only(主件剩余→W03) | +| 第4条 | R01(a004) | R01-only(主件剩余→W03) | + +组3:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。和R02(a002)的所有key相比,只有当前第n个key与R02(a002)的key不一致。保证key的比较没有问题。 + +n为实际解析出的key项目数(追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与R01既有记录完全重复。 + +例(n=3): +| R01 key1 | R01 key2 | R01 key3 | R02 key1 | R02 key2 | R02 key3 | 说明 | +|----------|----------|----------|----------|----------|----------|------| +| a002 | b002 | c003 | a002 | b002 | c002 | 仅第3个key不一致 | +| a002 | b003 | c002 | a002 | b002 | c002 | 仅第2个key不一致 | +| a003 | b002 | c002 | a002 | b002 | c002 | 仅第1个key不一致 | + +### 组4:从件剩余-R02(MT-N005) + +R01: a001→a002 +R02: a001→a002→a003→a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R02(a003) | R02-only(从件剩余→W02) | +| 第4条 | R02(a004) | R02-only(从件剩余→W02) | + +### 组5:极端不平衡 1:N(MT-N009) + +R01: a001(1条) +R02: a001→a001→a001→a001→a001(5条同key) + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配(第1件) | +| 第2条 | R01(a001)+R02(a001) | 匹配(第2件) | +| 第3条 | R01(a001)+R02(a001) | 匹配(第3件) | +| 第4条 | R01(a001)+R02(a001) | 匹配(第4件) | +| 第5条 | R01(a001)+R02(a001) | 匹配(第5件) | + +### 组6:主键重复-R01(异常系,MT-N006) + +R01: a001→a002→a002(重复) →a003 +R02: a001→a002→a003 + +说明:R01中键a002出现2次,违反"键值唯一"前提(1:N程序中主件键值必须唯一)。期望程序检出重复键并执行设计书规定的错误处理(错误输出/异常结束),不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a002) | 重复键(第2次出现) | +| 第4条 | R01(a003)+R02(a003) | 匹配 | + +### 组7:键值乱序-R01(异常系,MT-N007) + +R01: a001→a003→a002→a004(a003先于a002,违反升序) +R02: a001→a002→a003→a004(正常升序) + +说明:R01违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a003)+R02(a002) | 乱序错位(键不一致) | +| 第3条 | R01(a002)+R02(a003) | 乱序错位(键不一致) | +| 第4条 | R01(a004)+R02(a004) | 匹配 | +### 组8:键值乱序-R02(异常系,MT-N007) + +R01: a001→a002→a003→a004(正常升序) +R02: a001→a003→a002→a004(a003先于a002,违反升序) + +说明:R02违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a003) | 乱序错位(键不一致) | +| 第3条 | R01(a003)+R02(a002) | 乱序错位(键不一致) | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +### 组9:主键重复-R02(异常系,MT-N006) + +R01: a001→a002→a003 +R02: a001→a002→a002(重复) →a003 + +说明:1:N程序中R02(从件)同key多条属于正常(N件匹配),本组验证从件重复键时程序的处理(R01一条对应R02两条)。若设计书对R02也要求键值唯一(重複NG),则本组用于验证重复键检出/错误处理,不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配(第1件) | +| 第3条 | R01(a002)+R02(a002) | 同key第2条(N件处理/重复检出) | +| 第4条 | R01(a003)+R02(a003) | 匹配 | + +## 数据生成步骤 + +1. 从前提条件解析匹配键字段(如 A001>A002>A003) +2. 从COPYBOOK获取各字段的PIC定义 +3. 生成键值:键值以 a001/b001/c001 格式递增。组1〜组5保证键值唯一且升序;组6/组9生成重复键;组7/组8故意乱序 +4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。 +5. 按各组规则组装records数组 +6. 组号从1开始连续编号 +7. 生成后自检(MT-R001):统计每组匹配件数m1(R01中被匹配的件数)、m2(R02中被匹配的件数)、R01-only件数r1、R02-only件数r2,校验 m1 + r1 = R01输入件数 且 m2 + r2 = R02输入件数(组7/组8乱序组因匹配结果不确定,仅确认records数组包含R01与R02的全部输入记录) + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的9组一致(g1〜g9),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。每组生成的records必须与对应组的定义完全一致。 diff --git a/black-box-data-create/rules/pgm_pattern/マッチング(M-N).md b/black-box-data-create/rules/pgm_pattern/マッチング(M-N).md new file mode 100644 index 0000000..9c6fd2f --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/マッチング(M-N).md @@ -0,0 +1,102 @@ +# マッチング(M:N) 数据生成规则 + +## 概要 + +R01(主件)与R02(从件)按指定键进行M:N匹配:同一键下R01有M件、R02有N件时,程序按设计书输出M件、N件或M×N件(笛卡尔积)。 + +本规则定义4组数据:组1为完全匹配(MT-N001),组2〜组4覆盖M:N的三种输出形态(AM-N005笛卡尔积、AM-N003输出M件、AM-N004输出N件)。 + +## 输入 + +- 前提条件:从详细设计书解析匹配键(A001>A002>A003...) +- 输入文件:2个(R01、R02) +- 键条件:各组键值升序;同一键下R01/R02按组定义生成M条/N条同key数据 + +## 生成的数据组 + +| 组 | 用途 | R01记录数 | R02记录数 | 说明 | +|----|------|-----------|-----------|------| +| 1 | 完全匹配 | 4 | 4 | 双方各N件全部1:1匹配,无剩余(MT-N001) | +| 2 | M:N笛卡尔积 | 3 | 3 | 同key下R01×2、R02×2→M×N=4件(AM-N005) | +| 3 | M:N→M件 | 3 | 2 | 同key下R01×2、R02×1→输出M件(AM-N003) | +| 4 | M:N→N件 | 2 | 3 | 同key下R01×1、R02×2→输出N件(AM-N004) | + +### 组1:完全匹配(MT-N001) + +R01: a001→a002→a003→a004 +R02: a001→a002→a003→a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a003)+R02(a003) | 匹配 | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +### 组2:M:N笛卡尔积(AM-N005) + +R01: a001→a001→a002(a001×2) +R02: a001→a001→a002(a001×2) + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 组合1 | +| 第2条 | R01(a001)+R02(a001) | 组合2 | +| 第3条 | R01(a001)+R02(a001) | 组合3 | +| 第4条 | R01(a001)+R02(a001) | 组合4 | +| 第5条 | R01(a002)+R02(a002) | 匹配 | + +### 组3:M:N→M件(AM-N003) + +R01: a001→a001→a002(a001×2) +R02: a001→a002 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配(主件第1条) | +| 第2条 | R01(a001)+R02(a001) | 匹配(主件第2条)→输出M件 | +| 第3条 | R01(a002)+R02(a002) | 匹配 | + +### 组4:M:N→N件(AM-N004) + +R01: a001→a002 +R02: a001→a001→a002(a001×2) + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配(从件第1条) | +| 第2条 | R01(a001)+R02(a001) | 匹配(从件第2条)→输出N件 | +| 第3条 | R01(a002)+R02(a002) | 匹配 | + +## 多keyテスト + +需要注意当key为n个项目组合而成时,需要在末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 a002 = (a002, b002, c002)): +| 追加 key1 | 追加 key2 | 追加 key3 | 基准 key1 | 基准 key2 | 基准 key3 | 说明 | +|----------|----------|----------|----------|----------|----------|------| +| a001 | b002 | c002 | a002 | b002 | c002 | 仅第1个key不一致 | +| a002 | b001 | c002 | a002 | b002 | c002 | 仅第2个key不一致 | +| a002 | b002 | c001 | a002 | b002 | c002 | 仅第3个key不一致 | + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## 数据生成步骤 + +1. 从前提条件解析匹配键字段(如 A001>A002>A003) +2. 从COPYBOOK获取各字段的PIC定义 +3. 生成键值:键值以 a001/b001/c001 格式递增,各组按定义生成M/N条同key数据,键值升序 +4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致 +5. 按各组规则组装records数组 +6. 组号从1开始连续编号 +7. 生成后自检(MT-R001):统计每组匹配件数m1(R01中被匹配的件数)、m2(R02中被匹配的件数)、R01-only件数r1、R02-only件数r2,校验 m1 + r1 = R01输入件数 且 m2 + r2 = R02输入件数 + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。每组生成的records必须与对应组的定义完全一致。 diff --git a/black-box-data-create/rules/pgm_pattern/マッチング(N-1).md b/black-box-data-create/rules/pgm_pattern/マッチング(N-1).md new file mode 100644 index 0000000..f321b87 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/マッチング(N-1).md @@ -0,0 +1,188 @@ +# マッチング(N:1) 数据生成规则 + +## 概要 + +2个输入文件,R01为主件(N侧),R02为从件(1侧)。N件R01可对应1件R02(N:1)。 + +本规则定义9组数据:组1〜组5为正常系(覆盖MT-N001完全匹配、MT-N003的N:1匹配、MT-N004主件剩余、MT-N005从件剩余、MT-N010极端不平衡N:1),组6〜组9为异常系(覆盖MT-N006主键重复R01/R02、MT-N007键值乱序R01/R02)。 + +## 输入 + +- 前提条件:从详细设计书解析匹配键(A001>A002>A003...) +- 输入文件:2个(R01主件、R02从件) +- 键条件:组1/组3/组4(正常系)键值唯一且升序;组2/组5はR01同key多条(N件匹配)を意図的に生成、R02側は键值唯一 +- 组6(异常系):R01内故意生成重复键值(設計書が主件キーの一意性を要求する場合) +- 组7/组8(异常系):R01/R02故意违反升序前提 +- 组9(异常系):R02内故意生成重复键值(1侧・从件键值唯一違反) + +## 生成的数据组 + +| 组 | 用途 | R01记录数 | R02记录数 | 说明 | +|----|------|-----------|-----------|------| +| 1 | 1:1匹配 | 4 | 4 | 双方各N件全部1:1匹配(MT-N001),R01末尾追加n条多key变体(仅一个key不同→R01-only,验证键比较) | +| 2 | N:1匹配 | 5 | 3 | R02一件对应R01多件,R01同key多条(MT-N003) | +| 3 | 主件剩余-R01 | 4 | 2 | 从件读完主件剩余→W03(MT-N004) | +| 4 | 从件剩余-R02 | 2 | 4 | 主件读完从件剩余→W02(MT-N005) | +| 5 | 极端不平衡N:1 | 5 | 1 | R01=N条同key、R02=1条(MT-N010) | +| 6 | 主键重复-R01(异常) | 4 | 3 | R01内同一键出现2次(MT-N006) | +| 7 | 键值乱序-R01(异常) | 4 | 4 | R01违反升序前提(MT-N007) | +| 8 | 键值乱序-R02(异常) | 4 | 4 | R02违反升序前提(MT-N007) | +| 9 | 主键重复-R02(异常) | 3 | 4 | R02内同一键出现2次(MT-N006) | + +### 组1:1:1匹配(MT-N001) + +R01: a001→a002→a003→a004 +R02: a001→a002→a003→a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a003)+R02(a003) | 匹配 | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。和R02(a004)的所有key相比,只有当前第n个key与R02(a004)的key不一致。保证key的比较没有问题。 + +例(n=3): +| R01 key1 | R01 key2 | R01 key3 | R02 key1 | R02 key2 | R02 key3 | 说明 | +|----------|----------|----------|----------|----------|----------|------| +| a004 | b003 | c004 | a004 | b004 | c004 | 仅第2个key不一致 | +| a003 | b004 | c004 | a004 | b004 | c004 | 仅第1个key不一致 | +| a004 | b004 | c003 | a004 | b004 | c004 | 仅第3个key不一致 | + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数,严禁按示例机械生成3条)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与R01既有记录完全重复。这n条追加记录与R02的任何key都不完全一致,作为R01-only追加在R01末尾(不影响前面全部匹配)。 + +### 组2:N:1匹配(MT-N003) + +R01: a001→a002→a002→a002→a003(a002同key 3条) +R02: a001→a002→a003 + +说明:R02(从件・1侧)1件に対しR01(主件・N侧)N件が対応するN:1匹配(MT-N003)。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配(第1件) | +| 第3条 | R01(a002)+R02(a002) | 匹配(第2件) | +| 第4条 | R01(a002)+R02(a002) | 匹配(第3件) | +| 第5条 | R01(a003)+R02(a003) | 匹配 | + +### 组3:主件剩余-R01(MT-N004) + +R01: a001→a002→a003→a004 +R02: a001→a002 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R01(a003) | R01-only(主件剩余→W03) | +| 第4条 | R01(a004) | R01-only(主件剩余→W03) | + +### 组4:从件剩余-R02(MT-N005) + +R01: a001→a002 +R02: a001→a002→a003→a004 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R02(a003) | R02-only(从件剩余→W02) | +| 第4条 | R02(a004) | R02-only(从件剩余→W02) | + +### 组5:极端不平衡 N:1(MT-N010) + +R01: a001→a001→a001→a001→a001(5条同key) +R02: a001(1条) + +说明:N:1プログラムではR01(主件)の同key多条は本来N件匹配として正常だが、MT-N010(N:1 极端不平衡)として R01=N条・R02=1条 の極端な件数差を確認する。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配(第1件) | +| 第2条 | R01(a001)+R02(a001) | 同key第2条(N:1极端不平衡) | +| 第3条 | R01(a001)+R02(a001) | 同key第3条 | +| 第4条 | R01(a001)+R02(a001) | 同key第4条 | +| 第5条 | R01(a001)+R02(a001) | 同key第5条 | + +### 组6:主键重复-R01(异常系,MT-N006) + +R01: a001→a002→a002(重复) →a003 +R02: a001→a002→a003 + +说明:N:1程序中R01(主件)同key多条属于正常(N件匹配),本组验证主件重复键时程序的处理(N件R01对应1件R02)。若设计书对R01也要求键值唯一(1:1相当处理),则本组用于验证重复键检出/错误处理,不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配(第1件) | +| 第3条 | R01(a002)+R02(a002) | 同key第2条(N件处理/重复检出) | +| 第4条 | R01(a003)+R02(a003) | 匹配 | + +### 组7:键值乱序-R01(异常系,MT-N007) + +R01: a001→a003→a002→a004(a003先于a002,违反升序) +R02: a001→a002→a003→a004(正常升序) + +说明:R01违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a003)+R02(a002) | 乱序错位(键不一致) | +| 第3条 | R01(a002)+R02(a003) | 乱序错位(键不一致) | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +### 组8:键值乱序-R02(异常系,MT-N007) + +R01: a001→a002→a003→a004(正常升序) +R02: a001→a003→a002→a004(a003先于a002,违反升序) + +说明:R02违反升序前提,程序可能产生误匹配或未预期结果。本组用于确认程序对乱序输入的行为(按设计书确认期望结果),不要求生成"正确匹配"。 + +records数组(按文件读取顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a003) | 乱序错位(键不一致) | +| 第3条 | R01(a003)+R02(a002) | 乱序错位(键不一致) | +| 第4条 | R01(a004)+R02(a004) | 匹配 | + +### 组9:主键重复-R02(异常系,MT-N006) + +R01: a001→a002→a003 +R02: a001→a002→a002(重复) →a003 + +说明:R02中键a002出现2次,违反"键值唯一"前提(N:1程序中从件・1侧键值必须唯一)。期望程序检出重复键并执行设计书规定的错误处理(错误输出/异常结束),不得静默处理。 + +records数组(按处理顺序): +| 处理顺序 | 内容 | 说明 | +|---------|------|------| +| 第1条 | R01(a001)+R02(a001) | 匹配 | +| 第2条 | R01(a002)+R02(a002) | 匹配 | +| 第3条 | R02(a002) | 重复键(第2次出现) | +| 第4条 | R01(a003)+R02(a003) | 匹配 | + +## 数据生成步骤 + +1. 从前提条件解析匹配键字段(如 A001>A002>A003) +2. 从COPYBOOK获取各字段的PIC定义 +3. 生成键值:键值以 a001/b001/c001 格式递增。组1/组3/组4保证键值唯一且升序;组2/组5生成R01同key多条(N件匹配);组6/组9生成重复键;组7/组8故意乱序 +4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。 +5. 按各组规则组装records数组 +6. 组号从1开始连续编号 +7. 生成后自检(MT-R001):统计每组匹配件数m1(R01中被匹配的件数)、m2(R02中被匹配的件数)、R01-only件数r1、R02-only件数r2,校验 m1 + r1 = R01输入件数 且 m2 + r2 = R02输入件数(组8/组9乱序组因匹配结果不确定,仅确认records数组包含R01与R02的全部输入记录) + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的9组一致(g1〜g9),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。每组生成的records必须与对应组的定义完全一致。 diff --git a/black-box-data-create/rules/pgm_pattern/ランキング生成.md b/black-box-data-create/rules/pgm_pattern/ランキング生成.md new file mode 100644 index 0000000..f9bf164 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/ランキング生成.md @@ -0,0 +1,57 @@ +# ランキング生成 データ生成規則 + +## 概要 + +集計結果をスコア降順に順位付けし、ランキングレポートを出力するプログラムのテストデータ。 + +本规则定义5组数据,覆盖基本ランキング、同点処理、境界、異常系、件数確認。 + +## 输入 + +- 詳細設計書の処理詳細(順位キー、スコア項目、降順/昇順、同点時の順位規則) +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本ランキング | スコア降順で順位が正しく付与されるデータ(5件以上) | +| 2 | 同点処理 | 同スコアの順位規則(同順位・次順位スキップ等)を確認するデータ | +| 3 | 境界 | 1件のみ・0件のデータ | +| 4 | 異常系 | スコアがPIC定義を超える・不正値のデータ(エラー処理確認) | +| 5 | 件数確認 | 出力件数が入力件数と一致することを確認するデータ | + +### 组1:基本ランキング + +スコア: 95→80→72→65→50 の5件。降順に順位1〜5が付与されることを確認。 + +### 组2:同点処理 + +スコア: 80,80,70,70,50 の5件。同点時の順位規則(同順位付与、次順位スキップ)が設計書どおりであることを確認。 + +### 组3:境界 + +- 1件のみ(自身が1位)のデータ +- 0件(空ファイル)のデータ + +### 组4:異常系 + +スコアがPIC定義(例: 9(3))を超える値、または不正な値(非数値)を含むデータ。エラー処理が設計書どおりであることを確認。 + +### 组5:件数確認 + +ランキング出力件数(正常+異常)の合計が入力件数と一致するデータ。 + +## データ生成手順 + +1. 順位キー・スコア項目・順位規則の特定 +2. 组1〜组3: 正常系データ +3. 组4: 異常系データ +4. 组5: 件数確認データ +5. 生成後自检:順位の連続性と出力件数の整合確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md b/black-box-data-create/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md new file mode 100644 index 0000000..794c268 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md @@ -0,0 +1,32 @@ +# レイアウト編集のみ(GETPUT) データ生成規則 + +## 概要 + +入力ファイルまたはDBからレコードを読み取り、項目の編集・変換を行ってそのまま出力ファイルに書き出すプログラムのテストデータを生成する。 + +処理パターン:READ → 編集/計算/変換 → WRITE(シンプルなパイプライン) + +## 入力 + +- 詳細設計書の処理詳細(編集・変換内容) +- 入力ファイルのCOPYBOOKフィールド定義 +- 出力レコード定義(出力項目と設定元の対応) + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本データ | 通常パターン2〜5件。各フィールドに有効な値を設定。 | + +## データ生成手順 + +1. **入力フィールドの把握**: COPYBOOKから全入力フィールドを取得 +2. **データ生成**: + - 通常値: 各フィールドのPIC定義に従い有効な値を生成 + - 異なる値: レコード間で同じ項目に異なる値を設定しバリエーションを確保 +3. **値のルール**: + - PIC X(n): 左詰め・スペース埋め、実在しそうな文字列 + - PIC 9(n): 右詰め・先行ゼロ、実在しそうな数値 + - DATE/日付系: 実在しそうな日付 + - 金額/SALARY系: 現実的な金額 +4. **出力**: 1組 = 1 JSONファイル diff --git a/black-box-data-create/rules/pgm_pattern/内部テーブル検索.md b/black-box-data-create/rules/pgm_pattern/内部テーブル検索.md new file mode 100644 index 0000000..519ac10 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/内部テーブル検索.md @@ -0,0 +1,64 @@ +# 内部テーブル検索 データ生成規則 + +## 概要 + +WORKING-STORAGE上の内部テーブル(OCCURS)を SEARCH / SEARCH ALL で検索し、該当データを取得するプログラムのテストデータ。 + +本规则定义5组数据,覆盖等值查找、未找到、顺序查找、可变长表边界、异常系。 + +## 输入 + +- 詳細設計書の処理詳細(検索キー、SEARCH/SEARCH ALLの区分、INDEX操作) +- 内部テーブルのOCCURS定義(キー項目、OCCURS DEPENDING ON有無) + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | SEARCH ALL 等值查找(T-N001) | KEYが一致するデータが存在するケース | +| 2 | SEARCH ALL 未找到(T-N002) | KEYが一致するデータが存在しないケース | +| 3 | 多键/顺序SEARCH(T-N003/T-N004) | 複合キー検索、SEARCHによる順次検索 | +| 4 | OCCURS DEPENDING ON 边界(T-N006) | 可変長テーブルの境界(ODL=1、ODL=最大値) | +| 5 | 异常系(T-A001〜T-A003) | 未ソート表、INDEX越界、ODL=0(空表) | + +### 组1:SEARCH ALL 等值查找(T-N001) + +テーブルに検索対象のキーが存在するデータ(例: キー a001〜a005 の5件、検索対象 a003)。 + +### 组2:SEARCH ALL 未找到(T-N002) + +テーブルに検索対象のキーが存在しないデータ(例: a001〜a004、検索対象 z999)。 + +### 组3:多键/顺序SEARCH(T-N003/T-N004) + +- 複合キー(主キー+副キー)で検索するデータ +- SEARCH(非ALL)で順次探索するデータ(先頭・末尾・中間の位置バリエーション) + +### 组4:OCCURS DEPENDING ON 边界(T-N006) + +可変長テーブルで ODL=1(最小)と ODL=最大値(満杯)のデータ。境界での検索が正しく動作することを確認。 + +### 组5:异常系(T-A001〜T-A003) + +- SEARCH ALL 対象テーブルが昇順でないデータ(誤命中確認) +- INDEXがOCCURS範囲外になるデータ(T-A002) +- ODL=0(空テーブル)のデータ(T-A003) + +## 多keyテスト + +検索キーが複数項目の場合、各組末尾に多keyテストを追加(キー項目ごとに1件ずつ基準と異なる値のデータ)。 + +## データ生成手順 + +1. テーブル定義と検索キーの特定 +2. 组1/组2: 存在・非存在データ +3. 组3: 複合キー・順次検索データ +4. 组4: ODL境界データ +5. 组5: 異常系データ +6. 生成後自检(T-R001):検索後のINDEX位置が期待どおりであること + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/振り分け(EVALUATE).md b/black-box-data-create/rules/pgm_pattern/振り分け(EVALUATE).md new file mode 100644 index 0000000..1b57464 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/振り分け(EVALUATE).md @@ -0,0 +1,68 @@ +# 振り分け(EVALUATE) データ生成規則 + +## 概要 + +単一の入力ファイル(CSVまたは固定長)を読み取り、EVALUATE文による条件分岐に基づいて複数の出力ファイルに振り分けるプログラムのテストデータを生成する。 + +本规则定义5组数据,覆盖测试基准说明 §2.2「条件分支系测试(IF / EVALUATE)」中 EVALUATE文相关的 B-N006〜B-N009、B-A002。IF文による分岐は「振り分け(IF).md」を参照。 + +## 输入 + +- 詳細設計書の処理詳細(EVALUATE条件と出力先の対応) +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | EVALUATE WHEN 多值(B-N006) | 各WHEN分岐を1件ずつカバー | +| 2 | EVALUATE ALSO(B-N007) | ALSOによる複数条件組合せの全パターン | +| 3 | EVALUATE TRUE/FALSE(B-N008) | 条件式をTRUE/FALSEで判定するケース | +| 4 | EVALUATE WHEN OTHER(B-N009) | 全WHEN不成立→WHEN OTHER到達データ | +| 5 | EVALUATE 无WHEN OTHER(B-A002) | 全WHEN不成立→無操作になるデータ(○) | + +### 组1:EVALUATE WHEN 多值(B-N006) + +EVALUATEの各WHEN分岐を1件ずつカバーするデータ。WHENに複数値(VALUE THRU等)を指定する場合も各値に1件生成する。 + +例:WHEN 'T'→出力A、WHEN 'P'→出力B、WHEN 'M'→出力C + +### 组2:EVALUATE ALSO(B-N007) + +EVALUATE ... ALSO ... による複数条件の組合せ判定。各条件の組合せ(成立×成立、成立×不成立、不成立×成立、不成立×不成立)をカバーする。 + +### 组3:EVALUATE TRUE/FALSE(B-N008) + +EVALUATE TRUE / EVALUATE FALSE による条件式判定。各条件式がTRUEになるデータ・FALSEになるデータを生成する。 + +### 组4:EVALUATE WHEN OTHER(B-N009) + +全WHENの条件に該当しないデータを生成し、WHEN OTHER(デフォルト分岐)に到達することを確認する。 + +### 组5:EVALUATE 无WHEN OTHER(B-A002) + +WHEN OTHERを持たないEVALUATEで、全WHENに該当しないデータ。無操作で終了することを確認する(○)。 + +## データ生成手順 + +1. **EVALUATE条件の特定**: 処理詳細からすべてのEVALUATE条件(WHEN、ALSO、TRUE/FALSE、WHEN OTHER)、対応する出力先、関係演算子を抽出する +2. **分岐パターンの列挙**: 正常系(検証通過→正常出力)と異常系(エラー→ERROR-LOG)の全パターンを列挙する +3. **データ生成**: + - 各组の分岐パターンに最低1件のレコードを生成する + - フィールド値はPIC定義に従い生成する + - 正常系データ: 全チェックを通過する有効な値 + - 異常系データ: 該当するチェックに引っかかる値(不正な日付、不正な時刻、範囲外の値など) +4. **値のルール**: + - PIC X(n): 左詰め・スペース埋め + - PIC 9(n): 右詰め・先行ゼロ + - DATE/日付系: 実在しそうな日付(YYYYMMDD形式)、異常系では不正な日付(99999999等) + - TIME/時刻系: 実在しそうな時刻(HHMM形式)、異常系では範囲外の時刻 + - STATUS系: 0/1/9 などのコード値 + - CSV項番号フィールド: 分割後の項目数 +5. **生成後自检**: 各WHEN分岐・ALSO組合せが漏れなくカバーされていること、各出力先に最低1件振り分けられることを確認する + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。正常系→異常系→特殊分岐の順にレコードを並べる。 diff --git a/black-box-data-create/rules/pgm_pattern/振り分け(IF).md b/black-box-data-create/rules/pgm_pattern/振り分け(IF).md new file mode 100644 index 0000000..2a9871a --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/振り分け(IF).md @@ -0,0 +1,73 @@ +# 振り分け(IF) データ生成規則 + +## 概要 + +単一の入力ファイル(CSVまたは固定長)を読み取り、IF文による条件分岐に基づいて複数の出力ファイルに振り分けるプログラムのテストデータを生成する。 + +本规则定义5组数据,覆盖测试基准说明 §2.2「条件分支系测试(IF / EVALUATE)」中 IF文相关的 B-N001〜B-N005。B-A001(IF未闭合)は编译错误であり运行时测试対象外(代码审查范畴)のためデータ组としない。EVALUATE文による分岐は「振り分け(EVALUATE).md」を参照。 + +## 输入 + +- 詳細設計書の処理詳細(IF条件と出力先の対応) +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | IF 2路分支(B-N001) | IF条件成立/不成立(ELSE)を各1件以上カバー | +| 2 | IF 3路分支 ELSE IF(B-N002) | ELSE IF 链式分支の全分岐を各1件カバー | +| 3 | IF 复合条件 AND/OR(B-N003) | 複数条件のAND/OR組合せ全パターン | +| 4 | IF 88-level 条件名(B-N004) | 88条件名の成立/不成立 | +| 5 | IF 嵌套3层(B-N005) | 3層ネストIFの全組合せ(外層×中層×内層) | + +### 组1:IF 2路分支(B-N001) + +IF条件成立→出力A、不成立(ELSE)→出力B の2路分岐。成立・不成立それぞれ1件以上のデータを生成する。 + +例:STATUS='1'→正常出力、STATUS='9'→取消出力 + +### 组2:IF 3路分支 ELSE IF(B-N002) + +IF → ELSE IF → ELSE の链式分支。各分岐(3路)に1件ずつデータを生成する。 + +例:STATUS='0'→出力A、STATUS='1'→出力B、それ以外(ELSE)→出力C + +### 组3:IF 复合条件 AND/OR(B-N003) + +複数条件の組合せをカバーする。AND/ORそれぞれについて成立・不成立の全パターンを生成する。 + +例(AND):STATUS='1' AND TIME >= '0900' → 成立/STATUS='1' AND TIME < '0900' → 不成立 +例(OR):STATUS='1' OR STATUS='2' → 成立/STATUS='9' → 不成立 + +### 组4:IF 88-level 条件名(B-N004) + +88-level条件名(例:88 WS-VALID VALUE '0' '1')による判定。条件名成立・不成立のデータを生成する。 + +### 组5:IF 嵌套3层(B-N005) + +3層ネストIF(外層IFの中で中層IF、さらに内層IF)の全組合せデータ。内層IFが外層IFの変数を参照するケースを含む。 + +## データ生成手順 + +1. **IF条件の特定**: 処理詳細からすべてのIF条件、対応する出力先、関係演算子を抽出する +2. **分岐パターンの列挙**: 正常系(検証通過→正常出力)と異常系(エラー→ERROR-LOG)の全パターンを列挙する +3. **データ生成**: + - 各组の分岐パターンに最低1件のレコードを生成する + - フィールド値はPIC定義に従い生成する + - 正常系データ: 全チェックを通過する有効な値 + - 異常系データ: 該当するチェックに引っかかる値(不正な日付、不正な時刻、範囲外の値など) +4. **値のルール**: + - PIC X(n): 左詰め・スペース埋め + - PIC 9(n): 右詰め・先行ゼロ + - DATE/日付系: 実在しそうな日付(YYYYMMDD形式)、異常系では不正な日付(99999999等) + - TIME/時刻系: 実在しそうな時刻(HHMM形式)、異常系では範囲外の時刻 + - STATUS系: 0/1/9 などのコード値 + - CSV項番号フィールド: 分割後の項目数 +5. **生成後自检**: 各IF分岐が漏れなくカバーされていること、各出力先に最低1件振り分けられることを確認する + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。正常系→異常系→特殊分岐の順にレコードを並べる。 diff --git a/black-box-data-create/rules/pgm_pattern/項目チェック(半角20桁-4桁).md b/black-box-data-create/rules/pgm_pattern/項目チェック(半角20桁-4桁).md new file mode 100644 index 0000000..b4acb3e --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/項目チェック(半角20桁-4桁).md @@ -0,0 +1,55 @@ +# 項目チェック(半角20桁-4桁) データ生成規則 + +## 概要 + +入力ファイル(CSVまたは固定長)の半角フィールドに対し、半角20桁/4桁の桁数チェック(半角英数字・全角混入チェック含む)を実施し、正常データと異常データを別々の出力ファイルに振り分けるプログラムのテストデータを生成する。 + +本规则定义4组数据,覆盖测试基准说明 §2.2「校验测试(含重复 / 不含重复 / 半角)」中半角校验相关的 VF-N005、VF-N006、VF-A001、VF-A002。字段校验・重复检测は「項目チェック(重複含まず).md」を参照。 + +## 输入 + +- 詳細設計書の処理詳細(半角チェック対象フィールド、桁数) +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 半角长度20桁(VF-N005) | 半角20桁ちょうど・20桁以内→通過するデータ | +| 2 | 半角长度4桁(VF-N006) | 半角4桁ちょうど・4桁以内→通過するデータ | +| 3 | 半角超长21桁(VF-A001) | 半角21桁→截断/エラーになるデータ | +| 4 | 全角混入(VF-A002) | 半角フィールドに全角文字が入る→エラーになるデータ | + +### 组1:半角长度20桁(VF-N005) + +半角フィールドに20桁ちょうど・20桁以内の値を設定するデータ。通過することを確認。 + +### 组2:半角长度4桁(VF-N006) + +半角フィールドに4桁ちょうど・4桁以内の値を設定するデータ。通過することを確認。 + +### 组3:半角超长21桁(VF-A001) + +半角フィールドに21桁の値を設定するデータ。截断またはエラー処理が設計書どおりであることを確認。 + +### 组4:全角混入(VF-A002) + +半角フィールド(PIC X 半角域)に全角文字を混入させたデータ。エラーになることを確認。 + +## データ生成手順 + +1. **半角フィールドの特定**: 処理詳細から半角チェック対象フィールドと桁数(20桁/4桁)を特定する +2. **データ生成**: + - 组1/组2: 半角20桁・4桁以内の正常値 + - 组3: 21桁の超過値 + - 组4: 全角文字混入値 +3. **値のルール**: + - 半角チェック: 20桁以内→通過、21桁→エラー、4桁以内→通過、全角混入→エラー + - 半角英大文字チェック(設計書指定時): 英大文字のみ→通過、小文字・数字混入→エラー +4. **生成後自检(VF-R001)**: 正常件+エラー件=入力件数 を確認する + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/項目チェック(重複含まず).md b/black-box-data-create/rules/pgm_pattern/項目チェック(重複含まず).md new file mode 100644 index 0000000..2aa5608 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/項目チェック(重複含まず).md @@ -0,0 +1,82 @@ +# 項目チェック(重複含まず) データ生成規則 + +## 概要 + +入力ファイル(CSVまたは固定長)の各レコードに対し、複数の項目チェック(値域・桁数・コード値等)と重複チェック(重複含まず)を実施し、正常データと異常データを別々の出力ファイルに振り分けるプログラムのテストデータを生成する。 + +本规则定义5组数据,覆盖测试基准说明 §2.2「校验测试(含重复 / 不含重复 / 半角)」中字段校验・重复检测相关的 VF-N001〜VF-N004、VF-R001。半角20桁/4桁チェックは「項目チェック(半角20桁-4桁).md」を参照。 + +## 输入 + +- 詳細設計書の処理詳細(チェック項目一覧、重複キー) +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 字段校验通过(VF-N001) | 全項目が許容範囲内→正常出力されるデータ | +| 2 | 字段校验拒绝(VF-N002) | 不正値(日付・時刻・数値・コード値など)→エラー出力ファイルのデータ | +| 3 | 重复检测 正常(VF-N003) | キーが初回出現→通過するデータ | +| 4 | 重复检测 重复(VF-N004) | 同一キー2回目→エラー出力されるデータ + 末尾に多keyテスト | +| 5 | 件数确认(VF-R001) | 正常出力件数+エラー出力件数=入力件数 を確認するデータ | + +### 组1:字段校验通过(VF-N001) + +全チェック項目を通過する有効な値のレコード(3〜5件)。各フィールドはPIC定義どおり、日付・時刻・コード値とも許容範囲内。 + +### 组2:字段校验拒绝(VF-N002) + +各チェック項目に特化した不正値を含むレコード: +- 日付チェック: 不正日付(99999999等) +- 時刻チェック: 範囲外(2500等) +- 数値チェック: 非数値・桁数不足 +- フィールド数チェック: 項目数不足/過剰(CSV) +- コード値チェック: 無効コード + +### 组3:重复检测 正常(VF-N003) + +全レコードのキーが異なるデータ(重複なし)。初回出現キーのみで全件通過することを確認。 + +### 组4:重复检测 重复(VF-N004) + +同一キーを2件含むデータ。2回目の出現でエラー出力されることを確認。キーが複数項目の場合、基準キーに対し各キー項目のみ異なるN件を追加し、完全一致時のみ重複と判定されることを検証(多keyテスト)。 + +### 组5:件数确认(VF-R001) + +正常出力件数とエラー出力件数の合計が入力件数と一致することを確認するデータ(正常件+错误件=输入件数)。 + +## 多keyテスト + +重複チェックのキーが複数項目の場合、组4の末尾に追加。基準レコードの各キー項目のみ異なるN件を生成し、完全一致時のみ重複判定されることを検証する。 + +需要注意当key为n个项目组合而成时,需要在末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。 + +例(n=3,基准 (a001, b001, c001)): +- 追加1: (a002, b001, c001) → 仅第1个key不同 +- 追加2: (a001, b002, c001) → 仅第2个key不同 +- 追加3: (a001, b001, c002) → 仅第3个key不同 + +n为实际解析出的key项目数(例中n=3仅为示意,追加条数必须等于实际key项目数)。键项目数n=1时不追加多key测试数据。追加的每条记录必须恰好只改变一个key项目,且不得与既有记录完全重复。 + +## データ生成手順 + +1. **チェック項目の特定**: 処理詳細からすべてのチェック項目(フィールド数、値域、重複キー、時刻範囲、コード値など)を抽出する +2. **重複キーの特定**: 重複チェックのキー項目を特定する +3. **データ生成**: + - 组1: 全チェックを通過する有効値 + - 组2: 各チェック項目に特化した不正値 + - 组3/组4: 重複なし/重複ありデータ + - 组5: 件数整合確認データ +4. **値のルール**: + - フィールド数チェック: 正しい項目数/足りない/多い + - 日付チェック: 正常日付 vs 不正日付 (99999999等) + - 時刻チェック: 正常時刻 (0900-1800) vs 範囲外 (2500等) + - コード値チェック: 有効コード vs 無効コード + - 社員番号: 8桁数字 vs 非数字/桁数不足 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的5组一致(g1〜g5),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/2段階マッチング(1:1⇒1:1).md b/black-box-data-create/rules/pgm_pattern/2段階マッチング(1:1⇒1:1).md new file mode 100644 index 0000000..59511ca --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/2段階マッチング(1:1⇒1:1).md @@ -0,0 +1,62 @@ +# 2段階マッチング(1:1⇒1:1) データ生成規則 + +## 概要 + +第1段階のマッチングで中間ファイルを生成し、第2段階で再度1:1マッチングするプログラムのテストデータ(AM-N001)。 + +本规则定义4组数据:完全匹配、剩余键、中间文件/键异常、件数确认。 + +## 输入 + +- 詳細設計書のキー項目一覧(第1段階キー、第2段階キー) +- 各入力ファイル・中間ファイルのCOPYBOOKフィールド定義 +- キー条件:各段階ともキー昇順ソート済み(重複NG) + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 二级1:1→1:1完全匹配(AM-N001) | 第1段階 R01×R02 をキーAで1:1完全マッチ(4件)→中間M01;第2段階 M01×R03 をキーBで1:1完全マッチ(4件) | +| 2 | 剩余键(MT-N004/N005) | 第1段階・第2段階それぞれで主件/從件の剩余キーを含むデータ | +| 3 | 中间文件/键异常(AM-A001/A002) | 中間ファイルOPEN失敗(環境テスト)、第1段階キーAと第2段階キーBが異なる設計のデータ | +| 4 | 件数确认(AM-R001) | 第1段階・第2段階それぞれの出力件数(マッチ件数+剩余件数)が入力件数と一致するデータ | + +### 组1:完全匹配(AM-N001) + +第1段階: R01×R02 をキーAで1:1完全マッチ(a001〜a004、4件)→中間ファイルM01 +第2段階: M01×R03 をキーBで1:1完全マッチ(b001〜b004、4件) + +### 组2:剩余键(MT-N004/N005) + +第1段階: R01にR02に無いキーを末尾に1件(主件剩余)、R02にR01に無いキーを先頭に1件(從件剩余) +第2段階: M01×R03 でも同様に剩余キーを1件ずつ含める + +### 组3:中间文件/键异常(AM-A001/A002) + +- 中間ファイルがOPENできないケース(環境テスト項目として明記) +- 第1段階キーAと第2段階キーBが異なる設計のデータで、第2段階のマッチ結果が設計書どおりになること + +### 组4:件数确认(AM-R001) + +第1段階出力件数と第2段階出力件数(マッチ件数+剩余件数)の合計が入力件数と一致することを確認するデータ。 + +## 多keyテスト + +第1段階・第2段階のキーが複数項目の場合、各組末尾に多keyテストを追加(既存マッチング規則と同じ手順)。 + +需要注意当key为n个项目组合而成时,需要在末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。键项目数n=1时不追加多key测试数据。 + +## 数据生成步骤 + +1. 第1段階キー・第2段階キーの特定 +2. 组1: 完全マッチデータ(各4件) +3. 组2: 剩余キーを各段階に1件ずつ +4. 组3: キー不一致データ +5. 各組末尾に多keyテスト +6. 生成後自检(AM-R001):各段の出力件数の整合確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/2段階マッチング(M:N⇒M:N).md b/black-box-data-create/rules/pgm_pattern/2段階マッチング(M:N⇒M:N).md new file mode 100644 index 0000000..1e20769 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/2段階マッチング(M:N⇒M:N).md @@ -0,0 +1,62 @@ +# 2段階マッチング(M:N⇒M:N) データ生成規則 + +## 概要 + +第1段階のM:Nマッチング(M×N件の直積)で中間ファイルを生成し、第2段階で再度M:Nマッチングするプログラムのテストデータ(AM-N008)。 + +本规则定义4组数据:M:N匹配(直積)、不平衡、中间文件/键异常、件数确认。 + +## 输入 + +- 詳細設計書のキー項目一覧(第1段階キー、第2段階キー) +- 各入力ファイル・中間ファイルのCOPYBOOKフィールド定義 +- キー条件:各段階ともキー昇順ソート済み + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 二级M:N→M:N(AM-N008) | 第1段階 R01×R02 を同キーでM:Nマッチ(M×N件の直積)→中間M01;第2段階 M01×R03 を同キーで再マッチ(M×N件) | +| 2 | 不平衡(MT-N009/N010) | M=1件/N=N件、M=N件/N=1件 の极端不平衡データ | +| 3 | 中间文件/键异常(AM-A001/A002) | 中間ファイルOPEN失敗(環境テスト)、第1段階キーAと第2段階キーBが異なる設計のデータ | +| 4 | 件数确认(AM-R001) | 第1段階・第2段階それぞれの出力件数(マッチ件数+剩余件数)が入力件数と一致するデータ | + +### 组1:M:N匹配(AM-N008) + +第1段階: R01(同キーM件)×R02(同キーN件)をキーAでM:Nマッチ→M×N件→中間ファイルM01 +第2段階: M01×R03 をキーBで再マッチ(M×N件) + +### 组2:不平衡(MT-N009/N010) + +- M=1件、N=N件(同キー)のデータ +- M=N件、N=1件(同キー)のデータ + +### 组3:中间文件/键异常(AM-A001/A002) + +- 中間ファイルがOPENできないケース(環境テスト項目として明記) +- 第1段階キーAと第2段階キーBが異なる設計のデータで、第2段階のマッチ結果が設計書どおりになること + +### 组4:件数确认(AM-R001) + +第1段階出力件数と第2段階出力件数(マッチ件数+剩余件数)の合計が入力件数と一致することを確認するデータ。 + +## 多keyテスト + +第1段階・第2段階のキーが複数項目の場合、各組末尾に多keyテストを追加(既存マッチング規則と同じ手順)。 + +需要注意当key为n个项目组合而成时,需要在末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。键项目数n=1时不追加多key测试数据。 + +## 数据生成步骤 + +1. 第1段階キー・第2段階キーの特定 +2. 组1: M:N直積マッチデータ +3. 组2: 不平衡データ +4. 组3: キー不一致データ +5. 各組末尾に多keyテスト +6. 生成後自检(AM-R001):各段の出力件数の整合確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/pgm_pattern/2段階マッチング(N:1⇒N:1).md b/black-box-data-create/rules/pgm_pattern/2段階マッチング(N:1⇒N:1).md new file mode 100644 index 0000000..e390747 --- /dev/null +++ b/black-box-data-create/rules/pgm_pattern/2段階マッチング(N:1⇒N:1).md @@ -0,0 +1,63 @@ +# 2段階マッチング(N:1⇒N:1) データ生成規則 + +## 概要 + +第1段階のN:1マッチングで中間ファイルを生成し、第2段階で再度N:1マッチングするプログラムのテストデータ(AM-N002)。 + +本规则定义4组数据:N:1匹配、剩余/不平衡、中间文件/键异常、件数确认。 + +## 输入 + +- 詳細設計書のキー項目一覧(第1段階キー、第2段階キー) +- 各入力ファイル・中間ファイルのCOPYBOOKフィールド定義 +- キー条件:各段階ともキー昇順ソート済み(主件の重複NG) + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 二级N:1→N:1(AM-N002) | 第1段階 R01(主N件)×R02(從1件)をキーAでN:1マッチ→N件→中間M01;第2段階 M01×R03(從1件)をキーBでN:1マッチ→N件 | +| 2 | 剩余/不平衡(MT-N004/N005/N009) | 各段階で主件/從件の剩余キーと极端不平衡(R01=1件、R02=N件)を含むデータ | +| 3 | 中间文件/键异常(AM-A001/A002) | 中間ファイルOPEN失敗(環境テスト)、第1段階キーAと第2段階キーBが異なる設計のデータ | +| 4 | 件数确认(AM-R001) | 第1段階・第2段階それぞれの出力件数(マッチ件数+剩余件数)が入力件数と一致するデータ | + +### 组1:N:1匹配(AM-N002) + +第1段階: R01(主N件、同キー2件)×R02(從1件)をキーAでN:1マッチ→N件→中間ファイルM01 +第2段階: M01×R03(從1件)をキーBでN:1マッチ→N件 + +### 组2:剩余/不平衡(MT-N004/N005/N009) + +- 主件剩余: R01にR02に無いキーを末尾に追加 +- 從件剩余: R02にR01に無いキーを追加 +- 极端不平衡: R01=1件、R02=N件(同キー)のデータ + +### 组3:中间文件/键异常(AM-A001/A002) + +- 中間ファイルがOPENできないケース(環境テスト項目として明記) +- 第1段階キーAと第2段階キーBが異なる設計のデータで、第2段階のマッチ結果が設計書どおりになること + +### 组4:件数确认(AM-R001) + +第1段階出力件数と第2段階出力件数(マッチ件数+剩余件数)の合計が入力件数と一致することを確認するデータ。 + +## 多keyテスト + +第1段階・第2段階のキーが複数項目の場合、各組末尾に多keyテストを追加(既存マッチング規則と同じ手順)。 + +需要注意当key为n个项目组合而成时,需要在末尾追加n条数据。以基准数据的所有key为准,只有当前第n个key与基准不一致。保证key的比较没有问题。键项目数n=1时不追加多key测试数据。 + +## 数据生成步骤 + +1. 第1段階キー・第2段階キーの特定 +2. 组1: N:1マッチデータ +3. 组2: 剩余・不平衡データ +4. 组3: キー不一致データ +5. 各組末尾に多keyテスト +6. 生成後自检(AM-R001):各段の出力件数の整合確認 + +## 出力 + +每组输出1个JSON文件:`{プログラムID}_g{groupId}.json` + +组数约束:输出组数必须与本规则定义的4组一致(g1〜g4),组号连续从1开始。禁止添加本规则未定义的组,禁止缺组。 diff --git a/black-box-data-create/rules/special_feature/条件分支.md b/black-box-data-create/rules/special_feature/条件分支.md new file mode 100644 index 0000000..82a21b0 --- /dev/null +++ b/black-box-data-create/rules/special_feature/条件分支.md @@ -0,0 +1,27 @@ +# 条件分支 数据生成规则 + +## 概要 + +检索详细设计中処理詳細的所有条件分支,增加覆盖所有分支的输入数据。 + +## 输入 + +- 输入文件:详细设计书的処理詳細部分 + +## 生成的数据组 + +由你决定,以尽可能少的数据组完成分支覆盖 + + +## 数据生成步骤 + +1. 遍历処理詳細的所有内容 +2. 记录所有条件分支 +3. 找到条件判断数据与输入的关系 +4. 数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。 +5. 按各组规则组装records数组 +6. 组号从之前的最后一组+1开始连续编号 + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` diff --git a/black-box-data-create/tests/__init__.py b/black-box-data-create/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/black-box-data-create/tests/conftest.py b/black-box-data-create/tests/conftest.py new file mode 100644 index 0000000..d1634f4 --- /dev/null +++ b/black-box-data-create/tests/conftest.py @@ -0,0 +1,10 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +@pytest.fixture +def data_dir(): + return os.path.join(os.path.dirname(__file__), 'test_data') diff --git a/black-box-data-create/tests/test_api_client.py b/black-box-data-create/tests/test_api_client.py new file mode 100644 index 0000000..da08c64 --- /dev/null +++ b/black-box-data-create/tests/test_api_client.py @@ -0,0 +1,68 @@ +import json +from unittest.mock import patch, MagicMock +from agent.api_client import APIClient + + +def test_parse_json_valid(): + text = '{"groups": {"g1": {"records": []}}}' + result = APIClient._parse_json(text) + assert result is not None + assert 'groups' in result + + +def test_parse_json_with_markdown_wrapper(): + text = '```json\n{"key": "value"}\n```' + result = APIClient._parse_json(text) + assert result is not None + assert result['key'] == 'value' + + +def test_parse_json_with_text_before(): + text = '说明\n{"key": "value"}\n以上' + result = APIClient._parse_json(text) + assert result is not None + assert result['key'] == 'value' + + +def test_parse_json_invalid(): + text = '这不是有效的JSON。' + result = APIClient._parse_json(text) + assert result is None + + +@patch('agent.api_client.requests.post') +def test_generate_success(mock_post): + mock_response = MagicMock() + mock_response.json.return_value = { + 'choices': [{'message': {'content': '{"groups": {"g1": {"type": "json"}}}'}}] + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + client = APIClient(api_key='test-key') + result = client.generate("test prompt") + + assert result['groups']['g1']['type'] == 'json' + + +@patch('agent.api_client.requests.post') +def test_generate_retry_on_json_error(mock_post): + bad_response = MagicMock() + bad_response.json.return_value = { + 'choices': [{'message': {'content': 'invalid response'}}] + } + bad_response.raise_for_status = MagicMock() + + good_response = MagicMock() + good_response.json.return_value = { + 'choices': [{'message': {'content': '{"result": "ok"}'}}] + } + good_response.raise_for_status = MagicMock() + + mock_post.side_effect = [bad_response, good_response] + + client = APIClient(api_key='test-key', max_retries=3) + result = client.generate("test") + + assert result['result'] == 'ok' + assert mock_post.call_count == 2 diff --git a/black-box-data-create/tests/test_copy_parser.py b/black-box-data-create/tests/test_copy_parser.py new file mode 100644 index 0000000..ac8bbf8 --- /dev/null +++ b/black-box-data-create/tests/test_copy_parser.py @@ -0,0 +1,87 @@ +# tests/test_copy_parser.py +import os +import tempfile +from agent.input_parser import InputParser + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_single_copybook(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR, + db_md_path='dummy_db.md' + ) + fields = parser._parse_single_copybook( + os.path.join(FIXTURE_DIR, 'ZAN01REC.cpy'), + prefix='R01' + ) + + assert len(fields) == 8 + + f0 = fields[0] + assert f0.name == 'R01-APPL-ID' + assert f0.raw_name == '(A)APPL-ID' + assert 'X' in f0.pic_type + assert f0.pic_bytes == 8 + + f1 = fields[1] + assert f1.name == 'R01-EMP-ID' + assert '9' in f1.pic_type + assert f1.pic_bytes == 8 + + f3 = fields[3] + assert f3.name == 'R01-START-TIME' + assert f3.pic_bytes == 4 + + f7 = fields[7] + assert 'FILLER' in f7.name + assert f7.pic_bytes == 46 + + +def test_extract_copy_replacing(): + source_text = """ + FD R01INNFIL. + 01 R01INNREC. + COPY ZAN01REC REPLACING ==(A)== BY ==R01==. + FD R02INNFIL. + 01 R02INNREC. + COPY ZAN04REC REPLACING ==(A)== BY ==R02==. + """ + + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR, + db_md_path='dummy_db.md' + ) + parser._source_text = source_text + + result = parser._extract_copy_replacing() + assert len(result) == 2 + assert result[0] == ('ZAN01REC', 'R01') + assert result[1] == ('ZAN04REC', 'R02') + + +def test_calculate_pic_bytes_x(): + assert InputParser._calculate_pic_bytes('X(008)') == 8 + assert InputParser._calculate_pic_bytes('X(10)') == 10 + + +def test_calculate_pic_bytes_9(): + assert InputParser._calculate_pic_bytes('9(008)') == 8 + assert InputParser._calculate_pic_bytes('9(004)') == 4 + + +def test_calculate_pic_bytes_comp3(): + assert InputParser._calculate_pic_bytes('S9(009) COMP-3') == 5 + assert InputParser._calculate_pic_bytes('S9(7) COMP-3') == 4 + assert InputParser._calculate_pic_bytes('9(7) COMP-3') == 4 + + +def test_calculate_pic_bytes_comp(): + assert InputParser._calculate_pic_bytes('S9(4) COMP') == 2 + assert InputParser._calculate_pic_bytes('S9(9) COMP') == 4 + + +def test_calculate_pic_bytes_decimal(): + assert InputParser._calculate_pic_bytes('S9(7)V9(2)') == 9 diff --git a/black-box-data-create/tests/test_data/DB定義書.md b/black-box-data-create/tests/test_data/DB定義書.md new file mode 100644 index 0000000..34a4ecd --- /dev/null +++ b/black-box-data-create/tests/test_data/DB定義書.md @@ -0,0 +1,347 @@ +# DB定義書 + +## 変更履歴 + +| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 | +|----|---------|--------|--------|--------|------| +| 1 | 新規作成(全8テーブル) | AI | 2026/06/23 | | サブシステムA(6) + B(2) | +| 2 | SALARYDB 4テーブル追加 | AI | 2026/07/08 | | サブシステムC(給与計算) | + +--- + +# EMP_MASTER — 社員マスタ + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | EMP_MASTER | — | PK: EMP_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 部署ID | DEPT_ID | CHAR | 4 | | 4 | NOT NULL | | | | | | | +| 3 | 氏名 | EMP_NAME | VARCHAR | 50 | | 50 | NOT NULL | | | | | | | +| 4 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=在籍, '9'=退職 | + +### 備考 + +- サブシステムA: 参照のみ(SELECT) +- サブシステムB: 使用しない + +--- + +# LEAVE_RECORDS — 休暇申請記録 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | LEAVE_RECORDS | — | PK: APPLICATION_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 申請番号 | APPLICATION_ID | INTEGER | 10 | | 4 | NOT NULL | | ✓ | | | | 自動採番 | +| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | | +| 3 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | | | | | 01/02/03/04 | +| 4 | 開始日 | START_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 6 | 終了日 | END_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 7 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=有効, '9'=取消 | + +### 備考 + +- KIN02UPDがINSERT/DELETEを実行 +- KIN03EXPがSELECT(日付展開用) +- 取消はDELETE FROM で物理削除 + +--- + +# HOLIDAY_CALENDAR — 休日カレンダー + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | HOLIDAY_CALENDAR | — | PK: HOLIDAY_DATE | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 休日日付 | HOLIDAY_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD | +| 2 | 説明 | DESCRIPTION | VARCHAR | 50 | | 50 | NULL許可 | NULL | | | | | 例:「建国記念の日」 | + +### 備考 + +- 土日は曜日判定で処理するため、このテーブルには祝日のみ格納 +- KIN03EXP, KIN06CLDがSELECTで参照 + +--- + +# SICK_LEAVE_RATE — 病欠控除率 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | SICK_LEAVE_RATE | — | PK: LEAVE_TYPE | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | ✓ | | | | '04'固定 | +| 2 | 控除率 | DEDUCTION_RATE | DECIMAL | 3 | 2 | 3 | NOT NULL | | | | | | 例:0.50(50%控除) | + +### 備考 + +- 全社員共通の設定値 +- 現時点では参照プログラム未実装(設計上定義) + +--- + +# DAILY_RECORDS — 日別勤怠記録 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | DAILY_RECORDS | — | PK: (EMP_ID, TARGET_DATE) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象日 | TARGET_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD | +| 3 | 出勤時刻 | TIME_IN | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし | +| 4 | 退勤時刻 | TIME_OUT | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし | +| 5 | 年休時間 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 6 | 事假時間 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 7 | 因公特批假時間 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 8 | 病欠時間 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 9 | 未申請欠勤時間 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 | +| 10 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP | + +### 備考 + +- KIN08DBUがKIN07DAIの出力ファイルからINSERT +- 1社員1日=1レコード。出勤日のみ存在。 + +--- + +# MONTHLY_ABSENCE — 月次統計 + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 勤怠休暇管理DB | MONTHLY_ABSENCE | — | PK: (EMP_ID, YEAR_MONTH) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 年休合計 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 4 | 事假合計 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 5 | 因公特批假合計 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 6 | 病欠合計 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 7 | 未申請欠勤合計 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 8 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP | + +### 備考 + +- KIN08DBUが月次集計後にUPSERT(MERGE) +- サブシステムC(給与計算)への連携元 + +--- + +# OVT_APPLICATIONS — 個別加班申請テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 残業統計管理DB | OVT_APPLICATIONS | — | PK: APPL_ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 申請番号 | APPL_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD+SEQ | +| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | | +| 3 | 申請日 | APPL_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD | +| 4 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | | | | | W=平日, H=休日 | +| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 6 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM | +| 7 | 加班時間 | OVT_HOURS | DECIMAL | 4 | 1 | 4 | NOT NULL | | | | | | 0.1h単位 | +| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | 0=有効, 9=取消 | +| 9 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | | + +### 備考 + +- ZAN06UPDがINSERT/UPDATEを実行 +- STATUS=9で取消(物理削除はしない) + +--- + +# OVT_MONTHLY — 月次集計テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 残業統計管理DB | OVT_MONTHLY | — | PK: (EMP_ID, YEAR_MONTH, OVT_TYPE) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | ✓ | | | | W=平日, H=休日 | +| 4 | 加班時間合計 | OVT_HOURS | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | | +| 5 | 加班回数 | OVT_COUNT | INTEGER | 10 | | 4 | NOT NULL | | | | | | | +| 6 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | | + +### 備考 + +- ZAN06UPDが月次集計結果をUPSERT + +--- + +# EMP-MASTER(SALARYDB) — 社員マスタ(給与計算用) + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | EMP-MASTER | KYU01REC | PK: EMP-ID | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 氏名 | EMP-NAME | VARCHAR | 40 | | 40 | NOT NULL | | | | | | | +| 3 | 部署コード | DEPT-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | | +| 4 | 地域コード | REGION-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | | +| 5 | 職種コード | CATEGORY-CODE | CHAR | 3 | | 3 | NOT NULL | | | | | | | +| 6 | 基本給 | BASE-SALARY | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 7 | 時給 | HOURLY-RATE | DECIMAL | 7 | 0 | 4 | NOT NULL | | | | | | | +| 8 | 扶養人数 | DEPENDENT-COUNT | SMALLINT | — | | 2 | NOT NULL | | | | | | | +| 9 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '0'=在籍, '9'=退職 | +| 10 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU02REGがINSERT/UPSERTを実行 +- KYU04CALがSELECTで参照 + +--- + +# TAX-TABLE(SALARYDB) — 所得税率テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | TAX-TABLE | — | PK: (TAX-FROM, TAX-TO) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 課税下限 | TAX-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 2 | 課税上限 | TAX-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 3 | 税率 | TAX-RATE | DECIMAL | 5 | 4 | 3 | NOT NULL | | | | | | | +| 4 | 控除額 | DEDUCTION | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | + +### 備考 + +- KYU05DEDがSELECT ... BETWEENで参照 +- 課税所得に対する超過累進税率 + +--- + +# INSURANCE-TABLE(SALARYDB) — 社会保険料率テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | INSURANCE-TABLE | — | PK: (INS-INCOME-FROM, INS-INCOME-TO) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 所得下限 | INS-INCOME-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 2 | 所得上限 | INS-INCOME-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | | +| 3 | 健康保険料率 | INS-HEALTH-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | | +| 4 | 厚生年金保険料率 | INS-PENSION-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | | +| 5 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU05DEDが内部テーブル(SEARCH ALL)に取込んで参照 +- 被保険者負担分のみ + +--- + +# SALARY-RESULTS(SALARYDB) — 給与計算結果テーブル + +## Layout(テーブル定義) + +### DB基本情報 + +| DB名 | DB ID | COPY ID | INDEX | +|------|-------|---------|-------| +| 給与計算DB | SALARY-RESULTS | — | PK: (EMP-ID, YEAR-MONTH) | + +### カラム定義 + +| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 | +|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------| +| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | | +| 2 | 対象年月 | YEAR-MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM | +| 3 | 総支給額 | GROSS-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 4 | 源泉所得税 | INCOME-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 5 | 社会保険料 | INSURANCE | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 6 | 住民税 | RESIDENT-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 7 | 差引支給額 | NET-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | | +| 8 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | | + +### 備考 + +- KYU06UPDがINSERT/UPSERTを実行 diff --git a/black-box-data-create/tests/test_data/ZAN01REC.cpy b/black-box-data-create/tests/test_data/ZAN01REC.cpy new file mode 100644 index 0000000..18e3282 --- /dev/null +++ b/black-box-data-create/tests/test_data/ZAN01REC.cpy @@ -0,0 +1,11 @@ + * + * 加班申請レコード 80B + * + 03 (A)APPL-ID PIC X(008). + 03 (A)EMP-ID PIC 9(008). + 03 (A)APPL-DATE PIC 9(008). + 03 (A)START-TIME PIC 9(004). + 03 (A)END-TIME PIC 9(004). + 03 (A)STATUS PIC X(001). + 03 (A)OVT-TYPE PIC X(001). + 03 (A)FILLER PIC X(046). diff --git a/black-box-data-create/tests/test_data/ZAN02REC.cpy b/black-box-data-create/tests/test_data/ZAN02REC.cpy new file mode 100644 index 0000000..7cca953 --- /dev/null +++ b/black-box-data-create/tests/test_data/ZAN02REC.cpy @@ -0,0 +1,12 @@ + * + * マッチング結果レコード 80B + * + 03 (A)APPL-ID PIC X(008). + 03 (A)EMP-ID PIC 9(008). + 03 (A)APPL-DATE PIC 9(008). + 03 (A)START-TIME PIC 9(004). + 03 (A)END-TIME PIC 9(004). + 03 (A)STATUS PIC X(001). + 03 (A)OVT-TYPE PIC X(001). + 03 (A)PROC-SEQ PIC 9(002). + 03 (A)FILLER PIC X(044). diff --git a/black-box-data-create/tests/test_data/ZAN04REC.cpy b/black-box-data-create/tests/test_data/ZAN04REC.cpy new file mode 100644 index 0000000..f466156 --- /dev/null +++ b/black-box-data-create/tests/test_data/ZAN04REC.cpy @@ -0,0 +1,5 @@ + * + * DBCLEAN対象レコード 80B + * + 03 (A)APPL-ID PIC X(008). + 03 (A)FILLER PIC X(072). diff --git a/black-box-data-create/tests/test_data/cpy/ZAN01REC.cpy b/black-box-data-create/tests/test_data/cpy/ZAN01REC.cpy new file mode 100644 index 0000000..96519fe --- /dev/null +++ b/black-box-data-create/tests/test_data/cpy/ZAN01REC.cpy @@ -0,0 +1,11 @@ + * + * 加班申请记录 80B + * + 03 (A)APPL-ID PIC X(008). + 03 (A)EMP-ID PIC 9(008). + 03 (A)APPL-DATE PIC 9(008). + 03 (A)START-TIME PIC 9(004). + 03 (A)END-TIME PIC 9(004). + 03 (A)STATUS PIC X(001). + 03 (A)OVT-TYPE PIC X(001). + 03 (A)FILLER PIC X(046). diff --git a/black-box-data-create/tests/test_data/cpy/ZAN04REC.cpy b/black-box-data-create/tests/test_data/cpy/ZAN04REC.cpy new file mode 100644 index 0000000..96519fe --- /dev/null +++ b/black-box-data-create/tests/test_data/cpy/ZAN04REC.cpy @@ -0,0 +1,11 @@ + * + * 加班申请记录 80B + * + 03 (A)APPL-ID PIC X(008). + 03 (A)EMP-ID PIC 9(008). + 03 (A)APPL-DATE PIC 9(008). + 03 (A)START-TIME PIC 9(004). + 03 (A)END-TIME PIC 9(004). + 03 (A)STATUS PIC X(001). + 03 (A)OVT-TYPE PIC X(001). + 03 (A)FILLER PIC X(046). diff --git a/black-box-data-create/tests/test_data/詳細設計書_ZAN04MAT.md b/black-box-data-create/tests/test_data/詳細設計書_ZAN04MAT.md new file mode 100644 index 0000000..3d33b75 --- /dev/null +++ b/black-box-data-create/tests/test_data/詳細設計書_ZAN04MAT.md @@ -0,0 +1,144 @@ +# 詳細設計書 + +## 基本情報 + +| # | 項目 | 内容 | +|---|------|------| +| 1 | システム名 | 残業統計管理システム | +| 2 | プログラムID | ZAN04MAT | +| 3 | プログラム名 | 取消マッチング処理 | +| 4 | PGMタイプ | メイン | +| 5 | PGMパターン | マッチング(1:1) | +| 6 | 機能概要 | OVT-SORTED(有効申請)とOVT-CSORT(取消申請)を申請番号(APPL-ID)で1:1マッチングし、結果を振り分ける。 | +| 7 | | 申請番号一致(取消済):監査証跡としてERROR-LOGに記録 | +| 8 | | 申請のみ(取消なし):OVT-MATCHEDに出力(処理番号=1) | +| 9 | | 取消のみ(既DB登録済):OVT-DBCLEANに出力(DB削除用) | + +※PGMタイプ:メイン、サブ +※PGMパターン:マッチング(1:1、1:N、N:1、M:N)、レイアウト編集のみ(GETPUT)、振り分け(IF文、EVALUATE文)、キーブレイク(集計、集約、集計・集約の以外)、DB更新 + +### 前提条件 + +| NO | 対象ファイル | 条件 | +|----|-------------|------| +| 1 | ファイルR01(OVT-SORTED) | 申請番号(APPL-ID)で昇順ソート済、重複なし | +| 2 | ファイルR02(OVT-CSORT) | 申請番号(APPL-ID)で昇順ソート済、重複なし | + +### 使用ファイル一覧 + +| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 | +|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------| +| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | FB | | 80 | PS | 有効申請(APPL-ID昇順) | +| 2 | OVT-CSORT | R02 | ZAN04R02 | I | ZAN01REC | FB | | 80 | PS | 取消申請(APPL-ID昇順) | +| 3 | OVT-MATCHED | W01 | ZAN04W01 | O | ZAN02REC | FB | | 80 | PS | マッチング結果(処理番号付) | +| 4 | OVT-DBCLEAN | W02 | ZAN04W02 | O | ZAN04REC | FB | | 80 | PS | DB削除用(APPL-IDのみ) | +| 5 | ERROR-LOG | W03 | ZAN04W03 | O | ZAN05REC | VB | | 200 | PS | 監査証跡(取消マッチ記録) | + +### キー項目一覧 + +| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) | +|----|-----------|---------------------|-------------------------------------------| +| 1 | ファイルR01 | APPL-ID(重複NG) | APPL-ID | +| 2 | ファイルR02 | APPL-ID(重複NG) | APPL-ID | + +### 使用モジュール一覧 + +| NO | 機能 | プログラムID | 使用COPY名 | +|----|------|-------------|-----------| +| 1 | 運用日付取得SUB | SUB01DAT | ZANDATAC | +| 2 | メッセージ編集出力SUB | SUB02MSG | ZANMSGAC | +| 3 | ABEND処理SUB | SUB03END | ZANENDAC | + +--- + +## 処理詳細 + +``` +1.初期処理(1000ITTSOR) + 1-1.開始メッセージ出力 + 【メッセージ編集】 + メッセージ番号:1(開始メッセージ) + 1-2.コンパイル日時出力 + 【メッセージ編集】 + メッセージ番号:33(コンパイル日時) + PARM1:コンパイル日時 + PARM2:'COMPILED' + 1-3.ワークエリアの初期化 + 1-4.運用日付取得SUB(SUB01DAT)により運用日を取得する。 + 復帰コード≠ZEROの場合、メッセージを出力し、ABEND処理SUBを呼び出し異常終了する。 + 【メッセージ編集】 + メッセージ番号:5(サブエラー) + PARM1:'SUB01DAT' + PARM2:復帰コード + 【ABEND処理SUB】 + ABENDコード:999 + 1-5.使用ファイルのオープン + 1-6.R01を読み込む。(1100R01INNSOR)(1回目) + 1-7.R02を読み込む。(1200R02INNSOR)(1回目) + +2.主処理(2000MAJSOR)(R01、R02を全て読み終えるまで下記を繰り返す) + 2-1.マッチの場合(R01.APPL-ID = R02.APPL-ID) + 取消済みの申請のため、OVT-MATCHEDには出力しない。 + 監査証跡としてERROR-LOGにマッチ情報を出力する。(2100MATCHSOR) + 【STRING編集】 + ERR-CATEGORY = 04 + ERR-DETAIL = 'CANCEL-MATCH: ' + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 + R01を読み込む。(1100R01INNSOR)(2件目以降) + R02を読み込む。(1200R02INNSOR)(2件目以降) + 2-2.R01のみの場合(R01.APPL-ID < R02.APPL-ID) + R01をOVT-MATCHEDにSTRING編集して出力する。(2200R01OUTSOR) + 【STRING編集】 + W01 = 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 + ステータス + OVT-TYPE + 処理番号(01) + R01を読み込む。(1100R01INNSOR)(2件目以降) + 2-3.R02のみの場合(R01.APPL-ID > R02.APPL-ID) + R02をOVT-DBCLEANに出力する。(2300R02OUTSOR) + W02.APPL-ID = R02.APPL-ID + R02を読み込む。(1200R02INNSOR)(2件目以降) + +3.終了処理(3000STPSOR) + 3-1.入出力ファイルのクローズ + 3-2.入出力件数出力メッセージ出力 + 【入力メッセージ編集】 + メッセージ番号:6(入力件数メッセージ) + PARM1:当該入力ファイルのDD名 + PARM2:当該入力ファイルの件数 + 【出力メッセージ編集】 + メッセージ番号:7(出力件数メッセージ) + PARM1:当該出力ファイルのDD名 + PARM2:当該出力ファイルの件数 + 3-3.終了メッセージ出力 + 【メッセージ編集】 + メッセージ番号:2(終了メッセージ) +``` + +--- + +## 出力レコード定義 + +### 出力ファイル1(W01/OVT-MATCHED) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | APPL-ID | R01.APPL-ID | | +| 2 | EMP-ID | R01.EMP-ID | | +| 3 | APPL-DATE | R01.APPL-DATE | | +| 4 | START-TIME | R01.START-TIME | | +| 5 | END-TIME | R01.END-TIME | | +| 6 | STATUS | R01.STATUS | | +| 7 | OVT-TYPE | R01.OVT-TYPE | | +| 8 | PROC-SEQ | 01固定 | 同一申請番号グループ内連番 | +| 9 | FILLER | 初期値 | | + +### 出力ファイル2(W02/OVT-DBCLEAN) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | APPL-ID | R02.APPL-ID | DB削除対象申請番号 | +| 2 | FILLER | 初期値 | | + +### 出力ファイル3(W03/ERROR-LOG) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | ERR-CATEGORY | 04で固定 | 取消マッチ監査証跡 | +| 2 | ERR-DETAIL | STRINGで編集 | CANCEL-MATCH: + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 | diff --git a/black-box-data-create/tests/test_db_parser.py b/black-box-data-create/tests/test_db_parser.py new file mode 100644 index 0000000..ddb06f3 --- /dev/null +++ b/black-box-data-create/tests/test_db_parser.py @@ -0,0 +1,53 @@ +# tests/test_db_parser.py +import os +from agent.input_parser import InputParser +from agent.models import ProgramMeta + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_db_definition(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir='dummy_cpy', + db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md') + ) + + meta = ProgramMeta( + program_id='TEST', program_name='テスト', system_name='', + pgm_type='メイン', pgm_pattern='DB更新', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='mixed', + copy_fields={}, db_tables={} + ) + + parser._parse_db_definition(meta) + + assert len(meta.db_tables) > 0 + assert 'EMP_MASTER' in meta.db_tables + + emp = meta.db_tables['EMP_MASTER'] + assert emp.db_id == 'EMP_MASTER' + assert len(emp.columns) >= 3 + assert emp.pk_columns == ['EMP_ID'] + + +def test_parse_db_skipped_for_file_type(): + parser = InputParser( + design_md_path='dummy.md', source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', cpy_dir='dummy_cpy', + db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md') + ) + + meta = ProgramMeta( + program_id='TEST', program_name='テスト', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', + copy_fields={}, db_tables={} + ) + + parser._parse_db_definition(meta) + assert len(meta.db_tables) == 0 diff --git a/black-box-data-create/tests/test_input_parser.py b/black-box-data-create/tests/test_input_parser.py new file mode 100644 index 0000000..518d267 --- /dev/null +++ b/black-box-data-create/tests/test_input_parser.py @@ -0,0 +1,149 @@ +# tests/test_input_parser.py +import os +from agent.input_parser import InputParser + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +def test_parse_design_basic_info(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', + file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', + db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_basic_info(meta) + assert meta.program_id == 'ZAN04MAT' + assert meta.pgm_pattern == 'マッチング(1:1)' + assert meta.pgm_type == 'メイン' + assert '残業統計管理システム' in meta.system_name + + +def test_parse_use_files(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_use_files(meta) + assert len(meta.files) == 5 + r01 = [f for f in meta.files if f.identifier == 'R01'][0] + assert r01.file_db_name == 'OVT-SORTED' + assert r01.copy_group == 'ZAN01REC' + assert r01.medium == 'PS' + assert r01.io == 'I' + + +def test_determine_input_type_file(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + + from agent.models import ProgramMeta, FileInfo + meta = ProgramMeta(program_id='ZAN04MAT', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + meta.files = [ + FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1', + io='I', copy_group='C1', format='FB', record_len=80, + medium='PS', remarks=''), + FileInfo(no=2, file_db_name='F2', identifier='W01', dd_name='DD2', + io='O', copy_group='C2', format='FB', record_len=80, + medium='PS', remarks=''), + ] + parser._determine_input_type(meta) + assert meta.input_type == 'file' + + +def test_determine_input_type_mixed(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + + from agent.models import ProgramMeta, FileInfo + meta = ProgramMeta(program_id='TEST', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + meta.files = [ + FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1', + io='I', copy_group='C1', format='FB', record_len=80, + medium='PS', remarks=''), + FileInfo(no=2, file_db_name='DB1', identifier='DB', dd_name='', + io='I', copy_group='', format='', record_len=0, + medium='DB', remarks=''), + ] + parser._determine_input_type(meta) + assert meta.input_type == 'mixed' + + +def test_parse_process_detail(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_process_detail(meta) + assert '1000ITTSOR' in meta.process_detail + assert 'マッチの場合' in meta.process_detail + + +def test_parse_output_records(): + parser = InputParser( + design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'), + source_cbl_path='dummy.cbl', file_db_md_path='dummy.md', + cpy_dir='dummy_cpy', db_md_path='dummy_db.md' + ) + parser._design_text = parser._read_file(parser.design_md_path) + + from agent.models import ProgramMeta + meta = ProgramMeta(program_id='', program_name='', system_name='', + pgm_type='', pgm_pattern='', + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={}) + + parser._parse_output_records(meta) + assert 'OVT-MATCHED' in meta.output_records + assert 'OVT-DBCLEAN' in meta.output_records diff --git a/black-box-data-create/tests/test_integration.py b/black-box-data-create/tests/test_integration.py new file mode 100644 index 0000000..95533d6 --- /dev/null +++ b/black-box-data-create/tests/test_integration.py @@ -0,0 +1,77 @@ +# tests/test_integration.py +import os +import tempfile +from unittest.mock import patch, MagicMock +from agent import generate + +FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data') + + +@patch('agent.api_client.requests.post') +def test_full_pipeline_mock_api(mock_post): + """完整流水线测试,API 调用使用 mock。""" + mock_response = MagicMock() + mock_response.json.return_value = { + 'choices': [{'message': {'content': '''{ + "groups": { + "g1": { + "program": "ZAN04MAT", + "records": [ + { + "input": { + "R01INNFIL": { + "R01-APPL-ID": "A0000001", + "R01-EMP-ID": "00000101", + "R01-APPL-DATE": "20260101", + "R01-START-TIME": "0900", + "R01-END-TIME": "1800", + "R01-STATUS": "0", + "R01-OVT-TYPE": "W", + "R01-FILLER": "D000000000000000000000000000000000000000000001" + } + } + } + ] + } + } + }'''}}] + } + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + cpy_dir = FIXTURE_DIR + output_dir = tempfile.mkdtemp() + rules_dir = os.path.join(os.path.dirname(__file__), '..', 'rules') + + design_md = os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md') + source_cbl = os.path.join(FIXTURE_DIR, '..', '..', '..', + 'cobol-tna-system', 'src', 'ZAN04MAT.cbl') + + # Fallback if real source not found + if not os.path.exists(source_cbl): + source_cbl = os.path.join(output_dir, 'dummy.cbl') + with open(source_cbl, 'w', encoding='utf-8') as f: + f.write("COPY ZAN01REC REPLACING ==(A)== BY ==R01==.\n") + + try: + result = generate( + design_md=design_md, + source_cbl=source_cbl, + file_db_md='dummy.md', + cpy_dir=cpy_dir, + db_md='dummy_db.md', + output_dir=output_dir, + api_key='test-key', + rules_dir=rules_dir, + ) + + assert result['program_id'] == 'ZAN04MAT' + assert result['groups'] > 0 + assert len(result['output_files']) > 0 + + for path in result['output_files'].values(): + assert os.path.exists(path), f"输出文件不存在: {path}" + + finally: + if source_cbl.endswith('dummy.cbl') and os.path.exists(source_cbl): + os.remove(source_cbl) diff --git a/black-box-data-create/tests/test_markdown_utils.py b/black-box-data-create/tests/test_markdown_utils.py new file mode 100644 index 0000000..c1a493a --- /dev/null +++ b/black-box-data-create/tests/test_markdown_utils.py @@ -0,0 +1,65 @@ +# tests/test_markdown_utils.py +from agent.markdown_utils import extract_section, parse_table_rows, parse_table_from_section, find_row_by_key + +SAMPLE_MD = """ +## 基本情報 + +| # | 項目 | 内容 | +|---|------|------| +| 1 | システム名 | 残業統計管理システム | +| 4 | PGMパターン | マッチング(1:1) | +| 5 | 機能概要 | 取消マッチング処理 | + +## 使用ファイル一覧 + +| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 媒体 | 備考 | +|----|------------------|--------|------|-----|--------|------|------| +| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | PS | | +| 2 | ERROR-LOG | W01 | ZAN04W01 | O | ZAN05REC | PS | | + +## 出力レコード定義 + +### 出力ファイル1(W01/OVT-MATCHED) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | APPL-ID | R01.APPL-ID | | +| 2 | EMP-ID | R01.EMP-ID | | +""" + + +def test_extract_section(): + result = extract_section(SAMPLE_MD, "基本情報") + assert "残業統計管理システム" in result + assert "使用ファイル一覧" not in result + + +def test_extract_section_not_found(): + result = extract_section(SAMPLE_MD, "存在しないセクション") + assert result == '' + + +def test_parse_table_rows(): + rows = parse_table_from_section(SAMPLE_MD, "基本情報") + assert len(rows) >= 3 + assert rows[0]['項目'] == 'システム名' + + +def test_find_row_by_key(): + rows = parse_table_from_section(SAMPLE_MD, "基本情報") + row = find_row_by_key(rows, '項目', 'PGMパターン') + assert row is not None + assert row['内容'] == 'マッチング(1:1)' + + +def test_parse_use_file_table(): + rows = parse_table_from_section(SAMPLE_MD, "使用ファイル一覧") + assert len(rows) == 2 + assert rows[0]['識別子'] == 'R01' + assert rows[0]['DD名'] == 'ZAN04R01' + + +def test_extract_output_records_section(): + result = extract_section(SAMPLE_MD, "出力レコード定義") + assert "出力ファイル1" in result + assert "W01/OVT-MATCHED" in result diff --git a/black-box-data-create/tests/test_models.py b/black-box-data-create/tests/test_models.py new file mode 100644 index 0000000..50292b7 --- /dev/null +++ b/black-box-data-create/tests/test_models.py @@ -0,0 +1,59 @@ +from agent.models import FileInfo, CopyField, ProgramMeta, KeyInfo, ModuleInfo, TableColumn, TableInfo + + +def test_file_info(): + f = FileInfo( + no=1, file_db_name="OVT-SORTED", identifier="R01", + dd_name="ZAN04R01", io="I", copy_group="ZAN01REC", + format="FB", record_len=80, medium="PS", remarks="有效申请" + ) + assert f.identifier == "R01" + assert f.medium == "PS" + + +def test_copy_field(): + cf = CopyField(level=3, name="R01-APPL-ID", raw_name="(A)APPL-ID", + pic_type="X(008)", pic_bytes=8) + assert cf.name == "R01-APPL-ID" + assert cf.pic_bytes == 8 + + +def test_program_meta_defaults(): + meta = ProgramMeta( + program_id="TEST", program_name="", system_name="", + pgm_type="", pgm_pattern="", + summary_lines=[], prerequisites=[], + files=[], keys=[], modules=[], + process_detail="", output_records="", + input_type="file", + copy_fields={}, db_tables={} + ) + assert meta.program_id == "TEST" + assert meta.input_type == "file" + + +def test_table_info(): + col = TableColumn(no=1, name_jp="社員番号", name_en="EMP_ID", + type="CHAR", max_len="8", decimal_digits="", + byte_count="8", nullable=False, is_pk=True) + table = TableInfo(table_name="EMP_MASTER", db_id="EMP_MASTER", + copy_id="", columns=[col], pk_columns=["EMP_ID"]) + assert table.pk_columns == ["EMP_ID"] + assert len(table.columns) == 1 + + +def test_key_info(): + k = KeyInfo(no=1, file_name="ファイルR01", + sort_condition="A001>A002>A003", + key_condition="A001>A002>A003") + assert k.no == 1 + assert k.file_name == "ファイルR01" + assert "A001" in k.sort_condition + + +def test_module_info(): + m = ModuleInfo(no=1, function="メッセージ編集出力SUB", + program_id="SUB02MSG", copy_name="ZANMSGAC") + assert m.function == "メッセージ編集出力SUB" + assert m.program_id == "SUB02MSG" + assert m.copy_name == "ZANMSGAC" diff --git a/black-box-data-create/tests/test_output_writer.py b/black-box-data-create/tests/test_output_writer.py new file mode 100644 index 0000000..a069c29 --- /dev/null +++ b/black-box-data-create/tests/test_output_writer.py @@ -0,0 +1,83 @@ +import json +import os +import tempfile +from agent.output_writer import OutputWriter + + +def test_write_json_files(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": { + "program": "TEST", + "records": [ + {"input": {"R01": {"R01-FIELD": "A001"}}} + ] + } + } + } + + written = writer.write("TESTPGM", ai_result, input_type="file") + + assert len(written) == 1 + json_path = written["g1/json"] + assert os.path.exists(json_path) + + with open(json_path, 'r', encoding='utf-8') as f: + content = json.load(f) + assert content['program'] == 'TEST' + assert len(content['records']) == 1 + + +def test_write_mixed_type(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": { + "program": "MIXED", + "records": [{"input": {"R01": {"F1": "X"}}}], + "sql": "INSERT INTO T VALUES ('x');" + } + } + } + + written = writer.write("MIXEDPGM", ai_result, input_type="mixed") + + assert len(written) == 2 + assert os.path.exists(written["g1/json"]) + assert os.path.exists(written["g1/sql"]) + + +def test_write_multiple_groups(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + + ai_result = { + "groups": { + "g1": {"records": [{}]}, + "g2": {"records": [{}]}, + "g3": {"records": [{}]}, + } + } + + written = writer.write("MULTI", ai_result, input_type="file") + + assert len(written) == 3 + for g in ['g1', 'g2', 'g3']: + assert f'{g}/json' in written + assert os.path.exists(written[f'{g}/json']) + + +def test_directory_structure(): + with tempfile.TemporaryDirectory() as tmpdir: + writer = OutputWriter(tmpdir) + ai_result = {"groups": {"g1": {"records": [{}]}}} + writer.write("MYPROG", ai_result, input_type="file") + + expected_dir = os.path.join(tmpdir, "MYPROG", "g1") + assert os.path.isdir(expected_dir) + assert os.path.isfile(os.path.join(expected_dir, "MYPROG_g1.json")) diff --git a/black-box-data-create/tests/test_prompt_builder.py b/black-box-data-create/tests/test_prompt_builder.py new file mode 100644 index 0000000..329f9a4 --- /dev/null +++ b/black-box-data-create/tests/test_prompt_builder.py @@ -0,0 +1,96 @@ +from agent.prompt_builder import PromptBuilder +from agent.models import ProgramMeta, FileInfo, CopyField + + +def test_build_basic_prompt(): + meta = ProgramMeta( + program_id='ZAN04MAT', program_name='取消マッチング処理', + system_name='残業統計管理システム', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=['取消申請のマッチング'], prerequisites=[], + files=[], keys=[], modules=[], + process_detail='1.初期処理...\n2.主処理...', + output_records='### 出力ファイル1...', + input_type='file', copy_fields={}, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# マッチング(1:1) データ生成規則\n...', + group_descriptions=['两端不一致', '逆方向两端不一致', '中间不一致'], + group_count=3 + ) + + assert 'ZAN04MAT' in prompt + assert 'マッチング(1:1)' in prompt + assert '1.初期処理' in prompt + assert '生成するグループ数: 3' in prompt + assert 'g1: 两端不一致' in prompt + assert 'g2: 逆方向两端不一致' in prompt + assert 'g3: 中间不一致' in prompt + assert 'JSON形式' in prompt + + +def test_build_with_copy_fields(): + fields = { + 'R01': [ + CopyField(level=3, name='R01-APPL-ID', raw_name='(A)APPL-ID', + pic_type='X(008)', pic_bytes=8), + CopyField(level=3, name='R01-EMP-ID', raw_name='(A)EMP-ID', + pic_type='9(008)', pic_bytes=8), + ] + } + + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], + files=[ + FileInfo(no=1, file_db_name='INPUT-FILE', identifier='R01', + dd_name='TESTR01', io='I', copy_group='TESTREC', + format='FB', record_len=80, medium='PS', remarks='') + ], + keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields=fields, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# 規則', + group_descriptions=['テスト'], + group_count=1 + ) + + assert 'R01-APPL-ID' in prompt + assert 'X(008)' in prompt + assert 'R01-EMP-ID' in prompt + + +def test_build_db_prompt(): + meta = ProgramMeta( + program_id='TESTDB', program_name='DB更新', system_name='', + pgm_type='メイン', pgm_pattern='DB更新', + summary_lines=[], prerequisites=[], + files=[ + FileInfo(no=1, file_db_name='DB-TABLE', identifier='DB', + dd_name='', io='I', copy_group='', + format='', record_len=0, medium='DB', remarks='') + ], + keys=[], modules=[], + process_detail='', output_records='', + input_type='db', copy_fields={}, db_tables={} + ) + + builder = PromptBuilder() + prompt = builder.build( + meta=meta, + rules_text='# DB更新規則', + group_descriptions=['テスト'], + group_count=1 + ) + + assert 'DB' in prompt or 'SQL' in prompt + assert 'INSERT' in prompt diff --git a/black-box-data-create/tests/test_rule_loader.py b/black-box-data-create/tests/test_rule_loader.py new file mode 100644 index 0000000..7b3e843 --- /dev/null +++ b/black-box-data-create/tests/test_rule_loader.py @@ -0,0 +1,278 @@ +import os +import pytest +from agent.rule_loader import RuleLoader, PGM_PATTERN_MAP +from agent.models import ProgramMeta + +RULES_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules') + +ALL_35_TYPES = [ + 'マッチング(1:1)', + 'マッチング(1:N)', + 'マッチング(N:1)', + 'レイアウト編集のみ(GETPUT)', + '振り分け(IF文)', + '振り分け(EVALUATE文)', + 'キーブレイク(集計)', + 'キーブレイク(集約)', + 'DB更新', + '50分割', + '25分割', + '100分割', + '項目チェック(重複含まず)', + 'オンラインPGM', + 'CSV→FB変換(改行なし)', + '2段階マッチング(1:1⇒1:1)', + '2段階マッチング(N:1⇒N:1)', + 'マッチングM:N⇒出力M件', + 'マッチングM:N⇒出力N件', + 'マッチングM:N⇒出力M×N件', + 'CSV→FB変換(改行あり)', + '2段階マッチング(M:N⇒M:N)', + 'SELECT条件', + '内部テーブル検索', + 'サブプログラム使用', + 'DB検索', + '項目チェック(半角20桁/4桁)', + 'SYSIN読込', + 'ASCII→EBCDIC変換', + 'キーブレイク(集計集約以外)', + '項目チェック(重複含む)', + '1:N+キーブレイク(同キー)', + '1:N+キーブレイク(異キー)', + 'SORT(INPUT/OUTPUT PROCEDURE)', + 'MERGE(複数ファイル結合)', +] + +DETAILED_DESIGN_PATTERNS = [ + '29(ASCII→EBCDIC変換)', + '18(M:N→M件マッチング)', + '20(M:N→M×N件直積出力)', + 'マッチング(1:1→1:1 2段階)', + 'マッチング(N:1→N:1 2段階)', + 'マッチング(M:N→M:N 2段階)', + '33(1:N+異キーキーブレイク)', + '34(SORT INPUT/OUTPUT PROCEDURE)', + '11(25分割)', + '12(100分割)', + '27(半角20桁/4桁チェック)', + '19(M:N→N件マッチング+集計)', + 'DB更新 + SYSIN読込(P28)', + '編集出力(ランキング生成)', + '32(1:N+同キーキーブレイク)', + '編集出力(印刷制御)', +] + + +def test_pgm_pattern_map_has_known_patterns(): + assert 'マッチング(1:1)' in PGM_PATTERN_MAP + assert 'マッチング(1:N)' in PGM_PATTERN_MAP + assert 'DB更新' in PGM_PATTERN_MAP + + +@pytest.mark.parametrize('pattern', ALL_35_TYPES) +def test_all_classification_types_loadable(pattern): + """程序分类说明.md の35类型すべてが規則に解決できること。""" + loader = RuleLoader(RULES_DIR) + assert loader._load_pgm_pattern_rule(pattern) is not None, f'未実装: {pattern}' + + +@pytest.mark.parametrize('pattern', DETAILED_DESIGN_PATTERNS) +def test_detailed_design_pattern_names_loadable(pattern): + """詳細設計書の PGMパターン/プログラムタイプ 表記が規則に解決できること。""" + loader = RuleLoader(RULES_DIR) + assert loader._load_pgm_pattern_rule(pattern) is not None, f'マッピング不足: {pattern}' + + +def test_non_aggregate_keybreak_uses_dedicated_rule(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('キーブレイク(集計集約以外)') + assert rule is not None + assert '非集計' in rule + + +def test_two_stage_matching_split_by_form(): + loader = RuleLoader(RULES_DIR) + r1 = loader._load_pgm_pattern_rule('2段階マッチング(1:1⇒1:1)') + r2 = loader._load_pgm_pattern_rule('2段階マッチング(N:1⇒N:1)') + r3 = loader._load_pgm_pattern_rule('マッチング(M:N→M:N 2段階)') + assert r1 is not None and r2 is not None and r3 is not None + assert '1:1⇒1:1' in r1 + assert 'N:1⇒N:1' in r2 + assert 'M:N⇒M:N' in r3 + assert r1 != r2 and r2 != r3 and r1 != r3 + + +def test_load_matching_1_1_rule(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + assert len(combined) > 0 + assert 'マッチング(1:1)' in combined + assert '数据生成' in combined + + +def test_load_matching_1_n_rule(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:N)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + assert len(combined) > 0 + assert 'マッチング(1:N)' in combined + + +def test_matching_n_1_has_only_n_1_imbalance(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('マッチング(N:1)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 9 + assert 'N:1匹配(MT-N003)' in rule or 'N:1匹配(MT-N003)' in rule + assert '极端不平衡 N:1' in rule + assert '极端不平衡 1:N' not in rule + + +def test_if_branching_rule_covers_if_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('振り分け(IF文)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 5 + for marker in ('B-N001', 'B-N005', 'IF 2路分支'): + assert marker in rule + assert 'B-N006' not in rule + assert 'B-N010' not in rule + assert 'B-R001' not in rule + + +def test_evaluate_branching_rule_covers_evaluate_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('振り分け(EVALUATE文)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 5 + for marker in ('B-N006', 'B-N009', 'B-A002', 'EVALUATE WHEN 多值'): + assert marker in rule + assert 'B-N001' not in rule + assert 'B-N010' not in rule + assert 'B-R001' not in rule + + +def test_keybreak_summary_rule_covers_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('キーブレイク(集計)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 7 + for marker in ('KB-N001', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'): + assert marker in rule + assert 'KB-N002' not in rule + + +def test_keybreak_aggregation_rule_covers_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('キーブレイク(集約)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 7 + for marker in ('KB-N002', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'): + assert marker in rule + assert 'KB-N001' not in rule + + +def test_combined_keybreak_rule_removed_and_remapped(): + combined = os.path.join(RULES_DIR, 'pgm_pattern', 'キーブレイク(集計、集約).md') + assert not os.path.exists(combined) + + loader = RuleLoader(RULES_DIR) + summary = loader._load_pgm_pattern_rule('キーブレイク(集計)') + for pattern in ('キーブレイク(集計、集約)', '1:N+キーブレイク(同キー)', + '1:Nキーブレイク(同キー集約)', '32(1:N+同キーキーブレイク)'): + rule = loader._load_pgm_pattern_rule(pattern) + assert rule is not None + assert rule == summary + + +def test_item_check_no_dup_rule_covers_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('項目チェック(重複含まず)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 5 + for marker in ('VF-N001', 'VF-N002', 'VF-N003', 'VF-N004', 'VF-R001'): + assert marker in rule + assert 'VF-N005' not in rule + assert not os.path.exists(os.path.join(RULES_DIR, 'pgm_pattern', '項目チェック.md')) + generic = loader._load_pgm_pattern_rule('項目チェック') + assert generic == rule + + +def test_item_check_halfwidth_rule_covers_benchmark(): + loader = RuleLoader(RULES_DIR) + rule = loader._load_pgm_pattern_rule('項目チェック(半角20桁/4桁)') + assert rule is not None + _, count = loader._parse_group_info(rule) + assert count == 4 + for marker in ('VF-N005', 'VF-N006', 'VF-A001', 'VF-A002'): + assert marker in rule + assert 'VF-N001' not in rule + + +def test_load_nonexistent_pattern_raises(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='存在しないパターン', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='', output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + try: + loader.load(meta) + assert False, 'Should have raised FileNotFoundError' + except FileNotFoundError: + pass + + +def test_detect_conditional_branch(): + process = """ + 2-1-2.ロジック分岐判定(EVALUATE) + 2-1-2-1.STATUS='1'の場合 + INSERT処理 + """ + assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True + + +def test_no_keyword_match(): + process = "1.初期処理\n2.主処理\n3.終了処理" + assert RuleLoader._detect_feature(process, ['EVALUATE']) is False + + +def test_rule_with_conditional_feature(): + loader = RuleLoader(RULES_DIR) + meta = ProgramMeta( + program_id='TEST', program_name='', system_name='', + pgm_type='メイン', pgm_pattern='マッチング(1:1)', + summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], + process_detail='2-1-2.EVALUATEで条件分岐する。', + output_records='', + input_type='file', copy_fields={}, db_tables={} + ) + combined, descriptions, count = loader.load(meta) + + assert count > 0 + assert '条件分支' in combined diff --git a/requirements.txt b/requirements.txt index 7ca6594..e789d4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ fastapi==0.111.0 uvicorn==0.30.0 python-multipart==0.0.9 jinja2==3.1.4 +requests>=2.28.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..72aa6c6 --- /dev/null +++ b/run.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +"""COBOL 迁移验证平台统一入口。 + +用法: + python run.py verify <主工程参数...> # 透传给 main.py (COBOL->Java/Spark 迁移验证) + python run.py blackbox <黑盒参数...> # 透传给 black-box-data-create/main.py (LLM 测试数据生成) + +子命令后的参数原样转发给目标 main.py,由目标自行解析,本脚本不做任何处理。 +""" +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.abspath(__file__)) + +TARGETS = { + "verify": os.path.join(ROOT, "main.py"), + "blackbox": os.path.join(ROOT, "black-box-data-create", "main.py"), +} + +USAGE = """\ +用法: python run.py [参数...] + +子命令: + verify - 主工程: COBOL->Java/Spark 迁移验证(透传给 main.py) + blackbox - 黑盒工程: LLM 测试数据生成(透传给 black-box-data-create/main.py) +""" + + +def main(): + if len(sys.argv) < 2: + print(USAGE, file=sys.stderr) + return 2 + + engine = sys.argv[1] + target = TARGETS.get(engine) + if target is None: + print(f"错误: 未知子命令 '{engine}'\n", file=sys.stderr) + print(USAGE, file=sys.stderr) + return 2 + + if not os.path.exists(target): + print(f"错误: 找不到目标脚本: {target}", file=sys.stderr) + return 2 + + passthrough = sys.argv[2:] + return subprocess.run([sys.executable, target] + passthrough).returncode + + +if __name__ == "__main__": + sys.exit(main())