v1.0
This commit is contained in:
@@ -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更新・マッチング・キーブレイク等で必須。
|
||||
@@ -0,0 +1,258 @@
|
||||
# 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
|
||||
│ │ ├── 振り分け.md
|
||||
│ │ ├── 項目チェック.md
|
||||
│ │ ├── レイアウト編集のみ(GETPUT).md
|
||||
│ │ ├── キーブレイク(集計).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 | 1 | ✓ |
|
||||
| マッチング(1:N) | マッチング(1-N).md | 2 | ✓ |
|
||||
| マッチング(N:1) | マッチング(N-1).md | 1 | ✓ |
|
||||
| DB更新 | DB更新.md | 5 | ✓ |
|
||||
| 振り分け | 振り分け.md | 2 | — |
|
||||
| 項目チェック | 項目チェック.md | 2 | — |
|
||||
| GETPUT | レイアウト編集のみ(GETPUT).md | 3 | — |
|
||||
| キーブレイク(集計) | キーブレイク(集計).md | 2 | ✓ |
|
||||
| キーブレイク(集約) | キーブレイク(集約).md | 1 | ✓ |
|
||||
| キーブレイク(集計、集約) | キーブレイク(集計、集約).md | 1 | ✓ |
|
||||
| SELECT処理 | SELECT処理.md | 1 | ✓ |
|
||||
| MERGE | MERGE.md | 1 | ✓ |
|
||||
| 50分割 | 50分割.md | 1 | — |
|
||||
| CSV→FB変換 | CSV→FB変換.md | 1 | — |
|
||||
|
||||
**合計: 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 応答速度に依存)。
|
||||
@@ -0,0 +1,56 @@
|
||||
__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:
|
||||
"""生成测试数据的主入口函数。"""
|
||||
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)
|
||||
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,
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,108 @@
|
||||
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 生成的结果。"""
|
||||
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)
|
||||
content = response['choices'][0]['message']['content']
|
||||
data = self._parse_json(content)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
import re
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from agent.models import ProgramMeta
|
||||
|
||||
|
||||
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)': 'レイアウト編集のみ(GETPUT).md',
|
||||
'レイアウト編集のみ': 'レイアウト編集のみ(GETPUT).md',
|
||||
'GETPUT(編集出力)': 'GETPUT(編集出力).md',
|
||||
'項目チェック': '項目チェック.md',
|
||||
'項目チェック(重複なし)': '項目チェック.md',
|
||||
'振り分け': '振り分け.md',
|
||||
'振り分け(IF文、EVALUATE文)': '振り分け.md',
|
||||
'キーブレイク': 'キーブレイク(集計).md',
|
||||
'キーブレイク(集計)': 'キーブレイク(集計).md',
|
||||
'キーブレイク(集約)': 'キーブレイク(集約).md',
|
||||
'キーブレイク(集約)': 'キーブレイク(集約).md',
|
||||
'キーブレイク(集計、集約)': 'キーブレイク(集計、集約).md',
|
||||
'キーブレイク(集計、集約の以外)': 'キーブレイク(集計).md',
|
||||
'1:Nキーブレイク(同キー集約)': 'キーブレイク(集計、集約).md',
|
||||
'1:N+キーブレイク(同キー)': 'キーブレイク(集計、集約).md',
|
||||
'DB更新': 'DB更新.md',
|
||||
'DB更新 + SYSIN読込(P28)': 'DB更新.md',
|
||||
'SELECT処理': 'SELECT処理.md',
|
||||
'SELECT条件': 'SELECT処理.md',
|
||||
'50分割': '50分割.md',
|
||||
'MERGE(複数ファイル結合)': 'MERGE.md',
|
||||
'CSV→FB変換(改行あり)': 'CSV→FB変換.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パターン 中
|
||||
pgm_lower = pgm_pattern.lower()
|
||||
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]:
|
||||
"""从规则文本中解析组信息。"""
|
||||
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
|
||||
@@ -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` 规范。
|
||||
@@ -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": "<PromptBuilder生成的完整prompt>"
|
||||
}
|
||||
],
|
||||
"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 代码(添加映射条目)
|
||||
+347
@@ -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を実行
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.同項目を設定 | |
|
||||
@@ -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 | |
|
||||
@@ -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 | | | |
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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='ルール格納ディレクトリ')
|
||||
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
{"program": "KIN03EXP", "records": [{"input": {"HOLIDAY_CALENDAR": {"HOLIDAY_DATE": "20250101", "DESCRIPTION": "元日"}}}, {"input": {"HOLIDAY_CALENDAR": {"HOLIDAY_DATE": "20250113", "DESCRIPTION": "成人の日"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 1, "EMP_ID": "A0000001", "LEAVE_TYPE": "01", "START_DATE": "20250106", "START_TIME": "0900", "END_DATE": "20250108", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 2, "EMP_ID": "A0000001", "LEAVE_TYPE": "02", "START_DATE": "20250110", "START_TIME": "0900", "END_DATE": "20250114", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 3, "EMP_ID": "A0000001", "LEAVE_TYPE": "01", "START_DATE": "20250115", "START_TIME": "0900", "END_DATE": "20250115", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 4, "EMP_ID": "A0000002", "LEAVE_TYPE": "03", "START_DATE": "20250106", "START_TIME": "0900", "END_DATE": "20250106", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 5, "EMP_ID": "A0000002", "LEAVE_TYPE": "01", "START_DATE": "20250120", "START_TIME": "0900", "END_DATE": "20250122", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 6, "EMP_ID": "A0000003", "LEAVE_TYPE": "04", "START_DATE": "20250102", "START_TIME": "0900", "END_DATE": "20250103", "END_TIME": "1800", "STATUS": "1"}}}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"program": "KIN03EXP", "records": [{"input": {"HOLIDAY_CALENDAR": {"HOLIDAY_DATE": "20250101", "DESCRIPTION": "元日"}}}, {"input": {"HOLIDAY_CALENDAR": {"HOLIDAY_DATE": "20250113", "DESCRIPTION": "成人の日"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 7, "EMP_ID": "A0000004", "LEAVE_TYPE": "01", "START_DATE": "20250107", "START_TIME": "0900", "END_DATE": "20250107", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 8, "EMP_ID": "A0000005", "LEAVE_TYPE": "02", "START_DATE": "20250108", "START_TIME": "0900", "END_DATE": "20250109", "END_TIME": "1800", "STATUS": "1"}}}, {"input": {"LEAVE_RECORDS": {"APPLICATION_ID": 9, "EMP_ID": "A0000005", "LEAVE_TYPE": "03", "START_DATE": "20250115", "START_TIME": "0900", "END_DATE": "20250116", "END_TIME": "1800", "STATUS": "1"}}}]}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"program": "KIN09CSV",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"DAILY_RECORDS": {
|
||||
"EMP_ID": "A0001",
|
||||
"DATE": "20241001",
|
||||
"TIME_IN": "0900",
|
||||
"TIME_OUT": "1800",
|
||||
"LEAVE_TYPE": "0",
|
||||
"ANNUAL_H": "0000",
|
||||
"PERSONAL_H": "0000",
|
||||
"OFFICIAL_H": "0000",
|
||||
"SICK_H": "0000",
|
||||
"ABSENT_H": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"DAILY_RECORDS": {
|
||||
"EMP_ID": "A0002",
|
||||
"DATE": "20241001",
|
||||
"TIME_IN": "0830",
|
||||
"TIME_OUT": "1730",
|
||||
"LEAVE_TYPE": "1",
|
||||
"ANNUAL_H": "0100",
|
||||
"PERSONAL_H": "0000",
|
||||
"OFFICIAL_H": "0000",
|
||||
"SICK_H": "0000",
|
||||
"ABSENT_H": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"DAILY_RECORDS": {
|
||||
"EMP_ID": "A0001",
|
||||
"DATE": "20241002",
|
||||
"TIME_IN": "0900",
|
||||
"TIME_OUT": "1700",
|
||||
"LEAVE_TYPE": "0",
|
||||
"ANNUAL_H": "0000",
|
||||
"PERSONAL_H": "0200",
|
||||
"OFFICIAL_H": "0000",
|
||||
"SICK_H": "0000",
|
||||
"ABSENT_H": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"DAILY_RECORDS": {
|
||||
"EMP_ID": "A0003",
|
||||
"DATE": "20241001",
|
||||
"TIME_IN": "1000",
|
||||
"TIME_OUT": "1900",
|
||||
"LEAVE_TYPE": "2",
|
||||
"ANNUAL_H": "0000",
|
||||
"PERSONAL_H": "0000",
|
||||
"OFFICIAL_H": "0300",
|
||||
"SICK_H": "0000",
|
||||
"ABSENT_H": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"DAILY_RECORDS": {
|
||||
"EMP_ID": "A0002",
|
||||
"DATE": "20241002",
|
||||
"TIME_IN": "0830",
|
||||
"TIME_OUT": "1200",
|
||||
"LEAVE_TYPE": "3",
|
||||
"ANNUAL_H": "0000",
|
||||
"PERSONAL_H": "0000",
|
||||
"OFFICIAL_H": "0000",
|
||||
"SICK_H": "0400",
|
||||
"ABSENT_H": "0000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"program": "KYU01CVT",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "# コメント行です"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00001,山田太郎,001,01,01,300000,1500,2,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00003,鈴木一郎,002,02,02,250000,1200,1,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00004,\"山田"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "一郎\",002,02,02,250000,1200,1,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00005,田中花子,003,03,03,200000,1000,3,1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"program": "KYU01CVT",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00001,山田太郎,001,01,01,300000,1500"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00002,佐藤花子,002,02,02,250000,1200,1,1,extra"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": " ,鈴木次郎,003,03,03,200000,1000,1,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "A0001,田中三郎,004,04,04,350000,1800,2,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00005,高橋四郎,005,05,05,400000,ABC,3,1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU01R01": {
|
||||
"CSV-LINE": "00006,12345,006,06,06,150000,800,0,1"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"program": "KYU03AGG",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000001",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "008"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000001",
|
||||
"YEAR-MONTH": "202502",
|
||||
"ABSENT-HOURS": "004"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000001",
|
||||
"YEAR-MONTH": "202503",
|
||||
"ABSENT-HOURS": "002"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000002",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "012"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000002",
|
||||
"YEAR-MONTH": "202502",
|
||||
"ABSENT-HOURS": "006"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000003",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "010"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"program": "KYU03AGG",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000101",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000102",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "002"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000103",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "003"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000104",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "004"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "S000000105",
|
||||
"YEAR-MONTH": "202501",
|
||||
"ABSENT-HOURS": "005"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"program": "KYU04CAL",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU04R03": {
|
||||
"PAY-DATA": "202310,300000 "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000001",
|
||||
"TOTAL-HOURS": "+0000100",
|
||||
"ATTEND-TYPE": "W",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000002",
|
||||
"TOTAL-HOURS": "+0000500",
|
||||
"ATTEND-TYPE": "H",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000003",
|
||||
"TOTAL-HOURS": "+0000200",
|
||||
"ATTEND-TYPE": "L",
|
||||
"ALLOWANCE-TYPE": "LATE"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000004",
|
||||
"TOTAL-HOURS": "+0000300",
|
||||
"ATTEND-TYPE": "W",
|
||||
"ALLOWANCE-TYPE": "XXX"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000005",
|
||||
"TOTAL-HOURS": "+0000000",
|
||||
"ATTEND-TYPE": "W",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000006",
|
||||
"TOTAL-HOURS": "+0010000",
|
||||
"ATTEND-TYPE": "W",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"sql": "INSERT INTO EMP_MASTER (EMP_ID, EMP_NAME, DEPT_CODE, REGION_CODE, CATEGORY_CODE, BASE_SALARY, HOURLY_RATE, DEPENDENT_COUNT, STATUS) VALUES\n('A0000001', 'TANAKA TARO', '10', '01', '001', 300000, 1500, 2, '1'),\n('A0000002', 'SUZUKI ICHIRO', '20', '02', '002', 350000, 1800, 1, '1'),\n('A0000003', 'YAMADA HANAKO', '10', '01', '001', 280000, 1200, 3, '1'),\n('A0000004', 'SATOU JIRO', '30', '03', '003', 320000, 1600, 0, '1'),\n('A0000006', 'KATO TAKASHI', '20', '02', '002', 500000, 9999999, 4, '1');\n\nINSERT INTO OVT_MONTHLY (EMP_ID, YEAR_MONTH, OVT_TYPE, OVT_HOURS, OVT_COUNT) VALUES\n('A0000001', '202310', '1', 10, 5),\n('A0000003', '202310', '1', 5, 2),\n('A0000004', '202310', '1', 8, 4),\n('A0000006', '202310', '1', 50, 10);"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
INSERT INTO EMP_MASTER (EMP_ID, EMP_NAME, DEPT_CODE, REGION_CODE, CATEGORY_CODE, BASE_SALARY, HOURLY_RATE, DEPENDENT_COUNT, STATUS) VALUES
|
||||
('A0000001', 'TANAKA TARO', '10', '01', '001', 300000, 1500, 2, '1'),
|
||||
('A0000002', 'SUZUKI ICHIRO', '20', '02', '002', 350000, 1800, 1, '1'),
|
||||
('A0000003', 'YAMADA HANAKO', '10', '01', '001', 280000, 1200, 3, '1'),
|
||||
('A0000004', 'SATOU JIRO', '30', '03', '003', 320000, 1600, 0, '1'),
|
||||
('A0000006', 'KATO TAKASHI', '20', '02', '002', 500000, 9999999, 4, '1');
|
||||
|
||||
INSERT INTO OVT_MONTHLY (EMP_ID, YEAR_MONTH, OVT_TYPE, OVT_HOURS, OVT_COUNT) VALUES
|
||||
('A0000001', '202310', '1', 10, 5),
|
||||
('A0000003', '202310', '1', 5, 2),
|
||||
('A0000004', '202310', '1', 8, 4),
|
||||
('A0000006', '202310', '1', 50, 10);
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"program": "KYU04CAL",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU04R03": {
|
||||
"PAY-DATA": "202310,300000 "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000001",
|
||||
"TOTAL-HOURS": "+0000100",
|
||||
"ATTEND-TYPE": "W",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000001",
|
||||
"TOTAL-HOURS": "+0000200",
|
||||
"ATTEND-TYPE": "H",
|
||||
"ALLOWANCE-TYPE": "OVT"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000001",
|
||||
"TOTAL-HOURS": "+0000300",
|
||||
"ATTEND-TYPE": "L",
|
||||
"ALLOWANCE-TYPE": "LATE"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU04R02": {
|
||||
"EMP-ID": "A0000001",
|
||||
"TOTAL-HOURS": "+0000400",
|
||||
"ATTEND-TYPE": "X",
|
||||
"ALLOWANCE-TYPE": "TES"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"sql": "INSERT INTO EMP_MASTER (EMP_ID, EMP_NAME, DEPT_CODE, REGION_CODE, CATEGORY_CODE, BASE_SALARY, HOURLY_RATE, DEPENDENT_COUNT, STATUS) VALUES\n('A0000001', 'TANAKA TARO', '10', '01', '001', 300000, 1500, 2, '1');\n\nINSERT INTO OVT_MONTHLY (EMP_ID, YEAR_MONTH, OVT_TYPE, OVT_HOURS, OVT_COUNT) VALUES\n('A0000001', '202310', '1', 10, 5);"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
INSERT INTO EMP_MASTER (EMP_ID, EMP_NAME, DEPT_CODE, REGION_CODE, CATEGORY_CODE, BASE_SALARY, HOURLY_RATE, DEPENDENT_COUNT, STATUS) VALUES
|
||||
('A0000001', 'TANAKA TARO', '10', '01', '001', 300000, 1500, 2, '1');
|
||||
|
||||
INSERT INTO OVT_MONTHLY (EMP_ID, YEAR_MONTH, OVT_TYPE, OVT_HOURS, OVT_COUNT) VALUES
|
||||
('A0000001', '202310', '1', 10, 5);
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"program": "KYU05DED",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "00000001",
|
||||
"EMP-NAME": "TARO YAMADA ",
|
||||
"DEPT-CODE": "01",
|
||||
"BASE-SALARY": "000040000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "00000002",
|
||||
"EMP-NAME": "JIRO SUZUKI ",
|
||||
"DEPT-CODE": "02",
|
||||
"BASE-SALARY": "000080000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"EMP-ID": "00000003",
|
||||
"EMP-NAME": "SABURO TANAKA ",
|
||||
"DEPT-CODE": "03",
|
||||
"BASE-SALARY": "000010000"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"sql": "INSERT INTO TAX-TABLE (TAX-FROM, TAX-TO, TAX-RATE, DEDUCTION) VALUES (0, 50000, 0.05, 0), (50001, 100000, 0.10, 2000), (100001, 999999999, 0.20, 10000);\nINSERT INTO INSURANCE-TABLE (INS-INCOME-FROM, INS-INCOME-TO, INS-HEALTH-RATE, INS-PENSION-RATE) VALUES (0, 50000, 0.04, 0.06), (50001, 100000, 0.05, 0.07), (100001, 999999999, 0.06, 0.08);"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
INSERT INTO TAX-TABLE (TAX-FROM, TAX-TO, TAX-RATE, DEDUCTION) VALUES (0, 50000, 0.05, 0), (50001, 100000, 0.10, 2000), (100001, 999999999, 0.20, 10000);
|
||||
INSERT INTO INSURANCE-TABLE (INS-INCOME-FROM, INS-INCOME-TO, INS-HEALTH-RATE, INS-PENSION-RATE) VALUES (0, 50000, 0.04, 0.06), (50001, 100000, 0.05, 0.07), (100001, 999999999, 0.06, 0.08);
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"program": "KYU07DIV",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU07SYS": {
|
||||
"SYSIN-REC": "DEPT=01,03,05 "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000001",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "TARO YAMADA",
|
||||
"DEPT-CODE": "01",
|
||||
"REGION-CODE": "01",
|
||||
"CATEGORY-CODE": "01",
|
||||
"GROSS-PAYMENT": "0005000",
|
||||
"INCOME-TAX": "0000500",
|
||||
"INSURANCE": "0000100",
|
||||
"RESIDENT-TAX": "0000200",
|
||||
"NET-PAYMENT": "0004200",
|
||||
"TAXABLE-INCOME": "0004800",
|
||||
"PAY-AMOUNT": "0005000",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000002",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "JIRO SUZUKI",
|
||||
"DEPT-CODE": "01",
|
||||
"REGION-CODE": "01",
|
||||
"CATEGORY-CODE": "02",
|
||||
"GROSS-PAYMENT": "0006000",
|
||||
"INCOME-TAX": "0000600",
|
||||
"INSURANCE": "0000200",
|
||||
"RESIDENT-TAX": "0000300",
|
||||
"NET-PAYMENT": "0005500",
|
||||
"TAXABLE-INCOME": "0005800",
|
||||
"PAY-AMOUNT": "0006000",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000003",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "SABURO TANAKA",
|
||||
"DEPT-CODE": "03",
|
||||
"REGION-CODE": "02",
|
||||
"CATEGORY-CODE": "01",
|
||||
"GROSS-PAYMENT": "0004500",
|
||||
"INCOME-TAX": "0000450",
|
||||
"INSURANCE": "0000150",
|
||||
"RESIDENT-TAX": "0000180",
|
||||
"NET-PAYMENT": "0003700",
|
||||
"TAXABLE-INCOME": "0004300",
|
||||
"PAY-AMOUNT": "0004500",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000004",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "HANAKO SATO",
|
||||
"DEPT-CODE": "02",
|
||||
"REGION-CODE": "03",
|
||||
"CATEGORY-CODE": "03",
|
||||
"GROSS-PAYMENT": "0007000",
|
||||
"INCOME-TAX": "0000700",
|
||||
"INSURANCE": "0000300",
|
||||
"RESIDENT-TAX": "0000400",
|
||||
"NET-PAYMENT": "0006300",
|
||||
"TAXABLE-INCOME": "0006700",
|
||||
"PAY-AMOUNT": "0007000",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000005",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "ICHIRO WATANABE",
|
||||
"DEPT-CODE": "99",
|
||||
"REGION-CODE": "03",
|
||||
"CATEGORY-CODE": "02",
|
||||
"GROSS-PAYMENT": "0008000",
|
||||
"INCOME-TAX": "0000800",
|
||||
"INSURANCE": "0000400",
|
||||
"RESIDENT-TAX": "0000500",
|
||||
"NET-PAYMENT": "0007200",
|
||||
"TAXABLE-INCOME": "0007600",
|
||||
"PAY-AMOUNT": "0008000",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU07R01": {
|
||||
"EMP-ID": "A0000006",
|
||||
"REC-TYPE": "1",
|
||||
"EMP-NAME": "KENJI ITO",
|
||||
"DEPT-CODE": "05",
|
||||
"REGION-CODE": "05",
|
||||
"CATEGORY-CODE": "01",
|
||||
"GROSS-PAYMENT": "0005500",
|
||||
"INCOME-TAX": "0000550",
|
||||
"INSURANCE": "0000250",
|
||||
"RESIDENT-TAX": "0000350",
|
||||
"NET-PAYMENT": "0004800",
|
||||
"TAXABLE-INCOME": "0005200",
|
||||
"PAY-AMOUNT": "0005500",
|
||||
"FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"program": "KYU09MRG",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU09M01": {
|
||||
"EMP-ID": "00010001",
|
||||
"REC-TYPE": "S",
|
||||
"PAY-AMOUNT": "000100000",
|
||||
"GROSS-PAYMENT": "0001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M01": {
|
||||
"EMP-ID": "00020002",
|
||||
"REC-TYPE": "S",
|
||||
"PAY-AMOUNT": "000200000",
|
||||
"GROSS-PAYMENT": "0002"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M02": {
|
||||
"EMP-ID": "00010001",
|
||||
"REC-TYPE": "B",
|
||||
"PAY-AMOUNT": "000050000",
|
||||
"GROSS-PAYMENT": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M02": {
|
||||
"EMP-ID": "00030003",
|
||||
"REC-TYPE": "B",
|
||||
"PAY-AMOUNT": "000030000",
|
||||
"GROSS-PAYMENT": "0000"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"program": "KYU09MRG",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"KYU09M01": {
|
||||
"EMP-ID": "00000000",
|
||||
"REC-TYPE": "S",
|
||||
"PAY-AMOUNT": "000001000",
|
||||
"GROSS-PAYMENT": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M01": {
|
||||
"EMP-ID": "99999999",
|
||||
"REC-TYPE": "S",
|
||||
"PAY-AMOUNT": "000100000",
|
||||
"GROSS-PAYMENT": "9999"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M02": {
|
||||
"EMP-ID": "00000000",
|
||||
"REC-TYPE": "B",
|
||||
"PAY-AMOUNT": "000000500",
|
||||
"GROSS-PAYMENT": "0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"KYU09M02": {
|
||||
"EMP-ID": "99999999",
|
||||
"REC-TYPE": "B",
|
||||
"PAY-AMOUNT": "000050000",
|
||||
"GROSS-PAYMENT": "9999"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"program": "ZAN03CHK",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240103",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240104",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240105",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240101",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240102",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240103",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240104",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240106",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000002",
|
||||
"R02-DATE": "20240105",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"program": "ZAN03CHK",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240101",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240103",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240104",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240106",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20240105",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240102",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240103",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240104",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240105",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"program": "ZAN03CHK",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240101",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240103",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240104",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R01": {
|
||||
"R01-APPL-ID": "APP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20240105",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": " ",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240101",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240102",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240104",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN03R02": {
|
||||
"R02-EMP-ID": "00000001",
|
||||
"R02-DATE": "20240105",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"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": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20250102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000002",
|
||||
"R02-EMP-ID": "00000022",
|
||||
"R02-APPL-DATE": "20250102",
|
||||
"R02-START-TIME": "0900",
|
||||
"R02-END-TIME": "1800",
|
||||
"R02-STATUS": "3",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20250103",
|
||||
"R01-START-TIME": "1000",
|
||||
"R01-END-TIME": "1900",
|
||||
"R01-STATUS": "3",
|
||||
"R01-OVT-TYPE": "O",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000003",
|
||||
"R02-EMP-ID": "00000033",
|
||||
"R02-APPL-DATE": "20250103",
|
||||
"R02-START-TIME": "1000",
|
||||
"R02-END-TIME": "1900",
|
||||
"R02-STATUS": "4",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000004",
|
||||
"R01-EMP-ID": "00000004",
|
||||
"R01-APPL-DATE": "20250104",
|
||||
"R01-START-TIME": "1100",
|
||||
"R01-END-TIME": "2000",
|
||||
"R01-STATUS": "4",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000004",
|
||||
"R02-EMP-ID": "00000044",
|
||||
"R02-APPL-DATE": "20250104",
|
||||
"R02-START-TIME": "1100",
|
||||
"R02-END-TIME": "2000",
|
||||
"R02-STATUS": "5",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000005",
|
||||
"R02-EMP-ID": "00000055",
|
||||
"R02-APPL-DATE": "20250105",
|
||||
"R02-START-TIME": "1200",
|
||||
"R02-END-TIME": "2100",
|
||||
"R02-STATUS": "6",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"program": "ZAN04MAT",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000001",
|
||||
"R02-EMP-ID": "00000011",
|
||||
"R02-APPL-DATE": "20250101",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-STATUS": "7",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20250102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000002",
|
||||
"R02-EMP-ID": "00000022",
|
||||
"R02-APPL-DATE": "20250102",
|
||||
"R02-START-TIME": "0900",
|
||||
"R02-END-TIME": "1800",
|
||||
"R02-STATUS": "3",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20250103",
|
||||
"R01-START-TIME": "1000",
|
||||
"R01-END-TIME": "1900",
|
||||
"R01-STATUS": "3",
|
||||
"R01-OVT-TYPE": "O",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000003",
|
||||
"R02-EMP-ID": "00000033",
|
||||
"R02-APPL-DATE": "20250103",
|
||||
"R02-START-TIME": "1000",
|
||||
"R02-END-TIME": "1900",
|
||||
"R02-STATUS": "4",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000004",
|
||||
"R01-EMP-ID": "00000004",
|
||||
"R01-APPL-DATE": "20250104",
|
||||
"R01-START-TIME": "1100",
|
||||
"R01-END-TIME": "2000",
|
||||
"R01-STATUS": "4",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000004",
|
||||
"R02-EMP-ID": "00000044",
|
||||
"R02-APPL-DATE": "20250104",
|
||||
"R02-START-TIME": "1100",
|
||||
"R02-END-TIME": "2000",
|
||||
"R02-STATUS": "5",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000005",
|
||||
"R01-EMP-ID": "00000005",
|
||||
"R01-APPL-DATE": "20250105",
|
||||
"R01-START-TIME": "1200",
|
||||
"R01-END-TIME": "2100",
|
||||
"R01-STATUS": "5",
|
||||
"R01-OVT-TYPE": "O",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"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": "A0000001",
|
||||
"R02-EMP-ID": "00000011",
|
||||
"R02-APPL-DATE": "20250101",
|
||||
"R02-START-TIME": "0800",
|
||||
"R02-END-TIME": "1700",
|
||||
"R02-STATUS": "7",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20250103",
|
||||
"R01-START-TIME": "1000",
|
||||
"R01-END-TIME": "1900",
|
||||
"R01-STATUS": "3",
|
||||
"R01-OVT-TYPE": "O",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000004",
|
||||
"R01-EMP-ID": "00000004",
|
||||
"R01-APPL-DATE": "20250104",
|
||||
"R01-START-TIME": "1100",
|
||||
"R01-END-TIME": "2000",
|
||||
"R01-STATUS": "4",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000003",
|
||||
"R02-EMP-ID": "00000033",
|
||||
"R02-APPL-DATE": "20250103",
|
||||
"R02-START-TIME": "1000",
|
||||
"R02-END-TIME": "1900",
|
||||
"R02-STATUS": "4",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000002",
|
||||
"R02-EMP-ID": "00000022",
|
||||
"R02-APPL-DATE": "20250102",
|
||||
"R02-START-TIME": "0900",
|
||||
"R02-END-TIME": "1800",
|
||||
"R02-STATUS": "3",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000005",
|
||||
"R01-EMP-ID": "00000005",
|
||||
"R01-APPL-DATE": "20250105",
|
||||
"R01-START-TIME": "1200",
|
||||
"R01-END-TIME": "2100",
|
||||
"R01-STATUS": "5",
|
||||
"R01-OVT-TYPE": "O",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000005",
|
||||
"R02-EMP-ID": "00000055",
|
||||
"R02-APPL-DATE": "20250105",
|
||||
"R02-START-TIME": "1200",
|
||||
"R02-END-TIME": "2100",
|
||||
"R02-STATUS": "6",
|
||||
"R02-OVT-TYPE": "C",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"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": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"program": "ZAN04MAT",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"ZAN04R01": {
|
||||
"R01-APPL-ID": "A0000002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20250102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "C",
|
||||
"R01-FILLER": " "
|
||||
},
|
||||
"ZAN04R02": {
|
||||
"R02-APPL-ID": "A0000002",
|
||||
"R02-EMP-ID": "00000022",
|
||||
"R02-APPL-DATE": "20250102",
|
||||
"R02-START-TIME": "0900",
|
||||
"R02-END-TIME": "1800",
|
||||
"R02-STATUS": "3",
|
||||
"R02-OVT-TYPE": "O",
|
||||
"R02-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"program": "ZAN05CAL",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20230101",
|
||||
"R01-START-TIME": "0800",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00001",
|
||||
"R01-EMP-ID": "00000001",
|
||||
"R01-APPL-DATE": "20230101",
|
||||
"R01-START-TIME": "1300",
|
||||
"R01-END-TIME": "1700",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "02",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20230102",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1100",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20230102",
|
||||
"R01-START-TIME": "1400",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "02",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00002",
|
||||
"R01-EMP-ID": "00000002",
|
||||
"R01-APPL-DATE": "20230102",
|
||||
"R01-START-TIME": "1900",
|
||||
"R01-END-TIME": "2100",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "03",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20230103",
|
||||
"R01-START-TIME": "1000",
|
||||
"R01-END-TIME": "1300",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20230103",
|
||||
"R01-START-TIME": "1400",
|
||||
"R01-END-TIME": "1600",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "02",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20230103",
|
||||
"R01-START-TIME": "1700",
|
||||
"R01-END-TIME": "1900",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "03",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "GRP00003",
|
||||
"R01-EMP-ID": "00000003",
|
||||
"R01-APPL-DATE": "20230103",
|
||||
"R01-START-TIME": "2000",
|
||||
"R01-END-TIME": "2200",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "04",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"program": "ZAN05CAL",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00001",
|
||||
"R01-EMP-ID": "00000010",
|
||||
"R01-APPL-DATE": "20230201",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1700",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00002",
|
||||
"R01-EMP-ID": "00000011",
|
||||
"R01-APPL-DATE": "20230202",
|
||||
"R01-START-TIME": "0800",
|
||||
"R01-END-TIME": "1000",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00002",
|
||||
"R01-EMP-ID": "00000011",
|
||||
"R01-APPL-DATE": "20230202",
|
||||
"R01-START-TIME": "1100",
|
||||
"R01-END-TIME": "1500",
|
||||
"R01-STATUS": "1",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "02",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00003",
|
||||
"R01-EMP-ID": "00000012",
|
||||
"R01-APPL-DATE": "20230203",
|
||||
"R01-START-TIME": "1000",
|
||||
"R01-END-TIME": "1200",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "H",
|
||||
"R01-PROC-SEQ": "01",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00003",
|
||||
"R01-EMP-ID": "00000012",
|
||||
"R01-APPL-DATE": "20230203",
|
||||
"R01-START-TIME": "1300",
|
||||
"R01-END-TIME": "1400",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "02",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"R01": {
|
||||
"R01-APPL-ID": "SNG00003",
|
||||
"R01-EMP-ID": "00000012",
|
||||
"R01-APPL-DATE": "20230203",
|
||||
"R01-START-TIME": "1500",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "2",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-PROC-SEQ": "03",
|
||||
"R01-FILLER": " "
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
requests>=2.28.0
|
||||
@@ -0,0 +1,25 @@
|
||||
# 50分割 データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
入力ファイルのレコードを50件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。
|
||||
|
||||
## 入力
|
||||
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 通常分割 | 51件以上のデータ(50件の境界を超えるケース)+ 50件未満のデータ |
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. 入力ファイルのフィールド定義を取得
|
||||
2. 組1に51件以上のレコードを生成(分割境界テスト用)
|
||||
3. 各フィールドはPIC定義に従い実在しそうな値を生成
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -0,0 +1,28 @@
|
||||
# CSV→FB変換 データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
CSV形式の入力ファイルを読み取り、固定長(FB)レコードに変換して出力するプログラムのテストデータ。
|
||||
CSVのカンマ区切り分解、引用符内改行処理、項目チェックを含む。
|
||||
|
||||
## 入力
|
||||
|
||||
- 入力CSVの項目定義(処理詳細から特定)
|
||||
- 出力COPYBOOKのフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 正常データ | 全項目が正しいCSVレコード 3〜5件。異なる値のバリエーションを確保 |
|
||||
| 2 | 異常データ | 項目数不足、項目数過剰、不正な値のレコードを含むデータ |
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. CSVの項目数と項目名を処理詳細から特定
|
||||
2. 正常データ: カンマ区切りCSV行文字列を生成。値はPIC定義に従う
|
||||
3. 異常データ: 項目数不一致、空値、不正フォーマットを含む
|
||||
|
||||
## 出力
|
||||
|
||||
入力はCSV文字列(カンマ区切り)として1フィールドに格納。各組1 JSONファイル。
|
||||
@@ -0,0 +1,79 @@
|
||||
# DB更新 データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
入力ファイルのレコードをDBテーブルにINSERT/UPDATE/DELETEするプログラムのテストデータを生成する。
|
||||
出力はSQL INSERTファイル。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書の処理詳細(SQL操作と分岐条件)
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
- DBテーブル定義と主キー
|
||||
- キー項目一覧
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 | 条件 |
|
||||
|----|------|------|------|
|
||||
| 1 | 正常INSERT | 5件以上の新規INSERT + 末尾に多keyテスト | 必須 |
|
||||
| 2 | INSERT+UPDATE | 3件以上の重複主キーレコード(UPDATEトリガー用)+ 末尾に多keyテスト | 必須 |
|
||||
| 3 | DELETE対象 | 2件以上のDELETE対象レコード + 末尾に多keyテスト | 処理詳細にDELETE操作がある場合のみ |
|
||||
|
||||
## 多keyテスト(各組の末尾に追加)
|
||||
|
||||
キーが複数項目(例: A001, A002, A003)で構成される場合、以下のテストデータを追加する。
|
||||
|
||||
**手順:**
|
||||
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対象)**: 処理詳細にDELETE操作がある場合のみ生成
|
||||
- STATUS='9'などの削除フラグ付きデータ
|
||||
- 末尾に多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の後にカンマ区切りで複数行)
|
||||
@@ -0,0 +1,28 @@
|
||||
# GETPUT(編集出力)データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
DBまたはファイルからデータを読み取り、編集・整形して出力するプログラムのテストデータ。
|
||||
レイアウト編集のみ(GETPUT)と同様のシンプルなREAD→編集→WRITEパターン。
|
||||
|
||||
## 入力
|
||||
|
||||
- 入力ソースのフィールド定義(COPYBOOKまたはDBテーブル定義)
|
||||
- 出力レコード定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 基本データ | 通常値2〜5件。各フィールドに有効な値を設定。異なる値のバリエーションを確保 |
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. 入力フィールドを特定
|
||||
2. 各フィールドのPIC定義に従い値を生成
|
||||
3. 実在しそうな値(日付・時刻・金額・氏名等)を生成
|
||||
4. レコード間で異なる値を設定
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。DBからの入力の場合はSQLファイルも出力。
|
||||
@@ -0,0 +1,25 @@
|
||||
# MERGE(複数ファイル結合) データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
複数の入力ファイルをキーでソート・マージし、統合された1つの出力ファイルを生成するプログラムのテストデータ。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書のキー項目一覧(マージキー)
|
||||
- 各入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 全ファイルカバー | 全入力ファイルのデータが出力に反映されるデータ + 末尾多keyテスト |
|
||||
| 2 | キー境界 | 同一キー値が複数ファイルに分散しているケース + 末尾多keyテスト |
|
||||
|
||||
## 多keyテスト
|
||||
|
||||
キーが複数項目の場合、各組末尾に追加。各キー項目のみ異なるN件を生成し、正しいキーでマージされることを検証。
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -0,0 +1,31 @@
|
||||
# SELECT処理 データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
DBテーブルから条件付きSELECTでデータを取得し、取得結果に基づいて処理を行うプログラムのテストデータ。入力はDB(SELECT結果)であり、出力はSQL INSERTファイル。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書の処理詳細(SELECT文のWHERE条件)
|
||||
- DBテーブル定義(テーブル名、カラム、主キー)
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 基本データ | SELECT条件にマッチするデータ + マッチしないデータ + 末尾多keyテスト |
|
||||
|
||||
## 多keyテスト
|
||||
|
||||
WHERE条件に複数キーが関わる場合、基準データに対し各キー項目のみ異なるN件を生成。マッチするパターンとマッチしないパターンの両方をカバー。
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. SELECT文のWHERE条件を抽出
|
||||
2. 条件にマッチするレコードを生成
|
||||
3. 条件にマッチしないレコード(境界値)を生成
|
||||
4. 多keyテストデータを追加
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 SQL INSERTファイル。
|
||||
@@ -0,0 +1,32 @@
|
||||
# キーブレイク(集約) データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
キー項目でソート済みの入力ファイルを読み取り、キーブレイク時に集約レコード(カウント・合計など)を出力するプログラムのテストデータを生成する。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書のキー項目一覧
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 基本集約 | 複数グループの集約データ + 末尾に多keyテスト |
|
||||
| 2 | 境界ケース | キー境界での正しい集約切替を確認するデータ + 末尾に多keyテスト |
|
||||
|
||||
## 多keyテスト
|
||||
|
||||
キーが複数項目の場合、各組末尾に追加。1レコードを基準に、各キー項目のみ異なるN件を生成。
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. キー項目の特定
|
||||
2. 组1: 3グループ以上、各グループ内に複数レコード(集約対象)
|
||||
3. 组2: キー値が1件ずつ異なる境界データ
|
||||
4. 各組末尾に多keyテスト
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -0,0 +1,37 @@
|
||||
# キーブレイク(集計) データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
キー項目でソート済みの入力ファイルを読み取り、キーが変わるタイミング(キーブレイク)で集計値を計算し、1グループにつき1件の集計レコードを出力するプログラムのテストデータを生成する。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書のキー項目一覧(ソートキー)
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
- 集計対象フィールド(処理詳細から特定)
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 基本集計 | 複数グループ・各グループ内複数レコードのデータ + 末尾に多keyテスト |
|
||||
| 2 | 単一レコードグループ | 1レコードのみのグループを含むデータ + 末尾に多keyテスト |
|
||||
|
||||
## 多keyテスト(各組の末尾に追加)
|
||||
|
||||
キーが複数項目で構成される場合:
|
||||
1. 該当グループ内から1件のレコードを選び基準データとする
|
||||
2. N個のキー項目に対し、それぞれ1項目のみ異なるN件の追加レコードを生成
|
||||
3. これらのレコードは別グループとして扱われるべき(集計値が別になる)
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. キー項目を特定(キー項目一覧のソート条件から)
|
||||
2. 集計フィールドを特定(処理詳細からCOMPUTE/ADD対象を抽出)
|
||||
3. 组1: 3グループ以上、各グループ2〜5レコード、キー昇順
|
||||
4. 组2: 単一レコードグループと複数レコードグループの混在
|
||||
5. 各組末尾に多keyテストデータを追加
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -0,0 +1,25 @@
|
||||
# キーブレイク(集計、集約) データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
ソート済み入力ファイルを読み取り、キーブレイクで集計・集約を行い結果を出力するプログラムのテストデータ。複数ファイル入力の場合はマッチングと集計を組み合わせる。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書のキー項目一覧、使用ファイル一覧
|
||||
- 各入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 通常集計 | 複数グループ・各グループ複数レコード + 末尾多keyテスト |
|
||||
| 2 | 同キー内集約 | 同一キー内でN件集約されるデータ + 末尾多keyテスト |
|
||||
|
||||
## 多keyテスト
|
||||
|
||||
キーが複数項目の場合、各組末尾に追加。1レコードを基準に各キー項目のみ異なるN件を生成。
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -0,0 +1,78 @@
|
||||
# マッチング(1:1) 数据生成规则
|
||||
|
||||
## 概要
|
||||
|
||||
2个输入文件按指定键进行1:1匹配,输出匹配成功/仅R01/仅R02三种结果。
|
||||
|
||||
## 输入
|
||||
|
||||
- 前提条件:从详细设计书解析匹配键(A001>A002>A003...)
|
||||
- 输入文件:2个(R01、R02)
|
||||
- 键条件:键值唯一(重複NG),升序排序
|
||||
|
||||
## 生成的数据组
|
||||
|
||||
| 组 | 用途 | R01记录数 | R02记录数 | 说明 |
|
||||
|----|------|-----------|-----------|------|
|
||||
| 1 | 两端不匹配 | 4 | 4 | 第1条R01-only、最后1条R02-only、中间3条以上匹配 |
|
||||
| 2 | 反向两端不匹配 | 4 | 4 | 第1条R02-only、最后1条R01-only、中间3条以上匹配 |
|
||||
| 3 | 中间不匹配 | 5 | 5 | 首尾匹配、中间有R01-only和R02-only各1条 |
|
||||
|
||||
### 组1:两端不匹配
|
||||
|
||||
R01键:a001(R01-only) → a002 → a003 → a004
|
||||
R02键:a002 → a003 → a004 → a005(R02-only)
|
||||
|
||||
records数组(按处理顺序):
|
||||
| record序号 | 内容 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 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的比较没有问题。
|
||||
|
||||
|
||||
### 组2:反向两端不匹配
|
||||
|
||||
R01键:a002 → a003 → a004 → a005(R01-only)
|
||||
R02键:a001(R02-only) → a002 → a003 → a004
|
||||
|
||||
records数组:
|
||||
| record序号 | 内容 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 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) → a003 → a005
|
||||
|
||||
records数组:
|
||||
| record序号 | 内容 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 1 | R01(a001) + R02(a001) | 匹配 |
|
||||
| 2 | R01(a003) | R01-only |
|
||||
| 3 | R01(a004) + R02(a003) | 匹配 |
|
||||
| 4 | R02(a002) | R02-only |
|
||||
| 5 | R01(a005) + R02(a005) | 匹配 |
|
||||
|
||||
## 数据生成步骤
|
||||
|
||||
1. 从前提条件解析匹配键字段(如 A001>A002>A003)
|
||||
2. 从COPYBOOK获取各字段的PIC定义
|
||||
3. 生成键值:键值以 a001/b001/c001 格式递增,保证升序排列
|
||||
4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。
|
||||
5. 按各组规则组装records数组
|
||||
6. 组号从1开始连续编号
|
||||
|
||||
## 输出
|
||||
|
||||
每个组输出一个json文件:`{プログラムID}_g{groupId}.json`
|
||||
@@ -0,0 +1,69 @@
|
||||
# マッチング(1:N) 数据生成规则
|
||||
|
||||
## 概要
|
||||
|
||||
2个输入文件,R01为主驱动,R02为从侧。1件R01可对应多件R02(1:N)。
|
||||
|
||||
## 输入
|
||||
|
||||
- 输入文件:2个(R01主驱动、R02从侧)
|
||||
- 前提条件:双方按key升序排序
|
||||
- 核心逻辑:EVALUATE TRUE 3分岐(R01key<R02key / = / >)
|
||||
|
||||
## 生成的数据组
|
||||
|
||||
| 组 | 用途 | R01记录数 | R02记录数 | 说明 |
|
||||
|----|------|-----------|-----------|------|
|
||||
| 1 | 1:1匹配 | 3 | 3 | 全部1:1匹配,中间R01-only和R02-only |
|
||||
| 2 | 1:N匹配 | 3 | 5 | R01有1:N匹配(同key多条R02) |
|
||||
| 3 | R02-先结束 | 3 | 2 | R01先结束,第一条数据的R01key<第一条的r02数据 |
|
||||
| 4 | R02-only | 2 | 4 | R02先结束,第一条数据的R01key>第一条的r02数据 |
|
||||
|
||||
### 组1:1:1匹配
|
||||
|
||||
R01: a001→a002→a003
|
||||
R02: a001→a002→a003
|
||||
|
||||
records按处理顺序:
|
||||
| record序号 | 内容 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 1 | R01(a001)+R02(a001) | 匹配 |
|
||||
| 2 | R01(a002)+R02(a002) | 匹配 |
|
||||
| 3 | R01(a003)+R02(a003) | 匹配 |
|
||||
|
||||
组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。
|
||||
和R02(a003)的所有key相比,只有当前第n个key与R02(a003)的key不一致。保证key的比较没有问题。
|
||||
|
||||
|
||||
### 组2:1:N匹配
|
||||
|
||||
R01: a001→a002→a003
|
||||
R02: a001→a002→a002→a002→a003
|
||||
(同key有3条)
|
||||
|
||||
records按处理顺序:
|
||||
| record序号 | 内容 | 说明 |
|
||||
|-----------|------|------|
|
||||
| 1 | R01(a001)+R02(a001) | 匹配(第1件) |
|
||||
| 2 | R01(a002)+R02(a002) | 匹配(第1件) |
|
||||
| 3 | R01(a002)+R02(a002) | 匹配(第2件) |
|
||||
| 4 | R01(a002)+R02(a002) | 匹配(第3件) |
|
||||
| 5 | R01(a003)+R02(a003) | 匹配 |
|
||||
|
||||
### 组3:R02-先结束
|
||||
|
||||
R01: a001→a002→a003
|
||||
R02: a002→a002
|
||||
|
||||
records:全部R01-only
|
||||
|
||||
### 组4:R01-先结束
|
||||
|
||||
R01: a002→a003
|
||||
R02: a001→a002→a003→a004
|
||||
|
||||
records:R01(a001)→R02(b001~b004)→R01(a002)
|
||||
|
||||
## 输出
|
||||
|
||||
每组一个json文件:`{プログラムID}_g{groupId}.json`
|
||||
@@ -0,0 +1,36 @@
|
||||
# マッチング(N:1) データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
マッチング(1:1)の逆。R01が主駆動でR02が従側。1:Nの逆構成。
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | R01 | R02 | 説明 |
|
||||
|----|------|-----|-----|------|
|
||||
| 1 | 両端不一致 | 4 | 4 | 1件目R02-only、最終R01-only、中間3件マッチ + 末尾多keyテスト |
|
||||
| 2 | 逆方向両端不一致 | 4 | 4 | 1件目R01-only、最終R02-only、中間3件マッチ + 末尾多keyテスト |
|
||||
| 3 | 中間不一致 | 5 | 5 | 先頭末尾マッチ、中間にR01-onlyとR02-only各1件 + 末尾多keyテスト |
|
||||
|
||||
## 组1:両端不一致
|
||||
R01: a002→a003→a004→a005(R01-only)
|
||||
R02: a001(R02-only)→a002→a003→a004
|
||||
|
||||
## 组2:逆方向
|
||||
R01: a001(R01-only)→a002→a003→a004
|
||||
R02: a002→a003→a004→a005(R02-only)
|
||||
|
||||
## 组3:中間不一致
|
||||
R01: a001→a003(R01-only)→a004→a005
|
||||
R02: a001→a002(R02-only)→a004→a005
|
||||
|
||||
## 多keyテスト
|
||||
|
||||
キーが複数項目の場合、组1・组2の末尾に追加。
|
||||
组1: R01-onlyの最終R01データを基準に、各キー項目のみ異なるN件をR02側に追加
|
||||
组2: R02-onlyの最終R02データを基準に、各キー項目のみ異なるN件をR01側に追加
|
||||
これによりマッチングキーの比較が全項目正しく行われていることを検証。
|
||||
|
||||
## 出力
|
||||
|
||||
各組1 JSONファイル。
|
||||
@@ -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ファイル
|
||||
@@ -0,0 +1,54 @@
|
||||
# 振り分け データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
単一の入力ファイル(CSVまたは固定長)を読み取り、条件分岐に基づいて複数の出力ファイルに振り分けるプログラムのテストデータを生成する。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書の処理詳細(分岐条件と出力先の対応)
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 全分支カバー | すべての分岐条件をカバーするデータ。各出力先に最低1件ずつ振り分けられるようにする。 |
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. **分岐条件の特定**: 処理詳細からすべての分岐条件とそれに対応する出力先を抽出する
|
||||
2. **分岐パターンの列挙**: 正常系(検証通過→正常出力)と異常系(エラー→ERROR-LOG)の全パターンを列挙
|
||||
3. **データ生成**:
|
||||
- 各分岐パターンに最低1件のレコードを生成
|
||||
- フィールド値はPIC定義に従い生成
|
||||
- 正常系データ: 全チェックを通過する有効な値
|
||||
- 異常系データ: 該当するチェックに引っかかる値(不正な日付、不正な時刻、範囲外の値など)
|
||||
4. **値のルール**:
|
||||
- PIC X(n): 左詰め・スペース埋め
|
||||
- PIC 9(n): 右詰め・先行ゼロ
|
||||
- DATE/日付系: 実在しそうな日付(YYYYMMDD形式)、異常系では不正な日付(99999999等)
|
||||
- TIME/時刻系: 実在しそうな時刻(HHMM形式)、異常系では範囲外の時刻
|
||||
- STATUS系: 0/1/9 などのコード値
|
||||
- CSV項番号フィールド: 分割後の項目数
|
||||
5. **出力**: 1組 = 1 JSONファイルに全レコードを格納
|
||||
|
||||
## 出力形式
|
||||
|
||||
```json
|
||||
{
|
||||
"program": "ZAN01CHK",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"DD名": {
|
||||
"フィールド名": "値",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
各レコードが1回の処理に対応。正常系→異常系→特殊分岐の順に並べる。
|
||||
@@ -0,0 +1,31 @@
|
||||
# 項目チェック データ生成規則
|
||||
|
||||
## 概要
|
||||
|
||||
入力ファイル(CSV)の各レコードに対し、複数の項目チェックを実施し、正常データと異常データを別々の出力ファイルに振り分けるプログラムのテストデータを生成する。
|
||||
|
||||
## 入力
|
||||
|
||||
- 詳細設計書の処理詳細(チェック項目一覧)
|
||||
- 入力ファイルのCOPYBOOKフィールド定義
|
||||
|
||||
## 生成データ組
|
||||
|
||||
| 组 | 用途 | 内容 |
|
||||
|----|------|------|
|
||||
| 1 | 全チェックカバー | すべてのチェック項目について、合格データと不合格データの両方をカバーする。 |
|
||||
|
||||
## データ生成手順
|
||||
|
||||
1. **チェック項目の特定**: 処理詳細からすべてのチェック項目(フィールド数、値域、重複、時刻範囲、コード値など)を抽出する
|
||||
2. **各チェックに対するデータ生成**:
|
||||
- 合格データ: 全チェックを通過する有効な値
|
||||
- 不合格データ: 各チェック項目に特化した不正な値を含むレコード
|
||||
3. **値のルール**:
|
||||
- フィールド数チェック: 正しい項目数/足りない/多い
|
||||
- 日付チェック: 正常日付 vs 不正日付 (99999999等)
|
||||
- 時刻チェック: 正常時刻 (0900-1800) vs 範囲外 (2500等)
|
||||
- 重複チェック: 同一キー2件 vs 全件別キー
|
||||
- コード値チェック: 有効コード vs 無効コード
|
||||
- 社員番号: 8桁数字 vs 非数字/桁数不足
|
||||
4. **出力**: 1組 = 1 JSONファイル
|
||||
@@ -0,0 +1,27 @@
|
||||
# 条件分支 数据生成规则
|
||||
|
||||
## 概要
|
||||
|
||||
检索详细设计中処理詳細的所有条件分支,增加覆盖所有分支的输入数据。
|
||||
|
||||
## 输入
|
||||
|
||||
- 输入文件:详细设计书的処理詳細部分
|
||||
|
||||
## 生成的数据组
|
||||
|
||||
由你决定,以尽可能少的数据组完成分支覆盖
|
||||
|
||||
|
||||
## 数据生成步骤
|
||||
|
||||
1. 遍历処理詳細的所有内容
|
||||
2. 记录所有条件分支
|
||||
3. 找到条件判断数据与输入的关系
|
||||
4. 数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。
|
||||
5. 按各组规则组装records数组
|
||||
6. 组号从之前的最后一组+1开始连续编号
|
||||
|
||||
## 输出
|
||||
|
||||
每个组输出一个json文件:`{プログラムID}_g{groupId}.json`
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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')
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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を実行
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -0,0 +1,5 @@
|
||||
*
|
||||
* DBCLEAN対象レコード 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)FILLER PIC X(072).
|
||||
@@ -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: + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"))
|
||||
@@ -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
|
||||
@@ -0,0 +1,90 @@
|
||||
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_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
|
||||
Reference in New Issue
Block a user