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
+138 -58
View File
@@ -1050,17 +1050,23 @@ def _dec_str(s, length):
def _reconcile_unstring_fields(rec, left_field, operator, right_field, want_true,
fields, left_chain, assignments, path_assign):
right_root, right_chain = trace_to_root(right_field, assignments, fields, path_assign)
right_root, right_chain = trace_to_root(right_field, assignments, fields, path_assign=None)
if right_root not in rec:
logger.debug(f"字段间比较协调:右侧根 {right_root} 不在 rec,跳过")
return
all_entries = (left_chain or []) + (right_chain or [])
for _, asgn in all_entries:
if asgn.get('type') not in ('move', 'unstring_split'):
if asgn.get('type') not in ('move', 'unstring_split', 'move_literal'):
logger.debug(f"字段间比较协调:链含非 MOVE 类型 {asgn.get('type')},跳过")
return
left_val = str(rec.get(left_field, ''))
if not left_val.strip():
# 左字段无值时尝试反向:用右字段的值填充左字段
right_root_val = str(rec.get(right_root, ''))
if right_root_val.strip() and operator in ('=', '==') and want_true:
rec[left_field] = right_root_val
logger.debug(f"字段间比较协调(反向){left_field}<={right_root}={right_root_val}")
return
logger.debug(f"字段间比较协调:左侧 {left_field} 无值,跳过")
return
length = 0
@@ -1388,14 +1394,16 @@ def _enum_search_paths(node, fields):
base = re.sub(r'\s*\(.*?\)\s*$', '', cond_tree.field)
matching_val = cond_tree.value
elem_key = f'{base}({i + 1})'
subj = cond_tree.value
subj_is_field = any(f['name'] == subj for f in fields)
# 确保 match 值与字段 PIC 类型兼容
_fmt = next((f.get('pic_info', {}).get('type') for f in fields if f['name'] == elem_key), None)
if _fmt in ('alphanumeric', 'alphabetic'):
matching_val = str(matching_val).ljust(
next((f['pic_info'].get('length', 1) for f in fields if f['name'] == elem_key and f.get('pic_info')), 1)
)[:next((f['pic_info'].get('length', 1) for f in fields if f['name'] == elem_key and f.get('pic_info')), 1)]
if any(f['name'] == matching_val for f in fields):
extra_assign[elem_key] = [{'type': 'move', 'source_vars': [matching_val]}]
if subj_is_field:
extra_assign[elem_key] = [{'type': 'move', 'source_vars': [subj]}]
else:
extra_assign[elem_key] = [{'type': 'move_literal', 'literal': matching_val}]
non_match = _non_match_for(cond_tree, fields) or ' '
@@ -1408,11 +1416,15 @@ def _enum_search_paths(node, fields):
for k, v in sp_assign.items():
merged_assign.setdefault(k, []).extend(v if isinstance(v, list) else [v])
if cond_tree and isinstance(cond_tree, CondLeaf):
# Also set the subject field (right side of comparison) to match
subj = cond_tree.value
if any(f['name'] == subj for f in fields):
subj_is_field = any(f['name'] == subj for f in fields)
if subj_is_field:
# Field-to-field: constrain subject to reference table element value
paths.append(([(subj, cond_tree.op, elem_key, True)] + sp_cons, merged_assign))
else:
# Literal value: set subject field to literal, constrain table element
merged_assign[subj] = [{'type': 'move_literal', 'literal': matching_val}]
paths.append(([(elem_key, cond_tree.op, matching_val.rstrip(), True)] + sp_cons, merged_assign))
paths.append(([(elem_key, cond_tree.op, matching_val.rstrip(), True)] + sp_cons, merged_assign))
else:
paths.append((sp_cons, merged_assign))
@@ -1436,32 +1448,76 @@ def _enum_search_paths(node, fields):
return paths
def _rebuild_r01line_csv(rec, data_fields):
"""直接基于 WRK-CSV 字段构建 CSV 字符串写入 rec['R01LINE']
按 PIC 长度截断各字段,避免 _reconstruct_unstring_sources 污染导致字段过长的 bug
def _rebuild_r01line_csv(rec, data_fields, base_assignments=None):
"""基于 UNSTRING 目标字段动态构建 CSV 字符串写入源字段(如 R01LINE)
取代旧硬编码字段列表,支持任意程序的 UNSTRING 结构
按各目标字段的实际 PIC 长度 padding,确保运行时 UNSTRING 正确解析。
"""
csv_fields = [
('WRK-CSV-APPL-ID', 8), ('WRK-CSV-EMP-ID', 8), ('WRK-CSV-APPL-DATE', 8),
('WRK-CSV-START-TIME', 4), ('WRK-CSV-END-TIME', 4), ('WRK-CSV-STATUS', 1),
('WRK-CSV-OVT-TYPE', 1), ('WRK-CSV-FILLER', 46),
]
parts = []
for fname, flen in csv_fields:
val = str(rec.get(fname, ''))
if len(val) > flen:
val = val[:flen]
elif len(val) < flen:
val = val.ljust(flen)
parts.append(val)
csv_value = ','.join(parts)
r01_len = 80
for f in data_fields:
if f['name'] == 'R01LINE':
pi = f.get('pic_info', {})
r01_len = pi.get('length', 80) or 80
break
csv_value = csv_value.ljust(r01_len)[:r01_len]
rec['R01LINE'] = csv_value
if base_assignments is None:
return
groups = {}
for tgt, asgn_list in base_assignments.items():
for asgn in asgn_list:
if asgn.get('type') == 'unstring_split' and asgn.get('source_vars'):
src = asgn['source_vars'][0]
idx = asgn.get('index', 0)
groups.setdefault(src, []).append((idx, tgt))
if not groups:
return
for src_var, targets in groups.items():
targets.sort(key=lambda x: x[0])
resolved_src = src_var
for i, f in enumerate(data_fields):
if f['name'] == resolved_src:
grp_level = f.get('level', 0)
for f2 in data_fields[i + 1:]:
if f2.get('level', 0) <= grp_level or f2.get('level') == 77:
break
if f2.get('pic'):
resolved_src = f2['name']
break
break
if resolved_src not in rec:
continue
csv_parts = []
for idx, tgt in targets:
val = str(rec.get(tgt, ''))
length = 0
for f in data_fields:
if f['name'] == tgt and f.get('pic_info'):
pi = f['pic_info']
ftype = pi.get('type', '')
if ftype in ('alphanumeric', 'alphabetic'):
length = pi.get('length', 0)
elif ftype == 'numeric':
length = pi.get('digits', 0) + pi.get('decimal', 0)
break
if length > 0:
if len(val) > length:
val = val[:length]
elif len(val) < length:
val = val.ljust(length)
csv_parts.append(val)
csv_value = ','.join(csv_parts)
src_len = 0
for f in data_fields:
if f['name'] == resolved_src:
pi = f.get('pic_info', {})
if pi:
src_len = pi.get('length', 0)
break
if src_len > 0:
csv_value = csv_value.ljust(src_len)[:src_len]
rec[resolved_src] = csv_value
def generate_records(path_infos, data_fields, base_assignments=None, file_sec=None):
@@ -1476,7 +1532,6 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
records = []
kept_path_cons = []
term_types = []
_zan01_emp_err_count = 0
if path_infos:
for seq, (path_cons, path_assign, term_type) in enumerate(path_infos, start=1):
path_cons = _filter_stop(path_cons)
@@ -1595,29 +1650,54 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
except (ValueError, TypeError):
pass
break
# Pass B.13: C01CHKRRC 约束与 SUB04CHK 输入字段同步
# SUB04CHK 检查 C01CHKDAT(1:8) = SPACES → RC≠0
# 约束系统无法跨 CALL 追溯,需确保 WRK-CSV-EMP-ID/WRK-CSV-APPL-DATE
# 与预期的 C01CHKRRC 值一致,使运行时实际 CALL 返回正确结果。
# want=FalseC01CHKRRC=0,通过)→ EMP-ID 和 APPL-DATE 都有效
# want=TrueC01CHKRRC≠0,错误)→ 交替:
# 奇数个 → EMP-ID 空格 (#4-T)
# 偶数个 → EMP-ID有效 + DATE空格 (#5-T)
for c in path_cons:
if len(c) == 4 and c[0] == 'C01CHKRRC' and c[1] == '<>' and c[2] == 'ZERO':
if not c[3]:
if 'WRK-CSV-EMP-ID' in rec and str(rec.get('WRK-CSV-EMP-ID', '')).strip() == '':
rec['WRK-CSV-EMP-ID'] = '00000101'
if 'WRK-CSV-APPL-DATE' in rec and str(rec.get('WRK-CSV-APPL-DATE', '')).strip() == '':
rec['WRK-CSV-APPL-DATE'] = '20000101'
else:
_zan01_emp_err_count += 1
if _zan01_emp_err_count % 2 == 0:
if 'WRK-CSV-EMP-ID' in rec:
rec['WRK-CSV-EMP-ID'] = '00000101'
if 'WRK-CSV-APPL-DATE' in rec:
rec['WRK-CSV-APPL-DATE'] = ' '
break
# Pass B.13: C01CHKRRC 约束 → 按 IF 位置映射对应字段
# SUB04CHK 校验 W01* 字段(非空格/非零为有效)
# C01CHKRRC 约束按 2030VALIDATESOR 的 IF 出现顺序对应:
# 0→EMP-ID, 1→START-DATE, 2→START-TIME, 3→END-DATE, 4→END-TIME
# BrSeq frozenset 去重导致只有 3 种 path type 保留:
# [True]、[False,True]、[False×5]
_c01_wants = [
c[3] for c in path_cons
if len(c) == 4 and c[0] == 'C01CHKRRC' and c[1] == '<>' and c[2] == 'ZERO'
]
if _c01_wants:
_c01_csv_fields = [
'WRK-CSV-EMP-ID', 'WRK-CSV-START-DATE',
'WRK-CSV-START-TIME', 'WRK-CSV-END-DATE', 'WRK-CSV-END-TIME',
]
_csv_to_w01 = {}
if base_assignments:
for tgt, alist in base_assignments.items():
for a in alist:
if a.get('type') == 'move' and a.get('source_vars'):
_csv_to_w01.setdefault(a['source_vars'][0], tgt)
for idx, want in enumerate(_c01_wants):
if idx >= len(_c01_csv_fields):
break
csv_fld = _c01_csv_fields[idx]
w01_fld = _csv_to_w01.get(csv_fld)
for fname in (csv_fld, w01_fld):
if not fname or fname not in rec:
continue
if want:
pi = None
for f in data_fields:
if f['name'] == fname:
pi = f.get('pic_info', {})
break
length = 8
if pi:
length = pi.get('length', 8) or pi.get('digits', 8) + pi.get('decimal', 0)
rec[fname] = ' ' * length
else:
cur = str(rec.get(fname, '')).strip().rstrip('0').rstrip(' ')
if not cur or cur == '':
if 'TIME' in fname:
rec[fname] = '0900'
elif 'DATE' in fname:
rec[fname] = '20240115'
else:
rec[fname] = '00000101'
# Pass B.8: UNSTRING source reconstruction (targets → source)
if base_assignments:
_reconstruct_unstring_sources(rec, base_assignments, data_fields)
@@ -1638,7 +1718,7 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
# 否则运行时 UNSTRING 会从 R01LINE 取有效值覆盖 WS 的无效值
# 直接基于 WRK-CSV 字段构建 CSV,避免 _reconstruct_unstring_sources 解析
# R01INNREC 时因组名在 rec 中而跳过子字段解析的 bug
_rebuild_r01line_csv(rec, data_fields)
_rebuild_r01line_csv(rec, data_fields, base_assignments)
# Pass E: PIC 长度约束 — 模拟 COBOL 截断语义
for f in data_fields:
@@ -1676,7 +1756,7 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
rec2 = deepcopy(rec)
rec2['WRK-CSV-EMP-ID'] = '00000101'
rec2['WRK-CSV-APPL-DATE'] = ' '
_rebuild_r01line_csv(rec2, data_fields)
_rebuild_r01line_csv(rec2, data_fields, base_assignments)
records.append(rec2)
kept_path_cons.append(path_cons)
term_types.append(term_type)