feat: phase2 review fixes

- TIME injection: change from spaces to '2500' (hour>23) for NUMVAL trigger
- Runner: add .resolve() to work_dir to fix chdir+relative path breakage
- Coverage: per-target field overrides for DP#9-#12 (START-DATE=20240115 etc.)
- .gitignore: add compilation artifacts, temp scripts, test outputs
This commit is contained in:
hangshuo652
2026-07-15 21:46:28 +08:00
parent f3be17e5eb
commit 54d4e81240
13 changed files with 769 additions and 188 deletions
+164 -40
View File
@@ -160,6 +160,10 @@ class GixsqlOrchestrator:
copybook_dirs=[ascii_dir],
extra_srcs=extra_srcs,
)
log_dir = self.runtime_dir / "logs" / "compile"
log_dir.mkdir(parents=True, exist_ok=True)
log_dir.joinpath(f"{self.program_id}.log").write_text(
result.log, encoding='utf-8')
if result.success:
self.exe_path = Path(result.exe_path)
return DbPipelineResult(
@@ -303,10 +307,70 @@ class GixsqlOrchestrator:
if len(parts) == 2:
rec['R01LINE'] = f"{dup_eid.ljust(8)},{parts[1]}"
# ── Coverage-driven data modifications (per-scenario) ──
# Normal scenario or legacy single-run: no modifications needed.
# Collision scenario: INSERT duplicate, OVT-MONTHLY match, COMMIT threshold.
# Abnormal scenario: orphan cancel ABEND (last, to avoid polluting other branches).
apply_collision = scenario is not None and scenario.id == "collision"
apply_abnormal = scenario is not None and scenario.id == "abnormal"
if apply_collision:
# #10 T: Ensure >= 50 R01 records for COMMIT threshold (CNS-COMMIT-CNT=50)
r01_recs = [r for r in recs if 'R01APPL-ID' in r]
r01_count = len(r01_recs)
if r01_count < 50:
template = r01_recs[-1].copy() if r01_recs else {}
needed = 50 - r01_count
for i in range(needed):
nr = {}
for key, val in template.items():
if not key.startswith('R02'):
nr[key] = val
nr['R01APPL-ID'] = f"X50{str(i).zfill(5)}"
if 'R01EMP-ID' in nr:
nr['R01EMP-ID'] = str(int(str(nr.get('R01EMP-ID', '0') or '0')) + i + 10000).zfill(8)
recs.append(nr)
r01_recs = [r for r in recs if 'R01APPL-ID' in r]
logger.info(f" Coverage #10T: added {needed} R01-only records -> {len(r01_recs)} total")
# #8 T: Two R01 records with same APPL-ID -> 2nd INSERT collides -> UPDATE
if len(r01_recs) >= 4:
dup_appl_id = 'COLISN01'
for idx in (2, 3):
r01_recs[idx]['R01APPL-ID'] = dup_appl_id
if 'R02APPL-ID' in r01_recs[idx]:
r01_recs[idx]['R02APPL-ID'] = dup_appl_id
logger.info(f" Coverage #8T: set APPL-ID={dup_appl_id} on records [2]&[3] for INSERT duplicate")
# #11 T: Two R01 records with same (EMP-ID, APPL-DATE, OVT-TYPE)
if len(r01_recs) >= 2:
match_emp = r01_recs[1].get('R01EMP-ID', '00000000').strip() or '00000000'
match_date = r01_recs[1].get('R01APPL-DATE', '00000000').strip() or '00000000'
match_type = r01_recs[1].get('R01OVT-TYPE', '1').strip() or '1'
r01_recs[0]['R01EMP-ID'] = match_emp
r01_recs[0]['R01APPL-DATE'] = match_date
r01_recs[0]['R01OVT-TYPE'] = match_type
r01_recs[1]['R01EMP-ID'] = match_emp
r01_recs[1]['R01APPL-DATE'] = match_date
r01_recs[1]['R01OVT-TYPE'] = match_type
logger.info(
f" Coverage #11T: unified (EMP={match_emp} DATE={match_date}"
f" TYPE={match_type}) for R01 records [0]&[1]"
)
if apply_abnormal:
# #14 T: Last R02 record has non-existent APPL-ID -> orphan cancel ABEND
# NOTE: ABEND prevents 3000STPSOR (#19 T/F); covered by normal scenario.
r02_recs = [r for r in recs if 'R02APPL-ID' in r]
if r02_recs:
r02_recs[-1]['R02APPL-ID'] = 'ZZZZZZZZ'
logger.info(f" Coverage #14T: set last R02 APPL-ID='ZZZZZZZZ' for orphan cancel")
# 出力先ディレクトリ(シナリオ毎に分離)
run_label = f"run_{scenario.id}" if scenario else ""
output_root = self.work_dir / run_label if scenario else self.work_dir
input_dir = output_root / "input"
input_dir = output_root / "main" / "input"
input_dir.mkdir(parents=True, exist_ok=True)
# フラットファイル書き出し(全シナリオ同一)
@@ -404,7 +468,7 @@ class GixsqlOrchestrator:
sql_meta, declared_columns, records=recs)
# Write main JSON(シナリオ毎に分離)
json_outdir = output_root / "json"
json_outdir = output_root / "main" / "json"
json_outdir.mkdir(parents=True, exist_ok=True)
json_path = json_outdir / f"{self.program_id}.json"
output_json(json_records, json_path, roles,
@@ -443,8 +507,8 @@ class GixsqlOrchestrator:
# シナリオ毎の出力先
run_label = f"run_{scenario.id}" if scenario else ""
run_dir = self.runtime_dir / run_label if scenario else self.runtime_dir
input_dir = run_dir / "input"
output_dir = run_dir / "output"
input_dir = run_dir / "main" / "input"
output_dir = run_dir / "main" / "output"
gcov_dir = self.runtime_dir / "gcov"
input_dir.mkdir(parents=True, exist_ok=True)
output_dir.mkdir(parents=True, exist_ok=True)
@@ -453,17 +517,17 @@ class GixsqlOrchestrator:
# シナリオ毎の CWD = run_{id}/、単輪時は runtime_dir 直下
cwd = run_dir
# 入力ファイル(work_dir/run_{id}/input/ → runtime/run_{id}/input/
gen_input_dir = self.work_dir / f"run_{scenario.id}" / "input" if scenario else self.work_dir / "input"
# 入力ファイル(work_dir/run_{id}/main/input/ → runtime/run_{id}/main/input/
gen_input_dir = self.work_dir / f"run_{scenario.id}" / "main" / "input" if scenario else self.work_dir / "main" / "input"
if gen_input_dir.exists():
for f in gen_input_dir.iterdir():
if f.is_file():
(input_dir / f.name).write_bytes(f.read_bytes())
# JSON 出力(work_dir/run_{id}/json/ → runtime/run_{id}/json/
gen_json_dir = self.work_dir / f"run_{scenario.id}" / "json" if scenario else self.work_dir / "json"
# JSON 出力(work_dir/run_{id}/main/json/ → runtime/run_{id}/main/json/
gen_json_dir = self.work_dir / f"run_{scenario.id}" / "main" / "json" if scenario else self.work_dir / "main" / "json"
if gen_json_dir.exists():
json_dir = run_dir / "json"
json_dir = run_dir / "main" / "json"
json_dir.mkdir(parents=True, exist_ok=True)
for f in gen_json_dir.iterdir():
if f.is_file() and f.suffix.lower() == '.json':
@@ -474,9 +538,9 @@ class GixsqlOrchestrator:
env_overrides = {}
for fname, direction in assign_map.items():
if direction == "INPUT":
env_overrides[fname] = os.path.join("input", fname)
env_overrides[fname] = os.path.join("main", "input", fname)
else:
env_overrides[fname] = os.path.join("output", fname)
env_overrides[fname] = os.path.join("main", "output", fname)
# シナリオ毎の DB パス
db_path = self._current_db_path or self.db_path
@@ -501,6 +565,11 @@ class GixsqlOrchestrator:
env_overrides=env_overrides,
)
log_dir = self.runtime_dir / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_dir.joinpath(f"{run_label or self.program_id}.log").write_text(
result.log, encoding='utf-8')
# .gcda を gcov/ にコピー(シナリオ毎に gcov/run_{id}/
# GnuCOBOL は .gcno が生成された CWD (= compile CWD = exe_dir) に .gcda を書き出す。
# 複数シナリオで .gcno は共有されるため COPY で行う(MOVE 不可)。
@@ -512,13 +581,22 @@ class GixsqlOrchestrator:
gcda_src_dirs.append(self.runtime_dir) # 従来互換
gcda_dst_dir = gcov_dir / run_label if scenario else gcov_dir
gcda_dst_dir.mkdir(parents=True, exist_ok=True)
for ext in (".gcda", ".gcno"):
for sd in gcda_src_dirs:
for f in sd.glob(f"*{ext}"):
if f.is_file() and f.stat().st_size > 0:
dst = gcda_dst_dir / f.name
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
for sd in gcda_src_dirs:
for f in sd.glob("*.gcda"):
if f.is_file() and f.stat().st_size > 0:
dst = gcda_dst_dir / f.name
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
try:
shutil.copy2(str(f), str(dst))
except PermissionError:
pass
for f in sd.glob("*.gcno"):
if f.is_file() and f.stat().st_size > 0:
dst = gcda_dst_dir / f.name
try:
shutil.copy2(str(f), str(dst))
except PermissionError:
pass
return DbPipelineResult(
self.program_id, 3, result.success,
@@ -600,12 +678,21 @@ class GixsqlOrchestrator:
v3_root = Path(__file__).parent
extra_search = list(gcov_dir.glob("run_*")) + [self.work_dir / "bin"]
for search_dir in (v3_root, self.work_dir, self.runtime_dir, gcov_dir, Path.home(), *extra_search):
for ext in (".gcno", ".gcda"):
for f in search_dir.glob(f"*{ext}"):
if f.stat().st_size > 0:
dst = gcov_dir / f.name
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
for f in search_dir.glob("*.gcda"):
if f.stat().st_size > 0:
dst = gcov_dir / f.name
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
try:
shutil.copy2(str(f), str(dst))
except PermissionError:
pass
for f in search_dir.glob("*.gcno"):
if f.stat().st_size > 0:
dst = gcov_dir / f.name
try:
shutil.copy2(str(f), str(dst))
except PermissionError:
pass
# Count what we have
gcno_gcda_count = 0
for ext in (".gcno", ".gcda"):
@@ -915,26 +1002,37 @@ class GixsqlOrchestrator:
select_to_file[sel_name] = fname
assign_map[fname] = "UNKNOWN"
# Second pass: determine direction from OPEN statements
# Handle both simple (OPEN INPUT X) and compound (OPEN INPUT X OUTPUT Y)
# Second pass: determine direction from OPEN statements.
# COBOL allows multi-line OPEN where files listed without a direction
# keyword inherit the last stated direction:
# OPEN INPUT FILEA
# FILEB <-- inherits INPUT
# OUTPUT FILEC
# Strategy: extract OPEN body (up to terminating '.'), collapse
# whitespace, then parse direction→file pairs via splitting on
# direction keyword boundaries.
for m in re.finditer(
r'OPEN\s+((?:INPUT|OUTPUT|I-O|EXTEND)\s+\w+)'
r'((?:\s+(?:INPUT|OUTPUT|I-O|EXTEND)\s+\w+)*)',
src_text, re.IGNORECASE
r'OPEN\s+(.+?)\.', src_text, re.IGNORECASE | re.DOTALL
):
# Parse the OPEN payload: "INPUT X" + " OUTPUT Y"
payload = m.group(1) + m.group(2)
for part in re.finditer(
r'(INPUT|OUTPUT|I-O|EXTEND)\s+(\w+)', payload, re.IGNORECASE
):
direction = part.group(1).upper()
sel_name = part.group(2)
if sel_name in select_to_file:
fname = select_to_file[sel_name]
if direction in ("INPUT", "I-O"):
assign_map[fname] = "INPUT"
else:
assign_map[fname] = "OUTPUT"
full = re.sub(r'\s+', ' ', m.group(1)).strip()
# Split on direction keyword boundaries: "INPUT X Y OUTPUT Z"
# → ["INPUT X Y", "OUTPUT Z"]
tokens = re.split(r'\s+(?=(?:INPUT|OUTPUT|I-O|EXTEND)\s)', full, flags=re.IGNORECASE)
for seg in tokens:
seg = seg.strip()
if not seg:
continue
seg_m = re.match(r'(INPUT|OUTPUT|I-O|EXTEND)\s+([\w ]+)', seg, re.IGNORECASE)
if not seg_m:
continue
direction = seg_m.group(1).upper()
for fword in re.findall(r'\w+', seg_m.group(2)):
if fword in select_to_file:
fname = select_to_file[fword]
if direction in ("INPUT", "I-O"):
assign_map[fname] = "INPUT"
else:
assign_map[fname] = "OUTPUT"
return assign_map
@@ -1025,6 +1123,32 @@ class GixsqlOrchestrator:
if not rows:
logger.info(f" Table {table_name}: 0 initial rows (will be created at runtime)")
continue
# Debug
logger.info(f" Table {table_name}: {len(rows)} rows, cols={list(rows[0].keys()) if rows else []}")
# Filter columns: only keep those that actually exist in the table
try:
pragma_cols = conn.execute(
f"PRAGMA table_info([{table_name}])"
).fetchall()
valid_cols = {r[1].upper() for r in pragma_cols}
except Exception:
valid_cols = set()
remapped_rows = []
for row in rows:
new_row = {}
for k, v in row.items():
if k.upper() in valid_cols:
new_row[k] = v
if new_row:
remapped_rows.append(new_row)
rows = remapped_rows
if not rows:
logger.info(f" Table {table_name}: all rows filtered out, skipping")
continue
col_names = list(rows[0].keys())
placeholders = ", ".join("?" for _ in col_names)
quoted_cols = ", ".join(f"[{c}]" for c in col_names)